@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 +51 -0
- package/dist/bundler.js +30 -0
- package/dist/index.js +4 -0
- package/dist/worker-entry.js +25 -0
- package/package.json +2 -1
- package/src/shims/db-driver-swap.test.ts +46 -0
- package/src/shims/prisma-adapter-better-sqlite3.js +49 -0
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.
|
package/dist/worker-entry.js
CHANGED
|
@@ -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.
|
|
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;
|