vike 0.4.179-commit-9384166 → 0.4.179-commit-9822036

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.
@@ -105,6 +105,9 @@ const configDefinitionsBuiltIn = {
105
105
  cacheControl: {
106
106
  env: { server: true }
107
107
  },
108
+ injectScriptsAt: {
109
+ env: { server: true }
110
+ },
108
111
  name: {
109
112
  env: { config: true }
110
113
  },
@@ -9,13 +9,17 @@ const serializePageContextClientSide_js_1 = require("../serializePageContextClie
9
9
  const sanitizeJson_js_1 = require("./sanitizeJson.js");
10
10
  const inferHtmlTags_js_1 = require("./inferHtmlTags.js");
11
11
  const mergeScriptTags_js_1 = require("./mergeScriptTags.js");
12
+ const helpers_js_1 = require("../../../../shared/page-configs/helpers.js");
13
+ const getConfigValue_js_1 = require("../../../../shared/page-configs/getConfigValue.js");
12
14
  const globalContext_js_1 = require("../../globalContext.js");
13
15
  const picocolors_1 = __importDefault(require("@brillout/picocolors"));
16
+ const getConfigDefinedAt_js_1 = require("../../../../shared/page-configs/getConfigDefinedAt.js");
14
17
  const stamp = '__injectFilterEntry';
15
- function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript) {
18
+ function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript, isStream) {
16
19
  (0, utils_js_1.assert)([true, false].includes(pageContext._isHtmlOnly));
17
20
  const isHtmlOnly = pageContext._isHtmlOnly;
18
21
  const { isProduction } = (0, globalContext_js_1.getGlobalContext)();
22
+ const injectScriptsAt = getInjectScriptsAt(pageContext._pageId, pageContext._pageConfigs);
19
23
  const injectFilterEntries = pageAssets
20
24
  .filter((asset) => {
21
25
  if (asset.isEntry && asset.assetType === 'script') {
@@ -78,28 +82,39 @@ function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter,
78
82
  // ==========
79
83
  // JavaScript
80
84
  // ==========
81
- // In order to avoid the race-condition depicted in https://github.com/vikejs/vike/issues/567
82
- // 1. <script id="vike_pageContext" type="application/json"> must appear before the entry <script> (which loads Vike's client runtime).
83
- // 2. <script id="vike_pageContext" type="application/json"> can't be async nor defer.
84
- // Additionally:
85
- // 3. the entry <script> can't be defer, otherwise progressive hydration while SSR streaming won't work.
86
- // 4. the entry <script> should be towards the end of the HTML as performance-wise it's more interesting to parse
87
- // <div id="page-view"> before running the entry <script> which initiates the hydration.
88
- // See https://github.com/vikejs/vike/pull/1271
85
+ // - The pageContext JSON should be fully sent before Vike's client runtime starts executing.
86
+ // - Otherwise, race condition "SyntaxError: Unterminated string in JSON": https://github.com/vikejs/vike/issues/567
87
+ // - <script id="vike_pageContext" type="application/json"> must appear before the entry <script> (which loads Vike's client runtime).
88
+ // - <script id="vike_pageContext" type="application/json"> can't be async nor defer.
89
+ // - The entry <script> can't be defer, otherwise progressive hydration while SSR streaming won't work.
90
+ // - The entry <script> should be towards the end of the HTML as performance-wise it's more interesting to parse <div id="page-view"> before running the entry <script> which initiates the hydration.
91
+ // - But with HTML streaming, in order to support [Progressive Rendering](https://vike.dev/streaming#progressive-rendering), the entry <script> should be injected early instead.
89
92
  const positionJavaScriptEntry = (() => {
93
+ if (injectScriptsAt !== null) {
94
+ if (pageContext._pageContextPromise) {
95
+ (0, utils_js_1.assertWarning)(injectScriptsAt === 'HTML_END' || !isStream, `You're setting injectScriptsAt to ${picocolors_1.default.code(JSON.stringify(injectScriptsAt))} while using HTML streaming with a pageContext promise (https://vike.dev/streaming#initial-data-after-stream-end) which is contradictory: the pageContext promise is skipped.`, { onlyOnce: true });
96
+ }
97
+ return injectScriptsAt;
98
+ }
90
99
  if (pageContext._pageContextPromise) {
91
- (0, utils_js_1.assertWarning)(!streamFromReactStreamingPackage, "[getHtmlTags()] We recommend against using streaming and a pageContext promise at the same time, because progressive hydration won't work.", { onlyOnce: true });
92
- // If there is a pageContext._pageContextPromise (which is resolved after the stream has ended) then the pageContext JSON data needs to await for it: https://vike.dev/streaming#initial-data-after-stream-end
100
+ // - If there is a pageContext._pageContextPromise then <script id="vike_pageContext" type="application/json"> needs to await for it.
101
+ // - pageContext._pageContextPromise is typically resolved only after the page's components are rendered and the stream ended.
102
+ // - https://vike.dev/streaming#initial-data-after-stream-end
93
103
  return 'HTML_END';
94
104
  }
95
105
  if (streamFromReactStreamingPackage && !streamFromReactStreamingPackage.hasStreamEnded()) {
96
- // If there is a stream then, in order to support progressive hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary
106
+ // If there is a stream then, in order to support progressive hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary.
97
107
  return 'STREAM';
98
108
  }
99
- else {
100
- return 'HTML_END';
101
- }
109
+ return 'HTML_END';
102
110
  })();
111
+ if (pageContext._pageContextPromise && streamFromReactStreamingPackage) {
112
+ // - Should we show this warning for Solid as well? Solid seems to also support progressive rendering.
113
+ // - https://github.com/vikejs/vike-solid/issues/95
114
+ // - Vue doesn't seem to support progressive rendering yet.
115
+ // - https://github.com/vikejs/vike-vue/issues/85
116
+ (0, utils_js_1.assertWarning)(false, "We recommend against using HTML streaming and a pageContext promise (https://vike.dev/streaming#initial-data-after-stream-end) at the same time, because progressive hydration (https://vike.dev/streaming#progressive-rendering) won't work.", { onlyOnce: true });
117
+ }
103
118
  // <script id="vike_pageContext" type="application/json">
104
119
  if (!isHtmlOnly) {
105
120
  htmlTags.push({
@@ -195,3 +210,19 @@ function checkForWarnings(injectFilterEntries) {
195
210
  }
196
211
  });
197
212
  }
213
+ function getInjectScriptsAt(pageId, pageConfigs) {
214
+ if (pageConfigs.length === 0)
215
+ return null; // only support V1 design
216
+ const pageConfig = (0, helpers_js_1.getPageConfig)(pageId, pageConfigs);
217
+ const configValue = (0, getConfigValue_js_1.getConfigValueRuntime)(pageConfig, 'injectScriptsAt');
218
+ if (!configValue)
219
+ return null;
220
+ const injectScriptsAt = configValue.value;
221
+ (0, utils_js_1.assert)(configValue.definedAtData);
222
+ const configDefinedAt = (0, getConfigDefinedAt_js_1.getConfigDefinedAt)('Config', 'injectScriptsAt', configValue.definedAtData);
223
+ (0, utils_js_1.assertUsage)(injectScriptsAt === null ||
224
+ injectScriptsAt === 'HTML_BEGIN' ||
225
+ injectScriptsAt === 'HTML_END' ||
226
+ injectScriptsAt === 'STREAM', `${configDefinedAt} has an invalid value`);
227
+ return injectScriptsAt;
228
+ }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // Unit tests at ./injectHtmlTags.spec.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.injectAtClosingTag = exports.injectAtOpeningTag = exports.createHtmlHeadIfMissing = exports.injectHtmlTagsUsingStream = exports.injectHtmlTags = void 0;
4
+ exports.injectAtClosingTag = exports.injectAtOpeningTag = exports.joinHtmlTags = exports.createHtmlHeadIfMissing = exports.injectHtmlTagsUsingStream = exports.injectHtmlTags = void 0;
5
5
  const utils_js_1 = require("../../utils.js");
6
6
  function injectHtmlTags(htmlString, htmlTags, position) {
7
7
  const htmlFragment = joinHtmlTags(htmlTags.filter((h) => h.position === position));
@@ -14,7 +14,6 @@ exports.injectHtmlTags = injectHtmlTags;
14
14
  function injectHtmlTagsUsingStream(htmlTags, streamFromReactStreamingPackage) {
15
15
  const htmlFragment = joinHtmlTags(htmlTags.filter((h) => h.position === 'STREAM'));
16
16
  if (htmlFragment) {
17
- (0, utils_js_1.assert)(streamFromReactStreamingPackage);
18
17
  (0, utils_js_1.assert)(!streamFromReactStreamingPackage.hasStreamEnded());
19
18
  streamFromReactStreamingPackage.injectToStream(htmlFragment, { flush: true });
20
19
  }
@@ -24,6 +23,7 @@ function joinHtmlTags(htmlTags) {
24
23
  const htmlFragment = htmlTags.map((h) => resolveHtmlTag(h.htmlTag)).join('');
25
24
  return htmlFragment;
26
25
  }
26
+ exports.joinHtmlTags = joinHtmlTags;
27
27
  function injectHtmlFragment(position, htmlFragment, htmlString) {
28
28
  if (position === 'HTML_BEGIN') {
29
29
  {
@@ -9,7 +9,7 @@ const getViteDevScript_js_1 = require("./injectAssets/getViteDevScript.js");
9
9
  async function injectHtmlTagsToString(htmlParts, pageContext, injectFilter) {
10
10
  const pageAssets = await pageContext.__getPageAssets();
11
11
  const viteDevScript = await (0, getViteDevScript_js_1.getViteDevScript)();
12
- const htmlTags = (0, getHtmlTags_js_1.getHtmlTags)(pageContext, null, injectFilter, pageAssets, viteDevScript);
12
+ const htmlTags = (0, getHtmlTags_js_1.getHtmlTags)(pageContext, null, injectFilter, pageAssets, viteDevScript, false);
13
13
  let htmlString = htmlPartsToString(htmlParts, pageAssets);
14
14
  htmlString = injectToHtmlBegin(htmlString, htmlTags);
15
15
  htmlString = injectToHtmlEnd(htmlString, htmlTags);
@@ -21,17 +21,31 @@ function injectHtmlTagsToStream(pageContext, streamFromReactStreamingPackage, in
21
21
  let htmlTags;
22
22
  return {
23
23
  injectAtStreamBegin,
24
+ injectAtStreamAfterFirstChunk,
24
25
  injectAtStreamEnd
25
26
  };
26
27
  async function injectAtStreamBegin(htmlPartsBegin) {
27
28
  const pageAssets = await pageContext.__getPageAssets();
28
29
  const viteDevScript = await (0, getViteDevScript_js_1.getViteDevScript)();
29
- htmlTags = (0, getHtmlTags_js_1.getHtmlTags)(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript);
30
+ htmlTags = (0, getHtmlTags_js_1.getHtmlTags)(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript, true);
30
31
  let htmlBegin = htmlPartsToString(htmlPartsBegin, pageAssets);
31
32
  htmlBegin = injectToHtmlBegin(htmlBegin, htmlTags);
32
- (0, injectHtmlTags_js_1.injectHtmlTagsUsingStream)(htmlTags, streamFromReactStreamingPackage);
33
+ if (streamFromReactStreamingPackage) {
34
+ (0, injectHtmlTags_js_1.injectHtmlTagsUsingStream)(htmlTags, streamFromReactStreamingPackage);
35
+ }
33
36
  return htmlBegin;
34
37
  }
38
+ function injectAtStreamAfterFirstChunk() {
39
+ // React has its own stream injection mechanism, see injectHtmlTagsUsingStream()
40
+ if (streamFromReactStreamingPackage)
41
+ return null;
42
+ (0, utils_js_1.assert)(htmlTags);
43
+ const tags = htmlTags.filter((h) => h.position === 'STREAM');
44
+ if (tags.length === 0)
45
+ return null;
46
+ const htmlFragment = (0, injectHtmlTags_js_1.joinHtmlTags)(tags);
47
+ return htmlFragment;
48
+ }
35
49
  async function injectAtStreamEnd(htmlPartsEnd) {
36
50
  (0, utils_js_1.assert)(htmlTags);
37
51
  await resolvePageContextPromise(pageContext);
@@ -55,7 +55,7 @@ async function renderDocumentHtml(documentHtml, pageContext, onErrorWhileStreami
55
55
  }
56
56
  exports.renderDocumentHtml = renderDocumentHtml;
57
57
  async function renderHtmlStream(streamOriginal, injectString, pageContext, onErrorWhileStreaming, injectFilter) {
58
- const opts = {
58
+ const processStreamOptions = {
59
59
  onErrorWhileStreaming,
60
60
  enableEagerStreaming: pageContext.enableEagerStreaming
61
61
  };
@@ -64,17 +64,18 @@ async function renderHtmlStream(streamOriginal, injectString, pageContext, onErr
64
64
  if ((0, react_streaming_js_1.isStreamFromReactStreamingPackage)(streamOriginal) && !streamOriginal.disabled) {
65
65
  streamFromReactStreamingPackage = streamOriginal;
66
66
  }
67
- const { injectAtStreamBegin, injectAtStreamEnd } = (0, injectAssets_js_1.injectHtmlTagsToStream)(pageContext, streamFromReactStreamingPackage, injectFilter);
68
- (0, utils_js_1.objectAssign)(opts, {
69
- injectStringAtBegin: async () => {
70
- return await injectAtStreamBegin(injectString.htmlPartsBegin);
71
- },
72
- injectStringAtEnd: async () => {
73
- return await injectAtStreamEnd(injectString.htmlPartsEnd);
74
- }
75
- });
67
+ const { injectAtStreamBegin, injectAtStreamAfterFirstChunk, injectAtStreamEnd } = (0, injectAssets_js_1.injectHtmlTagsToStream)(pageContext, streamFromReactStreamingPackage, injectFilter);
68
+ processStreamOptions.injectStringAtBegin = async () => {
69
+ return await injectAtStreamBegin(injectString.htmlPartsBegin);
70
+ };
71
+ processStreamOptions.injectStringAtEnd = async () => {
72
+ return await injectAtStreamEnd(injectString.htmlPartsEnd);
73
+ };
74
+ processStreamOptions.injectStringAfterFirstChunk = () => {
75
+ return injectAtStreamAfterFirstChunk();
76
+ };
76
77
  }
77
- const streamWrapper = await (0, stream_js_1.processStream)(streamOriginal, opts);
78
+ const streamWrapper = await (0, stream_js_1.processStream)(streamOriginal, processStreamOptions);
78
79
  return streamWrapper;
79
80
  }
80
81
  function isTemplateWrapped(something) {
@@ -198,7 +198,7 @@ function pipeToStreamWritableNode(htmlRender, writable) {
198
198
  (0, utils_js_1.assert)(false);
199
199
  }
200
200
  exports.pipeToStreamWritableNode = pipeToStreamWritableNode;
201
- async function processStream(streamOriginal, { injectStringAtBegin, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }) {
201
+ async function processStream(streamOriginal, { injectStringAtBegin, injectStringAfterFirstChunk, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }) {
202
202
  const buffer = [];
203
203
  let streamOriginalHasStartedEmitting = false;
204
204
  let streamOriginalEnded = false;
@@ -210,6 +210,7 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
210
210
  let resolve;
211
211
  let reject;
212
212
  let promiseHasResolved = false;
213
+ let injectStringAfterFirstChunk_done = false;
213
214
  const streamWrapperPromise = new Promise((resolve_, reject_) => {
214
215
  resolve = (streamWrapper) => {
215
216
  promiseHasResolved = true;
@@ -223,8 +224,8 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
223
224
  let resolveReadyToWrite;
224
225
  const promiseReadyToWrite = new Promise((r) => (resolveReadyToWrite = r));
225
226
  if (injectStringAtBegin) {
226
- const injectionBegin = await injectStringAtBegin();
227
- writeStream(injectionBegin); // Adds injectionBegin to buffer
227
+ const injectedChunk = await injectStringAtBegin();
228
+ writeStream(injectedChunk); // Adds injectedChunk to buffer
228
229
  flushStream(); // Sets shouldFlushStream to true
229
230
  }
230
231
  // We call onStreamEvent() also when the stream ends in order to properly handle the situation when the stream didn't emit any data
@@ -254,6 +255,12 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
254
255
  onData(chunk) {
255
256
  onStreamDataOrEnd(() => {
256
257
  writeStream(chunk);
258
+ if (injectStringAfterFirstChunk && !injectStringAfterFirstChunk_done) {
259
+ const injectedChunk = injectStringAfterFirstChunk();
260
+ if (injectedChunk !== null)
261
+ writeStream(injectedChunk);
262
+ injectStringAfterFirstChunk_done = true;
263
+ }
257
264
  });
258
265
  },
259
266
  async onEnd(
@@ -268,8 +275,8 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
268
275
  streamOriginalEnded = true;
269
276
  });
270
277
  if (injectStringAtEnd) {
271
- const injectEnd = await injectStringAtEnd();
272
- writeStream(injectEnd);
278
+ const injectedChunk = await injectStringAtEnd();
279
+ writeStream(injectedChunk);
273
280
  }
274
281
  await promiseReadyToWrite; // E.g. if the user calls the pipe wrapper after the original writable has ended
275
282
  (0, utils_js_1.assert)(isReady());
@@ -157,6 +157,10 @@ function valueToJson(value, configName, definedAtData, importStatements) {
157
157
  configValueSerialized = (0, stringify_1.stringify)(value, {
158
158
  valueName,
159
159
  forbidReactElements: true,
160
+ // Replace import strings with import variables.
161
+ // - We don't need this anymore and could remove it.
162
+ // - We temporarily needed it for nested document configs (`config.document.{title,description,favicon}`), but we finally decided to go for flat document configs instead (`config.{title,description,favicon}`).
163
+ // - https://github.com/vikejs/vike-react/pull/113
160
164
  replacer(_, value) {
161
165
  if (typeof value === 'string') {
162
166
  const importData = (0, transformFileImports_js_1.parsePointerImportData)(value);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PROJECT_VERSION = exports.projectInfo = void 0;
4
- const PROJECT_VERSION = '0.4.179-commit-9384166';
4
+ const PROJECT_VERSION = '0.4.179-commit-9822036';
5
5
  exports.PROJECT_VERSION = PROJECT_VERSION;
6
6
  const projectInfo = {
7
7
  projectName: 'Vike',
@@ -104,6 +104,9 @@ const configDefinitionsBuiltIn = {
104
104
  cacheControl: {
105
105
  env: { server: true }
106
106
  },
107
+ injectScriptsAt: {
108
+ env: { server: true }
109
+ },
107
110
  name: {
108
111
  env: { config: true }
109
112
  },
@@ -22,10 +22,11 @@ type InjectFilterEntry = {
22
22
  isEntry: boolean;
23
23
  inject: PreloadFilterInject;
24
24
  };
25
+ type Position = 'HTML_BEGIN' | 'HTML_END' | 'STREAM';
25
26
  type HtmlTag = {
26
27
  htmlTag: string | (() => string);
27
- position: 'HTML_BEGIN' | 'HTML_END' | 'STREAM';
28
+ position: Position;
28
29
  };
29
30
  declare function getHtmlTags(pageContext: {
30
31
  _isStream: boolean;
31
- } & PageContextInjectAssets, streamFromReactStreamingPackage: null | StreamFromReactStreamingPackage, injectFilter: PreloadFilter, pageAssets: PageAsset[], viteDevScript: string): HtmlTag[];
32
+ } & PageContextInjectAssets, streamFromReactStreamingPackage: null | StreamFromReactStreamingPackage, injectFilter: PreloadFilter, pageAssets: PageAsset[], viteDevScript: string, isStream: boolean): HtmlTag[];
@@ -4,13 +4,17 @@ import { serializePageContextClientSide } from '../serializePageContextClientSid
4
4
  import { sanitizeJson } from './sanitizeJson.js';
5
5
  import { inferAssetTag, inferPreloadTag } from './inferHtmlTags.js';
6
6
  import { mergeScriptTags } from './mergeScriptTags.js';
7
+ import { getPageConfig } from '../../../../shared/page-configs/helpers.js';
8
+ import { getConfigValueRuntime } from '../../../../shared/page-configs/getConfigValue.js';
7
9
  import { getGlobalContext } from '../../globalContext.js';
8
10
  import pc from '@brillout/picocolors';
11
+ import { getConfigDefinedAt } from '../../../../shared/page-configs/getConfigDefinedAt.js';
9
12
  const stamp = '__injectFilterEntry';
10
- function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript) {
13
+ function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript, isStream) {
11
14
  assert([true, false].includes(pageContext._isHtmlOnly));
12
15
  const isHtmlOnly = pageContext._isHtmlOnly;
13
16
  const { isProduction } = getGlobalContext();
17
+ const injectScriptsAt = getInjectScriptsAt(pageContext._pageId, pageContext._pageConfigs);
14
18
  const injectFilterEntries = pageAssets
15
19
  .filter((asset) => {
16
20
  if (asset.isEntry && asset.assetType === 'script') {
@@ -73,28 +77,39 @@ function getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter,
73
77
  // ==========
74
78
  // JavaScript
75
79
  // ==========
76
- // In order to avoid the race-condition depicted in https://github.com/vikejs/vike/issues/567
77
- // 1. <script id="vike_pageContext" type="application/json"> must appear before the entry <script> (which loads Vike's client runtime).
78
- // 2. <script id="vike_pageContext" type="application/json"> can't be async nor defer.
79
- // Additionally:
80
- // 3. the entry <script> can't be defer, otherwise progressive hydration while SSR streaming won't work.
81
- // 4. the entry <script> should be towards the end of the HTML as performance-wise it's more interesting to parse
82
- // <div id="page-view"> before running the entry <script> which initiates the hydration.
83
- // See https://github.com/vikejs/vike/pull/1271
80
+ // - The pageContext JSON should be fully sent before Vike's client runtime starts executing.
81
+ // - Otherwise, race condition "SyntaxError: Unterminated string in JSON": https://github.com/vikejs/vike/issues/567
82
+ // - <script id="vike_pageContext" type="application/json"> must appear before the entry <script> (which loads Vike's client runtime).
83
+ // - <script id="vike_pageContext" type="application/json"> can't be async nor defer.
84
+ // - The entry <script> can't be defer, otherwise progressive hydration while SSR streaming won't work.
85
+ // - The entry <script> should be towards the end of the HTML as performance-wise it's more interesting to parse <div id="page-view"> before running the entry <script> which initiates the hydration.
86
+ // - But with HTML streaming, in order to support [Progressive Rendering](https://vike.dev/streaming#progressive-rendering), the entry <script> should be injected early instead.
84
87
  const positionJavaScriptEntry = (() => {
88
+ if (injectScriptsAt !== null) {
89
+ if (pageContext._pageContextPromise) {
90
+ assertWarning(injectScriptsAt === 'HTML_END' || !isStream, `You're setting injectScriptsAt to ${pc.code(JSON.stringify(injectScriptsAt))} while using HTML streaming with a pageContext promise (https://vike.dev/streaming#initial-data-after-stream-end) which is contradictory: the pageContext promise is skipped.`, { onlyOnce: true });
91
+ }
92
+ return injectScriptsAt;
93
+ }
85
94
  if (pageContext._pageContextPromise) {
86
- assertWarning(!streamFromReactStreamingPackage, "[getHtmlTags()] We recommend against using streaming and a pageContext promise at the same time, because progressive hydration won't work.", { onlyOnce: true });
87
- // If there is a pageContext._pageContextPromise (which is resolved after the stream has ended) then the pageContext JSON data needs to await for it: https://vike.dev/streaming#initial-data-after-stream-end
95
+ // - If there is a pageContext._pageContextPromise then <script id="vike_pageContext" type="application/json"> needs to await for it.
96
+ // - pageContext._pageContextPromise is typically resolved only after the page's components are rendered and the stream ended.
97
+ // - https://vike.dev/streaming#initial-data-after-stream-end
88
98
  return 'HTML_END';
89
99
  }
90
100
  if (streamFromReactStreamingPackage && !streamFromReactStreamingPackage.hasStreamEnded()) {
91
- // If there is a stream then, in order to support progressive hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary
101
+ // If there is a stream then, in order to support progressive hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary.
92
102
  return 'STREAM';
93
103
  }
94
- else {
95
- return 'HTML_END';
96
- }
104
+ return 'HTML_END';
97
105
  })();
106
+ if (pageContext._pageContextPromise && streamFromReactStreamingPackage) {
107
+ // - Should we show this warning for Solid as well? Solid seems to also support progressive rendering.
108
+ // - https://github.com/vikejs/vike-solid/issues/95
109
+ // - Vue doesn't seem to support progressive rendering yet.
110
+ // - https://github.com/vikejs/vike-vue/issues/85
111
+ assertWarning(false, "We recommend against using HTML streaming and a pageContext promise (https://vike.dev/streaming#initial-data-after-stream-end) at the same time, because progressive hydration (https://vike.dev/streaming#progressive-rendering) won't work.", { onlyOnce: true });
112
+ }
98
113
  // <script id="vike_pageContext" type="application/json">
99
114
  if (!isHtmlOnly) {
100
115
  htmlTags.push({
@@ -189,3 +204,19 @@ function checkForWarnings(injectFilterEntries) {
189
204
  }
190
205
  });
191
206
  }
207
+ function getInjectScriptsAt(pageId, pageConfigs) {
208
+ if (pageConfigs.length === 0)
209
+ return null; // only support V1 design
210
+ const pageConfig = getPageConfig(pageId, pageConfigs);
211
+ const configValue = getConfigValueRuntime(pageConfig, 'injectScriptsAt');
212
+ if (!configValue)
213
+ return null;
214
+ const injectScriptsAt = configValue.value;
215
+ assert(configValue.definedAtData);
216
+ const configDefinedAt = getConfigDefinedAt('Config', 'injectScriptsAt', configValue.definedAtData);
217
+ assertUsage(injectScriptsAt === null ||
218
+ injectScriptsAt === 'HTML_BEGIN' ||
219
+ injectScriptsAt === 'HTML_END' ||
220
+ injectScriptsAt === 'STREAM', `${configDefinedAt} has an invalid value`);
221
+ return injectScriptsAt;
222
+ }
@@ -1,13 +1,15 @@
1
1
  export { injectHtmlTags };
2
2
  export { injectHtmlTagsUsingStream };
3
3
  export { createHtmlHeadIfMissing };
4
+ export { joinHtmlTags };
4
5
  export { injectAtOpeningTag };
5
6
  export { injectAtClosingTag };
6
7
  import type { StreamFromReactStreamingPackage } from '../stream/react-streaming.js';
7
8
  import type { HtmlTag } from './getHtmlTags.js';
8
9
  type Position = 'HTML_BEGIN' | 'HTML_END';
9
10
  declare function injectHtmlTags(htmlString: string, htmlTags: HtmlTag[], position: Position): string;
10
- declare function injectHtmlTagsUsingStream(htmlTags: HtmlTag[], streamFromReactStreamingPackage: null | StreamFromReactStreamingPackage): void;
11
+ declare function injectHtmlTagsUsingStream(htmlTags: HtmlTag[], streamFromReactStreamingPackage: StreamFromReactStreamingPackage): void;
12
+ declare function joinHtmlTags(htmlTags: HtmlTag[]): string;
11
13
  declare function injectAtOpeningTag(tag: 'head' | 'html' | '!doctype', htmlString: string, htmlFragment: string): string;
12
14
  declare function injectAtClosingTag(tag: 'body' | 'html', htmlString: string, htmlFragment: string): string;
13
15
  declare function createHtmlHeadIfMissing(htmlString: string): string;
@@ -2,6 +2,7 @@
2
2
  export { injectHtmlTags };
3
3
  export { injectHtmlTagsUsingStream };
4
4
  export { createHtmlHeadIfMissing };
5
+ export { joinHtmlTags };
5
6
  // Only needed for unit tests
6
7
  export { injectAtOpeningTag };
7
8
  export { injectAtClosingTag };
@@ -16,7 +17,6 @@ function injectHtmlTags(htmlString, htmlTags, position) {
16
17
  function injectHtmlTagsUsingStream(htmlTags, streamFromReactStreamingPackage) {
17
18
  const htmlFragment = joinHtmlTags(htmlTags.filter((h) => h.position === 'STREAM'));
18
19
  if (htmlFragment) {
19
- assert(streamFromReactStreamingPackage);
20
20
  assert(!streamFromReactStreamingPackage.hasStreamEnded());
21
21
  streamFromReactStreamingPackage.injectToStream(htmlFragment, { flush: true });
22
22
  }
@@ -29,6 +29,7 @@ declare function injectHtmlTagsToStream(pageContext: PageContextInjectAssets & {
29
29
  _isStream: true;
30
30
  }, streamFromReactStreamingPackage: null | StreamFromReactStreamingPackage, injectFilter: PreloadFilter): {
31
31
  injectAtStreamBegin: (htmlPartsBegin: HtmlPart[]) => Promise<string>;
32
+ injectAtStreamAfterFirstChunk: () => null | string;
32
33
  injectAtStreamEnd: (htmlPartsEnd: HtmlPart[]) => Promise<string>;
33
34
  };
34
35
  type PageContextPromise = null | Promise<unknown> | (() => void | Promise<unknown>);
@@ -2,13 +2,13 @@ export { injectHtmlTagsToString };
2
2
  export { injectHtmlTagsToStream };
3
3
  import { assert, isCallable, isPromise } from '../utils.js';
4
4
  import { assertPageContextProvidedByUser } from '../../../shared/assertPageContextProvidedByUser.js';
5
- import { injectHtmlTags, createHtmlHeadIfMissing, injectHtmlTagsUsingStream } from './injectAssets/injectHtmlTags.js';
5
+ import { joinHtmlTags, injectHtmlTags, createHtmlHeadIfMissing, injectHtmlTagsUsingStream } from './injectAssets/injectHtmlTags.js';
6
6
  import { getHtmlTags } from './injectAssets/getHtmlTags.js';
7
7
  import { getViteDevScript } from './injectAssets/getViteDevScript.js';
8
8
  async function injectHtmlTagsToString(htmlParts, pageContext, injectFilter) {
9
9
  const pageAssets = await pageContext.__getPageAssets();
10
10
  const viteDevScript = await getViteDevScript();
11
- const htmlTags = getHtmlTags(pageContext, null, injectFilter, pageAssets, viteDevScript);
11
+ const htmlTags = getHtmlTags(pageContext, null, injectFilter, pageAssets, viteDevScript, false);
12
12
  let htmlString = htmlPartsToString(htmlParts, pageAssets);
13
13
  htmlString = injectToHtmlBegin(htmlString, htmlTags);
14
14
  htmlString = injectToHtmlEnd(htmlString, htmlTags);
@@ -19,17 +19,31 @@ function injectHtmlTagsToStream(pageContext, streamFromReactStreamingPackage, in
19
19
  let htmlTags;
20
20
  return {
21
21
  injectAtStreamBegin,
22
+ injectAtStreamAfterFirstChunk,
22
23
  injectAtStreamEnd
23
24
  };
24
25
  async function injectAtStreamBegin(htmlPartsBegin) {
25
26
  const pageAssets = await pageContext.__getPageAssets();
26
27
  const viteDevScript = await getViteDevScript();
27
- htmlTags = getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript);
28
+ htmlTags = getHtmlTags(pageContext, streamFromReactStreamingPackage, injectFilter, pageAssets, viteDevScript, true);
28
29
  let htmlBegin = htmlPartsToString(htmlPartsBegin, pageAssets);
29
30
  htmlBegin = injectToHtmlBegin(htmlBegin, htmlTags);
30
- injectHtmlTagsUsingStream(htmlTags, streamFromReactStreamingPackage);
31
+ if (streamFromReactStreamingPackage) {
32
+ injectHtmlTagsUsingStream(htmlTags, streamFromReactStreamingPackage);
33
+ }
31
34
  return htmlBegin;
32
35
  }
36
+ function injectAtStreamAfterFirstChunk() {
37
+ // React has its own stream injection mechanism, see injectHtmlTagsUsingStream()
38
+ if (streamFromReactStreamingPackage)
39
+ return null;
40
+ assert(htmlTags);
41
+ const tags = htmlTags.filter((h) => h.position === 'STREAM');
42
+ if (tags.length === 0)
43
+ return null;
44
+ const htmlFragment = joinHtmlTags(tags);
45
+ return htmlFragment;
46
+ }
33
47
  async function injectAtStreamEnd(htmlPartsEnd) {
34
48
  assert(htmlTags);
35
49
  await resolvePageContextPromise(pageContext);
@@ -52,7 +52,7 @@ async function renderDocumentHtml(documentHtml, pageContext, onErrorWhileStreami
52
52
  assert(false);
53
53
  }
54
54
  async function renderHtmlStream(streamOriginal, injectString, pageContext, onErrorWhileStreaming, injectFilter) {
55
- const opts = {
55
+ const processStreamOptions = {
56
56
  onErrorWhileStreaming,
57
57
  enableEagerStreaming: pageContext.enableEagerStreaming
58
58
  };
@@ -61,17 +61,18 @@ async function renderHtmlStream(streamOriginal, injectString, pageContext, onErr
61
61
  if (isStreamFromReactStreamingPackage(streamOriginal) && !streamOriginal.disabled) {
62
62
  streamFromReactStreamingPackage = streamOriginal;
63
63
  }
64
- const { injectAtStreamBegin, injectAtStreamEnd } = injectHtmlTagsToStream(pageContext, streamFromReactStreamingPackage, injectFilter);
65
- objectAssign(opts, {
66
- injectStringAtBegin: async () => {
67
- return await injectAtStreamBegin(injectString.htmlPartsBegin);
68
- },
69
- injectStringAtEnd: async () => {
70
- return await injectAtStreamEnd(injectString.htmlPartsEnd);
71
- }
72
- });
64
+ const { injectAtStreamBegin, injectAtStreamAfterFirstChunk, injectAtStreamEnd } = injectHtmlTagsToStream(pageContext, streamFromReactStreamingPackage, injectFilter);
65
+ processStreamOptions.injectStringAtBegin = async () => {
66
+ return await injectAtStreamBegin(injectString.htmlPartsBegin);
67
+ };
68
+ processStreamOptions.injectStringAtEnd = async () => {
69
+ return await injectAtStreamEnd(injectString.htmlPartsEnd);
70
+ };
71
+ processStreamOptions.injectStringAfterFirstChunk = () => {
72
+ return injectAtStreamAfterFirstChunk();
73
+ };
73
74
  }
74
- const streamWrapper = await processStream(streamOriginal, opts);
75
+ const streamWrapper = await processStream(streamOriginal, processStreamOptions);
75
76
  return streamWrapper;
76
77
  }
77
78
  function isTemplateWrapped(something) {
@@ -51,8 +51,9 @@ declare function getStreamReadableNode(htmlRender: HtmlRender): Promise<null | S
51
51
  declare function getStreamReadableWeb(htmlRender: HtmlRender): null | StreamReadableWeb;
52
52
  declare function pipeToStreamWritableWeb(htmlRender: HtmlRender, writable: StreamWritableWeb): boolean;
53
53
  declare function pipeToStreamWritableNode(htmlRender: HtmlRender, writable: StreamWritableNode): boolean;
54
- declare function processStream(streamOriginal: StreamProviderAny, { injectStringAtBegin, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }: {
54
+ declare function processStream(streamOriginal: StreamProviderAny, { injectStringAtBegin, injectStringAfterFirstChunk, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }: {
55
55
  injectStringAtBegin?: () => Promise<string>;
56
+ injectStringAfterFirstChunk?: () => string | null;
56
57
  injectStringAtEnd?: () => Promise<string>;
57
58
  onErrorWhileStreaming: (err: unknown) => void;
58
59
  enableEagerStreaming?: boolean;
@@ -203,7 +203,7 @@ function pipeToStreamWritableNode(htmlRender, writable) {
203
203
  checkType(htmlRender);
204
204
  assert(false);
205
205
  }
206
- async function processStream(streamOriginal, { injectStringAtBegin, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }) {
206
+ async function processStream(streamOriginal, { injectStringAtBegin, injectStringAfterFirstChunk, injectStringAtEnd, onErrorWhileStreaming, enableEagerStreaming }) {
207
207
  const buffer = [];
208
208
  let streamOriginalHasStartedEmitting = false;
209
209
  let streamOriginalEnded = false;
@@ -215,6 +215,7 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
215
215
  let resolve;
216
216
  let reject;
217
217
  let promiseHasResolved = false;
218
+ let injectStringAfterFirstChunk_done = false;
218
219
  const streamWrapperPromise = new Promise((resolve_, reject_) => {
219
220
  resolve = (streamWrapper) => {
220
221
  promiseHasResolved = true;
@@ -228,8 +229,8 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
228
229
  let resolveReadyToWrite;
229
230
  const promiseReadyToWrite = new Promise((r) => (resolveReadyToWrite = r));
230
231
  if (injectStringAtBegin) {
231
- const injectionBegin = await injectStringAtBegin();
232
- writeStream(injectionBegin); // Adds injectionBegin to buffer
232
+ const injectedChunk = await injectStringAtBegin();
233
+ writeStream(injectedChunk); // Adds injectedChunk to buffer
233
234
  flushStream(); // Sets shouldFlushStream to true
234
235
  }
235
236
  // We call onStreamEvent() also when the stream ends in order to properly handle the situation when the stream didn't emit any data
@@ -259,6 +260,12 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
259
260
  onData(chunk) {
260
261
  onStreamDataOrEnd(() => {
261
262
  writeStream(chunk);
263
+ if (injectStringAfterFirstChunk && !injectStringAfterFirstChunk_done) {
264
+ const injectedChunk = injectStringAfterFirstChunk();
265
+ if (injectedChunk !== null)
266
+ writeStream(injectedChunk);
267
+ injectStringAfterFirstChunk_done = true;
268
+ }
262
269
  });
263
270
  },
264
271
  async onEnd(
@@ -273,8 +280,8 @@ async function processStream(streamOriginal, { injectStringAtBegin, injectString
273
280
  streamOriginalEnded = true;
274
281
  });
275
282
  if (injectStringAtEnd) {
276
- const injectEnd = await injectStringAtEnd();
277
- writeStream(injectEnd);
283
+ const injectedChunk = await injectStringAtEnd();
284
+ writeStream(injectedChunk);
278
285
  }
279
286
  await promiseReadyToWrite; // E.g. if the user calls the pipe wrapper after the original writable has ended
280
287
  assert(isReady());
@@ -354,6 +354,11 @@ type ConfigBuiltIn = {
354
354
  * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
355
355
  */
356
356
  cacheControl?: string;
357
+ /** Where scripts are injected in the HTML.
358
+ *
359
+ * https://vike.dev/injectScriptsAt
360
+ */
361
+ injectScriptsAt?: 'HTML_BEGIN' | 'HTML_END' | 'STREAM' | null;
357
362
  /** Used by Vike extensions to set their name.
358
363
  *
359
364
  * https://vike.dev/extends
@@ -151,6 +151,10 @@ function valueToJson(value, configName, definedAtData, importStatements) {
151
151
  configValueSerialized = stringify(value, {
152
152
  valueName,
153
153
  forbidReactElements: true,
154
+ // Replace import strings with import variables.
155
+ // - We don't need this anymore and could remove it.
156
+ // - We temporarily needed it for nested document configs (`config.document.{title,description,favicon}`), but we finally decided to go for flat document configs instead (`config.{title,description,favicon}`).
157
+ // - https://github.com/vikejs/vike-react/pull/113
154
158
  replacer(_, value) {
155
159
  if (typeof value === 'string') {
156
160
  const importData = parsePointerImportData(value);
@@ -1,7 +1,7 @@
1
1
  export { projectInfo };
2
2
  export { PROJECT_VERSION };
3
- declare const PROJECT_VERSION: "0.4.179-commit-9384166";
3
+ declare const PROJECT_VERSION: "0.4.179-commit-9822036";
4
4
  declare const projectInfo: {
5
5
  projectName: "Vike";
6
- projectVersion: "0.4.179-commit-9384166";
6
+ projectVersion: "0.4.179-commit-9822036";
7
7
  };
@@ -1,6 +1,6 @@
1
1
  export { projectInfo };
2
2
  export { PROJECT_VERSION };
3
- const PROJECT_VERSION = '0.4.179-commit-9384166';
3
+ const PROJECT_VERSION = '0.4.179-commit-9822036';
4
4
  const projectInfo = {
5
5
  projectName: 'Vike',
6
6
  projectVersion: PROJECT_VERSION
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vike",
3
- "version": "0.4.179-commit-9384166",
3
+ "version": "0.4.179-commit-9822036",
4
4
  "scripts": {
5
5
  "dev": "tsc --watch",
6
6
  "build": "rimraf dist/ && pnpm run build:esm && pnpm run build:cjs",