diff --git a/.changeset/fair-cars-fry.md b/.changeset/fair-cars-fry.md new file mode 100644 index 00000000000..eb9e9fac6ad --- /dev/null +++ b/.changeset/fair-cars-fry.md @@ -0,0 +1,6 @@ +--- +'@builder.io/qwik': minor +--- + +PERF: Prefetching now happens dynamically without service worker if the prefetchImplementation is set to "html-append" (default). +PERF: Initial prefetching now includes dynamic imports, which improves initial click delay. diff --git a/.changeset/yellow-frogs-repeat.md b/.changeset/yellow-frogs-repeat.md new file mode 100644 index 00000000000..cb59a0e4203 --- /dev/null +++ b/.changeset/yellow-frogs-repeat.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik-city': minor +--- + +CHORE: the service workers have been deprecated and replaced with entries that unregister them. If you have it enabled in production, you can remove it after a while once you are sure all your users have the new version. diff --git a/packages/docs/src/entry.ssr.tsx b/packages/docs/src/entry.ssr.tsx index b88f9909ae5..f16130af5a2 100644 --- a/packages/docs/src/entry.ssr.tsx +++ b/packages/docs/src/entry.ssr.tsx @@ -1,10 +1,8 @@ import { renderToStream, type RenderToStreamOptions } from '@builder.io/qwik/server'; -import { manifest } from '@qwik-client-manifest'; import Root from './root'; export default function (opts: RenderToStreamOptions) { return renderToStream(, { - manifest, qwikLoader: { // The docs can be long so make sure to intercept events before the end of the document. position: 'top', diff --git a/packages/docs/src/repl/bundled.tsx b/packages/docs/src/repl/bundled.tsx index 6ce706ac5f1..5c4626a8935 100644 --- a/packages/docs/src/repl/bundled.tsx +++ b/packages/docs/src/repl/bundled.tsx @@ -14,6 +14,7 @@ import qCoreDts from '../../node_modules/@builder.io/qwik/dist/core.d.ts?raw-sou import qCoreMinMjs from '../../node_modules/@builder.io/qwik/dist/core.min.mjs?raw-source'; import qCoreMjs from '../../node_modules/@builder.io/qwik/dist/core.mjs?raw-source'; import qOptimizerCjs from '../../node_modules/@builder.io/qwik/dist/optimizer.cjs?raw-source'; +import qPreloaderMjs from '../../node_modules/@builder.io/qwik/dist/preloader.mjs?raw-source'; import qServerCjs from '../../node_modules/@builder.io/qwik/dist/server.cjs?raw-source'; import qServerDts from '../../node_modules/@builder.io/qwik/dist/server.d.ts?raw-source'; import qWasmCjs from '../../node_modules/@builder.io/qwik/bindings/qwik.wasm.cjs?raw-source'; @@ -55,6 +56,7 @@ export const bundled: PkgUrls = { '/dist/optimizer.cjs': qOptimizerCjs, '/dist/server.cjs': qServerCjs, '/dist/server.d.ts': qServerDts, + '/dist/preloader.mjs': qPreloaderMjs, '/bindings/qwik.wasm.cjs': qWasmCjs, '/bindings/qwik_wasm_bg.wasm': qWasmBinUrl, }, diff --git a/packages/docs/src/repl/repl-output-modules.tsx b/packages/docs/src/repl/repl-output-modules.tsx index 4cdf156fba0..ea6635b6e3c 100644 --- a/packages/docs/src/repl/repl-output-modules.tsx +++ b/packages/docs/src/repl/repl-output-modules.tsx @@ -1,4 +1,4 @@ -import { $, component$, useSignal } from '@builder.io/qwik'; +import { $, component$, createSignal, useSignal } from '@builder.io/qwik'; import { CodeBlock } from '../components/code-block/code-block'; import type { ReplModuleOutput } from './types'; const FILE_MODULE_DIV_ID = 'file-modules-client-modules'; @@ -35,22 +35,34 @@ export const ReplOutputModules = component$(({ outputs, headerText }: ReplOutput
- {outputs.map((o, i) => ( -
-
- {o.path} - {o.size ? ({o.size}) : null} + {outputs.map((o, i) => { + const isLarge = o.code.length > 3000; + if (isLarge && !o.shorten) { + o.shorten = createSignal(true); + } + const code = o.shorten?.value ? o.code.slice(0, 3000) : o.code; + return ( +
+
+ {o.path} + {o.size ? ({o.size}) : null} +
+
+ + {o.shorten && ( + + )} +
-
- -
-
- ))} + ); + })}
); diff --git a/packages/docs/src/repl/repl-output-panel.tsx b/packages/docs/src/repl/repl-output-panel.tsx index 5c2ba35c156..06385eb677f 100644 --- a/packages/docs/src/repl/repl-output-panel.tsx +++ b/packages/docs/src/repl/repl-output-panel.tsx @@ -1,4 +1,4 @@ -import { component$, useComputed$ } from '@builder.io/qwik'; +import { component$ } from '@builder.io/qwik'; import { CodeBlock } from '../components/code-block/code-block'; import { ReplOutputModules } from './repl-output-modules'; import { ReplOutputSymbols } from './repl-output-symbols'; @@ -8,10 +8,6 @@ import type { ReplAppInput, ReplStore } from './types'; export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProps) => { const diagnosticsLen = store.diagnostics.length + store.monacoDiagnostics.length; - const clientBundlesNoCore = useComputed$(() => - // Qwik Core is not interesting and is large, slowing down the UI - store.clientBundles.filter((b) => !b.path.endsWith('qwikCore.js')) - ); return (
@@ -115,7 +111,7 @@ export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProp ) : null} {store.selectedOutputPanel === 'clientBundles' ? ( - + ) : null} {store.selectedOutputPanel === 'serverModules' ? ( diff --git a/packages/docs/src/repl/types.ts b/packages/docs/src/repl/types.ts index faebfa0524b..11768f1cc4a 100644 --- a/packages/docs/src/repl/types.ts +++ b/packages/docs/src/repl/types.ts @@ -1,10 +1,10 @@ +import type { NoSerialize, Signal } from '@builder.io/qwik'; import type { Diagnostic, QwikManifest, QwikRollupPluginOptions, TransformModule, } from '@builder.io/qwik/optimizer'; -import type { NoSerialize } from '@builder.io/qwik'; export interface ReplAppInput { buildId: number; @@ -58,6 +58,7 @@ export interface ReplModuleOutput { path: string; code: string; size?: string; + shorten?: Signal; } export interface ReplMessageBase { diff --git a/packages/docs/src/repl/worker/app-bundle-client.ts b/packages/docs/src/repl/worker/app-bundle-client.ts index 6bc1dcbd5a1..4a2f6aa582f 100644 --- a/packages/docs/src/repl/worker/app-bundle-client.ts +++ b/packages/docs/src/repl/worker/app-bundle-client.ts @@ -104,14 +104,6 @@ export const appBundleClient = async ( }); } - result.transformedModules = result.transformedModules.filter((f) => { - return ( - !f.path.endsWith('app.js') && - !f.path.endsWith('entry.server.js') && - !f.path.endsWith('root.js') - ); - }); - result.events.push({ kind: 'console-log', scope: 'build', diff --git a/packages/docs/src/repl/worker/repl-plugins.ts b/packages/docs/src/repl/worker/repl-plugins.ts index 2e9cd574f9a..3866f685e84 100644 --- a/packages/docs/src/repl/worker/repl-plugins.ts +++ b/packages/docs/src/repl/worker/repl-plugins.ts @@ -29,6 +29,9 @@ export const replResolver = (options: ReplInputOptions, buildMode: 'client' | 's if (id === '@builder.io/qwik/server') { return '\0qwikServer'; } + if (id === '@builder.io/qwik/preloader') { + return '\0qwikPreloader'; + } // Simple relative file resolution if (id.startsWith('./')) { const extensions = ['', '.tsx', '.ts']; @@ -69,6 +72,12 @@ export const replResolver = (options: ReplInputOptions, buildMode: 'client' | 's } throw new Error(`Unable to load Qwik core`); } + if (id === '\0qwikPreloader') { + const rsp = await depResponse('@builder.io/qwik', '/preloader.mjs'); + if (rsp) { + return rsp.text(); + } + } // We're the fallback, we know all the files if (/\.[jt]sx?$/.test(id)) { diff --git a/packages/docs/src/root.tsx b/packages/docs/src/root.tsx index 1935489e3fa..e3c16689546 100644 --- a/packages/docs/src/root.tsx +++ b/packages/docs/src/root.tsx @@ -1,11 +1,11 @@ import { component$, useContextProvider, useStore } from '@builder.io/qwik'; import { QwikCityProvider, RouterOutlet, ServiceWorkerRegister } from '@builder.io/qwik-city'; +import { Insights } from '@builder.io/qwik-labs'; import RealMetricsOptimization from './components/real-metrics-optimization/real-metrics-optimization'; import { RouterHead } from './components/router-head/router-head'; +import { BUILDER_PUBLIC_API_KEY } from './constants'; import { GlobalStore, type SiteStore } from './context'; import './global.css'; -import { BUILDER_PUBLIC_API_KEY } from './constants'; -import { Insights } from '@builder.io/qwik-labs'; export const uwu = /*javascript*/ ` ;(function () { diff --git a/packages/docs/src/routes/api/qwik-city/api.json b/packages/docs/src/routes/api/qwik-city/api.json index 6c42d66b5a9..7a66897870d 100644 --- a/packages/docs/src/routes/api/qwik-city/api.json +++ b/packages/docs/src/routes/api/qwik-city/api.json @@ -782,7 +782,7 @@ } ], "kind": "Function", - "content": "```typescript\nServiceWorkerRegister: (props: {\n nonce?: string;\n}) => import(\"@builder.io/qwik\").JSXNode<\"script\">\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nprops\n\n\n\n\n{ nonce?: string; }\n\n\n\n\n\n
\n**Returns:**\n\nimport(\"@builder.io/qwik\").JSXNode<\"script\">", + "content": "Loads the service workers that are defined in the routes. Any file named `service-worker.*` (all JS extensions are allowed) will be picked up, bundled into a separate file, and registered as a service worker.\n\n\n```typescript\nServiceWorkerRegister: (props: {\n nonce?: string;\n}) => JSXOutput\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nprops\n\n\n\n\n{ nonce?: string; }\n\n\n\n\n\n
\n**Returns:**\n\nJSXOutput", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/sw-component.tsx", "mdFile": "qwik-city.serviceworkerregister.md" }, diff --git a/packages/docs/src/routes/api/qwik-city/index.md b/packages/docs/src/routes/api/qwik-city/index.md index 51792d84a2a..6af6d3b358c 100644 --- a/packages/docs/src/routes/api/qwik-city/index.md +++ b/packages/docs/src/routes/api/qwik-city/index.md @@ -2269,9 +2269,10 @@ export type ServerQRL = QRL< ## ServiceWorkerRegister +Loads the service workers that are defined in the routes. Any file named `service-worker.*` (all JS extensions are allowed) will be picked up, bundled into a separate file, and registered as a service worker. + ```typescript -ServiceWorkerRegister: (props: { nonce?: string }) => - import("@builder.io/qwik").JSXNode<"script">; +ServiceWorkerRegister: (props: { nonce?: string }) => JSXOutput; ```
@@ -2301,7 +2302,7 @@ props
**Returns:** -import("@builder.io/qwik").JSXNode<"script"> +JSXOutput [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/sw-component.tsx) diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 04faea738c8..63b6cdd0794 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -19,6 +19,20 @@ "content": "```typescript\nbasename(path: string, ext?: string): string;\n```\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\next\n\n\n\n\nstring\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nstring", "mdFile": "qwik.path.basename.md" }, + { + "name": "BundleGraphAdder", + "id": "bundlegraphadder", + "hierarchy": [ + { + "name": "BundleGraphAdder", + "id": "bundlegraphadder" + } + ], + "kind": "TypeAlias", + "content": "A function that returns a map of bundle names to their dependencies.\n\n\n```typescript\nexport type BundleGraphAdder = (manifest: QwikManifest) => Record;\n```\n**References:** [QwikManifest](#qwikmanifest)", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/bundle-graph.ts", + "mdFile": "qwik.bundlegraphadder.md" + }, { "name": "ComponentEntryStrategy", "id": "componententrystrategy", @@ -182,20 +196,6 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.inlineentrystrategy.md" }, - { - "name": "InsightManifest", - "id": "insightmanifest", - "hierarchy": [ - { - "name": "InsightManifest", - "id": "insightmanifest" - } - ], - "kind": "Interface", - "content": "```typescript\nexport interface InsightManifest \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[manual](#)\n\n\n\n\n\n\n\nRecord<string, string>\n\n\n\n\n\n
\n\n[prefetch](#)\n\n\n\n\n\n\n\n{ route: string; symbols: string\\[\\]; }\\[\\]\n\n\n\n\n\n
\n\n[type](#)\n\n\n\n\n\n\n\n'smart'\n\n\n\n\n\n
", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", - "mdFile": "qwik.insightmanifest.md" - }, { "name": "isAbsolute", "id": "path-isabsolute", @@ -406,10 +406,24 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikBundle \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[dynamicImports?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_\n\n\n
\n\n[hasSymbols?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_\n\n\n
\n\n[imports?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_\n\n\n
\n\n[origins?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_\n\n\n
\n\n[size](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n\n
\n\n[symbols?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_\n\n\n
", + "content": "```typescript\nexport interface QwikBundle \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[dynamicImports?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Dynamic imports\n\n\n
\n\n[imports?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Direct imports\n\n\n
\n\n[interactivity?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Interactivity score of the bundle\n\n\n
\n\n[origins?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Source files of the bundle\n\n\n
\n\n[size](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\nSize of the bundle\n\n\n
\n\n[symbols?](#)\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Symbols in the bundle\n\n\n
\n\n[total](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\nTotal size of this bundle's static import graph\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.qwikbundle.md" }, + { + "name": "QwikBundleGraph", + "id": "qwikbundlegraph", + "hierarchy": [ + { + "name": "QwikBundleGraph", + "id": "qwikbundlegraph" + } + ], + "kind": "TypeAlias", + "content": "Bundle graph.\n\nFormat: \\[ 'bundle-a.js', 3, 5 // Depends on 'bundle-b.js' and 'bundle-c.js' 'bundle-b.js', 5, // Depends on 'bundle-c.js' 'bundle-c.js', \\]\n\n\n```typescript\nexport type QwikBundleGraph = Array;\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", + "mdFile": "qwik.qwikbundlegraph.md" + }, { "name": "QwikManifest", "id": "qwikmanifest", @@ -420,7 +434,7 @@ } ], "kind": "Interface", - "content": "The metadata of the build. One of its uses is storing where QRL symbols are located.\n\n\n```typescript\nexport interface QwikManifest \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[bundles](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle); }\n\n\n\n\nAll code bundles, used to know the import graph\n\n\n
\n\n[injections?](#)\n\n\n\n\n\n\n\n[GlobalInjections](#globalinjections)\\[\\]\n\n\n\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n
\n\n[manifestHash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\nContent hash of the manifest, if this changes, the code changed\n\n\n
\n\n[mapping](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: string; }\n\n\n\n\nWhere QRLs are located\n\n\n
\n\n[options?](#)\n\n\n\n\n\n\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[platform?](#)\n\n\n\n\n\n\n\n{ \\[name: string\\]: string; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[symbols](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol); }\n\n\n\n\nQRL symbols\n\n\n
\n\n[version](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", + "content": "The metadata of the build. One of its uses is storing where QRL symbols are located.\n\n\n```typescript\nexport interface QwikManifest \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[bundleGraph?](#)\n\n\n\n\n\n\n\n[QwikBundleGraph](#qwikbundlegraph)\n\n\n\n\n_(Optional)_ All bundles in a compact graph format with probabilities\n\n\n
\n\n[bundles](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle); }\n\n\n\n\nAll code bundles, used to know the import graph\n\n\n
\n\n[injections?](#)\n\n\n\n\n\n\n\n[GlobalInjections](#globalinjections)\\[\\]\n\n\n\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n
\n\n[manifestHash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\nContent hash of the manifest, if this changes, the code changed\n\n\n
\n\n[mapping](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: string; }\n\n\n\n\nWhere QRLs are located\n\n\n
\n\n[options?](#)\n\n\n\n\n\n\n\n{ target?: string; buildMode?: string; entryStrategy?: { type: [EntryStrategy](#entrystrategy)\\['type'\\]; }; }\n\n\n\n\n_(Optional)_ The options used to build the manifest\n\n\n
\n\n[platform?](#)\n\n\n\n\n\n\n\n{ \\[name: string\\]: string; }\n\n\n\n\n_(Optional)_ The platform used to build the manifest\n\n\n
\n\n[preloader?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The preloader bundle fileName\n\n\n
\n\n[symbols](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol); }\n\n\n\n\nQRL symbols\n\n\n
\n\n[version](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\nThe version of the manifest\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.qwikmanifest.md" }, @@ -462,7 +476,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikSymbol \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[canonicalFilename](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[captures](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[ctxKind](#)\n\n\n\n\n\n\n\n'function' \\| 'event'\n\n\n\n\n\n
\n\n[ctxName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[displayName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[hash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[loc](#)\n\n\n\n\n\n\n\n\\[number, number\\]\n\n\n\n\n\n
\n\n[origin](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[parent](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
", + "content": "```typescript\nexport interface QwikSymbol \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[canonicalFilename](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[captures](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[ctxKind](#)\n\n\n\n\n\n\n\n'function' \\| 'eventHandler'\n\n\n\n\n\n
\n\n[ctxName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[displayName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[hash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[loc](#)\n\n\n\n\n\n\n\n\\[number, number\\]\n\n\n\n\n\n
\n\n[origin](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[parent](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.qwiksymbol.md" }, @@ -518,7 +532,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikVitePluginApi \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[getAssetsDir](#)\n\n\n\n\n\n\n\n() => string \\| undefined\n\n\n\n\n\n
\n\n[getClientOutDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\n[getClientPublicOutDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\n[getInsightsManifest](#)\n\n\n\n\n\n\n\n(clientOutDir?: string \\| null) => Promise<[InsightManifest](#insightmanifest) \\| null>\n\n\n\n\n\n
\n\n[getManifest](#)\n\n\n\n\n\n\n\n() => [QwikManifest](#qwikmanifest) \\| null\n\n\n\n\n\n
\n\n[getOptimizer](#)\n\n\n\n\n\n\n\n() => [Optimizer](#optimizer) \\| null\n\n\n\n\n\n
\n\n[getOptions](#)\n\n\n\n\n\n\n\n() => NormalizedQwikPluginOptions\n\n\n\n\n\n
\n\n[getRootDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
", + "content": "```typescript\nexport interface QwikVitePluginApi \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[getAssetsDir](#)\n\n\n\n\n\n\n\n() => string \\| undefined\n\n\n\n\n\n
\n\n[getClientOutDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\n[getClientPublicOutDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\n[getManifest](#)\n\n\n\n\n\n\n\n() => [QwikManifest](#qwikmanifest) \\| null\n\n\n\n\n\n
\n\n[getOptimizer](#)\n\n\n\n\n\n\n\n() => [Optimizer](#optimizer) \\| null\n\n\n\n\n\n
\n\n[getOptions](#)\n\n\n\n\n\n\n\n() => NormalizedQwikPluginOptions\n\n\n\n\n\n
\n\n[getRootDir](#)\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\n[registerBundleGraphAdder](#)\n\n\n\n\n\n\n\n(adder: [BundleGraphAdder](#bundlegraphadder)) => void\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/vite.ts", "mdFile": "qwik.qwikvitepluginapi.md" }, @@ -580,7 +594,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface ResolvedManifest \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[manifest](#)\n\n\n\n\n\n\n\n[QwikManifest](#qwikmanifest)\n\n\n\n\n\n
\n\n[mapper](#)\n\n\n\n\n\n\n\n[SymbolMapper](#symbolmapper)\n\n\n\n\n\n
", + "content": "```typescript\nexport interface ResolvedManifest \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[injections](#)\n\n\n\n\n\n\n\n[GlobalInjections](#globalinjections)\\[\\]\n\n\n\n\n\n
\n\n[manifest](#)\n\n\n\n\n\n\n\n[QwikManifest](#qwikmanifest)\n\n\n\n\n\n
\n\n[mapper](#)\n\n\n\n\n\n\n\n[SymbolMapper](#symbolmapper)\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.resolvedmanifest.md" }, @@ -594,7 +608,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface SegmentAnalysis \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[canonicalFilename](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[captures](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[ctxKind](#)\n\n\n\n\n\n\n\n'event' \\| 'function'\n\n\n\n\n\n
\n\n[ctxName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[displayName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[entry](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
\n\n[extension](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[hash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[loc](#)\n\n\n\n\n\n\n\n\\[number, number\\]\n\n\n\n\n\n
\n\n[name](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[origin](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[parent](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
", + "content": "```typescript\nexport interface SegmentAnalysis \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[canonicalFilename](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[captures](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n\n
\n\n[ctxKind](#)\n\n\n\n\n\n\n\n'eventHandler' \\| 'function'\n\n\n\n\n\n
\n\n[ctxName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[displayName](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[entry](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
\n\n[extension](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[hash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[loc](#)\n\n\n\n\n\n\n\n\\[number, number\\]\n\n\n\n\n\n
\n\n[name](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[origin](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[parent](#)\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.segmentanalysis.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.md b/packages/docs/src/routes/api/qwik-optimizer/index.md index 1664f10545f..722bdf1e224 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.md +++ b/packages/docs/src/routes/api/qwik-optimizer/index.md @@ -52,6 +52,24 @@ _(Optional)_ string +## BundleGraphAdder + +A function that returns a map of bundle names to their dependencies. + +```typescript +export type BundleGraphAdder = (manifest: QwikManifest) => Record< + string, + { + imports?: string[]; + dynamicImports?: string[]; + } +>; +``` + +**References:** [QwikManifest](#qwikmanifest) + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/bundle-graph.ts) + ## ComponentEntryStrategy ```typescript @@ -576,72 +594,6 @@ Description [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts) -## InsightManifest - -```typescript -export interface InsightManifest -``` - - - - - -
- -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[manual](#) - - - - - -Record<string, string> - - - -
- -[prefetch](#) - - - - - -{ route: string; symbols: string[]; }[] - - - -
- -[type](#) - - - - - -'smart' - - - -
- -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts) - ## isAbsolute ```typescript @@ -1315,37 +1267,37 @@ string[] -_(Optional)_ +_(Optional)_ Dynamic imports -[hasSymbols?](#) +[imports?](#) -boolean +string[] -_(Optional)_ +_(Optional)_ Direct imports -[imports?](#) +[interactivity?](#) -string[] +number -_(Optional)_ +_(Optional)_ Interactivity score of the bundle @@ -1360,7 +1312,7 @@ string[] -_(Optional)_ +_(Optional)_ Source files of the bundle @@ -1375,6 +1327,8 @@ number +Size of the bundle + @@ -1388,13 +1342,40 @@ string[] -_(Optional)_ +_(Optional)_ Symbols in the bundle + + + + +[total](#) + + + + + +number + + + +Total size of this bundle's static import graph [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts) +## QwikBundleGraph + +Bundle graph. + +Format: [ 'bundle-a.js', 3, 5 // Depends on 'bundle-b.js' and 'bundle-c.js' 'bundle-b.js', 5, // Depends on 'bundle-c.js' 'bundle-c.js', ] + +```typescript +export type QwikBundleGraph = Array; +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts) + ## QwikManifest The metadata of the build. One of its uses is storing where QRL symbols are located. @@ -1422,6 +1403,21 @@ Description +[bundleGraph?](#) + + + + + +[QwikBundleGraph](#qwikbundlegraph) + + + +_(Optional)_ All bundles in a compact graph format with probabilities + + + + [bundles](#) @@ -1488,11 +1484,11 @@ Where QRLs are located -{ target?: string; buildMode?: string; entryStrategy?: { [key: string]: any; }; } +{ target?: string; buildMode?: string; entryStrategy?: { type: [EntryStrategy](#entrystrategy)['type']; }; } -_(Optional)_ +_(Optional)_ The options used to build the manifest @@ -1507,7 +1503,22 @@ _(Optional)_ -_(Optional)_ +_(Optional)_ The platform used to build the manifest + + + + +[preloader?](#) + + + + + +string + + + +_(Optional)_ The preloader bundle fileName @@ -1537,6 +1548,8 @@ string +The version of the manifest + @@ -1895,7 +1908,7 @@ boolean -'function' \| 'event' +'function' \| 'eventHandler' @@ -2158,65 +2171,65 @@ Description -[getInsightsManifest](#) +[getManifest](#) -(clientOutDir?: string \| null) => Promise<[InsightManifest](#insightmanifest) \| null> +() => [QwikManifest](#qwikmanifest) \| null -[getManifest](#) +[getOptimizer](#) -() => [QwikManifest](#qwikmanifest) \| null +() => [Optimizer](#optimizer) \| null -[getOptimizer](#) +[getOptions](#) -() => [Optimizer](#optimizer) \| null +() => NormalizedQwikPluginOptions -[getOptions](#) +[getRootDir](#) -() => NormalizedQwikPluginOptions +() => string \| null -[getRootDir](#) +[registerBundleGraphAdder](#) -() => string \| null +(adder: [BundleGraphAdder](#bundlegraphadder)) => void @@ -2341,6 +2354,19 @@ Description +[injections](#) + + + + + +[GlobalInjections](#globalinjections)[] + + + + + + [manifest](#) @@ -2426,7 +2452,7 @@ boolean -'event' \| 'function' +'eventHandler' \| 'function' diff --git a/packages/docs/src/routes/api/qwik-server/api.json b/packages/docs/src/routes/api/qwik-server/api.json index 0d267468458..e8e8f2a706c 100644 --- a/packages/docs/src/routes/api/qwik-server/api.json +++ b/packages/docs/src/routes/api/qwik-server/api.json @@ -82,7 +82,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface PrefetchImplementation \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[linkFetchPriority?](#)\n\n\n\n\n\n\n\n'auto' \\| 'low' \\| 'high' \\| null\n\n\n\n\n_(Optional)_ Value of the `` attribute when link is used. Defaults to `null` if links are inserted.\n\n\n
\n\n[linkInsert?](#)\n\n\n\n\n\n\n\n'js-append' \\| 'html-append' \\| null\n\n\n\n\n_(Optional)_ `js-append`: Use JS runtime to create each `` and append to the body.\n\n`html-append`: Render each `` within html, appended at the end of the body.\n\n\n
\n\n[linkRel?](#)\n\n\n\n\n\n\n\n'prefetch' \\| 'preload' \\| 'modulepreload' \\| null\n\n\n\n\n_(Optional)_ Value of the `` attribute when link is used. Defaults to `prefetch` if links are inserted.\n\n\n
\n\n[prefetchEvent?](#)\n\n\n\n\n\n\n\n'always' \\| null\n\n\n\n\n_(Optional)_ Dispatch a `qprefetch` event with detail data containing the bundles that should be prefetched. The event dispatch script will be inlined into the document's HTML so any listeners of this event should already be ready to handle the event.\n\nThis implementation will inject a script similar to:\n\n```\n\n```\nBy default, the `prefetchEvent` implementation will be set to `always`.\n\n\n
\n\n[workerFetchInsert?](#)\n\n\n\n\n\n\n\n'always' \\| 'no-link-support' \\| null\n\n\n\n\n_(Optional)_ `always`: Always include the worker fetch JS runtime.\n\n`no-link-support`: Only include the worker fetch JS runtime when the browser doesn't support `` prefetch/preload/modulepreload.\n\n\n
", + "content": "```typescript\nexport interface PrefetchImplementation \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[debug?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ If true, the preloader will log debug information to the console.\n\nDefaults to `false`\n\n\n
\n\n[linkFetchPriority?](#)\n\n\n\n\n\n\n\n'auto' \\| 'low' \\| 'high' \\| null\n\n\n\n\n_(Optional)_ Value of the `` attribute when links are added. Defaults to `null`.\n\n\n
\n\n[linkInsert?](#)\n\n\n\n\n\n\n\n'js-append' \\| 'html-append' \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[linkRel?](#)\n\n\n\n\n\n\n\n'prefetch' \\| 'preload' \\| 'modulepreload' \\| null\n\n\n\n\n_(Optional)_ Value of the `` attribute when links are added. The preloader itself will autodetect which attribute to use based on the browser capabilities.\n\nDefaults to `modulepreload`.\n\n\n
\n\n[maxPreloads?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of preload links to add during SSR. These instruct the browser to preload likely bundles before the preloader script is active. This includes the 2 preloads used for the preloader script itself and the bundle information. Setting this to 0 will disable all preload links.\n\nDefaults to `5`\n\n\n
\n\n[maxSimultaneousPreloads?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of simultaneous preload links that the preloader will maintain.\n\nDefaults to `5`\n\n\n
\n\n[minPreloadProbability?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ The minimum probability for a bundle to be added to the preload queue.\n\nDefaults to `0.25` (25% probability)\n\n\n
\n\n[minProbability?](#)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ The minimum probability of a bundle to be added as a preload link during SSR.\n\nDefaults to `0.6` (60% probability)\n\n\n
\n\n[prefetchEvent?](#)\n\n\n\n\n\n\n\n'always' \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[workerFetchInsert?](#)\n\n\n\n\n\n\n\n'always' \\| 'no-link-support' \\| null\n\n\n\n\n_(Optional)_\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts", "mdFile": "qwik.prefetchimplementation.md" }, @@ -152,7 +152,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\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[base?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Specifies the root of the JS files of the client build. Setting a base, will cause the render of the `q:base` attribute in the `q:container` element.\n\n\n
\n\n[containerAttributes?](#)\n\n\n\n\n\n\n\nRecord<string, string>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[containerTagName?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ When set, the app is serialized into a fragment. And the returned html is not a complete document. Defaults to `html`\n\n\n
\n\n[locale?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Language to use when rendering the document.\n\n\n
\n\n[prefetchStrategy?](#)\n\n\n\n\n\n\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[qwikLoader?](#)\n\n\n\n\n\n\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`.\n\n\n
\n\n[qwikPrefetchServiceWorker?](#)\n\n\n\n\n\n\n\nQwikPrefetchServiceWorkerOptions\n\n\n\n\n_(Optional)_ Specifies if the Qwik Prefetch Service Worker script is added to the document or not.\n\nDefaults to `{ include: false }`. NOTE: This may be change in the future.\n\n\n
\n\n[serverData?](#)\n\n\n\n\n\n\n\nRecord<string, any>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[snapshot?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Defaults to `true`\n\n\n
", + "content": "```typescript\nexport interface RenderOptions extends SerializeDocumentOptions \n```\n**Extends:** [SerializeDocumentOptions](#serializedocumentoptions)\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[base?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Specifies the root of the JS files of the client build. Setting a base, will cause the render of the `q:base` attribute in the `q:container` element.\n\n\n
\n\n[containerAttributes?](#)\n\n\n\n\n\n\n\nRecord<string, string>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[containerTagName?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ When set, the app is serialized into a fragment. And the returned html is not a complete document. Defaults to `html`\n\n\n
\n\n[locale?](#)\n\n\n\n\n\n\n\nstring \\| ((options: [RenderOptions](#renderoptions)) => string)\n\n\n\n\n_(Optional)_ Language to use when rendering the document.\n\n\n
\n\n[prefetchStrategy?](#)\n\n\n\n\n\n\n\n[PrefetchStrategy](#prefetchstrategy) \\| null\n\n\n\n\n_(Optional)_\n\n\n
\n\n[qwikLoader?](#)\n\n\n\n\n\n\n\n[QwikLoaderOptions](#qwikloaderoptions)\n\n\n\n\n_(Optional)_ Specifies if the Qwik Loader script is added to the document or not.\n\nDefaults to `{ include: true }`.\n\n\n
\n\n[qwikPrefetchServiceWorker?](#)\n\n\n\n\n\n\n\nQwikPrefetchServiceWorkerOptions\n\n\n\n\n_(Optional)_\n\n\n
\n\n[serverData?](#)\n\n\n\n\n\n\n\nRecord<string, any>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[snapshot?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Defaults to `true`\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts", "mdFile": "qwik.renderoptions.md" }, @@ -292,7 +292,7 @@ } ], "kind": "Function", - "content": "```typescript\nexport declare function resolveManifest(manifest: QwikManifest | ResolvedManifest | undefined): ResolvedManifest | undefined;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nmanifest\n\n\n\n\nQwikManifest \\| ResolvedManifest \\| undefined\n\n\n\n\n\n
\n**Returns:**\n\nResolvedManifest \\| undefined", + "content": "Merges a given manifest with the built manifest and provides mappings for symbols.\n\n\n```typescript\nexport declare function resolveManifest(manifest?: Partial | undefined): ResolvedManifest | undefined;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nmanifest\n\n\n\n\nPartial<QwikManifest \\| ResolvedManifest> \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nResolvedManifest \\| undefined", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/render.ts", "mdFile": "qwik.resolvemanifest.md" }, @@ -306,7 +306,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface SerializeDocumentOptions \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[debug?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_\n\n\n
\n\n[manifest?](#)\n\n\n\n\n\n\n\nQwikManifest \\| ResolvedManifest\n\n\n\n\n_(Optional)_\n\n\n
\n\n[symbolMapper?](#)\n\n\n\n\n\n\n\nSymbolMapperFn\n\n\n\n\n_(Optional)_\n\n\n
", + "content": "```typescript\nexport interface SerializeDocumentOptions \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[debug?](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_\n\n\n
\n\n[manifest?](#)\n\n\n\n\n\n\n\nPartial<QwikManifest \\| ResolvedManifest>\n\n\n\n\n_(Optional)_\n\n\n
\n\n[symbolMapper?](#)\n\n\n\n\n\n\n\nSymbolMapperFn\n\n\n\n\n_(Optional)_\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts", "mdFile": "qwik.serializedocumentoptions.md" }, @@ -320,7 +320,7 @@ } ], "kind": "Function", - "content": "```typescript\nexport declare function setServerPlatform(manifest: QwikManifest | ResolvedManifest | undefined): Promise;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nmanifest\n\n\n\n\nQwikManifest \\| ResolvedManifest \\| undefined\n\n\n\n\n\n
\n**Returns:**\n\nPromise<void>", + "content": "```typescript\nexport declare function setServerPlatform(manifest?: Partial): Promise;\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nmanifest\n\n\n\n\nPartial<QwikManifest \\| ResolvedManifest>\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\nPromise<void>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/index.ts", "mdFile": "qwik.setserverplatform.md" }, diff --git a/packages/docs/src/routes/api/qwik-server/index.md b/packages/docs/src/routes/api/qwik-server/index.md index 819658c38ec..fe5279e0a25 100644 --- a/packages/docs/src/routes/api/qwik-server/index.md +++ b/packages/docs/src/routes/api/qwik-server/index.md @@ -233,6 +233,23 @@ Description +[debug?](#) + + + + + +boolean + + + +_(Optional)_ If true, the preloader will log debug information to the console. + +Defaults to `false` + + + + [linkFetchPriority?](#) @@ -243,7 +260,7 @@ Description -_(Optional)_ Value of the `` attribute when link is used. Defaults to `null` if links are inserted. +_(Optional)_ Value of the `` attribute when links are added. Defaults to `null`. @@ -258,9 +275,7 @@ _(Optional)_ Value of the `` attribute when link is us -_(Optional)_ `js-append`: Use JS runtime to create each `` and append to the body. - -`html-append`: Render each `` within html, appended at the end of the body. +_(Optional)_ @@ -275,32 +290,92 @@ _(Optional)_ `js-append`: Use JS runtime to create each `` and append to t -_(Optional)_ Value of the `` attribute when link is used. Defaults to `prefetch` if links are inserted. +_(Optional)_ Value of the `` attribute when links are added. The preloader itself will autodetect which attribute to use based on the browser capabilities. + +Defaults to `modulepreload`. -[prefetchEvent?](#) +[maxPreloads?](#) -'always' \| null +number -_(Optional)_ Dispatch a `qprefetch` event with detail data containing the bundles that should be prefetched. The event dispatch script will be inlined into the document's HTML so any listeners of this event should already be ready to handle the event. +_(Optional)_ Maximum number of preload links to add during SSR. These instruct the browser to preload likely bundles before the preloader script is active. This includes the 2 preloads used for the preloader script itself and the bundle information. Setting this to 0 will disable all preload links. -This implementation will inject a script similar to: +Defaults to `5` -``` - -``` + + + +[maxSimultaneousPreloads?](#) + + + + + +number + + + +_(Optional)_ Maximum number of simultaneous preload links that the preloader will maintain. -By default, the `prefetchEvent` implementation will be set to `always`. +Defaults to `5` + + + + +[minPreloadProbability?](#) + + + + + +number + + + +_(Optional)_ The minimum probability for a bundle to be added to the preload queue. + +Defaults to `0.25` (25% probability) + + + + +[minProbability?](#) + + + + + +number + + + +_(Optional)_ The minimum probability of a bundle to be added as a preload link during SSR. + +Defaults to `0.6` (60% probability) + + + + +[prefetchEvent?](#) + + + + + +'always' \| null + + + +_(Optional)_ @@ -315,9 +390,7 @@ By default, the `prefetchEvent` implementation will be set to `always`. -_(Optional)_ `always`: Always include the worker fetch JS runtime. - -`no-link-support`: Only include the worker fetch JS runtime when the browser doesn't support `` prefetch/preload/modulepreload. +_(Optional)_ @@ -630,9 +703,7 @@ QwikPrefetchServiceWorkerOptions -_(Optional)_ Specifies if the Qwik Prefetch Service Worker script is added to the document or not. - -Defaults to `{ include: false }`. NOTE: This may be change in the future. +_(Optional)_ @@ -990,9 +1061,11 @@ string ## resolveManifest +Merges a given manifest with the built manifest and provides mappings for symbols. + ```typescript export declare function resolveManifest( - manifest: QwikManifest | ResolvedManifest | undefined, + manifest?: Partial | undefined, ): ResolvedManifest | undefined; ``` @@ -1015,10 +1088,12 @@ manifest -QwikManifest \| ResolvedManifest \| undefined +Partial<QwikManifest \| ResolvedManifest> \| undefined +_(Optional)_ + **Returns:** @@ -1073,7 +1148,7 @@ _(Optional)_ -QwikManifest \| ResolvedManifest +Partial<QwikManifest \| ResolvedManifest> @@ -1103,7 +1178,7 @@ _(Optional)_ ```typescript export declare function setServerPlatform( - manifest: QwikManifest | ResolvedManifest | undefined, + manifest?: Partial, ): Promise; ``` @@ -1126,10 +1201,12 @@ manifest -QwikManifest \| ResolvedManifest \| undefined +Partial<QwikManifest \| ResolvedManifest> +_(Optional)_ + **Returns:** diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index ef3a05df838..d03d9d6e066 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -1760,7 +1760,7 @@ } ], "kind": "Function", - "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\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n nonce?: string;\n}) => JSXOutput\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; manifestHash?: string; manifestURL?: string; nonce?: string; }\n\n\n\n\n_(Optional)_ Options for the loading prefetch graph.\n\n- `base` - Base of the graph. For a default installation this will default to the q:base value `/build/`. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name.\n\n\n
\n**Returns:**\n\n[JSXOutput](#jsxoutput)", + "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\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class. You can remove this component from your app.\n> \n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n nonce?: string;\n}) => JSXOutput\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; manifestHash?: string; manifestURL?: string; nonce?: string; }\n\n\n\n\n_(Optional)_\n\n\n
\n**Returns:**\n\n[JSXOutput](#jsxoutput)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchgraph.md" }, @@ -1774,7 +1774,7 @@ } ], "kind": "Function", - "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\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\nOptions for the prefetch service worker.\n\n- `base` - Base URL for the service worker `import.meta.env.BASE_URL` or `/`. Default is `import.meta.env.BASE_URL` - `scope` - Base URL for when the service-worker will activate. Default is `/` - `path` - Path to the service worker. Default is `qwik-prefetch-service-worker.js` unless you pass a path that starts with a `/` then the base is ignored. Default is `qwik-prefetch-service-worker.js` - `verbose` - Verbose logging for the service worker installation. Default is `false` - `nonce` - Optional nonce value for security purposes, defaults to `undefined`.\n\n\n
\n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", + "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\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n**Returns:**\n\nJSXNode<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, @@ -3473,4 +3473,4 @@ "mdFile": "qwik.webviewhtmlattributes.md" } ] -} +} \ No newline at end of file diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index 88380b14447..ce41430014f 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -3559,9 +3559,9 @@ export interface ParamHTMLAttributes extends Attrs<'base', T, > 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. -Load the prefetch graph for the container. - -Each Qwik container needs to include its own prefetch graph. +> Warning: This API is now obsolete. +> +> This is no longer needed as the preloading happens automatically in qrl-class. You can remove this component from your app. ```typescript PrefetchGraph: (opts?: { @@ -3595,9 +3595,7 @@ opts -_(Optional)_ Options for the loading prefetch graph. - -- `base` - Base of the graph. For a default installation this will default to the q:base value `/build/`. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name. +_(Optional)_ @@ -3611,9 +3609,9 @@ _(Optional)_ Options for the loading prefetch graph. > 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. -Install a service worker which will prefetch the bundles. - -There can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component. +> Warning: This API is now obsolete. +> +> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects. ```typescript PrefetchServiceWorker: (opts: { @@ -3649,15 +3647,11 @@ opts -Options for the prefetch service worker. - -- `base` - Base URL for the service worker `import.meta.env.BASE_URL` or `/`. Default is `import.meta.env.BASE_URL` - `scope` - Base URL for when the service-worker will activate. Default is `/` - `path` - Path to the service worker. Default is `qwik-prefetch-service-worker.js` unless you pass a path that starts with a `/` then the base is ignored. Default is `qwik-prefetch-service-worker.js` - `verbose` - Verbose logging for the service worker installation. Default is `false` - `nonce` - Optional nonce value for security purposes, defaults to `undefined`. - **Returns:** -[JSXNode](#jsxnode)<'script'> +JSXNode<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) diff --git a/packages/docs/src/routes/docs/(qwik)/advanced/modules-prefetching/index.mdx b/packages/docs/src/routes/docs/(qwik)/advanced/modules-prefetching/index.mdx index 5397a75392b..48e649e09e0 100644 --- a/packages/docs/src/routes/docs/(qwik)/advanced/modules-prefetching/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/advanced/modules-prefetching/index.mdx @@ -18,11 +18,7 @@ created_at: '2023-03-20T23:45:13Z' # Prefetching Modules -Qwik provides various strategies to prefetch modules ahead of time. This page describes the **low-level** features of Qwik's prefetching. - -- [Pre-populate the Cache with service workers](../../../(qwikcity)/advanced/speculative-module-fetching/index.mdx) -- [Link rel](#link-rel) -- [Web Worker Fetch](#web-worker-fetch) +Qwik prefetches modules ahead of time. This page describes the **low-level** features of Qwik's prefetching. Prefetching modules allows applications to start downloading necessary code in the background before users actually need it. The ideal solution is to prefetch only the smallest amount of relevant code that is highly likely to be executed from a user's interaction, while also avoiding any JavaScript that _will not_ be used. @@ -38,87 +34,26 @@ For example, consider a product page that is mostly static except for one "Add t For our "Add to cart" example, the optimizer collects the symbols only for the click event listener and the renderer for the add to cart widget. There is no need to download, hydrate, and re-render any other parts of the application that aren't relevant. This demonstrates Qwik's capability to determine which interactions are possible and to prefetch only the necessary code for the event listener. In contrast, traditional approaches require the entire application or route, including framework code, to be prefetched just to add the click event listener. -## Prefetch Strategy - -The prefetching strategy is the logic that decides which JavaScript, if any, Qwik should prefetch in the background. By default, Qwik will prefetch any visible listeners on the page. To configure the prefetching strategy, use the options argument of the `renderToStream()` function, often found in the `src/entry.ssr.tsx` source file. Providing optimal prefetching strategies is a continual commitment of Qwik. - -```ts -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - manifest, - prefetchStrategy: { - // custom prefetching config - }, - ...opts, - }); -} -``` - -### Implementation - -Browsers offer numerous ways to implement a [prefetching strategy](#prefetchStrategy). Qwik can be configured to prefer one implementation over another, each with pros and cons. Depending on this configuration, the generated HTML content will include the chosen prefetch implementation. - -```ts -export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - manifest, - prefetchStrategy: { - implementation: { - // custom prefetching implementation - }, - }, - ...opts, - }); -} -``` - +## Preloader -| Option | Description | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `prefetchEvent` | Dispatch a `qprefetch` event with `detail` data containing the urls that should be prefetched. The event dispatch script will be inlined into the document's HTML. By default, the `prefetchEvent` implementation will be set to `always`. | -| `linkInsert` | Insert the `` element into the document. When using `html-append`, it will render each `` directly within the html, appended at the end of the body. Using the `js-append` option, it will instead insert some JavaScript, which creates the elements at runtime and appends them at the end of the body. | -| `linkRel` | This option is used to define the [`rel` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types) of the `` element. When the `linkInsert` option is used, the default is `prefetch`. Other options include `preload` and `modulepreload`. | -| `linkFetchPriority` | This option is used to define the [`fetchpriority` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#fetchpriority) of the `` element. When the `linkInsert` option is used, the default is `null`. Other options include `low`, `high` and `auto`. -| `workerFetchInsert` | Prefetch urls by calling a `fetch()` for each module, with the goal of populating the network cache. | +The preloader is the part of Qwik that decides which JavaScript, if any, Qwik should prefetch in the background. By default, Qwik will prefetch any event handlers on the page, as well as their dependencies. -#### Dispatched Prefetch Event +This is done probabilistically based on various metrics, including the real-use metrics collected from [Insights](/docs/labs/insights/) -[Speculative Module Fetching](../../../(qwikcity)/advanced/speculative-module-fetching/index.mdx) is the preferred caching strategy. This strategy listens for the `qprefetch` event, which is dispatched by the Qwik framework. The event contains a list of URLs that the background thread should use to pre-populate the browser's [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache). +To give an example, suppose we have two event handlers, one an `onClick` handler and the other a `beforePrint` handler. The click handler is much more likely to be used than the print handler, so Qwik will prefetch the click handler with a higher probability. Furthermore if both handlers are importing the Qwik core bundle, that core bundle is more important than either of the event handlers, and will be prefetched first. -Qwik should be configured to use the `prefetchEvent` implementation, which will dispatch a `qprefetch` event. By default, the `prefetchEvent` implementation will be set to `always`. Next, [Speculative Module Fetching](../../../(qwikcity)/advanced/speculative-module-fetching/index.mdx) will listen for this event and communicate with its service worker to persist the Request / Response object pairs so they are cached in long-lived memory. +During execution, new segments of code are imported and they each impact the probability of the segments to be loaded. -By using a service worker to intercept `fetch` requests from the browser, this approach allows granular control over caching, along with preventing duplicate requests for the same resource. - -Below is an example of manually dispatching the event. These events are dispatched from Qwik itself and do not require developers to dispatch these events manually. Additionally, the [service worker](../../../(qwikcity)/advanced/speculative-module-fetching/index.mdx) will automatically add listeners for these events. - -```ts -dispatchEvent(new CustomEvent("qprefetch", { detail: { - bundles: [...] -}})); -``` - -#### Link `rel` - -Using the `` element with the `rel` attribute is a common approach by today's frameworks, and Qwik can use this method by configuring the `linkInsert` and `linkRel` options. The link rel approach, although effective, currently faces a lack of support on all devices, at least at the time of writing. Additionally, during development, it can be misleading to assume that it works everywhere, since prefetching on mobile devices is not easily visible. - -For example, Safari does not support `modulepreload`. This is significant because mobile devices may benefit the most from module preloading. Similarly Firefox does not support link rel `prefetch` when on `https`. - -> Prefetch is a feature designed to enhance the speed of our visitors' experiences. However, the effectiveness can vary depending on the combination of browser and CDN/server used, highlighting the importance of an optimized setup to ensure the best performance. -> -> \- Rel=prefetch and the Importance of Effective HTTP/2 Prioritisation - -Additionally, it's possible for multiple requests for the same resource. For example, let's say we want to prefetch `module-a.js`, and while it's downloading regardless of how long it takes, the user interacts with the app. The app then decides to actually request and execute `module-a.js`. At the time of this writing, browsers will often fire off a second request making matters worse. +### Implementation -##### link rel="modulepreload" +The prefetching is done by adding `` elements to the document. SSR will determine the most likely bundles and add some `` elements to the HTML. -- Even though it's in the HTML spec, that doesn't mean your end-users are preloading your app correctly. [Can I Use: modulepreload](https://caniuse.com/link-rel-modulepreload) -- Not supported by [Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1425310). +Then, a script is addded that loads the preloader as well as the module graph, and it is passed all the bundles discovered during the SSR render. -#### Web Worker Fetch +The preloader will then add the remaining `` elements to the document head, keeping only a few active at a time so that high priority bundles can be added at any time. -`workerFetchInsert` instructs Qwik to employ a web worker to `fetch()` a JavaScript file, with the goal of priming the browser cache with the module. By using a web worker, the fetch and caching logic lives on another thread. The fetch response will also have an `immutable` or long cache-control header, so the browser doesn't make a second network request. +Most browsers support `rel="modulepreload"`, but for older browsers, Qwik will fall back to `rel="preload"` attribute. -The downside of this setting is that the fetched response is thrown away, and it's only at the browser level that hopefully the file is cached. ## Frequently Asked Prefetching Questions @@ -141,4 +76,4 @@ No, for several reasons: **QUESTION**: _Who is responsible for knowing what code to prefetch?_ -Qwik can automatically generate the prefetch instructions as part of the SSR rendering. By executing the application, Qwik has runtime knowledge of which components are visible, which events the users can trigger and what code will need to be downloaded. The result is that the prefetch is an ideal set of files for this page. No action on the developers' part is required other than adding the prefetching strategy to `renderToStream()`. +Qwik automatically generates the prefetch instructions as part of the SSR rendering. By executing the application, Qwik has runtime knowledge of which components are visible, which events the users can trigger and what code will need to be downloaded. The result is that the prefetch is an ideal set of files for this page. No action on the developers' part is required. diff --git a/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx b/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx index 5235499e0b2..e6cda4ecf3a 100644 --- a/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx @@ -614,7 +614,7 @@ bun run preview NOTE: - Your application should now have a production build running on localhost:4173. -- If you interact with the application now, the network tab of the dev tools should show that the bundles are instantly delivered from the [ServiceWorker cache](/docs/advanced/speculative-module-fetching/). +- If you interact with the application now, the network tab of the dev tools should show that the bundles have been [preloaded](/docs/advanced/speculative-module-fetching/). ## Review @@ -628,4 +628,4 @@ For more on just how much you can achieve with Qwik, check out the dedicated doc - [Form actions](/docs/(qwikcity)/action/index.mdx) && [zod validation](/docs/(qwikcity)/action/index.mdx#validation-and-type-safety) - [State management](/docs/(qwik)/components/state/index.mdx) - [Tasks](/docs/(qwik)/components/tasks/index.mdx#use-usetask-when-you-need-to) -- [ServiceWorker cache](/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx#pre-populating-the-cache-with-a-service-worker) +- [Preloading](/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx) diff --git a/packages/docs/src/routes/docs/(qwikcity)/advanced/content-security-policy/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/advanced/content-security-policy/index.mdx index e8062d4ee94..7f61da2335b 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/advanced/content-security-policy/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/advanced/content-security-policy/index.mdx @@ -69,39 +69,6 @@ export const onRequest: RequestHandler = event => { }; ``` -### Add it to the service worker as well - -```tsx {12,22} /nonce/ title="src/root.ts" -import { component$, useServerData } from "@builder.io/qwik"; -import { - QwikCityProvider, - RouterOutlet, - ServiceWorkerRegister, -} from "@builder.io/qwik-city"; -import { RouterHead } from "./components/router-head/router-head"; -import { isDev } from "@builder.io/qwik"; - -import "./global.css"; - -export default component$(() => { - const nonce = useServerData("nonce"); - return ( - - - - {!isDev && } - - - - - - {!isDev && } - - - ); -}); -``` - ### Custom scripts If you have custom script tags that you need to add the nonce to, you can use the `useServerData` hook to get the nonce from the server and add it to your script tags. diff --git a/packages/docs/src/routes/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx index 7bd9ed28371..d55f9fb3999 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/advanced/speculative-module-fetching/index.mdx @@ -13,7 +13,8 @@ contributors: - wtlin1228 - aendel - jemsco -updated_at: '2023-06-25T19:43:33Z' + - wmertens +updated_at: '2025-03-25T12:00:00Z' created_at: '2023-03-20T23:45:13Z' --- @@ -23,94 +24,11 @@ Qwik is able to load a page and become interactive extremely fast due to its abi Qwik's goal is to optimize loading by caching only the necessary parts of the application based on potential user interactions. It avoids loading unnecessary bundles by understanding which interactions are not possible. -- [Pre-populating the Cache with a Service Worker](#pre-populating-the-cache-with-a-service-worker) -- [Caching Request and Response Pairs](#caching-request-and-response-pairs) -- [Parallelizing Network Requests](#parallelizing-network-requests) - ### Pre-populating the Cache -Each page load will pre-populate the cache with bundles that _could_ be executed on the page by the user at that moment. For example, let's say that the page has a click listener on a button. When the page loads, the service worker's first job is to ensure the code for that click listener is already in the [cache](#cache-api). When the user clicks the button, Qwik makes a request to the event listener's function and any code dependencies to execute that function. The goal is for the code to already be in the [browser's cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) ready to execute. - -The initial page load prepares the cache for the next probable interaction and also downloads other necessary code incrementally in a separate thread. When a follow-up interaction happens, such as opening a modal or menu, Qwik will emit another event with additional code that could be used since the last interaction. Pre-populating the cache happens continuously as users interact with the application. - -### Pre-populate Cache Event - -The recommended strategy is to use a [service worker](#pre-populating-the-cache-with-a-service-worker) to populate the [browser's cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache). The Qwik framework itself should use the [prefetchEvent](../../../(qwik)/advanced/modules-prefetching/index.mdx#implementation) implementation, which is already the default. - -## Pre-populating the Cache with a Service Worker - -Traditionally, a service worker is used to cache most or all of the bundles that an application uses. [Service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) are commonly seen only as a way to make an application work offline. - -Qwik City uses service workers quite differently to provide a powerful caching strategy. Instead of downloading the entire application, the goal is to use the service worker to dynamically pre-populate the cache with what's _possible_ to execute. By _not_ downloading the entire application, resources are freed up, enabling users to request only the necessary parts they _could_ use to complete their current task on the screen. - -Additionally, the service worker will automatically add listeners for these events emitted from Qwik. - -### Background Task - -An advantage of using a service worker is that it's also an extension of a [worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), which runs in a background thread. - -> Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application. The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down. - -By pre-populating the cache from within a service worker (which is a worker), we're able to essentially run the code in a background task, in order to not interfere with the main UI thread. By not interfering with the main thread, we can enhance the performance of the Qwik application for users. - -### Interactively Pre-populating the Cache - -Qwik itself should be configured to use the [prefetchEvent](../../../(qwik)/advanced/modules-prefetching/index.mdx#implementation) implementation. This is the default. When Qwik emits the event, the service worker registration actively forwards the event data to the installed and active service worker. - -Running in a background thread, the service worker then fetches the modules and adds them to the browser’s [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache). The main thread only needs to emit data about the required bundles, while the service worker’s sole focus is to cache those bundles. - -1. If the browser already has it cached? Great, do nothing! -2. If the browser hasn't already cached this bundle, then let's kick off the fetch request. - -> The service worker ensures that multiple requests for the same bundle [do not happen at the same time](#avoiding-duplicate-requests). - -## Caching Request and Response Pairs - -In many traditional frameworks, the preferred strategy is to use the html `` tag with a `rel` attribute set to `prefetch`, `preload` or `modulepreload`. However, due to [known issues](../../../(qwik)/advanced/modules-prefetching/index.mdx#link-rel) Qwik avoids using this approach as the default prefetching strategy, it can still be [configured](../../../(qwik)/advanced/modules-prefetching/index.mdx) if required. - -Instead, Qwik prefers to use a newer approach that takes full advantage of the browser's [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), which is better supported compared to [modulepreload](../../../(qwik)/advanced/modules-prefetching/index.mdx#link-rel). - -### Cache API - -The [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), often associated with service workers, is a way to store request and response pairs in order for an application to work offline. In addition to enabling applications to work without connectivity, the same Cache API provides an extremely powerful caching mechanism available to Qwik. - -Using the installed and activated [service worker](#pre-populating-the-cache-with-a-service-worker) to intercept requests, Qwik is able to handle specific requests for _known_ bundles. In contrast to the common way service workers are used, the default does not attempt to handle all requests, only known bundles generated by Qwik. The site's installed service worker can still be [customized by each site](#user-service-worker-code). - -An advantage of Qwik's optimizer is that it generates a `q-manifest.json` file. The `q-manifest.json` includes a detailed module graph of how bundles are associated and which symbols are within each bundle. This same module graph data is provided to the service worker allowing for every network request for known bundles to be handled by the cache. - -### Dynamic Imports and Caching - -When Qwik requests a module it uses a dynamic `import()`. For example, let's say a user interaction happened, requiring Qwik to execute a dynamic import for `/build/q-abc.js`. The code to do so would look something like this: - -```ts -const module = await import('/build/q-abc.js'); -``` - -What's important here is that Qwik itself has no knowledge of a prefetching or caching strategy. It's simply making a request for a URL. However, because we've installed a service worker, and the service worker is intercepting requests, it's able to inspect the URL and say, "look, this is a request for `/build/q-abc.js`! This is one of our bundles! Let's first check to see if we already have this in the cache before we do an actual network request." - -This is the power of the service worker and Cache API! In another thread, Qwik pre-populates the cache for modules the user may soon request. If these modules are already cached, then the browser doesn't need to do anything. +Each page load will pre-populate the cache with bundles that _could_ be executed on the page by the user at that moment. For example, let's say that the page has a click listener on a button. When the page loads, the Qwik's first job is to ensure the code for that click listener is already in the [browser's cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache). When the user clicks the button, Qwik makes a request to the event listener's function and any code dependencies to execute that function. This might include importing other modules. -## Parallelizing Network Requests - -In the [Caching Request and Response Pairs](#cache-api) docs we explained the powerful combination of the [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) and [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker) APIs. However, in Qwik, we can go further by ensuring that duplicate requests are not created for the same bundle and prevent network waterfalls, all from within the background thread. - -### Avoiding Duplicate Requests - -As an example, let's say an end-user currently has a very slow connection. When they first request the landing page, the device downloads the HTML and renders the content (an area where Qwik really shines). On this slow connection, it'd be a shame if users had to download several hundred more kilobytes just to [make their app work and become interactive](https://www.builder.io/blog/hydration-is-pure-overhead). - -However, because the app was built with Qwik, the end-user doesn't need to download the entire application for it to become interactive. Instead, the end-user already downloaded the SSR rendered HTML app, and any interactive parts, such as an "Add to cart" button, can be prefetched immediately. - -> Note that we're only prefetching the actual listener code, and _not_ the entire stack of the component tree render functions. - -In this extremely common real-world example of a device with a slow connection, the device immediately starts to pre-populate the cache for the possible interactions that are visible by the end-user. However, due to their slow connection, even though we started to cache the modules as soon as possible in a [background thread](#background-task), the fetch request itself could still be in flight. - -For demo purposes, let's say the fetching for this bundle takes two seconds. However, after one second of viewing the page, the user clicks on the button. - -In a traditional framework, there's a good chance that absolutely nothing would happen! The event listener can't be added to the button yet if the framework hasn't finished downloading, hydrating and re-rendering. This means that the user's interaction would just be lost. - -With Qwik's caching strategy, if a user clicks a button and we already initiated a request one second earlier, with only one second remaining until it's fully received, then the end-user only needs to wait for that one second. Remember, they're on a slow connection in this demo. Luckily the user already received the fully rendered landing page and are already looking at a completed page. Next, they're only pre-populating the cache with the parts of the app they could interact with, and their slow connection is dedicated to just those bundle(s). This is in contrast to their slow connection downloading all of the app, just to execute one listener. - -Qwik can intercept requests for known bundles. If a fetch is in flight in a background thread and a user requests the same bundle, it'll ensure that the second request is able to re-use the same bundle, which may already be done downloading. Trying to perform any of this with the [link](../../../(qwik)/advanced/modules-prefetching/index.mdx#link-rel) also shows why Qwik preferred to use the caching API and intercepts requests with a service worker as the default instead of using [link](../../../(qwik)/advanced/modules-prefetching/index.mdx#link-rel). +The initial page load prepares the cache for the next probable interaction and also downloads other necessary code incrementally. When a follow-up interaction happens, such as opening a modal or menu, Qwik will ask the browser to preload the code for that interaction. Pre-populating the cache happens continuously as users interact with the application. ### Reducing Network Waterfalls @@ -138,62 +56,23 @@ console.log('Module C'); In this example, when Module `A` is first requested, the browser has no idea that it should also start requesting Module `B` and `C`. It doesn't even know it needs to start requesting Module `B`, until AFTER Module `A` has finished downloading. It's a common problem in that the browser doesn't know ahead of time what it should start to request, until after each module has finished downloading. -However, because our service worker contains a module graph generated from the manifest, we do know all of the modules which _will_ be requested next. So when either user interaction or a prefetch for a bundle happens, the browser initiates the request for all of the bundles that _will_ be requested. This allows us to drastically reduce the time it takes to request all bundles. - -## User Service Worker Code - -The default service worker that is installed by Qwik City can still be controlled entirely by the application. For example, the source file `src/routes/service-worker.ts` becomes `/service-worker.js`, which is the script requested by the browser. Notice how its place within `src/routes` still follows the same directory-based routing pattern. - -Below is an example of a default `src/routes/service-worker.ts` source file: - -```ts -import { setupServiceWorker } from '@builder.io/qwik-city/service-worker'; - -setupServiceWorker(); - -addEventListener('install', () => self.skipWaiting()); - -addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim())); -``` - -The source code for `src/routes/service-worker.ts` can be modified which includes opting-in, or opting-out, of setting up Qwik City's service worker. - -Notice that the `setupServiceWorker()` function is imported from `@builder.io/qwik-city/service-worker` and executed at the top of the source file. Developers have the flexibility to modify when and where this function is called according to their needs. For example, if a developer prefers to handle fetch requests first, they can add their fetch listener above the `setupServiceWorker()`. Or if they didn't want to use Qwik City's service worker at all, they would just remove `setupServiceWorker()` from the file. - -Additionally, the default `src/routes/service-worker.ts` file comes with an [install](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/install_event) and [activate](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/activate_event) event listeners, each added at the bottom of the file. The callbacks provided are the recommended callbacks. However, the developer can modify these callbacks depending on their own app's requirements. - -Another important note is that Qwik City's request intercepting is _only_ for Qwik bundles, it does not attempt to handle any requests which are not a part of its build. - -So while Qwik City does provide a way to help prefetch and cache bundles, it does not take full control of the app's service worker. This still allows developers to add their service worker logic without conflicting with Qwik. - -## Disabled During Development - -Speculative module fetching only kicks in preview or on a production build. In development, the service worker is disabled which also disables speculative module fetching. This is because during development we want to always ensure the latest development code is being used, rather than what's been previously cached. - - -### HTTP Cache vs. Service Worker Cache - -Speculative module fetching may not appear to be working partly due to the various levels of caching. For example, the browser itself may cache requests in its [HTTP cache](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching), and the service worker may cache requests in the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache). Just emptying one of the caches may not be enough to see the effects of speculative module fetching. - -### Misleading Empty Cache and Hard Reload +However, Qwik knows the module graph and controls QRL segment loading, we do know all of the modules which _will_ be requested next. So when Qwik sees that it will require a module, it will tell the browser to preload it and all of its dependencies. -When developers run [Empty Cache and Hard Reload](https://developer.chrome.com/blog/hard-reload/), it's a bit misleading because it actually _only_ empties the browser's HTTP cache. It's not, however, emptying the service worker's cache. Even though the browser's HTTP cache is empty, the service worker still has the previous cached requests. +### Details -Additionally, when "Empty Cache and Hard Reload" is used, the browser sends a `no-cache` cache-control header in the _request_ to the server. Because the request has a `no-cache` cache-control header, the service worker purposely does not use its own cache, and instead the browser performs the usual HTTP fetch again. +The Qwik build process generates a `q-manifest.json` file. The `q-manifest.json` includes a detailed module graph of how bundles are associated and which symbols are within each bundle. -### Emptying the Service Worker Cache +This is then further used to generate the `build/q-bundle-graph-xyz.js` file, which is a compact representation of the module graph, including probabilities of which bundlers are more likely to be requested next given a certain bundle has loaded, and is loaded by Qwik to preload QRL segment loading. -The recommended way to test speculative module fetching is to: +Qwik-City also injects all routes with their dependencies into this bundlegraph file. -- **Unregister the service worker**: In Chrome DevTools, go to the Application tab, and under Service Workers, click the "Unregister" link for the for your site's service worker. -- **Delete the "QwikBuild" Cache Storage**: In Chrome DevTools, go to the Application tab, and under Cache Storage on the left side, right click "Delete" on the "QwikBuild" cache storage. -- **Do not hard reload**: Instead of hard reloading, which would send a no-cache cache-control to the service worker, just click the URL bar and hit enter. This will send a normal request as if you were a first time visitor. +When a container resumes, it will fetch the bundlegraph file (probably from the cache) and use it to prefetch modules needed for event handlers and routes. -Note that this process is only for testing the speculative module fetching, and is not required for new builds. Each build will create a new service worker, and the old service worker will be automatically unregistered. +Static imports are preloaded with high priority, and dynamic imports are preloaded with low priority. The browser will decide when to download the modules. -### Debug Mode +The preloading happens with the `_preload` function, which is an internal Qwik API. It works by traversing the bundlegraph and adding `` tags for each bundle that is likely to be requested next. -The service worker in Qwik core, which uses the `` and `` components in `root.tsx` has a debug mode. +## During Development -To see the service worker logs, add `window.qwikPrefetchSW.push(['verbose', '', []])` to the JavaScript console and press the `Enter` key. +Speculative module fetching only really works in preview or on a production build. In development, we don't have the bundle graph available, so we can't prevent waterfalls from happening. However, we do ask the browser to preload the bundles that are most likely to be requested after SSR. diff --git a/packages/docs/src/routes/docs/(qwikcity)/api/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/api/index.mdx index 9df224ecbe1..607d82bf937 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/api/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/api/index.mdx @@ -300,7 +300,6 @@ export default component$(() => { - ); diff --git a/packages/docs/src/routes/docs/(qwikcity)/project-structure/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/project-structure/index.mdx index 76f55547850..67b7f08ae1b 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/project-structure/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/project-structure/index.mdx @@ -39,8 +39,7 @@ qwik-app-demo │ │ ā”œā”€ā”€ flower.css │ │ └── index.tsx │ ā”œā”€ā”€ index.tsx -│ ā”œā”€ā”€ layout.tsx -│ └── service-worker.ts +│ └── layout.tsx ā”œā”€ā”€ tsconfig.json └── vite.config.ts ``` diff --git a/packages/docs/src/routes/docs/deployments/cloudflare-pages/index.mdx b/packages/docs/src/routes/docs/deployments/cloudflare-pages/index.mdx index 8fa40decc68..72ede71a878 100644 --- a/packages/docs/src/routes/docs/deployments/cloudflare-pages/index.mdx +++ b/packages/docs/src/routes/docs/deployments/cloudflare-pages/index.mdx @@ -205,10 +205,9 @@ import { type PlatformCloudflarePages, } from '@builder.io/qwik-city/middleware/cloudflare-pages'; import qwikCityPlan from '@qwik-city-plan'; -import { manifest } from '@qwik-client-manifest'; import render from './entry.ssr'; -const fetch = createQwikCity({ render, qwikCityPlan, manifest }); +const fetch = createQwikCity({ render, qwikCityPlan }); export { fetch }; ``` diff --git a/packages/docs/vite.config.mts b/packages/docs/vite.config.mts index 4d7f3cc2073..e0d7cbfd1ac 100644 --- a/packages/docs/vite.config.mts +++ b/packages/docs/vite.config.mts @@ -173,7 +173,7 @@ export default defineConfig(async () => { ], }, }), - qwikVite(), + qwikVite({ debug: false }), partytownVite({ dest: resolve('dist', '~partytown'), }), diff --git a/packages/insights/src/entry.ssr.tsx b/packages/insights/src/entry.ssr.tsx index 51151b935aa..99e531d3dd4 100644 --- a/packages/insights/src/entry.ssr.tsx +++ b/packages/insights/src/entry.ssr.tsx @@ -10,12 +10,10 @@ * - `npm run build` */ import { renderToStream, type RenderToStreamOptions } from '@builder.io/qwik/server'; -import { manifest } from '@qwik-client-manifest'; import Root from './root'; export default function (opts: RenderToStreamOptions) { return renderToStream(, { - manifest, ...opts, // Use container attributes to set attributes on the html tag. containerAttributes: { diff --git a/packages/insights/src/routes/service-worker.ts b/packages/insights/src/routes/service-worker.ts index fb1bb3fd75e..965b4dd60f4 100644 --- a/packages/insights/src/routes/service-worker.ts +++ b/packages/insights/src/routes/service-worker.ts @@ -7,12 +7,8 @@ * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline. * You can also use this file to add more functionality that runs in the service worker. */ -import { setupServiceWorker } from '@builder.io/qwik-city/service-worker'; - -setupServiceWorker(); +export declare const self: ServiceWorkerGlobalScope; addEventListener('install', () => self.skipWaiting()); addEventListener('activate', () => self.clients.claim()); - -declare const self: ServiceWorkerGlobalScope; diff --git a/packages/qwik-city/package.json b/packages/qwik-city/package.json index 105aa62e53b..a8626d470bc 100644 --- a/packages/qwik-city/package.json +++ b/packages/qwik-city/package.json @@ -155,7 +155,7 @@ "require": "./lib/vite/index.cjs" }, "./service-worker": { - "types": "./lib/service-worker.d.ts", + "types": "./service-worker.d.ts", "import": "./lib/service-worker.mjs", "require": "./lib/service-worker.cjs" } diff --git a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts index e4b5eb1364c..1f9209a43ce 100644 --- a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts +++ b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.ts @@ -1,6 +1,3 @@ -import type { InsightManifest, QwikManifest } from '@builder.io/qwik/optimizer'; -import type { AppBundle } from '../../runtime/src/service-worker/types'; -import { removeExtension } from '../../utils/fs'; import type { BuildContext } from '../types'; export function generateServiceWorkerRegister(ctx: BuildContext, swRegister: string) { @@ -25,187 +22,6 @@ export function generateServiceWorkerRegister(ctx: BuildContext, swRegister: str return `export default ${JSON.stringify(swReg)};`; } -export function prependManifestToServiceWorker( - ctx: BuildContext, - manifest: QwikManifest, - prefetch: InsightManifest['prefetch'] | null, - swCode: string -) { - const key = `/* Qwik Service Worker */`; - if (swCode.includes(key)) { - // both SSR and SSG could have ran this code, - // just check if we already prepended the bundles - return null; - } - - // TODO: add dynamic imports that don't import routes - // (anything that doesn't contain _hw export) - - const appBundles: AppBundle[] = []; - const appBundlesCode = generateAppBundles(appBundles, manifest); - const libraryBundlesCode = generateLibraryBundles(appBundles, manifest); - const [linkBundlesCode] = generateLinkBundles(ctx, appBundles, manifest, prefetch); - - return [key, appBundlesCode, libraryBundlesCode, linkBundlesCode, swCode].join('\n'); -} - -export function generateAppBundles(appBundles: AppBundle[], manifest: QwikManifest) { - const sortedBundles = Object.keys(manifest.bundles).sort(); - for (const appBundleName of sortedBundles) { - const appBundle: AppBundle = [appBundleName, []]; - appBundles.push(appBundle); - - const symbolHashesInBundle: string[] = []; - - const manifestBundle = manifest.bundles[appBundleName]; - const staticDepsNames = Array.isArray(manifestBundle.imports) ? manifestBundle.imports : []; - - const dynamicDepsNames = []; - for (const dynamicDepName of manifestBundle.dynamicImports || []) { - const dynamicDep = manifest.bundles[dynamicDepName]; - if (!manifest.bundles[dynamicDepName]) { - continue; - } - if (dynamicDep.hasSymbols) { - dynamicDepsNames.push(dynamicDepName); - } - } - const depsNames = [...staticDepsNames, ...dynamicDepsNames]; - const depsNamesSet = new Set(depsNames); - - for (const depName of depsNames) { - clearTransitiveDeps(depsNamesSet, new Set(), depName); - } - - // set the imports based on the sorted index number - appBundle[1] = Array.from(depsNamesSet).map((dep) => sortedBundles.indexOf(dep)); - - if (manifestBundle.symbols) { - for (const manifestBundleSymbolName of manifestBundle.symbols) { - const symbol = manifest.symbols[manifestBundleSymbolName]; - if (symbol?.hash && !symbolHashesInBundle.includes(symbol.hash)) { - symbolHashesInBundle.push(symbol.hash); - } - } - } - - if (symbolHashesInBundle.length > 0) { - (appBundle as unknown as any)[2] = symbolHashesInBundle; - } - } - - function clearTransitiveDeps(deps: Set, seen: Set, depName: string) { - const childBundle = manifest.bundles[depName]; - - for (const childDepImport of childBundle.imports || []) { - if (deps.has(childDepImport)) { - deps.delete(childDepImport); - } - if (!seen.has(childDepImport)) { - seen.add(childDepImport); - clearTransitiveDeps(deps, seen, childDepImport); - } - } - } - - return `const appBundles=${JSON.stringify(appBundles)};`; -} - -function generateLibraryBundles(appBundles: AppBundle[], manifest: QwikManifest) { - const libraryBundleIds: number[] = []; - - for (const [bundleName, bundle] of Object.entries(manifest.bundles)) { - if (bundle.origins && bundle.origins.includes('@qwik-city-plan')) { - libraryBundleIds.push(getAppBundleIndex(appBundles, bundleName)); - break; - } - } - - return `const libraryBundleIds=${JSON.stringify(libraryBundleIds)};`; -} - -export function generateLinkBundles( - ctx: BuildContext, - appBundles: AppBundle[], - manifest: QwikManifest, - prefetch: InsightManifest['prefetch'] | null -) { - const linkBundles: string[] = []; - const symbolToBundle = new Map(); - const routeToBundles: Record = {}; - for (const bundleName in manifest.bundles || []) { - manifest.bundles[bundleName].symbols?.forEach((symbol) => { - const idx = symbol.lastIndexOf('_'); - symbolToBundle.set(idx === -1 ? symbol : symbol.substring(idx + 1), bundleName); - }); - } - - for (const r of ctx.routes) { - const linkBundleNames: string[] = []; - - const addFileBundles = (filePath: string) => { - for (const [bundleName, bundle] of Object.entries(manifest.bundles)) { - if (bundle.origins) { - for (const bundleOrigin of bundle.origins) { - const srcPath = removeExtension(filePath); - const bundleOriginPath = removeExtension(bundleOrigin); - - if (srcPath.endsWith(bundleOriginPath)) { - if (!linkBundleNames.includes(bundleName)) { - linkBundleNames.push(bundleName); - } - - if (bundle.dynamicImports) { - for (const dynamicImport of bundle.dynamicImports) { - if (!linkBundleNames.includes(dynamicImport)) { - linkBundleNames.push(dynamicImport); - } - } - } - } - } - } - } - }; - - for (const layout of r.layouts) { - addFileBundles(layout.filePath); - } - addFileBundles(r.filePath); - - if (prefetch) { - // process the symbols from insights prefetch - const symbolsForRoute = prefetch.find((p) => p.route === r.routeName); - symbolsForRoute?.symbols?.reverse().forEach((symbol) => { - const bundle = symbolToBundle.get(symbol); - if (bundle) { - const idx = linkBundleNames.indexOf(bundle); - if (idx !== -1) { - linkBundleNames.splice(idx, 1); - } - linkBundleNames.unshift(bundle); - } - }); - } - - linkBundles.push( - `[${r.pattern.toString()},${JSON.stringify( - linkBundleNames.map((bundleName) => getAppBundleIndex(appBundles, bundleName)) - )}]` - ); - routeToBundles[r.routeName] = linkBundleNames; - } - - return [`const linkBundles=[${linkBundles.join(',')}];`, routeToBundles] as [ - string, - typeof routeToBundles, - ]; -} - -function getAppBundleIndex(appBundles: AppBundle[], bundleName: string) { - return appBundles.findIndex((b) => b[0] === bundleName); -} - const SW_UNREGISTER = ` navigator.serviceWorker?.getRegistrations().then((regs) => { for (const reg of regs) { diff --git a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.unit.ts b/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.unit.ts deleted file mode 100644 index 95d73da7a59..00000000000 --- a/packages/qwik-city/src/buildtime/runtime-generation/generate-service-worker.unit.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { - InsightManifest, - QwikBundle, - QwikManifest, - QwikSymbol, -} from '@builder.io/qwik/optimizer'; -import { assert, expect, test } from 'vitest'; -import type { AppBundle } from '../../runtime/src/service-worker/types'; -import type { BuildContext, BuildRoute } from '../types'; -import { generateAppBundles, generateLinkBundles } from './generate-service-worker'; - -test('incorporate qwik-insights', () => { - const routes: BuildRoute[] = [ - { - routeName: '/', - pattern: /\//, - filePath: './src/routes/index.tsx', - layouts: [], - } /* satisfies Partial */ as any, - { - routeName: '/routeA', - pattern: /\/routeA/, - filePath: './src/routes/routeA/index.tsx', - layouts: [], - } /* satisfies Partial */ as any, - ]; - const ctx: BuildContext = { routes } /* satisfies Partial */ as any; - const appBundles: AppBundle[] = [ - ['q-bundle-a.js', [12]], - ['q-bundle-b.js', [34]], - ['q-bundle-123.js', [123]], - ['q-bundle-234.js', [234]], - ['q-bundle-345.js', [345]], - ]; - const manifest: QwikManifest = { - bundles: { - 'q-bundle-a.js': { - origins: ['./src/routes/index.tsx'], - } /* satisfies Partial */ as any, - 'q-bundle-b.js': { - origins: ['./src/routes/routeA/index.tsx'], - } /* satisfies Partial */ as any, - 'q-bundle-123.js': { - symbols: ['s_123'], - } /* satisfies Partial */ as any, - 'q-bundle-234.js': { - symbols: ['s_234'], - } /* satisfies Partial */ as any, - 'q-bundle-345.js': { - symbols: ['s_345'], - } /* satisfies Partial */ as any, - }, - } /* satisfies Partial */ as any; - const prefetch: InsightManifest['prefetch'] = [ - { route: '/', symbols: ['123', '234'] }, - { route: '/routeA', symbols: ['345'] }, - ]; - const [_, routeToBundles] = generateLinkBundles(ctx, appBundles, manifest, prefetch); - assert.deepEqual(routeToBundles['/'], ['q-bundle-123.js', 'q-bundle-234.js', 'q-bundle-a.js']); - assert.deepEqual(routeToBundles['/routeA'], ['q-bundle-345.js', 'q-bundle-b.js']); -}); - -test('generateAppBundles', () => { - const fakeManifest = { - symbols: { - s_aaa123: { hash: 'aaa123' } as QwikSymbol, - s_bbb123: { hash: 'bbb123' } as QwikSymbol, - s_ccc123: { hash: 'ccc123' } as QwikSymbol, - s_ddd123: { hash: 'ddd123' } as QwikSymbol, - s_eee123: { hash: 'eee123' } as QwikSymbol, - } as Record, - bundles: { - 'a.js': { - size: 0, - imports: ['b.js', 'c.js'], - symbols: ['s_aaa123'], - }, - 'b.js': { - size: 0, - imports: ['c.js', 'd.js'], - dynamicImports: ['e.js'], - symbols: ['s_bbb123'], - }, - 'c.js': { - size: 0, - imports: ['d.js'], - symbols: ['s_ccc123'], - }, - 'd.js': { - size: 0, - imports: [], - symbols: ['s_ddd123'], - }, - 'e.js': { - size: 0, - imports: [], - symbols: ['s_eee123'], - }, - } as Record, - } as QwikManifest; - - const actualAppBundles = generateAppBundles([], fakeManifest); - - const expectedAppBundles = [ - ['a.js', [1], ['aaa123']], - ['b.js', [2], ['bbb123']], - ['c.js', [3], ['ccc123']], - ['d.js', [], ['ddd123']], - ['e.js', [], ['eee123']], - ]; - const expectedResult = `const appBundles=${JSON.stringify(expectedAppBundles)};`; - - expect(actualAppBundles).toEqual(expectedResult); -}); diff --git a/packages/qwik-city/src/buildtime/vite/dev-server.ts b/packages/qwik-city/src/buildtime/vite/dev-server.ts index eeada990f5c..310d60ede56 100644 --- a/packages/qwik-city/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/src/buildtime/vite/dev-server.ts @@ -1,4 +1,4 @@ -import type { QwikManifest, QwikViteDevResponse } from '@builder.io/qwik/optimizer'; +import type { QwikViteDevResponse } from '@builder.io/qwik/optimizer'; import fs from 'node:fs'; import type { ServerResponse } from 'node:http'; import { join, resolve } from 'node:path'; @@ -10,8 +10,8 @@ import { } from '../../middleware/request-handler/resolve-request-handlers'; import { getQwikCityServerData } from '../../middleware/request-handler/response-page'; import { - getRouteMatchPathname, QDATA_JSON, + getRouteMatchPathname, runQwikCity, } from '../../middleware/request-handler/user-response'; import { matchRoute } from '../../runtime/src/route-matcher'; @@ -223,15 +223,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const serverRequestEv = await fromNodeHttp(url, req, res, 'dev'); Object.assign(serverRequestEv.platform, ctx.opts.platform); - const manifest: QwikManifest = { - manifestHash: '', - symbols: {}, - mapping: {}, - bundles: {}, - injections: [], - version: '1', - }; - const { _deserializeData, _serializeData, _verifySerializable } = await server.ssrLoadModule('@qwik-serializer'); const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable }; @@ -240,7 +231,6 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { serverRequestEv, loadedRoute, requestHandlers, - manifest, ctx.opts.trailingSlash, ctx.opts.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/buildtime/vite/get-route-imports.ts b/packages/qwik-city/src/buildtime/vite/get-route-imports.ts new file mode 100644 index 00000000000..146915bb8ad --- /dev/null +++ b/packages/qwik-city/src/buildtime/vite/get-route-imports.ts @@ -0,0 +1,46 @@ +import type { QwikBundle, QwikManifest } from '@builder.io/qwik/optimizer'; +import { removeExtension } from '../../utils/fs'; +import type { BuildRoute } from '../types'; +import { QWIK_CITY_PLAN_ID } from './plugin'; + +export function getRouteImports(routes: BuildRoute[], manifest: QwikManifest) { + const result: Record = {}; + routes.forEach((route) => { + const routePath = removeExtension(route.filePath); + const layoutPaths = route.layouts + ? route.layouts.map((layout) => removeExtension(layout.filePath)) + : []; + const routeAndLayoutPaths = [routePath, ...layoutPaths]; + + const bundles = []; + + for (const [bundleName, bundle] of Object.entries(manifest.bundles)) { + if (isBundlePartOfRoute(bundle, routeAndLayoutPaths)) { + bundles.push(bundleName); + } + } + if (bundles.length > 0) { + result[route.routeName] = { dynamicImports: bundles }; + } + }); + for (const bundleName of Object.keys(manifest.bundles)) { + const bundle = manifest.bundles[bundleName]; + if (bundle.origins?.some((s) => s.endsWith(QWIK_CITY_PLAN_ID))) { + // Don't consider the city plan for preloading + // we keep imports because something might be bundled with it + result[bundleName] = { imports: bundle.imports, dynamicImports: [] }; + break; + } + } + return result; +} + +function isBundlePartOfRoute(bundle: QwikBundle, routeAndLayoutPaths: string[]) { + if (!bundle.origins) { + return false; + } + for (const bundleOrigin of bundle.origins) { + const originPath = removeExtension(bundleOrigin); + return routeAndLayoutPaths.some((path) => path.endsWith(originPath)); + } +} diff --git a/packages/qwik-city/src/buildtime/vite/get-route-imports.unit.ts b/packages/qwik-city/src/buildtime/vite/get-route-imports.unit.ts new file mode 100644 index 00000000000..4f87f94ded9 --- /dev/null +++ b/packages/qwik-city/src/buildtime/vite/get-route-imports.unit.ts @@ -0,0 +1,106 @@ +import { type QwikBundle, type QwikManifest } from '@builder.io/qwik/optimizer'; +import { describe, expect, test } from 'vitest'; +import type { BuildLayout, BuildRoute } from '../types'; +import { getRouteImports } from './get-route-imports'; + +describe('modifyBundleGraph', () => { + test(`GIVEN 2 routes, one with a layout + AND a manifest with 3 bundles + THEN the bundle graph should contain the routes and their dependencies`, () => { + const size = 0; + const total = 0; + const fakeManifest = { + bundles: { + 'fake-bundle1.js': { + size, + total, + imports: ['fake-bundle-static-dep.js'], + origins: ['src/routes/index.tsx'], + }, + 'fake-bundle-static-dep.js': { + size, + total, + dynamicImports: ['fake-bundle-dynamic-dep.js'], + }, + 'fake-bundle-dynamic-dep.js': { size, total }, + 'fake-bundle-part-of-sub-route.js': { + size, + total, + origins: ['src/routes/subroute/index.tsx', 'src/some/other/component.tsx'], + }, + 'fake-bundle-part-of-layout.js': { size, total, origins: ['src/routes/layout.tsx'] }, + 'q-city-plan.js': { size, total, origins: ['@qwik-city-plan'] }, + } as Record, + } as QwikManifest; + + const fakeRoutes: BuildRoute[] = [ + { + routeName: '/', + filePath: '/home/qwik-app/src/routes/index.tsx', + }, + { + routeName: '/subroute', + filePath: '/home/qwik-app/src/routes/subroute/index.tsx', + layouts: [ + { + filePath: '/home/qwik-app/src/routes/layout.tsx', + }, + ] as BuildLayout[], + }, + ] as BuildRoute[]; + + const actualResult = getRouteImports(fakeRoutes, fakeManifest); + expect(actualResult).toMatchInlineSnapshot(` + { + "/": { + "dynamicImports": [ + "fake-bundle1.js", + ], + }, + "/subroute": { + "dynamicImports": [ + "fake-bundle-part-of-sub-route.js", + "fake-bundle-part-of-layout.js", + ], + }, + "q-city-plan.js": { + "dynamicImports": [], + "imports": undefined, + }, + } + `); + }); + + test(`GIVEN a mismatch between the bundle graph and the manifest + THEN the resulted bundle graph routes should not contain -1 (not found) indices `, () => { + const size = 0; + const total = 0; + const fakeManifest = { + bundles: { + 'fake-bundle1.js': { size, total, origins: ['src/routes/index.tsx'] }, + // šŸ‘‡ doesn't exist in the bundle graph for some reason + 'fake-bundle2.js': { size, total, origins: ['src/routes/index.tsx'] }, + } as Record, + } as QwikManifest; + + const fakeRoutes: BuildRoute[] = [ + { + routeName: '/', + filePath: '/home/qwik-app/src/routes/index.tsx', + }, + ] as BuildRoute[]; + + const actualResult = getRouteImports(fakeRoutes, fakeManifest); + + expect(actualResult).toMatchInlineSnapshot(` + { + "/": { + "dynamicImports": [ + "fake-bundle1.js", + "fake-bundle2.js", + ], + }, + } + `); + }); +}); diff --git a/packages/qwik-city/src/buildtime/vite/image-jsx.ts b/packages/qwik-city/src/buildtime/vite/image-jsx.ts index 9b708c22f18..cf2084b69d3 100644 --- a/packages/qwik-city/src/buildtime/vite/image-jsx.ts +++ b/packages/qwik-city/src/buildtime/vite/image-jsx.ts @@ -3,7 +3,7 @@ import type { PluginOption } from 'vite'; import { optimize } from 'svgo'; import fs from 'node:fs'; import path from 'node:path'; -import { parseId } from '../../../../qwik/src/optimizer/src/plugins/plugin'; +import { parseId } from '../../../../qwik/src/optimizer/src/plugins/vite-utils'; import type { QwikCityVitePluginOptions } from './types'; import type { Config as SVGOConfig } from 'svgo'; diff --git a/packages/qwik-city/src/buildtime/vite/plugin.ts b/packages/qwik-city/src/buildtime/vite/plugin.ts index b7ac8dbb904..880e5d22a9c 100644 --- a/packages/qwik-city/src/buildtime/vite/plugin.ts +++ b/packages/qwik-city/src/buildtime/vite/plugin.ts @@ -1,25 +1,9 @@ -import swRegister from '@qwik-city-sw-register-build'; -import { createMdxTransformer, type MdxTransform } from '../markdown/mdx'; -import { basename, join, resolve, extname } from 'node:path'; -import type { Plugin, PluginOption, UserConfig, Rollup } from 'vite'; -import { loadEnv } from 'vite'; -import { generateQwikCityPlan } from '../runtime-generation/generate-qwik-city-plan'; -import type { BuildContext } from '../types'; -import { createBuildContext, resetBuildContext } from '../context'; -import { isMenuFileName, normalizePath, removeExtension } from '../../utils/fs'; -import { validatePlugin } from './validate-plugin'; -import type { QwikCityPluginApi, QwikCityVitePluginOptions } from './types'; -import { build } from '../build'; -import { ssrDevMiddleware, staticDistMiddleware } from './dev-server'; -import { transformMenu } from '../markdown/menu'; -import { generateQwikCityEntries } from '../runtime-generation/generate-entries'; -import { patchGlobalThis } from '../../middleware/node/node-fetch'; import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; +import swRegister from '@qwik-city-sw-register-build'; import fs from 'node:fs'; -import { - generateServiceWorkerRegister, - prependManifestToServiceWorker, -} from '../runtime-generation/generate-service-worker'; +import { basename, extname, join, resolve } from 'node:path'; +import type { Plugin, PluginOption, Rollup, UserConfig } from 'vite'; +import { loadEnv } from 'vite'; import { NOT_FOUND_PATHS_ID, RESOLVED_NOT_FOUND_PATHS_ID, @@ -27,7 +11,21 @@ import { STATIC_PATHS_ID, } from '../../adapters/shared/vite'; import { postBuild } from '../../adapters/shared/vite/post-build'; +import { patchGlobalThis } from '../../middleware/node/node-fetch'; +import { isMenuFileName, normalizePath, removeExtension } from '../../utils/fs'; +import { build } from '../build'; +import { createBuildContext, resetBuildContext } from '../context'; +import { createMdxTransformer, type MdxTransform } from '../markdown/mdx'; +import { transformMenu } from '../markdown/menu'; +import { generateQwikCityEntries } from '../runtime-generation/generate-entries'; +import { generateQwikCityPlan } from '../runtime-generation/generate-qwik-city-plan'; +import { generateServiceWorkerRegister } from '../runtime-generation/generate-service-worker'; +import type { BuildContext } from '../types'; +import { ssrDevMiddleware, staticDistMiddleware } from './dev-server'; +import { getRouteImports } from './get-route-imports'; import { imagePlugin } from './image-jsx'; +import type { QwikCityPluginApi, QwikCityVitePluginOptions } from './types'; +import { validatePlugin } from './validate-plugin'; /** @public */ export function qwikCity(userOpts?: QwikCityVitePluginOptions): PluginOption[] { @@ -97,6 +95,9 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { if (!qwikPlugin) { throw new Error('Missing vite-plugin-qwik'); } + qwikPlugin.api.registerBundleGraphAdder?.((manifest) => { + return getRouteImports(ctx!.routes, manifest); + }); // @ts-ignore `format` removed in Vite 5 if (config.ssr?.format === 'cjs') { @@ -172,6 +173,7 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { if (isCityPlan || isSwRegister) { if (!ctx.isDevServer && ctx.isDirty) { await build(ctx); + ctx.isDirty = false; ctx.diagnostics.forEach((d) => { this.warn(d.message); @@ -264,38 +266,8 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { async handler() { if (ctx?.target === 'ssr' && !ctx?.isDevServer) { // ssr build - const manifest = qwikPlugin!.api.getManifest(); const clientOutDir = qwikPlugin!.api.getClientOutDir(); - if (manifest && clientOutDir) { - const basePathRelDir = api.getBasePathname().replace(/^\/|\/$/, ''); - const clientOutBaseDir = join(clientOutDir, basePathRelDir); - const insightsManifest = await qwikPlugin!.api.getInsightsManifest(clientOutDir); - - for (const swEntry of ctx.serviceWorkers) { - try { - const swClientDistPath = join(clientOutBaseDir, swEntry.chunkFileName); - const swCode = await fs.promises.readFile(swClientDistPath, 'utf-8'); - try { - const swCodeUpdate = prependManifestToServiceWorker( - ctx, - manifest, - insightsManifest?.prefetch || null, - swCode - ); - if (swCodeUpdate) { - await fs.promises.mkdir(clientOutDir, { recursive: true }); - await fs.promises.writeFile(swClientDistPath, swCodeUpdate); - } - } catch (e2) { - console.error(e2); - } - } catch (e) { - // safe to ignore if a service-worker.js not found - } - } - } - if (outDir && clientOutDir) { const assetsDir = qwikPlugin!.api.getAssetsDir(); const { staticPathsCode, notFoundPathsCode } = await postBuild( @@ -342,7 +314,7 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { } const QWIK_SERIALIZER = '@qwik-serializer'; -const QWIK_CITY_PLAN_ID = '@qwik-city-plan'; +export const QWIK_CITY_PLAN_ID = '@qwik-city-plan'; const QWIK_CITY_ENTRIES_ID = '@qwik-city-entries'; const QWIK_CITY = '@builder.io/qwik-city'; const QWIK_CITY_SW_REGISTER = '@qwik-city-sw-register'; diff --git a/packages/qwik-city/src/middleware/aws-lambda/index.ts b/packages/qwik-city/src/middleware/aws-lambda/index.ts index a2a2e138561..1ac392d0658 100644 --- a/packages/qwik-city/src/middleware/aws-lambda/index.ts +++ b/packages/qwik-city/src/middleware/aws-lambda/index.ts @@ -5,7 +5,7 @@ import type { QwikManifest, Render } from 'packages/qwik/src/server/types'; interface AwsOpt { render: Render; - manifest: QwikManifest; + manifest?: QwikManifest; qwikCityPlan: QwikCityPlan; } diff --git a/packages/qwik-city/src/middleware/request-handler/request-event.ts b/packages/qwik-city/src/middleware/request-handler/request-event.ts index 419aac7d241..d4ade181aae 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-event.ts @@ -1,32 +1,31 @@ -import type { - RequestEvent, - RequestEventLoader, - ServerRequestEvent, - ServerRequestMode, - RequestHandler, - RequestEventCommon, - ResolveValue, - QwikSerializer, - CacheControlTarget, - CacheControl, -} from './types'; +import type { ValueOrPromise } from '@builder.io/qwik'; +import { QDATA_KEY } from '../../runtime/src/constants'; import type { ActionInternal, + FailReturn, JSONValue, LoadedRoute, LoaderInternal, - FailReturn, } from '../../runtime/src/types'; +import { isPromise } from './../../runtime/src/utils'; +import { createCacheControl } from './cache-control'; import { Cookie } from './cookie'; +import { ServerError } from './error-handler'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { encoder } from './resolve-request-handlers'; -import { createCacheControl } from './cache-control'; -import type { ValueOrPromise } from '@builder.io/qwik'; -import type { QwikManifest, ResolvedManifest } from '@builder.io/qwik/optimizer'; +import type { + CacheControl, + CacheControlTarget, + QwikSerializer, + RequestEvent, + RequestEventCommon, + RequestEventLoader, + RequestHandler, + ResolveValue, + ServerRequestEvent, + ServerRequestMode, +} from './types'; import { IsQData, QDATA_JSON, QDATA_JSON_LEN } from './user-response'; -import { isPromise } from './../../runtime/src/utils'; -import { QDATA_KEY } from '../../runtime/src/constants'; -import { ServerError } from './error-handler'; const RequestEvLoaders = Symbol('RequestEvLoaders'); const RequestEvMode = Symbol('RequestEvMode'); @@ -42,7 +41,6 @@ export function createRequestEvent( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], - manifest: QwikManifest | ResolvedManifest | undefined, trailingSlash: boolean, basePathname: string, qwikSerializer: QwikSerializer, @@ -61,7 +59,6 @@ export function createRequestEvent( } sharedMap.set(IsQData, true); } - sharedMap.set('@manifest', manifest); let routeModuleIndex = -1; let writableStream: WritableStream | null = null; diff --git a/packages/qwik-city/src/middleware/request-handler/request-handler.ts b/packages/qwik-city/src/middleware/request-handler/request-handler.ts index b8626beb6a4..02ab430732d 100644 --- a/packages/qwik-city/src/middleware/request-handler/request-handler.ts +++ b/packages/qwik-city/src/middleware/request-handler/request-handler.ts @@ -15,7 +15,7 @@ export async function requestHandler( opts: ServerRenderOptions, qwikSerializer: QwikSerializer ): Promise | null> { - const { render, qwikCityPlan, manifest, checkOrigin } = opts; + const { render, qwikCityPlan, checkOrigin } = opts; const pathname = serverRequestEv.url.pathname; const matchPathname = getRouteMatchPathname(pathname, qwikCityPlan.trailingSlash); const routeAndHandlers = await loadRequestHandlers( @@ -31,7 +31,6 @@ export async function requestHandler( serverRequestEv, route, requestHandlers, - manifest, qwikCityPlan.trailingSlash, qwikCityPlan.basePathname, qwikSerializer diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index ce6f5b3eecf..6f240dbc157 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,16 +1,15 @@ -import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; +import type { LoadedRoute } from '../../runtime/src/types'; +import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; +import { AbortMessage, RedirectMessage } from './redirect-handler'; import { RequestEvQwikSerializer, createRequestEvent, getRequestMode, type RequestEventInternal, } from './request-event'; -import { ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; -import { AbortMessage, RedirectMessage } from './redirect-handler'; -import type { LoadedRoute } from '../../runtime/src/types'; import { encoder } from './resolve-request-handlers'; -import type { QwikManifest, ResolvedManifest } from '@builder.io/qwik/optimizer'; +import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; export interface QwikCityRun { response: Promise; @@ -36,7 +35,6 @@ export function runQwikCity( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, requestHandlers: RequestHandler[], - manifest: QwikManifest | ResolvedManifest | undefined, trailingSlash = true, basePathname = '/', qwikSerializer: QwikSerializer @@ -47,7 +45,6 @@ export function runQwikCity( serverRequestEv, loadedRoute, requestHandlers, - manifest, trailingSlash, basePathname, qwikSerializer, diff --git a/packages/qwik-city/src/runtime/src/api.md b/packages/qwik-city/src/runtime/src/api.md index 53864c93d81..2e8062c1de5 100644 --- a/packages/qwik-city/src/runtime/src/api.md +++ b/packages/qwik-city/src/runtime/src/api.md @@ -10,7 +10,6 @@ import { CookieOptions } from '@builder.io/qwik-city/middleware/request-handler' import { CookieValue } from '@builder.io/qwik-city/middleware/request-handler'; import { DeferReturn } from '@builder.io/qwik-city/middleware/request-handler'; import type { EnvGetter } from '@builder.io/qwik-city/middleware/request-handler'; -import { JSXNode } from '@builder.io/qwik'; import { JSXOutput } from '@builder.io/qwik'; import { QRL } from '@builder.io/qwik'; import { QRLEventHandlerMulti } from '@builder.io/qwik'; @@ -447,10 +446,10 @@ export type ServerQRL = QRL<((abort: AbortSignal, ...a // @public (undocumented) export const serverQrl: (qrl: QRL, options?: ServerConfig) => ServerQRL; -// @public (undocumented) +// @public export const ServiceWorkerRegister: (props: { nonce?: string; -}) => JSXNode<"script">; +}) => JSXOutput; // @public (undocumented) export interface StaticGenerate { diff --git a/packages/qwik-city/src/runtime/src/client-navigate.ts b/packages/qwik-city/src/runtime/src/client-navigate.ts index 20df78fc502..06b3284acef 100644 --- a/packages/qwik-city/src/runtime/src/client-navigate.ts +++ b/packages/qwik-city/src/runtime/src/client-navigate.ts @@ -1,7 +1,8 @@ import { isBrowser } from '@builder.io/qwik'; +// @ts-expect-error we don't have types for the preloader yet +import { p as preload } from '@builder.io/qwik/preloader'; import type { NavigationType, ScrollState } from './types'; import { isSamePath, toPath } from './utils'; -import { PREFETCHED_NAVIGATE_PATHS } from './constants'; export const clientNavigate = ( win: Window, @@ -43,9 +44,7 @@ export const newScrollState = (): ScrollState => { export const prefetchSymbols = (path: string) => { if (isBrowser) { path = path.endsWith('/') ? path : path + '/'; - if (!PREFETCHED_NAVIGATE_PATHS.has(path)) { - PREFETCHED_NAVIGATE_PATHS.add(path); - document.dispatchEvent(new CustomEvent('qprefetch', { detail: { links: [path] } })); - } + path = path.length > 1 && path.startsWith('/') ? path.slice(1) : path; + preload(path); } }; diff --git a/packages/qwik-city/src/runtime/src/constants.ts b/packages/qwik-city/src/runtime/src/constants.ts index c54e0a9a517..f5d9842f2cc 100644 --- a/packages/qwik-city/src/runtime/src/constants.ts +++ b/packages/qwik-city/src/runtime/src/constants.ts @@ -4,8 +4,6 @@ export const MODULE_CACHE = /*#__PURE__*/ new WeakMap(); export const CLIENT_DATA_CACHE = new Map>(); -export const PREFETCHED_NAVIGATE_PATHS = new Set(); - export const QACTION_KEY = 'qaction'; export const QFN_KEY = 'qfunc'; diff --git a/packages/qwik-city/src/runtime/src/service-worker/api.md b/packages/qwik-city/src/runtime/src/service-worker/api.md index 22b2654ad3d..b0da6cf098a 100644 --- a/packages/qwik-city/src/runtime/src/service-worker/api.md +++ b/packages/qwik-city/src/runtime/src/service-worker/api.md @@ -4,7 +4,7 @@ ```ts -// @public (undocumented) +// @public @deprecated (undocumented) export const setupServiceWorker: () => void; // (No @packageDocumentation comment for this package) diff --git a/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.ts b/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.ts deleted file mode 100644 index ea66db546ce..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { AwaitingRequests, Fetch } from './types'; -import { useCache } from './utils'; - -export const cachedFetch = ( - cache: Cache, - fetch: Fetch, - awaitingRequests: AwaitingRequests, - request: Request -) => - new Promise((promiseResolve, promiseReject) => { - const url = request.url; - const awaitingRequestResolves = awaitingRequests.get(url); - - if (awaitingRequestResolves) { - // there's already an active request happening - // don't start a new request - awaitingRequestResolves.push([promiseResolve, promiseReject]); - } else { - // there isn't already an active request for this url - // start a new request - const resolve = (response: Response) => { - // the response has been resolved - const resolves = awaitingRequests.get(url); - if (resolves) { - awaitingRequests.delete(url); - // loop through each of the active requests - for (const [awaitingResolve] of resolves) { - // clone a new response for each of the active requests - awaitingResolve(response.clone()); - } - } else { - // somehow the array of awaiting requests doesn't exist - promiseResolve(response.clone()); - } - }; - - const reject = (msg: any) => { - const resolves = awaitingRequests.get(url); - if (resolves) { - awaitingRequests.delete(url); - for (const [_, awaitingReject] of resolves) { - awaitingReject(msg); - } - } else { - promiseReject(msg); - } - }; - - // create a new array of the request waiting to be resolved - awaitingRequests.set(url, [[promiseResolve, promiseReject]]); - - cache - .match(url) - .then((cachedResponse) => { - if (useCache(request, cachedResponse)) { - // cached response found and user did not specifically send - // a request header to NOT use the cache (wasn't a hard refresh) - resolve(cachedResponse!); - } else { - // no cached response found or user didn't want to use the cache - // do a full network request - return fetch(request).then(async (networkResponse) => { - if (networkResponse.ok) { - // network response was good, let's cache it - await cache.put(url, networkResponse.clone()); - } - resolve(networkResponse); - }); - } - }) - .catch((err) => { - // network error, probably offline - return cache.match(url).then((cachedResponse) => { - if (cachedResponse) { - // luckily we have a cached version, let's use it instead of an offline message - resolve(cachedResponse); - } else { - // darn, we've got no connectivity and no cached response - reject(err); - } - }); - }); - } - }); diff --git a/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.unit.ts b/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.unit.ts deleted file mode 100644 index 5b730f0b1eb..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/cached-fetch.unit.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable no-empty-pattern */ -import { assert, beforeEach, test } from 'vitest'; -import { cachedFetch } from './cached-fetch'; -import type { AwaitingRequests, Fetch } from './types'; - -function mockRequest(url: string): Request { - return { - url, - } as any; -} - -function mockResponse(url: string, body: string, ok = true): Response { - return { url, body, ok, clone: () => ({ body, ok }) } as any; -} - -interface TestContext { - cache: TestCache; - fetchRequests: number; - fetchSuccess: Map; - fetchError: Map; - fetch: Fetch; - addFetchSuccess: (response: Response) => void; - addFetchError: (url: string, e: Error) => void; - awaitingRequests: AwaitingRequests; -} - -interface TestCache extends Cache { - get: (url: string) => Response | undefined; -} - -let ctx: TestContext; - -beforeEach(() => { - const cacheStore = new Map(); - ctx = { - cache: { - match: async (url: string) => { - return cacheStore.get(url); - }, - put: async (url: string, response: Response) => { - cacheStore.set(url, response); - }, - get: (url: string) => cacheStore.get(url), - } as TestCache, - fetchRequests: 0, - fetchSuccess: new Map(), - fetchError: new Map(), - fetch: async (r: Request) => { - ctx.fetchRequests++; - const e = ctx.fetchError.get(r.url); - if (e) { - throw e; - } - return ctx.fetchSuccess.get(r.url)!; - }, - addFetchSuccess: (r) => ctx.fetchSuccess.set(r.url, r), - addFetchError: (url: string, e: Error) => { - ctx.fetchError.set(url, e); - }, - awaitingRequests: new Map(), - }; -}); - -test('new request, Failed to fetch', async () => { - const req = mockRequest('/a.js'); - ctx.addFetchError('/a.js', new Error('Failed to fetch')); - - try { - const promises: Promise[] = []; - for (let i = 0; i < 10; i++) { - promises.push(cachedFetch(ctx.cache, ctx.fetch, ctx.awaitingRequests, req)); - } - assert.deepEqual(ctx.awaitingRequests.size, 1); - assert.deepEqual(ctx.awaitingRequests.get('/a.js')?.length, 10); - await Promise.all(promises); - throw new Error('should have thrown'); - } catch (e: any) { - assert.deepEqual(e.message, 'Failed to fetch'); - assert.deepEqual(ctx.fetchRequests, 1); - assert.deepEqual(ctx.awaitingRequests.size, 0); - } -}); - -test('new request, Failed to fetch', async () => { - const req = mockRequest('/a.js'); - ctx.addFetchError('/a.js', new Error('Failed to fetch')); - - try { - await cachedFetch(ctx.cache, ctx.fetch, ctx.awaitingRequests, req); - throw new Error('should have thrown'); - } catch (e: any) { - assert.deepEqual(e.message, 'Failed to fetch'); - assert.deepEqual(ctx.fetchRequests, 1); - assert.deepEqual(ctx.awaitingRequests.size, 0); - } -}); - -test('new request, no existing cache, cache ok response', async () => { - const req = mockRequest('/a.js'); - ctx.addFetchSuccess(mockResponse('/a.js', 'a')); - - const promises: Promise[] = []; - for (let i = 0; i < 10; i++) { - promises.push(cachedFetch(ctx.cache, ctx.fetch, ctx.awaitingRequests, req)); - } - assert.deepEqual(ctx.awaitingRequests.size, 1); - assert.deepEqual(ctx.awaitingRequests.get('/a.js')?.length, 10); - const responses = await Promise.all(promises); - assert.deepEqual(responses.length, 10); - assert.deepEqual(responses[0].body, 'a'); - assert.deepEqual(responses[1].body, 'a'); - assert.deepEqual(ctx.cache.get('/a.js')?.body as any, 'a'); -}); - -test('new request, no existing cache, do not cache 404 response', async () => { - const req = mockRequest('/a.js'); - ctx.addFetchSuccess(mockResponse('/a.js', '404', false)); - - const promises: Promise[] = []; - for (let i = 0; i < 10; i++) { - promises.push(cachedFetch(ctx.cache, ctx.fetch, ctx.awaitingRequests, req)); - } - assert.deepEqual(ctx.awaitingRequests.size, 1); - assert.deepEqual(ctx.awaitingRequests.get('/a.js')?.length, 10); - const responses = await Promise.all(promises); - assert.deepEqual(responses.length, 10); - assert.deepEqual(responses[0].body, '404'); - assert.deepEqual(responses[1].body, '404'); - assert.deepEqual(ctx.cache.get('/a.js'), undefined); -}); - -test('new request, no cache', async () => { - const req = mockRequest('/abc.js'); - const fetchRes = mockResponse('/abc.js', 'abc'); - ctx.addFetchSuccess(fetchRes); - - const res = await cachedFetch(ctx.cache, ctx.fetch, ctx.awaitingRequests, req); - assert.deepEqual(res.body as any, 'abc'); - assert.deepEqual(ctx.fetchRequests, 1); - assert.deepEqual(ctx.awaitingRequests.size, 0); -}); diff --git a/packages/qwik-city/src/runtime/src/service-worker/constants.ts b/packages/qwik-city/src/runtime/src/service-worker/constants.ts deleted file mode 100644 index 21a6bed6974..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AwaitingRequests } from './types'; - -export const qBuildCacheName = 'QwikBuild'; - -export const existingPrefetchUrls = new Set(); - -export const awaitingRequests: AwaitingRequests = new Map(); - -export const prefetchQueue: string[] = []; diff --git a/packages/qwik-city/src/runtime/src/service-worker/index.ts b/packages/qwik-city/src/runtime/src/service-worker/index.ts index 6f494c79fed..bfab0367053 100644 --- a/packages/qwik-city/src/runtime/src/service-worker/index.ts +++ b/packages/qwik-city/src/runtime/src/service-worker/index.ts @@ -1,13 +1,13 @@ -import type { AppBundle, LinkBundle } from './types'; -import { setupServiceWorkerScope } from './setup'; - -/** @public */ -export const setupServiceWorker = () => { - if (typeof self !== 'undefined' && typeof appBundles !== 'undefined') { - setupServiceWorkerScope(self as any, appBundles, libraryBundleIds, linkBundles); - } -}; - -declare const appBundles: AppBundle[]; -declare const libraryBundleIds: number[]; -declare const linkBundles: LinkBundle[]; +/** + * @deprecated This is no longer needed, Qwik now automatically embeds preloading logic into the + * application. + * + * If your service-worker.ts file contains no custom code, you should deploy to production until + * you're sure that all users picked up the new version, then you can remove it and also remove + * the `` component from your `Root.tsx`. + * + * If you do have custom service worker logic, you should keep the `service-worker.ts` file and + * `` component, but remove the `setupServiceWorker()` call. + * @public + */ +export const setupServiceWorker = () => {}; diff --git a/packages/qwik-city/src/runtime/src/service-worker/prefetch.ts b/packages/qwik-city/src/runtime/src/service-worker/prefetch.ts deleted file mode 100644 index d5ff970e5a3..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/prefetch.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { cachedFetch } from './cached-fetch'; -import { awaitingRequests, existingPrefetchUrls, prefetchQueue } from './constants'; -import type { AppBundle, Fetch, LinkBundle } from './types'; -import { getAppBundleByName, getAppBundlesNamesFromIds } from './utils'; - -export const prefetchBundleNames = ( - appBundles: AppBundle[], - qBuildCache: Cache, - fetch: Fetch, - baseUrl: URL, - prefetchAppBundleNames: (string | null)[] | undefined | null, - highPriority = false -) => { - if (Array.isArray(prefetchAppBundleNames)) { - addBundlesToPrefetchQueue(prefetchAppBundleNames, appBundles, baseUrl, highPriority); - } - drainQueue(qBuildCache, fetch); -}; - -export function addBundlesToPrefetchQueue( - bundlesToPrefetch: (string | null)[], - appBundles: AppBundle[], - baseUrl: URL, - highPriority: boolean -) { - for (const prefetchAppBundleName of bundlesToPrefetch) { - try { - const appBundle = getAppBundleByName(appBundles, prefetchAppBundleName); - - if (appBundle) { - const importedBundleNames = getAppBundlesNamesFromIds(appBundles, appBundle[1]); - const url = new URL(prefetchAppBundleName!, baseUrl).href; - const queueIndex = prefetchQueue.indexOf(url); - - if (queueIndex > -1) { - // already in the queue - if (highPriority) { - // move to the front of the queue - prefetchQueue.splice(queueIndex, 1); - prefetchQueue.unshift(url); - } - } else { - if (highPriority) { - // add to the front of the queue - prefetchQueue.unshift(url); - } else { - // add to the end of the queue - prefetchQueue.push(url); - } - addBundlesToPrefetchQueue(importedBundleNames, appBundles, baseUrl, highPriority); - } - } - } catch (e) { - console.error(e); - } - } -} - -export function drainQueue(qBuildCache: Cache, fetch: Fetch) { - // do not prefetch more than 6 requests at a time to ensure - // the browser is able to handle a user request as soon as possible - while (prefetchQueue.length > 0 && awaitingRequests.size < 6) { - const url = prefetchQueue.shift()!; - - if (!existingPrefetchUrls.has(url!)) { - const request = new Request(url); - - existingPrefetchUrls.add(url!); - cachedFetch(qBuildCache, fetch, awaitingRequests, request) - .catch(() => { - existingPrefetchUrls.delete(url!); - }) - .finally(() => drainQueue(qBuildCache, fetch)); - } - } -} - -export const prefetchLinkBundles = ( - appBundles: AppBundle[], - libraryBundleIds: number[], - linkBundles: LinkBundle[], - qBuildCache: Cache, - fetch: Fetch, - baseUrl: URL, - linkPathnames: string[] -) => { - try { - prefetchBundleNames( - appBundles, - qBuildCache, - fetch, - baseUrl, - getAppBundlesNamesFromIds(appBundles, libraryBundleIds) - ); - } catch (e) { - console.error(e); - } - - for (const linkPathname of linkPathnames) { - try { - for (const linkBundle of linkBundles) { - const [route, linkBundleIds] = linkBundle; - console; - if (route.test(linkPathname)) { - prefetchBundleNames( - appBundles, - qBuildCache, - fetch, - baseUrl, - getAppBundlesNamesFromIds(appBundles, linkBundleIds) - ); - break; - } - } - } catch (e) { - console.error(e); - } - } -}; - -export const prefetchWaterfall = ( - appBundles: AppBundle[], - qBuildCache: Cache, - fetch: Fetch, - requestedBuildUrl: URL -) => { - try { - const { baseUrl, requestedBundleName } = splitUrlToBaseAndBundle(requestedBuildUrl); - - prefetchBundleNames(appBundles, qBuildCache, fetch, baseUrl, [requestedBundleName], true); - } catch (e) { - console.error(e); - } -}; - -function splitUrlToBaseAndBundle(fullUrl: URL) { - const segments = fullUrl.href.split('/'); - const requestedBundleName = segments[segments.length - 1]; - segments[segments.length - 1] = ''; - const baseUrl = new URL(segments.join('/')); - - return { - baseUrl, - requestedBundleName, - }; -} diff --git a/packages/qwik-city/src/runtime/src/service-worker/prefetch.unit.ts b/packages/qwik-city/src/runtime/src/service-worker/prefetch.unit.ts deleted file mode 100644 index f4696c91785..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/prefetch.unit.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe } from 'node:test'; -import { afterEach, expect, test, vi } from 'vitest'; -import { cachedFetch } from './cached-fetch'; -import { awaitingRequests, existingPrefetchUrls, prefetchQueue } from './constants'; -import { addBundlesToPrefetchQueue, drainQueue } from './prefetch'; -import type { AppBundle, Fetch } from './types'; - -vi.mock('./cached-fetch'); - -function getStubWorkerCache() { - return { - add: (request: RequestInfo | URL) => Promise.resolve(), - addAll: (requests: RequestInfo[]) => Promise.resolve(), - delete: (request: RequestInfo | URL, options?: CacheQueryOptions) => Promise.resolve(true), - keys: (request?: RequestInfo | URL, options?: CacheQueryOptions) => Promise.resolve([]), - match: (request: RequestInfo | URL, options?: CacheQueryOptions) => Promise.resolve(undefined), - matchAll: (request?: RequestInfo | URL, options?: CacheQueryOptions) => Promise.resolve([]), - put: (request: RequestInfo | URL, response: Response) => Promise.resolve(), - }; -} - -function createFakeFetch(): Fetch { - return async function (request: Request) { - return new Response(); - }; -} - -afterEach(() => { - prefetchQueue.length = 0; - awaitingRequests.clear(); - existingPrefetchUrls.clear(); - vi.restoreAllMocks(); -}); - -const urlPrefix = 'http://localhost'; - -describe('addBundlesToPrefetchQueue', () => { - test(`GIVEN 3 modules with 2 imports each - WHEN attempting to prefetch the first 3 modules - THEN 9 modules should be added to the prefetching queue`, () => { - const fakeBaseUrl = new URL(urlPrefix); - const isHighPriority = false; - - const fakeAppBundles: AppBundle[] = [ - ['a.js', [1, 2]], - ['b.js', []], - ['c.js', []], - ['d.js', [4, 5]], - ['e.js', []], - ['f.js', []], - ['g.js', [7, 8]], - ['h.js', []], - ['i.js', []], - ]; - - const bundlesToPrefetch = ['a.js', 'd.js', 'g.js']; - - addBundlesToPrefetchQueue(bundlesToPrefetch, fakeAppBundles, fakeBaseUrl, isHighPriority); - - const expectedResult = [ - 'a.js', - 'b.js', - 'c.js', - 'd.js', - 'e.js', - 'f.js', - 'g.js', - 'h.js', - 'i.js', - ].map((bundle) => `${urlPrefix}/${bundle}`); - - expect(prefetchQueue).toEqual(expectedResult); - }); - test(`GIVEN 5 modules each importing the following one - WHEN attempting to prefetch the first module - THEN all 5 modules should be added to the prefetching queue`, () => { - const fakeBaseUrl = new URL(urlPrefix); - const isHighPriority = false; - - const fakeAppBundles: AppBundle[] = [ - ['a.js', [1]], - ['b.js', [2]], - ['c.js', [3]], - ['d.js', [4]], - ['e.js', []], - ]; - - const bundlesToPrefetch = ['a.js']; - - addBundlesToPrefetchQueue(bundlesToPrefetch, fakeAppBundles, fakeBaseUrl, isHighPriority); - - const expectedResult = ['a.js', 'b.js', 'c.js', 'd.js', 'e.js'].map( - (bundle) => `${urlPrefix}/${bundle}` - ); - - expect(prefetchQueue).toEqual(expectedResult); - }); - test(`GIVEN prefetch queue already has 2 modules - WHEN attempting to prefetch again the second module from the queue but with a high priority - THEN the second module should be added in front of the first`, () => { - const fakeBaseUrl = new URL(urlPrefix); - const isHighPriority = true; - - const fakeAppBundles: AppBundle[] = [ - ['a.js', []], - ['b.js', []], - ]; - - prefetchQueue.push(`${urlPrefix}/a.js`, `${urlPrefix}/b.js`); - - const bundlesToPrefetch = ['b.js']; - - addBundlesToPrefetchQueue(bundlesToPrefetch, fakeAppBundles, fakeBaseUrl, isHighPriority); - - const expectedResult = ['b.js', 'a.js'].map((bundle) => `${urlPrefix}/${bundle}`); - - expect(prefetchQueue).toEqual(expectedResult); - }); - test(`GIVEN prefetch queue already has module "a" - WHEN attempting to prefetch module "b" with a high priority - THEN module "b" should be added in front of "a"`, () => { - const fakeBaseUrl = new URL(urlPrefix); - const isHighPriority = true; - - const fakeAppBundles: AppBundle[] = [ - ['a.js', []], - ['b.js', []], - ]; - - prefetchQueue.push(`${urlPrefix}/a.js`); - - const bundlesToPrefetch = ['b.js']; - - addBundlesToPrefetchQueue(bundlesToPrefetch, fakeAppBundles, fakeBaseUrl, isHighPriority); - - const expectedResult = ['b.js', 'a.js'].map((bundle) => `${urlPrefix}/${bundle}`); - - expect(prefetchQueue).toEqual(expectedResult); - }); -}); - -describe('drainQueue', () => { - test(`GIVEN queue with 3 urls which are successfully fetched - THEN fetch should be called 3 times - and the requests should be added to the already cached requests`, async () => { - prefetchQueue.push(`${urlPrefix}/a.js`, `${urlPrefix}/b.js`, `${urlPrefix}/c.js`); - - vi.mocked(cachedFetch).mockResolvedValue(new Response()); - - drainQueue(getStubWorkerCache(), createFakeFetch()); - - expect(cachedFetch).toHaveBeenCalledTimes(3); - await vi.waitUntil(() => existingPrefetchUrls.size > 0); - - expect(existingPrefetchUrls.size).toBe(3); - }); - - test(`GIVEN queue with 4 urls which are successfully fetched - with one repeating url - THEN fetch should be called 3 times - and the requests should be added to the already cached requests`, async () => { - prefetchQueue.push( - `${urlPrefix}/a.js`, - `${urlPrefix}/b.js`, - `${urlPrefix}/c.js`, - `${urlPrefix}/a.js` - ); - - vi.mocked(cachedFetch).mockResolvedValue(new Response()); - - drainQueue(getStubWorkerCache(), createFakeFetch()); - - expect(cachedFetch).toHaveBeenCalledTimes(3); - await vi.waitUntil(() => existingPrefetchUrls.size > 0); - - expect(existingPrefetchUrls.size).toBe(3); - }); - - test(`GIVEN queue with 2 urls, one succeed and the second fails - THEN fetch should be called 2 times - and the "already cached requests" set should be set to 1`, async () => { - prefetchQueue.push(`${urlPrefix}/a.js`, `${urlPrefix}/b.js`); - - let requestCount = 0; - - vi.mocked(cachedFetch).mockImplementation(async (): Promise => { - if (requestCount === 1) { - throw new Error('Failed to fetch'); - } - requestCount++; - return new Response(); - }); - - drainQueue(getStubWorkerCache(), createFakeFetch()); - - expect(cachedFetch).toHaveBeenCalledTimes(2); - await vi.waitUntil(() => existingPrefetchUrls.size > 0); - expect(existingPrefetchUrls.size).toBe(1); - }); -}); diff --git a/packages/qwik-city/src/runtime/src/service-worker/setup.ts b/packages/qwik-city/src/runtime/src/service-worker/setup.ts deleted file mode 100644 index 56b86c4c28b..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/setup.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { cachedFetch } from './cached-fetch'; -import { awaitingRequests, qBuildCacheName } from './constants'; -import { prefetchBundleNames, prefetchLinkBundles, prefetchWaterfall } from './prefetch'; -import type { AppBundle, LinkBundle, ServiceWorkerMessageEvent } from './types'; -import { computeAppSymbols, getCacheToDelete, isAppBundleRequest, resolveSymbols } from './utils'; - -export const setupServiceWorkerScope = ( - swScope: ServiceWorkerGlobalScope, - appBundles: AppBundle[], - libraryBundleIds: number[], - linkBundles: LinkBundle[] -) => { - const swFetch = swScope.fetch.bind(swScope); - const appSymbols = computeAppSymbols(appBundles); - - swScope.addEventListener('activate', (event) => { - (async () => { - try { - // Delete any other caches that are not the current SW cache name - event.waitUntil( - swScope.caches.keys().then((keys) => - Promise.all( - keys.map((key) => { - if (key !== qBuildCacheName) { - return caches.delete(key); - } - }) - ) - ) - ); - - // Delete old bundles - const qBuildCache = await swScope.caches.open(qBuildCacheName); - const cachedRequestKeys = await qBuildCache.keys(); - const cachedUrls = cachedRequestKeys.map((r) => r.url); - const cachedRequestsToDelete = getCacheToDelete(appBundles, cachedUrls); - await Promise.all(cachedRequestsToDelete.map((r) => qBuildCache.delete(r))); - } catch (e) { - console.error(e); - } - })(); - }); - - swScope.addEventListener('message', async ({ data }: ServiceWorkerMessageEvent) => { - if (data.type === 'qprefetch' && typeof data.base === 'string') { - const qBuildCache = await swScope.caches.open(qBuildCacheName); - const baseUrl = new URL(data.base, swScope.origin); - - if (Array.isArray(data.links)) { - prefetchLinkBundles( - appBundles, - libraryBundleIds, - linkBundles, - qBuildCache, - swFetch, - baseUrl, - data.links - ); - } - - if (Array.isArray(data.bundles)) { - prefetchBundleNames(appBundles, qBuildCache, swFetch, baseUrl, data.bundles); - } - - if (Array.isArray(data.symbols)) { - prefetchBundleNames( - appBundles, - qBuildCache, - swFetch, - baseUrl, - resolveSymbols(appSymbols, data.symbols) - ); - } - } - }); - - swScope.addEventListener('fetch', (event: FetchEvent) => { - const request = event.request; - - if (request.method === 'GET') { - const url = new URL(request.url); - - if (isAppBundleRequest(appBundles, url.pathname)) { - event.respondWith( - swScope.caches.open(qBuildCacheName).then((qBuildCache) => { - prefetchWaterfall(appBundles, qBuildCache, swFetch, url); - return cachedFetch(qBuildCache, swFetch, awaitingRequests, request); - }) - ); - } - } - }); -}; - -declare const self: ServiceWorkerGlobalScope; diff --git a/packages/qwik-city/src/runtime/src/service-worker/types.ts b/packages/qwik-city/src/runtime/src/service-worker/types.ts deleted file mode 100644 index e3969942a7e..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface QPrefetchData { - links?: string[]; - bundles?: string[]; - symbols?: string[]; -} - -export interface QPrefetchMessage extends QPrefetchData { - type: 'qprefetch'; - base: string; -} - -export type ServiceWorkerMessage = QPrefetchMessage; - -export interface ServiceWorkerMessageEvent { - data: ServiceWorkerMessage; -} - -export type AppSymbols = Map; -export type AppBundle = - | [bundleName: string, importedBundleIds: number[]] - | [bundleName: string, importedBundleIds: number[], symbolHashesInBundle: string[]]; - -export type LinkBundle = [routePattern: RegExp, bundleIds: number[]]; - -export type Fetch = (r: Request) => Promise; - -export type AwaitingRequests = Map< - string, - [resolve: (response: Response | PromiseLike) => void, reject: (msg: any) => void][] ->; diff --git a/packages/qwik-city/src/runtime/src/service-worker/utils.ts b/packages/qwik-city/src/runtime/src/service-worker/utils.ts deleted file mode 100644 index 80388d09457..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { AppBundle, AppSymbols } from './types'; - -export const getCacheToDelete = (appBundles: AppBundle[], cachedUrls: string[]) => - cachedUrls.filter((url) => !appBundles.some((appBundle) => url.endsWith(appBundle[0]))); - -export const useCache = (request: Request, response: Response | undefined) => - !!response && !hasNoCacheHeader(response); - -const hasNoCacheHeader = (r: { headers: Headers }) => { - const cacheControl = r.headers.get('Cache-Control') || ''; - return cacheControl.includes('no-cache') || cacheControl.includes('max-age=0'); -}; - -export const isAppBundleRequest = (appBundles: AppBundle[], requestPathname: string) => - appBundles.some((b) => requestPathname.endsWith('/' + b[0])); - -export const getAppBundleByName = (appBundles: AppBundle[], appBundleName: string | null) => - appBundles.find((b) => b[0] === appBundleName); - -export const getAppBundlesNamesFromIds = (appBundles: AppBundle[], bundleIds: number[]) => - bundleIds.map((bundleId) => (appBundles[bundleId] ? appBundles[bundleId][0] : null)); - -export const resolveSymbols = (appSymbols: Map, symbolsHashes: string[]) => - symbolsHashes.map((s) => appSymbols.get(s)).filter((s) => s != null) as string[]; - -export const computeAppSymbols = (appBundles: AppBundle[]): AppSymbols => { - const appSymbols = new Map(); - for (const bundle of appBundles) { - const hashes = bundle[2]; - if (hashes) { - for (const hash of hashes) { - appSymbols.set(hash, bundle[0]); - } - } - } - return appSymbols; -}; diff --git a/packages/qwik-city/src/runtime/src/service-worker/utils.unit.ts b/packages/qwik-city/src/runtime/src/service-worker/utils.unit.ts deleted file mode 100644 index 5e2b14000ee..00000000000 --- a/packages/qwik-city/src/runtime/src/service-worker/utils.unit.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Request as NodeRequest, Response as NodeResponse } from 'undici'; -import type { AppBundle } from './types'; -import { getCacheToDelete, isAppBundleRequest, useCache } from './utils'; -import { assert, test } from 'vitest'; - -test('getCacheToDelete, delete bundles no longer possible', () => { - const appBundles: AppBundle[] = [ - ['q-abc.js', [], []], - ['q-def.js', [], []], - ]; - const cachedUrls = ['https://qwik.dev/build/q-abc.js', 'https://qwik.dev/build/q-xyz.js']; - const c = getCacheToDelete(appBundles, cachedUrls); - assert.deepEqual(c, ['https://qwik.dev/build/q-xyz.js']); -}); - -test('getCacheToDelete, none to delete', () => { - const appBundles: AppBundle[] = [ - ['q-abc.js', [], []], - ['q-def.js', [], []], - ]; - const cachedUrls = ['https://qwik.dev/build/q-abc.js']; - const c = getCacheToDelete(appBundles, cachedUrls); - assert.deepEqual(c, []); -}); - -test('isAppBundleRequest, in buildBundles', () => { - const appBundles: AppBundle[] = [ - ['q-abc.js', [], []], - ['q-def.js', [], []], - ]; - const pathname = '/build/q-abc.js'; - const c = isAppBundleRequest(appBundles, pathname); - assert.deepEqual(c, true); -}); - -test('isAppBundleRequest, not in buildBundles', () => { - const appBundles: AppBundle[] = [ - ['q-abc.js', [], []], - ['q-def.js', [], []], - ]; - const pathname = '/build/q-xyz.js'; - const c = isAppBundleRequest(appBundles, pathname); - assert.deepEqual(c, false); -}); - -test('do not useCache, no response', () => { - const request = mockRequest(); - const response = undefined; - const c = useCache(request, response); - assert.deepEqual(c, false); -}); - -test('do not useCache, response has max-age=0', () => { - const request = mockRequest(); - const response = mockResponse(); - response.headers.set('cache-control', 'max-age=0'); - const c = useCache(request, response); - assert.deepEqual(c, false); -}); - -test('do not useCache, response has no-cache', () => { - const request = mockRequest(); - const response = mockResponse(); - response.headers.set('cache-control', 'no-cache'); - const c = useCache(request, response); - assert.deepEqual(c, false); -}); - -test('useCache', () => { - const request = mockRequest(); - const response = mockResponse(); - const c = useCache(request, response); - assert.deepEqual(c, true); -}); - -export function mockRequest(url?: string): Request { - url = url || 'https://qwik.dev/'; - return new NodeRequest(url) as any; -} - -export function mockResponse(body?: any): Response { - return new NodeResponse(body) as any; -} diff --git a/packages/qwik-city/src/runtime/src/sw-component.tsx b/packages/qwik-city/src/runtime/src/sw-component.tsx index fedb3ffc525..2aa9bbd8c0c 100644 --- a/packages/qwik-city/src/runtime/src/sw-component.tsx +++ b/packages/qwik-city/src/runtime/src/sw-component.tsx @@ -1,6 +1,12 @@ -import { jsx } from '@builder.io/qwik'; import swRegister from '@qwik-city-sw-register'; - -/** @public */ -export const ServiceWorkerRegister = (props: { nonce?: string }) => - jsx('script', { dangerouslySetInnerHTML: swRegister, nonce: props.nonce }); +import type { JSXOutput } from '@builder.io/qwik'; +/** + * Loads the service workers that are defined in the routes. Any file named `service-worker.*` (all + * JS extensions are allowed) will be picked up, bundled into a separate file, and registered as a + * service worker. + * + * @public + */ +export const ServiceWorkerRegister = (props: { nonce?: string }): JSXOutput => ( + - * ``` + * Defaults to `0.25` (25% probability) + */ + minPreloadProbability?: number; + /** + * Value of the `` attribute when links are added. The preloader itself will + * autodetect which attribute to use based on the browser capabilities. * - * By default, the `prefetchEvent` implementation will be set to `always`. + * Defaults to `modulepreload`. */ + linkRel?: 'prefetch' | 'preload' | 'modulepreload' | null; + /** Value of the `` attribute when links are added. Defaults to `null`. */ + linkFetchPriority?: 'auto' | 'low' | 'high' | null; + /** @deprecated No longer used. */ + linkInsert?: 'js-append' | 'html-append' | null; + /** @deprecated No longer used. */ + workerFetchInsert?: 'always' | 'no-link-support' | null; + /** @deprecated No longer used. */ prefetchEvent?: 'always' | null; } @@ -101,8 +110,6 @@ export interface RenderResult { snapshotResult: SnapshotResult | undefined; isStatic: boolean; manifest?: QwikManifest; - /** @internal TODO: Move to snapshotResult */ - _symbols?: string[]; } /** @public */ @@ -112,24 +119,13 @@ export interface QwikLoaderOptions { } /** - * Options which determine how the Qwik Prefetch Service Worker is added to the document. - * - * Qwik Prefetch Service Worker is used to prefetch resources so that the QwikLoader will always - * have a cache hit. This will ensure that there will not be any delays for the end user while - * interacting with the application. - * + * @deprecated This is no longer used as the preloading happens automatically in qrl-class.ts. * @public */ export interface QwikPrefetchServiceWorkerOptions { - /** - * Should the Qwik Prefetch Service Worker be added to the container. Defaults to `false` until - * the QwikCity Service Worker is deprecated. - */ + /** @deprecated This is no longer used as the preloading happens automatically in qrl-class.ts. */ include?: boolean; - /** - * Where should the Qwik Prefetch Service Worker be added to the container. Defaults to `top` to - * get prefetching going as fast as possible. - */ + /** @deprecated This is no longer used as the preloading happens automatically in qrl-class.ts. */ position?: 'top' | 'bottom'; } @@ -154,11 +150,7 @@ export interface RenderOptions extends SerializeDocumentOptions { */ qwikLoader?: QwikLoaderOptions; - /** - * Specifies if the Qwik Prefetch Service Worker script is added to the document or not. - * - * Defaults to `{ include: false }`. NOTE: This may be change in the future. - */ + /** @deprecated Use `prefetchStrategy` instead */ qwikPrefetchServiceWorker?: QwikPrefetchServiceWorkerOptions; prefetchStrategy?: PrefetchStrategy | null; @@ -215,4 +207,4 @@ export type RenderToStream = (opts: RenderToStreamOptions) => Promise submoduleCli(config), [join(config.srcQwikDir, 'optimizer')]: () => submoduleOptimizer(config), - [join(config.srcQwikDir, 'prefetch-service-worker')]: () => submoduleQwikPrefetch(config), [join(config.srcQwikDir, 'server')]: () => submoduleServer(config), [join(config.srcQwikCityDir, 'runtime/src')]: () => buildQwikCity(config), }); } } catch (e: any) { - panic(String(e ? e.stack || e : 'Error')); + panic(e); } } diff --git a/scripts/submodule-core.ts b/scripts/submodule-core.ts index 779ad822404..df6f344ecac 100644 --- a/scripts/submodule-core.ts +++ b/scripts/submodule-core.ts @@ -22,7 +22,7 @@ async function submoduleCoreProd(config: BuildConfig) { const input: InputOptions = { input: join(config.tscDir, 'packages', 'qwik', 'src', 'core', 'index.js'), onwarn: rollupOnWarn, - external: ['@builder.io/qwik/build'], + external: ['@builder.io/qwik/build', '@builder.io/qwik/preloader'], plugins: [ { name: 'setVersion', @@ -57,6 +57,8 @@ async function submoduleCoreProd(config: BuildConfig) { sourcemap: true, globals: { '@builder.io/qwik/build': 'qwikBuild', + // not actually used + '@builder.io/qwik/preloader': 'qwikPreloader', }, banner: getBanner('@builder.io/qwik', config.distVersion), }; @@ -69,6 +71,7 @@ async function submoduleCoreProd(config: BuildConfig) { const inputCore = join(config.distQwikPkgDir, 'core.mjs'); const inputMin: InputOptions = { + external: ['@builder.io/qwik/preloader'], input: inputCore, onwarn: rollupOnWarn, plugins: [ @@ -221,20 +224,24 @@ async function submoduleCoreDev(config: BuildConfig) { }, }; - const esm = build({ + const esm = await build({ ...opts, - external: ['@builder.io/qwik/build'], + external: ['@builder.io/qwik/build', '@builder.io/qwik/preloader'], format: 'esm', outExtension: { '.js': '.mjs' }, }); + // We do a CJS build, only for the repl service worker const cjs = build({ ...opts, // we don't externalize qwik build because then the repl service worker sees require() define: { ...opts.define, + // We need to get rid of the import.meta.env values // Vite's base url 'import.meta.env.BASE_URL': '"globalThis.BASE_URL||\'/\'"', + // Vite's devserver mode + 'import.meta.env.DEV': 'false', }, format: 'cjs', outExtension: { '.js': '.cjs' }, diff --git a/scripts/submodule-preloader.ts b/scripts/submodule-preloader.ts new file mode 100644 index 00000000000..14559aed050 --- /dev/null +++ b/scripts/submodule-preloader.ts @@ -0,0 +1,43 @@ +import { join } from 'node:path'; +import { build } from 'vite'; +import { fileSize, type BuildConfig } from './util'; + +/** + * Builds the qwikloader javascript files using Vite. These files can be used by other tooling, and + * are provided in the package so CDNs could point to them. The @builder.io/optimizer submodule also + * provides a utility function. + */ +export async function submodulePreloader(config: BuildConfig) { + await build({ + build: { + lib: { + entry: join(config.srcQwikDir, 'core/preloader'), + formats: ['es'], + fileName: () => 'preloader.mjs', + }, + rollupOptions: { + external: ['@builder.io/qwik/build'], + }, + minify: 'terser', + terserOptions: { + compress: { + dead_code: true, + unused: true, + conditionals: true, + }, + mangle: { + toplevel: false, + module: false, + keep_fnames: true, + properties: { + regex: '^\\$.+\\$$', + }, + }, + }, + outDir: config.distQwikPkgDir, + }, + }); + + const preloaderSize = await fileSize(join(config.distQwikPkgDir, 'preloader.mjs')); + console.log(`🐮 preloader:`, preloaderSize); +} diff --git a/scripts/submodule-qwikprefetch.ts b/scripts/submodule-qwikprefetch.ts deleted file mode 100644 index 0bc74185d90..00000000000 --- a/scripts/submodule-qwikprefetch.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { type InputOptions, type OutputOptions, rollup } from 'rollup'; -import { - type BuildConfig, - ensureDir, - type PackageJSON, - readFile, - rollupOnWarn, - terser, - writeFile, -} from './util'; -import { join } from 'node:path'; -import { transform } from 'esbuild'; -import { writePackageJson } from './package-json'; - -/** - * Builds the qwikprefetch javascript files. These files can be used by other tooling, and are - * provided in the package so CDNs could point to them. The @builder.io/optimizer submodule also - * provides a utility function. - */ -export async function submoduleQwikPrefetch(config: BuildConfig) { - const prefetchSwDir = join(config.srcQwikDir, 'prefetch-service-worker'); - const input: InputOptions = { - input: join(prefetchSwDir, 'entry.ts'), - plugins: [ - { - name: 'qwikPrefetchTranspile', - resolveId(id) { - if (!id.endsWith('.ts')) { - return join(prefetchSwDir, id + '.ts'); - } - return null; - }, - async transform(code, id) { - const result = await transform(code, { - sourcefile: id, - target: 'es2017', - format: 'esm', - loader: 'ts', - }); - return result.code; - }, - }, - ], - onwarn: rollupOnWarn, - }; - - const defaultMinified: OutputOptions = { - // QWIK_PREFETCH_DEFAULT_MINIFIED - dir: config.distQwikPkgDir, - format: 'es', - entryFileNames: `qwik-prefetch.js`, - exports: 'none', - intro: `(()=>{`, - outro: `})()`, - plugins: [ - terser({ - compress: { - global_defs: { - 'window.BuildEvents': false, - }, - keep_fargs: false, - unsafe: true, - passes: 2, - }, - mangle: { - toplevel: true, - module: true, - properties: { - regex: '^\\$.+\\$$', - }, - }, - format: { - comments: /@vite/, - }, - }), - ], - }; - - const defaultDebug: OutputOptions = { - // QWIK_PREFETCH_DEFAULT_DEBUG - dir: config.distQwikPkgDir, - format: 'es', - entryFileNames: `qwik-prefetch.debug.js`, - exports: 'none', - intro: `(()=>{`, - outro: `})()`, - plugins: [ - terser({ - compress: { - global_defs: { - 'window.BuildEvents': false, - }, - inline: false, - join_vars: false, - loops: false, - sequences: false, - }, - format: { - comments: true, - beautify: true, - braces: true, - }, - mangle: false, - }), - ], - }; - - const build = await rollup(input); - - await Promise.all([build.write(defaultMinified), build.write(defaultDebug)]); - - await generatePrefetchSubmodule(config); -} - -/** Load each of the qwik scripts to be inlined with esbuild "define" as const variables. */ -export async function inlineQwikScriptsEsBuild(config: BuildConfig) { - const variableToFileMap = [ - ['QWIK_PREFETCH_DEFAULT_MINIFIED', 'qwik-prefetch.js'], - ['QWIK_PREFETCH_DEFAULT_DEBUG', 'qwik-prefetch.debug.js'], - ]; - - const define: { [varName: string]: string } = {}; - - await Promise.all( - variableToFileMap.map(async (varToFile) => { - const varName = `globalThis.${varToFile[0]}`; - const filePath = join(config.distQwikPkgDir, varToFile[1]); - const content = await readFile(filePath, 'utf-8'); - define[varName] = JSON.stringify(content.trim()); - }) - ); - - return define; -} - -async function generatePrefetchSubmodule(config: BuildConfig) { - const prefetchDistDir = join(config.distQwikPkgDir, 'prefetch'); - - const prefetchCode = await readFile(join(config.distQwikPkgDir, 'qwik-prefetch.js'), 'utf-8'); - const prefetchDebugCode = await readFile( - join(config.distQwikPkgDir, 'qwik-prefetch.debug.js'), - 'utf-8' - ); - - const code = [ - `const QWIK_PREFETCH = ${JSON.stringify(prefetchCode.trim())};`, - `const QWIK_PREFETCH_DEBUG = ${JSON.stringify(prefetchDebugCode.trim())};`, - ]; - - const esmCode = [...code, `export { QWIK_PREFETCH, QWIK_PREFETCH_DEBUG };`]; - const cjsCode = [ - ...code, - `exports.QWIK_PREFETCH = QWIK_PREFETCH;`, - `exports.QWIK_PREFETCH_DEBUG = QWIK_PREFETCH_DEBUG;`, - ]; - const dtsCode = [ - `export declare const QWIK_PREFETCH: string;`, - `export declare const QWIK_PREFETCH_DEBUG: string;`, - ]; - - ensureDir(prefetchDistDir); - await writeFile(join(prefetchDistDir, 'index.mjs'), esmCode.join('\n') + '\n'); - await writeFile(join(prefetchDistDir, 'index.cjs'), cjsCode.join('\n') + '\n'); - await writeFile(join(prefetchDistDir, 'index.d.ts'), dtsCode.join('\n') + '\n'); - - const prefetchPkg: PackageJSON = { - name: `@builder.io/qwik/prefetch`, - version: config.distVersion, - main: `index.mjs`, - types: `index.d.ts`, - private: true, - type: 'module', - }; - await writePackageJson(prefetchDistDir, prefetchPkg); -} diff --git a/scripts/submodule-server.ts b/scripts/submodule-server.ts index e0b1828822f..e97d28da0e7 100644 --- a/scripts/submodule-server.ts +++ b/scripts/submodule-server.ts @@ -27,6 +27,8 @@ export async function submoduleServer(config: BuildConfig) { external: [ /* no Node.js built-in externals allowed! */ '@builder.io/qwik-dom', '@builder.io/qwik/build', + '@builder.io/qwik/preloader', + '@qwik-client-manifest', ], }; @@ -69,6 +71,11 @@ export async function submoduleServer(config: BuildConfig) { 'globalThis.IS_ESM': 'false', 'globalThis.QWIK_VERSION': JSON.stringify(config.distVersion), 'globalThis.QWIK_DOM_VERSION': JSON.stringify(qwikDomVersion), + // We need to get rid of the import.meta.env values + // Vite's base url + 'import.meta.env.BASE_URL': '"globalThis.BASE_URL||\'/\'"', + // Vite's devserver mode + 'import.meta.env.DEV': 'false', }, }); @@ -129,6 +136,9 @@ if (typeof require !== 'function' && typeof location !== 'undefined' && typeof n } return self.qwikBuild; } + if (path === '@qwik-client-manifest') { + return {}; + } throw new Error('Unable to require() path "' + path + '" from a browser environment.'); }; }`; diff --git a/scripts/util.ts b/scripts/util.ts index a06732c1718..7da8724206c 100644 --- a/scripts/util.ts +++ b/scripts/util.ts @@ -283,8 +283,9 @@ export async function run( } } -export function panic(msg: string) { - console.error(`\nāŒ ${msg}\n`, new Error(msg).stack); +export function panic(msg: string | Error) { + const err = typeof msg === 'string' ? new Error(msg) : msg; + console.error(`\nāŒ `, err); process.exit(1); } diff --git a/starters/adapters/aws-lambda/src/entry_aws-lambda.tsx b/starters/adapters/aws-lambda/src/entry_aws-lambda.tsx index c224994f42a..5bb7a06b51f 100755 --- a/starters/adapters/aws-lambda/src/entry_aws-lambda.tsx +++ b/starters/adapters/aws-lambda/src/entry_aws-lambda.tsx @@ -14,14 +14,13 @@ import { type PlatformAwsLambda, } from "@builder.io/qwik-city/middleware/aws-lambda"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformAwsLambda {} } -export const { handle } = createQwikCity({ render, qwikCityPlan, manifest }); +export const { handle } = createQwikCity({ render, qwikCityPlan }); export const qwikApp = serverless({ handle }, { binary: true }); // handler is the default export for the lambda functions diff --git a/starters/adapters/azure-swa/src/entry.azure-swa.tsx b/starters/adapters/azure-swa/src/entry.azure-swa.tsx index 24845399608..96772095ea0 100644 --- a/starters/adapters/azure-swa/src/entry.azure-swa.tsx +++ b/starters/adapters/azure-swa/src/entry.azure-swa.tsx @@ -12,11 +12,10 @@ import { type PlatformAzure, } from "@builder.io/qwik-city/middleware/azure-swa"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformAzure {} } -export default createQwikCity({ render, qwikCityPlan, manifest }); +export default createQwikCity({ render, qwikCityPlan }); diff --git a/starters/adapters/bun/src/entry.bun.ts b/starters/adapters/bun/src/entry.bun.ts index 48859260f24..c9fdfb512dd 100644 --- a/starters/adapters/bun/src/entry.bun.ts +++ b/starters/adapters/bun/src/entry.bun.ts @@ -10,14 +10,12 @@ */ import { createQwikCity } from "@builder.io/qwik-city/middleware/bun"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; // Create the Qwik City Bun middleware const { router, notFound, staticFile } = createQwikCity({ render, qwikCityPlan, - manifest, }); // Allow for dynamic port diff --git a/starters/adapters/cloud-run/src/entry.cloud-run.tsx b/starters/adapters/cloud-run/src/entry.cloud-run.tsx index 4c0437f133c..ea46c1f591a 100644 --- a/starters/adapters/cloud-run/src/entry.cloud-run.tsx +++ b/starters/adapters/cloud-run/src/entry.cloud-run.tsx @@ -12,7 +12,6 @@ import { type PlatformNode, } from "@builder.io/qwik-city/middleware/node"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import { createServer } from "node:http"; import render from "./entry.ssr"; @@ -53,7 +52,6 @@ const DEFAULT_HEADERS = { const { router, notFound, staticFile } = createQwikCity({ render, qwikCityPlan, - manifest, static: { cacheControl: "public, max-age=31557600", }, diff --git a/starters/adapters/cloudflare-pages/src/entry.cloudflare-pages.tsx b/starters/adapters/cloudflare-pages/src/entry.cloudflare-pages.tsx index 510a6309229..0f3b524f489 100644 --- a/starters/adapters/cloudflare-pages/src/entry.cloudflare-pages.tsx +++ b/starters/adapters/cloudflare-pages/src/entry.cloudflare-pages.tsx @@ -12,13 +12,12 @@ import { type PlatformCloudflarePages, } from "@builder.io/qwik-city/middleware/cloudflare-pages"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformCloudflarePages {} } -const fetch = createQwikCity({ render, qwikCityPlan, manifest }); +const fetch = createQwikCity({ render, qwikCityPlan }); export { fetch }; diff --git a/starters/adapters/deno/src/entry.deno.ts b/starters/adapters/deno/src/entry.deno.ts index 96a4d5d2331..533145fae5b 100644 --- a/starters/adapters/deno/src/entry.deno.ts +++ b/starters/adapters/deno/src/entry.deno.ts @@ -10,14 +10,12 @@ */ import { createQwikCity } from "@builder.io/qwik-city/middleware/deno"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; // Create the Qwik City Deno middleware const { router, notFound, staticFile } = createQwikCity({ render, qwikCityPlan, - manifest, }); // Allow for dynamic port diff --git a/starters/adapters/express/src/entry.express.tsx b/starters/adapters/express/src/entry.express.tsx index adce1df37d8..4fe1c3a3732 100644 --- a/starters/adapters/express/src/entry.express.tsx +++ b/starters/adapters/express/src/entry.express.tsx @@ -13,7 +13,6 @@ import { } from "@builder.io/qwik-city/middleware/node"; import "dotenv/config"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; import express from "express"; import { fileURLToPath } from "node:url"; @@ -34,7 +33,6 @@ const PORT = process.env.PORT ?? 3000; const { router, notFound } = createQwikCity({ render, qwikCityPlan, - manifest, // getOrigin(req) { // // If deploying under a proxy, you may need to build the origin from the request headers // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto diff --git a/starters/adapters/firebase/src/entry-firebase.tsx b/starters/adapters/firebase/src/entry-firebase.tsx index 5b76d90f337..5ceb6ad2731 100755 --- a/starters/adapters/firebase/src/entry-firebase.tsx +++ b/starters/adapters/firebase/src/entry-firebase.tsx @@ -12,11 +12,10 @@ import { type PlatformFirebase, } from "@builder.io/qwik-city/middleware/firebase"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformFirebase {} } -export default createQwikCity({ render, qwikCityPlan, manifest }); +export default createQwikCity({ render, qwikCityPlan }); diff --git a/starters/adapters/netlify-edge/src/entry.netlify-edge.tsx b/starters/adapters/netlify-edge/src/entry.netlify-edge.tsx index 0f1d56b983e..1c2cb9e3aaf 100644 --- a/starters/adapters/netlify-edge/src/entry.netlify-edge.tsx +++ b/starters/adapters/netlify-edge/src/entry.netlify-edge.tsx @@ -12,11 +12,10 @@ import { type PlatformNetlify, } from "@builder.io/qwik-city/middleware/netlify-edge"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformNetlify {} } -export default createQwikCity({ render, qwikCityPlan, manifest }); +export default createQwikCity({ render, qwikCityPlan }); diff --git a/starters/adapters/node-server/src/entry.node-server.tsx b/starters/adapters/node-server/src/entry.node-server.tsx index 2117664097c..2f9ac8cbe9d 100644 --- a/starters/adapters/node-server/src/entry.node-server.tsx +++ b/starters/adapters/node-server/src/entry.node-server.tsx @@ -10,7 +10,6 @@ import { createQwikCity } from "@builder.io/qwik-city/middleware/node"; import qwikCityPlan from "@qwik-city-plan"; import render from "./entry.ssr"; -import { manifest } from "@qwik-client-manifest"; import { createServer } from "node:http"; // Allow for dynamic port @@ -20,7 +19,6 @@ const PORT = process.env.PORT ?? 3004; const { router, notFound, staticFile } = createQwikCity({ render, qwikCityPlan, - manifest, }); const server = createServer(); diff --git a/starters/adapters/vercel-edge/src/entry.vercel-edge.tsx b/starters/adapters/vercel-edge/src/entry.vercel-edge.tsx index e96dcabc4d0..564ec04f4da 100644 --- a/starters/adapters/vercel-edge/src/entry.vercel-edge.tsx +++ b/starters/adapters/vercel-edge/src/entry.vercel-edge.tsx @@ -12,11 +12,10 @@ import { type PlatformVercel, } from "@builder.io/qwik-city/middleware/vercel-edge"; import qwikCityPlan from "@qwik-city-plan"; -import { manifest } from "@qwik-client-manifest"; import render from "./entry.ssr"; declare global { interface QwikCityPlatform extends PlatformVercel {} } -export default createQwikCity({ render, qwikCityPlan, manifest }); +export default createQwikCity({ render, qwikCityPlan }); diff --git a/starters/apps/base/src/entry.ssr.tsx b/starters/apps/base/src/entry.ssr.tsx index e3de501302b..53210851238 100644 --- a/starters/apps/base/src/entry.ssr.tsx +++ b/starters/apps/base/src/entry.ssr.tsx @@ -14,12 +14,10 @@ import { renderToStream, type RenderToStreamOptions, } from "@builder.io/qwik/server"; -import { manifest } from "@qwik-client-manifest"; import Root from "./root"; export default function (opts: RenderToStreamOptions) { return renderToStream(, { - manifest, ...opts, // Use container attributes to set attributes on the html tag. containerAttributes: { diff --git a/starters/apps/base/src/routes/service-worker.ts b/starters/apps/base/src/routes/service-worker.ts deleted file mode 100644 index 3ab13b6b6af..00000000000 --- a/starters/apps/base/src/routes/service-worker.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * WHAT IS THIS FILE? - * - * The service-worker.ts file is used to have state of the art prefetching. - * https://qwik.dev/qwikcity/prefetching/overview/ - * - * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline. - * You can also use this file to add more functionality that runs in the service worker. - */ -import { setupServiceWorker } from "@builder.io/qwik-city/service-worker"; - -setupServiceWorker(); - -self.addEventListener("install", () => self.skipWaiting()); -self.addEventListener("activate", (ev) => ev.waitUntil(self.clients.claim())); - -declare const self: ServiceWorkerGlobalScope; diff --git a/starters/apps/empty/src/root.tsx b/starters/apps/empty/src/root.tsx index f87bd23766a..07dad6cad9a 100644 --- a/starters/apps/empty/src/root.tsx +++ b/starters/apps/empty/src/root.tsx @@ -1,11 +1,6 @@ -import { component$ } from "@builder.io/qwik"; -import { - QwikCityProvider, - RouterOutlet, - ServiceWorkerRegister, -} from "@builder.io/qwik-city"; +import { component$, isDev } from "@builder.io/qwik"; +import { QwikCityProvider, RouterOutlet } from "@builder.io/qwik-city"; import { RouterHead } from "./components/router-head/router-head"; -import { isDev } from "@builder.io/qwik"; import "./global.css"; @@ -31,7 +26,6 @@ export default component$(() => { - {!isDev && } ); diff --git a/starters/apps/empty/src/routes/service-worker.ts b/starters/apps/empty/src/routes/service-worker.ts deleted file mode 100644 index a10ab364265..00000000000 --- a/starters/apps/empty/src/routes/service-worker.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * WHAT IS THIS FILE? - * - * The service-worker.ts file is used to have state of the art prefetching. - * https://qwik.dev/qwikcity/prefetching/overview/ - * - * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline. - * You can also use this file to add more functionality that runs in the service worker. - */ -import { setupServiceWorker } from "@builder.io/qwik-city/service-worker"; - -setupServiceWorker(); - -addEventListener("install", () => self.skipWaiting()); - -addEventListener("activate", () => self.clients.claim()); - -declare const self: ServiceWorkerGlobalScope; diff --git a/starters/apps/library/src/entry.ssr.tsx b/starters/apps/library/src/entry.ssr.tsx index 3e6b49f7f25..4e13a960f62 100644 --- a/starters/apps/library/src/entry.ssr.tsx +++ b/starters/apps/library/src/entry.ssr.tsx @@ -14,12 +14,8 @@ import { renderToStream, type RenderToStreamOptions, } from "@builder.io/qwik/server"; -import { manifest } from "@qwik-client-manifest"; import Root from "./root"; export default function (opts: RenderToStreamOptions) { - return renderToStream(, { - manifest, - ...opts, - }); + return renderToStream(, opts); } diff --git a/starters/apps/perf.prod/src/entry.ssr.tsx b/starters/apps/perf.prod/src/entry.ssr.tsx index 157aef038bf..ff7cd0aa4db 100644 --- a/starters/apps/perf.prod/src/entry.ssr.tsx +++ b/starters/apps/perf.prod/src/entry.ssr.tsx @@ -1,5 +1,4 @@ import { renderToStream, RenderToStreamOptions } from "@builder.io/qwik/server"; -import { manifest } from "@qwik-client-manifest"; import Root from "./root"; /** @@ -8,8 +7,5 @@ import Root from "./root"; export default function (opts: RenderToStreamOptions) { // Render the Root component to a string // Pass in the manifest that was generated from the client build - return renderToStream(, { - manifest, - ...opts, - }); + return renderToStream(, opts); } diff --git a/starters/apps/perf.prod/vite.config.mts b/starters/apps/perf.prod/vite.config.mts new file mode 100644 index 00000000000..62f73900702 --- /dev/null +++ b/starters/apps/perf.prod/vite.config.mts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import { qwikVite } from "@builder.io/qwik/optimizer"; +export default defineConfig({ + plugins: [ + qwikVite({ + debug: true, + }), + ], + build: { + minify: true, + }, +}); diff --git a/starters/apps/playground/src/root.tsx b/starters/apps/playground/src/root.tsx index b1cddd70d44..7dc17045c96 100644 --- a/starters/apps/playground/src/root.tsx +++ b/starters/apps/playground/src/root.tsx @@ -1,10 +1,6 @@ import { component$ } from "@builder.io/qwik"; import { isDev } from "@builder.io/qwik"; -import { - QwikCityProvider, - RouterOutlet, - ServiceWorkerRegister, -} from "@builder.io/qwik-city"; +import { QwikCityProvider, RouterOutlet } from "@builder.io/qwik-city"; import { RouterHead } from "./components/router-head/router-head"; import "./global.css"; @@ -28,7 +24,6 @@ export default component$(() => { /> )} - {!isDev && } diff --git a/starters/apps/playground/src/routes/service-worker.ts b/starters/apps/playground/src/routes/service-worker.ts deleted file mode 100644 index a10ab364265..00000000000 --- a/starters/apps/playground/src/routes/service-worker.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * WHAT IS THIS FILE? - * - * The service-worker.ts file is used to have state of the art prefetching. - * https://qwik.dev/qwikcity/prefetching/overview/ - * - * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline. - * You can also use this file to add more functionality that runs in the service worker. - */ -import { setupServiceWorker } from "@builder.io/qwik-city/service-worker"; - -setupServiceWorker(); - -addEventListener("install", () => self.skipWaiting()); - -addEventListener("activate", () => self.clients.claim()); - -declare const self: ServiceWorkerGlobalScope; diff --git a/starters/apps/preloader-test/README.md b/starters/apps/preloader-test/README.md new file mode 100644 index 00000000000..54b8a46360b --- /dev/null +++ b/starters/apps/preloader-test/README.md @@ -0,0 +1,5 @@ +This is the test app for the preloader. + +Use `pnpm preview` to run the app and observe the network requests and the console output. + +The modules have been configured to have a random size between 0.5kb and 50kb, and when they load they will log a message to the console and add their id to the `_loaded` array. diff --git a/starters/apps/preloader-test/package.json b/starters/apps/preloader-test/package.json new file mode 100644 index 00000000000..9e8b42885d5 --- /dev/null +++ b/starters/apps/preloader-test/package.json @@ -0,0 +1,14 @@ +{ + "name": "preloader-test", + "scripts": { + "build": "qwik build", + "build.client": "vite build", + "build.preview": "vite build --ssr src/entry.preview.tsx", + "dev": "vite --mode ssr", + "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", + "preview": "qwik build preview && vite preview --open", + "start": "vite --open --mode ssr" + }, + "private": true, + "type": "module" +} diff --git a/starters/apps/preloader-test/src/components/router-head/router-head.tsx b/starters/apps/preloader-test/src/components/router-head/router-head.tsx new file mode 100644 index 00000000000..fb0870aee1f --- /dev/null +++ b/starters/apps/preloader-test/src/components/router-head/router-head.tsx @@ -0,0 +1,47 @@ +import { component$ } from "@builder.io/qwik"; +import { useDocumentHead, useLocation } from "@builder.io/qwik-city"; + +/** + * The RouterHead component is placed inside of the document `` element. + */ +export const RouterHead = component$(() => { + const head = useDocumentHead(); + const loc = useLocation(); + + return ( + <> + {head.title} + + + + + {head.meta.map((m) => ( + + ))} + + {head.links.map((l) => ( + + ))} + + {head.styles.map((s) => ( +