1import * as NodeFsPromises from "node:fs/promises";
2import * as NodePath from "node:path";
3import { 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 */
11const 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 */
19const 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. */
30const 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
48describe("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});