remove more v2 code and run pnpm install
This commit is contained in:
@@ -98,7 +98,6 @@
|
||||
"@trigger.dev/otlp-importer": "workspace:*",
|
||||
"@trigger.dev/platform": "1.0.12",
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"@trigger.dev/yalt": "workspace:*",
|
||||
"@types/pg": "8.6.6",
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"@upstash/ratelimit": "^1.1.3",
|
||||
@@ -189,7 +188,6 @@
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@total-typescript/ts-reset": "^0.4.2",
|
||||
"@trigger.dev/eslint-plugin": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/eslint": "^8.4.6",
|
||||
|
||||
+1
-2
@@ -48,7 +48,6 @@
|
||||
"devDependencies": {
|
||||
"@manypkg/cli": "^0.19.2",
|
||||
"@playwright/test": "^1.36.2",
|
||||
"@trigger.dev/cli": "workspace:*",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
"@types/node": "18.17.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
@@ -75,4 +74,4 @@
|
||||
"graphile-worker@0.16.6": "patches/graphile-worker@0.16.6.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Trigger.dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,25 +0,0 @@
|
||||
## ✨ @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 in your existing project.
|
||||
|
||||
## 💻 Usage
|
||||
|
||||
To initialize your project using `@trigger.dev/cli`, run any of the following three commands and answer the prompts:
|
||||
|
||||
### npm
|
||||
|
||||
```sh
|
||||
npx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
### yarn
|
||||
|
||||
```sh
|
||||
yarn dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
|
||||
### pnpm
|
||||
|
||||
```sh
|
||||
pnpm dlx @trigger.dev/cli@latest init
|
||||
```
|
||||
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"name": "@trigger.dev/cli",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The Trigger.dev CLI",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/triggerdotdev/trigger.dev.git",
|
||||
"directory": "packages/cli"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"typescript",
|
||||
"trigger.dev",
|
||||
"workflows",
|
||||
"orchestration",
|
||||
"events",
|
||||
"webhooks",
|
||||
"integrations",
|
||||
"apis",
|
||||
"jobs",
|
||||
"background-jobs",
|
||||
"nextjs"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"bin": {
|
||||
"trigger-cli": "./dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/gradient-string": "^1.1.2",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^18",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/ws": "^8.5.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsup": "^6.5.0",
|
||||
"type-fest": "^3.6.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vitest": "^0.34.4"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc",
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/yalt": "workspace:*",
|
||||
"@types/degit": "^2.8.3",
|
||||
"boxen": "^7.1.1",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"commander": "^9.4.1",
|
||||
"console-table-printer": "^2.11.2",
|
||||
"degit": "^2.8.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.19.11",
|
||||
"execa": "^7.0.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
"inquirer": "^9.1.4",
|
||||
"liquidjs": "^10.9.2",
|
||||
"localtunnel": "^2.0.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"ngrok": "5.0.0-beta.2",
|
||||
"node-fetch": "^3.3.0",
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"openai": "^4.5.0",
|
||||
"ora": "^6.1.2",
|
||||
"p-retry": "^6.1.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"posthog-node": "^3.1.1",
|
||||
"proxy-agent": "^6.3.0",
|
||||
"simple-git": "^3.19.0",
|
||||
"terminal-link": "^3.0.0",
|
||||
"tsconfck": "^2.1.2",
|
||||
"url": "^0.11.1",
|
||||
"ws": "^8.11.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import inquirer from "inquirer";
|
||||
import pathModule from "node:path";
|
||||
import { createIntegrationCommand } from "../commands/createIntegration";
|
||||
import { devCommand } from "../commands/dev";
|
||||
import { whoamiCommand } from "../commands/whoami.js";
|
||||
import { initCommand } from "../commands/init";
|
||||
import { CLOUD_TRIGGER_URL, COMMAND_NAME } from "../consts";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { getVersion } from "../utils/getVersion";
|
||||
import { updateCommand } from "../commands/update";
|
||||
import { sendEventCommand } from "../commands/sendEvent";
|
||||
import { checkApiKeyIsDevServer } from "../utils/getApiKeyType";
|
||||
|
||||
export const program = new Command();
|
||||
|
||||
program
|
||||
.name(COMMAND_NAME)
|
||||
.description("The Trigger.dev CLI")
|
||||
.version(getVersion(), "-v, --version", "Display the version number");
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.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-project)"
|
||||
)
|
||||
.option(
|
||||
"-t, --trigger-url <trigger-url>",
|
||||
"The URL of the Trigger.dev instance to use.",
|
||||
createUrlValidator("--trigger-url")
|
||||
)
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await initCommand(options);
|
||||
} catch (e) {
|
||||
telemetryClient.init.failed("unknown", options, e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("dev")
|
||||
.description("Tunnel your local project to Trigger.dev and start running jobs")
|
||||
.argument("[path]", "The path to the project", ".")
|
||||
.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."
|
||||
)
|
||||
.option(
|
||||
"-h, --handler-path <handler path>",
|
||||
"The URI path to the API handler function to use for this project.",
|
||||
"/api/trigger"
|
||||
)
|
||||
.option(
|
||||
"-t, --tunnel <url>",
|
||||
"An optional custom tunnel URL. Use only if you already have an open tunnel to your local dev server."
|
||||
)
|
||||
.option("-s, --https", "allows enabled https for the tunnel")
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (path, options) => {
|
||||
try {
|
||||
await devCommand(path, options);
|
||||
} catch (e) {
|
||||
telemetryClient.dev.failed("unknown", options, e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("create-integration")
|
||||
.description("Create a new integration package for Trigger.dev")
|
||||
.argument("[path]", "The path where you would like the package to be created", ".")
|
||||
.option(
|
||||
"-n, --package-name <package name>",
|
||||
"The name of the package to create (e.g. @trigger.dev/slack)"
|
||||
)
|
||||
.option(
|
||||
"-s, --sdk-package <integration package>",
|
||||
"The name of the SDK package to use (e.g. @slack/web-api)"
|
||||
)
|
||||
.option(
|
||||
"-o --openai-key <open ai api key>",
|
||||
"The OpenAI API key to use to generate the initial code for this integration"
|
||||
)
|
||||
.option(
|
||||
"-oo --openai-org <open ai org>",
|
||||
"The OpenAI org id to use to generate the initial code for this integration"
|
||||
)
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (path, options) => {
|
||||
await createIntegrationCommand(path, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command("update")
|
||||
.description(
|
||||
"Updates all @trigger.dev/* packages to their latest compatible versions or the specified version"
|
||||
)
|
||||
.argument("[path]", "The path to the directory that contains the package.json file", ".")
|
||||
.option("--to <version tag>", "The version to update to (ex: 2.1.4)", "latest")
|
||||
.action(async (path, options) => {
|
||||
await updateCommand(path, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command("whoami")
|
||||
.description("display the current logged in user and project details")
|
||||
.argument("[path]", "The path to the project", ".")
|
||||
.option("-p, --port <port>", "The local port your server is on", "3000")
|
||||
.option("-e, --env-file <name>", "The name of the env file to load", ".env.local")
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (path, options) => {
|
||||
try {
|
||||
await whoamiCommand(path, options);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("send-event")
|
||||
.description("Sends an event to the Trigger.dev API")
|
||||
.argument("[path]", "The path to the project", ".")
|
||||
.option("-e, --env-file <name>", "The name of the env file to load", ".env.local")
|
||||
.requiredOption("-n, --name <name>", "The name of the event to send")
|
||||
.requiredOption("-p, --payload <payload>", "The JSON payload to send with the event")
|
||||
.option("-i, --id <id>", "The ID of the event to send")
|
||||
.version(getVersion(), "-v, --version", "Display the version number")
|
||||
.action(async (path, options) => {
|
||||
try {
|
||||
await sendEventCommand(path, options);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
export const promptTriggerUrl = async (): Promise<string> => {
|
||||
const { instanceType } = await inquirer.prompt<{
|
||||
instanceType: "cloud" | "self-hosted";
|
||||
}>([
|
||||
{
|
||||
type: "list",
|
||||
name: "instanceType",
|
||||
message: "Are you using the Trigger.dev cloud or self-hosted?",
|
||||
choices: [
|
||||
{
|
||||
name: `Trigger.dev Cloud (${CLOUD_TRIGGER_URL})`,
|
||||
value: "cloud",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "Self hosted",
|
||||
value: "self-hosted",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
if (instanceType === "cloud") {
|
||||
return CLOUD_TRIGGER_URL;
|
||||
}
|
||||
|
||||
const { triggerUrl } = await inquirer.prompt<{ triggerUrl: string }>({
|
||||
type: "input",
|
||||
name: "triggerUrl",
|
||||
message: "Enter the URL of your self-hosted Trigger.dev instance",
|
||||
filter: (input) => {
|
||||
return tryToCreateValidUrlFromValue(input);
|
||||
},
|
||||
validate: (input) => {
|
||||
if (!input) {
|
||||
return "Please enter the URL of your self-hosted Trigger.dev instance";
|
||||
}
|
||||
|
||||
const possibleUrl = tryToCreateValidUrlFromValue(input);
|
||||
|
||||
try {
|
||||
new URL(possibleUrl);
|
||||
} catch (e) {
|
||||
return "Please enter a valid URL";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return triggerUrl;
|
||||
};
|
||||
|
||||
export const promptApiKey = async (instanceUrl: string): Promise<string> => {
|
||||
// First prompt if they want to enter their API key now, and if they say Yes, then prompt for it and return it
|
||||
const { apiKey } = await inquirer.prompt<{ apiKey: string }>({
|
||||
type: "password",
|
||||
name: "apiKey",
|
||||
message: `Enter your secret dev API key (Find yours ➡️ ${instanceUrl})`,
|
||||
validate: (input) => {
|
||||
// Make sure they enter something like tr_dev_********
|
||||
if (!input) {
|
||||
return "Please enter your secret dev API key";
|
||||
}
|
||||
|
||||
const result = checkApiKeyIsDevServer(input);
|
||||
|
||||
if (result.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!result.type) {
|
||||
return "Please enter a valid development API key (should start with tr_dev_)";
|
||||
}
|
||||
|
||||
return `Please enter your secret dev API key, you've entered a ${result.type.environment} ${result.type.type} key`;
|
||||
},
|
||||
});
|
||||
|
||||
return apiKey;
|
||||
};
|
||||
|
||||
export const promptEndpointSlug = async (path: string): Promise<string> => {
|
||||
const { endpointSlug } = await inquirer.prompt<{
|
||||
endpointSlug: string;
|
||||
}>({
|
||||
type: "input",
|
||||
name: "endpointSlug",
|
||||
default: slugify(pathModule.basename(path)),
|
||||
message: "Enter an ID for this project",
|
||||
validate: (input) => {
|
||||
if (!input) {
|
||||
return "Please enter an ID for this project";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return endpointSlug;
|
||||
};
|
||||
|
||||
export const obfuscateApiKey = (apiKey: string) => {
|
||||
const [prefix, slug, secretPart] = apiKey.split("_") as [string, string, string];
|
||||
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
|
||||
};
|
||||
|
||||
function slugify(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
// Create a URL validator for a specific flag name
|
||||
// If the input is a string like test-cloud.trigger.dev, we should automatically add https://
|
||||
// If the input is a string like localhost:3030, we should automatically add http://
|
||||
function createUrlValidator(flagName: string): (input: string) => string {
|
||||
return (input: string) => {
|
||||
try {
|
||||
const possibleUrl = tryToCreateValidUrlFromValue(input);
|
||||
|
||||
new URL(possibleUrl);
|
||||
|
||||
return possibleUrl;
|
||||
} catch (e) {
|
||||
throw new Error(`Please enter a valid URL for the ${flagName} flag`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function tryToCreateValidUrlFromValue(input: string): string {
|
||||
let possibleUrl = input;
|
||||
|
||||
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
||||
possibleUrl = input.includes("localhost") ? `http://${input}` : `https://${input}`;
|
||||
}
|
||||
|
||||
return possibleUrl;
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
import inquirer from "inquirer";
|
||||
import pathModule from "node:path";
|
||||
import ora from "ora";
|
||||
import { z } from "zod";
|
||||
import { COMMAND_NAME } from "../consts";
|
||||
import { getLatestPackageVersion } from "../utils/addDependencies";
|
||||
import { createFile, pathExists, readJSONFile, writeJSONFile } from "../utils/fileSystem";
|
||||
import { generateIntegrationFiles } from "../utils/generateIntegrationFiles";
|
||||
import { getPackageName } from "../utils/getPackagName";
|
||||
import { installDependencies } from "../utils/installDependencies";
|
||||
import { logger } from "../utils/logger";
|
||||
import { relativePath, resolvePath } from "../utils/parseNameAndPath";
|
||||
import { createIntegrationFileFromTemplate } from "../utils/createIntegrationFileFromTemplate";
|
||||
|
||||
const CLIOptionsSchema = z.object({
|
||||
packageName: z.string().optional(),
|
||||
sdkPackage: z.string().optional(),
|
||||
extraInfo: z.string().optional(),
|
||||
skipGeneratingCode: z.coerce.boolean().optional(),
|
||||
authMethod: z.enum(["api-key", "oauth", "both-methods"]).optional(),
|
||||
openaiKey: z.string().optional(),
|
||||
openaiOrg: z.string().optional(),
|
||||
});
|
||||
|
||||
type CLIOptions = z.infer<typeof CLIOptionsSchema>;
|
||||
type ResolvedCLIOptions = Required<CLIOptions>;
|
||||
|
||||
export async function createIntegrationCommand(path: string, cliOptions: any) {
|
||||
const result = CLIOptionsSchema.safeParse(cliOptions);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
|
||||
// make sure the resolvedPath doesn't exist
|
||||
// if it does, print a warning and exit
|
||||
const resolvedPathExists = await pathExists(resolvedPath);
|
||||
|
||||
if (resolvedPathExists) {
|
||||
logger.error(
|
||||
`The path ${resolvedPath} already exists. Please try again with a different path.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const resolvedOptions = await resolveOptionsWithPrompts(options, resolvedPath);
|
||||
|
||||
const latestVersion = await getLatestPackageVersion(resolvedOptions.sdkPackage, "latest");
|
||||
|
||||
if (!latestVersion) {
|
||||
logger.error(
|
||||
`Could not find the latest version of ${resolvedOptions.sdkPackage}. Please try again later.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const triggerMonorepoPath = await detectTriggerMonorepoPath(resolvedPath);
|
||||
|
||||
const sdkVersion = await getInternalOrExternalPackageVersion({
|
||||
path: "packages/trigger-sdk",
|
||||
packageName: "@trigger.dev/sdk",
|
||||
tag: "latest",
|
||||
monorepoPath: triggerMonorepoPath,
|
||||
});
|
||||
|
||||
if (!sdkVersion) {
|
||||
logger.error(`Could not find the latest version of @trigger.dev/sdk. Please try again later.`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const integrationKitVersion = await getInternalOrExternalPackageVersion({
|
||||
path: "packages/integration-kit",
|
||||
packageName: "@trigger.dev/integration-kit",
|
||||
tag: "latest",
|
||||
monorepoPath: triggerMonorepoPath,
|
||||
});
|
||||
|
||||
if (!integrationKitVersion) {
|
||||
logger.error(
|
||||
`Could not find the latest version of @trigger.dev/integration-kit. Please try again later.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const integrationVersion = await getInternalOrExternalPackageVersion({
|
||||
path: "integrations/github",
|
||||
packageName: "@trigger.dev/github",
|
||||
tag: "latest",
|
||||
monorepoPath: triggerMonorepoPath,
|
||||
prependWorkspace: false,
|
||||
});
|
||||
|
||||
if (!integrationVersion) {
|
||||
logger.error(
|
||||
`Could not find the latest version of @trigger.dev/github. Please try again later.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseVariables = {
|
||||
packageName: resolvedOptions.packageName,
|
||||
sdkPackage: resolvedOptions.sdkPackage,
|
||||
integrationVersion: integrationVersion,
|
||||
latestVersion: latestVersion,
|
||||
sdkVersion: sdkVersion,
|
||||
integrationKitVersion: integrationKitVersion,
|
||||
triggerMonorepoPath,
|
||||
};
|
||||
|
||||
const getOutputPath = (relativePath: string) => pathModule.join(resolvedPath, relativePath);
|
||||
|
||||
const miscIntegrationFiles = [
|
||||
{
|
||||
relativeTemplatePath: "package.json.j2",
|
||||
outputPath: getOutputPath("package.json"),
|
||||
},
|
||||
{
|
||||
// use `tsc --showConfig` to update external tsconfig
|
||||
relativeTemplatePath: `tsconfig-${triggerMonorepoPath ? "internal" : "external"}.json.j2`,
|
||||
outputPath: getOutputPath("tsconfig.json"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: `tsup.config-${triggerMonorepoPath ? "internal" : "external"}.js.j2`,
|
||||
outputPath: getOutputPath("tsup.config.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "README.md.j2",
|
||||
outputPath: getOutputPath("README.md"),
|
||||
},
|
||||
];
|
||||
|
||||
await createIntegrationFiles(miscIntegrationFiles, baseVariables);
|
||||
|
||||
// create src/*
|
||||
if (resolvedOptions.skipGeneratingCode) {
|
||||
const getSrcOutputPath = (relativePath: string) =>
|
||||
getOutputPath(pathModule.join("src", relativePath));
|
||||
|
||||
const srcIntegrationFiles = [
|
||||
{
|
||||
relativeTemplatePath: pathModule.join("payload-examples", "index.js.j2"),
|
||||
outputPath: getSrcOutputPath(pathModule.join("payload-examples", "index.ts")),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "events.js.j2",
|
||||
outputPath: getSrcOutputPath("events.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "index.js.j2",
|
||||
outputPath: getSrcOutputPath("index.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "models.js.j2",
|
||||
outputPath: getSrcOutputPath("models.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "schemas.js.j2",
|
||||
outputPath: getSrcOutputPath("schemas.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "types.js.j2",
|
||||
outputPath: getSrcOutputPath("types.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "utils.js.j2",
|
||||
outputPath: getSrcOutputPath("utils.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "webhooks.js.j2",
|
||||
outputPath: getSrcOutputPath("webhooks.ts"),
|
||||
},
|
||||
];
|
||||
|
||||
const validIdentifier = pathModule
|
||||
.basename(path)
|
||||
.replace(/[^a-zA-Z0-9]+/g, "")
|
||||
.replace(/^[0-9]+/g, "");
|
||||
|
||||
await createIntegrationFiles(srcIntegrationFiles, {
|
||||
...baseVariables,
|
||||
apiKeyPropertyName: "apiKey", // TODO: prompt for this
|
||||
authMethod: resolvedOptions.authMethod,
|
||||
identifier: validIdentifier.length ? validIdentifier : "packageName",
|
||||
});
|
||||
} else {
|
||||
await attemptToGenerateIntegrationFiles(pathModule.join(resolvedPath, "src"), resolvedOptions);
|
||||
}
|
||||
|
||||
// If inside the monorepo:
|
||||
if (triggerMonorepoPath) {
|
||||
//adds the integration run script to the job-catalog/package.json
|
||||
//adds the path to the integration to the job-catalog/tsconfig.json
|
||||
await updateJobCatalogWithNewIntegration(triggerMonorepoPath, resolvedPath, resolvedOptions);
|
||||
}
|
||||
|
||||
// Install the dependencies
|
||||
await installDependencies(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(
|
||||
` 2. Read the "Creating an Integration" guide at https://trigger.dev/docs/integrations/create`
|
||||
);
|
||||
|
||||
if (triggerMonorepoPath) {
|
||||
logger.info(` 3. Write some test jobs in the references/job-catalog`);
|
||||
}
|
||||
}
|
||||
|
||||
async function attemptToGenerateIntegrationFiles(path: string, options: ResolvedCLIOptions) {
|
||||
const spinner = ora("Generating integration code (may take ~30s)").start();
|
||||
|
||||
function generateExtraInfo(
|
||||
authMethod: "api-key" | "oauth" | "both-methods",
|
||||
extraInfo?: string
|
||||
): string {
|
||||
let authExtraInfo = "";
|
||||
|
||||
switch (authMethod) {
|
||||
case "api-key": {
|
||||
authExtraInfo =
|
||||
"Note that the only auth method that this integration supports is API keys so can only useLocalAuth to true and don't use the clientFactory option";
|
||||
break;
|
||||
}
|
||||
case "oauth": {
|
||||
authExtraInfo =
|
||||
"Note that the only auth method that this integration supports is OAuth so can only useLocalAuth to false and make sure to use the clientFactory option";
|
||||
break;
|
||||
}
|
||||
case "both-methods": {
|
||||
authExtraInfo =
|
||||
"Note that this integration supports both API keys and OAuth so the options passed to the constructor must support both.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${authExtraInfo}\n\n${extraInfo ?? ""}`;
|
||||
}
|
||||
|
||||
const extraInfo = generateExtraInfo(options.authMethod, options.extraInfo);
|
||||
|
||||
const files = await generateIntegrationFiles(
|
||||
{
|
||||
packageName: options.packageName,
|
||||
sdkPackage: options.sdkPackage,
|
||||
extraInfo,
|
||||
},
|
||||
options.openaiKey ?? process.env.OPENAI_API_KEY,
|
||||
options.openaiOrg ?? process.env.OPENAI_ORGANIZATION
|
||||
);
|
||||
|
||||
if (files) {
|
||||
await Promise.all(
|
||||
Object.entries(files).map(([file, contents]) => createFileInPath(path, file, contents))
|
||||
);
|
||||
|
||||
spinner.succeed(`Generated integration code in ${path}`);
|
||||
} else {
|
||||
spinner.fail("Failed to generate integration code");
|
||||
}
|
||||
}
|
||||
|
||||
async function createFileInPath(path: string, fileName: string, contents: string) {
|
||||
await createFile(pathModule.join(path, fileName), contents);
|
||||
}
|
||||
|
||||
const resolveOptionsWithPrompts = async (
|
||||
options: CLIOptions,
|
||||
path: string
|
||||
): Promise<ResolvedCLIOptions> => {
|
||||
const resolvedOptions: CLIOptions = { ...options };
|
||||
|
||||
try {
|
||||
if (!options.packageName) {
|
||||
resolvedOptions.packageName = await promptPackageName(path);
|
||||
}
|
||||
|
||||
if (!options.sdkPackage) {
|
||||
resolvedOptions.sdkPackage = await promptSdkPackage();
|
||||
}
|
||||
|
||||
if (!process.env.OPENAI_API_KEY && !options.openaiKey) {
|
||||
resolvedOptions.skipGeneratingCode = true;
|
||||
}
|
||||
|
||||
resolvedOptions.authMethod = await promptAuthMethod();
|
||||
|
||||
if (!resolvedOptions.skipGeneratingCode) {
|
||||
resolvedOptions.extraInfo = await promptExtraInfo();
|
||||
}
|
||||
} catch (err) {
|
||||
// If the user is not calling the command from an interactive terminal, inquirer will throw an error with isTTYError = true
|
||||
// If this happens, we catch the error, tell the user what has happened, and then continue to run the program with a default trigger project
|
||||
// Otherwise we have to do some fancy namespace extension logic on the Error type which feels overkill for one line
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (err instanceof Error && (err as any).isTTYError) {
|
||||
logger.warn(
|
||||
`'${COMMAND_NAME} create-integration' needs an interactive terminal to provide options`
|
||||
);
|
||||
|
||||
const { shouldContinue } = await inquirer.prompt<{
|
||||
shouldContinue: boolean;
|
||||
}>({
|
||||
name: "shouldContinue",
|
||||
type: "confirm",
|
||||
message: `Continue initializing your trigger.dev project?`,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!shouldContinue) {
|
||||
logger.info("Exiting...");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedOptions as ResolvedCLIOptions;
|
||||
};
|
||||
|
||||
export const promptPackageName = async (path: string): Promise<string> => {
|
||||
const basename = pathModule.basename(path);
|
||||
|
||||
const { packageName } = await inquirer.prompt<{ packageName: string }>({
|
||||
type: "input",
|
||||
name: "packageName",
|
||||
default: `@trigger.dev/${basename}`,
|
||||
message: "What is the name of your integration package?",
|
||||
validate: (input) => {
|
||||
if (!input) {
|
||||
return "Please enter a package name";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return packageName;
|
||||
};
|
||||
|
||||
export const promptSdkPackage = async (): Promise<string> => {
|
||||
const { sdkPackage } = await inquirer.prompt<{ sdkPackage: string }>({
|
||||
type: "input",
|
||||
name: "sdkPackage",
|
||||
message: "What is the name of the npm package for this API? (e.g. airtable)?",
|
||||
validate: (input) => {
|
||||
if (!input) {
|
||||
return "Please enter an SDK package name";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return sdkPackage;
|
||||
};
|
||||
|
||||
export const promptExtraInfo = async (): Promise<string | undefined> => {
|
||||
const { extraInfo } = await inquirer.prompt<{ extraInfo?: string }>({
|
||||
type: "input",
|
||||
name: "extraInfo",
|
||||
message:
|
||||
"Please describe in english anything else about using the SDK that might be useful (optional)",
|
||||
});
|
||||
|
||||
return extraInfo;
|
||||
};
|
||||
|
||||
// Choose between api-key, oauth, or both
|
||||
export const promptAuthMethod = async (): Promise<"api-key" | "oauth" | "both-methods"> => {
|
||||
const { authMethod } = await inquirer.prompt<{
|
||||
authMethod: "api-key" | "oauth" | "both-methods";
|
||||
}>({
|
||||
type: "list",
|
||||
name: "authMethod",
|
||||
message: "What authentication method does this API use?",
|
||||
choices: [
|
||||
{
|
||||
name: "API Key",
|
||||
value: "api-key",
|
||||
},
|
||||
{
|
||||
name: "OAuth",
|
||||
value: "oauth",
|
||||
},
|
||||
{
|
||||
name: "Both API Key and OAuth",
|
||||
value: "both-methods",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return authMethod;
|
||||
};
|
||||
|
||||
export const promptSkipGeneratingCode = async (): Promise<boolean> => {
|
||||
const { skipGeneratingCode } = await inquirer.prompt<{
|
||||
skipGeneratingCode: boolean;
|
||||
}>({
|
||||
type: "checkbox",
|
||||
name: "skipGeneratingCode",
|
||||
default: false,
|
||||
message: "Would you like to skip generating the initial code?",
|
||||
});
|
||||
|
||||
return skipGeneratingCode;
|
||||
};
|
||||
|
||||
// Find where the github repo is located and check if it's the trigger.dev monorepo
|
||||
async function detectTriggerMonorepoPath(path: string): Promise<string | undefined> {
|
||||
const gitPath = await findGitPath(path);
|
||||
|
||||
if (!gitPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the package.json file at
|
||||
const rootPackageJsonPath = pathModule.join(gitPath, "package.json");
|
||||
const rootPackageJsonExists = await pathExists(rootPackageJsonPath);
|
||||
|
||||
if (!rootPackageJsonExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootPackageJson = await readJSONFile(rootPackageJsonPath);
|
||||
|
||||
if (rootPackageJson.name === "triggerdotdev") {
|
||||
return gitPath;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async function getInternalOrExternalPackageVersion({
|
||||
packageName,
|
||||
tag,
|
||||
path,
|
||||
monorepoPath,
|
||||
prependWorkspace = true,
|
||||
}: {
|
||||
packageName: string;
|
||||
tag: string;
|
||||
path: string;
|
||||
monorepoPath?: string;
|
||||
prependWorkspace?: boolean;
|
||||
}): Promise<{ name: string; version: string } | undefined> {
|
||||
if (!monorepoPath) {
|
||||
return await getLatestPackageVersion(packageName, tag);
|
||||
}
|
||||
|
||||
// If there is a monorepo path then we will read the version from the package.json at that path
|
||||
const packageJsonPath = pathModule.join(monorepoPath, path, "package.json");
|
||||
const packageJsonExists = await pathExists(packageJsonPath);
|
||||
|
||||
if (!packageJsonExists) {
|
||||
return await getLatestPackageVersion(packageName, tag);
|
||||
}
|
||||
|
||||
const packageJson = await readJSONFile(packageJsonPath);
|
||||
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: `${prependWorkspace ? "workspace:^" : ""}${packageJson.version}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Recursively search for a .git folder
|
||||
async function findGitPath(path: string): Promise<string | undefined> {
|
||||
const gitPath = pathModule.join(path, ".git");
|
||||
|
||||
const gitPathExists = await pathExists(gitPath);
|
||||
|
||||
if (gitPathExists) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const parentPath = pathModule.dirname(path);
|
||||
|
||||
if (parentPath === path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return findGitPath(parentPath);
|
||||
}
|
||||
|
||||
async function updateJobCatalogWithNewIntegration(
|
||||
monorepoPath: string,
|
||||
integrationPath: string,
|
||||
resolvedOptions: ResolvedCLIOptions
|
||||
) {
|
||||
const packageName = getPackageName(resolvedOptions.packageName);
|
||||
const jobCatalogPath = pathModule.join(monorepoPath, "references", "job-catalog");
|
||||
const jobCatalogSrcFolder = pathModule.join(jobCatalogPath, "src");
|
||||
|
||||
const integrationFile = `export {}`;
|
||||
|
||||
await createFileInPath(jobCatalogSrcFolder, `${packageName}.ts`, integrationFile);
|
||||
|
||||
const packageJsonPath = pathModule.join(jobCatalogPath, "package.json");
|
||||
|
||||
const packageJson = await readJSONFile(packageJsonPath);
|
||||
const newPackageJson = {
|
||||
...packageJson,
|
||||
scripts: {
|
||||
...packageJson.scripts,
|
||||
[packageName]: `nodemon --watch src/${packageName}.ts -r tsconfig-paths/register -r dotenv/config src/${packageName}.ts`,
|
||||
},
|
||||
dependencies: {
|
||||
...packageJson.dependencies,
|
||||
[resolvedOptions.packageName]: `workspace:*`,
|
||||
},
|
||||
};
|
||||
|
||||
// Move "dev:trigger script" to the last position in the scripts object
|
||||
if (newPackageJson.scripts.hasOwnProperty("dev:trigger")) {
|
||||
const devTriggerScript = newPackageJson.scripts["dev:trigger"];
|
||||
delete newPackageJson.scripts["dev:trigger"];
|
||||
newPackageJson.scripts["dev:trigger"] = devTriggerScript;
|
||||
}
|
||||
|
||||
await writeJSONFile(packageJsonPath, newPackageJson);
|
||||
|
||||
const tsConfigPath = pathModule.join(jobCatalogPath, "tsconfig.json");
|
||||
const tsConfig = await readJSONFile(tsConfigPath);
|
||||
|
||||
const newTsConfig = {
|
||||
...tsConfig,
|
||||
compilerOptions: {
|
||||
...tsConfig.compilerOptions,
|
||||
paths: {
|
||||
...tsConfig.compilerOptions.paths,
|
||||
[resolvedOptions.packageName]: [
|
||||
`../../integrations/${pathModule.basename(integrationPath)}/src/index`,
|
||||
],
|
||||
[`${resolvedOptions.packageName}/*`]: [
|
||||
`../../integrations/${pathModule.basename(integrationPath)}/src/*`,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeJSONFile(tsConfigPath, newTsConfig);
|
||||
}
|
||||
|
||||
const createIntegrationFiles = async (
|
||||
files: {
|
||||
relativeTemplatePath: string;
|
||||
outputPath: string;
|
||||
}[],
|
||||
variables?: Record<string, any>
|
||||
) => {
|
||||
for (const file of files) {
|
||||
const result = await createIntegrationFileFromTemplate({ ...file, variables });
|
||||
handleCreateResult(file.outputPath, result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateResult = (
|
||||
outputPath: string,
|
||||
result: Awaited<ReturnType<typeof createIntegrationFileFromTemplate>>
|
||||
) => {
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to create ${pathModule.basename(outputPath)}: ${result.error}`);
|
||||
}
|
||||
logger.success(`✔ Created ${pathModule.basename(outputPath)} at ${relativePath(outputPath)}`);
|
||||
};
|
||||
@@ -1,637 +0,0 @@
|
||||
import boxen from "boxen";
|
||||
import childProcess from "child_process";
|
||||
import chokidar from "chokidar";
|
||||
import ngrok from "ngrok";
|
||||
import ora, { Ora } from "ora";
|
||||
import pRetry, { AbortError } from "p-retry";
|
||||
import util from "util";
|
||||
import { z } from "zod";
|
||||
import https from "https";
|
||||
import { Framework } from "../frameworks";
|
||||
import { standardWatchFilePaths, standardWatchIgnoreRegex } from "../frameworks/watchConfig";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { getEnvFilename } from "../utils/env";
|
||||
import fetch, { RequestInit } from "../utils/fetchUseProxy";
|
||||
import { getTriggerApiDetails } from "../utils/getTriggerApiDetails";
|
||||
import { JsRuntime, getJsRuntime } from "../utils/jsRuntime";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { RequireKeys } from "../utils/requiredKeys";
|
||||
import { Throttle } from "../utils/throttle";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
import { wait } from "../utils/wait";
|
||||
import { YaltTunnel } from "@trigger.dev/yalt";
|
||||
import chalk from "chalk";
|
||||
|
||||
const asyncExecFile = util.promisify(childProcess.execFile);
|
||||
|
||||
export const DevCommandOptionsSchema = z.object({
|
||||
port: z.coerce.number().optional(),
|
||||
hostname: z.string().optional(),
|
||||
envFile: z.string().optional(),
|
||||
handlerPath: z.string(),
|
||||
clientId: z.string().optional(),
|
||||
tunnel: z
|
||||
.string()
|
||||
.url()
|
||||
.regex(/^(http|https).+/, "only http/https URLs are accepted")
|
||||
.optional(),
|
||||
https: z.boolean().default(false).optional(),
|
||||
});
|
||||
|
||||
export type DevCommandOptions = z.infer<typeof DevCommandOptionsSchema>;
|
||||
type ResolvedOptions = RequireKeys<DevCommandOptions, "handlerPath" | "envFile">;
|
||||
|
||||
const throttleTimeMs = 1000;
|
||||
|
||||
const formattedDate = new Intl.DateTimeFormat("en", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
});
|
||||
|
||||
let runtime: JsRuntime;
|
||||
|
||||
type TunnelUrl = {
|
||||
type: "tunnel";
|
||||
url: string;
|
||||
};
|
||||
|
||||
type ResolvedUrl = {
|
||||
type: "resolved";
|
||||
hostname: string;
|
||||
port: number;
|
||||
https: boolean;
|
||||
};
|
||||
|
||||
type ServerUrl = TunnelUrl | ResolvedUrl;
|
||||
|
||||
type TunnelEndpoint = TunnelUrl & {
|
||||
handlerPath: string;
|
||||
};
|
||||
|
||||
type ResolvedEndpoint = ResolvedUrl & {
|
||||
handlerPath: string;
|
||||
};
|
||||
|
||||
type ServerEndpoint = TunnelEndpoint | ResolvedEndpoint;
|
||||
|
||||
export async function devCommand(path: string, anyOptions: any) {
|
||||
telemetryClient.dev.started(path, anyOptions);
|
||||
|
||||
const result = DevCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
telemetryClient.dev.failed("invalid_options", anyOptions, result.error);
|
||||
return;
|
||||
}
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
runtime = await getJsRuntime(resolvedPath, logger);
|
||||
//check for outdated packages, don't immediately await this
|
||||
const checkForOutdatedPackagesPromise = runtime.checkForOutdatedPackages();
|
||||
|
||||
// Read from package.json to get the endpointId
|
||||
const endpointId = await getEndpointId(runtime, options.clientId);
|
||||
if (!endpointId) {
|
||||
logger.error(
|
||||
"You must run the `init` command first to setup the project – you are missing \n'trigger.dev': { 'endpointId': 'your-client-id' } from your package.json file, or pass in the --client-id option to this command"
|
||||
);
|
||||
telemetryClient.dev.failed("missing_endpoint_id", options);
|
||||
return;
|
||||
}
|
||||
logger.success(`✔️ [trigger.dev] Detected TriggerClient id: ${endpointId}`);
|
||||
|
||||
//resolve the options using the detected framework (use default if there isn't a matching framework)
|
||||
const packageManager = await runtime.getUserPackageManager();
|
||||
const framework = await runtime.getFramework();
|
||||
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", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
const { apiUrl, apiKey, apiKeySource } = apiDetails;
|
||||
logger.success(`✔️ [trigger.dev] Found API Key in ${apiKeySource}`);
|
||||
|
||||
//verify that the endpoint can be reached
|
||||
const verifiedEndpoint = await verifyEndpoint(resolvedOptions, endpointId, apiKey, framework);
|
||||
if (!verifiedEndpoint) {
|
||||
logger.error(
|
||||
`✖ [trigger.dev] Your endpoint couldn't be verified. Make sure your app is running and try again. ${resolvedOptions.handlerPath}`
|
||||
);
|
||||
logger.info(
|
||||
` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port, or -s to specify https, or -t to specify the tunnel-url pointing to the local dev server.`
|
||||
);
|
||||
telemetryClient.dev.failed("no_server_found", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryClient.dev.serverRunning(path, resolvedOptions);
|
||||
|
||||
// Setup tunnel
|
||||
const endpointUrl = await resolveEndpointUrl(apiUrl, apiKey, verifiedEndpoint);
|
||||
|
||||
if (!endpointUrl) {
|
||||
telemetryClient.dev.failed("failed_to_create_tunnel", resolvedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
const endpointHandlerUrl = `${endpointUrl}${verifiedEndpoint.handlerPath}`;
|
||||
telemetryClient.dev.tunnelRunning(path, resolvedOptions);
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
const outdatedPackages = await checkForOutdatedPackagesPromise;
|
||||
|
||||
if (outdatedPackages) {
|
||||
console.log(
|
||||
chalk.bgYellow(
|
||||
`New @trigger.dev/* packages available (${outdatedPackages.from} -> ${outdatedPackages.to})`
|
||||
)
|
||||
);
|
||||
console.log(chalk.bgBlue("Run npx @trigger.dev/cli@latest update"));
|
||||
}
|
||||
|
||||
const connectingSpinner = ora(`[trigger.dev] Registering endpoint ${endpointHandlerUrl}...`);
|
||||
let hasConnected = false;
|
||||
const abortController = new AbortController();
|
||||
|
||||
const r = () => {
|
||||
refresh({
|
||||
endpointId,
|
||||
spinner: connectingSpinner,
|
||||
path: resolvedPath,
|
||||
endpointHandlerUrl,
|
||||
resolvedOptions,
|
||||
hasConnected,
|
||||
abortController,
|
||||
});
|
||||
};
|
||||
|
||||
const throttle = new Throttle(r, throttleTimeMs);
|
||||
|
||||
watcher.on("all", (_event, _path) => {
|
||||
throttle.call();
|
||||
});
|
||||
|
||||
//Do initial refresh
|
||||
throttle.call();
|
||||
}
|
||||
|
||||
type RefreshOptions = {
|
||||
spinner: Ora;
|
||||
path: string;
|
||||
endpointId: string;
|
||||
endpointHandlerUrl: string;
|
||||
resolvedOptions: ResolvedOptions;
|
||||
hasConnected: boolean;
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
async function refresh(options: RefreshOptions) {
|
||||
//stop any existing refreshes
|
||||
options.abortController.abort();
|
||||
options.abortController = new AbortController();
|
||||
|
||||
// Read from env file to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(options.path, options.resolvedOptions.envFile);
|
||||
if (!apiDetails) {
|
||||
options.spinner.fail("[trigger.dev] Failed to connect: Missing API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiKey, apiUrl } = apiDetails;
|
||||
const apiClient = new TriggerApi(apiKey, apiUrl);
|
||||
|
||||
try {
|
||||
const index = await pRetry(() => startIndexing({ ...options, apiClient }), {
|
||||
retries: 5,
|
||||
signal: options.abortController.signal,
|
||||
maxTimeout: 5000,
|
||||
});
|
||||
options.spinner.text = `[trigger.dev] Refreshing ${formattedDate.format(index.updatedAt)}`;
|
||||
|
||||
if (!options.hasConnected) {
|
||||
options.hasConnected = true;
|
||||
telemetryClient.dev.connected(options.path, options.resolvedOptions);
|
||||
}
|
||||
|
||||
//this is for backwards-compatibility with older servers
|
||||
if (index.id === undefined) {
|
||||
options.spinner.succeed(`[trigger.dev] Refreshed ${formattedDate.format(index.updatedAt)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
//wait 750ms before attempting to get the indexing result
|
||||
await wait(750);
|
||||
|
||||
const indexResult = await pRetry(() => fetchIndexResult({ indexId: index.id, apiClient }), {
|
||||
//this means we're polling, same distance between each attempt
|
||||
factor: 1,
|
||||
retries: 10,
|
||||
signal: options.abortController.signal,
|
||||
});
|
||||
|
||||
if (indexResult.status === "FAILURE") {
|
||||
options.spinner.fail(
|
||||
`[trigger.dev] Refreshing failed ${formattedDate.format(indexResult.updatedAt)}`
|
||||
);
|
||||
logger.error(
|
||||
boxen(indexResult.error.message, {
|
||||
padding: 1,
|
||||
borderStyle: "double",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
options.spinner.succeed(
|
||||
`[trigger.dev] Refreshed ${formattedDate.format(indexResult.updatedAt)}`
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof AbortError) {
|
||||
options.spinner.fail(e.message);
|
||||
logger.info(` [trigger.dev] Will attempt again on the next file change…`);
|
||||
return;
|
||||
}
|
||||
|
||||
let message: string = "";
|
||||
if (e instanceof Error) {
|
||||
message = e.message;
|
||||
} else {
|
||||
message = "Unknown error";
|
||||
}
|
||||
|
||||
options.spinner.fail(message);
|
||||
logger.info(` [trigger.dev] Will attempt again on the next file change…`);
|
||||
|
||||
if (!options.hasConnected) {
|
||||
telemetryClient.dev.failed("failed_to_connect", options.resolvedOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startIndexing({
|
||||
spinner,
|
||||
path,
|
||||
endpointId,
|
||||
endpointHandlerUrl,
|
||||
resolvedOptions,
|
||||
apiClient,
|
||||
}: RefreshOptions & { apiClient: TriggerApi }) {
|
||||
spinner.start();
|
||||
const refreshedEndpointId = await getEndpointId(runtime, resolvedOptions.clientId);
|
||||
|
||||
const authorizedKey = await apiClient.whoami();
|
||||
if (!authorizedKey) {
|
||||
telemetryClient.dev.failed("invalid_api_key", resolvedOptions);
|
||||
throw new AbortError(
|
||||
"[trigger.dev] The API key you provided is not authorized. Try visiting your dashboard to get a new API key."
|
||||
);
|
||||
}
|
||||
|
||||
telemetryClient.identify(
|
||||
authorizedKey.organization.id,
|
||||
authorizedKey.project.id,
|
||||
authorizedKey.userId
|
||||
);
|
||||
|
||||
const result = await refreshEndpoint(
|
||||
apiClient,
|
||||
refreshedEndpointId ?? endpointId,
|
||||
endpointHandlerUrl
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return { id: result.data.endpointIndex?.id, updatedAt: new Date(result.data.updatedAt) };
|
||||
}
|
||||
|
||||
async function fetchIndexResult({
|
||||
indexId,
|
||||
apiClient,
|
||||
}: {
|
||||
indexId: string;
|
||||
apiClient: TriggerApi;
|
||||
}) {
|
||||
const result = await apiClient.getEndpointIndex(indexId);
|
||||
|
||||
if (result.status === "STARTED" || result.status === "PENDING") {
|
||||
throw new Error("Indexing is still in progress");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function resolveOptions(
|
||||
framework: Framework | undefined,
|
||||
path: string,
|
||||
unresolvedOptions: DevCommandOptions
|
||||
): Promise<ResolvedOptions> {
|
||||
if (!framework) {
|
||||
logger.info(" [trigger.dev] 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,
|
||||
tunnel: unresolvedOptions.tunnel,
|
||||
https: unresolvedOptions.https,
|
||||
};
|
||||
}
|
||||
|
||||
//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,
|
||||
tunnel: unresolvedOptions.tunnel,
|
||||
https: unresolvedOptions.https,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyEndpoint(
|
||||
resolvedOptions: ResolvedOptions,
|
||||
endpointId: string,
|
||||
apiKey: string,
|
||||
framework?: Framework
|
||||
) {
|
||||
const serverUrls = findServerUrls(resolvedOptions, framework);
|
||||
|
||||
//try each url
|
||||
for (const serverUrl of serverUrls) {
|
||||
const protocol = resolvedOptions.https ? "https" : "http";
|
||||
const url =
|
||||
serverUrl.type === "tunnel"
|
||||
? serverUrl.url
|
||||
: `${protocol}://${serverUrl.hostname}:${serverUrl.port}`;
|
||||
const localEndpointHandlerUrl = `${url}${resolvedOptions.handlerPath}`;
|
||||
|
||||
const spinner = ora(
|
||||
`[trigger.dev] Looking for your trigger endpoint: ${localEndpointHandlerUrl}`
|
||||
).start();
|
||||
|
||||
try {
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false, // Ignore self-signed certificates
|
||||
});
|
||||
|
||||
// Conditionally include the agent in fetch options
|
||||
const fetchOptions: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-trigger-api-key": apiKey,
|
||||
"x-trigger-action": "PING",
|
||||
"x-trigger-endpoint-id": endpointId,
|
||||
},
|
||||
...(resolvedOptions.https && { agent }),
|
||||
};
|
||||
|
||||
const response = await fetch(localEndpointHandlerUrl, fetchOptions);
|
||||
|
||||
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 {
|
||||
...serverUrl,
|
||||
handlerPath: resolvedOptions.handlerPath,
|
||||
https: resolvedOptions.https ?? false,
|
||||
};
|
||||
} catch (err) {
|
||||
spinner.fail(`[trigger.dev] No server found (${localEndpointHandlerUrl}).`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export function getEndpointId(runtime: JsRuntime, clientId?: string) {
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
} else return runtime.getEndpointId();
|
||||
}
|
||||
|
||||
function findServerUrls(resolvedOptions: ResolvedOptions, framework?: Framework): ServerUrl[] {
|
||||
if (resolvedOptions.tunnel) {
|
||||
logger.info(` Using provided tunnel URL: ${resolvedOptions.tunnel}`);
|
||||
return [{ type: "tunnel", url: resolvedOptions.tunnel }];
|
||||
}
|
||||
|
||||
//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: ResolvedUrl[] = [];
|
||||
for (const hostname of hostnames) {
|
||||
for (const port of ports) {
|
||||
urls.push({ type: "resolved", hostname, port, https: resolvedOptions.https ?? false });
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
async function resolveEndpointUrl(apiUrl: string, apiKey: string, endpoint: ServerEndpoint) {
|
||||
// use tunnel URL if provided
|
||||
if (endpoint.type === "tunnel") {
|
||||
return endpoint.url;
|
||||
}
|
||||
|
||||
const apiURL = new URL(apiUrl);
|
||||
|
||||
// if the API is localhost and the hostname is localhost
|
||||
if (apiURL.hostname === "localhost" && endpoint.hostname === "localhost") {
|
||||
return `http://${endpoint.hostname}:${endpoint.port}`;
|
||||
}
|
||||
|
||||
const triggerApi = new TriggerApi(apiKey, apiUrl);
|
||||
|
||||
const supportsTunneling = await triggerApi.supportsTunneling();
|
||||
|
||||
if (supportsTunneling) {
|
||||
const tunnelSpinner = ora(`🚇 Creating Trigger.dev tunnel`).start();
|
||||
const tunnelUrl = await createNativeTunnel(
|
||||
endpoint.hostname,
|
||||
endpoint.port,
|
||||
endpoint.https,
|
||||
triggerApi,
|
||||
tunnelSpinner
|
||||
);
|
||||
|
||||
if (tunnelUrl) {
|
||||
tunnelSpinner.succeed(`🚇 Trigger.dev tunnel ready`);
|
||||
}
|
||||
|
||||
return tunnelUrl;
|
||||
} else {
|
||||
// Setup tunnel
|
||||
const tunnelSpinner = ora(`🚇 Creating tunnel`).start();
|
||||
const tunnelUrl = await createNgrokTunnel(endpoint.hostname, endpoint.port, tunnelSpinner);
|
||||
|
||||
if (tunnelUrl) {
|
||||
tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`);
|
||||
}
|
||||
|
||||
return tunnelUrl;
|
||||
}
|
||||
}
|
||||
|
||||
let yaltTunnel: YaltTunnel | null = null;
|
||||
|
||||
async function createNativeTunnel(
|
||||
hostname: string,
|
||||
port: number,
|
||||
https: boolean,
|
||||
triggerApi: TriggerApi,
|
||||
spinner: Ora
|
||||
) {
|
||||
try {
|
||||
const response = await triggerApi.createTunnel();
|
||||
|
||||
// import WS dynamically
|
||||
const WebSocket = await import("ws");
|
||||
|
||||
yaltTunnel = new YaltTunnel(
|
||||
response.url,
|
||||
`${hostname}:${port}`,
|
||||
https,
|
||||
{
|
||||
WebSocket: WebSocket.default,
|
||||
connectionTimeout: getConnectionTimeoutValue(),
|
||||
maxRetries: 10,
|
||||
},
|
||||
{ verbose: process.env.TUNNEL_VERBOSE === "1" }
|
||||
);
|
||||
|
||||
await yaltTunnel.connect();
|
||||
|
||||
return `https://${response.url}`;
|
||||
} catch (e) {
|
||||
spinner.fail(`Failed to create tunnel.\n${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectionTimeoutValue() {
|
||||
if (typeof process.env.TUNNEL_CONNECTION_TIMEOUT === "string") {
|
||||
return parseInt(process.env.TUNNEL_CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
async function createNgrokTunnel(hostname: string, port: number, spinner: Ora) {
|
||||
try {
|
||||
return await ngrok.connect({ addr: `${hostname}:${port}` });
|
||||
} catch (error: any) {
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
error.message.includes("`version` property is required")
|
||||
) {
|
||||
await upgradeNgrokConfig(spinner);
|
||||
|
||||
try {
|
||||
return await ngrok.connect(port);
|
||||
} catch (retryError) {
|
||||
spinner.fail(
|
||||
`Ngrok failed to create a tunnel for port ${port} after configuration upgrade.\n${retryError}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
error.message.includes("connect ECONNREFUSED 127.0.0.1:4041")
|
||||
) {
|
||||
spinner.fail(
|
||||
`Ngrok failed to create a tunnel for port ${port} because ngrok is already running.\n You may want to use -t flag to use an existing URL that points to the local dev server.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
spinner.fail(`Ngrok failed to create a tunnel for port ${port}.\n${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeNgrokConfig(spinner: Ora) {
|
||||
try {
|
||||
await asyncExecFile("ngrok", ["config", "upgrade"]);
|
||||
spinner.info("Ngrok configuration upgraded successfully.");
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to upgrade ngrok configuration.\n${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshEndpoint(apiClient: TriggerApi, endpointId: string, endpointUrl: string) {
|
||||
try {
|
||||
const response = await apiClient.registerEndpoint({
|
||||
id: endpointId,
|
||||
url: endpointUrl,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: response.error,
|
||||
retryable: response.retryable,
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true as const, data: response.data };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
return { success: false as const, error: e.message, retryable: true };
|
||||
} else {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Unknown error",
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs/promises";
|
||||
import inquirer from "inquirer";
|
||||
import pathModule from "path";
|
||||
import { simpleGit } from "simple-git";
|
||||
import { promptApiKey, promptTriggerUrl } from "../cli/index";
|
||||
import { CLOUD_API_URL, CLOUD_TRIGGER_URL, COMMAND_NAME } from "../consts";
|
||||
import { Framework, frameworkNames, getFramework } from "../frameworks";
|
||||
import { telemetryClient } from "../telemetry/telemetry";
|
||||
import { addDependencies } from "../utils/addDependencies";
|
||||
import {
|
||||
getEnvFilename,
|
||||
setApiKeyEnvironmentVariable,
|
||||
setApiUrlEnvironmentVariable,
|
||||
setPublicApiKeyEnvironmentVariable,
|
||||
} 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 { readPackageJson } from "../utils/readPackageJson";
|
||||
import { renderTitle } from "../utils/renderTitle";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
import { getJsRuntime } from "../utils/jsRuntime";
|
||||
|
||||
export type InitCommandOptions = {
|
||||
projectPath: string;
|
||||
triggerUrl?: string;
|
||||
endpointSlug?: string;
|
||||
apiKey?: string;
|
||||
apiUrl?: string;
|
||||
};
|
||||
|
||||
type ResolvedOptions = Required<InitCommandOptions>;
|
||||
|
||||
export const initCommand = async (options: InitCommandOptions) => {
|
||||
telemetryClient.init.started(options);
|
||||
|
||||
const resolvedPath = resolvePath(options.projectPath);
|
||||
|
||||
// assuming nodejs by default
|
||||
let runtimeId: string = "nodejs";
|
||||
try {
|
||||
runtimeId = (await getJsRuntime(resolvedPath, logger)).id;
|
||||
} catch {}
|
||||
if (runtimeId !== "nodejs") {
|
||||
logger.error(
|
||||
`We currently only support automatic setup for NodeJS projects. This is a ${runtimeId} project. View our manual installation guides here: https://trigger.dev/docs/documentation/quickstarts/introduction`
|
||||
);
|
||||
telemetryClient.init.failed("not_supported_runtime", options);
|
||||
return;
|
||||
}
|
||||
|
||||
await renderTitle(resolvedPath);
|
||||
|
||||
if (options.triggerUrl === CLOUD_TRIGGER_URL) {
|
||||
logger.info(`✨ Initializing project in Trigger.dev Cloud`);
|
||||
} else if (typeof options.triggerUrl === "string") {
|
||||
logger.info(`✨ Initializing project using Trigger.dev at ${options.triggerUrl}`);
|
||||
} else {
|
||||
logger.info(`✨ Initializing Trigger.dev in project`);
|
||||
}
|
||||
|
||||
const packageManager = await getUserPackageManager(resolvedPath);
|
||||
const framework = await getFramework(resolvedPath, packageManager);
|
||||
|
||||
if (!framework) {
|
||||
logger.error(
|
||||
`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_supported_project", options);
|
||||
return;
|
||||
}
|
||||
logger.success(`✔ Detected ${framework.name} project`);
|
||||
|
||||
const hasGitChanges = await detectGitChanges(resolvedPath);
|
||||
if (hasGitChanges) {
|
||||
// Warn the user that they have git changes
|
||||
logger.warn(
|
||||
"⚠️ You have uncommitted git changes, you may want to commit them before continuing."
|
||||
);
|
||||
}
|
||||
|
||||
const isTypescriptProject = await detectTypescriptProject(resolvedPath);
|
||||
telemetryClient.init.isTypescriptProject(isTypescriptProject, options);
|
||||
|
||||
const optionsAfterPrompts = await resolveOptionsWithPrompts(options, resolvedPath);
|
||||
const apiKey = optionsAfterPrompts.apiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
logger.error("You must provide an API key to continue.");
|
||||
telemetryClient.init.failed("no_api_key", optionsAfterPrompts);
|
||||
return;
|
||||
}
|
||||
|
||||
const apiClient = new TriggerApi(apiKey, optionsAfterPrompts.apiUrl);
|
||||
const authorizedKey = await apiClient.whoami();
|
||||
|
||||
if (!authorizedKey) {
|
||||
logger.error(
|
||||
`🛑 The API key you provided is not authorized. Try visiting your dashboard at ${optionsAfterPrompts.triggerUrl} to get a new API key.`
|
||||
);
|
||||
|
||||
telemetryClient.init.failed("invalid_api_key", optionsAfterPrompts);
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryClient.identify(
|
||||
authorizedKey.organization.id,
|
||||
authorizedKey.project.id,
|
||||
authorizedKey.userId
|
||||
);
|
||||
|
||||
const endpointSlug = authorizedKey.project.slug;
|
||||
const resolvedOptions: ResolvedOptions = { ...optionsAfterPrompts, endpointSlug };
|
||||
|
||||
//install dependencies
|
||||
const dependencies = await framework.dependencies();
|
||||
await addDependencies(resolvedPath, dependencies);
|
||||
telemetryClient.init.addedDependencies(resolvedOptions);
|
||||
|
||||
// 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);
|
||||
await setPublicApiKeyEnvironmentVariable(
|
||||
resolvedPath,
|
||||
envName,
|
||||
framework.publicKeyEnvName,
|
||||
authorizedKey.pkApiKey
|
||||
);
|
||||
|
||||
const installOptions = {
|
||||
typescript: isTypescriptProject,
|
||||
packageManager,
|
||||
endpointSlug: resolvedOptions.endpointSlug,
|
||||
};
|
||||
|
||||
telemetryClient.init.install(resolvedOptions, framework.name, installOptions);
|
||||
await framework.install(resolvedPath, installOptions);
|
||||
|
||||
telemetryClient.init.postInstall(resolvedOptions, framework.name, installOptions);
|
||||
await framework.postInstall(resolvedPath, installOptions);
|
||||
|
||||
await addConfigurationToPackageJson(resolvedPath, resolvedOptions);
|
||||
|
||||
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(
|
||||
projectUrl: string,
|
||||
packageManager: PackageManager,
|
||||
framework: Framework
|
||||
) {
|
||||
logger.success(`✔ Successfully initialized Trigger.dev!`);
|
||||
logger.warn(
|
||||
`⚠️ Warning: We don't currently support long-running servers! For more details, check out https://github.com/triggerdotdev/trigger.dev/issues/244.`
|
||||
);
|
||||
logger.info("Next steps:");
|
||||
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`
|
||||
);
|
||||
logger.info(` 3. View your jobs at ${projectUrl}`);
|
||||
|
||||
logger.info(
|
||||
`🔗 Head over to our docs at https://trigger.dev/docs to learn more about how to create different kinds of jobs and add integrations.`
|
||||
);
|
||||
}
|
||||
|
||||
async function addConfigurationToPackageJson(path: string, options: ResolvedOptions) {
|
||||
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`);
|
||||
}
|
||||
|
||||
type OptionsAfterPrompts = Required<Omit<InitCommandOptions, "endpointSlug">> & {
|
||||
endpointSlug: InitCommandOptions["endpointSlug"];
|
||||
};
|
||||
|
||||
const resolveOptionsWithPrompts = async (
|
||||
options: InitCommandOptions,
|
||||
path: string
|
||||
): Promise<OptionsAfterPrompts> => {
|
||||
const resolvedOptions: InitCommandOptions = { ...options };
|
||||
|
||||
try {
|
||||
if (!options.triggerUrl) {
|
||||
resolvedOptions.triggerUrl = await promptTriggerUrl();
|
||||
}
|
||||
|
||||
if (resolvedOptions.triggerUrl === CLOUD_TRIGGER_URL) {
|
||||
resolvedOptions.apiUrl = CLOUD_API_URL;
|
||||
} else {
|
||||
resolvedOptions.apiUrl = resolvedOptions.triggerUrl;
|
||||
}
|
||||
|
||||
telemetryClient.init.resolvedApiUrl(resolvedOptions.apiUrl, resolvedOptions);
|
||||
|
||||
if (!options.apiKey) {
|
||||
resolvedOptions.apiKey = await promptApiKey(resolvedOptions.triggerUrl!);
|
||||
}
|
||||
telemetryClient.init.resolvedApiKey(resolvedOptions);
|
||||
|
||||
if (!options.endpointSlug) {
|
||||
const packageJSONPath = pathModule.join(path, "package.json");
|
||||
const packageJSON = await readJSONFile(packageJSONPath);
|
||||
|
||||
if (packageJSON && packageJSON["trigger.dev"] && packageJSON["trigger.dev"].endpointId) {
|
||||
resolvedOptions.endpointSlug = packageJSON["trigger.dev"].endpointId;
|
||||
telemetryClient.init.resolvedEndpointSlug(resolvedOptions);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// If the user is not calling the command from an interactive terminal, inquirer will throw an error with isTTYError = true
|
||||
// If this happens, we catch the error, tell the user what has happened, and then continue to run the program with a default trigger project
|
||||
// Otherwise we have to do some fancy namespace extension logic on the Error type which feels overkill for one line
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (err instanceof Error && (err as any).isTTYError) {
|
||||
logger.warn(`'${COMMAND_NAME} init' needs an interactive terminal to provide options`);
|
||||
|
||||
const { shouldContinue } = await inquirer.prompt<{
|
||||
shouldContinue: boolean;
|
||||
}>({
|
||||
name: "shouldContinue",
|
||||
type: "confirm",
|
||||
message: `Continue initializing your trigger.dev project?`,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!shouldContinue) {
|
||||
telemetryClient.init.failed("non_interactive_terminal", options);
|
||||
logger.info("Exiting...");
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
telemetryClient.init.failed("unknown", options, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedOptions as OptionsAfterPrompts;
|
||||
};
|
||||
|
||||
// Detects if there are any uncommitted git changes at path
|
||||
async function detectGitChanges(path: string): Promise<boolean> {
|
||||
const git = simpleGit(path);
|
||||
|
||||
try {
|
||||
const isRepo = await git.checkIsRepo();
|
||||
|
||||
if (isRepo) {
|
||||
// Check if there are uncommitted changes
|
||||
const status = await git.status();
|
||||
return status.files.length > 0;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function detectTypescriptProject(path: string): Promise<boolean> {
|
||||
// Checks for the presence of a tsconfig.json file
|
||||
try {
|
||||
await fs.access(pathModule.join(path, "tsconfig.json"));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import ora from "ora";
|
||||
import { z } from "zod";
|
||||
import { getTriggerApiDetails } from "../utils/getTriggerApiDetails";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
|
||||
const SendEventCommandOptionsSchema = z.object({
|
||||
envFile: z.string(),
|
||||
name: z.string(),
|
||||
payload: z.string(),
|
||||
id: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SendEventCommandOptions = z.infer<typeof SendEventCommandOptionsSchema>;
|
||||
|
||||
export async function sendEventCommand(path: string, anyOptions: any) {
|
||||
const result = SendEventCommandOptionsSchema.safeParse(anyOptions);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Sending event", { options: result.data });
|
||||
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
|
||||
// Read from .env.local or .env to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, options.envFile);
|
||||
|
||||
if (!apiDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiUrl, apiKey } = apiDetails;
|
||||
|
||||
const parsedPayload = safeJSONParse(options.payload);
|
||||
|
||||
if (typeof parsedPayload !== "object") {
|
||||
logger.error(
|
||||
`The payload must be a valid JSON object. You can also pipe the payload in via stdin.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = options.id ?? randomUUID();
|
||||
const name = options.name;
|
||||
|
||||
const spinner = ora(`[trigger.dev] Sending event ${name} with id ${id}`).start();
|
||||
|
||||
const triggerApi = new TriggerApi(apiKey, apiUrl);
|
||||
|
||||
const ok = await triggerApi.sendEvent(id, name, parsedPayload);
|
||||
|
||||
if (ok) {
|
||||
spinner.succeed(`[trigger.dev] Event ${name} with id ${id} sent`);
|
||||
} else {
|
||||
spinner.fail(`[trigger.dev] Event ${name} with id ${id} failed to send`);
|
||||
}
|
||||
}
|
||||
|
||||
function safeJSONParse(payload: string): any {
|
||||
try {
|
||||
return JSON.parse(payload);
|
||||
} catch (e) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import path from "path";
|
||||
import inquirer from "inquirer";
|
||||
import { run, RunOptions } from "npm-check-updates";
|
||||
import { installDependencies } from "../utils/installDependencies.js";
|
||||
import { readJSONFileSync, writeJSONFile } from "../utils/fileSystem.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export const UpdateCommandOptionsSchema = z.object({
|
||||
to: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
|
||||
|
||||
type NcuRunOptionTarget = "latest" | `@${string}`;
|
||||
|
||||
export async function updateCommand(projectPath: string, anyOptions: any) {
|
||||
const parseRes = UpdateCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!parseRes.success) {
|
||||
logger.error(parseRes.error.message);
|
||||
return;
|
||||
}
|
||||
const options = parseRes.data;
|
||||
|
||||
const triggerDevPackage = "@trigger.dev";
|
||||
const packageJSONPath = path.join(projectPath, "package.json");
|
||||
const packageData = readJSONFileSync(packageJSONPath);
|
||||
|
||||
if (!packageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageMaps: { [k: string]: { type: string; version: string } } = {};
|
||||
const packageDependencies = packageData.dependencies || {};
|
||||
const packageDevDependencies = packageData.devDependencies || {};
|
||||
Object.keys(packageDependencies).forEach((i) => {
|
||||
packageMaps[i] = { type: "dependencies", version: packageDependencies[i] };
|
||||
});
|
||||
Object.keys(packageDevDependencies).forEach((i) => {
|
||||
packageMaps[i] = {
|
||||
type: "devDependencies",
|
||||
version: packageDevDependencies[i],
|
||||
};
|
||||
});
|
||||
|
||||
const targetVersion = getTargetVersion(options.to);
|
||||
|
||||
// Use npm-check-updates to get updated dependency versions
|
||||
const ncuOptions: RunOptions = {
|
||||
packageData,
|
||||
upgrade: true,
|
||||
jsonUpgraded: true,
|
||||
target: targetVersion,
|
||||
};
|
||||
|
||||
// Can either give a json like package.json or just with deps and their new versions
|
||||
const updatedDependencies: { [k: string]: any } | void = await run(ncuOptions);
|
||||
|
||||
if (!updatedDependencies) return;
|
||||
|
||||
const ifUpdatedDependenciesIsPackageJSON =
|
||||
updatedDependencies.hasOwnProperty("dependencies") ||
|
||||
updatedDependencies.hasOwnProperty("devDependencies");
|
||||
|
||||
const dependencies = updatedDependencies.dependencies || {};
|
||||
const devDependencies = updatedDependencies.devDependencies || {};
|
||||
|
||||
const allDependencies = ifUpdatedDependenciesIsPackageJSON
|
||||
? Object.keys({ ...dependencies, ...devDependencies })
|
||||
: Object.keys(updatedDependencies);
|
||||
|
||||
const triggerPackages = allDependencies.filter((pkg) => pkg.startsWith(triggerDevPackage));
|
||||
|
||||
// If there are no @trigger.dev packages
|
||||
if (triggerPackages.length === 0) {
|
||||
logger.success(`✔ All @trigger.dev/* packages are up to date.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter the packages with null and what don't match what
|
||||
// they are installed with so that they can be updated
|
||||
const packagesToUpdate = triggerPackages.filter((pkg: string) => updatedDependencies[pkg]);
|
||||
|
||||
// If no packages require any updation
|
||||
if (packagesToUpdate.length === 0) {
|
||||
logger.success(`✔ All @trigger.dev/* packages are up to date.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let applyUpdates = targetVersion !== "latest";
|
||||
|
||||
if (targetVersion === "latest") {
|
||||
applyUpdates = await hasUserConfirmed(packagesToUpdate, packageMaps, updatedDependencies);
|
||||
}
|
||||
|
||||
if (applyUpdates) {
|
||||
const newPackageJSON = packageData;
|
||||
packagesToUpdate.forEach((packageName) => {
|
||||
const tmp = packageMaps[packageName];
|
||||
if (tmp) {
|
||||
newPackageJSON[tmp.type][packageName] = updatedDependencies[packageName];
|
||||
}
|
||||
});
|
||||
await writeJSONFile(packageJSONPath, newPackageJSON);
|
||||
await installDependencies(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
// expects a version number, or latest.
|
||||
// if version number is specified, prepend it with '@' for ncu.
|
||||
function getTargetVersion(toVersion?: string): NcuRunOptionTarget {
|
||||
if (!toVersion) {
|
||||
return "latest";
|
||||
}
|
||||
return toVersion === "latest" ? "latest" : `@${toVersion}`;
|
||||
}
|
||||
|
||||
async function hasUserConfirmed(
|
||||
packagesToUpdate: string[],
|
||||
packageMaps: { [x: string]: { type: string; version: string } },
|
||||
updatedDependencies: { [x: string]: any }
|
||||
): Promise<boolean> {
|
||||
// Inform the user of the dependencies that can be updated
|
||||
console.log("\nNewer versions found for the following packages:");
|
||||
console.table(
|
||||
packagesToUpdate.map((i) => ({
|
||||
name: i,
|
||||
old: packageMaps[i]?.version,
|
||||
new: updatedDependencies[i],
|
||||
}))
|
||||
);
|
||||
|
||||
// Ask the user if they want to update the dependencies
|
||||
const { confirm } = await inquirer.prompt({
|
||||
type: "confirm",
|
||||
name: "confirm",
|
||||
message: "Do you want to update these packages in package.json and re-install dependencies?",
|
||||
});
|
||||
|
||||
return confirm;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { TriggerApi } from "../utils/triggerApi";
|
||||
import { DevCommandOptions, getEndpointId } from "./dev";
|
||||
import ora from "ora";
|
||||
import { getTriggerApiDetails } from "../utils/getTriggerApiDetails";
|
||||
import { getJsRuntime } from "../utils/jsRuntime";
|
||||
|
||||
export const WhoAmICommandOptionsSchema = z.object({
|
||||
envFile: z.string(),
|
||||
});
|
||||
|
||||
export type WhoAmICommandOptions = z.infer<typeof WhoAmICommandOptionsSchema>;
|
||||
|
||||
export async function whoamiCommand(path: string, anyOptions: any) {
|
||||
const loadingSpinner = ora(`Hold while we fetch your data`);
|
||||
loadingSpinner.start();
|
||||
|
||||
const result = WhoAmICommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!result.success) {
|
||||
logger.error(result.error.message);
|
||||
return;
|
||||
}
|
||||
const options = result.data;
|
||||
|
||||
const resolvedPath = resolvePath(path);
|
||||
|
||||
// Read from package.json to get the endpointId
|
||||
const runtime = await getJsRuntime(resolvedPath, logger);
|
||||
const endpointId = await getEndpointId(runtime);
|
||||
if (!endpointId) {
|
||||
logger.error(
|
||||
"You must run the `init` command first to setup the project – you are missing \n'trigger.dev': { 'endpointId': 'your-client-id' } from your package.json file, or pass in the --client-id option to this command"
|
||||
);
|
||||
loadingSpinner.stop();
|
||||
return;
|
||||
}
|
||||
// Read from .env.local or .env to get the TRIGGER_API_KEY and TRIGGER_API_URL
|
||||
const apiDetails = await getTriggerApiDetails(resolvedPath, options.envFile);
|
||||
|
||||
if (!apiDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerAPI = new TriggerApi(apiDetails.apiKey, apiDetails.apiUrl);
|
||||
const userData = await triggerAPI.whoami();
|
||||
|
||||
loadingSpinner.stop();
|
||||
|
||||
logger.info(`
|
||||
environment: ${userData?.type}
|
||||
Trigger Client Id: ${endpointId}
|
||||
User ID: ${userData?.userId}
|
||||
Project:
|
||||
id: ${userData?.project.id}
|
||||
slug: ${userData?.project.slug}
|
||||
name: ${userData?.project.name}
|
||||
Organization:
|
||||
id: ${userData?.organization.id}
|
||||
slug: ${userData?.organization.slug}
|
||||
title: ${userData?.organization.title}
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily.
|
||||
// Path is in relation to a single index.js file inside ./dist
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const distPath = path.dirname(__filename);
|
||||
export const PKG_ROOT = path.join(distPath, "../");
|
||||
|
||||
export const TITLE_TEXT = `
|
||||
_____ _ _
|
||||
|_ _| ___ |_| ___ ___ ___ ___ _| | ___ _ _
|
||||
| | | _|| || . || . || -_|| _| _ | . || -_|| | |
|
||||
|_| |_| |_||_ ||_ ||___||_| |_||___||___| \\_/
|
||||
|___||___|
|
||||
`;
|
||||
|
||||
export const DEFAULT_APP_NAME = "my-triggers";
|
||||
export const COMMAND_NAME = "@trigger.dev/cli";
|
||||
export const TEMPLATE_ORGANIZATION = "triggerdotdev";
|
||||
export const CLOUD_TRIGGER_URL = "https://cloud.trigger.dev";
|
||||
export const CLOUD_API_URL = "https://api.trigger.dev";
|
||||
@@ -1,81 +0,0 @@
|
||||
import mock from "mock-fs";
|
||||
import { Astro } from ".";
|
||||
import { getFramework } from "..";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe("Astro project detection", () => {
|
||||
test("has dependency", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { astro: "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("astro");
|
||||
});
|
||||
|
||||
test("no dependency, has astro.config.js", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
"astro.config.js": "module.exports = {}",
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("astro");
|
||||
});
|
||||
|
||||
test("no dependency, has astro.config.mjs", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
"astro.config.mjs": "module.exports = {}",
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).toEqual("astro");
|
||||
});
|
||||
|
||||
test("no dependency, no astro.config.*", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
});
|
||||
|
||||
const framework = await getFramework("", "npm");
|
||||
expect(framework?.id).not.toEqual("astro");
|
||||
});
|
||||
});
|
||||
|
||||
describe("install", () => {
|
||||
test("javascript", async () => {
|
||||
mock({
|
||||
src: {
|
||||
pages: {},
|
||||
},
|
||||
});
|
||||
|
||||
const astro = new Astro();
|
||||
await astro.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/example.js")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.js")).toEqual(true);
|
||||
});
|
||||
|
||||
test("typescript", async () => {
|
||||
mock({
|
||||
app: {
|
||||
routes: {},
|
||||
},
|
||||
"tsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const astro = new Astro();
|
||||
await astro.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/example.ts")).toEqual(true);
|
||||
expect(await pathExists("src/jobs/index.ts")).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
import { Framework, ProjectInstallOptions } from "..";
|
||||
import { InstallPackage } from "../../utils/addDependencies";
|
||||
import { pathExists, someFileExists } 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 Astro implements Framework {
|
||||
id = "astro";
|
||||
name = "Astro";
|
||||
|
||||
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
|
||||
const configFilenames = [
|
||||
"astro.config.js",
|
||||
"astro.config.mjs",
|
||||
"astro.config.cjs",
|
||||
"astro.config.ts",
|
||||
];
|
||||
//check for astro.config.mjs
|
||||
const hasConfigFile = await someFileExists(path, configFilenames);
|
||||
if (hasConfigFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//check for the astro package
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (packageJsonContent?.dependencies?.astro) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async dependencies(): Promise<InstallPackage[]> {
|
||||
return [
|
||||
{ name: "@trigger.dev/sdk", tag: "latest" },
|
||||
{ name: "@trigger.dev/astro", tag: "latest" },
|
||||
{ name: "@trigger.dev/react", tag: "latest" },
|
||||
];
|
||||
}
|
||||
|
||||
possibleEnvFilenames(): string[] {
|
||||
return [".env", ".env.development"];
|
||||
}
|
||||
|
||||
async install(path: string, { typescript, endpointSlug }: ProjectInstallOptions): Promise<void> {
|
||||
const pathAlias = await getPathAlias({
|
||||
projectPath: path,
|
||||
isTypescriptProject: typescript,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
const templatesDir = pathModule.join(templatesPath(), "astro");
|
||||
const srcFolder = pathModule.join(path, "src");
|
||||
const fileExtension = typescript ? ".ts" : ".js";
|
||||
|
||||
//create src/pages/api/trigger.js
|
||||
const apiRoutePath = pathModule.join(srcFolder, "pages", "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}`);
|
||||
|
||||
//src/trigger.js
|
||||
const triggerFilePath = pathModule.join(srcFolder, `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}`);
|
||||
|
||||
//src/jobs/example.js
|
||||
const exampleJobFilePath = pathModule.join(srcFolder, "jobs", `example${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}`);
|
||||
|
||||
//src/jobs/index.js
|
||||
const jobsIndexFilePath = pathModule.join(srcFolder, "jobs", `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}`);
|
||||
}
|
||||
|
||||
async postInstall(path: string, options: ProjectInstallOptions): Promise<void> {
|
||||
logger.warn(
|
||||
`⚠︎ Ensure your astro.config output is "server" or "hybrid":\nhttps://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project`
|
||||
);
|
||||
}
|
||||
|
||||
defaultHostnames = ["localhost", "[::]"];
|
||||
defaultPorts = [4321, 4322, 4323, 4324];
|
||||
watchFilePaths = standardWatchFilePaths;
|
||||
watchIgnoreRegex = /(node_modules)/;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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)/;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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 Hono implements Framework {
|
||||
id = "hono";
|
||||
name = "Hono.dev";
|
||||
|
||||
async isMatch(path: string, packageManager: PackageManager): Promise<boolean> {
|
||||
//check for the express package
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (packageJsonContent?.dependencies?.hono) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async dependencies(): Promise<InstallPackage[]> {
|
||||
return [
|
||||
{ name: "@trigger.dev/sdk", tag: "latest" },
|
||||
{ name: "@trigger.dev/hono", tag: "latest" },
|
||||
];
|
||||
}
|
||||
|
||||
possibleEnvFilenames(): string[] {
|
||||
return [".dev.vars", ".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 Hono.dev. \nFollow the steps in our quickstart installation guide: https://trigger.dev/docs/documentation/quickstarts/hono",
|
||||
{ padding: 1, margin: 1, borderStyle: "double", borderColor: "magenta" }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
defaultHostnames = ["127.0.0.1", "localhost", "[::]"];
|
||||
defaultPorts = [3000, 8000, 80, 8080];
|
||||
watchFilePaths = standardWatchFilePaths;
|
||||
watchIgnoreRegex = /(node_modules)/;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { InstallPackage } from "../utils/addDependencies";
|
||||
import { PackageManager } from "../utils/getUserPkgManager";
|
||||
import { Astro } from "./astro";
|
||||
import { Express } from "./express";
|
||||
import { Hono } from "./hono";
|
||||
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[];
|
||||
|
||||
/** Defaults to TRIGGER_PUBLIC_API_KEY */
|
||||
publicKeyEnvName?: 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>;
|
||||
|
||||
/** 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[];
|
||||
|
||||
/** 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(), new Astro(), new Express(), new Hono()];
|
||||
|
||||
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(", ");
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import pathModule from "path";
|
||||
import boxen from "boxen";
|
||||
import { Framework } from "..";
|
||||
import { templatesPath } from "../../paths";
|
||||
import { InstallPackage } from "../../utils/addDependencies";
|
||||
import { createFileFromTemplate } from "../../utils/createFileFromTemplate";
|
||||
import { pathExists, someFileExists } from "../../utils/fileSystem";
|
||||
import { PackageManager } from "../../utils/getUserPkgManager";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { getPathAlias } from "../../utils/pathAlias";
|
||||
import { readPackageJson } from "../../utils/readPackageJson";
|
||||
import { standardWatchFilePaths } from "../watchConfig";
|
||||
import { telemetryClient } from "../../telemetry/telemetry";
|
||||
import { detectMiddlewareUsage } from "./middleware";
|
||||
|
||||
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"];
|
||||
}
|
||||
|
||||
publicKeyEnvName = "NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY";
|
||||
|
||||
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 nextJsVersion = await detectNextVersion(path);
|
||||
const routeDir = pathModule.join(path, usesSrcDir ? "src" : "");
|
||||
const pathAlias = await getPathAlias({
|
||||
projectPath: path,
|
||||
isTypescriptProject: options.typescript,
|
||||
extraDirectories: usesSrcDir ? ["src"] : undefined,
|
||||
});
|
||||
const fileExtension = options.typescript ? ".ts" : ".js";
|
||||
|
||||
if (nextJsDir === "pages") {
|
||||
const apiRoutePath = pathModule.join(routeDir, "pages", "api", `trigger${fileExtension}`);
|
||||
if (nextJsVersion && nextJsVersion !== 'latest' && nextJsVersion < "13.5") {
|
||||
await createTriggerRoute({
|
||||
path: routeDir,
|
||||
apiRoutePath,
|
||||
template: "pagesApiRoute.js",
|
||||
fileExtension,
|
||||
endpointSlug: options.endpointSlug,
|
||||
pathAlias
|
||||
});
|
||||
} else {
|
||||
await createTriggerRoute({
|
||||
path: routeDir,
|
||||
apiRoutePath,
|
||||
template: "pagesApiRouteWithConfigObject.js",
|
||||
fileExtension,
|
||||
endpointSlug: options.endpointSlug,
|
||||
pathAlias
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const apiRoutePath = pathModule.join(routeDir, "app", "api", "trigger", `route${fileExtension}`);
|
||||
await createTriggerRoute({
|
||||
path: routeDir,
|
||||
apiRoutePath,
|
||||
template: "appApiRoute.js",
|
||||
fileExtension,
|
||||
endpointSlug: options.endpointSlug,
|
||||
pathAlias
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async postInstall(
|
||||
path: string,
|
||||
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
|
||||
): Promise<void> {
|
||||
const result = await detectMiddlewareUsage(path, options.typescript);
|
||||
if (result.hasMiddleware) {
|
||||
switch (result.conflict) {
|
||||
case "possible": {
|
||||
logger.warn(
|
||||
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${result.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 });
|
||||
break;
|
||||
}
|
||||
case "likely": {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${result.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 });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultHostnames = ["localhost"];
|
||||
defaultPorts = [3000, 3001, 3002];
|
||||
watchFilePaths = standardWatchFilePaths;
|
||||
watchIgnoreRegex = /(node_modules|\.next)/;
|
||||
}
|
||||
|
||||
async function detectNextConfigFile(path: string): Promise<boolean> {
|
||||
const configFilenames = [
|
||||
"next.config.js",
|
||||
"next.config.mjs",
|
||||
"next.config.cjs",
|
||||
"next.config.ts",
|
||||
];
|
||||
|
||||
return someFileExists(path, configFilenames);
|
||||
}
|
||||
|
||||
export async function detectNextDependency(path: string): Promise<boolean> {
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (!packageJsonContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (packageJsonContent.dependencies?.next !== undefined) return true;
|
||||
if (packageJsonContent.devDependencies?.next !== undefined) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
export async function detectNextVersion(path: string) {
|
||||
const packageJsonContent = await readPackageJson(path);
|
||||
if (!packageJsonContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versionNumberPattern = /[\d.]+|latest/;
|
||||
if (packageJsonContent.dependencies?.next !== undefined)
|
||||
return packageJsonContent.dependencies?.next?.match(versionNumberPattern)?.at(0) ?? null;
|
||||
if (packageJsonContent.devDependencies?.next !== undefined)
|
||||
return packageJsonContent.devDependencies?.next?.match(versionNumberPattern)?.at(0) ?? null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createTriggerRoute(options: {
|
||||
path: string,
|
||||
apiRoutePath: string,
|
||||
template: string,
|
||||
fileExtension: string,
|
||||
endpointSlug: string,
|
||||
pathAlias: string | undefined
|
||||
}) {
|
||||
const { path, apiRoutePath, template, pathAlias, endpointSlug, fileExtension } = options;
|
||||
|
||||
const templatesDir = pathModule.join(templatesPath(), "nextjs");
|
||||
const apiRouteResult = await createFileFromTemplate({
|
||||
templatePath: pathModule.join(templatesDir, template),
|
||||
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}`);
|
||||
logger.info(
|
||||
boxen(`If you're deploying to Vercel, configure your max duration in ${apiRoutePath}`, {
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
borderStyle: "double",
|
||||
borderColor: "magenta",
|
||||
})
|
||||
);
|
||||
|
||||
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}`);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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";
|
||||
import { detectUseOfSrcDir } from ".";
|
||||
|
||||
type Result =
|
||||
| {
|
||||
hasMiddleware: false;
|
||||
}
|
||||
| {
|
||||
hasMiddleware: true;
|
||||
conflict: "unlikely" | "possible" | "likely";
|
||||
middlewarePath: string;
|
||||
};
|
||||
|
||||
export async function detectMiddlewareUsage(path: string, typescript: boolean): Promise<Result> {
|
||||
const usesSrcDir = await detectUseOfSrcDir(path);
|
||||
const middlewarePath = pathModule.join(
|
||||
path,
|
||||
usesSrcDir ? "src" : "",
|
||||
`middleware.${typescript ? "ts" : "js"}`
|
||||
);
|
||||
|
||||
try {
|
||||
return await detectMiddleware(path, typescript, middlewarePath);
|
||||
} catch (e) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: pathModule.relative(process.cwd(), middlewarePath),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function detectMiddleware(
|
||||
path: string,
|
||||
typescript: boolean,
|
||||
middlewarePath: string
|
||||
): Promise<Result> {
|
||||
const middlewareExists = await pathExists(middlewarePath);
|
||||
if (!middlewareExists) {
|
||||
return { hasMiddleware: false };
|
||||
}
|
||||
|
||||
const middlewareRelativeFilePath = pathModule.relative(process.cwd(), middlewarePath);
|
||||
|
||||
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
|
||||
|
||||
if (!matcher || matcher.length === 0) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (matcher.length === 0) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "unlikely",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
|
||||
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "likely",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
|
||||
const fileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
const regex = /matcher:\s*(\[.*\]|["'].*["'])/g;
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
import mock from "mock-fs";
|
||||
import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir, detectNextVersion } from ".";
|
||||
import { getFramework } from "..";
|
||||
import { pathExists } from "../../utils/fileSystem";
|
||||
import { detectMiddlewareUsage } from "./middleware";
|
||||
|
||||
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("has dev dependency", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ devDependencies: { 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, has next.config.mjs", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { foo: "1.0.0" } }),
|
||||
"next.config.mjs": "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("Next version detection", () => {
|
||||
test("detect Nextjs latest version", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { next: "latest" } }),
|
||||
});
|
||||
|
||||
const nextJsVersion = await detectNextVersion("");
|
||||
expect(nextJsVersion).toEqual("latest");
|
||||
});
|
||||
|
||||
test("detect Nextjs 13.0.0 version", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ dependencies: { next: "13.0.0" } }),
|
||||
});
|
||||
|
||||
const nextJsVersion = await detectNextVersion("");
|
||||
expect(nextJsVersion).toEqual("13.0.0");
|
||||
});
|
||||
|
||||
test("detect Nextjs version as a dev dependency", async () => {
|
||||
mock({
|
||||
"package.json": JSON.stringify({ devDependencies: { next: "^12.0.0" } }),
|
||||
});
|
||||
|
||||
const nextJsVersion = await detectNextVersion("");
|
||||
expect(nextJsVersion).toEqual("12.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Next middleware detection", () => {
|
||||
test("no middleware", async () => {
|
||||
mock({});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(false);
|
||||
});
|
||||
|
||||
test("Basic middleware", async () => {
|
||||
mock({
|
||||
"middleware.js": `import { NextResponse } from 'next/server'
|
||||
|
||||
export function middleware(request) {
|
||||
return NextResponse.redirect(new URL('/home', request.url))
|
||||
}
|
||||
|
||||
// See "Matching Paths" below to learn more
|
||||
export const config = {
|
||||
matcher: '/about/:path*',
|
||||
}`,
|
||||
});
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("possible");
|
||||
});
|
||||
|
||||
test("Wildcard that throws middleware", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: "*",
|
||||
}`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("possible");
|
||||
});
|
||||
|
||||
test("Array middleware", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: ['/about/:path*', "/dashboard/:path*"],
|
||||
}`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("possible");
|
||||
});
|
||||
|
||||
test("With dashes middleware", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: ["/configurations-test/:path*", "/projects/:path*"],
|
||||
};`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("possible");
|
||||
});
|
||||
|
||||
test("Likely double quoted string", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: "/(.*)",
|
||||
};`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("likely");
|
||||
});
|
||||
|
||||
test("Likely single quoted string", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: '/(.*)',
|
||||
};`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("likely");
|
||||
});
|
||||
|
||||
test("Likely double quoted array", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: ["/pages/", "/(.*)"],
|
||||
};`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("likely");
|
||||
});
|
||||
|
||||
test("Likely single quoted array", async () => {
|
||||
mock({
|
||||
"middleware.js": `export const config = {
|
||||
matcher: ['/pages/', '/(.*)'],
|
||||
};`,
|
||||
});
|
||||
|
||||
const result = await detectMiddlewareUsage("", false);
|
||||
expect(result.hasMiddleware).toEqual(true);
|
||||
if (!result.hasMiddleware) throw "Should have middleware";
|
||||
expect(result.middlewarePath).toEqual("middleware.js");
|
||||
expect(result.conflict).toEqual("likely");
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
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)/;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export const standardWatchFilePaths = [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
"pnpm-lock.yaml",
|
||||
];
|
||||
|
||||
export const standardWatchIgnoreRegex = /(node_modules)/;
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from "./cli/index";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
const main = async () => {
|
||||
await program.parseAsync();
|
||||
};
|
||||
|
||||
main().catch((err) => {
|
||||
if (err instanceof Error) {
|
||||
logger.error(err);
|
||||
} else {
|
||||
logger.error("An unknown error has occurred. Please open an issue on github with the below:");
|
||||
logger.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import { PostHog } from "posthog-node";
|
||||
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_9aSDbJCaDUMdZdHxxMPTvcj7A9fsl3mCgM1RBPmPsl7";
|
||||
|
||||
export class TelemetryClient {
|
||||
#client: PostHog;
|
||||
#sessionId: string;
|
||||
#version: string;
|
||||
|
||||
constructor() {
|
||||
this.#client = new PostHog(postHogApiKey, {
|
||||
host: "https://eu.posthog.com",
|
||||
flushAt: 1,
|
||||
});
|
||||
this.#sessionId = `cli-${nanoid()}`;
|
||||
this.#version = getVersion();
|
||||
}
|
||||
|
||||
identify(organizationId: string, projectId: string, userId?: string) {
|
||||
if (userId) {
|
||||
this.#client.alias({
|
||||
distinctId: userId,
|
||||
alias: this.#sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
this.#client.groupIdentify({
|
||||
groupType: "organization",
|
||||
groupKey: organizationId,
|
||||
});
|
||||
|
||||
this.#client.groupIdentify({
|
||||
groupType: "project",
|
||||
groupKey: projectId,
|
||||
});
|
||||
}
|
||||
|
||||
init = {
|
||||
started: (options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_started",
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
isTypescriptProject: (isTypescriptProject: boolean, options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_typescriptproject",
|
||||
properties: { ...this.#initProperties(options), isTypescriptProject },
|
||||
});
|
||||
},
|
||||
resolvedApiUrl: (apiUrl: string | undefined, options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_resolved_apiurl",
|
||||
properties: { ...this.#initProperties(options), apiUrl },
|
||||
});
|
||||
},
|
||||
resolvedApiKey: (options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_resolved_apikey",
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
resolvedEndpointSlug: (options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_resolved_endpoint_slug",
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
addedDependencies: (options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_added_dependencies",
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
install: (
|
||||
options: InitCommandOptions,
|
||||
framework: string,
|
||||
installOptions: ProjectInstallOptions
|
||||
) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
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) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_completed",
|
||||
properties: this.#initProperties(options),
|
||||
});
|
||||
},
|
||||
warning: (reason: string, options: InitCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_warning",
|
||||
properties: {
|
||||
...this.#initProperties(options),
|
||||
reason,
|
||||
},
|
||||
});
|
||||
},
|
||||
failed: (reason: string, options: InitCommandOptions, error?: unknown) => {
|
||||
const errorString = error instanceof Error ? error.message : String(error);
|
||||
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_init_failed",
|
||||
properties: {
|
||||
...this.#initProperties(options),
|
||||
reason,
|
||||
error: errorString,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
dev = {
|
||||
started: (path: string, options: Record<string, string | number | boolean>) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_dev_started",
|
||||
properties: { ...options, path },
|
||||
});
|
||||
},
|
||||
serverRunning: (path: string, options: DevCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_dev_server_running",
|
||||
properties: { ...options, path },
|
||||
});
|
||||
},
|
||||
tunnelRunning: (path: string, options: DevCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_dev_tunnel_running",
|
||||
properties: { ...options, path },
|
||||
});
|
||||
},
|
||||
connected: (path: string, options: DevCommandOptions) => {
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_dev_connected",
|
||||
properties: { ...options, path },
|
||||
});
|
||||
},
|
||||
failed: (
|
||||
reason: string,
|
||||
options: Record<string, string | number | boolean>,
|
||||
error?: unknown
|
||||
) => {
|
||||
const errorString = error instanceof Error ? error.message : String(error);
|
||||
|
||||
this.#client.capture({
|
||||
distinctId: this.#sessionId,
|
||||
event: "cli_dev_failed",
|
||||
properties: {
|
||||
...options,
|
||||
reason,
|
||||
error: errorString,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
#initProperties(options: InitCommandOptions) {
|
||||
return {
|
||||
version: this.#version,
|
||||
hadApiKey: options.apiKey !== undefined,
|
||||
triggerUrl: options.triggerUrl,
|
||||
endpointSlug: options.endpointSlug,
|
||||
apiUrl: options.apiUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const telemetryClient = new TelemetryClient();
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createAstroRoute } from "@trigger.dev/astro";
|
||||
//you may need to update this path to point at your trigger.ts file
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
//import your jobs
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
export const prerender = false;
|
||||
export const { POST } = createAstroRoute(client);
|
||||
@@ -1,44 +0,0 @@
|
||||
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) => {
|
||||
// Use a Task to generate a random number. Using a Tasks means it only runs once.
|
||||
const result = await io.runTask("generate-random-number", async () => {
|
||||
return {
|
||||
num: Math.floor(Math.random() * 10000),
|
||||
};
|
||||
});
|
||||
|
||||
// Use the random number in a joke and log it to the console.
|
||||
await io.logger.info(`Why was the number ${result.num} afraid of the number 7?`);
|
||||
|
||||
// Wait 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);
|
||||
|
||||
// Use a Task to display the answer. Tasks are important to use in all Jobs as they allow your Runs to resume again after e.g. a serverless function timeout. Learn more about Tasks in the docs: https://trigger.dev/docs/documentation/concepts/tasks
|
||||
await io.runTask(
|
||||
"task-example",
|
||||
async () => {
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
},
|
||||
{ name: `Answer: Because 7,8,9! And ${result.num} was next 🤦` }
|
||||
);
|
||||
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
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// export all your job files here
|
||||
|
||||
export * from "./example";
|
||||
@@ -1,7 +0,0 @@
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "${endpointSlug}",
|
||||
apiKey: import.meta.env.TRIGGER_API_KEY,
|
||||
apiUrl: import.meta.env.TRIGGER_API_URL,
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
# {{ packageName }}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { EventSpecification } from "@trigger.dev/sdk";
|
||||
import { CommentEvent, IssueEvent } from "./schemas";
|
||||
import { Get{{ identifier | capitalize }}Payload } from "./types";
|
||||
import {
|
||||
commentCreated,
|
||||
commentRemoved,
|
||||
commentUpdated,
|
||||
issueCreated,
|
||||
issueRemoved,
|
||||
issueUpdated,
|
||||
} from "./payload-examples";
|
||||
import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils";
|
||||
|
||||
export const onComment: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent>> = {
|
||||
name: "Comment",
|
||||
title: "On Comment",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
examples: [commentCreated, commentRemoved, commentUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onCommentProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onCommentCreated: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "create">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Created",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [commentCreated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "create">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentRemoved: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "remove">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Removed",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [commentRemoved],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "remove">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentUpdated: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "update">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Updated",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [commentUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "update">,
|
||||
runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
|
||||
export const onIssue: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent>> = {
|
||||
name: "Issue",
|
||||
title: "On Issue",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
examples: [issueCreated, issueRemoved, issueUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onIssueProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onIssueCreated: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "create">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Created",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [issueCreated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "create">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueRemoved: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "remove">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Removed",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [issueRemoved],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "remove">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueUpdated: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "update">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Updated",
|
||||
source: "{{ identifier }}.com",
|
||||
icon: "{{ identifier }}",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [issueUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "update">,
|
||||
runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
@@ -1,245 +0,0 @@
|
||||
import {
|
||||
TriggerIntegration,
|
||||
RunTaskOptions,
|
||||
IO,
|
||||
IOTask,
|
||||
IntegrationTaskKey,
|
||||
RunTaskErrorCallback,
|
||||
Json,
|
||||
retry,
|
||||
ConnectionAuth,
|
||||
Prettify,
|
||||
} from "@trigger.dev/sdk";
|
||||
import {{ identifier | capitalize }}Client from "{{ sdkPackage }}";
|
||||
|
||||
import * as events from "./events";
|
||||
import { {{ identifier | capitalize }}ReturnType, Serialized{{ identifier | capitalize }}Output } from "./types";
|
||||
import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks";
|
||||
import { Models } from "./models";
|
||||
|
||||
export type {{ identifier | capitalize }}IntegrationOptions = {
|
||||
id: string;
|
||||
{{ apiKeyPropertyName }}?: string;
|
||||
};
|
||||
|
||||
export type {{ identifier | capitalize }}RunTask = InstanceType<typeof {{ identifier | capitalize }}>["runTask"];
|
||||
|
||||
export class {{ identifier | capitalize }}{{ " " }} implements TriggerIntegration {
|
||||
private _options: {{ identifier | capitalize }}IntegrationOptions;
|
||||
private _client?: any;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(private options: {{ identifier | capitalize }}IntegrationOptions) {
|
||||
if (Object.keys(options).includes("{{ apiKeyPropertyName }}") && !options.{{ apiKeyPropertyName }}) {
|
||||
{# FIXME: For some reason the spaces after the variables vanish #}
|
||||
throw `Can't create {{ identifier | capitalize }} integration (${options.id}) as {{ apiKeyPropertyName }} was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
{% case authMethod %}
|
||||
{% when "api-key" %}
|
||||
return "LOCAL" as const;
|
||||
{% when "oauth" %}
|
||||
return "HOSTED" as const;
|
||||
{% when "both-methods" %}
|
||||
return this._options.{{ apiKeyPropertyName }} ? "LOCAL" : "HOSTED";
|
||||
{% endcase %}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "{{ identifier }}", name: "{{ identifier | capitalize }}" };
|
||||
}
|
||||
|
||||
get source() {
|
||||
return createWebhookEventSource(this);
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const {{ identifier }} = new {{ identifier | capitalize }}(this._options);
|
||||
{{ identifier }}._io = io;
|
||||
{{ identifier }}._connectionKey = connectionKey;
|
||||
{{ identifier }}._client = this.createClient(auth);
|
||||
return {{ identifier }};
|
||||
}
|
||||
|
||||
createClient(auth?: ConnectionAuth) {
|
||||
// oauth
|
||||
if (auth) {
|
||||
return new {{ identifier | capitalize }}Client({
|
||||
auth: auth.accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
// apiKey auth
|
||||
if (this._options.{{ apiKeyPropertyName }}) {
|
||||
return new {{ identifier | capitalize }}Client({
|
||||
apiKey: this._options.{{ apiKeyPropertyName }},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("No auth");
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: {{ identifier | capitalize }}Client, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "{{ identifier }}",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback ?? onError
|
||||
);
|
||||
}
|
||||
|
||||
// top-level task
|
||||
|
||||
request<T = any>(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
route: string | URL;
|
||||
options: Parameters<{{ identifier | capitalize }}Client["request"]>[1];
|
||||
}
|
||||
): {{ identifier | capitalize }}ReturnType<T> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const response = await client.request(params.route, params.options);
|
||||
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
name: "Send Request",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Route", text: params.route.toString() },
|
||||
...(params.options.method ? [{ label: "Method", text: params.options.method }] : []),
|
||||
],
|
||||
callback: { enabled: true },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// nested tasks
|
||||
|
||||
get models() {
|
||||
return new Models(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
// events
|
||||
|
||||
onComment(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onComment, params);
|
||||
}
|
||||
|
||||
onCommentCreated(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentCreated, params);
|
||||
}
|
||||
|
||||
onCommentRemoved(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentRemoved, params);
|
||||
}
|
||||
|
||||
onCommentUpdated(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentUpdated, params);
|
||||
}
|
||||
|
||||
// triggers (webhooks)
|
||||
|
||||
// private, just here to keep webhook logic in a separate file
|
||||
get #webhooks() {
|
||||
return new Webhooks(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
webhook = this.#webhooks.webhook;
|
||||
webhooks = this.#webhooks.webhooks;
|
||||
|
||||
createWebhook = this.#webhooks.createWebhook;
|
||||
deleteWebhook = this.#webhooks.deleteWebhook;
|
||||
updateWebhook = this.#webhooks.updateWebhook;
|
||||
}
|
||||
|
||||
class {{ identifier | capitalize }}ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly request: Request,
|
||||
readonly response: Response
|
||||
) {
|
||||
super(message);
|
||||
this.name = "{{ identifier | capitalize }}ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
function is{{ identifier | capitalize }}ApiError(error: unknown): error is {{ identifier | capitalize }}ApiError {
|
||||
if (typeof error !== "object" || error === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const apiError = error as {{ identifier | capitalize }}ApiError;
|
||||
|
||||
return (
|
||||
apiError.name === "{{ identifier | capitalize }}ApiError" &&
|
||||
apiError.request instanceof Request &&
|
||||
apiError.response instanceof Response
|
||||
);
|
||||
}
|
||||
|
||||
function shouldRetry(method: string, status: number) {
|
||||
return status === 429 || (method === "GET" && status >= 500);
|
||||
}
|
||||
|
||||
export function onError(error: unknown): ReturnType<RunTaskErrorCallback> {
|
||||
if (!is{{ identifier | capitalize }}ApiError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRetry(error.request.method, error.response.status)) {
|
||||
return {
|
||||
skipRetrying: true,
|
||||
};
|
||||
}
|
||||
|
||||
const rateLimitRemaining = error.response.headers.get("ratelimit-remaining");
|
||||
const rateLimitReset = error.response.headers.get("ratelimit-reset");
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
if (!Number.isNaN(resetDate.getTime())) {
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const serialize{{ identifier | capitalize }}Output = <T>(obj: T): Prettify<Serialized{{ identifier | capitalize }}Output<T>> => {
|
||||
return JSON.parse(JSON.stringify(obj), (key, value) => {
|
||||
if (typeof value === "function" || key.startsWith("_")) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Model, ModelVersion } from "{{ sdkPackage }}";
|
||||
|
||||
import { {{ identifier | capitalize }}RunTask } from "./index";
|
||||
import { modelProperties } from "./utils";
|
||||
import { {{ identifier | capitalize }}ReturnType } from "./types";
|
||||
|
||||
export class Models {
|
||||
constructor(private runTask: {{ identifier | capitalize }}RunTask) {}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): {{ identifier | capitalize }}ReturnType<Model> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.get(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "Get Model",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return new Versions(this.runTask);
|
||||
}
|
||||
}
|
||||
|
||||
class Versions {
|
||||
constructor(private runTask: {{ identifier | capitalize }}RunTask) {}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
}
|
||||
): {{ identifier | capitalize }}ReturnType<ModelVersion> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.get(params.model_owner, params.model_name, params.version_id);
|
||||
},
|
||||
{
|
||||
name: "Get Model Version",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): {{ identifier | capitalize }}ReturnType<ModelVersion[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.list(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "List Models",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "{{ packageName }}",
|
||||
"version": "{{ integrationVersion.version }}",
|
||||
"description": "Trigger.dev integration for {{ sdkPackage }}",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.d.ts",
|
||||
"dist/index.js.map"
|
||||
],
|
||||
"devDependencies": {
|
||||
{% if triggerMonorepoPath %}
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@trigger.dev/tsup": "workspace:*",
|
||||
{% endif %}
|
||||
"@types/node": "16.x",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsup": "7.1.x",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && npm run build:tsup",
|
||||
"build:tsup": "tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"{{ latestVersion.name }}": "^{{ latestVersion.version }}",
|
||||
"{{ sdkVersion.name }}": "{{ sdkVersion.version }}",
|
||||
"{{ integrationKitVersion.name }}": "{{ integrationKitVersion.version }}",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { EventSpecificationExample } from "@trigger.dev/sdk";
|
||||
|
||||
import CommentCreated from "./CommentCreated.json"
|
||||
import CommentRemoved from "./CommentRemoved.json"
|
||||
import CommentUpdated from "./CommentUpdated.json"
|
||||
import IssueCreated from "./IssueCreated.json"
|
||||
import IssueRemoved from "./IssueRemoved.json"
|
||||
import IssueUpdated from "./IssueUpdated.json"
|
||||
|
||||
export const commentCreated: EventSpecificationExample = {
|
||||
id: "CommentCreated",
|
||||
name: "Comment created",
|
||||
payload: CommentCreated,
|
||||
};
|
||||
export const commentRemoved: EventSpecificationExample = {
|
||||
id: "CommentRemoved",
|
||||
name: "Comment removed",
|
||||
payload: CommentRemoved,
|
||||
};
|
||||
export const commentUpdated: EventSpecificationExample = {
|
||||
id: "CommentUpdated",
|
||||
name: "Comment updated",
|
||||
payload: CommentUpdated,
|
||||
};
|
||||
|
||||
export const issueCreated: EventSpecificationExample = {
|
||||
id: "IssueCreated",
|
||||
name: "Issue created",
|
||||
payload: IssueCreated,
|
||||
};
|
||||
export const issueRemoved: EventSpecificationExample = {
|
||||
id: "IssueRemoved",
|
||||
name: "Issue removed",
|
||||
payload: IssueRemoved,
|
||||
};
|
||||
export const issueUpdated: EventSpecificationExample = {
|
||||
id: "IssueUpdated",
|
||||
name: "Issue updated",
|
||||
payload: IssueUpdated,
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const WebhookResourceTypeSchema = z.union([
|
||||
z.literal("Comment"),
|
||||
z.literal("Issue"),
|
||||
]);
|
||||
export type WebhookResourceType = z.infer<typeof WebhookResourceTypeSchema>;
|
||||
|
||||
export const WebhookActionTypeSchema = z.union([
|
||||
z.literal("create"),
|
||||
z.literal("remove"),
|
||||
z.literal("update"),
|
||||
]);
|
||||
export type WebhookActionType = z.infer<typeof WebhookActionTypeSchema>;
|
||||
|
||||
const IssueLabelDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
color: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
organizationId: z.string(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
teamId: z.string().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const IssueDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
|
||||
assigneeId: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
labelIds: z.array(z.string()),
|
||||
labels: z.array(IssueLabelDataSchema.pick({ id: true, color: true, name: true })),
|
||||
number: z.number(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
previousIdentifiers: z.array(z.string()),
|
||||
priority: z.number(),
|
||||
priorityLabel: z.string(),
|
||||
projectId: z.string().optional().nullable(),
|
||||
sortOrder: z.number(),
|
||||
team: z.object({ id: z.string(), key: z.string(), name: z.string() }),
|
||||
teamId: z.string(),
|
||||
title: z.string(),
|
||||
trashed: z.boolean().optional().nullable(),
|
||||
triagedAt: z.coerce.date().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const CommentDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
body: z.string(),
|
||||
botActorId: z.string().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
editedAt: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
issue: IssueDataSchema.pick({ id: true, title: true }),
|
||||
issueId: z.string(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
reactionData: z.array(z.object({}).passthrough()),
|
||||
updatedAt: z.coerce.date(),
|
||||
userId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export const WebhookPayloadBaseSchema = z.object({
|
||||
createdAt: z.coerce.date(),
|
||||
organizationId: z.string().optional().nullable(),
|
||||
url: z.string().url().optional().nullable(),
|
||||
webhookId: z.string(),
|
||||
webhookTimestamp: z.coerce.date(),
|
||||
});
|
||||
|
||||
const CREATE = z.literal("create");
|
||||
const REMOVE = z.literal("remove");
|
||||
const UPDATE = z.literal("update");
|
||||
|
||||
export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Comment"),
|
||||
data: CommentDataSchema,
|
||||
});
|
||||
export const CommentEventSchema = z.discriminatedUnion("action", [
|
||||
CommentEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
CommentEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
CommentEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: CommentDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type CommentEvent = z.infer<typeof CommentEventSchema>;
|
||||
|
||||
export const IssueEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Issue"),
|
||||
data: IssueDataSchema,
|
||||
});
|
||||
export const IssueEventSchema = z.discriminatedUnion("action", [
|
||||
IssueEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
IssueEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
IssueEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: IssueDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type IssueEvent = z.infer<typeof IssueEventSchema>;
|
||||
|
||||
export const WebhookPayloadSchema = z.union([
|
||||
CommentEventSchema,
|
||||
IssueEventSchema,
|
||||
]);
|
||||
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>;
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSources": false,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["dom", "dom.iterable", "es2019"],
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/integration.json",
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
name: "main",
|
||||
entry: ["./src/index.ts"],
|
||||
outDir: "./dist",
|
||||
platform: "node",
|
||||
format: ["cjs"],
|
||||
legacyOutput: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
dts: true,
|
||||
treeshake: {
|
||||
preset: "smallest",
|
||||
},
|
||||
esbuildPlugins: [],
|
||||
external: ["http", "https", "util", "events", "tty", "os", "timers"],
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig, deepMergeOptions, integrationOptions } from "@trigger.dev/tsup";
|
||||
|
||||
const options = deepMergeOptions(integrationOptions, {
|
||||
// extend base config here
|
||||
});
|
||||
|
||||
export default defineConfig(options);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Request } from "{{ sdkPackage }}";
|
||||
import { WebhookActionType, WebhookPayload } from "./schemas";
|
||||
|
||||
export type Get{{ identifier | capitalize }}Payload<
|
||||
TPayload extends WebhookPayload,
|
||||
TAction extends any = any,
|
||||
> = TAction extends WebhookActionType ? Extract<TPayload, { action: TAction }> : TPayload;
|
||||
|
||||
type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type Serialized{{ identifier | capitalize }}Output<T> = T extends object
|
||||
? T extends Array<infer U>
|
||||
? Array<Serialized{{ identifier | capitalize }}Output<U>>
|
||||
: { [K in keyof T as Exclude<K, FunctionKeys<T> | `_${string}`>]: Serialized{{ identifier | capitalize }}Output<T[K]> }
|
||||
: T;
|
||||
|
||||
export type {{ identifier | capitalize }}ReturnType<
|
||||
TPayload extends Omit<Request, "_request">,
|
||||
K extends unknown = unknown,
|
||||
> = Promise<
|
||||
Awaited<Serialized{{ identifier | capitalize }}Output<Awaited<K extends keyof TPayload ? TPayload[K] : TPayload>>>
|
||||
>;
|
||||
|
||||
export type AwaitNested<T extends object, K extends keyof T> = Omit<T, K> & {
|
||||
[key in K]: Awaited<T[K]>;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { CommentEvent, IssueEvent, WebhookPayload } from "./schemas";
|
||||
import { Get{{ identifier | capitalize }}Payload } from "./types";
|
||||
|
||||
export type QueryVariables = {
|
||||
after: string;
|
||||
before: string;
|
||||
first: number;
|
||||
includeArchived: boolean;
|
||||
last: number;
|
||||
};
|
||||
|
||||
export type Nullable<T> = Partial<{
|
||||
[K in keyof T]: T[K] | null;
|
||||
}>;
|
||||
|
||||
export const onCommentProperties = (payload: Get{{ identifier | capitalize }}Payload<CommentEvent>) => {
|
||||
return [
|
||||
{ label: "Comment ID", text: payload.data.id },
|
||||
{ label: "Issue ID", text: payload.data.issueId },
|
||||
{ label: "Issue Title", text: payload.data.issue.title, url: payload.url ?? undefined },
|
||||
];
|
||||
};
|
||||
|
||||
export const onIssueProperties = (payload: Get{{ identifier | capitalize }}Payload<IssueEvent>) => {
|
||||
return [
|
||||
{ label: "Issue ID", text: payload.data.id },
|
||||
{
|
||||
label: "Issue",
|
||||
text: `[${payload.data.team.key}-${payload.data.number}] ${payload.data.title}`,
|
||||
url: payload.url ?? undefined,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const queryProperties = (query: Nullable<QueryVariables>) => {
|
||||
return [
|
||||
...(query.after ? [{ label: "After", text: query.after }] : []),
|
||||
...(query.before ? [{ label: "Before", text: query.before }] : []),
|
||||
...(query.first ? [{ label: "First", text: String(query.first) }] : []),
|
||||
...(query.last ? [{ label: "Last", text: String(query.last) }] : []),
|
||||
...(query.includeArchived
|
||||
? [{ label: "Include archived", text: String(query.includeArchived) }]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
export const updatedFromProperties = (payload: WebhookPayload) => {
|
||||
if (payload.action !== "update") return [];
|
||||
return [
|
||||
{
|
||||
label: "Updated Keys",
|
||||
text: Object.keys(payload.updatedFrom)
|
||||
.filter((key) => !["editedAt", "updatedAt"].includes(key))
|
||||
.join(", "),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const modelProperties = (
|
||||
params: Partial<{
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
destination: string;
|
||||
}>
|
||||
) => {
|
||||
return [
|
||||
...(params.model_owner ? [{ label: "Model Owner", text: params.model_owner }] : []),
|
||||
...(params.model_name ? [{ label: "Model Name", text: params.model_name }] : []),
|
||||
...(params.version_id ? [{ label: "Model Version", text: params.version_id }] : []),
|
||||
...(params.destination ? [{ label: "Destination Model", text: params.destination }] : []),
|
||||
];
|
||||
};
|
||||
|
||||
export const streamingProperty = (params: { stream?: boolean }) => {
|
||||
return [{ label: "Streaming Enabled", text: String(!!params.stream) }];
|
||||
};
|
||||
@@ -1,296 +0,0 @@
|
||||
import {
|
||||
EventFilter,
|
||||
ExternalSource,
|
||||
ExternalSourceTrigger,
|
||||
HandlerEvent,
|
||||
IntegrationTaskKey,
|
||||
Logger,
|
||||
} from "@trigger.dev/sdk";
|
||||
import {
|
||||
Document,
|
||||
{{ identifier | capitalize }}Webhooks,
|
||||
WebhookPayload,
|
||||
DeletePayload,
|
||||
Webhook,
|
||||
} from "{{ sdkPackage }}";
|
||||
import { z } from "zod";
|
||||
import * as events from "./events";
|
||||
import { {{ identifier | capitalize }}, {{ identifier | capitalize }}RunTask, serialize{{ identifier | capitalize }}Output } from "./index";
|
||||
import { WebhookPayloadSchema } from "./schemas";
|
||||
import { {{ identifier | capitalize }}ReturnType } from "./types";
|
||||
import { queryProperties } from "./utils";
|
||||
|
||||
export class Webhooks {
|
||||
runTask: {{ identifier | capitalize }}RunTask;
|
||||
|
||||
constructor(runTask: {{ identifier | capitalize }}RunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
webhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<Webhook> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serialize{{ identifier | capitalize }}Output(await client.webhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Get Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
webhooks(key: IntegrationTaskKey, params?: Document.WebhooksQueryVariables): {{ identifier | capitalize }}ReturnType<Webhook[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
let connections = await client.webhooks(params);
|
||||
const hooks = connections.nodes;
|
||||
while (connections.pageInfo.hasNextPage) {
|
||||
connections = await connections.fetchNext();
|
||||
hooks.push(...connections.nodes);
|
||||
}
|
||||
return serialize{{ identifier | capitalize }}Output(hooks);
|
||||
},
|
||||
{
|
||||
name: "List Webhooks",
|
||||
params,
|
||||
properties: queryProperties(params ?? {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: Document.WebhookCreateInput
|
||||
): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId });
|
||||
return serialize{{ identifier | capitalize }}Output({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook URL", text: params.url },
|
||||
{ label: "Resource Types", text: params.resourceTypes.join(", ") },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteWebhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<DeletePayload> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serialize{{ identifier | capitalize }}Output(await client.deleteWebhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Delete Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string; input: Document.WebhookUpdateInput }
|
||||
): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const payload = await client.updateWebhook(params.id, params.input);
|
||||
return serialize{{ identifier | capitalize }}Output({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Update Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook ID", text: params.id },
|
||||
...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []),
|
||||
...(params.input.resourceTypes
|
||||
? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type {{ identifier | capitalize }}Events = (typeof events)[keyof typeof events];
|
||||
|
||||
export type TriggerParams = {
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
type CreateTriggersResult<TEventSpecification extends {{ identifier | capitalize }}Events> = ExternalSourceTrigger<
|
||||
TEventSpecification,
|
||||
ReturnType<typeof createWebhookEventSource>
|
||||
>;
|
||||
|
||||
export function createTrigger<TEventSpecification extends {{ identifier | capitalize }}Events>(
|
||||
source: ReturnType<typeof createWebhookEventSource>,
|
||||
event: TEventSpecification,
|
||||
params: TriggerParams
|
||||
): CreateTriggersResult<TEventSpecification> {
|
||||
return new ExternalSourceTrigger({
|
||||
event,
|
||||
params,
|
||||
source,
|
||||
options: {},
|
||||
});
|
||||
}
|
||||
|
||||
const WebhookRegistrationDataSchema = z.object({
|
||||
success: z.literal(true),
|
||||
webhook: z.object({
|
||||
id: z.string(),
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export function createWebhookEventSource(
|
||||
integration: {{ identifier | capitalize }}
|
||||
): ExternalSource<{{ identifier | capitalize }}, TriggerParams, "HTTP", {}> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "{{ identifier }}.webhook",
|
||||
schema: z.object({
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
version: "0.1.0",
|
||||
integration,
|
||||
key: (params) => `${params.teamId ? params.teamId : "all"}`,
|
||||
handler: webhookHandler,
|
||||
register: async (event, io, ctx) => {
|
||||
const { params, source: httpSource, options } = event;
|
||||
|
||||
// (key-specific) stored data, undefined if not registered yet
|
||||
const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data);
|
||||
|
||||
// set of events to register
|
||||
const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing]));
|
||||
const registeredOptions = {
|
||||
event: allEvents,
|
||||
};
|
||||
|
||||
// easily identify webhooks on {{ identifier }}
|
||||
const label = `trigger.${params.teamId ? params.teamId : "all"}`;
|
||||
|
||||
if (httpSource.active && webhookData.success) {
|
||||
const hasMissingOptions = Object.values(options).some(
|
||||
(option) => option.missing.length > 0
|
||||
);
|
||||
if (!hasMissingOptions) return;
|
||||
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: webhookData.data.webhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// check for existing hooks that match url
|
||||
const listResponse = await io.integration.webhooks("list-webhooks");
|
||||
const existingWebhook = listResponse.find((w) => w.url === httpSource.url);
|
||||
|
||||
if (existingWebhook) {
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: existingWebhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const createPayload = await io.integration.createWebhook("create-webhook", {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
teamId: params.teamId,
|
||||
url: httpSource.url,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(createPayload),
|
||||
secret: (await createPayload.webhook)?.secret,
|
||||
options: registeredOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: {{ identifier | capitalize }}) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] Handling webhook payload");
|
||||
|
||||
const { rawEvent: request, source } = event;
|
||||
|
||||
const payloadUuid = request.headers.get("{{ identifier | capitalize }}-Delivery");
|
||||
const payloadEvent = request.headers.get("{{ identifier | capitalize }}-Event");
|
||||
|
||||
if (!payloadUuid || !payloadEvent) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] Missing required {{ identifier | capitalize }} headers");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] No body found");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
const signature = request.headers.get("WEBHOOK_SIGNATURE_HEADER");
|
||||
|
||||
if (!signature) {
|
||||
logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, no signature found");
|
||||
throw Error("[@trigger.dev/{{ identifier }}] No signature found");
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
const body = JSON.parse(rawBody);
|
||||
const webhookHelper = new {{ identifier | capitalize }}Webhooks(source.secret);
|
||||
|
||||
if (!webhookHelper.verify(Buffer.from(rawBody), signature)) {
|
||||
logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, they don't match");
|
||||
throw Error("[@trigger.dev/{{ identifier }}] Invalid signature");
|
||||
}
|
||||
|
||||
const webhookPayload = WebhookPayloadSchema.parse(body);
|
||||
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
id: payloadUuid,
|
||||
name: payloadEvent,
|
||||
source: "{{ identifier }}.app",
|
||||
payload: webhookPayload,
|
||||
context: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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);
|
||||
|
||||
//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration
|
||||
//export const maxDuration = 60;
|
||||
@@ -1,44 +0,0 @@
|
||||
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) => {
|
||||
// Use a Task to generate a random number. Using a Tasks means it only runs once.
|
||||
const result = await io.runTask("generate-random-number", async () => {
|
||||
return {
|
||||
num: Math.floor(Math.random() * 10000),
|
||||
};
|
||||
});
|
||||
|
||||
// Use the random number in a joke and log it to the console.
|
||||
await io.logger.info(`Why was the number ${result.num} afraid of the number 7?`);
|
||||
|
||||
// Wait 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);
|
||||
|
||||
// Use a Task to display the answer. Tasks are important to use in all Jobs as they allow your Runs to resume again after e.g. a serverless function timeout. Learn more about Tasks in the docs: https://trigger.dev/docs/documentation/concepts/tasks
|
||||
await io.runTask(
|
||||
"task-example",
|
||||
async () => {
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
},
|
||||
{ name: `Answer: Because 7,8,9! And ${result.num} was next 🤦` }
|
||||
);
|
||||
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
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// export all your job files here
|
||||
|
||||
export * from "./examples";
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createPagesRoute } from "@trigger.dev/nextjs";
|
||||
import { client } from "${routePathPrefix}trigger";
|
||||
|
||||
import "${routePathPrefix}jobs";
|
||||
|
||||
//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration
|
||||
//export const config = {
|
||||
// maxDuration: 60,
|
||||
//};
|
||||
|
||||
//this route is used to send and receive data with Trigger.dev
|
||||
const { handler, config } = createPagesRoute(client);
|
||||
export { config };
|
||||
|
||||
export default handler;
|
||||
@@ -1,7 +0,0 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
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);
|
||||
@@ -1,44 +0,0 @@
|
||||
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) => {
|
||||
// Use a Task to generate a random number. Using a Tasks means it only runs once.
|
||||
const result = await io.runTask("generate-random-number", async () => {
|
||||
return {
|
||||
num: Math.floor(Math.random() * 10000),
|
||||
};
|
||||
});
|
||||
|
||||
// Use the random number in a joke and log it to the console.
|
||||
await io.logger.info(`Why was the number ${result.num} afraid of the number 7?`);
|
||||
|
||||
// Wait 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);
|
||||
|
||||
// Use a Task to display the answer. Tasks are important to use in all Jobs as they allow your Runs to resume again after e.g. a serverless function timeout. Learn more about Tasks in the docs: https://trigger.dev/docs/documentation/concepts/tasks
|
||||
await io.runTask(
|
||||
"task-example",
|
||||
async () => {
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
},
|
||||
{ name: `Answer: Because 7,8,9! And ${result.num} was next 🤦` }
|
||||
);
|
||||
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
|
||||
},
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export type TaskMetadata = {
|
||||
id: string;
|
||||
exportName: string;
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { execa } from "execa";
|
||||
import ora, { type Ora } from "ora";
|
||||
import pathModule from "path";
|
||||
import { getUserPackageManager, type PackageManager } from "./getUserPkgManager";
|
||||
import fs from "fs/promises";
|
||||
import fetch from "./fetchUseProxy";
|
||||
import { z } from "zod";
|
||||
|
||||
export type InstallPackage = {
|
||||
name: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type InstalledPackage = {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export async function addDependencies(projectDir: string, packages: Array<InstallPackage>) {
|
||||
const pkgManager = await getUserPackageManager(projectDir);
|
||||
|
||||
const spinner = ora("Adding @trigger.dev dependencies to package.json...").start();
|
||||
|
||||
const installedPackages = await addDependenciesToPackageJson(projectDir, packages);
|
||||
|
||||
spinner.succeed(
|
||||
chalk.green(
|
||||
`Successfully added dependencies to package.json: ${installedPackages
|
||||
.map((pkg) => `${pkg.name}@${pkg.version}`)
|
||||
.join(", ")}`
|
||||
)
|
||||
);
|
||||
|
||||
const installSpinner = await runInstallCommand(pkgManager, projectDir);
|
||||
|
||||
(installSpinner || ora()).stop();
|
||||
}
|
||||
|
||||
async function addDependenciesToPackageJson(
|
||||
projectDir: string,
|
||||
packages: Array<InstallPackage>
|
||||
): Promise<Array<InstalledPackage>> {
|
||||
const pkgJsonPath = pathModule.join(projectDir, "package.json");
|
||||
const pkgBuffer = await fs.readFile(pkgJsonPath);
|
||||
const pkgJson = JSON.parse(pkgBuffer.toString());
|
||||
|
||||
const packagesToInstall = await getLatestPackageVersions(packages);
|
||||
|
||||
// Add the dependencies to the package.json file
|
||||
pkgJson.dependencies = {
|
||||
...pkgJson.dependencies,
|
||||
...Object.fromEntries(packagesToInstall.map((pkg) => [pkg.name, `^${pkg.version}`])),
|
||||
};
|
||||
|
||||
// Write the updated package.json file
|
||||
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
||||
|
||||
return packagesToInstall;
|
||||
}
|
||||
|
||||
const PackageSchema = z.object({
|
||||
name: z.string(),
|
||||
"dist-tags": z.record(z.string()),
|
||||
});
|
||||
|
||||
export async function getLatestPackageVersion(
|
||||
packageName: string,
|
||||
tag: string
|
||||
): Promise<InstalledPackage | undefined> {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
const parsedBody = PackageSchema.safeParse(body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersion = parsedBody.data["dist-tags"][tag];
|
||||
|
||||
if (!latestVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: packageName,
|
||||
version: latestVersion,
|
||||
};
|
||||
}
|
||||
|
||||
async function getLatestPackageVersions(
|
||||
packages: Array<InstallPackage>
|
||||
): Promise<Array<InstalledPackage>> {
|
||||
const latestPackageVersions = await Promise.all(
|
||||
packages.map((pkg) => getLatestPackageVersion(pkg.name, pkg.tag))
|
||||
);
|
||||
|
||||
return latestPackageVersions.filter(Boolean) as Array<InstalledPackage>;
|
||||
}
|
||||
|
||||
async function runInstallCommand(
|
||||
pkgManager: PackageManager,
|
||||
projectDir: string
|
||||
): Promise<Ora | null> {
|
||||
switch (pkgManager) {
|
||||
// When using npm, inherit the stderr stream so that the progress bar is shown
|
||||
case "npm":
|
||||
await execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
return null;
|
||||
// When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
|
||||
case "pnpm":
|
||||
const pnpmSpinner = ora("Running pnpm install...").start();
|
||||
const pnpmSubprocess = execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
pnpmSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (text.includes("Progress")) {
|
||||
pnpmSpinner.text = text.includes("|") ? text.split(" | ")[1] ?? "" : text;
|
||||
}
|
||||
});
|
||||
pnpmSubprocess.on("error", (e) => rej(e));
|
||||
pnpmSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return pnpmSpinner;
|
||||
case "yarn":
|
||||
const yarnSpinner = ora("Running yarn...").start();
|
||||
const yarnSubprocess = execa(pkgManager, [], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
yarnSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
yarnSpinner.text = data.toString();
|
||||
});
|
||||
yarnSubprocess.on("error", (e) => rej(e));
|
||||
yarnSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return yarnSpinner;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
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,61 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import { Liquid } from "liquidjs";
|
||||
import path from "path";
|
||||
|
||||
import { pathExists } from "./fileSystem";
|
||||
import { templatesPath } from "../paths";
|
||||
|
||||
type Result =
|
||||
| {
|
||||
success: true;
|
||||
alreadyExisted: boolean;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
const templatesDir = path.join(templatesPath(), "integration");
|
||||
|
||||
const liquid = new Liquid({
|
||||
root: templatesDir,
|
||||
trimTagRight: true,
|
||||
trimOutputRight: true,
|
||||
});
|
||||
|
||||
export async function createIntegrationFileFromTemplate(params: {
|
||||
relativeTemplatePath: string;
|
||||
variables?: Record<string, any>;
|
||||
outputPath: string;
|
||||
}): Promise<Result> {
|
||||
if (await pathExists(params.outputPath)) {
|
||||
return {
|
||||
success: true,
|
||||
alreadyExisted: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await liquid.renderFile(params.relativeTemplatePath, params.variables);
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
export async function setPublicApiKeyEnvironmentVariable(
|
||||
dir: string,
|
||||
fileName: string,
|
||||
varName: string | undefined,
|
||||
publicApiKey: string
|
||||
) {
|
||||
await setEnvironmentVariable(
|
||||
dir,
|
||||
fileName,
|
||||
varName ?? "TRIGGER_PUBLIC_API_KEY",
|
||||
publicApiKey,
|
||||
true,
|
||||
renderApiKey
|
||||
);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import node_fetch, { RequestInfo as _RequestInfo, RequestInit as _RequestInit } from "node-fetch";
|
||||
import { ProxyAgent } from "proxy-agent";
|
||||
|
||||
export type RequestInfo = _RequestInfo;
|
||||
export type RequestInit = _RequestInit;
|
||||
|
||||
export default function fetch(url: RequestInfo, init?: RequestInit) {
|
||||
const fetchInit: RequestInit = { ...init };
|
||||
|
||||
// If agent is not specified, specify proxy-agent and use environment variables such as HTTPS_PROXY.
|
||||
if (!fetchInit.agent) {
|
||||
fetchInit.agent = new ProxyAgent();
|
||||
}
|
||||
|
||||
return node_fetch(url, fetchInit);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import fsModule, { writeFile } from "fs/promises";
|
||||
import fsSync from "fs";
|
||||
import pathModule from "path";
|
||||
|
||||
// Creates a file at the given path, if the directory doesn't exist it will be created
|
||||
export async function createFile(path: string, contents: string): Promise<string> {
|
||||
await fsModule.mkdir(pathModule.dirname(path), { recursive: true });
|
||||
await fsModule.writeFile(path, contents);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fsModule.access(path);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function someFileExists(directory: string, filenames: string[]): Promise<boolean> {
|
||||
for (let index = 0; index < filenames.length; index++) {
|
||||
const filename = filenames[index];
|
||||
if (!filename) continue;
|
||||
|
||||
const path = pathModule.join(directory, filename);
|
||||
if (await pathExists(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function removeFile(path: string) {
|
||||
await fsModule.unlink(path);
|
||||
}
|
||||
|
||||
export async function readFile(path: string) {
|
||||
return await fsModule.readFile(path, "utf8");
|
||||
}
|
||||
|
||||
export async function readJSONFile(path: string) {
|
||||
const fileContents = await fsModule.readFile(path, "utf8");
|
||||
|
||||
return JSON.parse(fileContents);
|
||||
}
|
||||
|
||||
export async function writeJSONFile(path: string, json: any) {
|
||||
await writeFile(path, JSON.stringify(json, null, 2));
|
||||
}
|
||||
|
||||
export function readJSONFileSync(path: string) {
|
||||
const fileContents = fsSync.readFileSync(path, "utf8");
|
||||
|
||||
return JSON.parse(fileContents);
|
||||
}
|
||||
@@ -1,783 +0,0 @@
|
||||
import { OpenAI } from "openai";
|
||||
import { z } from "zod";
|
||||
|
||||
function createExampleResponse(args: {
|
||||
files: Record<string, string>;
|
||||
packageName: string;
|
||||
sdkPackage: string;
|
||||
}) {
|
||||
return `
|
||||
Here is an example of what the files should look like for the package "${
|
||||
args.packageName
|
||||
}" using the SDK package "${args.sdkPackage}".
|
||||
|
||||
(Note that these files are formatted as a pair of JSON key/values, where the key is the file name and the value is the file contents.)
|
||||
|
||||
${JSON.stringify(args.files, null, 2)}
|
||||
`;
|
||||
}
|
||||
|
||||
function createPrompt(packageName: string, sdkPackage: string, extraInfo?: string) {
|
||||
return `'I\'m wanting to know what the minimal starting point index.ts, types.ts, and tasks.ts files should look like for the package "${packageName}" using the SDK package "${sdkPackage}". ${
|
||||
extraInfo ?? ""
|
||||
}`;
|
||||
}
|
||||
|
||||
function createExampleMessages() {
|
||||
return [
|
||||
{
|
||||
role: "user",
|
||||
content: createPrompt(
|
||||
"@trigger.dev.slack",
|
||||
"@slack/web-api",
|
||||
'Note that the only auth method support for slack is OAuth2 so so authSource() should be "HOSTED"'
|
||||
),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: createExampleResponse({
|
||||
packageName: "@trigger.dev/slack",
|
||||
sdkPackage: "@slack/web-api",
|
||||
files: {
|
||||
"index.ts": `
|
||||
import type {
|
||||
Block,
|
||||
ChatPostMessageResponse,
|
||||
KnownBlock,
|
||||
MessageAttachment,
|
||||
MessageMetadata,
|
||||
WebAPIPlatformError,
|
||||
} from "@slack/web-api";
|
||||
import { WebClient } from "@slack/web-api";
|
||||
import {
|
||||
retry,
|
||||
type ConnectionAuth,
|
||||
type IO,
|
||||
type IOTask,
|
||||
type IntegrationTaskKey,
|
||||
type Json,
|
||||
type RunTaskErrorCallback,
|
||||
type RunTaskOptions,
|
||||
type TriggerIntegration,
|
||||
} from "@trigger.dev/sdk";
|
||||
|
||||
export type SlackIntegrationOptions = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type ConversationsJoinResponse = Awaited<ReturnType<SlackClientType["conversations"]["join"]>>;
|
||||
|
||||
type SlackClientType = InstanceType<typeof WebClient>;
|
||||
|
||||
export type ChatPostMessageArguments = {
|
||||
channel: string;
|
||||
text?: string;
|
||||
as_user?: boolean;
|
||||
attachments?: MessageAttachment[];
|
||||
blocks?: (KnownBlock | Block)[];
|
||||
icon_emoji?: string;
|
||||
icon_url?: string;
|
||||
metadata?: MessageMetadata;
|
||||
link_names?: boolean;
|
||||
mrkdwn?: boolean;
|
||||
parse?: "full" | "none";
|
||||
reply_broadcast?: boolean;
|
||||
thread_ts?: string;
|
||||
unfurl_links?: boolean;
|
||||
unfurl_media?: boolean;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export class Slack implements TriggerIntegration {
|
||||
private _options: SlackIntegrationOptions;
|
||||
private _client?: WebClient;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(private options: SlackIntegrationOptions) {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
return "HOSTED" as const;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "slack", name: "Slack.com" };
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const slack = new Slack(this._options);
|
||||
slack._io = io;
|
||||
slack._connectionKey = connectionKey;
|
||||
if (!auth) {
|
||||
throw new Error("No auth");
|
||||
}
|
||||
slack._client = new WebClient(auth.accessToken);
|
||||
return slack;
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: WebClient, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "slack",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
|
||||
postMessage(
|
||||
key: IntegrationTaskKey,
|
||||
params: ChatPostMessageArguments
|
||||
): Promise<ChatPostMessageResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
try {
|
||||
return client.chat.postMessage(params);
|
||||
} catch (error) {
|
||||
if (isPlatformError(error)) {
|
||||
if (error.data.error === "not_in_channel") {
|
||||
const joinResponse = await this.joinConversation(\`Join \${params.channel}\`, {
|
||||
channel: params.channel,
|
||||
});
|
||||
|
||||
if (joinResponse.ok) {
|
||||
return client.chat.postMessage(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Post Message",
|
||||
params,
|
||||
icon: "slack",
|
||||
properties: [
|
||||
{
|
||||
label: "Channel ID",
|
||||
text: params.channel,
|
||||
},
|
||||
...(params.text ? [{ label: "Message", text: params.text }] : []),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
joinConversation(
|
||||
key: IntegrationTaskKey,
|
||||
params: { channel: string }
|
||||
): Promise<ConversationsJoinResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
return client.conversations.join(params);
|
||||
},
|
||||
{
|
||||
name: "Join Channel",
|
||||
params,
|
||||
icon: "slack",
|
||||
properties: [
|
||||
{
|
||||
label: "Channel ID",
|
||||
text: params.channel,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isPlatformError(error: unknown): error is WebAPIPlatformError {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"code" in error &&
|
||||
typeof error.code === "string" &&
|
||||
error.code === "slack_webapi_platform_error"
|
||||
);
|
||||
}
|
||||
`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: createPrompt(
|
||||
"@trigger.dev/typeform",
|
||||
"@typeform/api-client",
|
||||
'Note that the only auth method support for typeform is API Key so authSource() should be "LOCAL"'
|
||||
),
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: createExampleResponse({
|
||||
packageName: "@trigger.dev/typeform",
|
||||
sdkPackage: "@typeform/api-client",
|
||||
files: {
|
||||
"index.ts": `
|
||||
import { safeParseBody } from "@trigger.dev/integration-kit";
|
||||
import {
|
||||
ConnectionAuth,
|
||||
EventSpecification,
|
||||
ExternalSource,
|
||||
ExternalSourceTrigger,
|
||||
HandlerEvent,
|
||||
IO,
|
||||
IOTask,
|
||||
IntegrationTaskKey,
|
||||
Json,
|
||||
Logger,
|
||||
RunTaskErrorCallback,
|
||||
RunTaskOptions,
|
||||
TriggerIntegration,
|
||||
retry,
|
||||
} from "@trigger.dev/sdk";
|
||||
import { createClient } from "@typeform/api-client";
|
||||
import { createHmac } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { Forms } from "./forms";
|
||||
import { Responses } from "./responses";
|
||||
import {
|
||||
FormResponseEvent,
|
||||
GetWebhookResponse,
|
||||
TypeformIntegrationOptions,
|
||||
TypeformSDK,
|
||||
} from "./types";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
type TypeformSource = ReturnType<typeof createWebhookEventSource>;
|
||||
type TypeformTrigger = ReturnType<typeof createWebhookEventTrigger>;
|
||||
export type TypeformRunTask = InstanceType<typeof Typeform>["runTask"];
|
||||
|
||||
export class Typeform implements TriggerIntegration {
|
||||
private _options: TypeformIntegrationOptions;
|
||||
private _client?: TypeformSDK;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(private options: TypeformIntegrationOptions) {
|
||||
if (Object.keys(options).includes("token") && !options.token) {
|
||||
throw \`Can't create Typeform integration (\${options.id}) as token was undefined\`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
return "LOCAL" as const;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "typeform", name: "Typeform" };
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const typeform = new Typeform(this._options);
|
||||
typeform._io = io;
|
||||
typeform._connectionKey = connectionKey;
|
||||
typeform._client = createClient({ token: this._options.token });
|
||||
return typeform;
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: TypeformSDK, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "typeform",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
|
||||
get forms() {
|
||||
return new Forms(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get responses() {
|
||||
return new Responses(this.runTask.bind(this));
|
||||
}
|
||||
}
|
||||
`,
|
||||
"forms.ts": `
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { GetFormParams, GetFormResponse, ListFormsParams, TypeformRunTask } from ".";
|
||||
import { Typeform } from "@typeform/api-client";
|
||||
|
||||
export class Forms {
|
||||
runTask: TypeformRunTask;
|
||||
|
||||
constructor(runTask: TypeformRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey, params: ListFormsParams): Promise<Typeform.API.Forms.List> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.forms.list(params ?? {});
|
||||
},
|
||||
{
|
||||
name: "List Forms",
|
||||
params,
|
||||
properties: [
|
||||
...(params?.workspaceId ? [{ label: "Workspace ID", text: params.workspaceId }] : []),
|
||||
...(params?.search ? [{ label: "Search", text: params.search }] : []),
|
||||
...(params?.page ? [{ label: "Page", text: String(params.page) }] : []),
|
||||
...(params?.pageSize ? [{ label: "Page Size", text: String(params.pageSize) }] : []),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get(key: IntegrationTaskKey, params: GetFormParams): Promise<GetFormResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.forms.get(params);
|
||||
},
|
||||
{
|
||||
name: "Get Form",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Form ID",
|
||||
text: params.uid,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
`,
|
||||
"responses.ts": `
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import {
|
||||
GetAllResponsesParams,
|
||||
GetAllResponsesResponse,
|
||||
ListResponsesParams,
|
||||
ListResponsesResponse,
|
||||
TypeformRunTask,
|
||||
} from ".";
|
||||
|
||||
export class Responses {
|
||||
runTask: TypeformRunTask;
|
||||
|
||||
constructor(runTask: TypeformRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
list(key: IntegrationTaskKey, params: ListResponsesParams): Promise<ListResponsesResponse> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
return client.responses.list(params);
|
||||
},
|
||||
{
|
||||
name: "List Responses",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Form ID",
|
||||
text: params.uid,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
all(key: IntegrationTaskKey, params: GetAllResponsesParams): Promise<GetAllResponsesResponse> {
|
||||
const pageSize = 50;
|
||||
|
||||
const listResponsesForPage = (before?: string) => {
|
||||
const pageParams = {
|
||||
...params,
|
||||
submitted_at: "desc",
|
||||
before,
|
||||
pageSize: pageSize,
|
||||
};
|
||||
|
||||
return this.list(\`page\${before ? \`-before-\${before}\` : ""}\`, pageParams);
|
||||
};
|
||||
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
// We're going to create a subtask for each page of responses
|
||||
const firstPage = await listResponsesForPage();
|
||||
let token = firstPage.items[firstPage.items.length - 1].token;
|
||||
|
||||
const totalPages = Math.ceil(firstPage.total_items / pageSize);
|
||||
const allResponses = firstPage.items;
|
||||
|
||||
for (let i = 1; i < totalPages; i++) {
|
||||
const page = await listResponsesForPage(token);
|
||||
token = page.items[page.items.length - 1].token;
|
||||
allResponses.push(...page.items);
|
||||
}
|
||||
|
||||
return allResponses;
|
||||
},
|
||||
{
|
||||
name: "Get All Responses",
|
||||
params,
|
||||
properties: [
|
||||
{
|
||||
label: "Form ID",
|
||||
text: params.uid,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
`,
|
||||
"types.ts": `
|
||||
import { Prettify } from "@trigger.dev/integration-kit";
|
||||
import { Typeform, createClient } from "@typeform/api-client";
|
||||
|
||||
export type TypeformIntegrationOptions = {
|
||||
id: string;
|
||||
token: string;
|
||||
apiBaseUrl?: string;
|
||||
};
|
||||
|
||||
export type TypeformSDK = ReturnType<typeof createClient>;
|
||||
|
||||
export type ListFormsParams = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
workspaceId?: string;
|
||||
} | void;
|
||||
|
||||
export type ListFormsResponse = Prettify<Typeform.API.Forms.List>;
|
||||
|
||||
export type ListResponsesParams = {
|
||||
uid: string;
|
||||
pageSize?: number;
|
||||
since?: string;
|
||||
until?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
ids?: string | string[];
|
||||
completed?: boolean;
|
||||
sort?: string;
|
||||
query?: string;
|
||||
fields?: string | string[];
|
||||
};
|
||||
|
||||
//fix because the Typeform SDK doesn't have the \`token\` property on an item…
|
||||
type ResponseListItem = Prettify<Typeform.API.Responses.List["items"][number]> & {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type ListResponsesResponse = Prettify<
|
||||
Omit<Typeform.API.Responses.List, "items"> & { items: ResponseListItem[] }
|
||||
>;
|
||||
|
||||
export type GetFormParams = {
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export type GetFormResponse = Prettify<Typeform.Form>;
|
||||
|
||||
export type GetAllResponsesParams = Prettify<Omit<ListResponsesParams, "pageSize">>;
|
||||
|
||||
export type GetAllResponsesResponse = Prettify<Typeform.API.Responses.List["items"]>;
|
||||
|
||||
export type GetWebhookResponse = Prettify<Typeform.Webhook>;
|
||||
|
||||
export type ListWebhooksParams = {
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export type ListWebhooksResponse = Prettify<Typeform.API.Webhooks.List>;
|
||||
|
||||
export type CreateWebhookParams = {
|
||||
uid: string;
|
||||
tag: string;
|
||||
url: string;
|
||||
enabled?: boolean;
|
||||
secret?: string;
|
||||
verifySSL?: boolean;
|
||||
};
|
||||
|
||||
export type UpdateWebhookParams = {
|
||||
uid: string;
|
||||
tag: string;
|
||||
url: string;
|
||||
enabled?: boolean;
|
||||
secret?: string;
|
||||
verifySSL?: boolean;
|
||||
};
|
||||
|
||||
export type GetWebhookParams = {
|
||||
uid: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type DeleteWebhookParams = {
|
||||
uid: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type DeleteWebhookResponse = null;
|
||||
|
||||
export type FormResponseEvent = {
|
||||
event_id: string;
|
||||
event_type: "form_response";
|
||||
form_response: {
|
||||
form_id: string;
|
||||
token: string;
|
||||
landed_at: string;
|
||||
submitted_at: string;
|
||||
calculated: {
|
||||
score: number;
|
||||
};
|
||||
variables: Array<
|
||||
| {
|
||||
key: string;
|
||||
type: "number";
|
||||
number: number;
|
||||
}
|
||||
| {
|
||||
key: string;
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
>;
|
||||
hidden: Record<string, string>;
|
||||
definition: {
|
||||
id: string;
|
||||
title: string;
|
||||
fields: Array<{
|
||||
id: string;
|
||||
ref: string;
|
||||
type: string;
|
||||
title: string;
|
||||
allow_multiple_selections?: boolean;
|
||||
allow_other_choice?: boolean;
|
||||
properties?: Record<string, string | boolean | number>;
|
||||
choices?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
}>;
|
||||
}>;
|
||||
ending: {
|
||||
id: string;
|
||||
title: string;
|
||||
ref: string;
|
||||
type: string;
|
||||
properties?: Record<string, string | boolean | number>;
|
||||
};
|
||||
};
|
||||
answers: Array<FormResponseAnswer>;
|
||||
ending: {
|
||||
id: string;
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type FormResponseAnswerBase = {
|
||||
field: {
|
||||
id: string;
|
||||
type: string;
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
type FormResponseAnswerEmail = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "email";
|
||||
email: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswerText = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswerNumber = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "number";
|
||||
number: number;
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswerBoolean = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "boolean";
|
||||
boolean: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswerDate = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "date";
|
||||
date: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswerChoice = Prettify<
|
||||
FormResponseAnswerBase & {
|
||||
type: "choice";
|
||||
choice: {
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
type FormResponseAnswer = Prettify<
|
||||
| FormResponseAnswerEmail
|
||||
| FormResponseAnswerText
|
||||
| FormResponseAnswerNumber
|
||||
| FormResponseAnswerBoolean
|
||||
| FormResponseAnswerDate
|
||||
| FormResponseAnswerChoice
|
||||
>;
|
||||
|
||||
`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
export async function generateIntegrationFiles(
|
||||
payload: {
|
||||
packageName: string;
|
||||
sdkPackage: string;
|
||||
extraInfo?: string;
|
||||
},
|
||||
apiKey: string,
|
||||
openAiOrg: string | undefined
|
||||
) {
|
||||
if (!apiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey, organization: openAiOrg });
|
||||
|
||||
try {
|
||||
const result = await openai.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You will be provided with the task of creating the typescript files necessary for a new Trigger.dev integration package. You will be provided the name of the integration package and the name of the SDK package.",
|
||||
},
|
||||
...createExampleMessages(),
|
||||
{
|
||||
role: "user",
|
||||
content: createPrompt(payload.packageName, payload.sdkPackage, payload.extraInfo),
|
||||
},
|
||||
],
|
||||
function_call: { name: "createTypescriptFiles" },
|
||||
functions: [
|
||||
{
|
||||
name: "createTypescriptFiles",
|
||||
description: "Creates the initial typescript files for a new Trigger.dev package",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"index.ts": {
|
||||
type: "string",
|
||||
description: "The contents of the index.ts file",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const firstChoice = result.choices[0];
|
||||
|
||||
if (!firstChoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = firstChoice.message;
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.function_call || !message.function_call.arguments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionCallArgs = safeJsonParse(message.function_call.arguments);
|
||||
|
||||
if (!functionCallArgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesSchema = z.record(z.string());
|
||||
|
||||
const files = filesSchema.safeParse(functionCallArgs);
|
||||
|
||||
if (!files.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
return files.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(jsonString: string) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { checkApiKeyIsDevServer } from "./getApiKeyType";
|
||||
|
||||
describe("Test API keys", () => {
|
||||
test("dev server succeeds", async () => {
|
||||
const result = checkApiKeyIsDevServer("tr_dev_12345");
|
||||
expect(result.success).toEqual(true);
|
||||
});
|
||||
|
||||
test("dev public fails", async () => {
|
||||
const result = checkApiKeyIsDevServer("pk_dev_12345");
|
||||
expect(result.success).toEqual(false);
|
||||
if (result.success) return;
|
||||
expect(result.type?.environment).toEqual("dev");
|
||||
expect(result.type?.type).toEqual("public");
|
||||
});
|
||||
|
||||
test("prod server fails", async () => {
|
||||
const result = checkApiKeyIsDevServer("tr_prod_12345");
|
||||
expect(result.success).toEqual(false);
|
||||
if (result.success) return;
|
||||
expect(result.type?.environment).toEqual("prod");
|
||||
expect(result.type?.type).toEqual("server");
|
||||
});
|
||||
|
||||
test("prod public fails", async () => {
|
||||
const result = checkApiKeyIsDevServer("pk_prod_12345");
|
||||
expect(result.success).toEqual(false);
|
||||
if (result.success) return;
|
||||
expect(result.type?.environment).toEqual("prod");
|
||||
expect(result.type?.type).toEqual("public");
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
export type ApiKeyType = {
|
||||
environment: "dev" | "prod";
|
||||
type: "server" | "public";
|
||||
};
|
||||
|
||||
type Result =
|
||||
| {
|
||||
success: true;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
type: ApiKeyType | undefined;
|
||||
};
|
||||
|
||||
export function checkApiKeyIsDevServer(apiKey: string): Result {
|
||||
const type = getApiKeyType(apiKey);
|
||||
|
||||
if (!type) {
|
||||
return { success: false, type: undefined };
|
||||
}
|
||||
|
||||
if (type.environment === "dev" && type.type === "server") {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
export function getApiKeyType(apiKey: string): ApiKeyType | undefined {
|
||||
if (apiKey.startsWith("tr_dev_")) {
|
||||
return {
|
||||
environment: "dev",
|
||||
type: "server",
|
||||
};
|
||||
}
|
||||
|
||||
if (apiKey.startsWith("pk_dev_")) {
|
||||
return {
|
||||
environment: "dev",
|
||||
type: "public",
|
||||
};
|
||||
}
|
||||
|
||||
// If they enter a prod key (tr_prod_), let them know
|
||||
if (apiKey.startsWith("tr_prod_")) {
|
||||
return {
|
||||
environment: "prod",
|
||||
type: "server",
|
||||
};
|
||||
}
|
||||
|
||||
if (apiKey.startsWith("pk_prod_")) {
|
||||
return {
|
||||
environment: "prod",
|
||||
type: "public",
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export function getPackageName(inputString: string): string {
|
||||
const packageName = inputString.split("/")[1];
|
||||
return packageName ?? "";
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { logger } from "./logger";
|
||||
import { CLOUD_API_URL } from "../consts";
|
||||
import { checkApiKeyIsDevServer } from "./getApiKeyType";
|
||||
import { readEnvVariables } from "./readEnvVariables";
|
||||
|
||||
export async function getTriggerApiDetails(path: string, envFile: string) {
|
||||
const envVarsToRead = ["TRIGGER_API_KEY", "TRIGGER_API_URL"];
|
||||
const resolvedEnvVars = await readEnvVariables(path, envFile, envVarsToRead);
|
||||
|
||||
const apiKey = resolvedEnvVars.TRIGGER_API_KEY;
|
||||
const apiUrl = resolvedEnvVars.TRIGGER_API_URL;
|
||||
|
||||
if (!apiKey) {
|
||||
logger.error(
|
||||
`You must add TRIGGER_API_KEY to your ${envFile} file or set as runtime environment variable.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = checkApiKeyIsDevServer(apiKey.value);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.type) {
|
||||
logger.error(
|
||||
`Your TRIGGER_API_KEY isn't a secret dev API key, you've entered a ${result.type.environment} ${result.type.type} key`
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
"Your TRIGGER_API_KEY isn't a secret dev API key. It should start with tr_dev_."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: apiKey.value,
|
||||
apiUrl: apiUrl?.value ?? CLOUD_API_URL,
|
||||
apiKeySource:
|
||||
apiKey.source.type === "runtime" ? "process runtime" : `${apiKey.source.name} file`,
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { pathExists } from "./fileSystem";
|
||||
import { getUserPackageManager } from "./getUserPkgManager";
|
||||
import * as pathModule from "path";
|
||||
import { Mock } from "vitest";
|
||||
|
||||
vi.mock('path', () => {
|
||||
const path = {
|
||||
join: vi.fn().mockImplementation((...paths: string[]) => paths.join('/')),
|
||||
};
|
||||
|
||||
return {
|
||||
...path,
|
||||
default: path,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./fileSystem.ts', () => ({
|
||||
pathExists: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
describe(getUserPackageManager.name, () => {
|
||||
let path: string;
|
||||
|
||||
beforeEach(() => {
|
||||
path = randomUUID();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
})
|
||||
|
||||
describe(`should use ${pathExists.name} to check for package manager artifacts`, () => {
|
||||
it('should join the path with the artifact name', async () => {
|
||||
await getUserPackageManager(path);
|
||||
|
||||
expect(pathModule.join).toBeCalledWith(path, 'yarn.lock');
|
||||
expect(pathModule.join).toBeCalledWith(path, 'pnpm-lock.yaml');
|
||||
expect(pathModule.join).toBeCalledWith(path, 'package-lock.json');
|
||||
});
|
||||
|
||||
it(`should call ${pathExists.name} with the path.join result`, async () => {
|
||||
const expected = randomUUID();
|
||||
|
||||
(pathModule.join as Mock).mockReturnValueOnce(expected);
|
||||
|
||||
await getUserPackageManager(path);
|
||||
|
||||
expect(pathExists).toBeCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should return "yarn" if yarn.lock exists', async () => {
|
||||
(pathExists as Mock).mockImplementation((path: string) => path.endsWith('yarn.lock'));
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('yarn');
|
||||
});
|
||||
|
||||
it('should return "pnpm" if pnpm-lock.yaml exists', async () => {
|
||||
(pathExists as Mock).mockImplementation(async (path: string) => path.endsWith('pnpm-lock.yaml'));
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('pnpm');
|
||||
});
|
||||
|
||||
it('should return "npm" if package-lock.json exists', async () => {
|
||||
(pathExists as Mock).mockImplementation((path: string) => path.endsWith('package-lock.json'));
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('npm');
|
||||
});
|
||||
|
||||
it('should return "npm" if npm-shrinkwrap.json exists', async () => {
|
||||
(pathExists as Mock).mockImplementation((path: string) => path.endsWith('npm-shrinkwrap.json'));
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('npm');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`if doesn't found artifacts, should use process.env.npm_config_user_agent to detect package manager`, () => {
|
||||
beforeEach(() => {
|
||||
(pathExists as Mock).mockResolvedValue(false);
|
||||
})
|
||||
|
||||
it('should return "yarn" if process.env.npm_config_user_agent starts with "yarn"', async () => {
|
||||
process.env.npm_config_user_agent = 'yarn';
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('yarn');
|
||||
});
|
||||
|
||||
it('should return "pnpm" if process.env.npm_config_user_agent starts with "pnpm"', async () => {
|
||||
process.env.npm_config_user_agent = 'pnpm';
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('pnpm');
|
||||
});
|
||||
|
||||
it('if doesn\'t start with "yarn" or "pnpm", should return "npm"', async () => {
|
||||
process.env.npm_config_user_agent = randomUUID();
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('npm');
|
||||
});
|
||||
|
||||
it('should return "npm" if process.env.npm_config_user_agent is not set', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
expect(await getUserPackageManager(path)).toBe('npm');
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
import pathModule from "path";
|
||||
import { pathExists } from "./fileSystem";
|
||||
|
||||
export type PackageManager = "npm" | "pnpm" | "yarn";
|
||||
|
||||
export async function getUserPackageManager(path: string): Promise<PackageManager> {
|
||||
try {
|
||||
return await detectPackageManagerFromArtifacts(path);
|
||||
} catch (error) {
|
||||
return detectPackageManagerFromCurrentCommand();
|
||||
}
|
||||
}
|
||||
|
||||
function detectPackageManagerFromCurrentCommand(): PackageManager {
|
||||
// This environment variable is set by npm and yarn but pnpm seems less consistent
|
||||
const userAgent = process.env.npm_config_user_agent;
|
||||
|
||||
if (userAgent) {
|
||||
if (userAgent.startsWith("yarn")) {
|
||||
return "yarn";
|
||||
} else if (userAgent.startsWith("pnpm")) {
|
||||
return "pnpm";
|
||||
} else {
|
||||
return "npm";
|
||||
}
|
||||
} else {
|
||||
// If no user agent is set, assume npm
|
||||
return "npm";
|
||||
}
|
||||
}
|
||||
|
||||
async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
|
||||
const packageFiles = [
|
||||
{ name: "yarn.lock", pm: "yarn" } as const,
|
||||
{ name: "pnpm-lock.yaml", pm: "pnpm" } as const,
|
||||
{ name: "package-lock.json", pm: "npm" } as const,
|
||||
{ name: "npm-shrinkwrap.json", pm: "npm" } as const,
|
||||
];
|
||||
|
||||
for (const { name, pm } of packageFiles) {
|
||||
const exists = await pathExists(pathModule.join(path, name));
|
||||
if (exists) {
|
||||
return pm;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not detect package manager from artifacts");
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { type PackageJson } from "type-fest";
|
||||
import path from "path";
|
||||
import { PKG_ROOT } from "../consts";
|
||||
import { readJSONFileSync } from "./fileSystem";
|
||||
|
||||
export function getVersion() {
|
||||
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
||||
|
||||
const packageJsonContent = readJSONFileSync(packageJsonPath) as PackageJson;
|
||||
|
||||
return packageJsonContent.version ?? "1.0.0";
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { execSync } from "child_process";
|
||||
import { execa } from "execa";
|
||||
import inquirer from "inquirer";
|
||||
import ora from "ora";
|
||||
import path from "path";
|
||||
import { logger } from "./logger";
|
||||
import { pathExists, removeFile } from "./fileSystem";
|
||||
|
||||
const isGitInstalled = (dir: string): boolean => {
|
||||
try {
|
||||
execSync("git --version", { cwd: dir });
|
||||
return true;
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */
|
||||
const isRootGitRepo = (dir: string): Promise<boolean> => {
|
||||
return pathExists(path.join(dir, ".git"));
|
||||
};
|
||||
|
||||
/** @returns Whether or not this directory or a parent directory has a `.git` directory. */
|
||||
const isInsideGitRepo = async (dir: string): Promise<boolean> => {
|
||||
try {
|
||||
// If this command succeeds, we're inside a git repo
|
||||
await execa("git", ["rev-parse", "--is-inside-work-tree"], {
|
||||
cwd: dir,
|
||||
stdout: "ignore",
|
||||
});
|
||||
return true;
|
||||
} catch (_e) {
|
||||
// Else, it will throw a git-error and we return false
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getGitVersion = () => {
|
||||
const stdout = execSync("git --version").toString().trim();
|
||||
const gitVersionTag = stdout.split(" ")[2];
|
||||
const major = gitVersionTag?.split(".")[0];
|
||||
const minor = gitVersionTag?.split(".")[1];
|
||||
return { major: Number(major), minor: Number(minor) };
|
||||
};
|
||||
|
||||
/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */
|
||||
const getDefaultBranch = () => {
|
||||
const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim();
|
||||
|
||||
return stdout;
|
||||
};
|
||||
|
||||
// This initializes the Git-repository for the project
|
||||
export const initializeGit = async (projectDir: string) => {
|
||||
logger.info("Initializing Git...");
|
||||
|
||||
if (!isGitInstalled(projectDir)) {
|
||||
logger.warn("Git is not installed. Skipping Git initialization.");
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora("Creating a new git repo...\n").start();
|
||||
|
||||
const isRoot = await isRootGitRepo(projectDir);
|
||||
const isInside = await isInsideGitRepo(projectDir);
|
||||
const dirName = path.parse(projectDir).name; // skip full path for logging
|
||||
|
||||
if (isInside && isRoot) {
|
||||
// Dir is a root git repo
|
||||
spinner.stop();
|
||||
const { overwriteGit } = await inquirer.prompt<{
|
||||
overwriteGit: boolean;
|
||||
}>({
|
||||
name: "overwriteGit",
|
||||
type: "confirm",
|
||||
message: `${chalk.redBright.bold(
|
||||
"Warning:"
|
||||
)} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,
|
||||
default: false,
|
||||
});
|
||||
if (!overwriteGit) {
|
||||
spinner.info("Skipping Git initialization.");
|
||||
return;
|
||||
}
|
||||
// Deleting the .git folder
|
||||
await removeFile(path.join(projectDir, ".git"));
|
||||
} else if (isInside && !isRoot) {
|
||||
// Dir is inside a git worktree
|
||||
spinner.stop();
|
||||
const { initializeChildGitRepo } = await inquirer.prompt<{
|
||||
initializeChildGitRepo: boolean;
|
||||
}>({
|
||||
name: "initializeChildGitRepo",
|
||||
type: "confirm",
|
||||
message: `${chalk.redBright.bold(
|
||||
"Warning:"
|
||||
)} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,
|
||||
default: false,
|
||||
});
|
||||
if (!initializeChildGitRepo) {
|
||||
spinner.info("Skipping Git initialization.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We're good to go, initializing the git repo
|
||||
try {
|
||||
const branchName = getDefaultBranch();
|
||||
|
||||
// --initial-branch flag was added in git v2.28.0
|
||||
const { major, minor } = getGitVersion();
|
||||
if (major < 2 || minor < 28) {
|
||||
await execa("git", ["init"], { cwd: projectDir });
|
||||
await execa("git", ["branch", "-m", branchName], { cwd: projectDir });
|
||||
} else {
|
||||
await execa("git", ["init", `--initial-branch=${branchName}`], {
|
||||
cwd: projectDir,
|
||||
});
|
||||
}
|
||||
await execa("git", ["add", "."], { cwd: projectDir });
|
||||
spinner.succeed(
|
||||
`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold("git")}\n`
|
||||
);
|
||||
} catch (error) {
|
||||
// Safeguard, should be unreachable
|
||||
spinner.fail(
|
||||
`${chalk.bold.red("Failed:")} could not initialize git. Update git to the latest version!\n`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import { getUserPackageManager, type PackageManager } from "./getUserPkgManager";
|
||||
import { logger } from "./logger";
|
||||
import ora, { type Ora } from "ora";
|
||||
import chalk from "chalk";
|
||||
import { execa } from "execa";
|
||||
|
||||
export async function installDependencies(projectDir: string) {
|
||||
logger.info("Installing dependencies...");
|
||||
|
||||
const pkgManager = await getUserPackageManager(projectDir);
|
||||
|
||||
const installSpinner = await runInstallCommand(pkgManager, projectDir);
|
||||
|
||||
// If the spinner was used to show the progress, use succeed method on it
|
||||
// If not, use the succeed on a new spinner
|
||||
(installSpinner || ora()).succeed(chalk.green("Successfully installed dependencies!\n"));
|
||||
}
|
||||
|
||||
async function runInstallCommand(
|
||||
pkgManager: PackageManager,
|
||||
projectDir: string
|
||||
): Promise<Ora | null> {
|
||||
switch (pkgManager) {
|
||||
// When using npm, inherit the stderr stream so that the progress bar is shown
|
||||
case "npm":
|
||||
await execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
return null;
|
||||
// When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
|
||||
case "pnpm":
|
||||
const pnpmSpinner = ora("Running pnpm install...").start();
|
||||
const pnpmSubprocess = execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
pnpmSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (text.includes("Progress")) {
|
||||
pnpmSpinner.text = text.includes("|") ? text.split(" | ")[1] ?? "" : text;
|
||||
}
|
||||
});
|
||||
pnpmSubprocess.on("error", (e) => rej(e));
|
||||
pnpmSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return pnpmSpinner;
|
||||
case "yarn":
|
||||
const yarnSpinner = ora("Running yarn...").start();
|
||||
const yarnSubprocess = execa(pkgManager, [], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
yarnSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
yarnSpinner.text = data.toString();
|
||||
});
|
||||
yarnSubprocess.on("error", (e) => rej(e));
|
||||
yarnSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return yarnSpinner;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Framework, getFramework } from "../frameworks";
|
||||
import { PackageManager, getUserPackageManager } from "./getUserPkgManager";
|
||||
import { Logger } from "./logger";
|
||||
import { run as ncuRun } from "npm-check-updates";
|
||||
import fs from "fs/promises";
|
||||
import pathModule from "path";
|
||||
|
||||
export abstract class JsRuntime {
|
||||
logger: Logger;
|
||||
projectRootPath: string;
|
||||
constructor(projectRootPath: string, logger: Logger) {
|
||||
this.logger = logger;
|
||||
this.projectRootPath = projectRootPath;
|
||||
}
|
||||
abstract get id(): string;
|
||||
abstract checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined>;
|
||||
abstract getUserPackageManager(): Promise<PackageManager | undefined>;
|
||||
abstract getFramework(): Promise<Framework | undefined>;
|
||||
abstract getEndpointId(): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export async function getJsRuntime(projectRootPath: string, logger: Logger): Promise<JsRuntime> {
|
||||
if (await NodeJsRuntime.isNodeJsRuntime(projectRootPath)) {
|
||||
return new NodeJsRuntime(projectRootPath, logger);
|
||||
} else if (await DenoRuntime.isDenoJsRuntime(projectRootPath)) {
|
||||
return new DenoRuntime(projectRootPath, logger);
|
||||
}
|
||||
throw new Error("Unsupported runtime");
|
||||
}
|
||||
|
||||
class NodeJsRuntime extends JsRuntime {
|
||||
static async isNodeJsRuntime(projectRootPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(pathModule.join(projectRootPath, "package.json"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "nodejs";
|
||||
}
|
||||
get packageJsonPath(): string {
|
||||
return pathModule.join(this.projectRootPath, "package.json");
|
||||
}
|
||||
|
||||
async checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined> {
|
||||
const updates = (await ncuRun({
|
||||
packageFile: `${this.packageJsonPath}`,
|
||||
filter: "/trigger.dev/.+$/",
|
||||
upgrade: false,
|
||||
})) as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
if (typeof updates === "undefined" || Object.keys(updates).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageFile = await fs.readFile(this.packageJsonPath);
|
||||
const data = JSON.parse(Buffer.from(packageFile).toString("utf8"));
|
||||
const dependencies = data.dependencies;
|
||||
|
||||
const hasUpdates = Object.keys(updates).length > 0;
|
||||
|
||||
if (!hasUpdates) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstDep = Object.keys(updates)[0];
|
||||
|
||||
if (!firstDep) {
|
||||
return;
|
||||
}
|
||||
|
||||
const from = dependencies[firstDep];
|
||||
const to = updates[firstDep];
|
||||
|
||||
if (!to || !from) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
async getUserPackageManager() {
|
||||
return getUserPackageManager(this.projectRootPath);
|
||||
}
|
||||
|
||||
async getFramework() {
|
||||
const userPackageManager = await this.getUserPackageManager();
|
||||
return getFramework(this.projectRootPath, userPackageManager);
|
||||
}
|
||||
async getEndpointId() {
|
||||
const pkgJsonPath = pathModule.join(this.projectRootPath, "package.json");
|
||||
const pkgBuffer = await fs.readFile(pkgJsonPath);
|
||||
const pkgJson = JSON.parse(pkgBuffer.toString());
|
||||
const value = pkgJson["trigger.dev"]?.endpointId;
|
||||
if (!value || typeof value !== "string") return undefined;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class DenoRuntime extends JsRuntime {
|
||||
getDenoJsonPath(): Promise<string> {
|
||||
try {
|
||||
return fs
|
||||
.stat(pathModule.join(this.projectRootPath, "deno.json"))
|
||||
.then(() => pathModule.join(this.projectRootPath, "deno.json"));
|
||||
} catch {
|
||||
return fs
|
||||
.stat(pathModule.join(this.projectRootPath, "deno.jsonc"))
|
||||
.then(() => pathModule.join(this.projectRootPath, "deno.jsonc"));
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "deno";
|
||||
}
|
||||
|
||||
static async isDenoJsRuntime(projectRootPath: string): Promise<boolean> {
|
||||
try {
|
||||
try {
|
||||
await fs.stat(pathModule.join(projectRootPath, "deno.json"));
|
||||
} catch (e) {
|
||||
await fs.stat(pathModule.join(projectRootPath, "deno.jsonc"));
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkForOutdatedPackages(): Promise<{ from: string; to: string } | undefined> {
|
||||
return;
|
||||
}
|
||||
async getUserPackageManager() {
|
||||
return undefined;
|
||||
}
|
||||
async getFramework() {
|
||||
// not implemented currently
|
||||
return undefined;
|
||||
}
|
||||
async getEndpointId() {
|
||||
const pkgJsonPath = await this.getDenoJsonPath();
|
||||
const pkgBuffer = await fs.readFile(pkgJsonPath);
|
||||
const pkgJson = JSON.parse(pkgBuffer.toString());
|
||||
return pkgJson["trigger.dev"]?.endpointId;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
|
||||
export type Logger = typeof logger;
|
||||
export const logger = {
|
||||
error(...args: unknown[]) {
|
||||
console.log(chalk.red(...args));
|
||||
},
|
||||
warn(...args: unknown[]) {
|
||||
console.log(chalk.yellow(...args));
|
||||
},
|
||||
info(...args: unknown[]) {
|
||||
console.log(chalk.cyan(...args));
|
||||
},
|
||||
success(...args: unknown[]) {
|
||||
console.log(chalk.green(...args));
|
||||
},
|
||||
table(rows: any) {
|
||||
console.table(rows);
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import pathModule from "path";
|
||||
|
||||
// Takes a relative path (like .) and resolves it to a full path (like /Users/username/Projects/my-triggers)
|
||||
export const resolvePath = (input: string) => {
|
||||
return pathModule.resolve(process.cwd(), input);
|
||||
};
|
||||
|
||||
// Takes an absolute path and derives the relative path from the current working directory
|
||||
export const relativePath = (input: string) => {
|
||||
return pathModule.relative(process.cwd(), input);
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
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,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without alias", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": JSON.stringify({}),
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without paths", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": {}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig without matching alias", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./app/*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toBeUndefined();
|
||||
});
|
||||
|
||||
test("jsconfig src dir", async () => {
|
||||
mock({
|
||||
"src/pages": {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: false,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
|
||||
test("jsconfig no src dir", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
"jsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: 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,
|
||||
extraDirectories: ["src"],
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
|
||||
test("tsconfig no src dir", async () => {
|
||||
mock({
|
||||
pages: {},
|
||||
"tsconfig.json": `{"compilerOptions": { "paths": {
|
||||
"~/*": ["./*"]
|
||||
}}}`,
|
||||
});
|
||||
|
||||
const alias = await getPathAlias({
|
||||
projectPath: "",
|
||||
isTypescriptProject: true,
|
||||
});
|
||||
|
||||
expect(alias).toEqual("~");
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
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 extra directories, or root if none are specified 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;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import pathModule from "path";
|
||||
import { pathExists, readFile } from "./fileSystem";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
const ENV_FILES_FALLBACK = [".env", ".env.local", ".env.development.local"];
|
||||
|
||||
export type EnvVarSourceRuntime = {
|
||||
type: "runtime";
|
||||
};
|
||||
|
||||
export type EnvVarSourceFile = {
|
||||
type: "file";
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type EnvVarSource = EnvVarSourceRuntime | EnvVarSourceFile;
|
||||
|
||||
export type EnvironmentVariable = {
|
||||
value: string;
|
||||
source: EnvVarSource;
|
||||
};
|
||||
|
||||
export type EnvironmentVariables = {
|
||||
[name: string]: EnvironmentVariable | undefined;
|
||||
};
|
||||
|
||||
// Reads `varsToRead` from `process.env` and `envFile` (with fallbacks).
|
||||
// `process.env` takes precedence over the `envFile`.
|
||||
export async function readEnvVariables(
|
||||
path: string,
|
||||
envFile: string,
|
||||
varsToRead: string[]
|
||||
): Promise<EnvironmentVariables> {
|
||||
const resolvedEnvFile = await readEnvFilesWithBackups(path, envFile);
|
||||
const parsedEnvFile = resolvedEnvFile
|
||||
? { output: dotenv.parse(resolvedEnvFile.content), filename: resolvedEnvFile.fileName }
|
||||
: {};
|
||||
|
||||
return Object.fromEntries(
|
||||
varsToRead.map((envVar) => [
|
||||
envVar,
|
||||
readFromRuntime(envVar) ?? readFromFile(envVar, parsedEnvFile),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
async function readEnvFilesWithBackups(
|
||||
path: string,
|
||||
envFile: string
|
||||
): Promise<{ content: string; fileName: string } | undefined> {
|
||||
const envFilePath = pathModule.join(path, envFile);
|
||||
const envFileExists = await pathExists(envFilePath);
|
||||
|
||||
if (envFileExists) {
|
||||
const content = await readFile(envFilePath);
|
||||
|
||||
return { content, fileName: envFile };
|
||||
}
|
||||
|
||||
for (const fallBack of ENV_FILES_FALLBACK) {
|
||||
const fallbackPath = pathModule.join(path, fallBack);
|
||||
const fallbackExists = await pathExists(fallbackPath);
|
||||
|
||||
if (fallbackExists) {
|
||||
const content = await readFile(fallbackPath);
|
||||
|
||||
return { content, fileName: fallBack };
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function readFromRuntime(envVar: string): EnvironmentVariable | undefined {
|
||||
const val = process.env[envVar];
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
value: val,
|
||||
source: {
|
||||
type: "runtime",
|
||||
} as EnvVarSourceRuntime,
|
||||
};
|
||||
}
|
||||
|
||||
function readFromFile(
|
||||
envVar: string,
|
||||
parsedEnvFile: { output?: dotenv.DotenvParseOutput; filename?: string }
|
||||
): EnvironmentVariable | undefined {
|
||||
const val = parsedEnvFile.output ? parsedEnvFile.output[envVar] : undefined;
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
value: val,
|
||||
source: {
|
||||
type: "file",
|
||||
name: parsedEnvFile.filename,
|
||||
} as EnvVarSourceFile,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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" });
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import pathModule from "path";
|
||||
import { type PackageJson } from "type-fest";
|
||||
import { readJSONFile } from "./fileSystem";
|
||||
|
||||
export async function readPackageJson(directory: string): Promise<PackageJson | undefined> {
|
||||
const packageJsonPath = pathModule.join(directory, "package.json");
|
||||
return readJSONFile(packageJsonPath)
|
||||
.then((f) => f as PackageJson)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function removeFileExtension(filename: string) {
|
||||
return filename.replace(/\.[^.]+$/, "");
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Obfuscate the API key (will be in the format tr_dev_XXXX)
|
||||
export function renderApiKey(apiKey: string): string {
|
||||
return apiKey.replace(/tr_dev_[a-zA-Z0-9]{6}/g, "tr_dev_********");
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import gradient from "gradient-string";
|
||||
import { TITLE_TEXT } from "../consts";
|
||||
import { getUserPackageManager } from "./getUserPkgManager";
|
||||
|
||||
// colors brought in from vscode poimandres theme
|
||||
const poimandresTheme = {
|
||||
blue: "#add7ff",
|
||||
cyan: "#89ddff",
|
||||
green: "#5de4c7",
|
||||
magenta: "#fae4fc",
|
||||
red: "#d0679d",
|
||||
yellow: "#fffac2",
|
||||
};
|
||||
|
||||
export const renderTitle = async (projectDirectory: string) => {
|
||||
const triggerGradient = gradient(Object.values(poimandresTheme));
|
||||
|
||||
// resolves weird behavior where the ascii is offset
|
||||
const pkgManager = await getUserPackageManager(projectDirectory);
|
||||
if (pkgManager === "yarn" || pkgManager === "pnpm") {
|
||||
console.log("");
|
||||
}
|
||||
console.log(triggerGradient.multiline(TITLE_TEXT));
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
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,5 +0,0 @@
|
||||
import { TEMPLATE_ORGANIZATION } from "../consts";
|
||||
|
||||
export function createTemplateRef(templateName: string): string {
|
||||
return `github:${TEMPLATE_ORGANIZATION}/${templateName}`;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export class Throttle {
|
||||
throttleTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly fn: () => any,
|
||||
private readonly delay: number
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
call() {
|
||||
if (this.throttleTimeout) {
|
||||
clearTimeout(this.throttleTimeout);
|
||||
}
|
||||
this.throttleTimeout = setTimeout(this.fn, this.delay);
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import fetch from "./fetchUseProxy";
|
||||
import { z } from "zod";
|
||||
import { GetEndpointIndexResponseSchema } from "@trigger.dev/core";
|
||||
|
||||
export type CreateEndpointOptions = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type EndpointData = {
|
||||
id: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
environmentId: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
indexingHookIdentifier: string;
|
||||
endpointIndex: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EndpointResponse =
|
||||
| {
|
||||
ok: true;
|
||||
data: EndpointData;
|
||||
}
|
||||
| { ok: false; error: string; retryable: boolean };
|
||||
|
||||
const RETRYABLE_PATTERN = /Could not connect to endpoint/i;
|
||||
|
||||
const WhoamiResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
type: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
project: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
organization: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
title: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
}),
|
||||
pkApiKey: z.string(),
|
||||
userId: z.string().optional(),
|
||||
});
|
||||
|
||||
const CreateTunnelResponseSchema = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type WhoamiResponse = z.infer<typeof WhoamiResponseSchema>;
|
||||
|
||||
export class TriggerApi {
|
||||
constructor(
|
||||
private apiKey: string,
|
||||
private baseUrl: string
|
||||
) { }
|
||||
|
||||
async supportsTunneling(): Promise<boolean> {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/tunnels`, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async createTunnel() {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/tunnels`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not create tunnel: ${response.status}`);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
const parsedBody = CreateTunnelResponseSchema.safeParse(body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
throw new Error(`Could not create tunnel: ${parsedBody.error.message}`);
|
||||
}
|
||||
|
||||
return parsedBody.data;
|
||||
}
|
||||
|
||||
async whoami(): Promise<WhoamiResponse | undefined> {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/whoami`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
|
||||
const parsed = WhoamiResponseSchema.safeParse(body);
|
||||
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async sendEvent(id: string, name: string, payload: any) {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/events`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: {
|
||||
id,
|
||||
name,
|
||||
payload,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async registerEndpoint(options: CreateEndpointOptions): Promise<EndpointResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/endpoints`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const rawBody = await response.text();
|
||||
|
||||
if (typeof rawBody === "string") {
|
||||
const rawJson = safeJsonParse(rawBody);
|
||||
|
||||
if (!rawJson) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "An unknown issue occurred when registering with Trigger.dev",
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const parsedJson = z.object({ error: z.string() }).safeParse(rawJson);
|
||||
|
||||
if (!parsedJson.success) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "An unknown issue occurred when registering with Trigger.dev",
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: parsedJson.data.error,
|
||||
retryable: RETRYABLE_PATTERN.test(parsedJson.data.error),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
error: "An unknown issue occurred when registering with Trigger.dev",
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data: data as any as EndpointData,
|
||||
};
|
||||
}
|
||||
|
||||
async getEndpointIndex(indexId: string) {
|
||||
const response = await fetch(`${this.baseUrl}/api/v1/endpointindex/${indexId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
const parsed = GetEndpointIndexResponseSchema.safeParse(body);
|
||||
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "FAILURE" as const,
|
||||
error: {
|
||||
message: `Bad response from Trigger.dev (${response.status})`,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(raw: string | null | undefined): unknown {
|
||||
if (typeof raw !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export async function wait(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"include": ["src/globals.d.ts", "./src/**/*.ts", "tsup.config.ts", "./test/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
/* LANGUAGE COMPILATION OPTIONS */
|
||||
"target": "ES2020",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
/* EMIT RULES */
|
||||
"outDir": "./dist",
|
||||
"noEmit": true, // TSUP takes care of emitting js for us, in a MUCH faster way
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
|
||||
/* TYPE CHECKING RULES */
|
||||
"strict": true,
|
||||
// "noImplicitAny": true, // Included in "Strict"
|
||||
// "noImplicitThis": true, // Included in "Strict"
|
||||
// "strictBindCallApply": true, // Included in "Strict"
|
||||
// "strictFunctionTypes": true, // Included in "Strict"
|
||||
// "strictNullChecks": true, // Included in "Strict"
|
||||
// "strictPropertyInitialization": true, // Included in "Strict"
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"noUncheckedIndexedAccess": true, // TLDR - Checking an indexed value (array[0]) now forces type <T | undefined> as there is no confirmation that index exists
|
||||
// THE BELOW ARE EXTRA STRICT OPTIONS THAT SHOULD ONLY BY CONSIDERED IN VERY SAFE PROJECTS
|
||||
// "exactOptionalPropertyTypes": true, // TLDR - Setting to undefined is not the same as a property not being defined at all
|
||||
// "noPropertyAccessFromIndexSignature": true, // TLDR - Use dot notation for objects if youre sure it exists, use ['index'] notaion if unsure
|
||||
|
||||
/* OTHER OPTIONS */
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
// "emitDecoratorMetadata": true,
|
||||
// "experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@trigger.dev/yalt": ["../yalt/src/index"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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,
|
||||
dts: true,
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
minify: !isDev,
|
||||
metafile: !isDev,
|
||||
sourcemap: true,
|
||||
target: "esnext",
|
||||
outDir: "dist",
|
||||
onSuccess: isDev ? `${copyTemplates} && node dist/index.js` : copyTemplates,
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true
|
||||
},
|
||||
})
|
||||
Generated
+512
-9379
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,7 @@
|
||||
packages:
|
||||
- "config-packages/*"
|
||||
- "packages/*"
|
||||
- "integrations/*"
|
||||
- "apps/**"
|
||||
- "references/*"
|
||||
- "docs"
|
||||
- "perf"
|
||||
- "runtime_tests"
|
||||
- "!packages/cli-v3/e2e/**"
|
||||
|
||||
Reference in New Issue
Block a user