hadars 0.1.24 → 0.1.26
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/cli.js +93 -62
- package/dist/index.js +0 -2
- package/dist/slim-react/index.cjs +91 -41
- package/dist/slim-react/index.js +91 -44
- package/dist/slim-react/jsx-runtime.js +0 -1
- package/dist/ssr-render-worker.js +81 -46
- package/dist/ssr-watch.js +12 -17
- package/package.json +1 -1
- package/src/slim-react/render.ts +50 -16
- package/src/slim-react/renderContext.ts +34 -38
- package/src/utils/rspack.ts +9 -13
- package/dist/chunk-EZUCZHGV.js +0 -11
package/src/slim-react/render.ts
CHANGED
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
pushContextValue,
|
|
34
34
|
popContextValue,
|
|
35
35
|
getContextValue,
|
|
36
|
-
|
|
36
|
+
swapContextMap,
|
|
37
|
+
captureMap,
|
|
37
38
|
} from "./renderContext";
|
|
38
39
|
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
@@ -613,7 +614,11 @@ function renderComponent(
|
|
|
613
614
|
};
|
|
614
615
|
const r = renderChildren(props.children, writer, isSvg);
|
|
615
616
|
if (r && typeof (r as any).then === "function") {
|
|
616
|
-
|
|
617
|
+
const m = captureMap();
|
|
618
|
+
return (r as Promise<void>).then(
|
|
619
|
+
() => { swapContextMap(m); finish(); },
|
|
620
|
+
(e) => { swapContextMap(m); finish(); throw e; },
|
|
621
|
+
);
|
|
617
622
|
}
|
|
618
623
|
finish();
|
|
619
624
|
return;
|
|
@@ -645,19 +650,29 @@ function renderComponent(
|
|
|
645
650
|
|
|
646
651
|
// Async component
|
|
647
652
|
if (result instanceof Promise) {
|
|
653
|
+
const m = captureMap();
|
|
648
654
|
return result.then((resolved) => {
|
|
655
|
+
swapContextMap(m);
|
|
649
656
|
const r = renderNode(resolved, writer, isSvg);
|
|
650
657
|
if (r && typeof (r as any).then === "function") {
|
|
651
|
-
|
|
658
|
+
const m2 = captureMap();
|
|
659
|
+
return (r as Promise<void>).then(
|
|
660
|
+
() => { swapContextMap(m2); finish(); },
|
|
661
|
+
(e) => { swapContextMap(m2); finish(); throw e; },
|
|
662
|
+
);
|
|
652
663
|
}
|
|
653
664
|
finish();
|
|
654
|
-
}, (e) => { finish(); throw e; });
|
|
665
|
+
}, (e) => { swapContextMap(m); finish(); throw e; });
|
|
655
666
|
}
|
|
656
667
|
|
|
657
668
|
const r = renderNode(result, writer, isSvg);
|
|
658
669
|
|
|
659
670
|
if (r && typeof (r as any).then === "function") {
|
|
660
|
-
|
|
671
|
+
const m = captureMap();
|
|
672
|
+
return (r as Promise<void>).then(
|
|
673
|
+
() => { swapContextMap(m); finish(); },
|
|
674
|
+
(e) => { swapContextMap(m); finish(); throw e; },
|
|
675
|
+
);
|
|
661
676
|
}
|
|
662
677
|
finish();
|
|
663
678
|
}
|
|
@@ -691,7 +706,9 @@ function renderChildArray(
|
|
|
691
706
|
const r = renderNode(children[i], writer, isSvg);
|
|
692
707
|
if (r && typeof (r as any).then === "function") {
|
|
693
708
|
// One child went async – continue the rest asynchronously
|
|
709
|
+
const m = captureMap();
|
|
694
710
|
return (r as Promise<void>).then(() => {
|
|
711
|
+
swapContextMap(m);
|
|
695
712
|
popTreeContext(savedTree);
|
|
696
713
|
// Continue with remaining children
|
|
697
714
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
@@ -716,7 +733,9 @@ function renderChildArrayFrom(
|
|
|
716
733
|
const savedTree = pushTreeContext(totalChildren, i);
|
|
717
734
|
const r = renderNode(children[i], writer, isSvg);
|
|
718
735
|
if (r && typeof (r as any).then === "function") {
|
|
736
|
+
const m = captureMap();
|
|
719
737
|
return (r as Promise<void>).then(() => {
|
|
738
|
+
swapContextMap(m);
|
|
720
739
|
popTreeContext(savedTree);
|
|
721
740
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
722
741
|
});
|
|
@@ -765,7 +784,7 @@ async function renderSuspense(
|
|
|
765
784
|
const buffer = new BufferWriter();
|
|
766
785
|
const r = renderNode(children, buffer, isSvg);
|
|
767
786
|
if (r && typeof (r as any).then === "function") {
|
|
768
|
-
await r;
|
|
787
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
769
788
|
}
|
|
770
789
|
// Success – wrap with React's Suspense boundary markers so hydrateRoot
|
|
771
790
|
// can locate the boundary in the DOM (<!--$--> … <!--/$-->).
|
|
@@ -775,7 +794,7 @@ async function renderSuspense(
|
|
|
775
794
|
return;
|
|
776
795
|
} catch (error: unknown) {
|
|
777
796
|
if (error && typeof (error as any).then === "function") {
|
|
778
|
-
await (error as Promise<unknown>);
|
|
797
|
+
const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
|
|
779
798
|
attempts++;
|
|
780
799
|
} else {
|
|
781
800
|
throw error;
|
|
@@ -788,7 +807,9 @@ async function renderSuspense(
|
|
|
788
807
|
writer.write("<!--$?-->");
|
|
789
808
|
if (fallback) {
|
|
790
809
|
const r = renderNode(fallback, writer, isSvg);
|
|
791
|
-
if (r && typeof (r as any).then === "function")
|
|
810
|
+
if (r && typeof (r as any).then === "function") {
|
|
811
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
812
|
+
}
|
|
792
813
|
}
|
|
793
814
|
writer.write("<!--/$-->");
|
|
794
815
|
}
|
|
@@ -806,9 +827,11 @@ async function renderSuspense(
|
|
|
806
827
|
export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
807
828
|
const encoder = new TextEncoder();
|
|
808
829
|
|
|
809
|
-
|
|
830
|
+
const contextMap = new Map<object, unknown>();
|
|
831
|
+
return new ReadableStream({
|
|
810
832
|
async start(controller) {
|
|
811
833
|
resetRenderState();
|
|
834
|
+
const prev = swapContextMap(contextMap);
|
|
812
835
|
|
|
813
836
|
const writer: Writer = {
|
|
814
837
|
lastWasText: false,
|
|
@@ -824,13 +847,17 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
824
847
|
|
|
825
848
|
try {
|
|
826
849
|
const r = renderNode(element, writer);
|
|
827
|
-
if (r && typeof (r as any).then === "function")
|
|
850
|
+
if (r && typeof (r as any).then === "function") {
|
|
851
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
852
|
+
}
|
|
828
853
|
controller.close();
|
|
829
854
|
} catch (error) {
|
|
830
855
|
controller.error(error);
|
|
856
|
+
} finally {
|
|
857
|
+
swapContextMap(prev);
|
|
831
858
|
}
|
|
832
859
|
},
|
|
833
|
-
})
|
|
860
|
+
});
|
|
834
861
|
}
|
|
835
862
|
|
|
836
863
|
/**
|
|
@@ -838,10 +865,13 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
838
865
|
* Retries the full tree when a component throws a Promise (Suspense protocol),
|
|
839
866
|
* so useServerData and similar hooks work without requiring explicit <Suspense>.
|
|
840
867
|
*/
|
|
841
|
-
export function renderToString(element: SlimNode): Promise<string> {
|
|
842
|
-
|
|
868
|
+
export async function renderToString(element: SlimNode): Promise<string> {
|
|
869
|
+
const contextMap = new Map<object, unknown>();
|
|
870
|
+
const prev = swapContextMap(contextMap);
|
|
871
|
+
try {
|
|
843
872
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
844
873
|
resetRenderState();
|
|
874
|
+
swapContextMap(contextMap); // re-activate our map on each retry
|
|
845
875
|
const chunks: string[] = [];
|
|
846
876
|
const writer: Writer = {
|
|
847
877
|
lastWasText: false,
|
|
@@ -850,18 +880,22 @@ export function renderToString(element: SlimNode): Promise<string> {
|
|
|
850
880
|
};
|
|
851
881
|
try {
|
|
852
882
|
const r = renderNode(element, writer);
|
|
853
|
-
if (r && typeof (r as any).then === "function")
|
|
883
|
+
if (r && typeof (r as any).then === "function") {
|
|
884
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
885
|
+
}
|
|
854
886
|
return chunks.join("");
|
|
855
887
|
} catch (error) {
|
|
856
888
|
if (error && typeof (error as any).then === "function") {
|
|
857
|
-
await (error as Promise<unknown>);
|
|
889
|
+
const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
|
|
858
890
|
continue;
|
|
859
891
|
}
|
|
860
892
|
throw error;
|
|
861
893
|
}
|
|
862
894
|
}
|
|
863
895
|
throw new Error("[slim-react] renderToString exceeded maximum retries");
|
|
864
|
-
}
|
|
896
|
+
} finally {
|
|
897
|
+
swapContextMap(prev);
|
|
898
|
+
}
|
|
865
899
|
}
|
|
866
900
|
|
|
867
901
|
/** Alias matching React 18+ server API naming. */
|
|
@@ -5,66 +5,62 @@
|
|
|
5
5
|
* multiple slim-react instances (the render worker's direct import and the
|
|
6
6
|
* SSR bundle's bundled copy) share the same singletons without coordination.
|
|
7
7
|
*
|
|
8
|
-
* Context values are stored in
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Context values are stored in a plain Map that is created per render call.
|
|
9
|
+
* Node.js is single-threaded: only one render is executing between any two
|
|
10
|
+
* await points. The renderer captures the map reference before every await
|
|
11
|
+
* and restores it in the continuation, so concurrent renders stay isolated
|
|
12
|
+
* without any external dependencies.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// cannot resolve node:async_hooks (e.g. rspack without target:node set) do
|
|
17
|
-
// not fail at build time — the SSR render process always runs in Node.js and
|
|
18
|
-
// will find the module at runtime regardless.
|
|
19
|
-
const CONTEXT_STORE_KEY = "__slimReactContextStore";
|
|
15
|
+
// The context map for the render that is currently executing (between awaits).
|
|
16
|
+
// Kept on globalThis so both slim-react instances share the same slot.
|
|
17
|
+
const MAP_KEY = "__slimReactContextMap";
|
|
20
18
|
const _g = globalThis as any;
|
|
21
|
-
if (!_g[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
if (!("__slimReactContextMap" in _g)) _g[MAP_KEY] = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Swap in a new context map and return the previous one.
|
|
23
|
+
* Called at render entry-points and at every await/then continuation to
|
|
24
|
+
* restore the correct map for the resuming render.
|
|
25
|
+
*/
|
|
26
|
+
export function swapContextMap(
|
|
27
|
+
map: Map<object, unknown> | null,
|
|
28
|
+
): Map<object, unknown> | null {
|
|
29
|
+
const prev: Map<object, unknown> | null = _g[MAP_KEY];
|
|
30
|
+
_g[MAP_KEY] = map;
|
|
31
|
+
return prev;
|
|
31
32
|
}
|
|
32
|
-
const _contextStore: { run<T>(store: Map<object,unknown>, fn: () => T): T; getStore(): Map<object,unknown> | undefined } | null = _g[CONTEXT_STORE_KEY];
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
export function
|
|
36
|
-
return
|
|
34
|
+
/** Return the active map without changing it (used to capture before an await). */
|
|
35
|
+
export function captureMap(): Map<object, unknown> | null {
|
|
36
|
+
return _g[MAP_KEY];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Read the current value for a context within the active render.
|
|
41
|
-
* Falls back to `_defaultValue` (or `_currentValue` for external contexts).
|
|
42
|
-
*/
|
|
39
|
+
/** Read the current value for a context within the active render. */
|
|
43
40
|
export function getContextValue<T>(context: object): T {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
41
|
+
const map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
42
|
+
if (map && map.has(context)) return map.get(context) as T;
|
|
46
43
|
const c = context as any;
|
|
47
44
|
return ("_defaultValue" in c ? c._defaultValue : c._currentValue) as T;
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
/**
|
|
51
|
-
* Push a new value
|
|
52
|
-
* Returns the previous value so the caller can restore it
|
|
48
|
+
* Push a new Provider value into the active map.
|
|
49
|
+
* Returns the previous value so the caller can restore it on exit.
|
|
53
50
|
*/
|
|
54
51
|
export function pushContextValue(context: object, value: unknown): unknown {
|
|
55
|
-
const
|
|
52
|
+
const map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
56
53
|
const c = context as any;
|
|
57
|
-
const prev =
|
|
58
|
-
?
|
|
54
|
+
const prev = map && map.has(context)
|
|
55
|
+
? map.get(context)
|
|
59
56
|
: ("_defaultValue" in c ? c._defaultValue : c._currentValue);
|
|
60
|
-
|
|
57
|
+
map?.set(context, value);
|
|
61
58
|
return prev;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
/** Restore a previously saved context value (called by Provider on exit). */
|
|
65
62
|
export function popContextValue(context: object, prev: unknown): void {
|
|
66
|
-
|
|
67
|
-
if (store) store.set(context, prev);
|
|
63
|
+
(_g[MAP_KEY] as Map<object, unknown> | null)?.set(context, prev);
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
export interface TreeContext {
|
package/src/utils/rspack.ts
CHANGED
|
@@ -218,25 +218,18 @@ const buildCompilerConfig = (
|
|
|
218
218
|
react: slimReactIndex,
|
|
219
219
|
'react/jsx-runtime': slimReactJsx,
|
|
220
220
|
'react/jsx-dev-runtime': slimReactJsx,
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
'@emotion/styled': path.resolve(process.cwd(), 'node_modules', '@emotion', 'styled'),
|
|
221
|
+
// @emotion/* is bundled (not external) so that its `react` imports are
|
|
222
|
+
// resolved through the alias above to slim-react. If left external,
|
|
223
|
+
// emotion loads real React from node_modules and calls
|
|
224
|
+
// ReactSharedInternals.H.useContext which requires React's dispatcher.
|
|
226
225
|
} : undefined;
|
|
227
226
|
|
|
228
227
|
const externals = isServerBuild ? [
|
|
229
228
|
// Node.js built-ins — must not be bundled; resolved by the runtime.
|
|
230
|
-
// Both the bare name and the node: prefix are listed because rspack
|
|
231
|
-
// may encounter either form depending on how the import is written.
|
|
232
|
-
'node:async_hooks', 'async_hooks',
|
|
233
229
|
'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
'@emotion/react',
|
|
230
|
+
// @emotion/server is only used outside component rendering (CSS extraction)
|
|
231
|
+
// and does not call React hooks, so it is safe to leave as external.
|
|
237
232
|
'@emotion/server',
|
|
238
|
-
'@emotion/cache',
|
|
239
|
-
'@emotion/styled',
|
|
240
233
|
] : undefined;
|
|
241
234
|
|
|
242
235
|
const extraPlugins: any[] = [];
|
|
@@ -256,6 +249,9 @@ const buildCompilerConfig = (
|
|
|
256
249
|
alias: resolveAliases,
|
|
257
250
|
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
258
251
|
mainFields: isServerBuild ? ['main', 'module'] : ['browser', 'module', 'main'],
|
|
252
|
+
// for server builds exclude the "browser" condition so packages with package.json
|
|
253
|
+
// "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
|
|
254
|
+
...(isServerBuild ? { conditionNames: ['node', 'require', 'default'] } : {}),
|
|
259
255
|
};
|
|
260
256
|
|
|
261
257
|
// Production client builds get vendor splitting and deterministic module IDs.
|
package/dist/chunk-EZUCZHGV.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw new Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
__require
|
|
11
|
-
};
|