hadars 0.1.17 → 0.1.18
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-TGSIYGY2.js +40 -0
- package/dist/cli.js +656 -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 +874 -0
- package/dist/slim-react/index.d.ts +180 -0
- package/dist/slim-react/index.js +784 -0
- package/dist/slim-react/jsx-runtime.cjs +51 -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 +619 -108
- package/dist/ssr-watch.js +34 -13
- package/dist/utils/Head.tsx +3 -6
- package/index.ts +1 -1
- package/package.json +2 -2
- 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 +686 -0
- package/src/slim-react/renderContext.ts +105 -0
- package/src/slim-react/types.ts +27 -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/dist/ssr-watch.js
CHANGED
|
@@ -171,25 +171,21 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
171
171
|
const isServerBuild = Boolean(
|
|
172
172
|
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
173
173
|
);
|
|
174
|
+
const slimReactIndex = pathMod.resolve(packageDir, "slim-react", "index.js");
|
|
175
|
+
const slimReactJsx = pathMod.resolve(packageDir, "slim-react", "jsx-runtime.js");
|
|
174
176
|
const resolveAliases = isServerBuild ? {
|
|
175
|
-
//
|
|
176
|
-
react:
|
|
177
|
-
"react-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"react/jsx-dev-runtime": path.resolve(process.cwd(), "node_modules", "react", "jsx-dev-runtime.js"),
|
|
181
|
-
// ensure emotion packages resolve to the project's node_modules so we don't pick up a browser-specific entry
|
|
177
|
+
// Route all React imports to slim-react for SSR.
|
|
178
|
+
react: slimReactIndex,
|
|
179
|
+
"react/jsx-runtime": slimReactJsx,
|
|
180
|
+
"react/jsx-dev-runtime": slimReactJsx,
|
|
181
|
+
// Keep emotion on the project's node_modules (server-safe entry).
|
|
182
182
|
"@emotion/react": path.resolve(process.cwd(), "node_modules", "@emotion", "react"),
|
|
183
183
|
"@emotion/server": path.resolve(process.cwd(), "node_modules", "@emotion", "server"),
|
|
184
184
|
"@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
|
|
185
185
|
"@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
|
|
186
186
|
} : void 0;
|
|
187
187
|
const externals = isServerBuild ? [
|
|
188
|
-
|
|
189
|
-
"react-dom",
|
|
190
|
-
// keep common aliases external as well
|
|
191
|
-
"react/jsx-runtime",
|
|
192
|
-
"react/jsx-dev-runtime",
|
|
188
|
+
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
193
189
|
// emotion should be external on server builds to avoid client/browser code
|
|
194
190
|
"@emotion/react",
|
|
195
191
|
"@emotion/server",
|
|
@@ -244,8 +240,33 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
244
240
|
template: opts.htmlTemplate ? pathMod.resolve(process.cwd(), opts.htmlTemplate) : clientScriptPath,
|
|
245
241
|
scriptLoading: "module",
|
|
246
242
|
filename: "out.html",
|
|
247
|
-
inject: "
|
|
243
|
+
inject: "head",
|
|
244
|
+
minify: opts.mode === "production"
|
|
248
245
|
}),
|
|
246
|
+
// Add `async` to the emitted module script so DOMContentLoaded fires
|
|
247
|
+
// as soon as HTML is parsed — without waiting for the bundle to execute.
|
|
248
|
+
// `<script type="module" async>` is valid: it downloads in parallel and
|
|
249
|
+
// executes without blocking DOMContentLoaded, while retaining module
|
|
250
|
+
// semantics (strict mode, ES imports, etc.).
|
|
251
|
+
{
|
|
252
|
+
apply(compiler) {
|
|
253
|
+
compiler.hooks.emit.tapAsync("HadarsAsyncModuleScript", (compilation, cb) => {
|
|
254
|
+
const asset = compilation.assets["out.html"];
|
|
255
|
+
if (asset) {
|
|
256
|
+
const html = asset.source();
|
|
257
|
+
const updated = html.replace(
|
|
258
|
+
/(<script\b[^>]*\btype="module"[^>]*)(>)/g,
|
|
259
|
+
(match, before, end) => before.includes("async") ? match : `${before} async${end}`
|
|
260
|
+
);
|
|
261
|
+
compilation.assets["out.html"] = {
|
|
262
|
+
source: () => updated,
|
|
263
|
+
size: () => Buffer.byteLength(updated)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
cb();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
},
|
|
249
270
|
isDev && new ReactRefreshPlugin(),
|
|
250
271
|
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
251
272
|
...extraPlugins
|
package/dist/utils/Head.tsx
CHANGED
|
@@ -273,8 +273,7 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
273
273
|
() => { unsuspend.cache.set(cacheKey, { status: 'suspense-resolved' }); },
|
|
274
274
|
);
|
|
275
275
|
unsuspend.cache.set(cacheKey, { status: 'pending', promise: suspensePromise });
|
|
276
|
-
|
|
277
|
-
return undefined;
|
|
276
|
+
throw suspensePromise; // slim-react will await and retry
|
|
278
277
|
}
|
|
279
278
|
throw thrown;
|
|
280
279
|
}
|
|
@@ -294,12 +293,10 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
294
293
|
reason => { unsuspend.cache.set(cacheKey, { status: 'rejected', reason }); },
|
|
295
294
|
);
|
|
296
295
|
unsuspend.cache.set(cacheKey, { status: 'pending', promise });
|
|
297
|
-
|
|
298
|
-
return undefined;
|
|
296
|
+
throw promise; // slim-react will await and retry
|
|
299
297
|
}
|
|
300
298
|
if (existing.status === 'pending') {
|
|
301
|
-
|
|
302
|
-
return undefined;
|
|
299
|
+
throw existing.promise; // slim-react will await and retry
|
|
303
300
|
}
|
|
304
301
|
if (existing.status === 'rejected') throw existing.reason;
|
|
305
302
|
return existing.value as T;
|
package/index.ts
CHANGED
|
@@ -12,4 +12,4 @@ export type {
|
|
|
12
12
|
HadarsEntryModule,
|
|
13
13
|
HadarsApp,
|
|
14
14
|
} from "./src/types/hadars";
|
|
15
|
-
export { HadarsHead, HadarsContext, loadModule } from "./src/index";
|
|
15
|
+
export { HadarsHead, HadarsContext, loadModule, CacheSegment, deleteSegment, clearSegments } from "./src/index";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build:lib": "tsup src/index.tsx --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
|
|
32
|
+
"build:lib": "tsup src/index.tsx src/slim-react/index.ts src/slim-react/jsx-runtime.ts --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
|
|
33
33
|
"build:cli": "node build-scripts/build-cli.mjs",
|
|
34
34
|
"build:all": "npm run build:lib && npm run build:cli",
|
|
35
35
|
"test": "bun test test/ssr.test.ts",
|
package/src/build.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { isBun, isDeno, isNode } from "./utils/runtime";
|
|
|
9
9
|
import { RspackDevServer } from "@rspack/dev-server";
|
|
10
10
|
import pathMod from "node:path";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
-
import { createRequire } from 'node:module';
|
|
13
12
|
import crypto from 'node:crypto';
|
|
14
13
|
import fs from 'node:fs/promises';
|
|
15
14
|
import { existsSync } from 'node:fs';
|
|
@@ -17,6 +16,7 @@ import os from 'node:os';
|
|
|
17
16
|
import { spawn } from 'node:child_process';
|
|
18
17
|
import cluster from 'node:cluster';
|
|
19
18
|
import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
|
|
19
|
+
import { processSegmentCache } from "./utils/segmentCache";
|
|
20
20
|
const encoder = new TextEncoder();
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -73,17 +73,7 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
|
|
|
73
73
|
const HEAD_MARKER = '<meta name="HADARS_HEAD">';
|
|
74
74
|
const BODY_MARKER = '<meta name="HADARS_BODY">';
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
let _renderToString: ((element: any) => string) | null = null;
|
|
78
|
-
async function getRenderToString(): Promise<(element: any) => string> {
|
|
79
|
-
if (!_renderToString) {
|
|
80
|
-
const req = createRequire(pathMod.resolve(process.cwd(), '__hadars_fake__.js'));
|
|
81
|
-
const resolved = req.resolve('react-dom/server');
|
|
82
|
-
const mod = await import(pathToFileURL(resolved).href);
|
|
83
|
-
_renderToString = mod.renderToString;
|
|
84
|
-
}
|
|
85
|
-
return _renderToString!;
|
|
86
|
-
}
|
|
76
|
+
import { renderToString as slimRenderToString } from './slim-react/index';
|
|
87
77
|
|
|
88
78
|
// Round-robin thread pool for SSR rendering — used on Bun/Deno where
|
|
89
79
|
// node:cluster is not available but node:worker_threads is.
|
|
@@ -210,26 +200,21 @@ async function buildSsrResponse(
|
|
|
210
200
|
getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
|
|
211
201
|
unsuspendForRender: any,
|
|
212
202
|
): Promise<Response> {
|
|
213
|
-
// Pre-load renderer before starting the stream so the set→call→clear
|
|
214
|
-
// sequence around __hadarsUnsuspend is fully synchronous (no await between them).
|
|
215
|
-
const renderToString = await getRenderToString();
|
|
216
|
-
|
|
217
203
|
const responseStream = new ReadableStream({
|
|
218
204
|
async start(controller) {
|
|
219
205
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
220
206
|
// Flush the shell (precontentHtml) immediately so the browser can
|
|
221
207
|
// start loading CSS/fonts before renderToString blocks the thread.
|
|
222
208
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
223
|
-
await Promise.resolve(); // yield to let the runtime flush the shell chunk
|
|
224
209
|
|
|
225
|
-
// set → call (synchronous) → clear: no await in between, safe under concurrency
|
|
226
210
|
let bodyHtml: string;
|
|
227
211
|
try {
|
|
228
212
|
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
229
|
-
bodyHtml =
|
|
213
|
+
bodyHtml = await slimRenderToString(ReactPage);
|
|
230
214
|
} finally {
|
|
231
215
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
232
216
|
}
|
|
217
|
+
bodyHtml = processSegmentCache(bodyHtml);
|
|
233
218
|
controller.enqueue(encoder.encode(bodyHtml + postContent));
|
|
234
219
|
controller.close();
|
|
235
220
|
},
|
|
@@ -697,7 +682,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
697
682
|
getFinalProps,
|
|
698
683
|
} = (await import(importPath)) as HadarsEntryModule<any>;
|
|
699
684
|
|
|
700
|
-
const { ReactPage, status, headHtml
|
|
685
|
+
const { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
701
686
|
document: {
|
|
702
687
|
body: Component as React.FC<HadarsProps<object>>,
|
|
703
688
|
lang: 'en',
|
|
@@ -707,7 +692,6 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
707
692
|
},
|
|
708
693
|
});
|
|
709
694
|
|
|
710
|
-
const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
|
|
711
695
|
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
712
696
|
} catch (err: any) {
|
|
713
697
|
console.error('[hadars] SSR render error:', err);
|
|
@@ -885,7 +869,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
885
869
|
});
|
|
886
870
|
}
|
|
887
871
|
|
|
888
|
-
const { ReactPage, status, headHtml
|
|
872
|
+
const { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
889
873
|
document: {
|
|
890
874
|
body: Component as React.FC<HadarsProps<object>>,
|
|
891
875
|
lang: 'en',
|
|
@@ -895,7 +879,6 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
895
879
|
},
|
|
896
880
|
});
|
|
897
881
|
|
|
898
|
-
const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
|
|
899
882
|
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
900
883
|
} catch (err: any) {
|
|
901
884
|
console.error('[hadars] SSR render error:', err);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getSegment, CACHE_TAG } from '../utils/segmentCache';
|
|
3
|
+
|
|
4
|
+
interface CacheSegmentProps {
|
|
5
|
+
/**
|
|
6
|
+
* Unique cache key for this segment. Use a key that encodes all values
|
|
7
|
+
* the output depends on, e.g. `"product-" + product.id`.
|
|
8
|
+
*/
|
|
9
|
+
cacheKey: string;
|
|
10
|
+
/**
|
|
11
|
+
* Time-to-live in milliseconds. Omit for entries that never expire.
|
|
12
|
+
*/
|
|
13
|
+
ttl?: number;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Caches the server-rendered HTML of its children across requests.
|
|
19
|
+
*
|
|
20
|
+
* **Server (SSR):**
|
|
21
|
+
* - Cache miss — children are rendered normally as part of the main
|
|
22
|
+
* `renderToString` call, so React context propagates correctly. The output
|
|
23
|
+
* is wrapped in a `<hadars-c>` marker that `processSegmentCache` uses to
|
|
24
|
+
* extract and store the HTML. The marker is stripped before the response
|
|
25
|
+
* is sent; the browser never sees it.
|
|
26
|
+
* - Cache hit — children are **not** rendered at all. The cached HTML is
|
|
27
|
+
* injected directly, saving the entire subtree render cost.
|
|
28
|
+
*
|
|
29
|
+
* **Client:** renders children normally (no caching). Because the server
|
|
30
|
+
* strips the marker wrapper, the client output matches the server HTML and
|
|
31
|
+
* React hydration succeeds without warnings for deterministic components.
|
|
32
|
+
*
|
|
33
|
+
* **Note:** components that rely on request-specific data (cookies, auth,
|
|
34
|
+
* personalisation) must not be wrapped in `CacheSegment` unless the cache
|
|
35
|
+
* key encodes that data — otherwise a cached response for one user could be
|
|
36
|
+
* served to another.
|
|
37
|
+
*/
|
|
38
|
+
export function CacheSegment({ cacheKey, ttl, children }: CacheSegmentProps) {
|
|
39
|
+
// Client: render children normally — no server cache on the client.
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
return <>{children}</>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cached = getSegment(cacheKey);
|
|
45
|
+
|
|
46
|
+
if (cached !== null) {
|
|
47
|
+
// Cache hit: skip rendering children entirely.
|
|
48
|
+
// The <hadars-c> wrapper is stripped by processSegmentCache before
|
|
49
|
+
// the response is sent to the browser.
|
|
50
|
+
return React.createElement(CACHE_TAG as any, {
|
|
51
|
+
'data-key': cacheKey,
|
|
52
|
+
'data-cache': 'hit',
|
|
53
|
+
dangerouslySetInnerHTML: { __html: cached },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cache miss: render children as normal React elements so that React
|
|
58
|
+
// context (providers, etc.) propagates correctly into the subtree.
|
|
59
|
+
// processSegmentCache will extract the rendered HTML and store it.
|
|
60
|
+
const props: Record<string, unknown> = {
|
|
61
|
+
'data-key': cacheKey,
|
|
62
|
+
'data-cache': 'miss',
|
|
63
|
+
};
|
|
64
|
+
if (ttl != null) props['data-ttl'] = ttl;
|
|
65
|
+
|
|
66
|
+
return React.createElement(CACHE_TAG as any, props, children);
|
|
67
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -10,6 +10,8 @@ export type {
|
|
|
10
10
|
HadarsApp,
|
|
11
11
|
} from "./types/hadars";
|
|
12
12
|
export { Head as HadarsHead, useServerData, initServerDataCache } from './utils/Head';
|
|
13
|
+
export { CacheSegment } from './components/CacheSegment';
|
|
14
|
+
export { deleteSegment, clearSegments } from './utils/segmentCache';
|
|
13
15
|
import { AppProviderSSR, AppProviderCSR } from "./utils/Head";
|
|
14
16
|
|
|
15
17
|
export const HadarsContext = typeof window === 'undefined' ? AppProviderSSR : AppProviderCSR;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SlimNode } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Context implementation for SSR.
|
|
5
|
+
*
|
|
6
|
+
* Because SSR is single-pass and synchronous within each component,
|
|
7
|
+
* we just track the "current" value on the context object and
|
|
8
|
+
* save / restore around Provider renders (handled by the renderer).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface Context<T> {
|
|
12
|
+
_currentValue: T;
|
|
13
|
+
Provider: ContextProvider<T>;
|
|
14
|
+
Consumer: (props: { children: (value: T) => SlimNode }) => SlimNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ContextProvider<T> = ((props: {
|
|
18
|
+
value: T;
|
|
19
|
+
children?: SlimNode;
|
|
20
|
+
}) => SlimNode) & {
|
|
21
|
+
_context: Context<T>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createContext<T>(defaultValue: T): Context<T> {
|
|
25
|
+
const context: Context<T> = {
|
|
26
|
+
_currentValue: defaultValue,
|
|
27
|
+
Provider: null!,
|
|
28
|
+
Consumer: null!,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Provider is a function component recognised by the renderer.
|
|
32
|
+
// The `_context` tag tells the renderer to push / pop the value.
|
|
33
|
+
const Provider = function ContextProvider({
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
value: T;
|
|
37
|
+
children?: SlimNode;
|
|
38
|
+
}): SlimNode {
|
|
39
|
+
return children ?? null;
|
|
40
|
+
} as unknown as ContextProvider<T>;
|
|
41
|
+
|
|
42
|
+
Provider._context = context;
|
|
43
|
+
context.Provider = Provider;
|
|
44
|
+
|
|
45
|
+
context.Consumer = ({ children }) => {
|
|
46
|
+
return (children as unknown as (value: T) => SlimNode)(
|
|
47
|
+
context._currentValue,
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return context;
|
|
52
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR hook implementations.
|
|
3
|
+
*
|
|
4
|
+
* On the server every hook is either a no-op or returns the initial /
|
|
5
|
+
* snapshot value. This is enough for the vast majority of React-
|
|
6
|
+
* compatible libraries to work during server-side rendering.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { makeId } from "./renderContext";
|
|
10
|
+
|
|
11
|
+
// ---- useState ----
|
|
12
|
+
export function useState<T>(
|
|
13
|
+
initialState: T | (() => T),
|
|
14
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
15
|
+
const value =
|
|
16
|
+
typeof initialState === "function"
|
|
17
|
+
? (initialState as () => T)()
|
|
18
|
+
: initialState;
|
|
19
|
+
return [value, () => {}];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---- useReducer ----
|
|
23
|
+
export function useReducer<S, A>(
|
|
24
|
+
_reducer: (state: S, action: A) => S,
|
|
25
|
+
initialState: S,
|
|
26
|
+
): [S, (action: A) => void] {
|
|
27
|
+
return [initialState, () => {}];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---- useEffect / useLayoutEffect / useInsertionEffect ----
|
|
31
|
+
export function useEffect(
|
|
32
|
+
_effect: () => void | (() => void),
|
|
33
|
+
_deps?: any[],
|
|
34
|
+
) {}
|
|
35
|
+
export function useLayoutEffect(
|
|
36
|
+
_effect: () => void | (() => void),
|
|
37
|
+
_deps?: any[],
|
|
38
|
+
) {}
|
|
39
|
+
export function useInsertionEffect(
|
|
40
|
+
_effect: () => void | (() => void),
|
|
41
|
+
_deps?: any[],
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
// ---- useRef ----
|
|
45
|
+
export function useRef<T>(initialValue: T): { current: T } {
|
|
46
|
+
return { current: initialValue };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- useMemo / useCallback ----
|
|
50
|
+
export function useMemo<T>(factory: () => T, _deps?: any[]): T {
|
|
51
|
+
return factory();
|
|
52
|
+
}
|
|
53
|
+
export function useCallback<T extends Function>(callback: T, _deps?: any[]): T {
|
|
54
|
+
return callback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- useId ----
|
|
58
|
+
export function useId(): string {
|
|
59
|
+
return makeId();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- useDebugValue ----
|
|
63
|
+
export function useDebugValue(_value: any, _format?: (v: any) => any) {}
|
|
64
|
+
|
|
65
|
+
// ---- useImperativeHandle ----
|
|
66
|
+
export function useImperativeHandle(
|
|
67
|
+
_ref: any,
|
|
68
|
+
_createHandle: () => any,
|
|
69
|
+
_deps?: any[],
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
// ---- useSyncExternalStore ----
|
|
73
|
+
export function useSyncExternalStore<T>(
|
|
74
|
+
_subscribe: (onStoreChange: () => void) => () => void,
|
|
75
|
+
getSnapshot: () => T,
|
|
76
|
+
getServerSnapshot?: () => T,
|
|
77
|
+
): T {
|
|
78
|
+
return (getServerSnapshot || getSnapshot)();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- useTransition ----
|
|
82
|
+
export function useTransition(): [boolean, (callback: () => void) => void] {
|
|
83
|
+
return [false, (cb) => cb()];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- useDeferredValue ----
|
|
87
|
+
export function useDeferredValue<T>(value: T): T {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- useOptimistic (React 19) ----
|
|
92
|
+
export function useOptimistic<T>(passthrough: T): [T, () => void] {
|
|
93
|
+
return [passthrough, () => {}];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- useFormStatus (React 19) ----
|
|
97
|
+
export function useFormStatus() {
|
|
98
|
+
return { pending: false, data: null, method: null, action: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- useActionState (React 19) ----
|
|
102
|
+
export function useActionState<S>(
|
|
103
|
+
_action: (state: S, payload: any) => S | Promise<S>,
|
|
104
|
+
initialState: S,
|
|
105
|
+
_permalink?: string,
|
|
106
|
+
): [S, (payload: any) => void, boolean] {
|
|
107
|
+
return [initialState, () => {}, false];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- use (React 19 – Suspense integration) ----
|
|
111
|
+
export function use<T>(
|
|
112
|
+
usable: (Promise<T> & { status?: string; value?: T; reason?: any }) | { _currentValue: T },
|
|
113
|
+
): T {
|
|
114
|
+
// Context object
|
|
115
|
+
if (
|
|
116
|
+
typeof usable === "object" &&
|
|
117
|
+
usable !== null &&
|
|
118
|
+
"_currentValue" in usable
|
|
119
|
+
) {
|
|
120
|
+
return (usable as { _currentValue: T })._currentValue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Promise – Suspense protocol
|
|
124
|
+
const promise = usable as Promise<T> & {
|
|
125
|
+
status?: string;
|
|
126
|
+
value?: T;
|
|
127
|
+
reason?: any;
|
|
128
|
+
};
|
|
129
|
+
if (promise.status === "fulfilled") return promise.value!;
|
|
130
|
+
if (promise.status === "rejected") throw promise.reason;
|
|
131
|
+
throw promise; // caught by the nearest Suspense boundary
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- startTransition ----
|
|
135
|
+
export function startTransition(callback: () => void) {
|
|
136
|
+
callback();
|
|
137
|
+
}
|