diff --git a/.changeset/empty-sites-tie.md b/.changeset/empty-sites-tie.md new file mode 100644 index 00000000000..fd16386ebf5 --- /dev/null +++ b/.changeset/empty-sites-tie.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik': patch +--- + +:zap: the qwikloader is no longer embedded in the SSR results. Instead, the same techniques are used as for the preloader to ensure that the qwikloader is active as soon as possible, loaded from a separate bundle. This reduces SSR page size by several kB end ensures that subsequent qwikloader loads are nearly instant. diff --git a/packages/docs/src/entry.ssr.tsx b/packages/docs/src/entry.ssr.tsx index 62ba15a7119..0c9922c47d2 100644 --- a/packages/docs/src/entry.ssr.tsx +++ b/packages/docs/src/entry.ssr.tsx @@ -34,10 +34,6 @@ export default function (opts: RenderToStreamOptions) { } } return renderToStream(, { - qwikLoader: { - // The docs can be long so make sure to intercept events before the end of the document. - position: 'top', - }, ...opts, containerAttributes: { lang: 'en', diff --git a/packages/docs/src/repl/bundled.tsx b/packages/docs/src/repl/bundled.tsx index 5c4626a8935..bf56057f5c6 100644 --- a/packages/docs/src/repl/bundled.tsx +++ b/packages/docs/src/repl/bundled.tsx @@ -15,6 +15,8 @@ import qCoreMinMjs from '../../node_modules/@builder.io/qwik/dist/core.min.mjs?r 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'; +// we use the debug version for the repl so it's understandable +import qQwikLoaderJs from '../../node_modules/@builder.io/qwik/dist/qwikloader.debug.js?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'; @@ -57,6 +59,7 @@ export const bundled: PkgUrls = { '/dist/server.cjs': qServerCjs, '/dist/server.d.ts': qServerDts, '/dist/preloader.mjs': qPreloaderMjs, + '/dist/qwikloader.js': qQwikLoaderJs, '/bindings/qwik.wasm.cjs': qWasmCjs, '/bindings/qwik_wasm_bg.wasm': qWasmBinUrl, }, diff --git a/packages/docs/src/repl/worker/repl-plugins.ts b/packages/docs/src/repl/worker/repl-plugins.ts index 3866f685e84..50f82b9fa01 100644 --- a/packages/docs/src/repl/worker/repl-plugins.ts +++ b/packages/docs/src/repl/worker/repl-plugins.ts @@ -78,7 +78,13 @@ export const replResolver = (options: ReplInputOptions, buildMode: 'client' | 's return rsp.text(); } } - + // this id is unchanged because it's an entry point + if (id === '@builder.io/qwik/qwikloader.js') { + const rsp = await depResponse('@builder.io/qwik', '/qwikloader.js'); + if (rsp) { + return rsp.text(); + } + } // We're the fallback, we know all the files if (/\.[jt]sx?$/.test(id)) { throw new Error(`load: unknown module ${id}`); diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index ae12dba8caf..686ac48fe8c 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -465,7 +465,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\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[assets?](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikAsset](#qwikasset); }\n\n\n\n\n_(Optional)_ All assets. The key is the fileName relative to the rootDir\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[bundleGraphAsset?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The bundle graph fileName\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. The key is the bundle fileName relative to \"build/\"\n\n\n
\n\n[core?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The Qwik core bundle fileName\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. The key is the symbol name, the value is the bundle fileName\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
", + "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\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[assets?](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikAsset](#qwikasset); }\n\n\n\n\n_(Optional)_ All assets. The key is the fileName relative to the rootDir\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[bundleGraphAsset?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The bundle graph fileName\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. The key is the bundle fileName relative to \"build/\"\n\n\n
\n\n[core?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The Qwik core bundle fileName\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. The key is the symbol name, the value is the bundle fileName\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[qwikLoader?](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The Qwik loader 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" }, @@ -667,7 +667,7 @@ } ], "kind": "TypeAlias", - "content": "The manifest values that are needed for SSR.\n\n\n```typescript\nexport type ServerQwikManifest = Pick;\n```\n**References:** [QwikManifest](#qwikmanifest)", + "content": "The manifest values that are needed for SSR.\n\n\n```typescript\nexport type ServerQwikManifest = Pick;\n```\n**References:** [QwikManifest](#qwikmanifest)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.serverqwikmanifest.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.mdx b/packages/docs/src/routes/api/qwik-optimizer/index.mdx index e8391d82c60..7fd0e49e53e 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.mdx +++ b/packages/docs/src/routes/api/qwik-optimizer/index.mdx @@ -1640,6 +1640,21 @@ _(Optional)_ The preloader bundle fileName +[qwikLoader?](#) + + + + + +string + + + +_(Optional)_ The Qwik loader bundle fileName + + + + [symbols](#) @@ -2764,6 +2779,7 @@ export type ServerQwikManifest = Pick< | "mapping" | "preloader" | "core" + | "qwikLoader" >; ``` diff --git a/packages/docs/src/routes/api/qwik-server/api.json b/packages/docs/src/routes/api/qwik-server/api.json index 7e5b9051f90..e518919e2fb 100644 --- a/packages/docs/src/routes/api/qwik-server/api.json +++ b/packages/docs/src/routes/api/qwik-server/api.json @@ -138,7 +138,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikLoaderOptions \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[include?](#)\n\n\n\n\n\n\n\n'always' \\| 'never' \\| 'auto'\n\n\n\n\n_(Optional)_\n\n\n
\n\n[position?](#)\n\n\n\n\n\n\n\n'top' \\| 'bottom'\n\n\n\n\n_(Optional)_\n\n\n
", + "content": "```typescript\nexport interface QwikLoaderOptions \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[include?](#)\n\n\n\n\n\n\n\n'always' \\| 'never' \\| 'auto'\n\n\n\n\n_(Optional)_ Whether to include the qwikloader script in the document. Normally you don't need to worry about this, but in case of multi-container apps using different Qwik versions, you might want to only enable it on one of the containers.\n\nDefaults to `'auto'`.\n\n\n
\n\n[position?](#)\n\n\n\n\n\n\n\n'top' \\| 'bottom'\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.qwikloaderoptions.md" }, diff --git a/packages/docs/src/routes/api/qwik-server/index.mdx b/packages/docs/src/routes/api/qwik-server/index.mdx index f698bf7f02f..0ac981f413b 100644 --- a/packages/docs/src/routes/api/qwik-server/index.mdx +++ b/packages/docs/src/routes/api/qwik-server/index.mdx @@ -575,7 +575,9 @@ Description -_(Optional)_ +_(Optional)_ Whether to include the qwikloader script in the document. Normally you don't need to worry about this, but in case of multi-container apps using different Qwik versions, you might want to only enable it on one of the containers. + +Defaults to `'auto'`. diff --git a/packages/qwik/src/optimizer/src/manifest.ts b/packages/qwik/src/optimizer/src/manifest.ts index dbedff8bbad..0653a6528f2 100644 --- a/packages/qwik/src/optimizer/src/manifest.ts +++ b/packages/qwik/src/optimizer/src/manifest.ts @@ -486,6 +486,10 @@ export function generateManifestFromBundles( manifest.preloader = bundleFileName; } else if (modulePaths.some((m) => /[/\\]qwik[/\\]dist[/\\]core\.[^/]*js$/.test(m))) { manifest.core = bundleFileName; + } else if ( + modulePaths.some((m) => /[/\\]qwik[/\\]dist[/\\]qwikloader(\.debug)?\.[^/]*js$/.test(m)) + ) { + manifest.qwikLoader = bundleFileName; } } diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index 97676c274eb..a196bad64df 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -407,6 +407,19 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) { debug(`transformedOutputs.clear()`); clientTransformedOutputs.clear(); serverTransformedOutputs.clear(); + + if (opts.target === 'client') { + const ql = await _ctx.resolve('@builder.io/qwik/qwikloader.js', undefined, { + skipSelf: true, + }); + if (ql) { + _ctx.emitFile({ + id: ql.id, + type: 'chunk', + preserveSignature: 'allow-extension', + }); + } + } }; const getIsServer = (viteOpts?: { ssr?: boolean }) => { @@ -880,12 +893,13 @@ export const isDev = ${JSON.stringify(isDev)}; if (manifest?.manifestHash) { serverManifest = { manifestHash: manifest.manifestHash, - injections: manifest.injections, - bundleGraph: manifest.bundleGraph, - mapping: manifest.mapping, - preloader: manifest.preloader, core: manifest.core, + preloader: manifest.preloader, + qwikLoader: manifest.qwikLoader, bundleGraphAsset: manifest.bundleGraphAsset, + injections: manifest.injections, + mapping: manifest.mapping, + bundleGraph: manifest.bundleGraph, }; } return `// @qwik-client-manifest @@ -924,11 +938,12 @@ export const manifest = ${JSON.stringify(serverManifest)};\n`; function manualChunks(id: string, { getModuleInfo }: Rollup.ManualChunkMeta) { // The preloader has to stay in a separate chunk if it's a client build // the vite preload helper must be included or to prevent breaking circular dependencies - if ( - opts.target === 'client' && - (id.endsWith(QWIK_PRELOADER_REAL_ID) || id === '\0vite/preload-helper.js') - ) { - return 'qwik-preloader'; + if (opts.target === 'client') { + if (id.endsWith(QWIK_PRELOADER_REAL_ID) || id === '\0vite/preload-helper.js') { + return 'qwik-preloader'; + } else if (/qwik[\\/]dist[\\/]qwikloader\.js$/.test(id)) { + return 'qwik-loader'; + } } const module = getModuleInfo(id)!; diff --git a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md index d2027831a98..6030200d353 100644 --- a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md +++ b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md @@ -213,6 +213,7 @@ export interface QwikManifest { [name: string]: string; }; preloader?: string; + qwikLoader?: string; symbols: { [symbolName: string]: QwikSymbol; }; @@ -362,7 +363,7 @@ export { SegmentEntryStrategy as HookEntryStrategy } export { SegmentEntryStrategy } // @public -export type ServerQwikManifest = Pick; +export type ServerQwikManifest = Pick; // @public (undocumented) export interface SingleEntryStrategy { diff --git a/packages/qwik/src/optimizer/src/types.ts b/packages/qwik/src/optimizer/src/types.ts index 10b7bcbf7e5..738c0392f65 100644 --- a/packages/qwik/src/optimizer/src/types.ts +++ b/packages/qwik/src/optimizer/src/types.ts @@ -236,6 +236,8 @@ export interface QwikManifest { preloader?: string; /** The Qwik core bundle fileName */ core?: string; + /** The Qwik loader bundle fileName */ + qwikLoader?: string; /** CSS etc to inject in the document head */ injections?: GlobalInjections[]; /** The version of the manifest */ @@ -263,6 +265,7 @@ export type ServerQwikManifest = Pick< | 'mapping' | 'preloader' | 'core' + | 'qwikLoader' >; /** diff --git a/packages/qwik/src/qwikloader.ts b/packages/qwik/src/qwikloader.ts index 30fc79fb234..3f13bfbf45d 100644 --- a/packages/qwik/src/qwikloader.ts +++ b/packages/qwik/src/qwikloader.ts @@ -23,275 +23,297 @@ type qWindow = Window & { * @param doc - Document to use for setting up global listeners, and to determine all the browser * supported events. */ -(() => { - const doc = document as Document & { __q_context__?: [Element, Event, URL] | 0 }; - const win = window as unknown as qWindow; - const events = new Set(); - const roots = new Set([doc]); +const doc = document as Document & { __q_context__?: [Element, Event, URL] | 0 }; +const win = window as unknown as qWindow; +const events = new Set(); +const roots = new Set([doc]); - let hasInitialized: number; +let hasInitialized: number; - const nativeQuerySelectorAll = (root: ParentNode, selector: string) => - Array.from(root.querySelectorAll(selector)); - const querySelectorAll = (query: string) => { - const elements: Element[] = []; - roots.forEach((root) => elements.push(...nativeQuerySelectorAll(root, query))); - return elements; - }; - const findShadowRoots = (fragment: EventTarget & ParentNode) => { - processEventOrNode(fragment); - nativeQuerySelectorAll(fragment, '[q\\:shadowroot]').forEach((parent) => { - const shadowRoot = parent.shadowRoot; - shadowRoot && findShadowRoots(shadowRoot); - }); - }; +const nativeQuerySelectorAll = (root: ParentNode, selector: string) => + Array.from(root.querySelectorAll(selector)); +const querySelectorAll = (query: string) => { + const elements: Element[] = []; + roots.forEach((root) => elements.push(...nativeQuerySelectorAll(root, query))); + return elements; +}; +const findShadowRoots = (fragment: EventTarget & ParentNode) => { + processEventOrNode(fragment); + nativeQuerySelectorAll(fragment, '[q\\:shadowroot]').forEach((parent) => { + const shadowRoot = parent.shadowRoot; + shadowRoot && findShadowRoots(shadowRoot); + }); +}; - const isPromise = (promise: Promise) => promise && typeof promise.then === 'function'; +const isPromise = (promise: Promise) => promise && typeof promise.then === 'function'; - const broadcast = (infix: string, ev: Event, type = ev.type) => { - querySelectorAll('[on' + infix + '\\:' + type + ']').forEach((el) => - dispatch(el, infix, ev, type) +// Give a grace period before unregistering the event listener +let doNotClean = true; +const broadcast = (infix: string, ev: Event, type = ev.type) => { + let found = doNotClean; + querySelectorAll('[on' + infix + '\\:' + type + ']').forEach((el) => { + found = true; + dispatch(el, infix, ev, type); + }); + if (!found) { + window[infix.slice(1) as 'window' | 'document'].removeEventListener( + type, + infix === '-window' ? processWindowEvent : processDocumentEvent ); - }; + } +}; - const resolveContainer = (containerEl: QContainerElement) => { - if (containerEl._qwikjson_ === undefined) { - const parentJSON = containerEl === doc.documentElement ? doc.body : containerEl; - let script = parentJSON.lastElementChild; - while (script) { - if (script.tagName === 'SCRIPT' && script.getAttribute('type') === 'qwik/json') { - containerEl._qwikjson_ = JSON.parse( - script.textContent!.replace(/\\x3C(\/?script)/gi, '<$1') - ); - break; - } - script = script.previousElementSibling; +const resolveContainer = (containerEl: QContainerElement) => { + if (containerEl._qwikjson_ === undefined) { + const parentJSON = containerEl === doc.documentElement ? doc.body : containerEl; + let script = parentJSON.lastElementChild; + while (script) { + if (script.tagName === 'SCRIPT' && script.getAttribute('type') === 'qwik/json') { + containerEl._qwikjson_ = JSON.parse( + script.textContent!.replace(/\\x3C(\/?script)/gi, '<$1') + ); + break; } + script = script.previousElementSibling; } - }; + } +}; - const createEvent = (eventName: string, detail?: T['detail']) => - new CustomEvent(eventName, { - detail, - }) as T; +const createEvent = (eventName: string, detail?: T['detail']) => + new CustomEvent(eventName, { + detail, + }) as T; - const dispatch = async ( - element: Element & { _qc_?: QContext | undefined }, - onPrefix: string, - ev: Event, - eventName = ev.type - ) => { - const attrName = 'on' + onPrefix + ':' + eventName; - if (element.hasAttribute('preventdefault:' + eventName)) { - ev.preventDefault(); - } - if (element.hasAttribute('stoppropagation:' + eventName)) { - ev.stopPropagation(); - } - const ctx = element._qc_; - const relevantListeners = ctx && ctx.li.filter((li) => li[0] === attrName); - if (relevantListeners && relevantListeners.length > 0) { - for (const listener of relevantListeners) { - // listener[1] holds the QRL - const results = listener[1].getFn([element, ev], () => element.isConnected)(ev, element); - const cancelBubble = ev.cancelBubble; - if (isPromise(results)) { - await results; - } - // forcing async with await resets ev.cancelBubble to false - if (cancelBubble) { - ev.stopPropagation(); - } +const dispatch = async ( + element: Element & { _qc_?: QContext | undefined }, + onPrefix: string, + ev: Event, + eventName = ev.type +) => { + const attrName = 'on' + onPrefix + ':' + eventName; + if (element.hasAttribute('preventdefault:' + eventName)) { + ev.preventDefault(); + } + if (element.hasAttribute('stoppropagation:' + eventName)) { + ev.stopPropagation(); + } + const ctx = element._qc_; + const relevantListeners = ctx && ctx.li.filter((li) => li[0] === attrName); + if (relevantListeners && relevantListeners.length > 0) { + for (const listener of relevantListeners) { + // listener[1] holds the QRL + const results = listener[1].getFn([element, ev], () => element.isConnected)(ev, element); + const cancelBubble = ev.cancelBubble; + if (isPromise(results)) { + await results; + } + // forcing async with await resets ev.cancelBubble to false + if (cancelBubble) { + ev.stopPropagation(); } - return; } - const attrValue = element.getAttribute(attrName); - if (attrValue) { - const container = element.closest('[q\\:container]')! as QContainerElement; - const qBase = container.getAttribute('q:base')!; - const qVersion = container.getAttribute('q:version') || 'unknown'; - const qManifest = container.getAttribute('q:manifest-hash') || 'dev'; - const base = new URL(qBase, doc.baseURI); - for (const qrl of attrValue.split('\n')) { - const url = new URL(qrl, base); - const href = url.href; - const symbol = url.hash.replace(/^#?([^?[|]*).*$/, '$1') || 'default'; - const reqTime = performance.now(); - let handler: undefined | any; - let importError: undefined | 'sync' | 'async' | 'no-symbol'; - let error: undefined | Error; - const isSync = qrl.startsWith('#'); - const eventData: QwikSymbolEvent['detail'] = { - qBase, - qManifest, - qVersion, - href, - symbol, - element, - reqTime, - }; - if (isSync) { - const hash = container.getAttribute('q:instance')!; - handler = ((doc as any)['qFuncs_' + hash] || [])[Number.parseInt(symbol)]; + return; + } + const attrValue = element.getAttribute(attrName); + if (attrValue) { + const container = element.closest('[q\\:container]')! as QContainerElement; + const qBase = container.getAttribute('q:base')!; + const qVersion = container.getAttribute('q:version') || 'unknown'; + const qManifest = container.getAttribute('q:manifest-hash') || 'dev'; + const base = new URL(qBase, doc.baseURI); + for (const qrl of attrValue.split('\n')) { + const url = new URL(qrl, base); + const href = url.href; + const symbol = url.hash.replace(/^#?([^?[|]*).*$/, '$1') || 'default'; + const reqTime = performance.now(); + let handler: undefined | any; + let importError: undefined | 'sync' | 'async' | 'no-symbol'; + let error: undefined | Error; + const isSync = qrl.startsWith('#'); + const eventData: QwikSymbolEvent['detail'] = { + qBase, + qManifest, + qVersion, + href, + symbol, + element, + reqTime, + }; + if (isSync) { + const hash = container.getAttribute('q:instance')!; + handler = ((doc as any)['qFuncs_' + hash] || [])[Number.parseInt(symbol)]; + if (!handler) { + importError = 'sync'; + error = new Error('sym:' + symbol); + } + } else { + emitEvent('qsymbol', eventData); + const uri = url.href.split('#')[0]; + try { + const module = import(/* @vite-ignore */ uri); + resolveContainer(container); + handler = (await module)[symbol]; if (!handler) { - importError = 'sync'; - error = new Error('sym:' + symbol); - } - } else { - emitEvent('qsymbol', eventData); - const uri = url.href.split('#')[0]; - try { - const module = import(/* @vite-ignore */ uri); - resolveContainer(container); - handler = (await module)[symbol]; - if (!handler) { - importError = 'no-symbol'; - error = new Error(`${symbol} not in ${uri}`); - } - } catch (err) { - importError ||= 'async'; - error = err as Error; + importError = 'no-symbol'; + error = new Error(`${symbol} not in ${uri}`); } + } catch (err) { + importError ||= 'async'; + error = err as Error; } - if (!handler) { - emitEvent('qerror', { - importError, - error, - ...eventData, - }); - console.error(error); - // break out of the loop if handler is not found - break; - } - const previousCtx = doc.__q_context__; - if (element.isConnected) { - try { - doc.__q_context__ = [element, ev, url]; - const results = handler(ev, element); - // only await if there is a promise returned - if (isPromise(results)) { - await results; - } - } catch (error) { - emitEvent('qerror', { error, ...eventData }); - } finally { - doc.__q_context__ = previousCtx; + } + if (!handler) { + emitEvent('qerror', { + importError, + error, + ...eventData, + }); + console.error(error); + // break out of the loop if handler is not found + break; + } + const previousCtx = doc.__q_context__; + if (element.isConnected) { + try { + doc.__q_context__ = [element, ev, url]; + const results = handler(ev, element); + // only await if there is a promise returned + if (isPromise(results)) { + await results; } + } catch (error) { + emitEvent('qerror', { error, ...eventData }); + } finally { + doc.__q_context__ = previousCtx; } } } - }; + } +}; - const emitEvent = (eventName: string, detail?: T['detail']) => { - doc.dispatchEvent(createEvent(eventName, detail)); - }; +const emitEvent = (eventName: string, detail?: T['detail']) => { + doc.dispatchEvent(createEvent(eventName, detail)); +}; - const camelToKebab = (str: string) => str.replace(/([A-Z])/g, (a) => '-' + a.toLowerCase()); +const camelToKebab = (str: string) => str.replace(/([A-Z])/g, (a) => '-' + a.toLowerCase()); - /** - * Event handler responsible for processing browser events. - * - * If browser emits an event, the `eventProcessor` walks the DOM tree looking for corresponding - * `(${event.type})`. If found the event's URL is parsed and `import()`ed. - * - * @param ev - Browser event. - */ - const processDocumentEvent = async (ev: Event) => { - // eslint-disable-next-line prefer-const - let type = camelToKebab(ev.type); - let element = ev.target as Element | null; - broadcast('-document', ev, type); +/** + * Event handler responsible for processing browser events. + * + * If browser emits an event, the `eventProcessor` walks the DOM tree looking for corresponding + * `(${event.type})`. If found the event's URL is parsed and `import()`ed. + * + * @param ev - Browser event. + */ +const processDocumentEvent = async (ev: Event) => { + // eslint-disable-next-line prefer-const + let type = camelToKebab(ev.type); + let element = ev.target as Element | null; + broadcast('-document', ev, type); - while (element && element.getAttribute) { - const results = dispatch(element, '', ev, type); - let cancelBubble = ev.cancelBubble; - if (isPromise(results)) { - await results; - } - // if another async handler stopPropagation - cancelBubble = - cancelBubble || ev.cancelBubble || element.hasAttribute('stoppropagation:' + ev.type); - element = ev.bubbles && cancelBubble !== true ? element.parentElement : null; + while (element && element.getAttribute) { + const results = dispatch(element, '', ev, type); + let cancelBubble = ev.cancelBubble; + if (isPromise(results)) { + await results; } - }; + // if another async handler stopPropagation + cancelBubble ||= + cancelBubble || ev.cancelBubble || element.hasAttribute('stoppropagation:' + ev.type); + element = ev.bubbles && cancelBubble !== true ? element.parentElement : null; + } +}; - const processWindowEvent = (ev: Event) => { - broadcast('-window', ev, camelToKebab(ev.type)); - }; +const processWindowEvent = (ev: Event) => { + broadcast('-window', ev, camelToKebab(ev.type)); +}; - const processReadyStateChange = () => { - const readyState = doc.readyState; - if (!hasInitialized && (readyState == 'interactive' || readyState == 'complete')) { - roots.forEach(findShadowRoots); - // document is ready - hasInitialized = 1; +const processReadyStateChange = () => { + const readyState = doc.readyState; + if (!hasInitialized && (readyState == 'interactive' || readyState == 'complete')) { + roots.forEach(findShadowRoots); + // document is ready + hasInitialized = 1; - emitEvent('qinit'); - const riC = win.requestIdleCallback ?? win.setTimeout; - riC.bind(win)(() => emitEvent('qidle')); + emitEvent('qinit'); + const riC = win.requestIdleCallback ?? win.setTimeout; + riC.bind(win)(() => emitEvent('qidle')); - if (events.has('qvisible')) { - const results = querySelectorAll('[on\\:qvisible]'); - const observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - observer.unobserve(entry.target); - dispatch(entry.target, '', createEvent('qvisible', entry)); - } + if (events.has('qvisible')) { + const results = querySelectorAll('[on\\:qvisible]'); + const observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + observer.unobserve(entry.target); + dispatch(entry.target, '', createEvent('qvisible', entry)); } - }); - results.forEach((el) => observer.observe(el)); - } + } + }); + results.forEach((el) => observer.observe(el)); } - }; + } +}; - const addEventListener = ( - el: EventTarget, - eventName: string, - handler: (ev: Event) => void, - capture = false - ) => { - return el.addEventListener(eventName, handler, { capture, passive: false }); - }; +const addEventListener = ( + el: EventTarget, + eventName: string, + handler: (ev: Event) => void, + capture = false +) => { + el.addEventListener(eventName, handler, { capture, passive: false }); +}; - const processEventOrNode = (...eventNames: (string | (EventTarget & ParentNode))[]) => { - for (const eventNameOrNode of eventNames) { - if (typeof eventNameOrNode === 'string') { - // If it is string we just add the event to window and each of our roots. - if (!events.has(eventNameOrNode)) { - roots.forEach((root) => - addEventListener(root, eventNameOrNode, processDocumentEvent, true) - ); - addEventListener(win, eventNameOrNode, processWindowEvent, true); - events.add(eventNameOrNode); - } - } else { - // If it is a new root, we also need this root to catch up to all of the events so far. - if (!roots.has(eventNameOrNode)) { - events.forEach((eventName) => - addEventListener(eventNameOrNode, eventName, processDocumentEvent, true) - ); - roots.add(eventNameOrNode); - } +let cleanTimer: NodeJS.Timeout; +const processEventOrNode = (...eventNames: (string | (EventTarget & ParentNode))[]) => { + doNotClean = true; + clearTimeout(cleanTimer); + /** + * Give 20s to have nodes appear that use this event. Newly added nodes will have listeners + * attached by the DOM renderer so won't use the qwikloader. + */ + cleanTimer = setTimeout(() => (doNotClean = false), 20_000); + for (const eventNameOrNode of eventNames) { + if (typeof eventNameOrNode === 'string') { + // If it is string we just add the event to window and each of our roots. + if (!events.has(eventNameOrNode)) { + roots.forEach((root) => + addEventListener(root, eventNameOrNode, processDocumentEvent, true) + ); + addEventListener(win, eventNameOrNode, processWindowEvent, true); + events.add(eventNameOrNode); + } + } else { + // If it is a new root, we also need this root to catch up to all of the events so far. + if (!roots.has(eventNameOrNode)) { + events.forEach((eventName) => + addEventListener(eventNameOrNode, eventName, processDocumentEvent, true) + ); + roots.add(eventNameOrNode); } } - }; + } +}; - if (!('__q_context__' in doc)) { - // Mark qwik-loader presence but falsy - doc.__q_context__ = 0; - const qwikevents = win.qwikevents; - // If `qwikEvents` is an array, process it. +// Only the first qwikloader will handle events +if (!('__q_context__' in doc)) { + // Mark qwik-loader presence but falsy + doc.__q_context__ = 0; + const qwikevents = win.qwikevents; + // If `qwikEvents` is an array, process it. + if (qwikevents) { if (Array.isArray(qwikevents)) { processEventOrNode(...qwikevents); + } else { + // Assume that there will probably be click or input listeners + processEventOrNode('click', 'input'); } - // Now rig up `qwikEvents` so we get notified of new registrations by other containers. - win.qwikevents = { - events: events, - roots: roots, - push: processEventOrNode, - }; - addEventListener(doc, 'readystatechange', processReadyStateChange); - processReadyStateChange(); } -})(); + // Now rig up `qwikEvents` so we get notified of new registrations by other containers. + win.qwikevents = { + events: events, + roots: roots, + push: processEventOrNode, + }; + addEventListener(doc, 'readystatechange', processReadyStateChange); + processReadyStateChange(); +} diff --git a/packages/qwik/src/qwikloader.unit.ts b/packages/qwik/src/qwikloader.unit.ts index fb1fe126acb..26858da227f 100644 --- a/packages/qwik/src/qwikloader.unit.ts +++ b/packages/qwik/src/qwikloader.unit.ts @@ -21,8 +21,13 @@ test('qwikloader script', () => { * dereference objects etc, but that actually results in worse compression */ const compressed = compress(Buffer.from(qwikLoader), { mode: 1, quality: 11 }); - expect([compressed.length, qwikLoader.length]).toEqual([1421, 3118]); + expect([compressed.length, qwikLoader.length]).toMatchInlineSnapshot(` + [ + 1506, + 3292, + ] + `); expect(qwikLoader).toMatchInlineSnapshot( - `"(()=>{const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),a=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},i=t=>{w(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&i(e)}))},c=t=>t&&"function"==typeof t.then,l=(t,e,n=e.type)=>{a("[on"+t+"\\\\:"+n+"]").forEach((o=>b(o,t,e,n)))},f=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},p=(t,e)=>new CustomEvent(t,{detail:e}),b=async(e,n,o,r=o.type)=>{const s="on"+n+":"+r;e.hasAttribute("preventdefault:"+r)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+r)&&o.stopPropagation();const a=e._qc_,i=a&&a.li.filter((t=>t[0]===s));if(i&&i.length>0){for(const t of i){const n=t[1].getFn([e,o],(()=>e.isConnected))(o,e),r=o.cancelBubble;c(n)&&await n,r&&o.stopPropagation()}return}const l=e.getAttribute(s);if(l){const n=e.closest("[q\\\\:container]"),r=n.getAttribute("q:base"),s=n.getAttribute("q:version")||"unknown",a=n.getAttribute("q:manifest-hash")||"dev",i=new URL(r,t.baseURI);for(const p of l.split("\\n")){const l=new URL(p,i),b=l.href,h=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",q=performance.now();let _,d,y;const w=p.startsWith("#"),g={qBase:r,qManifest:a,qVersion:s,href:b,symbol:h,element:e,reqTime:q};if(w){const e=n.getAttribute("q:instance");_=(t["qFuncs_"+e]||[])[Number.parseInt(h)],_||(d="sync",y=Error("sym:"+h))}else{u("qsymbol",g);const t=l.href.split("#")[0];try{const e=import(t);f(n),_=(await e)[h],_||(d="no-symbol",y=Error(\`\${h} not in \${t}\`))}catch(t){d||(d="async"),y=t}}if(!_){u("qerror",{importError:d,error:y,...g}),console.error(y);break}const m=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l];const n=_(o,e);c(n)&&await n}catch(t){u("qerror",{error:t,...g})}finally{t.__q_context__=m}}}},u=(e,n)=>{t.dispatchEvent(p(e,n))},h=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),q=async t=>{let e=h(t.type),n=t.target;for(l("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type),n=t.bubbles&&!0!==r?n.parentElement:null}},_=t=>{l("-window",t,h(t.type))},d=()=>{var s;const c=t.readyState;if(!r&&("interactive"==c||"complete"==c)&&(o.forEach(i),r=1,u("qinit"),(null!=(s=e.requestIdleCallback)?s:e.setTimeout).bind(e)((()=>u("qidle"))),n.has("qvisible"))){const t=a("[on\\\\:qvisible]"),e=new IntersectionObserver((t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",p("qvisible",n)))}));t.forEach((t=>e.observe(t)))}},y=(t,e,n,o=!1)=>t.addEventListener(e,n,{capture:o,passive:!1}),w=(...t)=>{for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>y(t,r,q,!0))),y(e,r,_,!0),n.add(r)):o.has(r)||(n.forEach((t=>y(r,t,q,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;Array.isArray(r)&&w(...r),e.qwikevents={events:n,roots:o,push:w},y(t,"readystatechange",d),d()}})();"` + `"const t=document,e=window,n=new Set,o=new Set([t]);let r;const s=(t,e)=>Array.from(t.querySelectorAll(e)),i=t=>{const e=[];return o.forEach((n=>e.push(...s(n,t)))),e},a=t=>{g(t),s(t,"[q\\\\:shadowroot]").forEach((t=>{const e=t.shadowRoot;e&&a(e)}))},c=t=>t&&"function"==typeof t.then;let l=!0;const f=(t,e,n=e.type)=>{let o=l;i("[on"+t+"\\\\:"+n+"]").forEach((r=>{o=!0,b(r,t,e,n)})),o||window[t.slice(1)].removeEventListener(n,"-window"===t?d:_)},p=e=>{if(void 0===e._qwikjson_){let n=(e===t.documentElement?t.body:e).lastElementChild;for(;n;){if("SCRIPT"===n.tagName&&"qwik/json"===n.getAttribute("type")){e._qwikjson_=JSON.parse(n.textContent.replace(/\\\\x3C(\\/?script)/gi,"<$1"));break}n=n.previousElementSibling}}},u=(t,e)=>new CustomEvent(t,{detail:e}),b=async(e,n,o,r=o.type)=>{const s="on"+n+":"+r;e.hasAttribute("preventdefault:"+r)&&o.preventDefault(),e.hasAttribute("stoppropagation:"+r)&&o.stopPropagation();const i=e._qc_,a=i&&i.li.filter((t=>t[0]===s));if(a&&a.length>0){for(const t of a){const n=t[1].getFn([e,o],(()=>e.isConnected))(o,e),r=o.cancelBubble;c(n)&&await n,r&&o.stopPropagation()}return}const l=e.getAttribute(s);if(l){const n=e.closest("[q\\\\:container]"),r=n.getAttribute("q:base"),s=n.getAttribute("q:version")||"unknown",i=n.getAttribute("q:manifest-hash")||"dev",a=new URL(r,t.baseURI);for(const f of l.split("\\n")){const l=new URL(f,a),u=l.href,b=l.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",q=performance.now();let _,d,w;const m=f.startsWith("#"),y={qBase:r,qManifest:i,qVersion:s,href:u,symbol:b,element:e,reqTime:q};if(m){const e=n.getAttribute("q:instance");_=(t["qFuncs_"+e]||[])[Number.parseInt(b)],_||(d="sync",w=Error("sym:"+b))}else{h("qsymbol",y);const t=l.href.split("#")[0];try{const e=import(t);p(n),_=(await e)[b],_||(d="no-symbol",w=Error(\`\${b} not in \${t}\`))}catch(t){d||(d="async"),w=t}}if(!_){h("qerror",{importError:d,error:w,...y}),console.error(w);break}const g=t.__q_context__;if(e.isConnected)try{t.__q_context__=[e,o,l];const n=_(o,e);c(n)&&await n}catch(t){h("qerror",{error:t,...y})}finally{t.__q_context__=g}}}},h=(e,n)=>{t.dispatchEvent(u(e,n))},q=t=>t.replace(/([A-Z])/g,(t=>"-"+t.toLowerCase())),_=async t=>{let e=q(t.type),n=t.target;for(f("-document",t,e);n&&n.getAttribute;){const o=b(n,"",t,e);let r=t.cancelBubble;c(o)&&await o,r||(r=r||t.cancelBubble||n.hasAttribute("stoppropagation:"+t.type)),n=t.bubbles&&!0!==r?n.parentElement:null}},d=t=>{f("-window",t,q(t.type))},w=()=>{var s;const c=t.readyState;if(!r&&("interactive"==c||"complete"==c)&&(o.forEach(a),r=1,h("qinit"),(null!=(s=e.requestIdleCallback)?s:e.setTimeout).bind(e)((()=>h("qidle"))),n.has("qvisible"))){const t=i("[on\\\\:qvisible]"),e=new IntersectionObserver((t=>{for(const n of t)n.isIntersecting&&(e.unobserve(n.target),b(n.target,"",u("qvisible",n)))}));t.forEach((t=>e.observe(t)))}},m=(t,e,n,o=!1)=>{t.addEventListener(e,n,{capture:o,passive:!1})};let y;const g=(...t)=>{l=!0,clearTimeout(y),y=setTimeout((()=>l=!1),2e4);for(const r of t)"string"==typeof r?n.has(r)||(o.forEach((t=>m(t,r,_,!0))),m(e,r,d,!0),n.add(r)):o.has(r)||(n.forEach((t=>m(r,t,_,!0))),o.add(r))};if(!("__q_context__"in t)){t.__q_context__=0;const r=e.qwikevents;r&&(Array.isArray(r)?g(...r):g("click","input")),e.qwikevents={events:n,roots:o,push:g},m(t,"readystatechange",w),w()}"` ); }); diff --git a/packages/qwik/src/server/qwik.server.api.md b/packages/qwik/src/server/qwik.server.api.md index acf1dc2f15d..a4a2a9e438f 100644 --- a/packages/qwik/src/server/qwik.server.api.md +++ b/packages/qwik/src/server/qwik.server.api.md @@ -83,9 +83,8 @@ export interface PreloaderOptions { // @public (undocumented) export interface QwikLoaderOptions { - // (undocumented) include?: 'always' | 'never' | 'auto'; - // (undocumented) + // @deprecated (undocumented) position?: 'top' | 'bottom'; } diff --git a/packages/qwik/src/server/render.ts b/packages/qwik/src/server/render.ts index 9619b795a42..25dc438e8f9 100644 --- a/packages/qwik/src/server/render.ts +++ b/packages/qwik/src/server/render.ts @@ -103,20 +103,7 @@ export async function renderToStream( if (containerTagName === 'html') { stream.write(DOCTYPE); } else { - // The container is not `` so we don't include the qwikloader by default stream.write(''); - if (opts.qwikLoader) { - if (opts.qwikLoader.include === undefined) { - opts.qwikLoader.include = 'never'; - } - if (opts.qwikLoader.position === undefined) { - opts.qwikLoader.position = 'bottom'; - } - } else { - opts.qwikLoader = { - include: 'never', - }; - } } if (!resolvedManifest && !isDev) { @@ -132,25 +119,18 @@ export async function renderToStream( : []; const includeMode = opts.qwikLoader?.include ?? 'auto'; - const positionMode = opts.qwikLoader?.position ?? 'bottom'; + const qwikLoaderChunk = resolvedManifest?.manifest.qwikLoader; let didAddQwikLoader = false; - if (positionMode === 'top' && includeMode !== 'never') { - didAddQwikLoader = true; - const qwikLoaderScript = getQwikLoaderScript({ - debug: opts.debug, - }); - beforeContent.push( + if (includeMode !== 'never' && qwikLoaderChunk) { + beforeContent.unshift( + jsx('link', { rel: 'modulepreload', href: `${buildBase}${qwikLoaderChunk}` }), jsx('script', { - id: 'qwikloader', - dangerouslySetInnerHTML: qwikLoaderScript, - }) - ); - // Assume there will be at least click and input handlers - beforeContent.push( - jsx('script', { - dangerouslySetInnerHTML: `window.qwikevents.push('click','input')`, + type: 'module', + async: true, + src: `${buildBase}${qwikLoaderChunk}`, }) ); + didAddQwikLoader = true; } preloaderPre(buildBase, resolvedManifest, opts.preloader, beforeContent, opts.serverData?.nonce); @@ -195,15 +175,17 @@ export async function renderToStream( ); } - const needLoader = !didAddQwikLoader && (!snapshotResult || snapshotResult.mode !== 'static'); + const needLoader = !snapshotResult || snapshotResult.mode !== 'static'; const includeLoader = includeMode === 'always' || (includeMode === 'auto' && needLoader); - if (includeLoader) { + if (!didAddQwikLoader && includeLoader) { const qwikLoaderScript = getQwikLoaderScript({ debug: opts.debug, }); children.push( jsx('script', { id: 'qwikloader', + // execute even before DOM order + async: true, dangerouslySetInnerHTML: qwikLoaderScript, nonce: opts.serverData?.nonce, }) @@ -213,9 +195,7 @@ export async function renderToStream( // We emit the events separately so other qwikloaders can see them const extraListeners = Array.from(containerState.$events$, (s) => JSON.stringify(s)); if (extraListeners.length > 0) { - const content = - (includeLoader ? `window.qwikevents` : `(window.qwikevents||=[])`) + - `.push(${extraListeners.join(', ')})`; + const content = `(window.qwikevents||(window.qwikevents=[])).push(${extraListeners.join(',')})`; children.push( jsx('script', { dangerouslySetInnerHTML: content, diff --git a/packages/qwik/src/server/types.ts b/packages/qwik/src/server/types.ts index 51ea762c1ae..a07f5ad5c50 100644 --- a/packages/qwik/src/server/types.ts +++ b/packages/qwik/src/server/types.ts @@ -124,7 +124,15 @@ export interface RenderResult { /** @public */ export interface QwikLoaderOptions { + /** + * Whether to include the qwikloader script in the document. Normally you don't need to worry + * about this, but in case of multi-container apps using different Qwik versions, you might want + * to only enable it on one of the containers. + * + * Defaults to `'auto'`. + */ include?: 'always' | 'never' | 'auto'; + /** @deprecated No longer used, the qwikloader is always loaded as soon as possible */ position?: 'top' | 'bottom'; } diff --git a/scripts/submodule-qwikloader.ts b/scripts/submodule-qwikloader.ts index ad8cdedc969..d145e3b0093 100644 --- a/scripts/submodule-qwikloader.ts +++ b/scripts/submodule-qwikloader.ts @@ -31,7 +31,6 @@ export async function submoduleQwikLoader(config: BuildConfig) { minify: false, outDir: config.distQwikPkgDir, }, - // plugins: [debugTerserPlugin()], }); // Read the debug version @@ -48,6 +47,11 @@ export async function submoduleQwikLoader(config: BuildConfig) { unsafe: true, passes: 2, }, + mangle: { + keep_fnames: false, + properties: false, + toplevel: true, + }, // uncomment this to understand the minified version better // format: { semicolons: false }, });