1/**
2 * Enforces the deploy-target decoupling contract: the framework-generic
3 * modules (`SvelteKit.ts`, `index.ts`) must contain ZERO Cloudflare-specific
4 * imports. Everything platform-specific (the adapter fork, the worker shim,
5 * the rolldown finishing pass) lives behind the `./cloudflare` subpath
6 * module.
7 */
8import * as NodeFs from "node:fs";
9import * as NodePath from "node:path";
10import { fileURLToPath } from "node:url";
11import { describe, expect, it } from "vitest";
12
13const srcDir = NodePath.resolve(
14 NodePath.dirname(fileURLToPath(import.meta.url)),
15 "..",
16);
17
18/** Every static import / re-export specifier in a module's source. */
19const importSpecifiers = (file: string): Array<string> => {
20 const source = NodeFs.readFileSync(NodePath.join(srcDir, file), "utf8");
21 const specifiers: Array<string> = [];
22 // `import ... from "x"`, `export ... from "x"`, and bare `import "x"`.
23 const pattern =
24 /(?:^|\n)\s*(?:import|export)\s+(?:[^"'\n]*?from\s*)?["']([^"']+)["']/g;
25 for (const match of source.matchAll(pattern)) {
26 specifiers.push(match[1] as string);
27 }
28 return specifiers;
29};
30
31// Cloudflare-specific dependency surface: platform packages, the bundler for
32// the workerd finishing pass, and the in-package cloudflare modules.
33const FORBIDDEN = [
34 /^@alchemy\.run\/cloudflare-runtime(?:\/|$)/,
35 /^@cloudflare\//,
36 /^cloudflare:/,
37 /^\.\/cloudflare(?:\.ts)?$/,
38 /^rolldown$/,
39 /\/Adapter(\.ts)?$/,
40 /\.\/Adapter/,
41 /\.\/WorkerShim/,
42];
43
44const FRAMEWORK_GENERIC_MODULES = ["SvelteKit.ts", "UserConfig.ts", "index.ts"];
45
46describe("target decoupling", () => {
47 for (const file of FRAMEWORK_GENERIC_MODULES) {
48 it(`${file} imports nothing Cloudflare-specific`, () => {
49 const specifiers = importSpecifiers(file);
50 expect(specifiers.length).toBeGreaterThan(0);
51 const offending = specifiers.filter((specifier) =>
52 FORBIDDEN.some((pattern) => pattern.test(specifier)),
53 );
54 expect(offending).toEqual([]);
55 });
56 }
57
58 it("platform target modules are the only src modules importing the adapter and rolldown", () => {
59 const files = NodeFs.readdirSync(srcDir).filter((file) =>
60 file.endsWith(".ts"),
61 );
62 for (const file of files) {
63 if (
64 file === "cloudflare.ts" ||
65 file === "Adapter.ts" ||
66 file === "WorkerShim.ts"
67 )
68 continue;
69 // `source.ts` is Cloudflare-specific by contract (it implements
70 // alchemy's Cloudflare Worker source) — it may import ./cloudflare.ts
71 // but must not reach around it to the adapter/bundler internals.
72 // `aws.ts` is the AWS target module — platform-specific by the same
73 // contract: it bundles the Lambda server with rolldown, but must
74 // stay decoupled from the Cloudflare adapter internals.
75 const allowRolldown = file === "aws.ts" || file === "node.ts";
76 const specifiers = importSpecifiers(file);
77 const offending = specifiers.filter(
78 (specifier) =>
79 (!allowRolldown && /^rolldown$/.test(specifier)) ||
80 /\.\/Adapter/.test(specifier) ||
81 /\.\/WorkerShim/.test(specifier) ||
82 /@alchemy\.run\/cloudflare-runtime/.test(specifier),
83 );
84 expect(
85 offending,
86 `${file} must not import ${offending.join(", ")}`,
87 ).toEqual([]);
88 }
89 });
90});