hadars 0.1.22 → 0.1.24
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/chunk-EZUCZHGV.js +11 -0
- package/dist/cli.js +114 -55
- package/dist/index.js +2 -0
- package/dist/slim-react/index.cjs +89 -41
- package/dist/slim-react/index.d.ts +1 -0
- package/dist/slim-react/index.js +92 -41
- package/dist/slim-react/jsx-runtime.js +1 -0
- package/dist/ssr-render-worker.js +91 -36
- package/dist/ssr-watch.js +24 -19
- package/package.json +1 -1
- package/src/slim-react/context.ts +3 -1
- package/src/slim-react/hooks.ts +3 -3
- package/src/slim-react/index.ts +2 -4
- package/src/slim-react/render.ts +37 -32
- package/src/slim-react/renderContext.ts +63 -4
- package/src/utils/rspack.ts +13 -19
package/src/slim-react/render.ts
CHANGED
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
popComponentScope,
|
|
31
31
|
snapshotContext,
|
|
32
32
|
restoreContext,
|
|
33
|
+
pushContextValue,
|
|
34
|
+
popContextValue,
|
|
35
|
+
getContextValue,
|
|
36
|
+
runWithContextStore,
|
|
33
37
|
} from "./renderContext";
|
|
34
38
|
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
@@ -566,7 +570,7 @@ function renderComponent(
|
|
|
566
570
|
// React.Consumer (React 19) — call the children render prop with the current value
|
|
567
571
|
if (typeOf === REACT_CONSUMER) {
|
|
568
572
|
const ctx = (type as any)._context;
|
|
569
|
-
const value = ctx
|
|
573
|
+
const value = ctx ? getContextValue(ctx) : undefined;
|
|
570
574
|
const result: SlimNode =
|
|
571
575
|
typeof props.children === "function" ? props.children(value) : null;
|
|
572
576
|
const savedScope = pushComponentScope();
|
|
@@ -594,8 +598,7 @@ function renderComponent(
|
|
|
594
598
|
if (isProvider) {
|
|
595
599
|
// Resolve the actual context object from any provider variant
|
|
596
600
|
ctx = (type as any)._context ?? type;
|
|
597
|
-
prevCtxValue = ctx.
|
|
598
|
-
ctx._currentValue = props.value;
|
|
601
|
+
prevCtxValue = pushContextValue(ctx, props.value);
|
|
599
602
|
}
|
|
600
603
|
|
|
601
604
|
// Each component gets a fresh local-ID counter (for multiple useId calls).
|
|
@@ -606,11 +609,11 @@ function renderComponent(
|
|
|
606
609
|
if (isProvider && typeof type !== "function") {
|
|
607
610
|
const finish = () => {
|
|
608
611
|
popComponentScope(savedScope);
|
|
609
|
-
ctx
|
|
612
|
+
popContextValue(ctx, prevCtxValue);
|
|
610
613
|
};
|
|
611
614
|
const r = renderChildren(props.children, writer, isSvg);
|
|
612
615
|
if (r && typeof (r as any).then === "function") {
|
|
613
|
-
return (r as Promise<void>).then(finish);
|
|
616
|
+
return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
|
|
614
617
|
}
|
|
615
618
|
finish();
|
|
616
619
|
return;
|
|
@@ -631,13 +634,13 @@ function renderComponent(
|
|
|
631
634
|
}
|
|
632
635
|
} catch (e) {
|
|
633
636
|
popComponentScope(savedScope);
|
|
634
|
-
if (isProvider) ctx
|
|
637
|
+
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
635
638
|
throw e;
|
|
636
639
|
}
|
|
637
640
|
|
|
638
641
|
const finish = () => {
|
|
639
642
|
popComponentScope(savedScope);
|
|
640
|
-
if (isProvider) ctx
|
|
643
|
+
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
641
644
|
};
|
|
642
645
|
|
|
643
646
|
// Async component
|
|
@@ -645,16 +648,16 @@ function renderComponent(
|
|
|
645
648
|
return result.then((resolved) => {
|
|
646
649
|
const r = renderNode(resolved, writer, isSvg);
|
|
647
650
|
if (r && typeof (r as any).then === "function") {
|
|
648
|
-
return (r as Promise<void>).then(finish);
|
|
651
|
+
return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
|
|
649
652
|
}
|
|
650
653
|
finish();
|
|
651
|
-
});
|
|
654
|
+
}, (e) => { finish(); throw e; });
|
|
652
655
|
}
|
|
653
656
|
|
|
654
657
|
const r = renderNode(result, writer, isSvg);
|
|
655
658
|
|
|
656
659
|
if (r && typeof (r as any).then === "function") {
|
|
657
|
-
return (r as Promise<void>).then(finish);
|
|
660
|
+
return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
|
|
658
661
|
}
|
|
659
662
|
finish();
|
|
660
663
|
}
|
|
@@ -803,7 +806,7 @@ async function renderSuspense(
|
|
|
803
806
|
export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
804
807
|
const encoder = new TextEncoder();
|
|
805
808
|
|
|
806
|
-
return new ReadableStream({
|
|
809
|
+
return runWithContextStore(() => new ReadableStream({
|
|
807
810
|
async start(controller) {
|
|
808
811
|
resetRenderState();
|
|
809
812
|
|
|
@@ -827,7 +830,7 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
827
830
|
controller.error(error);
|
|
828
831
|
}
|
|
829
832
|
},
|
|
830
|
-
});
|
|
833
|
+
}));
|
|
831
834
|
}
|
|
832
835
|
|
|
833
836
|
/**
|
|
@@ -835,28 +838,30 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
835
838
|
* Retries the full tree when a component throws a Promise (Suspense protocol),
|
|
836
839
|
* so useServerData and similar hooks work without requiring explicit <Suspense>.
|
|
837
840
|
*/
|
|
838
|
-
export
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
841
|
+
export function renderToString(element: SlimNode): Promise<string> {
|
|
842
|
+
return runWithContextStore(async () => {
|
|
843
|
+
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
844
|
+
resetRenderState();
|
|
845
|
+
const chunks: string[] = [];
|
|
846
|
+
const writer: Writer = {
|
|
847
|
+
lastWasText: false,
|
|
848
|
+
write(c) { chunks.push(c); this.lastWasText = false; },
|
|
849
|
+
text(s) { chunks.push(s); this.lastWasText = true; },
|
|
850
|
+
};
|
|
851
|
+
try {
|
|
852
|
+
const r = renderNode(element, writer);
|
|
853
|
+
if (r && typeof (r as any).then === "function") await r;
|
|
854
|
+
return chunks.join("");
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (error && typeof (error as any).then === "function") {
|
|
857
|
+
await (error as Promise<unknown>);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
throw error;
|
|
855
861
|
}
|
|
856
|
-
throw error;
|
|
857
862
|
}
|
|
858
|
-
|
|
859
|
-
|
|
863
|
+
throw new Error("[slim-react] renderToString exceeded maximum retries");
|
|
864
|
+
});
|
|
860
865
|
}
|
|
861
866
|
|
|
862
867
|
/** Alias matching React 18+ server API naming. */
|
|
@@ -1,12 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render-time context for tree-position-based `useId
|
|
2
|
+
* Render-time context for tree-position-based `useId` and React Context values.
|
|
3
3
|
*
|
|
4
4
|
* State lives on `globalThis` rather than module-level variables so that
|
|
5
5
|
* multiple slim-react instances (the render worker's direct import and the
|
|
6
|
-
* SSR bundle's bundled copy) share the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* SSR bundle's bundled copy) share the same singletons without coordination.
|
|
7
|
+
*
|
|
8
|
+
* Context values are stored in an AsyncLocalStorage<Map> so each concurrent
|
|
9
|
+
* SSR request gets its own isolated scope that propagates through all awaits.
|
|
10
|
+
* Call `runWithContextStore` at the start of every render to establish the scope.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Shared AsyncLocalStorage instance — kept on globalThis so both copies of
|
|
14
|
+
// slim-react (direct import + SSR bundle) use the same store.
|
|
15
|
+
// The import is done with require() inside a try/catch so that bundlers that
|
|
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";
|
|
20
|
+
const _g = globalThis as any;
|
|
21
|
+
if (!_g[CONTEXT_STORE_KEY]) {
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const { AsyncLocalStorage } = require("node:async_hooks") as typeof import("node:async_hooks");
|
|
25
|
+
_g[CONTEXT_STORE_KEY] = new AsyncLocalStorage<Map<object, unknown>>();
|
|
26
|
+
} catch {
|
|
27
|
+
// Fallback: no-op store — context values fall back to _defaultValue.
|
|
28
|
+
// This should never happen in a real SSR environment.
|
|
29
|
+
_g[CONTEXT_STORE_KEY] = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const _contextStore: { run<T>(store: Map<object,unknown>, fn: () => T): T; getStore(): Map<object,unknown> | undefined } | null = _g[CONTEXT_STORE_KEY];
|
|
33
|
+
|
|
34
|
+
/** Wrap a render entry-point so it gets its own isolated context-value scope. */
|
|
35
|
+
export function runWithContextStore<T>(fn: () => T): T {
|
|
36
|
+
return _contextStore ? _contextStore.run(new Map(), fn) : fn();
|
|
37
|
+
}
|
|
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
|
+
*/
|
|
43
|
+
export function getContextValue<T>(context: object): T {
|
|
44
|
+
const store = _contextStore?.getStore();
|
|
45
|
+
if (store && store.has(context)) return store.get(context) as T;
|
|
46
|
+
const c = context as any;
|
|
47
|
+
return ("_defaultValue" in c ? c._defaultValue : c._currentValue) as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Push a new value for a context Provider onto the per-request store.
|
|
52
|
+
* Returns the previous value so the caller can restore it later.
|
|
9
53
|
*/
|
|
54
|
+
export function pushContextValue(context: object, value: unknown): unknown {
|
|
55
|
+
const store = _contextStore?.getStore();
|
|
56
|
+
const c = context as any;
|
|
57
|
+
const prev = store && store.has(context)
|
|
58
|
+
? store.get(context)
|
|
59
|
+
: ("_defaultValue" in c ? c._defaultValue : c._currentValue);
|
|
60
|
+
if (store) store.set(context, value);
|
|
61
|
+
return prev;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Restore a previously saved context value (called by Provider on exit). */
|
|
65
|
+
export function popContextValue(context: object, prev: unknown): void {
|
|
66
|
+
const store = _contextStore?.getStore();
|
|
67
|
+
if (store) store.set(context, prev);
|
|
68
|
+
}
|
|
10
69
|
|
|
11
70
|
export interface TreeContext {
|
|
12
71
|
id: number;
|
package/src/utils/rspack.ts
CHANGED
|
@@ -18,7 +18,7 @@ const loaderPath = existsSync(pathMod.resolve(packageDir, 'loader.cjs'))
|
|
|
18
18
|
? pathMod.resolve(packageDir, 'loader.cjs')
|
|
19
19
|
: pathMod.resolve(packageDir, 'loader.ts');
|
|
20
20
|
|
|
21
|
-
const getConfigBase = (mode: "development" | "production"): Omit<Configuration, "entry" | "output" | "plugins"> => {
|
|
21
|
+
const getConfigBase = (mode: "development" | "production", isServerBuild = false): Omit<Configuration, "entry" | "output" | "plugins"> => {
|
|
22
22
|
const isDev = mode === 'development';
|
|
23
23
|
return {
|
|
24
24
|
experiments: {
|
|
@@ -141,9 +141,12 @@ const buildCompilerConfig = (
|
|
|
141
141
|
opts: EntryOptions,
|
|
142
142
|
includeHotPlugin: boolean,
|
|
143
143
|
): Configuration => {
|
|
144
|
-
const Config = getConfigBase(opts.mode);
|
|
145
144
|
const { base } = opts;
|
|
146
145
|
const isDev = opts.mode === 'development';
|
|
146
|
+
const isServerBuild = Boolean(
|
|
147
|
+
(opts.output && typeof opts.output === 'object' && (opts.output.library || String(opts.output.filename || '').includes('ssr')))
|
|
148
|
+
);
|
|
149
|
+
const Config = getConfigBase(opts.mode, isServerBuild);
|
|
147
150
|
|
|
148
151
|
// shallow-clone base config to avoid mutating shared Config while preserving RegExp and plugin instances
|
|
149
152
|
const localConfig: any = {
|
|
@@ -203,15 +206,6 @@ const buildCompilerConfig = (
|
|
|
203
206
|
localConfig.module.rules.push(...opts.moduleRules);
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
// For server (SSR) builds we should avoid bundling react/react-dom so
|
|
207
|
-
// the runtime uses the same React instance as the host. If the output
|
|
208
|
-
// is a library/module (i.e. `opts.output.library` present or filename
|
|
209
|
-
// contains "ssr"), treat it as a server build and mark react/react-dom
|
|
210
|
-
// as externals and alias React imports to the project's node_modules.
|
|
211
|
-
const isServerBuild = Boolean(
|
|
212
|
-
(opts.output && typeof opts.output === 'object' && (opts.output.library || String(opts.output.filename || '').includes('ssr')))
|
|
213
|
-
);
|
|
214
|
-
|
|
215
209
|
// slim-react: the SSR-only React-compatible renderer bundled with hadars.
|
|
216
210
|
// On server builds we replace the real React with slim-react so that hooks
|
|
217
211
|
// get safe SSR stubs, context works, and renderToStream / Suspense are
|
|
@@ -232,6 +226,11 @@ const buildCompilerConfig = (
|
|
|
232
226
|
} : undefined;
|
|
233
227
|
|
|
234
228
|
const externals = isServerBuild ? [
|
|
229
|
+
// 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
|
+
'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
|
|
235
234
|
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
236
235
|
// emotion should be external on server builds to avoid client/browser code
|
|
237
236
|
'@emotion/react',
|
|
@@ -291,7 +290,7 @@ const buildCompilerConfig = (
|
|
|
291
290
|
externals,
|
|
292
291
|
...(optimization !== undefined ? { optimization } : {}),
|
|
293
292
|
plugins: [
|
|
294
|
-
new rspack.HtmlRspackPlugin({
|
|
293
|
+
!isServerBuild && new rspack.HtmlRspackPlugin({
|
|
295
294
|
publicPath: base || '/',
|
|
296
295
|
template: opts.htmlTemplate
|
|
297
296
|
? pathMod.resolve(process.cwd(), opts.htmlTemplate)
|
|
@@ -301,12 +300,7 @@ const buildCompilerConfig = (
|
|
|
301
300
|
inject: 'head',
|
|
302
301
|
minify: opts.mode === 'production',
|
|
303
302
|
}),
|
|
304
|
-
|
|
305
|
-
// as soon as HTML is parsed — without waiting for the bundle to execute.
|
|
306
|
-
// `<script type="module" async>` is valid: it downloads in parallel and
|
|
307
|
-
// executes without blocking DOMContentLoaded, while retaining module
|
|
308
|
-
// semantics (strict mode, ES imports, etc.).
|
|
309
|
-
{
|
|
303
|
+
!isServerBuild && {
|
|
310
304
|
apply(compiler: any) {
|
|
311
305
|
compiler.hooks.emit.tapAsync('HadarsAsyncModuleScript', (compilation: any, cb: () => void) => {
|
|
312
306
|
const asset = compilation.assets['out.html'];
|
|
@@ -327,7 +321,7 @@ const buildCompilerConfig = (
|
|
|
327
321
|
},
|
|
328
322
|
},
|
|
329
323
|
isDev && !isServerBuild && new ReactRefreshPlugin(),
|
|
330
|
-
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
324
|
+
includeHotPlugin && isDev && !isServerBuild && new rspack.HotModuleReplacementPlugin(),
|
|
331
325
|
...extraPlugins,
|
|
332
326
|
],
|
|
333
327
|
...localConfig,
|