Express frameworks docs + CLI (#512)

* Manual setup docs

* Added the onboarding

* The emails package now works with Node > 18

* Added CLi support for Express, including custom init command finished messages

* Need to actually log out the installation complete message…

* Fix for an old Remix reference

* Renamed the page export

* Use resolvedOptions.triggerUrl

* Typo in manual instructions
This commit is contained in:
Matt Aitken
2023-09-27 17:44:50 +01:00
committed by GitHub
parent 35d0c2a06f
commit 2397fcb640
11 changed files with 400 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Added Express support to the CLI
@@ -51,7 +51,7 @@ export function FrameworkSelector() {
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
<NextjsLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
<ExpressLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
@@ -1,21 +1,116 @@
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
};
export default function Page() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<FrameworkComingSoon
frameworkName="Express"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
githubIssueNumber={451}
>
<ExpressLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<div>
<Callout
variant={"info"}
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
className="mb-8"
>
Trigger.dev has full support for serverless. We will be adding support for long-running
servers soon.
</Callout>
<div>
<StepNumber
stepNumber="1"
title="Manually set up Trigger.dev in your existing Express project"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/express"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Express app" />
<StepContentContainer>
<RunDevCommand />
<Callout variant="info">
You may be using the `start` script instead, in which case substitute `dev` in the
above commands.
</Callout>
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</div>
</div>
</div>
</PageGradient>
);
}
+193 -1
View File
@@ -1 +1,193 @@
We're in the process of building support for the Express framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues).
## Installing Required Packages
Start by installing the necessary packages in your Express.js project directory. You can use npm, pnpm, or yarn as your package manager.
<CodeGroup>
```bash npm
npm install @trigger.dev/sdk @trigger.dev/express
```
```bash pnpm
pnpm install @trigger.dev/sdk @trigger.dev/express
```
```bash yarn
yarn add @trigger.dev/sdk @trigger.dev/express
```
</CodeGroup>
<br />
<Note>Ensure that you execute this command within a Express project.</Note>
## Obtaining the Development Server API Key
To locate your development Server API key, login to the [Trigger.dev
dashboard](https://cloud.trigger.dev) and select the Project you want to
connect to. Then click on the Environments & API Keys tab in the left menu.
You can copy your development Server API Key from the field at the top of this page.
(Your development key will start with `tr_dev_`).
## Adding Environment Variables
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
```bash
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
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
Create a file for your Trigger client, in this case we create it at `<root>/trigger.(ts/js)`
```ts trigger.(ts/js)
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.
## Adding the API endpoint
There are a few different options depending on how your Express project is configured.
- App middleware
- Entire app for Trigger.dev (only relevant if it's the only thing your project is for)
Select the appropriate code example from below:
<CodeGroup>
```typescript app middleware
//import the client from the other file
import { client } from "./trigger";
import { createMiddleware } from "@trigger.dev/express";
//import your job files
import "./jobs/example";
//..your existing Express code
const app: Express = express();
//add the middleware
app.use(createMiddleware(client));
//..the rest of your Express code
```
```typescript entire app
//if the entire app is just for Trigger.dev
import { client } from "./trigger";
import { createExpressServer } from "@trigger.dev/express";
//import your job files
import "./jobs/example";
//this creates an app
createExpressServer(client);
```
</CodeGroup>
## Creating the Example Job
Create a Job file. In this case created `<root>/jobs/example.(ts/js)`
```typescript jobs/example.(ts/js)
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "../trigger";
// your first job
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
```
## Adding Configuration to `package.json`
Inside the `package.json` file, add the following configuration under the root object:
```json
"trigger.dev": {
"endpointId": "my-app"
}
```
Replace **"my-app"** with the appropriate identifier you used in the trigger.js configuration file.
## Running
### Run your Express app
Run your Express app locally, like you normally would. For example:
<CodeGroup>
```bash npm
npm run dev
```
```bash pnpm
pnpm run dev
```
```bash yarn
yarn run dev
```
</CodeGroup>
<Note>You might use `npm run start` instead of dev</Note>
### Run the CLI 'dev' command
In a **_separate 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>
<Note>
You can optionally pass the hostname if you're not running on localhost by adding
`--hostname <host>`. Example, in case your Express is running on 0.0.0.0: `--hostname 0.0.0.0`.
</Note>
+1
View File
@@ -57,6 +57,7 @@
},
"dependencies": {
"@types/degit": "^2.8.3",
"boxen": "^7.1.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
"commander": "^9.4.1",
+7 -5
View File
@@ -129,18 +129,20 @@ export const initCommand = async (options: InitCommandOptions) => {
await addConfigurationToPackageJson(resolvedPath, resolvedOptions);
await printNextSteps(resolvedOptions, authorizedKey, packageManager, framework);
const projectUrl = `${resolvedOptions.triggerUrl}/orgs/${authorizedKey.organization.slug}/projects/${authorizedKey.project.slug}`;
if (framework.printInstallationComplete) {
await framework.printInstallationComplete(projectUrl);
} else {
await printNextSteps(projectUrl, packageManager, framework);
}
telemetryClient.init.completed(resolvedOptions);
};
async function printNextSteps(
options: ResolvedOptions,
authorizedKey: WhoamiResponse,
projectUrl: string,
packageManager: PackageManager,
framework: Framework
) {
const projectUrl = `${options.triggerUrl}/orgs/${authorizedKey.organization.slug}/projects/${authorizedKey.project.slug}`;
logger.success(`✔ Successfully initialized Trigger.dev!`);
logger.info("Next steps:");
@@ -0,0 +1,28 @@
import mock from "mock-fs";
import { Express } from ".";
import { getFramework } from "..";
import { pathExists } from "../../utils/fileSystem";
afterEach(() => {
mock.restore();
});
describe("Express project detection", () => {
test("has dependency", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { express: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).toEqual("express");
});
test("no dependency", async () => {
mock({
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
});
const framework = await getFramework("", "npm");
expect(framework?.id).not.toEqual("express");
});
});
@@ -0,0 +1,51 @@
import { Framework, ProjectInstallOptions } from "..";
import { InstallPackage } from "../../utils/addDependencies";
import { PackageManager } from "../../utils/getUserPkgManager";
import { logger } from "../../utils/logger";
import { readPackageJson } from "../../utils/readPackageJson";
import { standardWatchFilePaths } from "../watchConfig";
import boxen from "boxen";
export class Express implements Framework {
id = "express";
name = "Express";
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
//check for the express package
const packageJsonContent = await readPackageJson(path);
if (packageJsonContent?.dependencies?.express) {
return true;
}
return false;
}
async dependencies(): Promise<InstallPackage[]> {
return [
{ name: "@trigger.dev/sdk", tag: "latest" },
{ name: "@trigger.dev/express", tag: "latest" },
];
}
possibleEnvFilenames(): string[] {
return [".env"];
}
async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise<void> {}
async postInstall(path: string, options: ProjectInstallOptions): Promise<void> {}
async printInstallationComplete(projectUrl: string): Promise<void> {
logger.info(
boxen(
"Automatic installation isn't currently supported for Express. \nFollow the steps in our manual installation guide: https://trigger.dev/docs/documentation/guides/manual/express",
{ padding: 1, margin: 1, borderStyle: "double", borderColor: "magenta" }
)
);
}
defaultHostnames = ["localhost", "[::]"];
defaultPorts = [3000, 8000, 80, 8080];
watchFilePaths = standardWatchFilePaths;
watchIgnoreRegex = /(node_modules)/;
}
+5 -1
View File
@@ -1,6 +1,7 @@
import { InstallPackage } from "../utils/addDependencies";
import { PackageManager } from "../utils/getUserPkgManager";
import { Astro } from "./astro";
import { Express } from "./express";
import { NextJs } from "./nextjs";
import { Remix } from "./remix";
@@ -32,6 +33,9 @@ export interface Framework {
/** You can check for middleware, add extra instructions, etc */
postInstall(path: string, options: ProjectInstallOptions): Promise<void>;
/** You can (optionally) override the initComplete messages */
printInstallationComplete?(projectUrl: string): Promise<void>;
/** Used by the dev command, if a hostname isn't passed in */
defaultHostnames: string[];
@@ -46,7 +50,7 @@ export interface Framework {
}
/** 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(), new Astro()];
const frameworks: Framework[] = [new NextJs(), new Remix(), new Astro(), new Express()];
export const getFramework = async (
path: string,
+1 -1
View File
@@ -34,6 +34,6 @@
"typescript": "^4.9.4"
},
"engines": {
"node": ">=18.0.0 <19.0.0"
"node": ">=18.0.0"
}
}
+3 -13
View File
@@ -639,6 +639,7 @@ importers:
'@types/mock-fs': ^4.13.1
'@types/node': '16'
'@types/node-fetch': ^2.6.2
boxen: ^7.1.1
chalk: ^5.2.0
chokidar: ^3.5.3
commander: ^9.4.1
@@ -670,6 +671,7 @@ importers:
zod: 3.21.4
dependencies:
'@types/degit': 2.8.3
boxen: 7.1.1
chalk: 5.2.0
chokidar: 3.5.3
commander: 9.5.0
@@ -14639,7 +14641,7 @@ packages:
/axios/0.21.4_debug@4.3.2:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies:
follow-redirects: 1.15.2_debug@4.3.2
follow-redirects: 1.15.2
transitivePeerDependencies:
- debug
dev: false
@@ -19183,18 +19185,6 @@ packages:
debug:
optional: true
/follow-redirects/1.15.2_debug@4.3.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dependencies:
debug: 4.3.2
dev: false
/for-each/0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies: