rwsdk 1.0.1 → 1.0.3

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.
@@ -0,0 +1 @@
1
+ export declare function stripBase(path: string, base: string): string;
@@ -0,0 +1,5 @@
1
+ export function stripBase(path, base) {
2
+ return base && base !== "/" && path.startsWith(base)
3
+ ? "/" + path.slice(base.length)
4
+ : path;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { stripBase } from "./stripBase.mjs";
3
+ describe("stripBase", () => {
4
+ it("should strip base prefix from path", () => {
5
+ expect(stripBase("/auth/src/client.tsx", "/auth/")).toBe("/src/client.tsx");
6
+ });
7
+ it("should be a no-op when base is /", () => {
8
+ expect(stripBase("/src/client.tsx", "/")).toBe("/src/client.tsx");
9
+ });
10
+ it("should be a no-op when path does not start with base", () => {
11
+ expect(stripBase("/src/client.tsx", "/auth/")).toBe("/src/client.tsx");
12
+ });
13
+ it("should handle path equal to base", () => {
14
+ expect(stripBase("/auth/", "/auth/")).toBe("/");
15
+ });
16
+ it("should handle multi-level base paths", () => {
17
+ expect(stripBase("/org/app/src/client.tsx", "/org/app/")).toBe("/src/client.tsx");
18
+ });
19
+ it("should be a no-op when base is empty string", () => {
20
+ expect(stripBase("/src/client.tsx", "")).toBe("/src/client.tsx");
21
+ });
22
+ });
@@ -317,7 +317,17 @@ export function defineRoutes(routes) {
317
317
  if (!isClientReference(componentHandler) && !requestInfo.rw.pageRouteResolved) {
318
318
  requestInfo.rw.pageRouteResolved = Promise.withResolvers();
319
319
  }
320
- return await renderPage(requestInfo, WrappedComponent, onError);
320
+ const response = await renderPage(requestInfo, WrappedComponent, onError);
321
+ // context(justinvdm, 2026-03-17): renderPage stores rendering
322
+ // errors on rw.renderError instead of throwing, to avoid
323
+ // corrupting React's internal RSC stream state. We check for
324
+ // it here and throw so except handlers can process it.
325
+ if (requestInfo.rw.renderError) {
326
+ const error = requestInfo.rw.renderError;
327
+ requestInfo.rw.renderError = undefined;
328
+ throw error;
329
+ }
330
+ return response;
321
331
  }
322
332
  // Handle non-component final handler (e.g., returns new Response)
323
333
  const tailResult = await componentHandler(getRequestInfo());
@@ -347,6 +357,10 @@ Route handlers must return one of:
347
357
  });
348
358
  }
349
359
  catch (error) {
360
+ // context(justinvdm, 2026-03-17): If pageRouteResolved was set up for
361
+ // the component that threw, resolve it so the worker doesn't hang
362
+ // waiting on it after the except handler returns its response.
363
+ getRequestInfo().rw.pageRouteResolved?.resolve();
350
364
  return await executeExceptHandlers(error, currentRouteIndex);
351
365
  }
352
366
  }
@@ -13,6 +13,7 @@ export type RwContext = {
13
13
  inlineScripts: Set<string>;
14
14
  pageRouteResolved: PromiseWithResolvers<void> | undefined;
15
15
  actionResult?: unknown;
16
+ renderError?: unknown;
16
17
  };
17
18
  export type DocumentProps<T extends RequestInfo = RequestInfo> = T & {
18
19
  children: React.ReactNode;
@@ -16,6 +16,17 @@ export const defineApp = (routes) => {
16
16
  __rwRoutes: routes,
17
17
  fetch: async (request, env, cf) => {
18
18
  globalThis.__webpack_require__ = ssrWebpackRequire;
19
+ // context(justinvdm, 17 Mar 2026): Strip the Vite base path from the
20
+ // request URL so that routes defined as "/" match requests to "/app/"
21
+ // when base: '/app/' is configured. Vite injects BASE_URL automatically.
22
+ const base = import.meta.env.BASE_URL;
23
+ if (base && base !== "/") {
24
+ const url = new URL(request.url);
25
+ if (url.pathname.startsWith(base)) {
26
+ url.pathname = "/" + url.pathname.slice(base.length);
27
+ request = new Request(url.toString(), request);
28
+ }
29
+ }
19
30
  // context(justinvdm, 5 Feb 2025): Serve assets requests using the assets service binding
20
31
  // todo(justinvdm, 5 Feb 2025): Find a way to avoid this so asset requests are served directly
21
32
  // rather than first needing to go through the worker
@@ -107,7 +118,7 @@ export const defineApp = (routes) => {
107
118
  }
108
119
  return pageElement;
109
120
  };
110
- const renderPage = async (requestInfo, Page, onError) => {
121
+ const renderPage = async (requestInfo, Page, _onError) => {
111
122
  if (isClientReference(requestInfo.rw.Document)) {
112
123
  if (import.meta.env.DEV) {
113
124
  console.error("Document cannot be a client component");
@@ -116,6 +127,17 @@ export const defineApp = (routes) => {
116
127
  status: 500,
117
128
  });
118
129
  }
130
+ // context(justinvdm, 2026-03-17): Capture rendering errors so the
131
+ // router can throw them and route to except handlers. We store the
132
+ // error on rw.renderError instead of throwing from renderPage, because
133
+ // throwing mid-render corrupts React's internal RSC stream state
134
+ // (causing "chunk.reason.enqueueModel is not a function" in subsequent
135
+ // renders). By returning normally, streams are cleaned up properly.
136
+ const onError = (error) => {
137
+ if (!rw.renderError) {
138
+ rw.renderError = error;
139
+ }
140
+ };
119
141
  const actionResult = normalizeActionResult(requestInfo.rw.actionResult);
120
142
  const isDataOnly = request.headers.get("x-rsc-data-only") === "true";
121
143
  const pageElement = isDataOnly && actionResult !== undefined
@@ -149,13 +171,28 @@ export const defineApp = (routes) => {
149
171
  nonce: rw.nonce,
150
172
  });
151
173
  }
152
- let html = await renderDocumentHtmlStream({
153
- rscPayloadStream: rscPayloadStream,
154
- Document: rw.Document,
155
- requestInfo: requestInfo,
156
- onError,
157
- shouldSSR: rw.ssr,
158
- });
174
+ let html;
175
+ try {
176
+ html = await renderDocumentHtmlStream({
177
+ rscPayloadStream: rscPayloadStream,
178
+ Document: rw.Document,
179
+ requestInfo: requestInfo,
180
+ onError,
181
+ shouldSSR: rw.ssr,
182
+ });
183
+ }
184
+ catch (renderError) {
185
+ // context(justinvdm, 2026-03-17): If renderDocumentHtmlStream throws
186
+ // AND we already captured the error via onError, return a minimal
187
+ // response. The router will detect rw.renderError and route to
188
+ // except handlers. We must not re-throw here because throwing from
189
+ // renderPage corrupts React's internal RSC stream state, preventing
190
+ // the except handler from rendering its error page.
191
+ if (rw.renderError) {
192
+ return new Response(null, { status: 500 });
193
+ }
194
+ throw renderError;
195
+ }
159
196
  if (injectRSCPayloadStream) {
160
197
  html = html.pipeThrough(injectRSCPayloadStream);
161
198
  }
@@ -167,20 +204,13 @@ export const defineApp = (routes) => {
167
204
  headers: responseHeaders,
168
205
  });
169
206
  };
170
- const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => {
171
- try {
172
- resolve(await router.handle({
173
- request,
174
- renderPage,
175
- getRequestInfo: getRequestInfo,
176
- runWithRequestInfoOverrides,
177
- onError: reject,
178
- rscActionHandler,
179
- }));
180
- }
181
- catch (e) {
182
- reject(e);
183
- }
207
+ const response = await runWithRequestInfo(outerRequestInfo, () => router.handle({
208
+ request,
209
+ renderPage,
210
+ getRequestInfo: getRequestInfo,
211
+ runWithRequestInfoOverrides,
212
+ onError: () => { },
213
+ rscActionHandler,
184
214
  }));
185
215
  // context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
186
216
  // we need to return a mutable response object.
@@ -1,7 +1,7 @@
1
1
  import debug from "debug";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, rm, writeFile } from "node:fs/promises";
4
- import { dirname, resolve } from "node:path";
3
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
6
6
  import { runDirectivesScan } from "./runDirectivesScan.mjs";
7
7
  const log = debug("rwsdk:vite:build-app");
@@ -87,4 +87,47 @@ export async function buildApp({ builder, clientEntryPoints, clientFiles, server
87
87
  };
88
88
  await builder.build(workerEnv);
89
89
  console.log("Build complete!");
90
+ // context(zshannon, 16 Mar 2026): Nest client output under base subdirectory
91
+ // for Cloudflare's assets module, which maps URL paths directly to file paths.
92
+ const base = builder.config.base || "/";
93
+ if (base !== "/") {
94
+ const subdir = base.replace(/^\/|\/$/g, "");
95
+ const clientDir = resolve(projectRootDir, "dist", "client");
96
+ const tmpDir = resolve(projectRootDir, "dist", "_client_tmp");
97
+ const nestDir = join(clientDir, subdir);
98
+ await rm(tmpDir, { force: true, recursive: true });
99
+ await cp(clientDir, tmpDir, { recursive: true });
100
+ await rm(clientDir, { force: true, recursive: true });
101
+ await mkdir(nestDir, { recursive: true });
102
+ for (const entry of await readdir(tmpDir)) {
103
+ await cp(join(tmpDir, entry), join(nestDir, entry), { recursive: true });
104
+ }
105
+ await rm(tmpDir, { force: true, recursive: true });
106
+ // context(justinvdm, 17 Mar 2026): The Cloudflare Vite plugin generates a
107
+ // wrangler.json in the dist output. We need to patch its assets.directory
108
+ // to account for the nesting we just did.
109
+ const workerDistDir = resolve(projectRootDir, "dist", "worker");
110
+ const wranglerCandidates = ["wrangler.json", "wrangler.jsonc", "wrangler.toml"];
111
+ for (const candidate of wranglerCandidates) {
112
+ const wranglerPath = join(workerDistDir, candidate);
113
+ if (existsSync(wranglerPath) && candidate !== "wrangler.toml") {
114
+ const content = await readFile(wranglerPath, "utf-8");
115
+ const wrangler = JSON.parse(content);
116
+ if (wrangler.assets?.directory) {
117
+ const currentDir = wrangler.assets.directory;
118
+ const fixedDir = currentDir.replace(`/${subdir}`, "");
119
+ if (fixedDir !== currentDir) {
120
+ wrangler.assets.directory = fixedDir;
121
+ await writeFile(wranglerPath, JSON.stringify(wrangler, null, 2));
122
+ }
123
+ else {
124
+ console.warn(`Warning: ${candidate} assets.directory "${currentDir}" ` +
125
+ `did not contain expected "/${subdir}" segment`);
126
+ }
127
+ }
128
+ break;
129
+ }
130
+ }
131
+ console.log(`Nested client assets under ${subdir}/`);
132
+ }
90
133
  }
@@ -23,8 +23,11 @@ export function linkWorkerBundle({ code, manifestContent, projectRootDir, base,
23
23
  }
24
24
  // 3. Deprefix any remaining placeholders that were not in the manifest.
25
25
  // This handles public assets that don't go through the bundler.
26
+ // context(justinvdm, 17 Mar 2026): Prepend base (without trailing slash)
27
+ // so public asset paths are correct under a non-default base.
26
28
  log("Deprefixing remaining asset placeholders");
27
- newCode = newCode.replaceAll("rwsdk_asset:", "");
29
+ const basePrefix = (base ? base : "/").replace(/\/$/, "");
30
+ newCode = newCode.replaceAll("rwsdk_asset:", basePrefix);
28
31
  return {
29
32
  code: newCode,
30
33
  map: null,
@@ -53,4 +53,14 @@ describe("linkWorkerBundle", () => {
53
53
  });
54
54
  expect(result.code).toContain(`const publicImg = "/images/photo.jpg";`);
55
55
  });
56
+ it("should deprefix remaining asset placeholders with base prefix", () => {
57
+ const code = `const publicImg = "rwsdk_asset:/images/photo.jpg";`;
58
+ const result = linkWorkerBundle({
59
+ code,
60
+ manifestContent,
61
+ projectRootDir,
62
+ base: "/app/",
63
+ });
64
+ expect(result.code).toContain(`const publicImg = "/app/images/photo.jpg";`);
65
+ });
56
66
  });
@@ -1,5 +1,5 @@
1
1
  import { type Plugin } from "vite";
2
- export declare function transformJsxScriptTagsCode(code: string, clientEntryPoints: Set<string>, manifest: Record<string, any> | undefined, projectRootDir: string): Promise<{
2
+ export declare function transformJsxScriptTagsCode(code: string, clientEntryPoints: Set<string>, manifest: Record<string, any> | undefined, projectRootDir: string, base: string): Promise<{
3
3
  code: string;
4
4
  map: null;
5
5
  } | undefined>;
@@ -1,12 +1,19 @@
1
1
  import debug from "debug";
2
2
  import { Node, Project, SyntaxKind, } from "ts-morph";
3
3
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
4
+ import { stripBase } from "../lib/stripBase.mjs";
4
5
  const log = debug("rwsdk:vite:transform-jsx-script-tags");
5
- function transformAssetPath(importPath, projectRootDir) {
6
+ function transformAssetPath(importPath, projectRootDir, base) {
6
7
  if (process.env.VITE_IS_DEV_SERVER === "1") {
8
+ // context(justinvdm, 17 Mar 2026): In dev mode with a non-default base,
9
+ // prefix the path so Vite can resolve it under the base URL.
10
+ if (base && base !== "/" && importPath.startsWith("/") && !importPath.startsWith(base)) {
11
+ return base.replace(/\/$/, "") + importPath;
12
+ }
7
13
  return importPath;
8
14
  }
9
- const normalizedImportPath = normalizeModulePath(importPath, projectRootDir);
15
+ const strippedPath = stripBase(importPath, base);
16
+ const normalizedImportPath = normalizeModulePath(strippedPath, projectRootDir);
10
17
  return `rwsdk_asset:${normalizedImportPath}`;
11
18
  }
12
19
  // Note: This plugin only runs during discovery phase (Phase 1)
@@ -25,7 +32,7 @@ function hasJsxFunctions(text) {
25
32
  text.includes('jsxDEV("link"') ||
26
33
  text.includes("jsxDEV('link'"));
27
34
  }
28
- function transformScriptImports(scriptContent, clientEntryPoints, manifest, projectRootDir) {
35
+ function transformScriptImports(scriptContent, clientEntryPoints, manifest, projectRootDir, base) {
29
36
  const scriptProject = new Project({ useInMemoryFileSystem: true });
30
37
  try {
31
38
  const wrappedContent = `function __wrapper() {${scriptContent}}`;
@@ -45,9 +52,10 @@ function transformScriptImports(scriptContent, clientEntryPoints, manifest, proj
45
52
  const importPath = args[0].getLiteralValue();
46
53
  if (importPath.startsWith("/")) {
47
54
  log("Found dynamic import with root-relative path: %s", importPath);
48
- entryPoints.push(importPath);
49
- clientEntryPoints.add(importPath);
50
- const transformedImportPath = transformAssetPath(importPath, projectRootDir);
55
+ const normalizedEntry = stripBase(importPath, base);
56
+ entryPoints.push(normalizedEntry);
57
+ clientEntryPoints.add(normalizedEntry);
58
+ const transformedImportPath = transformAssetPath(importPath, projectRootDir, base);
51
59
  args[0].setLiteralValue(transformedImportPath);
52
60
  hasChanges = true;
53
61
  }
@@ -68,7 +76,7 @@ function transformScriptImports(scriptContent, clientEntryPoints, manifest, proj
68
76
  return { content: undefined, hasChanges: false, entryPoints: [] };
69
77
  }
70
78
  }
71
- export async function transformJsxScriptTagsCode(code, clientEntryPoints, manifest = {}, projectRootDir) {
79
+ export async function transformJsxScriptTagsCode(code, clientEntryPoints, manifest = {}, projectRootDir, base) {
72
80
  // context(justinvdm, 15 Jun 2025): Optimization to exit early
73
81
  // to avoidunnecessary ts-morph parsing
74
82
  if (!hasJsxFunctions(code)) {
@@ -136,9 +144,10 @@ export async function transformJsxScriptTagsCode(code, clientEntryPoints, manife
136
144
  Node.isNoSubstitutionTemplateLiteral(initializer)) {
137
145
  const srcValue = initializer.getLiteralValue();
138
146
  if (srcValue.startsWith("/")) {
139
- entryPoints.push(srcValue);
140
- clientEntryPoints.add(srcValue);
141
- const transformedSrc = transformAssetPath(srcValue, projectRootDir);
147
+ const normalizedSrcEntry = stripBase(srcValue, base);
148
+ entryPoints.push(normalizedSrcEntry);
149
+ clientEntryPoints.add(normalizedSrcEntry);
150
+ const transformedSrc = transformAssetPath(srcValue, projectRootDir, base);
142
151
  modifications.push({
143
152
  type: "literalValue",
144
153
  node: initializer,
@@ -153,7 +162,7 @@ export async function transformJsxScriptTagsCode(code, clientEntryPoints, manife
153
162
  Node.isNoSubstitutionTemplateLiteral(initializer))) {
154
163
  hasStringLiteralChildren = true;
155
164
  const scriptContent = initializer.getLiteralValue();
156
- const { content: transformedContent, hasChanges: contentHasChanges, entryPoints: dynamicEntryPoints, } = transformScriptImports(scriptContent, clientEntryPoints, manifest, projectRootDir);
165
+ const { content: transformedContent, hasChanges: contentHasChanges, entryPoints: dynamicEntryPoints, } = transformScriptImports(scriptContent, clientEntryPoints, manifest, projectRootDir, base);
157
166
  entryPoints.push(...dynamicEntryPoints);
158
167
  if (contentHasChanges && transformedContent) {
159
168
  const isTemplateLiteral = Node.isNoSubstitutionTemplateLiteral(initializer);
@@ -189,7 +198,7 @@ export async function transformJsxScriptTagsCode(code, clientEntryPoints, manife
189
198
  Node.isNoSubstitutionTemplateLiteral(initializer))) {
190
199
  const hrefValue = initializer.getLiteralValue();
191
200
  if (hrefValue.startsWith("/")) {
192
- const transformedHref = transformAssetPath(hrefValue, projectRootDir);
201
+ const transformedHref = transformAssetPath(hrefValue, projectRootDir, base);
193
202
  modifications.push({
194
203
  type: "literalValue",
195
204
  node: initializer,
@@ -313,10 +322,12 @@ ${mod.callExprText}
313
322
  }
314
323
  export const transformJsxScriptTagsPlugin = ({ clientEntryPoints, projectRootDir, }) => {
315
324
  let isBuild = false;
325
+ let base = "/";
316
326
  return {
317
327
  name: "rwsdk:vite:transform-jsx-script-tags",
318
328
  configResolved(config) {
319
329
  isBuild = config.command === "build";
330
+ base = config.base || "/";
320
331
  },
321
332
  async transform(code, id) {
322
333
  if (isBuild &&
@@ -331,7 +342,7 @@ export const transformJsxScriptTagsPlugin = ({ clientEntryPoints, projectRootDir
331
342
  process.env.VERBOSE && log("Code:\n%s", code);
332
343
  // During discovery phase, never use manifest - it doesn't exist yet
333
344
  const result = await transformJsxScriptTagsCode(code, clientEntryPoints, {}, // Empty manifest during discovery
334
- projectRootDir);
345
+ projectRootDir, base);
335
346
  if (result) {
336
347
  log("Transformed JSX script tags in %s", id);
337
348
  process.env.VERBOSE &&
@@ -24,7 +24,7 @@ describe("transformJsxScriptTagsCode", () => {
24
24
  })
25
25
  `;
26
26
  const clientEntryPoints = new Set();
27
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/Users/justin/rw/forks/workers-sdk/sdk/sdk");
27
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/Users/justin/rw/forks/workers-sdk/sdk/sdk", "/");
28
28
  const expected = `import { requestInfo } from "rwsdk/worker";
29
29
 
30
30
  (
@@ -45,7 +45,7 @@ nonce: requestInfo.rw.nonce
45
45
  })
46
46
  `;
47
47
  const clientEntryPoints = new Set();
48
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
48
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
49
49
  const expected = `import { requestInfo } from "rwsdk/worker";
50
50
 
51
51
  (
@@ -63,7 +63,7 @@ nonce: requestInfo.rw.nonce
63
63
  jsx("script", { type: "module", children: "import('/src/client.tsx')" })
64
64
  `;
65
65
  const clientEntryPoints = new Set();
66
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
66
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
67
67
  const expected = `import { requestInfo } from "rwsdk/worker";
68
68
 
69
69
  (
@@ -89,7 +89,7 @@ nonce: requestInfo.rw.nonce
89
89
  })
90
90
  `;
91
91
  const clientEntryPoints = new Set();
92
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
92
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
93
93
  const expected = `import { requestInfo } from "rwsdk/worker";
94
94
 
95
95
  (
@@ -120,7 +120,7 @@ import('/src/entry.js');
120
120
  })
121
121
  `;
122
122
  const clientEntryPoints = new Set();
123
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
123
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
124
124
  const expected = `import { requestInfo } from "rwsdk/worker";
125
125
 
126
126
  (
@@ -146,7 +146,7 @@ nonce: requestInfo.rw.nonce
146
146
  })
147
147
  `;
148
148
  const clientEntryPoints = new Set();
149
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
149
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
150
150
  const expected = `
151
151
  jsx("link", {
152
152
  rel: "preload",
@@ -165,7 +165,7 @@ nonce: requestInfo.rw.nonce
165
165
  })
166
166
  `;
167
167
  const clientEntryPoints = new Set();
168
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
168
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
169
169
  const expected = `
170
170
  jsx("link", {
171
171
  href: "rwsdk_asset:/src/client.tsx",
@@ -197,7 +197,7 @@ nonce: requestInfo.rw.nonce
197
197
  })
198
198
  `;
199
199
  const clientEntryPoints = new Set();
200
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
200
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
201
201
  const expected = `import { requestInfo } from "rwsdk/worker";
202
202
 
203
203
  jsx("html", {
@@ -231,7 +231,7 @@ nonce: requestInfo.rw.nonce
231
231
  jsx("div", { children: "No scripts or links here" })
232
232
  `;
233
233
  const clientEntryPoints = new Set();
234
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
234
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
235
235
  expect(result).toBeUndefined();
236
236
  });
237
237
  it("handles paths not found in manifest", async () => {
@@ -242,7 +242,7 @@ nonce: requestInfo.rw.nonce
242
242
  })
243
243
  `;
244
244
  const clientEntryPoints = new Set();
245
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
245
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
246
246
  const expected = `import { requestInfo } from "rwsdk/worker";
247
247
 
248
248
  (
@@ -263,7 +263,7 @@ nonce: requestInfo.rw.nonce
263
263
  })
264
264
  `;
265
265
  const clientEntryPoints = new Set();
266
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
266
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
267
267
  const expected = `import { requestInfo } from "rwsdk/worker";
268
268
 
269
269
  (
@@ -284,7 +284,7 @@ nonce: requestInfo.rw.nonce
284
284
  })
285
285
  `;
286
286
  const clientEntryPoints = new Set();
287
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
287
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
288
288
  expect(result?.code).toEqual(`import { requestInfo } from "rwsdk/worker";
289
289
 
290
290
  jsx("script", {
@@ -302,7 +302,7 @@ nonce: requestInfo.rw.nonce
302
302
  })
303
303
  `;
304
304
  const clientEntryPoints = new Set();
305
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
305
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
306
306
  expect(result?.code).toEqual(undefined);
307
307
  });
308
308
  it("does not add nonce to script tags that already have nonce", async () => {
@@ -314,7 +314,7 @@ nonce: requestInfo.rw.nonce
314
314
  })
315
315
  `;
316
316
  const clientEntryPoints = new Set();
317
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
317
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
318
318
  expect(result?.code).toEqual(undefined);
319
319
  });
320
320
  it("uses existing requestInfo import if already present", async () => {
@@ -328,7 +328,7 @@ nonce: requestInfo.rw.nonce
328
328
  })
329
329
  `;
330
330
  const clientEntryPoints = new Set();
331
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
331
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
332
332
  expect(result?.code).toEqual(`
333
333
  import { foo } from 'bar';
334
334
  import { requestInfo, someOtherThing } from "rwsdk/worker";
@@ -355,7 +355,7 @@ nonce: requestInfo.rw.nonce
355
355
  })
356
356
  `;
357
357
  const clientEntryPoints = new Set();
358
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
358
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
359
359
  expect(result?.code).toEqual(`
360
360
  import { foo } from 'bar';
361
361
  import { someOtherThing, requestInfo } from "rwsdk/worker";
@@ -376,7 +376,7 @@ nonce: requestInfo.rw.nonce
376
376
  `;
377
377
  // Call without providing manifest (simulating dev mode)
378
378
  const clientEntryPoints = new Set();
379
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
379
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
380
380
  const expected = `import { requestInfo } from "rwsdk/worker";
381
381
 
382
382
  (
@@ -496,7 +496,7 @@ export const Document = ({
496
496
  }, this);
497
497
  `;
498
498
  const clientEntryPoints = new Set();
499
- const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir");
499
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
500
500
  // For this complex test, we'll just verify the key transformations
501
501
  const expected = `
502
502
  import { jsxDEV } from "react/jsx-dev-runtime";
@@ -616,4 +616,54 @@ columnNumber: 2
616
616
  }, this);`;
617
617
  expect(normalizeCode(result?.code || "")).toEqual(normalizeCode(expected));
618
618
  });
619
+ it("strips base prefix from script src entry points", async () => {
620
+ const code = `
621
+ jsx("script", {
622
+ src: "/auth/src/client.tsx",
623
+ type: "module"
624
+ })
625
+ `;
626
+ const clientEntryPoints = new Set();
627
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/auth/");
628
+ expect(clientEntryPoints.has("/src/client.tsx")).toBe(true);
629
+ expect(clientEntryPoints.has("/auth/src/client.tsx")).toBe(false);
630
+ expect(result?.code).toContain('scriptsToBeLoaded.add("/src/client.tsx")');
631
+ expect(result?.code).not.toContain('scriptsToBeLoaded.add("/auth/src/client.tsx")');
632
+ });
633
+ it("strips base prefix from dynamic imports in inline scripts", async () => {
634
+ const code = `
635
+ jsx("script", {
636
+ type: "module",
637
+ children: "import('/auth/src/client.tsx')"
638
+ })
639
+ `;
640
+ const clientEntryPoints = new Set();
641
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/auth/");
642
+ expect(clientEntryPoints.has("/src/client.tsx")).toBe(true);
643
+ expect(clientEntryPoints.has("/auth/src/client.tsx")).toBe(false);
644
+ expect(result?.code).toContain('scriptsToBeLoaded.add("/src/client.tsx")');
645
+ });
646
+ it("strips base prefix from link preload href in asset paths", async () => {
647
+ const code = `
648
+ jsx("link", {
649
+ rel: "modulepreload",
650
+ href: "/auth/src/client.tsx"
651
+ })
652
+ `;
653
+ const clientEntryPoints = new Set();
654
+ const result = await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/auth/");
655
+ expect(result?.code).toContain("rwsdk_asset:/src/client.tsx");
656
+ expect(result?.code).not.toContain("rwsdk_asset:/auth/src/client.tsx");
657
+ });
658
+ it("does not strip when base is /", async () => {
659
+ const code = `
660
+ jsx("script", {
661
+ src: "/src/client.tsx",
662
+ type: "module"
663
+ })
664
+ `;
665
+ const clientEntryPoints = new Set();
666
+ await transformJsxScriptTagsCode(code, clientEntryPoints, mockManifest, "/project/root/dir", "/");
667
+ expect(clientEntryPoints.has("/src/client.tsx")).toBe(true);
668
+ });
619
669
  });
@@ -1 +1,153 @@
1
- export declare const vitePreamblePlugin: () => import("vite").Plugin<any>;
1
+ import type { ResolvedConfig } from "vite";
2
+ export declare const vitePreamblePlugin: () => {
3
+ configResolved(config: ResolvedConfig): void;
4
+ hotUpdate?: import("rollup").ObjectHook<(this: import("rollup").MinimalPluginContext & {
5
+ environment: import("vite").DevEnvironment;
6
+ }, options: import("vite").HotUpdateOptions) => Array<import("vite").EnvironmentModuleNode> | void | Promise<Array<import("vite").EnvironmentModuleNode> | void>>;
7
+ resolveId?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, source: string, importer: string | undefined, options: {
8
+ attributes: Record<string, string>;
9
+ custom?: import("rollup").CustomPluginOptions;
10
+ ssr?: boolean | undefined;
11
+ isEntry: boolean;
12
+ }) => Promise<import("rollup").ResolveIdResult> | import("rollup").ResolveIdResult, {
13
+ filter?: {
14
+ id?: RegExp | RegExp[] | {
15
+ include?: RegExp | RegExp[] | undefined;
16
+ exclude?: RegExp | RegExp[] | undefined;
17
+ };
18
+ };
19
+ }>;
20
+ load?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, id: string, options?: {
21
+ ssr?: boolean | undefined;
22
+ }) => Promise<import("rollup").LoadResult> | import("rollup").LoadResult, {
23
+ filter?: {
24
+ id?: string | RegExp | (string | RegExp)[] | {
25
+ include?: string | RegExp | (string | RegExp)[] | undefined;
26
+ exclude?: string | RegExp | (string | RegExp)[] | undefined;
27
+ };
28
+ };
29
+ }>;
30
+ transform?: import("rollup").ObjectHook<(this: import("rollup").TransformPluginContext, code: string, id: string, options?: {
31
+ ssr?: boolean | undefined;
32
+ }) => Promise<import("rollup").TransformResult> | import("rollup").TransformResult, {
33
+ filter?: {
34
+ id?: string | RegExp | (string | RegExp)[] | {
35
+ include?: string | RegExp | (string | RegExp)[] | undefined;
36
+ exclude?: string | RegExp | (string | RegExp)[] | undefined;
37
+ };
38
+ code?: string | RegExp | (string | RegExp)[] | {
39
+ include?: string | RegExp | (string | RegExp)[] | undefined;
40
+ exclude?: string | RegExp | (string | RegExp)[] | undefined;
41
+ };
42
+ };
43
+ }>;
44
+ sharedDuringBuild?: boolean;
45
+ perEnvironmentStartEndDuringDev?: boolean;
46
+ perEnvironmentWatchChangeDuringDev?: boolean;
47
+ enforce?: "pre" | "post";
48
+ apply?: "serve" | "build" | ((this: void, config: import("vite").UserConfig, env: import("vite").ConfigEnv) => boolean);
49
+ applyToEnvironment?: (environment: {
50
+ name: string;
51
+ getTopLevelConfig(): ResolvedConfig;
52
+ config: ResolvedConfig & {
53
+ define?: Record<string, any>;
54
+ resolve: Required<import("vite").ResolveOptions>;
55
+ consumer: "client" | "server";
56
+ keepProcessEnv?: boolean;
57
+ optimizeDeps: import("vite").DepOptimizationOptions;
58
+ dev: import("vite").ResolvedDevEnvironmentOptions;
59
+ build: import("vite").ResolvedBuildEnvironmentOptions;
60
+ plugins: readonly import("vite").Plugin[];
61
+ };
62
+ logger: import("vite").Logger;
63
+ }) => boolean | Promise<boolean> | import("vite").PluginOption;
64
+ config?: import("rollup").ObjectHook<(this: import("vite").ConfigPluginContext, config: import("vite").UserConfig, env: import("vite").ConfigEnv) => Omit<import("vite").UserConfig, "plugins"> | null | void | Promise<Omit<import("vite").UserConfig, "plugins"> | null | void>>;
65
+ configEnvironment?: import("rollup").ObjectHook<(this: import("vite").ConfigPluginContext, name: string, config: import("vite").EnvironmentOptions, env: import("vite").ConfigEnv & {
66
+ isSsrTargetWebworker?: boolean;
67
+ }) => import("vite").EnvironmentOptions | null | void | Promise<import("vite").EnvironmentOptions | null | void>>;
68
+ configureServer?: import("rollup").ObjectHook<import("vite").ServerHook>;
69
+ configurePreviewServer?: import("rollup").ObjectHook<import("vite").PreviewServerHook>;
70
+ transformIndexHtml?: import("vite").IndexHtmlTransform;
71
+ buildApp?: import("rollup").ObjectHook<import("vite").BuildAppHook>;
72
+ handleHotUpdate?: import("rollup").ObjectHook<(this: import("vite").MinimalPluginContextWithoutEnvironment, ctx: import("vite").HmrContext) => Array<import("vite").ModuleNode> | void | Promise<Array<import("vite").ModuleNode> | void>>;
73
+ api?: any;
74
+ cacheKey?: string | undefined;
75
+ name: string;
76
+ version?: string | undefined;
77
+ renderError?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, error?: Error | undefined) => void | Promise<void>, {
78
+ sequential?: boolean;
79
+ }> | undefined;
80
+ augmentChunkHash?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, chunk: import("rollup").RenderedChunk) => string | void, {}> | undefined;
81
+ generateBundle?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: import("rollup").NormalizedOutputOptions, bundle: import("rollup").OutputBundle, isWrite: boolean) => void | Promise<void>, {}> | undefined;
82
+ outputOptions?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: import("rollup").OutputOptions) => import("rollup").OutputOptions | import("rollup").NullValue, {}> | undefined;
83
+ renderChunk?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, code: string, chunk: import("rollup").RenderedChunk, options: import("rollup").NormalizedOutputOptions, meta: {
84
+ chunks: Record<string, import("rollup").RenderedChunk>;
85
+ }) => string | {
86
+ code: string;
87
+ map?: import("rollup").SourceMapInput;
88
+ } | import("rollup").NullValue | Promise<string | {
89
+ code: string;
90
+ map?: import("rollup").SourceMapInput;
91
+ } | import("rollup").NullValue>, {}> | undefined;
92
+ renderDynamicImport?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: {
93
+ customResolution: string | null;
94
+ format: import("rollup").InternalModuleFormat;
95
+ moduleId: string;
96
+ targetModuleId: string | null;
97
+ chunk: import("rollup").PreRenderedChunkWithFileName;
98
+ targetChunk: import("rollup").PreRenderedChunkWithFileName | null;
99
+ getTargetChunkImports: () => import("rollup").DynamicImportTargetChunk[] | null;
100
+ targetModuleAttributes: Record<string, string>;
101
+ }) => {
102
+ left: string;
103
+ right: string;
104
+ } | import("rollup").NullValue, {}> | undefined;
105
+ renderStart?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, outputOptions: import("rollup").NormalizedOutputOptions, inputOptions: import("rollup").NormalizedInputOptions) => void | Promise<void>, {
106
+ sequential?: boolean;
107
+ }> | undefined;
108
+ resolveFileUrl?: import("rollup").ObjectHook<import("rollup").ResolveFileUrlHook, {}> | undefined;
109
+ resolveImportMeta?: import("rollup").ObjectHook<import("rollup").ResolveImportMetaHook, {}> | undefined;
110
+ writeBundle?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: import("rollup").NormalizedOutputOptions, bundle: import("rollup").OutputBundle) => void | Promise<void>, {
111
+ sequential?: boolean;
112
+ }> | undefined;
113
+ footer?: import("rollup").ObjectHook<import("rollup").AddonHook, {}> | undefined;
114
+ banner?: import("rollup").ObjectHook<import("rollup").AddonHook, {}> | undefined;
115
+ intro?: import("rollup").ObjectHook<import("rollup").AddonHook, {}> | undefined;
116
+ outro?: import("rollup").ObjectHook<import("rollup").AddonHook, {}> | undefined;
117
+ buildEnd?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, error?: Error | undefined) => void | Promise<void>, {
118
+ sequential?: boolean;
119
+ }> | undefined;
120
+ buildStart?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: import("rollup").NormalizedInputOptions) => void | Promise<void>, {
121
+ sequential?: boolean;
122
+ }> | undefined;
123
+ closeBundle?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, error?: Error | undefined) => void | Promise<void>, {
124
+ sequential?: boolean;
125
+ }> | undefined;
126
+ closeWatcher?: import("rollup").ObjectHook<(this: import("rollup").PluginContext) => void | Promise<void>, {
127
+ sequential?: boolean;
128
+ }> | undefined;
129
+ moduleParsed?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, info: import("rollup").ModuleInfo) => void | Promise<void>, {
130
+ sequential?: boolean;
131
+ }> | undefined;
132
+ onLog?: import("rollup").ObjectHook<(this: import("rollup").MinimalPluginContext, level: import("rollup").LogLevel, log: import("rollup").RollupLog) => boolean | import("rollup").NullValue, {}> | undefined;
133
+ options?: import("rollup").ObjectHook<(this: import("rollup").MinimalPluginContext, options: import("rollup").InputOptions) => import("rollup").InputOptions | import("rollup").NullValue | Promise<import("rollup").InputOptions | import("rollup").NullValue>, {}> | undefined;
134
+ resolveDynamicImport?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, specifier: string | import("rollup").AstNode, importer: string, options: {
135
+ attributes: Record<string, string>;
136
+ importerAttributes: Record<string, string>;
137
+ }) => import("rollup").ResolveIdResult | Promise<import("rollup").ResolveIdResult>, {}> | undefined;
138
+ shouldTransformCachedModule?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, options: {
139
+ ast: import("rollup").ProgramNode;
140
+ attributes: Record<string, string>;
141
+ code: string;
142
+ id: string;
143
+ meta: import("rollup").CustomPluginOptions;
144
+ moduleSideEffects: boolean | "no-treeshake";
145
+ resolvedSources: import("rollup").ResolvedIdMap;
146
+ syntheticNamedExports: boolean | string;
147
+ }) => boolean | import("rollup").NullValue | Promise<boolean | import("rollup").NullValue>, {}> | undefined;
148
+ watchChange?: import("rollup").ObjectHook<(this: import("rollup").PluginContext, id: string, change: {
149
+ event: import("rollup").ChangeEvent;
150
+ }) => void | Promise<void>, {
151
+ sequential?: boolean;
152
+ }> | undefined;
153
+ };
@@ -1,11 +1,21 @@
1
1
  import MagicString from "magic-string";
2
2
  import { virtualPlugin } from "./virtualPlugin.mjs";
3
- export const vitePreamblePlugin = () => virtualPlugin("vite-preamble", async () => {
4
- const s = new MagicString(`
5
- import RefreshRuntime from "/@react-refresh"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;
3
+ export const vitePreamblePlugin = () => {
4
+ let base = "/";
5
+ const inner = virtualPlugin("vite-preamble", async () => {
6
+ const refreshPath = base.replace(/\/$/, "") + "/@react-refresh";
7
+ const s = new MagicString(`
8
+ import RefreshRuntime from "${refreshPath}"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;
6
9
  `);
10
+ return {
11
+ code: s.toString(),
12
+ map: s.generateMap(),
13
+ };
14
+ });
7
15
  return {
8
- code: s.toString(),
9
- map: s.generateMap(),
16
+ ...inner,
17
+ configResolved(config) {
18
+ base = config.base || "/";
19
+ },
10
20
  };
11
- });
21
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -176,7 +176,7 @@
176
176
  "glob": "~13.0.6",
177
177
  "ignore": "~7.0.5",
178
178
  "jsonc-parser": "~3.3.1",
179
- "kysely": "~0.28.11",
179
+ "kysely": "~0.28.12",
180
180
  "kysely-do": "~0.0.1-rc.1",
181
181
  "lodash": "~4.17.23",
182
182
  "magic-string": "~0.30.21",