rwsdk 0.1.26 → 0.2.0-alpha.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 (41) 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/clientNavigation.d.ts +6 -3
  21. package/dist/runtime/clientNavigation.js +72 -8
  22. package/dist/runtime/entries/types/client.d.ts +5 -0
  23. package/dist/runtime/lib/manifest.d.ts +2 -0
  24. package/dist/runtime/lib/manifest.js +17 -0
  25. package/dist/runtime/lib/router.d.ts +1 -0
  26. package/dist/runtime/register/worker.js +17 -5
  27. package/dist/runtime/render/renderRscThenableToHtmlStream.d.ts +3 -3
  28. package/dist/runtime/render/renderRscThenableToHtmlStream.js +7 -3
  29. package/dist/runtime/render/stylesheets.d.ts +9 -0
  30. package/dist/runtime/render/stylesheets.js +45 -0
  31. package/dist/runtime/worker.js +1 -0
  32. package/dist/scripts/debug-sync.mjs +125 -13
  33. package/dist/scripts/ensure-deploy-env.mjs +2 -2
  34. package/dist/scripts/smoke-test.mjs +6 -0
  35. package/dist/vite/manifestPlugin.d.mts +4 -0
  36. package/dist/vite/manifestPlugin.mjs +151 -0
  37. package/dist/vite/redwoodPlugin.mjs +4 -0
  38. package/dist/vite/ssrBridgePlugin.mjs +17 -8
  39. package/dist/vite/transformJsxScriptTagsPlugin.mjs +74 -33
  40. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +43 -15
  41. 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>;
@@ -1,6 +1,9 @@
1
+ export interface ClientNavigationOptions {
2
+ onNavigate?: () => void;
3
+ scrollToTop?: boolean;
4
+ scrollBehavior?: "auto" | "smooth" | "instant";
5
+ }
1
6
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
2
- export declare function initClientNavigation(opts?: {
3
- onNavigate: () => void;
4
- }): {
7
+ export declare function initClientNavigation(opts?: ClientNavigationOptions): {
5
8
  handleResponse: (response: Response) => boolean;
6
9
  };
@@ -27,12 +27,70 @@ export function validateClickEvent(event, target) {
27
27
  }
28
28
  return true;
29
29
  }
30
- export function initClientNavigation(opts = {
31
- onNavigate: async function onNavigate() {
32
- // @ts-expect-error
33
- await globalThis.__rsc_callServer();
34
- },
35
- }) {
30
+ export function initClientNavigation(opts = {}) {
31
+ // Merge user options with defaults
32
+ const options = {
33
+ onNavigate: async function onNavigate() {
34
+ // @ts-expect-error
35
+ await globalThis.__rsc_callServer();
36
+ },
37
+ scrollToTop: true,
38
+ scrollBehavior: 'instant',
39
+ ...opts,
40
+ };
41
+ // Prevent browser's automatic scroll restoration for popstate
42
+ if ('scrollRestoration' in history) {
43
+ history.scrollRestoration = 'manual';
44
+ }
45
+ // Set up scroll behavior management
46
+ let popStateWasCalled = false;
47
+ let savedScrollPosition = null;
48
+ const observer = new MutationObserver(() => {
49
+ if (popStateWasCalled && savedScrollPosition) {
50
+ // Restore scroll position for popstate navigation (always instant)
51
+ window.scrollTo({
52
+ top: savedScrollPosition.y,
53
+ left: savedScrollPosition.x,
54
+ behavior: 'instant',
55
+ });
56
+ savedScrollPosition = null;
57
+ }
58
+ else if (options.scrollToTop && !popStateWasCalled) {
59
+ // Scroll to top for anchor click navigation (configurable)
60
+ window.scrollTo({
61
+ top: 0,
62
+ left: 0,
63
+ behavior: options.scrollBehavior,
64
+ });
65
+ // Update the current history entry with the new scroll position (top)
66
+ // This ensures that if we navigate back and then forward again,
67
+ // we return to the top position, not some previous scroll position
68
+ window.history.replaceState({
69
+ ...window.history.state,
70
+ scrollX: 0,
71
+ scrollY: 0
72
+ }, "", window.location.href);
73
+ }
74
+ popStateWasCalled = false;
75
+ });
76
+ const handleScrollPopState = (event) => {
77
+ popStateWasCalled = true;
78
+ // Save the scroll position that the browser would have restored to
79
+ const state = event.state;
80
+ if (state && typeof state === 'object' && 'scrollX' in state && 'scrollY' in state) {
81
+ savedScrollPosition = { x: state.scrollX, y: state.scrollY };
82
+ }
83
+ else {
84
+ // Fallback: try to get scroll position from browser's session history
85
+ // This is a best effort since we can't directly access the browser's stored position
86
+ savedScrollPosition = { x: window.scrollX, y: window.scrollY };
87
+ }
88
+ };
89
+ const main = document.querySelector("main") || document.body;
90
+ if (main) {
91
+ window.addEventListener("popstate", handleScrollPopState);
92
+ observer.observe(main, { childList: true, subtree: true });
93
+ }
36
94
  // Intercept all anchor tag clicks
37
95
  document.addEventListener("click", async function handleClickEvent(event) {
38
96
  // Prevent default navigation
@@ -43,12 +101,18 @@ export function initClientNavigation(opts = {
43
101
  const el = event.target;
44
102
  const a = el.closest("a");
45
103
  const href = a?.getAttribute("href");
104
+ // Save current scroll position before navigating
105
+ window.history.replaceState({
106
+ path: window.location.pathname,
107
+ scrollX: window.scrollX,
108
+ scrollY: window.scrollY
109
+ }, "", window.location.href);
46
110
  window.history.pushState({ path: href }, "", window.location.origin + href);
47
- await opts.onNavigate();
111
+ await options.onNavigate();
48
112
  }, true);
49
113
  // Handle browser back/forward buttons
50
114
  window.addEventListener("popstate", async function handlePopState() {
51
- await opts.onNavigate();
115
+ await options.onNavigate();
52
116
  });
53
117
  // Return a handleResponse function for use with initClient
54
118
  return {
@@ -1 +1,6 @@
1
1
  import "./shared";
2
+ export interface ClientNavigationOptions {
3
+ onNavigate?: () => void;
4
+ scrollToTop?: boolean;
5
+ scrollBehavior?: "auto" | "smooth" | "instant";
6
+ }
@@ -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
  };
19
20
  export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response> | void | Promise<void> | Promise<Response | void>;
20
21
  type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response>;
@@ -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
  };
45
46
  const outerRequestInfo = {
46
47
  request,
@@ -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
  }
@@ -15,9 +15,9 @@ const promptForDeployment = async () => {
15
15
  });
16
16
  return new Promise((resolve) => {
17
17
  // Handle Ctrl+C (SIGINT)
18
- rl.on('SIGINT', () => {
18
+ rl.on("SIGINT", () => {
19
19
  rl.close();
20
- console.log('\nDeployment cancelled.');
20
+ console.log("\nDeployment cancelled.");
21
21
  process.exit(1);
22
22
  });
23
23
  rl.question("Do you want to proceed with deployment? (y/N): ", (answer) => {
@@ -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;