rwsdk 0.1.28 → 0.2.0-alpha.1

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 (37) hide show
  1. package/dist/lib/smokeTests/browser.d.mts +4 -2
  2. package/dist/lib/smokeTests/browser.mjs +159 -7
  3. package/dist/lib/smokeTests/codeUpdates.d.mts +1 -0
  4. package/dist/lib/smokeTests/codeUpdates.mjs +47 -0
  5. package/dist/lib/smokeTests/development.d.mts +1 -1
  6. package/dist/lib/smokeTests/development.mjs +10 -3
  7. package/dist/lib/smokeTests/environment.mjs +1 -14
  8. package/dist/lib/smokeTests/release.d.mts +1 -1
  9. package/dist/lib/smokeTests/release.mjs +3 -2
  10. package/dist/lib/smokeTests/reporting.mjs +30 -2
  11. package/dist/lib/smokeTests/runSmokeTests.mjs +2 -2
  12. package/dist/lib/smokeTests/state.d.mts +8 -0
  13. package/dist/lib/smokeTests/state.mjs +10 -0
  14. package/dist/lib/smokeTests/templates/SmokeTestClient.template.js +17 -2
  15. package/dist/lib/smokeTests/templates/smokeTestClientStyles.module.css.template.d.ts +1 -0
  16. package/dist/lib/smokeTests/templates/smokeTestClientStyles.module.css.template.js +9 -0
  17. package/dist/lib/smokeTests/templates/smokeTestUrlStyles.css.template.d.ts +1 -0
  18. package/dist/lib/smokeTests/templates/smokeTestUrlStyles.css.template.js +9 -0
  19. package/dist/lib/smokeTests/types.d.mts +1 -0
  20. package/dist/runtime/lib/manifest.d.ts +2 -0
  21. package/dist/runtime/lib/manifest.js +17 -0
  22. package/dist/runtime/lib/router.d.ts +1 -0
  23. package/dist/runtime/register/worker.js +17 -5
  24. package/dist/runtime/render/renderRscThenableToHtmlStream.d.ts +3 -3
  25. package/dist/runtime/render/renderRscThenableToHtmlStream.js +7 -3
  26. package/dist/runtime/render/stylesheets.d.ts +9 -0
  27. package/dist/runtime/render/stylesheets.js +45 -0
  28. package/dist/runtime/worker.js +1 -0
  29. package/dist/scripts/debug-sync.mjs +125 -13
  30. package/dist/scripts/smoke-test.mjs +6 -0
  31. package/dist/vite/manifestPlugin.d.mts +4 -0
  32. package/dist/vite/manifestPlugin.mjs +151 -0
  33. package/dist/vite/redwoodPlugin.mjs +4 -0
  34. package/dist/vite/ssrBridgePlugin.mjs +17 -8
  35. package/dist/vite/transformJsxScriptTagsPlugin.mjs +74 -33
  36. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +43 -15
  37. package/package.json +1 -1
@@ -3,6 +3,7 @@ export function getSmokeTestClientTemplate() {
3
3
 
4
4
  import React, { useState } from "react";
5
5
  import { smokeTestAction } from "./__smokeTestFunctions";
6
+ import clientStyles from "./smokeTestClientStyles.module.css";
6
7
 
7
8
  interface SmokeTestStatus {
8
9
  status: string;
@@ -80,8 +81,22 @@ export const SmokeTestClient: React.FC = () => {
80
81
  {loading ? "Checking..." : "Run Smoke Test"}
81
82
  </button>
82
83
 
84
+ {/* Client Stylesheet Marker */}
85
+ <div
86
+ className="smoke-test-url-styles"
87
+ data-testid="smoke-test-url-styles"
88
+ >
89
+ <p>A red box should appear above this text (from URL import)</p>
90
+ </div>
91
+ <div
92
+ className={clientStyles.smokeTestClientStyles}
93
+ data-testid="smoke-test-client-styles"
94
+ >
95
+ <p>A blue box should appear above this text (from CSS module)</p>
96
+ </div>
97
+
83
98
  {/* HMR Testing Marker - Do not modify this comment */}
84
- <div
99
+ <div
85
100
  id="client-hmr-marker"
86
101
  data-testid="client-hmr-marker"
87
102
  data-hmr-text="original"
@@ -146,7 +161,7 @@ export const SmokeTestClient: React.FC = () => {
146
161
  </div>
147
162
  )}
148
163
 
149
- <div
164
+ <div
150
165
  id="smoke-test-client-timestamp"
151
166
  data-client-timestamp={lastCheck?.timestamp ?? ""}
152
167
  data-status={lastCheck?.status ?? ""}
@@ -0,0 +1 @@
1
+ export declare const template = "\n.smokeTestClientStyles {\n /* This is a comment to test HMR */\n background-color: rgb(0, 0, 255);\n width: 100px;\n height: 100px;\n}\n";
@@ -0,0 +1,9 @@
1
+ /* eslint-disable quotes */
2
+ export const template = `
3
+ .smokeTestClientStyles {
4
+ /* This is a comment to test HMR */
5
+ background-color: rgb(0, 0, 255);
6
+ width: 100px;
7
+ height: 100px;
8
+ }
9
+ `;
@@ -0,0 +1 @@
1
+ export declare const template = "\n.smoke-test-url-styles {\n /* This is a comment to test HMR */\n background-color: rgb(255, 0, 0);\n width: 100px;\n height: 100px;\n}\n";
@@ -0,0 +1,9 @@
1
+ /* eslint-disable quotes */
2
+ export const template = `
3
+ .smoke-test-url-styles {
4
+ /* This is a comment to test HMR */
5
+ background-color: rgb(255, 0, 0);
6
+ width: 100px;
7
+ height: 100px;
8
+ }
9
+ `;
@@ -23,6 +23,7 @@ export interface SmokeTestOptions {
23
23
  copyProject?: boolean;
24
24
  realtime?: boolean;
25
25
  skipHmr?: boolean;
26
+ skipStyleTests?: boolean;
26
27
  }
27
28
  export interface TestResources {
28
29
  tempDirCleanup?: () => Promise<void>;
@@ -0,0 +1,2 @@
1
+ import { type RequestInfo } from "../requestInfo/types";
2
+ export declare const getManifest: (requestInfo: RequestInfo) => Promise<Record<string, any>>;
@@ -0,0 +1,17 @@
1
+ let manifest;
2
+ export const getManifest = async (requestInfo) => {
3
+ if (manifest) {
4
+ return manifest;
5
+ }
6
+ if (import.meta.env.VITE_IS_DEV_SERVER) {
7
+ const url = new URL(requestInfo.request.url);
8
+ url.searchParams.set("scripts", JSON.stringify(Array.from(requestInfo.rw.scriptsToBeLoaded)));
9
+ url.pathname = "/__rwsdk_manifest";
10
+ manifest = await fetch(url.toString()).then((res) => res.json());
11
+ }
12
+ else {
13
+ const { default: prodManifest } = await import("virtual:rwsdk:manifest.js");
14
+ manifest = prodManifest;
15
+ }
16
+ return manifest;
17
+ };
@@ -15,6 +15,7 @@ export type RwContext = {
15
15
  ssr: boolean;
16
16
  layouts?: React.FC<LayoutProps<any>>[];
17
17
  databases: Map<string, Kysely<any>>;
18
+ scriptsToBeLoaded: Set<string>;
18
19
  pageRouteResolved: PromiseWithResolvers<void> | undefined;
19
20
  };
20
21
  export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response> | void | Promise<void> | Promise<Response | void>;
@@ -1,5 +1,6 @@
1
1
  import { registerServerReference as baseRegisterServerReference, registerClientReference as baseRegisterClientReference, decodeReply, } from "react-server-dom-webpack/server.edge";
2
2
  import { getServerModuleExport } from "../imports/worker.js";
3
+ import { requestInfo } from "../requestInfo/worker.js";
3
4
  export function registerServerReference(action, id, name) {
4
5
  if (typeof action !== "function") {
5
6
  return action;
@@ -12,11 +13,22 @@ export function registerClientReference(id, exportName, value) {
12
13
  ? value
13
14
  : () => null;
14
15
  const reference = baseRegisterClientReference({}, id, exportName);
15
- return Object.defineProperties(wrappedValue, {
16
- ...Object.getOwnPropertyDescriptors(reference),
17
- $$async: { value: true },
18
- $$isClientReference: { value: true },
19
- });
16
+ const finalDescriptors = Object.getOwnPropertyDescriptors(reference);
17
+ const idDescriptor = finalDescriptors.$$id;
18
+ if (idDescriptor && idDescriptor.hasOwnProperty("value")) {
19
+ const originalValue = idDescriptor.value;
20
+ finalDescriptors.$$id = {
21
+ configurable: idDescriptor.configurable,
22
+ enumerable: idDescriptor.enumerable,
23
+ get() {
24
+ requestInfo.rw.scriptsToBeLoaded.add(id);
25
+ return originalValue;
26
+ },
27
+ };
28
+ }
29
+ finalDescriptors.$$async = { value: true };
30
+ finalDescriptors.$$isClientReference = { value: true };
31
+ return Object.defineProperties(wrappedValue, finalDescriptors);
20
32
  }
21
33
  export async function __smokeTestActionHandler(timestamp) {
22
34
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -1,9 +1,9 @@
1
- import { type DocumentProps } from "../lib/router";
2
- import { type RequestInfo } from "../requestInfo/types";
1
+ import { type DocumentProps } from "../lib/router.js";
2
+ import { type RequestInfo } from "../requestInfo/types.js";
3
3
  export declare const renderRscThenableToHtmlStream: ({ thenable, Document, requestInfo, shouldSSR, onError, }: {
4
4
  thenable: any;
5
5
  Document: React.FC<DocumentProps>;
6
6
  requestInfo: RequestInfo;
7
7
  shouldSSR: boolean;
8
8
  onError: (error: unknown) => void;
9
- }) => Promise<import("react-dom/server").ReactDOMServerReadableStream>;
9
+ }) => Promise<import("react-dom/server.js").ReactDOMServerReadableStream>;
@@ -1,9 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { use } from "react";
3
3
  import { renderToReadableStream } from "react-dom/server.edge";
4
+ import { Stylesheets } from "./stylesheets.js";
4
5
  export const renderRscThenableToHtmlStream = async ({ thenable, Document, requestInfo, shouldSSR, onError, }) => {
5
6
  const Component = () => {
6
- const node = use(thenable).node;
7
+ const RscApp = () => {
8
+ const node = use(thenable).node;
9
+ return (_jsxs(_Fragment, { children: [_jsx(Stylesheets, { requestInfo: requestInfo }), _jsx("div", { id: "hydrate-root", children: node })] }));
10
+ };
7
11
  // todo(justinvdm, 18 Jun 2025): We can build on this later to allow users
8
12
  // surface context. e.g:
9
13
  // * we assign `user: requestInfo.clientCtx` here
@@ -17,7 +21,7 @@ export const renderRscThenableToHtmlStream = async ({ thenable, Document, reques
17
21
  };
18
22
  return (_jsxs(Document, { ...requestInfo, children: [_jsx("script", { nonce: requestInfo.rw.nonce, dangerouslySetInnerHTML: {
19
23
  __html: `globalThis.__RWSDK_CONTEXT = ${JSON.stringify(clientContext)}`,
20
- } }), _jsx("div", { id: "hydrate-root", children: node })] }));
24
+ } }), _jsx(RscApp, {})] }));
21
25
  };
22
26
  return await renderToReadableStream(_jsx(Component, {}), {
23
27
  nonce: requestInfo.rw.nonce,
@@ -0,0 +1,9 @@
1
+ import { type RequestInfo } from "../requestInfo/types.js";
2
+ export type CssEntry = {
3
+ url: string;
4
+ content: string;
5
+ absolutePath: string;
6
+ };
7
+ export declare const Stylesheets: ({ requestInfo }: {
8
+ requestInfo: RequestInfo;
9
+ }) => import("react/jsx-runtime.js").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { use } from "react";
3
+ import { getManifest } from "../lib/manifest.js";
4
+ const findCssForModule = (scriptId, manifest) => {
5
+ const css = new Set();
6
+ const visited = new Set();
7
+ const inner = (id) => {
8
+ if (visited.has(id)) {
9
+ return;
10
+ }
11
+ visited.add(id);
12
+ const entry = manifest[id];
13
+ if (!entry) {
14
+ return;
15
+ }
16
+ if (entry.css) {
17
+ for (const href of entry.css) {
18
+ css.add(href);
19
+ }
20
+ }
21
+ };
22
+ inner(scriptId);
23
+ return Array.from(css);
24
+ };
25
+ export const Stylesheets = ({ requestInfo }) => {
26
+ const manifest = use(getManifest(requestInfo));
27
+ const allStylesheets = new Set();
28
+ for (const scriptId of requestInfo.rw.scriptsToBeLoaded) {
29
+ const css = findCssForModule(scriptId, manifest);
30
+ for (const entry of css) {
31
+ allStylesheets.add(entry);
32
+ }
33
+ }
34
+ return (_jsx(_Fragment, { children: Array.from(allStylesheets).map((entry) => {
35
+ if (typeof entry === "string") {
36
+ return (_jsx("link", { rel: "stylesheet", href: entry, precedence: "first" }, entry));
37
+ }
38
+ if (import.meta.env.VITE_IS_DEV_SERVER) {
39
+ return (_jsx("style", { "data-vite-dev-id": entry.absolutePath, dangerouslySetInnerHTML: { __html: entry.content } }, entry.url));
40
+ }
41
+ else {
42
+ return (_jsx("link", { rel: "stylesheet", href: entry.url, precedence: "first" }, entry.url));
43
+ }
44
+ }) }));
45
+ };
@@ -41,6 +41,7 @@ export const defineApp = (routes) => {
41
41
  rscPayload: true,
42
42
  ssr: true,
43
43
  databases: new Map(),
44
+ scriptsToBeLoaded: new Set(),
44
45
  pageRouteResolved: undefined,
45
46
  };
46
47
  const outerRequestInfo = {
@@ -24,6 +24,93 @@ const getPackageManagerInfo = (targetDir) => {
24
24
  }
25
25
  return pnpmResult;
26
26
  };
27
+ /**
28
+ * @summary Workaround for pnpm's local tarball dependency resolution.
29
+ *
30
+ * @description
31
+ * When installing a new version of the SDK from a local tarball (e.g., during
32
+ * development with `rwsync`), pnpm creates a new, uniquely-named directory in
33
+ * the `.pnpm` store (e.g., `rwsdk@file+...`).
34
+ *
35
+ * A challenge arises when other packages list `rwsdk` as a peer dependency.
36
+ * pnpm may not consistently update the symlinks for these peer dependencies
37
+ * to point to the newest `rwsdk` instance. This can result in a state where
38
+ * multiple versions of `rwsdk` coexist in `node_modules`, with some parts of
39
+ * the application using a stale version.
40
+ *
41
+ * This function addresses the issue by:
42
+ * 1. Identifying the most recently installed `rwsdk` instance in the `.pnpm`
43
+ * store after a `pnpm install` run.
44
+ * 2. Forcefully updating the top-level `node_modules/rwsdk` symlink to point
45
+ * to this new instance.
46
+ * 3. Traversing all other `rwsdk`-related directories in the `.pnpm` store
47
+ * and updating their internal `rwsdk` symlinks to also point to the correct
48
+ * new instance.
49
+ *
50
+ * I am sorry for this ugly hack, I am sure there is a better way, and that it is me
51
+ * doing something wrong. The aim is not to go down this rabbit hole right now
52
+ * -- @justinvdm
53
+ */
54
+ const hackyPnpmSymlinkFix = async (targetDir) => {
55
+ console.log("💣 Performing pnpm symlink fix...");
56
+ const pnpmDir = path.join(targetDir, "node_modules", ".pnpm");
57
+ if (!existsSync(pnpmDir)) {
58
+ console.log(" 🤔 No .pnpm directory found.");
59
+ return;
60
+ }
61
+ try {
62
+ const entries = await fs.readdir(pnpmDir);
63
+ // Find ALL rwsdk directories, not just file-based ones, to handle
64
+ // all kinds of stale peer dependencies.
65
+ const rwsdkDirs = entries.filter((e) => e.startsWith("rwsdk@"));
66
+ console.log(" Found rwsdk directories:", rwsdkDirs);
67
+ if (rwsdkDirs.length === 0) {
68
+ console.log(" 🤔 No rwsdk directories found to hack.");
69
+ return;
70
+ }
71
+ let latestDir = "";
72
+ let latestMtime = new Date(0);
73
+ for (const dir of rwsdkDirs) {
74
+ const fullPath = path.join(pnpmDir, dir);
75
+ const stats = await fs.stat(fullPath);
76
+ if (stats.mtime > latestMtime) {
77
+ latestMtime = stats.mtime;
78
+ latestDir = dir;
79
+ }
80
+ }
81
+ console.log(" Latest rwsdk directory:", latestDir);
82
+ if (!latestDir) {
83
+ console.log(" 🤔 Could not determine the latest rwsdk directory.");
84
+ return;
85
+ }
86
+ const goldenSourcePath = path.join(pnpmDir, latestDir, "node_modules", "rwsdk");
87
+ if (!existsSync(goldenSourcePath)) {
88
+ console.error(` ❌ Golden source path does not exist: ${goldenSourcePath}`);
89
+ return;
90
+ }
91
+ console.log(` 🎯 Golden rwsdk path is: ${goldenSourcePath}`);
92
+ // 1. Fix top-level symlink
93
+ const topLevelSymlink = path.join(targetDir, "node_modules", "rwsdk");
94
+ await fs.rm(topLevelSymlink, { recursive: true, force: true });
95
+ await fs.symlink(goldenSourcePath, topLevelSymlink, "dir");
96
+ console.log(` ✅ Symlinked ${topLevelSymlink} -> ${goldenSourcePath}`);
97
+ // 2. Fix peer dependency symlinks
98
+ const allPnpmDirs = await fs.readdir(pnpmDir);
99
+ for (const dir of allPnpmDirs) {
100
+ if (dir === latestDir || !dir.includes("rwsdk"))
101
+ continue;
102
+ const peerSymlink = path.join(pnpmDir, dir, "node_modules", "rwsdk");
103
+ if (existsSync(peerSymlink)) {
104
+ await fs.rm(peerSymlink, { recursive: true, force: true });
105
+ await fs.symlink(goldenSourcePath, peerSymlink, "dir");
106
+ console.log(` ✅ Hijacked symlink in ${dir}`);
107
+ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ console.error(" ❌ Failed during hacky pnpm symlink fix:", error);
112
+ }
113
+ };
27
114
  const performFullSync = async (sdkDir, targetDir, cacheBust = false) => {
28
115
  const sdkPackageJsonPath = path.join(sdkDir, "package.json");
29
116
  let originalSdkPackageJson = null;
@@ -58,25 +145,47 @@ const performFullSync = async (sdkDir, targetDir, cacheBust = false) => {
58
145
  .readFile(lockfilePath, "utf-8")
59
146
  .catch(() => null);
60
147
  try {
61
- const cmd = pm.name;
62
- const args = [pm.command];
63
- if (pm.name === "yarn") {
64
- args.push(`file:${tarballPath}`);
148
+ if (pm.name === "pnpm") {
149
+ console.log("🛠️ Using pnpm overrides to install SDK...");
150
+ if (originalPackageJson) {
151
+ const targetPackageJson = JSON.parse(originalPackageJson);
152
+ targetPackageJson.pnpm = targetPackageJson.pnpm || {};
153
+ targetPackageJson.pnpm.overrides =
154
+ targetPackageJson.pnpm.overrides || {};
155
+ targetPackageJson.pnpm.overrides.rwsdk = `file:${tarballPath}`;
156
+ await fs.writeFile(packageJsonPath, JSON.stringify(targetPackageJson, null, 2));
157
+ }
158
+ // We use install here, which respects the overrides.
159
+ // We also don't want to fail if the lockfile is out of date.
160
+ await $("pnpm", ["install", "--no-frozen-lockfile"], {
161
+ cwd: targetDir,
162
+ stdio: "inherit",
163
+ });
164
+ if (process.env.RWSDK_PNPM_SYMLINK_FIX) {
165
+ await hackyPnpmSymlinkFix(targetDir);
166
+ }
65
167
  }
66
168
  else {
67
- args.push(tarballPath);
169
+ const cmd = pm.name;
170
+ const args = [pm.command];
171
+ if (pm.name === "yarn") {
172
+ args.push(`file:${tarballPath}`);
173
+ }
174
+ else {
175
+ args.push(tarballPath);
176
+ }
177
+ await $(cmd, args, {
178
+ cwd: targetDir,
179
+ stdio: "inherit",
180
+ });
68
181
  }
69
- await $(cmd, args, {
70
- cwd: targetDir,
71
- stdio: "inherit",
72
- });
73
182
  }
74
183
  finally {
75
184
  if (originalPackageJson) {
76
185
  console.log("Restoring package.json...");
77
186
  await fs.writeFile(packageJsonPath, originalPackageJson);
78
187
  }
79
- if (originalLockfile) {
188
+ if (originalLockfile && pm.name !== "pnpm") {
80
189
  console.log(`Restoring ${pm.lockFile}...`);
81
190
  await fs.writeFile(lockfilePath, originalLockfile);
82
191
  }
@@ -131,7 +240,9 @@ const performSync = async (sdkDir, targetDir) => {
131
240
  }
132
241
  if (packageJsonChanged) {
133
242
  console.log("📦 package.json changed, performing full sync...");
134
- await performFullSync(sdkDir, targetDir);
243
+ // We always cache-bust on a full sync now to ensure pnpm's overrides
244
+ // see a new version and the hacky symlink fix runs on a clean slate.
245
+ await performFullSync(sdkDir, targetDir, true);
135
246
  }
136
247
  else {
137
248
  await performFastSync(sdkDir, targetDir);
@@ -150,7 +261,8 @@ export const debugSync = async (opts) => {
150
261
  return;
151
262
  }
152
263
  // --- Watch Mode Logic ---
153
- const lockfilePath = path.join(targetDir, "node_modules", ".rwsync.lock");
264
+ // Use global lock based on SDK directory since all instances sync from the same source
265
+ const lockfilePath = path.join(sdkDir, ".rwsync.lock");
154
266
  let release;
155
267
  // Ensure the directory for the lockfile exists
156
268
  await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
@@ -161,7 +273,7 @@ export const debugSync = async (opts) => {
161
273
  }
162
274
  catch (e) {
163
275
  if (e.code === "ELOCKED") {
164
- console.error(`❌ Another rwsync process is already watching ${targetDir}.`);
276
+ console.error(`❌ Another rwsync process is already running for this SDK.`);
165
277
  console.error(` If this is not correct, please remove the lockfile at ${lockfilePath}`);
166
278
  process.exit(1);
167
279
  }
@@ -30,6 +30,8 @@ if (fileURLToPath(import.meta.url) === process.argv[1]) {
30
30
  copyProject: false, // Default to false - don't copy project to artifacts
31
31
  realtime: false, // Default to false - don't just test realtime
32
32
  skipHmr: false, // Default to false - run HMR tests
33
+ // todo(justinvdm, 2025-07-31): Remove this once style tests working with headless
34
+ skipStyleTests: true, // Default to true - skip style tests
33
35
  // sync: will be set below
34
36
  };
35
37
  // Log if we're in CI
@@ -53,6 +55,9 @@ if (fileURLToPath(import.meta.url) === process.argv[1]) {
53
55
  else if (arg === "--skip-hmr") {
54
56
  options.skipHmr = true;
55
57
  }
58
+ else if (arg === "--run-style-tests") {
59
+ options.skipStyleTests = false;
60
+ }
56
61
  else if (arg === "--keep") {
57
62
  options.keep = true;
58
63
  }
@@ -89,6 +94,7 @@ Options:
89
94
  --skip-release Skip testing the release/production deployment
90
95
  --skip-client Skip client-side tests, only run server-side checks
91
96
  --skip-hmr Skip hot module replacement (HMR) tests
97
+ --run-style-tests Enable stylesheet-related tests (disabled by default)
92
98
  --path=PATH Project directory to test
93
99
  --artifact-dir=DIR Directory to store test artifacts (default: .artifacts)
94
100
  --keep Keep temporary test directory after tests complete
@@ -0,0 +1,4 @@
1
+ import { type Plugin } from "vite";
2
+ export declare const manifestPlugin: ({ manifestPath, }: {
3
+ manifestPath: string;
4
+ }) => Plugin;
@@ -0,0 +1,151 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import debug from "debug";
3
+ import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
4
+ const log = debug("rwsdk:vite:manifest-plugin");
5
+ const virtualModuleId = "virtual:rwsdk:manifest.js";
6
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
7
+ const getCssForModule = (server, moduleId, css) => {
8
+ const stack = [moduleId];
9
+ const visited = new Set();
10
+ while (stack.length > 0) {
11
+ const currentModuleId = stack.pop();
12
+ if (visited.has(currentModuleId)) {
13
+ continue;
14
+ }
15
+ visited.add(currentModuleId);
16
+ const moduleNode = server.environments.client.moduleGraph.getModuleById(currentModuleId);
17
+ if (!moduleNode) {
18
+ continue;
19
+ }
20
+ for (const importedModule of moduleNode.importedModules) {
21
+ if (importedModule.url.endsWith(".css")) {
22
+ const absolutePath = importedModule.file;
23
+ css.add({
24
+ url: importedModule.url,
25
+ // The `ssrTransformResult` has the CSS content, because the default
26
+ // transform for CSS is to a string of the CSS content.
27
+ content: importedModule.ssrTransformResult?.code ?? "",
28
+ absolutePath,
29
+ });
30
+ }
31
+ if (importedModule.id) {
32
+ stack.push(importedModule.id);
33
+ }
34
+ }
35
+ }
36
+ };
37
+ export const manifestPlugin = ({ manifestPath, }) => {
38
+ let isBuild = false;
39
+ let root;
40
+ return {
41
+ name: "rwsdk:manifest",
42
+ configResolved(config) {
43
+ log("Config resolved, command=%s", config.command);
44
+ isBuild = config.command === "build";
45
+ root = config.root;
46
+ },
47
+ resolveId(id) {
48
+ if (id === virtualModuleId) {
49
+ process.env.VERBOSE && log("Resolving virtual module id=%s", id);
50
+ return resolvedVirtualModuleId;
51
+ }
52
+ },
53
+ async load(id) {
54
+ if (id === resolvedVirtualModuleId) {
55
+ process.env.VERBOSE && log("Loading virtual module id=%s", id);
56
+ if (!isBuild) {
57
+ process.env.VERBOSE && log("Not a build, returning empty manifest");
58
+ return `export default {}`;
59
+ }
60
+ log("Reading manifest from %s", manifestPath);
61
+ const manifestContent = await readFile(manifestPath, "utf-8");
62
+ const manifest = JSON.parse(manifestContent);
63
+ const normalizedManifest = {};
64
+ for (const key in manifest) {
65
+ const normalizedKey = normalizeModulePath(key, root, {
66
+ isViteStyle: false,
67
+ });
68
+ const entry = manifest[key];
69
+ delete manifest[key];
70
+ normalizedManifest[normalizedKey] = entry;
71
+ entry.file = normalizeModulePath(entry.file, root, {
72
+ isViteStyle: false,
73
+ });
74
+ const normalizedCss = [];
75
+ if (entry.css) {
76
+ for (const css of entry.css) {
77
+ normalizedCss.push(normalizeModulePath(css, root, {
78
+ isViteStyle: false,
79
+ }));
80
+ }
81
+ entry.css = normalizedCss;
82
+ }
83
+ }
84
+ return `export default ${JSON.stringify(normalizedManifest)}`;
85
+ }
86
+ },
87
+ configEnvironment(name, config) {
88
+ if (name !== "worker" && name !== "ssr") {
89
+ return;
90
+ }
91
+ log("Configuring environment: name=%s", name);
92
+ config.optimizeDeps ??= {};
93
+ config.optimizeDeps.esbuildOptions ??= {};
94
+ config.optimizeDeps.esbuildOptions.plugins ??= [];
95
+ config.optimizeDeps.esbuildOptions.plugins.push({
96
+ name: "rwsdk:manifest:esbuild",
97
+ setup(build) {
98
+ log("Setting up esbuild plugin for environment: %s", name);
99
+ build.onResolve({ filter: /^virtual:rwsdk:manifest\.js$/ }, () => {
100
+ log("Resolving virtual manifest module in esbuild");
101
+ return {
102
+ path: "virtual:rwsdk:manifest.js",
103
+ external: true,
104
+ };
105
+ });
106
+ },
107
+ });
108
+ },
109
+ configureServer(server) {
110
+ log("Configuring server middleware for manifest");
111
+ server.middlewares.use("/__rwsdk_manifest", async (req, res, next) => {
112
+ log("Manifest request received: %s", req.url);
113
+ try {
114
+ const url = new URL(req.url, `http://${req.headers.host}`);
115
+ const scripts = JSON.parse(url.searchParams.get("scripts") || "[]");
116
+ process.env.VERBOSE && log("Transforming scripts: %o", scripts);
117
+ for (const script of scripts) {
118
+ await server.environments.client.transformRequest(script);
119
+ }
120
+ const manifest = {};
121
+ log("Building manifest from module graph");
122
+ for (const file of server.environments.client.moduleGraph.fileToModulesMap.keys()) {
123
+ const modules = server.environments.client.moduleGraph.getModulesByFile(file);
124
+ if (!modules) {
125
+ continue;
126
+ }
127
+ for (const module of modules) {
128
+ if (module.file) {
129
+ const css = new Set();
130
+ getCssForModule(server, module.id, css);
131
+ manifest[normalizeModulePath(module.file, server.config.root)] =
132
+ {
133
+ file: module.url,
134
+ css: Array.from(css),
135
+ };
136
+ }
137
+ }
138
+ }
139
+ log("Manifest built successfully");
140
+ process.env.VERBOSE && log("Manifest: %o", manifest);
141
+ res.setHeader("Content-Type", "application/json");
142
+ res.end(JSON.stringify(manifest));
143
+ }
144
+ catch (e) {
145
+ log("Error building manifest: %o", e);
146
+ next(e);
147
+ }
148
+ });
149
+ },
150
+ };
151
+ };
@@ -22,6 +22,7 @@ import { prismaPlugin } from "./prismaPlugin.mjs";
22
22
  import { ssrBridgePlugin } from "./ssrBridgePlugin.mjs";
23
23
  import { hasPkgScript } from "../lib/hasPkgScript.mjs";
24
24
  import { devServerTimingPlugin } from "./devServerTimingPlugin.mjs";
25
+ import { manifestPlugin } from "./manifestPlugin.mjs";
25
26
  const determineWorkerEntryPathname = async (projectRootDir, workerConfigPath, options) => {
26
27
  if (options.entry?.worker) {
27
28
  return resolve(projectRootDir, options.entry.worker);
@@ -101,6 +102,9 @@ export const redwoodPlugin = async (options = {}) => {
101
102
  transformJsxScriptTagsPlugin({
102
103
  manifestPath: resolve(projectRootDir, "dist", "client", ".vite", "manifest.json"),
103
104
  }),
105
+ manifestPlugin({
106
+ manifestPath: resolve(projectRootDir, "dist", "client", ".vite", "manifest.json"),
107
+ }),
104
108
  moveStaticAssetsPlugin({ rootDir: projectRootDir }),
105
109
  prismaPlugin({ projectRootDir }),
106
110
  ];