1import { PlatformServices } from "@/Util/PlatformServices.ts";
2import { expect, it } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import * as ChildProcess from "effect/process/ChildProcess";
6import { fileURLToPath } from "node:url";
7import { nodePath, nodeSupportsDevMode } from "../nodeProbe.ts";
8
9// Pins buildless node dev end to end: the register-dev-mode hooks must
10// (1) resolve the monorepo's own packages through their `bun` export
11// condition onto src/ — otherwise the CLI runs from src while a user's
12// stack (which imports `alchemy`) resolves built lib/, splitting the
13// process across two copies of the package — while leaving packages
14// without a `bun` condition (published sigil) on their built output, and
15// (2) transpile `.tsx` with ALCHEMY'S tsconfig (react-jsx) regardless of
16// the invoking project: the child's cwd is an example on purpose. Oxc resolves
17// the nearest tsconfig per source file, so the CLI's React files cannot inherit
18// the example's JSX settings (which previously produced classic-runtime output
19// and "ReferenceError: React is not defined").
20it.live.skipIf(!nodeSupportsDevMode)(
21 "dev-mode hooks resolve monorepo packages to src and transpile tsx",
22 () =>
23 Effect.gen(function* () {
24 const packageDir = fileURLToPath(new URL("../..", import.meta.url));
25 const exampleDir = fileURLToPath(
26 new URL("../../../../examples/aws-dev", import.meta.url),
27 );
28 const runtimeTsx = fileURLToPath(
29 new URL("../../src/Cli/components/view/Runtime.tsx", import.meta.url),
30 );
31 const resolutionProbe = fileURLToPath(
32 new URL("./fixtures/register-dev-mode-probe.ts", import.meta.url),
33 );
34 // No explicit sigil probe: two-arg import.meta.resolve is flagged in
35 // node, and Runtime.tsx importing sigil transitively already proves
36 // the published package (no `bun` condition) loads from its dist.
37 const script = `
38 const probe = await import(${JSON.stringify(resolutionProbe)});
39 console.log("alchemy=" + probe.alchemyUrl);
40 console.log("sub=" + probe.subpathUrl);
41 await import(${JSON.stringify(runtimeTsx)});
42 console.log("tsx=ok");
43 `;
44 const handle = yield* ChildProcess.make(
45 nodePath!,
46 [
47 "--import",
48 `${packageDir}/bin/register-dev-mode.js`,
49 "--input-type=module",
50 "-e",
51 script,
52 ],
53 {
54 cwd: exampleDir,
55 env: {
56 NO_COLOR: "1",
57 // The Bun test runner's markers must not select Bun for this
58 // explicitly Node/pnpm invocation.
59 npm_execpath: "",
60 npm_config_user_agent: "",
61 },
62 extendEnv: true,
63 stdin: "ignore",
64 stdout: "pipe",
65 stderr: "pipe",
66 killSignal: "SIGKILL",
67 },
68 );
69 const [stdout, stderr, exitCode] = yield* Effect.all(
70 [
71 handle.stdout.pipe(
72 Stream.decodeText,
73 Stream.runCollect,
74 Effect.map((chunks) => chunks.join("")),
75 ),
76 handle.stderr.pipe(
77 Stream.decodeText,
78 Stream.runCollect,
79 Effect.map((chunks) => chunks.join("")),
80 ),
81 handle.exitCode,
82 ],
83 { concurrency: 3 },
84 );
85 expect(stderr).toBe("");
86 expect(exitCode).toBe(0);
87 expect(stdout).toMatch(/alchemy=file:.*\/src\/index\.ts/);
88 expect(stdout).toMatch(/sub=file:.*\/src\/Cloudflare\/index\.ts/);
89 expect(stdout).toContain("tsx=ok");
90
91 const repoDir = fileURLToPath(new URL("../../../..", import.meta.url));
92 const pnpm = Bun.which("pnpm");
93 expect(pnpm).not.toBeNull();
94 const cli = yield* ChildProcess.make(pnpm!, ["alchemy", "--version"], {
95 cwd: repoDir,
96 env: {
97 NO_COLOR: "1",
98 // The Bun test runner's markers must not select Bun for this
99 // explicitly Node/pnpm invocation.
100 npm_execpath: "",
101 npm_config_user_agent: "",
102 },
103 extendEnv: true,
104 stdin: "ignore",
105 stdout: "pipe",
106 stderr: "pipe",
107 killSignal: "SIGKILL",
108 });
109 const [cliStdout, cliStderr, cliExitCode] = yield* Effect.all(
110 [
111 cli.stdout.pipe(
112 Stream.decodeText,
113 Stream.runCollect,
114 Effect.map((chunks) => chunks.join("")),
115 ),
116 cli.stderr.pipe(
117 Stream.decodeText,
118 Stream.runCollect,
119 Effect.map((chunks) => chunks.join("")),
120 ),
121 cli.exitCode,
122 ],
123 { concurrency: 3 },
124 );
125 expect(cliStderr).toBe("");
126 expect(cliExitCode).toBe(0);
127 expect(cliStdout).toMatch(/alchemy v.*\(node [^,]+, src\)/);
128 }).pipe(Effect.scoped, Effect.provide(PlatformServices)),
129 { tags: ["local"], timeout: 60_000 },
130);