hadars 0.3.0 → 0.3.2
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/README.md +16 -14
- package/dist/{chunk-H72BZXOA.js → chunk-2J2L2H3H.js} +21 -11
- package/dist/{chunk-LY5MTHFV.js → chunk-TV37IMRB.js} +50 -51
- package/dist/cli.js +83 -89
- package/dist/cloudflare.cjs +68 -59
- package/dist/cloudflare.js +2 -2
- package/dist/lambda.cjs +70 -67
- package/dist/lambda.js +4 -10
- package/dist/slim-react/index.cjs +50 -51
- package/dist/slim-react/index.js +1 -1
- package/dist/ssr-render-worker.js +46 -47
- package/dist/ssr-watch.js +9 -6
- package/package.json +2 -2
- package/src/build.ts +9 -5
- package/src/lambda.ts +7 -14
- package/src/slim-react/render.ts +39 -25
- package/src/slim-react/renderContext.ts +34 -11
- package/src/utils/rspack.ts +9 -6
- package/src/utils/serve.ts +1 -28
- package/src/utils/ssrHandler.ts +35 -10
|
@@ -134,7 +134,16 @@ function componentCalledUseId() {
|
|
|
134
134
|
function snapshotContext() {
|
|
135
135
|
const st = s();
|
|
136
136
|
const ctx = st.currentTreeContext;
|
|
137
|
-
|
|
137
|
+
const depth = _treeDepth;
|
|
138
|
+
return {
|
|
139
|
+
tree: { id: ctx.id, overflow: ctx.overflow },
|
|
140
|
+
localId: st.localIdCounter,
|
|
141
|
+
treeDepth: depth,
|
|
142
|
+
// Snapshot the live stack so that popTreeContext reads correct saved values
|
|
143
|
+
// even if another concurrent render's resetRenderState stomped the arrays.
|
|
144
|
+
idStack: _treeIdStack.slice(0, depth),
|
|
145
|
+
ovStack: _treeOvStack.slice(0, depth)
|
|
146
|
+
};
|
|
138
147
|
}
|
|
139
148
|
function restoreContext(snap) {
|
|
140
149
|
const st = s();
|
|
@@ -143,6 +152,10 @@ function restoreContext(snap) {
|
|
|
143
152
|
ctx.overflow = snap.tree.overflow;
|
|
144
153
|
st.localIdCounter = snap.localId;
|
|
145
154
|
_treeDepth = snap.treeDepth;
|
|
155
|
+
for (let i = 0; i < snap.treeDepth; i++) {
|
|
156
|
+
_treeIdStack[i] = snap.idStack[i];
|
|
157
|
+
_treeOvStack[i] = snap.ovStack[i];
|
|
158
|
+
}
|
|
146
159
|
}
|
|
147
160
|
function getTreeId() {
|
|
148
161
|
const { id, overflow } = s().currentTreeContext;
|
|
@@ -256,6 +269,14 @@ function restoreDispatcher(prev) {
|
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
// src/slim-react/render.ts
|
|
272
|
+
function captureRenderCtx() {
|
|
273
|
+
return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
|
|
274
|
+
}
|
|
275
|
+
function restoreRenderCtx(ctx) {
|
|
276
|
+
swapContextMap(ctx.m);
|
|
277
|
+
restoreUnsuspend(ctx.u);
|
|
278
|
+
restoreContext(ctx.t);
|
|
279
|
+
}
|
|
259
280
|
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
260
281
|
"area",
|
|
261
282
|
"base",
|
|
@@ -699,11 +720,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
699
720
|
if (e && typeof e.then === "function") {
|
|
700
721
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
701
722
|
patchPromiseStatus(e);
|
|
702
|
-
const
|
|
703
|
-
const u = captureUnsuspend();
|
|
723
|
+
const rctx = captureRenderCtx();
|
|
704
724
|
return e.then(() => {
|
|
705
|
-
|
|
706
|
-
restoreUnsuspend(u);
|
|
725
|
+
restoreRenderCtx(rctx);
|
|
707
726
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
708
727
|
});
|
|
709
728
|
}
|
|
@@ -740,17 +759,14 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
740
759
|
};
|
|
741
760
|
const r2 = renderChildren(props.children, writer, isSvg);
|
|
742
761
|
if (r2 && typeof r2.then === "function") {
|
|
743
|
-
const
|
|
744
|
-
const u = captureUnsuspend();
|
|
762
|
+
const rctx = captureRenderCtx();
|
|
745
763
|
return r2.then(
|
|
746
764
|
() => {
|
|
747
|
-
|
|
748
|
-
restoreUnsuspend(u);
|
|
765
|
+
restoreRenderCtx(rctx);
|
|
749
766
|
finish();
|
|
750
767
|
},
|
|
751
768
|
(e) => {
|
|
752
|
-
|
|
753
|
-
restoreUnsuspend(u);
|
|
769
|
+
restoreRenderCtx(rctx);
|
|
754
770
|
finish();
|
|
755
771
|
throw e;
|
|
756
772
|
}
|
|
@@ -779,11 +795,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
779
795
|
if (e && typeof e.then === "function") {
|
|
780
796
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
781
797
|
patchPromiseStatus(e);
|
|
782
|
-
const
|
|
783
|
-
const u = captureUnsuspend();
|
|
798
|
+
const rctx = captureRenderCtx();
|
|
784
799
|
return e.then(() => {
|
|
785
|
-
|
|
786
|
-
restoreUnsuspend(u);
|
|
800
|
+
restoreRenderCtx(rctx);
|
|
787
801
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
788
802
|
});
|
|
789
803
|
}
|
|
@@ -795,30 +809,25 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
795
809
|
savedIdTree = pushTreeContext(1, 0);
|
|
796
810
|
}
|
|
797
811
|
if (result instanceof Promise) {
|
|
798
|
-
const
|
|
799
|
-
const u = captureUnsuspend();
|
|
812
|
+
const rctx = captureRenderCtx();
|
|
800
813
|
return result.then((resolved) => {
|
|
801
|
-
|
|
802
|
-
restoreUnsuspend(u);
|
|
814
|
+
restoreRenderCtx(rctx);
|
|
803
815
|
let asyncSavedIdTree;
|
|
804
816
|
if (componentCalledUseId()) {
|
|
805
817
|
asyncSavedIdTree = pushTreeContext(1, 0);
|
|
806
818
|
}
|
|
807
819
|
const r2 = renderNode(resolved, writer, isSvg);
|
|
808
820
|
if (r2 && typeof r2.then === "function") {
|
|
809
|
-
const
|
|
810
|
-
const u2 = captureUnsuspend();
|
|
821
|
+
const rctx2 = captureRenderCtx();
|
|
811
822
|
return r2.then(
|
|
812
823
|
() => {
|
|
813
|
-
|
|
814
|
-
restoreUnsuspend(u2);
|
|
824
|
+
restoreRenderCtx(rctx2);
|
|
815
825
|
if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
|
|
816
826
|
popComponentScope(savedScope);
|
|
817
827
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
818
828
|
},
|
|
819
829
|
(e) => {
|
|
820
|
-
|
|
821
|
-
restoreUnsuspend(u2);
|
|
830
|
+
restoreRenderCtx(rctx2);
|
|
822
831
|
if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
|
|
823
832
|
popComponentScope(savedScope);
|
|
824
833
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -830,8 +839,7 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
830
839
|
popComponentScope(savedScope);
|
|
831
840
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
832
841
|
}, (e) => {
|
|
833
|
-
|
|
834
|
-
restoreUnsuspend(u);
|
|
842
|
+
restoreRenderCtx(rctx);
|
|
835
843
|
popComponentScope(savedScope);
|
|
836
844
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
837
845
|
throw e;
|
|
@@ -839,19 +847,16 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
839
847
|
}
|
|
840
848
|
const r = renderNode(result, writer, isSvg);
|
|
841
849
|
if (r && typeof r.then === "function") {
|
|
842
|
-
const
|
|
843
|
-
const u = captureUnsuspend();
|
|
850
|
+
const rctx = captureRenderCtx();
|
|
844
851
|
return r.then(
|
|
845
852
|
() => {
|
|
846
|
-
|
|
847
|
-
restoreUnsuspend(u);
|
|
853
|
+
restoreRenderCtx(rctx);
|
|
848
854
|
if (savedIdTree !== void 0) popTreeContext(savedIdTree);
|
|
849
855
|
popComponentScope(savedScope);
|
|
850
856
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
851
857
|
},
|
|
852
858
|
(e) => {
|
|
853
|
-
|
|
854
|
-
restoreUnsuspend(u);
|
|
859
|
+
restoreRenderCtx(rctx);
|
|
855
860
|
if (savedIdTree !== void 0) popTreeContext(savedIdTree);
|
|
856
861
|
popComponentScope(savedScope);
|
|
857
862
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -876,11 +881,9 @@ function renderChildArrayFrom(children, startIndex, writer, isSvg) {
|
|
|
876
881
|
const savedTree = pushTreeContext(totalChildren, i);
|
|
877
882
|
const r = renderNode(child, writer, isSvg);
|
|
878
883
|
if (r && typeof r.then === "function") {
|
|
879
|
-
const
|
|
880
|
-
const u = captureUnsuspend();
|
|
884
|
+
const rctx = captureRenderCtx();
|
|
881
885
|
return r.then(() => {
|
|
882
|
-
|
|
883
|
-
restoreUnsuspend(u);
|
|
886
|
+
restoreRenderCtx(rctx);
|
|
884
887
|
popTreeContext(savedTree);
|
|
885
888
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
886
889
|
});
|
|
@@ -904,11 +907,9 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
904
907
|
try {
|
|
905
908
|
const r = renderNode(children, buffer, isSvg);
|
|
906
909
|
if (r && typeof r.then === "function") {
|
|
907
|
-
const
|
|
908
|
-
const u = captureUnsuspend();
|
|
910
|
+
const rctx = captureRenderCtx();
|
|
909
911
|
await r;
|
|
910
|
-
|
|
911
|
-
restoreUnsuspend(u);
|
|
912
|
+
restoreRenderCtx(rctx);
|
|
912
913
|
}
|
|
913
914
|
writer.write("<!--$-->");
|
|
914
915
|
buffer.flushTo(writer);
|
|
@@ -922,11 +923,9 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
922
923
|
if (fallback) {
|
|
923
924
|
const r = renderNode(fallback, writer, isSvg);
|
|
924
925
|
if (r && typeof r.then === "function") {
|
|
925
|
-
const
|
|
926
|
-
const u = captureUnsuspend();
|
|
926
|
+
const rctx = captureRenderCtx();
|
|
927
927
|
await r;
|
|
928
|
-
|
|
929
|
-
restoreUnsuspend(u);
|
|
928
|
+
restoreRenderCtx(rctx);
|
|
930
929
|
}
|
|
931
930
|
}
|
|
932
931
|
writer.write("<!--/$-->");
|
|
@@ -955,9 +954,9 @@ async function renderToString(element, options) {
|
|
|
955
954
|
resetRenderState(idPrefix);
|
|
956
955
|
const r = renderNode(element, writer);
|
|
957
956
|
if (r && typeof r.then === "function") {
|
|
958
|
-
const
|
|
957
|
+
const rctx = captureRenderCtx();
|
|
959
958
|
await r;
|
|
960
|
-
|
|
959
|
+
restoreRenderCtx(rctx);
|
|
961
960
|
}
|
|
962
961
|
return output;
|
|
963
962
|
} finally {
|
package/dist/ssr-watch.js
CHANGED
|
@@ -265,6 +265,12 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
265
265
|
// changed files, making repeat dev starts significantly faster.
|
|
266
266
|
cache: true,
|
|
267
267
|
externals,
|
|
268
|
+
// externalsPresets.node externalises ALL Node.js built-ins (bare names
|
|
269
|
+
// and the node: prefix) for both static and dynamic imports. This
|
|
270
|
+
// complements the explicit `externals` array: the preset handles the
|
|
271
|
+
// node: URI scheme that rspack cannot resolve as a file, while the
|
|
272
|
+
// array keeps '@emotion/server' as an explicit external.
|
|
273
|
+
...isServerBuild ? { externalsPresets: { node: true } } : {},
|
|
268
274
|
...optimization !== void 0 ? { optimization } : {},
|
|
269
275
|
plugins: [
|
|
270
276
|
!isServerBuild && new rspack.HtmlRspackPlugin({
|
|
@@ -317,7 +323,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
317
323
|
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
318
324
|
// and vice versa, causing an infinite rebuild loop.
|
|
319
325
|
watchOptions: {
|
|
320
|
-
ignored: ["**/node_modules/**", "**/.hadars/**"]
|
|
326
|
+
ignored: ["**/node_modules/**", "**/.hadars/**", "/tmp/**"]
|
|
321
327
|
}
|
|
322
328
|
};
|
|
323
329
|
};
|
|
@@ -326,7 +332,7 @@ var compileEntry = async (entry2, opts) => {
|
|
|
326
332
|
if (opts.watch) {
|
|
327
333
|
await new Promise((resolve, reject) => {
|
|
328
334
|
let first = true;
|
|
329
|
-
compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**"] }, (err, stats) => {
|
|
335
|
+
compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**", "/tmp/**"] }, (err, stats) => {
|
|
330
336
|
if (err) {
|
|
331
337
|
if (first) {
|
|
332
338
|
first = false;
|
|
@@ -359,10 +365,7 @@ var compileEntry = async (entry2, opts) => {
|
|
|
359
365
|
}
|
|
360
366
|
console.log(stats?.toString({
|
|
361
367
|
colors: true,
|
|
362
|
-
|
|
363
|
-
children: true,
|
|
364
|
-
chunks: true,
|
|
365
|
-
chunkModules: true
|
|
368
|
+
preset: "minimal"
|
|
366
369
|
}));
|
|
367
370
|
resolve(stats);
|
|
368
371
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build:lib": "tsup src/index.tsx src/lambda.ts src/cloudflare.ts src/slim-react/index.ts src/slim-react/jsx-runtime.ts --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
|
|
43
43
|
"build:cli": "node build-scripts/build-cli.mjs",
|
|
44
|
-
"build:all": "
|
|
44
|
+
"build:all": "node build-scripts/build-all.mjs",
|
|
45
45
|
"test": "bun test test/render-compare.test.tsx && bun test test/ssr.test.ts",
|
|
46
46
|
"prepare": "npm run build:all",
|
|
47
47
|
"prepublishOnly": "npm run build:all"
|
package/src/build.ts
CHANGED
|
@@ -701,6 +701,14 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
701
701
|
fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8')
|
|
702
702
|
);
|
|
703
703
|
|
|
704
|
+
// Hoist and pre-import the SSR module at startup so the first request does
|
|
705
|
+
// not pay the module parse/eval cost. The file: URL is stable for the life
|
|
706
|
+
// of the process (no cache-busting needed in run mode).
|
|
707
|
+
const componentPath = pathToFileURL(
|
|
708
|
+
pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME)
|
|
709
|
+
).href;
|
|
710
|
+
const ssrModulePromise = import(componentPath) as Promise<HadarsEntryModule<any>>;
|
|
711
|
+
|
|
704
712
|
const runHandler: CacheFetchHandler = async (req, ctx) => {
|
|
705
713
|
const request = parseRequest(req);
|
|
706
714
|
if (handler) {
|
|
@@ -733,16 +741,12 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
733
741
|
if (routeRes) return routeRes;
|
|
734
742
|
}
|
|
735
743
|
|
|
736
|
-
const componentPath = pathToFileURL(
|
|
737
|
-
pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME)
|
|
738
|
-
).href;
|
|
739
|
-
|
|
740
744
|
try {
|
|
741
745
|
const {
|
|
742
746
|
default: Component,
|
|
743
747
|
getInitProps,
|
|
744
748
|
getFinalProps,
|
|
745
|
-
} =
|
|
749
|
+
} = await ssrModulePromise;
|
|
746
750
|
|
|
747
751
|
if (renderPool && request.headers.get('Accept') !== 'application/json') {
|
|
748
752
|
// Worker runs the full lifecycle — no non-serializable objects cross the thread boundary.
|
package/src/lambda.ts
CHANGED
|
@@ -193,20 +193,13 @@ export function createLambdaHandler(options: HadarsOptions, bundled?: LambdaBund
|
|
|
193
193
|
? makePrecontentHtmlGetter(Promise.resolve(bundled.outHtml))
|
|
194
194
|
: makePrecontentHtmlGetter(fs.readFile(pathMod.join(cwd, StaticPath, 'out.html'), 'utf-8'));
|
|
195
195
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (!ssrModulePromise) {
|
|
204
|
-
ssrModulePromise = import(
|
|
205
|
-
pathToFileURL(pathMod.resolve(cwd, HadarsFolder, SSR_FILENAME)).href
|
|
206
|
-
) as Promise<HadarsEntryModule<any>>;
|
|
207
|
-
}
|
|
208
|
-
return ssrModulePromise;
|
|
209
|
-
};
|
|
196
|
+
// Start loading the SSR module immediately — during Lambda's init phase this
|
|
197
|
+
// is "free" time not billed against request latency. Lazy loading would
|
|
198
|
+
// push the module parse cost onto the first request.
|
|
199
|
+
const ssrModulePromise: Promise<HadarsEntryModule<any>> = bundled
|
|
200
|
+
? Promise.resolve(bundled.ssrModule)
|
|
201
|
+
: import(pathToFileURL(pathMod.resolve(cwd, HadarsFolder, SSR_FILENAME)).href) as Promise<HadarsEntryModule<any>>;
|
|
202
|
+
const getSsrModule = () => ssrModulePromise;
|
|
210
203
|
|
|
211
204
|
const runHandler = async (req: Request): Promise<Response> => {
|
|
212
205
|
const request = parseRequest(req);
|
package/src/slim-react/render.ts
CHANGED
|
@@ -38,7 +38,21 @@ import {
|
|
|
38
38
|
captureMap,
|
|
39
39
|
captureUnsuspend,
|
|
40
40
|
restoreUnsuspend,
|
|
41
|
+
type ContextSnapshot,
|
|
41
42
|
} from "./renderContext";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Capture all three concurrent-render globals in one call.
|
|
46
|
+
* Must be called immediately before every `await` and the returned token
|
|
47
|
+
* passed to restoreRenderCtx immediately after resuming — just like the
|
|
48
|
+
* individual captureMap / captureUnsuspend calls they replace.
|
|
49
|
+
*/
|
|
50
|
+
function captureRenderCtx(): { m: ReturnType<typeof captureMap>; u: unknown; t: ContextSnapshot } {
|
|
51
|
+
return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
|
|
52
|
+
}
|
|
53
|
+
function restoreRenderCtx(ctx: ReturnType<typeof captureRenderCtx>): void {
|
|
54
|
+
swapContextMap(ctx.m); restoreUnsuspend(ctx.u); restoreContext(ctx.t);
|
|
55
|
+
}
|
|
42
56
|
import { installDispatcher, restoreDispatcher } from "./dispatcher";
|
|
43
57
|
|
|
44
58
|
// ---------------------------------------------------------------------------
|
|
@@ -627,9 +641,9 @@ function renderComponent(
|
|
|
627
641
|
if (e && typeof (e as any).then === "function") {
|
|
628
642
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
629
643
|
patchPromiseStatus(e as Promise<unknown>);
|
|
630
|
-
const
|
|
644
|
+
const rctx = captureRenderCtx();
|
|
631
645
|
return (e as Promise<unknown>).then(() => {
|
|
632
|
-
|
|
646
|
+
restoreRenderCtx(rctx);
|
|
633
647
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
634
648
|
});
|
|
635
649
|
}
|
|
@@ -686,10 +700,10 @@ function renderComponent(
|
|
|
686
700
|
};
|
|
687
701
|
const r = renderChildren(props.children, writer, isSvg);
|
|
688
702
|
if (r && typeof (r as any).then === "function") {
|
|
689
|
-
const
|
|
703
|
+
const rctx = captureRenderCtx();
|
|
690
704
|
return (r as Promise<void>).then(
|
|
691
|
-
() => {
|
|
692
|
-
(e) => {
|
|
705
|
+
() => { restoreRenderCtx(rctx); finish(); },
|
|
706
|
+
(e) => { restoreRenderCtx(rctx); finish(); throw e; },
|
|
693
707
|
);
|
|
694
708
|
}
|
|
695
709
|
finish();
|
|
@@ -722,9 +736,9 @@ function renderComponent(
|
|
|
722
736
|
if (e && typeof (e as any).then === "function") {
|
|
723
737
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
724
738
|
patchPromiseStatus(e as Promise<unknown>);
|
|
725
|
-
const
|
|
739
|
+
const rctx = captureRenderCtx();
|
|
726
740
|
return (e as Promise<unknown>).then(() => {
|
|
727
|
-
|
|
741
|
+
restoreRenderCtx(rctx);
|
|
728
742
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
729
743
|
});
|
|
730
744
|
}
|
|
@@ -744,9 +758,9 @@ function renderComponent(
|
|
|
744
758
|
|
|
745
759
|
// Async component
|
|
746
760
|
if (result instanceof Promise) {
|
|
747
|
-
const
|
|
761
|
+
const rctx = captureRenderCtx();
|
|
748
762
|
return result.then((resolved) => {
|
|
749
|
-
|
|
763
|
+
restoreRenderCtx(rctx);
|
|
750
764
|
// Check useId after the async body has finished executing.
|
|
751
765
|
let asyncSavedIdTree: number | undefined;
|
|
752
766
|
if (componentCalledUseId()) {
|
|
@@ -754,17 +768,17 @@ function renderComponent(
|
|
|
754
768
|
}
|
|
755
769
|
const r = renderNode(resolved, writer, isSvg);
|
|
756
770
|
if (r && typeof (r as any).then === "function") {
|
|
757
|
-
const
|
|
771
|
+
const rctx2 = captureRenderCtx();
|
|
758
772
|
// Only allocate cleanup closures when actually going async.
|
|
759
773
|
return (r as Promise<void>).then(
|
|
760
774
|
() => {
|
|
761
|
-
|
|
775
|
+
restoreRenderCtx(rctx2);
|
|
762
776
|
if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
|
|
763
777
|
popComponentScope(savedScope);
|
|
764
778
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
765
779
|
},
|
|
766
780
|
(e) => {
|
|
767
|
-
|
|
781
|
+
restoreRenderCtx(rctx2);
|
|
768
782
|
if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
|
|
769
783
|
popComponentScope(savedScope);
|
|
770
784
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -777,7 +791,7 @@ function renderComponent(
|
|
|
777
791
|
popComponentScope(savedScope);
|
|
778
792
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
779
793
|
}, (e) => {
|
|
780
|
-
|
|
794
|
+
restoreRenderCtx(rctx);
|
|
781
795
|
// savedIdTree is always undefined here (async component skips the push).
|
|
782
796
|
popComponentScope(savedScope);
|
|
783
797
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -788,17 +802,17 @@ function renderComponent(
|
|
|
788
802
|
const r = renderNode(result, writer, isSvg);
|
|
789
803
|
|
|
790
804
|
if (r && typeof (r as any).then === "function") {
|
|
791
|
-
const
|
|
805
|
+
const rctx = captureRenderCtx();
|
|
792
806
|
// Only allocate cleanup closures when actually going async.
|
|
793
807
|
return (r as Promise<void>).then(
|
|
794
808
|
() => {
|
|
795
|
-
|
|
809
|
+
restoreRenderCtx(rctx);
|
|
796
810
|
if (savedIdTree !== undefined) popTreeContext(savedIdTree);
|
|
797
811
|
popComponentScope(savedScope);
|
|
798
812
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
799
813
|
},
|
|
800
814
|
(e) => {
|
|
801
|
-
|
|
815
|
+
restoreRenderCtx(rctx);
|
|
802
816
|
if (savedIdTree !== undefined) popTreeContext(savedIdTree);
|
|
803
817
|
popComponentScope(savedScope);
|
|
804
818
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -839,9 +853,9 @@ function renderChildArrayFrom(
|
|
|
839
853
|
const savedTree = pushTreeContext(totalChildren, i);
|
|
840
854
|
const r = renderNode(child, writer, isSvg);
|
|
841
855
|
if (r && typeof (r as any).then === "function") {
|
|
842
|
-
const
|
|
856
|
+
const rctx = captureRenderCtx();
|
|
843
857
|
return (r as Promise<void>).then(() => {
|
|
844
|
-
|
|
858
|
+
restoreRenderCtx(rctx);
|
|
845
859
|
popTreeContext(savedTree);
|
|
846
860
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
847
861
|
});
|
|
@@ -895,9 +909,9 @@ async function renderSuspense(
|
|
|
895
909
|
try {
|
|
896
910
|
const r = renderNode(children, buffer, isSvg);
|
|
897
911
|
if (r && typeof (r as any).then === "function") {
|
|
898
|
-
const
|
|
912
|
+
const rctx = captureRenderCtx();
|
|
899
913
|
await r;
|
|
900
|
-
|
|
914
|
+
restoreRenderCtx(rctx);
|
|
901
915
|
}
|
|
902
916
|
// Success – wrap with React's Suspense boundary markers so hydrateRoot
|
|
903
917
|
// can locate the boundary in the DOM (<!--$--> … <!--/$-->).
|
|
@@ -918,9 +932,9 @@ async function renderSuspense(
|
|
|
918
932
|
if (fallback) {
|
|
919
933
|
const r = renderNode(fallback, writer, isSvg);
|
|
920
934
|
if (r && typeof (r as any).then === "function") {
|
|
921
|
-
const
|
|
935
|
+
const rctx = captureRenderCtx();
|
|
922
936
|
await r;
|
|
923
|
-
|
|
937
|
+
restoreRenderCtx(rctx);
|
|
924
938
|
}
|
|
925
939
|
}
|
|
926
940
|
writer.write("<!--/$-->");
|
|
@@ -984,7 +998,7 @@ export function renderToStream(
|
|
|
984
998
|
try {
|
|
985
999
|
const r = renderNode(element, writer);
|
|
986
1000
|
if (r && typeof (r as any).then === "function") {
|
|
987
|
-
const
|
|
1001
|
+
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
988
1002
|
}
|
|
989
1003
|
writer.flush!(); // encode everything accumulated (sync renders: the whole page)
|
|
990
1004
|
controller.close();
|
|
@@ -1037,7 +1051,7 @@ export async function renderPreflight(
|
|
|
1037
1051
|
// so a single pass is guaranteed to complete with all promises resolved.
|
|
1038
1052
|
const r = renderNode(element, NULL_WRITER);
|
|
1039
1053
|
if (r && typeof (r as any).then === "function") {
|
|
1040
|
-
const
|
|
1054
|
+
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
1041
1055
|
}
|
|
1042
1056
|
} finally {
|
|
1043
1057
|
swapContextMap(prev);
|
|
@@ -1075,7 +1089,7 @@ export async function renderToString(
|
|
|
1075
1089
|
// so a single pass is guaranteed to complete with all promises resolved.
|
|
1076
1090
|
const r = renderNode(element, writer);
|
|
1077
1091
|
if (r && typeof (r as any).then === "function") {
|
|
1078
|
-
const
|
|
1092
|
+
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
1079
1093
|
}
|
|
1080
1094
|
return output;
|
|
1081
1095
|
} finally {
|
|
@@ -220,21 +220,44 @@ export function componentCalledUseId(): boolean {
|
|
|
220
220
|
return s().localIdCounter > 0;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
export
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
export interface ContextSnapshot {
|
|
224
|
+
tree: TreeContext;
|
|
225
|
+
localId: number;
|
|
226
|
+
treeDepth: number;
|
|
227
|
+
/** Saved parent-context id values for the stack slots 0..treeDepth-1.
|
|
228
|
+
* Required so that concurrent renders cannot corrupt popTreeContext calls
|
|
229
|
+
* that run after an await continuation. */
|
|
230
|
+
idStack: number[];
|
|
231
|
+
ovStack: string[];
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
export function
|
|
234
|
+
export function snapshotContext(): ContextSnapshot {
|
|
235
|
+
const st = s();
|
|
236
|
+
const ctx = st.currentTreeContext;
|
|
237
|
+
const depth = _treeDepth;
|
|
238
|
+
return {
|
|
239
|
+
tree: { id: ctx.id, overflow: ctx.overflow },
|
|
240
|
+
localId: st.localIdCounter,
|
|
241
|
+
treeDepth: depth,
|
|
242
|
+
// Snapshot the live stack so that popTreeContext reads correct saved values
|
|
243
|
+
// even if another concurrent render's resetRenderState stomped the arrays.
|
|
244
|
+
idStack: _treeIdStack.slice(0, depth),
|
|
245
|
+
ovStack: _treeOvStack.slice(0, depth),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function restoreContext(snap: ContextSnapshot): void {
|
|
231
250
|
const st = s();
|
|
232
251
|
const ctx = st.currentTreeContext;
|
|
233
|
-
|
|
234
|
-
ctx.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
ctx.id = snap.tree.id;
|
|
253
|
+
ctx.overflow = snap.tree.overflow;
|
|
254
|
+
st.localIdCounter = snap.localId;
|
|
255
|
+
_treeDepth = snap.treeDepth;
|
|
256
|
+
// Restore the stack so subsequent popTreeContext calls see the right values.
|
|
257
|
+
for (let i = 0; i < snap.treeDepth; i++) {
|
|
258
|
+
_treeIdStack[i] = snap.idStack[i]!;
|
|
259
|
+
_treeOvStack[i] = snap.ovStack[i]!;
|
|
260
|
+
}
|
|
238
261
|
}
|
|
239
262
|
|
|
240
263
|
/**
|
package/src/utils/rspack.ts
CHANGED
|
@@ -338,6 +338,12 @@ const buildCompilerConfig = (
|
|
|
338
338
|
// changed files, making repeat dev starts significantly faster.
|
|
339
339
|
cache: true,
|
|
340
340
|
externals,
|
|
341
|
+
// externalsPresets.node externalises ALL Node.js built-ins (bare names
|
|
342
|
+
// and the node: prefix) for both static and dynamic imports. This
|
|
343
|
+
// complements the explicit `externals` array: the preset handles the
|
|
344
|
+
// node: URI scheme that rspack cannot resolve as a file, while the
|
|
345
|
+
// array keeps '@emotion/server' as an explicit external.
|
|
346
|
+
...(isServerBuild ? { externalsPresets: { node: true } } : {}),
|
|
341
347
|
...(optimization !== undefined ? { optimization } : {}),
|
|
342
348
|
plugins: [
|
|
343
349
|
!isServerBuild && new rspack.HtmlRspackPlugin({
|
|
@@ -393,7 +399,7 @@ const buildCompilerConfig = (
|
|
|
393
399
|
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
394
400
|
// and vice versa, causing an infinite rebuild loop.
|
|
395
401
|
watchOptions: {
|
|
396
|
-
ignored: ['**/node_modules/**', '**/.hadars/**'],
|
|
402
|
+
ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'],
|
|
397
403
|
},
|
|
398
404
|
};
|
|
399
405
|
};
|
|
@@ -418,7 +424,7 @@ export const compileEntry = async (entry: string, opts: EntryOptions & { watch?:
|
|
|
418
424
|
let first = true;
|
|
419
425
|
// Pass ignored patterns directly — compiler.watch(watchOptions) replaces
|
|
420
426
|
// the config-level watchOptions, so we must repeat them here.
|
|
421
|
-
compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**'] }, (err: any, stats: any) => {
|
|
427
|
+
compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'] }, (err: any, stats: any) => {
|
|
422
428
|
if (err) {
|
|
423
429
|
if (first) { first = false; reject(err); }
|
|
424
430
|
else { console.error('rspack watch error', err); }
|
|
@@ -452,10 +458,7 @@ export const compileEntry = async (entry: string, opts: EntryOptions & { watch?:
|
|
|
452
458
|
|
|
453
459
|
console.log(stats?.toString({
|
|
454
460
|
colors: true,
|
|
455
|
-
|
|
456
|
-
children: true,
|
|
457
|
-
chunks: true,
|
|
458
|
-
chunkModules: true,
|
|
461
|
+
preset: 'minimal',
|
|
459
462
|
}));
|
|
460
463
|
|
|
461
464
|
resolve(stats);
|
package/src/utils/serve.ts
CHANGED
|
@@ -58,33 +58,6 @@ function withRequestLogging(handler: FetchHandler): FetchHandler {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const COMPRESSIBLE_RE = /^text\/|\/json|\/javascript|\/xml|\/wasm/;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Wraps a handler to apply on-the-fly gzip compression via CompressionStream.
|
|
65
|
-
* Unlike a buffering approach, this pipes the response ReadableStream through
|
|
66
|
-
* a CompressionStream so bytes are compressed and flushed as they arrive —
|
|
67
|
-
* streaming responses remain streaming.
|
|
68
|
-
*/
|
|
69
|
-
function withCompression(handler: FetchHandler): FetchHandler {
|
|
70
|
-
return async (req, ctx) => {
|
|
71
|
-
const res = await handler(req, ctx);
|
|
72
|
-
if (!res || !res.body) return res;
|
|
73
|
-
const accept = req.headers.get('Accept-Encoding') ?? '';
|
|
74
|
-
if (!accept.includes('gzip')) return res;
|
|
75
|
-
// Skip if already encoded (e.g. pre-compressed cache entry).
|
|
76
|
-
if (res.headers.has('content-encoding')) return res;
|
|
77
|
-
const ct = res.headers.get('content-type') ?? '';
|
|
78
|
-
if (!COMPRESSIBLE_RE.test(ct)) return res;
|
|
79
|
-
const compressed = res.body.pipeThrough(
|
|
80
|
-
new (globalThis as any).CompressionStream('gzip'),
|
|
81
|
-
);
|
|
82
|
-
const headers = new Headers(res.headers);
|
|
83
|
-
headers.set('content-encoding', 'gzip');
|
|
84
|
-
headers.delete('content-length'); // length is unknown after compression
|
|
85
|
-
return new Response(compressed, { status: res.status, headers });
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
61
|
|
|
89
62
|
export async function serve(
|
|
90
63
|
port: number,
|
|
@@ -92,7 +65,7 @@ export async function serve(
|
|
|
92
65
|
/** Bun WebSocketHandler — ignored on Deno and Node.js. */
|
|
93
66
|
websocket?: unknown,
|
|
94
67
|
): Promise<void> {
|
|
95
|
-
fetchHandler =
|
|
68
|
+
fetchHandler = withRequestLogging(fetchHandler);
|
|
96
69
|
|
|
97
70
|
// ── Bun ────────────────────────────────────────────────────────────────
|
|
98
71
|
if (isBun) {
|