@solcreek/adapter-creek 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -11,10 +11,57 @@
11
11
  */
12
12
  import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
+ import { createRequire } from "node:module";
15
+ import { pathToFileURL } from "node:url";
14
16
  import { generateWorkerEntry } from "./worker-entry.js";
15
17
  import { bundleForWorkers } from "./bundler.js";
16
18
  import { writeManifest } from "./manifest.js";
17
19
  const OUTPUT_DIR = ".creek/adapter-output";
20
+ /**
21
+ * Decode Prisma 7's query-compiler WASM and stage it as a real `.wasm` file so
22
+ * it flows through the CompiledWasm pipeline (precompiled at bundle time,
23
+ * registered by byte length). Prisma's client instantiates the compiler via
24
+ * `new WebAssembly.Module(bytes)` from a base64-embedded `.mjs`, which workerd
25
+ * rejects at runtime ("Wasm code generation disallowed by embedder"); the
26
+ * worker-entry Module patch swaps the matching precompiled module in by length.
27
+ *
28
+ * Only the sqlite `fast` variant matters on D1: it is the generator default
29
+ * and the only base64 module the generated client imports (so the only byte
30
+ * length queried at runtime). Staging `small` too would just add an
31
+ * unreferenced CompiledWasm module to the bundle — dead weight — so we don't.
32
+ * A project that overrides `compilerBuild = "small"` is unsupported for now.
33
+ * No-op when @prisma/client isn't installed.
34
+ */
35
+ async function collectPrismaCompilerWasm(projectDir, outputDir, wasmFiles) {
36
+ let runtimeDir;
37
+ try {
38
+ const req = createRequire(path.join(projectDir, "noop.js"));
39
+ runtimeDir = path.join(path.dirname(req.resolve("@prisma/client/package.json")), "runtime");
40
+ }
41
+ catch {
42
+ return; // Not a Prisma project.
43
+ }
44
+ const base = "query_compiler_fast_bg.sqlite";
45
+ const mjs = path.join(runtimeDir, `${base}.wasm-base64.mjs`);
46
+ try {
47
+ // Import the base64 module's `wasm` named export (no regex parsing).
48
+ const mod = (await import(pathToFileURL(mjs).href));
49
+ if (!mod.wasm)
50
+ return;
51
+ const bytes = Buffer.from(mod.wasm, "base64");
52
+ const stageDir = path.join(outputDir, ".prisma-wasm");
53
+ await fs.mkdir(stageDir, { recursive: true });
54
+ const dest = path.join(stageDir, `${base}.wasm`);
55
+ await fs.writeFile(dest, bytes);
56
+ wasmFiles.set(`${base}.wasm`, dest);
57
+ console.log(` [Creek Adapter] Prisma compiler wasm staged: ${base} (${bytes.byteLength} bytes)`);
58
+ }
59
+ catch {
60
+ // Absent/unreadable (or not a sqlite Prisma project) — skip. A genuinely
61
+ // missing compiler surfaces as the original workerd error at runtime,
62
+ // which is the pre-existing behaviour.
63
+ }
64
+ }
18
65
  export async function handleBuild(ctx) {
19
66
  const outputDir = path.join(ctx.projectDir, OUTPUT_DIR);
20
67
  const assetsDir = path.join(outputDir, "assets");
@@ -94,6 +141,10 @@ export async function handleBuild(ctx) {
94
141
  }
95
142
  }
96
143
  }
144
+ // Stage Prisma 7's query-compiler WASM (sqlite) so a Prisma app runs on D1
145
+ // unchanged: its base64-embedded compiler is precompiled here and swapped in
146
+ // at runtime by the worker-entry WebAssembly.Module patch. No-op otherwise.
147
+ await collectPrismaCompilerWasm(ctx.projectDir, outputDir, wasmFiles);
97
148
  // Step 3: Collect manifests from .next/ for embedding in the worker.
98
149
  // Next.js route modules call loadManifest() which uses fs.readFileSync().
99
150
  // CF Workers doesn't have fs, so we embed all manifests and shim the loader.
package/dist/bundler.js CHANGED
@@ -13,6 +13,10 @@ import { execFileSync } from "node:child_process";
13
13
  import { builtinModules, createRequire } from "node:module";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { WORKER_COMPATIBILITY_DATE, WORKER_COMPATIBILITY_FLAGS } from "./compat.js";
16
+ /** Escape a string for safe interpolation into a RegExp source. */
17
+ function escapeRegExp(s) {
18
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
16
20
  /**
17
21
  * Resolve wrangler's CLI entry script through Node module resolution.
18
22
  *
@@ -1077,6 +1081,32 @@ export async function bundleForWorkers(opts) {
1077
1081
  await fs.rm(entryPath, { force: true });
1078
1082
  await fs.rm(configPath, { force: true });
1079
1083
  await fs.rm(bundleDir, { recursive: true, force: true });
1084
+ // Prune orphaned plain wasm copies. We write each wasm under its plain name
1085
+ // (above) so wrangler can resolve `import __wasm from "./<name>.wasm"`;
1086
+ // wrangler then emits a content-hashed sibling `<hash>-<name>.wasm` and
1087
+ // rewrites the import to it. The plain copy is left behind unreferenced —
1088
+ // dead weight in the uploaded script (significant for large wasm like
1089
+ // Prisma's ~3.5MB query compiler). Delete a plain copy only when a hashed
1090
+ // sibling exists AND the bundled worker no longer imports the plain path, so
1091
+ // wasm that wrangler kept un-hashed is never touched.
1092
+ try {
1093
+ const outFiles = await fs.readdir(opts.outputDir);
1094
+ const workerCode = await fs.readFile(path.join(opts.outputDir, "worker.js"), "utf-8");
1095
+ for (const [name] of opts.wasmFiles) {
1096
+ const plain = name.endsWith(".wasm") ? name : name + ".wasm";
1097
+ if (!outFiles.includes(plain))
1098
+ continue;
1099
+ const hashedSibling = new RegExp(`^[0-9a-f]{6,}-${escapeRegExp(plain)}$`);
1100
+ const hasHashed = outFiles.some((f) => hashedSibling.test(f));
1101
+ const stillImportsPlain = workerCode.includes(`./${plain}`);
1102
+ if (hasHashed && !stillImportsPlain) {
1103
+ await fs.rm(path.join(opts.outputDir, plain), { force: true });
1104
+ }
1105
+ }
1106
+ }
1107
+ catch {
1108
+ // Best-effort: if worker.js is absent or readdir fails, keep all files.
1109
+ }
1080
1110
  // List output files
1081
1111
  const files = await fs.readdir(opts.outputDir);
1082
1112
  return files.filter(f => !f.startsWith("__"));
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ const SHIMS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..",
13
13
  const DB_DRIVER_ALIASES = {
14
14
  // Drizzle: `drizzle-orm/better-sqlite3` → D1-backed drizzle. No WASM.
15
15
  "drizzle-orm/better-sqlite3$": path.join(SHIMS_DIR, "drizzle-better-sqlite3.js"),
16
+ // Prisma 7: `@prisma/adapter-better-sqlite3` → D1-backed PrismaD1 adapter.
17
+ // The query-compiler WASM is precompiled in build.ts and swapped in by the
18
+ // worker-entry WebAssembly.Module patch.
19
+ "@prisma/adapter-better-sqlite3$": path.join(SHIMS_DIR, "prisma-adapter-better-sqlite3.js"),
16
20
  // The native better-sqlite3 client the user passes to drizzle() — stubbed,
17
21
  // since the swap ignores it and uses env.DB. Keeps the native .node out of
18
22
  // the Workers bundle.
@@ -353,6 +353,31 @@ if (typeof WebAssembly !== "undefined" && typeof WebAssembly.instantiate === "fu
353
353
  return __origCompile(bytes);
354
354
  };
355
355
  }
356
+ // Prisma 7's query compiler instantiates its wasm synchronously via
357
+ // \`new WebAssembly.Module(bytes)\` (decoded from a base64 module), which
358
+ // workerd also rejects. Swap in the pre-compiled CompiledWasm module
359
+ // (registered by byte length at build time) when the bytes match; a
360
+ // constructor returning an object yields that object to the \`new\`
361
+ // expression. Falls back to the original elsewhere.
362
+ if (typeof WebAssembly.Module === "function") {
363
+ const __OrigModule = WebAssembly.Module;
364
+ const __ModulePatched = function(bytes) {
365
+ const precompiled = __findPrecompiled(bytes);
366
+ if (precompiled) return precompiled;
367
+ return Reflect.construct(__OrigModule, arguments, __ModulePatched);
368
+ };
369
+ __ModulePatched.prototype = __OrigModule.prototype;
370
+ if (typeof __OrigModule.imports === "function") {
371
+ __ModulePatched.imports = function(m) { return __OrigModule.imports(m); };
372
+ }
373
+ if (typeof __OrigModule.exports === "function") {
374
+ __ModulePatched.exports = function(m) { return __OrigModule.exports(m); };
375
+ }
376
+ if (typeof __OrigModule.customSections === "function") {
377
+ __ModulePatched.customSections = function(m, n) { return __OrigModule.customSections(m, n); };
378
+ }
379
+ WebAssembly.Module = __ModulePatched;
380
+ }
356
381
  }
357
382
 
358
383
  // Polyfill process methods and env that Next.js uses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/adapter-creek",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Next.js deployment adapter for Creek (Cloudflare Workers)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,6 +46,7 @@
46
46
  "dependencies": {
47
47
  "@next/routing": "16.2.3",
48
48
  "@node-rs/xxhash": "^1.7.6",
49
+ "@prisma/adapter-d1": "^7.8.0",
49
50
  "@solcreek/adapter-core": "^0.2.0",
50
51
  "@solcreek/adapter-next-core": "^0.1.1",
51
52
  "sql.js": "^1.14.1",
@@ -12,6 +12,17 @@ vi.mock("drizzle-orm/d1", () => ({
12
12
  drizzle: (client: unknown, config: unknown) => ({ __d1Client: client, __config: config }),
13
13
  }));
14
14
 
15
+ // The Prisma shim imports `@prisma/adapter-d1` (an adapter-creek dependency).
16
+ // Mock PrismaD1 so connect() can be asserted without a live D1 binding.
17
+ vi.mock("@prisma/adapter-d1", () => ({
18
+ PrismaD1: class {
19
+ db: unknown;
20
+ constructor(db: unknown) { this.db = db; }
21
+ connect() { return { kind: "d1-adapter", db: this.db }; }
22
+ connectToShadowDb() { return { kind: "d1-shadow", db: this.db }; }
23
+ },
24
+ }));
25
+
15
26
  const SHIMS_DIR = path.dirname(fileURLToPath(import.meta.url));
16
27
 
17
28
  afterEach(() => {
@@ -35,6 +46,9 @@ describe("DB driver-swap aliases (modifyConfig)", () => {
35
46
  expect(alias["drizzle-orm/better-sqlite3$"]).toBe(
36
47
  path.join(SHIMS_DIR, "drizzle-better-sqlite3.js"),
37
48
  );
49
+ expect(alias["@prisma/adapter-better-sqlite3$"]).toBe(
50
+ path.join(SHIMS_DIR, "prisma-adapter-better-sqlite3.js"),
51
+ );
38
52
  expect(alias["better-sqlite3$"]).toBe(
39
53
  path.join(SHIMS_DIR, "better-sqlite3-stub.js"),
40
54
  );
@@ -103,6 +117,38 @@ describe("drizzle-better-sqlite3 shim", () => {
103
117
  });
104
118
  });
105
119
 
120
+ describe("prisma-adapter-better-sqlite3 shim", () => {
121
+ it("exposes provider synchronously and ignores the local config", async () => {
122
+ const { PrismaBetterSqlite3 } = await import("./prisma-adapter-better-sqlite3.js");
123
+ // Constructed at module scope, before any request env exists.
124
+ const adapter = new PrismaBetterSqlite3({ url: "file:./dev.db" });
125
+ expect(adapter.provider).toBe("sqlite");
126
+ expect(adapter.adapterName).toBe("@prisma/adapter-d1");
127
+ });
128
+
129
+ it("connects against env.DB lazily (at first query, not construction)", async () => {
130
+ const { PrismaBetterSqlite3 } = await import("./prisma-adapter-better-sqlite3.js");
131
+ const adapter = new PrismaBetterSqlite3({ url: "file:./dev.db" });
132
+
133
+ const fakeD1 = { tag: "d1" };
134
+ (globalThis as { __creekEnv?: () => unknown }).__creekEnv = () => ({ DB: fakeD1 });
135
+
136
+ const conn = adapter.connect() as { kind: string; db: unknown };
137
+ expect(conn.kind).toBe("d1-adapter");
138
+ expect(conn.db).toBe(fakeD1);
139
+
140
+ const shadow = adapter.connectToShadowDb() as { kind: string };
141
+ expect(shadow.kind).toBe("d1-shadow");
142
+ });
143
+
144
+ it("throws a helpful error when env.DB is unavailable at connect()", async () => {
145
+ const { PrismaBetterSqlite3 } = await import("./prisma-adapter-better-sqlite3.js");
146
+ const adapter = new PrismaBetterSqlite3({ url: "file:./dev.db" });
147
+ (globalThis as { __creekEnv?: () => unknown }).__creekEnv = () => ({});
148
+ expect(() => adapter.connect()).toThrow(/D1 binding `env\.DB` is unavailable/);
149
+ });
150
+ });
151
+
106
152
  describe("better-sqlite3 stub", () => {
107
153
  it("constructs but refuses queries (swap uses D1 instead)", async () => {
108
154
  const { default: Database } = await import("./better-sqlite3-stub.js");
@@ -0,0 +1,49 @@
1
+ // Creek build-time swap for `@prisma/adapter-better-sqlite3` → Cloudflare D1.
2
+ //
3
+ // Only active in the Creek/Workers build (aliased by adapter-creek's
4
+ // modifyConfig); local dev keeps the real better-sqlite3 adapter. Prisma 7
5
+ // requires a driver adapter, so the user already passes
6
+ // `new PrismaClient({ adapter: new PrismaBetterSqlite3(...) })`. The ONLY
7
+ // env-specific difference on Workers is which adapter backs the client — the
8
+ // schema, generated client, and queries are identical. We mirror the
9
+ // better-sqlite3 factory's shape but back it with `@prisma/adapter-d1` over the
10
+ // request's D1 binding.
11
+ //
12
+ // The user constructs the adapter at module scope, before env.DB exists.
13
+ // Prisma reads `provider` synchronously at PrismaClient construction and only
14
+ // calls `connect()` at the first query (request time), so D1 is resolved
15
+ // lazily inside connect() — never at construction.
16
+ import { PrismaD1 } from "@prisma/adapter-d1";
17
+
18
+ function resolveD1() {
19
+ const env = (globalThis.__creekEnv && globalThis.__creekEnv()) || {};
20
+ const db = env.DB;
21
+ if (!db) {
22
+ throw new Error(
23
+ "[creek] D1 binding `env.DB` is unavailable. Add `database = true` under [resources] in creek.toml.",
24
+ );
25
+ }
26
+ return db;
27
+ }
28
+
29
+ class PrismaBetterSqlite3 {
30
+ constructor() {
31
+ // Read synchronously by @prisma/client at construction time.
32
+ this.provider = "sqlite";
33
+ this.adapterName = "@prisma/adapter-d1";
34
+ }
35
+
36
+ connect() {
37
+ return new PrismaD1(resolveD1()).connect();
38
+ }
39
+
40
+ connectToShadowDb() {
41
+ const factory = new PrismaD1(resolveD1());
42
+ return factory.connectToShadowDb
43
+ ? factory.connectToShadowDb()
44
+ : factory.connect();
45
+ }
46
+ }
47
+
48
+ export { PrismaBetterSqlite3 };
49
+ export default PrismaBetterSqlite3;