v3: semi-automatic package update command (#1052)
* fix package manager detection * fix cli update check while in beta * add update command and warn on mismatches * update fixes and better integration with dev and deploy * fix for ci * update docs * changeset * be more specific about prompts before updates * fail on version mismatch during ci * update github actions docs * fix package manager copypasta * remove ncu * ignore companyicons * improve warnings and errors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
v3 CLI update command and package manager detection fix
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The CLI `dev` command runs a server for your tasks. It will watches for changes in your `/trigger` directory and communicates with the Trigger.dev platform to register your tasks, perform runs, and send data back and forth.
|
||||
|
||||
It can also update your `@trigger.dev/*` packages to prevent version mismatches and failed deploys. You will always be prompted first.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
|
||||
@@ -21,13 +21,16 @@ yarn dlx trigger.dev@beta deploy
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>Will fail in CI if any version mismatches are detected. Ensure everything runs locally first using the [dev](/v3/cli-dev) command and don't bypass the version checks!</Warning>
|
||||
|
||||
It performs a few steps to deploy:
|
||||
|
||||
1. Typechecks the code.
|
||||
2. Compiles and bundles the code.
|
||||
3. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
4. Deploys the code to the cloud.
|
||||
5. Registers the tasks as a new version in the environment (prod by default).
|
||||
1. Optionally updates packages when running locally.
|
||||
2. Typechecks the code.
|
||||
3. Compiles and bundles the code.
|
||||
4. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
5. Deploys the code to the cloud.
|
||||
6. Registers the tasks as a new version in the environment (prod by default).
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ yarn dlx trigger.dev@beta dev
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will first perform an update check to prevent version mismatches, failed deploys, and other errors. You will always be prompted first.
|
||||
|
||||
You will see in the terminal that the server is running and listening for requests. When you run a task, you will see it in the terminal along with a link to view it in the dashboard.
|
||||
|
||||
It is worth noting that each task runs in a separate Node process. This means that if you have a long-running task, it will not block other tasks from running.
|
||||
|
||||
@@ -5,6 +5,8 @@ description: "You can easily deploy your tasks with GitHub actions."
|
||||
|
||||
This simple GitHub action file will deploy you Trigger.dev tasks when new code is pushed to the `main` branch and the `trigger` directory has changes in it.
|
||||
|
||||
<Warning>The deploy step will fail if any version mismatches are detected. Please see the [version pinning](/v3/github-actions#version-pinning) section for more details.</Warning>
|
||||
|
||||
```yaml .github/workflows/release-trigger.yml
|
||||
name: Deploy to Trigger.dev
|
||||
|
||||
@@ -42,3 +44,19 @@ If you already have a GitHub action file, you can just add the final step "🚀
|
||||
You need to add the `TRIGGER_ACCESS_TOKEN` secret to your repository. You can create a new access token by going to your profile page and then clicking on the "Personal Access Tokens" tab.
|
||||
|
||||
To set it in GitHub go to your repository, click on "Settings", "Secrets and variables" and then "Actions". Add a new secret with the name `TRIGGER_ACCESS_TOKEN` and use the value of your access token.
|
||||
|
||||
## Version pinning
|
||||
|
||||
The CLI and `@trigger.dev/*` package versions need to be in sync, otherwise there will be errors and unpredictable behavior. Hence, the `deploy` command will automatically fail during CI on any version mismatches.
|
||||
|
||||
To ensure a smooth CI experience you can pin the CLI version in the deploy step, like so:
|
||||
|
||||
```yaml .github/workflows/release-trigger.yml
|
||||
- name: 🚀 Deploy Trigger.dev
|
||||
env:
|
||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||
run: |
|
||||
npx trigger.dev@3.0.0-beta.16 deploy
|
||||
```
|
||||
|
||||
You should use the version you run locally during dev and manual deploy. The current version is displayed in the banner, but you can also check it by appending `--version` to any command.
|
||||
|
||||
@@ -171,25 +171,7 @@ async function yourBackendFunction() {
|
||||
|
||||
## Upgrading your project
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Upgrade the v2 Trigger.dev packages">
|
||||
|
||||
You can run this command to upgrade all the packages to the beta:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli@beta update --to beta
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Follow the v3 quick start">
|
||||
|
||||
Follow the [v3 quick start](/v3/quick-start) to get started with v3.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
Just follow the [v3 quick start](/v3/quick-start) to get started with v3. Our new CLI will take care of the rest.
|
||||
|
||||
## Using v2 together with v3
|
||||
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"node-fetch": "^3.3.0",
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"p-debounce": "^4.0.0",
|
||||
"p-throttle": "^6.1.0",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { configureWhoamiCommand } from "../commands/whoami.js";
|
||||
import { COMMAND_NAME } from "../consts.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { configureListProfilesCommand } from "../commands/list-profiles.js";
|
||||
import { configureUpdateCommand } from "../commands/update.js";
|
||||
|
||||
export const program = new Command();
|
||||
|
||||
@@ -23,3 +24,4 @@ configureDeployCommand(program);
|
||||
configureWhoamiCommand(program);
|
||||
configureLogoutCommand(program);
|
||||
configureListProfilesCommand(program);
|
||||
configureUpdateCommand(program);
|
||||
|
||||
@@ -57,6 +57,7 @@ import { safeJsonParse } from "../utilities/safeJsonParse";
|
||||
import { JavascriptProject } from "../utilities/javascriptProject";
|
||||
import { cliRootPath } from "../utilities/resolveInternalFilePath";
|
||||
import { escapeImportPath, spinner } from "../utilities/windows";
|
||||
import { updateTriggerPackages } from "./update";
|
||||
import { docs, getInTouch } from "../utilities/links";
|
||||
|
||||
const DeployCommandOptions = CommonCommandOptions.extend({
|
||||
@@ -74,6 +75,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({
|
||||
outputMetafile: z.string().optional(),
|
||||
apiUrl: z.string().optional(),
|
||||
saveLogs: z.boolean().default(false),
|
||||
skipUpdateCheck: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type DeployCommandOptions = z.infer<typeof DeployCommandOptions>;
|
||||
@@ -90,6 +92,7 @@ export function configureDeployCommand(program: Command) {
|
||||
"prod"
|
||||
)
|
||||
.option("--skip-typecheck", "Whether to skip the pre-build typecheck")
|
||||
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
|
||||
.option(
|
||||
"--ignore-env-var-check",
|
||||
"Detected missing environment variables won't block deployment"
|
||||
@@ -167,6 +170,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
|
||||
intro("Deploying project");
|
||||
|
||||
if (!options.skipUpdateCheck) {
|
||||
await updateTriggerPackages(dir, { ...options }, true, true);
|
||||
}
|
||||
|
||||
const authorization = await login({
|
||||
embedded: true,
|
||||
defaultApiUrl: options.apiUrl,
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { findUp, pathExists } from "find-up";
|
||||
import { cliRootPath } from "../utilities/resolveInternalFilePath";
|
||||
import { escapeImportPath } from "../utilities/windows";
|
||||
import { updateTriggerPackages } from "./update";
|
||||
|
||||
let apiClient: CliApiClient | undefined;
|
||||
|
||||
@@ -61,6 +62,7 @@ const DevCommandOptions = CommonCommandOptions.extend({
|
||||
debugOtel: z.boolean().default(false),
|
||||
config: z.string().optional(),
|
||||
projectRef: z.string().optional(),
|
||||
skipUpdateCheck: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type DevCommandOptions = z.infer<typeof DevCommandOptions>;
|
||||
@@ -78,6 +80,7 @@ export function configureDevCommand(program: Command) {
|
||||
)
|
||||
.option("--debugger", "Enable the debugger")
|
||||
.option("--debug-otel", "Enable OpenTelemetry debugging")
|
||||
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
|
||||
).action(async (path, options) => {
|
||||
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
|
||||
await devCommand(path, opts);
|
||||
@@ -132,7 +135,13 @@ async function startDev(
|
||||
}
|
||||
|
||||
await printStandloneInitialBanner(true);
|
||||
printDevBanner();
|
||||
|
||||
if (!options.skipUpdateCheck) {
|
||||
console.log(); // spacing
|
||||
await updateTriggerPackages(dir, { ...options }, false, true);
|
||||
}
|
||||
|
||||
printDevBanner(!options.skipUpdateCheck);
|
||||
|
||||
logger.debug("Starting dev session", { dir, options, authorization });
|
||||
|
||||
|
||||
@@ -1,148 +1,300 @@
|
||||
import { confirm } from "@clack/prompts";
|
||||
import { RunOptions, run } from "npm-check-updates";
|
||||
import path from "path";
|
||||
import { confirm, intro, isCancel, log, outro } from "@clack/prompts";
|
||||
import { z } from "zod";
|
||||
import { chalkError, chalkSuccess } from "../utilities/cliOutput.js";
|
||||
import { readJSONFileSync, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { installDependencies } from "../utilities/installDependencies.js";
|
||||
import { readJSONFile, removeFile, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { spinner } from "../utilities/windows.js";
|
||||
import { CommonCommandOptions, OutroCommandError, wrapCommandAction } from "../cli/common.js";
|
||||
import { Command } from "commander";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { PackageJson } from "type-fest";
|
||||
import { printStandloneInitialBanner, updateCheck } from "../utilities/initialBanner.js";
|
||||
import { join, resolve } from "path";
|
||||
import { JavascriptProject } from "../utilities/javascriptProject.js";
|
||||
import { PackageManager } from "../utilities/getUserPackageManager.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { chalkError, prettyWarning } from "../utilities/cliOutput.js";
|
||||
|
||||
export const UpdateCommandOptionsSchema = z.object({
|
||||
to: z.string().optional(),
|
||||
export const UpdateCommandOptions = CommonCommandOptions.pick({
|
||||
logLevel: true,
|
||||
skipTelemetry: true,
|
||||
});
|
||||
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptions>;
|
||||
|
||||
type NcuRunOptionTarget = "latest" | `@${string}`;
|
||||
|
||||
export async function updateCommand(projectPath: string, anyOptions: any) {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Checking settings");
|
||||
|
||||
const parseRes = UpdateCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!parseRes.success) {
|
||||
loadingSpinner.stop(chalkError(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) {
|
||||
loadingSpinner.stop(chalkError("Couldn't load package.json"));
|
||||
return;
|
||||
}
|
||||
|
||||
loadingSpinner.message("Checking for updates");
|
||||
|
||||
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) {
|
||||
loadingSpinner.stop(chalkError("Couldn't update dependencies"));
|
||||
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) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already 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) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already 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];
|
||||
}
|
||||
export function configureUpdateCommand(program: Command) {
|
||||
return program
|
||||
.command("update")
|
||||
.description("Updates all @trigger.dev/* packages to match the CLI version")
|
||||
.argument("[path]", "The path to the directory that contains the package.json file", ".")
|
||||
.option(
|
||||
"-l, --log-level <level>",
|
||||
"The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.",
|
||||
"log"
|
||||
)
|
||||
.option("--skip-telemetry", "Opt-out of sending telemetry")
|
||||
.action(async (path, options) => {
|
||||
wrapCommandAction("dev", UpdateCommandOptions, options, async (opts) => {
|
||||
await printStandloneInitialBanner(true);
|
||||
await updateCommand(path, opts);
|
||||
});
|
||||
});
|
||||
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}`;
|
||||
const triggerPackageFilter = /^@trigger\.dev/;
|
||||
|
||||
export async function updateCommand(dir: string, options: UpdateCommandOptions) {
|
||||
await updateTriggerPackages(dir, options);
|
||||
}
|
||||
|
||||
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],
|
||||
}))
|
||||
export async function updateTriggerPackages(
|
||||
dir: string,
|
||||
options: UpdateCommandOptions,
|
||||
embedded?: boolean,
|
||||
requireUpdate?: boolean
|
||||
) {
|
||||
if (!embedded) {
|
||||
intro("Updating packages");
|
||||
}
|
||||
|
||||
const projectPath = resolve(process.cwd(), dir);
|
||||
|
||||
const { packageJson, readonlyPackageJson, packageJsonPath } = await getPackageJson(projectPath);
|
||||
|
||||
if (!packageJson) {
|
||||
log.error("Failed to load package.json. Try to re-run with `-l debug` to see what's going on.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cliVersion = getVersion();
|
||||
const newCliVersion = await updateCheck();
|
||||
|
||||
if (newCliVersion) {
|
||||
prettyWarning(
|
||||
"You're not running the latest CLI version, please consider updating ASAP",
|
||||
`Current: ${cliVersion}\nLatest: ${newCliVersion}`,
|
||||
"Run latest: npx trigger.dev@beta"
|
||||
);
|
||||
}
|
||||
|
||||
const triggerDependencies = getTriggerDependencies(packageJson);
|
||||
|
||||
function getVersionMismatches(deps: Dependency[], targetVersion: string): Dependency[] {
|
||||
const mismatches: Dependency[] = [];
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep.version === targetVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mismatches.push(dep);
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
const versionMismatches = getVersionMismatches(triggerDependencies, cliVersion);
|
||||
|
||||
if (versionMismatches.length === 0) {
|
||||
if (!embedded) {
|
||||
outro(`Nothing to do${newCliVersion ? " ..but you should really update your CLI!" : ""}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
prettyWarning(
|
||||
"Mismatch between your CLI version and installed packages",
|
||||
"We recommend pinned versions for guaranteed compatibility"
|
||||
);
|
||||
|
||||
// Ask the user if they want to update the dependencies
|
||||
const shouldContinue = await confirm({
|
||||
message: "Do you want to update these packages in package.json and re-install dependencies?",
|
||||
});
|
||||
if (!process.stdout.isTTY) {
|
||||
// Running in CI with version mismatch detected
|
||||
outro("Deploy failed");
|
||||
|
||||
return shouldContinue as boolean;
|
||||
console.log(
|
||||
`ERROR: Version mismatch detected while running in CI. This won't end well. Aborting.
|
||||
|
||||
Please run the dev command locally and check that your CLI version matches the one printed below. Additionally, all \`@trigger.dev/*\` packages also need to match this version.
|
||||
|
||||
If your local CLI version doesn't match the one below, you may want to add the \`trigger.dev\` package to your dependencies. You will also have to update your workflow deploy command to \`npx trigger.dev deploy\` to ensure your pinned CLI version is used.
|
||||
|
||||
CLI version: ${cliVersion}
|
||||
|
||||
Current package versions that don't match the CLI:
|
||||
${versionMismatches.map((dep) => `- ${dep.name}@${dep.version}`).join("\n")}\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.message(""); // spacing
|
||||
|
||||
// Always require user confirmation
|
||||
const userWantsToUpdate = await updateConfirmation(versionMismatches, cliVersion);
|
||||
|
||||
if (isCancel(userWantsToUpdate)) {
|
||||
throw new OutroCommandError();
|
||||
}
|
||||
|
||||
if (!userWantsToUpdate) {
|
||||
if (requireUpdate) {
|
||||
outro("You shall not pass!");
|
||||
|
||||
logger.log(
|
||||
`${chalkError(
|
||||
"X Error:"
|
||||
)} Update required: Version mismatches are a common source of bugs and errors. Please update or use \`--skip-update-check\` at your own risk.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!embedded) {
|
||||
outro("You've been warned!");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const installSpinner = spinner();
|
||||
installSpinner.start("Writing new package.json file");
|
||||
|
||||
// Backup package.json
|
||||
const packageJsonBackupPath = `${packageJsonPath}.bak`;
|
||||
await writeJSONFile(packageJsonBackupPath, readonlyPackageJson, true);
|
||||
|
||||
const exitHandler = async (sig: any) => {
|
||||
log.warn(
|
||||
`You may have to manually roll back any package.json changes. Backup written to ${packageJsonBackupPath}`
|
||||
);
|
||||
};
|
||||
|
||||
// Add exit handler to warn about manual rollback of package.json
|
||||
// Automatically rolling back can end up overwriting with an empty file instead
|
||||
process.prependOnceListener("exit", exitHandler);
|
||||
|
||||
// Update package.json
|
||||
mutatePackageJsonWithUpdatedPackages(packageJson, versionMismatches, cliVersion);
|
||||
await writeJSONFile(packageJsonPath, packageJson, true);
|
||||
|
||||
async function revertPackageJsonChanges() {
|
||||
await writeJSONFile(packageJsonPath, readonlyPackageJson, true);
|
||||
await removeFile(packageJsonBackupPath);
|
||||
}
|
||||
|
||||
installSpinner.message("Installing new package versions");
|
||||
|
||||
const jsProject = new JavascriptProject(projectPath);
|
||||
|
||||
let packageManager: PackageManager | undefined;
|
||||
|
||||
try {
|
||||
packageManager = await jsProject.getPackageManager();
|
||||
|
||||
installSpinner.message(`Installing new package versions with ${packageManager}`);
|
||||
|
||||
await jsProject.install();
|
||||
} catch (error) {
|
||||
installSpinner.stop(
|
||||
`Failed to install new package versions${packageManager ? ` with ${packageManager}` : ""}`
|
||||
);
|
||||
|
||||
// Remove exit handler in case of failure
|
||||
process.removeListener("exit", exitHandler);
|
||||
|
||||
await revertPackageJsonChanges();
|
||||
throw error;
|
||||
}
|
||||
|
||||
installSpinner.stop("Installed new package versions");
|
||||
|
||||
// Remove exit handler once packages have been updated, also delete backup file
|
||||
process.removeListener("exit", exitHandler);
|
||||
await removeFile(packageJsonBackupPath);
|
||||
|
||||
if (!embedded) {
|
||||
outro(
|
||||
`Packages updated${newCliVersion ? " ..but you should really update your CLI too!" : ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type Dependency = {
|
||||
type: "dependencies" | "devDependencies";
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
function getTriggerDependencies(packageJson: PackageJson): Dependency[] {
|
||||
const deps: Dependency[] = [];
|
||||
|
||||
for (const type of ["dependencies", "devDependencies"] as const) {
|
||||
for (const [name, version] of Object.entries(packageJson[type] ?? {})) {
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (version.startsWith("workspace")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!triggerPackageFilter.test(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ignoredPackages = ["@trigger.dev/companyicons"];
|
||||
|
||||
if (ignoredPackages.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deps.push({ type, name, version });
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function mutatePackageJsonWithUpdatedPackages(
|
||||
packageJson: PackageJson,
|
||||
depsToUpdate: Dependency[],
|
||||
targetVersion: string
|
||||
) {
|
||||
for (const { type, name, version } of depsToUpdate) {
|
||||
if (!packageJson[type]) {
|
||||
throw new Error(
|
||||
`No ${type} entry found in package.json. Please try to upgrade manually instead.`
|
||||
);
|
||||
}
|
||||
|
||||
packageJson[type]![name] = targetVersion;
|
||||
}
|
||||
}
|
||||
|
||||
function printUpdateTable(depsToUpdate: Dependency[], targetVersion: string): void {
|
||||
log.message("Suggested updates");
|
||||
|
||||
const tableData = depsToUpdate.map((dep) => ({
|
||||
package: dep.name,
|
||||
old: dep.version,
|
||||
new: targetVersion,
|
||||
}));
|
||||
|
||||
logger.table(tableData);
|
||||
}
|
||||
|
||||
async function updateConfirmation(depsToUpdate: Dependency[], targetVersion: string) {
|
||||
printUpdateTable(depsToUpdate, targetVersion);
|
||||
|
||||
let confirmMessage = "Would you like to apply those updates?";
|
||||
|
||||
return await confirm({
|
||||
message: confirmMessage,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPackageJson(absoluteProjectPath: string) {
|
||||
const packageJsonPath = join(absoluteProjectPath, "package.json");
|
||||
|
||||
const readonlyPackageJson = Object.freeze((await readJSONFile(packageJsonPath)) as PackageJson);
|
||||
|
||||
const packageJson = structuredClone(readonlyPackageJson);
|
||||
|
||||
return { packageJson, readonlyPackageJson, packageJsonPath };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function assertExhaustive(x: never): never {
|
||||
throw new Error("Unexpected object: " + x);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import chalk from "chalk";
|
||||
|
||||
export const green = "#4FFF54";
|
||||
@@ -63,3 +64,26 @@ export function prettyPrintDate(date: Date = new Date()) {
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
export function prettyWarning(header: string, body?: string, footer?: string) {
|
||||
const prefix = "Warning: ";
|
||||
const indent = Array(prefix.length).fill(" ").join("");
|
||||
const spacing = "\n\n";
|
||||
|
||||
const prettyPrefix = chalkWarning(prefix);
|
||||
|
||||
const withIndents = (text?: string) =>
|
||||
text
|
||||
?.split("\n")
|
||||
.map((line) => `${indent}${line}`)
|
||||
.join("\n");
|
||||
|
||||
const prettyBody = withIndents(body);
|
||||
const prettyFooter = withIndents(footer);
|
||||
|
||||
log.warn(
|
||||
`${prettyPrefix}${header}${prettyBody ? `${spacing}${prettyBody}` : ""}${
|
||||
prettyFooter ? `${spacing}${prettyFooter}` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ export async function safeFeadJSONFile(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJSONFile(path: string, json: any) {
|
||||
await writeFile(path, JSON.stringify(json), "utf8");
|
||||
export async function writeJSONFile(path: string, json: any, pretty = false) {
|
||||
await writeFile(path, JSON.stringify(json, undefined, pretty ? 2 : undefined), "utf8");
|
||||
}
|
||||
|
||||
export function readJSONFileSync(path: string) {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { findUp } from "find-up";
|
||||
import { basename } from "path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export type PackageManager = "npm" | "pnpm" | "yarn";
|
||||
|
||||
export async function getUserPackageManager(path: string): Promise<PackageManager> {
|
||||
const packageManager = await detectPackageManager(path);
|
||||
logger.debug("Detected package manager", { packageManager });
|
||||
return packageManager;
|
||||
}
|
||||
|
||||
async function detectPackageManager(path: string): Promise<PackageManager> {
|
||||
try {
|
||||
return await detectPackageManagerFromArtifacts(path);
|
||||
} catch (error) {
|
||||
@@ -29,19 +37,30 @@ function detectPackageManagerFromCurrentCommand(): PackageManager {
|
||||
}
|
||||
|
||||
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,
|
||||
];
|
||||
const artifacts = {
|
||||
yarn: "yarn.lock",
|
||||
pnpm: "pnpm-lock.yaml",
|
||||
npm: "package-lock.json",
|
||||
npmShrinkwrap: "npm-shrinkwrap.json",
|
||||
};
|
||||
|
||||
for (const { name, pm } of packageFiles) {
|
||||
const foundPath = await findUp(name, { cwd: path });
|
||||
if (typeof foundPath === "string") {
|
||||
return pm;
|
||||
}
|
||||
const foundPath = await findUp(Object.values(artifacts), { cwd: path });
|
||||
|
||||
if (!foundPath) {
|
||||
throw new Error("Could not detect package manager from artifacts");
|
||||
}
|
||||
|
||||
throw new Error("Could not detect package manager from artifacts");
|
||||
logger.debug("Found path from package manager artifacts", { foundPath });
|
||||
|
||||
switch (basename(foundPath)) {
|
||||
case artifacts.yarn:
|
||||
return "yarn";
|
||||
case artifacts.pnpm:
|
||||
return "pnpm";
|
||||
case artifacts.npm:
|
||||
case artifacts.npmShrinkwrap:
|
||||
return "npm";
|
||||
default:
|
||||
throw new Error(`Unhandled package manager detection path: ${foundPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { logger } from "./logger.js";
|
||||
import { spinner } from "./windows";
|
||||
|
||||
export async function printInitialBanner(performUpdateCheck = true) {
|
||||
const packageVersion = getVersion();
|
||||
const text = `\n${logo()} ${chalkGrey(`(${packageVersion})`)}\n`;
|
||||
const cliVersion = getVersion();
|
||||
const text = `\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`;
|
||||
|
||||
logger.info(text);
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function printInitialBanner(performUpdateCheck = true) {
|
||||
// Log a slightly more noticeable message if this is a major bump
|
||||
if (maybeNewVersion !== undefined) {
|
||||
loadingSpinner.stop(`Update available ${chalk.green(maybeNewVersion)}`);
|
||||
const currentMajor = parseInt(packageVersion.split(".")[0]!);
|
||||
const currentMajor = parseInt(cliVersion.split(".")[0]!);
|
||||
const newMajor = parseInt(maybeNewVersion.split(".")[0]!);
|
||||
if (newMajor > currentMajor) {
|
||||
logger.warn(
|
||||
@@ -38,9 +38,9 @@ After installation, run Trigger.dev with \`npx trigger.dev\`.`
|
||||
}
|
||||
|
||||
export async function printStandloneInitialBanner(performUpdateCheck = true) {
|
||||
const packageVersion = getVersion();
|
||||
const cliVersion = getVersion();
|
||||
|
||||
logger.log(`\n${logo()} ${chalkGrey("(v3 Developer Preview)")}`);
|
||||
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`);
|
||||
|
||||
if (performUpdateCheck) {
|
||||
const maybeNewVersion = await updateCheck();
|
||||
@@ -54,7 +54,11 @@ export async function printStandloneInitialBanner(performUpdateCheck = true) {
|
||||
logger.log(`${chalkGrey("-".repeat(54))}`);
|
||||
}
|
||||
|
||||
export function printDevBanner() {
|
||||
export function printDevBanner(printTopBorder = true) {
|
||||
if (printTopBorder) {
|
||||
logger.log(chalkGrey("-".repeat(54)));
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`${chalkGrey("Key:")} ${chalkWorker("Version")} ${chalkGrey("|")} ${chalkTask(
|
||||
"Task"
|
||||
@@ -68,7 +72,7 @@ async function doUpdateCheck(): Promise<string | undefined> {
|
||||
try {
|
||||
// default cache for update check is 1 day
|
||||
update = await checkForUpdate(pkg, {
|
||||
distTag: pkg.version.startsWith("0.0.0") ? "beta" : "latest",
|
||||
distTag: pkg.version.startsWith("3.0.0-beta") ? "beta" : "latest",
|
||||
});
|
||||
} catch (err) {
|
||||
// ignore error
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { execa } from "execa";
|
||||
import { getUserPackageManager, type PackageManager } from "./getUserPackageManager.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { spinner } from "./windows.js";
|
||||
|
||||
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 || spinner()).stop(chalk.green("Successfully installed dependencies!\n"));
|
||||
}
|
||||
|
||||
async function runInstallCommand(
|
||||
pkgManager: PackageManager,
|
||||
projectDir: string
|
||||
): Promise<ReturnType<typeof spinner> | 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 loadingSpinner = spinner();
|
||||
loadingSpinner.start("Running pnpm install...");
|
||||
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")) {
|
||||
loadingSpinner.message(text.includes("|") ? text.split(" | ")[1] ?? "" : text);
|
||||
}
|
||||
});
|
||||
pnpmSubprocess.on("error", (e) => rej(e));
|
||||
pnpmSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return loadingSpinner;
|
||||
}
|
||||
case "yarn": {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Running yarn...");
|
||||
const yarnSubprocess = execa(pkgManager, [], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
yarnSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
loadingSpinner.message(data.toString());
|
||||
});
|
||||
yarnSubprocess.on("error", (e) => rej(e));
|
||||
yarnSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return loadingSpinner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,17 +25,6 @@ export async function installPackages(
|
||||
);
|
||||
}
|
||||
|
||||
async function getPackageVersion(path: string) {
|
||||
try {
|
||||
const packageJsonPath = join(path, "package.json");
|
||||
const packageJson = await readJSONFile(packageJsonPath);
|
||||
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Expects path to be in the format:
|
||||
// - source-map-support/register.js
|
||||
// - @opentelemetry/api
|
||||
|
||||
@@ -3,6 +3,8 @@ import { join } from "node:path";
|
||||
import { readJSONFileSync } from "./fileSystem";
|
||||
import { logger } from "./logger";
|
||||
import { PackageManager, getUserPackageManager } from "./getUserPackageManager";
|
||||
import { PackageJson } from "type-fest";
|
||||
import { assertExhaustive } from "./assertExhaustive";
|
||||
|
||||
export type ResolveOptions = { allowDev: boolean };
|
||||
|
||||
@@ -49,14 +51,14 @@ const BuiltInModules = new Set([
|
||||
]);
|
||||
|
||||
export class JavascriptProject {
|
||||
private _packageJson?: any;
|
||||
private _packageJson?: PackageJson;
|
||||
private _packageManager?: PackageManager;
|
||||
|
||||
constructor(private projectPath: string) {}
|
||||
|
||||
private get packageJson() {
|
||||
if (!this._packageJson) {
|
||||
this._packageJson = readJSONFileSync(join(this.projectPath, "package.json"));
|
||||
this._packageJson = readJSONFileSync(join(this.projectPath, "package.json")) as PackageJson;
|
||||
}
|
||||
|
||||
return this._packageJson;
|
||||
@@ -64,21 +66,29 @@ export class JavascriptProject {
|
||||
|
||||
public get scripts(): Record<string, string> {
|
||||
return {
|
||||
postinstall: this.packageJson.scripts?.postinstall,
|
||||
postinstall: this.packageJson.scripts?.postinstall ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
async install(): Promise<void> {
|
||||
const command = await this.#getCommand();
|
||||
|
||||
try {
|
||||
await command.installDependencies({
|
||||
cwd: this.projectPath,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to install dependencies using ${command.name}`, {
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(packageName: string, options?: ResolveOptions): Promise<string | undefined> {
|
||||
if (BuiltInModules.has(packageName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this._packageManager) {
|
||||
this._packageManager = await getUserPackageManager(this.projectPath);
|
||||
}
|
||||
|
||||
const packageManager = this._packageManager;
|
||||
|
||||
const opts = { allowDev: false, ...options };
|
||||
|
||||
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
|
||||
@@ -95,12 +105,7 @@ export class JavascriptProject {
|
||||
}
|
||||
}
|
||||
|
||||
const command =
|
||||
packageManager === "npm"
|
||||
? new NPMCommands()
|
||||
: packageManager === "pnpm"
|
||||
? new PNPMCommands()
|
||||
: new YarnCommands();
|
||||
const command = await this.#getCommand();
|
||||
|
||||
try {
|
||||
const version = await command.resolveDependencyVersion(packageName, {
|
||||
@@ -117,6 +122,29 @@ export class JavascriptProject {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #getCommand(): Promise<PackageManagerCommands> {
|
||||
const packageManager = await this.getPackageManager();
|
||||
|
||||
switch (packageManager) {
|
||||
case "npm":
|
||||
return new NPMCommands();
|
||||
case "pnpm":
|
||||
return new PNPMCommands();
|
||||
case "yarn":
|
||||
return new YarnCommands();
|
||||
default:
|
||||
assertExhaustive(packageManager);
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageManager(): Promise<PackageManager> {
|
||||
if (!this._packageManager) {
|
||||
this._packageManager = await getUserPackageManager(this.projectPath);
|
||||
}
|
||||
|
||||
return this._packageManager;
|
||||
}
|
||||
}
|
||||
|
||||
type PnpmList = {
|
||||
@@ -140,6 +168,10 @@ type PackageManagerOptions = {
|
||||
};
|
||||
|
||||
interface PackageManagerCommands {
|
||||
name: string;
|
||||
|
||||
installDependencies(options: PackageManagerOptions): Promise<void>;
|
||||
|
||||
resolveDependencyVersion(
|
||||
packageName: string,
|
||||
options: PackageManagerOptions
|
||||
@@ -151,15 +183,21 @@ class PNPMCommands implements PackageManagerCommands {
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(
|
||||
packageName: string,
|
||||
options: PackageManagerOptions
|
||||
): Promise<string | undefined> {
|
||||
const cmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
const { stdout } = await $({ cwd: options.cwd })`${cmd} list ${packageName} -r --json`;
|
||||
private get cmd() {
|
||||
return process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
}
|
||||
|
||||
async installDependencies(options: PackageManagerOptions) {
|
||||
const { stdout, stderr } = await $({ cwd: options.cwd })`${this.cmd} install`;
|
||||
|
||||
logger.debug(`Installing dependencies using ${this.name}`, { stdout, stderr });
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(packageName: string, options: PackageManagerOptions) {
|
||||
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} list ${packageName} -r --json`;
|
||||
const result = JSON.parse(stdout) as PnpmList;
|
||||
|
||||
logger.debug(`Resolving ${packageName} version using pnpm`, { result });
|
||||
logger.debug(`Resolving ${packageName} version using ${this.name}`, { result });
|
||||
|
||||
// Return the first dependency version that matches the package name
|
||||
for (const dep of result) {
|
||||
@@ -189,15 +227,21 @@ class NPMCommands implements PackageManagerCommands {
|
||||
return "npm";
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(
|
||||
packageName: string,
|
||||
options: PackageManagerOptions
|
||||
): Promise<string | undefined> {
|
||||
const cmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const { stdout } = await $({ cwd: options.cwd })`${cmd} list ${packageName} --json`;
|
||||
private get cmd() {
|
||||
return process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
}
|
||||
|
||||
async installDependencies(options: PackageManagerOptions) {
|
||||
const { stdout, stderr } = await $({ cwd: options.cwd })`${this.cmd} install`;
|
||||
|
||||
logger.debug(`Installing dependencies using ${this.name}`, { stdout, stderr });
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(packageName: string, options: PackageManagerOptions) {
|
||||
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} list ${packageName} --json`;
|
||||
const output = JSON.parse(stdout) as NpmListOutput;
|
||||
|
||||
logger.debug(`Resolving ${packageName} version using npm`, { output });
|
||||
logger.debug(`Resolving ${packageName} version using ${this.name}`, { output });
|
||||
|
||||
return this.#recursivelySearchDependencies(output.dependencies, packageName);
|
||||
}
|
||||
@@ -227,17 +271,22 @@ class YarnCommands implements PackageManagerCommands {
|
||||
return "yarn";
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(
|
||||
packageName: string,
|
||||
options: PackageManagerOptions
|
||||
): Promise<string | undefined> {
|
||||
const cmd = process.platform === "win32" ? "yarn.cmd" : "yarn";
|
||||
private get cmd() {
|
||||
return process.platform === "win32" ? "yarn.cmd" : "yarn";
|
||||
}
|
||||
|
||||
const { stdout } = await $({ cwd: options.cwd })`${cmd} info ${packageName} --json`;
|
||||
async installDependencies(options: PackageManagerOptions) {
|
||||
const { stdout, stderr } = await $({ cwd: options.cwd })`${this.cmd} install`;
|
||||
|
||||
logger.debug(`Installing dependencies using ${this.name}`, { stdout, stderr });
|
||||
}
|
||||
|
||||
async resolveDependencyVersion(packageName: string, options: PackageManagerOptions) {
|
||||
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} info ${packageName} --json`;
|
||||
|
||||
const lines = stdout.split("\n");
|
||||
|
||||
logger.debug(`Resolving ${packageName} version using yarn`, { lines });
|
||||
logger.debug(`Resolving ${packageName} version using ${this.name}`, { lines });
|
||||
|
||||
for (const line of lines) {
|
||||
const json = JSON.parse(line);
|
||||
|
||||
Generated
-3
@@ -1561,9 +1561,6 @@ importers:
|
||||
node-fetch:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
npm-check-updates:
|
||||
specifier: ^16.12.2
|
||||
version: 16.12.3
|
||||
object-hash:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
|
||||
Reference in New Issue
Block a user