vike 0.4.148-commit-70e7518 → 0.4.149-commit-041ee42

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 (26) hide show
  1. package/dist/cjs/node/plugin/plugins/devConfig/index.js +20 -9
  2. package/dist/cjs/node/plugin/plugins/importUserCode/v1-design/getVikeConfig/crawlPlusFiles.js +25 -23
  3. package/dist/cjs/node/runtime/html/injectAssets/getHtmlTags.js +117 -81
  4. package/dist/cjs/node/runtime/html/injectAssets/inferHtmlTags.js +5 -5
  5. package/dist/cjs/node/runtime/html/injectAssets/mergeScriptTags.js +2 -1
  6. package/dist/cjs/node/runtime/renderPage/getPageAssets/retrieveAssetsProd.js +2 -3
  7. package/dist/cjs/node/runtime/renderPage/getPageAssets/sortPageAssetsForEarlyHintsHeader.js +6 -0
  8. package/dist/cjs/node/runtime/renderPage/inferMediaType.js +38 -0
  9. package/dist/cjs/shared/page-configs/assertExports.js +11 -4
  10. package/dist/cjs/utils/projectInfo.js +1 -1
  11. package/dist/esm/client/shared/getPageContextSerializedInHtml.js +7 -1
  12. package/dist/esm/node/plugin/plugins/devConfig/index.js +20 -9
  13. package/dist/esm/node/plugin/plugins/importUserCode/v1-design/getVikeConfig/crawlPlusFiles.js +25 -23
  14. package/dist/esm/node/runtime/html/injectAssets/getHtmlTags.d.ts +4 -0
  15. package/dist/esm/node/runtime/html/injectAssets/getHtmlTags.js +117 -81
  16. package/dist/esm/node/runtime/html/injectAssets/inferHtmlTags.d.ts +2 -0
  17. package/dist/esm/node/runtime/html/injectAssets/inferHtmlTags.js +4 -4
  18. package/dist/esm/node/runtime/html/injectAssets/mergeScriptTags.js +2 -1
  19. package/dist/esm/node/runtime/renderPage/getPageAssets/retrieveAssetsProd.js +2 -3
  20. package/dist/esm/node/runtime/renderPage/getPageAssets/sortPageAssetsForEarlyHintsHeader.js +6 -0
  21. package/dist/esm/node/runtime/renderPage/inferMediaType.d.ts +2 -2
  22. package/dist/esm/node/runtime/renderPage/inferMediaType.js +38 -0
  23. package/dist/esm/shared/page-configs/assertExports.js +11 -4
  24. package/dist/esm/utils/projectInfo.d.ts +2 -2
  25. package/dist/esm/utils/projectInfo.js +1 -1
  26. package/package.json +2 -2
@@ -24,22 +24,33 @@ function devConfig() {
24
24
  config() {
25
25
  return {
26
26
  appType: 'custom',
27
+ // TODO:v1-release: remove (AFAICT we only need to use config.optimizeDeps for the old design)
27
28
  optimizeDeps: {
28
29
  exclude: [
29
- // We exclude the vike client to be able to use `import.meta.glob()`
30
+ // We exclude Vike's client runtime to be able to use Vite's import.meta.glob()
30
31
  'vike/client',
31
32
  'vike/client/router',
32
33
  'vike/routing',
33
- // - We also exclude @brillout/json-serializer and @brillout/picocolors to avoid:
34
- // ```
35
- // 9:28:58 AM [vite] ✨ new dependencies optimized: @brillout/json-serializer/parse
36
- // 9:28:58 AM [vite] ✨ optimized dependencies changed. reloading
37
- // ```
38
- // - Previously, we had to exclude @brillout/json-serializer and @brillout/picocolors because of pnpm, but this doesn't seem to be the case anymore.
39
- // - Actually, this should be still the case? How can Vite resolve @brillout/json-serializer when using pnpm?
34
+ // We exclude @brillout/json-serializer and @brillout/picocolors to avoid:
35
+ // ```
36
+ // 9:28:58 AM [vite] ✨ new dependencies optimized: @brillout/json-serializer/parse
37
+ // 9:28:58 AM [vite] ✨ optimized dependencies changed. reloading
38
+ // ```
40
39
  '@brillout/json-serializer/parse',
41
40
  '@brillout/json-serializer/stringify',
42
- '@brillout/picocolors'
41
+ '@brillout/picocolors',
42
+ // We exclude all packages that depend on any optimizeDeps.exclude entry because, otherwise, the entry cannot be resolved when using pnpm. For example:
43
+ // ```
44
+ // Failed to resolve import "@brillout/json-serializer/parse" from "../../packages/vike-react-query/dist/renderer/VikeReactQueryWrapper.js". Does the file exist?
45
+ // 343| // ../../node_modules/.pnpm/react-streaming@0.3.16_react-dom@18.2.0_react@18.2.0/node_modules/react-streaming/dist/esm/client/useAsync.js
46
+ // 344| import { parse as parse2 } from "@brillout/json-serializer/parse";
47
+ // ```
48
+ // The source map is confusing, the import actually lives at node_modules/.vite/deps/vike-react-query_renderer_VikeReactQueryWrapper.js which contains:
49
+ // ```js
50
+ // // ../../node_modules/.pnpm/react-streaming@0.3.16_react-dom@18.2.0_react@18.2.0/node_modules/react-streaming/dist/esm/client/useAsync.js
51
+ // import { parse as parse2 } from "@brillout/json-serializer/parse";
52
+ // ```
53
+ 'react-streaming'
43
54
  ]
44
55
  }
45
56
  };
@@ -56,18 +56,8 @@ async function crawlPlusFiles(userRootDir, outDirAbsoluteFilesystem, isDev) {
56
56
  exports.crawlPlusFiles = crawlPlusFiles;
57
57
  // Same as fastGlob() but using `$ git ls-files`
58
58
  async function gitLsFiles(userRootDir, outDirRelativeFromUserRootDir) {
59
- // Test whether Git is installed and whether userRootDir is inside a Git repository
60
- {
61
- let stdout;
62
- try {
63
- const res = await execA('git rev-parse --is-inside-work-tree', { cwd: userRootDir });
64
- stdout = res.stdout;
65
- }
66
- catch {
67
- return null;
68
- }
69
- (0, utils_js_1.assert)(stdout.trim() === 'true');
70
- }
59
+ if (!(await isUsingGit(userRootDir)))
60
+ return null;
71
61
  const cmd = [
72
62
  'git ls-files',
73
63
  ...utils_js_1.scriptFileExtensionList.map((ext) => `"**/+*.${ext}"`),
@@ -76,20 +66,13 @@ async function gitLsFiles(userRootDir, outDirRelativeFromUserRootDir) {
76
66
  // --cached adds the tracked files to the output
77
67
  '--others --cached --exclude-standard'
78
68
  ].join(' ');
79
- let stdout;
80
- try {
81
- const res = await execA(cmd, { cwd: userRootDir });
82
- stdout = res.stdout;
83
- }
84
- catch (err) {
85
- if (err.message.includes('not a git repository'))
86
- return null;
87
- throw err;
88
- }
89
- let files = stdout.split('\n').filter(Boolean);
69
+ let files = await runCmd(cmd, userRootDir);
90
70
  files = files.filter(
91
71
  // We have to repeat the same exclusion logic here because the `git ls-files` option --exclude only applies to untracked files. (We use --exclude only to speed up the command.)
92
72
  (file) => getIgnoreFilter(file, outDirRelativeFromUserRootDir));
73
+ // Remove tracked but deleted files
74
+ const filesIgnore = await runCmd('git ls-files --deleted', userRootDir);
75
+ files = files.filter((file) => !filesIgnore.includes(file));
93
76
  return files;
94
77
  }
95
78
  // Same as gitLsFiles() but using fast-glob
@@ -126,3 +109,22 @@ function getIgnoreFilter(file, outDirRelativeFromUserRootDir) {
126
109
  !file.includes('.telefunc.') &&
127
110
  (!outDirRelativeFromUserRootDir || !file.startsWith(`${outDirRelativeFromUserRootDir}/`)));
128
111
  }
112
+ // Whether Git is installed and whether userRootDir is inside a Git repository
113
+ async function isUsingGit(userRootDir) {
114
+ let res;
115
+ try {
116
+ res = await execA('git rev-parse --is-inside-work-tree', { cwd: userRootDir });
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ const { stdout, stderr } = res;
122
+ (0, utils_js_1.assert)(stderr.toString().trim() === '');
123
+ (0, utils_js_1.assert)(stdout.toString().trim() === 'true');
124
+ return true;
125
+ }
126
+ async function runCmd(cmd, cwd) {
127
+ const res = await execA(cmd, { cwd });
128
+ (0, utils_js_1.assert)(res.stderr === '');
129
+ return res.stdout.toString().trim().split('\n');
130
+ }
@@ -12,39 +12,37 @@ const getViteDevScripts_js_1 = require("./getViteDevScripts.js");
12
12
  const mergeScriptTags_js_1 = require("./mergeScriptTags.js");
13
13
  const globalContext_js_1 = require("../../globalContext.js");
14
14
  const picocolors_1 = __importDefault(require("@brillout/picocolors"));
15
+ const stamp = '__injectFilterEntry';
15
16
  async function getHtmlTags(pageContext, injectToStream, injectFilter) {
16
17
  (0, utils_js_1.assert)([true, false].includes(pageContext._isHtmlOnly));
17
18
  const isHtmlOnly = pageContext._isHtmlOnly;
18
- const globalContext = (0, globalContext_js_1.getGlobalContext)();
19
- const { isProduction } = globalContext;
20
- const injectJavaScriptDuringStream = !pageContext._pageContextPromise && !!injectToStream;
19
+ const { isProduction } = (0, globalContext_js_1.getGlobalContext)();
21
20
  const pageAssets = await pageContext.__getPageAssets();
22
- const stamp = Symbol('injectFilterEntryStamp');
23
- const getInject = (asset) => {
24
- if (!isProduction) {
25
- return 'HTML_BEGIN';
26
- }
27
- if (asset.assetType === 'style' || asset.assetType === 'font') {
28
- return 'HTML_BEGIN';
29
- }
30
- if (asset.assetType === 'script') {
31
- return 'HTML_END';
32
- }
33
- return false;
34
- };
35
21
  const injectFilterEntries = pageAssets
36
22
  .filter((asset) => {
37
23
  if (asset.isEntry && asset.assetType === 'script') {
38
- // We could in principle allow the user to change the position of <script> but we don't because of `getMergedScriptTag()`
39
- return false;
40
- }
41
- if (isHtmlOnly && asset.assetType === 'script') {
24
+ // We could allow the user to change the position of <script> but we currently don't:
25
+ // - Because of mergeScriptEntries()
26
+ // - We would need to add STREAM to to PreloadFilterInject
27
+ // To suppor this, we should add the JavaScript entry to injectFilterEntries (with an `src` value of `null`)
42
28
  return false;
43
29
  }
44
30
  return true;
45
31
  })
46
32
  .map((asset) => {
47
- const inject = getInject(asset);
33
+ const inject = (() => {
34
+ if (!isProduction) {
35
+ // In development, we should always load assets as soon as possible, in order to eagerly process assets (e.g. applying the transform() hooks of Vite plugins) which are lazily discovered.
36
+ return 'HTML_BEGIN';
37
+ }
38
+ if (asset.assetType === 'style' || asset.assetType === 'font') {
39
+ return 'HTML_BEGIN';
40
+ }
41
+ if (asset.assetType === 'script') {
42
+ return 'HTML_END';
43
+ }
44
+ return false;
45
+ })();
48
46
  const entry = {
49
47
  ...asset,
50
48
  inject,
@@ -53,92 +51,104 @@ async function getHtmlTags(pageContext, injectToStream, injectFilter) {
53
51
  };
54
52
  return entry;
55
53
  });
56
- assertInjectFilterEntries(injectFilterEntries, stamp);
54
+ assertInjectFilterEntries(injectFilterEntries);
55
+ // ==============
56
+ // injectFilter()
57
+ // ==============
57
58
  if (injectFilter && isProduction) {
58
59
  Object.seal(injectFilterEntries); // `Object.seal()` instead of `Object.freeze()` to allow the user to `assets.sort()`
59
60
  Object.values(injectFilterEntries).forEach((entry) => (0, utils_js_1.freezePartial)(entry, { inject: (val) => val === false || val === 'HTML_BEGIN' || val === 'HTML_END' }));
61
+ // Call the user's injectFilter() hook https://vike.dev/injectFilter
60
62
  const res = injectFilter(injectFilterEntries);
61
- (0, utils_js_1.assertUsage)(res === undefined, 'Wrong injectFilter() usage, see https://vike.dev/injectFilter');
62
- assertInjectFilterUsage(injectFilterEntries, stamp);
63
- injectFilterEntries.forEach((a) => {
64
- /*
65
- if (a.assetType === 'script' && a.isEntry) {
66
- assertUsage(a.inject, `[injectFilter()] ${a.src} needs to be injected`)
67
- }
68
- */
69
- if (a.assetType === 'style' && a.isEntry) {
70
- // In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
71
- // - https://github.com/vitejs/vite/issues/2282
72
- // - https://github.com/vikejs/vike/issues/261
73
- (0, utils_js_1.assertWarning)(a.inject, `[injectFilter()] We recommend against not injecting ${a.src}`, {
74
- onlyOnce: true
75
- });
76
- }
77
- if (!isHtmlOnly && a.assetType === 'script') {
78
- (0, utils_js_1.assertWarning)(a.inject, `[injectFilter()] We recommend against not preloading JavaScript (${a.src})`, {
79
- onlyOnce: true
80
- });
81
- }
82
- });
63
+ (0, utils_js_1.assertUsage)(res === undefined, `injectFilter() should return ${picocolors_1.default.cyan('undefined')}, see https://vike.dev/injectFilter`);
64
+ assertInjectFilterUsage(injectFilterEntries);
83
65
  }
84
66
  const htmlTags = [];
67
+ // ==============
85
68
  // Non-JavaScript
86
- for (const asset of injectFilterEntries) {
87
- if (asset.assetType !== 'script' && asset.inject) {
88
- const htmlTag = asset.isEntry ? (0, inferHtmlTags_js_1.inferAssetTag)(asset) : (0, inferHtmlTags_js_1.inferPreloadTag)(asset);
89
- htmlTags.push({ htmlTag, position: asset.inject });
90
- }
91
- }
69
+ // ==============
70
+ injectFilterEntries
71
+ .filter((asset) => asset.assetType !== 'script' && asset.inject)
72
+ .forEach((asset) => {
73
+ if (!asset.inject)
74
+ return;
75
+ const htmlTag = asset.isEntry ? (0, inferHtmlTags_js_1.inferAssetTag)(asset) : (0, inferHtmlTags_js_1.inferPreloadTag)(asset);
76
+ htmlTags.push({ htmlTag, position: asset.inject });
77
+ });
78
+ // ==========
92
79
  // JavaScript
93
- const positionProd = injectJavaScriptDuringStream ? 'STREAM' : 'HTML_END';
94
- const positionScript = !isProduction ? 'HTML_BEGIN' : positionProd;
95
- const positionJsonData = !isProduction && !pageContext._pageContextPromise && !pageContext._isStream ? 'HTML_BEGIN' : positionProd;
96
- const jsScript = await getMergedScriptTag(pageAssets, isProduction);
97
- if (jsScript) {
80
+ // ==========
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
89
+ const positionJavaScriptEntry = (() => {
90
+ if (pageContext._pageContextPromise) {
91
+ (0, utils_js_1.assertWarning)(!injectToStream, "[getHtmlTags()] We recommend against using streaming and a pageContext promise at the same time as partial 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/stream#initial-data-after-stream-end
93
+ return 'HTML_END';
94
+ }
95
+ if (injectToStream) {
96
+ // If there is a stream then, in order to support partial hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary
97
+ return 'STREAM';
98
+ }
99
+ else {
100
+ return 'HTML_END';
101
+ }
102
+ })();
103
+ // <script id="vike_pageContext" type="application/json">
104
+ if (!isHtmlOnly) {
98
105
  htmlTags.push({
99
- htmlTag: jsScript,
100
- position: positionScript
106
+ htmlTag: () =>
107
+ // Needs to be called after resolvePageContextPromise()
108
+ getPageContextJsonScriptTag(pageContext),
109
+ position: positionJavaScriptEntry
101
110
  });
102
111
  }
103
- for (const asset of injectFilterEntries) {
104
- if (asset.assetType === 'script' && asset.inject) {
105
- const htmlTag = asset.isEntry ? (0, inferHtmlTags_js_1.inferAssetTag)(asset) : (0, inferHtmlTags_js_1.inferPreloadTag)(asset);
106
- const position = asset.inject === 'HTML_END' ? positionScript : asset.inject;
107
- htmlTags.push({ htmlTag, position });
108
- }
109
- }
110
- // `pageContext` JSON data
111
- if (!isHtmlOnly) {
112
- // Don't allow the user to manipulate with injectFilter(): injecting <script type="application/json"> before the stream can break the app when:
113
- // - using https://vike.dev/stream#initial-data-after-stream-end
114
- // - `pageContext` is modified during the stream, e.g. https://github.com/brillout/vike-with-pinia which uses https://vuejs.org/api/composition-api-lifecycle.html#onserverprefetch
115
- // The <script> tags are handled separately by vike down below.
112
+ // The JavaScript entry <script> tag
113
+ const scriptEntry = await mergeScriptEntries(pageAssets, isProduction);
114
+ if (scriptEntry) {
116
115
  htmlTags.push({
117
- // Needs to be called after `resolvePageContextPromise()`
118
- htmlTag: () => getPageContextTag(pageContext),
119
- position: positionJsonData
116
+ htmlTag: scriptEntry,
117
+ position: positionJavaScriptEntry
120
118
  });
121
119
  }
120
+ // Preload tags
121
+ injectFilterEntries
122
+ .filter((asset) => asset.assetType === 'script')
123
+ .forEach((asset) => {
124
+ (0, utils_js_1.assert)(!asset.isEntry); // Users cannot re-order JavaScript entries, see creation of injectFilterEntries
125
+ const htmlTag = (0, inferHtmlTags_js_1.inferPreloadTag)(asset);
126
+ if (!asset.inject)
127
+ return;
128
+ // Ideally, instead of this conditional ternary operator, we should add STREAM to PreloadFilterInject (or a better fitting name such as HTML_STREAM)
129
+ const position = asset.inject === 'HTML_END' ? positionJavaScriptEntry : asset.inject;
130
+ htmlTags.push({ htmlTag, position });
131
+ });
122
132
  return htmlTags;
123
133
  }
124
134
  exports.getHtmlTags = getHtmlTags;
125
- async function getMergedScriptTag(pageAssets, isProduction) {
126
- const scriptAssets = pageAssets.filter((pageAsset) => pageAsset.isEntry && pageAsset.assetType === 'script');
135
+ async function mergeScriptEntries(pageAssets, isProduction) {
136
+ const scriptEntries = pageAssets.filter((pageAsset) => pageAsset.isEntry && pageAsset.assetType === 'script');
127
137
  const viteScripts = await (0, getViteDevScripts_js_1.getViteDevScripts)();
128
- const scriptTagsHtml = `${viteScripts}${scriptAssets.map((asset) => (0, inferHtmlTags_js_1.inferAssetTag)(asset)).join('')}`;
138
+ const scriptTagsHtml = `${viteScripts}${scriptEntries.map((asset) => (0, inferHtmlTags_js_1.inferAssetTag)(asset)).join('')}`;
129
139
  const scriptTag = (0, mergeScriptTags_js_1.mergeScriptTags)(scriptTagsHtml, isProduction);
130
140
  return scriptTag;
131
141
  }
132
- function getPageContextTag(pageContext) {
142
+ function getPageContextJsonScriptTag(pageContext) {
133
143
  const pageContextSerialized = (0, sanitizeJson_js_1.sanitizeJson)((0, serializePageContextClientSide_js_1.serializePageContextClientSide)(pageContext));
134
144
  const htmlTag = `<script id="vike_pageContext" type="application/json">${pageContextSerialized}</script>`;
135
145
  // @ts-expect-error
136
146
  pageContext._pageContextHtmlTag = htmlTag;
137
147
  return htmlTag;
138
148
  }
139
- function assertInjectFilterEntries(injectFilterEntries, stamp) {
149
+ function assertInjectFilterEntries(injectFilterEntries) {
140
150
  try {
141
- assertInjectFilterUsage(injectFilterEntries, stamp);
151
+ checkForWrongUsage(injectFilterEntries);
142
152
  }
143
153
  catch (err) {
144
154
  if (err?.message.includes('[Wrong Usage]')) {
@@ -147,7 +157,11 @@ function assertInjectFilterEntries(injectFilterEntries, stamp) {
147
157
  throw err;
148
158
  }
149
159
  }
150
- function assertInjectFilterUsage(injectFilterEntries, stamp) {
160
+ function assertInjectFilterUsage(injectFilterEntries) {
161
+ checkForWrongUsage(injectFilterEntries);
162
+ checkForWarnings(injectFilterEntries);
163
+ }
164
+ function checkForWrongUsage(injectFilterEntries) {
151
165
  injectFilterEntries.forEach((entry, i) => {
152
166
  (0, utils_js_1.assertUsage)((0, utils_js_1.isObject)(entry), `[injectFilter()] Entry ${i} isn't an object`);
153
167
  (0, utils_js_1.assertUsage)(typeof entry.src === 'string', `[injectFilter()] Entry ${i} is missing property ${picocolors_1.default.cyan('src')}`);
@@ -156,6 +170,28 @@ function assertInjectFilterUsage(injectFilterEntries, stamp) {
156
170
  (0, utils_js_1.assert)(entry.assetType === null || typeof entry.assetType === 'string');
157
171
  (0, utils_js_1.assert)(entry.mediaType === null || typeof entry.mediaType === 'string');
158
172
  (0, utils_js_1.assert)(typeof entry.isEntry === 'boolean');
159
- (0, utils_js_1.assert)(Object.keys(entry).length === 5);
173
+ (0, utils_js_1.assert)(Object.keys(entry).length === 6);
174
+ });
175
+ }
176
+ function checkForWarnings(injectFilterEntries) {
177
+ injectFilterEntries.forEach((a) => {
178
+ /*
179
+ if (a.assetType === 'script' && a.isEntry) {
180
+ assertUsage(a.inject, `[injectFilter()] ${a.src} needs to be injected`)
181
+ }
182
+ */
183
+ if (a.assetType === 'style' && a.isEntry) {
184
+ // In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
185
+ // - https://github.com/vitejs/vite/issues/2282
186
+ // - https://github.com/vikejs/vike/issues/261
187
+ (0, utils_js_1.assertWarning)(a.inject, `[injectFilter()] We recommend against not injecting ${a.src}`, {
188
+ onlyOnce: true
189
+ });
190
+ }
191
+ if (a.assetType === 'script') {
192
+ (0, utils_js_1.assertWarning)(a.inject, `[injectFilter()] We recommend against not preloading JavaScript (${a.src})`, {
193
+ onlyOnce: true
194
+ });
195
+ }
160
196
  });
161
197
  }
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.inferEarlyHintLink = exports.inferPreloadTag = exports.inferAssetTag = void 0;
3
+ exports.scriptAttrs = exports.inferEarlyHintLink = exports.inferPreloadTag = exports.inferAssetTag = void 0;
4
4
  const utils_js_1 = require("../../utils.js");
5
+ // We can't use `defer` here. With `defer`, the entry script won't start before `</body>` has been parsed, preventing partial hydration during SSR streaming, see https://github.com/vikejs/vike/pull/1271
6
+ const scriptAttrs = 'type="module" async';
7
+ exports.scriptAttrs = scriptAttrs;
5
8
  function inferPreloadTag(pageAsset) {
6
9
  const { src, assetType, mediaType } = pageAsset;
7
10
  const rel = getRel(pageAsset);
@@ -22,10 +25,7 @@ function inferAssetTag(pageAsset) {
22
25
  const { src, assetType, mediaType } = pageAsset;
23
26
  if (assetType === 'script') {
24
27
  (0, utils_js_1.assert)(mediaType === 'text/javascript');
25
- // Using <script async> seems problematic:
26
- // - in dev: https://github.com/vikejs/vike/issues/524
27
- // - in prod: https://github.com/vikejs/vike/issues/567
28
- return `<script type="module" src="${src}" defer></script>`;
28
+ return `<script src="${src}" ${scriptAttrs}></script>`;
29
29
  }
30
30
  if (assetType === 'style') {
31
31
  return `<link rel="stylesheet" type="text/css" href="${src}">`;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeScriptTags = void 0;
4
4
  const utils_js_1 = require("../../utils.js");
5
+ const inferHtmlTags_js_1 = require("./inferHtmlTags.js");
5
6
  const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims;
6
7
  const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;
7
8
  const typeRE = /\btype\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;
@@ -34,7 +35,7 @@ function mergeScriptTags(scriptTagsHtml, isProduction) {
34
35
  }
35
36
  });
36
37
  if (contents.length > 0) {
37
- scriptTag += `<script type="module" defer>\n${contents.join('\n')}\n</script>`;
38
+ scriptTag += `<script ${inferHtmlTags_js_1.scriptAttrs}>\n${contents.join('\n')}\n</script>`;
38
39
  }
39
40
  }
40
41
  }
@@ -14,8 +14,7 @@ function retrieveAssetsProd(clientDependencies, clientManifest, includeAssetsImp
14
14
  if (onlyAssets) {
15
15
  if (!includeAssetsImportedByServer)
16
16
  return;
17
- // We assume that all npm packages have already built their Vike page files.
18
- // - Bundlers (Rollup, esbuild, tsup, ...) extract the CSS out of JavaScript => we can assume JavaScript to not import any CSS/assets
17
+ // We assume that all npm packages have already built their files: bundlers (Rollup, esbuild, tsup, ...) extract the CSS out of JavaScript => we can assume JavaScript to not import any CSS/assets.
19
18
  if ((0, utils_js_1.isNpmPackageImport)(id))
20
19
  return;
21
20
  if (id.includes('.page.server.')) {
@@ -52,7 +51,7 @@ function collectAssets(manifestKey, assetUrls, visistedAssets, manifest, onlyCol
52
51
  assetUrls.add(`/${asset}`);
53
52
  }
54
53
  }
55
- // Support `config.build.cssCodeSplit: false`, https://github.com/vikejs/vike/issues/644
54
+ // Support `config.build.cssCodeSplit: false`, see https://github.com/vikejs/vike/issues/644
56
55
  function collectSingleStyle(assetUrls, manifest) {
57
56
  const style = manifest['style.css'];
58
57
  if (style && Object.values(manifest).filter((asset) => asset.file.endsWith('.css')).length === 1) {
@@ -22,6 +22,12 @@ function sortPageAssetsForEarlyHintsHeader(pageAssets) {
22
22
  if (assetType === 'image')
23
23
  return priority;
24
24
  priority--;
25
+ if (assetType === 'video')
26
+ return priority;
27
+ priority--;
28
+ if (assetType === 'audio')
29
+ return priority;
30
+ priority--;
25
31
  // Others
26
32
  if (assetType !== 'script')
27
33
  return priority;
@@ -36,6 +36,44 @@ function inferMediaType(href) {
36
36
  if (href.endsWith('.woff2')) {
37
37
  return { assetType: 'font', mediaType: 'font/woff2' };
38
38
  }
39
+ // Videos
40
+ if (href.endsWith('.mp4')) {
41
+ return { assetType: 'video', mediaType: 'video/mp4' };
42
+ }
43
+ if (href.endsWith('.webm')) {
44
+ return { assetType: 'video', mediaType: 'video/webm' };
45
+ }
46
+ if (href.endsWith('.ogv')) {
47
+ return { assetType: 'video', mediaType: 'video/ogg' };
48
+ }
49
+ if (href.endsWith('.mpeg') || href.endsWith('.mpg')) {
50
+ return { assetType: 'video', mediaType: 'video/mpeg' };
51
+ }
52
+ if (href.endsWith('.avi')) {
53
+ return { assetType: 'video', mediaType: 'video/x-msvideo' };
54
+ }
55
+ if (href.endsWith('.mov') || href.endsWith('.qt')) {
56
+ return { assetType: 'video', mediaType: 'video/quicktime' };
57
+ }
58
+ // Audios
59
+ if (href.endsWith('.mp3')) {
60
+ return { assetType: 'audio', mediaType: 'audio/mpeg' };
61
+ }
62
+ if (href.endsWith('.wav')) {
63
+ return { assetType: 'audio', mediaType: 'audio/wav' };
64
+ }
65
+ if (href.endsWith('.ogg')) {
66
+ return { assetType: 'audio', mediaType: 'audio/ogg' };
67
+ }
68
+ if (href.endsWith('.m4a')) {
69
+ return { assetType: 'audio', mediaType: 'audio/aac' };
70
+ }
71
+ if (href.endsWith('midi') || href.endsWith('.mid')) {
72
+ return { assetType: 'audio', mediaType: 'audio/midi' };
73
+ }
74
+ if (href.endsWith('.flac')) {
75
+ return { assetType: 'audio', mediaType: 'audio/flac' };
76
+ }
39
77
  return null;
40
78
  }
41
79
  exports.inferMediaType = inferMediaType;
@@ -34,13 +34,20 @@ function assertExports(fileExports, filePathToShowToUser, configName) {
34
34
  if (exportsRelevant.length === 1) {
35
35
  return;
36
36
  }
37
- else {
38
- (0, utils_js_1.assert)(exportsRelevant.length === 0);
39
- let errMsg = `${filePathToShowToUser} doesn't export any value, but it should have a ${picocolors_1.default.cyan('export default')}`;
37
+ const exportDefault = picocolors_1.default.cyan('export default');
38
+ const exportConfigName = picocolors_1.default.cyan(`export { ${configName} }`);
39
+ if (exportsRelevant.length === 0) {
40
+ let errMsg = `${filePathToShowToUser} doesn't export any value, but it should have a ${exportDefault}`;
40
41
  if (configName)
41
- errMsg += ` or ${picocolors_1.default.cyan(`export { ${configName} }`)}`;
42
+ errMsg += ` or ${exportConfigName}`;
42
43
  (0, utils_js_1.assertUsage)(false, errMsg);
43
44
  }
45
+ else {
46
+ (0, utils_js_1.assert)(exportsRelevant.length === 2);
47
+ (0, utils_js_1.assertWarning)(false, `${filePathToShowToUser} remove ${exportConfigName} or ${exportDefault}`, {
48
+ onlyOnce: true
49
+ });
50
+ }
44
51
  }
45
52
  else {
46
53
  // !configName => isConfigFile
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PROJECT_VERSION = exports.projectInfo = void 0;
4
4
  const assertSingleInstance_js_1 = require("./assertSingleInstance.js");
5
- const PROJECT_VERSION = '0.4.148-commit-70e7518';
5
+ const PROJECT_VERSION = '0.4.149-commit-041ee42';
6
6
  exports.PROJECT_VERSION = PROJECT_VERSION;
7
7
  const projectInfo = {
8
8
  projectName: 'Vike',
@@ -2,9 +2,15 @@ import { parse } from '@brillout/json-serializer/parse';
2
2
  import { hasProp, assert, assertUsage, objectAssign } from '../server-routing-runtime/utils.js';
3
3
  export { getPageContextSerializedInHtml };
4
4
  function getPageContextSerializedInHtml() {
5
+ // elem should exist because:
6
+ // 1. <script id="vike_pageContext" type="application/json"> appears before the <script> that loads Vike's client runtime (which includes this file)
7
+ // 2. <script id="vike_pageContext" type="application/json"> is neither async nor defer
8
+ // See https://github.com/vikejs/vike/pull/1271
5
9
  const id = 'vike_pageContext';
6
10
  const elem = document.getElementById(id);
7
- assertUsage(elem, `The element #${id} (which Vike automatically injects into the HTML) is missing from the DOM. This may happen if your HTML is malformed. Make sure your HTML isn't malformed, and make sure you don't remove #${id} from the HTML nor from the DOM.`);
11
+ assertUsage(elem,
12
+ // It seems like it can be missing because of malformed HTML: https://github.com/vikejs/vike/issues/913
13
+ `Couldn't find #${id} (which Vike automatically injects in the HTML): make sure it exists (i.e. don't remove it and make sure your HTML isn't malformed)`);
8
14
  const pageContextJson = elem.textContent;
9
15
  assert(pageContextJson);
10
16
  const pageContextSerializedInHtml = parse(pageContextJson);
@@ -22,22 +22,33 @@ function devConfig() {
22
22
  config() {
23
23
  return {
24
24
  appType: 'custom',
25
+ // TODO:v1-release: remove (AFAICT we only need to use config.optimizeDeps for the old design)
25
26
  optimizeDeps: {
26
27
  exclude: [
27
- // We exclude the vike client to be able to use `import.meta.glob()`
28
+ // We exclude Vike's client runtime to be able to use Vite's import.meta.glob()
28
29
  'vike/client',
29
30
  'vike/client/router',
30
31
  'vike/routing',
31
- // - We also exclude @brillout/json-serializer and @brillout/picocolors to avoid:
32
- // ```
33
- // 9:28:58 AM [vite] ✨ new dependencies optimized: @brillout/json-serializer/parse
34
- // 9:28:58 AM [vite] ✨ optimized dependencies changed. reloading
35
- // ```
36
- // - Previously, we had to exclude @brillout/json-serializer and @brillout/picocolors because of pnpm, but this doesn't seem to be the case anymore.
37
- // - Actually, this should be still the case? How can Vite resolve @brillout/json-serializer when using pnpm?
32
+ // We exclude @brillout/json-serializer and @brillout/picocolors to avoid:
33
+ // ```
34
+ // 9:28:58 AM [vite] ✨ new dependencies optimized: @brillout/json-serializer/parse
35
+ // 9:28:58 AM [vite] ✨ optimized dependencies changed. reloading
36
+ // ```
38
37
  '@brillout/json-serializer/parse',
39
38
  '@brillout/json-serializer/stringify',
40
- '@brillout/picocolors'
39
+ '@brillout/picocolors',
40
+ // We exclude all packages that depend on any optimizeDeps.exclude entry because, otherwise, the entry cannot be resolved when using pnpm. For example:
41
+ // ```
42
+ // Failed to resolve import "@brillout/json-serializer/parse" from "../../packages/vike-react-query/dist/renderer/VikeReactQueryWrapper.js". Does the file exist?
43
+ // 343| // ../../node_modules/.pnpm/react-streaming@0.3.16_react-dom@18.2.0_react@18.2.0/node_modules/react-streaming/dist/esm/client/useAsync.js
44
+ // 344| import { parse as parse2 } from "@brillout/json-serializer/parse";
45
+ // ```
46
+ // The source map is confusing, the import actually lives at node_modules/.vite/deps/vike-react-query_renderer_VikeReactQueryWrapper.js which contains:
47
+ // ```js
48
+ // // ../../node_modules/.pnpm/react-streaming@0.3.16_react-dom@18.2.0_react@18.2.0/node_modules/react-streaming/dist/esm/client/useAsync.js
49
+ // import { parse as parse2 } from "@brillout/json-serializer/parse";
50
+ // ```
51
+ 'react-streaming'
41
52
  ]
42
53
  }
43
54
  };
@@ -50,18 +50,8 @@ async function crawlPlusFiles(userRootDir, outDirAbsoluteFilesystem, isDev) {
50
50
  }
51
51
  // Same as fastGlob() but using `$ git ls-files`
52
52
  async function gitLsFiles(userRootDir, outDirRelativeFromUserRootDir) {
53
- // Test whether Git is installed and whether userRootDir is inside a Git repository
54
- {
55
- let stdout;
56
- try {
57
- const res = await execA('git rev-parse --is-inside-work-tree', { cwd: userRootDir });
58
- stdout = res.stdout;
59
- }
60
- catch {
61
- return null;
62
- }
63
- assert(stdout.trim() === 'true');
64
- }
53
+ if (!(await isUsingGit(userRootDir)))
54
+ return null;
65
55
  const cmd = [
66
56
  'git ls-files',
67
57
  ...scriptFileExtensionList.map((ext) => `"**/+*.${ext}"`),
@@ -70,20 +60,13 @@ async function gitLsFiles(userRootDir, outDirRelativeFromUserRootDir) {
70
60
  // --cached adds the tracked files to the output
71
61
  '--others --cached --exclude-standard'
72
62
  ].join(' ');
73
- let stdout;
74
- try {
75
- const res = await execA(cmd, { cwd: userRootDir });
76
- stdout = res.stdout;
77
- }
78
- catch (err) {
79
- if (err.message.includes('not a git repository'))
80
- return null;
81
- throw err;
82
- }
83
- let files = stdout.split('\n').filter(Boolean);
63
+ let files = await runCmd(cmd, userRootDir);
84
64
  files = files.filter(
85
65
  // We have to repeat the same exclusion logic here because the `git ls-files` option --exclude only applies to untracked files. (We use --exclude only to speed up the command.)
86
66
  (file) => getIgnoreFilter(file, outDirRelativeFromUserRootDir));
67
+ // Remove tracked but deleted files
68
+ const filesIgnore = await runCmd('git ls-files --deleted', userRootDir);
69
+ files = files.filter((file) => !filesIgnore.includes(file));
87
70
  return files;
88
71
  }
89
72
  // Same as gitLsFiles() but using fast-glob
@@ -120,3 +103,22 @@ function getIgnoreFilter(file, outDirRelativeFromUserRootDir) {
120
103
  !file.includes('.telefunc.') &&
121
104
  (!outDirRelativeFromUserRootDir || !file.startsWith(`${outDirRelativeFromUserRootDir}/`)));
122
105
  }
106
+ // Whether Git is installed and whether userRootDir is inside a Git repository
107
+ async function isUsingGit(userRootDir) {
108
+ let res;
109
+ try {
110
+ res = await execA('git rev-parse --is-inside-work-tree', { cwd: userRootDir });
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ const { stdout, stderr } = res;
116
+ assert(stderr.toString().trim() === '');
117
+ assert(stdout.toString().trim() === 'true');
118
+ return true;
119
+ }
120
+ async function runCmd(cmd, cwd) {
121
+ const res = await execA(cmd, { cwd });
122
+ assert(res.stderr === '');
123
+ return res.stdout.toString().trim().split('\n');
124
+ }
@@ -15,6 +15,10 @@ type InjectFilterEntry = {
15
15
  src: string;
16
16
  assetType: null | PageAsset['assetType'];
17
17
  mediaType: null | PageAsset['mediaType'];
18
+ /** Whether src is a root in the dependency graph.
19
+ * - For JavaScript this means whether src is imported by a dynamic import() statement.
20
+ * - All CSS files have `isEntry: true`
21
+ */
18
22
  isEntry: boolean;
19
23
  inject: PreloadFilterInject;
20
24
  };
@@ -7,39 +7,37 @@ import { getViteDevScripts } from './getViteDevScripts.js';
7
7
  import { mergeScriptTags } from './mergeScriptTags.js';
8
8
  import { getGlobalContext } from '../../globalContext.js';
9
9
  import pc from '@brillout/picocolors';
10
+ const stamp = '__injectFilterEntry';
10
11
  async function getHtmlTags(pageContext, injectToStream, injectFilter) {
11
12
  assert([true, false].includes(pageContext._isHtmlOnly));
12
13
  const isHtmlOnly = pageContext._isHtmlOnly;
13
- const globalContext = getGlobalContext();
14
- const { isProduction } = globalContext;
15
- const injectJavaScriptDuringStream = !pageContext._pageContextPromise && !!injectToStream;
14
+ const { isProduction } = getGlobalContext();
16
15
  const pageAssets = await pageContext.__getPageAssets();
17
- const stamp = Symbol('injectFilterEntryStamp');
18
- const getInject = (asset) => {
19
- if (!isProduction) {
20
- return 'HTML_BEGIN';
21
- }
22
- if (asset.assetType === 'style' || asset.assetType === 'font') {
23
- return 'HTML_BEGIN';
24
- }
25
- if (asset.assetType === 'script') {
26
- return 'HTML_END';
27
- }
28
- return false;
29
- };
30
16
  const injectFilterEntries = pageAssets
31
17
  .filter((asset) => {
32
18
  if (asset.isEntry && asset.assetType === 'script') {
33
- // We could in principle allow the user to change the position of <script> but we don't because of `getMergedScriptTag()`
34
- return false;
35
- }
36
- if (isHtmlOnly && asset.assetType === 'script') {
19
+ // We could allow the user to change the position of <script> but we currently don't:
20
+ // - Because of mergeScriptEntries()
21
+ // - We would need to add STREAM to to PreloadFilterInject
22
+ // To suppor this, we should add the JavaScript entry to injectFilterEntries (with an `src` value of `null`)
37
23
  return false;
38
24
  }
39
25
  return true;
40
26
  })
41
27
  .map((asset) => {
42
- const inject = getInject(asset);
28
+ const inject = (() => {
29
+ if (!isProduction) {
30
+ // In development, we should always load assets as soon as possible, in order to eagerly process assets (e.g. applying the transform() hooks of Vite plugins) which are lazily discovered.
31
+ return 'HTML_BEGIN';
32
+ }
33
+ if (asset.assetType === 'style' || asset.assetType === 'font') {
34
+ return 'HTML_BEGIN';
35
+ }
36
+ if (asset.assetType === 'script') {
37
+ return 'HTML_END';
38
+ }
39
+ return false;
40
+ })();
43
41
  const entry = {
44
42
  ...asset,
45
43
  inject,
@@ -48,91 +46,103 @@ async function getHtmlTags(pageContext, injectToStream, injectFilter) {
48
46
  };
49
47
  return entry;
50
48
  });
51
- assertInjectFilterEntries(injectFilterEntries, stamp);
49
+ assertInjectFilterEntries(injectFilterEntries);
50
+ // ==============
51
+ // injectFilter()
52
+ // ==============
52
53
  if (injectFilter && isProduction) {
53
54
  Object.seal(injectFilterEntries); // `Object.seal()` instead of `Object.freeze()` to allow the user to `assets.sort()`
54
55
  Object.values(injectFilterEntries).forEach((entry) => freezePartial(entry, { inject: (val) => val === false || val === 'HTML_BEGIN' || val === 'HTML_END' }));
56
+ // Call the user's injectFilter() hook https://vike.dev/injectFilter
55
57
  const res = injectFilter(injectFilterEntries);
56
- assertUsage(res === undefined, 'Wrong injectFilter() usage, see https://vike.dev/injectFilter');
57
- assertInjectFilterUsage(injectFilterEntries, stamp);
58
- injectFilterEntries.forEach((a) => {
59
- /*
60
- if (a.assetType === 'script' && a.isEntry) {
61
- assertUsage(a.inject, `[injectFilter()] ${a.src} needs to be injected`)
62
- }
63
- */
64
- if (a.assetType === 'style' && a.isEntry) {
65
- // In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
66
- // - https://github.com/vitejs/vite/issues/2282
67
- // - https://github.com/vikejs/vike/issues/261
68
- assertWarning(a.inject, `[injectFilter()] We recommend against not injecting ${a.src}`, {
69
- onlyOnce: true
70
- });
71
- }
72
- if (!isHtmlOnly && a.assetType === 'script') {
73
- assertWarning(a.inject, `[injectFilter()] We recommend against not preloading JavaScript (${a.src})`, {
74
- onlyOnce: true
75
- });
76
- }
77
- });
58
+ assertUsage(res === undefined, `injectFilter() should return ${pc.cyan('undefined')}, see https://vike.dev/injectFilter`);
59
+ assertInjectFilterUsage(injectFilterEntries);
78
60
  }
79
61
  const htmlTags = [];
62
+ // ==============
80
63
  // Non-JavaScript
81
- for (const asset of injectFilterEntries) {
82
- if (asset.assetType !== 'script' && asset.inject) {
83
- const htmlTag = asset.isEntry ? inferAssetTag(asset) : inferPreloadTag(asset);
84
- htmlTags.push({ htmlTag, position: asset.inject });
85
- }
86
- }
64
+ // ==============
65
+ injectFilterEntries
66
+ .filter((asset) => asset.assetType !== 'script' && asset.inject)
67
+ .forEach((asset) => {
68
+ if (!asset.inject)
69
+ return;
70
+ const htmlTag = asset.isEntry ? inferAssetTag(asset) : inferPreloadTag(asset);
71
+ htmlTags.push({ htmlTag, position: asset.inject });
72
+ });
73
+ // ==========
87
74
  // JavaScript
88
- const positionProd = injectJavaScriptDuringStream ? 'STREAM' : 'HTML_END';
89
- const positionScript = !isProduction ? 'HTML_BEGIN' : positionProd;
90
- const positionJsonData = !isProduction && !pageContext._pageContextPromise && !pageContext._isStream ? 'HTML_BEGIN' : positionProd;
91
- const jsScript = await getMergedScriptTag(pageAssets, isProduction);
92
- if (jsScript) {
75
+ // ==========
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
84
+ const positionJavaScriptEntry = (() => {
85
+ if (pageContext._pageContextPromise) {
86
+ assertWarning(!injectToStream, "[getHtmlTags()] We recommend against using streaming and a pageContext promise at the same time as partial 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/stream#initial-data-after-stream-end
88
+ return 'HTML_END';
89
+ }
90
+ if (injectToStream) {
91
+ // If there is a stream then, in order to support partial hydration, inject the JavaScript during the stream after React(/Vue/Solid/...) resolved the first suspense boundary
92
+ return 'STREAM';
93
+ }
94
+ else {
95
+ return 'HTML_END';
96
+ }
97
+ })();
98
+ // <script id="vike_pageContext" type="application/json">
99
+ if (!isHtmlOnly) {
93
100
  htmlTags.push({
94
- htmlTag: jsScript,
95
- position: positionScript
101
+ htmlTag: () =>
102
+ // Needs to be called after resolvePageContextPromise()
103
+ getPageContextJsonScriptTag(pageContext),
104
+ position: positionJavaScriptEntry
96
105
  });
97
106
  }
98
- for (const asset of injectFilterEntries) {
99
- if (asset.assetType === 'script' && asset.inject) {
100
- const htmlTag = asset.isEntry ? inferAssetTag(asset) : inferPreloadTag(asset);
101
- const position = asset.inject === 'HTML_END' ? positionScript : asset.inject;
102
- htmlTags.push({ htmlTag, position });
103
- }
104
- }
105
- // `pageContext` JSON data
106
- if (!isHtmlOnly) {
107
- // Don't allow the user to manipulate with injectFilter(): injecting <script type="application/json"> before the stream can break the app when:
108
- // - using https://vike.dev/stream#initial-data-after-stream-end
109
- // - `pageContext` is modified during the stream, e.g. https://github.com/brillout/vike-with-pinia which uses https://vuejs.org/api/composition-api-lifecycle.html#onserverprefetch
110
- // The <script> tags are handled separately by vike down below.
107
+ // The JavaScript entry <script> tag
108
+ const scriptEntry = await mergeScriptEntries(pageAssets, isProduction);
109
+ if (scriptEntry) {
111
110
  htmlTags.push({
112
- // Needs to be called after `resolvePageContextPromise()`
113
- htmlTag: () => getPageContextTag(pageContext),
114
- position: positionJsonData
111
+ htmlTag: scriptEntry,
112
+ position: positionJavaScriptEntry
115
113
  });
116
114
  }
115
+ // Preload tags
116
+ injectFilterEntries
117
+ .filter((asset) => asset.assetType === 'script')
118
+ .forEach((asset) => {
119
+ assert(!asset.isEntry); // Users cannot re-order JavaScript entries, see creation of injectFilterEntries
120
+ const htmlTag = inferPreloadTag(asset);
121
+ if (!asset.inject)
122
+ return;
123
+ // Ideally, instead of this conditional ternary operator, we should add STREAM to PreloadFilterInject (or a better fitting name such as HTML_STREAM)
124
+ const position = asset.inject === 'HTML_END' ? positionJavaScriptEntry : asset.inject;
125
+ htmlTags.push({ htmlTag, position });
126
+ });
117
127
  return htmlTags;
118
128
  }
119
- async function getMergedScriptTag(pageAssets, isProduction) {
120
- const scriptAssets = pageAssets.filter((pageAsset) => pageAsset.isEntry && pageAsset.assetType === 'script');
129
+ async function mergeScriptEntries(pageAssets, isProduction) {
130
+ const scriptEntries = pageAssets.filter((pageAsset) => pageAsset.isEntry && pageAsset.assetType === 'script');
121
131
  const viteScripts = await getViteDevScripts();
122
- const scriptTagsHtml = `${viteScripts}${scriptAssets.map((asset) => inferAssetTag(asset)).join('')}`;
132
+ const scriptTagsHtml = `${viteScripts}${scriptEntries.map((asset) => inferAssetTag(asset)).join('')}`;
123
133
  const scriptTag = mergeScriptTags(scriptTagsHtml, isProduction);
124
134
  return scriptTag;
125
135
  }
126
- function getPageContextTag(pageContext) {
136
+ function getPageContextJsonScriptTag(pageContext) {
127
137
  const pageContextSerialized = sanitizeJson(serializePageContextClientSide(pageContext));
128
138
  const htmlTag = `<script id="vike_pageContext" type="application/json">${pageContextSerialized}</script>`;
129
139
  // @ts-expect-error
130
140
  pageContext._pageContextHtmlTag = htmlTag;
131
141
  return htmlTag;
132
142
  }
133
- function assertInjectFilterEntries(injectFilterEntries, stamp) {
143
+ function assertInjectFilterEntries(injectFilterEntries) {
134
144
  try {
135
- assertInjectFilterUsage(injectFilterEntries, stamp);
145
+ checkForWrongUsage(injectFilterEntries);
136
146
  }
137
147
  catch (err) {
138
148
  if (err?.message.includes('[Wrong Usage]')) {
@@ -141,7 +151,11 @@ function assertInjectFilterEntries(injectFilterEntries, stamp) {
141
151
  throw err;
142
152
  }
143
153
  }
144
- function assertInjectFilterUsage(injectFilterEntries, stamp) {
154
+ function assertInjectFilterUsage(injectFilterEntries) {
155
+ checkForWrongUsage(injectFilterEntries);
156
+ checkForWarnings(injectFilterEntries);
157
+ }
158
+ function checkForWrongUsage(injectFilterEntries) {
145
159
  injectFilterEntries.forEach((entry, i) => {
146
160
  assertUsage(isObject(entry), `[injectFilter()] Entry ${i} isn't an object`);
147
161
  assertUsage(typeof entry.src === 'string', `[injectFilter()] Entry ${i} is missing property ${pc.cyan('src')}`);
@@ -150,6 +164,28 @@ function assertInjectFilterUsage(injectFilterEntries, stamp) {
150
164
  assert(entry.assetType === null || typeof entry.assetType === 'string');
151
165
  assert(entry.mediaType === null || typeof entry.mediaType === 'string');
152
166
  assert(typeof entry.isEntry === 'boolean');
153
- assert(Object.keys(entry).length === 5);
167
+ assert(Object.keys(entry).length === 6);
168
+ });
169
+ }
170
+ function checkForWarnings(injectFilterEntries) {
171
+ injectFilterEntries.forEach((a) => {
172
+ /*
173
+ if (a.assetType === 'script' && a.isEntry) {
174
+ assertUsage(a.inject, `[injectFilter()] ${a.src} needs to be injected`)
175
+ }
176
+ */
177
+ if (a.assetType === 'style' && a.isEntry) {
178
+ // In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
179
+ // - https://github.com/vitejs/vite/issues/2282
180
+ // - https://github.com/vikejs/vike/issues/261
181
+ assertWarning(a.inject, `[injectFilter()] We recommend against not injecting ${a.src}`, {
182
+ onlyOnce: true
183
+ });
184
+ }
185
+ if (a.assetType === 'script') {
186
+ assertWarning(a.inject, `[injectFilter()] We recommend against not preloading JavaScript (${a.src})`, {
187
+ onlyOnce: true
188
+ });
189
+ }
154
190
  });
155
191
  }
@@ -1,7 +1,9 @@
1
1
  export { inferAssetTag };
2
2
  export { inferPreloadTag };
3
3
  export { inferEarlyHintLink };
4
+ export { scriptAttrs };
4
5
  import type { PageAsset } from '../../renderPage/getPageAssets.js';
6
+ declare const scriptAttrs = "type=\"module\" async";
5
7
  declare function inferPreloadTag(pageAsset: PageAsset): string;
6
8
  declare function inferAssetTag(pageAsset: PageAsset): string;
7
9
  declare function inferEarlyHintLink(pageAsset: PageAsset): string;
@@ -1,7 +1,10 @@
1
1
  export { inferAssetTag };
2
2
  export { inferPreloadTag };
3
3
  export { inferEarlyHintLink };
4
+ export { scriptAttrs };
4
5
  import { assert } from '../../utils.js';
6
+ // We can't use `defer` here. With `defer`, the entry script won't start before `</body>` has been parsed, preventing partial hydration during SSR streaming, see https://github.com/vikejs/vike/pull/1271
7
+ const scriptAttrs = 'type="module" async';
5
8
  function inferPreloadTag(pageAsset) {
6
9
  const { src, assetType, mediaType } = pageAsset;
7
10
  const rel = getRel(pageAsset);
@@ -21,10 +24,7 @@ function inferAssetTag(pageAsset) {
21
24
  const { src, assetType, mediaType } = pageAsset;
22
25
  if (assetType === 'script') {
23
26
  assert(mediaType === 'text/javascript');
24
- // Using <script async> seems problematic:
25
- // - in dev: https://github.com/vikejs/vike/issues/524
26
- // - in prod: https://github.com/vikejs/vike/issues/567
27
- return `<script type="module" src="${src}" defer></script>`;
27
+ return `<script src="${src}" ${scriptAttrs}></script>`;
28
28
  }
29
29
  if (assetType === 'style') {
30
30
  return `<link rel="stylesheet" type="text/css" href="${src}">`;
@@ -1,5 +1,6 @@
1
1
  export { mergeScriptTags };
2
2
  import { assert } from '../../utils.js';
3
+ import { scriptAttrs } from './inferHtmlTags.js';
3
4
  const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims;
4
5
  const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;
5
6
  const typeRE = /\btype\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;
@@ -32,7 +33,7 @@ function mergeScriptTags(scriptTagsHtml, isProduction) {
32
33
  }
33
34
  });
34
35
  if (contents.length > 0) {
35
- scriptTag += `<script type="module" defer>\n${contents.join('\n')}\n</script>`;
36
+ scriptTag += `<script ${scriptAttrs}>\n${contents.join('\n')}\n</script>`;
36
37
  }
37
38
  }
38
39
  }
@@ -12,8 +12,7 @@ function retrieveAssetsProd(clientDependencies, clientManifest, includeAssetsImp
12
12
  if (onlyAssets) {
13
13
  if (!includeAssetsImportedByServer)
14
14
  return;
15
- // We assume that all npm packages have already built their Vike page files.
16
- // - Bundlers (Rollup, esbuild, tsup, ...) extract the CSS out of JavaScript => we can assume JavaScript to not import any CSS/assets
15
+ // We assume that all npm packages have already built their files: bundlers (Rollup, esbuild, tsup, ...) extract the CSS out of JavaScript => we can assume JavaScript to not import any CSS/assets.
17
16
  if (isNpmPackageImport(id))
18
17
  return;
19
18
  if (id.includes('.page.server.')) {
@@ -49,7 +48,7 @@ function collectAssets(manifestKey, assetUrls, visistedAssets, manifest, onlyCol
49
48
  assetUrls.add(`/${asset}`);
50
49
  }
51
50
  }
52
- // Support `config.build.cssCodeSplit: false`, https://github.com/vikejs/vike/issues/644
51
+ // Support `config.build.cssCodeSplit: false`, see https://github.com/vikejs/vike/issues/644
53
52
  function collectSingleStyle(assetUrls, manifest) {
54
53
  const style = manifest['style.css'];
55
54
  if (style && Object.values(manifest).filter((asset) => asset.file.endsWith('.css')).length === 1) {
@@ -20,6 +20,12 @@ function sortPageAssetsForEarlyHintsHeader(pageAssets) {
20
20
  if (assetType === 'image')
21
21
  return priority;
22
22
  priority--;
23
+ if (assetType === 'video')
24
+ return priority;
25
+ priority--;
26
+ if (assetType === 'audio')
27
+ return priority;
28
+ priority--;
23
29
  // Others
24
30
  if (assetType !== 'script')
25
31
  return priority;
@@ -1,7 +1,7 @@
1
1
  export { inferMediaType };
2
2
  export type { MediaType };
3
3
  type MediaType = null | {
4
- assetType: 'image' | 'script' | 'font' | 'style';
5
- mediaType: 'text/javascript' | 'text/css' | 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif' | 'image/svg+xml' | 'font/ttf' | 'font/woff' | 'font/woff2';
4
+ assetType: 'image' | 'script' | 'font' | 'style' | 'audio' | 'video' | 'document' | 'fetch' | 'track' | 'worker' | 'embed' | 'object';
5
+ mediaType: 'text/javascript' | 'text/css' | 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif' | 'image/svg+xml' | 'font/ttf' | 'font/woff' | 'font/woff2' | 'video/mp4' | 'video/webm' | 'video/ogg' | 'video/mpeg' | 'video/x-msvideo' | 'video/quicktime' | 'audio/mpeg' | 'audio/wav' | 'audio/ogg' | 'audio/aac' | 'audio/midi' | 'audio/flac';
6
6
  };
7
7
  declare function inferMediaType(href: string): MediaType;
@@ -34,5 +34,43 @@ function inferMediaType(href) {
34
34
  if (href.endsWith('.woff2')) {
35
35
  return { assetType: 'font', mediaType: 'font/woff2' };
36
36
  }
37
+ // Videos
38
+ if (href.endsWith('.mp4')) {
39
+ return { assetType: 'video', mediaType: 'video/mp4' };
40
+ }
41
+ if (href.endsWith('.webm')) {
42
+ return { assetType: 'video', mediaType: 'video/webm' };
43
+ }
44
+ if (href.endsWith('.ogv')) {
45
+ return { assetType: 'video', mediaType: 'video/ogg' };
46
+ }
47
+ if (href.endsWith('.mpeg') || href.endsWith('.mpg')) {
48
+ return { assetType: 'video', mediaType: 'video/mpeg' };
49
+ }
50
+ if (href.endsWith('.avi')) {
51
+ return { assetType: 'video', mediaType: 'video/x-msvideo' };
52
+ }
53
+ if (href.endsWith('.mov') || href.endsWith('.qt')) {
54
+ return { assetType: 'video', mediaType: 'video/quicktime' };
55
+ }
56
+ // Audios
57
+ if (href.endsWith('.mp3')) {
58
+ return { assetType: 'audio', mediaType: 'audio/mpeg' };
59
+ }
60
+ if (href.endsWith('.wav')) {
61
+ return { assetType: 'audio', mediaType: 'audio/wav' };
62
+ }
63
+ if (href.endsWith('.ogg')) {
64
+ return { assetType: 'audio', mediaType: 'audio/ogg' };
65
+ }
66
+ if (href.endsWith('.m4a')) {
67
+ return { assetType: 'audio', mediaType: 'audio/aac' };
68
+ }
69
+ if (href.endsWith('midi') || href.endsWith('.mid')) {
70
+ return { assetType: 'audio', mediaType: 'audio/midi' };
71
+ }
72
+ if (href.endsWith('.flac')) {
73
+ return { assetType: 'audio', mediaType: 'audio/flac' };
74
+ }
37
75
  return null;
38
76
  }
@@ -28,13 +28,20 @@ function assertExports(fileExports, filePathToShowToUser, configName) {
28
28
  if (exportsRelevant.length === 1) {
29
29
  return;
30
30
  }
31
- else {
32
- assert(exportsRelevant.length === 0);
33
- let errMsg = `${filePathToShowToUser} doesn't export any value, but it should have a ${pc.cyan('export default')}`;
31
+ const exportDefault = pc.cyan('export default');
32
+ const exportConfigName = pc.cyan(`export { ${configName} }`);
33
+ if (exportsRelevant.length === 0) {
34
+ let errMsg = `${filePathToShowToUser} doesn't export any value, but it should have a ${exportDefault}`;
34
35
  if (configName)
35
- errMsg += ` or ${pc.cyan(`export { ${configName} }`)}`;
36
+ errMsg += ` or ${exportConfigName}`;
36
37
  assertUsage(false, errMsg);
37
38
  }
39
+ else {
40
+ assert(exportsRelevant.length === 2);
41
+ assertWarning(false, `${filePathToShowToUser} remove ${exportConfigName} or ${exportDefault}`, {
42
+ onlyOnce: true
43
+ });
44
+ }
38
45
  }
39
46
  else {
40
47
  // !configName => isConfigFile
@@ -1,13 +1,13 @@
1
1
  export { projectInfo };
2
2
  export type { ProjectTag };
3
3
  export { PROJECT_VERSION };
4
- declare const PROJECT_VERSION: "0.4.148-commit-70e7518";
4
+ declare const PROJECT_VERSION: "0.4.149-commit-041ee42";
5
5
  type PackageName = typeof projectInfo.npmPackageName;
6
6
  type ProjectVersion = typeof projectInfo.projectVersion;
7
7
  type ProjectTag = `[${PackageName}]` | `[${PackageName}@${ProjectVersion}]`;
8
8
  declare const projectInfo: {
9
9
  projectName: "Vike";
10
- projectVersion: "0.4.148-commit-70e7518";
10
+ projectVersion: "0.4.149-commit-041ee42";
11
11
  npmPackageName: "vike";
12
12
  githubRepository: "https://github.com/vikejs/vike";
13
13
  };
@@ -1,7 +1,7 @@
1
1
  export { projectInfo };
2
2
  export { PROJECT_VERSION };
3
3
  import { onProjectInfo } from './assertSingleInstance.js';
4
- const PROJECT_VERSION = '0.4.148-commit-70e7518';
4
+ const PROJECT_VERSION = '0.4.149-commit-041ee42';
5
5
  const projectInfo = {
6
6
  projectName: 'Vike',
7
7
  projectVersion: PROJECT_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vike",
3
- "version": "0.4.148-commit-70e7518",
3
+ "version": "0.4.149-commit-041ee42",
4
4
  "scripts": {
5
5
  "dev": "tsc --watch",
6
6
  "build": "rimraf dist/ && pnpm run build:esm && pnpm run build:cjs",
@@ -162,7 +162,7 @@
162
162
  "vike": "./node/cli/bin-entry.js"
163
163
  },
164
164
  "devDependencies": {
165
- "@brillout/release-me": "^0.1.10",
165
+ "@brillout/release-me": "^0.1.11",
166
166
  "@types/estree": "^1.0.0",
167
167
  "@types/jest": "^27.4.1",
168
168
  "@types/node": "^20.1.0",