CLI now supports multiple frameworks (with tests) (#480)
* Early work defining CLI framework support * WIP moving CLI init logic to the Framework class * Installing files should now work for Nextjs * Some fixes * WIP creating unit tests for Next.js project detection * Delete old jest config * Latest lockfile * Detect use of src directory test * Tests for detection pages/app directory * Correct detection of Next.js project * Renamed test file * Create install files from template files with replacements. With tests * Added multiple uses of the same replacement * Created a test for the install step (it fails right now with JS) * Removed unused import * Another test that should pass but currently fails… * Path alias fixed and now has tests * New pathAlias function used * Nextjs page install tests * Fixed app directory install (with tests) * Removed e2e CLI test, switched to unit testing strategy instead * Latest lockfile * The install files are now actual files that are copied and transformed * Got the template files working correctly after building * Next steps are now framework specific * createFileFromTemplate now works with a path again. Uses mock if specified. * Renamed apiRoute.js to pagesApiRoute.js * Simplified pages file generation * Next.js app API route template * Next.js App routing support, with common files logic shared * Dev command now uses framework default values if they exist and aren’t overridden * Unused import * pathAlias now works for all frameworks * Added a test to detect Next from the next.config.js * WIP on Remix framework support * Tests for Remix install * Replaced references to Next.js * Use a green ✔️ instead of ✅ in the CLI * Support for multiple hostnames * Tunneling can now use the hostname and port * Work on multiple ports * Improved the error messages. Added some extra pots to Next.js * Update the Remix templates to have .server in the imports * Remix updated to use server-runtime instead of node. Node v18+ * Frameworks can specify the watch paths and ignore paths * Define the watch variables above, so we can easily log them for debugging * Don’t wait for outdated package checking when running the dev command * Improved the Remix manual setup guide * Rewriting docs for quickstart * Updated the Next.js quickstart * Remix quick start * Added a changeset * Improved the Next.js manual setup
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/remix": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
CLI now supports multiple frameworks (starting with Next.js and Remix)
|
||||
@@ -32,50 +32,30 @@ Create a `.env` file at the root of your project and include your Trigger API ke
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://cloud.trigger.dev
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
To set up the Trigger Client for your project, follow these steps:
|
||||
Create a file at `<root>/app/trigger.ts`, where `<root>` represents the root directory of your project.
|
||||
|
||||
1. **Create Configuration File:**
|
||||
Next, add the following code to the file which creates and exports a new `TriggerClient`:
|
||||
|
||||
In your project directory, create a configuration file named `trigger.ts` or `trigger.js`, depending on whether your project uses TypeScript (`.ts`) or JavaScript (`.js`).
|
||||
```typescript app/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
2. **Choose Directory:**
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
Create the configuration file inside the **app** directory of your project.
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add Configuration Code:**
|
||||
|
||||
Open the configuration file you created and add the following code:
|
||||
|
||||
```typescript app/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
|
||||
|
||||
4. **Example Directory Structure :**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── app/
|
||||
├── routes/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Creating the API Route
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
The CLI `dev` command allows the Trigger.dev service to send messages to your site. This is required for registering Jobs, triggering them and running tasks. To achieve this it creates a tunnel (using [ngrok](https://ngrok.com/)) so Trigger.dev can send messages to your machine.
|
||||
|
||||
You should leave the `dev` command running when you're developing.
|
||||
|
||||
In a **new terminal window or tab** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on the default port by adding
|
||||
`--port 3001` to the end.
|
||||
</Note>
|
||||
@@ -0,0 +1,26 @@
|
||||
```typescript
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
<Step title="Triggering the Job">
|
||||
|
||||
There are two way to trigger this Job.
|
||||
|
||||
1. Use the "Test" functionality in the dashboard.
|
||||
2. Use the Trigger.dev API (either via our SDK or a web request)
|
||||
|
||||
#### "Testing" from the dashboard
|
||||
|
||||
Click into the Job and then open the "Test" tab. You should see this page:
|
||||
|
||||

|
||||
|
||||
This Job doesn't have a payload schema (meaning it takes an empty object), so you can simple click the "Run test" button.
|
||||
|
||||
**Congratulations, you should get redirected so you can see your first Run!**
|
||||
|
||||
</Step>
|
||||
@@ -0,0 +1,80 @@
|
||||
<Step title="Create a Trigger.dev account">
|
||||
|
||||
You can either:
|
||||
|
||||
- Use the [Trigger.dev Cloud](https://cloud.trigger.dev).
|
||||
- Or [self-host](/documentation/guides/self-hosting) the service.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create your first project">
|
||||
|
||||
Once you've created an account, follow the steps in the app to:
|
||||
|
||||
1. Complete your account details.
|
||||
2. Create your first Organization and Project.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Getting an API key">
|
||||
|
||||
1. Go to the "Environments & API Keys" page in your project.
|
||||

|
||||
|
||||
2. Copy the `DEV` **SERVER** API key.
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run the CLI `init` command">
|
||||
|
||||
The easiest way to get started it to use the CLI. It will add Trigger.dev to your existing project, setup a route and give you an example file.
|
||||
|
||||
In a terminal window run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will ask you a couple of questions
|
||||
|
||||
1. Are you using the [Trigger.dev Cloud](https://cloud.trigger.dev) or [self-hosting](/documentation/guides/self-hosting)?
|
||||
2. Enter your development API key. Enter the key you copied earlier.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run your site">
|
||||
|
||||
Make sure your site is running locally, we will connect to it to register your Jobs.
|
||||
|
||||
<Warning>You must leave this running for the rest of the steps.</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Step>
|
||||
@@ -0,0 +1,20 @@
|
||||
## What's next?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Write your first Job" icon="hexagon-plus" href="/documentation/guides/create-a-job">
|
||||
A Guide for how to create your first real Job
|
||||
</Card>
|
||||
<Card
|
||||
title="What is Trigger.dev"
|
||||
icon="wand-magic-sparkles"
|
||||
href="/documentation/concepts/what-is-triggerdotdev"
|
||||
>
|
||||
Learn more about how Trigger.dev works and how it can help you.
|
||||
</Card>
|
||||
<Card title="Examples" icon="slot-machine" href="/examples">
|
||||
One of the quickest ways to learn how Trigger.dev works is to view some example Jobs.
|
||||
</Card>
|
||||
<Card title="Get help" icon="hire-a-helper" href="/documentation/get-help">
|
||||
Struggling getting setup or have a question? We're here to help.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -22,19 +22,15 @@ To begin, install the necessary packages in your Next.js project directory. You
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
|
||||
npm i @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger-dev/nextjs
|
||||
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -58,7 +54,7 @@ Create a `.env.local` file at the root of your project and include your Trigger
|
||||
```bash
|
||||
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://cloud.trigger.dev
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
|
||||
```
|
||||
|
||||
@@ -66,59 +62,23 @@ Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained f
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
To set up the Trigger Client for your project, follow these steps:
|
||||
Create a file at `<root>/src/trigger.ts` or `<root>/trigger.ts` depending on whether you're using the `src` directory or not. `<root>` represents the root directory of your project.
|
||||
|
||||
1. **Create Configuration File:**
|
||||
Next, add the following code to the file which creates and exports a new `TriggerClient`:
|
||||
|
||||
In your project directory, create a configuration file named `trigger.ts` or `trigger.js`, depending on whether your project uses TypeScript (`.ts`) or JavaScript (`.js`).
|
||||
```typescript src/trigger.(ts/js)
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
2. **Choose Directory:**
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
Depending on your project structure, choose the appropriate directory for the configuration file. If your project uses a `src` directory, create the file within it. Otherwise, create it directly in the project root.
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add Configuration Code:**
|
||||
|
||||
Open the configuration file you created and add the following code:
|
||||
|
||||
```typescript
|
||||
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
|
||||
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
|
||||
|
||||
4. **File Location:**
|
||||
|
||||
Depending on your project structure, save the configuration file in the appropriate location:
|
||||
|
||||
- If your project uses a **src** directory, save the file within the **src** directory.
|
||||
- If your project does not use a **src** directory, save the file in the project root.
|
||||
|
||||
**Example Directory Structure with src:**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── src/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
|
||||
**Example Directory Structure without src:**
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── trigger.ts
|
||||
├── other files...
|
||||
```
|
||||
|
||||
By following these steps, you'll configure the Trigger Client to work with your project, regardless of whether you have a separate **src** directory and whether you're using TypeScript or JavaScript files.
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Creating the API Route
|
||||
|
||||
@@ -238,18 +198,31 @@ Your `package.json` file might look something like this:
|
||||
|
||||
Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client.
|
||||
|
||||
## Next Steps
|
||||
## Running
|
||||
|
||||
Start your Next.js project locally, and then execute the `dev` CLI command to run Trigger.dev locally. You should run this command every time you want to use Trigger.dev locally.
|
||||
### Run your Next.js app
|
||||
|
||||

|
||||
Run your Next.js app locally, like you normally would. For example:
|
||||
|
||||
<Warning>
|
||||
Make sure your Next.js site is running locally before continuing. You must also leave this `dev`
|
||||
terminal command running while you develop.
|
||||
</Warning>
|
||||
<CodeGroup>
|
||||
|
||||
In a **new terminal window or tab** run:
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
@@ -271,9 +244,10 @@ yarn dlx @trigger.dev/cli@latest dev
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Next.js is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
`--hostname <host>`. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
|
||||
@@ -17,106 +17,12 @@ Trigger.dev works with either the Pages or App Router configuration.
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Create a Trigger.dev account
|
||||
<Steps titleSize="h3">
|
||||
<Snippet file="quickstart-setup-steps.mdx" />
|
||||
|
||||
You can either:
|
||||
<Step title="Run the CLI `dev` command">
|
||||
|
||||
- Use the [Trigger.dev Cloud](https://cloud.trigger.dev).
|
||||
- Or [self-host](/documentation/guides/self-hosting) the service.
|
||||
|
||||
### Create your first project
|
||||
|
||||
Once you've created an account, follow the steps in the app to:
|
||||
|
||||
1. Complete your account details.
|
||||
2. Create your first Organization and Project.
|
||||
|
||||
### Getting an API key
|
||||
|
||||
1. Go to the "Environments & API Keys" page in your project.
|
||||

|
||||
|
||||
2. Copy the `DEV` **SERVER** API key.
|
||||

|
||||
|
||||
## Run the CLI `init` command
|
||||
|
||||
The easiest way to get started it to use the CLI. It will add Trigger.dev to your existing Next.js project, setup a route and give you an example file.
|
||||
|
||||
In a terminal window run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will ask you a few questions
|
||||
|
||||
1. Are you using the [Trigger.dev Cloud](https://cloud.trigger.dev) or [self-hosting](/documentation/guides/self-hosting)?
|
||||
2. Enter your development API key. Enter the key you copied earlier.
|
||||
3. Enter a unique ID for your endpoint (you can just use the default by hitting enter)
|
||||
|
||||
## Run your Next.js site
|
||||
|
||||
Make sure your Next.js site is running locally, we will connect to it to register your Jobs.
|
||||
|
||||
<Warning>You must leave this running for the rest of the steps.</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Run the CLI `dev` command
|
||||
|
||||
The CLI `dev` command allows the Trigger.dev service to send messages to your Next.js site. This is required for registering Jobs, triggering them and running tasks. To achieve this it creates a tunnel (using [ngrok](https://ngrok.com/)) so Trigger.dev can send messages to your machine.
|
||||
|
||||
You should leave the `dev` command running when you're developing.
|
||||
|
||||
In a **new terminal window or tab** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
<Snippet file="quickstart-cli-dev.mdx" />
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Experiencing an error? This could be due to middleware.">
|
||||
@@ -152,7 +58,7 @@ yarn add concurrently --dev
|
||||
"dev": "concurrently --kill-others npm:dev:*",
|
||||
"dev:next": "next dev",
|
||||
"dev:trigger": "npx @trigger.dev/cli dev",
|
||||
...
|
||||
//...
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -160,73 +66,24 @@ yarn add concurrently --dev
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Your first job
|
||||
</Step>
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file either `api/trigger/route.ts` or `pages/api/trigger.ts`.
|
||||
<Step title="Your first job">
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file either `src/jobs/examples.(ts/js)` or `jobs/examples.(ts/js)`.
|
||||
|
||||
In there is this Job:
|
||||
|
||||
```typescript
|
||||
//Job definition – uses the client
|
||||
client.defineJob({
|
||||
// 1. Metadata
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.0.1",
|
||||
// 2. Trigger
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
// 3. Run function
|
||||
run: async (payload, io, ctx) => {
|
||||
// do something
|
||||
await io.logger.info("Hello world!", { payload });
|
||||
|
||||
return {
|
||||
message: "Hello world!",
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
<Snippet file="quickstart-example-job.mdx" />
|
||||
|
||||
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
|
||||
|
||||

|
||||
|
||||
## Triggering the Job
|
||||
</Step>
|
||||
|
||||
There are two way to trigger this Job.
|
||||
<Snippet file="quickstart-running-your-job.mdx" />
|
||||
|
||||
1. Use the "Test" functionality in the dashboard.
|
||||
2. Use the Trigger.dev API (either via our SDK or a web request)
|
||||
</Steps>
|
||||
|
||||
### "Testing" from the dashboard
|
||||
|
||||
Click into the Job and then open the "Test" tab. You should see this page:
|
||||
|
||||

|
||||
|
||||
This Job doesn't have a payload schema (meaning it takes an empty object), so you can simple click the "Run test" button.
|
||||
|
||||
Congratulations, you should get redirected so you can see your first Run!
|
||||
|
||||
## What's next?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Write your first Job" icon="hexagon-plus" href="/documentation/guides/create-a-job">
|
||||
A Guide for how to create your first real Job
|
||||
</Card>
|
||||
<Card
|
||||
title="What is Trigger.dev"
|
||||
icon="wand-magic-sparkles"
|
||||
href="/documentation/concepts/what-is-triggerdotdev"
|
||||
>
|
||||
Learn more about how Trigger.dev works and how it can help you.
|
||||
</Card>
|
||||
<Card title="Examples" icon="slot-machine" href="/examples">
|
||||
One of the quickest ways to learn how Trigger.dev works is to view some example Jobs.
|
||||
</Card>
|
||||
<Card title="Get help" icon="hire-a-helper" href="/documentation/get-help">
|
||||
Struggling getting setup or have a question? We're here to help.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
|
||||
@@ -4,4 +4,83 @@ sidebarTitle: "Remix"
|
||||
description: "Start creating Jobs in 5 minutes in your Remix project."
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-remix.mdx" />
|
||||
This quick start guide will get you up and running with Trigger.dev.
|
||||
|
||||
<Accordion title="Need to create a new Remix project to add Trigger.dev to?">
|
||||
No problem, create a blank project by running the `create-remix` command in your terminal then continue with this quickstart guide as normal:
|
||||
|
||||
```bash
|
||||
npx create-remix@latest
|
||||
```
|
||||
|
||||
Trigger.dev works with Remix v1 and v2.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Steps titleSize="h3">
|
||||
<Snippet file="quickstart-setup-steps.mdx" />
|
||||
|
||||
<Step title="Run the CLI `dev` command">
|
||||
|
||||
<Snippet file="quickstart-cli-dev.mdx" />
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Advanced: Run your Remix server together with the CLI">
|
||||
You can modify your `package.json` to run both the Remix server and the CLI `dev` command together.
|
||||
|
||||
1. Install the `concurrently` package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add concurrently --dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
2. Modify your `package.json` file's `dev` script.
|
||||
|
||||
```json package.json
|
||||
//...
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others npm:dev:*",
|
||||
//your normal remix dev command would go here
|
||||
"dev:remix": "remix dev",
|
||||
"dev:trigger": "npx @trigger.dev/cli dev",
|
||||
//...
|
||||
}
|
||||
//...
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file either `app/jobs/example.server.(ts/js)`.
|
||||
|
||||
In there is this Job:
|
||||
|
||||
<Snippet file="quickstart-example-job.mdx" />
|
||||
|
||||
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Snippet file="quickstart-running-your-job.mdx" />
|
||||
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## ✨ @trigger.dev/cli - Initialize your Next.js project to start using Trigger.dev
|
||||
## ✨ @trigger.dev/cli - Initialize your project to start using Trigger.dev
|
||||
|
||||
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly your Next.js project.
|
||||
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly in your existing project.
|
||||
|
||||
## 💻 Usage
|
||||
|
||||
To initialize your Next.js project using `@trigger.dev/cli`, run any of the following three commands and answer the prompts:
|
||||
To initialize your project using `@trigger.dev/cli`, run any of the following three commands and answer the prompts:
|
||||
|
||||
### npm
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: [
|
||||
"<rootDir>/test/**/*.ts?(x)",
|
||||
"<rootDir>/test/**/?(*.)+(spec|test).ts?(x)",
|
||||
"<rootDir>/src/**/?(*.)+(spec|test).ts?(x)"
|
||||
],
|
||||
};
|
||||
@@ -35,15 +35,13 @@
|
||||
"trigger-cli": "./dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gmrchk/cli-testing-library": "^0.1.2",
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/gradient-string": "^1.1.2",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "16",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsup": "^6.5.0",
|
||||
"type-fest": "^3.6.0",
|
||||
"typescript": "^4.9.5",
|
||||
@@ -68,6 +66,7 @@
|
||||
"gradient-string": "^2.0.2",
|
||||
"inquirer": "^9.1.4",
|
||||
"localtunnel": "^2.0.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"ngrok": "5.0.0-beta.2",
|
||||
"node-fetch": "^3.3.0",
|
||||
|
||||
@@ -17,12 +17,12 @@ program.name(COMMAND_NAME).description("The Trigger.dev CLI").version("0.0.1");
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description("Initialize Trigger.dev in your Next.js project")
|
||||
.option("-p, --project-path <project-path>", "The path to the Next.js project", ".")
|
||||
.description("Initialize Trigger.dev in your project")
|
||||
.option("-p, --project-path <project-path>", "The path to the project", ".")
|
||||
.option("-k, --api-key <api-key>", "The development API key to use for the project.")
|
||||
.option(
|
||||
"-e, --endpoint-id <endpoint-id>",
|
||||
"The unique ID for the endpoint to use for this project. (e.g. my-nextjs-project)"
|
||||
"The unique ID for the endpoint to use for this project. (e.g. my-project)"
|
||||
)
|
||||
.option(
|
||||
"-t, --trigger-url <trigger-url>",
|
||||
@@ -41,11 +41,11 @@ program
|
||||
|
||||
program
|
||||
.command("dev")
|
||||
.description("Tunnel your local Next.js project to Trigger.dev and start running jobs")
|
||||
.description("Tunnel your local project to Trigger.dev and start running jobs")
|
||||
.argument("[path]", "The path to the project", ".")
|
||||
.option("-p, --port <port>", "The local port your server is on", "3000")
|
||||
.option("-H, --hostname <hostname>", "Hostname on which the application is served", "localhost")
|
||||
.option("-e, --env-file <name>", "The name of the env file to load", ".env.local")
|
||||
.option("-p, --port <port>", "Override the local port your server is on")
|
||||
.option("-H, --hostname <hostname>", "Override the hostname on which the application is served")
|
||||
.option("-e, --env-file <name>", "Override the name of the env file to load")
|
||||
.option(
|
||||
"-i, --client-id <name>",
|
||||
"The ID of the client to use for this project. Will use the value from the package.json file if not provided."
|
||||
|
||||
@@ -207,7 +207,7 @@ export default defineConfig([
|
||||
// Install the dependencies
|
||||
await installDependencies(resolvedPath);
|
||||
|
||||
logger.success(`✅ Successfully initialized ${resolvedOptions.packageName} at ${resolvedPath}`);
|
||||
logger.success(`✔ Successfully initialized ${resolvedOptions.packageName} at ${resolvedPath}`);
|
||||
logger.info("Next steps:");
|
||||
logger.info(` 1. If you generated code, double check it for errors.`);
|
||||
logger.info(
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import chalk from "chalk";
|
||||
import childProcess from "child_process";
|
||||
import chokidar from "chokidar";
|
||||
import fs from "fs/promises";
|
||||
import ngrok from "ngrok";
|
||||
import fetch from "../utils/fetchUseProxy";
|
||||
import { run as ncuRun } from "npm-check-updates";
|
||||
import ora, { Ora } from "ora";
|
||||
import pathModule from "path";
|
||||
import util from "util";
|
||||
import { z } from "zod";
|
||||
import { Framework, getFramework } from "../frameworks";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { getEnvFilename } from "../utils/env";
|
||||
import fetch from "../utils/fetchUseProxy";
|
||||
import { getTriggerApiDetails } from "../utils/getTriggerApiDetails";
|
||||
import { getUserPackageManager } from "../utils/getUserPkgManager";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { RequireKeys } from "../utils/requiredKeys";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
import { run as ncuRun } from 'npm-check-updates'
|
||||
import chalk from "chalk";
|
||||
import { standardWatchIgnoreRegex, standardWatchFilePaths } from "../frameworks/watchConfig";
|
||||
|
||||
const asyncExecFile = util.promisify(childProcess.execFile);
|
||||
|
||||
export const DevCommandOptionsSchema = z.object({
|
||||
port: z.coerce.number(),
|
||||
hostname: z.string(),
|
||||
envFile: z.string(),
|
||||
port: z.coerce.number().optional(),
|
||||
hostname: z.string().optional(),
|
||||
envFile: z.string().optional(),
|
||||
handlerPath: z.string(),
|
||||
clientId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type DevCommandOptions = z.infer<typeof DevCommandOptionsSchema>;
|
||||
type ResolvedOptions = RequireKeys<DevCommandOptions, "handlerPath" | "envFile">;
|
||||
|
||||
const throttleTimeMs = 1000;
|
||||
|
||||
@@ -47,7 +53,9 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
await checkForOutdatedPackages(resolvedPath)
|
||||
|
||||
//check for outdated packages, don't await this
|
||||
checkForOutdatedPackages(resolvedPath);
|
||||
|
||||
// Read from package.json to get the endpointId
|
||||
const endpointId = await getEndpointIdFromPackageJson(resolvedPath, options);
|
||||
@@ -60,50 +68,44 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
}
|
||||
logger.success(`✔️ [trigger.dev] Detected TriggerClient id: ${endpointId}`);
|
||||
|
||||
// Read from .env.local or .env to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, options.envFile);
|
||||
//resolve the options using the detected framework (use default if there isn't a matching framework)
|
||||
const packageManager = await getUserPackageManager(resolvedPath);
|
||||
const framework = await getFramework(resolvedPath, packageManager);
|
||||
const resolvedOptions = await resolveOptions(framework, resolvedPath, options);
|
||||
|
||||
// Read from .env.local or .env to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, resolvedOptions.envFile);
|
||||
if (!apiDetails) {
|
||||
telemetryClient.dev.failed("missing_api_key", options);
|
||||
telemetryClient.dev.failed("missing_api_key", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiUrl, envFile, apiKey } = apiDetails;
|
||||
|
||||
logger.success(`✔️ [trigger.dev] Found API Key in ${envFile} file`);
|
||||
|
||||
logger.info(` [trigger.dev] Looking for Next.js site on port ${options.port}`);
|
||||
|
||||
const localEndpointHandlerUrl = `http://${options.hostname}:${options.port}${options.handlerPath}`;
|
||||
|
||||
try {
|
||||
await fetch(localEndpointHandlerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-trigger-api-key": apiKey,
|
||||
"x-trigger-action": "PING",
|
||||
"x-trigger-endpoint-id": endpointId,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
//verify that the endpoint can be reached
|
||||
const verifiedEndpoint = await verifyEndpoint(resolvedOptions, endpointId, apiKey, framework);
|
||||
if (!verifiedEndpoint) {
|
||||
logger.error(
|
||||
`❌ [trigger.dev] No server found on port ${options.port}. Make sure your Next.js app is running and try again.`
|
||||
`✖ [trigger.dev] Failed to find a valid Trigger.dev endpoint. Make sure your app is running and try again.`
|
||||
);
|
||||
telemetryClient.dev.failed("no_server_found", options);
|
||||
logger.info(` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port.`);
|
||||
telemetryClient.dev.failed("no_server_found", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryClient.dev.serverRunning(path, options);
|
||||
const { hostname, port, handlerPath } = verifiedEndpoint;
|
||||
|
||||
telemetryClient.dev.serverRunning(path, resolvedOptions);
|
||||
|
||||
// Setup tunnel
|
||||
const endpointUrl = await resolveEndpointUrl(apiUrl, options.port, options.hostname);
|
||||
const endpointUrl = await resolveEndpointUrl(apiUrl, port, hostname);
|
||||
if (!endpointUrl) {
|
||||
telemetryClient.dev.failed("failed_to_create_tunnel", options);
|
||||
telemetryClient.dev.failed("failed_to_create_tunnel", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
const endpointHandlerUrl = `${endpointUrl}${options.handlerPath}`;
|
||||
telemetryClient.dev.tunnelRunning(path, options);
|
||||
const endpointHandlerUrl = `${endpointUrl}${handlerPath}`;
|
||||
telemetryClient.dev.tunnelRunning(path, resolvedOptions);
|
||||
|
||||
const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`);
|
||||
|
||||
@@ -113,13 +115,13 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
const refresh = async () => {
|
||||
connectingSpinner.start();
|
||||
|
||||
const refreshedEndpointId = await getEndpointIdFromPackageJson(resolvedPath, options);
|
||||
const refreshedEndpointId = await getEndpointIdFromPackageJson(resolvedPath, resolvedOptions);
|
||||
|
||||
// Read from .env.local to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
// Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, envFile);
|
||||
|
||||
if (!apiDetails) {
|
||||
connectingSpinner.fail(`❌ [trigger.dev] Failed to connect: Missing API Key`);
|
||||
connectingSpinner.fail(`[trigger.dev] Failed to connect: Missing API Key`);
|
||||
logger.info(`Will attempt again on the next file change…`);
|
||||
attemptCount = 0;
|
||||
return;
|
||||
@@ -131,10 +133,10 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
const authorizedKey = await apiClient.whoami(apiKey);
|
||||
if (!authorizedKey) {
|
||||
logger.error(
|
||||
`🛑 The API key you provided is not authorized. Try visiting your dashboard to get a new API key.`
|
||||
`✖ [trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key.`
|
||||
);
|
||||
|
||||
telemetryClient.dev.failed("invalid_api_key", options);
|
||||
telemetryClient.dev.failed("invalid_api_key", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,18 +161,18 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
|
||||
if (!hasConnected) {
|
||||
hasConnected = true;
|
||||
telemetryClient.dev.connected(path, options);
|
||||
telemetryClient.dev.connected(path, resolvedOptions);
|
||||
}
|
||||
} else {
|
||||
attemptCount++;
|
||||
|
||||
if (attemptCount === 10 || !result.retryable) {
|
||||
connectingSpinner.fail(`🚨 Failed to connect: ${result.error}`);
|
||||
connectingSpinner.fail(`Failed to connect: ${result.error}`);
|
||||
logger.info(`Will attempt again on the next file change…`);
|
||||
attemptCount = 0;
|
||||
|
||||
if (!hasConnected) {
|
||||
telemetryClient.dev.failed("failed_to_connect", options);
|
||||
telemetryClient.dev.failed("failed_to_connect", resolvedOptions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -182,25 +184,19 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes to .ts files and refresh endpoints
|
||||
const watcher = chokidar.watch(
|
||||
[
|
||||
`${resolvedPath}/**/*.ts`,
|
||||
`${resolvedPath}/**/*.tsx`,
|
||||
`${resolvedPath}/**/*.js`,
|
||||
`${resolvedPath}/**/*.jsx`,
|
||||
`${resolvedPath}/**/*.json`,
|
||||
`${resolvedPath}/pnpm-lock.yaml`,
|
||||
],
|
||||
{
|
||||
ignored: /(node_modules|\.next)/,
|
||||
//don't trigger a watch when it collects the paths
|
||||
ignoreInitial: true,
|
||||
}
|
||||
// Watch for changes to files and refresh endpoints
|
||||
const watchPaths = (framework?.watchFilePaths ?? standardWatchFilePaths).map(
|
||||
(path) => `${resolvedPath}/${path}`
|
||||
);
|
||||
const ignored = framework?.watchIgnoreRegex ?? standardWatchIgnoreRegex;
|
||||
const watcher = chokidar.watch(watchPaths, {
|
||||
ignored,
|
||||
//don't trigger a watch when it collects the paths
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on("all", (_event, _path) => {
|
||||
// console.log(_event, _path);
|
||||
console.log(_event, _path);
|
||||
throttle(refresh, throttleTimeMs);
|
||||
});
|
||||
|
||||
@@ -208,34 +204,128 @@ export async function devCommand(path: string, anyOptions: any) {
|
||||
throttle(refresh, throttleTimeMs);
|
||||
}
|
||||
|
||||
export async function checkForOutdatedPackages(path: string) {
|
||||
|
||||
const updates = await ncuRun({
|
||||
packageFile: `${path}/package.json`,
|
||||
filter: "/trigger.dev\/.+$/",
|
||||
upgrade: false,
|
||||
}) as {
|
||||
[key: string]: string;
|
||||
async function resolveOptions(
|
||||
framework: Framework | undefined,
|
||||
path: string,
|
||||
unresolvedOptions: DevCommandOptions
|
||||
): Promise<ResolvedOptions> {
|
||||
if (!framework) {
|
||||
logger.info("Failed to detect framework, using default values");
|
||||
return {
|
||||
port: unresolvedOptions.port ?? 3000,
|
||||
hostname: unresolvedOptions.hostname ?? "localhost",
|
||||
envFile: unresolvedOptions.envFile ?? ".env",
|
||||
handlerPath: unresolvedOptions.handlerPath,
|
||||
clientId: unresolvedOptions.clientId,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof updates === 'undefined' || Object.keys(updates).length === 0) {
|
||||
//get env filename
|
||||
const envName = await getEnvFilename(path, framework.possibleEnvFilenames());
|
||||
|
||||
return {
|
||||
port: unresolvedOptions.port,
|
||||
hostname: unresolvedOptions.hostname,
|
||||
envFile: unresolvedOptions.envFile ?? envName ?? ".env",
|
||||
handlerPath: unresolvedOptions.handlerPath,
|
||||
clientId: unresolvedOptions.clientId,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyEndpoint(
|
||||
resolvedOptions: ResolvedOptions,
|
||||
endpointId: string,
|
||||
apiKey: string,
|
||||
framework?: Framework
|
||||
) {
|
||||
//create list of hostnames to try
|
||||
const hostnames = [];
|
||||
if (resolvedOptions.hostname) {
|
||||
hostnames.push(resolvedOptions.hostname);
|
||||
}
|
||||
if (framework) {
|
||||
hostnames.push(...framework.defaultHostnames);
|
||||
} else {
|
||||
hostnames.push("localhost");
|
||||
}
|
||||
|
||||
//create list of ports to try
|
||||
const ports = [];
|
||||
if (resolvedOptions.port) {
|
||||
ports.push(resolvedOptions.port);
|
||||
}
|
||||
if (framework) {
|
||||
ports.push(...framework.defaultPorts);
|
||||
} else {
|
||||
ports.push(3000);
|
||||
}
|
||||
|
||||
//create list of urls to try
|
||||
const urls: { hostname: string; port: number }[] = [];
|
||||
for (const hostname of hostnames) {
|
||||
for (const port of ports) {
|
||||
urls.push({ hostname, port });
|
||||
}
|
||||
}
|
||||
|
||||
//try each hostname
|
||||
for (const url of urls) {
|
||||
const { hostname, port } = url;
|
||||
const localEndpointHandlerUrl = `http://${hostname}:${port}${resolvedOptions.handlerPath}`;
|
||||
|
||||
const spinner = ora(
|
||||
`[trigger.dev] Looking for your trigger endpoint: ${localEndpointHandlerUrl}`
|
||||
).start();
|
||||
|
||||
try {
|
||||
const response = await fetch(localEndpointHandlerUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-trigger-api-key": apiKey,
|
||||
"x-trigger-action": "PING",
|
||||
"x-trigger-endpoint-id": endpointId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok || response.status !== 200) {
|
||||
spinner.fail(
|
||||
`[trigger.dev] Server responded with ${response.status} (${localEndpointHandlerUrl}).`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
spinner.succeed(`[trigger.dev] Found your trigger endpoint: ${localEndpointHandlerUrl}`);
|
||||
return { hostname, port, handlerPath: resolvedOptions.handlerPath };
|
||||
} catch (err) {
|
||||
spinner.fail(`[trigger.dev] No server found (${localEndpointHandlerUrl}).`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export async function checkForOutdatedPackages(path: string) {
|
||||
const updates = (await ncuRun({
|
||||
packageFile: `${path}/package.json`,
|
||||
filter: "/trigger.dev/.+$/",
|
||||
upgrade: false,
|
||||
})) as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
if (typeof updates === "undefined" || Object.keys(updates).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageFile = await fs.readFile(`${path}/package.json`);
|
||||
const data = JSON.parse(Buffer.from(packageFile).toString('utf8'));
|
||||
const data = JSON.parse(Buffer.from(packageFile).toString("utf8"));
|
||||
const dependencies = data.dependencies;
|
||||
console.log(
|
||||
chalk.bgYellow('Updates available for trigger.dev packages')
|
||||
);
|
||||
console.log(
|
||||
chalk.bgBlue('Run npx @trigger.dev/cli@latest update')
|
||||
);
|
||||
console.log(chalk.bgYellow("Updates available for trigger.dev packages"));
|
||||
console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update"));
|
||||
|
||||
for (let dep in updates) {
|
||||
console.log(`${dep} ${dependencies[dep]} → ${updates[dep]}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getEndpointIdFromPackageJson(path: string, options: DevCommandOptions) {
|
||||
@@ -256,14 +346,14 @@ export async function getEndpointIdFromPackageJson(path: string, options: DevCom
|
||||
async function resolveEndpointUrl(apiUrl: string, port: number, hostname: string) {
|
||||
const apiURL = new URL(apiUrl);
|
||||
|
||||
if (apiURL.hostname === "localhost") {
|
||||
//if the API is localhost and the hostname is localhost
|
||||
if (apiURL.hostname === "localhost" && hostname === "localhost") {
|
||||
return `http://${hostname}:${port}`;
|
||||
}
|
||||
|
||||
// Setup tunnel
|
||||
const tunnelSpinner = ora(`🚇 Creating tunnel`).start();
|
||||
|
||||
const tunnelUrl = await createTunnel(port, tunnelSpinner);
|
||||
const tunnelUrl = await createTunnel(hostname, port, tunnelSpinner);
|
||||
|
||||
if (tunnelUrl) {
|
||||
tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`);
|
||||
@@ -272,9 +362,9 @@ async function resolveEndpointUrl(apiUrl: string, port: number, hostname: string
|
||||
return tunnelUrl;
|
||||
}
|
||||
|
||||
async function createTunnel(port: number, spinner: Ora) {
|
||||
async function createTunnel(hostname: string, port: number, spinner: Ora) {
|
||||
try {
|
||||
return await ngrok.connect(port);
|
||||
return await ngrok.connect({ addr: `${hostname}:${port}` });
|
||||
} catch (error: any) {
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
|
||||
@@ -3,19 +3,22 @@
|
||||
import fs from "fs/promises";
|
||||
import inquirer from "inquirer";
|
||||
import pathModule from "path";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
import { simpleGit } from "simple-git";
|
||||
import { parse } from "tsconfck";
|
||||
import { pathToFileURL } from "url";
|
||||
import { promptApiKey, promptTriggerUrl } from "../cli/index";
|
||||
import { CLOUD_API_URL, CLOUD_TRIGGER_URL, COMMAND_NAME } from "../consts";
|
||||
import { TelemetryClient, telemetryClient } from "../telemetry/telemetry";
|
||||
import { Framework, frameworkNames, getFramework } from "../frameworks";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { addDependencies } from "../utils/addDependencies";
|
||||
import { detectNextJsProject } from "../utils/detectNextJsProject";
|
||||
import { pathExists, readJSONFile } from "../utils/fileSystem";
|
||||
import {
|
||||
getEnvFilename,
|
||||
setApiKeyEnvironmentVariable,
|
||||
setApiUrlEnvironmentVariable,
|
||||
} from "../utils/env";
|
||||
import { readJSONFile } from "../utils/fileSystem";
|
||||
import { PackageManager, getUserPackageManager } from "../utils/getUserPkgManager";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { renderApiKey } from "../utils/renderApiKey";
|
||||
import { readPackageJson } from "../utils/readPackageJson";
|
||||
import { renderTitle } from "../utils/renderTitle";
|
||||
import { TriggerApi, WhoamiResponse } from "../utils/triggerApi";
|
||||
|
||||
@@ -44,21 +47,19 @@ export const initCommand = async (options: InitCommandOptions) => {
|
||||
logger.info(`✨ Initializing Trigger.dev in project`);
|
||||
}
|
||||
|
||||
// Detect if are are in a Next.js project
|
||||
const isNextJsProject = await detectNextJsProject(resolvedPath);
|
||||
const packageManager = await getUserPackageManager(resolvedPath);
|
||||
const framework = await getFramework(resolvedPath, packageManager);
|
||||
|
||||
if (!isNextJsProject) {
|
||||
if (!framework) {
|
||||
logger.error(
|
||||
"We currently only support automatic setup for Next.js projects (we didn't detect one). View our manual installation guides for all frameworks: https://trigger.dev/docs/documentation/quickstarts/introduction"
|
||||
`We currently only support automatic setup for ${frameworkNames()} projects (we didn't detect one). View our manual installation guides for all frameworks: https://trigger.dev/docs/documentation/quickstarts/introduction`
|
||||
);
|
||||
telemetryClient.init.failed("not_nextjs_project", options);
|
||||
telemetryClient.init.failed("not_supported_project", options);
|
||||
return;
|
||||
} else {
|
||||
logger.success("✅ Detected Next.js project");
|
||||
}
|
||||
logger.success(`✔ Detected ${framework.name} project`);
|
||||
|
||||
const hasGitChanges = await detectGitChanges(resolvedPath);
|
||||
|
||||
if (hasGitChanges) {
|
||||
// Warn the user that they have git changes
|
||||
logger.warn(
|
||||
@@ -69,11 +70,7 @@ export const initCommand = async (options: InitCommandOptions) => {
|
||||
const isTypescriptProject = await detectTypescriptProject(resolvedPath);
|
||||
telemetryClient.init.isTypescriptProject(isTypescriptProject, options);
|
||||
|
||||
const optionsAfterPrompts = await resolveOptionsWithPrompts(
|
||||
options,
|
||||
resolvedPath,
|
||||
telemetryClient
|
||||
);
|
||||
const optionsAfterPrompts = await resolveOptionsWithPrompts(options, resolvedPath);
|
||||
const apiKey = optionsAfterPrompts.apiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
@@ -103,61 +100,51 @@ export const initCommand = async (options: InitCommandOptions) => {
|
||||
const endpointSlug = authorizedKey.project.slug;
|
||||
const resolvedOptions: ResolvedOptions = { ...optionsAfterPrompts, endpointSlug };
|
||||
|
||||
await addDependencies(resolvedPath, [
|
||||
{ name: "@trigger.dev/sdk", tag: "latest" },
|
||||
{ name: "@trigger.dev/nextjs", tag: "latest" },
|
||||
]);
|
||||
|
||||
//install dependencies
|
||||
const dependencies = await framework.dependencies();
|
||||
await addDependencies(resolvedPath, dependencies);
|
||||
telemetryClient.init.addedDependencies(resolvedOptions);
|
||||
|
||||
// Setup environment variables
|
||||
await setupEnvironmentVariables(resolvedPath, resolvedOptions);
|
||||
|
||||
const usesSrcDir = await detectUseOfSrcDir(resolvedPath);
|
||||
|
||||
if (usesSrcDir) {
|
||||
logger.info("📁 Detected use of src directory");
|
||||
// Setup environment variables (create a file if there isn't one)
|
||||
let envName = await getEnvFilename(resolvedPath, framework.possibleEnvFilenames());
|
||||
if (!envName) {
|
||||
envName = framework.possibleEnvFilenames()[0]!;
|
||||
const newEnvPath = pathModule.join(resolvedPath, framework.possibleEnvFilenames()[0]!);
|
||||
await fs.writeFile(newEnvPath, "");
|
||||
}
|
||||
await setApiKeyEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiKey);
|
||||
await setApiUrlEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiUrl);
|
||||
|
||||
const nextJsDir = await detectPagesOrAppDir(resolvedPath, usesSrcDir);
|
||||
const installOptions = {
|
||||
typescript: isTypescriptProject,
|
||||
packageManager,
|
||||
endpointSlug: resolvedOptions.endpointSlug,
|
||||
};
|
||||
|
||||
const routeDir = pathModule.join(resolvedPath, usesSrcDir ? "src" : "");
|
||||
telemetryClient.init.install(resolvedOptions, framework.name, installOptions);
|
||||
await framework.install(resolvedPath, installOptions);
|
||||
|
||||
if (nextJsDir === "pages") {
|
||||
telemetryClient.init.createFiles(resolvedOptions, "pages");
|
||||
await createTriggerPageRoute(
|
||||
resolvedPath,
|
||||
routeDir,
|
||||
resolvedOptions,
|
||||
isTypescriptProject,
|
||||
usesSrcDir
|
||||
);
|
||||
} else {
|
||||
telemetryClient.init.createFiles(resolvedOptions, "app");
|
||||
await createTriggerAppRoute(
|
||||
resolvedPath,
|
||||
routeDir,
|
||||
resolvedOptions,
|
||||
isTypescriptProject,
|
||||
usesSrcDir
|
||||
);
|
||||
}
|
||||
|
||||
await detectMiddlewareUsage(resolvedPath, usesSrcDir);
|
||||
telemetryClient.init.postInstall(resolvedOptions, framework.name, installOptions);
|
||||
await framework.postInstall(resolvedPath, installOptions);
|
||||
|
||||
await addConfigurationToPackageJson(resolvedPath, resolvedOptions);
|
||||
|
||||
await printNextSteps(resolvedOptions, authorizedKey);
|
||||
await printNextSteps(resolvedOptions, authorizedKey, packageManager, framework);
|
||||
telemetryClient.init.completed(resolvedOptions);
|
||||
};
|
||||
|
||||
async function printNextSteps(options: ResolvedOptions, authorizedKey: WhoamiResponse) {
|
||||
async function printNextSteps(
|
||||
options: ResolvedOptions,
|
||||
authorizedKey: WhoamiResponse,
|
||||
packageManager: PackageManager,
|
||||
framework: Framework
|
||||
) {
|
||||
const projectUrl = `${options.triggerUrl}/orgs/${authorizedKey.organization.slug}/projects/${authorizedKey.project.slug}`;
|
||||
|
||||
logger.success(`✅ Successfully initialized Trigger.dev!`);
|
||||
logger.success(`✔ Successfully initialized Trigger.dev!`);
|
||||
|
||||
logger.info("Next steps:");
|
||||
logger.info(` 1. Run your Next.js project locally with 'npm run dev'`);
|
||||
logger.info(` 1. Run your ${framework.name} project locally with '${packageManager} run dev'`);
|
||||
logger.info(
|
||||
` 2. In a separate terminal, run 'npx @trigger.dev/cli@latest dev' to watch for changes and automatically register Trigger.dev jobs`
|
||||
);
|
||||
@@ -169,18 +156,21 @@ async function printNextSteps(options: ResolvedOptions, authorizedKey: WhoamiRes
|
||||
}
|
||||
|
||||
async function addConfigurationToPackageJson(path: string, options: ResolvedOptions) {
|
||||
const pkgJsonPath = pathModule.join(path, "package.json");
|
||||
const pkgBuffer = await fs.readFile(pkgJsonPath);
|
||||
const pkgJson = JSON.parse(pkgBuffer.toString());
|
||||
const pkgJson = await readPackageJson(path);
|
||||
|
||||
if (!pkgJson) {
|
||||
throw new Error("Could not find package.json");
|
||||
}
|
||||
|
||||
pkgJson["trigger.dev"] = {
|
||||
endpointId: options.endpointSlug,
|
||||
};
|
||||
|
||||
// Write the updated package.json file
|
||||
const pkgJsonPath = pathModule.join(path, "package.json");
|
||||
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
||||
|
||||
logger.success(`✅ Wrote trigger.dev config to package.json`);
|
||||
logger.success(`✔ Wrote trigger.dev config to package.json`);
|
||||
}
|
||||
|
||||
type OptionsAfterPrompts = Required<Omit<InitCommandOptions, "endpointSlug">> & {
|
||||
@@ -189,8 +179,7 @@ type OptionsAfterPrompts = Required<Omit<InitCommandOptions, "endpointSlug">> &
|
||||
|
||||
const resolveOptionsWithPrompts = async (
|
||||
options: InitCommandOptions,
|
||||
path: string,
|
||||
telemetryClient: TelemetryClient
|
||||
path: string
|
||||
): Promise<OptionsAfterPrompts> => {
|
||||
const resolvedOptions: InitCommandOptions = { ...options };
|
||||
|
||||
@@ -279,474 +268,3 @@ async function detectTypescriptProject(path: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function detectUseOfSrcDir(path: string): Promise<boolean> {
|
||||
// Detects if the project is using a src directory
|
||||
try {
|
||||
await fs.access(pathModule.join(path, "src"));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect the use of pages or app dir in the Next.js project
|
||||
// Import the next.config.js file and check for experimental: { appDir: true }
|
||||
async function detectPagesOrAppDir(path: string, usesSrcDir = false): Promise<"pages" | "app"> {
|
||||
const nextConfigPath = pathModule.join(path, "next.config.js");
|
||||
const importedConfig = await import(pathToFileURL(nextConfigPath).toString()).catch(() => ({}));
|
||||
|
||||
if (importedConfig?.default?.experimental?.appDir) {
|
||||
return "app";
|
||||
} else {
|
||||
// We need to check if src/app/page.tsx exists
|
||||
// Or app/page.tsx exists
|
||||
// If so then we return app
|
||||
// If not return pages
|
||||
|
||||
const extensionsToCheck = ["jsx", "tsx", "js", "ts"];
|
||||
const basePath = pathModule.join(path, usesSrcDir ? "src" : "", "app", `page.`);
|
||||
|
||||
for (const extension of extensionsToCheck) {
|
||||
const appPagePath = basePath + extension;
|
||||
const appPageExists = await pathExists(appPagePath);
|
||||
|
||||
if (appPageExists) {
|
||||
return "app";
|
||||
}
|
||||
}
|
||||
|
||||
return "pages";
|
||||
}
|
||||
}
|
||||
|
||||
async function detectMiddlewareUsage(path: string, usesSrcDir = false) {
|
||||
const middlewarePath = pathModule.join(path, usesSrcDir ? "src" : "", "middleware.ts");
|
||||
|
||||
const middlewareExists = await pathExists(middlewarePath);
|
||||
|
||||
if (!middlewareExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
|
||||
|
||||
if (!matcher || matcher.length === 0) {
|
||||
logger.warn(
|
||||
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
return;
|
||||
}
|
||||
|
||||
if (matcher.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof matcher === "string") {
|
||||
const matcherRegex = pathToRegexp(matcher);
|
||||
|
||||
// Check to see if /api/trigger matches the regex, if it does, then we need to output a warning with a link to the docs to fix it
|
||||
if (matcherRegex.test("/api/trigger")) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict_api_trigger", { projectPath: path });
|
||||
}
|
||||
} else if (Array.isArray(matcher) && matcher.every((m) => typeof m === "string")) {
|
||||
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
|
||||
|
||||
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
|
||||
const fileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
const regex = /matcher:\s*(\[.*\]|".*")/s;
|
||||
let match = regex.exec(fileContent);
|
||||
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (match.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let matcherString: string = match[1] as string;
|
||||
|
||||
// Handle array scenario
|
||||
if (matcherString.startsWith("[") && matcherString.endsWith("]")) {
|
||||
matcherString = matcherString.slice(1, -1); // Remove brackets
|
||||
const arrayRegex = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g;
|
||||
let arrayMatch;
|
||||
const matches: string[] = [];
|
||||
while ((arrayMatch = arrayRegex.exec(matcherString)) !== null) {
|
||||
matches.push((arrayMatch[1] as string).slice(1, -1)); // remove quotes
|
||||
}
|
||||
return matches;
|
||||
} else {
|
||||
// Handle single string scenario
|
||||
return [matcherString.slice(1, -1)]; // remove quotes
|
||||
}
|
||||
}
|
||||
|
||||
// Find the alias that points to the "src" directory.
|
||||
// So for example, the paths object could be:
|
||||
// {
|
||||
// "@/*": ["./src/*"]
|
||||
// }
|
||||
// In this case, we would return "@"
|
||||
function getPathAlias(tsconfig: any, usesSrcDir: boolean) {
|
||||
if (!tsconfig.compilerOptions.paths) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = tsconfig.compilerOptions.paths;
|
||||
|
||||
const alias = Object.keys(paths).find((key) => {
|
||||
const value = paths[key];
|
||||
|
||||
if (value.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = value[0];
|
||||
|
||||
if (usesSrcDir) {
|
||||
return path === "./src/*";
|
||||
} else {
|
||||
return path === "./*";
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure to remove the trailing "/*"
|
||||
if (alias) {
|
||||
return alias.slice(0, -2);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async function createTriggerAppRoute(
|
||||
projectPath: string,
|
||||
path: string,
|
||||
options: ResolvedOptions,
|
||||
isTypescriptProject: boolean,
|
||||
usesSrcDir = false
|
||||
) {
|
||||
const configFileName = isTypescriptProject ? "tsconfig.json" : "jsconfig.json";
|
||||
const tsConfigPath = pathModule.join(projectPath, configFileName);
|
||||
const { tsconfig } = await parse(tsConfigPath);
|
||||
|
||||
const extension = isTypescriptProject ? ".ts" : ".js";
|
||||
const triggerFileName = `trigger${extension}`;
|
||||
const examplesFileName = `examples${extension}`;
|
||||
const examplesIndexFileName = `index${extension}`;
|
||||
const routeFileName = `route${extension}`;
|
||||
|
||||
const pathAlias = getPathAlias(tsconfig, usesSrcDir);
|
||||
const routePathPrefix = pathAlias ? pathAlias + "/" : "../../../";
|
||||
|
||||
const routeContent = `
|
||||
import { createAppRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
//this route is used to send and receive data with Trigger.dev
|
||||
export const { POST, dynamic } = createAppRoute(client);
|
||||
`;
|
||||
|
||||
const triggerContent = `
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "${options.endpointSlug}",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
`;
|
||||
|
||||
const jobsPathPrefix = pathAlias ? pathAlias + "/" : "../";
|
||||
|
||||
const jobsContent = `
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "${jobsPathPrefix}trigger";
|
||||
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
|
||||
`;
|
||||
|
||||
const examplesIndexContent = `
|
||||
// import all your job files here
|
||||
|
||||
export * from "./examples"
|
||||
`;
|
||||
|
||||
const directories = pathModule.join(path, "app", "api", "trigger");
|
||||
await fs.mkdir(directories, { recursive: true });
|
||||
|
||||
const fileExists = await pathExists(pathModule.join(directories, routeFileName));
|
||||
|
||||
if (fileExists) {
|
||||
logger.info("Skipping creation of app route because it already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(pathModule.join(directories, routeFileName), routeContent);
|
||||
|
||||
logger.success(
|
||||
`✅ Created app route at ${usesSrcDir ? "src/" : ""}app/api/${removeFileExtension(
|
||||
triggerFileName
|
||||
)}/${routeFileName}`
|
||||
);
|
||||
|
||||
const triggerFileExists = await pathExists(pathModule.join(path, triggerFileName));
|
||||
|
||||
if (!triggerFileExists) {
|
||||
await fs.writeFile(pathModule.join(path, triggerFileName), triggerContent);
|
||||
|
||||
logger.success(`✅ Created trigger client at ${usesSrcDir ? "src/" : ""}${triggerFileName}`);
|
||||
}
|
||||
|
||||
const exampleDirectories = pathModule.join(path, "jobs");
|
||||
await fs.mkdir(exampleDirectories, { recursive: true });
|
||||
|
||||
const exampleFileExists = await pathExists(pathModule.join(exampleDirectories, examplesFileName));
|
||||
|
||||
if (!exampleFileExists) {
|
||||
await fs.writeFile(pathModule.join(exampleDirectories, examplesFileName), jobsContent);
|
||||
|
||||
await fs.writeFile(
|
||||
pathModule.join(exampleDirectories, examplesIndexFileName),
|
||||
examplesIndexContent
|
||||
);
|
||||
|
||||
logger.success(`✅ Created example job at ${usesSrcDir ? "src/" : ""}jobs/examples.ts`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createTriggerPageRoute(
|
||||
projectPath: string,
|
||||
path: string,
|
||||
options: ResolvedOptions,
|
||||
isTypescriptProject: boolean,
|
||||
usesSrcDir = false
|
||||
) {
|
||||
const configFileName = isTypescriptProject ? "tsconfig.json" : "jsconfig.json";
|
||||
const tsConfigPath = pathModule.join(projectPath, configFileName);
|
||||
const { tsconfig } = await parse(tsConfigPath);
|
||||
|
||||
const pathAlias = getPathAlias(tsconfig, usesSrcDir);
|
||||
const routePathPrefix = pathAlias ? pathAlias + "/" : "../../";
|
||||
|
||||
const extension = isTypescriptProject ? ".ts" : ".js";
|
||||
const triggerFileName = `trigger${extension}`;
|
||||
const examplesFileName = `examples${extension}`;
|
||||
const examplesIndexFileName = `index${extension}`;
|
||||
|
||||
const routeContent = `
|
||||
import { createPagesRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
//this route is used to send and receive data with Trigger.dev
|
||||
const { handler, config } = createPagesRoute(client);
|
||||
export { config };
|
||||
|
||||
export default handler;
|
||||
`;
|
||||
|
||||
const triggerContent = `
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "${options.endpointSlug}",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
`;
|
||||
|
||||
const jobsPathPrefix = pathAlias ? pathAlias + "/" : "../";
|
||||
|
||||
const jobsContent = `
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "${jobsPathPrefix}trigger";
|
||||
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const examplesIndexContent = `
|
||||
// import all your job files here
|
||||
|
||||
export * from "./examples"
|
||||
`;
|
||||
|
||||
const directories = pathModule.join(path, "pages", "api");
|
||||
await fs.mkdir(directories, { recursive: true });
|
||||
|
||||
// Don't overwrite the file if it already exists
|
||||
const exists = await pathExists(pathModule.join(directories, triggerFileName));
|
||||
|
||||
if (exists) {
|
||||
logger.info("Skipping creation of pages route because it already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(pathModule.join(directories, triggerFileName), routeContent);
|
||||
logger.success(
|
||||
`✅ Created pages route at ${usesSrcDir ? "src/" : ""}pages/api/${triggerFileName}`
|
||||
);
|
||||
|
||||
const triggerFileExists = await pathExists(pathModule.join(path, triggerFileName));
|
||||
|
||||
if (!triggerFileExists) {
|
||||
await fs.writeFile(pathModule.join(path, triggerFileName), triggerContent);
|
||||
|
||||
logger.success(`✅ Created TriggerClient at ${usesSrcDir ? "src/" : ""}${triggerFileName}`);
|
||||
}
|
||||
|
||||
const exampleDirectories = pathModule.join(path, "jobs");
|
||||
await fs.mkdir(exampleDirectories, { recursive: true });
|
||||
|
||||
const exampleFileExists = await pathExists(pathModule.join(exampleDirectories, examplesFileName));
|
||||
|
||||
if (!exampleFileExists) {
|
||||
await fs.writeFile(pathModule.join(exampleDirectories, examplesFileName), jobsContent);
|
||||
|
||||
await fs.writeFile(
|
||||
pathModule.join(exampleDirectories, examplesIndexFileName),
|
||||
examplesIndexContent
|
||||
);
|
||||
|
||||
logger.success(
|
||||
`✅ Created example job at ${usesSrcDir ? "src/" : ""}jobs/examples/${examplesFileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupEnvironmentVariables(path: string, options: ResolvedOptions) {
|
||||
if (options.apiKey) {
|
||||
await setupEnvironmentVariable(
|
||||
path,
|
||||
".env.local",
|
||||
"TRIGGER_API_KEY",
|
||||
options.apiKey,
|
||||
true,
|
||||
renderApiKey
|
||||
);
|
||||
}
|
||||
|
||||
if (options.triggerUrl) {
|
||||
await setupEnvironmentVariable(path, ".env.local", "TRIGGER_API_URL", options.triggerUrl, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupEnvironmentVariable(
|
||||
dir: string,
|
||||
fileName: string,
|
||||
variableName: string,
|
||||
value: string,
|
||||
replaceValue: boolean = true,
|
||||
renderer: (value: string) => string = (value) => value
|
||||
) {
|
||||
const path = pathModule.join(dir, fileName);
|
||||
const envFileExists = await pathExists(path);
|
||||
|
||||
if (!envFileExists) {
|
||||
await fs.writeFile(path, "");
|
||||
}
|
||||
|
||||
const envFileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
if (envFileContent.includes(variableName)) {
|
||||
if (!replaceValue) {
|
||||
logger.info(
|
||||
`☑ Skipping setting ${variableName}=${renderer(value)} because it already exists`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Update the existing value
|
||||
const updatedEnvFileContent = envFileContent.replace(
|
||||
new RegExp(`${variableName}=.*\\n`, "g"),
|
||||
`${variableName}=${value}\n`
|
||||
);
|
||||
|
||||
await fs.writeFile(path, updatedEnvFileContent);
|
||||
|
||||
logger.success(`✅ Set ${variableName}=${renderer(value)} in ${fileName}`);
|
||||
} else {
|
||||
await fs.appendFile(path, `\n${variableName}=${value}`);
|
||||
|
||||
logger.success(`✅ Added ${variableName}=${renderer(value)} to ${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFileExtension(filename: string) {
|
||||
return filename.replace(/\.[^.]+$/, "");
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function updateCommand(projectPath: string) {
|
||||
|
||||
// If there are no @trigger.dev packages
|
||||
if (triggerPackages.length === 0) {
|
||||
logger.success(`✅ All @trigger.dev/* packages are up to date.`);
|
||||
logger.success(`✔ All @trigger.dev/* packages are up to date.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function updateCommand(projectPath: string) {
|
||||
|
||||
// If no packages require any updation
|
||||
if (packagesToUpdate.length === 0) {
|
||||
logger.success(`✅ All @trigger.dev/* packages are up to date.`);
|
||||
logger.success(`✔ All @trigger.dev/* packages are up to date.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { InstallPackage } from "../utils/addDependencies";
|
||||
import { PackageManager } from "../utils/getUserPkgManager";
|
||||
import { NextJs } from "./nextjs";
|
||||
import { Remix } from "./remix";
|
||||
|
||||
export type ProjectInstallOptions = {
|
||||
typescript: boolean;
|
||||
packageManager: PackageManager;
|
||||
endpointSlug: string;
|
||||
};
|
||||
|
||||
export interface Framework {
|
||||
/** A unique id for the framework */
|
||||
id: string;
|
||||
|
||||
/** Display to the user in messages */
|
||||
name: string;
|
||||
|
||||
/** Is this folder a project using this framework? */
|
||||
isMatch(path: string, packageManager: PackageManager): Promise<boolean>;
|
||||
|
||||
/** List of packages to install */
|
||||
dependencies(): Promise<InstallPackage[]>;
|
||||
|
||||
/** Priority list of env filenames, e.g. ".env" */
|
||||
possibleEnvFilenames(): string[];
|
||||
|
||||
/** Install the required files */
|
||||
install(path: string, options: ProjectInstallOptions): Promise<void>;
|
||||
|
||||
/** You can check for middleware, add extra instructions, etc */
|
||||
postInstall(path: string, options: ProjectInstallOptions): Promise<void>;
|
||||
|
||||
/** Used by the dev command, if a hostname isn't passed in */
|
||||
defaultHostnames: string[];
|
||||
|
||||
/** Used by the dev command, if a port isn't passed in */
|
||||
defaultPorts: number[];
|
||||
|
||||
/** These filenames are watched for changes with the dev command, can use globs. */
|
||||
watchFilePaths: string[];
|
||||
|
||||
/** These folders are ignored when watching for changes with the dev command */
|
||||
watchIgnoreRegex: RegExp;
|
||||
}
|
||||
|
||||
/** The order of these matters. The first one that matches the folder will be used, so stricter ones should be first. */
|
||||
const frameworks: Framework[] = [new NextJs(), new Remix()];
|
||||
|
||||
export const getFramework = async (
|
||||
path: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<Framework | undefined> => {
|
||||
for (const framework of frameworks) {
|
||||
if (await framework.isMatch(path, packageManager)) {
|
||||
return framework;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export function frameworkNames() {
|
||||
return frameworks.map((f) => f.name).join(", ");
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import fs from "fs/promises";
|
||||
import pathModule from "path";
|
||||
import { Framework } from "..";
|
||||
import { templatesPath } from "../../paths";
|
||||
import { InstallPackage } from "../../utils/addDependencies";
|
||||
import { createFileFromTemplate } from "../../utils/createFileFromTemplate";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
import { PackageManager } from "../../utils/getUserPkgManager";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { getPathAlias } from "../../utils/pathAlias";
|
||||
import { readPackageJson } from "../../utils/readPackageJson";
|
||||
import { detectMiddlewareUsage } from "./middleware";
|
||||
import { standardWatchFilePaths } from "../watchConfig";
|
||||
|
||||
export class NextJs implements Framework {
|
||||
id = "nextjs";
|
||||
name = "Next.js";
|
||||
|
||||
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
|
||||
const hasNextConfigFile = await detectNextConfigFile(path);
|
||||
if (hasNextConfigFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return await detectNextDependency(path);
|
||||
}
|
||||
|
||||
async dependencies(): Promise<InstallPackage[]> {
|
||||
return [
|
||||
{ name: "@trigger.dev/sdk", tag: "latest" },
|
||||
{ name: "@trigger.dev/nextjs", tag: "latest" },
|
||||
{ name: "@trigger.dev/react", tag: "latest" },
|
||||
];
|
||||
}
|
||||
|
||||
possibleEnvFilenames(): string[] {
|
||||
return [".env.local", ".env"];
|
||||
}
|
||||
|
||||
async install(
|
||||
path: string,
|
||||
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
|
||||
): Promise<void> {
|
||||
const usesSrcDir = await detectUseOfSrcDir(path);
|
||||
if (usesSrcDir) {
|
||||
logger.info("📁 Detected use of src directory");
|
||||
}
|
||||
|
||||
const nextJsDir = await detectPagesOrAppDir(path);
|
||||
const routeDir = pathModule.join(path, usesSrcDir ? "src" : "");
|
||||
const pathAlias = await getPathAlias({
|
||||
projectPath: path,
|
||||
isTypescriptProject: options.typescript,
|
||||
extraDirectories: usesSrcDir ? ["src"] : undefined,
|
||||
});
|
||||
|
||||
if (nextJsDir === "pages") {
|
||||
await createTriggerPageRoute(routeDir, options.endpointSlug, options.typescript, pathAlias);
|
||||
} else {
|
||||
await createTriggerAppRoute(routeDir, options.endpointSlug, options.typescript, pathAlias);
|
||||
}
|
||||
}
|
||||
|
||||
async postInstall(
|
||||
path: string,
|
||||
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
|
||||
): Promise<void> {
|
||||
await detectMiddlewareUsage(path);
|
||||
}
|
||||
|
||||
defaultHostnames = ["localhost"];
|
||||
defaultPorts = [3000, 3001, 3002];
|
||||
watchFilePaths = standardWatchFilePaths;
|
||||
watchIgnoreRegex = /(node_modules|\.next)/;
|
||||
}
|
||||
|
||||
async function detectNextConfigFile(path: string): Promise<boolean> {
|
||||
return pathExists(pathModule.join(path, "next.config.js"));
|
||||
}
|
||||
|
||||
export async function detectNextDependency(path: string): Promise<boolean> {
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (!packageJsonContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return packageJsonContent.dependencies?.next !== undefined;
|
||||
}
|
||||
|
||||
export async function detectUseOfSrcDir(path: string): Promise<boolean> {
|
||||
// Detects if the project is using a src directory
|
||||
try {
|
||||
await fs.access(pathModule.join(path, "src"));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function detectPagesOrAppDir(path: string): Promise<"pages" | "app"> {
|
||||
const withoutSrcAppPath = pathModule.join(path, "app");
|
||||
if (await pathExists(withoutSrcAppPath)) {
|
||||
return "app";
|
||||
}
|
||||
|
||||
const withSrcAppPath = pathModule.join(path, "src", "app");
|
||||
if (await pathExists(withSrcAppPath)) {
|
||||
return "app";
|
||||
}
|
||||
|
||||
return "pages";
|
||||
}
|
||||
|
||||
async function createTriggerPageRoute(
|
||||
path: string,
|
||||
endpointSlug: string,
|
||||
isTypescriptProject: boolean,
|
||||
pathAlias: string | undefined
|
||||
) {
|
||||
const templatesDir = pathModule.join(templatesPath(), "nextjs");
|
||||
const fileExtension = isTypescriptProject ? ".ts" : ".js";
|
||||
|
||||
//pages/api/trigger.js or src/pages/api/trigger.js
|
||||
const apiRoutePath = pathModule.join(path, "pages", "api", `trigger${fileExtension}`);
|
||||
const apiRouteResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "pagesApiRoute.js"),
|
||||
replacements: {
|
||||
routePathPrefix: pathAlias ? pathAlias + "/" : "../../",
|
||||
},
|
||||
outputPath: apiRoutePath,
|
||||
});
|
||||
if (!apiRouteResult.success) {
|
||||
throw new Error("Failed to create API route file");
|
||||
}
|
||||
logger.success(`✔ Created API route at ${apiRoutePath}`);
|
||||
|
||||
await createJobsAndTriggerFile(path, endpointSlug, fileExtension, pathAlias, templatesDir);
|
||||
}
|
||||
|
||||
async function createTriggerAppRoute(
|
||||
path: string,
|
||||
endpointSlug: string,
|
||||
isTypescriptProject: boolean,
|
||||
pathAlias: string | undefined
|
||||
) {
|
||||
const templatesDir = pathModule.join(templatesPath(), "nextjs");
|
||||
const fileExtension = isTypescriptProject ? ".ts" : ".js";
|
||||
|
||||
//app/api/trigger/route.js or src/app/api/trigger/route.js
|
||||
const apiRoutePath = pathModule.join(path, "app", "api", "trigger", `route${fileExtension}`);
|
||||
const apiRouteResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "appApiRoute.js"),
|
||||
replacements: {
|
||||
routePathPrefix: pathAlias ? pathAlias + "/" : "../../",
|
||||
},
|
||||
outputPath: apiRoutePath,
|
||||
});
|
||||
if (!apiRouteResult.success) {
|
||||
throw new Error("Failed to create API route file");
|
||||
}
|
||||
logger.success(`✔ Created API route at ${apiRoutePath}`);
|
||||
|
||||
await createJobsAndTriggerFile(path, endpointSlug, fileExtension, pathAlias, templatesDir);
|
||||
}
|
||||
|
||||
async function createJobsAndTriggerFile(
|
||||
path: string,
|
||||
endpointSlug: string,
|
||||
fileExtension: string,
|
||||
pathAlias: string | undefined,
|
||||
templatesDir: string
|
||||
) {
|
||||
//trigger.js or src/trigger.js
|
||||
const triggerFilePath = pathModule.join(path, `trigger${fileExtension}`);
|
||||
const triggerResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "trigger.js"),
|
||||
replacements: {
|
||||
endpointSlug,
|
||||
},
|
||||
outputPath: triggerFilePath,
|
||||
});
|
||||
if (!triggerResult.success) {
|
||||
throw new Error("Failed to create trigger file");
|
||||
}
|
||||
logger.success(`✔ Created Trigger client at ${triggerFilePath}`);
|
||||
|
||||
//example jobs
|
||||
const exampleDirectory = pathModule.join(path, "jobs");
|
||||
|
||||
//jobs/examples.js or src/jobs/examples.js
|
||||
const exampleJobFilePath = pathModule.join(exampleDirectory, `examples${fileExtension}`);
|
||||
const exampleJobResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "exampleJob.js"),
|
||||
replacements: {
|
||||
jobsPathPrefix: pathAlias ? pathAlias + "/" : "../",
|
||||
},
|
||||
outputPath: exampleJobFilePath,
|
||||
});
|
||||
if (!exampleJobResult.success) {
|
||||
throw new Error("Failed to create example job file");
|
||||
}
|
||||
logger.success(`✔ Created example job at ${exampleJobFilePath}`);
|
||||
|
||||
//jobs/index.js or src/jobs/index.js
|
||||
const jobsIndexFilePath = pathModule.join(exampleDirectory, `index${fileExtension}`);
|
||||
const jobsIndexResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "jobsIndex.js"),
|
||||
replacements: {
|
||||
jobsPathPrefix: pathAlias ? pathAlias + "/" : "../",
|
||||
},
|
||||
outputPath: jobsIndexFilePath,
|
||||
});
|
||||
if (!jobsIndexResult.success) {
|
||||
throw new Error("Failed to create jobs index file");
|
||||
}
|
||||
logger.success(`✔ Created jobs index at ${jobsIndexFilePath}`);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import fs from "fs/promises";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
import pathModule from "path";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { telemetryClient } from "../../telemetry/telemetry";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
|
||||
export async function detectMiddlewareUsage(path: string, usesSrcDir = false) {
|
||||
const middlewarePath = pathModule.join(path, usesSrcDir ? "src" : "", "middleware.ts");
|
||||
|
||||
const middlewareExists = await pathExists(middlewarePath);
|
||||
|
||||
if (!middlewareExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
|
||||
|
||||
if (!matcher || matcher.length === 0) {
|
||||
logger.warn(
|
||||
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
return;
|
||||
}
|
||||
|
||||
if (matcher.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof matcher === "string") {
|
||||
const matcherRegex = pathToRegexp(matcher);
|
||||
|
||||
// Check to see if /api/trigger matches the regex, if it does, then we need to output a warning with a link to the docs to fix it
|
||||
if (matcherRegex.test("/api/trigger")) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict_api_trigger", { projectPath: path });
|
||||
}
|
||||
} else if (Array.isArray(matcher) && matcher.every((m) => typeof m === "string")) {
|
||||
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
|
||||
|
||||
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
|
||||
const fileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
const regex = /matcher:\s*(\[.*\]|".*")/s;
|
||||
let match = regex.exec(fileContent);
|
||||
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (match.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let matcherString: string = match[1] as string;
|
||||
|
||||
// Handle array scenario
|
||||
if (matcherString.startsWith("[") && matcherString.endsWith("]")) {
|
||||
matcherString = matcherString.slice(1, -1); // Remove brackets
|
||||
const arrayRegex = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g;
|
||||
let arrayMatch;
|
||||
const matches: string[] = [];
|
||||
while ((arrayMatch = arrayRegex.exec(matcherString)) !== null) {
|
||||
matches.push((arrayMatch[1] as string).slice(1, -1)); // remove quotes
|
||||
}
|
||||
return matches;
|
||||
} else {
|
||||
// Handle single string scenario
|
||||
return [matcherString.slice(1, -1)]; // remove quotes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import mock from "mock-fs";
|
||||
import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir } from ".";
|
||||
import { getFramework } from "..";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("Next project detection", () => {
|
||||
test("has dependency", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { next: "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("nextjs");
|
||||
});
|
||||
|
||||
test("no dependency, has next.config.js", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
"next.config.js": "module.exports = {}",
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("nextjs");
|
||||
});
|
||||
|
||||
test("no dependency, no next.config.js", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).not.toEqual("nextjs");
|
||||
});
|
||||
});
|
||||
|
||||
describe("src directory", () => {
|
||||
test("has src directory", async () => {
|
||||
mock({
|
||||
src: {
|
||||
"some-file.txt": "file content here",
|
||||
},
|
||||
});
|
||||
|
||||
const hasSrcDirectory = await detectUseOfSrcDir("");
|
||||
expect(hasSrcDirectory).toEqual(true);
|
||||
});
|
||||
|
||||
test("no src directory", async () => {
|
||||
mock({
|
||||
app: {
|
||||
"some-file.txt": "file content here",
|
||||
},
|
||||
});
|
||||
|
||||
const hasSrcDirectory = await detectUseOfSrcDir("");
|
||||
expect(hasSrcDirectory).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("detect pages or app directory", () => {
|
||||
test("detect 'app' from src/app directory", async () => {
|
||||
mock({
|
||||
"src/app": {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
});
|
||||
|
||||
test("detect 'app' from src/app directory", async () => {
|
||||
mock({
|
||||
"src/app": {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
});
|
||||
|
||||
test("detect 'pages' from src/pages directory", async () => {
|
||||
mock({
|
||||
"src/pages": {
|
||||
"some-file.txt": "file content here",
|
||||
},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
});
|
||||
|
||||
test("detect 'pages' from pages directory", async () => {
|
||||
mock({
|
||||
pages: {
|
||||
"some-file.txt": "file content here",
|
||||
},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pages install", () => {
|
||||
test("src/pages + javascript", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("src/trigger.js")).toEqual(true);
|
||||
expect(await pathExists("src/pages/api/trigger.js")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.js")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/examples.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("pages + javascript", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("trigger.js")).toEqual(true);
|
||||
expect(await pathExists("pages/api/trigger.js")).toEqual(true);
|
||||
expect(await pathExists("jobs/index.js")).toEqual(true);
|
||||
expect(await pathExists("jobs/examples.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("src/pages + typescript", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("src/trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("src/pages/api/trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.ts")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/examples.ts")).toEqual(true);
|
||||
});
|
||||
|
||||
test("pages + typescript", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("pages");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("pages/api/trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("jobs/index.ts")).toEqual(true);
|
||||
expect(await pathExists("jobs/examples.ts")).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("app install", () => {
|
||||
test("src/app + javascript", async () => {
|
||||
mock({
|
||||
"src/app": {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("src/trigger.js")).toEqual(true);
|
||||
expect(await pathExists("src/app/api/trigger/route.js")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.js")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/examples.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("app + javascript", async () => {
|
||||
mock({
|
||||
app: {},
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("trigger.js")).toEqual(true);
|
||||
expect(await pathExists("app/api/trigger/route.js")).toEqual(true);
|
||||
expect(await pathExists("jobs/index.js")).toEqual(true);
|
||||
expect(await pathExists("jobs/examples.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("src/app + typescript", async () => {
|
||||
mock({
|
||||
"src/app": {},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("src/trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("src/app/api/trigger/route.ts")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.ts")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/examples.ts")).toEqual(true);
|
||||
});
|
||||
|
||||
test("app + typescript", async () => {
|
||||
mock({
|
||||
app: {},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const projectType = await detectPagesOrAppDir("");
|
||||
expect(projectType).toEqual("app");
|
||||
|
||||
const nextJs = new NextJs();
|
||||
await nextJs.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("app/api/trigger/route.ts")).toEqual(true);
|
||||
expect(await pathExists("jobs/index.ts")).toEqual(true);
|
||||
expect(await pathExists("jobs/examples.ts")).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Framework, ProjectInstallOptions } from "..";
|
||||
import { InstallPackage } from "../../utils/addDependencies";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
import { PackageManager } from "../../utils/getUserPkgManager";
|
||||
import pathModule from "path";
|
||||
import { getPathAlias } from "../../utils/pathAlias";
|
||||
import { createFileFromTemplate } from "../../utils/createFileFromTemplate";
|
||||
import { templatesPath } from "../../paths";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { readPackageJson } from "../../utils/readPackageJson";
|
||||
import { standardWatchFilePaths } from "../watchConfig";
|
||||
|
||||
export class Remix implements Framework {
|
||||
id = "remix";
|
||||
name = "Remix";
|
||||
|
||||
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
|
||||
//check for remix.config.js
|
||||
const hasConfigFile = await pathExists(pathModule.join(path, "remix.config.js"));
|
||||
if (hasConfigFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//check for any packages starting with @remix-run
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (!packageJsonContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = Object.keys(packageJsonContent.dependencies || {});
|
||||
const dependencyWithRemix = keys.find((key) => key.startsWith("@remix-run"));
|
||||
if (dependencyWithRemix) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async dependencies(): Promise<InstallPackage[]> {
|
||||
return [
|
||||
{ name: "@trigger.dev/sdk", tag: "latest" },
|
||||
{ name: "@trigger.dev/remix", tag: "latest" },
|
||||
{ name: "@trigger.dev/react", tag: "latest" },
|
||||
];
|
||||
}
|
||||
|
||||
possibleEnvFilenames(): string[] {
|
||||
return [".env"];
|
||||
}
|
||||
|
||||
async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise<void> {
|
||||
const pathAlias = await getPathAlias({
|
||||
projectPath: path,
|
||||
isTypescriptProject: typescript,
|
||||
extraDirectories: ["app"],
|
||||
});
|
||||
const templatesDir = pathModule.join(templatesPath(), "remix");
|
||||
const appFolder = pathModule.join(path, "app");
|
||||
const fileExtension = typescript ? ".ts" : ".js";
|
||||
|
||||
//create app/api.trigger.js
|
||||
const apiRoutePath = pathModule.join(appFolder, "routes", `api.trigger${fileExtension}`);
|
||||
const apiRouteResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "apiRoute.js"),
|
||||
replacements: {
|
||||
routePathPrefix: pathAlias ? pathAlias + "/" : "../../",
|
||||
},
|
||||
outputPath: apiRoutePath,
|
||||
});
|
||||
if (!apiRouteResult.success) {
|
||||
throw new Error("Failed to create API route file");
|
||||
}
|
||||
logger.success(`✔ Created API route at ${apiRoutePath}`);
|
||||
|
||||
//app/trigger.server.js
|
||||
const triggerFilePath = pathModule.join(appFolder, `trigger.server${fileExtension}`);
|
||||
const triggerResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "trigger.js"),
|
||||
replacements: {
|
||||
endpointSlug,
|
||||
},
|
||||
outputPath: triggerFilePath,
|
||||
});
|
||||
if (!triggerResult.success) {
|
||||
throw new Error("Failed to create trigger file");
|
||||
}
|
||||
logger.success(`✔ Created Trigger client at ${triggerFilePath}`);
|
||||
|
||||
//app/jobs/example.server.js
|
||||
const exampleJobFilePath = pathModule.join(appFolder, "jobs", `example.server${fileExtension}`);
|
||||
const exampleJobResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, "exampleJob.js"),
|
||||
replacements: {
|
||||
jobsPathPrefix: pathAlias ? pathAlias + "/" : "../",
|
||||
},
|
||||
outputPath: exampleJobFilePath,
|
||||
});
|
||||
if (!exampleJobResult.success) {
|
||||
throw new Error("Failed to create example job file");
|
||||
}
|
||||
logger.success(`✔ Created example job at ${exampleJobFilePath}`);
|
||||
}
|
||||
|
||||
async postInstall(path: string, options: ProjectInstallOptions): Promise<void> {}
|
||||
|
||||
defaultHostnames = ["localhost"];
|
||||
defaultPorts = [3000, 8788, 3333];
|
||||
watchFilePaths = standardWatchFilePaths;
|
||||
watchIgnoreRegex = /(node_modules|build)/;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import mock from "mock-fs";
|
||||
import { Remix } from ".";
|
||||
import { getFramework } from "..";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("Remix project detection", () => {
|
||||
test("has dependency", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { "@remix-run/express": "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("remix");
|
||||
});
|
||||
|
||||
test("no dependency, has remix.config.js", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
"remix.config.js": "module.exports = {}",
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("remix");
|
||||
});
|
||||
|
||||
test("no dependency, no remix.config.js", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).not.toEqual("remix");
|
||||
});
|
||||
});
|
||||
|
||||
describe("install", () => {
|
||||
test("javascript", async () => {
|
||||
mock({
|
||||
app: {
|
||||
routes: {},
|
||||
},
|
||||
});
|
||||
|
||||
const remix = new Remix();
|
||||
await remix.install("", { typescript: false, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("app/trigger.server.js")).toEqual(true);
|
||||
expect(await pathExists("app/routes/api.trigger.js")).toEqual(true);
|
||||
expect(await pathExists("app/jobs/example.server.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("typescript", async () => {
|
||||
mock({
|
||||
app: {
|
||||
routes: {},
|
||||
},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const remix = new Remix();
|
||||
await remix.install("", { typescript: true, packageManager: "npm", endpointSlug: "foo" });
|
||||
expect(await pathExists("app/trigger.server.ts")).toEqual(true);
|
||||
expect(await pathExists("app/routes/api.trigger.ts")).toEqual(true);
|
||||
expect(await pathExists("app/jobs/example.server.ts")).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export const standardWatchFilePaths = [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
"pnpm-lock.yaml",
|
||||
];
|
||||
|
||||
export const standardWatchIgnoreRegex = /(node_modules)/;
|
||||
@@ -0,0 +1,14 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
export function rootPath() {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
export function templatesPath() {
|
||||
const root = rootPath();
|
||||
const templatePath = path.join(root, "templates");
|
||||
return templatePath;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { InitCommandOptions } from "../commands/init";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getVersion } from "../utils/getVersion";
|
||||
import { DevCommandOptions } from "../commands/dev";
|
||||
import { ProjectInstallOptions } from "../frameworks";
|
||||
|
||||
const postHogApiKey = "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW";
|
||||
|
||||
@@ -82,11 +83,26 @@ export class TelemetryClient {
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
createFiles: (options: InitCommandOptions, routerType: "pages" | "app") => {
|
||||
install: (
|
||||
options: InitCommandOptions,
|
||||
framework: string,
|
||||
installOptions: ProjectInstallOptions
|
||||
) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_create_files",
|
||||
properties: { ...this.#initProperties(options), routerType },
|
||||
event: "cli_init_install",
|
||||
properties: { ...this.#initProperties(options), framework, ...installOptions },
|
||||
});
|
||||
},
|
||||
postInstall: (
|
||||
options: InitCommandOptions,
|
||||
framework: string,
|
||||
installOptions: ProjectInstallOptions
|
||||
) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_post_install",
|
||||
properties: { ...this.#initProperties(options), framework, ...installOptions },
|
||||
});
|
||||
},
|
||||
completed: (options: InitCommandOptions) => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createAppRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
//this route is used to send and receive data with Trigger.dev
|
||||
export const { POST, dynamic } = createAppRoute(client);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "${jobsPathPrefix}trigger";
|
||||
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
// export all your job files here
|
||||
|
||||
export * from "./examples";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createPagesRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
//this route is used to send and receive data with Trigger.dev
|
||||
const { handler, config } = createPagesRoute(client);
|
||||
export { config };
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "${endpointSlug}",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createRemixRoute } from "@trigger.dev/remix";
|
||||
import { client } from "${routePathPrefix}trigger.server";
|
||||
|
||||
// Remix will automatically strip files with side effects
|
||||
// So you need to *export* your Job definitions like this:
|
||||
export * from "${routePathPrefix}jobs/example.server";
|
||||
|
||||
export const { action } = createRemixRoute(client);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "${jobsPathPrefix}trigger.server";
|
||||
|
||||
// Your first job
|
||||
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline
|
||||
export const job = client.defineJob({
|
||||
// This is the unique identifier for your Job, it must be unique across all Jobs in your project
|
||||
id: "example-job",
|
||||
name: "Example Job: a joke with a delay",
|
||||
version: "0.0.1",
|
||||
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// This logs a message to the console
|
||||
await io.logger.info("🧪 Example Job: a joke with a delay");
|
||||
await io.logger.info("How do you comfort a JavaScript bug?");
|
||||
// This waits for 5 seconds, the second parameter is the number of seconds to wait, you can add delays of up to a year
|
||||
await io.wait("Wait 5 seconds for the punchline...", 5);
|
||||
await io.logger.info("You console it! 🤦");
|
||||
await io.logger.info(
|
||||
"✨ Congratulations, You just ran your first successful Trigger.dev Job! ✨"
|
||||
);
|
||||
// To learn how to write much more complex (and probably funnier) Jobs, check out our docs: https://trigger.dev/docs/documentation/guides/create-a-job
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "${endpointSlug}",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import fs from "fs/promises";
|
||||
import mock from "mock-fs";
|
||||
import path from "path";
|
||||
import { createFileFromTemplate, replaceAll } from "./createFileFromTemplate";
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
const preReplacement = `import { createPagesRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "\${routePathPrefix}trigger";
|
||||
import { other } from "\${routePathPrefix}trigger";
|
||||
import "\${anotherPathPrefix}jobs";
|
||||
|
||||
const { handler, config } = createPagesRoute(client);`;
|
||||
|
||||
const postReplacement = `import { createPagesRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "@/trigger";
|
||||
import { other } from "@/trigger";
|
||||
import "@/src/jobs";
|
||||
|
||||
const { handler, config } = createPagesRoute(client);`;
|
||||
|
||||
describe("Replace function", () => {
|
||||
test("simple replacements", async () => {
|
||||
const output = replaceAll(preReplacement, {
|
||||
routePathPrefix: "@/",
|
||||
anotherPathPrefix: "@/src/",
|
||||
});
|
||||
expect(output).toEqual(postReplacement);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Template files", () => {
|
||||
test("basic template", async () => {
|
||||
mock({
|
||||
templates: {
|
||||
"some-file.js": preReplacement,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await createFileFromTemplate({
|
||||
templatePath: path.join("templates", "some-file.js"),
|
||||
replacements: {
|
||||
routePathPrefix: "@/",
|
||||
anotherPathPrefix: "@/src/",
|
||||
},
|
||||
outputPath: "foo/output.ts",
|
||||
mockTemplatePath: true,
|
||||
});
|
||||
|
||||
expect(result.success).toEqual(true);
|
||||
if (!result.success) return;
|
||||
expect(result.alreadyExisted).toEqual(false);
|
||||
|
||||
const fileContents = await fs.readFile("foo/output.ts", "utf-8");
|
||||
expect(fileContents).toEqual(postReplacement);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import fs from "fs/promises";
|
||||
import { pathExists } from "./fileSystem";
|
||||
import path from "path";
|
||||
import { readFileIgnoringMock } from "./readFileIgnoringMock";
|
||||
|
||||
type Result =
|
||||
| {
|
||||
success: true;
|
||||
alreadyExisted: boolean;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function createFileFromTemplate(params: {
|
||||
templatePath: string;
|
||||
replacements: Record<string, string>;
|
||||
outputPath: string;
|
||||
mockTemplatePath?: boolean;
|
||||
}): Promise<Result> {
|
||||
let template = "";
|
||||
if (params.mockTemplatePath === true) {
|
||||
template = await fs.readFile(params.templatePath, "utf-8");
|
||||
} else {
|
||||
template = await readFileIgnoringMock(params.templatePath);
|
||||
}
|
||||
|
||||
if (await pathExists(params.outputPath)) {
|
||||
return {
|
||||
success: true,
|
||||
alreadyExisted: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const output = replaceAll(template, params.replacements);
|
||||
|
||||
const directoryName = path.dirname(params.outputPath);
|
||||
await fs.mkdir(directoryName, { recursive: true });
|
||||
await fs.writeFile(params.outputPath, output);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
alreadyExisted: false,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
return {
|
||||
success: false,
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: JSON.stringify(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// find strings that match ${varName} and replace with the value from a Record<string, string> where { varName: "value" }
|
||||
export function replaceAll(input: string, replacements: Record<string, string>) {
|
||||
let output = input;
|
||||
for (const [key, value] of Object.entries(replacements)) {
|
||||
output = output.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import pathModule from "path";
|
||||
import { readPackageJson } from "./readPackageJson";
|
||||
|
||||
/** Detects if the project is a Next.js project at path */
|
||||
export async function detectNextJsProject(path: string): Promise<boolean> {
|
||||
const hasNextConfigFile = await detectNextConfigFile(path);
|
||||
if (hasNextConfigFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return await detectNextDependency(path);
|
||||
}
|
||||
|
||||
async function detectNextConfigFile(path: string): Promise<boolean> {
|
||||
return fs
|
||||
.access(pathModule.join(path, "next.config.js"))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async function detectNextDependency(path: string): Promise<boolean> {
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (!packageJsonContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return packageJsonContent.dependencies?.next !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import fs from "fs/promises";
|
||||
import pathModule from "path";
|
||||
import { pathExists } from "./fileSystem";
|
||||
import { logger } from "./logger";
|
||||
import { renderApiKey } from "./renderApiKey";
|
||||
|
||||
export async function getEnvFilename(
|
||||
directory: string,
|
||||
possibleNames: string[]
|
||||
): Promise<string | undefined> {
|
||||
if (possibleNames.length === 0) {
|
||||
throw new Error("No possible names provided");
|
||||
}
|
||||
|
||||
for (let index = 0; index < possibleNames.length; index++) {
|
||||
const name = possibleNames[index];
|
||||
if (!name) continue;
|
||||
|
||||
const path = pathModule.join(directory, name);
|
||||
const envFileExists = await pathExists(path);
|
||||
if (envFileExists) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function setApiKeyEnvironmentVariable(dir: string, fileName: string, apiKey: string) {
|
||||
await setEnvironmentVariable(dir, fileName, "TRIGGER_API_KEY", apiKey, true, renderApiKey);
|
||||
}
|
||||
|
||||
export async function setApiUrlEnvironmentVariable(dir: string, fileName: string, apiUrl: string) {
|
||||
await setEnvironmentVariable(dir, fileName, "TRIGGER_API_URL", apiUrl, true);
|
||||
}
|
||||
|
||||
async function setEnvironmentVariable(
|
||||
dir: string,
|
||||
fileName: string,
|
||||
variableName: string,
|
||||
value: string,
|
||||
replaceValue: boolean = true,
|
||||
renderer: (value: string) => string = (value) => value
|
||||
) {
|
||||
const path = pathModule.join(dir, fileName);
|
||||
const envFileExists = await pathExists(path);
|
||||
|
||||
if (!envFileExists) {
|
||||
await fs.writeFile(path, "");
|
||||
}
|
||||
|
||||
const envFileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
if (envFileContent.includes(variableName)) {
|
||||
if (!replaceValue) {
|
||||
logger.info(
|
||||
`☑ Skipping setting ${variableName}=${renderer(value)} because it already exists`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Update the existing value
|
||||
const updatedEnvFileContent = envFileContent.replace(
|
||||
new RegExp(`${variableName}=.*\\n`, "g"),
|
||||
`${variableName}=${value}\n`
|
||||
);
|
||||
|
||||
await fs.writeFile(path, updatedEnvFileContent);
|
||||
|
||||
logger.success(`✔ Set ${variableName}=${renderer(value)} in ${fileName}`);
|
||||
} else {
|
||||
await fs.appendFile(path, `\n${variableName}=${value}`);
|
||||
|
||||
logger.success(`✔ Added ${variableName}=${renderer(value)} to ${fileName}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import mock from "mock-fs";
|
||||
import { getPathAlias } from "./pathAlias";
|
||||
|
||||
describe("javascript config", () => {
|
||||
test("no jsconfig means no alias", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without alias", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without paths", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": {}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without matching alias", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./app/*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig src dir", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
|
||||
test("jsconfig no src dir", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
usesSrcDir: false,
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
|
||||
test("tsconfig no src dir", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"tsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: true,
|
||||
usesSrcDir: true,
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
|
||||
test("tsconfig no src dir", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
"tsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: true,
|
||||
usesSrcDir: false,
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import pathModule from "path";
|
||||
import { pathExists } from "./fileSystem";
|
||||
import { parse } from "tsconfck";
|
||||
|
||||
type Options = { projectPath: string; isTypescriptProject: boolean; extraDirectories?: string[] };
|
||||
|
||||
// Find the alias that points to the "src" directory.
|
||||
// So for example, the paths object could be:
|
||||
// {
|
||||
// "@/*": ["./src/*"]
|
||||
// }
|
||||
// In this case, we would return "@"
|
||||
export async function getPathAlias({
|
||||
projectPath,
|
||||
isTypescriptProject,
|
||||
extraDirectories,
|
||||
}: Options) {
|
||||
const configFileName = isTypescriptProject ? "tsconfig.json" : "jsconfig.json";
|
||||
const tsConfigPath = pathModule.join(projectPath, configFileName);
|
||||
const configFileExists = await pathExists(tsConfigPath);
|
||||
|
||||
//no config and javascript, no alias
|
||||
if (!isTypescriptProject && !configFileExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tsconfig } = await parse(tsConfigPath);
|
||||
|
||||
const paths = tsconfig?.compilerOptions?.paths;
|
||||
if (paths === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alias = Object.keys(paths).find((key) => {
|
||||
const value = paths[key];
|
||||
|
||||
if (value.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = value[0];
|
||||
if (extraDirectories && extraDirectories.length > 0) {
|
||||
return path === `./${extraDirectories.join("/")}/*`;
|
||||
} else {
|
||||
return path === "./*";
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure to remove the trailing "/*"
|
||||
if (alias) {
|
||||
return alias.slice(0, -2);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import mock from "mock-fs";
|
||||
import fs from "fs/promises";
|
||||
|
||||
export function readFileIgnoringMock(filePath: string): Promise<string> {
|
||||
return mock.bypass(async () => {
|
||||
return await fs.readFile(filePath, { encoding: "utf-8" });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function removeFileExtension(filename: string) {
|
||||
return filename.replace(/\.[^.]+$/, "");
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> &
|
||||
Omit<T, K> extends infer O
|
||||
? { [P in keyof O]: O[P] }
|
||||
: never;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { prepareEnvironment } from "@gmrchk/cli-testing-library";
|
||||
import { CLITestEnvironment } from "@gmrchk/cli-testing-library/lib/types";
|
||||
import { join } from "node:path";
|
||||
|
||||
let environment: CLITestEnvironment;
|
||||
|
||||
beforeAll(async () => {
|
||||
// This will create a "sandbox" terminal under `/var/folders`
|
||||
environment = await prepareEnvironment();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await environment.cleanup();
|
||||
});
|
||||
|
||||
// this test is not returning timeout
|
||||
describe.skip('cli', () => {
|
||||
// can be any path with a nextjs project
|
||||
const NEXT_PROJECT_PATH = join(__dirname, '..', '..', '..', 'examples', 'nextjs-example');
|
||||
|
||||
it('should be able to execute cli', async () => {
|
||||
const { waitForText, getStdout, wait, pressKey } = await environment.spawn('node', `${join(__dirname, '..', 'dist', 'index.js')} init -p ${NEXT_PROJECT_PATH}`)
|
||||
|
||||
console.log('getStdout() :>> ', getStdout());
|
||||
|
||||
// this promises never resolves
|
||||
// maybe we have a conflict between vitest and @gmrchk/cli-testing-library?
|
||||
// with jest works fine, but with vitest not
|
||||
await waitForText('Detected Next.js project');
|
||||
|
||||
console.log('getStdout() :>> ', getStdout());
|
||||
|
||||
await waitForText('Are you using the Trigger.dev cloud or self-hosted?');
|
||||
|
||||
console.log('getStdout() :>> ', getStdout());
|
||||
|
||||
await pressKey('enter');
|
||||
|
||||
console.log('getStdout() :>> ', getStdout());
|
||||
|
||||
// wait next prompt, make assertions and keep going
|
||||
});
|
||||
}, 20000)
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
const isDev = process.env.npm_lifecycle_event === "dev";
|
||||
//command to copy the "templates" folder to dist/templates
|
||||
const copyTemplates = "cp -r src/templates dist";
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
@@ -12,5 +14,5 @@ export default defineConfig({
|
||||
sourcemap: true,
|
||||
target: "esnext",
|
||||
outDir: "dist",
|
||||
onSuccess: isDev ? "node dist/index.js" : undefined,
|
||||
onSuccess: isDev ? `${copyTemplates} && node dist/index.js` : copyTemplates,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/node": "^1.19.3",
|
||||
"@remix-run/server-runtime": "^2.0.0",
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/ws": "^8.5.3",
|
||||
@@ -34,12 +34,13 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@trigger.dev/sdk": "workspace:^2.1.3"
|
||||
"@trigger.dev/sdk": "workspace:^2.1.3",
|
||||
"@remix-run/server-runtime": ">1.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ActionArgs } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import type { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export function createRemixRoute(client: TriggerClient) {
|
||||
const action = async ({ request }: ActionArgs) => {
|
||||
const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const response = await client.handleRequest(request);
|
||||
|
||||
if (!response) {
|
||||
@@ -13,5 +12,4 @@ export function createRemixRoute(client: TriggerClient) {
|
||||
return json(response.body, { status: response.status });
|
||||
};
|
||||
return { action };
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user