Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(qwik-city): noSPA (experimental) #6937

Merged
merged 2 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-turtles-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/qwik-city': patch
---

Added experimental feature `noSPA`. This disables history patching, slightly reducing code size and startup time. Use this when your application is MPA only, meaning you don't use the Link component. To enable this, add it to the `experimental` array of the `qwikVite` plugin (not the `qwikCity` plugin).
57 changes: 54 additions & 3 deletions packages/docs/src/routes/api/qwik-optimizer/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@
"id": "experimentalfeatures"
}
],
"kind": "TypeAlias",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\n\n```typescript\nexport type ExperimentalFeatures = (typeof experimental)[number];\n```",
"kind": "Enum",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n<table><thead><tr><th>\n\nMember\n\n\n</th><th>\n\nValue\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nnoSPA\n\n\n</td><td>\n\n`\"noSPA\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n</td></tr>\n<tr><td>\n\npreventNavigate\n\n\n</td><td>\n\n`\"preventNavigate\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n</td></tr>\n<tr><td>\n\nvalibot\n\n\n</td><td>\n\n`\"valibot\"`\n\n\n</td><td>\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts",
"mdFile": "qwik.experimentalfeatures.md"
},
Expand Down Expand Up @@ -261,6 +261,23 @@
"content": "```typescript\nnormalize(path: string): string;\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\npath\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nstring",
"mdFile": "qwik.path.normalize.md"
},
{
"name": "noSPA",
"id": "experimentalfeatures-nospa",
"hierarchy": [
{
"name": "ExperimentalFeatures",
"id": "experimentalfeatures-nospa"
},
{
"name": "noSPA",
"id": "experimentalfeatures-nospa"
}
],
"kind": "EnumMember",
"content": "",
"mdFile": "qwik.experimentalfeatures.nospa.md"
},
{
"name": "Optimizer",
"id": "optimizer",
Expand Down Expand Up @@ -334,6 +351,23 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
"mdFile": "qwik.path.md"
},
{
"name": "preventNavigate",
"id": "experimentalfeatures-preventnavigate",
"hierarchy": [
{
"name": "ExperimentalFeatures",
"id": "experimentalfeatures-preventnavigate"
},
{
"name": "preventNavigate",
"id": "experimentalfeatures-preventnavigate"
}
],
"kind": "EnumMember",
"content": "",
"mdFile": "qwik.experimentalfeatures.preventnavigate.md"
},
{
"name": "QwikBuildMode",
"id": "qwikbuildmode",
Expand Down Expand Up @@ -414,7 +448,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface QwikRollupPluginOptions \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[buildMode?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBuildMode](#qwikbuildmode)\n\n\n</td><td>\n\n_(Optional)_ Build `production` or `development`<!-- -->.\n\nDefault `development`\n\n\n</td></tr>\n<tr><td>\n\n[csr?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[debug?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Prints verbose Qwik plugin debug logs.\n\nDefault `false`\n\n\n</td></tr>\n<tr><td>\n\n[entryStrategy?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[EntryStrategy](#entrystrategy)\n\n\n</td><td>\n\n_(Optional)_ The Qwik entry strategy to use while building for production. During development the type is always `segment`<!-- -->.\n\nDefault `{ type: \"smart\" }`<!-- -->)\n\n\n</td></tr>\n<tr><td>\n\n[experimental?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[ExperimentalFeatures](#experimentalfeatures)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Experimental features. These can come and go in patch releases, and their API is not guaranteed to be stable between releases.\n\n\n</td></tr>\n<tr><td>\n\n[lint?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Run eslint on the source files for the ssr build or dev server. This can slow down startup on large projects. Defaults to `true`\n\n\n</td></tr>\n<tr><td>\n\n[manifestInput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikManifest](#qwikmanifest)\n\n\n</td><td>\n\n_(Optional)_ The SSR build requires the manifest generated during the client build. The `manifestInput` option can be used to manually provide a manifest.\n\nDefault `undefined`\n\n\n</td></tr>\n<tr><td>\n\n[manifestOutput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n(manifest: [QwikManifest](#qwikmanifest)<!-- -->) =&gt; Promise&lt;void&gt; \\| void\n\n\n</td><td>\n\n_(Optional)_ The client build will create a manifest and this hook is called with the generated build data.\n\nDefault `undefined`\n\n\n</td></tr>\n<tr><td>\n\n[optimizerOptions?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[OptimizerOptions](#optimizeroptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[rootDir?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The root of the application, which is commonly the same directory as `package.json` and `rollup.config.js`<!-- -->.\n\nDefault `process.cwd()`\n\n\n</td></tr>\n<tr><td>\n\n[srcDir?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The source directory to find all the Qwik components. Since Qwik does not have a single input, the `srcDir` is used to recursively find Qwik files.\n\nDefault `src`\n\n\n</td></tr>\n<tr><td>\n\n[srcInputs?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[TransformModuleInput](#transformmoduleinput)<!-- -->\\[\\] \\| null\n\n\n</td><td>\n\n_(Optional)_ Alternative to `srcDir`<!-- -->, where `srcInputs` is able to provide the files manually. This option is useful for an environment without a file system, such as a webworker.\n\nDefault: `null`\n\n\n</td></tr>\n<tr><td>\n\n[target?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBuildTarget](#qwikbuildtarget)\n\n\n</td><td>\n\n_(Optional)_ Target `client` or `ssr`<!-- -->.\n\nDefault `client`\n\n\n</td></tr>\n<tr><td>\n\n[transformedModuleOutput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n((transformedModules: [TransformModule](#transformmodule)<!-- -->\\[\\]) =&gt; Promise&lt;void&gt; \\| void) \\| null\n\n\n</td><td>\n\n_(Optional)_ Hook that's called after the build and provides all of the transformed modules that were used before bundling.\n\n\n</td></tr>\n</tbody></table>",
"content": "```typescript\nexport interface QwikRollupPluginOptions \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[buildMode?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBuildMode](#qwikbuildmode)\n\n\n</td><td>\n\n_(Optional)_ Build `production` or `development`<!-- -->.\n\nDefault `development`\n\n\n</td></tr>\n<tr><td>\n\n[csr?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[debug?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Prints verbose Qwik plugin debug logs.\n\nDefault `false`\n\n\n</td></tr>\n<tr><td>\n\n[entryStrategy?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[EntryStrategy](#entrystrategy)\n\n\n</td><td>\n\n_(Optional)_ The Qwik entry strategy to use while building for production. During development the type is always `segment`<!-- -->.\n\nDefault `{ type: \"smart\" }`<!-- -->)\n\n\n</td></tr>\n<tr><td>\n\n[experimental?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n(keyof typeof [ExperimentalFeatures](#experimentalfeatures)<!-- -->)\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Experimental features. These can come and go in patch releases, and their API is not guaranteed to be stable between releases.\n\n\n</td></tr>\n<tr><td>\n\n[lint?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Run eslint on the source files for the ssr build or dev server. This can slow down startup on large projects. Defaults to `true`\n\n\n</td></tr>\n<tr><td>\n\n[manifestInput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikManifest](#qwikmanifest)\n\n\n</td><td>\n\n_(Optional)_ The SSR build requires the manifest generated during the client build. The `manifestInput` option can be used to manually provide a manifest.\n\nDefault `undefined`\n\n\n</td></tr>\n<tr><td>\n\n[manifestOutput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n(manifest: [QwikManifest](#qwikmanifest)<!-- -->) =&gt; Promise&lt;void&gt; \\| void\n\n\n</td><td>\n\n_(Optional)_ The client build will create a manifest and this hook is called with the generated build data.\n\nDefault `undefined`\n\n\n</td></tr>\n<tr><td>\n\n[optimizerOptions?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[OptimizerOptions](#optimizeroptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[rootDir?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The root of the application, which is commonly the same directory as `package.json` and `rollup.config.js`<!-- -->.\n\nDefault `process.cwd()`\n\n\n</td></tr>\n<tr><td>\n\n[srcDir?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The source directory to find all the Qwik components. Since Qwik does not have a single input, the `srcDir` is used to recursively find Qwik files.\n\nDefault `src`\n\n\n</td></tr>\n<tr><td>\n\n[srcInputs?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[TransformModuleInput](#transformmoduleinput)<!-- -->\\[\\] \\| null\n\n\n</td><td>\n\n_(Optional)_ Alternative to `srcDir`<!-- -->, where `srcInputs` is able to provide the files manually. This option is useful for an environment without a file system, such as a webworker.\n\nDefault: `null`\n\n\n</td></tr>\n<tr><td>\n\n[target?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBuildTarget](#qwikbuildtarget)\n\n\n</td><td>\n\n_(Optional)_ Target `client` or `ssr`<!-- -->.\n\nDefault `client`\n\n\n</td></tr>\n<tr><td>\n\n[transformedModuleOutput?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n((transformedModules: [TransformModule](#transformmodule)<!-- -->\\[\\]) =&gt; Promise&lt;void&gt; \\| void) \\| null\n\n\n</td><td>\n\n_(Optional)_ Hook that's called after the build and provides all of the transformed modules that were used before bundling.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/rollup.ts",
"mdFile": "qwik.qwikrolluppluginoptions.md"
},
Expand Down Expand Up @@ -856,6 +890,23 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
"mdFile": "qwik.transpileoption.md"
},
{
"name": "valibot",
"id": "experimentalfeatures-valibot",
"hierarchy": [
{
"name": "ExperimentalFeatures",
"id": "experimentalfeatures-valibot"
},
{
"name": "valibot",
"id": "experimentalfeatures-valibot"
}
],
"kind": "EnumMember",
"content": "",
"mdFile": "qwik.experimentalfeatures.valibot.md"
},
{
"name": "versions",
"id": "versions",
Expand Down
66 changes: 64 additions & 2 deletions packages/docs/src/routes/api/qwik-optimizer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,66 @@ export type EntryStrategy =

Use `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.

Add experimental features to this enum definition.

```typescript
export type ExperimentalFeatures = (typeof experimental)[number];
export declare enum ExperimentalFeatures
```

<table><thead><tr><th>

Member

</th><th>

Value

</th><th>

Description

</th></tr></thead>
<tbody><tr><td>

noSPA

</td><td>

`"noSPA"`

</td><td>

**_(ALPHA)_** Disable SPA navigation handler in Qwik City

</td></tr>
<tr><td>

preventNavigate

</td><td>

`"preventNavigate"`

</td><td>

**_(ALPHA)_** Enable the usePreventNavigate hook

</td></tr>
<tr><td>

valibot

</td><td>

`"valibot"`

</td><td>

**_(ALPHA)_** Enable the Valibot form validation

</td></tr>
</tbody></table>

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts)

## extname
Expand Down Expand Up @@ -699,6 +755,8 @@ string

string

## noSPA

## Optimizer

```typescript
Expand Down Expand Up @@ -1204,6 +1262,8 @@ Description

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts)

## preventNavigate

## QwikBuildMode

```typescript
Expand Down Expand Up @@ -1620,7 +1680,7 @@ Default `{ type: "smart" }`)

</td><td>

[ExperimentalFeatures](#experimentalfeatures)[]
(keyof typeof [ExperimentalFeatures](#experimentalfeatures))[]

</td><td>

Expand Down Expand Up @@ -3572,6 +3632,8 @@ export type TranspileOption = boolean | undefined | null;

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts)

## valibot

## versions

```typescript
Expand Down
29 changes: 23 additions & 6 deletions packages/qwik-city/src/runtime/src/router-outlet-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@ import {
useContext,
_jsxBranch,
useServerData,
sync$,
} from '@builder.io/qwik';

import { ContentInternalContext } from './contexts';
import shim from './spa-shim';
import spaInit from './spa-init';
import type { ClientSPAWindow } from './qwik-city-component';
import type { ScrollHistoryState } from './scroll-restoration';

/** @public */
export const RouterOutlet = component$(() => {
const serverData = useServerData<Record<string, string>>('containerAttributes');
if (!serverData) {
throw new Error('PrefetchServiceWorker component must be rendered on the server.');
}
// TODO Option to remove this shim, especially for MFEs.
const shimScript = shim(serverData['q:base']);

_jsxBranch();

const nonce = useServerData<string | undefined>('nonce');
const { value } = useContext(ContentInternalContext);
if (value && value.length > 0) {
const contentsLen = value.length;
Expand All @@ -37,7 +36,25 @@ export const RouterOutlet = component$(() => {
return (
<>
{cmp}
<script dangerouslySetInnerHTML={shimScript} nonce={nonce}></script>
{!__EXPERIMENTAL__.noSPA && (
<script
document:onQCInit$={spaInit}
document:onQInit$={sync$(() => {
// Minify window and history
((window: ClientSPAWindow, history: History & { state?: ScrollHistoryState }) => {
if (!window._qcs && history.scrollRestoration === 'manual') {
window._qcs = true;

const scrollState = history.state?._qCityScroll;
if (scrollState) {
window.scrollTo(scrollState.x, scrollState.y);
}
document.dispatchEvent(new Event('qcinit'));
}
})(window, history);
})}
></script>
)}
</>
);
}
Expand Down
68 changes: 34 additions & 34 deletions packages/qwik-city/src/runtime/src/spa-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,50 @@ import { event$ } from '@builder.io/qwik';
// - Robust, fully relies only on history. (scrollRestoration = 'manual')

// ! DO NOT IMPORT OR USE ANY EXTERNAL REFERENCES IN THIS SCRIPT.
export default event$((container: HTMLElement) => {
export default event$((_: Event, el: Element) => {
const win: ClientSPAWindow = window;

const currentPath = location.pathname + location.search;

const spa = '_qCitySPA';
const historyPatch = '_qCityHistoryPatch';
const bootstrap = '_qCityBootstrap';
const initPopstate = '_qCityInitPopstate';
const initAnchors = '_qCityInitAnchors';
const initVisibility = '_qCityInitVisibility';
const initScroll = '_qCityInitScroll';
const scrollEnabled = '_qCityScrollEnabled';
const debounceTimeout = '_qCityScrollDebounce';
const scrollHistory = '_qCityScroll';

const checkAndScroll = (scrollState: ScrollState | undefined) => {
if (scrollState) {
win.scrollTo(scrollState.x, scrollState.y);
}
};

const currentScrollState = (): ScrollState => {
const elm = document.documentElement;
return {
x: elm.scrollLeft,
y: elm.scrollTop,
w: Math.max(elm.scrollWidth, elm.clientWidth),
h: Math.max(elm.scrollHeight, elm.clientHeight),
};
};

const saveScrollState = (scrollState?: ScrollState) => {
const state: ScrollHistoryState = history.state || {};
state[scrollHistory] = scrollState || currentScrollState();
history.replaceState(state, '');
};

if (
!win[spa] &&
!win[initPopstate] &&
!win[initAnchors] &&
!win[initVisibility] &&
!win[initScroll]
) {
const currentPath = location.pathname + location.search;

const historyPatch = '_qCityHistoryPatch';
const bootstrap = '_qCityBootstrap';
const scrollEnabled = '_qCityScrollEnabled';
const debounceTimeout = '_qCityScrollDebounce';
const scrollHistory = '_qCityScroll';

const checkAndScroll = (scrollState: ScrollState | undefined) => {
if (scrollState) {
win.scrollTo(scrollState.x, scrollState.y);
}
};

const currentScrollState = (): ScrollState => {
const elm = document.documentElement;
return {
x: elm.scrollLeft,
y: elm.scrollTop,
w: Math.max(elm.scrollWidth, elm.clientWidth),
h: Math.max(elm.scrollHeight, elm.clientHeight),
};
};

const saveScrollState = (scrollState?: ScrollState) => {
const state: ScrollHistoryState = history.state || {};
state[scrollHistory] = scrollState || currentScrollState();
history.replaceState(state, '');
};

saveScrollState();

win[initPopstate] = () => {
Expand All @@ -71,14 +70,15 @@ export default event$((container: HTMLElement) => {
clearTimeout(win[debounceTimeout]);

if (currentPath !== location.pathname + location.search) {
const getContainer = (el: Element) => el.closest('[q\\:container]');
// Hook into useNavigate context, if available.
// We hijack a <Link> here, goes through the loader, resumes, app, etc. Simple.
// TODO Will only work with <Link>, is there a better way?
const link = container.querySelector('a[q\\:link]');
const link = getContainer(el)?.querySelector('a[q\\:link]');

if (link) {
// Re-acquire container, link may be in a nested container.
const container = link.closest('[q\\:container]')!;
const container = getContainer(link)!;
const bootstrapLink = link.cloneNode() as HTMLAnchorElement;
bootstrapLink.setAttribute('q:nbs', '');
bootstrapLink.style.display = 'none';
Expand Down
Loading