From 993a22ff2d6be02970c3c26438801f6e2321b766 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Mon, 17 Jun 2024 15:09:57 +0200 Subject: [PATCH] refactor(optimizer): nicer module finding + fix dev - refactored manifest generation - dev server collects QRL parents during ssr build - optimizer also stores dev info for noop QRLs - dev server also uses dev info as an extra fallback - dev source URLs are kept similar looking to the source tree - QRLs are annotated with parent so they can be built on demand --- .../src/routes/api/qwik-optimizer/api.json | 4 +- .../src/routes/api/qwik-optimizer/index.md | 13 +- packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.md | 2 +- packages/qwik/src/core/api.md | 5 +- packages/qwik/src/core/internal.ts | 2 +- packages/qwik/src/core/platform/types.ts | 3 +- packages/qwik/src/core/qrl/qrl.ts | 17 +- packages/qwik/src/core/qrl/qrl.unit.ts | 6 + packages/qwik/src/core/use/use-task.ts | 4 + ...est__example_derived_signals_children.snap | 2 +- ...re__test__example_derived_signals_cmp.snap | 2 +- ...ple_derived_signals_complext_children.snap | 2 +- ...re__test__example_derived_signals_div.snap | 2 +- ...ple_derived_signals_multiple_children.snap | 2 +- ..._core__test__example_dev_mode_inlined.snap | 2 +- ...example_immutable_function_components.snap | 2 +- .../qwik_core__test__example_issue_33443.snap | 2 +- .../qwik_core__test__example_issue_4438.snap | 2 +- ..._core__test__example_mutable_children.snap | 2 +- ...wik_core__test__example_noop_dev_mode.snap | 122 +++++++++++ ...ore__test__example_preserve_filenames.snap | 2 +- ...core__test__example_qwik_react_inline.snap | 2 +- ...core__test__example_strip_client_code.snap | 2 +- ...core__test__example_transpile_ts_only.snap | 2 +- .../snapshots/qwik_core__test__issue_476.snap | 2 +- .../qwik_core__test__special_jsx.snap | 2 +- packages/qwik/src/optimizer/core/src/test.rs | 38 ++++ .../qwik/src/optimizer/core/src/transform.rs | 17 +- packages/qwik/src/optimizer/core/src/words.rs | 1 + packages/qwik/src/optimizer/src/api.md | 9 +- packages/qwik/src/optimizer/src/manifest.ts | 106 +++++----- .../qwik/src/optimizer/src/plugins/plugin.ts | 190 ++++++++---------- .../qwik/src/optimizer/src/plugins/rollup.ts | 16 +- .../{vite-server.ts => vite-dev-server.ts} | 64 +++--- .../qwik/src/optimizer/src/plugins/vite.ts | 25 +-- packages/qwik/src/optimizer/src/types.ts | 24 +-- packages/qwik/src/server/platform.ts | 12 +- packages/qwik/src/testing/platform.ts | 6 + 39 files changed, 456 insertions(+), 264 deletions(-) create mode 100644 packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_noop_dev_mode.snap rename packages/qwik/src/optimizer/src/plugins/{vite-server.ts => vite-dev-server.ts} (86%) diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index a1b1a9c55d3..1002986fa93 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -400,7 +400,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikManifest \n```\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[bundles](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle); }\n\n\n\n\n\n
\n\n[injections?](#)\n\n\n\n\n\n\n\n[GlobalInjections](#globalinjections)\\[\\]\n\n\n\n\n_(Optional)_\n\n\n
\n\n[manifestHash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
\n\n[mapping](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: string; }\n\n\n\n\n\n
\n\n[options?](#)\n\n\n\n\n\n\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[platform?](#)\n\n\n\n\n\n\n\n{ \\[name: string\\]: string; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[symbols](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol); }\n\n\n\n\n\n
\n\n[version](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", + "content": "The metadata of the build. One of its uses is storing where QRL symbols are located.\n\n\n```typescript\nexport interface QwikManifest \n```\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[bundles](#)\n\n\n\n\n\n\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle); }\n\n\n\n\nAll code bundles, used to know the import graph\n\n\n
\n\n[injections?](#)\n\n\n\n\n\n\n\n[GlobalInjections](#globalinjections)\\[\\]\n\n\n\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n
\n\n[manifestHash](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\nContent hash of the manifest, if this changes, the code changed\n\n\n
\n\n[mapping](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: string; }\n\n\n\n\nWhere QRLs are located\n\n\n
\n\n[options?](#)\n\n\n\n\n\n\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[platform?](#)\n\n\n\n\n\n\n\n{ \\[name: string\\]: string; }\n\n\n\n\n_(Optional)_\n\n\n
\n\n[symbols](#)\n\n\n\n\n\n\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol); }\n\n\n\n\nQRL symbols\n\n\n
\n\n[version](#)\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.qwikmanifest.md" }, @@ -644,7 +644,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type SymbolMapperFn = (symbolName: string, mapper: SymbolMapper | undefined) => readonly [symbol: string, chunk: string] | undefined;\n```\n**References:** [SymbolMapper](#symbolmapper)", + "content": "```typescript\nexport type SymbolMapperFn = (symbolName: string, mapper: SymbolMapper | undefined, parent?: string) => readonly [symbol: string, chunk: string] | undefined;\n```\n**References:** [SymbolMapper](#symbolmapper)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.symbolmapperfn.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.md b/packages/docs/src/routes/api/qwik-optimizer/index.md index f658f242106..3f64e1d494a 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.md +++ b/packages/docs/src/routes/api/qwik-optimizer/index.md @@ -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 ``` @@ -1580,6 +1582,8 @@ Description +All code bundles, used to know the import graph + @@ -1593,7 +1597,7 @@ Description -_(Optional)_ +_(Optional)_ CSS etc to inject in the document head @@ -1608,6 +1612,8 @@ string +Content hash of the manifest, if this changes, the code changed + @@ -1621,6 +1627,8 @@ string +Where QRLs are located + @@ -1664,6 +1672,8 @@ _(Optional)_ +QRL symbols + @@ -2758,6 +2768,7 @@ export type SymbolMapper = Record< export type SymbolMapperFn = ( symbolName: string, mapper: SymbolMapper | undefined, + parent?: string, ) => readonly [symbol: string, chunk: string] | undefined; ``` diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index c99a639c616..ab33e84b6fd 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -544,7 +544,7 @@ } ], "kind": "Interface", - "content": "Low-level API for platform abstraction.\n\nDifferent platforms (browser, node, service workers) may have different ways of handling things such as `requestAnimationFrame` and imports. To make Qwik platform-independent Qwik uses the `CorePlatform` API to access the platform API.\n\n`CorePlatform` also is responsible for importing symbols. The import map is different on the client (browser) then on the server. For this reason, the server has a manifest that is used to map symbols to javascript chunks. The manifest is encapsulated in `CorePlatform`, for this reason, the `CorePlatform` can't be global as there may be multiple applications running at server concurrently.\n\nThis is a low-level API and there should not be a need for you to access this.\n\n\n```typescript\nexport interface CorePlatform \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[chunkForSymbol](#)\n\n\n\n\n\n\n\n(symbolName: string, chunk: string \\| null) => readonly \\[symbol: string, chunk: string\\] \\| undefined\n\n\n\n\nRetrieve chunk name for the symbol.\n\nWhen the application is running on the server the symbols may be imported from different files (as server build is typically a single javascript chunk.) For this reason, it is necessary to convert the chunks from server format to client (browser) format. This is done by looking up symbols (which are globally unique) in the manifest. (Manifest is the mapping of symbols to the client chunk names.)\n\n\n
\n\n[importSymbol](#)\n\n\n\n\n\n\n\n(containerEl: Element \\| undefined, url: string \\| URL \\| undefined \\| null, symbol: string) => [ValueOrPromise](#valueorpromise)<any>\n\n\n\n\nRetrieve a symbol value from QRL.\n\nQwik needs to lazy load data and closures. For this Qwik uses QRLs that are serializable references of resources that are needed. The QRLs contain all the information necessary to retrieve the reference using `importSymbol`.\n\nWhy not use `import()`? Because `import()` is relative to the current file, and the current file is always the Qwik framework. So QRLs have additional information that allows them to serialize imports relative to application base rather than the Qwik framework file.\n\n\n
\n\n[isServer](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\nTrue of running on the server platform.\n\n\n
\n\n[nextTick](#)\n\n\n\n\n\n\n\n(fn: () => any) => Promise<any>\n\n\n\n\nPerform operation on next tick.\n\n\n
\n\n[raf](#)\n\n\n\n\n\n\n\n(fn: () => any) => Promise<any>\n\n\n\n\nPerform operation on next request-animation-frame.\n\n\n
", + "content": "Low-level API for platform abstraction.\n\nDifferent platforms (browser, node, service workers) may have different ways of handling things such as `requestAnimationFrame` and imports. To make Qwik platform-independent Qwik uses the `CorePlatform` API to access the platform API.\n\n`CorePlatform` also is responsible for importing symbols. The import map is different on the client (browser) then on the server. For this reason, the server has a manifest that is used to map symbols to javascript chunks. The manifest is encapsulated in `CorePlatform`, for this reason, the `CorePlatform` can't be global as there may be multiple applications running at server concurrently.\n\nThis is a low-level API and there should not be a need for you to access this.\n\n\n```typescript\nexport interface CorePlatform \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[chunkForSymbol](#)\n\n\n\n\n\n\n\n(symbolName: string, chunk: string \\| null, parent?: string) => readonly \\[symbol: string, chunk: string\\] \\| undefined\n\n\n\n\nRetrieve chunk name for the symbol.\n\nWhen the application is running on the server the symbols may be imported from different files (as server build is typically a single javascript chunk.) For this reason, it is necessary to convert the chunks from server format to client (browser) format. This is done by looking up symbols (which are globally unique) in the manifest. (Manifest is the mapping of symbols to the client chunk names.)\n\n\n
\n\n[importSymbol](#)\n\n\n\n\n\n\n\n(containerEl: Element \\| undefined, url: string \\| URL \\| undefined \\| null, symbol: string) => [ValueOrPromise](#valueorpromise)<any>\n\n\n\n\nRetrieve a symbol value from QRL.\n\nQwik needs to lazy load data and closures. For this Qwik uses QRLs that are serializable references of resources that are needed. The QRLs contain all the information necessary to retrieve the reference using `importSymbol`.\n\nWhy not use `import()`? Because `import()` is relative to the current file, and the current file is always the Qwik framework. So QRLs have additional information that allows them to serialize imports relative to application base rather than the Qwik framework file.\n\n\n
\n\n[isServer](#)\n\n\n\n\n\n\n\nboolean\n\n\n\n\nTrue of running on the server platform.\n\n\n
\n\n[nextTick](#)\n\n\n\n\n\n\n\n(fn: () => any) => Promise<any>\n\n\n\n\nPerform operation on next tick.\n\n\n
\n\n[raf](#)\n\n\n\n\n\n\n\n(fn: () => any) => Promise<any>\n\n\n\n\nPerform operation on next request-animation-frame.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/platform/types.ts", "mdFile": "qwik.coreplatform.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index d494a041f0f..6a71eb33845 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -1558,7 +1558,7 @@ Description -(symbolName: string, chunk: string \| null) => readonly [symbol: string, chunk: string] \| undefined +(symbolName: string, chunk: string \| null, parent?: string) => readonly [symbol: string, chunk: string] \| undefined diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index f19e1aff9d0..82e20f25229 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -142,7 +142,7 @@ export interface ContextId { // @public export interface CorePlatform { - chunkForSymbol: (symbolName: string, chunk: string | null) => readonly [symbol: string, chunk: string] | undefined; + chunkForSymbol: (symbolName: string, chunk: string | null, parent?: string) => readonly [symbol: string, chunk: string] | undefined; importSymbol: (containerEl: Element | undefined, url: string | URL | undefined | null, symbol: string) => ValueOrPromise; isServer: boolean; nextTick: (fn: () => any) => Promise; @@ -542,6 +542,9 @@ export type NativeWheelEvent = WheelEvent; // @internal (undocumented) export const _noopQrl: (symbolName: string, lexicalScopeCapture?: any[]) => QRL; +// @internal (undocumented) +export const _noopQrlDEV: (symbolName: string, opts: QRLDev, lexicalScopeCapture?: any[]) => QRL; + // @public export type NoSerialize = (T & { __no_serialize__: true; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 57629802450..cad738b5834 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -1,5 +1,5 @@ export { _pauseFromContexts, _serializeData } from './container/pause'; -export { _noopQrl, _regSymbol } from './qrl/qrl'; +export { _noopQrl, _noopQrlDEV, _regSymbol } from './qrl/qrl'; export { _renderSSR } from './render/ssr/render-ssr'; export { _hW } from './render/dom/notify-render'; export { _wrapSignal, _wrapProp } from './state/signal'; diff --git a/packages/qwik/src/core/platform/types.ts b/packages/qwik/src/core/platform/types.ts index a7833c95d1d..2dd7fecec54 100644 --- a/packages/qwik/src/core/platform/types.ts +++ b/packages/qwik/src/core/platform/types.ts @@ -97,7 +97,8 @@ export interface CorePlatform { // chunkForSymbol: ( symbolName: string, - chunk: string | null + chunk: string | null, + parent?: string ) => readonly [symbol: string, chunk: string] | undefined; } diff --git a/packages/qwik/src/core/qrl/qrl.ts b/packages/qwik/src/core/qrl/qrl.ts index b48b3c16e43..fa71916643f 100644 --- a/packages/qwik/src/core/qrl/qrl.ts +++ b/packages/qwik/src/core/qrl/qrl.ts @@ -124,6 +124,17 @@ export const _noopQrl = ( return createQRL(null, symbolName, null, null, null, lexicalScopeCapture, null); }; +/** @internal */ +export const _noopQrlDEV = ( + symbolName: string, + opts: QRLDev, + lexicalScopeCapture: any[] = EMPTY_ARRAY +): QRL => { + const newQrl = _noopQrl(symbolName, lexicalScopeCapture) as QRLInternal; + newQrl.dev = opts; + return newQrl; +}; + /** @internal */ export const qrlDEV = ( chunkOrFn: string | (() => Promise), @@ -163,12 +174,14 @@ export const serializeQRL = (qrl: QRLInternal, opts: QRLSerializeOptions = {}) = const platform = getPlatform(); if (platform) { - const result = platform.chunkForSymbol(refSymbol, chunk); + const result = platform.chunkForSymbol(refSymbol, chunk, qrl.dev?.file); if (result) { chunk = result[1]; if (!qrl.$refSymbol$) { symbol = result[0]; } + } else { + console.error('serializeQRL: Cannot resolve symbol', symbol, 'in', chunk, qrl.dev?.file); } } @@ -197,7 +210,7 @@ export const serializeQRL = (qrl: QRLInternal, opts: QRLSerializeOptions = {}) = throwErrorAndStop('Sync QRL without containerState'); } } - let output = `${chunk}#${symbol}`; + let output = `${encodeURI(chunk)}#${symbol}`; const capture = qrl.$capture$; const captureRef = qrl.$captureRef$; if (captureRef && captureRef.length) { diff --git a/packages/qwik/src/core/qrl/qrl.unit.ts b/packages/qwik/src/core/qrl/qrl.unit.ts index 7f28f16d6d4..6203a8619a3 100644 --- a/packages/qwik/src/core/qrl/qrl.unit.ts +++ b/packages/qwik/src/core/qrl/qrl.unit.ts @@ -99,6 +99,12 @@ describe('serialization', () => { serializeQRL(createQRL('c', 's1', null, null, [1 as any, '2'], null, null)), 'c#s1[1 2]' ); + assert.equal( + serializeQRL( + createQRL('src/routes/[...index]/c', 's1', null, null, [1 as any, '2'], null, null) + ), + 'src/routes/%5B...index%5D/c#s1[1 2]' + ); }); test('should parse reference', () => { diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index b569bf866b4..b687efe2626 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -793,6 +793,10 @@ const getTaskHandlerQrl = (task: SubscriberEffect): QRL<(ev: Event) => void> => [task], taskQrl.$symbol$ ); + // Needed for chunk lookup in dev mode + if (taskQrl.dev) { + taskHandler.dev = taskQrl.dev; + } return taskHandler; }; diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_children.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_children.snap index 5a0df7c0d4f..19510ba041f 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_children.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_children.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2728 +assertion_line: 2749 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_cmp.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_cmp.snap index 82163dec076..4a07bd23a74 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_cmp.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_cmp.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2861 +assertion_line: 2882 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap index ad4c9435fde..365a3cd8514 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2830 +assertion_line: 2850 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_div.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_div.snap index 9fe590e94fe..3b9a2028b0c 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_div.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_div.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2640 +assertion_line: 2661 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_multiple_children.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_multiple_children.snap index 3679e3c5760..b16f3e42837 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_multiple_children.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_multiple_children.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2783 +assertion_line: 2804 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_dev_mode_inlined.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_dev_mode_inlined.snap index eaf9dfdf0bc..c17ff151777 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_dev_mode_inlined.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_dev_mode_inlined.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2187 +assertion_line: 2208 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_immutable_function_components.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_immutable_function_components.snap index 115e6554c99..e8589924e07 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_immutable_function_components.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_immutable_function_components.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2469 +assertion_line: 2490 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_33443.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_33443.snap index ae561cafb9a..0eabedc2669 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_33443.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_33443.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2914 +assertion_line: 2935 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_4438.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_4438.snap index 6d906afdb60..35709fa70d9 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_4438.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_issue_4438.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2704 +assertion_line: 2725 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_mutable_children.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_mutable_children.snap index d63d01af881..336b916c0fb 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_mutable_children.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_mutable_children.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2361 +assertion_line: 2382 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_noop_dev_mode.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_noop_dev_mode.snap new file mode 100644 index 00000000000..efa63976cce --- /dev/null +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_noop_dev_mode.snap @@ -0,0 +1,122 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +assertion_line: 3502 +expression: output +--- +==INPUT== + + +import { component$, useStore, serverStuff$, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + const stuff = useStore(); + serverStuff$(async () => { + // should be removed but keep scope + console.log(stuff.count) + }) + serverStuff$(async () => { + // should be removed + }) + + return ( + +

stuff.count} + onClick$={() => console.log('warn')} + > + Hello Qwik +

+
+ ); +}); + +============================= test.js == + +import { componentQrl } from "@builder.io/qwik"; +import { qrlDEV } from "@builder.io/qwik"; +export const App = /*#__PURE__*/ componentQrl(/*#__PURE__*/ qrlDEV(()=>import("./app_component_ckepmxzlub0"), "App_component_ckEPmXZlub0", { + file: "/user/qwik/src/test.tsx", + lo: 107, + hi: 569, + displayName: "App_component" +})); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;AAGA,OAAO,MAAM,oBAAM;;;;;IAoBhB\"}") +============================= app_component_ckepmxzlub0.js (ENTRY POINT)== + +import { _jsxC } from "@builder.io/qwik"; +import { _jsxQ } from "@builder.io/qwik"; +import { _noopQrlDEV } from "@builder.io/qwik"; +import { serverStuffQrl } from "@builder.io/qwik"; +import { useStore } from "@builder.io/qwik"; +export const App_component_ckEPmXZlub0 = ()=>{ + const stuff = useStore(); + serverStuffQrl(/*#__PURE__*/ _noopQrlDEV("App_component_serverStuff_ebyHaP15ytQ", { + file: "/user/qwik/src/test.tsx", + lo: 0, + hi: 0, + displayName: "App_component_serverStuff" + }, [ + stuff + ])); + serverStuffQrl(/*#__PURE__*/ _noopQrlDEV("App_component_serverStuff_1_PQCqO0ANabY", { + file: "/user/qwik/src/test.tsx", + lo: 0, + hi: 0, + displayName: "App_component_serverStuff_1" + })); + return /*#__PURE__*/ _jsxC(Cmp, { + children: /*#__PURE__*/ _jsxQ("p", null, { + class: "stuff", + shouldRemove$: /*#__PURE__*/ _noopQrlDEV("App_component_Cmp_p_shouldRemove_uU0MG0jvQD4", { + file: "/user/qwik/src/test.tsx", + lo: 0, + hi: 0, + displayName: "App_component_Cmp_p_shouldRemove" + }, [ + stuff + ]), + onClick$: /*#__PURE__*/ _noopQrlDEV("App_component_Cmp_p_onClick_vuXzfUTkpto", { + file: "/user/qwik/src/test.tsx", + lo: 0, + hi: 0, + displayName: "App_component_Cmp_p_onClick" + }) + }, "Hello Qwik", 3, null, { + fileName: "test.tsx", + lineNumber: 16, + columnNumber: 13 + }) + }, 3, "u6_0", { + fileName: "test.tsx", + lineNumber: 15, + columnNumber: 9 + }); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;yCAG8B,IAAM;IAChC,MAAM,QAAQ;IACd;;;;;;;;IAIA;;;;;;IAIA,qBACI,MAAC;kBACG,cAAA,MAAC;YAAE,OAAM;YACL,aAAa;;;;;;;;YACb,QAAQ;;;;;;WACX;;;;;;;;;;AAKb\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "app_component_ckepmxzlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 107, + 569 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_preserve_filenames.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_preserve_filenames.snap index 9a723b60fc7..65bb27d6ae4 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_preserve_filenames.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_preserve_filenames.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2546 +assertion_line: 2567 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap index ba50a96458e..a161ad0d09b 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 3088 +assertion_line: 3109 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_client_code.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_client_code.snap index b13f8f8883b..3c3f7909cc9 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_client_code.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_client_code.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 1814 +assertion_line: 1816 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_transpile_ts_only.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_transpile_ts_only.snap index 71aaee415c8..caaca8aa2f4 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_transpile_ts_only.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_transpile_ts_only.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2491 +assertion_line: 2512 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__issue_476.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__issue_476.snap index 467306ea848..4abf0c9f463 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__issue_476.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__issue_476.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 1974 +assertion_line: 1976 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__special_jsx.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__special_jsx.snap index 412cfa7a10f..29aab4dc067 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__special_jsx.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__special_jsx.snap @@ -1,6 +1,6 @@ --- source: packages/qwik/src/optimizer/core/src/test.rs -assertion_line: 2165 +assertion_line: 2167 expression: output --- ==INPUT== diff --git a/packages/qwik/src/optimizer/core/src/test.rs b/packages/qwik/src/optimizer/core/src/test.rs index 46bdcf2a5f6..1662ddfe208 100644 --- a/packages/qwik/src/optimizer/core/src/test.rs +++ b/packages/qwik/src/optimizer/core/src/test.rs @@ -3497,6 +3497,44 @@ fn example_of_synchronous_qrl() { }); } +#[test] +fn example_noop_dev_mode() { + test_input!(TestInput { + code: r#" +import { component$, useStore, serverStuff$, $ } from '@builder.io/qwik'; + +export const App = component$(() => { + const stuff = useStore(); + serverStuff$(async () => { + // should be removed but keep scope + console.log(stuff.count) + }) + serverStuff$(async () => { + // should be removed + }) + + return ( + +

stuff.count} + onClick$={() => console.log('warn')} + > + Hello Qwik +

+
+ ); +}); +"# + .to_string(), + mode: EmitMode::Dev, + transpile_ts: true, + transpile_jsx: true, + strip_event_handlers: true, + strip_ctx_name: Some(vec!["server".into()]), + ..TestInput::default() + }); +} + // TODO(misko): Make this test work by implementing strict serialization. // #[test] // fn example_of_synchronous_qrl_that_cant_be_serialized() { diff --git a/packages/qwik/src/optimizer/core/src/transform.rs b/packages/qwik/src/optimizer/core/src/transform.rs index 0af05019e31..d028eb201bc 100644 --- a/packages/qwik/src/optimizer/core/src/transform.rs +++ b/packages/qwik/src/optimizer/core/src/transform.rs @@ -761,8 +761,8 @@ impl<'a> QwikTransform<'a> { .as_ref() .map(|e| { fix_path( - dbg!(&self.options.path_data.base_dir), - dbg!(&self.options.path_data.abs_dir), + &self.options.path_data.base_dir, + &self.options.path_data.abs_dir, &["./", e.as_ref()].concat(), ) .map(|f| f.to_string()) @@ -1661,6 +1661,17 @@ impl<'a> QwikTransform<'a> { value: symbol_name.into(), raw: None, }))]; + + let mut fn_name: &JsWord = &_NOOP_QRL; + if self.options.mode == EmitMode::Dev { + args.push(get_qrl_dev_obj( + &self.options.path_data.abs_path, + &hook_data, + &DUMMY_SP, + )); + fn_name = &_NOOP_QRL_DEV; + }; + // Injects state if !hook_data.scoped_idents.is_empty() { args.push(ast::Expr::Array(ast::ArrayLit { @@ -1677,7 +1688,7 @@ impl<'a> QwikTransform<'a> { .collect(), })) } - self.create_internal_call(&_NOOP_QRL, args, true) + self.create_internal_call(fn_name, args, true) } } diff --git a/packages/qwik/src/optimizer/core/src/words.rs b/packages/qwik/src/optimizer/core/src/words.rs index c6865214026..a21cc91ccd7 100644 --- a/packages/qwik/src/optimizer/core/src/words.rs +++ b/packages/qwik/src/optimizer/core/src/words.rs @@ -14,6 +14,7 @@ lazy_static! { pub static ref _INLINED_QRL: JsWord = JsWord::from("inlinedQrl"); pub static ref _INLINED_QRL_DEV: JsWord = JsWord::from("inlinedQrlDEV"); pub static ref _NOOP_QRL: JsWord = JsWord::from("_noopQrl"); + pub static ref _NOOP_QRL_DEV: JsWord = JsWord::from("_noopQrlDEV"); pub static ref _REST_PROPS: JsWord = JsWord::from("_restProps"); // TODO rename hooks to qrls pub static ref QHOOK: JsWord = JsWord::from("$"); diff --git a/packages/qwik/src/optimizer/src/api.md b/packages/qwik/src/optimizer/src/api.md index 27b1c7d7774..6f4305f3872 100644 --- a/packages/qwik/src/optimizer/src/api.md +++ b/packages/qwik/src/optimizer/src/api.md @@ -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; }; @@ -238,7 +234,6 @@ export interface QwikManifest { platform?: { [name: string]: string; }; - // (undocumented) symbols: { [symbolName: string]: QwikSymbol; }; @@ -382,7 +377,7 @@ export type SourceMapsOption = 'external' | 'inline' | undefined | null; export type SymbolMapper = Record; // @public (undocumented) -export type SymbolMapperFn = (symbolName: string, mapper: SymbolMapper | undefined) => readonly [symbol: string, chunk: string] | undefined; +export type SymbolMapperFn = (symbolName: string, mapper: SymbolMapper | undefined, parent?: string) => readonly [symbol: string, chunk: string] | undefined; // @public (undocumented) export type SystemEnvironment = 'node' | 'deno' | 'bun' | 'webworker' | 'browsermain' | 'unknown'; diff --git a/packages/qwik/src/optimizer/src/manifest.ts b/packages/qwik/src/optimizer/src/manifest.ts index e179ebc2581..6bb517e9732 100644 --- a/packages/qwik/src/optimizer/src/manifest.ts +++ b/packages/qwik/src/optimizer/src/manifest.ts @@ -1,13 +1,6 @@ -import type { NormalizedQwikPluginOptions } from './plugins/plugin'; -import type { - GeneratedOutputBundle, - GlobalInjections, - HookAnalysis, - Path, - QwikBundle, - QwikManifest, - QwikSymbol, -} from './types'; +import type { OutputBundle } from 'rollup'; +import { type NormalizedQwikPluginOptions } from './plugins/plugin'; +import type { GlobalInjections, HookAnalysis, Path, QwikBundle, QwikManifest } from './types'; // This is just the initial prioritization of the symbols and entries // at build time so there's less work during each SSR. However, SSR should @@ -178,8 +171,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]; @@ -247,7 +240,7 @@ export function generateManifestFromBundles( path: Path, hooks: HookAnalysis[], injections: GlobalInjections[], - outputBundles: GeneratedOutputBundle[], + outputBundles: OutputBundle, opts: NormalizedQwikPluginOptions ) { const manifest: QwikManifest = { @@ -264,55 +257,29 @@ export function generateManifestFromBundles( }, }; - for (const hook of hooks) { - const buildFilePath = `${hook.canonicalFilename}.${hook.extension}`; - - const outputBundle = outputBundles.find((b) => { - return Object.keys(b.modules).find((f) => f.endsWith(buildFilePath)); - }); - - if (outputBundle) { - const symbolName = hook.name; - const bundleFileName = path.basename(outputBundle.fileName); - - manifest.mapping[symbolName] = bundleFileName; - - manifest.symbols[symbolName] = { - origin: hook.origin, - displayName: hook.displayName, - canonicalFilename: hook.canonicalFilename, - hash: hook.hash, - ctxKind: hook.ctxKind, - ctxName: hook.ctxName, - captures: hook.captures, - parent: hook.parent, - loc: hook.loc, - }; - - addBundleToManifest(path, manifest, outputBundle, bundleFileName); + // We need to find our QRL exports + const qrlNames = new Set([...hooks.map((h) => h.name)]); + for (const [fileName, outputBundle] of Object.entries(outputBundles)) { + if (outputBundle.type !== 'chunk') { + continue; } - } - - for (const outputBundle of outputBundles) { - const bundleFileName = path.basename(outputBundle.fileName); - addBundleToManifest(path, manifest, outputBundle, bundleFileName); - } - - return updateSortAndPriorities(manifest); -} + const bundleFileName = path.basename(fileName); -function addBundleToManifest( - path: Path, - manifest: QwikManifest, - outputBundle: GeneratedOutputBundle, - bundleFileName: string -) { - if (!manifest.bundles[bundleFileName]) { const buildDirName = path.dirname(outputBundle.fileName); const bundle: QwikBundle = { - size: outputBundle.size, + size: outputBundle.code.length, }; + for (const symbol of outputBundle.exports) { + if (qrlNames.has(symbol)) { + // When not minifying we see both the entry and the hook file + // The hook file will only have 1 export, we want the entry + if (!manifest.mapping[symbol] || outputBundle.exports.length !== 1) { + manifest.mapping[symbol] = bundleFileName; + } + } + } + const bundleImports = outputBundle.imports .filter((i) => path.dirname(i) === buildDirName) .map((i) => path.relative(buildDirName, i)); @@ -327,11 +294,36 @@ function addBundleToManifest( bundle.dynamicImports = bundleDynamicImports; } - const modulePaths = Object.keys(outputBundle.modules).filter((m) => !m.startsWith(`\u0000`)); + // Rollup doesn't provide the moduleIds in the outputBundle but Vite does + const ids = outputBundle.moduleIds || Object.keys(outputBundle.modules); + const modulePaths = ids.filter((m) => !m.startsWith(`\u0000`)); if (modulePaths.length > 0) { bundle.origins = modulePaths; } manifest.bundles[bundleFileName] = bundle; } + + 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.name}`, manifest); + throw new Error(`Unable to find bundle for hook: ${hook.hash}`); + } + (manifest.bundles[bundle].symbols ||= []).push(symbol); + manifest.symbols[symbol] = { + origin: hook.origin, + displayName: hook.displayName, + canonicalFilename: hook.canonicalFilename, + hash: hook.hash, + ctxKind: hook.ctxKind, + ctxName: hook.ctxName, + captures: hook.captures, + parent: hook.parent, + loc: hook.loc, + }; + } + + return updateSortAndPriorities(manifest); } diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index 1ce2ca86a7c..2ce329f0dbc 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -1,11 +1,10 @@ -import type { Rollup } from 'vite'; +import type { Rollup, Plugin } from 'vite'; import { hashCode } from '../../../core/util/hash_code'; import { generateManifestFromBundles, getValidManifest } from '../manifest'; import { createOptimizer } from '../optimizer'; import type { Diagnostic, EntryStrategy, - GeneratedOutputBundle, GlobalInjections, HookAnalysis, InsightManifest, @@ -19,6 +18,7 @@ import type { TransformOutput, } from '../types'; import { createLinter, type QwikLinter } from './eslint-plugin'; +import type { LoadResult, OutputBundle, TransformResult } from 'rollup'; const REG_CTX_NAME = ['server']; @@ -65,10 +65,10 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { const ssrResults = new Map(); const ssrTransformedOutputs = new Map(); + const foundQrls = new Map(); let internalOptimizer: Optimizer | null = null; let linter: QwikLinter | undefined = undefined; - const hookManifest: Record = {}; let diagnosticsCallback: ( d: Diagnostic[], optimizer: Optimizer, @@ -302,7 +302,6 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } else { opts.lint = updatedOpts.buildMode === 'development'; } - return { ...opts }; }; @@ -331,10 +330,9 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } }; - const buildStart = async (ctx: any) => { - debug(`buildStart()`, opts.buildMode, opts.scope); + const buildStart = async (ctx: Rollup.PluginContext) => { + debug(`buildStart()`, opts.buildMode, opts.scope, opts.target); const optimizer = getOptimizer(); - if (optimizer.sys.env === 'node' && opts.target === 'ssr' && opts.lint) { try { linter = await createLinter(optimizer.sys, opts.rootDir, opts.tsconfigFileNames); @@ -404,12 +402,11 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { debug(`buildStart() add transformedOutput`, key, output.hook?.displayName); transformedOutputs.set(key, [output, key]); ssrTransformedOutputs.set(key, [output, key]); - if (output.hook) { - hookManifest[output.hook.hash] = key; - } else if (output.isEntry) { + if (opts.target === 'client' && output.isEntry) { ctx.emitFile({ id: key, type: 'chunk', + preserveSignature: 'allow-extension', }); } } @@ -425,9 +422,9 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { ctx: Rollup.PluginContext, id: string, importer: string | undefined, - ssrOpts?: { ssr?: boolean } + resolveOpts?: Parameters>[2] ) => { - debug(`resolveId()`, 'Start', id, importer); + debug(`resolveId()`, 'Start', id, importer, resolveOpts, opts.target); if (id.startsWith('\0') || id.startsWith('/@fs')) { return; } @@ -456,12 +453,8 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { moduleSideEffects: false, }; } - let firstInput: string; - if (Array.isArray(opts.input)) { - firstInput = opts.input[0]; - } else { - firstInput = Object.values(opts.input)[0]; - } + + const firstInput = Object.values(opts.input)[0]; return { id: normalizePath(getPath().resolve(firstInput, QWIK_CLIENT_MANIFEST_ID)), moduleSideEffects: false, @@ -469,13 +462,32 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } const path = getPath(); - const isSSR = ssrOpts?.ssr ?? opts.target === 'ssr'; + const isSSR = !!resolveOpts?.ssr; if (importer) { // Only process ids that look like paths if (!(id.startsWith('.') || path.isAbsolute(id))) { return; } + if (opts.target === 'ssr') { + const match = /^([^?]*)\?_qrl_parent=(.*)/.exec(id); + if (match) { + // ssr mode asking for a client qrl, this will fall through to the devserver + // building here via ctx.load doesn't seem to work (target is always ssr?) + // eslint-disable-next-line prefer-const + let [, qrlId, parentId] = match; + // If the parent is not in root (e.g. pnpm symlink), the qrl also isn't + if (parentId.startsWith(opts.rootDir)) { + qrlId = `${opts.rootDir}${qrlId}`; + } + if (!transformedOutputs.has(qrlId)) { + // fall back to dev server which can wait for transform() to finish + return null; + } + debug(`resolveId()`, 'Resolved', qrlId); + return { id: qrlId }; + } + } const parsedId = parseId(id); let importeePathId = normalizePath(parsedId.pathId); const ext = path.extname(importeePathId).toLowerCase(); @@ -518,14 +530,19 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } } } + // We don't (yet) know this id + debug(`resolveId()`, 'Not resolved', id, importer, resolveOpts); return null; }; - - const load = async (_ctx: any, id: string, ssrOpts: { ssr?: boolean } = {}) => { + const load = async ( + ctx: Rollup.PluginContext, + id: string, + loadOpts?: Parameters>[1] + ): Promise => { if (id.startsWith('\0') || id.startsWith('/@fs/')) { return; } - const isSSR = ssrOpts?.ssr ?? opts.target === 'ssr'; + const isSSR = !!loadOpts?.ssr; if (opts.resolveQwikBuild && id.endsWith(QWIK_BUILD_ID)) { debug(`load()`, QWIK_BUILD_ID, opts.buildMode); return { @@ -549,15 +566,11 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { if (transformedModule) { debug(`load()`, 'Found', id); + let { code } = transformedModule[0]; + const { map, hook } = transformedModule[0]; - let code = transformedModule[0].code; - let firstInput: string; - if (Array.isArray(opts.input)) { - firstInput = opts.input[0]; - } else { - firstInput = Object.values(opts.input)[0]; - } if (opts.target === 'ssr') { + const firstInput = Object.values(opts.input)[0]; // doing this because vite will not use resolveId() when "noExternal" is false // so we need to turn the @qwik-client-manifest import into a relative import code = code.replace( @@ -565,15 +578,10 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { normalizePath(path.resolve(firstInput, QWIK_CLIENT_MANIFEST_ID)) ); } - return { - code, - map: transformedModule[0].map, - meta: { - hook: transformedModule[0].hook, - }, - }; + return { code, map, meta: { hook } }; } + debug('load()', 'Not found', id, parsedId); return null; }; @@ -581,12 +589,12 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { ctx: Rollup.PluginContext, code: string, id: string, - ssrOpts: { ssr?: boolean } = {} - ) { + transformOpts: Parameters>[2] = {} + ): Promise { if (id.startsWith('\0') || id.startsWith('/@fs/')) { return; } - const isSSR = ssrOpts.ssr ?? opts.target === 'ssr'; + const isSSR = !!transformOpts.ssr; const currentOutputs = isSSR ? ssrTransformedOutputs : transformedOutputs; if (currentOutputs.has(id)) { return; @@ -604,9 +612,17 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { TRANSFORM_REGEX.test(pathId) || insideRoots(ext, dir, opts.srcDir, opts.vendorRoots) ) { + /** Strip client|server code from server|client */ const strip = opts.target === 'client' || opts.target === 'ssr'; const normalizedID = normalizePath(pathId); - debug(`transform()`, 'Transforming', pathId); + debug(`transform()`, 'Transforming', { + pathId, + id, + parsedPathId, + strip, + isSSR, + target: opts.target, + }); let filePath = base; if (opts.srcDir) { @@ -616,20 +632,9 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { const srcDir = opts.srcDir ? opts.srcDir : normalizePath(dir); const mode = opts.target === 'lib' ? 'lib' : opts.buildMode === 'development' ? 'dev' : 'prod'; - // const entryStrategy: EntryStrategy = ['hoist', 'hook', 'inline'].includes(opts.entryStrategy.type) - // ? opts.entryStrategy - // : { - // type: 'hook', - // manual: hookManifest, - // }; const entryStrategy: EntryStrategy = opts.entryStrategy; const transformOpts: TransformModulesOptions = { - input: [ - { - code: code, - path: filePath, - }, - ], + input: [{ code, path: filePath }], entryStrategy, minify: 'simplify', // Always enable sourcemaps in dev for click-to-source @@ -638,17 +643,17 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { transpileJsx: true, explicitExtensions: true, preserveFilenames: true, - srcDir: srcDir, + srcDir, rootDir: opts.rootDir, - mode: mode, - scope: opts.scope ? opts.scope : void 0, + mode, + scope: opts.scope || undefined, }; + if (isSSR) { - transformOpts.isServer = isSSR; + transformOpts.isServer = true; transformOpts.entryStrategy = { type: 'hoist' }; } if (strip) { - transformOpts.isServer = isSSR; if (isSSR) { transformOpts.stripCtxName = CLIENT_STRIP_CTX_NAME; transformOpts.stripEventHandlers = true; @@ -660,7 +665,18 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } const newOutput = optimizer.transformModulesSync(transformOpts); + const module = newOutput.modules.find((mod) => !isAdditionalFile(mod))!; + if (opts.target === 'ssr') { + // we're in dev mode. All QRLs that might be emitted in SSR HTML are defined here. + // register them so that they can be resolved by the dev server + const matches = module.code.matchAll(/_([a-zA-Z0-9]{11,11})['"][,)]/g); + for (const [, symbol] of matches) { + foundQrls.set(symbol, id); + } + } + // uncomment to show transform results + // debug({ isSSR, strip }, transformOpts, newOutput); diagnosticsCallback(newOutput.diagnostics, optimizer, srcDir); if (isSSR) { @@ -673,47 +689,19 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { } const deps = new Set(); for (const mod of newOutput.modules) { - if (isTransformedFile(mod)) { + if (mod !== module) { const key = normalizePath(path.join(srcDir, mod.path)); currentOutputs.set(key, [mod, id]); deps.add(key); - } - } - if (isSSR && strip) { - const clientTransformOpts: TransformModulesOptions = { - input: [ - { - code: code, - path: filePath, - }, - ], - entryStrategy: opts.entryStrategy, - minify: 'simplify', - sourceMaps: opts.sourcemap || 'development' === opts.buildMode, - transpileTs: true, - transpileJsx: true, - explicitExtensions: true, - preserveFilenames: true, - srcDir: srcDir, - rootDir: opts.rootDir, - mode: mode, - scope: opts.scope ? opts.scope : void 0, - }; - clientTransformOpts.stripCtxName = SERVER_STRIP_CTX_NAME; - clientTransformOpts.stripExports = SERVER_STRIP_EXPORTS; - clientTransformOpts.isServer = false; - const clientNewOutput = optimizer.transformModulesSync(clientTransformOpts); - - diagnosticsCallback(clientNewOutput.diagnostics, optimizer, srcDir); - - results.set(normalizedID, clientNewOutput); - for (const mod of clientNewOutput.modules) { - if (isTransformedFile(mod)) { - const key = normalizePath(path.join(srcDir, mod.path)); - ctx.addWatchFile(key); - transformedOutputs.set(key, [mod, id]); - deps.add(key); + // rollup must be told about entry points + if (opts.target === 'client' && mod.isEntry) { + ctx.emitFile({ + id: key, + type: 'chunk', + preserveSignature: 'allow-extension', + }); } + ctx.addWatchFile(key); } } @@ -725,7 +713,6 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { await ctx.load({ id }); } - const module = newOutput.modules.find((mod) => !isTransformedFile(mod))!; return { code: module.code, map: module.map, @@ -736,16 +723,14 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { }; } - debug(`transform()`, 'No Transforming', id); + debug(`transform()`, 'Not transforming', id); return null; }; - const createOutputAnalyzer = () => { - const outputBundles: GeneratedOutputBundle[] = []; + const createOutputAnalyzer = (rollupBundle: OutputBundle) => { const injections: GlobalInjections[] = []; - const addBundle = (b: GeneratedOutputBundle) => outputBundles.push(b); const addInjection = (b: GlobalInjections) => injections.push(b); const generateManifest = async () => { const optimizer = getOptimizer(); @@ -756,7 +741,7 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { .map((mod) => mod.hook) .filter((h) => !!h) as HookAnalysis[]; - const manifest = generateManifestFromBundles(path, hooks, injections, outputBundles, opts); + const manifest = generateManifestFromBundles(path, hooks, injections, rollupBundle, opts); for (const symbol of Object.values(manifest.symbols)) { if (symbol.origin) { @@ -780,7 +765,7 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { return manifest; }; - return { addBundle, addInjection, generateManifest }; + return { addInjection, generateManifest }; }; const getOptions = () => opts; @@ -869,6 +854,7 @@ export const manifest = ${JSON.stringify(manifest)};\n`; transform, validateSource, setSourceMapSupport, + foundQrls, }; } @@ -887,7 +873,7 @@ const insideRoots = (ext: string, dir: string, srcDir: string | null, vendorRoot return false; }; -function isTransformedFile(mod: TransformModule) { +function isAdditionalFile(mod: TransformModule) { return mod.isEntry || mod.hook; } diff --git a/packages/qwik/src/optimizer/src/plugins/rollup.ts b/packages/qwik/src/optimizer/src/plugins/rollup.ts index acfc40fd685..e5d14621c77 100644 --- a/packages/qwik/src/optimizer/src/plugins/rollup.ts +++ b/packages/qwik/src/optimizer/src/plugins/rollup.ts @@ -117,22 +117,8 @@ export function qwikRollup(qwikRollupOpts: QwikRollupPluginOptions = {}): any { if (opts.target === 'client') { // client build - const outputAnalyzer = qwikPlugin.createOutputAnalyzer(); - - for (const fileName in rollupBundle) { - const b = rollupBundle[fileName]; - if (b.type === 'chunk') { - outputAnalyzer.addBundle({ - fileName, - modules: b.modules, - imports: b.imports, - dynamicImports: b.dynamicImports, - size: b.code.length, - }); - } - } - const optimizer = qwikPlugin.getOptimizer(); + const outputAnalyzer = qwikPlugin.createOutputAnalyzer(rollupBundle); const manifest = await outputAnalyzer.generateManifest(); manifest.platform = { ...versions, diff --git a/packages/qwik/src/optimizer/src/plugins/vite-server.ts b/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts similarity index 86% rename from packages/qwik/src/optimizer/src/plugins/vite-server.ts rename to packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts index 6dee7ad1def..ef7ed97b38a 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite-server.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts @@ -4,7 +4,7 @@ import { magenta } from 'kleur/colors'; import type { IncomingMessage, ServerResponse } from 'http'; import type { Connect, ViteDevServer } from 'vite'; -import type { OptimizerSystem, Path, QwikManifest, SymbolMapper } from '../types'; +import type { OptimizerSystem, Path, QwikManifest } from '../types'; import { type NormalizedQwikPluginOptions, parseId } from './plugin'; import type { QwikViteDevResponse } from './vite'; import { formatError } from './vite-utils'; @@ -28,12 +28,14 @@ function getOrigin(req: IncomingMessage) { } export async function configureDevServer( + base: string, server: ViteDevServer, opts: NormalizedQwikPluginOptions, sys: OptimizerSystem, path: Path, isClientDevOnly: boolean, - clientDevInput: string | undefined + clientDevInput: string | undefined, + foundQrls: Map ) { if (typeof fetch !== 'function' && sys.env === 'node') { // polyfill fetch() when not available in Node.js @@ -54,7 +56,7 @@ export async function configureDevServer( } // qwik middleware injected BEFORE vite internal middlewares - server.middlewares.use(async (req: any, res: any, next: any) => { + server.middlewares.use(async (req, res, next) => { try { const { ORIGIN } = process.env; const domain = ORIGIN ?? getOrigin(req); @@ -84,12 +86,7 @@ export async function configureDevServer( return; } - let firstInput: string; - if (Array.isArray(opts.input)) { - firstInput = opts.input[0]; - } else { - firstInput = Object.values(opts.input)[0]; - } + const firstInput = Object.values(opts.input)[0]; const ssrModule = await server.ssrLoadModule(firstInput); const render: Render = ssrModule.default ?? ssrModule.render; @@ -136,10 +133,6 @@ export async function configureDevServer( }); }); - const srcBase = opts.srcDir - ? path.relative(opts.rootDir, opts.srcDir).replace(/\\/g, '/') - : 'src'; - const renderOpts: RenderToStreamOptions = { debug: true, locale: serverData.locale, @@ -148,26 +141,33 @@ export async function configureDevServer( manifest: isClientDevOnly ? undefined : manifest, symbolMapper: isClientDevOnly ? undefined - : (symbolName: string, mapper: SymbolMapper | undefined) => { + : (symbolName, mapper, parent) => { if (symbolName === SYNC_QRL) { return [symbolName, '']; } - const defaultChunk = [ - symbolName, - `/${srcBase}/${symbolName.toLowerCase()}.js`, - ] as const; - if (mapper) { - const hash = getSymbolHash(symbolName); - return mapper[hash] ?? defaultChunk; - } else { - return defaultChunk; + const chunk = mapper && mapper[getSymbolHash(symbolName)]; + if (chunk) { + return chunk; } + parent ||= foundQrls.get(symbolName); + if (!parent) { + console.error( + 'qwik vite-dev-server: unknown qrl requested without parent:', + symbolName + ); + return [symbolName, `${base}${symbolName.toLowerCase()}.js`]; + } + const parentPath = path.dirname(parent); + // support getting files through pnpm link symlinks + const qrlPath = parentPath.startsWith(opts.rootDir) + ? path.relative(opts.rootDir, parentPath) + : parentPath; + const qrlFile = `${qrlPath}/${symbolName.toLowerCase()}.js?_qrl_parent=${parent}`; + return [symbolName, `${base}${qrlFile}`]; }, prefetchStrategy: null, serverData, - containerAttributes: { - ...serverData.containerAttributes, - }, + containerAttributes: { ...serverData.containerAttributes }, }; res.setHeader('Content-Type', 'text/html; charset=utf-8'); @@ -206,6 +206,18 @@ export async function configureDevServer( next(); } } else { + // We didn't ssr, but maybe a qrl was requested + const parent = url.searchParams.get('_qrl_parent'); + if (parent && url.pathname.endsWith('.js')) { + // load the parent so it populates the qrl cache + await server.transformRequest(parent); + const result = await server.transformRequest(url.pathname); + if (result) { + res.setHeader('content-type', 'application/javascript'); + res.write(result.code); + res.end(); + } + } next(); } } catch (e: any) { diff --git a/packages/qwik/src/optimizer/src/plugins/vite.ts b/packages/qwik/src/optimizer/src/plugins/vite.ts index 0ab90e473f5..676bffc6b5f 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite.ts @@ -33,7 +33,7 @@ import { type QwikPluginOptions, } from './plugin'; import { createRollupError, normalizeRollupOutputOptions } from './rollup'; -import { VITE_DEV_CLIENT_QS, configureDevServer, configurePreviewServer } from './vite-server'; +import { VITE_DEV_CLIENT_QS, configureDevServer, configurePreviewServer } from './vite-dev-server'; const DEDUPE = [QWIK_CORE_ID, QWIK_JSX_RUNTIME_ID, QWIK_JSX_DEV_RUNTIME_ID]; @@ -490,18 +490,10 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { if (opts.target === 'client') { // client build - const outputAnalyzer = qwikPlugin.createOutputAnalyzer(); + const outputAnalyzer = qwikPlugin.createOutputAnalyzer(rollupBundle); for (const [fileName, b] of Object.entries(rollupBundle)) { - if (b.type === 'chunk') { - outputAnalyzer.addBundle({ - fileName, - modules: b.modules, - imports: b.imports, - dynamicImports: b.dynamicImports, - size: b.code.length, - }); - } else { + if (b.type === 'asset') { const baseFilename = basePathname + fileName; if (STYLING.some((ext) => fileName.endsWith(ext))) { if (typeof b.source === 'string' && b.source.length < opts.inlineStylesUpToBytes) { @@ -666,7 +658,16 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { const opts = qwikPlugin.getOptions(); const sys = qwikPlugin.getSys(); const path = qwikPlugin.getPath(); - await configureDevServer(server, opts, sys, path, isClientDevOnly, clientDevInput); + await configureDevServer( + basePathname, + server, + opts, + sys, + path, + isClientDevOnly, + clientDevInput, + qwikPlugin.foundQrls + ); }; const isNEW = (globalThis as any).__qwikCityNew === true; if (isNEW) { diff --git a/packages/qwik/src/optimizer/src/types.ts b/packages/qwik/src/optimizer/src/types.ts index 527fc88e347..80874ab2901 100644 --- a/packages/qwik/src/optimizer/src/types.ts +++ b/packages/qwik/src/optimizer/src/types.ts @@ -201,12 +201,21 @@ export interface SmartEntryStrategy { manual?: Record; } -/** @public */ +/** + * The metadata of the build. One of its uses is storing where QRL symbols are located. + * + * @public + */ export interface QwikManifest { + /** Content hash of the manifest, if this changes, the code changed */ manifestHash: string; + /** QRL symbols */ symbols: { [symbolName: string]: QwikSymbol }; + /** Where QRLs are located */ mapping: { [symbolName: string]: string }; + /** All code bundles, used to know the import graph */ bundles: { [fileName: string]: QwikBundle }; + /** CSS etc to inject in the document head */ injections?: GlobalInjections[]; version: string; options?: { @@ -233,7 +242,8 @@ export type SymbolMapper = Record readonly [symbol: string, chunk: string] | undefined; /** @public */ @@ -265,16 +275,6 @@ export interface GlobalInjections { location: 'head' | 'body'; } -export interface GeneratedOutputBundle { - fileName: string; - modules: { - [id: string]: any; - }; - imports: string[]; - dynamicImports: string[]; - size: number; -} - // PATH UTIL *************** /** @public */ diff --git a/packages/qwik/src/server/platform.ts b/packages/qwik/src/server/platform.ts index 87d97ddfd82..a275d3924d4 100644 --- a/packages/qwik/src/server/platform.ts +++ b/packages/qwik/src/server/platform.ts @@ -15,7 +15,7 @@ export function createPlatform( const mapper = resolvedManifest?.mapper; const mapperFn = opts.symbolMapper ? opts.symbolMapper - : (symbolName: string): readonly [string, string] | undefined => { + : (symbolName: string, _chunk: any, parent?: string): readonly [string, string] | undefined => { if (mapper) { const hash = getSymbolHash(symbolName); const result = mapper[hash]; @@ -27,7 +27,11 @@ export function createPlatform( if (isRegistered) { return [symbolName, '_'] as const; } - console.error('Cannot resolve symbol', symbolName, 'in', mapper); + if (parent) { + // In dev mode, SSR may need to refer to a symbol that wasn't built yet on the client + return [symbolName, `${parent}?qrl=${symbolName}`] as const; + } + console.error('Cannot resolve symbol', symbolName, 'in', mapper, parent); } return result; } @@ -65,8 +69,8 @@ export function createPlatform( }); }); }, - chunkForSymbol(symbolName: string) { - return mapperFn(symbolName, mapper); + chunkForSymbol(symbolName: string, _chunk, parent) { + return mapperFn(symbolName, mapper, parent); }, }; return serverPlatform; diff --git a/packages/qwik/src/testing/platform.ts b/packages/qwik/src/testing/platform.ts index c87325eb6f0..56e3c19c75f 100644 --- a/packages/qwik/src/testing/platform.ts +++ b/packages/qwik/src/testing/platform.ts @@ -26,10 +26,16 @@ function createPlatform() { const importPath = toPath(urlDoc); const mod = moduleCache.get(importPath); if (mod) { + if (!mod || !(symbolName in mod)) { + throw new Error(`Q-ERROR: missing symbol '${symbolName}' in module '${url}'.`); + } return mod[symbolName]; } return import(importPath).then((mod) => { moduleCache.set(importPath, mod); + if (!mod || !(symbolName in mod)) { + throw new Error(`Q-ERROR: missing symbol '${symbolName}' in module '${url}'.`); + } return mod[symbolName]; }); },