1import { PlatformServices } from "@/Util/PlatformServices.ts";
2import { describe, expect, it } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Stream from "effect/Stream";
7import * as ChildProcess from "effect/process/ChildProcess";
8import { nodePath, nodeSupportsDevMode } from "../nodeProbe.ts";
9
10// Use the published bin layout but replace the entry with a real progress
11// render, so startup is exercised without credentials or cloud resources.
12const runPublishedLauncher = (
13 nodeEnv: string | undefined,
14 jsx?: string,
15 runtime = "bun",
16) =>
17 Effect.gen(function* () {
18 const fs = yield* FileSystem.FileSystem;
19 const path = yield* Path.Path;
20 const packageDir = yield* path.fromFileUrl(
21 new URL("../../", import.meta.url),
22 );
23 const project = yield* fs.makeTempDirectoryScoped({
24 prefix: "alchemy-launcher-",
25 });
26 const installed = path.join(project, "node_modules", "alchemy");
27 const bin = path.join(installed, "bin");
28 yield* fs.makeDirectory(bin, { recursive: true });
29 yield* fs.copyFile(
30 path.join(packageDir, "bin", "cli.js"),
31 path.join(bin, "cli.js"),
32 );
33 const config = path.join(packageDir, "bin", "tsconfig.json");
34 yield* fs.copyFile(config, path.join(bin, "tsconfig.json"));
35 yield* fs.writeFileString(
36 path.join(installed, "package.json"),
37 JSON.stringify({ type: "module", bin: { alchemy: "./bin/cli.js" } }),
38 );
39 yield* fs.symlink(
40 path.join(packageDir, "node_modules"),
41 path.join(installed, "node_modules"),
42 );
43 yield* fs.makeDirectory(path.join(project, "node_modules", ".bin"));
44 yield* fs.symlink(
45 path.join(bin, "cli.js"),
46 path.join(project, "node_modules", ".bin", "alchemy"),
47 );
48 yield* fs.chmod(path.join(bin, "cli.js"), 0o755);
49 const fixture = new URL(
50 "./fixtures/launcher-production.tsx",
51 import.meta.url,
52 ).href;
53 yield* fs.writeFileString(
54 path.join(bin, "alchemy.js"),
55 `await import(${JSON.stringify(fixture)});\n`,
56 );
57 if (jsx !== undefined) {
58 yield* fs.writeFileString(
59 path.join(project, "tsconfig.json"),
60 JSON.stringify({
61 compilerOptions: {
62 jsx,
63 ...(jsx === "preserve" ? { jsxImportSource: "solid-js" } : {}),
64 },
65 }),
66 );
67 }
68 const args = ["deploy", "stack.run.ts", "--stage", "test", "--yes"];
69 const handle = yield* ChildProcess.make(
70 runtime,
71 [
72 ...(runtime === "bun"
73 ? ["--bun", "alchemy"]
74 : [path.join(bin, "cli.js")]),
75 ...args,
76 ],
77 {
78 cwd: project,
79 env: {
80 NODE_ENV: nodeEnv,
81 CI: "true",
82 npm_execpath: "",
83 npm_config_user_agent: `bun/${process.versions.bun}`,
84 BUN_OPTIONS: "",
85 BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
86 },
87 extendEnv: true,
88 stdin: "ignore",
89 stdout: "pipe",
90 stderr: "pipe",
91 forceKillAfter: "1 second",
92 },
93 );
94 const [stdout, stderr, exitCode] = yield* Effect.all(
95 [
96 handle.stdout.pipe(Stream.decodeText, Stream.mkString),
97 handle.stderr.pipe(Stream.decodeText, Stream.mkString),
98 handle.exitCode,
99 ],
100 { concurrency: 3 },
101 );
102 expect({ exitCode, stderr, failure: exitCode === 0 ? "" : stdout }).toEqual(
103 {
104 exitCode: 0,
105 stderr: "",
106 failure: "",
107 },
108 );
109 expect(stdout).toContain("Starting deployment");
110 expect(stdout).toContain("Deployment complete");
111 expect(stdout).toContain(
112 JSON.stringify({
113 cwd: yield* fs.realPath(project),
114 args,
115 }),
116 );
117 }).pipe(Effect.scoped, Effect.provide(PlatformServices));
118
119describe.sequential(
120 "published Bun launcher",
121 { tags: ["unit", "local"] },
122 () => {
123 it.live.skipIf(!nodeSupportsDevMode)(
124 "renders production progress through the Node shebang handoff",
125 () => runPublishedLauncher("development", "preserve", nodePath!),
126 );
127 for (const nodeEnv of [undefined, "development", "production"]) {
128 for (const jsx of [undefined, "react-jsx", "react-jsxdev", "preserve"]) {
129 it.live(
130 `renders production progress with NODE_ENV=${nodeEnv} and jsx=${jsx}`,
131 () => runPublishedLauncher(nodeEnv, jsx),
132 );
133 }
134 }
135 },
136);