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 },
});
|