vike 0.4.148 → 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 (28) 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 +52 -26
  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/html/renderHtml.js +1 -1
  7. package/dist/cjs/node/runtime/renderPage/getPageAssets/retrieveAssetsProd.js +2 -3
  8. package/dist/cjs/node/runtime/renderPage/getPageAssets/sortPageAssetsForEarlyHintsHeader.js +6 -0
  9. package/dist/cjs/node/runtime/renderPage/inferMediaType.js +38 -0
  10. package/dist/cjs/shared/page-configs/assertExports.js +11 -4
  11. package/dist/cjs/utils/projectInfo.js +1 -1
  12. package/dist/esm/client/shared/getPageContextSerializedInHtml.js +7 -1
  13. package/dist/esm/node/plugin/plugins/devConfig/index.js +20 -9
  14. package/dist/esm/node/plugin/plugins/importUserCode/v1-design/getVikeConfig/crawlPlusFiles.js +52 -26
  15. package/dist/esm/node/runtime/html/injectAssets/getHtmlTags.d.ts +4 -0
  16. package/dist/esm/node/runtime/html/injectAssets/getHtmlTags.js +117 -81
  17. package/dist/esm/node/runtime/html/injectAssets/inferHtmlTags.d.ts +2 -0
  18. package/dist/esm/node/runtime/html/injectAssets/inferHtmlTags.js +4 -4
  19. package/dist/esm/node/runtime/html/injectAssets/mergeScriptTags.js +2 -1
  20. package/dist/esm/node/runtime/html/renderHtml.js +1 -1
  21. package/dist/esm/node/runtime/renderPage/getPageAssets/retrieveAssetsProd.js +2 -3
  22. package/dist/esm/node/runtime/renderPage/getPageAssets/sortPageAssetsForEarlyHintsHeader.js +6 -0
  23. package/dist/esm/node/runtime/renderPage/inferMediaType.d.ts +2 -2
  24. package/dist/esm/node/runtime/renderPage/inferMediaType.js +38 -0
  25. package/dist/esm/shared/page-configs/assertExports.js +11 -4
  26. package/dist/esm/utils/projectInfo.d.ts +2 -2
  27. package/dist/esm/utils/projectInfo.js +1 -1
  28. package/package.json +2 -2
@@ -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
  };
@@ -8,19 +8,24 @@ const execA = promisify(exec);
8
8
  async function crawlPlusFiles(userRootDir, outDirAbsoluteFilesystem, isDev) {
9
9
  assertPosixPath(userRootDir);
10
10
  assertPosixPath(outDirAbsoluteFilesystem);
11
+ // Vike prepends userRootDir without resolving, e.g. outDirRelativeFromUserRootDir can be /home/rom/my-monorepo/my-app/../my-build/dist/ while userRootDir is /home/rom/my-monorepo/my-app/
11
12
  assert(outDirAbsoluteFilesystem.startsWith(userRootDir));
12
- const outDir = path.posix.relative(userRootDir, outDirAbsoluteFilesystem);
13
- assert(!outDir.startsWith('.'));
13
+ let outDirRelativeFromUserRootDir = path.posix.relative(userRootDir, outDirAbsoluteFilesystem);
14
+ if (outDirRelativeFromUserRootDir.startsWith('../')) {
15
+ // config.outDir is outside of config.root => it's going to be ignored anyways
16
+ outDirRelativeFromUserRootDir = null;
17
+ }
18
+ assert(outDirRelativeFromUserRootDir === null || !outDirRelativeFromUserRootDir.startsWith('.'));
14
19
  const timeBase = new Date().getTime();
15
20
  let files = [];
16
- const res = await gitLsFiles(userRootDir, outDir);
21
+ const res = await gitLsFiles(userRootDir, outDirRelativeFromUserRootDir);
17
22
  if (res &&
18
23
  // Fallback to fast-glob for users that dynamically generate plus files (we assume generetad plus files to be skipped because they are usually included in .gitignore)
19
24
  res.length > 0) {
20
25
  files = res;
21
26
  }
22
27
  else {
23
- files = await fastGlob(userRootDir, outDir);
28
+ files = await fastGlob(userRootDir, outDirRelativeFromUserRootDir);
24
29
  }
25
30
  {
26
31
  const time = new Date().getTime() - timeBase;
@@ -44,46 +49,39 @@ async function crawlPlusFiles(userRootDir, outDirAbsoluteFilesystem, isDev) {
44
49
  return plusFiles;
45
50
  }
46
51
  // Same as fastGlob() but using `$ git ls-files`
47
- async function gitLsFiles(userRootDir, outDir) {
52
+ async function gitLsFiles(userRootDir, outDirRelativeFromUserRootDir) {
53
+ if (!(await isUsingGit(userRootDir)))
54
+ return null;
48
55
  const cmd = [
49
56
  'git ls-files',
50
57
  ...scriptFileExtensionList.map((ext) => `"**/+*.${ext}"`),
51
- ...getIgnorePatterns(outDir).map((pattern) => `--exclude="${pattern}"`),
58
+ ...getIgnorePatterns(outDirRelativeFromUserRootDir).map((pattern) => `--exclude="${pattern}"`),
52
59
  // --others lists untracked files only (but using .gitignore because --exclude-standard)
53
60
  // --cached adds the tracked files to the output
54
61
  '--others --cached --exclude-standard'
55
62
  ].join(' ');
56
- let stdout;
57
- try {
58
- const res = await execA(cmd, { cwd: userRootDir });
59
- stdout = res.stdout;
60
- }
61
- catch (err) {
62
- if (err.message.includes('not a git repository'))
63
- return null;
64
- throw err;
65
- }
66
- let files = stdout.split('\n').filter(Boolean);
67
- assert(!outDir.startsWith('/'));
63
+ let files = await runCmd(cmd, userRootDir);
68
64
  files = files.filter(
69
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.)
70
- (file) => getIgnoreFilter(file, outDir));
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));
71
70
  return files;
72
71
  }
73
72
  // Same as gitLsFiles() but using fast-glob
74
- async function fastGlob(userRootDir, outDir) {
73
+ async function fastGlob(userRootDir, outDirRelativeFromUserRootDir) {
75
74
  const files = await glob(`**/+*.${scriptFileExtensions}`, {
76
- ignore: getIgnorePatterns(outDir),
75
+ ignore: getIgnorePatterns(outDirRelativeFromUserRootDir),
77
76
  cwd: userRootDir,
78
77
  dot: false
79
78
  });
80
79
  return files;
81
80
  }
82
81
  // Same as getIgnoreFilter() but as glob pattern
83
- function getIgnorePatterns(outDir) {
84
- return [
82
+ function getIgnorePatterns(outDirRelativeFromUserRootDir) {
83
+ const ignorePatterns = [
85
84
  '**/node_modules/**',
86
- `${outDir}/**`,
87
85
  // Allow:
88
86
  // ```
89
87
  // +Page.js
@@ -91,8 +89,36 @@ function getIgnorePatterns(outDir) {
91
89
  // ```
92
90
  '**/*.telefunc.*'
93
91
  ];
92
+ if (outDirRelativeFromUserRootDir) {
93
+ assert(!outDirRelativeFromUserRootDir.startsWith('/'));
94
+ ignorePatterns.push(`${outDirRelativeFromUserRootDir}/**`);
95
+ }
96
+ return ignorePatterns;
94
97
  }
95
98
  // Same as getIgnorePatterns() but for Array.filter()
96
- function getIgnoreFilter(file, outDir) {
97
- return !file.includes('node_modules/') && !file.includes('.telefunc.') && !file.startsWith(`${outDir}/`);
99
+ function getIgnoreFilter(file, outDirRelativeFromUserRootDir) {
100
+ assert(!file.startsWith('/'));
101
+ assert(outDirRelativeFromUserRootDir === null || !outDirRelativeFromUserRootDir.startsWith('/'));
102
+ return (!file.includes('node_modules/') &&
103
+ !file.includes('.telefunc.') &&
104
+ (!outDirRelativeFromUserRootDir || !file.startsWith(`${outDirRelativeFromUserRootDir}/`)));
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');
98
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
  }
@@ -174,7 +174,7 @@ async function renderTemplate(templateContent, pageContext) {
174
174
  };
175
175
  assertUsage(!isPromise(templateVar), getErrMsg('a promise', `Did you forget to ${pc.cyan('await')} the promise?`));
176
176
  if (templateVar === undefined || templateVar === null) {
177
- assertWarning(false, getErrMsg(`${pc.cyan(String(templateVar))} which will be converted to an empty string`, `Pass an empty string instead of ${pc.cyan(String(templateVar))} to remove this warning.`), { onlyOnce: false });
177
+ assertWarning(false, getErrMsg(`${pc.cyan(String(templateVar))} which will be converted to an empty string`, `Pass the empty string ${pc.cyan("''")} instead of ${pc.cyan(String(templateVar))} to remove this warning.`), { onlyOnce: false });
178
178
  templateVar = '';
179
179
  }
180
180
  {
@@ -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";
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";
10
+ projectVersion: "0.4.149-commit-041ee42";
11
11
  npmPackageName: "vike";
12
12
  githubRepository: "https://github.com/vikejs/vike";
13
13
  };