From dda39629f8fa33caf9e85487e827e93d7b319468 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 6 Oct 2024 13:13:32 +0200 Subject: [PATCH 1/2] feat(qwik-city): noSPA disables history patching This also refactors the SPA init loading to leverage qwikloader and sync$, reducing HTML size. --- .changeset/curvy-turtles-marry.md | 5 ++ .../runtime/src/router-outlet-component.tsx | 29 ++++++-- .../qwik-city/src/runtime/src/spa-init.ts | 68 +++++++++---------- .../qwik-city/src/runtime/src/spa-shim.ts | 51 -------------- .../qwik/src/optimizer/src/plugins/plugin.ts | 9 ++- 5 files changed, 70 insertions(+), 92 deletions(-) create mode 100644 .changeset/curvy-turtles-marry.md delete mode 100644 packages/qwik-city/src/runtime/src/spa-shim.ts diff --git a/.changeset/curvy-turtles-marry.md b/.changeset/curvy-turtles-marry.md new file mode 100644 index 00000000000..c587609bd16 --- /dev/null +++ b/.changeset/curvy-turtles-marry.md @@ -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). diff --git a/packages/qwik-city/src/runtime/src/router-outlet-component.tsx b/packages/qwik-city/src/runtime/src/router-outlet-component.tsx index 8854e29f2c0..5d2c789d221 100644 --- a/packages/qwik-city/src/runtime/src/router-outlet-component.tsx +++ b/packages/qwik-city/src/runtime/src/router-outlet-component.tsx @@ -6,10 +6,13 @@ 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$(() => { @@ -17,12 +20,8 @@ export const RouterOutlet = component$(() => { 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('nonce'); const { value } = useContext(ContentInternalContext); if (value && value.length > 0) { const contentsLen = value.length; @@ -37,7 +36,25 @@ export const RouterOutlet = component$(() => { return ( <> {cmp} - + {!__EXPERIMENTAL__.noSPA && ( + + )} ); } diff --git a/packages/qwik-city/src/runtime/src/spa-init.ts b/packages/qwik-city/src/runtime/src/spa-init.ts index e9e2e87defc..4249420d45a 100644 --- a/packages/qwik-city/src/runtime/src/spa-init.ts +++ b/packages/qwik-city/src/runtime/src/spa-init.ts @@ -14,44 +14,13 @@ 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] && @@ -59,6 +28,36 @@ export default event$((container: HTMLElement) => { !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] = () => { @@ -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 here, goes through the loader, resumes, app, etc. Simple. // TODO Will only work with , 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'; diff --git a/packages/qwik-city/src/runtime/src/spa-shim.ts b/packages/qwik-city/src/runtime/src/spa-shim.ts deleted file mode 100644 index d4229e9c905..00000000000 --- a/packages/qwik-city/src/runtime/src/spa-shim.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { ClientSPAWindow } from './qwik-city-component'; -import type { ScrollHistoryState } from './scroll-restoration'; - -import { isDev, isServer } from '@builder.io/qwik/build'; -import { getPlatform } from '@builder.io/qwik'; - -import init from './spa-init'; - -export default (base: string) => { - if (isServer) { - const [symbol, bundle] = getPlatform().chunkForSymbol(init.getSymbol(), null, init.dev?.file)!; - const args = [base, bundle, symbol].map((x) => JSON.stringify(x)).join(','); - return `(${shim.toString()})(${args});`; - } -}; - -// SPA shim script: -// - Inlined, loads immediately, checks SPA status. -// - Manually imports and runs the standalone symbol for SPA recovery. -// - Robust, fully relies only on history state. (scrollRestoration = 'manual') -// - If the check here doesn't pass, your page was never SPA. (no SPA pops possible) - -// ! DO NOT IMPORT OR USE ANY EXTERNAL REFERENCES IN THIS SCRIPT. -const shim = async (base: string, path: string, symbol: string) => { - if (!(window as ClientSPAWindow)._qcs && history.scrollRestoration === 'manual') { - // TODO Option to remove this shim especially for MFEs, like loader, for now we only run once. - (window as ClientSPAWindow)._qcs = true; - - const scrollState = (history.state as ScrollHistoryState)?._qCityScroll; - if (scrollState) { - window.scrollTo(scrollState.x, scrollState.y); - } - - const script = document.currentScript as HTMLScriptElement; - if (script) { - // Inside shadow DOM, we can't get a hold of a container. So we can't - // load the SPA shim. - const container = script!.closest('[q\\:container]')!; - const url = new URL(path, new URL(base, document.baseURI)); - - if (isDev) { - // Bypass dev import hijack. (not going to work here) - // eslint-disable-next-line no-new-func - const imp = new Function('url', 'return import(url)'); - (await imp(url.href))[symbol](container); - } else { - (await import(url.href))[symbol](container); - } - } - } -}; diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index e342e7e8a79..c5db87521c8 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -55,7 +55,14 @@ const CLIENT_STRIP_CTX_NAME = [ ]; /** List experimental features here */ -export const experimental = ['preventNavigate', 'valibot'] as const; +export const experimental = [ + /** Enable the usePreventNavigate hook */ + 'preventNavigate', + /** Enable the Valibot form validation */ + 'valibot', + /** Disable SPA navigation handler in Qwik City */ + 'noSPA', +] as const; /** * Use `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or * `false` via an exact string replacement. From 60df208d325e43f54653259c1b81b16633c6e830 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 6 Oct 2024 13:19:00 +0200 Subject: [PATCH 2/2] refactor(optimizer): allow documenting experimental features by using a string enum we can refer to the items directly and they end up in the API docs. Plus the code is cleaner. --- .../src/routes/api/qwik-optimizer/api.json | 57 +++++++++++++++- .../src/routes/api/qwik-optimizer/index.md | 66 ++++++++++++++++++- packages/qwik/src/optimizer/src/api.md | 10 +-- .../qwik/src/optimizer/src/plugins/plugin.ts | 30 ++++----- .../src/optimizer/src/plugins/plugin.unit.ts | 4 +- .../qwik/src/optimizer/src/plugins/rollup.ts | 4 +- .../qwik/src/optimizer/src/plugins/vite.ts | 4 +- 7 files changed, 145 insertions(+), 30 deletions(-) diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 7c78088183f..1ce05405480 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -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\n\n\n\n
\n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
\n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
\n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
\n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts", "mdFile": "qwik.experimentalfeatures.md" }, @@ -261,6 +261,23 @@ "content": "```typescript\nnormalize(path: string): string;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\npath\n\n\n\n\nstring\n\n\n\n\n\n
\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", @@ -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", @@ -414,7 +448,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikRollupPluginOptions \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[buildMode?](#)\n\n\n\n\n\n\n\n[QwikBuildMode](#qwikbuildmode)\n\n\n\n\n_(Optional)_ Build `production` or `development`.\n\nDefault `development`\n\n\n
\n\n[csr?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_\n\n\n
\n\n[debug?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Prints verbose Qwik plugin debug logs.\n\nDefault `false`\n\n\n
\n\n[entryStrategy?](#)\n\n\n\n\n\n\n\n[EntryStrategy](#entrystrategy)\n\n\n\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
\n\n[experimental?](#)\n\n\n\n\n\n\n\n[ExperimentalFeatures](#experimentalfeatures)\\[\\]\n\n\n\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
\n\n[lint?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\n[manifestInput?](#)\n\n\n\n\n\n\n\n[QwikManifest](#qwikmanifest)\n\n\n\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
\n\n[manifestOutput?](#)\n\n\n\n\n\n\n\n(manifest: [QwikManifest](#qwikmanifest)) => Promise<void> \\| void\n\n\n\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
\n\n[optimizerOptions?](#)\n\n\n\n\n\n\n\n[OptimizerOptions](#optimizeroptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n[rootDir?](#)\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\n[srcDir?](#)\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\n[srcInputs?](#)\n\n\n\n\n\n\n\n[TransformModuleInput](#transformmoduleinput)\\[\\] \\| null\n\n\n\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
\n\n[target?](#)\n\n\n\n\n\n\n\n[QwikBuildTarget](#qwikbuildtarget)\n\n\n\n\n_(Optional)_ Target `client` or `ssr`.\n\nDefault `client`\n\n\n
\n\n[transformedModuleOutput?](#)\n\n\n\n\n\n\n\n((transformedModules: [TransformModule](#transformmodule)\\[\\]) => Promise<void> \\| void) \\| null\n\n\n\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
", + "content": "```typescript\nexport interface QwikRollupPluginOptions \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[buildMode?](#)\n\n\n\n\n\n\n\n[QwikBuildMode](#qwikbuildmode)\n\n\n\n\n_(Optional)_ Build `production` or `development`.\n\nDefault `development`\n\n\n
\n\n[csr?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_\n\n\n
\n\n[debug?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Prints verbose Qwik plugin debug logs.\n\nDefault `false`\n\n\n
\n\n[entryStrategy?](#)\n\n\n\n\n\n\n\n[EntryStrategy](#entrystrategy)\n\n\n\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
\n\n[experimental?](#)\n\n\n\n\n\n\n\n(keyof typeof [ExperimentalFeatures](#experimentalfeatures))\\[\\]\n\n\n\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
\n\n[lint?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\n[manifestInput?](#)\n\n\n\n\n\n\n\n[QwikManifest](#qwikmanifest)\n\n\n\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
\n\n[manifestOutput?](#)\n\n\n\n\n\n\n\n(manifest: [QwikManifest](#qwikmanifest)) => Promise<void> \\| void\n\n\n\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
\n\n[optimizerOptions?](#)\n\n\n\n\n\n\n\n[OptimizerOptions](#optimizeroptions)\n\n\n\n\n_(Optional)_\n\n\n
\n\n[rootDir?](#)\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\n[srcDir?](#)\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\n[srcInputs?](#)\n\n\n\n\n\n\n\n[TransformModuleInput](#transformmoduleinput)\\[\\] \\| null\n\n\n\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
\n\n[target?](#)\n\n\n\n\n\n\n\n[QwikBuildTarget](#qwikbuildtarget)\n\n\n\n\n_(Optional)_ Target `client` or `ssr`.\n\nDefault `client`\n\n\n
\n\n[transformedModuleOutput?](#)\n\n\n\n\n\n\n\n((transformedModules: [TransformModule](#transformmodule)\\[\\]) => Promise<void> \\| void) \\| null\n\n\n\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
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/rollup.ts", "mdFile": "qwik.qwikrolluppluginoptions.md" }, @@ -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", diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.md b/packages/docs/src/routes/api/qwik-optimizer/index.md index b5e807c46ac..68f01c9fa21 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.md +++ b/packages/docs/src/routes/api/qwik-optimizer/index.md @@ -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 ``` + + + + +
+ +Member + + + +Value + + + +Description + +
+ +noSPA + + + +`"noSPA"` + + + +**_(ALPHA)_** Disable SPA navigation handler in Qwik City + +
+ +preventNavigate + + + +`"preventNavigate"` + + + +**_(ALPHA)_** Enable the usePreventNavigate hook + +
+ +valibot + + + +`"valibot"` + + + +**_(ALPHA)_** Enable the Valibot form validation + +
+ [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts) ## extname @@ -699,6 +755,8 @@ string string +## noSPA + ## Optimizer ```typescript @@ -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 @@ -1620,7 +1680,7 @@ Default `{ type: "smart" }`) -[ExperimentalFeatures](#experimentalfeatures)[] +(keyof typeof [ExperimentalFeatures](#experimentalfeatures))[] @@ -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 diff --git a/packages/qwik/src/optimizer/src/api.md b/packages/qwik/src/optimizer/src/api.md index 8e936478a22..df24882b4fe 100644 --- a/packages/qwik/src/optimizer/src/api.md +++ b/packages/qwik/src/optimizer/src/api.md @@ -44,10 +44,12 @@ export type DiagnosticCategory = 'error' | 'warning' | 'sourceError'; // @public (undocumented) export type EntryStrategy = InlineEntryStrategy | HoistEntryStrategy | SingleEntryStrategy | HookEntryStrategy_2 | SegmentEntryStrategy | ComponentEntryStrategy | SmartEntryStrategy; -// Warning: (ae-forgotten-export) The symbol "experimental" needs to be exported by the entry point index.d.ts -// // @alpha -export type ExperimentalFeatures = (typeof experimental)[number]; +export enum ExperimentalFeatures { + noSPA = "noSPA", + preventNavigate = "preventNavigate", + valibot = "valibot" +} // @public (undocumented) export interface GlobalInjections { @@ -225,7 +227,7 @@ export interface QwikRollupPluginOptions { debug?: boolean; entryStrategy?: EntryStrategy; // Warning: (ae-incompatible-release-tags) The symbol "experimental" is marked as @public, but its signature references "ExperimentalFeatures" which is marked as @alpha - experimental?: ExperimentalFeatures[]; + experimental?: (keyof typeof ExperimentalFeatures)[]; lint?: boolean; manifestInput?: QwikManifest; manifestOutput?: (manifest: QwikManifest) => Promise | void; diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index c5db87521c8..0669b3f1efc 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -54,22 +54,22 @@ const CLIENT_STRIP_CTX_NAME = [ 'event$', ]; -/** List experimental features here */ -export const experimental = [ - /** Enable the usePreventNavigate hook */ - 'preventNavigate', - /** Enable the Valibot form validation */ - 'valibot', - /** Disable SPA navigation handler in Qwik City */ - 'noSPA', -] as const; /** * 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. + * * @alpha */ -export type ExperimentalFeatures = (typeof experimental)[number]; +export enum ExperimentalFeatures { + /** Enable the usePreventNavigate hook */ + preventNavigate = 'preventNavigate', + /** Enable the Valibot form validation */ + valibot = 'valibot', + /** Disable SPA navigation handler in Qwik City */ + noSPA = 'noSPA', +} export interface QwikPackages { id: string; @@ -334,10 +334,10 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { opts.experimental = undefined; for (const feature of updatedOpts.experimental ?? []) { - if (!experimental.includes(feature as ExperimentalFeatures)) { + if (!ExperimentalFeatures[feature as ExperimentalFeatures]) { console.error(`Qwik plugin: Unknown experimental feature: ${feature}`); } else { - (opts.experimental ||= {} as any)[feature as ExperimentalFeatures] = true; + (opts.experimental ||= {} as any)[feature] = true; } } @@ -1051,15 +1051,15 @@ export interface QwikPluginOptions { lint?: boolean; /** * Experimental features. These can come and go in patch releases, and their API is not guaranteed - * to be stable between releases + * to be stable between releases. */ - experimental?: ExperimentalFeatures[]; + experimental?: (keyof typeof ExperimentalFeatures)[]; } export interface NormalizedQwikPluginOptions extends Omit, 'vendorRoots' | 'experimental'> { input: string[] | { [entry: string]: string }; - experimental?: Record; + experimental?: Record; } /** @public */ diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.unit.ts b/packages/qwik/src/optimizer/src/plugins/plugin.unit.ts index fa668099a3e..360e41fe5ad 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.unit.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.unit.ts @@ -1,7 +1,7 @@ import path, { resolve } from 'node:path'; import { assert, test } from 'vitest'; import type { QwikManifest } from '../types'; -import { createPlugin, experimental } from './plugin'; +import { ExperimentalFeatures, createPlugin } from './plugin'; import { normalizePath } from '../../../testing/util'; import { qwikVite } from './vite'; @@ -209,7 +209,7 @@ test('resolveQwikBuild false', async () => { test('experimental[]', async () => { const plugin = await mockPlugin(); - const flag = experimental[0]; + const flag = Object.values(ExperimentalFeatures)[0]; if (!flag) { // we can't test this without a flag return; diff --git a/packages/qwik/src/optimizer/src/plugins/rollup.ts b/packages/qwik/src/optimizer/src/plugins/rollup.ts index 64183a6c162..0438aec6235 100644 --- a/packages/qwik/src/optimizer/src/plugins/rollup.ts +++ b/packages/qwik/src/optimizer/src/plugins/rollup.ts @@ -338,8 +338,8 @@ export interface QwikRollupPluginOptions { * Experimental features. These can come and go in patch releases, and their API is not guaranteed * to be stable between releases. */ - experimental?: ExperimentalFeatures[]; + experimental?: (keyof typeof ExperimentalFeatures)[]; } -export type { ExperimentalFeatures } from './plugin'; +export { ExperimentalFeatures } from './plugin'; type P = Rollup.Plugin & { api: T }; export interface QwikRollupPlugin extends P {} diff --git a/packages/qwik/src/optimizer/src/plugins/vite.ts b/packages/qwik/src/optimizer/src/plugins/vite.ts index 5689461b0be..85e759deb22 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite.ts @@ -991,7 +991,7 @@ interface QwikVitePluginCommonOptions { * Experimental features. These can come and go in patch releases, and their API is not guaranteed * to be stable between releases */ - experimental?: ExperimentalFeatures[]; + experimental?: (keyof typeof ExperimentalFeatures)[]; } interface QwikVitePluginCSROptions extends QwikVitePluginCommonOptions { @@ -1080,7 +1080,7 @@ interface QwikVitePluginCSROptions extends QwikVitePluginCommonOptions { /** @public */ export type QwikVitePluginOptions = QwikVitePluginCSROptions | QwikVitePluginSSROptions; -export type { ExperimentalFeatures } from './plugin'; +export { ExperimentalFeatures } from './plugin'; /** @public */ export interface QwikVitePluginApi {