rwsdk 1.2.10 → 1.2.12

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.
@@ -34,6 +34,13 @@ export function validateClickEvent(event, target) {
34
34
  }
35
35
  let IS_CLIENT_NAVIGATION = false;
36
36
  let scrollRestoration = null;
37
+ let currentPathKey = null;
38
+ function getLocationPathKey() {
39
+ return `${window.location.pathname ?? ""}${window.location.search ?? ""}`;
40
+ }
41
+ function getUrlPathKey(url) {
42
+ return `${url.pathname ?? ""}${url.search ?? ""}` || getLocationPathKey();
43
+ }
37
44
  export async function navigate(href, options = { history: "push" }) {
38
45
  if (!IS_CLIENT_NAVIGATION) {
39
46
  window.location.href = href;
@@ -52,6 +59,7 @@ export async function navigate(href, options = { history: "push" }) {
52
59
  else {
53
60
  scrollRestoration?.replaceEntry(href, url, nextScrollPosition);
54
61
  }
62
+ currentPathKey = getUrlPathKey(url);
55
63
  if (scrollToTop) {
56
64
  scrollRestoration?.setPendingScroll({
57
65
  ...nextScrollPosition,
@@ -109,6 +117,7 @@ export function initClientNavigation(opts = {}) {
109
117
  IS_CLIENT_NAVIGATION = true;
110
118
  scrollRestoration = createScrollRestoration();
111
119
  scrollRestoration.initialize();
120
+ currentPathKey = getLocationPathKey();
112
121
  document.addEventListener("click", async function handleClickEvent(event) {
113
122
  if (!validateClickEvent(event, event.target)) {
114
123
  return;
@@ -120,6 +129,12 @@ export function initClientNavigation(opts = {}) {
120
129
  await navigate(href, { history: "push", onNavigate: opts.onNavigate });
121
130
  }, true);
122
131
  window.addEventListener("popstate", async function handlePopState() {
132
+ const nextPathKey = getLocationPathKey();
133
+ const isHashOnlyChange = nextPathKey === currentPathKey;
134
+ currentPathKey = nextPathKey;
135
+ if (isHashOnlyChange) {
136
+ return;
137
+ }
123
138
  scrollRestoration?.restorePopStateScroll();
124
139
  await opts.onNavigate?.();
125
140
  await globalThis.__rsc_callServer(null, null, "navigation");
@@ -103,7 +103,7 @@ describe("onNavigate callback (issue #1123 regression)", () => {
103
103
  }),
104
104
  });
105
105
  vi.stubGlobal("window", {
106
- location: { href: "http://localhost/" },
106
+ location: { href: "http://localhost/", pathname: "/", search: "" },
107
107
  addEventListener: vi.fn((event, handler) => {
108
108
  if (event === "popstate")
109
109
  capturedPopstateHandler = handler;
@@ -167,6 +167,7 @@ describe("onNavigate callback (issue #1123 regression)", () => {
167
167
  const onNavigate = vi.fn();
168
168
  initClientNavigation({ onNavigate });
169
169
  expect(capturedPopstateHandler).not.toBeNull();
170
+ window.location.pathname = "/about";
170
171
  await capturedPopstateHandler();
171
172
  expect(onNavigate).toHaveBeenCalled();
172
173
  });
@@ -208,11 +209,13 @@ describe("initClientNavigation", () => {
208
209
  let capturedScrollHandler = null;
209
210
  let capturedPagehideHandler = null;
210
211
  let capturedVisibilityChangeHandler = null;
212
+ let capturedPopstateHandler = null;
211
213
  beforeEach(() => {
212
214
  historyState = {};
213
215
  capturedScrollHandler = null;
214
216
  capturedPagehideHandler = null;
215
217
  capturedVisibilityChangeHandler = null;
218
+ capturedPopstateHandler = null;
216
219
  vi.clearAllMocks();
217
220
  const mockHistory = {
218
221
  scrollRestoration: "auto",
@@ -236,7 +239,7 @@ describe("initClientNavigation", () => {
236
239
  }),
237
240
  });
238
241
  vi.stubGlobal("window", {
239
- location: { href: "http://localhost/" },
242
+ location: { href: "http://localhost/", pathname: "/", search: "" },
240
243
  addEventListener: vi.fn((event, handler) => {
241
244
  if (event === "scroll") {
242
245
  capturedScrollHandler = handler;
@@ -244,6 +247,9 @@ describe("initClientNavigation", () => {
244
247
  if (event === "pagehide") {
245
248
  capturedPagehideHandler = handler;
246
249
  }
250
+ if (event === "popstate") {
251
+ capturedPopstateHandler = handler;
252
+ }
247
253
  }),
248
254
  history: mockHistory,
249
255
  fetch: vi.fn(),
@@ -279,6 +285,22 @@ describe("initClientNavigation", () => {
279
285
  initClientNavigation();
280
286
  expect(history.scrollRestoration).toBe("manual");
281
287
  });
288
+ it("ignores hash-only popstate events so anchor links keep their native scroll", async () => {
289
+ const onNavigate = vi.fn();
290
+ globalThis.__rsc_callServer = vi.fn().mockResolvedValue(undefined);
291
+ const { onHydrated } = initClientNavigation({ onNavigate });
292
+ expect(capturedPopstateHandler).not.toBeNull();
293
+ window.location.hash =
294
+ "#heading";
295
+ window.location.href =
296
+ "http://localhost/#heading";
297
+ window.scrollY = 500;
298
+ await capturedPopstateHandler();
299
+ onHydrated();
300
+ expect(onNavigate).not.toHaveBeenCalled();
301
+ expect(globalThis.__rsc_callServer).not.toHaveBeenCalled();
302
+ expect(window.scrollTo).not.toHaveBeenCalled();
303
+ });
282
304
  it("does not write to history state on scroll", () => {
283
305
  initClientNavigation();
284
306
  expect(capturedScrollHandler).not.toBeNull();
@@ -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 {
@@ -2,7 +2,16 @@ import { DurableObject } from "cloudflare:workers";
2
2
  import { DODialect } from "kysely-do";
3
3
  import { Kysely, ParseJSONResultsPlugin, } from "kysely";
4
4
  import debug from "../debug.js";
5
+ import { DOSqliteIntrospector } from "./DOSqliteIntrospector.js";
5
6
  import { createMigrator } from "./index.js";
7
+ // context(justinvdm, 9 Jun 2026): Wrapper around kysely-do's DODialect that
8
+ // uses our custom introspector to avoid SQLITE_AUTH on Cloudflare internal
9
+ // _cf_* tables. See DOSqliteIntrospector for details.
10
+ class DODialectWithCustomIntrospector extends DODialect {
11
+ createIntrospector(db) {
12
+ return new DOSqliteIntrospector(db);
13
+ }
14
+ }
6
15
  const log = debug("sdk:do-db");
7
16
  // Base class for Durable Objects that need Kysely database access
8
17
  export class SqliteDurableObject extends DurableObject {
@@ -12,7 +21,7 @@ export class SqliteDurableObject extends DurableObject {
12
21
  this.migrations = migrations;
13
22
  this.migrationTableName = migrationTableName;
14
23
  this.kysely = new Kysely({
15
- dialect: new DODialect({ ctx }),
24
+ dialect: new DODialectWithCustomIntrospector({ ctx }),
16
25
  plugins: plugins,
17
26
  });
18
27
  }
@@ -94,10 +94,8 @@ export const transformServerFunctions = (code, normalizedId, environment, server
94
94
  process.env.VERBOSE &&
95
95
  log(`Transforming for ${environment} environment: normalizedId=%s`, normalizedId);
96
96
  const exportInfo = findExportInfo(code, normalizedId);
97
- const allExports = new Set([
98
- ...exportInfo.localFunctions,
99
- ...exportInfo.reExports.map((r) => r.localName),
100
- ]);
97
+ const reExportNames = new Set(exportInfo.reExports.map((r) => r.localName));
98
+ const allExports = new Set(Array.from(exportInfo.localFunctions).filter((name) => !reExportNames.has(name)));
101
99
  // Check for default function exports that should also be named exports
102
100
  const defaultFunctionName = findDefaultFunctionName(code, normalizedId);
103
101
  if (defaultFunctionName) {
@@ -105,11 +103,26 @@ export const transformServerFunctions = (code, normalizedId, environment, server
105
103
  }
106
104
  // Generate completely new code for SSR
107
105
  const s = new MagicString("");
108
- if (environment === "ssr") {
109
- s.append('import { createServerReference } from "rwsdk/__ssr";\n\n');
106
+ const hasDefExport = hasDefaultExport(code, normalizedId);
107
+ if (allExports.size > 0 || hasDefExport) {
108
+ if (environment === "ssr") {
109
+ s.append('import { createServerReference } from "rwsdk/__ssr";\n\n');
110
+ }
111
+ else {
112
+ s.append('import { createServerReference } from "rwsdk/client";\n\n');
113
+ }
110
114
  }
111
- else {
112
- s.append('import { createServerReference } from "rwsdk/client";\n\n');
115
+ for (const reExport of exportInfo.reExports) {
116
+ const reExportStatement = reExport.originalName === "default"
117
+ ? `export { default as ${reExport.localName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`
118
+ : reExport.originalName === reExport.localName
119
+ ? `export { ${reExport.originalName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`
120
+ : `export { ${reExport.originalName} as ${reExport.localName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`;
121
+ s.append(reExportStatement);
122
+ log(`Preserved ${environment} re-export for function: %s (original: %s) from %s in normalizedId=%s`, reExport.localName, reExport.originalName, reExport.moduleSpecifier, normalizedId);
123
+ }
124
+ if (exportInfo.reExports.length > 0 && allExports.size > 0) {
125
+ s.append("\n");
113
126
  }
114
127
  const ext = path.extname(normalizedId).toLowerCase();
115
128
  const lang = ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : SgLang.TypeScript;
@@ -151,7 +164,7 @@ export const transformServerFunctions = (code, normalizedId, environment, server
151
164
  }
152
165
  }
153
166
  // Check for default export in the actual module (not re-exports)
154
- if (hasDefaultExport(code, normalizedId)) {
167
+ if (hasDefExport) {
155
168
  let method;
156
169
  let source = "action";
157
170
  const patterns = [
@@ -169,6 +169,17 @@ export const getProject = serverQuery([
169
169
  SERVER_QUERY_ARRAY_POST_CODE,
170
170
  };
171
171
  describe("TRANSFORMS", () => {
172
+ it("preserves client re-exports so serverQuery metadata comes from the defining module", () => {
173
+ const barrelResult = transformServerFunctions(`
174
+ "use server";
175
+
176
+ export { getProject } from "./queries";
177
+ `, "/actions.ts", "client", new Set());
178
+ expect(barrelResult?.code).toContain(`export { getProject } from "./queries";`);
179
+ expect(barrelResult?.code).not.toContain(`createServerReference("/actions.ts", "getProject")`);
180
+ const queryResult = transformServerFunctions(SERVER_QUERY_GET_CODE, "/queries.ts", "client", new Set());
181
+ expect(queryResult?.code).toContain(`createServerReference("/queries.ts", "getProject", "GET", "query")`);
182
+ });
172
183
  for (const [key, CODE] of Object.entries(TEST_CASES)) {
173
184
  describe(key, () => {
174
185
  it(`CLIENT`, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {