remove more v2 code and run pnpm install

This commit is contained in:
Eric Allam
2024-08-05 14:47:41 +01:00
committed by Eric Allam
parent d71925fe27
commit 835c9cb671
95 changed files with 513 additions and 18310 deletions
-2
View File
@@ -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
View File
@@ -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
-21
View File
@@ -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.
-25
View File
@@ -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
```
-97
View File
@@ -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"
}
}
-284
View File
@@ -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)}`);
};
-637
View File
@@ -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,
};
}
}
}
-295
View File
@@ -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;
}
}
-72
View File
@@ -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;
}
}
-141
View File
@@ -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;
}
-65
View File
@@ -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);
}
-22
View File
@@ -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);
});
});
-128
View File
@@ -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)/;
}
-51
View File
@@ -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)/;
}
-74
View File
@@ -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(", ");
}
-277
View File
@@ -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");
});
});
-110
View File
@@ -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)/;
-18
View File
@@ -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);
});
-14
View File
@@ -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;
}
-199
View File
@@ -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,
});
-4
View File
@@ -1,4 +0,0 @@
export type TaskMetadata = {
id: string;
exportName: string;
};
-158
View File
@@ -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),
};
}
}
-91
View File
@@ -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}`);
}
}
-16
View File
@@ -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);
}
-59
View File
@@ -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");
});
});
-65
View File
@@ -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;
}
-4
View File
@@ -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");
}
-12
View File
@@ -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";
}
-131
View File
@@ -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;
}
}
-151
View File
@@ -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;
}
}
-20
View File
@@ -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);
};
-131
View File
@@ -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("~");
});
});
-55
View File
@@ -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;
}
-102
View File
@@ -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" });
});
}
-10
View File
@@ -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(/\.[^.]+$/, "");
}
-4
View File
@@ -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_********");
}
-24
View File
@@ -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));
};
-4
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
import { TEMPLATE_ORGANIZATION } from "../consts";
export function createTemplateRef(templateName: string): string {
return `github:${TEMPLATE_ORGANIZATION}/${templateName}`;
}
-18
View File
@@ -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);
}
}
-241
View File
@@ -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;
}
}
-5
View File
@@ -1,5 +0,0 @@
export async function wait(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
-55
View File
@@ -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"]
}
-18
View File
@@ -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,
});
-7
View File
@@ -1,7 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true
},
})
+512 -9379
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,10 +1,7 @@
packages:
- "config-packages/*"
- "packages/*"
- "integrations/*"
- "apps/**"
- "references/*"
- "docs"
- "perf"
- "runtime_tests"
- "!packages/cli-v3/e2e/**"