hadars 0.1.16 → 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/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
- // force all react imports to resolve to this project's react
176
- react: path.resolve(process.cwd(), "node_modules", "react"),
177
- "react-dom": path.resolve(process.cwd(), "node_modules", "react-dom"),
178
- // also map react/jsx-runtime to avoid duplicates when automatic runtime is used
179
- "react/jsx-runtime": path.resolve(process.cwd(), "node_modules", "react", "jsx-runtime.js"),
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
- "react",
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",
@@ -233,6 +229,9 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
233
229
  clean: false
234
230
  },
235
231
  mode: opts.mode,
232
+ // Persist transformed modules to disk — subsequent starts only recompile
233
+ // changed files, making repeat dev starts significantly faster.
234
+ cache: true,
236
235
  externals,
237
236
  ...optimization !== void 0 ? { optimization } : {},
238
237
  plugins: [
@@ -241,8 +240,33 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
241
240
  template: opts.htmlTemplate ? pathMod.resolve(process.cwd(), opts.htmlTemplate) : clientScriptPath,
242
241
  scriptLoading: "module",
243
242
  filename: "out.html",
244
- inject: "body"
243
+ inject: "head",
244
+ minify: opts.mode === "production"
245
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
+ },
246
270
  isDev && new ReactRefreshPlugin(),
247
271
  includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
248
272
  ...extraPlugins
@@ -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
- unsuspend.hasPending = true;
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
- unsuspend.hasPending = true;
298
- return undefined;
296
+ throw promise; // slim-react will await and retry
299
297
  }
300
298
  if (existing.status === 'pending') {
301
- unsuspend.hasPending = true;
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.16",
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
  /**
@@ -36,6 +36,16 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
36
36
  }
37
37
  if (matches.length === 0) return templatePath;
38
38
 
39
+ await ensureHadarsTmpDir();
40
+
41
+ // Cache by content hash — same template content → skip Tailwind re-scan on restart.
42
+ const sourceHash = crypto.createHash('md5').update(html).digest('hex').slice(0, 8);
43
+ const cachedPath = pathMod.join(HADARS_TMP_DIR, `template-${sourceHash}.html`);
44
+ try {
45
+ await fs.access(cachedPath);
46
+ return cachedPath; // cache hit
47
+ } catch { /* cache miss — process below */ }
48
+
39
49
  const { default: postcss } = await import('postcss');
40
50
  let plugins: any[] = [];
41
51
  try {
@@ -56,25 +66,14 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
56
66
  }
57
67
  }
58
68
 
59
- const tmpPath = pathMod.join(os.tmpdir(), `hadars-template-${Date.now()}.html`);
60
- await fs.writeFile(tmpPath, processedHtml);
61
- return tmpPath;
69
+ await fs.writeFile(cachedPath, processedHtml);
70
+ return cachedPath;
62
71
  }
63
72
 
64
73
  const HEAD_MARKER = '<meta name="HADARS_HEAD">';
65
74
  const BODY_MARKER = '<meta name="HADARS_BODY">';
66
75
 
67
- // Resolve renderToString from react-dom/server in the project's node_modules.
68
- let _renderToString: ((element: any) => string) | null = null;
69
- async function getRenderToString(): Promise<(element: any) => string> {
70
- if (!_renderToString) {
71
- const req = createRequire(pathMod.resolve(process.cwd(), '__hadars_fake__.js'));
72
- const resolved = req.resolve('react-dom/server');
73
- const mod = await import(pathToFileURL(resolved).href);
74
- _renderToString = mod.renderToString;
75
- }
76
- return _renderToString!;
77
- }
76
+ import { renderToString as slimRenderToString } from './slim-react/index';
78
77
 
79
78
  // Round-robin thread pool for SSR rendering — used on Bun/Deno where
80
79
  // node:cluster is not available but node:worker_threads is.
@@ -201,26 +200,21 @@ async function buildSsrResponse(
201
200
  getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
202
201
  unsuspendForRender: any,
203
202
  ): Promise<Response> {
204
- // Pre-load renderer before starting the stream so the set→call→clear
205
- // sequence around __hadarsUnsuspend is fully synchronous (no await between them).
206
- const renderToString = await getRenderToString();
207
-
208
203
  const responseStream = new ReadableStream({
209
204
  async start(controller) {
210
205
  const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
211
206
  // Flush the shell (precontentHtml) immediately so the browser can
212
207
  // start loading CSS/fonts before renderToString blocks the thread.
213
208
  controller.enqueue(encoder.encode(precontentHtml));
214
- await Promise.resolve(); // yield to let the runtime flush the shell chunk
215
209
 
216
- // set → call (synchronous) → clear: no await in between, safe under concurrency
217
210
  let bodyHtml: string;
218
211
  try {
219
212
  (globalThis as any).__hadarsUnsuspend = unsuspendForRender;
220
- bodyHtml = renderToString(ReactPage);
213
+ bodyHtml = await slimRenderToString(ReactPage);
221
214
  } finally {
222
215
  (globalThis as any).__hadarsUnsuspend = null;
223
216
  }
217
+ bodyHtml = processSegmentCache(bodyHtml);
224
218
  controller.enqueue(encoder.encode(bodyHtml + postContent));
225
219
  controller.close();
226
220
  },
@@ -395,6 +389,11 @@ const getSuffix = (mode: Mode) => mode === 'development' ? `?v=${Date.now()}` :
395
389
 
396
390
  const HadarsFolder = './.hadars';
397
391
  const StaticPath = `${HadarsFolder}/static`;
392
+ // Dedicated temp directory — keeps all hadars temp files out of the root of
393
+ // os.tmpdir() so rspack's file watcher doesn't traverse unrelated system files
394
+ // (e.g. Steam/Chrome shared-memory device files) in that directory.
395
+ const HADARS_TMP_DIR = pathMod.join(os.tmpdir(), 'hadars');
396
+ const ensureHadarsTmpDir = () => fs.mkdir(HADARS_TMP_DIR, { recursive: true });
398
397
 
399
398
  const validateOptions = (options: HadarsRuntimeOptions) => {
400
399
  if (!options.entry) {
@@ -481,7 +480,8 @@ export const dev = async (options: HadarsRuntimeOptions) => {
481
480
  throw err;
482
481
  }
483
482
 
484
- const tmpFilePath = pathMod.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
483
+ await ensureHadarsTmpDir();
484
+ const tmpFilePath = pathMod.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
485
485
  await fs.writeFile(tmpFilePath, clientScript);
486
486
 
487
487
  // SSR live-reload id to force re-import
@@ -601,27 +601,32 @@ export const dev = async (options: HadarsRuntimeOptions) => {
601
601
  }
602
602
  })();
603
603
 
604
- // Wait for both client and SSR builds to finish in parallel.
605
- await Promise.all([clientBuildDone, ssrBuildDone]);
606
-
607
- // Continue reading stdout to forward logs and pick up SSR rebuild signals.
608
- if (stdoutReader) {
609
- const reader = stdoutReader as ReadableStreamDefaultReader<Uint8Array>;
610
- (async () => {
611
- try {
612
- while (true) {
613
- const { done, value } = await reader.read();
614
- if (done) break;
615
- const chunk = decoder.decode(value, { stream: true });
616
- try { process.stdout.write(chunk); } catch (e) { }
617
- if (chunk.includes(rebuildMarker)) {
618
- ssrBuildId = crypto.randomBytes(4).toString('hex');
619
- console.log('[hadars] SSR bundle updated, build id:', ssrBuildId);
604
+ // Both builds run in parallel this promise resolves when they're both done.
605
+ // We do NOT await it here; the server starts immediately below so that the
606
+ // port is bound right away. Incoming requests await this promise before
607
+ // processing, so they hold in-flight and all resolve together once ready.
608
+ const readyPromise = Promise.all([clientBuildDone, ssrBuildDone]);
609
+
610
+ readyPromise.then(() => {
611
+ // Continue reading stdout to forward logs and pick up SSR rebuild signals.
612
+ if (stdoutReader) {
613
+ const reader = stdoutReader as ReadableStreamDefaultReader<Uint8Array>;
614
+ (async () => {
615
+ try {
616
+ while (true) {
617
+ const { done, value } = await reader.read();
618
+ if (done) break;
619
+ const chunk = decoder.decode(value, { stream: true });
620
+ try { process.stdout.write(chunk); } catch (e) { }
621
+ if (chunk.includes(rebuildMarker)) {
622
+ ssrBuildId = crypto.randomBytes(4).toString('hex');
623
+ console.log('[hadars] SSR bundle updated, build id:', ssrBuildId);
624
+ }
620
625
  }
621
- }
622
- } catch (e) { }
623
- })();
624
- }
626
+ } catch (e) { }
627
+ })();
628
+ }
629
+ });
625
630
 
626
631
  // Forward stderr asynchronously
627
632
  (async () => {
@@ -636,10 +641,12 @@ export const dev = async (options: HadarsRuntimeOptions) => {
636
641
  })();
637
642
 
638
643
  const getPrecontentHtml = makePrecontentHtmlGetter(
639
- fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8')
644
+ readyPromise.then(() => fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8'))
640
645
  );
641
646
 
642
647
  await serve(port, async (req, ctx) => {
648
+ // Hold requests until both builds are ready. Once resolved this is a no-op.
649
+ await readyPromise;
643
650
  const request = parseRequest(req);
644
651
  if (handler) {
645
652
  const res = await handler(request);
@@ -675,7 +682,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
675
682
  getFinalProps,
676
683
  } = (await import(importPath)) as HadarsEntryModule<any>;
677
684
 
678
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
685
+ const { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
679
686
  document: {
680
687
  body: Component as React.FC<HadarsProps<object>>,
681
688
  lang: 'en',
@@ -685,7 +692,6 @@ export const dev = async (options: HadarsRuntimeOptions) => {
685
692
  },
686
693
  });
687
694
 
688
- const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
689
695
  return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
690
696
  } catch (err: any) {
691
697
  console.error('[hadars] SSR render error:', err);
@@ -716,7 +722,8 @@ export const build = async (options: HadarsRuntimeOptions) => {
716
722
  .replace('$_MOD_PATH$', entry + `?v=${Date.now()}`);
717
723
  }
718
724
 
719
- const tmpFilePath = pathMod.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
725
+ await ensureHadarsTmpDir();
726
+ const tmpFilePath = pathMod.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
720
727
  await fs.writeFile(tmpFilePath, clientScript);
721
728
 
722
729
  // Pre-process the HTML template's <style> blocks through PostCSS (e.g. Tailwind).
@@ -862,7 +869,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
862
869
  });
863
870
  }
864
871
 
865
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
872
+ const { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
866
873
  document: {
867
874
  body: Component as React.FC<HadarsProps<object>>,
868
875
  lang: 'en',
@@ -872,7 +879,6 @@ export const run = async (options: HadarsRuntimeOptions) => {
872
879
  },
873
880
  });
874
881
 
875
- const unsuspend = (renderPayload.appProps.context as any)?._unsuspend ?? null;
876
882
  return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
877
883
  } catch (err: any) {
878
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
+ }