vike 0.4.219 → 0.4.220-commit-a9f46b8

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 (77) hide show
  1. package/dist/cjs/node/api/build.js +23 -58
  2. package/dist/cjs/node/api/context.js +6 -8
  3. package/dist/cjs/node/api/prepareViteApiCall.js +6 -7
  4. package/dist/cjs/node/cli/context.js +16 -0
  5. package/dist/cjs/node/cli/entry.js +2 -0
  6. package/dist/cjs/node/cli/utils.js +1 -0
  7. package/dist/cjs/node/plugin/plugins/autoFullBuild.js +32 -19
  8. package/dist/cjs/node/plugin/plugins/baseUrls.js +1 -1
  9. package/dist/cjs/node/plugin/plugins/commonConfig.js +10 -8
  10. package/dist/cjs/node/plugin/plugins/importUserCode/v1-design/getVirtualFilePageConfigs.js +3 -2
  11. package/dist/cjs/node/prerender/{isPrerenderAutoRunEnabled.js → context.js} +9 -1
  12. package/dist/cjs/node/prerender/runPrerender.js +55 -33
  13. package/dist/cjs/node/prerender/utils.js +1 -0
  14. package/dist/cjs/node/runtime/globalContext.js +2 -22
  15. package/dist/cjs/node/runtime/page-files/setup.js +1 -1
  16. package/dist/cjs/node/runtime/utils.js +1 -0
  17. package/dist/cjs/shared/page-configs/loadConfigValues.js +5 -1
  18. package/dist/cjs/utils/PROJECT_VERSION.js +1 -1
  19. package/dist/cjs/utils/assertSetup.js +15 -1
  20. package/dist/cjs/utils/catchInfiniteLoop.js +34 -0
  21. package/dist/cjs/utils/isDev.js +2 -0
  22. package/dist/cjs/utils/makePublicCopy.js +32 -0
  23. package/dist/esm/client/client-routing-runtime/history.d.ts +3 -1
  24. package/dist/esm/client/client-routing-runtime/history.js +23 -18
  25. package/dist/esm/client/client-routing-runtime/index.d.ts +0 -1
  26. package/dist/esm/client/client-routing-runtime/index.js +0 -1
  27. package/dist/esm/client/client-routing-runtime/initClientRouter.js +2 -2
  28. package/dist/esm/client/client-routing-runtime/initOnLinkClick.js +3 -4
  29. package/dist/esm/client/client-routing-runtime/initOnPopState.d.ts +0 -10
  30. package/dist/esm/client/client-routing-runtime/initOnPopState.js +50 -62
  31. package/dist/esm/client/client-routing-runtime/renderPageClientSide.js +15 -15
  32. package/dist/esm/client/client-routing-runtime/scrollRestoration.d.ts +4 -6
  33. package/dist/esm/client/client-routing-runtime/scrollRestoration.js +17 -12
  34. package/dist/esm/client/client-routing-runtime/setScrollPosition.d.ts +1 -1
  35. package/dist/esm/client/client-routing-runtime/setScrollPosition.js +29 -5
  36. package/dist/esm/client/client-routing-runtime/utils.d.ts +1 -0
  37. package/dist/esm/client/client-routing-runtime/utils.js +1 -0
  38. package/dist/esm/client/shared/normalizeClientSideUrl.js +2 -3
  39. package/dist/esm/node/api/build.d.ts +1 -6
  40. package/dist/esm/node/api/build.js +20 -25
  41. package/dist/esm/node/api/context.d.ts +4 -4
  42. package/dist/esm/node/api/context.js +6 -9
  43. package/dist/esm/node/api/prepareViteApiCall.d.ts +0 -1
  44. package/dist/esm/node/api/prepareViteApiCall.js +7 -8
  45. package/dist/esm/node/cli/context.d.ts +5 -0
  46. package/dist/esm/node/cli/context.js +14 -0
  47. package/dist/esm/node/cli/entry.js +2 -0
  48. package/dist/esm/node/cli/parseCli.d.ts +3 -1
  49. package/dist/esm/node/cli/utils.d.ts +1 -0
  50. package/dist/esm/node/cli/utils.js +1 -0
  51. package/dist/esm/node/plugin/plugins/autoFullBuild.js +31 -18
  52. package/dist/esm/node/plugin/plugins/baseUrls.js +1 -1
  53. package/dist/esm/node/plugin/plugins/commonConfig.d.ts +8 -2
  54. package/dist/esm/node/plugin/plugins/commonConfig.js +8 -6
  55. package/dist/esm/node/plugin/plugins/importUserCode/v1-design/getVirtualFilePageConfigs.js +3 -2
  56. package/dist/esm/node/prerender/{isPrerenderAutoRunEnabled.d.ts → context.d.ts} +4 -0
  57. package/dist/esm/node/prerender/{isPrerenderAutoRunEnabled.js → context.js} +9 -1
  58. package/dist/esm/node/prerender/runPrerender.d.ts +42 -1
  59. package/dist/esm/node/prerender/runPrerender.js +56 -34
  60. package/dist/esm/node/prerender/utils.d.ts +1 -0
  61. package/dist/esm/node/prerender/utils.js +1 -0
  62. package/dist/esm/node/runtime/globalContext.js +3 -23
  63. package/dist/esm/node/runtime/page-files/setup.js +1 -1
  64. package/dist/esm/node/runtime/utils.d.ts +1 -0
  65. package/dist/esm/node/runtime/utils.js +1 -0
  66. package/dist/esm/shared/page-configs/PageConfig.d.ts +6 -3
  67. package/dist/esm/shared/page-configs/loadConfigValues.js +6 -2
  68. package/dist/esm/utils/PROJECT_VERSION.d.ts +1 -1
  69. package/dist/esm/utils/PROJECT_VERSION.js +1 -1
  70. package/dist/esm/utils/assertSetup.js +15 -1
  71. package/dist/esm/utils/catchInfiniteLoop.d.ts +2 -0
  72. package/dist/esm/utils/catchInfiniteLoop.js +32 -0
  73. package/dist/esm/utils/isDev.js +2 -0
  74. package/dist/esm/utils/makePublicCopy.d.ts +3 -0
  75. package/dist/esm/utils/makePublicCopy.js +30 -0
  76. package/dist/esm/utils/projectInfo.d.ts +1 -1
  77. package/package.json +1 -1
@@ -9,7 +9,11 @@ async function loadConfigValues(pageConfig, isDev) {
9
9
  !isDev) {
10
10
  return pageConfig;
11
11
  }
12
- const configValuesLoaded = await pageConfig.loadConfigValuesAll();
12
+ const { moduleId, moduleExports } = pageConfig.loadConfigValuesAll();
13
+ const configValuesLoaded = await moduleExports;
14
+ // `configValuesLoaded` is sometimes `undefined` https://github.com/vikejs/vike/discussions/2092
15
+ if (!configValuesLoaded)
16
+ (0, utils_js_1.assert)(false, { moduleExports, configValuesLoaded, moduleId });
13
17
  const configValues = (0, parsePageConfigs_js_1.parseConfigValuesSerialized)(configValuesLoaded.configValuesSerialized);
14
18
  Object.assign(pageConfig.configValues, configValues);
15
19
  (0, utils_js_1.objectAssign)(pageConfig, { isAllLoaded: true });
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PROJECT_VERSION = void 0;
4
4
  // Automatically updated by @brillout/release-me
5
- exports.PROJECT_VERSION = '0.4.219';
5
+ exports.PROJECT_VERSION = '0.4.220-commit-a9f46b8';
@@ -121,7 +121,7 @@ function getEnvDescription() {
121
121
  // https://github.com/cloudflare/workers-sdk/issues/7886
122
122
  function assertNodeEnvIsNotUndefinedString() {
123
123
  const nodeEnv = getNodeEnv();
124
- (0, assert_js_1.assertWarning)(nodeEnv !== 'undefined', `${picocolors_1.default.cyan('process.env.NODE_ENV==="undefined"')} which is unexpected: ${picocolors_1.default.cyan('process.env.NODE_ENV')} can be set to the *value* ${picocolors_1.default.cyan('undefined')} (i.e. ${picocolors_1.default.cyan('process.env.NODE_ENV===undefined')}) but it shouldn't be set to the *string* ${picocolors_1.default.cyan('"undefined"')} ${picocolors_1.default.underline('https://vike.dev/NODE_ENV')}`, { onlyOnce: true });
124
+ (0, assert_js_1.assertWarning)(nodeEnv !== 'undefined', `${picocolors_1.default.cyan('process.env.NODE_ENV==="undefined"')} which is unexpected: ${picocolors_1.default.cyan('process.env.NODE_ENV')} is allowed to be the *value* ${picocolors_1.default.cyan('undefined')} (i.e. ${picocolors_1.default.cyan('process.env.NODE_ENV===undefined')}) but it shouldn't be the *string* ${picocolors_1.default.cyan('"undefined"')} ${picocolors_1.default.underline('https://vike.dev/NODE_ENV')}`, { onlyOnce: true });
125
125
  }
126
126
  function isNodeEnvDev() {
127
127
  const nodeEnv = getNodeEnv();
@@ -141,6 +141,20 @@ function getNodeEnv() {
141
141
  catch {
142
142
  return undefined;
143
143
  }
144
+ /*
145
+ // Should we show the following warning? So far I don't think so because of the following. Maybe we can show it once we enable users to disable warnings.
146
+ // - The warning isn't always actionable, e.g. if it's a tool that dynamically sets `process.env.NODE_ENV`.
147
+ // - We assume that tools use `process.env.NODE_ENV` and not someting like `const { env } = process; env.NODE_ENV`. Thus, in practice, `val` overrides `val2` so having `val!==val2` isn't an issue.
148
+ {
149
+ const val2 = process.env['NODE' + '_ENV']
150
+ if (val2)
151
+ assertWarning(
152
+ val === val2,
153
+ `Dynamically setting process.env.NODE_ENV to ${val2} hasn't any effect because process.env.NODE_ENV is being statically replaced to ${val}.`,
154
+ { onlyOnce: true }
155
+ )
156
+ }
157
+ //*/
144
158
  return val;
145
159
  }
146
160
  function setNodeEnvProduction() {
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.catchInfiniteLoop = catchInfiniteLoop;
4
+ const assert_js_1 = require("./assert.js");
5
+ const trackers = {};
6
+ function catchInfiniteLoop(functionName, maxNumberOfCalls = 100, withinSeconds = 5) {
7
+ // Init
8
+ const now = new Date();
9
+ let tracker = (trackers[functionName] ?? (trackers[functionName] = createTracker(now)));
10
+ // Reset
11
+ const elapsedTime = now.getTime() - tracker.start.getTime();
12
+ if (elapsedTime > withinSeconds * 1000)
13
+ tracker = trackers[functionName] = createTracker(now);
14
+ // Count
15
+ tracker.count++;
16
+ // Error
17
+ const msg = `[Infinite Loop] ${functionName} called ${tracker.count} times within ${withinSeconds} seconds`;
18
+ if (tracker.count > maxNumberOfCalls) {
19
+ (0, assert_js_1.assert)(false, msg);
20
+ }
21
+ // Warning, at 50% threshold
22
+ if (!tracker.warned && tracker.count > maxNumberOfCalls * 0.5) {
23
+ // Warning is shown upon 10 calls a second, on average during 5 seconds, given the default parameters
24
+ (0, assert_js_1.assertWarning)(false, msg, { onlyOnce: false });
25
+ tracker.warned = true;
26
+ }
27
+ }
28
+ function createTracker(now) {
29
+ const tracker = {
30
+ count: 0,
31
+ start: now
32
+ };
33
+ return tracker;
34
+ }
@@ -7,6 +7,8 @@ const assert_js_1 = require("./assert.js");
7
7
  function isDevCheck(configEnv) {
8
8
  const { isPreview, command } = configEnv;
9
9
  // Released at vite@5.1.0 which is guaranteed with `assertVersion('Vite', version, '5.1.0')`
10
+ // - Release: https://github.com/vitejs/vite/blob/6f7466e6211027686f40ad7e4ce6ec8477414546/packages/vite/CHANGELOG.md#510-beta4-2024-01-26:~:text=fix(preview)%3A-,set%20isPreview%20true,-(%2315695)%20(93fce55
11
+ // - Surprisingly, this assert() can fail: https://github.com/vikejs/vike/issues/2120
10
12
  (0, assert_js_1.assert)(typeof isPreview === 'boolean');
11
13
  return command === 'serve' && !isPreview;
12
14
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makePublicCopy = makePublicCopy;
4
+ const assert_js_1 = require("./assert.js");
5
+ const objectKeys_js_1 = require("./objectKeys.js");
6
+ /** Prefix internal properties with `_` + show warning */
7
+ function makePublicCopy(obj, objName, propsPublic, propsInternalNoWarning) {
8
+ const objPublic = {};
9
+ (0, objectKeys_js_1.objectKeys)(obj).forEach((key) => {
10
+ const val = obj[key];
11
+ if (propsPublic.includes(key)) {
12
+ objPublic[key] = val;
13
+ }
14
+ else {
15
+ const keyPublic = key.startsWith('_') ? key : `_${key}`;
16
+ if (propsInternalNoWarning?.includes(key)) {
17
+ // @ts-expect-error
18
+ objPublic[keyPublic] = val;
19
+ }
20
+ else {
21
+ Object.defineProperty(objPublic, keyPublic, {
22
+ enumerable: true,
23
+ get() {
24
+ (0, assert_js_1.assertWarning)(false, `Using internal ${objName}.${keyPublic} which may break in any minor version update. Reach out on GitHub and elaborate your use case so that the Vike team can add official support for your use case.`, { onlyOnce: true });
25
+ return val;
26
+ }
27
+ });
28
+ }
29
+ }
30
+ });
31
+ return objPublic;
32
+ }
@@ -1,4 +1,5 @@
1
1
  export { pushHistoryState };
2
+ export { replaceHistoryStateOriginal };
2
3
  export { onPopStateBegin };
3
4
  export { saveScrollPosition };
4
5
  export { initHistoryState };
@@ -17,13 +18,14 @@ type ScrollPosition = {
17
18
  };
18
19
  declare function saveScrollPosition(): void;
19
20
  declare function pushHistoryState(url: string, overwriteLastHistoryEntry: boolean): void;
21
+ declare function replaceHistoryStateOriginal(state: unknown, url: string): void;
20
22
  declare function monkeyPatchHistoryAPI(): void;
21
23
  type HistoryInfo = {
22
24
  url: `/${string}`;
23
25
  state: StateEnhanced;
24
26
  };
25
27
  declare function onPopStateBegin(): {
26
- isNewState: boolean;
28
+ isHistoryStateEnhanced: boolean;
27
29
  previous: HistoryInfo;
28
30
  current: HistoryInfo;
29
31
  };
@@ -1,10 +1,11 @@
1
1
  export { pushHistoryState };
2
+ export { replaceHistoryStateOriginal };
2
3
  export { onPopStateBegin };
3
4
  export { saveScrollPosition };
4
5
  export { initHistoryState };
5
6
  export { monkeyPatchHistoryAPI };
6
7
  import { getCurrentUrl } from '../shared/getCurrentUrl.js';
7
- import { assert, assertUsage, getGlobalObject, hasProp, isObject } from './utils.js';
8
+ import { assert, assertUsage, getGlobalObject, isObject } from './utils.js';
8
9
  initHistoryState(); // we redundantly call initHistoryState() to ensure it's called early
9
10
  const globalObject = getGlobalObject('history.ts', { previous: getHistoryInfo() });
10
11
  // `window.history.state === null` when:
@@ -90,6 +91,11 @@ function replaceHistoryState(state, url) {
90
91
  const url_ = url ?? null; // Passing `undefined` chokes older Edge versions.
91
92
  window.history.replaceState(state, '', url_);
92
93
  }
94
+ function replaceHistoryStateOriginal(state, url) {
95
+ // Bypass all monkey patches.
96
+ // - Useful, for example, to avoid other tools listening to history.replaceState() calls
97
+ History.prototype.replaceState.bind(window.history)(state, '', url);
98
+ }
93
99
  // Monkey patch:
94
100
  // - history.pushState()
95
101
  // - history.replaceState()
@@ -116,21 +122,19 @@ function monkeyPatchHistoryAPI() {
116
122
  });
117
123
  }
118
124
  function isVikeEnhanced(state) {
119
- const yes = isObject(state) && '_isVikeEnhanced' in state;
120
- if (yes)
121
- assertStateVikeEnhanced(state);
122
- return yes;
123
- }
124
- function assertStateVikeEnhanced(state) {
125
- assert(isObject(state));
126
- assert(hasProp(state, '_isVikeEnhanced', 'true'));
127
- // TODO/eventually: remove assert() below to save client-side KBs
128
- assert(hasProp(state, 'timestamp', 'number'));
129
- assert(hasProp(state, 'scrollPosition'));
130
- if (state.scrollPosition !== null) {
131
- assert(hasProp(state, 'scrollPosition', 'object'));
132
- assert(hasProp(state.scrollPosition, 'x', 'number') && hasProp(state.scrollPosition, 'y', 'number'));
125
+ if (isObject(state) && '_isVikeEnhanced' in state) {
126
+ /* We don't use the assert() below to save client-side KBs.
127
+ assert(hasProp(state, '_isVikeEnhanced', 'true'))
128
+ assert(hasProp(state, 'timestamp', 'number'))
129
+ assert(hasProp(state, 'scrollPosition'))
130
+ if (state.scrollPosition !== null) {
131
+ assert(hasProp(state, 'scrollPosition', 'object'))
132
+ assert(hasProp(state.scrollPosition, 'x', 'number') && hasProp(state.scrollPosition, 'y', 'number'))
133
+ }
134
+ //*/
135
+ return true;
133
136
  }
137
+ return false;
134
138
  }
135
139
  function getHistoryInfo() {
136
140
  return {
@@ -140,12 +144,13 @@ function getHistoryInfo() {
140
144
  }
141
145
  function onPopStateBegin() {
142
146
  const { previous } = globalObject;
143
- const isNewState = window.history.state === null;
144
- if (isNewState)
147
+ const isHistoryStateEnhanced = window.history.state !== null;
148
+ if (!isHistoryStateEnhanced)
145
149
  enhanceHistoryState();
150
+ assert(isVikeEnhanced(window.history.state));
146
151
  const current = getHistoryInfo();
147
152
  globalObject.previous = current;
148
- return { isNewState, previous, current };
153
+ return { isHistoryStateEnhanced, previous, current };
149
154
  }
150
155
  function initHistoryState() {
151
156
  enhanceHistoryState();
@@ -1,6 +1,5 @@
1
1
  export { navigate, reload } from './navigate.js';
2
2
  export { prefetch } from './prefetch.js';
3
- export { onPopState } from './initOnPopState.js';
4
3
  export { PROJECT_VERSION as version } from './utils.js';
5
4
  import type { PageContextBuiltInClientWithClientRouting } from '../../shared/types.js';
6
5
  /** @deprecated
@@ -5,5 +5,4 @@
5
5
  // Use package.json#exports to make the imports isomorphic.
6
6
  export { navigate, reload } from './navigate.js';
7
7
  export { prefetch } from './prefetch.js';
8
- export { onPopState } from './initOnPopState.js';
9
8
  export { PROJECT_VERSION as version } from './utils.js';
@@ -3,7 +3,7 @@ import { assert } from './utils.js';
3
3
  import { getRenderCount, renderPageClientSide } from './renderPageClientSide.js';
4
4
  import { initOnPopState } from './initOnPopState.js';
5
5
  import { initOnLinkClick } from './initOnLinkClick.js';
6
- import { setupNativeScrollRestoration } from './scrollRestoration.js';
6
+ import { scrollRestoration_init } from './scrollRestoration.js';
7
7
  import { autoSaveScrollPosition } from './setScrollPosition.js';
8
8
  import { initLinkPrefetchHandlers } from './prefetch.js';
9
9
  import { initHistoryState, monkeyPatchHistoryAPI } from './history.js';
@@ -28,9 +28,9 @@ async function renderFirstPage() {
28
28
  });
29
29
  }
30
30
  function initHistoryAndScroll() {
31
+ scrollRestoration_init();
31
32
  monkeyPatchHistoryAPI();
32
33
  initHistoryState(); // we redundantly call initHistoryState() to ensure it's called early
33
- setupNativeScrollRestoration();
34
34
  autoSaveScrollPosition();
35
35
  // Handle back-/forward navigation
36
36
  initOnPopState();
@@ -1,6 +1,4 @@
1
- // Code adapted from https://github.com/HenrikJoreteg/internal-nav-helper/blob/5199ec5448d0b0db7ec63cf76d88fa6cad878b7d/src/index.js#L11-L29
2
1
  export { initOnLinkClick };
3
- import { assert } from './utils.js';
4
2
  import { isSameAsCurrentUrl, skipLink } from './skipLink.js';
5
3
  import { renderPageClientSide } from './renderPageClientSide.js';
6
4
  import { scrollToHashOrTop } from './setScrollPosition.js';
@@ -14,10 +12,12 @@ async function onClick(ev) {
14
12
  if (!linkTag)
15
13
  return;
16
14
  const href = linkTag.getAttribute('href');
15
+ if (href === null)
16
+ return;
17
17
  // Workaround for Firefox bug: clicking on a hash link that doesn't change the current URL causes Firefox to erroneously set `window.history.state = null` without firing any signal that we can detect.
18
18
  // - https://github.com/vikejs/vike/issues/1962
19
19
  // - https://github.com/sveltejs/kit/issues/8725
20
- if (href?.includes('#') && isSameAsCurrentUrl(href)) {
20
+ if (href.includes('#') && isSameAsCurrentUrl(href)) {
21
21
  // Prevent Firefox from setting `window.history.state` to `null`
22
22
  ev.preventDefault();
23
23
  // Replicate the browser's native behavior
@@ -26,7 +26,6 @@ async function onClick(ev) {
26
26
  }
27
27
  if (skipLink(linkTag))
28
28
  return;
29
- assert(href);
30
29
  ev.preventDefault();
31
30
  let scrollTarget;
32
31
  {
@@ -1,12 +1,2 @@
1
1
  export { initOnPopState };
2
- export { onPopState };
3
- import { type HistoryInfo } from './history.js';
4
2
  declare function initOnPopState(): void;
5
- type Listener = (arg: {
6
- previous: HistoryInfo;
7
- }) => void | boolean;
8
- /** Control back-/forward navigation.
9
- *
10
- * https://vike.dev/onPopState
11
- */
12
- declare function onPopState(listener: Listener): void;
@@ -1,71 +1,59 @@
1
1
  export { initOnPopState };
2
- export { onPopState };
3
- import { assertWarning, getGlobalObject } from './utils.js';
4
2
  import { onPopStateBegin } from './history.js';
5
3
  import { renderPageClientSide } from './renderPageClientSide.js';
6
4
  import { setScrollPosition } from './setScrollPosition.js';
7
- const globalObject = getGlobalObject('initOnPopState.ts', { listeners: [] });
5
+ import { catchInfiniteLoop } from './utils.js';
6
+ // The 'popstate' event is trigged when the browser doesn't fully load the new URL, for example:
7
+ // - `location.hash='#foo'` triggers the popstate event while `location.href='/foo'` doesn't.
8
+ // - Clicking on the browser's back-/forward button triggers a popstate event only if the history entry was generated with history.pushState() — no popstate event is fired upon Server Routing.
9
+ // Concretely, 'popstate' is fired when:
10
+ // 1. Back-/forward navigation:
11
+ // - By the user using the browser's back-/forward navigation
12
+ // - By the app using `history.back()` / `history.forward()` / `history.go()`
13
+ // > Except of history entries triggered by Server Routing, see comment above.
14
+ // 2. URL hash changes:
15
+ // - By the user clicking on `<a href="#some-hash">`
16
+ // - The popstate event is *only* triggered if `href` starts with '#' (even if `href==='/foo#bar'` and the current URL has the same pathname '/foo' then popstate isn't triggered)
17
+ // - Vike doesn't intercept hash links (see `skipLink()`) and let's the browser handle them.
18
+ // - By the app using a `location` API such as `location.hash = 'some-hash'`
19
+ // - Only upon hash navigation: setting `location.href='/foo'` triggers a full page reload and no popstate event is fired.
20
+ // - Also upon `location.href='/foo#bar'` while the current URL is '/foo' (unlike <a> clicks).
21
+ // Notes:
22
+ // - The 'hashchange' event is fired after popstate, so we cannot use it to distinguish between hash and non-hash navigations.
23
+ // - It isn't possible to monkey patch the `location` APIs. (Chrome throws `TypeError: Cannot redefine property` when attempt to overwrite any `location` property.)
24
+ // - Text links aren't supported: https://github.com/vikejs/vike/issues/2114
25
+ // - docs/ is a good playground to test all this.
8
26
  function initOnPopState() {
9
- // - The popstate event is trigged upon:
10
- // - Back-/forward navigation.
11
- // - By user clicking on his browser's back-/forward navigation (or using a shortcut)
12
- // - By JavaScript: `history.back()` / `history.forward()`
13
- // - URL hash change.
14
- // - Click on `<a href="#some-hash" />`
15
- // - The popstate event is *only* triggered if `href` starts with '#' (even if `href` is '/#some-hash' while the current URL's pathname is '/' then the popstate still isn't triggered)
16
- // - `location.hash = 'some-hash'`
17
- // - The `event` argument of `window.addEventListener('popstate', (event) => /*...*/)` is useless: the History API doesn't provide the previous state (the popped state), see https://stackoverflow.com/questions/48055323/is-history-state-always-the-same-as-popstate-event-state
18
- window.addEventListener('popstate', async () => {
19
- const { isNewState, previous, current } = onPopStateBegin();
20
- const scrollTarget = current.state.scrollPosition || undefined;
21
- const isUserPushStateNavigation = current.state.triggeredBy === 'user' || previous.state.triggeredBy === 'user';
22
- const isHashNavigation = removeHash(current.url) === removeHash(previous.url) && current.url !== previous.url;
23
- // - `isNewState === true` when:
24
- // - Click on `<a href="#some-hash" />` (note that Vike's `initOnLinkClick()` handler skips hash links)
25
- // - `location.hash = 'some-hash'`
26
- // - `isNewState === false` when `popstate` was triggered by the user clicking on his browser's forward/backward history button.
27
- const isHashNavigationNew = isHashNavigation && isNewState;
28
- const isBackwardNavigation = !current.state.timestamp || !previous.state.timestamp ? null : current.state.timestamp < previous.state.timestamp;
29
- // We have to scroll ourselves because we use `window.history.scrollRestoration = 'manual'`. So far this seems to work. Alternatives in case it doesn't work:
30
- // - Alternative: we use `window.history.scrollRestoration = 'auto'`
31
- // - Problem: I don't think it's possbible to set `window.history.scrollRestoration = 'auto'` only for hash navigation and not for non-hash navigations?
32
- // - Problem: inconsistencies between browsers? For example specification says that setting `window.history.scrollRestoration` only affects the current entry in the session history but this contradicts what people are experiencing in practice.
33
- // - Specification: https://html.spec.whatwg.org/multipage/history.html#the-history-interface
34
- // - Practice: https://stackoverflow.com/questions/70188241/history-scrollrestoration-manual-doesnt-prevent-safari-from-restoring-scrol
35
- // - Alternative: we completely take over hash navigation and reproduce the browser's native behavior upon hash navigation.
36
- // - By using the `hashchange` event.
37
- // - Problem: conflict if user wants to override the browser's default behavior? E.g. for smooth scrolling, or when using hashes for saving states of some fancy animations.
38
- if (isHashNavigation) {
39
- if (!isHashNavigationNew) {
40
- setScrollPosition(scrollTarget);
41
- }
42
- else {
43
- // The browser already scrolled to `#${hash}` => the current scroll position is the right one => we saved it with `enhanceHistoryState()`.
44
- }
45
- return;
46
- }
47
- let doNotRenderIfSamePage = isUserPushStateNavigation;
48
- let abort;
49
- globalObject.listeners.forEach((listener) => {
50
- abort || (abort = listener({ previous }));
51
- });
52
- if (abort) {
53
- return;
54
- }
55
- if (abort === false) {
56
- doNotRenderIfSamePage = false;
57
- }
58
- await renderPageClientSide({ scrollTarget, isBackwardNavigation, doNotRenderIfSamePage });
59
- });
27
+ window.addEventListener('popstate', onPopState);
60
28
  }
61
- // TODO/eventually: deprecate this onPopState(listener) function and let the user define +onPopState.js instead?
62
- /** Control back-/forward navigation.
63
- *
64
- * https://vike.dev/onPopState
65
- */
66
- function onPopState(listener) {
67
- assertWarning(false, 'onPopState() is experimental', { onlyOnce: true });
68
- globalObject.listeners.push(listener);
29
+ async function onPopState() {
30
+ catchInfiniteLoop('onPopState()');
31
+ const { isHistoryStateEnhanced, previous, current } = onPopStateBegin();
32
+ // - `isHistoryStateEnhanced===false` <=> new hash navigation:
33
+ // - Click on `<a href="#some-hash">`
34
+ // - Using the `location` API (only hash navigation, see comments above).
35
+ // - `isHistoryStateEnhanced===true` <=> back-/forward navigation (including back-/forward hash navigation).
36
+ // > Only back-/forward client-side navigation: no 'popstate' event is fired upon Server Routing (when the user clicks on a link before the page's JavaScript loaded), see comments above.
37
+ if (!isHistoryStateEnhanced) {
38
+ // Let the browser handle it
39
+ return;
40
+ }
41
+ else {
42
+ await handleBackForwardNavigation(previous, current);
43
+ }
44
+ }
45
+ async function handleBackForwardNavigation(previous, current) {
46
+ const scrollTarget = current.state.scrollPosition || undefined;
47
+ const isHashNavigation = removeHash(current.url) === removeHash(previous.url) && current.url !== previous.url;
48
+ if (isHashNavigation) {
49
+ // We have to scroll ourselves because we have set `window.history.scrollRestoration = 'manual'`
50
+ setScrollPosition(scrollTarget);
51
+ return;
52
+ }
53
+ const isUserPushStateNavigation = current.state.triggeredBy === 'user' || previous.state.triggeredBy === 'user';
54
+ const doNotRenderIfSamePage = isUserPushStateNavigation;
55
+ const isBackwardNavigation = !current.state.timestamp || !previous.state.timestamp ? null : current.state.timestamp < previous.state.timestamp;
56
+ await renderPageClientSide({ scrollTarget, isBackwardNavigation, doNotRenderIfSamePage });
69
57
  }
70
58
  function removeHash(url) {
71
59
  return url.split('#')[0];
@@ -2,7 +2,7 @@ export { renderPageClientSide };
2
2
  export { getRenderCount };
3
3
  export { disableClientRouting };
4
4
  export { firstRenderStartPromise };
5
- import { assert, isSameErrorMessage, objectAssign, redirectHard, getGlobalObject, executeHook, hasProp, augmentType, genPromise, isCallable } from './utils.js';
5
+ import { assert, isSameErrorMessage, objectAssign, redirectHard, getGlobalObject, executeHook, hasProp, augmentType, genPromise, isCallable, catchInfiniteLoop } from './utils.js';
6
6
  import { getPageContextFromClientHooks, getPageContextFromServerHooks, getPageContextFromHooks_isHydration, getPageContextFromHooks_serialized, setPageContextInitIsPassedToClient } from './getPageContextFromHooks.js';
7
7
  import { createPageContext } from './createPageContext.js';
8
8
  import { addLinkPrefetchHandlers, addLinkPrefetchHandlers_unwatch, addLinkPrefetchHandlers_watch, getPageContextPrefetched, populatePageContextPrefetchCache } from './prefetch.js';
@@ -15,7 +15,7 @@ import { assertNoInfiniteAbortLoop, getPageContextFromAllRewrites, isAbortError,
15
15
  import { route } from '../../shared/route/index.js';
16
16
  import { isClientSideRoutable } from './isClientSideRoutable.js';
17
17
  import { setScrollPosition } from './setScrollPosition.js';
18
- import { browserNativeScrollRestoration_disable, setInitialRenderIsDone } from './scrollRestoration.js';
18
+ import { scrollRestoration_initialRenderIsDone } from './scrollRestoration.js';
19
19
  import { getErrorPageId } from '../../shared/error-page.js';
20
20
  import { setPageContextCurrent } from './getPageContextCurrent.js';
21
21
  import { getRouteStringParameterList } from '../../shared/route/resolveRouteString.js';
@@ -31,6 +31,7 @@ const globalObject = getGlobalObject('renderPageClientSide.ts', (() => {
31
31
  })());
32
32
  const { firstRenderStartPromise } = globalObject;
33
33
  async function renderPageClientSide(renderArgs) {
34
+ catchInfiniteLoop('renderPageClientSide()');
34
35
  const { urlOriginal = getCurrentUrl(), overwriteLastHistoryEntry = false, isBackwardNavigation, pageContextsFromRewrite = [], redirectCount = 0, doNotRenderIfSamePage, isClientSideNavigation = true, pageContextInitClient } = renderArgs;
35
36
  let { scrollTarget } = renderArgs;
36
37
  const { previousPageContext } = globalObject;
@@ -391,18 +392,18 @@ async function renderPageClientSide(renderArgs) {
391
392
  }
392
393
  }
393
394
  };
394
- // We use globalObject.onRenderClientPromise in order to ensure that there is never two concurrent onRenderClient() calls
395
- if (globalObject.onRenderClientPromise) {
395
+ // We use globalObject.onRenderClientPreviousPromise in order to ensure that there is never two concurrent onRenderClient() calls
396
+ if (globalObject.onRenderClientPreviousPromise) {
396
397
  // Make sure that the previous render has finished
397
- await globalObject.onRenderClientPromise;
398
- assert(globalObject.onRenderClientPromise === undefined);
398
+ await globalObject.onRenderClientPreviousPromise;
399
+ assert(globalObject.onRenderClientPreviousPromise === undefined);
399
400
  if (isRenderOutdated())
400
401
  return;
401
402
  }
402
403
  changeUrl(urlOriginal, overwriteLastHistoryEntry);
403
404
  globalObject.previousPageContext = pageContext;
404
- assert(globalObject.onRenderClientPromise === undefined);
405
- globalObject.onRenderClientPromise = (async () => {
405
+ assert(globalObject.onRenderClientPreviousPromise === undefined);
406
+ const onRenderClientPromise = (async () => {
406
407
  let onRenderClientError;
407
408
  try {
408
409
  await executeOnRenderClientHook(pageContext, true);
@@ -410,12 +411,13 @@ async function renderPageClientSide(renderArgs) {
410
411
  catch (err) {
411
412
  onRenderClientError = err;
412
413
  }
413
- globalObject.onRenderClientPromise = undefined;
414
+ globalObject.onRenderClientPreviousPromise = undefined;
414
415
  globalObject.isFirstRenderDone = true;
415
416
  return onRenderClientError;
416
417
  })();
417
- const onRenderClientError = await globalObject.onRenderClientPromise;
418
- assert(globalObject.onRenderClientPromise === undefined);
418
+ globalObject.onRenderClientPreviousPromise = onRenderClientPromise;
419
+ const onRenderClientError = await onRenderClientPromise;
420
+ assert(globalObject.onRenderClientPreviousPromise === undefined);
419
421
  if (onRenderClientError) {
420
422
  await onError(onRenderClientError);
421
423
  if (!isErrorPage)
@@ -475,9 +477,8 @@ async function renderPageClientSide(renderArgs) {
475
477
  }
476
478
  }
477
479
  // Page scrolling
478
- setScrollPosition(scrollTarget);
479
- browserNativeScrollRestoration_disable();
480
- setInitialRenderIsDone();
480
+ setScrollPosition(scrollTarget, urlOriginal);
481
+ scrollRestoration_initialRenderIsDone();
481
482
  if (pageContext._hasPageContextFromServer)
482
483
  setPageContextInitIsPassedToClient(pageContext);
483
484
  // Add link prefetch handlers
@@ -488,7 +489,6 @@ async function renderPageClientSide(renderArgs) {
488
489
  function changeUrl(url, overwriteLastHistoryEntry) {
489
490
  if (getCurrentUrl() === url)
490
491
  return;
491
- browserNativeScrollRestoration_disable();
492
492
  pushHistoryState(url, overwriteLastHistoryEntry);
493
493
  }
494
494
  function handleErrorFetchingStaticAssets(err, pageContext, isFirstRender) {
@@ -1,6 +1,4 @@
1
- export { browserNativeScrollRestoration_disable };
2
- export { setupNativeScrollRestoration };
3
- export { setInitialRenderIsDone };
4
- declare function setupNativeScrollRestoration(): void;
5
- declare function setInitialRenderIsDone(): void;
6
- declare function browserNativeScrollRestoration_disable(): void;
1
+ export { scrollRestoration_init };
2
+ export { scrollRestoration_initialRenderIsDone };
3
+ declare function scrollRestoration_init(): void;
4
+ declare function scrollRestoration_initialRenderIsDone(): void;
@@ -1,25 +1,30 @@
1
- // Handle the browser's native scroll restoration mechanism
2
- export { browserNativeScrollRestoration_disable };
3
- export { setupNativeScrollRestoration };
4
- export { setInitialRenderIsDone };
1
+ export { scrollRestoration_init };
2
+ export { scrollRestoration_initialRenderIsDone };
3
+ // Using `window.history.scrollRestoration` to recover scroll position when user reloads the page or Cmd-Shift-T back to it.
4
+ // We let the browser do it because it's fast.
5
+ // - Alternatively we could inject an inline script `<script>scrollTo(history.state.scrollPosition)</script>` early, which seems to be equally fast. (See for example https://vike.dev/usePageContext which sets the main scroll position and the navigation scroll position equally fast.)
6
+ // - Firefox doesn't restore the scroll position upon page reload but does upon Cmd-Shift-T
7
+ // See also: https://github.com/cyco130/knave/blob/e9e1bc7687848504293197f1b314b7d12ad0d228/design.md#scroll-restoration
5
8
  import { getGlobalObject, onPageHide, onPageShow } from './utils.js';
6
9
  const globalObject = getGlobalObject('scrollRestoration.ts', {});
7
- // We use the browser's native scroll restoration mechanism only for the first render
8
- function setupNativeScrollRestoration() {
9
- browserNativeScrollRestoration_enable();
10
- onPageHide(browserNativeScrollRestoration_enable);
11
- onPageShow(() => globalObject.initialRenderIsDone && browserNativeScrollRestoration_disable());
10
+ function scrollRestoration_init() {
11
+ // Use the native scroll restoration mechanism only for the first render
12
+ scrollRestoration_enable();
13
+ onPageHide(scrollRestoration_enable);
14
+ onPageShow(() => globalObject.initialRenderIsDone && scrollRestoration_disable());
12
15
  }
13
- function setInitialRenderIsDone() {
16
+ function scrollRestoration_initialRenderIsDone() {
14
17
  globalObject.initialRenderIsDone = true;
18
+ scrollRestoration_disable();
15
19
  }
16
- function browserNativeScrollRestoration_disable() {
20
+ function scrollRestoration_disable() {
17
21
  if ('scrollRestoration' in window.history) {
18
22
  window.history.scrollRestoration = 'manual';
19
23
  }
20
24
  }
21
- function browserNativeScrollRestoration_enable() {
25
+ function scrollRestoration_enable() {
22
26
  if ('scrollRestoration' in window.history) {
27
+ // Use the browser's native scroll restoration mechanism
23
28
  window.history.scrollRestoration = 'auto';
24
29
  }
25
30
  }
@@ -6,6 +6,6 @@ import { type ScrollPosition } from './history.js';
6
6
  type ScrollTarget = undefined | {
7
7
  preserveScroll: boolean;
8
8
  } | ScrollPosition;
9
- declare function setScrollPosition(scrollTarget: ScrollTarget): void;
9
+ declare function setScrollPosition(scrollTarget: ScrollTarget, url?: string): void;
10
10
  declare function scrollToHashOrTop(hash: null | string): void;
11
11
  declare function autoSaveScrollPosition(): void;