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.
- package/dist/client.d.mts +5 -5
- package/dist/client.d.ts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/legacy.d.mts +3 -3
- package/dist/legacy.d.ts +3 -3
- package/dist/parser.d.mts +1 -1
- package/dist/parser.d.ts +1 -1
- package/dist/plugins.d.mts +6 -33
- package/dist/plugins.d.ts +6 -33
- package/dist/plugins.mjs +35 -199
- package/dist/scripts.d.mts +4 -4
- package/dist/scripts.d.ts +4 -4
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/shared/{unhead.CHsGwxB0.d.ts → unhead.-D8hRpkn.d.ts} +2 -2
- package/dist/shared/{unhead.BfINO8Du.d.mts → unhead.B2jfOxG1.d.mts} +2 -2
- package/dist/shared/{unhead.D91IvQ_J.d.mts → unhead.B7bBMqva.d.mts} +1 -1
- package/dist/shared/{unhead.DCXRrsx_.d.ts → unhead.BRwJvCZb.d.ts} +2 -2
- package/dist/shared/{unhead.LatFr5s0.d.mts → unhead.BX9134DF.d.mts} +1 -1
- package/dist/shared/{unhead.m4TuRTvV.d.mts → unhead.BoZ-Ul8T.d.mts} +1 -1
- package/dist/shared/{unhead.DvqMLXpO.d.mts → unhead.ClE7lB2Z.d.mts} +1 -1
- package/dist/shared/{unhead.DoIxTwSF.d.ts → unhead.CqVawd25.d.ts} +1 -1
- package/dist/shared/{unhead.Bbk6Q9rK.d.mts → unhead.Cv5yrrUd.d.mts} +2 -2
- package/dist/shared/{unhead.CGloe9Qc.d.mts → unhead.Dc5Pn4qI.d.mts} +2 -2
- package/dist/shared/{unhead.9aHQrEvB.d.ts → unhead.DdarjSpi.d.ts} +2 -2
- package/dist/shared/{unhead.BcynQ3qJ.d.mts → unhead.DeoGMp34.d.mts} +1 -1
- package/dist/shared/{unhead.BcynQ3qJ.d.ts → unhead.DeoGMp34.d.ts} +1 -1
- package/dist/shared/unhead.Dl_lRDXb.d.mts +38 -0
- package/dist/shared/unhead.Dl_lRDXb.d.ts +38 -0
- package/dist/shared/{unhead.Ogog_XUX.d.ts → unhead.cXt2Dgt5.d.ts} +1 -1
- package/dist/shared/unhead.ebqUBTt1.mjs +513 -0
- package/dist/shared/{unhead.BmdY3z1V.d.ts → unhead.gui9LmZS.d.ts} +1 -1
- package/dist/shared/{unhead.CvqH1vcy.d.ts → unhead.q49lHHFN.d.ts} +1 -1
- package/dist/stream/client.d.mts +24 -5
- package/dist/stream/client.d.ts +24 -5
- package/dist/stream/server.d.mts +9 -6
- package/dist/stream/server.d.ts +9 -6
- package/dist/stream/server.mjs +15 -4
- package/dist/stream/unplugin.d.mts +81 -0
- package/dist/stream/unplugin.d.ts +81 -0
- package/dist/stream/unplugin.mjs +166 -0
- package/dist/stream/vite.d.mts +19 -27
- package/dist/stream/vite.d.ts +19 -27
- package/dist/stream/vite.mjs +6 -78
- package/dist/types.d.mts +6 -6
- package/dist/types.d.ts +6 -6
- package/dist/utils.d.mts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/validate.d.mts +236 -0
- package/dist/validate.d.ts +236 -0
- package/dist/validate.mjs +49 -0
- package/package.json +20 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HookableCore } from 'hookable';
|
|
2
|
-
import {
|
|
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 {
|
|
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'>;
|
package/dist/stream/client.d.mts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import { C as ClientUnhead } from '../shared/unhead.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 };
|
package/dist/stream/client.d.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import { C as ClientUnhead } from '../shared/unhead.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 };
|
package/dist/stream/server.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { S as ServerUnhead } from '../shared/unhead.
|
|
2
|
-
import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.
|
|
3
|
-
import {
|
|
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
|
|
170
|
-
*
|
|
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)
|
package/dist/stream/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { S as ServerUnhead } from '../shared/unhead.
|
|
2
|
-
import { f as CreateStreamableServerHeadOptions, U as Unhead, s as SSRHeadPayload } from '../shared/unhead.
|
|
3
|
-
import {
|
|
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
|
|
170
|
-
*
|
|
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)
|
package/dist/stream/server.mjs
CHANGED
|
@@ -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
|
-
|
|
47
|
+
const nonceAttr = nonce ? ` nonce="${nonce.replace(/"/g, """)}"` : "";
|
|
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:
|
|
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 };
|
package/dist/stream/vite.d.mts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 {
|
|
31
|
-
export type { StreamingPluginOptions };
|
|
23
|
+
export { StreamingPluginOptions, createStreamingPlugin };
|
package/dist/stream/vite.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 {
|
|
31
|
-
export type { StreamingPluginOptions };
|
|
23
|
+
export { StreamingPluginOptions, createStreamingPlugin };
|