rwsdk 0.1.0-alpha.9 → 0.1.0

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.
Files changed (87) hide show
  1. package/dist/runtime/client.d.ts +3 -1
  2. package/dist/runtime/client.js +15 -11
  3. package/dist/runtime/clientNavigation.d.ts +3 -0
  4. package/dist/runtime/clientNavigation.js +43 -0
  5. package/dist/runtime/entries/client.d.ts +1 -0
  6. package/dist/runtime/entries/client.js +1 -0
  7. package/dist/runtime/entries/worker.d.ts +2 -0
  8. package/dist/runtime/entries/worker.js +2 -0
  9. package/dist/runtime/imports/ClientOnly.d.ts +3 -0
  10. package/dist/runtime/imports/ClientOnly.js +8 -0
  11. package/dist/runtime/imports/NoSSRStub.d.ts +1 -0
  12. package/dist/runtime/imports/NoSSRStub.js +4 -0
  13. package/dist/runtime/imports/client.js +15 -2
  14. package/dist/runtime/imports/worker.d.ts +1 -1
  15. package/dist/runtime/imports/worker.js +7 -5
  16. package/dist/runtime/lib/db/DOWorkerDialect.d.ts +29 -0
  17. package/dist/runtime/lib/db/DOWorkerDialect.js +66 -0
  18. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +14 -0
  19. package/dist/runtime/lib/db/SqliteDurableObject.js +42 -0
  20. package/dist/runtime/lib/db/create.d.ts +3 -0
  21. package/dist/runtime/lib/db/create.js +36 -0
  22. package/dist/runtime/lib/db/createDb.d.ts +2 -0
  23. package/dist/runtime/lib/db/createDb.js +27 -0
  24. package/dist/runtime/lib/db/index.d.ts +3 -0
  25. package/dist/runtime/lib/db/index.js +3 -0
  26. package/dist/runtime/lib/db/logger.d.ts +2 -0
  27. package/dist/runtime/lib/db/logger.js +41 -0
  28. package/dist/runtime/lib/db/migrations.d.ts +23 -0
  29. package/dist/runtime/lib/db/migrations.js +34 -0
  30. package/dist/runtime/lib/db/types.d.ts +0 -0
  31. package/dist/runtime/lib/db/types.js +1 -0
  32. package/dist/runtime/lib/debug.d.ts +2 -0
  33. package/dist/runtime/lib/debug.js +36 -0
  34. package/dist/runtime/lib/router.d.ts +6 -1
  35. package/dist/runtime/lib/router.js +9 -2
  36. package/dist/runtime/register/ssr.d.ts +2 -0
  37. package/dist/runtime/register/ssr.js +14 -1
  38. package/dist/runtime/register/worker.d.ts +1 -1
  39. package/dist/runtime/register/worker.js +5 -2
  40. package/dist/runtime/render/renderRscThenableToHtmlStream.d.ts +2 -1
  41. package/dist/runtime/render/renderRscThenableToHtmlStream.js +17 -3
  42. package/dist/runtime/render/renderToStream.d.ts +9 -0
  43. package/dist/runtime/render/renderToStream.js +26 -0
  44. package/dist/runtime/render/renderToString.d.ts +7 -0
  45. package/dist/runtime/render/renderToString.js +26 -0
  46. package/dist/runtime/render/transformRscToHtmlStream.js +1 -0
  47. package/dist/runtime/worker.js +17 -10
  48. package/dist/scripts/debug-sync.mjs +8 -6
  49. package/dist/scripts/worker-run.mjs +1 -0
  50. package/dist/vite/configPlugin.mjs +8 -17
  51. package/dist/vite/createDirectiveLookupPlugin.d.mts +1 -0
  52. package/dist/vite/createDirectiveLookupPlugin.mjs +88 -57
  53. package/dist/vite/devServerTimingPlugin.d.mts +2 -0
  54. package/dist/vite/devServerTimingPlugin.mjs +24 -0
  55. package/dist/vite/directivesPlugin.mjs +168 -70
  56. package/dist/vite/findImportSpecifiers.d.mts +16 -0
  57. package/dist/vite/findImportSpecifiers.mjs +152 -0
  58. package/dist/vite/findImportSpecifiers.test.d.mts +1 -0
  59. package/dist/vite/findImportSpecifiers.test.mjs +73 -0
  60. package/dist/vite/findSpecifiers.d.mts +31 -0
  61. package/dist/vite/findSpecifiers.mjs +230 -0
  62. package/dist/vite/hasDirective.d.mts +7 -0
  63. package/dist/vite/hasDirective.mjs +54 -0
  64. package/dist/vite/hasOwnCloudflareVitePlugin.d.mts +3 -0
  65. package/dist/vite/hasOwnCloudflareVitePlugin.mjs +14 -0
  66. package/dist/vite/invalidateModule.d.mts +2 -0
  67. package/dist/vite/invalidateModule.mjs +14 -0
  68. package/dist/vite/miniflareHMRPlugin.d.mts +8 -0
  69. package/dist/vite/miniflareHMRPlugin.mjs +133 -0
  70. package/dist/vite/normalizeModulePath.mjs +12 -1
  71. package/dist/vite/redwoodPlugin.d.mts +1 -0
  72. package/dist/vite/redwoodPlugin.mjs +19 -4
  73. package/dist/vite/resolveModuleId.d.mts +6 -0
  74. package/dist/vite/resolveModuleId.mjs +14 -0
  75. package/dist/vite/ssrBridgePlugin.d.mts +5 -1
  76. package/dist/vite/ssrBridgePlugin.mjs +4 -43
  77. package/dist/vite/transformClientComponents.d.mts +1 -0
  78. package/dist/vite/transformClientComponents.mjs +61 -125
  79. package/dist/vite/transformJsxScriptTagsPlugin.mjs +14 -3
  80. package/dist/vite/transformServerFunctions.d.mts +11 -3
  81. package/dist/vite/transformServerFunctions.mjs +256 -171
  82. package/dist/vite/transformServerFunctions.test.mjs +22 -3
  83. package/dist/vite/useClientLookupPlugin.mjs +1 -0
  84. package/dist/vite/useServerLookupPlugin.mjs +1 -0
  85. package/dist/vite/useServerPlugin.d.mts +1 -1
  86. package/dist/vite/useServerPlugin.mjs +1 -1
  87. package/package.json +14 -3
@@ -186,11 +186,18 @@ export function render(Document, routes,
186
186
  /**
187
187
  * @param options - Configuration options for rendering.
188
188
  * @param options.rscPayload - Toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity no longer works.
189
+ * @param options.ssr - Disable sever side rendering for all these routes. This only allow client side rendering`, which requires `rscPayload` to be enabled.
189
190
  */
190
- options = { rscPayload: true }) {
191
+ options = {}) {
192
+ options = {
193
+ rscPayload: true,
194
+ ssr: true,
195
+ ...options,
196
+ };
191
197
  const documentMiddleware = ({ rw }) => {
192
198
  rw.Document = Document;
193
- rw.rscPayload = options.rscPayload;
199
+ rw.rscPayload = options.rscPayload ?? true;
200
+ rw.ssr = options.ssr ?? true;
194
201
  };
195
202
  return [documentMiddleware, ...routes];
196
203
  }
@@ -1 +1,3 @@
1
+ export declare const loadServerModule: ((id: string) => Promise<any>) & import("lodash").MemoizedFunction;
2
+ export declare const getServerModuleExport: (id: string) => Promise<any>;
1
3
  export declare const createServerReference: (id: string, name: string) => any;
@@ -1,5 +1,18 @@
1
+ import memoize from "lodash/memoize";
1
2
  import { createServerReference as baseCreateServerReference } from "react-server-dom-webpack/client.edge";
2
- import { getServerModuleExport } from "../imports/worker.js";
3
+ export const loadServerModule = memoize(async (id) => {
4
+ const { useServerLookup } = await import("virtual:use-server-lookup");
5
+ const moduleFn = useServerLookup[id];
6
+ if (!moduleFn) {
7
+ throw new Error(`(worker) No module found for '${id}' in module lookup for "use server" directive`);
8
+ }
9
+ return await moduleFn();
10
+ });
11
+ export const getServerModuleExport = async (id) => {
12
+ const [file, name] = id.split("#");
13
+ const module = await loadServerModule(file);
14
+ return module[name];
15
+ };
3
16
  const ssrCallServer = async (id, args) => {
4
17
  const action = await getServerModuleExport(id);
5
18
  if (typeof action !== "function") {
@@ -1,4 +1,4 @@
1
1
  export declare function registerServerReference(action: Function, id: string, name: string): Function;
2
- export declare function registerClientReference<Target extends Record<string, any>>(id: string, exportName: string): () => null;
2
+ export declare function registerClientReference<Target extends Record<string, any>>(id: string, exportName: string, value: any): any;
3
3
  export declare function __smokeTestActionHandler(timestamp?: number): Promise<unknown>;
4
4
  export declare function rscActionHandler(req: Request): Promise<unknown>;
@@ -8,9 +8,12 @@ export function registerServerReference(action, id, name) {
8
8
  // Note: We no longer need to register in a Map since we use virtual lookup
9
9
  return baseRegisterServerReference(action, id, name);
10
10
  }
11
- export function registerClientReference(id, exportName) {
11
+ export function registerClientReference(id, exportName, value) {
12
+ const wrappedValue = (value && typeof value === "function") || typeof value === "object"
13
+ ? value
14
+ : () => null;
12
15
  const reference = baseRegisterClientReference({}, id, exportName);
13
- return Object.defineProperties(() => null, {
16
+ return Object.defineProperties(wrappedValue, {
14
17
  ...Object.getOwnPropertyDescriptors(reference),
15
18
  $$async: { value: true },
16
19
  $$isClientReference: { value: true },
@@ -1,7 +1,8 @@
1
1
  import { type DocumentProps } from "../lib/router";
2
2
  import { type RequestInfo } from "../requestInfo/types";
3
- export declare const renderRscThenableToHtmlStream: ({ thenable, Document, requestInfo, }: {
3
+ export declare const renderRscThenableToHtmlStream: ({ thenable, Document, requestInfo, shouldSSR, }: {
4
4
  thenable: any;
5
5
  Document: React.FC<DocumentProps>;
6
6
  requestInfo: RequestInfo;
7
+ shouldSSR: boolean;
7
8
  }) => Promise<import("react-dom/server").ReactDOMServerReadableStream>;
@@ -1,9 +1,23 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { use } from "react";
3
3
  import { renderToReadableStream } from "react-dom/server.edge";
4
- export const renderRscThenableToHtmlStream = async ({ thenable, Document, requestInfo, }) => {
4
+ export const renderRscThenableToHtmlStream = async ({ thenable, Document, requestInfo, shouldSSR, }) => {
5
5
  const Component = () => {
6
- return (_jsx(Document, { ...requestInfo, children: use(thenable).node }));
6
+ const node = use(thenable).node;
7
+ // todo(justinvdm, 18 Jun 2025): We can build on this later to allow users
8
+ // surface context. e.g:
9
+ // * we assign `user: requestInfo.clientCtx` here
10
+ // * user populates requestInfo.clientCtx on worker side
11
+ // * user can import a read only `import { clientCtx } from "rwsdk/client"`
12
+ // on client side
13
+ const clientContext = {
14
+ rw: {
15
+ ssr: shouldSSR,
16
+ },
17
+ };
18
+ return (_jsxs(Document, { ...requestInfo, children: [_jsx("script", { nonce: requestInfo.rw.nonce, dangerouslySetInnerHTML: {
19
+ __html: `globalThis.__RWSDK_CONTEXT = ${JSON.stringify(clientContext)}`,
20
+ } }), _jsx("div", { id: "hydrate-root", children: node })] }));
7
21
  };
8
22
  return await renderToReadableStream(_jsx(Component, {}), {
9
23
  nonce: requestInfo.rw.nonce,
@@ -0,0 +1,9 @@
1
+ import { ReactElement, FC } from "react";
2
+ import { DocumentProps } from "../lib/router";
3
+ export interface RenderToStreamOptions {
4
+ Document?: FC<DocumentProps>;
5
+ injectRSCPayload?: boolean;
6
+ onError?: (error: unknown) => void;
7
+ }
8
+ export declare const IdentityDocument: FC<DocumentProps>;
9
+ export declare const renderToStream: (element: ReactElement, { Document, injectRSCPayload: shouldInjectRSCPayload, onError, }?: RenderToStreamOptions) => Promise<ReadableStream>;
@@ -0,0 +1,26 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderToRscStream } from "./renderToRscStream";
3
+ import { transformRscToHtmlStream } from "./transformRscToHtmlStream";
4
+ import { requestInfo } from "../requestInfo/worker";
5
+ import { injectRSCPayload } from "rsc-html-stream/server";
6
+ export const IdentityDocument = ({ children }) => (_jsx(_Fragment, { children: children }));
7
+ export const renderToStream = async (element, { Document = IdentityDocument, injectRSCPayload: shouldInjectRSCPayload = false, onError, } = {}) => {
8
+ let rscStream = renderToRscStream({
9
+ node: element,
10
+ actionResult: null,
11
+ onError,
12
+ });
13
+ if (shouldInjectRSCPayload) {
14
+ const [rscPayloadStream1, rscPayloadStream2] = rscStream.tee();
15
+ rscStream = rscPayloadStream1;
16
+ rscStream = rscStream.pipeThrough(injectRSCPayload(rscPayloadStream2, {
17
+ nonce: requestInfo.rw.nonce,
18
+ }));
19
+ }
20
+ const htmlStream = await transformRscToHtmlStream({
21
+ stream: rscStream,
22
+ Document,
23
+ requestInfo,
24
+ });
25
+ return htmlStream;
26
+ };
@@ -0,0 +1,7 @@
1
+ import { FC, ReactElement } from "react";
2
+ import { DocumentProps } from "../lib/router";
3
+ export interface RenderToStringOptions {
4
+ Document?: FC<DocumentProps>;
5
+ injectRSCPayload?: boolean;
6
+ }
7
+ export declare const renderToString: (element: ReactElement, options?: RenderToStringOptions) => Promise<string>;
@@ -0,0 +1,26 @@
1
+ import { renderToStream } from "./renderToStream";
2
+ export const renderToString = async (element, options) => {
3
+ const stream = await new Promise((resolve, reject) => renderToStream(element, {
4
+ ...options,
5
+ onError: reject,
6
+ })
7
+ .then(resolve)
8
+ .catch(reject));
9
+ const reader = stream.getReader();
10
+ const decoder = new TextDecoder();
11
+ let result = "";
12
+ try {
13
+ while (true) {
14
+ const { done, value } = await reader.read();
15
+ if (done)
16
+ break;
17
+ result += decoder.decode(value, { stream: true });
18
+ }
19
+ // Flush any remaining bytes
20
+ result += decoder.decode();
21
+ return result;
22
+ }
23
+ finally {
24
+ reader.releaseLock();
25
+ }
26
+ };
@@ -13,5 +13,6 @@ export const transformRscToHtmlStream = ({ stream, Document, requestInfo, }) =>
13
13
  thenable,
14
14
  Document,
15
15
  requestInfo,
16
+ shouldSSR: requestInfo.rw.ssr,
16
17
  });
17
18
  };
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { transformRscToHtmlStream } from "./render/transformRscToHtmlStream";
3
3
  import { renderToRscStream } from "./render/renderToRscStream";
4
- import { ssrWebpackRequire } from "rwsdk/__ssr_bridge";
5
4
  import { rscActionHandler } from "./register/worker";
6
5
  import { injectRSCPayload } from "rsc-html-stream/server";
7
6
  import { ErrorResponse } from "./error";
@@ -9,6 +8,7 @@ import { getRequestInfo, runWithRequestInfo, runWithRequestInfoOverrides, } from
9
8
  import { defineRoutes } from "./lib/router";
10
9
  import { generateNonce } from "./lib/utils";
11
10
  import { IS_DEV } from "./constants";
11
+ import { ssrWebpackRequire } from "./imports/worker";
12
12
  export const defineApp = (routes) => {
13
13
  return {
14
14
  fetch: async (request, env, cf) => {
@@ -39,6 +39,8 @@ export const defineApp = (routes) => {
39
39
  Document: DefaultDocument,
40
40
  nonce: generateNonce(),
41
41
  rscPayload: true,
42
+ ssr: true,
43
+ databases: new Map(),
42
44
  };
43
45
  const outerRequestInfo = {
44
46
  request,
@@ -87,7 +89,8 @@ export const defineApp = (routes) => {
87
89
  actionResult = await rscActionHandler(request);
88
90
  }
89
91
  const pageElement = createPageElement(requestInfo, Page);
90
- const rscPayloadStream = renderToRscStream({
92
+ const { rscPayload: shouldInjectRSCPayload } = rw;
93
+ let rscPayloadStream = renderToRscStream({
91
94
  node: pageElement,
92
95
  actionResult: actionResult instanceof Response ? null : actionResult,
93
96
  onError,
@@ -99,17 +102,21 @@ export const defineApp = (routes) => {
99
102
  },
100
103
  });
101
104
  }
102
- const [rscPayloadStream1, rscPayloadStream2] = rscPayloadStream.tee();
103
- const htmlStream = await transformRscToHtmlStream({
104
- stream: rscPayloadStream1,
105
+ let injectRSCPayloadStream;
106
+ if (shouldInjectRSCPayload) {
107
+ const [rscPayloadStream1, rscPayloadStream2] = rscPayloadStream.tee();
108
+ rscPayloadStream = rscPayloadStream1;
109
+ injectRSCPayloadStream = injectRSCPayload(rscPayloadStream2, {
110
+ nonce: rw.nonce,
111
+ });
112
+ }
113
+ let html = await transformRscToHtmlStream({
114
+ stream: rscPayloadStream,
105
115
  Document: rw.Document,
106
116
  requestInfo: requestInfo,
107
117
  });
108
- let html = htmlStream;
109
- if (rw.rscPayload) {
110
- html = htmlStream.pipeThrough(injectRSCPayload(rscPayloadStream2, {
111
- nonce: rw.nonce,
112
- }));
118
+ if (injectRSCPayloadStream) {
119
+ html = html.pipeThrough(injectRSCPayloadStream);
113
120
  }
114
121
  return new Response(html, {
115
122
  headers: {
@@ -8,12 +8,14 @@ export const debugSync = async (opts) => {
8
8
  const syncCommand = `echo 🏗️ rebuilding... && pnpm build && rm -rf ${targetDir}/node_modules/rwsdk/dist ${targetDir}/node_modules/rwsdk/package.json && echo 📁 syncing sdk from ${process.cwd()} to ${targetDir}/node_modules/rwsdk/... && cp -r package.json dist ${targetDir}/node_modules/rwsdk/ && echo ✅ done syncing`;
9
9
  // Run initial sync
10
10
  await $({ stdio: "inherit", shell: true }) `${syncCommand}`;
11
- console.log("🧹 Cleaning Vite cache...");
12
- await $({
13
- stdio: "inherit",
14
- shell: true,
15
- cwd: targetDir,
16
- }) `npm run clean:vite`;
11
+ if (!process.env.NO_CLEAN_VITE) {
12
+ console.log("🧹 Cleaning Vite cache...");
13
+ await $({
14
+ stdio: "inherit",
15
+ shell: true,
16
+ cwd: targetDir,
17
+ }) `rm -rf node_modules/.vite`;
18
+ }
17
19
  // If dev flag is present, clean vite cache and start dev server
18
20
  if (dev) {
19
21
  console.log("🚀 Starting dev server...");
@@ -41,6 +41,7 @@ export const runWorkerScript = async (relativeScriptPath) => {
41
41
  plugins: [
42
42
  redwood({
43
43
  configPath: tmpWorkerPath.path,
44
+ includeCloudflarePlugin: true,
44
45
  entry: {
45
46
  worker: scriptPath,
46
47
  },
@@ -49,6 +49,7 @@ export const configPlugin = ({ mode, silent, projectRootDir, clientEntryPathname
49
49
  },
50
50
  optimizeDeps: {
51
51
  noDiscovery: false,
52
+ include: ["rwsdk/client"],
52
53
  esbuildOptions: {
53
54
  jsx: "automatic",
54
55
  jsxImportSource: "react",
@@ -58,23 +59,13 @@ export const configPlugin = ({ mode, silent, projectRootDir, clientEntryPathname
58
59
  },
59
60
  },
60
61
  },
62
+ resolve: {
63
+ conditions: ["browser", "module"],
64
+ },
61
65
  },
62
66
  ssr: {
63
67
  resolve: {
64
- conditions: [
65
- "workerd",
66
- // context(justinvdm, 11 Jun 2025): Some packages meant for cloudflare workers, yet
67
- // their deps have only node import conditions, e.g. `agents` package (meant for CF),
68
- // has `pkce-challenge` package as a dep, which has only node import conditions.
69
- // https://github.com/crouchcd/pkce-challenge/blob/master/package.json#L17
70
- //
71
- // Once the transformed code for this environment is in turn processed in the `worker` environment,
72
- // @cloudflare/vite-plugin should take care of any relevant polyfills for deps with
73
- // node builtins imports that can be polyfilled though, so it is worth us including this condition here.
74
- // However, it does mean we will try to run packages meant for node that cannot be run on cloudflare workers.
75
- // That's the trade-off, but arguably worth it. (context(justinvdm, 11 Jun 2025))
76
- "node",
77
- ],
68
+ conditions: ["workerd", "module", "browser"],
78
69
  noExternal: true,
79
70
  },
80
71
  define: {
@@ -84,11 +75,10 @@ export const configPlugin = ({ mode, silent, projectRootDir, clientEntryPathname
84
75
  noDiscovery: false,
85
76
  entries: [workerEntryPathname],
86
77
  exclude: externalModules,
87
- include: ["rwsdk/__ssr_bridge"],
78
+ include: ["rwsdk/__ssr", "rwsdk/__ssr_bridge"],
88
79
  esbuildOptions: {
89
80
  jsx: "automatic",
90
81
  jsxImportSource: "react",
91
- conditions: ["workerd"],
92
82
  plugins: [],
93
83
  },
94
84
  },
@@ -108,6 +98,7 @@ export const configPlugin = ({ mode, silent, projectRootDir, clientEntryPathname
108
98
  conditions: [
109
99
  "workerd",
110
100
  "react-server",
101
+ "module",
111
102
  // context(justinvdm, 11 Jun 2025): Some packages meant for cloudflare workers, yet
112
103
  // their deps have only node import conditions, e.g. `agents` package (meant for CF),
113
104
  // has `pkce-challenge` package as a dep, which has only node import conditions.
@@ -126,7 +117,7 @@ export const configPlugin = ({ mode, silent, projectRootDir, clientEntryPathname
126
117
  },
127
118
  optimizeDeps: {
128
119
  noDiscovery: false,
129
- include: [],
120
+ include: ["rwsdk/worker"],
130
121
  exclude: [],
131
122
  entries: [workerEntryPathname],
132
123
  esbuildOptions: {
@@ -1,5 +1,6 @@
1
1
  import { Plugin } from "vite";
2
2
  interface DirectiveLookupConfig {
3
+ kind: "client" | "server";
3
4
  directive: "use client" | "use server";
4
5
  virtualModuleName: string;
5
6
  exportName: string;
@@ -1,26 +1,19 @@
1
1
  import MagicString from "magic-string";
2
2
  import path from "path";
3
3
  import { readFile } from "fs/promises";
4
- import { glob } from "glob";
5
4
  import debug from "debug";
6
5
  import { normalizeModulePath } from "./normalizeModulePath.mjs";
7
- import { ensureAliasArray } from "./ensureAliasArray.mjs";
8
6
  import { pathExists } from "fs-extra";
9
7
  import { stat } from "fs/promises";
10
8
  import { getSrcPaths } from "../lib/getSrcPaths.js";
9
+ import { hasDirective } from "./hasDirective.mjs";
11
10
  export const findFilesContainingDirective = async ({ projectRootDir, files, directive, debugNamespace, }) => {
12
11
  const log = debug(debugNamespace);
13
12
  const verboseLog = debug(`verbose:${debugNamespace}`);
14
13
  log("Starting search for '%s' files in projectRootDir=%s", directive, projectRootDir);
15
- const filesOutsideNodeModules = await getSrcPaths(projectRootDir);
16
- const filesInsideNodeModules = await glob("**/node_modules/**/*.{js,mjs,cjs}", {
17
- cwd: projectRootDir,
18
- absolute: true,
19
- nodir: true,
20
- });
21
- const allFiles = [...filesOutsideNodeModules, ...filesInsideNodeModules];
22
- log("Found %d files to scan for '%s' directive", allFiles.length, directive);
23
- for (const file of allFiles) {
14
+ const filesToScan = await getSrcPaths(projectRootDir);
15
+ log("Found %d files to scan for '%s' directive", filesToScan.length, directive);
16
+ for (const file of filesToScan) {
24
17
  try {
25
18
  const stats = await stat(file);
26
19
  if (!stats.isFile()) {
@@ -29,18 +22,10 @@ export const findFilesContainingDirective = async ({ projectRootDir, files, dire
29
22
  }
30
23
  verboseLog("Scanning file: %s", file);
31
24
  const content = await readFile(file, "utf-8");
32
- const lines = content.split("\n");
33
- for (const line of lines) {
34
- const trimmedLine = line.trim();
35
- if (trimmedLine.length > 0) {
36
- if (trimmedLine.startsWith(`"${directive}"`) ||
37
- trimmedLine.startsWith(`'${directive}'`)) {
38
- const normalizedPath = normalizeModulePath(projectRootDir, file);
39
- log("Found '%s' directive in file: %s -> %s", directive, file, normalizedPath);
40
- files.add(normalizedPath);
41
- }
42
- break;
43
- }
25
+ if (hasDirective(content, directive)) {
26
+ const normalizedPath = normalizeModulePath(projectRootDir, file);
27
+ log("Found '%s' directive in file: %s -> %s", directive, file, normalizedPath);
28
+ files.add(normalizedPath);
44
29
  }
45
30
  }
46
31
  catch (error) {
@@ -48,37 +33,79 @@ export const findFilesContainingDirective = async ({ projectRootDir, files, dire
48
33
  }
49
34
  }
50
35
  log("Completed scan. Found %d %s files total", files.size, directive);
36
+ verboseLog("Found files for %s: %j", directive, Array.from(files));
51
37
  };
52
- const resolveOptimizedDep = async (projectRootDir, filePath, environment, debugNamespace) => {
38
+ const resolveOptimizedDep = async (projectRootDir, id, environment, debugNamespace) => {
53
39
  const log = debug(debugNamespace);
54
40
  const verboseLog = debug(`verbose:${debugNamespace}`);
55
41
  try {
56
- const getDepsDir = (env) => env === "client" ? "deps" : `deps_${env}`;
57
- const getManifestPath = (env) => path.join(projectRootDir, "node_modules", ".vite", getDepsDir(env), "_metadata.json");
58
- const getOptimizedPath = (env, fileName) => path.join("/", "node_modules", ".vite", getDepsDir(env), fileName);
59
- const manifestPath = getManifestPath(environment);
60
- verboseLog("Checking for manifest at: %s", manifestPath);
42
+ const depsDir = environment === "client" ? "deps" : `deps_${environment}`;
43
+ const nodeModulesDepsDirPath = path.join("node_modules", ".vite", depsDir);
44
+ const depsDirPath = path.join(projectRootDir, nodeModulesDepsDirPath);
45
+ const manifestPath = path.join(depsDirPath, "_metadata.json");
46
+ log("Checking for manifest at: %s", manifestPath);
61
47
  const manifestExists = await pathExists(manifestPath);
62
48
  if (!manifestExists) {
63
- verboseLog("Manifest not found at %s", manifestPath);
49
+ log("Manifest not found at %s", manifestPath);
64
50
  return undefined;
65
51
  }
66
52
  const manifestContent = await readFile(manifestPath, "utf-8");
67
53
  const manifest = JSON.parse(manifestContent);
68
- if (manifest.optimized && manifest.optimized[filePath]) {
69
- const optimizedFile = manifest.optimized[filePath].file;
70
- const optimizedPath = getOptimizedPath(environment, optimizedFile);
71
- log("Found optimized dependency: %s -> %s", filePath, optimizedPath);
54
+ if (manifest.optimized && manifest.optimized[id]) {
55
+ const optimizedFile = manifest.optimized[id].file;
56
+ const optimizedPath = path.join("/", nodeModulesDepsDirPath, optimizedFile);
57
+ log("Found optimized dependency: filePath=%s, optimizedPath=%s", id, optimizedPath);
72
58
  return optimizedPath;
73
59
  }
74
- verboseLog("File %s not found in optimized dependencies", filePath);
60
+ verboseLog("File not found in optimized dependencies: id=%s", id);
75
61
  return undefined;
76
62
  }
77
63
  catch (error) {
78
- verboseLog("Error resolving optimized dependency for %s: %s", filePath, error);
64
+ verboseLog("Error resolving optimized dependency for id=%s: %s", id, error);
79
65
  return undefined;
80
66
  }
81
67
  };
68
+ const addOptimizedDepsEntries = async ({ projectRootDir, directive, environment, debugNamespace, files, }) => {
69
+ const log = debug(debugNamespace);
70
+ const verboseLog = debug(`verbose:${debugNamespace}`);
71
+ try {
72
+ const depsDir = environment === "client" ? "deps" : `deps_${environment}`;
73
+ const depsDirPath = path.join(projectRootDir, "node_modules", ".vite", depsDir);
74
+ const manifestPath = path.join(depsDirPath, "_metadata.json");
75
+ verboseLog("Checking for manifest at: %s", manifestPath);
76
+ const manifestExists = await pathExists(manifestPath);
77
+ if (!manifestExists) {
78
+ verboseLog("Manifest not found at %s", manifestPath);
79
+ return;
80
+ }
81
+ const manifestContent = await readFile(manifestPath, "utf-8");
82
+ const manifest = JSON.parse(manifestContent);
83
+ for (const entryId of Object.keys(manifest.optimized)) {
84
+ if (entryId.startsWith("/node_modules/")) {
85
+ const srcPath = manifest.optimized[entryId].src;
86
+ const resolvedSrcPath = path.resolve(projectRootDir, "node_modules", ".vite", "deps", srcPath);
87
+ let contents;
88
+ try {
89
+ contents = await readFile(resolvedSrcPath, "utf-8");
90
+ }
91
+ catch (error) {
92
+ verboseLog("Error reading file %s: %s", resolvedSrcPath, error);
93
+ continue;
94
+ }
95
+ if (hasDirective(contents, directive)) {
96
+ log("Adding optimized entry to files: %s", entryId);
97
+ files.add(entryId);
98
+ }
99
+ else {
100
+ log("Skipping optimized entry %s because it does not contain the '%s' directive", entryId, directive);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ verboseLog("Error adding optimized deps entries: %s", error);
107
+ }
108
+ };
82
109
  export const createDirectiveLookupPlugin = async ({ projectRootDir, files, config, }) => {
83
110
  const debugNamespace = `rwsdk:vite:${config.pluginName}`;
84
111
  const log = debug(debugNamespace);
@@ -91,14 +118,26 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
91
118
  directive: config.directive,
92
119
  debugNamespace,
93
120
  });
121
+ let devServer;
94
122
  return {
95
123
  name: `rwsdk:${config.pluginName}`,
96
124
  config(_, { command, isPreview }) {
97
125
  isDev = !isPreview && command === "serve";
98
126
  log("Development mode: %s", isDev);
99
127
  },
100
- configEnvironment(env, viteConfig) {
128
+ configureServer(server) {
129
+ devServer = server;
130
+ },
131
+ async configEnvironment(env, viteConfig) {
101
132
  log("Configuring environment: env=%s", env);
133
+ // Add optimized deps entries that match our pattern
134
+ await addOptimizedDepsEntries({
135
+ projectRootDir,
136
+ files,
137
+ directive: config.directive,
138
+ environment: env,
139
+ debugNamespace,
140
+ });
102
141
  viteConfig.optimizeDeps ??= {};
103
142
  viteConfig.optimizeDeps.esbuildOptions ??= {};
104
143
  viteConfig.optimizeDeps.esbuildOptions.plugins ??= [];
@@ -125,26 +164,16 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
125
164
  if (shouldOptimizeForEnv) {
126
165
  log("Applying optimizeDeps and aliasing for environment: %s", env);
127
166
  viteConfig.optimizeDeps.include ??= [];
128
- const aliases = ensureAliasArray(viteConfig);
129
167
  for (const file of files) {
130
168
  const actualFilePath = path.join(projectRootDir, file);
131
- if (file.includes("/node_modules/")) {
132
- verboseLog("Adding to optimizeDeps.include: %s -> %s", file);
133
- viteConfig.optimizeDeps.include.push(file);
134
- const findRegex = new RegExp(`^${file.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")}$`);
135
- aliases.push({ find: findRegex, replacement: actualFilePath });
136
- verboseLog("Added alias for `node_modules` module matching directive in env=%s: %s -> %s", env, file, actualFilePath);
137
- }
138
- else {
139
- verboseLog("Adding to optimizeDeps.entries: %s", actualFilePath);
140
- const entries = Array.isArray(viteConfig.optimizeDeps.entries)
141
- ? viteConfig.optimizeDeps.entries
142
- : [].concat(viteConfig.optimizeDeps.entries ?? []);
143
- viteConfig.optimizeDeps.entries = entries;
144
- entries.push(actualFilePath);
145
- }
169
+ verboseLog("Adding to optimizeDeps.entries: %s", actualFilePath);
170
+ const entries = Array.isArray(viteConfig.optimizeDeps.entries)
171
+ ? viteConfig.optimizeDeps.entries
172
+ : [].concat(viteConfig.optimizeDeps.entries ?? []);
173
+ viteConfig.optimizeDeps.entries = entries;
174
+ entries.push(actualFilePath);
146
175
  }
147
- log("Environment configuration complete for env=%s with %d optimizeDeps includes and %d aliases", env, Array.from(files).filter((f) => f.includes("/node_modules/")).length, files.size);
176
+ log("Environment configuration complete for env=%s", env);
148
177
  }
149
178
  else {
150
179
  log("Skipping optimizeDeps and aliasing for environment: %s", env);
@@ -155,18 +184,20 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
155
184
  if (source === config.virtualModuleName ||
156
185
  source === `/@id/${config.virtualModuleName}`) {
157
186
  log("Resolving %s module", config.virtualModuleName);
158
- return config.virtualModuleName;
187
+ // context(justinvdm, 16 Jun 2025): Include .js extension
188
+ // so it goes through vite processing chain
189
+ return config.virtualModuleName + ".js";
159
190
  }
160
191
  verboseLog("No resolution for id=%s", source);
161
192
  },
162
193
  async load(id) {
163
194
  verboseLog("Loading id=%s", id);
164
- if (id === config.virtualModuleName) {
195
+ if (id === config.virtualModuleName + ".js") {
165
196
  log("Loading %s module with %d files", config.virtualModuleName, files.size);
166
197
  const environment = this.environment?.name || "client";
167
198
  log("Current environment: %s, isDev: %s", environment, isDev);
168
199
  const optimizedDeps = {};
169
- if (isDev) {
200
+ if (isDev && devServer) {
170
201
  for (const file of files) {
171
202
  const resolvedPath = await resolveOptimizedDep(projectRootDir, file, environment, debugNamespace);
172
203
  if (resolvedPath) {
@@ -0,0 +1,2 @@
1
+ import { Plugin } from "vite";
2
+ export declare const devServerTimingPlugin: () => Plugin;
@@ -0,0 +1,24 @@
1
+ import debug from "debug";
2
+ const log = debug("rwsdk:vite:dev-server-timing-plugin");
3
+ export const devServerTimingPlugin = () => {
4
+ const startTime = Date.now();
5
+ let hasLoggedFirstResponse = false;
6
+ return {
7
+ name: "rwsdk:dev-server-timing",
8
+ configureServer(server) {
9
+ server.middlewares.use((_req, res, next) => {
10
+ if (!hasLoggedFirstResponse) {
11
+ res.on("finish", () => {
12
+ if (!hasLoggedFirstResponse) {
13
+ hasLoggedFirstResponse = true;
14
+ const endTime = Date.now();
15
+ const duration = endTime - startTime;
16
+ log(`🚀 Dev server first response completed in ${duration}ms`);
17
+ }
18
+ });
19
+ }
20
+ next();
21
+ });
22
+ },
23
+ };
24
+ };