Skip to content

Commit a8d3e57

Browse files
committed
perf(core): don't require entry chunks for QRLs
Now the bundler can decide which QRLs to host toghether. - chunk URLs now can include the exported attr as `chunk#attr` - manifest generation is now more robust - refactored manifest generation
1 parent cf8fa7f commit a8d3e57

File tree

16 files changed

+242
-116
lines changed

16 files changed

+242
-116
lines changed

packages/docs/src/routes/api/qwik-optimizer/api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@
400400
}
401401
],
402402
"kind": "Interface",
403-
"content": "```typescript\nexport interface QwikManifest \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[bundles](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle)<!-- -->; }\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[injections?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[GlobalInjections](#globalinjections)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[manifestHash](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[mapping](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: string; }\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[options?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[platform?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[name: string\\]: string; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[symbols](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol)<!-- -->; }\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[version](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
403+
"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<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[bundles](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle)<!-- -->; }\n\n\n</td><td>\n\nAll code bundles, used to know the import graph\n\n\n</td></tr>\n<tr><td>\n\n[injections?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[GlobalInjections](#globalinjections)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n</td></tr>\n<tr><td>\n\n[manifestHash](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nContent hash of the manifest, if this changes, the code changed\n\n\n</td></tr>\n<tr><td>\n\n[mapping](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: string; }\n\n\n</td><td>\n\nWhere QRLs are located. If there's a `#`<!-- -->, the string after it is the export\n\n\n</td></tr>\n<tr><td>\n\n[options?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[platform?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[name: string\\]: string; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[symbols](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol)<!-- -->; }\n\n\n</td><td>\n\nQRL symbols\n\n\n</td></tr>\n<tr><td>\n\n[version](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
404404
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
405405
"mdFile": "qwik.qwikmanifest.md"
406406
},

packages/docs/src/routes/api/qwik-optimizer/index.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,8 @@ _(Optional)_
15471547

15481548
## QwikManifest
15491549

1550+
The metadata of the build. One of its uses is storing where QRL symbols are located.
1551+
15501552
```typescript
15511553
export interface QwikManifest
15521554
```
@@ -1580,6 +1582,8 @@ Description
15801582

15811583
</td><td>
15821584

1585+
All code bundles, used to know the import graph
1586+
15831587
</td></tr>
15841588
<tr><td>
15851589

@@ -1593,7 +1597,7 @@ Description
15931597

15941598
</td><td>
15951599

1596-
_(Optional)_
1600+
_(Optional)_ CSS etc to inject in the document head
15971601

15981602
</td></tr>
15991603
<tr><td>
@@ -1608,6 +1612,8 @@ string
16081612

16091613
</td><td>
16101614

1615+
Content hash of the manifest, if this changes, the code changed
1616+
16111617
</td></tr>
16121618
<tr><td>
16131619

@@ -1621,6 +1627,8 @@ string
16211627

16221628
</td><td>
16231629

1630+
Where QRLs are located. If there's a `#`, the string after it is the export
1631+
16241632
</td></tr>
16251633
<tr><td>
16261634

@@ -1664,6 +1672,8 @@ _(Optional)_
16641672

16651673
</td><td>
16661674

1675+
QRL symbols
1676+
16671677
</td></tr>
16681678
<tr><td>
16691679

packages/docs/vite.config.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export default defineConfig(async () => {
138138
defaultHandler(level, log);
139139
},
140140
output: {
141-
experimentalMinChunkSize: 5000,
141+
experimentalMinChunkSize: 2000,
142142
assetFileNames: 'assets/[hash].[ext]',
143143
},
144144
},

packages/qwik/src/core/platform/platform.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ export const createPlatform = (): CorePlatform => {
2222
if (!containerEl) {
2323
throw qError(QError_qrlMissingContainer, url, symbolName);
2424
}
25+
let attr: string | undefined;
26+
if (typeof url === 'string') {
27+
[url, attr] = url.split('#');
28+
}
2529
const urlDoc = toUrl(containerEl.ownerDocument, containerEl, url).toString();
2630
const urlCopy = new URL(urlDoc);
2731
urlCopy.hash = '';
2832
urlCopy.search = '';
2933
const importURL = urlCopy.href;
3034
return import(/* @vite-ignore */ importURL).then((mod) => {
35+
if (attr) {
36+
mod = mod[attr];
37+
}
3138
return mod[symbolName];
3239
});
3340
},

packages/qwik/src/core/qrl/qrl.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -225,26 +225,16 @@ export const serializeQRLs = (
225225
return mapJoin(existingQRLs, (qrl) => serializeQRL(qrl, opts), '\n');
226226
};
227227

228-
/** `./chunk#symbol[captures] */
228+
/** `./chunk#[attr#][symbol][[captures]] */
229229
export const parseQRL = <T = any>(qrl: string, containerEl?: Element): QRLInternal<T> => {
230-
const endIdx = qrl.length;
231-
const hashIdx = indexOf(qrl, 0, '#');
232-
const captureIdx = indexOf(qrl, hashIdx, '[');
233-
234-
const chunkEndIdx = Math.min(hashIdx, captureIdx);
235-
const chunk = qrl.substring(0, chunkEndIdx);
236-
237-
const symbolStartIdx = hashIdx == endIdx ? hashIdx : hashIdx + 1;
238-
const symbolEndIdx = captureIdx;
239-
const symbol =
240-
symbolStartIdx == symbolEndIdx ? 'default' : qrl.substring(symbolStartIdx, symbolEndIdx);
241-
242-
const captureStartIdx = captureIdx;
243-
const captureEndIdx = endIdx;
244-
const capture =
245-
captureStartIdx === captureEndIdx
246-
? EMPTY_ARRAY
247-
: qrl.substring(captureStartIdx + 1, captureEndIdx - 1).split(' ');
230+
const parse = /^(?<c>[^#[]*)#?((?<a>[^#]+)#)?(?<s>[^[]*)(\[(?<p>[^\]]*)\])?$/.exec(qrl);
231+
if (!parse) {
232+
throw new Error(`Invalid QRL format "${qrl}"`);
233+
}
234+
const { c, a, s, p } = parse.groups!;
235+
const chunk = `${c}${a ? `#${a}` : ''}`;
236+
const symbol = s || 'default';
237+
const capture = p ? p.split(' ') : [];
248238

249239
const iQrl = createQRL<any>(chunk, symbol, null, null, capture, null, null);
250240
if (containerEl) {
@@ -253,12 +243,6 @@ export const parseQRL = <T = any>(qrl: string, containerEl?: Element): QRLIntern
253243
return iQrl as QRLInternal<T>;
254244
};
255245

256-
const indexOf = (text: string, startIdx: number, char: string) => {
257-
const endIdx = text.length;
258-
const charIdx = text.indexOf(char, startIdx == endIdx ? 0 : startIdx);
259-
return charIdx == -1 ? endIdx : charIdx;
260-
};
261-
262246
const addToArray = (array: any[], obj: any) => {
263247
const index = array.indexOf(obj);
264248
if (index === -1) {

packages/qwik/src/core/qrl/qrl.unit.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ describe('serialization', () => {
4848
$symbol$: 'default',
4949
resolved: undefined,
5050
});
51+
matchProps(parseQRL('./chunk#di#'), {
52+
$chunk$: './chunk#di',
53+
$symbol$: 'default',
54+
resolved: undefined,
55+
});
5156
matchProps(parseQRL('./chunk#mySymbol'), {
5257
$chunk$: './chunk',
5358
$symbol$: 'mySymbol',
5459
});
55-
matchProps(parseQRL('./chunk#mySymbol'), {
56-
$chunk$: './chunk',
60+
matchProps(parseQRL('#mySymbol'), {
61+
$chunk$: '',
5762
$symbol$: 'mySymbol',
5863
});
5964
matchProps(parseQRL('./chunk#s1'), {
@@ -76,6 +81,11 @@ describe('serialization', () => {
7681
$symbol$: 's1',
7782
$capture$: ['1', 'b'],
7883
});
84+
matchProps(parseQRL('./chunk#a#s1[1 b]'), {
85+
$chunk$: './chunk#a',
86+
$symbol$: 's1',
87+
$capture$: ['1', 'b'],
88+
});
7989
matchProps(parseQRL('./chunk[1 b]'), {
8090
$chunk$: './chunk',
8191
$capture$: ['1', 'b'],

packages/qwik/src/optimizer/core/src/transform.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,6 @@ impl<'a> QwikTransform<'a> {
752752
) -> ast::CallExpr {
753753
let canonical_filename = get_canonical_filename(&symbol_name);
754754

755-
// We import from the given entry, or from the hook file directly
756755
let mut url = ["./", &canonical_filename].concat();
757756
if self.options.explicit_extensions {
758757
url.push('.');

packages/qwik/src/optimizer/src/api.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,17 +212,13 @@ export interface QwikBundle {
212212
symbols?: string[];
213213
}
214214

215-
// @public (undocumented)
215+
// @public
216216
export interface QwikManifest {
217-
// (undocumented)
218217
bundles: {
219218
[fileName: string]: QwikBundle;
220219
};
221-
// (undocumented)
222220
injections?: GlobalInjections[];
223-
// (undocumented)
224221
manifestHash: string;
225-
// (undocumented)
226222
mapping: {
227223
[symbolName: string]: string;
228224
};
@@ -238,7 +234,6 @@ export interface QwikManifest {
238234
platform?: {
239235
[name: string]: string;
240236
};
241-
// (undocumented)
242237
symbols: {
243238
[symbolName: string]: QwikSymbol;
244239
};

packages/qwik/src/optimizer/src/manifest.ts

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import type { NormalizedQwikPluginOptions } from './plugins/plugin';
1+
import type { OutputBundle } from 'rollup';
2+
import { QWIK_ENTRIES_ID, type NormalizedQwikPluginOptions } from './plugins/plugin';
23
import type {
3-
GeneratedOutputBundle,
44
GlobalInjections,
55
HookAnalysis,
66
Path,
77
QwikBundle,
88
QwikManifest,
9-
QwikSymbol,
9+
TransformModule,
1010
} from './types';
1111

1212
// This is just the initial prioritization of the symbols and entries
@@ -178,8 +178,8 @@ function sortBundleNames(manifest: QwikManifest) {
178178

179179
function updateSortAndPriorities(manifest: QwikManifest) {
180180
const prioritizedSymbolNames = prioritizeSymbolNames(manifest);
181-
const prioritizedSymbols: { [symbolName: string]: QwikSymbol } = {};
182-
const prioritizedMapping: { [symbolName: string]: string } = {};
181+
const prioritizedSymbols: QwikManifest['symbols'] = {};
182+
const prioritizedMapping: QwikManifest['mapping'] = {};
183183

184184
for (const symbolName of prioritizedSymbolNames) {
185185
prioritizedSymbols[symbolName] = manifest.symbols[symbolName];
@@ -228,6 +228,20 @@ function sortAlphabetical(a: string, b: string) {
228228
return 0;
229229
}
230230

231+
export function generateQwikEntries(transformedOutputs: Map<string, [TransformModule, string]>) {
232+
// Capture all hooks
233+
return `
234+
// Roundabout way to get import info for hooks
235+
export default {
236+
BEGIN: 'BEGIN',
237+
${[...transformedOutputs.entries()]
238+
.filter(([_, [mod]]) => mod.hook)
239+
.map(([sym, [mod]]) => `"${mod.hook!.name}": () => import("${sym}")`)
240+
.join(',\n')}
241+
}
242+
`;
243+
}
244+
231245
export function getValidManifest(manifest: QwikManifest | undefined | null) {
232246
if (
233247
manifest != null &&
@@ -243,12 +257,11 @@ export function getValidManifest(manifest: QwikManifest | undefined | null) {
243257
return undefined;
244258
}
245259

246-
const hookRegex = /_[a-zA-Z0-9]{11}$/;
247260
export function generateManifestFromBundles(
248261
path: Path,
249262
hooks: HookAnalysis[],
250263
injections: GlobalInjections[],
251-
outputBundles: GeneratedOutputBundle[],
264+
outputBundles: OutputBundle,
252265
opts: NormalizedQwikPluginOptions
253266
) {
254267
const manifest: QwikManifest = {
@@ -265,11 +278,75 @@ export function generateManifestFromBundles(
265278
},
266279
};
267280

268-
for (const outputBundle of outputBundles) {
269-
const bundleFileName = path.basename(outputBundle.fileName);
281+
for (const [fileName, outputBundle] of Object.entries(outputBundles)) {
282+
if (outputBundle.type !== 'chunk') {
283+
continue;
284+
}
285+
const bundleFileName = path.basename(fileName);
286+
287+
// Our special @qwik-entries chunk
288+
// We need to parse this to get the minified locations of the js packets
289+
if (outputBundle.moduleIds.some((id) => id.endsWith(QWIK_ENTRIES_ID))) {
290+
const { code } = outputBundle;
291+
const match = /BEGIN[^:]*:[^,]*,([^}]*)}/m.exec(code);
292+
if (!match) {
293+
console.error(`Could not parse @qwik-entries chunk ${bundleFileName}: `, code);
294+
throw new Error(`Could not parse @qwik-entries chunk ${bundleFileName}`);
295+
}
296+
// the chunk contains a JSON object with the entry points
297+
// individual entries look like this:
298+
// "s_2y2nxB87G0c": __vitePreload(() => import("./q-Bi49h4Yp.js").then((n) => n.ba), true ? [] : void 0),
299+
// or minified:
300+
// s_17zcY0gsYE4:t(()=>import("./q-Co_1fcGT.js").then(_=>_.cO),[]),
301+
302+
// locally embedded qrls look like
303+
// o=_=>{},E=Object.freeze(Object.defineProperty({__proto__:null,s_H7LftCVcX8A:o},Symbol.toStringTag,{value:"Module"}));
304+
// and then referenced as
305+
// s_H7LftCVcX8A:t(()=>Promise.resolve().then(()=>E),void 0)
306+
const parts = match[1].split(',');
307+
for (const part of parts) {
308+
const info =
309+
/\s*['"]?(?<symbol>[a-zA-Z0-9_]+)['"]?:(.*import\(['"]\.\/(?<path>[^'"]+))?(.*then\(.*=>.*\.(?<attr>\w+))?/.exec(
310+
part
311+
);
312+
if (info) {
313+
const { symbol } = info.groups!;
314+
let { path, attr } = info.groups!;
315+
if (!path) {
316+
// Sadly rollup decided to move the chunk into our @qwik-entries chunk, and we have to keep it
317+
const match = /Promise.resolve.*then\(.*=>(?<internal>[^)])/.exec(part);
318+
if (!match) {
319+
console.error(
320+
`Could not parse entry ${part} in the following code. Please open an issue.`,
321+
code
322+
);
323+
throw new Error(`Could not parse entry symbol ${symbol}`);
324+
}
325+
const { internal } = match.groups!;
326+
// Now we have to find the export of the internal symbol
327+
// ;export{e as _,E as s};
328+
const findExport = new RegExp(
329+
`\\bexport\\s*{[^}]*\\b${internal}( as (?<exported>[^}]+))?\\b`
330+
).exec(code);
331+
if (!findExport) {
332+
console.error(
333+
`Could not find export for ${symbol} in the following code. Please open an issue.`,
334+
code
335+
);
336+
throw new Error(`Could not find export for ${symbol}`);
337+
}
338+
attr = findExport.groups?.exported || internal;
339+
340+
path = bundleFileName;
341+
}
342+
manifest.mapping[symbol] = `${path}${attr ? `#${attr}` : ''}`;
343+
}
344+
}
345+
}
346+
270347
const buildDirName = path.dirname(outputBundle.fileName);
271348
const bundle: QwikBundle = {
272-
size: outputBundle.size,
349+
size: outputBundle.code.length,
273350
};
274351
const bundleImports = outputBundle.imports
275352
.filter((i) => path.dirname(i) === buildDirName)
@@ -285,26 +362,23 @@ export function generateManifestFromBundles(
285362
bundle.dynamicImports = bundleDynamicImports;
286363
}
287364

288-
const modulePaths = Object.keys(outputBundle.modules).filter((m) => !m.startsWith(`\u0000`));
365+
const modulePaths = outputBundle.moduleIds.filter((m) => !m.startsWith(`\u0000`));
289366
if (modulePaths.length > 0) {
290367
bundle.origins = modulePaths;
291368
}
292-
const symbols = outputBundle.exports.filter((e) => hookRegex.test(e));
293-
if (symbols.length > 0) {
294-
bundle.symbols = symbols;
295-
}
296369

297370
manifest.bundles[bundleFileName] = bundle;
298-
Object.assign(manifest.mapping, ...symbols.map((s) => ({ [s]: bundleFileName })));
299371
}
300372

301373
for (const hook of hooks) {
302374
const symbol = hook.name;
303375
const bundle = manifest.mapping[symbol];
304376
if (!bundle) {
305-
console.error(`Unable to find bundle for hook: ${hook.hash}`, manifest);
377+
console.error(`Unable to find bundle for hook: ${hook.name}`, manifest);
306378
throw new Error(`Unable to find bundle for hook: ${hook.hash}`);
307379
}
380+
const key = bundle.split('#')[0];
381+
(manifest.bundles[key].symbols ||= []).push(symbol);
308382
manifest.symbols[symbol] = {
309383
origin: hook.origin,
310384
displayName: hook.displayName,

0 commit comments

Comments
 (0)