v3 CLI compiling E2E test suite (#1135)

* Boilerplate server-only use case

* wip: integration suite instrumentation setup

* Working poc testing compileProject

* Add pnpm script to run e2e tests only

* Use vitest globals

* Remove commented line

* Remove useless export

* Add modifier to test only one fixture project

* Handle package manager and log level choice

* Update server-only example

* Setup / teardown + split compile for package manager capabilities

* Ignore yarn files

* Fix issue with corepack, store version in engines field

* Rename test file

* Fix npm updates yarn.lock

* Move typecheking in a dedicated test

* Stop bundling the compile command to allow for more granular testing

* Put config resolving in separate test

* Add no-config test case and add test case expected errors configuration

* Add wantCompilationError option

* Add dependencies handling

* Use packageManager passed as option to resolve required deps

* Remove unused guard clauses

* Add postinstall & hash handling step

* Add worker start test

* Handle yarn.lock copy renaming on sigterm and sigkill

* Update vitest and use concurrent option

* Add a readme file

* Add CI workflow

* Fix handle cli deps

* Run cli v3 e2e tests on publish action

* Increase timeout on deps resolving step

* Add changeset

* Remove .pnp.cjs as we use yarn with nodeLinker node-modules

* Add missing .yarnrc.yml file

* No need to build CLI to run E2E tests

* Remove bun.lockb files

* Update beige-pears-explode.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
This commit is contained in:
Émile Ré
2024-06-14 14:28:36 +04:00
committed by GitHub
parent e08b4569e5
commit d4ccdf7105
45 changed files with 11635 additions and 36 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Add an e2e suite to test compiling with v3 CLI.
+47 -3
View File
@@ -1,9 +1,53 @@
name: "🧪 E2E Tests"
name: "E2E"
on:
workflow_call:
inputs:
package:
description: The identifier of the job to run
default: webapp
required: false
type: string
jobs:
e2e:
name: "🧪 E2E Tests"
cli-v3:
name: "🧪 CLI v3 tests"
if: inputs.package == 'cli-v3' || inputs.package == ''
runs-on: buildjet-8vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
package-manager: ["npm", "pnpm", "yarn"]
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 8.15.5
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 20.11.1
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile --filter trigger.dev...
- name: 🔧 Build v3 cli monorepo dependencies
run: pnpm run build --filter trigger.dev^...
- name: 🔧 Build worker template files
run: pnpm --filter trigger.dev run build:workers
- name: Run E2E Tests
run: |
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
webapp:
name: "🧪 Webapp tests"
if: inputs.package == 'webapp' || inputs.package == ''
runs-on: buildjet-16vcpu-ubuntu-2204
steps:
- name: 🐳 Login to Docker Hub
+2
View File
@@ -29,4 +29,6 @@ jobs:
# e2e:
# uses: ./.github/workflows/e2e.yml
# with:
# package: webapp
# secrets: inherit
+5 -3
View File
@@ -49,9 +49,11 @@ jobs:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
# e2e:
# uses: ./.github/workflows/e2e.yml
# secrets: inherit
e2e:
uses: ./.github/workflows/e2e.yml
with:
package: cli-v3
secrets: inherit
publish:
needs: [typecheck, units]
+177
View File
@@ -0,0 +1,177 @@
# Trigger.dev CLI E2E suite
E2E test suite for the Trigger.dev v3 CLI.
Note: this only works with Trigger.dev v3 projects and later. There is no E2E test suite for the [@trigger.dev/cli](https://www.npmjs.com/package/@trigger.dev/cli) package yet.
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly in your existing project.
## Description
This suite aims to test the outputs fo the `triggerdev deploy` command.
To do so, it runs the deploy code against fixture projects that are located under `packages/cli-v3/e2e/fixtures/`.
Those fixtures reproduce minimal project structure and contents, in order to reproduce known bugs and run fast.
**Notes**
- The suite uses vitest
- Everything happens locally
- There is no login required
- There is not real project reference needed
- No docker image is created or built, instead, the bundled worker file is started with node directly inside the vitest process
## Usage
If you have not done it yet, build the CLI:
```sh
pnpm run build --filter trigger.dev
```
Then, run the v3 CLI E2E test suite:
```sh
pnpm --filter trigger.dev run test:e2e
```
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------- |
| `MOD=<fixture-name>` | The name of any folder directly nested under `packages/cli-v3/e2e/fixtures/` |
| `PM=<package-manager>` | The package manager to use. One of `npm`, `pnpm`, `yarn`. Defaults to `npm` |
Example:
```sh
MOD=server-only PM=yarn pnpm --filter trigger.dev run test:e2e
```
This will run the test suite for the `server-only` fixture using `yarn` to install and resolve dependencies.
## Debugging
When debugging an issue with the `triggerdev deploy` or `triggerdev dev` command, it is recommended to reproduce it with a minimal project fixture in the e2e suite.
Check [Adding a fixture](#adding-a-fixture) for more information.
Then run:
```sh
MOD=<fixture-name> pnpm run test:e2e
```
This will test your fixture project, and generate outputs in the `packages/cli-v3/e2e/fixtures/<fixture-name>/.trigger` folder, so you can easily debug.
## Adding a fixture
1. Create a new `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
It will hold the project to test.
2. Add a `package.json` file in your `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
Use the following template:
```json package.json
{
"name": "<fixture-name>",
"private": true,
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"packageManager": "pnpm@8.15.5"
}
```
> The `engines` field is used to store the versions of pnpm and yarn to use when running the suite.
3. Add an empty `pnpm-workspace.yaml` in your `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
This is necessary to prevent the Trigger.dev monorepo from handling this project.
Please check https://github.com/pnpm/pnpm/issues/2412 for more inforation.
4. Add an empty `yarn.lock` in your fixture folder.
This is necessary to allow to use `yarn` without having a warning on the current project being a `pnpm` project.
5. Install the fixture dependencies and generate lockfiles.
Like you would in any project.
E.g. if your fixture contains a trigger task that uses the `jsdom` library:
```sh
cd packages/cli-v3/e2e/fixtures/<fixture-name>
corepack use pnpm@8.15.5
pnpm install jsdom
```
> This will update the `package.json` and generate the `pnpm-lock.yaml` file.
6. To run the test suite against multiple package manager, we need to generate the other lockfiles.
```sh
cd packages/cli-v3/e2e/fixtures/<fixture-name>
rm -rf node_modules
npm install
rm -rf node_modules
corepack use yarn # will update the yarn lockfile
```
> Do it in this order, otherwise `npm install` will update the existing `yarn.lock` file with legacy version 1.
7. Create a new `packages/cli-v3/e2e/fixtures/trigger` folder, and create a trigger task in it.
Here is an example:
```javascript
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log("Hello, World!", payload);
},
});
```
8. Add a trigger configuration file.
The configuration file is mandatory here, the E2E suite does not execute `trigger.dev` commands.
```javascript
export const config = {
project: "<fixture-name>",
triggerDirectories: ["./trigger"],
};
```
> The project reference can be anything here, as the suite runs locally without connecting to the platform.
9. Commit your changes.
10. Add your fixture test configuration in `testCases.json`.
```json testCases.json
[
...
{
"name": "<fixture-name>",
},
...
]
```
You can configure your test case by adding other properties to the JSON object. Here is the `TestCase` type for reference:
```typescript
type TestCase = {
name: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantBadConfigError?: boolean;
wantCompilationError?: boolean;
wantWorkerError?: boolean;
wantDependenciesError?: boolean;
wantInstallationError?: boolean;
};
```
> You might expect a specific error at a specific test, so use those configuration option at your discretion.
+243
View File
@@ -0,0 +1,243 @@
import { esbuildDecorators } from "@anatine/esbuild-decorators";
import { build } from "esbuild";
import { readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { basename, join, posix, resolve } from "node:path";
import invariant from "tiny-invariant";
import {
bundleDependenciesPlugin,
mockServerOnlyPlugin,
workerSetupImportConfigPlugin,
} from "../src/utilities/build.js";
import { ReadConfigResult } from "../src/utilities/configFiles.js";
import { writeJSONFile } from "../src/utilities/fileSystem.js";
import { logger } from "../src/utilities/logger.js";
import { createTaskFileImports, gatherTaskFiles } from "../src/utilities/taskFiles.js";
import { escapeImportPath } from "../src/utilities/windows.js";
type CompileOptions = {
outputMetafile?: string;
resolvedConfig: ReadConfigResult;
tempDir: string;
};
export async function compile(options: CompileOptions) {
if (options.resolvedConfig.status === "error") {
throw new Error("cannot resolve config");
}
const {
tempDir,
resolvedConfig: { config },
} = options;
const configPath =
options.resolvedConfig.status === "file" ? options.resolvedConfig.path : undefined;
// COPIED FROM compileProject()
// const compileSpinner = spinner();
// compileSpinner.start(`Building project in ${config.projectDir}`);
const taskFiles = await gatherTaskFiles(config);
const workerFacade = readFileSync(
resolve("./dist/workers/prod/worker-facade.js"),
// join(cliRootPath(), "workers", "prod", "worker-facade.js"),
"utf-8"
);
// const workerSetupPath = join(cliRootPath(), "workers", "prod", "worker-setup.js");
const workerSetupPath = resolve("./dist/workers/prod/worker-setup.js");
let workerContents = workerFacade
.replace("__TASKS__", createTaskFileImports(taskFiles))
.replace(
"__WORKER_SETUP__",
`import { tracingSDK, otelTracer, otelLogger } from "${escapeImportPath(workerSetupPath)}";`
);
if (configPath) {
logger.debug("Importing project config from", { configPath });
workerContents = workerContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`import * as importedConfigExports from "${escapeImportPath(
configPath
)}"; const importedConfig = importedConfigExports.config; const handleError = importedConfigExports.handleError;`
);
} else {
workerContents = workerContents.replace(
"__IMPORTED_PROJECT_CONFIG__",
`const importedConfig = undefined; const handleError = undefined;`
);
}
const result = await build({
stdin: {
contents: workerContents,
// resolveDir: process.cwd(),
resolveDir: config.projectDir,
sourcefile: "__entryPoint.ts",
},
bundle: true,
metafile: true,
write: false,
minify: false,
sourcemap: "external", // does not set the //# sourceMappingURL= comment in the file, we handle it ourselves
logLevel: "error",
platform: "node",
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
target: ["node18", "es2020"],
// outdir: "out",
outdir: resolve(config.projectDir, "out"),
// banner: {
// js: `process.on("uncaughtException", function(error, origin) { if (error instanceof Error) { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: error.name, message: error.message, stack: error.stack }, origin }, version: "v1" } }); } else { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: "Error", message: typeof error === "string" ? error : JSON.stringify(error) }, origin }, version: "v1" } }); } });`,
// },
footer: {
js: "process.exit();",
},
define: {
TRIGGER_API_URL: `"${config.triggerUrl}"`,
__PROJECT_CONFIG__: JSON.stringify(config),
},
plugins: [
mockServerOnlyPlugin(),
bundleDependenciesPlugin("workerFacade", config.dependenciesToBundle, config.tsconfigPath),
workerSetupImportConfigPlugin(configPath),
esbuildDecorators({
tsconfig: config.tsconfigPath,
tsx: true,
force: false,
}),
],
});
if (result.errors.length > 0) {
// compileSpinner.stop("Build failed, aborting deployment");
// span.setAttributes({
// "build.workerErrors": result.errors.map(
// (error) => `Error: ${error.text} at ${error.location?.file}`
// ),
// });
throw new Error("Build failed, aborting deployment");
}
if (options.outputMetafile) {
await writeJSONFile(join(options.outputMetafile, "worker.json"), result.metafile);
}
const entryPointContents = readFileSync(
resolve("./dist/workers/prod/entry-point.js"),
// join(cliRootPath(), "workers", "prod", "entry-point.js"),
"utf-8"
);
const entryPointResult = await build({
stdin: {
contents: entryPointContents,
// resolveDir: process.cwd(),
resolveDir: config.projectDir,
sourcefile: "index.ts",
},
bundle: true,
metafile: true,
write: false,
minify: false,
sourcemap: false,
logLevel: "error",
platform: "node",
packages: "external",
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
target: ["node18", "es2020"],
// outdir: "out",
outdir: resolve(config.projectDir, "out"),
define: {
__PROJECT_CONFIG__: JSON.stringify(config),
},
plugins: [
bundleDependenciesPlugin("entryPoint.ts", config.dependenciesToBundle, config.tsconfigPath),
],
});
if (entryPointResult.errors.length > 0) {
// compileSpinner.stop("Build failed, aborting deployment");
// span.setAttributes({
// "build.entryPointErrors": entryPointResult.errors.map(
// (error) => `Error: ${error.text} at ${error.location?.file}`
// ),
// });
throw new Error("Build failed, aborting deployment");
}
if (options.outputMetafile) {
await writeJSONFile(
join(options.outputMetafile, "entry-point.json"),
entryPointResult.metafile
);
}
// Create a tmp directory to store the build
// const tempDir = await createTempDir();
logger.debug(`Writing compiled files to ${tempDir}`);
// Get the metaOutput for the result build
// const metaOutput = result.metafile!.outputs[posix.join("out", "stdin.js")];
const metaOutput =
result.metafile!.outputs[
posix.join("e2e", "fixtures", basename(config.projectDir), "out", "stdin.js")
];
invariant(metaOutput, "Meta output for the result build is missing");
// Get the metaOutput for the entryPoint build
// const entryPointMetaOutput =
// entryPointResult.metafile!.outputs[posix.join("out", "stdin.js")];
const entryPointMetaOutput =
entryPointResult.metafile!.outputs[
posix.join("e2e", "fixtures", basename(config.projectDir), "out", "stdin.js")
];
invariant(entryPointMetaOutput, "Meta output for the entryPoint build is missing");
// Get the outputFile and the sourceMapFile for the result build
const workerOutputFile = result.outputFiles.find(
(file) => file.path === join(config.projectDir, "out", "stdin.js")
);
invariant(workerOutputFile, "Output file for the result build is missing");
const workerSourcemapFile = result.outputFiles.find(
(file) => file.path === join(config.projectDir, "out", "stdin.js.map")
);
invariant(workerSourcemapFile, "Sourcemap file for the result build is missing");
// Get the outputFile for the entryPoint build
const entryPointOutputFile = entryPointResult.outputFiles.find(
(file) => file.path === join(config.projectDir, "out", "stdin.js")
);
invariant(entryPointOutputFile, "Output file for the entryPoint build is missing");
// Save the result outputFile to /tmp/dir/worker.js (and make sure to map the sourceMap to the correct location in the file)
await writeFile(
join(tempDir, "worker.js"),
`${workerOutputFile.text}\n//# sourceMappingURL=worker.js.map`
);
// Save the sourceMapFile to /tmp/dir/worker.js.map
await writeFile(join(tempDir, "worker.js.map"), workerSourcemapFile.text);
// Save the entryPoint outputFile to /tmp/dir/index.js
await writeFile(join(tempDir, "index.js"), entryPointOutputFile.text);
return {
workerMetaOutput: metaOutput,
workerOutputFile,
entryPointMetaOutput,
entryPointOutputFile,
};
}
@@ -0,0 +1,38 @@
import { readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { cliRootPath } from "../src/utilities/resolveInternalFilePath";
import { ReadConfigResult } from "../src/utilities/configFiles";
type CreateContainerFileOptions = {
resolvedConfig: ReadConfigResult;
tempDir: string;
};
export async function createContainerFile(options: CreateContainerFileOptions) {
if (options.resolvedConfig.status === "error") {
throw new Error("cannot resolve config");
}
const {
resolvedConfig: { config },
tempDir,
} = options;
// COPIED FROM compileProject()
// Write the Containerfile to /mpt / dir / Containerfile;
// const containerFilePath = join(cliRootPath(), "Containerfile.prod");
const containerFilePath = resolve("./src/Containerfile.prod");
let containerFileContents = readFileSync(containerFilePath, "utf-8");
if (config.postInstall) {
containerFileContents = containerFileContents.replace(
"__POST_INSTALL__",
`RUN ${config.postInstall}`
);
} else {
containerFileContents = containerFileContents.replace("__POST_INSTALL__", "");
}
await writeFile(join(tempDir, "Containerfile"), containerFileContents);
}
+28
View File
@@ -0,0 +1,28 @@
import { createHash } from "node:crypto";
import { OutputFile } from "esbuild";
type CreateDeployHashOptions = {
dependencies: { [k: string]: string };
entryPointOutputFile: OutputFile;
workerOutputFile: OutputFile;
};
export async function createDeployHash(options: CreateDeployHashOptions) {
const { entryPointOutputFile, workerOutputFile } = options;
// COPIED FROM compileProject()
const contentHasher = createHash("sha256");
contentHasher.update(Buffer.from(entryPointOutputFile.text));
contentHasher.update(Buffer.from(workerOutputFile.text));
contentHasher.update(Buffer.from(JSON.stringify(dependencies)));
const contentHash = contentHasher.digest("hex");
// span.setAttributes({
// contentHash: contentHash,
// });
// span.end();
return { contentHash };
}
+1
View File
@@ -0,0 +1 @@
.yarn
@@ -0,0 +1 @@
nodeLinker: node-modules
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"name": "server-only",
"private": true,
"dependencies": {
"@infisical/sdk": "^2.2.3",
"@trigger.dev/sdk": "3.0.0-beta.33"
},
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
# https://github.com/pnpm/pnpm/issues/2412
@@ -0,0 +1,4 @@
export const config = {
project: "infisical-sdk",
dependenciesToBundle: ["@infisical/sdk", "@infisical/sdk-darwin-arm64"],
};
@@ -0,0 +1,9 @@
import { task } from "@trigger.dev/sdk/v3";
import { LogLevel } from "@infisical/sdk";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log("Hello, World!", payload, LogLevel.Debug);
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
nodeLinker: node-modules
+14
View File
@@ -0,0 +1,14 @@
{
"name": "no-config",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "no-config",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
}
}
@@ -0,0 +1,9 @@
{
"name": "no-config",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
+9
View File
@@ -0,0 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.: {}
@@ -0,0 +1 @@
# https://github.com/pnpm/pnpm/issues/2412
@@ -0,0 +1,12 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"no-config@workspace:.":
version: 0.0.0-use.local
resolution: "no-config@workspace:."
languageName: unknown
linkType: soft
@@ -0,0 +1 @@
nodeLinker: node-modules
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"name": "server-only",
"private": true,
"dependencies": {
"@trigger.dev/sdk": "3.0.0-beta.33",
"server-only": "^0.0.1"
},
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
# https://github.com/pnpm/pnpm/issues/2412
@@ -0,0 +1,4 @@
export const config = {
project: "server-only",
triggerDirectories: ["./trigger"],
};
@@ -0,0 +1,9 @@
import "server-only";
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log("Hello, World!", payload);
},
});
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env node
import { log } from "@clack/prompts";
import { Metafile } from "esbuild";
import { join } from "node:path";
import terminalLink from "terminal-link";
import { SkipLoggingError } from "../src/cli/common.js";
import {
copyAdditionalFiles,
resolveDependencies,
resolveRequiredDependencies,
} from "../src/commands/deploy.js";
import { ReadConfigResult } from "../src/utilities/configFiles.js";
import { writeJSONFile } from "../src/utilities/fileSystem.js";
import { PackageManager } from "../src/utilities/getUserPackageManager.js";
import { JavascriptProject } from "../src/utilities/javascriptProject.js";
import { logger } from "../src/utilities/logger.js";
type HandleDependenciesOptions = {
entryPointMetaOutput: Metafile["outputs"]["out/stdin.js"];
metaOutput: Metafile["outputs"]["out/stdin.js"];
packageManager: PackageManager;
resolvedConfig: ReadConfigResult;
tempDir: string;
};
class JavascriptProjectLocal extends JavascriptProject {
constructor(
projectPath: string,
private overridenPackageManager: PackageManager
) {
super(projectPath);
}
async getPackageManager(): Promise<PackageManager> {
return Promise.resolve(this.overridenPackageManager);
}
}
export async function handleDependencies(options: HandleDependenciesOptions) {
if (options.resolvedConfig.status === "error") {
throw new Error("cannot resolve config");
}
const {
entryPointMetaOutput,
metaOutput,
packageManager,
resolvedConfig: { config },
tempDir,
} = options;
// COPIED FROM compileProject()
logger.debug("Getting the imports for the worker and entryPoint builds", {
workerImports: metaOutput.imports,
entryPointImports: entryPointMetaOutput.imports,
});
// Get all the required dependencies from the metaOutputs and save them to /tmp/dir/package.json
const allImports = [...metaOutput.imports, ...entryPointMetaOutput.imports];
// const javascriptProject = new JavascriptProject(config.projectDir);
const javascriptProject = new JavascriptProjectLocal(config.projectDir, packageManager);
const dependencies = await resolveRequiredDependencies(allImports, config, javascriptProject);
logger.debug("gatherRequiredDependencies()", { dependencies });
const packageJsonContents = {
name: "trigger-worker",
version: "0.0.0",
description: "",
dependencies,
scripts: {
...javascriptProject.scripts,
},
};
// span.setAttributes({
// ...flattenAttributes(packageJsonContents, "packageJson.contents"),
// });
await writeJSONFile(join(tempDir, "package.json"), packageJsonContents);
const copyResult = await copyAdditionalFiles(config, tempDir);
if (!copyResult.ok) {
// compileSpinner.stop("Project built with warnings");
log.warn(
`No additionalFiles matches for:\n\n${copyResult.noMatches
.map((glob) => `- "${glob}"`)
.join("\n")}\n\nIf this is unexpected you should check your ${terminalLink(
"glob patterns",
"https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer"
)} are valid.`
);
}
// } else {
// compileSpinner.stop("Project built successfully");
// }
const resolvingDependenciesResult = await resolveDependencies(
tempDir,
packageJsonContents,
config
);
if (!resolvingDependenciesResult) {
throw new SkipLoggingError("Failed to resolve dependencies");
}
return { dependencies };
}
+343
View File
@@ -0,0 +1,343 @@
import { execa, execaNode } from "execa";
import { readFileSync } from "node:fs";
import { mkdir, rename, rm } from "node:fs/promises";
import { join, resolve } from "node:path";
import { typecheckProject } from "../src/commands/deploy";
import { readConfig, ReadConfigFileResult } from "../src/utilities/configFiles";
import { PackageManager } from "../src/utilities/getUserPackageManager";
import { logger } from "../src/utilities/logger";
import { compile } from "./compile";
import { createContainerFile } from "./createContainerFile";
import { createDeployHash } from "./createDeployHash";
import { handleDependencies } from "./handleDependencies";
import { Loglevel, LogLevelSchema, PackageManagerSchema } from "./schemas";
import allTestCases from "./testCases.json";
type TestCase = {
name: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantBadConfigError?: boolean;
wantCompilationError?: boolean;
wantWorkerError?: boolean;
wantDependenciesError?: boolean;
wantInstallationError?: boolean;
};
const testCases: TestCase[] = process.env.MOD
? allTestCases.filter(({ name }) => process.env.MOD === name)
: allTestCases;
let logLevel: Loglevel = "log";
let packageManager: PackageManager = "npm";
try {
logLevel = LogLevelSchema.parse(process.env.LOG);
} catch (e) {
console.error(e);
console.log("Using default log level 'log'");
}
logger.loggerLevel = logLevel;
try {
packageManager = PackageManagerSchema.parse(process.env.PM);
} catch (e) {
console.error(e);
console.log("Using default package manager 'npm'");
}
if (testCases.length > 0) {
console.log(`Using ${packageManager}`);
describe.each(testCases)(
"fixture $name",
async ({
name,
skipTypecheck,
wantConfigNotFoundError,
wantBadConfigError,
wantCompilationError,
wantWorkerError,
wantDependenciesError,
wantInstallationError,
}: TestCase) => {
const fixtureDir = resolve(join(process.cwd(), "e2e/fixtures", name));
beforeAll(async () => {
await rm(resolve(join(fixtureDir, ".trigger")), { force: true, recursive: true });
await rm(resolve(join(fixtureDir, "node_modules")), { force: true, recursive: true });
if (packageManager === "npm") {
// `npm ci` & `npm install` will update an existing yarn.lock
try {
await rename(
resolve(join(fixtureDir, "yarn.lock")),
resolve(join(fixtureDir, "yarn.lock.copy"))
);
} catch (e) {
await rename(
resolve(join(fixtureDir, "yarn.lock.copy")),
resolve(join(fixtureDir, "yarn.lock"))
);
}
}
});
afterAll(async () => {
if (packageManager === "npm") {
try {
await rename(
resolve(join(fixtureDir, "yarn.lock.copy")),
resolve(join(fixtureDir, "yarn.lock"))
);
} catch {}
}
});
test(
"installs",
async () => {
await expect(
(async () => {
if (["pnpm", "yarn"].includes(packageManager)) {
const buffer = readFileSync(resolve(join(fixtureDir, "package.json")), "utf8");
const pkgJSON = JSON.parse(buffer.toString());
const version = pkgJSON.engines[packageManager];
console.log(
`Detected ${packageManager}@${version} from package.json 'engines' field`
);
const { stdout, stderr } = await execa(
"corepack",
["use", `${packageManager}@${version}`],
{
cwd: fixtureDir,
}
);
console.log(stdout);
if (stderr) console.error(stderr);
} else {
const { stdout, stderr } = await execa(
packageManager,
installArgs(packageManager),
{
cwd: fixtureDir,
}
);
console.log(stdout);
if (stderr) console.error(stderr);
}
})()
).resolves.not.toThrowError();
},
{ timeout: 60_000 }
);
test(
wantConfigNotFoundError || wantBadConfigError
? "does not resolve config"
: "resolves config",
async () => {
const expectation = expect(
(async () => {
global.resolvedConfig = await readConfig(fixtureDir, { cwd: fixtureDir });
})()
);
if (wantConfigNotFoundError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
}
if (wantBadConfigError) {
expect(global.resolvedConfig).toBe("error");
} else {
expect(global.resolvedConfig).not.toBe("error");
}
}
);
describe.skipIf(wantConfigNotFoundError || wantBadConfigError)("with resolved config", () => {
beforeAll(async () => {
global.tempDir = await mkdir(
join((global.resolvedConfig as ReadConfigFileResult).config.projectDir, ".trigger"),
{ recursive: true }
);
});
afterAll(() => {
delete global.tempDir;
delete global.resolvedConfig;
});
test.skipIf(skipTypecheck).concurrent("typechecks", async () => {
await expect(
(async () =>
await typecheckProject((global.resolvedConfig as ReadConfigFileResult).config))()
).resolves.not.toThrowError();
});
test.concurrent(
wantCompilationError ? "does not compile" : "compiles",
async () => {
const expectation = expect(
(async () => {
const {
workerMetaOutput,
workerOutputFile,
entryPointMetaOutput,
entryPointOutputFile,
} = await compile({
resolvedConfig: global.resolvedConfig!,
tempDir: global.tempDir!,
});
global.entryPointMetaOutput = entryPointMetaOutput;
global.entryPointOutputFile = entryPointOutputFile;
global.workerMetaOutput = workerMetaOutput;
global.workerOutputFile = workerOutputFile;
})()
);
if (wantCompilationError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
}
},
{ timeout: 60_000 }
);
describe.skipIf(wantCompilationError)("with successful compilation", () => {
afterAll(() => {
delete global.entryPointMetaOutput;
delete global.entryPointOutputFile;
delete global.workerMetaOutput;
delete global.workerOutputFile;
});
test(
wantDependenciesError ? "does not resolve dependencies" : "resolves dependencies",
async () => {
const expectation = expect(
(async () => {
const { dependencies } = await handleDependencies({
entryPointMetaOutput: global.entryPointMetaOutput!,
metaOutput: global.workerMetaOutput!,
resolvedConfig: global.resolvedConfig!,
tempDir: global.tempDir!,
packageManager,
});
global.dependencies = dependencies;
})()
);
if (wantDependenciesError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
}
},
{ timeout: 120_000 }
);
describe.skipIf(wantDependenciesError)("with resolved dependencies", () => {
afterAll(() => {
delete global.dependencies;
});
test.concurrent("copies postinstall command into Containerfile.prod", async () => {
await expect(
(async () => {
await createContainerFile({
resolvedConfig: global.resolvedConfig!,
tempDir: global.tempDir!,
});
})()
).resolves.not.toThrowError();
});
test.concurrent("creates deploy hash", async () => {
await expect(
(async () => {
await createDeployHash({
dependencies: global.dependencies!,
entryPointOutputFile: global.entryPointOutputFile!,
workerOutputFile: global.workerOutputFile!,
});
})()
).resolves.not.toThrowError();
});
describe("with Containerfile ready", () => {
test(
"installs dependencies",
async () => {
const expectation = expect(
(async () => {
const { stdout, stderr } = await execa(
"npm",
["ci", "--no-audit", "--no-fund"],
{
cwd: resolve(join(fixtureDir, ".trigger")),
}
);
console.log(stdout);
if (stderr) console.error(stderr);
})()
);
if (wantInstallationError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
}
},
{ timeout: 60_000 }
);
test(
wantWorkerError ? "'node worker.js' fails" : "'node worker.js' succeeds",
async () => {
const expectation = expect(
(async () => {
const { stdout, stderr } = await execaNode("worker.js", {
cwd: resolve(join(fixtureDir, ".trigger")),
});
console.log(stdout);
if (stderr) console.error(stderr);
})()
);
if (wantWorkerError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
}
},
{ timeout: 60_000 }
);
});
});
});
});
}
);
} else if (process.env.MOD) {
throw new Error(`Unknown fixture '${process.env.MOD}'`);
} else {
throw new Error("Nothing to test");
}
function installArgs(packageManager: string) {
switch (packageManager) {
case "bun":
return ["install", "--frozen-lockfile"];
case "pnpm":
case "yarn":
throw new Error("pnpm and yarn must install using `corepack use`");
case "npm":
return ["ci", "--no-audit"];
default:
throw new Error(`Unknown package manager '${packageManager}'`);
}
}
+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
export const LogLevelSchema = z
.enum(["debug", "info", "log", "warn", "error", "none"])
.default("log");
export type Loglevel = z.infer<typeof LogLevelSchema>;
export const PackageManagerSchema = z.enum(["npm", "pnpm", "yarn"]).default("npm");
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "no-config",
"wantConfigNotFoundError": true
},
{
"name": "server-only",
"skipTypecheck": true
}
]
+9
View File
@@ -0,0 +1,9 @@
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["e2e/vitest.d.ts"],
globals: true,
exclude: [...configDefaults.exclude, "src/**/*"],
},
});
+13
View File
@@ -0,0 +1,13 @@
import { Metafile, OutputFile } from "esbuild";
import { ReadConfigResult } from "../src/utilities/configFiles";
declare global {
var dependencies: { [k: string]: string } | undefined;
var entryPointMetaOutput: Metafile["outputs"]["out/stdin.js"] | undefined;
var entryPointOutputFile: OutputFile | undefined;
var resolvedConfig: ReadConfigResult | undefined;
var tempDir: string | undefined;
var workerMetaOutput: Metafile["outputs"]["out/stdin.js"] | undefined;
var workerOutputFile: OutputFile | undefined;
}
+4 -3
View File
@@ -53,7 +53,7 @@
"tsup": "^8.0.1",
"type-fest": "^3.6.0",
"typescript": "^5.3.3",
"vitest": "^0.34.4",
"vitest": "^1.6.0",
"xdg-app-paths": "^8.3.0"
},
"scripts": {
@@ -68,7 +68,8 @@
"dev:test": "nodemon -w src/Containerfile.prod -x npm run build:prod-containerfile",
"clean": "rimraf dist",
"start": "node dist/index.js",
"test": "vitest"
"test": "vitest",
"test:e2e": "vitest --run -c e2e/vite.config.js"
},
"dependencies": {
"@anatine/esbuild-decorators": "^0.2.19",
@@ -131,4 +132,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}
+7 -9
View File
@@ -1105,7 +1105,7 @@ async function compileProject(
return await tracer.startActiveSpan("compileProject", async (span) => {
try {
if (!options.skipTypecheck) {
const typecheck = await typecheckProject(config, options);
const typecheck = await typecheckProject(config);
if (!typecheck) {
throw new Error("Typecheck failed, aborting deployment");
@@ -1353,8 +1353,7 @@ async function compileProject(
const resolvingDependenciesResult = await resolveDependencies(
tempDir,
packageJsonContents,
config,
options
config
);
if (!resolvingDependenciesResult) {
@@ -1491,11 +1490,10 @@ async function resolveEnvironmentVariables(
// in the `.trigger/cache` directory. If the package-lock.json is found, we'll write it to the project directory
// If the package-lock.json is not found, we will run `npm install --package-lock-only` and then write the package-lock.json
// to the project directory, and finally we'll write the digest to the `.trigger/cache` directory with the contents of the package-lock.json
async function resolveDependencies(
export async function resolveDependencies(
projectDir: string,
packageJsonContents: any,
config: ResolvedConfig,
options: DeployCommandOptions
config: ResolvedConfig
) {
return await tracer.startActiveSpan("resolveDependencies", async (span) => {
const resolvingDepsSpinner = spinner();
@@ -1633,7 +1631,7 @@ async function resolveDependencies(
});
}
async function typecheckProject(config: ResolvedConfig, options: DeployCommandOptions) {
export async function typecheckProject(config: ResolvedConfig) {
return await tracer.startActiveSpan("typecheckProject", async (span) => {
try {
const typecheckSpinner = spinner();
@@ -1686,7 +1684,7 @@ async function typecheckProject(config: ResolvedConfig, options: DeployCommandOp
// Returns the dependencies that are required by the output that are found in output and the CLI package dependencies
// Returns the dependency names and the version to use (taken from the CLI deps package.json)
async function resolveRequiredDependencies(
export async function resolveRequiredDependencies(
imports: Metafile["outputs"][string]["imports"],
config: ResolvedConfig,
project: JavascriptProject
@@ -1814,7 +1812,7 @@ type AdditionalFilesReturn =
noMatches: string[];
};
async function copyAdditionalFiles(
export async function copyAdditionalFiles(
config: ResolvedConfig,
tempDir: string
): Promise<AdditionalFilesReturn> {
+11 -8
View File
@@ -115,15 +115,18 @@ async function findFilePath(dir: string, fileName: string): Promise<string | und
export type ReadConfigOptions = {
projectRef?: string;
configFile?: string;
cwd?: string;
};
export type ReadConfigFileResult = {
status: "file";
config: ResolvedConfig;
path: string;
module?: any;
};
export type ReadConfigResult =
| {
status: "file";
config: ResolvedConfig;
path: string;
module?: any;
}
| ReadConfigFileResult
| {
status: "in-memory";
config: ResolvedConfig;
@@ -137,7 +140,7 @@ export async function readConfig(
dir: string,
options?: ReadConfigOptions
): Promise<ReadConfigResult> {
const absoluteDir = path.resolve(process.cwd(), dir);
const absoluteDir = path.resolve(options?.cwd || process.cwd(), dir);
const configPath = await getConfigPath(dir, options?.configFile);
@@ -226,7 +229,7 @@ export async function resolveConfig(path: string, config: Config): Promise<Resol
config.triggerDirectories = await findTriggerDirectories(path);
}
config.triggerDirectories = resolveTriggerDirectories(config.triggerDirectories);
config.triggerDirectories = resolveTriggerDirectories(path, config.triggerDirectories);
logger.debug("Resolved trigger directories", { triggerDirectories: config.triggerDirectories });
+2 -2
View File
@@ -67,8 +67,8 @@ async function gatherTaskFilesFromDir(
return taskFiles;
}
export function resolveTriggerDirectories(dirs: string[]): string[] {
return dirs.map((dir) => resolve(dir));
export function resolveTriggerDirectories(projectDir: string, dirs: string[]): string[] {
return dirs.map((dir) => resolve(projectDir, dir));
}
const IGNORED_DIRS = ["node_modules", ".git", "dist", "build"];
+2 -2
View File
@@ -1,6 +1,6 @@
// See: https://www.totaltypescript.com/tsconfig-cheat-sheet
{
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./e2e/**/*.ts"],
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
@@ -30,5 +30,5 @@
"@trigger.dev/core-apps/*": ["../core-apps/src/*"]
}
},
"exclude": ["node_modules"]
"exclude": ["node_modules", "./e2e/fixtures"]
}
+4 -3
View File
@@ -1,7 +1,8 @@
import { defineConfig } from 'vitest/config';
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true
globals: true,
exclude: [...configDefaults.exclude, "e2e/**/*"],
},
})
});
+155 -3
View File
@@ -1707,8 +1707,8 @@ importers:
specifier: ^3.6.0
version: 3.13.0
vitest:
specifier: ^0.34.4
version: 0.34.4
specifier: ^1.6.0
version: 1.6.0(@types/node@18.17.1)
xdg-app-paths:
specifier: ^8.3.0
version: 8.3.0
@@ -17063,6 +17063,14 @@ packages:
chai: 4.4.1
dev: true
/@vitest/expect@1.6.0:
resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
dependencies:
'@vitest/spy': 1.6.0
'@vitest/utils': 1.6.0
chai: 4.4.1
dev: true
/@vitest/runner@0.28.5:
resolution: {integrity: sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==}
dependencies:
@@ -17086,6 +17094,14 @@ packages:
pathe: 1.1.1
dev: true
/@vitest/runner@1.6.0:
resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
dependencies:
'@vitest/utils': 1.6.0
p-limit: 5.0.0
pathe: 1.1.1
dev: true
/@vitest/snapshot@0.34.4:
resolution: {integrity: sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==}
dependencies:
@@ -17101,6 +17117,14 @@ packages:
pretty-format: 29.7.0
dev: true
/@vitest/snapshot@1.6.0:
resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
dependencies:
magic-string: 0.30.8
pathe: 1.1.1
pretty-format: 29.7.0
dev: true
/@vitest/spy@0.28.5:
resolution: {integrity: sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==}
dependencies:
@@ -17118,6 +17142,12 @@ packages:
tinyspy: 2.2.1
dev: true
/@vitest/spy@1.6.0:
resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
dependencies:
tinyspy: 2.2.1
dev: true
/@vitest/utils@0.28.5:
resolution: {integrity: sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==}
dependencies:
@@ -17144,6 +17174,15 @@ packages:
pretty-format: 29.7.0
dev: true
/@vitest/utils@1.6.0:
resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
dependencies:
diff-sequences: 29.6.3
estree-walker: 3.0.3
loupe: 2.3.7
pretty-format: 29.7.0
dev: true
/@web3-storage/multipart-parser@1.0.0:
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
@@ -18793,7 +18832,7 @@ packages:
check-error: 1.0.3
deep-eql: 4.1.3
get-func-name: 2.0.2
loupe: 2.3.6
loupe: 2.3.7
pathval: 1.1.1
type-detect: 4.0.8
dev: true
@@ -35212,6 +35251,27 @@ packages:
- terser
dev: true
/vite-node@1.6.0(@types/node@18.17.1):
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.2.7(@types/node@18.17.1)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vite-tsconfig-paths@4.0.5:
resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==}
dependencies:
@@ -35433,6 +35493,42 @@ packages:
fsevents: 2.3.3
dev: true
/vite@5.2.7(@types/node@18.17.1):
resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': 18.17.1
esbuild: 0.20.2
postcss: 8.4.38
rollup: 4.13.2
optionalDependencies:
fsevents: 2.3.3
dev: true
/vitefu@0.2.4(vite@4.4.9):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
peerDependencies:
@@ -35619,6 +35715,62 @@ packages:
- terser
dev: true
/vitest@1.6.0(@types/node@18.17.1):
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': 1.6.0
'@vitest/ui': 1.6.0
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
dependencies:
'@types/node': 18.17.1
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
'@vitest/snapshot': 1.6.0
'@vitest/spy': 1.6.0
'@vitest/utils': 1.6.0
acorn-walk: 8.3.2
chai: 4.4.1
debug: 4.3.4(supports-color@8.1.1)
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.8
pathe: 1.1.1
picocolors: 1.0.0
std-env: 3.7.0
strip-literal: 2.1.0
tinybench: 2.6.0
tinypool: 0.8.3
vite: 5.2.7(@types/node@18.17.1)
vite-node: 1.6.0(@types/node@18.17.1)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vscode-oniguruma@1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+1
View File
@@ -7,3 +7,4 @@ packages:
- "docs"
- "perf"
- "runtime_tests"
- "!packages/cli-v3/e2e/**"