Skip to content

Commit d2418ac

Browse files
committed
Setup / teardown + split compile for package manager capabilities
1 parent 4f663a0 commit d2418ac

File tree

4 files changed

+1389
-1626
lines changed

4 files changed

+1389
-1626
lines changed

packages/cli-v3/e2e/compile.test.ts

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,76 @@
11
import { execaNode } from "execa";
22
import { renameSync } from "node:fs";
33
import { join, resolve } from "node:path";
4+
import { typecheckProject } from "../src/commands/deploy";
5+
import { readConfig } from "../src/utilities/configFiles";
6+
import { rm } from "node:fs/promises";
47

58
type TestCase = {
69
name: string;
7-
options: string[];
10+
skipTypecheck: boolean;
811
};
912

10-
const testCases: TestCase[] = [
13+
const allTestCases: TestCase[] = [
1114
{
1215
name: "server-only",
13-
options: ["--skip-typecheck"],
16+
skipTypecheck: true,
1417
},
1518
{
1619
name: "infisical-sdk",
17-
options: ["--skip-typecheck"],
20+
skipTypecheck: true,
1821
},
1922
];
2023

21-
for (let testCase of testCases) {
22-
const { options, name } = testCase;
24+
const testCases = process.env.MOD
25+
? [allTestCases.find(({ name }) => process.env.MOD === name)]
26+
: allTestCases;
2327

24-
if (process.env.MOD && process.env.MOD !== name) continue;
28+
if (testCases.length > 0) {
29+
describe.each(testCases as TestCase[])("fixture $name", ({ name, skipTypecheck }) => {
30+
const fixtureDir = resolve(join(process.cwd(), "e2e/fixtures", name));
31+
const commandPath = resolve(join(process.cwd(), "dist/e2e.js"));
32+
const logLevel = process.env.LOG || "log";
2533

26-
const fixtureDir = resolve(join(process.cwd(), "e2e/fixtures", name));
27-
const commandPath = resolve(join(process.cwd(), "dist/e2e.js"));
28-
const logLevel = process.env.LOG || "log";
34+
beforeAll(async () => {
35+
await rm(resolve(join(fixtureDir, ".trigger")), { recursive: true });
36+
// await rm(resolve(join(fixtureDir, "node_modules")), { recursive: true });
37+
// await rm(resolve(join(fixtureDir, ".pnpm_store")), { recursive: true });
38+
});
2939

30-
togglePackageManager(true, fixtureDir, process.env.PM);
40+
test(
41+
"compiles",
42+
async () => {
43+
const resolvedConfig = await readConfig(fixtureDir);
3144

32-
test(
33-
`project fixture "${testCase.name}" compiles`,
34-
async () => {
35-
await expect(
36-
(async () => {
37-
const { stdout } = await execaNode(
38-
commandPath,
39-
["deploy-compile", fixtureDir, "--log-level", logLevel, ...options],
40-
{ cwd: fixtureDir }
41-
);
42-
console.log(stdout);
43-
})()
44-
).resolves.not.toThrowError();
45-
},
46-
{ timeout: 60_000 }
47-
);
45+
if (resolvedConfig.status === "error") {
46+
throw new Error(`cannot resolve config in directory ${fixtureDir}`);
47+
}
4848

49-
togglePackageManager(false, fixtureDir, process.env.PM);
49+
if (!skipTypecheck) {
50+
const typecheck = await typecheckProject(resolvedConfig.config);
51+
52+
if (!typecheck) {
53+
throw new Error("Typecheck failed, aborting deployment");
54+
}
55+
}
56+
57+
let compileArgs = ["deploy-compile", fixtureDir, "--log-level", logLevel];
58+
if (skipTypecheck) compileArgs.push("--skip-typecheck");
59+
60+
await expect(
61+
(async () => {
62+
const { stdout } = await execaNode(commandPath, compileArgs, { cwd: fixtureDir });
63+
console.log(stdout);
64+
})()
65+
).resolves.not.toThrowError();
66+
},
67+
{ timeout: 60_000 }
68+
);
69+
});
70+
} else if (process.env.MOD) {
71+
throw new Error(`Unknown fixture ${process.env.MOD}`);
72+
} else {
73+
throw new Error("Nothing to test");
5074
}
5175

5276
// For now to avoid changes in codebase.

packages/cli-v3/e2e/compile.ts

Lines changed: 199 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
#!/usr/bin/env node
22

3+
import { esbuildDecorators } from "@anatine/esbuild-decorators";
34
import { Command, Option } from "commander";
5+
import { build } from "esbuild";
6+
import { readFileSync } from "node:fs";
7+
import { mkdir, writeFile } from "node:fs/promises";
8+
import { join, posix } from "node:path";
9+
import invariant from "tiny-invariant";
410
import { z } from "zod";
11+
import { fromZodError } from "zod-validation-error";
512

6-
import { compileProject, DeployCommandOptions } from "../src/commands/deploy.js";
13+
import {
14+
bundleDependenciesPlugin,
15+
mockServerOnlyPlugin,
16+
workerSetupImportConfigPlugin,
17+
} from "../src/utilities/build.js";
718
import { readConfig } from "../src/utilities/configFiles.js";
19+
import { writeJSONFile } from "../src/utilities/fileSystem.js";
820
import { logger } from "../src/utilities/logger.js";
9-
import { fromZodError } from "zod-validation-error";
21+
import { cliRootPath } from "../src/utilities/resolveInternalFilePath.js";
22+
import { createTaskFileImports, gatherTaskFiles } from "../src/utilities/taskFiles.js";
23+
import { escapeImportPath, spinner } from "../src/utilities/windows.js";
1024

1125
const CompileCommandOptionsSchema = z.object({
1226
logLevel: z.enum(["debug", "info", "log", "warn", "error", "none"]).default("log"),
1327
skipTypecheck: z.boolean().default(false),
14-
config: z.string().optional(),
15-
projectRef: z.string().optional(),
1628
outputMetafile: z.string().optional(),
1729
});
1830

@@ -31,11 +43,6 @@ export function configureCompileCommand(program: Command) {
3143
"log"
3244
)
3345
.option("--skip-typecheck", "Whether to skip the pre-build typecheck")
34-
.option("-c, --config <config file>", "The name of the config file, found at [path]")
35-
.option(
36-
"-p, --project-ref <project ref>",
37-
"The project ref. Required if there is no config file. This will override the project specified in the config file."
38-
)
3946
.addOption(
4047
new Option(
4148
"--output-metafile <path>",
@@ -52,20 +59,194 @@ async function compile(dir: string, options: CompileCommandOptions) {
5259
}
5360
logger.loggerLevel = parsedOptions.data.logLevel;
5461

55-
const resolvedConfig = await readConfig(dir, {
56-
configFile: options.config,
57-
projectRef: options.projectRef,
58-
});
62+
const resolvedConfig = await readConfig(dir);
5963

6064
if (resolvedConfig.status === "error") {
6165
throw new Error(`cannot resolve config in directory ${dir}`);
6266
}
6367

64-
const { path } = await compileProject(
65-
resolvedConfig.config,
66-
options as DeployCommandOptions,
67-
resolvedConfig.status === "file" ? resolvedConfig.path : undefined
68+
const { config } = resolvedConfig;
69+
const configPath = resolvedConfig.status === "file" ? resolvedConfig.path : undefined;
70+
71+
// COPIED FROM compileProject()
72+
// const compileSpinner = spinner();
73+
// compileSpinner.start(`Building project in ${config.projectDir}`);
74+
75+
const taskFiles = await gatherTaskFiles(config);
76+
const workerFacade = readFileSync(
77+
join(cliRootPath(), "workers", "prod", "worker-facade.js"),
78+
"utf-8"
79+
);
80+
81+
const workerSetupPath = join(cliRootPath(), "workers", "prod", "worker-setup.js");
82+
83+
let workerContents = workerFacade
84+
.replace("__TASKS__", createTaskFileImports(taskFiles))
85+
.replace(
86+
"__WORKER_SETUP__",
87+
`import { tracingSDK, otelTracer, otelLogger } from "${escapeImportPath(workerSetupPath)}";`
88+
);
89+
90+
if (configPath) {
91+
logger.debug("Importing project config from", { configPath });
92+
93+
workerContents = workerContents.replace(
94+
"__IMPORTED_PROJECT_CONFIG__",
95+
`import * as importedConfigExports from "${escapeImportPath(
96+
configPath
97+
)}"; const importedConfig = importedConfigExports.config; const handleError = importedConfigExports.handleError;`
98+
);
99+
} else {
100+
workerContents = workerContents.replace(
101+
"__IMPORTED_PROJECT_CONFIG__",
102+
`const importedConfig = undefined; const handleError = undefined;`
103+
);
104+
}
105+
106+
const result = await build({
107+
stdin: {
108+
contents: workerContents,
109+
resolveDir: process.cwd(),
110+
sourcefile: "__entryPoint.ts",
111+
},
112+
bundle: true,
113+
metafile: true,
114+
write: false,
115+
minify: false,
116+
sourcemap: "external", // does not set the //# sourceMappingURL= comment in the file, we handle it ourselves
117+
logLevel: "error",
118+
platform: "node",
119+
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
120+
target: ["node18", "es2020"],
121+
outdir: "out",
122+
banner: {
123+
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" } }); } });`,
124+
},
125+
define: {
126+
TRIGGER_API_URL: `"${config.triggerUrl}"`,
127+
__PROJECT_CONFIG__: JSON.stringify(config),
128+
},
129+
plugins: [
130+
mockServerOnlyPlugin(),
131+
bundleDependenciesPlugin("workerFacade", config.dependenciesToBundle, config.tsconfigPath),
132+
workerSetupImportConfigPlugin(configPath),
133+
esbuildDecorators({
134+
tsconfig: config.tsconfigPath,
135+
tsx: true,
136+
force: false,
137+
}),
138+
],
139+
});
140+
141+
if (result.errors.length > 0) {
142+
// compileSpinner.stop("Build failed, aborting deployment");
143+
144+
// span.setAttributes({
145+
// "build.workerErrors": result.errors.map(
146+
// (error) => `Error: ${error.text} at ${error.location?.file}`
147+
// ),
148+
// });
149+
150+
throw new Error("Build failed, aborting deployment");
151+
}
152+
153+
if (options.outputMetafile) {
154+
await writeJSONFile(join(options.outputMetafile, "worker.json"), result.metafile);
155+
}
156+
157+
const entryPointContents = readFileSync(
158+
join(cliRootPath(), "workers", "prod", "entry-point.js"),
159+
"utf-8"
68160
);
69161

70-
console.log(path);
162+
const entryPointResult = await build({
163+
stdin: {
164+
contents: entryPointContents,
165+
resolveDir: process.cwd(),
166+
sourcefile: "index.ts",
167+
},
168+
bundle: true,
169+
metafile: true,
170+
write: false,
171+
minify: false,
172+
sourcemap: false,
173+
logLevel: "error",
174+
platform: "node",
175+
packages: "external",
176+
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
177+
target: ["node18", "es2020"],
178+
outdir: "out",
179+
define: {
180+
__PROJECT_CONFIG__: JSON.stringify(config),
181+
},
182+
plugins: [
183+
bundleDependenciesPlugin("entryPoint.ts", config.dependenciesToBundle, config.tsconfigPath),
184+
],
185+
});
186+
187+
if (entryPointResult.errors.length > 0) {
188+
// compileSpinner.stop("Build failed, aborting deployment");
189+
190+
// span.setAttributes({
191+
// "build.entryPointErrors": entryPointResult.errors.map(
192+
// (error) => `Error: ${error.text} at ${error.location?.file}`
193+
// ),
194+
// });
195+
196+
throw new Error("Build failed, aborting deployment");
197+
}
198+
199+
if (options.outputMetafile) {
200+
await writeJSONFile(
201+
join(options.outputMetafile, "entry-point.json"),
202+
entryPointResult.metafile
203+
);
204+
}
205+
206+
// Create a tmp directory to store the build
207+
// const tempDir = await createTempDir();
208+
const tempDir = await mkdir(join(config.projectDir, ".trigger"), { recursive: true });
209+
210+
logger.debug(`Writing compiled files to ${tempDir}`);
211+
212+
// Get the metaOutput for the result build
213+
const metaOutput = result.metafile!.outputs[posix.join("out", "stdin.js")];
214+
215+
invariant(metaOutput, "Meta output for the result build is missing");
216+
217+
// Get the metaOutput for the entryPoint build
218+
const entryPointMetaOutput = entryPointResult.metafile!.outputs[posix.join("out", "stdin.js")];
219+
220+
invariant(entryPointMetaOutput, "Meta output for the entryPoint build is missing");
221+
222+
// Get the outputFile and the sourceMapFile for the result build
223+
const workerOutputFile = result.outputFiles.find(
224+
(file) => file.path === join(config.projectDir, "out", "stdin.js")
225+
);
226+
227+
invariant(workerOutputFile, "Output file for the result build is missing");
228+
229+
const workerSourcemapFile = result.outputFiles.find(
230+
(file) => file.path === join(config.projectDir, "out", "stdin.js.map")
231+
);
232+
233+
invariant(workerSourcemapFile, "Sourcemap file for the result build is missing");
234+
235+
// Get the outputFile for the entryPoint build
236+
237+
const entryPointOutputFile = entryPointResult.outputFiles.find(
238+
(file) => file.path === join(config.projectDir, "out", "stdin.js")
239+
);
240+
241+
invariant(entryPointOutputFile, "Output file for the entryPoint build is missing");
242+
243+
// Save the result outputFile to /tmp/dir/worker.js (and make sure to map the sourceMap to the correct location in the file)
244+
await writeFile(
245+
join(tempDir!, "worker.js"),
246+
`${workerOutputFile.text}\n//# sourceMappingURL=worker.js.map`
247+
);
248+
// Save the sourceMapFile to /tmp/dir/worker.js.map
249+
await writeFile(join(tempDir!, "worker.js.map"), workerSourcemapFile.text);
250+
// Save the entryPoint outputFile to /tmp/dir/index.js
251+
await writeFile(join(tempDir!, "index.js"), entryPointOutputFile.text);
71252
}

0 commit comments

Comments
 (0)