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:
nicktrn
2024-04-23 16:08:46 +01:00
committed by GitHub
parent 3913e57ef4
commit b207601732
20 changed files with 496 additions and 304 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
v3 CLI update command and package manager detection fix
+2
View File
@@ -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
+8 -5
View File
@@ -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
+2
View File
@@ -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.
+18
View File
@@ -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.
+1 -19
View File
@@ -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
-1
View File
@@ -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",
+2
View File
@@ -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);
+7
View File
@@ -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,
+10 -1
View File
@@ -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 });
+285 -133
View File
@@ -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}` : ""
}`
);
}
+2 -2
View File
@@ -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}`);
}
}
+11 -7
View File
@@ -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);
-3
View File
@@ -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