hadars 0.1.17 → 0.1.19
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-OS3V4CPN.js +42 -0
- package/dist/cli.js +777 -143
- package/dist/index.cjs +61 -6
- package/dist/index.d.ts +40 -1
- package/dist/index.js +58 -6
- package/dist/jsx-runtime-97ca74a5.d.ts +18 -0
- package/dist/slim-react/index.cjs +1001 -0
- package/dist/slim-react/index.d.ts +180 -0
- package/dist/slim-react/index.js +911 -0
- package/dist/slim-react/jsx-runtime.cjs +52 -0
- package/dist/slim-react/jsx-runtime.d.ts +1 -0
- package/dist/slim-react/jsx-runtime.js +10 -0
- package/dist/ssr-render-worker.js +740 -108
- package/dist/ssr-watch.js +34 -13
- package/dist/utils/Head.tsx +3 -6
- package/index.ts +1 -1
- package/package.json +3 -3
- package/src/build.ts +6 -23
- package/src/components/CacheSegment.tsx +67 -0
- package/src/index.tsx +2 -0
- package/src/slim-react/context.ts +52 -0
- package/src/slim-react/hooks.ts +137 -0
- package/src/slim-react/index.ts +225 -0
- package/src/slim-react/jsx-runtime.ts +7 -0
- package/src/slim-react/jsx.ts +53 -0
- package/src/slim-react/render.ts +863 -0
- package/src/slim-react/renderContext.ts +105 -0
- package/src/slim-react/types.ts +33 -0
- package/src/ssr-render-worker.ts +83 -118
- package/src/utils/Head.tsx +3 -6
- package/src/utils/response.tsx +42 -105
- package/src/utils/rspack.ts +42 -15
- package/src/utils/segmentCache.ts +87 -0
package/src/utils/rspack.ts
CHANGED
|
@@ -216,26 +216,27 @@ const buildCompilerConfig = (
|
|
|
216
216
|
(opts.output && typeof opts.output === 'object' && (opts.output.library || String(opts.output.filename || '').includes('ssr')))
|
|
217
217
|
);
|
|
218
218
|
|
|
219
|
+
// slim-react: the SSR-only React-compatible renderer bundled with hadars.
|
|
220
|
+
// On server builds we replace the real React with slim-react so that hooks
|
|
221
|
+
// get safe SSR stubs, context works, and renderToStream / Suspense are
|
|
222
|
+
// natively supported. The client build is untouched and uses real React.
|
|
223
|
+
const slimReactIndex = pathMod.resolve(packageDir, 'slim-react', 'index.js');
|
|
224
|
+
const slimReactJsx = pathMod.resolve(packageDir, 'slim-react', 'jsx-runtime.js');
|
|
225
|
+
|
|
219
226
|
const resolveAliases: Record<string, string> | undefined = isServerBuild ? {
|
|
220
|
-
//
|
|
221
|
-
react:
|
|
222
|
-
'react-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
'react
|
|
226
|
-
// ensure emotion packages resolve to the project's node_modules so we don't pick up a browser-specific entry
|
|
227
|
-
'@emotion/react': path.resolve(process.cwd(), 'node_modules', '@emotion', 'react'),
|
|
227
|
+
// Route all React imports to slim-react for SSR.
|
|
228
|
+
react: slimReactIndex,
|
|
229
|
+
'react/jsx-runtime': slimReactJsx,
|
|
230
|
+
'react/jsx-dev-runtime': slimReactJsx,
|
|
231
|
+
// Keep emotion on the project's node_modules (server-safe entry).
|
|
232
|
+
'@emotion/react': path.resolve(process.cwd(), 'node_modules', '@emotion', 'react'),
|
|
228
233
|
'@emotion/server': path.resolve(process.cwd(), 'node_modules', '@emotion', 'server'),
|
|
229
|
-
'@emotion/cache':
|
|
234
|
+
'@emotion/cache': path.resolve(process.cwd(), 'node_modules', '@emotion', 'cache'),
|
|
230
235
|
'@emotion/styled': path.resolve(process.cwd(), 'node_modules', '@emotion', 'styled'),
|
|
231
236
|
} : undefined;
|
|
232
237
|
|
|
233
238
|
const externals = isServerBuild ? [
|
|
234
|
-
|
|
235
|
-
'react-dom',
|
|
236
|
-
// keep common aliases external as well
|
|
237
|
-
'react/jsx-runtime',
|
|
238
|
-
'react/jsx-dev-runtime',
|
|
239
|
+
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
239
240
|
// emotion should be external on server builds to avoid client/browser code
|
|
240
241
|
'@emotion/react',
|
|
241
242
|
'@emotion/server',
|
|
@@ -301,8 +302,34 @@ const buildCompilerConfig = (
|
|
|
301
302
|
: clientScriptPath,
|
|
302
303
|
scriptLoading: 'module',
|
|
303
304
|
filename: 'out.html',
|
|
304
|
-
inject: '
|
|
305
|
+
inject: 'head',
|
|
306
|
+
minify: opts.mode === 'production',
|
|
305
307
|
}),
|
|
308
|
+
// Add `async` to the emitted module script so DOMContentLoaded fires
|
|
309
|
+
// as soon as HTML is parsed — without waiting for the bundle to execute.
|
|
310
|
+
// `<script type="module" async>` is valid: it downloads in parallel and
|
|
311
|
+
// executes without blocking DOMContentLoaded, while retaining module
|
|
312
|
+
// semantics (strict mode, ES imports, etc.).
|
|
313
|
+
{
|
|
314
|
+
apply(compiler: any) {
|
|
315
|
+
compiler.hooks.emit.tapAsync('HadarsAsyncModuleScript', (compilation: any, cb: () => void) => {
|
|
316
|
+
const asset = compilation.assets['out.html'];
|
|
317
|
+
if (asset) {
|
|
318
|
+
const html: string = asset.source();
|
|
319
|
+
const updated = html.replace(
|
|
320
|
+
/(<script\b[^>]*\btype="module"[^>]*)(>)/g,
|
|
321
|
+
(match, before: string, end: string) =>
|
|
322
|
+
before.includes('async') ? match : `${before} async${end}`,
|
|
323
|
+
);
|
|
324
|
+
compilation.assets['out.html'] = {
|
|
325
|
+
source: () => updated,
|
|
326
|
+
size: () => Buffer.byteLength(updated),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
cb();
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
},
|
|
306
333
|
isDev && new ReactRefreshPlugin(),
|
|
307
334
|
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
308
335
|
...extraPlugins,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side segment cache for CacheSegment.
|
|
3
|
+
*
|
|
4
|
+
* The store lives on globalThis so all module instances (framework source,
|
|
5
|
+
* compiled dist, user SSR bundle) share the exact same Map regardless of
|
|
6
|
+
* how `hadars` was resolved by Node's module loader.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface SegmentEntry {
|
|
10
|
+
html: string;
|
|
11
|
+
expiresAt: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getStore(): Map<string, SegmentEntry> {
|
|
15
|
+
const g = globalThis as any;
|
|
16
|
+
if (!g.__hadarsSegmentStore) {
|
|
17
|
+
g.__hadarsSegmentStore = new Map<string, SegmentEntry>();
|
|
18
|
+
}
|
|
19
|
+
return g.__hadarsSegmentStore;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSegment(key: string): string | null {
|
|
23
|
+
const entry = getStore().get(key);
|
|
24
|
+
if (!entry) return null;
|
|
25
|
+
if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
|
|
26
|
+
getStore().delete(key);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return entry.html;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function setSegment(key: string, html: string, ttl?: number): void {
|
|
33
|
+
getStore().set(key, {
|
|
34
|
+
html,
|
|
35
|
+
expiresAt: ttl != null ? Date.now() + ttl : null,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function deleteSegment(key: string): void {
|
|
40
|
+
getStore().delete(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearSegments(): void {
|
|
44
|
+
getStore().clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom element name used as a boundary marker in the rendered HTML.
|
|
49
|
+
* Must contain a hyphen (valid custom element). Stripped by processSegmentCache
|
|
50
|
+
* before the response is sent — the browser never sees this tag.
|
|
51
|
+
*/
|
|
52
|
+
export const CACHE_TAG = 'hadars-c';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Post-processes the HTML string produced by renderToString:
|
|
56
|
+
*
|
|
57
|
+
* - **Cache miss** markers (`data-cache="miss"`): extract inner HTML, store it
|
|
58
|
+
* in the segment cache, strip the wrapper tag.
|
|
59
|
+
* - **Cache hit** markers (`data-cache="hit"`): strip the wrapper tag (the
|
|
60
|
+
* cached HTML is already the element content via dangerouslySetInnerHTML).
|
|
61
|
+
*
|
|
62
|
+
* Nested `CacheSegment` components are handled correctly because the
|
|
63
|
+
* non-greedy regex naturally matches the innermost tag first, and we iterate
|
|
64
|
+
* until the string stabilises.
|
|
65
|
+
*/
|
|
66
|
+
export function processSegmentCache(html: string): string {
|
|
67
|
+
let prev: string;
|
|
68
|
+
do {
|
|
69
|
+
prev = html;
|
|
70
|
+
html = html.replace(
|
|
71
|
+
/<hadars-c([^>]*)>([\s\S]*?)<\/hadars-c>/g,
|
|
72
|
+
(match, attrs: string, content: string) => {
|
|
73
|
+
const cacheM = /data-cache="([^"]+)"/.exec(attrs);
|
|
74
|
+
const keyM = /data-key="([^"]+)"/.exec(attrs);
|
|
75
|
+
const ttlM = /data-ttl="(\d+)"/.exec(attrs);
|
|
76
|
+
if (!cacheM || !keyM) return match;
|
|
77
|
+
if (cacheM[1] === 'miss') {
|
|
78
|
+
setSegment(keyM[1]!, content, ttlM ? Number(ttlM[1]) : undefined);
|
|
79
|
+
return content;
|
|
80
|
+
}
|
|
81
|
+
if (cacheM[1] === 'hit') return content;
|
|
82
|
+
return match;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
} while (html !== prev);
|
|
86
|
+
return html;
|
|
87
|
+
}
|