| 1 | import { PlatformServices } from "@/Util/PlatformServices.ts"; |
| 2 | import { expect, it } from "alchemy-test"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Stream from "effect/Stream"; |
| 5 | import * as ChildProcess from "effect/process/ChildProcess"; |
| 6 | import { fileURLToPath } from "node:url"; |
| 7 | import { 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"). |
| 20 | it.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 | ); |