rwsdk 1.2.11 → 1.2.12-test.20260615101652

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.
@@ -38,8 +38,10 @@ export declare function runRelease(cwd: string, projectDir: string, resourceUniq
38
38
  }>;
39
39
  /**
40
40
  * Run a local production preview server (build + preview) and return the URL.
41
+ * Uses port 0 so concurrent tests in the same playground do not collide on
42
+ * the fixed 4173 port.
41
43
  */
42
- export declare function runPreviewServer(packageManager?: string, cwd?: string): Promise<{
44
+ export declare function runPreviewServer(packageManager?: string, cwd?: string, port?: string | number): Promise<{
43
45
  url: string;
44
46
  stopPreview: () => Promise<void>;
45
47
  }>;
@@ -7,8 +7,8 @@ import { parse as parseJsonc } from "jsonc-parser";
7
7
  import { setTimeout } from "node:timers/promises";
8
8
  import { basename, dirname, join, relative, resolve } from "path";
9
9
  import { $ } from "../../lib/$.mjs";
10
- import { checkServerUp } from "./browser.mjs";
11
10
  import { extractLastJson, parseJson } from "../../lib/jsonUtils.mjs";
11
+ import { checkServerUp } from "./browser.mjs";
12
12
  import { IS_DEBUG_MODE, PREVIEW_SERVER_TIMEOUT } from "./constants.mjs";
13
13
  const log = debug("rwsdk:e2e:release");
14
14
  /**
@@ -403,8 +403,10 @@ export async function runRelease(cwd, projectDir, resourceUniqueKey) {
403
403
  }
404
404
  /**
405
405
  * Run a local production preview server (build + preview) and return the URL.
406
+ * Uses port 0 so concurrent tests in the same playground do not collide on
407
+ * the fixed 4173 port.
406
408
  */
407
- export async function runPreviewServer(packageManager = "pnpm", cwd) {
409
+ export async function runPreviewServer(packageManager = "pnpm", cwd, port = "0") {
408
410
  console.log("🚀 Building for production preview...");
409
411
  const pm = packageManager === "yarn-classic" ? "yarn" : packageManager;
410
412
  await $(pm, ["run", "build"], {
@@ -430,7 +432,11 @@ export async function runPreviewServer(packageManager = "pnpm", cwd) {
430
432
  await previewProcess.catch(() => { });
431
433
  console.log("Preview server stopped");
432
434
  };
433
- previewProcess = $(pm, ["run", "preview", "--", "--port", "4173", "--strictPort"], {
435
+ let serverUrlResolver;
436
+ const serverUrlPromise = new Promise((resolve) => {
437
+ serverUrlResolver = resolve;
438
+ });
439
+ previewProcess = $(pm, ["run", "preview", "--port", String(port), "--strictPort"], {
434
440
  all: true,
435
441
  detached: process.platform !== "win32",
436
442
  cleanup: true,
@@ -439,6 +445,24 @@ export async function runPreviewServer(packageManager = "pnpm", cwd) {
439
445
  env: { ...process.env, NODE_ENV: "production" },
440
446
  stdio: "pipe",
441
447
  });
448
+ // Vite prints the local URL once it has bound to a port, e.g.
449
+ // " ➜ Local: http://localhost:55271/"
450
+ // Strip ANSI escape codes first: when color output is enabled the port can
451
+ // be wrapped in its own color sequence (e.g. "http://localhost:\u001b[1m52829"),
452
+ // which breaks a naive regex.
453
+ const urlPattern = /Local:\s+(http:\/\/localhost:\d+)/i;
454
+ const ansiEscapePattern = /\u001b\[[0-9;]*m/g;
455
+ previewProcess.all?.on("data", (data) => {
456
+ const chunk = data.toString();
457
+ if (IS_DEBUG_MODE) {
458
+ process.stdout.write(chunk);
459
+ }
460
+ const strippedChunk = chunk.replace(ansiEscapePattern, "");
461
+ const match = strippedChunk.match(urlPattern);
462
+ if (match && match[1]) {
463
+ serverUrlResolver(match[1]);
464
+ }
465
+ });
442
466
  previewProcess.catch((error) => {
443
467
  if (!isErrorExpected) {
444
468
  log("Preview server process exited unexpectedly: %O", error);
@@ -468,13 +492,10 @@ export async function runPreviewServer(packageManager = "pnpm", cwd) {
468
492
  await fs.writeFile(deployConfigPath, JSON.stringify(deployConfig, null, 2));
469
493
  };
470
494
  await ensurePreviewDeployConfig();
471
- // context(justinvdm, 2026-05-13): Give the CI preview path the same
472
- // readiness budget as the dev server so local agent-ci runs can absorb build
473
- // and startup latency without falling back to Cloudflare.
474
- const reachableDebugUrl = await checkServerUp("http://localhost:4173", "/__debug", Math.max(1, Math.ceil(PREVIEW_SERVER_TIMEOUT / 2000)), false);
475
- const serverUrl = new URL(reachableDebugUrl).origin;
476
- console.log(`✅ Preview server started at ${serverUrl}`);
477
- return { url: serverUrl, stopPreview };
495
+ const serverUrl = await serverUrlPromise;
496
+ const reachableDebugUrl = await checkServerUp(serverUrl, "/__debug", Math.max(1, Math.ceil(PREVIEW_SERVER_TIMEOUT / 2000)), false);
497
+ console.log(`✅ Preview server started at ${new URL(reachableDebugUrl).origin}`);
498
+ return { url: new URL(reachableDebugUrl).origin, stopPreview };
478
499
  }
479
500
  /**
480
501
  * Check if a resource name includes a specific resource unique key
@@ -44,6 +44,14 @@ export interface SetupPlaygroundEnvironmentOptions {
44
44
  * @default true
45
45
  */
46
46
  autoStartDevServer?: boolean;
47
+ /**
48
+ * Whether to automatically start the deployment after provisioning it.
49
+ * Set to false when the test manages its own deployment lifecycle (e.g.
50
+ * redeploys). The deploy playground environment is still set up so
51
+ * `createDeployment()` can be called from the test.
52
+ * @default true
53
+ */
54
+ autoStartDeployment?: boolean;
47
55
  }
48
56
  /**
49
57
  * A Vitest hook that sets up a playground environment for a test file.
@@ -67,6 +75,7 @@ export declare function createDevServer(): {
67
75
  export declare function createDeployment(): {
68
76
  projectDir: string;
69
77
  start: () => Promise<DeploymentInstance>;
78
+ redeploy: () => Promise<DeploymentInstance>;
70
79
  };
71
80
  /**
72
81
  * Executes a test function with a retry mechanism for specific error codes.
@@ -98,8 +98,8 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
98
98
  * This ensures that tests run in a clean, isolated environment.
99
99
  */
100
100
  export function setupPlaygroundEnvironment(options) {
101
- const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, autoStartDevServer = true, } = typeof options === "string"
102
- ? { sourceProjectDir: options, autoStartDevServer: true }
101
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, autoStartDevServer = true, autoStartDeployment = true, } = typeof options === "string"
102
+ ? { sourceProjectDir: options, autoStartDevServer: true, autoStartDeployment: true }
103
103
  : options;
104
104
  ensureHooksRegistered();
105
105
  beforeAll(async () => {
@@ -150,15 +150,17 @@ export function setupPlaygroundEnvironment(options) {
150
150
  projectDir: deployEnv.targetDir,
151
151
  cleanup: deployEnv.cleanup,
152
152
  };
153
- const deployControl = createDeployment();
154
- globalDeploymentInstancePromise = deployControl
155
- .start()
156
- .then((instance) => {
157
- globalDeploymentInstance = instance;
158
- return instance;
159
- });
160
- // Prevent unhandled promise rejections
161
- globalDeploymentInstancePromise.catch(() => { });
153
+ if (autoStartDeployment) {
154
+ const deployControl = createDeployment();
155
+ globalDeploymentInstancePromise = deployControl
156
+ .start()
157
+ .then((instance) => {
158
+ globalDeploymentInstance = instance;
159
+ return instance;
160
+ });
161
+ // Prevent unhandled promise rejections
162
+ globalDeploymentInstancePromise.catch(() => { });
163
+ }
162
164
  }
163
165
  else {
164
166
  globalDeployPlaygroundEnv = null;
@@ -213,88 +215,120 @@ export function createDeployment() {
213
215
  }
214
216
  const { projectDir } = globalDeployPlaygroundEnv;
215
217
  let instance = null;
218
+ const dirName = basename(projectDir);
219
+ // Match formats: {projectName}-t-{hash}, {projectName}-test-{hash}, or {projectName}-e2e-test-{hash}
220
+ const match = dirName.match(/-t-([a-f0-9]+)$/) ||
221
+ dirName.match(/-test-([a-f0-9]+)$/) ||
222
+ dirName.match(/-e2e-test-([a-f0-9]+)$/);
223
+ const resourceUniqueKey = match
224
+ ? match[1]
225
+ : Math.random().toString(36).substring(2, 15);
226
+ if (SKIP_DEPLOYMENT_TESTS) {
227
+ return {
228
+ projectDir,
229
+ start: async () => {
230
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
231
+ },
232
+ redeploy: async () => {
233
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
234
+ },
235
+ };
236
+ }
237
+ const deployToCloudflare = async () => {
238
+ const newInstance = await pollValue(async () => {
239
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
240
+ // A fresh *.workers.dev subdomain can return 200 with Cloudflare's
241
+ // "There is nothing here yet" placeholder before the worker code
242
+ // propagates globally. Wait until the response body contains the
243
+ // rwsdk-rendered marker so tests don't run against the placeholder.
244
+ await poll(async () => {
245
+ try {
246
+ const response = await fetch(deployResult.url);
247
+ const body = await response.text();
248
+ return body.includes("__RWSDK_CONTEXT");
249
+ }
250
+ catch (e) {
251
+ return false;
252
+ }
253
+ }, {
254
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
255
+ });
256
+ const cleanup = async () => {
257
+ const performCleanup = async () => {
258
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
259
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
260
+ }
261
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
262
+ };
263
+ performCleanup().catch((error) => {
264
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
265
+ });
266
+ return Promise.resolve();
267
+ };
268
+ return {
269
+ url: deployResult.url,
270
+ workerName: deployResult.workerName,
271
+ resourceUniqueKey,
272
+ projectDir: projectDir,
273
+ cleanup,
274
+ };
275
+ }, {
276
+ timeout: DEPLOYMENT_TIMEOUT,
277
+ minTries: DEPLOYMENT_MIN_TRIES,
278
+ onRetry: (error, tries) => {
279
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
280
+ },
281
+ });
282
+ return newInstance;
283
+ };
284
+ const startPreview = async (port = "0") => {
285
+ console.log("PR mode detected — using local preview instead of deploy");
286
+ const previewResult = await runPreviewServer(process.env.PACKAGE_MANAGER || "pnpm", projectDir, port);
287
+ return {
288
+ url: previewResult.url,
289
+ workerName: `preview-${resourceUniqueKey}`,
290
+ resourceUniqueKey,
291
+ projectDir,
292
+ cleanup: async () => {
293
+ await previewResult.stopPreview().catch((error) => {
294
+ console.warn(`Warning: Background preview cleanup failed: ${error.message}`);
295
+ });
296
+ return Promise.resolve();
297
+ },
298
+ };
299
+ };
216
300
  return {
217
301
  projectDir,
218
302
  start: async () => {
219
303
  if (instance)
220
304
  return instance;
221
- if (SKIP_DEPLOYMENT_TESTS) {
222
- throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
223
- }
224
- const dirName = basename(projectDir);
225
- // Match formats: {projectName}-t-{hash}, {projectName}-test-{hash}, or {projectName}-e2e-test-{hash}
226
- const match = dirName.match(/-t-([a-f0-9]+)$/) ||
227
- dirName.match(/-test-([a-f0-9]+)$/) ||
228
- dirName.match(/-e2e-test-([a-f0-9]+)$/);
229
- const resourceUniqueKey = match
230
- ? match[1]
231
- : Math.random().toString(36).substring(2, 15);
232
- if (IS_PULL_REQUEST) {
233
- console.log("PR mode detected — using local preview instead of deploy");
234
- const previewResult = await runPreviewServer(process.env.PACKAGE_MANAGER ||
235
- "pnpm", projectDir);
236
- instance = {
237
- url: previewResult.url,
238
- workerName: `preview-${resourceUniqueKey}`,
239
- resourceUniqueKey,
240
- projectDir,
241
- cleanup: async () => {
242
- await previewResult.stopPreview().catch((error) => {
243
- console.warn(`Warning: Background preview cleanup failed: ${error.message}`);
244
- });
245
- return Promise.resolve();
246
- },
247
- };
248
- deploymentInstances.push(instance);
249
- return instance;
305
+ instance = IS_PULL_REQUEST
306
+ ? await startPreview()
307
+ : await deployToCloudflare();
308
+ deploymentInstances.push(instance);
309
+ return instance;
310
+ },
311
+ redeploy: async () => {
312
+ // Stop/clean up the previous preview instance so the new one can bind
313
+ // to the same port. For real Cloudflare deploys the worker is updated
314
+ // in place, so no intermediate cleanup is needed.
315
+ let redeployPort = "0";
316
+ if (instance) {
317
+ const oldInstance = instance;
318
+ const index = deploymentInstances.indexOf(oldInstance);
319
+ if (index !== -1) {
320
+ deploymentInstances.splice(index, 1);
321
+ }
322
+ if (IS_PULL_REQUEST) {
323
+ redeployPort = new URL(oldInstance.url).port;
324
+ await oldInstance.cleanup();
325
+ }
250
326
  }
251
- const newInstance = await pollValue(async () => {
252
- const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
253
- // A fresh *.workers.dev subdomain can return 200 with Cloudflare's
254
- // "There is nothing here yet" placeholder before the worker code
255
- // propagates globally. Wait until the response body contains the
256
- // rwsdk-rendered marker so tests don't run against the placeholder.
257
- await poll(async () => {
258
- try {
259
- const response = await fetch(deployResult.url);
260
- const body = await response.text();
261
- return body.includes("__RWSDK_CONTEXT");
262
- }
263
- catch (e) {
264
- return false;
265
- }
266
- }, {
267
- timeout: DEPLOYMENT_CHECK_TIMEOUT,
268
- });
269
- const cleanup = async () => {
270
- const performCleanup = async () => {
271
- if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
272
- await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
273
- }
274
- await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
275
- };
276
- performCleanup().catch((error) => {
277
- console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
278
- });
279
- return Promise.resolve();
280
- };
281
- return {
282
- url: deployResult.url,
283
- workerName: deployResult.workerName,
284
- resourceUniqueKey,
285
- projectDir: projectDir,
286
- cleanup,
287
- };
288
- }, {
289
- timeout: DEPLOYMENT_TIMEOUT,
290
- minTries: DEPLOYMENT_MIN_TRIES,
291
- onRetry: (error, tries) => {
292
- console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
293
- },
294
- });
295
- instance = newInstance;
296
- deploymentInstances.push(newInstance);
297
- return newInstance;
327
+ instance = IS_PULL_REQUEST
328
+ ? await startPreview(redeployPort)
329
+ : await deployToCloudflare();
330
+ deploymentInstances.push(instance);
331
+ return instance;
298
332
  },
299
333
  };
300
334
  }
@@ -14,6 +14,7 @@ export { default as React } from "react";
14
14
  export { ClientOnly } from "./ClientOnly.js";
15
15
  export { initClientNavigation, navigate } from "./navigation.js";
16
16
  import { getCachedNavigationResponse } from "./navigationCache.js";
17
+ import { createClientVersionHeaders, isStaleReloadResponse, } from "./stale.js";
17
18
  import { isActionResponse } from "./types";
18
19
  export const fetchTransport = (transportContext) => {
19
20
  const fetchCallServer = async (id, args, source = "action", method = "POST") => {
@@ -38,27 +39,27 @@ export const fetchTransport = (transportContext) => {
38
39
  // Fall back to network fetch on cache miss
39
40
  fetchPromise = fetch(url, {
40
41
  method: "GET",
42
+ headers: createClientVersionHeaders(),
41
43
  redirect: "manual",
42
44
  });
43
45
  }
44
46
  }
45
47
  else {
46
- const headers = new Headers();
47
- // Add x-rsc-data-only header if we want to skip the React tree render on the server
48
+ const extraHeaders = {};
48
49
  if (source === "query") {
49
- headers.set("x-rsc-data-only", "true");
50
+ extraHeaders["x-rsc-data-only"] = "true";
50
51
  }
51
52
  if (method === "GET") {
52
53
  fetchPromise = fetch(url, {
53
54
  method: "GET",
54
- headers,
55
+ headers: createClientVersionHeaders(extraHeaders),
55
56
  redirect: "manual",
56
57
  });
57
58
  }
58
59
  else {
59
60
  fetchPromise = fetch(url, {
60
61
  method: "POST",
61
- headers,
62
+ headers: createClientVersionHeaders(extraHeaders),
62
63
  redirect: "manual",
63
64
  body: args != null ? await encodeReply(args) : null,
64
65
  });
@@ -83,9 +84,14 @@ export const fetchTransport = (transportContext) => {
83
84
  }
84
85
  return rawActionResult;
85
86
  };
87
+ const response = await fetchPromise;
88
+ // Single place where stale reload happens
89
+ if (isStaleReloadResponse(response)) {
90
+ window.location.reload();
91
+ return undefined;
92
+ }
86
93
  // If there's a response handler, check the response first
87
94
  if (transportContext.handleResponse) {
88
- const response = await fetchPromise;
89
95
  const shouldContinue = transportContext.handleResponse(response);
90
96
  if (!shouldContinue) {
91
97
  return undefined;
@@ -100,8 +106,7 @@ export const fetchTransport = (transportContext) => {
100
106
  const result = await streamData;
101
107
  return processActionResponse(result.actionResult);
102
108
  }
103
- // Original behavior when no handler is present
104
- const response = await fetchPromise;
109
+ // Fallback when no handler is present
105
110
  const location = response.headers.get("Location");
106
111
  if (response.status >= 300 && response.status < 400 && location) {
107
112
  window.location.href = location;
@@ -1,5 +1,6 @@
1
+ import { createClientVersionHeaders, getClientBuildVersion, } from "./stale.js";
1
2
  const TAB_ID_STORAGE_KEY = "rwsdk-navigation-tab-id";
2
- const BUILD_ID = "rwsdk"; // Stable build identifier
3
+ const DEFAULT_BUILD_ID = "rwsdk";
3
4
  let cacheState = null;
4
5
  function getOrInitializeCacheState() {
5
6
  if (cacheState) {
@@ -22,7 +23,7 @@ function getOrInitializeCacheState() {
22
23
  cacheState = {
23
24
  tabId: tabId || "1",
24
25
  generation: 0,
25
- buildId: BUILD_ID,
26
+ buildId: getClientBuildVersion() ?? DEFAULT_BUILD_ID,
26
27
  };
27
28
  return cacheState;
28
29
  }
@@ -127,9 +128,7 @@ export async function preloadNavigationUrl(rawUrl, env, cacheStorage) {
127
128
  const request = new Request(url.toString(), {
128
129
  method: "GET",
129
130
  redirect: "manual",
130
- headers: {
131
- "x-prefetch": "true",
132
- },
131
+ headers: createClientVersionHeaders({ "x-prefetch": "true" }),
133
132
  });
134
133
  const cacheName = getCurrentCacheName();
135
134
  const cache = await storage.open(cacheName);
@@ -0,0 +1,5 @@
1
+ import { CLIENT_VERSION_HEADER, isStaleClientError } from "../lib/stale.js";
2
+ export declare function getClientBuildVersion(): string | undefined;
3
+ export declare function isStaleReloadResponse(response: Response): boolean;
4
+ export declare function createClientVersionHeaders(extraHeaders?: Record<string, string>): Headers;
5
+ export { CLIENT_VERSION_HEADER, isStaleClientError };
@@ -0,0 +1,21 @@
1
+ import { CLIENT_VERSION_HEADER, STALE_RESPONSE_HEADER, STALE_RESPONSE_VALUE_RELOAD, isStaleClientError, } from "../lib/stale.js";
2
+ export function getClientBuildVersion() {
3
+ return import.meta.env.VITE_RWSDK_BUILD_ID;
4
+ }
5
+ export function isStaleReloadResponse(response) {
6
+ return (response.headers?.get(STALE_RESPONSE_HEADER) === STALE_RESPONSE_VALUE_RELOAD);
7
+ }
8
+ export function createClientVersionHeaders(extraHeaders) {
9
+ const headers = new Headers();
10
+ const clientBuildVersion = getClientBuildVersion();
11
+ if (clientBuildVersion) {
12
+ headers.set(CLIENT_VERSION_HEADER, clientBuildVersion);
13
+ }
14
+ if (extraHeaders) {
15
+ for (const [key, value] of Object.entries(extraHeaders)) {
16
+ headers.set(key, value);
17
+ }
18
+ }
19
+ return headers;
20
+ }
21
+ export { CLIENT_VERSION_HEADER, isStaleClientError };
@@ -4,5 +4,7 @@ interface ImportMeta {
4
4
  interface ImportMetaEnv {
5
5
  readonly DEV: boolean;
6
6
  readonly VITE_IS_DEV_SERVER: string;
7
+ readonly VITE_RWSDK_BUILD_ID: string;
8
+ readonly VITE_RWSDK_SYNCED_STATE_TEST_FAST_RECONNECT?: string;
7
9
  readonly BASE_URL: string;
8
10
  }
@@ -0,0 +1,8 @@
1
+ import { type DatabaseMetadata, type DatabaseMetadataOptions, type Kysely, type SchemaMetadata, type TableMetadata } from "kysely";
2
+ export declare class DOSqliteIntrospector {
3
+ #private;
4
+ constructor(db: Kysely<any>);
5
+ getSchemas(): Promise<SchemaMetadata[]>;
6
+ getTables(options?: DatabaseMetadataOptions): Promise<TableMetadata[]>;
7
+ getMetadata(options: DatabaseMetadataOptions): Promise<DatabaseMetadata>;
8
+ }
@@ -0,0 +1,129 @@
1
+ // NOTE(justinvdm, 9 Jun 2026): This file copies Kysely's SqliteIntrospector.
2
+ // Before trying to simplify it, read this comment.
3
+ //
4
+ // Problem: Cloudflare's DO SQLite adds internal tables (_cf_KV, _cf_METADATA)
5
+ // when storage.put/setAlarm is used. Kysely's SqliteIntrospector discovers
6
+ // these via sqlite_master, then runs PRAGMA table_info on each. Cloudflare's
7
+ // authorizer rejects PRAGMA on _cf_* tables with SQLITE_AUTH, breaking
8
+ // migrations and rendering the DO unusable.
9
+ //
10
+ // Why we copied instead of composed:
11
+ // - Kysely's SqliteIntrospector uses JS private fields (#db, #tablesQuery,
12
+ // #getTableMetadata). Private fields are truly private — subclasses cannot
13
+ // override them, and wrappers cannot intercept internal calls.
14
+ // - We considered a lighter approach: query sqlite_master with Kysely's
15
+ // builder, then run PRAGMA table_info per-table. This is lighter but
16
+ // requires inlining the table name into raw SQL. Even with escaping,
17
+ // any injection risk is unacceptable. The CTE approach below uses
18
+ // `pragma_table_info(tl.name)` where `tl.name` is a column reference,
19
+ // not an inlined value — zero injection surface.
20
+ // - We also considered Kysely plugins (AST rewriting, SQL string rewriting)
21
+ // but these are fragile: they depend on Kysely's internal AST shape and
22
+ // SQL formatting, both of which can change between releases.
23
+ //
24
+ // This is a copy of Kysely's SqliteIntrospector (MIT licensed) with one
25
+ // change: `.where('name', 'not like', '_cf_%')` to exclude Cloudflare tables.
26
+ //
27
+ // See: https://github.com/redwoodjs/sdk/issues/1219
28
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
29
+ if (kind === "m") throw new TypeError("Private method is not writable");
30
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
31
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
32
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
33
+ };
34
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
35
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
36
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
37
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
38
+ };
39
+ var _DOSqliteIntrospector_instances, _DOSqliteIntrospector_db, _DOSqliteIntrospector_tablesQuery, _DOSqliteIntrospector_getTableMetadata;
40
+ import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql, } from "kysely";
41
+ export class DOSqliteIntrospector {
42
+ constructor(db) {
43
+ _DOSqliteIntrospector_instances.add(this);
44
+ _DOSqliteIntrospector_db.set(this, void 0);
45
+ __classPrivateFieldSet(this, _DOSqliteIntrospector_db, db, "f");
46
+ }
47
+ async getSchemas() {
48
+ // Sqlite doesn't support schemas.
49
+ return [];
50
+ }
51
+ async getTables(options = { withInternalKyselyTables: false }) {
52
+ return await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_getTableMetadata).call(this, options);
53
+ }
54
+ async getMetadata(options) {
55
+ return {
56
+ tables: await this.getTables(options),
57
+ };
58
+ }
59
+ }
60
+ _DOSqliteIntrospector_db = new WeakMap(), _DOSqliteIntrospector_instances = new WeakSet(), _DOSqliteIntrospector_tablesQuery = function _DOSqliteIntrospector_tablesQuery(qb, options) {
61
+ let tablesQuery = qb
62
+ .selectFrom("sqlite_master")
63
+ .where("type", "in", ["table", "view"])
64
+ .where("name", "not like", "sqlite_%")
65
+ // context(justinvdm, 9 Jun 2026): Exclude Cloudflare internal tables.
66
+ // These are added by the DO runtime and cannot be introspected.
67
+ .where("name", "not like", "_cf_%")
68
+ .select(["name", "sql", "type"])
69
+ .orderBy("name");
70
+ if (!options.withInternalKyselyTables) {
71
+ tablesQuery = tablesQuery
72
+ .where("name", "!=", DEFAULT_MIGRATION_TABLE)
73
+ .where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE);
74
+ }
75
+ return tablesQuery;
76
+ }, _DOSqliteIntrospector_getTableMetadata = async function _DOSqliteIntrospector_getTableMetadata(options) {
77
+ const tablesResult = await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f"), options).execute();
78
+ const tableMetadata = await __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f")
79
+ .with("table_list", (qb) => __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, qb, options))
80
+ .selectFrom([
81
+ "table_list as tl",
82
+ sql `pragma_table_info(tl.name)`.as("p"),
83
+ ])
84
+ .select([
85
+ "tl.name as table",
86
+ "p.cid",
87
+ "p.name",
88
+ "p.type",
89
+ "p.notnull",
90
+ "p.dflt_value",
91
+ "p.pk",
92
+ ])
93
+ .orderBy("tl.name")
94
+ .orderBy("p.cid")
95
+ .execute();
96
+ const columnsByTable = {};
97
+ for (const row of tableMetadata) {
98
+ columnsByTable[row.table] ??= [];
99
+ columnsByTable[row.table].push(row);
100
+ }
101
+ return tablesResult.map(({ name, sql: tableSql, type }) => {
102
+ let autoIncrementCol = tableSql
103
+ ?.split(/[\(\),]/)
104
+ ?.find((it) => it.toLowerCase().includes("autoincrement"))
105
+ ?.trimStart()
106
+ ?.split(/\s+/)?.[0]
107
+ ?.replace(/["`]/g, "");
108
+ const columns = columnsByTable[name] ?? [];
109
+ if (!autoIncrementCol) {
110
+ const pkCols = columns.filter((r) => r.pk > 0);
111
+ if (pkCols.length === 1 &&
112
+ pkCols[0].type.toLowerCase() === "integer") {
113
+ autoIncrementCol = pkCols[0].name;
114
+ }
115
+ }
116
+ return {
117
+ name: name,
118
+ isView: type === "view",
119
+ columns: columns.map((col) => ({
120
+ name: col.name,
121
+ dataType: col.type,
122
+ isNullable: !col.notnull,
123
+ isAutoIncrementing: col.name === autoIncrementCol,
124
+ hasDefaultValue: col.dflt_value != null,
125
+ comment: undefined,
126
+ })),
127
+ };
128
+ });
129
+ };
@@ -1,4 +1,5 @@
1
- import { DatabaseConnection, Driver, QueryResult, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
1
+ import { DatabaseConnection, Driver, QueryResult, SqliteAdapter, SqliteQueryCompiler } from "kysely";
2
+ import { DOSqliteIntrospector } from "./DOSqliteIntrospector.js";
2
3
  type DOWorkerDialectConfig = {
3
4
  kyselyExecuteQuery: (compiledQuery: {
4
5
  sql: string;
@@ -11,7 +12,7 @@ export declare class DOWorkerDialect {
11
12
  createAdapter(): SqliteAdapter;
12
13
  createDriver(): DOWorkerDriver;
13
14
  createQueryCompiler(): SqliteQueryCompiler;
14
- createIntrospector(db: any): SqliteIntrospector;
15
+ createIntrospector(db: any): DOSqliteIntrospector;
15
16
  }
16
17
  declare class DOWorkerDriver implements Driver {
17
18
  config: DOWorkerDialectConfig;
@@ -1,5 +1,6 @@
1
- import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from "kysely";
1
+ import { SqliteAdapter, SqliteQueryCompiler, } from "kysely";
2
2
  import debug from "../debug";
3
+ import { DOSqliteIntrospector } from "./DOSqliteIntrospector.js";
3
4
  const log = debug("sdk:db:do-worker-dialect");
4
5
  export class DOWorkerDialect {
5
6
  constructor(config) {
@@ -15,7 +16,7 @@ export class DOWorkerDialect {
15
16
  return new SqliteQueryCompiler();
16
17
  }
17
18
  createIntrospector(db) {
18
- return new SqliteIntrospector(db);
19
+ return new DOSqliteIntrospector(db);
19
20
  }
20
21
  }
21
22
  class DOWorkerDriver {