unhead 3.0.4 → 3.1.0

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.
Files changed (53) hide show
  1. package/dist/client.d.mts +5 -5
  2. package/dist/client.d.ts +5 -5
  3. package/dist/index.d.mts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/legacy.d.mts +3 -3
  6. package/dist/legacy.d.ts +3 -3
  7. package/dist/parser.d.mts +1 -1
  8. package/dist/parser.d.ts +1 -1
  9. package/dist/plugins.d.mts +6 -33
  10. package/dist/plugins.d.ts +6 -33
  11. package/dist/plugins.mjs +35 -199
  12. package/dist/scripts.d.mts +4 -4
  13. package/dist/scripts.d.ts +4 -4
  14. package/dist/server.d.mts +4 -4
  15. package/dist/server.d.ts +4 -4
  16. package/dist/shared/{unhead.CHsGwxB0.d.ts → unhead.-D8hRpkn.d.ts} +2 -2
  17. package/dist/shared/{unhead.BfINO8Du.d.mts → unhead.B2jfOxG1.d.mts} +2 -2
  18. package/dist/shared/{unhead.D91IvQ_J.d.mts → unhead.B7bBMqva.d.mts} +1 -1
  19. package/dist/shared/{unhead.DCXRrsx_.d.ts → unhead.BRwJvCZb.d.ts} +2 -2
  20. package/dist/shared/{unhead.LatFr5s0.d.mts → unhead.BX9134DF.d.mts} +1 -1
  21. package/dist/shared/{unhead.m4TuRTvV.d.mts → unhead.BoZ-Ul8T.d.mts} +1 -1
  22. package/dist/shared/{unhead.DvqMLXpO.d.mts → unhead.ClE7lB2Z.d.mts} +1 -1
  23. package/dist/shared/{unhead.DoIxTwSF.d.ts → unhead.CqVawd25.d.ts} +1 -1
  24. package/dist/shared/{unhead.Bbk6Q9rK.d.mts → unhead.Cv5yrrUd.d.mts} +2 -2
  25. package/dist/shared/{unhead.CGloe9Qc.d.mts → unhead.Dc5Pn4qI.d.mts} +2 -2
  26. package/dist/shared/{unhead.9aHQrEvB.d.ts → unhead.DdarjSpi.d.ts} +2 -2
  27. package/dist/shared/{unhead.BcynQ3qJ.d.mts → unhead.DeoGMp34.d.mts} +1 -1
  28. package/dist/shared/{unhead.BcynQ3qJ.d.ts → unhead.DeoGMp34.d.ts} +1 -1
  29. package/dist/shared/unhead.Dl_lRDXb.d.mts +38 -0
  30. package/dist/shared/unhead.Dl_lRDXb.d.ts +38 -0
  31. package/dist/shared/{unhead.Ogog_XUX.d.ts → unhead.cXt2Dgt5.d.ts} +1 -1
  32. package/dist/shared/unhead.ebqUBTt1.mjs +513 -0
  33. package/dist/shared/{unhead.BmdY3z1V.d.ts → unhead.gui9LmZS.d.ts} +1 -1
  34. package/dist/shared/{unhead.CvqH1vcy.d.ts → unhead.q49lHHFN.d.ts} +1 -1
  35. package/dist/stream/client.d.mts +24 -5
  36. package/dist/stream/client.d.ts +24 -5
  37. package/dist/stream/server.d.mts +9 -6
  38. package/dist/stream/server.d.ts +9 -6
  39. package/dist/stream/server.mjs +15 -4
  40. package/dist/stream/unplugin.d.mts +81 -0
  41. package/dist/stream/unplugin.d.ts +81 -0
  42. package/dist/stream/unplugin.mjs +166 -0
  43. package/dist/stream/vite.d.mts +19 -27
  44. package/dist/stream/vite.d.ts +19 -27
  45. package/dist/stream/vite.mjs +6 -78
  46. package/dist/types.d.mts +6 -6
  47. package/dist/types.d.ts +6 -6
  48. package/dist/utils.d.mts +2 -2
  49. package/dist/utils.d.ts +2 -2
  50. package/dist/validate.d.mts +236 -0
  51. package/dist/validate.d.ts +236 -0
  52. package/dist/validate.mjs +49 -0
  53. package/package.json +20 -4
@@ -1,5 +1,5 @@
1
1
  import { HookableCore } from 'hookable';
2
- import { r as GenericScript, aK as ScriptHttpEvents, D as DataKeys, _ as MaybeEventFnHandlers, x as HttpEventAttributes, aH as SchemaAugmentations, t as HeadTag, at as ResolvableHead, aT as TagPosition, aU as TagPriority, ao as ProcessesTemplateParams, aG as ResolvesDuplicates, aW as TemplateParams } from './unhead.BcynQ3qJ.js';
2
+ import { t as GenericScript, aN as ScriptHttpEvents, D as DataKeys, a1 as MaybeEventFnHandlers, z as HttpEventAttributes, aK as SchemaAugmentations, v as HeadTag, aw as ResolvableHead, aX as TagPosition, aY as TagPriority, ar as ProcessesTemplateParams, aJ as ResolvesDuplicates, a_ as TemplateParams } from './unhead.DeoGMp34.js';
3
3
 
4
4
  type UseScriptStatus = 'awaitingLoad' | 'loading' | 'loaded' | 'error' | 'removed';
5
5
  type UseScriptContext<T extends Record<symbol | string, any>> = ScriptInstance<T>;
@@ -1,4 +1,4 @@
1
- import { aq as RawInput, at as ResolvableHead, aF as ResolvableValue, ay as ResolvableProperties, G as GenericLink, D as DataKeys, aH as SchemaAugmentations, a3 as MetaGeneric, r as GenericScript, a_ as UnheadHtmlAttributes, aZ as UnheadBodyAttributesWithoutEvents } from './unhead.BcynQ3qJ.js';
1
+ import { at as RawInput, aw as ResolvableHead, aI as ResolvableValue, aB as ResolvableProperties, G as GenericLink, D as DataKeys, aK as SchemaAugmentations, a6 as MetaGeneric, t as GenericScript, b2 as UnheadHtmlAttributes, b1 as UnheadBodyAttributesWithoutEvents } from './unhead.DeoGMp34.js';
2
2
 
3
3
  type Base = RawInput<'base'>;
4
4
  type HtmlAttributes = RawInput<'htmlAttrs'>;
@@ -1,13 +1,32 @@
1
- import { C as ClientUnhead } from '../shared/unhead.Bbk6Q9rK.mjs';
2
- import { c as CreateClientHeadOptions, U as Unhead } from '../shared/unhead.m4TuRTvV.mjs';
3
- import { aM as SerializableHead, at as ResolvableHead } from '../shared/unhead.BcynQ3qJ.mjs';
1
+ import { C as ClientUnhead } from '../shared/unhead.Cv5yrrUd.mjs';
2
+ import { aP as SerializableHead, aw as ResolvableHead } from '../shared/unhead.DeoGMp34.mjs';
3
+ import { U as Unhead, c as CreateClientHeadOptions } from '../shared/unhead.BoZ-Ul8T.mjs';
4
4
  import 'hookable';
5
5
 
6
- interface UnheadStreamQueue {
6
+ /**
7
+ * Shape of the streaming queue written to `window[streamKey]` (default
8
+ * `window.__unhead__`) by the server-emitted bootstrap script. The client
9
+ * IIFE reads from it to replay queued entries, and the streaming client
10
+ * wraps the resulting head instance.
11
+ *
12
+ * Both the server bootstrap (`createBootstrapScript`) and the client
13
+ * (`createStreamableHead`, iife) must agree on this shape.
14
+ */
15
+ interface StreamingGlobal {
16
+ /** Queued entry batches pushed before the client IIFE took over. */
7
17
  _q: SerializableHead[][];
18
+ /** Resolved Unhead instance, populated once the IIFE initialises. */
8
19
  _head?: Unhead<any>;
20
+ /** True while framework hydration is in progress (client push suppression). */
21
+ _hydrationLocked?: () => boolean;
22
+ /** Push an entry batch onto the queue (pre-init) or the head (post-init). */
9
23
  push: (entries: SerializableHead[]) => void;
10
24
  }
25
+ /**
26
+ * @deprecated Use `StreamingGlobal` instead. Kept as an alias for back-compat.
27
+ */
28
+ type UnheadStreamQueue = StreamingGlobal;
29
+
11
30
  declare const DEFAULT_STREAM_KEY = "__unhead__";
12
31
  interface CreateStreamableClientHeadOptions extends Omit<CreateClientHeadOptions, 'render'> {
13
32
  streamKey?: string;
@@ -19,4 +38,4 @@ interface CreateStreamableClientHeadOptions extends Omit<CreateClientHeadOptions
19
38
  declare function createStreamableHead<T = ResolvableHead>(options?: CreateStreamableClientHeadOptions): ClientUnhead<T> | undefined;
20
39
 
21
40
  export { DEFAULT_STREAM_KEY, createStreamableHead };
22
- export type { CreateStreamableClientHeadOptions, UnheadStreamQueue };
41
+ export type { CreateStreamableClientHeadOptions, StreamingGlobal, UnheadStreamQueue };
@@ -1,13 +1,32 @@
1
- import { C as ClientUnhead } from '../shared/unhead.CHsGwxB0.js';
2
- import { c as CreateClientHeadOptions, U as Unhead } from '../shared/unhead.BmdY3z1V.js';
3
- import { aM as SerializableHead, at as ResolvableHead } from '../shared/unhead.BcynQ3qJ.js';
1
+ import { C as ClientUnhead } from '../shared/unhead.-D8hRpkn.js';
2
+ import { aP as SerializableHead, aw as ResolvableHead } from '../shared/unhead.DeoGMp34.js';
3
+ import { U as Unhead, c as CreateClientHeadOptions } from '../shared/unhead.gui9LmZS.js';
4
4
  import 'hookable';
5
5
 
6
- interface UnheadStreamQueue {
6
+ /**
7
+ * Shape of the streaming queue written to `window[streamKey]` (default
8
+ * `window.__unhead__`) by the server-emitted bootstrap script. The client
9
+ * IIFE reads from it to replay queued entries, and the streaming client
10
+ * wraps the resulting head instance.
11
+ *
12
+ * Both the server bootstrap (`createBootstrapScript`) and the client
13
+ * (`createStreamableHead`, iife) must agree on this shape.
14
+ */
15
+ interface StreamingGlobal {
16
+ /** Queued entry batches pushed before the client IIFE took over. */
7
17
  _q: SerializableHead[][];
18
+ /** Resolved Unhead instance, populated once the IIFE initialises. */
8
19
  _head?: Unhead<any>;
20
+ /** True while framework hydration is in progress (client push suppression). */
21
+ _hydrationLocked?: () => boolean;
22
+ /** Push an entry batch onto the queue (pre-init) or the head (post-init). */
9
23
  push: (entries: SerializableHead[]) => void;
10
24
  }
25
+ /**
26
+ * @deprecated Use `StreamingGlobal` instead. Kept as an alias for back-compat.
27
+ */
28
+ type UnheadStreamQueue = StreamingGlobal;
29
+
11
30
  declare const DEFAULT_STREAM_KEY = "__unhead__";
12
31
  interface CreateStreamableClientHeadOptions extends Omit<CreateClientHeadOptions, 'render'> {
13
32
  streamKey?: string;
@@ -19,4 +38,4 @@ interface CreateStreamableClientHeadOptions extends Omit<CreateClientHeadOptions
19
38
  declare function createStreamableHead<T = ResolvableHead>(options?: CreateStreamableClientHeadOptions): ClientUnhead<T> | undefined;
20
39
 
21
40
  export { DEFAULT_STREAM_KEY, createStreamableHead };
22
- export type { CreateStreamableClientHeadOptions, UnheadStreamQueue };
41
+ export type { CreateStreamableClientHeadOptions, StreamingGlobal, UnheadStreamQueue };
@@ -1,6 +1,6 @@
1
- import { S as ServerUnhead } from '../shared/unhead.BfINO8Du.mjs';
2
- import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.m4TuRTvV.mjs';
3
- import { at as ResolvableHead } from '../shared/unhead.BcynQ3qJ.mjs';
1
+ import { S as ServerUnhead } from '../shared/unhead.B2jfOxG1.mjs';
2
+ import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.BoZ-Ul8T.mjs';
3
+ import { aw as ResolvableHead } from '../shared/unhead.DeoGMp34.mjs';
4
4
  import 'hookable';
5
5
 
6
6
  /**
@@ -67,9 +67,10 @@ declare function createStreamableHead<T = ResolvableHead>(options?: CreateStream
67
67
  * use this directly to inject the bootstrap into your shell `<head>`.
68
68
  *
69
69
  * @param streamKey - The window property name for the stream queue (default: '__unhead__')
70
+ * @param nonce - Optional CSP nonce to stamp on the script tag
70
71
  * @returns An inline `<script>` tag string
71
72
  */
72
- declare function createBootstrapScript(streamKey?: string): string;
73
+ declare function createBootstrapScript(streamKey?: string, nonce?: string): string;
73
74
  /**
74
75
  * Renders the current head state and clears entries atomically.
75
76
  *
@@ -166,8 +167,10 @@ interface StreamingTemplateParts {
166
167
  /**
167
168
  * @experimental
168
169
  *
169
- * Prepares a template for streaming SSR by splitting it at body tag boundaries
170
- * and injecting head content into both parts.
170
+ * Prepares a template for streaming SSR by splitting it at the SSR outlet
171
+ * marker (`<!--app-html-->` / `<!--ssr-outlet-->`) when present, so the
172
+ * streamed app content lands inside the mount container. Falls back to
173
+ * splitting at body tag boundaries when no marker is found.
171
174
  *
172
175
  * This is the recommended way to handle streaming templates as it:
173
176
  * - Uses consistent template parsing (same as transformHtmlTemplateRaw)
@@ -1,6 +1,6 @@
1
- import { S as ServerUnhead } from '../shared/unhead.9aHQrEvB.js';
2
- import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.BmdY3z1V.js';
3
- import { at as ResolvableHead } from '../shared/unhead.BcynQ3qJ.js';
1
+ import { S as ServerUnhead } from '../shared/unhead.DdarjSpi.js';
2
+ import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.gui9LmZS.js';
3
+ import { aw as ResolvableHead } from '../shared/unhead.DeoGMp34.js';
4
4
  import 'hookable';
5
5
 
6
6
  /**
@@ -67,9 +67,10 @@ declare function createStreamableHead<T = ResolvableHead>(options?: CreateStream
67
67
  * use this directly to inject the bootstrap into your shell `<head>`.
68
68
  *
69
69
  * @param streamKey - The window property name for the stream queue (default: '__unhead__')
70
+ * @param nonce - Optional CSP nonce to stamp on the script tag
70
71
  * @returns An inline `<script>` tag string
71
72
  */
72
- declare function createBootstrapScript(streamKey?: string): string;
73
+ declare function createBootstrapScript(streamKey?: string, nonce?: string): string;
73
74
  /**
74
75
  * Renders the current head state and clears entries atomically.
75
76
  *
@@ -166,8 +167,10 @@ interface StreamingTemplateParts {
166
167
  /**
167
168
  * @experimental
168
169
  *
169
- * Prepares a template for streaming SSR by splitting it at body tag boundaries
170
- * and injecting head content into both parts.
170
+ * Prepares a template for streaming SSR by splitting it at the SSR outlet
171
+ * marker (`<!--app-html-->` / `<!--ssr-outlet-->`) when present, so the
172
+ * streamed app content lands inside the mount container. Falls back to
173
+ * splitting at body tag boundaries when no marker is found.
171
174
  *
172
175
  * This is the recommended way to handle streaming templates as it:
173
176
  * - Uses consistent template parsing (same as transformHtmlTemplateRaw)
@@ -42,9 +42,10 @@ function getStreamKey(head) {
42
42
  assertValidStreamKey(key);
43
43
  return key;
44
44
  }
45
- function createBootstrapScript(streamKey = DEFAULT_STREAM_KEY) {
45
+ function createBootstrapScript(streamKey = DEFAULT_STREAM_KEY, nonce) {
46
46
  assertValidStreamKey(streamKey);
47
- return `<script>window.${streamKey}={_q:[],push(e){this._q.push(e)}}<\/script>`;
47
+ const nonceAttr = nonce ? ` nonce="${nonce.replace(/"/g, "&quot;")}"` : "";
48
+ return `<script${nonceAttr}>window.${streamKey}={_q:[],push(e){this._q.push(e)}}<\/script>`;
48
49
  }
49
50
  function renderShell(head) {
50
51
  const result = head.render();
@@ -110,8 +111,18 @@ function prepareStreamingTemplate(head, template, preRenderedState) {
110
111
  const bodyEnd = parsed.indexes.bodyTagEnd;
111
112
  const bodyCloseStart = parsed.indexes.bodyCloseTagStart;
112
113
  if (bodyEnd >= 0 && bodyCloseStart >= 0) {
113
- const shellPart = template.substring(0, bodyEnd);
114
114
  const bodyInterior = template.substring(bodyEnd, bodyCloseStart);
115
+ const markerMatch = bodyInterior.match(/<!--\s*(?:app-html|ssr-outlet)\s*-->/);
116
+ let beforeStream;
117
+ let afterStream;
118
+ if (markerMatch) {
119
+ beforeStream = bodyInterior.substring(0, markerMatch.index);
120
+ afterStream = bodyInterior.substring(markerMatch.index + markerMatch[0].length);
121
+ } else {
122
+ beforeStream = "";
123
+ afterStream = bodyInterior;
124
+ }
125
+ const shellPart = template.substring(0, bodyEnd) + beforeStream;
115
126
  const endPart = template.substring(bodyCloseStart);
116
127
  const shellParsed = parseHtmlForIndexes(`${shellPart}</body></html>`);
117
128
  const shell = applyHeadToHtml(shellParsed, {
@@ -122,7 +133,7 @@ function prepareStreamingTemplate(head, template, preRenderedState) {
122
133
  }).replace("</body></html>", "");
123
134
  return {
124
135
  shell,
125
- end: bodyInterior + ssr.bodyTags + endPart
136
+ end: afterStream + ssr.bodyTags + endPart
126
137
  };
127
138
  }
128
139
  return {
@@ -0,0 +1,81 @@
1
+ import * as unplugin from 'unplugin';
2
+ import { UnpluginOptions } from 'unplugin';
3
+
4
+ declare const VIRTUAL_CLIENT_ID = "virtual:@unhead/streaming-client";
5
+ declare const VIRTUAL_IIFE_ID = "virtual:@unhead/streaming-iife.js";
6
+ type Nonce = string | (() => string | undefined);
7
+ interface StreamingPluginOptions {
8
+ /** Framework package e.g. '@unhead/vue' */
9
+ framework: string;
10
+ /** Plugin name (optional, defaults to `${framework}:streaming`) */
11
+ name?: string;
12
+ /**
13
+ * File extension filter for transform hook, e.g. /\.vue$/. Optional;
14
+ * only required by frameworks whose client streaming support relies on
15
+ * source-level AST injection (React/Solid/Svelte). Vue does not use it.
16
+ */
17
+ filter?: RegExp;
18
+ /** Transform handler called for files matching `filter`. */
19
+ transform?: (code: string, id: string, options?: {
20
+ ssr?: boolean;
21
+ }) => {
22
+ code: string;
23
+ map?: any;
24
+ } | null | undefined | void;
25
+ /**
26
+ * How to load the streaming client (vite-only, ignored on webpack/rspack/rollup where
27
+ * index.html injection isn't available; frameworks inject the iife themselves in SSR).
28
+ * - 'async' (default): Non-blocking external script. In dev served from a virtual
29
+ * module; in production emitted as a real asset chunk via `emitFile`.
30
+ * - 'inline': Inline the IIFE directly in HTML. Largest HTML, smallest TTFB,
31
+ * always safe in production. Recommended for streaming SSR.
32
+ * - 'module': ES module dynamic import of the client bootstrap. Vite rewrites the
33
+ * import path through its module graph so it survives production builds.
34
+ * @default 'async'
35
+ */
36
+ mode?: 'async' | 'inline' | 'module';
37
+ /**
38
+ * CSP nonce forwarded on every injected `<script>` tag. Pass a string or a
39
+ * function returning a string (useful when the nonce rotates per request).
40
+ * Omit to inject without a nonce.
41
+ */
42
+ nonce?: Nonce;
43
+ /**
44
+ * Stream key global name; must match `experimentalStreamKey` on the server
45
+ * head instance. Used by dev-mode warnings to detect when the server
46
+ * bootstrap script hasn't run (common misconfig).
47
+ * @default '__unhead__'
48
+ */
49
+ streamKey?: string;
50
+ /**
51
+ * Emit a warning when the client IIFE runs but no server bootstrap queue
52
+ * has been installed (i.e. server didn't call `wrapStream` /
53
+ * `renderSSRHeadShell`). Dev-only.
54
+ * @default true in dev, false in prod
55
+ */
56
+ warnOnMissingServerBootstrap?: boolean;
57
+ }
58
+ /**
59
+ * Builds the bundler-agnostic unplugin hook set for the streaming plugin. Exposed so
60
+ * framework wrappers (e.g. `@unhead/vue/bundler`) can bake in their own
61
+ * `framework`, `filter`, and `transform` while still using this factory
62
+ * to produce hooks that work across vite/webpack/rspack/rollup/esbuild via `createUnplugin`.
63
+ *
64
+ * SSR detection is bundler-specific:
65
+ * - vite build: `config.env.isSsrBuild`
66
+ * - vite dev (v6+ environments): `this.environment.name === 'ssr'` per-transform
67
+ * - webpack/rspack: `compiler.options.name === 'server'` or `target === 'node'`
68
+ */
69
+ declare function buildStreamingPluginOptions(options: StreamingPluginOptions): UnpluginOptions;
70
+ /**
71
+ * Internal cross-bundler unplugin factory. Framework wrappers pick a single bundler's
72
+ * output (`.vite`, `.webpack`, `.rspack`, etc.) to expose via their own subpath export.
73
+ *
74
+ * Consumers should prefer the unified framework bundler entry (e.g.
75
+ * `@unhead/{vue,react,svelte,solid-js}/bundler`) rather than importing this
76
+ * directly.
77
+ */
78
+ declare const createStreamingPlugin: unplugin.UnpluginInstance<StreamingPluginOptions, boolean>;
79
+
80
+ export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, buildStreamingPluginOptions, createStreamingPlugin };
81
+ export type { Nonce, StreamingPluginOptions };
@@ -0,0 +1,81 @@
1
+ import * as unplugin from 'unplugin';
2
+ import { UnpluginOptions } from 'unplugin';
3
+
4
+ declare const VIRTUAL_CLIENT_ID = "virtual:@unhead/streaming-client";
5
+ declare const VIRTUAL_IIFE_ID = "virtual:@unhead/streaming-iife.js";
6
+ type Nonce = string | (() => string | undefined);
7
+ interface StreamingPluginOptions {
8
+ /** Framework package e.g. '@unhead/vue' */
9
+ framework: string;
10
+ /** Plugin name (optional, defaults to `${framework}:streaming`) */
11
+ name?: string;
12
+ /**
13
+ * File extension filter for transform hook, e.g. /\.vue$/. Optional;
14
+ * only required by frameworks whose client streaming support relies on
15
+ * source-level AST injection (React/Solid/Svelte). Vue does not use it.
16
+ */
17
+ filter?: RegExp;
18
+ /** Transform handler called for files matching `filter`. */
19
+ transform?: (code: string, id: string, options?: {
20
+ ssr?: boolean;
21
+ }) => {
22
+ code: string;
23
+ map?: any;
24
+ } | null | undefined | void;
25
+ /**
26
+ * How to load the streaming client (vite-only, ignored on webpack/rspack/rollup where
27
+ * index.html injection isn't available; frameworks inject the iife themselves in SSR).
28
+ * - 'async' (default): Non-blocking external script. In dev served from a virtual
29
+ * module; in production emitted as a real asset chunk via `emitFile`.
30
+ * - 'inline': Inline the IIFE directly in HTML. Largest HTML, smallest TTFB,
31
+ * always safe in production. Recommended for streaming SSR.
32
+ * - 'module': ES module dynamic import of the client bootstrap. Vite rewrites the
33
+ * import path through its module graph so it survives production builds.
34
+ * @default 'async'
35
+ */
36
+ mode?: 'async' | 'inline' | 'module';
37
+ /**
38
+ * CSP nonce forwarded on every injected `<script>` tag. Pass a string or a
39
+ * function returning a string (useful when the nonce rotates per request).
40
+ * Omit to inject without a nonce.
41
+ */
42
+ nonce?: Nonce;
43
+ /**
44
+ * Stream key global name; must match `experimentalStreamKey` on the server
45
+ * head instance. Used by dev-mode warnings to detect when the server
46
+ * bootstrap script hasn't run (common misconfig).
47
+ * @default '__unhead__'
48
+ */
49
+ streamKey?: string;
50
+ /**
51
+ * Emit a warning when the client IIFE runs but no server bootstrap queue
52
+ * has been installed (i.e. server didn't call `wrapStream` /
53
+ * `renderSSRHeadShell`). Dev-only.
54
+ * @default true in dev, false in prod
55
+ */
56
+ warnOnMissingServerBootstrap?: boolean;
57
+ }
58
+ /**
59
+ * Builds the bundler-agnostic unplugin hook set for the streaming plugin. Exposed so
60
+ * framework wrappers (e.g. `@unhead/vue/bundler`) can bake in their own
61
+ * `framework`, `filter`, and `transform` while still using this factory
62
+ * to produce hooks that work across vite/webpack/rspack/rollup/esbuild via `createUnplugin`.
63
+ *
64
+ * SSR detection is bundler-specific:
65
+ * - vite build: `config.env.isSsrBuild`
66
+ * - vite dev (v6+ environments): `this.environment.name === 'ssr'` per-transform
67
+ * - webpack/rspack: `compiler.options.name === 'server'` or `target === 'node'`
68
+ */
69
+ declare function buildStreamingPluginOptions(options: StreamingPluginOptions): UnpluginOptions;
70
+ /**
71
+ * Internal cross-bundler unplugin factory. Framework wrappers pick a single bundler's
72
+ * output (`.vite`, `.webpack`, `.rspack`, etc.) to expose via their own subpath export.
73
+ *
74
+ * Consumers should prefer the unified framework bundler entry (e.g.
75
+ * `@unhead/{vue,react,svelte,solid-js}/bundler`) rather than importing this
76
+ * directly.
77
+ */
78
+ declare const createStreamingPlugin: unplugin.UnpluginInstance<StreamingPluginOptions, boolean>;
79
+
80
+ export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, buildStreamingPluginOptions, createStreamingPlugin };
81
+ export type { Nonce, StreamingPluginOptions };
@@ -0,0 +1,166 @@
1
+ import { createUnplugin } from 'unplugin';
2
+
3
+ const VIRTUAL_CLIENT_ID = "virtual:@unhead/streaming-client";
4
+ const VIRTUAL_IIFE_ID = "virtual:@unhead/streaming-iife.js";
5
+ const RESOLVED_ID = `\0${VIRTUAL_CLIENT_ID}`;
6
+ const RESOLVED_IIFE_ID = `\0${VIRTUAL_IIFE_ID}`;
7
+ const VIRTUAL_RE = /virtual:@unhead\/streaming/;
8
+ const RESOLVED_RE = /^\0virtual:@unhead\/streaming/;
9
+ let iifeCode;
10
+ let iifeCodeLoading;
11
+ async function loadIifeCode() {
12
+ if (iifeCode)
13
+ return;
14
+ iifeCodeLoading ||= import('unhead/stream/iife').then((mod) => {
15
+ iifeCode = mod.streamingIifeCode;
16
+ });
17
+ await iifeCodeLoading;
18
+ }
19
+ function resolveNonce(nonce) {
20
+ if (!nonce)
21
+ return void 0;
22
+ return typeof nonce === "function" ? nonce() : nonce;
23
+ }
24
+ function buildClientStub(framework, streamKey, warnOnMissing) {
25
+ const key = JSON.stringify(streamKey);
26
+ const warnBranch = warnOnMissing ? `else{console.warn('[unhead] streaming client loaded but window['+${key}+'] is undefined; did the server call wrapStream()/renderSSRHeadShell()?')}` : "";
27
+ return `import{createHead}from'${framework}/client'
28
+ const s=window[${key}];if(s){const q=s._q;s._q=[];const h=createHead({document});q.forEach(e=>h.push(e));s.push=e=>h.push(e);s._head=h}${warnBranch}`;
29
+ }
30
+ function buildStreamingPluginOptions(options) {
31
+ const {
32
+ framework,
33
+ name,
34
+ mode = "async",
35
+ nonce,
36
+ streamKey = "__unhead__",
37
+ warnOnMissingServerBootstrap
38
+ } = options;
39
+ const state = {
40
+ isBuild: false,
41
+ ssr: false
42
+ };
43
+ function isSSRCall(hookThis, opts) {
44
+ const envName = hookThis?.environment?.name;
45
+ return envName === "ssr" || envName === "server" || opts?.ssr === true || state.ssr;
46
+ }
47
+ function warnEnabled() {
48
+ return warnOnMissingServerBootstrap ?? !state.isBuild;
49
+ }
50
+ return {
51
+ name: name ?? `${framework}:streaming`,
52
+ enforce: "pre",
53
+ async buildStart() {
54
+ await loadIifeCode();
55
+ if (mode === "async" && state.isBuild && typeof this.emitFile === "function") {
56
+ if (!iifeCode)
57
+ throw new Error("[unhead] Streaming IIFE not built. Run `pnpm build` in packages/unhead first.");
58
+ state.emittedIifeFileName = this.emitFile({
59
+ type: "asset",
60
+ name: "unhead-streaming.js",
61
+ source: iifeCode
62
+ });
63
+ }
64
+ },
65
+ resolveId: {
66
+ filter: { id: VIRTUAL_RE },
67
+ handler(id) {
68
+ if (id === VIRTUAL_CLIENT_ID || id === `/${VIRTUAL_CLIENT_ID}`)
69
+ return RESOLVED_ID;
70
+ if (id === VIRTUAL_IIFE_ID || id === `/${VIRTUAL_IIFE_ID}`)
71
+ return RESOLVED_IIFE_ID;
72
+ }
73
+ },
74
+ load: {
75
+ filter: { id: RESOLVED_RE },
76
+ handler(id, opts) {
77
+ const isSSR = isSSRCall(this, opts);
78
+ if (id === RESOLVED_ID) {
79
+ if (isSSR)
80
+ return { code: "export {}", moduleType: "js" };
81
+ return {
82
+ code: buildClientStub(framework, streamKey, warnEnabled()),
83
+ moduleType: "js"
84
+ };
85
+ }
86
+ if (id === RESOLVED_IIFE_ID) {
87
+ if (isSSR)
88
+ return { code: "", moduleType: "js" };
89
+ if (!iifeCode)
90
+ throw new Error("[unhead] Streaming IIFE not built. Run `pnpm build` in packages/unhead first.");
91
+ return { code: iifeCode, moduleType: "js" };
92
+ }
93
+ }
94
+ },
95
+ ...options.transform && options.filter ? {
96
+ transform: {
97
+ filter: { id: options.filter },
98
+ handler(code, id, opts) {
99
+ return options.transform(code, id, { ssr: isSSRCall(this, opts) });
100
+ }
101
+ }
102
+ } : {},
103
+ webpack(compiler) {
104
+ const { name: n, target } = compiler.options;
105
+ if (n === "server" || target === "node" || target === "async-node")
106
+ state.ssr = true;
107
+ },
108
+ rspack(compiler) {
109
+ const { name: n, target } = compiler.options;
110
+ if (n === "server" || target === "node" || target === "async-node")
111
+ state.ssr = true;
112
+ },
113
+ vite: {
114
+ apply(_config, env) {
115
+ if (env.isSsrBuild)
116
+ state.ssr = true;
117
+ if (env.command === "build")
118
+ state.isBuild = true;
119
+ return true;
120
+ },
121
+ configResolved(config) {
122
+ if (config.command === "build")
123
+ state.isBuild = true;
124
+ },
125
+ transformIndexHtml: {
126
+ // `order: 'pre'` is separate from the plugin-level `enforce: 'pre'`:
127
+ // it runs this HTML transform before other non-pre HTML transforms
128
+ // so the virtual module `<script>` tags we inject go through the
129
+ // full Vite plugin pipeline (resolveId/load) and aren't stripped or
130
+ // rewritten by downstream HTML transforms.
131
+ order: "pre",
132
+ handler() {
133
+ const nonceValue = resolveNonce(nonce);
134
+ const nonceAttr = nonceValue ? { nonce: nonceValue } : {};
135
+ if (mode === "inline") {
136
+ if (!iifeCode)
137
+ throw new Error("[unhead] Streaming IIFE not built. Run `pnpm build` in packages/unhead first.");
138
+ return [{
139
+ tag: "script",
140
+ attrs: nonceAttr,
141
+ children: iifeCode,
142
+ injectTo: "head-prepend"
143
+ }];
144
+ }
145
+ if (mode === "async") {
146
+ const src = state.isBuild && state.emittedIifeFileName ? `/${state.emittedIifeFileName}` : `/${VIRTUAL_IIFE_ID}`;
147
+ return [{
148
+ tag: "script",
149
+ attrs: { ...nonceAttr, async: true, src },
150
+ injectTo: "head-prepend"
151
+ }];
152
+ }
153
+ return [{
154
+ tag: "script",
155
+ attrs: nonceAttr,
156
+ children: `import("/${VIRTUAL_CLIENT_ID}")`,
157
+ injectTo: "head-prepend"
158
+ }];
159
+ }
160
+ }
161
+ }
162
+ };
163
+ }
164
+ const createStreamingPlugin = createUnplugin(buildStreamingPluginOptions);
165
+
166
+ export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, buildStreamingPluginOptions, createStreamingPlugin };
@@ -1,31 +1,23 @@
1
1
  import { Plugin } from 'vite';
2
+ import { StreamingPluginOptions } from './unplugin.mjs';
3
+ export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, buildStreamingPluginOptions } from './unplugin.mjs';
4
+ import 'unplugin';
2
5
 
3
- declare const VIRTUAL_CLIENT_ID = "virtual:@unhead/streaming-client";
4
- declare const VIRTUAL_IIFE_ID = "virtual:@unhead/streaming-iife.js";
5
- interface StreamingPluginOptions {
6
- /** Framework package e.g. '@unhead/vue' */
7
- framework: string;
8
- /** Plugin name (optional, defaults to `${framework}:streaming`) */
9
- name?: string;
10
- /** File extension filter for transform hook, e.g. /\.vue$/ */
11
- filter: RegExp;
12
- /** Transform handler called for files matching `filter` */
13
- transform: (code: string, id: string, options?: {
14
- ssr?: boolean;
15
- }) => {
16
- code: string;
17
- map?: any;
18
- } | null | undefined | void;
19
- /**
20
- * How to load the streaming client:
21
- * - 'async': Load as async script (non-blocking, may have brief queue delay)
22
- * - 'inline': Inline the IIFE directly in HTML (larger HTML, but immediate execution)
23
- * - 'module': Use ES module import (original behavior, waits for bundle)
24
- * @default 'async'
25
- */
26
- mode?: 'async' | 'inline' | 'module';
27
- }
6
+ /**
7
+ * @deprecated Import from `unhead/stream/unplugin` instead and call `.vite(options)`
8
+ * on the returned unplugin instance. The `unhead/stream/vite` subpath will be
9
+ * removed in a future major release.
10
+ *
11
+ * ```ts
12
+ * // Before
13
+ * import { createStreamingPlugin } from 'unhead/stream/vite'
14
+ * const plugin = createStreamingPlugin({ framework, filter, transform })
15
+ *
16
+ * // After
17
+ * import { createStreamingPlugin } from 'unhead/stream/unplugin'
18
+ * const plugin = createStreamingPlugin.vite({ framework, filter, transform })
19
+ * ```
20
+ */
28
21
  declare function createStreamingPlugin(options: StreamingPluginOptions): Plugin;
29
22
 
30
- export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, createStreamingPlugin };
31
- export type { StreamingPluginOptions };
23
+ export { StreamingPluginOptions, createStreamingPlugin };
@@ -1,31 +1,23 @@
1
1
  import { Plugin } from 'vite';
2
+ import { StreamingPluginOptions } from './unplugin.js';
3
+ export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, buildStreamingPluginOptions } from './unplugin.js';
4
+ import 'unplugin';
2
5
 
3
- declare const VIRTUAL_CLIENT_ID = "virtual:@unhead/streaming-client";
4
- declare const VIRTUAL_IIFE_ID = "virtual:@unhead/streaming-iife.js";
5
- interface StreamingPluginOptions {
6
- /** Framework package e.g. '@unhead/vue' */
7
- framework: string;
8
- /** Plugin name (optional, defaults to `${framework}:streaming`) */
9
- name?: string;
10
- /** File extension filter for transform hook, e.g. /\.vue$/ */
11
- filter: RegExp;
12
- /** Transform handler called for files matching `filter` */
13
- transform: (code: string, id: string, options?: {
14
- ssr?: boolean;
15
- }) => {
16
- code: string;
17
- map?: any;
18
- } | null | undefined | void;
19
- /**
20
- * How to load the streaming client:
21
- * - 'async': Load as async script (non-blocking, may have brief queue delay)
22
- * - 'inline': Inline the IIFE directly in HTML (larger HTML, but immediate execution)
23
- * - 'module': Use ES module import (original behavior, waits for bundle)
24
- * @default 'async'
25
- */
26
- mode?: 'async' | 'inline' | 'module';
27
- }
6
+ /**
7
+ * @deprecated Import from `unhead/stream/unplugin` instead and call `.vite(options)`
8
+ * on the returned unplugin instance. The `unhead/stream/vite` subpath will be
9
+ * removed in a future major release.
10
+ *
11
+ * ```ts
12
+ * // Before
13
+ * import { createStreamingPlugin } from 'unhead/stream/vite'
14
+ * const plugin = createStreamingPlugin({ framework, filter, transform })
15
+ *
16
+ * // After
17
+ * import { createStreamingPlugin } from 'unhead/stream/unplugin'
18
+ * const plugin = createStreamingPlugin.vite({ framework, filter, transform })
19
+ * ```
20
+ */
28
21
  declare function createStreamingPlugin(options: StreamingPluginOptions): Plugin;
29
22
 
30
- export { VIRTUAL_CLIENT_ID, VIRTUAL_IIFE_ID, createStreamingPlugin };
31
- export type { StreamingPluginOptions };
23
+ export { StreamingPluginOptions, createStreamingPlugin };