@unhead/react 3.0.0-beta.4 → 3.0.0-beta.5

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/client.d.mts CHANGED
@@ -9,6 +9,6 @@ declare function createHead(options?: CreateClientHeadOptions): Unhead;
9
9
  declare function UnheadProvider({ children, head }: {
10
10
  children: ReactNode;
11
11
  head?: ReturnType<typeof createHead>;
12
- }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead> | null>>;
12
+ }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead, unknown> | null>>;
13
13
 
14
14
  export { UnheadProvider, createHead };
package/dist/client.d.ts CHANGED
@@ -9,6 +9,6 @@ declare function createHead(options?: CreateClientHeadOptions): Unhead;
9
9
  declare function UnheadProvider({ children, head }: {
10
10
  children: ReactNode;
11
11
  head?: ReturnType<typeof createHead>;
12
- }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead> | null>>;
12
+ }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead, unknown> | null>>;
13
13
 
14
14
  export { UnheadProvider, createHead };
package/dist/client.mjs CHANGED
@@ -1,15 +1,13 @@
1
1
  import { createElement } from 'react';
2
- import { createHead as createHead$1, createDebouncedFn, renderDOMHead } from 'unhead/client';
2
+ import { createDomRenderer, createDebouncedFn, createHead as createHead$1 } from 'unhead/client';
3
3
  export { renderDOMHead } from 'unhead/client';
4
4
  import { U as UnheadContext } from './shared/react.DF9T1fqs.mjs';
5
5
 
6
6
  function createHead(options = {}) {
7
- const head = createHead$1({
8
- domOptions: {
9
- render: createDebouncedFn(() => renderDOMHead(head), (fn) => setTimeout(fn, 0))
10
- },
11
- ...options
12
- });
7
+ const domRenderer = createDomRenderer();
8
+ let head;
9
+ const debouncedRenderer = createDebouncedFn(() => domRenderer(head), (fn) => setTimeout(fn, 0));
10
+ head = createHead$1({ render: debouncedRenderer, ...options });
13
11
  return head;
14
12
  }
15
13
  function UnheadProvider({ children, head }) {
package/dist/server.d.mts CHANGED
@@ -3,11 +3,11 @@ import { ReactNode } from 'react';
3
3
  import * as unhead_types from 'unhead/types';
4
4
  import { Unhead } from 'unhead/types';
5
5
  export { CreateServerHeadOptions, SSRHeadPayload, Unhead } from 'unhead/types';
6
- export { createHead, extractUnheadInputFromHtml, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
6
+ export { createHead, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
7
7
 
8
8
  declare function UnheadProvider({ children, value }: {
9
9
  children: ReactNode;
10
10
  value: Unhead;
11
- }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead> | null>>;
11
+ }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead, unknown> | null>>;
12
12
 
13
13
  export { UnheadProvider };
package/dist/server.d.ts CHANGED
@@ -3,11 +3,11 @@ import { ReactNode } from 'react';
3
3
  import * as unhead_types from 'unhead/types';
4
4
  import { Unhead } from 'unhead/types';
5
5
  export { CreateServerHeadOptions, SSRHeadPayload, Unhead } from 'unhead/types';
6
- export { createHead, extractUnheadInputFromHtml, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
6
+ export { createHead, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
7
7
 
8
8
  declare function UnheadProvider({ children, value }: {
9
9
  children: ReactNode;
10
10
  value: Unhead;
11
- }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead> | null>>;
11
+ }): react.FunctionComponentElement<react.ProviderProps<Unhead<unhead_types.ResolvableHead, unknown> | null>>;
12
12
 
13
13
  export { UnheadProvider };
package/dist/server.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createElement } from 'react';
2
2
  import { U as UnheadContext } from './shared/react.DF9T1fqs.mjs';
3
- export { createHead, extractUnheadInputFromHtml, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
3
+ export { createHead, renderSSRHead, transformHtmlTemplate } from 'unhead/server';
4
4
 
5
5
  function UnheadProvider({ children, value }) {
6
6
  return createElement(UnheadContext.Provider, { value }, children);
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from 'react';
2
+ import { Unhead } from 'unhead/types';
3
+ export { CreateStreamableClientHeadOptions, UnheadStreamQueue, createStreamableHead } from 'unhead/stream/client';
4
+
5
+ declare function UnheadProvider({ value, children }: {
6
+ value: Unhead;
7
+ children: ReactNode;
8
+ }): ReactNode;
9
+ /**
10
+ * Client-side HeadStream - renders empty script with suppressHydrationWarning
11
+ * to match server-side structure without hydration mismatch errors.
12
+ */
13
+ declare function HeadStream(): ReactNode;
14
+
15
+ export { HeadStream, UnheadProvider };
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from 'react';
2
+ import { Unhead } from 'unhead/types';
3
+ export { CreateStreamableClientHeadOptions, UnheadStreamQueue, createStreamableHead } from 'unhead/stream/client';
4
+
5
+ declare function UnheadProvider({ value, children }: {
6
+ value: Unhead;
7
+ children: ReactNode;
8
+ }): ReactNode;
9
+ /**
10
+ * Client-side HeadStream - renders empty script with suppressHydrationWarning
11
+ * to match server-side structure without hydration mismatch errors.
12
+ */
13
+ declare function HeadStream(): ReactNode;
14
+
15
+ export { HeadStream, UnheadProvider };
@@ -0,0 +1,12 @@
1
+ import { createElement } from 'react';
2
+ import { U as UnheadContext } from '../shared/react.DF9T1fqs.mjs';
3
+ export { createStreamableHead } from 'unhead/stream/client';
4
+
5
+ function UnheadProvider({ value, children }) {
6
+ return createElement(UnheadContext.Provider, { value }, children);
7
+ }
8
+ function HeadStream() {
9
+ return createElement("script", { suppressHydrationWarning: true });
10
+ }
11
+
12
+ export { HeadStream, UnheadProvider };
@@ -0,0 +1,56 @@
1
+ import { Writable } from 'node:stream';
2
+ import { ReactNode } from 'react';
3
+ import { StreamableHeadContext, CreateStreamableServerHeadOptions } from 'unhead/stream/server';
4
+ export { BaseStreamableHeadContext, CreateStreamableServerHeadOptions, StreamableHeadContext, StreamingTemplateParts, WebStreamableHeadContext, prepareStreamingTemplate, renderSSRHeadShell, renderSSRHeadSuspenseChunk, wrapStream } from 'unhead/stream/server';
5
+ import { Unhead, ResolvableHead } from 'unhead/types';
6
+
7
+ declare function UnheadProvider({ value, children }: {
8
+ value: Unhead;
9
+ children: ReactNode;
10
+ }): ReactNode;
11
+ /**
12
+ * Streaming head component for React.
13
+ * Place inside Suspense boundaries after async components that use useHead.
14
+ */
15
+ declare function HeadStream(): ReactNode;
16
+ /**
17
+ * A pipe function from React's renderToPipeableStream
18
+ */
19
+ type ReactPipeFunction = (writable: Writable) => void;
20
+ /**
21
+ * React-specific context returned by createStreamableHead.
22
+ * Extends core StreamableHeadContext with React's wrap helper.
23
+ */
24
+ interface ReactStreamableHeadContext<T = ResolvableHead> extends Pick<StreamableHeadContext<T>, 'head' | 'onShellReady'> {
25
+ /**
26
+ * Wrap React's pipe function to handle head injection automatically
27
+ * @param pipe - The pipe function from renderToPipeableStream
28
+ * @param template - The HTML template (from Vite's transformIndexHtml)
29
+ * @returns A new pipe function that handles shell rendering
30
+ */
31
+ wrap: (pipe: ReactPipeFunction, template: string) => (writable: Writable) => void;
32
+ }
33
+ /**
34
+ * Creates a head instance configured for React streaming SSR.
35
+ *
36
+ * Returns a context with:
37
+ * - `head`: The Unhead instance for UnheadProvider
38
+ * - `onShellReady`: Callback to pass to renderToPipeableStream
39
+ * - `wrap`: Wraps React's pipe to handle head injection
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * const { head, onShellReady, wrap } = createStreamableHead()
44
+ *
45
+ * const { pipe } = renderToPipeableStream(
46
+ * <UnheadProvider value={head}><App /></UnheadProvider>,
47
+ * { onShellReady }
48
+ * )
49
+ *
50
+ * return { pipe: wrap(pipe, template) }
51
+ * ```
52
+ */
53
+ declare function createStreamableHead<T = ResolvableHead>(options?: CreateStreamableServerHeadOptions): ReactStreamableHeadContext<T>;
54
+
55
+ export { HeadStream, UnheadProvider, createStreamableHead };
56
+ export type { ReactStreamableHeadContext };
@@ -0,0 +1,56 @@
1
+ import { Writable } from 'node:stream';
2
+ import { ReactNode } from 'react';
3
+ import { StreamableHeadContext, CreateStreamableServerHeadOptions } from 'unhead/stream/server';
4
+ export { BaseStreamableHeadContext, CreateStreamableServerHeadOptions, StreamableHeadContext, StreamingTemplateParts, WebStreamableHeadContext, prepareStreamingTemplate, renderSSRHeadShell, renderSSRHeadSuspenseChunk, wrapStream } from 'unhead/stream/server';
5
+ import { Unhead, ResolvableHead } from 'unhead/types';
6
+
7
+ declare function UnheadProvider({ value, children }: {
8
+ value: Unhead;
9
+ children: ReactNode;
10
+ }): ReactNode;
11
+ /**
12
+ * Streaming head component for React.
13
+ * Place inside Suspense boundaries after async components that use useHead.
14
+ */
15
+ declare function HeadStream(): ReactNode;
16
+ /**
17
+ * A pipe function from React's renderToPipeableStream
18
+ */
19
+ type ReactPipeFunction = (writable: Writable) => void;
20
+ /**
21
+ * React-specific context returned by createStreamableHead.
22
+ * Extends core StreamableHeadContext with React's wrap helper.
23
+ */
24
+ interface ReactStreamableHeadContext<T = ResolvableHead> extends Pick<StreamableHeadContext<T>, 'head' | 'onShellReady'> {
25
+ /**
26
+ * Wrap React's pipe function to handle head injection automatically
27
+ * @param pipe - The pipe function from renderToPipeableStream
28
+ * @param template - The HTML template (from Vite's transformIndexHtml)
29
+ * @returns A new pipe function that handles shell rendering
30
+ */
31
+ wrap: (pipe: ReactPipeFunction, template: string) => (writable: Writable) => void;
32
+ }
33
+ /**
34
+ * Creates a head instance configured for React streaming SSR.
35
+ *
36
+ * Returns a context with:
37
+ * - `head`: The Unhead instance for UnheadProvider
38
+ * - `onShellReady`: Callback to pass to renderToPipeableStream
39
+ * - `wrap`: Wraps React's pipe to handle head injection
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * const { head, onShellReady, wrap } = createStreamableHead()
44
+ *
45
+ * const { pipe } = renderToPipeableStream(
46
+ * <UnheadProvider value={head}><App /></UnheadProvider>,
47
+ * { onShellReady }
48
+ * )
49
+ *
50
+ * return { pipe: wrap(pipe, template) }
51
+ * ```
52
+ */
53
+ declare function createStreamableHead<T = ResolvableHead>(options?: CreateStreamableServerHeadOptions): ReactStreamableHeadContext<T>;
54
+
55
+ export { HeadStream, UnheadProvider, createStreamableHead };
56
+ export type { ReactStreamableHeadContext };
@@ -0,0 +1,51 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { createElement, useContext } from 'react';
3
+ import { renderSSRHeadSuspenseChunk, createStreamableHead as createStreamableHead$1, prepareStreamingTemplate } from 'unhead/stream/server';
4
+ export { prepareStreamingTemplate, renderSSRHeadShell, renderSSRHeadSuspenseChunk, wrapStream } from 'unhead/stream/server';
5
+ import { U as UnheadContext } from '../shared/react.DF9T1fqs.mjs';
6
+
7
+ function UnheadProvider({ value, children }) {
8
+ return createElement(UnheadContext.Provider, { value }, children);
9
+ }
10
+ function HeadStream() {
11
+ const head = useContext(UnheadContext);
12
+ if (!head) {
13
+ throw new Error("HeadStream: head context not found");
14
+ }
15
+ const update = renderSSRHeadSuspenseChunk(head);
16
+ return createElement("script", {
17
+ suppressHydrationWarning: true,
18
+ dangerouslySetInnerHTML: update ? { __html: update } : void 0
19
+ });
20
+ }
21
+ function createStreamableHead(options = {}) {
22
+ const { head, onShellReady, shellReady } = createStreamableHead$1(options);
23
+ return {
24
+ head,
25
+ onShellReady,
26
+ wrap: (pipe, template) => {
27
+ return (writable) => {
28
+ shellReady.then(async () => {
29
+ try {
30
+ const { shell, end } = await prepareStreamingTemplate(head, template);
31
+ writable.write(shell);
32
+ const passthrough = new PassThrough();
33
+ passthrough.on("data", (chunk) => writable.write(chunk));
34
+ passthrough.on("end", () => {
35
+ writable.write(end);
36
+ writable.end();
37
+ });
38
+ passthrough.on("error", (err) => {
39
+ writable.destroy(err);
40
+ });
41
+ pipe(passthrough);
42
+ } catch (err) {
43
+ writable.destroy(err instanceof Error ? err : new Error(String(err)));
44
+ }
45
+ });
46
+ };
47
+ }
48
+ };
49
+ }
50
+
51
+ export { HeadStream, UnheadProvider, createStreamableHead };
@@ -0,0 +1,27 @@
1
+ import * as vite from 'vite';
2
+ import { StreamingPluginOptions } from 'unhead/stream/vite';
3
+
4
+ /**
5
+ * Vite plugin for React streaming SSR support.
6
+ * Automatically injects HeadStream components into React components that use useHead hooks.
7
+ *
8
+ * @returns Vite plugin configuration object with:
9
+ * - `name`: Plugin identifier
10
+ * - `enforce`: Plugin execution order ('pre')
11
+ * - `transform`: Transform hook for processing JSX/TSX files
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * // vite.config.ts
16
+ * import { unheadReactPlugin } from '@unhead/react/stream/vite'
17
+ *
18
+ * export default {
19
+ * plugins: [
20
+ * unheadReactPlugin()
21
+ * ]
22
+ * }
23
+ * ```
24
+ */
25
+ declare function unheadReactPlugin(options?: Pick<StreamingPluginOptions, 'mode'>): vite.Plugin<any>;
26
+
27
+ export { unheadReactPlugin as default, unheadReactPlugin };
@@ -0,0 +1,27 @@
1
+ import * as vite from 'vite';
2
+ import { StreamingPluginOptions } from 'unhead/stream/vite';
3
+
4
+ /**
5
+ * Vite plugin for React streaming SSR support.
6
+ * Automatically injects HeadStream components into React components that use useHead hooks.
7
+ *
8
+ * @returns Vite plugin configuration object with:
9
+ * - `name`: Plugin identifier
10
+ * - `enforce`: Plugin execution order ('pre')
11
+ * - `transform`: Transform hook for processing JSX/TSX files
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * // vite.config.ts
16
+ * import { unheadReactPlugin } from '@unhead/react/stream/vite'
17
+ *
18
+ * export default {
19
+ * plugins: [
20
+ * unheadReactPlugin()
21
+ * ]
22
+ * }
23
+ * ```
24
+ */
25
+ declare function unheadReactPlugin(options?: Pick<StreamingPluginOptions, 'mode'>): vite.Plugin<any>;
26
+
27
+ export { unheadReactPlugin as default, unheadReactPlugin };
@@ -0,0 +1,86 @@
1
+ import MagicString from 'magic-string';
2
+ import { findStaticImports } from 'mlly';
3
+ import { parseSync, Visitor } from 'oxc-parser';
4
+ import { createStreamingPlugin } from 'unhead/stream/vite';
5
+
6
+ function transform(code, id, isSSR, s) {
7
+ const lang = id.endsWith(".tsx") ? "tsx" : id.endsWith(".jsx") ? "jsx" : "tsx";
8
+ const result = parseSync(id, code, { lang });
9
+ if (result.errors.length > 0)
10
+ return false;
11
+ const returns = [];
12
+ const visitor = new Visitor({
13
+ FunctionDeclaration: (node) => processFunction(node),
14
+ FunctionExpression: (node) => processFunction(node),
15
+ ArrowFunctionExpression: (node) => processFunction(node)
16
+ });
17
+ function processFunction(node) {
18
+ if (!node.body)
19
+ return;
20
+ const bodyCode = code.slice(node.body.start, node.body.end);
21
+ if (!bodyCode.includes("useHead") && !bodyCode.includes("useSeoMeta") && !bodyCode.includes("useHeadSafe"))
22
+ return;
23
+ if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") {
24
+ returns.push({ jsxStart: node.body.start, jsxEnd: node.body.end });
25
+ return;
26
+ }
27
+ const innerVisitor = new Visitor({
28
+ ReturnStatement(innerNode) {
29
+ if (!innerNode.argument)
30
+ return;
31
+ let arg = innerNode.argument;
32
+ if (arg.type === "ParenthesizedExpression" && arg.expression)
33
+ arg = arg.expression;
34
+ if (arg.type === "JSXElement" || arg.type === "JSXFragment")
35
+ returns.push({ jsxStart: arg.start, jsxEnd: arg.end });
36
+ }
37
+ });
38
+ innerVisitor.visit(node.body);
39
+ }
40
+ visitor.visit(result.program);
41
+ if (returns.length === 0)
42
+ return false;
43
+ returns.sort((a, b) => b.jsxStart - a.jsxStart);
44
+ for (const ret of returns) {
45
+ const jsxCode = code.slice(ret.jsxStart, ret.jsxEnd);
46
+ s.overwrite(ret.jsxStart, ret.jsxEnd, `<><HeadStream />${jsxCode}</>`);
47
+ }
48
+ const importPath = isSSR ? "@unhead/react/stream/server" : "@unhead/react/stream/client";
49
+ const imports = findStaticImports(code);
50
+ const existing = imports.find((i) => i.specifier === importPath);
51
+ if (existing) {
52
+ if (!existing.imports?.includes("HeadStream")) {
53
+ const inner = existing.imports?.replace(/^\{\s*|\s*\}\s*$/g, "").trim() || "";
54
+ s.overwrite(existing.start, existing.end, `import { ${inner ? `${inner}, ` : ""}HeadStream } from '${importPath}'
55
+ `);
56
+ }
57
+ } else {
58
+ const last = imports[imports.length - 1];
59
+ if (last)
60
+ s.appendLeft(last.end, `import { HeadStream } from '${importPath}'
61
+ `);
62
+ else
63
+ s.prepend(`import { HeadStream } from '${importPath}'
64
+ `);
65
+ }
66
+ return true;
67
+ }
68
+ function unheadReactPlugin(options) {
69
+ return createStreamingPlugin({
70
+ framework: "@unhead/react",
71
+ mode: options?.mode,
72
+ transform(code, id, opts) {
73
+ if (!/\.[jt]sx$/.test(id))
74
+ return null;
75
+ const s = new MagicString(code);
76
+ if (!transform(code, id, opts?.ssr ?? false, s))
77
+ return null;
78
+ return {
79
+ code: s.toString(),
80
+ map: s.generateMap({ includeContent: true, source: id })
81
+ };
82
+ }
83
+ });
84
+ }
85
+
86
+ export { unheadReactPlugin as default, unheadReactPlugin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unhead/react",
3
3
  "type": "module",
4
- "version": "3.0.0-beta.4",
4
+ "version": "3.0.0-beta.5",
5
5
  "description": "Full-stack <head> manager built for React.",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
@@ -29,6 +29,14 @@
29
29
  "types": "./dist/client.d.ts",
30
30
  "default": "./dist/client.mjs"
31
31
  },
32
+ "./stream/server": {
33
+ "types": "./dist/stream/server.d.ts",
34
+ "default": "./dist/stream/server.mjs"
35
+ },
36
+ "./stream/client": {
37
+ "types": "./dist/stream/client.d.ts",
38
+ "default": "./dist/stream/client.mjs"
39
+ },
32
40
  "./utils": {
33
41
  "types": "./dist/utils.d.ts",
34
42
  "default": "./dist/utils.mjs"
@@ -36,6 +44,10 @@
36
44
  "./plugins": {
37
45
  "types": "./dist/plugins.d.ts",
38
46
  "default": "./dist/plugins.mjs"
47
+ },
48
+ "./stream/vite": {
49
+ "types": "./dist/stream/vite.d.ts",
50
+ "default": "./dist/stream/vite.mjs"
39
51
  }
40
52
  },
41
53
  "main": "dist/index.mjs",
@@ -49,11 +61,20 @@
49
61
  "client": [
50
62
  "dist/client"
51
63
  ],
64
+ "stream/server": [
65
+ "dist/stream/server"
66
+ ],
67
+ "stream/client": [
68
+ "dist/stream/client"
69
+ ],
52
70
  "plugins": [
53
71
  "dist/plugins"
54
72
  ],
55
73
  "utils": [
56
74
  "dist/utils"
75
+ ],
76
+ "stream/vite": [
77
+ "dist/stream/vite"
57
78
  ]
58
79
  }
59
80
  },
@@ -62,7 +83,13 @@
62
83
  "dist"
63
84
  ],
64
85
  "peerDependencies": {
65
- "react": ">=18.3.1"
86
+ "react": ">=18.3.1",
87
+ "vite": ">=6"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "vite": {
91
+ "optional": true
92
+ }
66
93
  },
67
94
  "build": {
68
95
  "external": [
@@ -70,14 +97,18 @@
70
97
  ]
71
98
  },
72
99
  "dependencies": {
73
- "unhead": "3.0.0-beta.4"
100
+ "magic-string": "^0.30.21",
101
+ "mlly": "^1.8.0",
102
+ "oxc-parser": "^0.106.0",
103
+ "unhead": "3.0.0-beta.5"
74
104
  },
75
105
  "devDependencies": {
76
106
  "@testing-library/react": "^16.3.1",
77
107
  "@types/react": "^19.2.7",
78
108
  "@types/react-dom": "^19.2.3",
79
109
  "react": "^19.2.3",
80
- "react-dom": "^19.2.3"
110
+ "react-dom": "^19.2.3",
111
+ "vite": "7.2.2"
81
112
  },
82
113
  "scripts": {
83
114
  "build": "unbuild",