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.
- package/dist/lib/smokeTests/browser.d.mts +4 -2
- package/dist/lib/smokeTests/browser.mjs +159 -7
- package/dist/lib/smokeTests/codeUpdates.d.mts +1 -0
- package/dist/lib/smokeTests/codeUpdates.mjs +47 -0
- package/dist/lib/smokeTests/development.d.mts +1 -1
- package/dist/lib/smokeTests/development.mjs +10 -3
- package/dist/lib/smokeTests/environment.mjs +1 -14
- package/dist/lib/smokeTests/release.d.mts +1 -1
- package/dist/lib/smokeTests/release.mjs +3 -2
- package/dist/lib/smokeTests/reporting.mjs +30 -2
- package/dist/lib/smokeTests/runSmokeTests.mjs +2 -2
- package/dist/lib/smokeTests/state.d.mts +8 -0
- package/dist/lib/smokeTests/state.mjs +10 -0
- package/dist/lib/smokeTests/templates/SmokeTestClient.template.js +17 -2
- package/dist/lib/smokeTests/templates/smokeTestClientStyles.module.css.template.d.ts +1 -0
- package/dist/lib/smokeTests/templates/smokeTestClientStyles.module.css.template.js +9 -0
- package/dist/lib/smokeTests/templates/smokeTestUrlStyles.css.template.d.ts +1 -0
- package/dist/lib/smokeTests/templates/smokeTestUrlStyles.css.template.js +9 -0
- package/dist/lib/smokeTests/types.d.mts +1 -0
- package/dist/runtime/clientNavigation.d.ts +6 -3
- package/dist/runtime/clientNavigation.js +72 -8
- package/dist/runtime/entries/types/client.d.ts +5 -0
- package/dist/runtime/lib/manifest.d.ts +2 -0
- package/dist/runtime/lib/manifest.js +17 -0
- package/dist/runtime/lib/router.d.ts +1 -0
- package/dist/runtime/register/worker.js +17 -5
- package/dist/runtime/render/renderRscThenableToHtmlStream.d.ts +3 -3
- package/dist/runtime/render/renderRscThenableToHtmlStream.js +7 -3
- package/dist/runtime/render/stylesheets.d.ts +9 -0
- package/dist/runtime/render/stylesheets.js +45 -0
- package/dist/runtime/worker.js +1 -0
- package/dist/scripts/debug-sync.mjs +125 -13
- package/dist/scripts/ensure-deploy-env.mjs +2 -2
- package/dist/scripts/smoke-test.mjs +6 -0
- package/dist/vite/manifestPlugin.d.mts +4 -0
- package/dist/vite/manifestPlugin.mjs +151 -0
- package/dist/vite/redwoodPlugin.mjs +4 -0
- package/dist/vite/ssrBridgePlugin.mjs +17 -8
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +74 -33
- package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +43 -15
- 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 @@
|
|
|
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";
|
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
111
|
+
await options.onNavigate();
|
|
48
112
|
}, true);
|
|
49
113
|
// Handle browser back/forward buttons
|
|
50
114
|
window.addEventListener("popstate", async function handlePopState() {
|
|
51
|
-
await
|
|
115
|
+
await options.onNavigate();
|
|
52
116
|
});
|
|
53
117
|
// Return a handleResponse function for use with initClient
|
|
54
118
|
return {
|
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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(
|
|
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
|
+
};
|
package/dist/runtime/worker.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
18
|
+
rl.on("SIGINT", () => {
|
|
19
19
|
rl.close();
|
|
20
|
-
console.log(
|
|
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
|