Cli v3 e2e: fixtures (#1184)

* Run tests

* Remove useless comments

* Fix readme

* Add fixtures for config step

* Add fixture for additionalDependencies name parsing scope issue

* Add URL to commit fix

* Some fixtures will only use 1 package manager

* Add resolve-trigger-deps fixture

* use rimraf in test setup

* Add legacy-peer-deps fixture

* Use vitest fixtures

* Add nested peer dep locking issue

* Add missing peer dep resolving fixture

* Get rid of global namespace, fix parallelism

* Add console logs to debug log level

* Add changeset

* Remove comments

* Update README.md
This commit is contained in:
Émile Ré
2024-06-27 14:55:54 +02:00
committed by GitHub
parent d22a460555
commit 52b6f48a94
74 changed files with 12687 additions and 429 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
---
Add e2e fixtures corresponding to past issues
Implement e2e suite parallelism
Enhance log level for specific e2e suite messages
+22 -29
View File
@@ -92,7 +92,15 @@ This will test your fixture project, and generate outputs in the `packages/cli-v
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.
5. Add the following `.yarnrc.yaml` in your fixture folder.
This will avoid having `.pnp.cjs` and `.pnp.loader.mjs` and keep versioned files to a minimum.
```yaml .yarnrc.yml
nodeLinker: node-modules
```
6. 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:
@@ -105,19 +113,19 @@ This will test your fixture project, and generate outputs in the `packages/cli-v
> 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.
7. 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
corepack use yarn@4.2.2 # 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.
8. Create a new `packages/cli-v3/e2e/fixtures/trigger` folder, and create a trigger task in it.
Here is an example:
@@ -132,7 +140,7 @@ This will test your fixture project, and generate outputs in the `packages/cli-v
});
```
8. Add a trigger configuration file.
9. Add a trigger configuration file.
The configuration file is mandatory here, the E2E suite does not execute `trigger.dev` commands.
@@ -145,33 +153,18 @@ This will test your fixture project, and generate outputs in the `packages/cli-v
> The project reference can be anything here, as the suite runs locally without connecting to the platform.
9. Commit your changes.
10. Commit your changes.
10. Add your fixture test configuration in `testCases.json`.
11. Add your fixture test configuration in `fixtures.config.js`.
```json testCases.json
[
...
```javascript fixtures.config.js
export const fixturesConfig = [
// ...
{
"name": "<fixture-name>",
id: "<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.
> You might expect a specific error for a specific test, so use those configuration option at your discretion.
+2 -45
View File
@@ -33,19 +33,9 @@ export async function compile(options: CompileOptions) {
} = 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 workerFacade = readFileSync(resolve("./dist/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
@@ -74,7 +64,6 @@ export async function compile(options: CompileOptions) {
const result = await build({
stdin: {
contents: workerContents,
// resolveDir: process.cwd(),
resolveDir: config.projectDir,
sourcefile: "__entryPoint.ts",
},
@@ -87,11 +76,7 @@ export async function compile(options: CompileOptions) {
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();",
},
@@ -112,14 +97,6 @@ export async function compile(options: CompileOptions) {
});
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");
}
@@ -127,16 +104,11 @@ export async function compile(options: CompileOptions) {
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 entryPointContents = readFileSync(resolve("./dist/workers/prod/entry-point.js"), "utf-8");
const entryPointResult = await build({
stdin: {
contents: entryPointContents,
// resolveDir: process.cwd(),
resolveDir: config.projectDir,
sourcefile: "index.ts",
},
@@ -150,7 +122,6 @@ export async function compile(options: CompileOptions) {
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),
@@ -161,14 +132,6 @@ export async function compile(options: CompileOptions) {
});
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");
}
@@ -179,13 +142,9 @@ export async function compile(options: CompileOptions) {
);
}
// 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")
@@ -194,8 +153,6 @@ export async function compile(options: CompileOptions) {
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")
+1 -1
View File
@@ -8,7 +8,7 @@ type CreateDeployHashOptions = {
};
export async function createDeployHash(options: CreateDeployHashOptions) {
const { entryPointOutputFile, workerOutputFile } = options;
const { dependencies, entryPointOutputFile, workerOutputFile } = options;
// COPIED FROM compileProject()
const contentHasher = createHash("sha256");
+63
View File
@@ -0,0 +1,63 @@
export interface TestCase {
resolveEnv?: { [key: string]: string };
id: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantConfigInvalidError?: boolean;
wantCompilationError?: boolean;
wantWorkerError?: boolean;
wantDependenciesError?: boolean;
wantInstallationError?: boolean;
}
export const fixturesConfig: TestCase[] = [
{
id: "config-invalid",
wantConfigInvalidError: true,
},
{
id: "config-not-found",
wantConfigNotFoundError: true,
},
{
id: "dep-to-add-scope-parsing",
skipTypecheck: true,
},
{
id: "infisical-sdk",
skipTypecheck: true,
// Should not fail
wantCompilationError: true,
},
{
id: "lock-nested-peer-deps",
skipTypecheck: true,
resolveEnv: {
npm_config_legacy_peer_deps: "true",
},
},
{
id: "resolve-legacy-peer-deps",
skipTypecheck: true,
// Should fail with better error at resolve
wantWorkerError: true,
},
{
id: "resolve-trigger-deps",
skipTypecheck: true,
},
{
id: "server-only",
skipTypecheck: true,
},
{
id: "trigger-dir-missing",
skipTypecheck: true,
},
{
id: "trigger-dir-not-found",
skipTypecheck: true,
// Should fail way before
wantCompilationError: true,
},
];
@@ -1,10 +1,10 @@
{
"name": "no-config",
"name": "config-invalid",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "no-config",
"name": "config-invalid",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
@@ -1,5 +1,5 @@
{
"name": "no-config",
"name": "config-invalid",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
@@ -0,0 +1,4 @@
export const config = {
// 'project' field is mandatory
triggerDirectories: [],
};
@@ -5,8 +5,8 @@ __metadata:
version: 8
cacheKey: 10c0
"no-config@workspace:.":
"config-invalid@workspace:.":
version: 0.0.0-use.local
resolution: "no-config@workspace:."
resolution: "config-invalid@workspace:."
languageName: unknown
linkType: soft
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,14 @@
{
"name": "config-not-found",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "config-not-found",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
}
}
@@ -0,0 +1,9 @@
{
"name": "config-not-found",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
@@ -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
"config-not-found@workspace:.":
version: 0.0.0-use.local
resolution: "config-not-found@workspace:."
languageName: unknown
linkType: soft
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"name": "dep-to-add-scope-parsing",
"description": "https://github.com/triggerdotdev/trigger.dev/commit/b8477ea2b08828e547885a6f2a14c15074852879",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"workspaces": [
"packages/*"
]
}
@@ -0,0 +1,3 @@
module.exports = {
MESSAGE: "Hello, World!",
};
@@ -0,0 +1,4 @@
{
"name": "@dep-to-add-scope-parsing/dep-to-add",
"private": true
}
@@ -0,0 +1,9 @@
import { task } from "@trigger.dev/sdk/v3";
import { MESSAGE } from "@dep-to-add-scope-parsing/dep-to-add";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log(MESSAGE, payload);
},
});
@@ -0,0 +1,8 @@
{
"name": "@dep-to-add-scope-parsing/trigger",
"private": true,
"dependencies": {
"@dep-to-add-scope-parsing/dep-to-add": "*",
"@trigger.dev/sdk": "3.0.0-beta.37"
}
}
@@ -0,0 +1,5 @@
export const config = {
project: "dep-to-add-scope-parsing",
additionalPackages: ["@dep-to-add-scope-parsing/dep-to-add"],
triggerDirectories: ["./packages/trigger"],
};
@@ -0,0 +1 @@
nodeLinker: node-modules
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
{
"name": "lock-nested-peer-deps",
"description": "nested peer dep resolved (installed), but legacy-peer-dep true when creating container lockfile. https://github.com/triggerdotdev/trigger.dev/commit/c092c0f9db70039e2f1bc495547a723c1f1a21cf",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"dependencies": {
"@trigger.dev/sdk": "3.0.0-beta.40",
"react-dom": "18.3.1"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
# https://github.com/pnpm/pnpm/issues/2412
@@ -0,0 +1,3 @@
export const config = {
project: "lock-nested-peer-deps",
};
@@ -0,0 +1,17 @@
import { task } from "@trigger.dev/sdk/v3";
import { renderToPipeableStream } from "react-dom/server";
function App() {
return <div>Hello World</div>;
}
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
stream.pipe(payload);
},
});
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
legacy-peer-deps=true
@@ -0,0 +1 @@
nodeLinker: node-modules
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
{
"name": "resolve-legacy-peer-deps",
"description": "",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"dependencies": {
"@trigger.dev/sdk": "3.0.0-beta.40"
},
"peerDependencies": {
"lodash": "4.17.21"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
# https://github.com/pnpm/pnpm/issues/2412
@@ -0,0 +1,3 @@
export const config = {
project: "resolve-legacy-peer-deps",
};
@@ -0,0 +1,9 @@
import { concat } from "lodash/array";
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log(concat(["Hello"], "World!").join(", "), payload);
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,14 @@
{
"name": "resolve-trigger-deps",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "resolve-trigger-deps",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
}
}
@@ -0,0 +1,10 @@
{
"name": "resolve-trigger-deps",
"description": "https://github.com/triggerdotdev/trigger.dev/commit/6406924b025de766af68dcceea04d26993211ed0",
"private": true,
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
@@ -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,4 @@
export const config = {
project: "resolve-trigger-deps",
triggerDirectories: ["./trigger"],
};
@@ -0,0 +1,8 @@
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log("Hello, World!", payload);
},
});
@@ -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
"resolve-trigger-deps@workspace:.":
version: 0.0.0-use.local
resolution: "resolve-trigger-deps@workspace:."
languageName: unknown
linkType: soft
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,14 @@
{
"name": "trigger-dir-missing",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trigger-dir-missing",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
}
}
@@ -0,0 +1,9 @@
{
"name": "trigger-dir-missing",
"private": true,
"packageManager": "yarn@4.2.2+sha256.1aa43a5304405be7a7cb9cb5de7b97de9c4e8ddd3273e4dad00d6ae3eb39f0ef",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
@@ -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,3 @@
export const config = {
project: "bad-config",
};
@@ -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
"trigger-dir-missing@workspace:.":
version: 0.0.0-use.local
resolution: "trigger-dir-missing@workspace:."
languageName: unknown
linkType: soft
@@ -0,0 +1 @@
nodeLinker: node-modules
@@ -0,0 +1,14 @@
{
"name": "trigger-dir-not-found",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trigger-dir-not-found",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
}
}
@@ -0,0 +1,9 @@
{
"name": "trigger-dir-not-found",
"private": true,
"packageManager": "yarn@4.2.2+sha256.1aa43a5304405be7a7cb9cb5de7b97de9c4e8ddd3273e4dad00d6ae3eb39f0ef",
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
}
}
@@ -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,4 @@
export const config = {
project: "bad-config",
triggerDirectories: ["./unknown-folder"],
};
@@ -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
"trigger-dir-not-found@workspace:.":
version: 0.0.0-use.local
resolution: "trigger-dir-not-found@workspace:."
languageName: unknown
linkType: soft
+1 -13
View File
@@ -50,7 +50,6 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
tempDir,
} = options;
// COPIED FROM compileProject()
logger.debug("Getting the imports for the worker and entryPoint builds", {
workerImports: metaOutput.imports,
entryPointImports: entryPointMetaOutput.imports,
@@ -59,7 +58,6 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
// 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);
@@ -75,18 +73,11 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
...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}"`)
@@ -96,9 +87,6 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
)} are valid.`
);
}
// } else {
// compileSpinner.stop("Project built successfully");
// }
const resolvingDependenciesResult = await resolveDependencies(
tempDir,
@@ -110,5 +98,5 @@ export async function handleDependencies(options: HandleDependenciesOptions) {
throw new SkipLoggingError("Failed to resolve dependencies");
}
return { dependencies };
return dependencies;
}
+285 -289
View File
@@ -1,333 +1,314 @@
import { execa, execaNode } from "execa";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { mkdir, rename, rm } from "node:fs/promises";
import { join, resolve } from "node:path";
import { rimraf } from "rimraf";
import { typecheckProject } from "../src/commands/deploy";
import { readConfig, ReadConfigFileResult } from "../src/utilities/configFiles";
import { PackageManager } from "../src/utilities/getUserPackageManager";
import { readConfig, ReadConfigFileResult, ReadConfigResult } from "../src/utilities/configFiles";
import {
detectPackageManagerFromArtifacts,
LOCKFILES,
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";
import { E2EOptions, E2EOptionsSchema } from "./schemas";
import { fixturesConfig, TestCase } from "./fixtures.config";
import { Metafile, OutputFile } from "esbuild";
type TestCase = {
name: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantBadConfigError?: boolean;
wantCompilationError?: boolean;
wantWorkerError?: boolean;
wantDependenciesError?: boolean;
wantInstallationError?: boolean;
};
interface E2EFixtureTest extends TestCase {
dir: string;
packageManager: PackageManager;
tempDir: string;
}
const TIMEOUT = 120_000;
const testCases: TestCase[] = process.env.MOD
? allTestCases.filter(({ name }) => process.env.MOD === name)
: allTestCases;
? fixturesConfig.filter(({ id }) => process.env.MOD === id)
: fixturesConfig;
let logLevel: Loglevel = "log";
let packageManager: PackageManager = "npm";
let options: E2EOptions;
try {
logLevel = LogLevelSchema.parse(process.env.LOG);
options = E2EOptionsSchema.parse({
logLevel: process.env.LOG,
packageManager: process.env.PM,
});
} catch (e) {
console.error(e);
console.log("Using default log level 'log'");
options = {
logLevel: "log",
};
}
logger.loggerLevel = logLevel;
try {
packageManager = PackageManagerSchema.parse(process.env.PM);
} catch (e) {
console.error(e);
console.log("Using default package manager 'npm'");
}
logger.loggerLevel = options.logLevel;
if (testCases.length > 0) {
console.log(`Using ${packageManager}`);
describe.concurrent("bundling", async () => {
beforeEach<E2EFixtureTest>(async ({ dir, packageManager }) => {
await rimraf(join(dir, "**/node_modules/**"), {
glob: true,
});
await rimraf(join(dir, ".yarn"), { glob: true });
if (
packageManager === "npm" &&
(existsSync(resolve(join(dir, "yarn.lock"))) ||
existsSync(resolve(join(dir, "yarn.lock.copy"))))
) {
// `npm ci` & `npm install` will update an existing yarn.lock
try {
await rename(resolve(join(dir, "yarn.lock")), resolve(join(dir, "yarn.lock.copy")));
} catch (e) {
await rename(resolve(join(dir, "yarn.lock.copy")), resolve(join(dir, "yarn.lock")));
}
}
describe.each(testCases)(
"fixture $name",
async ({
name,
skipTypecheck,
wantConfigNotFoundError,
wantBadConfigError,
wantCompilationError,
wantWorkerError,
wantDependenciesError,
wantInstallationError,
}: TestCase) => {
const fixtureDir = resolve(join(process.cwd(), "e2e/fixtures", name));
await installFixtureDeps(dir, packageManager);
}, TIMEOUT);
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"))
);
afterEach<E2EFixtureTest>(async ({ dir, packageManager }) => {
if (packageManager === "npm") {
try {
await rename(resolve(join(dir, "yarn.lock.copy")), resolve(join(dir, "yarn.lock")));
} catch {}
}
vi.unstubAllEnvs();
});
for (let testCase of testCases) {
test.extend<E2EFixtureTest>({
...testCase,
dir: async ({ id }, use) => await use(resolve(join(process.cwd(), "e2e/fixtures", id))),
packageManager: async ({ dir }, use) => await use(await parsePackageManager(options, dir)),
tempDir: async ({ dir }, use) => {
const existingTempDir = resolve(join(dir, ".trigger"));
if (existsSync(existingTempDir)) {
await rm(existingTempDir, { force: true, recursive: true });
}
}
});
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();
await use((await mkdir(join(dir, ".trigger"), { recursive: true })) as string);
},
{ timeout: 60_000 }
);
})(
`fixture '${testCase.id}'`,
{ timeout: TIMEOUT },
async ({
dir,
packageManager,
resolveEnv,
skip,
skipTypecheck,
tempDir,
wantCompilationError,
wantConfigInvalidError,
wantConfigNotFoundError,
wantDependenciesError,
wantInstallationError,
wantWorkerError,
}) => {
if (
options.packageManager &&
!existsSync(resolve(dir, LOCKFILES[options.packageManager]))
) {
skip();
}
test(
wantConfigNotFoundError || wantBadConfigError
? "does not resolve config"
: "resolves config",
async () => {
const expectation = expect(
let resolvedConfig: ReadConfigResult;
const configExpect = expect(
(async () => {
global.resolvedConfig = await readConfig(fixtureDir, { cwd: fixtureDir });
})()
resolvedConfig = await readConfig(dir, { cwd: dir });
})(),
wantConfigNotFoundError || wantConfigInvalidError
? "does not resolve config"
: "resolves config"
);
if (wantConfigNotFoundError) {
await expectation.rejects.toThrowError();
} else {
await expectation.resolves.not.toThrowError();
await configExpect.rejects.toThrowError();
return;
}
if (wantBadConfigError) {
expect(global.resolvedConfig).toBe("error");
} else {
expect(global.resolvedConfig).not.toBe("error");
await configExpect.resolves.not.toThrowError();
if (wantConfigInvalidError) {
expect(resolvedConfig!.status).toBe("error");
return;
}
expect(resolvedConfig!).not.toBe("error");
if (!skipTypecheck) {
await expect(
(async () =>
await typecheckProject((resolvedConfig! as ReadConfigFileResult).config))(),
"typechecks"
).resolves.not.toThrowError();
}
let entryPointMetaOutput: Metafile["outputs"]["out/stdin.js"];
let entryPointOutputFile: OutputFile;
let workerMetaOutput: Metafile["outputs"]["out/stdin.js"];
let workerOutputFile: OutputFile;
const compileExpect = expect(
(async () => {
const compilationResult = await compile({
resolvedConfig: resolvedConfig!,
tempDir,
});
entryPointMetaOutput = compilationResult.entryPointMetaOutput;
entryPointOutputFile = compilationResult.entryPointOutputFile;
workerMetaOutput = compilationResult.workerMetaOutput;
workerOutputFile = compilationResult.workerOutputFile;
})(),
wantCompilationError ? "does not compile" : "compiles"
);
if (wantCompilationError) {
await compileExpect.rejects.toThrowError();
return;
}
await compileExpect.resolves.not.toThrowError();
let dependencies: { [k: string]: string };
if (resolveEnv) {
for (let envKey in resolveEnv) {
vi.stubEnv(envKey, resolveEnv[envKey]!);
}
}
const depsExpectation = expect(
(async () => {
dependencies = await handleDependencies({
entryPointMetaOutput: entryPointMetaOutput!,
metaOutput: workerMetaOutput!,
resolvedConfig: resolvedConfig!,
tempDir,
packageManager,
});
})(),
wantDependenciesError ? "does not resolve dependencies" : "resolves dependencies"
);
if (wantDependenciesError) {
await depsExpectation.rejects.toThrowError();
return;
}
await depsExpectation.resolves.not.toThrowError();
if (resolveEnv) {
vi.unstubAllEnvs();
}
await expect(
(async () => {
await createContainerFile({
resolvedConfig: resolvedConfig!,
tempDir,
});
})(),
"copies postinstall command into Containerfile.prod"
).resolves.not.toThrowError();
await expect(
(async () => {
await createDeployHash({
dependencies: dependencies!,
entryPointOutputFile: entryPointOutputFile!,
workerOutputFile: workerOutputFile!,
});
})(),
"creates deploy hash"
).resolves.not.toThrowError();
const installBundleDepsExpect = expect(
(async () => {
const { stdout: installStdout, stderr: installStderr } = await execa(
"npm",
["ci", "--no-audit", "--no-fund"],
{
cwd: tempDir,
NODE_PATH: resolve(join(tempDir, "node_modules")),
}
);
debug(installStdout);
if (installStderr) console.error(installStderr);
})(),
wantInstallationError ? "does not install dependencies" : "installs dependencies"
);
if (wantInstallationError) {
await installBundleDepsExpect.rejects.toThrowError();
return;
}
await installBundleDepsExpect.resolves.not.toThrowError();
const workerStartExpect = expect(
(async () => {
const { stdout, stderr } = await execaNode("worker.js", {
cwd: tempDir,
env: {
// Since we don't start the worker in a container, limit node resolution algorithm to the '.trigger/node_modules' folder
NODE_PATH: resolve(join(tempDir, "node_modules")),
},
});
debug(stdout);
if (stderr) console.error(stderr);
})(),
wantWorkerError ? "worker does not start" : "worker starts"
);
if (wantWorkerError) {
await workerStartExpect.rejects.toThrowError();
return;
}
await workerStartExpect.resolves.not.toThrowError();
}
);
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 debug(message: string) {
if (options.logLevel === "debug") {
console.log(message);
}
}
async function installFixtureDeps(dir: string, packageManager: PackageManager) {
if (["pnpm", "yarn"].includes(packageManager)) {
const buffer = readFileSync(resolve(join(dir, "package.json")), "utf8");
const pkgJSON = JSON.parse(buffer.toString());
const version = pkgJSON.engines[packageManager];
debug(`Detected ${packageManager}@${version} from package.json 'engines' field`);
const { stdout, stderr } = await execa("corepack", ["use", `${packageManager}@${version}`], {
cwd: dir,
});
debug(stdout);
if (stderr) console.error(stderr);
} else {
const { stdout, stderr } = await execa(packageManager, installArgs(packageManager), {
cwd: dir,
NODE_PATH: resolve(join(dir, "node_modules")),
});
debug(stdout);
if (stderr) console.error(stderr);
}
}
function installArgs(packageManager: string) {
switch (packageManager) {
case "bun":
@@ -341,3 +322,18 @@ function installArgs(packageManager: string) {
throw new Error(`Unknown package manager '${packageManager}'`);
}
}
async function parsePackageManager(
options: E2EOptions,
fixtureDir: string
): Promise<PackageManager> {
let packageManager: PackageManager;
if (options.packageManager) {
packageManager = options.packageManager;
} else {
packageManager = await detectPackageManagerFromArtifacts(fixtureDir);
}
return packageManager;
}
+7 -5
View File
@@ -1,7 +1,9 @@
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");
const LogLevelSchema = z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log");
const PackageManagerSchema = z.enum(["npm", "pnpm", "yarn"]);
export const E2EOptionsSchema = z.object({
logLevel: LogLevelSchema,
packageManager: PackageManagerSchema.optional(),
});
export type E2EOptions = z.infer<typeof E2EOptionsSchema>;
-10
View File
@@ -1,10 +0,0 @@
[
{
"name": "no-config",
"wantConfigNotFoundError": true
},
{
"name": "server-only",
"skipTypecheck": true
}
]
-1
View File
@@ -2,7 +2,6 @@ import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["e2e/vitest.d.ts"],
globals: true,
exclude: [...configDefaults.exclude, "src/**/*"],
},
-13
View File
@@ -1,13 +0,0 @@
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;
}
+3 -3
View File
@@ -49,7 +49,7 @@
"npm-run-all": "^4.1.5",
"open": "^10.0.3",
"p-retry": "^6.1.0",
"rimraf": "^3.0.2",
"rimraf": "^5.0.7",
"tsup": "^8.0.1",
"type-fest": "^3.6.0",
"typescript": "^5.3.3",
@@ -122,12 +122,12 @@
"terminal-link": "^3.0.0",
"tiny-invariant": "^1.2.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.4.0",
"update-check": "^1.5.4",
"url": "^0.11.1",
"ws": "^8.12.0",
"zod": "3.22.3",
"zod-validation-error": "^1.5.0",
"typescript": "^5.4.0"
"zod-validation-error": "^1.5.0"
},
"engines": {
"node": ">=18.0.0"
@@ -227,9 +227,11 @@ export async function readConfig(
export async function resolveConfig(path: string, config: Config): Promise<ResolvedConfig> {
if (!config.triggerDirectories) {
config.triggerDirectories = await findTriggerDirectories(path);
// TODO trigger-dir-missing: throw error if no trigger directory is found
}
config.triggerDirectories = resolveTriggerDirectories(path, config.triggerDirectories);
// TODO trigger-dir-not-found: throw error if trigger directories do not exist
logger.debug("Resolved trigger directories", { triggerDirectories: config.triggerDirectories });
@@ -3,6 +3,12 @@ import { basename } from "path";
import { logger } from "./logger";
export type PackageManager = "npm" | "pnpm" | "yarn";
export const LOCKFILES = {
npm: "package-lock.json",
npmShrinkwrap: "npm-shrinkwrap.json",
pnpm: "pnpm-lock.yaml",
yarn: "yarn.lock",
};
export async function getUserPackageManager(path: string): Promise<PackageManager> {
const packageManager = await detectPackageManager(path);
@@ -36,15 +42,8 @@ function detectPackageManagerFromCurrentCommand(): PackageManager {
}
}
async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
const artifacts = {
yarn: "yarn.lock",
pnpm: "pnpm-lock.yaml",
npm: "package-lock.json",
npmShrinkwrap: "npm-shrinkwrap.json",
};
const foundPath = await findUp(Object.values(artifacts), { cwd: path });
export async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
const foundPath = await findUp(Object.values(LOCKFILES), { cwd: path });
if (!foundPath) {
throw new Error("Could not detect package manager from artifacts");
@@ -53,12 +52,12 @@ async function detectPackageManagerFromArtifacts(path: string): Promise<PackageM
logger.debug("Found path from package manager artifacts", { foundPath });
switch (basename(foundPath)) {
case artifacts.yarn:
case LOCKFILES.yarn:
return "yarn";
case artifacts.pnpm:
case LOCKFILES.pnpm:
return "pnpm";
case artifacts.npm:
case artifacts.npmShrinkwrap:
case LOCKFILES.npm:
case LOCKFILES.npmShrinkwrap:
return "npm";
default:
throw new Error(`Unhandled package manager detection path: ${foundPath}`);
+12 -2
View File
@@ -1701,8 +1701,8 @@ importers:
specifier: ^6.1.0
version: 6.1.0
rimraf:
specifier: ^3.0.2
version: 3.0.2
specifier: ^5.0.7
version: 5.0.7
tsup:
specifier: ^8.0.1
version: 8.0.1(patch_hash=a5ztaafw5l4qfghy2hjjuynb34)(postcss@8.4.27)(typescript@5.4.5)
@@ -23437,6 +23437,7 @@ packages:
/glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
@@ -31554,6 +31555,7 @@ packages:
/rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3
@@ -31580,6 +31582,14 @@ packages:
glob: 10.3.10
dev: false
/rimraf@5.0.7:
resolution: {integrity: sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==}
engines: {node: '>=14.18'}
hasBin: true
dependencies:
glob: 10.3.10
dev: true
/rollup-plugin-inject@3.0.2:
resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.