Skip to content

Commit

Permalink
perf(core): don't require entry chunks for QRLs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wmertens committed May 27, 2024
1 parent cf8fa7f commit 544678b
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 125 deletions.
2 changes: 1 addition & 1 deletion packages/docs/src/routes/api/qwik-optimizer/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@
}
],
"kind": "Interface",
"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>",
"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>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
"mdFile": "qwik.qwikmanifest.md"
},
Expand Down
12 changes: 11 additions & 1 deletion packages/docs/src/routes/api/qwik-optimizer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,8 @@ _(Optional)_

## QwikManifest

The metadata of the build. One of its uses is storing where QRL symbols are located.

```typescript
export interface QwikManifest
```
Expand Down Expand Up @@ -1580,6 +1582,8 @@ Description

</td><td>

All code bundles, used to know the import graph

</td></tr>
<tr><td>

Expand All @@ -1593,7 +1597,7 @@ Description

</td><td>

_(Optional)_
_(Optional)_ CSS etc to inject in the document head

</td></tr>
<tr><td>
Expand All @@ -1608,6 +1612,8 @@ string

</td><td>

Content hash of the manifest, if this changes, the code changed

</td></tr>
<tr><td>

Expand All @@ -1621,6 +1627,8 @@ string

</td><td>

Where QRLs are located. If there's a `#`, the string after it is the export

</td></tr>
<tr><td>

Expand Down Expand Up @@ -1664,6 +1672,8 @@ _(Optional)_

</td><td>

QRL symbols

</td></tr>
<tr><td>

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default defineConfig(async () => {
defaultHandler(level, log);
},
output: {
experimentalMinChunkSize: 5000,
experimentalMinChunkSize: 2000,
assetFileNames: 'assets/[hash].[ext]',
},
},
Expand Down
7 changes: 7 additions & 0 deletions packages/qwik/src/core/platform/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ export const createPlatform = (): CorePlatform => {
if (!containerEl) {
throw qError(QError_qrlMissingContainer, url, symbolName);
}
let attr: string | undefined;
if (typeof url === 'string') {
[url, attr] = url.split('#');
}
const urlDoc = toUrl(containerEl.ownerDocument, containerEl, url).toString();
const urlCopy = new URL(urlDoc);
urlCopy.hash = '';
urlCopy.search = '';
const importURL = urlCopy.href;
return import(/* @vite-ignore */ importURL).then((mod) => {
if (attr) {
mod = mod[attr];
}
return mod[symbolName];
});
},
Expand Down
34 changes: 9 additions & 25 deletions packages/qwik/src/core/qrl/qrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,26 +225,16 @@ export const serializeQRLs = (
return mapJoin(existingQRLs, (qrl) => serializeQRL(qrl, opts), '\n');
};

/** `./chunk#symbol[captures] */
/** `./chunk#[attr#][symbol][[captures]] */
export const parseQRL = <T = any>(qrl: string, containerEl?: Element): QRLInternal<T> => {
const endIdx = qrl.length;
const hashIdx = indexOf(qrl, 0, '#');
const captureIdx = indexOf(qrl, hashIdx, '[');

const chunkEndIdx = Math.min(hashIdx, captureIdx);
const chunk = qrl.substring(0, chunkEndIdx);

const symbolStartIdx = hashIdx == endIdx ? hashIdx : hashIdx + 1;
const symbolEndIdx = captureIdx;
const symbol =
symbolStartIdx == symbolEndIdx ? 'default' : qrl.substring(symbolStartIdx, symbolEndIdx);

const captureStartIdx = captureIdx;
const captureEndIdx = endIdx;
const capture =
captureStartIdx === captureEndIdx
? EMPTY_ARRAY
: qrl.substring(captureStartIdx + 1, captureEndIdx - 1).split(' ');
const parse = /^(?<c>[^#[]*)#?((?<a>[^#]+)#)?(?<s>[^[]*)(\[(?<p>[^\]]*)\])?$/.exec(qrl);
if (!parse) {
throw new Error(`Invalid QRL format "${qrl}"`);
}
const { c, a, s, p } = parse.groups!;
const chunk = `${c}${a ? `#${a}` : ''}`;
const symbol = s || 'default';
const capture = p ? p.split(' ') : [];

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

const indexOf = (text: string, startIdx: number, char: string) => {
const endIdx = text.length;
const charIdx = text.indexOf(char, startIdx == endIdx ? 0 : startIdx);
return charIdx == -1 ? endIdx : charIdx;
};

const addToArray = (array: any[], obj: any) => {
const index = array.indexOf(obj);
if (index === -1) {
Expand Down
14 changes: 12 additions & 2 deletions packages/qwik/src/core/qrl/qrl.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ describe('serialization', () => {
$symbol$: 'default',
resolved: undefined,
});
matchProps(parseQRL('./chunk#di#'), {
$chunk$: './chunk#di',
$symbol$: 'default',
resolved: undefined,
});
matchProps(parseQRL('./chunk#mySymbol'), {
$chunk$: './chunk',
$symbol$: 'mySymbol',
});
matchProps(parseQRL('./chunk#mySymbol'), {
$chunk$: './chunk',
matchProps(parseQRL('#mySymbol'), {
$chunk$: '',
$symbol$: 'mySymbol',
});
matchProps(parseQRL('./chunk#s1'), {
Expand All @@ -76,6 +81,11 @@ describe('serialization', () => {
$symbol$: 's1',
$capture$: ['1', 'b'],
});
matchProps(parseQRL('./chunk#a#s1[1 b]'), {
$chunk$: './chunk#a',
$symbol$: 's1',
$capture$: ['1', 'b'],
});
matchProps(parseQRL('./chunk[1 b]'), {
$chunk$: './chunk',
$capture$: ['1', 'b'],
Expand Down
1 change: 0 additions & 1 deletion packages/qwik/src/optimizer/core/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,6 @@ impl<'a> QwikTransform<'a> {
) -> ast::CallExpr {
let canonical_filename = get_canonical_filename(&symbol_name);

// We import from the given entry, or from the hook file directly
let mut url = ["./", &canonical_filename].concat();
if self.options.explicit_extensions {
url.push('.');
Expand Down
7 changes: 1 addition & 6 deletions packages/qwik/src/optimizer/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,13 @@ export interface QwikBundle {
symbols?: string[];
}

// @public (undocumented)
// @public
export interface QwikManifest {
// (undocumented)
bundles: {
[fileName: string]: QwikBundle;
};
// (undocumented)
injections?: GlobalInjections[];
// (undocumented)
manifestHash: string;
// (undocumented)
mapping: {
[symbolName: string]: string;
};
Expand All @@ -238,7 +234,6 @@ export interface QwikManifest {
platform?: {
[name: string]: string;
};
// (undocumented)
symbols: {
[symbolName: string]: QwikSymbol;
};
Expand Down
117 changes: 100 additions & 17 deletions packages/qwik/src/optimizer/src/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { NormalizedQwikPluginOptions } from './plugins/plugin';
import type { OutputBundle } from 'rollup';
import { QWIK_ENTRIES_ID, type NormalizedQwikPluginOptions } from './plugins/plugin';
import type {
GeneratedOutputBundle,
GlobalInjections,
HookAnalysis,
Path,
QwikBundle,
QwikManifest,
QwikSymbol,
TransformModule,
} from './types';

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

function updateSortAndPriorities(manifest: QwikManifest) {
const prioritizedSymbolNames = prioritizeSymbolNames(manifest);
const prioritizedSymbols: { [symbolName: string]: QwikSymbol } = {};
const prioritizedMapping: { [symbolName: string]: string } = {};
const prioritizedSymbols: QwikManifest['symbols'] = {};
const prioritizedMapping: QwikManifest['mapping'] = {};

for (const symbolName of prioritizedSymbolNames) {
prioritizedSymbols[symbolName] = manifest.symbols[symbolName];
Expand Down Expand Up @@ -228,6 +228,21 @@ function sortAlphabetical(a: string, b: string) {
return 0;
}

export function generateQwikEntries(transformedOutputs: Map<string, [TransformModule, string]>) {
// Capture all hooks
return `
// Roundabout way to get import info for hooks
export default {
"begin-imports": true,
${[...transformedOutputs.entries()]
.filter(([_, [mod]]) => mod.hook)
.map(([sym, [mod]]) => `"${mod.hook!.name}": () => import("${sym}"),`)
.join('\n')}
"end-imports": true,
}
`;
}

export function getValidManifest(manifest: QwikManifest | undefined | null) {
if (
manifest != null &&
Expand All @@ -243,12 +258,11 @@ export function getValidManifest(manifest: QwikManifest | undefined | null) {
return undefined;
}

const hookRegex = /_[a-zA-Z0-9]{11}$/;
export function generateManifestFromBundles(
path: Path,
hooks: HookAnalysis[],
injections: GlobalInjections[],
outputBundles: GeneratedOutputBundle[],
outputBundles: OutputBundle,
opts: NormalizedQwikPluginOptions
) {
const manifest: QwikManifest = {
Expand All @@ -265,11 +279,83 @@ export function generateManifestFromBundles(
},
};

for (const outputBundle of outputBundles) {
const bundleFileName = path.basename(outputBundle.fileName);
for (const [fileName, outputBundle] of Object.entries(outputBundles)) {
if (outputBundle.type !== 'chunk') {
continue;
}
const bundleFileName = path.basename(fileName);

// Our special @qwik-entries chunk
// We need to parse this to get the minified locations of the js packets
if (outputBundle.moduleIds.some((id) => id.endsWith(QWIK_ENTRIES_ID))) {
const { code } = outputBundle;
const match =
/^(?<left>.*){\s*['"]?begin-imports[^:]*:[^,}]*,?(?<entries>[^}]*)end-imports[^:]*:[^,}]*}(?<right>.*)/m.exec(
code
);
if (!match) {
console.error(
`Could not parse @qwik-entries chunk ${bundleFileName}, please open an issue and provide this code: `,
code
);
throw new Error(`Could not parse @qwik-entries chunk ${bundleFileName}`);
}
// the chunk contains a JSON object with the entry points
// individual entries look like this:
// "s_2y2nxB87G0c": __vitePreload(() => import("./q-Bi49h4Yp.js").then((n) => n.ba), true ? [] : void 0),
// or minified:
// s_17zcY0gsYE4:t(()=>import("./q-Co_1fcGT.js").then(_=>_.cO),[]),

// locally embedded qrls look like
// o=_=>{},E=Object.freeze(Object.defineProperty({__proto__:null,s_H7LftCVcX8A:o},Symbol.toStringTag,{value:"Module"}));
// and then referenced as
// s_H7LftCVcX8A:t(()=>Promise.resolve().then(()=>E),void 0)
const parts = match.groups!.entries.split(',');
for (const part of parts) {
const info =
/\s*['"]?(?<symbol>[a-zA-Z0-9_]+)['"]?:(.*import\(['"]\.\/(?<path>[^'"]+))?(.*then\(.*=>.*\.(?<attr>\w+))?/.exec(
part
);
if (info) {
const { symbol } = info.groups!;
let { path, attr } = info.groups!;
if (!path) {
// Sadly rollup decided to move the chunk into our @qwik-entries chunk, and we have to keep it
const match = /Promise.resolve.*then\(.*=>(?<internal>[^)])/.exec(part);
if (!match) {
console.error(
`Could not parse entry ${part} in the following code. Please open an issue.`,
code
);
throw new Error(`Could not parse entry symbol ${symbol}`);
}
const { internal } = match.groups!;
// Now we have to find the export of the internal symbol
// ;export{e as _,E as s};
const findExport = new RegExp(
`\\bexport\\s*{[^}]*\\b${internal}( as (?<exported>[^}]+))?\\b`
).exec(code);
if (!findExport) {
console.error(
`Could not find export for ${symbol} in the following code. Please open an issue.`,
code
);
throw new Error(`Could not find export for ${symbol}`);
}
attr = findExport.groups?.exported || internal;

path = bundleFileName;
}
manifest.mapping[symbol] = `${path}${attr ? `#${attr}` : ''}`;
}
}
// Successfully parsed, remove the entries
outputBundle.code = `${match.groups!.left}0;${match.groups!.right}`;
}

const buildDirName = path.dirname(outputBundle.fileName);
const bundle: QwikBundle = {
size: outputBundle.size,
size: outputBundle.code.length,
};
const bundleImports = outputBundle.imports
.filter((i) => path.dirname(i) === buildDirName)
Expand All @@ -285,26 +371,23 @@ export function generateManifestFromBundles(
bundle.dynamicImports = bundleDynamicImports;
}

const modulePaths = Object.keys(outputBundle.modules).filter((m) => !m.startsWith(`\u0000`));
const modulePaths = outputBundle.moduleIds.filter((m) => !m.startsWith(`\u0000`));
if (modulePaths.length > 0) {
bundle.origins = modulePaths;
}
const symbols = outputBundle.exports.filter((e) => hookRegex.test(e));
if (symbols.length > 0) {
bundle.symbols = symbols;
}

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

for (const hook of hooks) {
const symbol = hook.name;
const bundle = manifest.mapping[symbol];
if (!bundle) {
console.error(`Unable to find bundle for hook: ${hook.hash}`, manifest);
console.error(`Unable to find bundle for hook: ${hook.name}`, manifest);
throw new Error(`Unable to find bundle for hook: ${hook.hash}`);
}
const key = bundle.split('#')[0];
(manifest.bundles[key].symbols ||= []).push(symbol);
manifest.symbols[symbol] = {
origin: hook.origin,
displayName: hook.displayName,
Expand Down
Loading

0 comments on commit 544678b

Please sign in to comment.