| 1 | import * as NodeFsPromises from "node:fs/promises"; |
| 2 | import * as NodePath from "node:path"; |
| 3 | import { describe, expect, it } from "vitest"; |
| 4 | |
| 5 | /** |
| 6 | * The framework-generic orchestration modules. Everything Cloudflare-specific |
| 7 | * must live behind the `./cloudflare` deploy-target subpath (plus the |
| 8 | * `./adapter` fork it selects and the alchemy `./source` provider, which are |
| 9 | * Cloudflare modules by nature). |
| 10 | */ |
| 11 | const GENERIC_MODULES = ["Waku.ts", "index.ts"]; |
| 12 | |
| 13 | /** |
| 14 | * Module specifiers a framework-generic module must never import. The default |
| 15 | * target module *specifier string* (`"@alchemy.run/frontend-frameworks/waku/cloudflare"`) is |
| 16 | * allowed as data — it is resolved from the project's node_modules at |
| 17 | * runtime, never imported by the generic module graph. |
| 18 | */ |
| 19 | const FORBIDDEN = [ |
| 20 | /^@alchemy\.run\/cloudflare-runtime/, // runtime, Vite, Rolldown, ... |
| 21 | /^@cloudflare\//, |
| 22 | /cloudflare:/, |
| 23 | /^wrangler/, |
| 24 | /(^|\/)adapter(\.ts|\.js)?$/, // the adapter fork |
| 25 | /(^|\/)cloudflare(\.ts|\.js)?$/, // the ./cloudflare target module |
| 26 | /^@alchemy\.run\/frameworks\/waku\/(adapter|cloudflare)$/, |
| 27 | ]; |
| 28 | |
| 29 | /** Extract every static/dynamic import + re-export specifier. */ |
| 30 | const importSpecifiers = (source: string): Array<string> => { |
| 31 | const specifiers: Array<string> = []; |
| 32 | const patterns = [ |
| 33 | // import ... from "x"; export ... from "x"; |
| 34 | /(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*["']([^"']+)["']/g, |
| 35 | // side-effect imports: import "x"; |
| 36 | /(?:^|\n)\s*import\s*["']([^"']+)["']/g, |
| 37 | // dynamic imports: import("x") |
| 38 | /import\s*\(\s*["']([^"']+)["']\s*\)/g, |
| 39 | ]; |
| 40 | for (const pattern of patterns) { |
| 41 | for (const match of source.matchAll(pattern)) { |
| 42 | specifiers.push(match[1]!); |
| 43 | } |
| 44 | } |
| 45 | return specifiers; |
| 46 | }; |
| 47 | |
| 48 | describe("target decoupling", () => { |
| 49 | for (const module of GENERIC_MODULES) { |
| 50 | it(`src/${module} imports nothing cloudflare-specific`, async () => { |
| 51 | const source = await NodeFsPromises.readFile( |
| 52 | NodePath.resolve(import.meta.dirname, "..", module), |
| 53 | "utf8", |
| 54 | ); |
| 55 | const specifiers = importSpecifiers(source); |
| 56 | // Sanity: the extractor sees the module's real imports. |
| 57 | expect(specifiers.length).toBeGreaterThan(0); |
| 58 | const offending = specifiers.filter((specifier) => |
| 59 | FORBIDDEN.some((pattern) => pattern.test(specifier)), |
| 60 | ); |
| 61 | expect(offending).toEqual([]); |
| 62 | }); |
| 63 | } |
| 64 | |
| 65 | it("src/cloudflare.ts is the module that owns the cloudflare imports", async () => { |
| 66 | const source = await NodeFsPromises.readFile( |
| 67 | NodePath.resolve(import.meta.dirname, "../cloudflare.ts"), |
| 68 | "utf8", |
| 69 | ); |
| 70 | const specifiers = importSpecifiers(source); |
| 71 | expect(specifiers).toContain("@alchemy.run/cloudflare-runtime/vite"); |
| 72 | }); |
| 73 | }); |