diff --git a/package.json b/package.json index 65592728c08..e411250b436 100644 --- a/package.json +++ b/package.json @@ -197,8 +197,8 @@ "build.core": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --optimizer --insights --qwikrouter --api --platform-binding", "build.core.dev": "node --require ./scripts/runBefore.ts scripts/index.ts --qwik --optimizer --insights --qwikrouter --platform-binding --dev", "build.eslint": "node --require ./scripts/runBefore.ts scripts/index.ts --eslint", - "build.full": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm", - "build.local": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy", + "build.full": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --optimizer --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm", + "build.local": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --optimizer --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy", "build.only_javascript": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --api", "build.packages.docs": "pnpm -C ./packages/docs/ run build", "build.packages.insights": "pnpm -C ./packages/insights/ run build", diff --git a/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx index 55289e4e23b..14a0a00d8b6 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx @@ -9,7 +9,9 @@ import CodeSandbox from '../../../../../components/code-sandbox/index.tsx'; `Each` is a built-in Qwik component for rendering keyed lists. -It is most useful when list items have a stable identity and you want Qwik to preserve and move existing rows instead of re-rendering the whole list when the order changes. +For most list rendering, write normal `items.map()` code and let Qwik optimize compatible keyed loops automatically, either directly to `Each` or through the same keyed runtime internally. + +This page documents the manual `Each` API for the cases where you want to use it explicitly. For the default list-rendering guidance, see [Rendering](../rendering/index.mdx). ```tsx /Each/ /key$/ /item$/ @@ -76,33 +78,17 @@ export default component$(() => { }); ``` -## `Each` vs `items.map()` - -If your list items have stable keys, prefer `Each`. - -It is the specialized keyed-list primitive in Qwik and is designed to preserve and move existing rows efficiently. - -Use `items.map()` for simple list rendering or when you do not have a stable key: - -```tsx - -``` +## When to use manual `Each` -Prefer `Each` when: +Most applications should use `items.map()` in component templates. -- items have stable ids -- rows are frequently reordered, inserted, or removed -- you want to preserve existing row DOM and component instances +Manual `Each` is useful when: -Prefer `map()` when: +- you want to make the keyed-list behavior explicit in the source +- you are building a reusable abstraction around `Each` +- you prefer the explicit `items`, `key$`, and `item$` API shape for a specific list -- there is no stable key for the item -- you want ordinary JSX list rendering without `Each`'s keyed-row preservation behavior -- the row output should be recomputed from replaced item objects even when the key stays the same +If you want the usual authoring experience, use `items.map()` and let the optimizer rewrite compatible loops for you. ## How keyed updates work @@ -127,5 +113,5 @@ Using unstable keys defeats the main benefit of `Each` and can produce confusing ## Related -- For general list rendering, see [Rendering](../rendering/index.mdx) +- For default list rendering with `items.map()`, automatic optimization, warnings, and opt-out comments, see [Rendering](../rendering/index.mdx) - For the generated API entry, see [API Reference](/api/qwik/#each) diff --git a/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx index 061f2ac73f9..f44ffa9f276 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx @@ -100,9 +100,9 @@ export const Child = component$((props: { name: string }) => { ### Rendering a list of items -Qwik supports rendering lists with either `items.map()` or the [`Each`](../each/index.mdx) component. +Write lists with normal `items.map()` in component templates. -For keyed collections, prefer [`Each`](../each/index.mdx). If you render with `items.map()`, every item in the list must have a unique `key` property on the first child returned by the mapping function. The `key` must be a string or number and must be unique within the list. +If you render with `items.map()`, every item in the list must have a unique `key` property on the first child returned by the mapping function. The `key` must be a string or number and must be unique within the list. ```tsx {6} /data.map/ /key/#a import { component$ } from '@qwik.dev/core'; @@ -122,6 +122,59 @@ export const Parent = component$(() => { It is not recommended to use the array's index as the key unless you can guarantee that the data for a given key will always be the same. It is always preferred to use some unique identifier from the data as the key. +## Automatic `.map()` optimization + +For compatible render loops, Qwik optimizes keyed `items.map()` calls into the same keyed-list machinery used by [`Each`](../each/index.mdx) automatically during compilation. + +The optimization applies when: + +- the callback returns a single JSX node +- the first returned node has a `key` +- the key does not use the callback's index parameter +- the key is not derived from a function call + +For example, this authoring pattern: + +```tsx +{items.value.map((item) => ( +
{item.label}
+))} +``` + +can be optimized automatically. + +For simple capture-free loops, Qwik can lower the render loop directly to `Each`. + +When the row render or key expression captures values from the surrounding scope, Qwik can still lower the loop through an internal fallback-assisted path. In that case, Qwik keeps the original `.map()` behavior available and uses keyed `Each` rendering when the captured values are safe for that path. + +If a `.map()` looks like a keyed list render but cannot be optimized, Qwik emits a `map-to-each` optimizer warning with the reason. If the conditions are not met, the original `.map()` stays unchanged. + +Common reasons for the warning are: + +- the returned JSX node is missing a `key` +- the key uses the callback's index parameter +- the key comes from a function call +- the callback does not return a single JSX node + +## Disabling the optimization + +If you want to opt out for a specific render loop, use: + +```tsx +{ + /* @qwik-disable-next-line map-to-each */ + items.map((item) =>
{item.label}
) +} +``` + +That disables the optimization and silences `map-to-each` warnings for that call. + +## Manual `Each` + +Manual [`Each`](../each/index.mdx) is still available as a public API, but it is now the explicit or advanced form rather than the default recommendation. + +Use manual `Each` when you want to make the keyed-list behavior explicit in the source or when you are building an abstraction around its `items`, `key$`, and `item$` props. + ### Rendering Conditionally Conditional rendering is done with the Javascipt [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `?`, the `&&` operator, or just by using `if` statements. diff --git a/packages/docs/src/routes/docs/menu.md b/packages/docs/src/routes/docs/menu.md index 24ab77432b4..a765b2ebbf0 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -16,8 +16,8 @@ - [Tasks & Lifecycle]() - [Context]() - [Slots]() -- [Each]() - [Rendering]() +- [Each (Reference)]() - [Styling]() - [API Reference](/api/qwik/) diff --git a/packages/optimizer/core/src/parse.rs b/packages/optimizer/core/src/parse.rs index f11061b77b0..243543ee731 100644 --- a/packages/optimizer/core/src/parse.rs +++ b/packages/optimizer/core/src/parse.rs @@ -437,6 +437,10 @@ pub fn transform_code(config: TransformCodeOptions) -> Result = Vec::with_capacity(segments.len() + 10); @@ -615,7 +619,8 @@ pub fn transform_code(config: TransformCodeOptions) -> Resultp0.value.selected.value ? "danger" : ""; const _hf0_str = 'p0.value.selected.value?"danger":""'; const _hf1 = (p0)=>p0.value.id; const _hf1_str = "p0.value.id"; +export const App_component_table_Each_item_oE2PAPFAIIE = (row)=>{ + return /*#__PURE__*/ _jsxSorted("tr", { + class: _fnSignal(_hf0, [ + row + ], _hf0_str) + }, null, /*#__PURE__*/ _jsxSorted("td", null, null, _fnSignal(_hf1, [ + row + ], _hf1_str), 1, null), 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;mBAgBmB,GAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,GAAG,WAAW;;mBAExC,GAAI,KAAK,CAAC,EAAE;;0DANN;yBAEb,WAAC;QAEC,KAAK;;;2BAEL,WAAC\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_table_Each_item_oE2PAPFAIIE", + "entry": null, + "displayName": "test.tsx_App_component_table_Each_item", + "hash": "oE2PAPFAIIE", + "canonicalFilename": "test.tsx_App_component_table_Each_item_oE2PAPFAIIE", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "row" + ] +} +*/ +============================= test.tsx_App_component_table_Each_key_flZ5LUpd25I.js (ENTRY POINT)== + +export const App_component_table_Each_key_flZ5LUpd25I = (row)=>{ + return row.value.id; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"yDAYuB;WAGN,IAAI,KAAK,CAAC,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_table_Each_key_flZ5LUpd25I", + "entry": null, + "displayName": "test.tsx_App_component_table_Each_key", + "hash": "flZ5LUpd25I", + "canonicalFilename": "test.tsx_App_component_table_Each_key_flZ5LUpd25I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "row" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_table_Each_item_oE2PAPFAIIE = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_table_Each_item_oE2PAPFAIIE"), "App_component_table_Each_item_oE2PAPFAIIE"); +const q_App_component_table_Each_key_flZ5LUpd25I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_table_Each_key_flZ5LUpd25I"), "App_component_table_Each_key_flZ5LUpd25I"); +// export const App_component_ckEPmXZlub0 = ()=>{ const data = { value: [ @@ -80,19 +156,15 @@ export const App_component_ckEPmXZlub0 = ()=>{ } ] }; - return /*#__PURE__*/ _jsxSorted("table", null, null, data.value.map((row)=>{ - return /*#__PURE__*/ _jsxSorted("tr", { - class: _fnSignal(_hf0, [ - row - ], _hf0_str) - }, null, /*#__PURE__*/ _jsxSorted("td", null, null, _fnSignal(_hf1, [ - row - ], _hf1_str), 1, null), 1, row.value.id); - }), 1, "u6_0"); + return /*#__PURE__*/ _jsxSorted("table", null, null, _jsxSorted(Each, null, { + items: _wrapProp(data), + key$: q_App_component_table_Each_key_flZ5LUpd25I, + item$: q_App_component_table_Each_item_oE2PAPFAIIE + }, null, 3, null), 1, "u6_0"); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;mBAgBmB,GAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,GAAG,WAAW;;mBAExC,GAAI,KAAK,CAAC,EAAE;;yCAfC;IAC5B,MAAM,OAAO;QAAE,OAAO;YACpB;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAK;gBAAE;YAAE;YAC9C;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAM;gBAAE;YAAE;YAC/C;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAK;gBAAE;YAAE;SAC/C;IAAA;IAED,qBACE,WAAC,qBACE,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC;QACf,qBACE,WAAC;YAEC,KAAK;;;+BAEL,WAAC;;mCAHI,IAAI,KAAK,CAAC,EAAE;IAMvB;AAGN\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;;yCAG8B;IAC5B,MAAM,OAAO;QAAE,OAAO;YACpB;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAK;gBAAE;YAAE;YAC9C;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAM;gBAAE;YAAE;YAC/C;gBAAE,OAAO;oBAAE,IAAI;oBAAG,UAAU;wBAAE,OAAO;oBAAK;gBAAE;YAAE;SAC/C;IAAA;IAED,qBACE,WAAC,qBAGK;yBAFH;;;;AAYP\"}") /* { "origin": "test.tsx", diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__issue_5008.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__issue_5008.snap index 356adb4a049..645e1dff6a8 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__issue_5008.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__issue_5008.snap @@ -1,6 +1,6 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 3564 +assertion_line: 3565 expression: output --- ==INPUT== @@ -124,4 +124,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 216, + "hi": 312, + "startLine": 9, + "startCol": 14, + "endLine": 11, + "endCol": 14 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 318, + "hi": 400, + "startLine": 12, + "startCol": 14, + "endLine": 14, + "endCol": 14 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl.snap index 04fc761c0b2..7e67f463c32 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5803 expression: output --- ==INPUT== @@ -72,4 +71,25 @@ export const Tree = /*#__PURE__*/ componentQrl(q_Tree_component_T0k10EAlawU); Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;AAGA,wCAAwC;AACxC,yEAAyE;AACzE,8DAA8D;;AAC9D,OAAO,MAAM,qBAAO,2CAOjB\"}") == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 327, + "hi": 377, + "startLine": 11, + "startCol": 32, + "endLine": 11, + "endCol": 81 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl_inline.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl_inline.snap index 7a6ecbc949b..35cb103db3a 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl_inline.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__root_level_self_referential_qrl_inline.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5826 expression: output --- ==INPUT== @@ -62,4 +61,25 @@ q_Tree_component_XMEiO6Rrd3Y.s((props)=>{ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/node_modules/qwik-tree/index.qwik.jsx\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;AAGA,wCAAwC;AACxC,yEAAyE;AACzE,8DAA8D;;AAC9D,OAAO,MAAM,qBAAO,2CAOjB;+BAP4B,CAAC;IAC/B,qBACC,WAAC;kBACC;QACA,MAAM,QAAQ,IAAI,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,sBAAU,UAAC;gCAAS;8BAAA;;;;;;;;;;AAG9D\"}") == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "./node_modules/qwik-tree/index.qwik.jsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 327, + "hi": 377, + "startLine": 11, + "startCol": 32, + "endLine": 11, + "endCol": 81 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index.snap index 17fede91f57..811eebdee22 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5002 expression: output --- ==INPUT== @@ -142,4 +141,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 152, + "hi": 292, + "startLine": 8, + "startCol": 8, + "endLine": 10, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index_and_capture_ref.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index_and_capture_ref.snap index b92f271ace4..ca26c3c5451 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index_and_capture_ref.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_multiple_qrls_with_item_and_index_and_capture_ref.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5026 expression: output --- ==INPUT== @@ -170,4 +169,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 225, + "hi": 387, + "startLine": 10, + "startCol": 8, + "endLine": 12, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl.snap index a639ab28344..8c11f489108 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 4847 expression: output --- ==INPUT== @@ -225,4 +224,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key is derived from a function call.", + "highlights": [ + { + "lo": 240, + "hi": 1516, + "startLine": 8, + "startCol": 12, + "endLine": 42, + "endCol": 12 + } + ], + "suggestions": [ + "Use a stable key expression without function calls." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_2.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_2.snap index 7b2e9b260ad..b542ba594bc 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_2.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_2.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 4903 expression: output --- ==INPUT== @@ -179,4 +178,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 334, + "hi": 575, + "startLine": 11, + "startCol": 12, + "endLine": 20, + "endCol": 12 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 605, + "hi": 715, + "startLine": 22, + "startCol": 14, + "endLine": 26, + "endCol": 14 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_index.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_index.snap index e3b53fdce85..6c265d7b35e 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_index.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_index.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 4944 expression: output --- ==INPUT== @@ -235,4 +234,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key is derived from a function call.", + "highlights": [ + { + "lo": 294, + "hi": 1608, + "startLine": 9, + "startCol": 12, + "endLine": 44, + "endCol": 12 + } + ], + "suggestions": [ + "Use a stable key expression without function calls." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_nested_components.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_nested_components.snap index 3fec3c1d76c..9becccafc18 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_nested_components.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_extract_single_qrl_with_nested_components.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5053 expression: output --- ==INPUT== @@ -145,4 +144,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 230, + "hi": 320, + "startLine": 7, + "startCol": 18, + "endLine": 7, + "endCol": 107 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_move_props_related_to_iteration_variables_to_var_props.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_move_props_related_to_iteration_variables_to_var_props.snap index da017bd4d2d..19826dab4a3 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_move_props_related_to_iteration_variables_to_var_props.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_move_props_related_to_iteration_variables_to_var_props.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5348 expression: output --- ==INPUT== @@ -105,4 +104,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 412, + "hi": 684, + "startLine": 16, + "startCol": 10, + "endLine": 24, + "endCol": 10 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_not_transform_events_on_non_elements.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_not_transform_events_on_non_elements.snap index 64233cfd1a6..de61e5cdaab 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_not_transform_events_on_non_elements.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_not_transform_events_on_non_elements.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 4768 expression: output --- ==INPUT== @@ -125,4 +124,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 267, + "hi": 336, + "startLine": 10, + "startCol": 14, + "endLine": 12, + "endCol": 14 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_and_item_index_in_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_and_item_index_in_loop.snap index d78e8361a6c..e713217f0e3 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_and_item_index_in_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_and_item_index_in_loop.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5460 expression: output --- ==INPUT== @@ -114,4 +113,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 157, + "hi": 296, + "startLine": 8, + "startCol": 8, + "endLine": 11, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_in_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_in_loop.snap index 2cf4ad9d192..948d512a6ff 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_in_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_block_scoped_variables_in_loop.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5409 expression: output --- ==INPUT== @@ -108,4 +107,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 157, + "hi": 288, + "startLine": 8, + "startCol": 8, + "endLine": 11, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_component_with_normal_function.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_component_with_normal_function.snap index ac82bb7c52c..49de9bad8db 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_component_with_normal_function.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_component_with_normal_function.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5074 expression: output --- ==INPUT== @@ -145,4 +144,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 240, + "hi": 330, + "startLine": 7, + "startCol": 18, + "endLine": 7, + "endCol": 107 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_handlers_capturing_cross_scope_in_nested_loops.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_handlers_capturing_cross_scope_in_nested_loops.snap index e0d62a50822..94b42d6a958 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_handlers_capturing_cross_scope_in_nested_loops.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_handlers_capturing_cross_scope_in_nested_loops.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5542 expression: output --- ==INPUT== @@ -195,4 +194,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 175, + "hi": 738, + "startLine": 8, + "startCol": 8, + "endLine": 24, + "endCol": 8 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 292, + "hi": 700, + "startLine": 12, + "startCol": 14, + "endLine": 21, + "endCol": 14 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_loop_multiple_handler_with_different_captures.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_loop_multiple_handler_with_different_captures.snap index 15799990f51..6187e9573e7 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_loop_multiple_handler_with_different_captures.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_loop_multiple_handler_with_different_captures.snap @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 5709 expression: output --- ==INPUT== @@ -160,4 +159,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 158, + "hi": 494, + "startLine": 8, + "startCol": 8, + "endLine": 20, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_and_item_index_in_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_and_item_index_in_loop.snap index c8e46ef5b01..bd9c1dfc759 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_and_item_index_in_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_and_item_index_in_loop.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5485 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -118,4 +117,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 157, + "hi": 338, + "startLine": 8, + "startCol": 8, + "endLine": 12, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_in_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_in_loop.snap index bb4e5291af1..01b1746298e 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_in_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_block_scoped_variables_in_loop.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5434 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -114,4 +113,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 157, + "hi": 330, + "startLine": 8, + "startCol": 8, + "endLine": 12, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers.snap index 31b68361839..592602d69a6 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5121 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -168,4 +167,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 164, + "hi": 387, + "startLine": 6, + "startCol": 6, + "endLine": 10, + "endCol": 6 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers_case2.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers_case2.snap index f8b9ad6e00b..3e8f6013c79 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers_case2.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_multiple_event_handlers_case2.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5144 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -172,4 +171,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 164, + "hi": 399, + "startLine": 6, + "startCol": 6, + "endLine": 10, + "endCol": 6 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops.snap index 0d51c8624ef..5635450d50e 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5095 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -164,4 +163,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 210, + "hi": 434, + "startLine": 7, + "startCol": 6, + "endLine": 13, + "endCol": 6 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 289, + "hi": 419, + "startLine": 9, + "startCol": 10, + "endLine": 11, + "endCol": 10 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops_handler_captures_only_inner_scope.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops_handler_captures_only_inner_scope.snap index 8f3617052ec..6256df3feda 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops_handler_captures_only_inner_scope.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_nested_loops_handler_captures_only_inner_scope.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5612 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -122,4 +121,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 175, + "hi": 508, + "startLine": 8, + "startCol": 8, + "endLine": 19, + "endCol": 8 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 239, + "hi": 483, + "startLine": 10, + "startCol": 12, + "endLine": 17, + "endCol": 12 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_same_element_one_handler_with_captures_one_without.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_same_element_one_handler_with_captures_one_without.snap index f860b3db0b2..08f4538d1f8 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_same_element_one_handler_with_captures_one_without.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_same_element_one_handler_with_captures_one_without.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5580 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -148,4 +147,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 158, + "hi": 418, + "startLine": 8, + "startCol": 8, + "endLine": 18, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_three_nested_loops_handler_captures_outer_only.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_three_nested_loops_handler_captures_outer_only.snap index 574c36aef79..dce0a03b20c 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_three_nested_loops_handler_captures_outer_only.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_three_nested_loops_handler_captures_outer_only.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5645 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -146,4 +145,65 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 214, + "hi": 652, + "startLine": 11, + "startCol": 8, + "endLine": 26, + "endCol": 8 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 335, + "hi": 614, + "startLine": 15, + "startCol": 14, + "endLine": 23, + "endCol": 14 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + }, + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 406, + "hi": 577, + "startLine": 17, + "startCol": 18, + "endLine": 21, + "endCol": 18 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_two_handlers_capturing_different_block_scope_in_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_two_handlers_capturing_different_block_scope_in_loop.snap index fb47ecc3c3d..2aecba0f173 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_two_handlers_capturing_different_block_scope_in_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__should_transform_two_handlers_capturing_different_block_scope_in_loop.snap @@ -1,6 +1,5 @@ --- -source: packages/optimizer/core/src/test.rs -assertion_line: 5511 +source: packages/qwik/src/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -153,4 +152,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma */ == DIAGNOSTICS == -[] +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 157, + "hi": 465, + "startLine": 8, + "startCol": 8, + "endLine": 17, + "endCol": 8 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_non_candidate_has_no_warning.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_non_candidate_has_no_warning.snap new file mode 100644 index 00000000000..6dee607700e --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_non_candidate_has_no_warning.snap @@ -0,0 +1,62 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a', 'b']; + return
{items.map((item) => item.toUpperCase())}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + 'a', + 'b' + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item)=>item.toUpperCase()), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;QAAK;KAAI;IACxB,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAC,OAAS,KAAK,WAAW;AACnD\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 178 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_component_root.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_component_root.snap new file mode 100644 index 00000000000..b5922b9d730 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_component_root.snap @@ -0,0 +1,181 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$, Each } from '@qwik.dev/core'; + +const Row = component$((props: { text: string }) => { + return

{props.text}

; +}); + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
{items.map(item => )}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +qrl(()=>import("./test.tsx_Row_component_OHQZg3MmUDY"), "Row_component_OHQZg3MmUDY"); +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;AAOA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_Row_component_OHQZg3MmUDY.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const Row_component_OHQZg3MmUDY = (props)=>{ + return /*#__PURE__*/ _jsxSorted("p", null, null, _wrapProp(props, "text"), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAGuB,CAAC;IACtB,qBAAO,WAAC,2BAAG;AACb\"}") +/* +{ + "origin": "test.tsx", + "name": "Row_component_OHQZg3MmUDY", + "entry": null, + "displayName": "test.tsx_Row_component", + "hash": "OHQZg3MmUDY", + "canonicalFilename": "test.tsx_Row_component_OHQZg3MmUDY", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 77, + 139 + ], + "paramNames": [ + "props" + ] +} +*/ +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_Row_component_OHQZg3MmUDY = /*#__PURE__*/ qrl(()=>import("./test.tsx_Row_component_OHQZg3MmUDY"), "Row_component_OHQZg3MmUDY"); +// +const Row = /*#__PURE__*/ componentQrl(q_Row_component_OHQZg3MmUDY); +export const App_component_div_Each_item_f829DjfZibU = (item)=>/*#__PURE__*/ _jsxSorted(Row, null, { + text: _wrapProp(item, "text") + }, null, 3, null); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;MAGM,oBAAM;wDAMa,qBAAQ,WAAC;QAAkB,IAAI,YAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_1"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;yCAO8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBAAuB;eAAlB;;;;;AACf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 173, + 305 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>item.id; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDASyB,OAAkB,KAAK,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_comment.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_comment.snap new file mode 100644 index 00000000000..d9a3691002a --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_comment.snap @@ -0,0 +1,69 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7543 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
{ + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item.text}) + }
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, /* @qwik-disable-next-line map-to-each */ items.map((item)=>/*#__PURE__*/ _jsxSorted("span", null, null, _wrapProp(item, "text"), 1, item.id)), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBACN,uCAAuC,GACvC,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,8BAAoB,kBAAV,KAAK,EAAE;AAE1C\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 266 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_sibling_jsx_comment.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_sibling_jsx_comment.snap new file mode 100644 index 00000000000..7f59f5f87fd --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_sibling_jsx_comment.snap @@ -0,0 +1,69 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7585 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
+ {/* @qwik-disable-next-line map-to-each */} + {items.map((item) => {item.text})} +
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item)=>/*#__PURE__*/ _jsxSorted("span", null, null, _wrapProp(item, "text"), 1, item.id)), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBAEL,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,8BAAoB,kBAAV,KAAK,EAAE;AAE3C\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 268 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_inside_normal_function_is_not_rewritten.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_inside_normal_function_is_not_rewritten.snap new file mode 100644 index 00000000000..4bc2c7dfadd --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_inside_normal_function_is_not_rewritten.snap @@ -0,0 +1,68 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7231 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const renderItems = () => items.map((item) => {item.text}); + return
{renderItems()}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAIhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + const renderItems = ()=>items.map((item)=>/*#__PURE__*/ _jsxSorted("span", null, null, _wrapProp(item, "text"), 1, item.id)); + return /*#__PURE__*/ _jsxSorted("div", null, null, renderItems(), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,cAAc,IAAM,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,8BAAoB,kBAAV,KAAK,EAAE;IAChE,qBAAO,WAAC,mBAAK;AACf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 255 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_nested_control_flow.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_nested_control_flow.snap new file mode 100644 index 00000000000..b09e3dcb254 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_nested_control_flow.snap @@ -0,0 +1,158 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', altId: 'b', vip: true, name: 'A' }]; + return
{items.map((item, idx) => { + let label = item.name; + if (item.vip) { + label = label + ':' + idx; + } + + let key = item.id; + if (item.altId) { + key = item.altId; + } + + return
{label}
; + })}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAehB\"}") +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_div_Each_item_f829DjfZibU = (item, idx)=>{ + let label = item.name; + if (item.vip) label = label + ':' + idx; + return /*#__PURE__*/ _jsxSorted("article", null, null, label, 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;wDAK0B,MAAM;IAC5B,IAAI,QAAQ,KAAK,IAAI;IACrB,IAAI,KAAK,GAAG,EACV,QAAQ,QAAQ,MAAM;yBAQjB,WAAC,uBAAmB\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item", + "idx" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + altId: 'b', + vip: true, + name: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,OAAO;YAAK,KAAK;YAAM,MAAM;QAAI;KAAE;IAC7D,qBAAO,WAAC,mBAWC;eAXI;;;;;AAaf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 419 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item, idx)=>{ + let key = item.id; + if (item.altId) key = item.altId; + return key; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAK0B,MAAM;IAM5B,IAAI,MAAM,KAAK,EAAE;IACjB,IAAI,KAAK,KAAK,EACZ,MAAM,KAAK,KAAK;WAGG\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item", + "idx" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_is_not_rewritten.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_is_not_rewritten.snap new file mode 100644 index 00000000000..ef2a7300a12 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_is_not_rewritten.snap @@ -0,0 +1,68 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7211 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const rendered = items.map((item) => {item.text}); + return
{rendered}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAIhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + const rendered = items.map((item)=>/*#__PURE__*/ _jsxSorted("span", null, null, _wrapProp(item, "text"), 1, item.id)); + return /*#__PURE__*/ _jsxSorted("div", null, null, rendered, 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,WAAW,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,8BAAoB,kBAAV,KAAK,EAAE;IACvD,qBAAO,WAAC,mBAAK;AACf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 241 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_without_jsx_transpile_is_not_rewritten.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_without_jsx_transpile_is_not_rewritten.snap new file mode 100644 index 00000000000..267800f7f64 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_without_jsx_transpile_is_not_rewritten.snap @@ -0,0 +1,65 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7251 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const rendered = items.map((item) => {item.text}); + return
{rendered}
; +}); + +============================= test.tsx == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAIhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.tsx (ENTRY POINT)== + +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + const rendered = items.map((item)=>{item.text}); + return
{rendered}
; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,WAAW,MAAM,GAAG,CAAC,CAAC,QAAU,KAAK,KAAK,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG;IACtE,QAAQ,KAAK,WAAW;AAC1B\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "tsx", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 241 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_shared_alias.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_shared_alias.snap new file mode 100644 index 00000000000..0b697ab342d --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_shared_alias.snap @@ -0,0 +1,146 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ data: { id: 'a', label: 'A' } }]; + return
{items.map((item) => { + const data = item.data; + return
{data.label}
; + })}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMhB\"}") +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_div_Each_item_f829DjfZibU = (item)=>{ + const data = item.data; + return /*#__PURE__*/ _jsxSorted("section", null, null, _wrapProp(data, "label"), 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;wDAK0B;IACtB,MAAM,OAAO,KAAK,IAAI;yBACf,WAAC,iCAAuB\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + data: { + id: 'a', + label: 'A' + } + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,MAAM;gBAAE,IAAI;gBAAK,OAAO;YAAI;QAAE;KAAE;IACjD,qBAAO,WAAC,mBAEC;eAFI;;;;;AAIf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 276 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>{ + const data = item.data; + return data.id; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAK0B;IACtB,MAAM,OAAO,KAAK,IAAI;WACD,KAAK,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_simple.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_simple.snap new file mode 100644 index 00000000000..46630ba4494 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_simple.snap @@ -0,0 +1,137 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = { value: [{ key: 'a', text: 'A' }] }; + return
{items.value.map(item =>
{item.text}
)}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_div_Each_item_f829DjfZibU = (item)=>/*#__PURE__*/ _jsxSorted("div", null, null, _wrapProp(item, "text"), 1, null); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;wDAK+B,qBAAQ,WAAC,6BAAoB\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = { + value: [ + { + key: 'a', + text: 'A' + } + ] + }; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, null, { + items: _wrapProp(items), + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAE,OAAO;YAAC;gBAAE,KAAK;gBAAK,MAAM;YAAI;SAAE;IAAC;IACjD,qBAAO,WAAC,mBAA6B;yBAAxB;;;;AACf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 227 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>item.key; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAK+B,OAAkB,KAAK,GAAG\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_dynamic_item_component.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_dynamic_item_component.snap new file mode 100644 index 00000000000..8edad67abc8 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_dynamic_item_component.snap @@ -0,0 +1,75 @@ +--- +source: packages/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const logos = [{ title: 'Qwik', alt: 'Qwik', Logo, downloadHref: '/logos/qwik.svg' }]; + return
{logos.map((item) => ( + + + + ))}
; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAOhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const logos = [ + { + title: 'Qwik', + alt: 'Qwik', + Logo, + downloadHref: '/logos/qwik.svg' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, logos.map((item)=>/*#__PURE__*/ _jsxSorted(Card, { + title: _wrapProp(item, "title") + }, null, /*#__PURE__*/ _jsxSorted(item.Logo, { + alt: _wrapProp(item, "alt") + }, null, null, 3, "u6_0"), 1, item.title)), 1, "u6_1"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,OAAO;YAAQ,KAAK;YAAQ;YAAM,cAAc;QAAkB;KAAE;IACrF,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAC,qBACtB,WAAC;YAAsB,KAAK,YAAE;+BAC5B,WAAC,KAAK,IAAI;YAAC,GAAG,YAAE;sCADP,KAAK,KAAK;AAIzB\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 320 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_local_function_component.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_local_function_component.snap new file mode 100644 index 00000000000..76744794472 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_local_function_component.snap @@ -0,0 +1,148 @@ +--- +source: packages/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + function Filter(props: { filter: string }) { + return
  • {props.filter}
  • ; + } + + return
      {['all', 'active'].map((filter) => )}
    ; + }); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMf\"}") +============================= test.tsx_map_item_nf66XgXHgFs.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +// +export const map_item_nf66XgXHgFs = (filter)=>{ + const Filter = _captures[0]; + return /*#__PURE__*/ _jsxSorted(Filter, { + filter: filter + }, null, null, 3, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;qCAQqC;;yBAAW,WAAC;QAAO,QAAQ\"}") +/* +{ + "origin": "test.tsx", + "name": "map_item_nf66XgXHgFs", + "entry": null, + "displayName": "test.tsx_map_item", + "hash": "nf66XgXHgFs", + "canonicalFilename": "test.tsx_map_item_nf66XgXHgFs", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "filter" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _map } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_map_item_nf66XgXHgFs = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_item_nf66XgXHgFs"), "map_item_nf66XgXHgFs"); +const q_map_key_bn60zrX6S0A = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_key_bn60zrX6S0A"), "map_key_bn60zrX6S0A"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + function Filter(props) { + return /*#__PURE__*/ _jsxSorted("li", null, null, _wrapProp(props, "filter"), 1, "u6_0"); + } + return /*#__PURE__*/ _jsxSorted("ul", null, null, /*#__PURE__*/ _map([ + 'all', + 'active' + ], (filter)=>_jsxSorted(Filter, { + filter: filter + }, null, null, 3, filter), q_map_key_bn60zrX6S0A, q_map_item_nf66XgXHgFs, [ + Filter + ]), 1, "u6_1"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;;yCAG8B;IAC5B,SAAS,OAAO,KAAyB;QACvC,qBAAO,WAAC,4BAAI;IACd;IAEA,qBAAO,WAAC,qCAAI;QAAC;QAAO;KAAS,EAAK,CAAC,SAAW,WAAC;YAAO,QAAQ;0BAAa;;;AAC5E\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 272 + ] +} +*/ +============================= test.tsx_map_key_bn60zrX6S0A.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +// +export const map_key_bn60zrX6S0A = (filter)=>{ + _captures[0]; + return filter; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;oCAQqC;;WAAwC\"}") +/* +{ + "origin": "test.tsx", + "name": "map_key_bn60zrX6S0A", + "entry": null, + "displayName": "test.tsx_map_key", + "hash": "bn60zrX6S0A", + "canonicalFilename": "test.tsx_map_key_bn60zrX6S0A", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "filter" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_warning_disabled_by_comment.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_warning_disabled_by_comment.snap new file mode 100644 index 00000000000..52f4e99d308 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_warning_disabled_by_comment.snap @@ -0,0 +1,65 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7564 +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    { + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item}) + }
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + 'a' + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, /* @qwik-disable-next-line map-to-each */ items.map((item)=>/*#__PURE__*/ _jsxSorted("span", null, null, item, 1, "u6_0")), 1, "u6_1"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;KAAI;IACnB,qBAAO,WAAC,mBACN,uCAAuC,GACvC,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,oBAAM;AAE/B\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 228 + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_item_click_handler.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_item_click_handler.snap new file mode 100644 index 00000000000..9fd5babe722 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_item_click_handler.snap @@ -0,0 +1,176 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => ( + + ))}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAOhB\"}") +============================= test.tsx_App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ.js (ENTRY POINT)== + +export const App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ = (_, _1, item)=>item.text; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"wEAMoC,eAAM,KAAK,IAAI\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item_button_q_e_click", + "hash": "eTuP0Bi09IQ", + "canonicalFilename": "test.tsx_App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ", + "path": "", + "extension": "js", + "parent": "App_component_div_Each_item_f829DjfZibU", + "ctxKind": "eventHandler", + "ctxName": "onClick$", + "captures": false, + "loc": [ + 201, + 216 + ], + "paramNames": [ + "_", + "_1", + "item" + ] +} +*/ +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ"), "App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ"); +// +export const App_component_div_Each_item_f829DjfZibU = (item)=>/*#__PURE__*/ _jsxSorted("button", { + "q-e:click": q_App_component_div_Each_item_button_q_e_click_eTuP0Bi09IQ, + "q:p": item + }, null, _wrapProp(item, "text"), 4, null); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;wDAK0B,qBACtB,WAAC;QAAqB,WAAQ;;uBAC3B\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBACN;eADW;;;;;AAKf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 265 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>item.id; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAK0B,OACT,KAAK,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_key_capture.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_key_capture.snap new file mode 100644 index 00000000000..06dddf19e94 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_key_capture.snap @@ -0,0 +1,146 @@ +--- +source: packages/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const prefix = { value: 'item:' }; + return
    {items.map((item) => {item.text})}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAIhB\"}") +============================= test.tsx_map_item_nf66XgXHgFs.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const map_item_nf66XgXHgFs = (item)=>{ + _captures[0]; + return /*#__PURE__*/ _jsxSorted("span", null, null, _wrapProp(item, "text"), 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;qCAM0B;;yBAAS,WAAC,8BAAmC\"}") +/* +{ + "origin": "test.tsx", + "name": "map_item_nf66XgXHgFs", + "entry": null, + "displayName": "test.tsx_map_item", + "hash": "nf66XgXHgFs", + "canonicalFilename": "test.tsx_map_item_nf66XgXHgFs", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _map } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_map_item_nf66XgXHgFs = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_item_nf66XgXHgFs"), "map_item_nf66XgXHgFs"); +const q_map_key_bn60zrX6S0A = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_key_bn60zrX6S0A"), "map_key_bn60zrX6S0A"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + const prefix = { + value: 'item:' + }; + return /*#__PURE__*/ _jsxSorted("div", null, null, /*#__PURE__*/ _map(items, (item)=>_jsxSorted("span", null, null, _wrapProp(item, "text"), 1, prefix.value + item.id), q_map_key_bn60zrX6S0A, q_map_item_nf66XgXHgFs, [ + prefix + ]), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,SAAS;QAAE,OAAO;IAAQ;IAChC,qBAAO,WAAC,sCAAK,OAAU,CAAC,OAAS,WAAC,8BAAmC,kBAAzB,OAAO,KAAK,GAAG,KAAK,EAAE;;;AACpE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 264 + ] +} +*/ +============================= test.tsx_map_key_bn60zrX6S0A.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +// +export const map_key_bn60zrX6S0A = (item)=>{ + const prefix = _captures[0]; + return prefix.value + item.id; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;oCAM0B;;WAAoB,OAAO,KAAK,GAAG,KAAK,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "map_key_bn60zrX6S0A", + "entry": null, + "displayName": "test.tsx_map_key", + "hash": "bn60zrX6S0A", + "canonicalFilename": "test.tsx_map_key_bn60zrX6S0A", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_loop_slicing.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_loop_slicing.snap new file mode 100644 index 00000000000..e4a7cda6008 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_loop_slicing.snap @@ -0,0 +1,151 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', parts: ['A', 'B'] }]; + return
    {items.map((item) => { + let label = ''; + for (const part of item.parts) { + label += part; + } + const key = item.id; + return
    {label}
    ; + })}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAUhB\"}") +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_div_Each_item_f829DjfZibU = (item)=>{ + let label = ''; + for (const part of item.parts)label += part; + return /*#__PURE__*/ _jsxSorted("article", null, null, label, 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;wDAK0B;IACtB,IAAI,QAAQ;IACZ,KAAK,MAAM,QAAQ,KAAK,KAAK,CAC3B,SAAS;yBAGJ,WAAC,uBAAmB\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + parts: [ + 'A', + 'B' + ] + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I, + item$: q_App_component_div_Each_item_f829DjfZibU + }, null, 3, null), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,OAAO;gBAAC;gBAAK;aAAI;QAAC;KAAE;IAC9C,qBAAO,WAAC,mBAMC;eANI;;;;;AAQf\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 345 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>{ + const key = item.id; + return key; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAK0B;IAKtB,MAAM,MAAM,KAAK,EAAE;WACE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap new file mode 100644 index 00000000000..7af7ae3bb6a --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap @@ -0,0 +1,157 @@ +--- +source: packages/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const extra = { suffix: '!' }; + return
    {items.map((item) => {item.text + extra.suffix})}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAIhB\"}") +============================= test.tsx_map_item_nf66XgXHgFs.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +import { _fnSignal } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +// +const _hf0 = (p0, p1)=>p1.text + p0.suffix; +const _hf0_str = "p1.text+p0.suffix"; +export const map_item_nf66XgXHgFs = (item)=>{ + const extra = _captures[0]; + return /*#__PURE__*/ _jsxSorted("span", null, null, _fnSignal(_hf0, [ + extra, + item + ], _hf0_str), 1, null); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;uBAMwD,GAAK,IAAI,GAAG,GAAM,MAAM;;qCAAtD;;yBAAS,WAAC\"}") +/* +{ + "origin": "test.tsx", + "name": "map_item_nf66XgXHgFs", + "entry": null, + "displayName": "test.tsx_map_item", + "hash": "nf66XgXHgFs", + "canonicalFilename": "test.tsx_map_item_nf66XgXHgFs", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _fnSignal } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _map } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const _hf0 = (p0, p1)=>p1.text + p0.suffix; +const _hf0_str = "p1.text+p0.suffix"; +// +const q_map_item_nf66XgXHgFs = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_item_nf66XgXHgFs"), "map_item_nf66XgXHgFs"); +const q_map_key_bn60zrX6S0A = /*#__PURE__*/ qrl(()=>import("./test.tsx_map_key_bn60zrX6S0A"), "map_key_bn60zrX6S0A"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + const extra = { + suffix: '!' + }; + return /*#__PURE__*/ _jsxSorted("div", null, null, /*#__PURE__*/ _map(items, (item)=>_jsxSorted("span", null, null, _fnSignal(_hf0, [ + extra, + item + ], _hf0_str), 1, item.id), q_map_key_bn60zrX6S0A, q_map_item_nf66XgXHgFs, [ + extra + ]), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;uBAMwD,GAAK,IAAI,GAAG,GAAM,MAAM;;;;;;yCAHlD;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,QAAQ;QAAE,QAAQ;IAAI;IAC5B,qBAAO,WAAC,sCAAK,OAAU,CAAC,OAAS,WAAC;;;yBAAU,KAAK,EAAE;;;AACrD\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 260 + ] +} +*/ +============================= test.tsx_map_key_bn60zrX6S0A.js (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +// +export const map_key_bn60zrX6S0A = (item)=>{ + _captures[0]; + return item.id; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;oCAM0B;;WAAoB,KAAK,EAAE\"}") +/* +{ + "origin": "test.tsx", + "name": "map_key_bn60zrX6S0A", + "entry": null, + "displayName": "test.tsx_map_key", + "hash": "bn60zrX6S0A", + "canonicalFilename": "test.tsx_map_key_bn60zrX6S0A", + "path": "", + "extension": "js", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_without_jsx_transpile.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_without_jsx_transpile.snap new file mode 100644 index 00000000000..66d16d1962b --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_without_jsx_transpile.snap @@ -0,0 +1,126 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ key: 'a', text: 'A' }]; + return
    {items.map(item => )}
    ; +}); + +============================= test.tsx == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_div_Each_item_f829DjfZibU.tsx (ENTRY POINT)== + +export const App_component_div_Each_item_f829DjfZibU = (item)=>; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"wDAKyB,QAAS,KAAoB,MAAM,KAAK,IAAI\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_item_f829DjfZibU", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_item", + "hash": "f829DjfZibU", + "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "path": "", + "extension": "tsx", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "item$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +============================= test.tsx_App_component_ckEPmXZlub0.tsx (ENTRY POINT)== + +import { Each } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_div_Each_item_f829DjfZibU = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_item_f829DjfZibU"), "App_component_div_Each_item_f829DjfZibU"); +const q_App_component_div_Each_key_07Oo5ReT59I = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_div_Each_key_07Oo5ReT59I"), "App_component_div_Each_key_07Oo5ReT59I"); +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + key: 'a', + text: 'A' + } + ]; + return
    {}
    ; +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,KAAK;YAAK,MAAM;QAAI;KAAE;IACvC,QAAQ,kBAAK,6GAA8D;AAC7E\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "tsx", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 213 + ] +} +*/ +============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.tsx (ENTRY POINT)== + +export const App_component_div_Each_key_07Oo5ReT59I = (item)=>item.key; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\"uDAKyB,OAAmB,KAAK,GAAG\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_div_Each_key_07Oo5ReT59I", + "entry": null, + "displayName": "test.tsx_App_component_div_Each_key", + "hash": "07Oo5ReT59I", + "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "path": "", + "extension": "tsx", + "parent": "App_component_ckEPmXZlub0", + "ctxKind": "jSXProp", + "ctxName": "key$", + "captures": false, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ] +} +*/ +== DIAGNOSTICS == + +[] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_callback_returns_array.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_callback_returns_array.snap new file mode 100644 index 00000000000..26593f901a9 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_callback_returns_array.snap @@ -0,0 +1,88 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => [
    {item.text}
    ])}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item)=>[ + /*#__PURE__*/ _jsxSorted("div", null, null, _wrapProp(item, "text"), 1, item.id) + ]), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAC,OAAS;0BAAC,WAAC,6BAAmB,kBAAV,KAAK,EAAE;SAAoB;AACzE\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 212 + ] +} +*/ +== DIAGNOSTICS == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the callback does not return a single JSX node.", + "highlights": [ + { + "lo": 143, + "hi": 202, + "startLine": 6, + "startCol": 16, + "endLine": 6, + "endCol": 74 + } + ], + "suggestions": [ + "Return a single JSX node from the .map() callback." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_has_no_key.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_has_no_key.snap new file mode 100644 index 00000000000..2f5cb682d58 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_has_no_key.snap @@ -0,0 +1,82 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    {items.map(item =>
    {item}
    )}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + 'a' + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item)=>/*#__PURE__*/ _jsxSorted("div", null, null, item, 1, "u6_0")), 1, "u6_1"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;KAAI;IACnB,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAA,qBAAQ,WAAC,mBAAK;AACvC\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 170 + ] +} +*/ +== DIAGNOSTICS == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "highlights": [ + { + "lo": 124, + "hi": 160, + "startLine": 6, + "startCol": 16, + "endLine": 6, + "endCol": 51 + } + ], + "suggestions": [ + "Add a stable key to the returned JSX node." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_comes_from_call.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_comes_from_call.snap new file mode 100644 index 00000000000..3ac1dc25160 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_comes_from_call.snap @@ -0,0 +1,86 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) =>
    {item.text}
    )}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAGhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + { + id: 'a', + text: 'A' + } + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item)=>/*#__PURE__*/ _jsxSorted("div", null, null, _wrapProp(item, "text"), 1, getKey(item))), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAC,qBAAS,WAAC,6BAAwB,kBAAf,OAAO;AACpD\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 215 + ] +} +*/ +== DIAGNOSTICS == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key is derived from a function call.", + "highlights": [ + { + "lo": 143, + "hi": 205, + "startLine": 6, + "startCol": 16, + "endLine": 6, + "endCol": 77 + } + ], + "suggestions": [ + "Use a stable key expression without function calls." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_uses_second_param_alias.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_uses_second_param_alias.snap new file mode 100644 index 00000000000..d7092a476aa --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_uses_second_param_alias.snap @@ -0,0 +1,88 @@ +--- +source: packages/qwik/src/optimizer/core/src/test.rs +expression: output +--- +==INPUT== + + +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    {items.map((item, position) => { + const key = position; + return
    {item}
    ; + })}
    ; +}); + +============================= test.js == + +import { componentQrl } from "@qwik.dev/core"; +import { qrl } from "@qwik.dev/core"; +// +const q_App_component_ckEPmXZlub0 = /*#__PURE__*/ qrl(()=>import("./test.tsx_App_component_ckEPmXZlub0"), "App_component_ckEPmXZlub0"); +// +export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0); + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;;AAGA,OAAO,MAAM,oBAAM,0CAMhB\"}") +============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +// +export const App_component_ckEPmXZlub0 = ()=>{ + const items = [ + 'a' + ]; + return /*#__PURE__*/ _jsxSorted("div", null, null, items.map((item, position)=>{ + const key = position; + return /*#__PURE__*/ _jsxSorted("div", null, null, item, 1, key); + }), 1, "u6_0"); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;KAAI;IACnB,qBAAO,WAAC,mBAAK,MAAM,GAAG,CAAC,CAAC,MAAM;QAC5B,MAAM,MAAM;QACZ,qBAAO,WAAC,mBAAe,SAAN;IACnB;AACF\"}") +/* +{ + "origin": "test.tsx", + "name": "App_component_ckEPmXZlub0", + "entry": null, + "displayName": "test.tsx_App_component", + "hash": "ckEPmXZlub0", + "canonicalFilename": "test.tsx_App_component_ckEPmXZlub0", + "path": "", + "extension": "js", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 78, + 236 + ] +} +*/ +== DIAGNOSTICS == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "highlights": [ + { + "lo": 124, + "hi": 226, + "startLine": 6, + "startCol": 16, + "endLine": 9, + "endCol": 4 + } + ], + "suggestions": [ + "Use a stable key derived from the item instead of the callback's index parameter." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index c5a17a11d82..394eb3897eb 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -1,6 +1,7 @@ #![allow(unused_must_use)] use super::*; +use crate::utils::DiagnosticCategory; use serde_json::to_string_pretty; use swc_atoms::Atom; @@ -7010,6 +7011,982 @@ export const action = formAction$((data) => { }); } +fn combined_modules_code(output: &TransformOutput) -> String { + output + .modules + .iter() + .map(|module| module.code.as_str()) + .collect::>() + .join("\n") +} + +#[test] +fn should_transform_map_to_each_simple() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = { value: [{ key: 'a', text: 'A' }] }; + return
    {items.value.map(item =>
    {item.text}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains("_jsxSorted(Each") || combined_code.contains("_jsxSplit(Each"), + "Expected Each render in generated output.\n{}", + combined_code + ); + assert!( + !combined_code.contains(".map((item)=>") && !combined_code.contains(".map(item=>"), + "Expected map callback to be rewritten.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for successful rewrite: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_transform_map_to_each_without_jsx_transpile() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ key: 'a', text: 'A' }]; + return
    {items.map(item => )}
    ; +}); +"# + .to_string(), + transpile_ts: false, + transpile_jsx: false, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains(" { + const items = [{ data: { id: 'a', label: 'A' } }]; + return
    {items.map((item, idx) => { + const data = item.data; + const title = data.label + idx; + return
    {title}
    ; + })}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains("Each"), + "Expected Each render in generated output.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for successful rewrite: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_transform_map_to_each_with_outer_capture() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const extra = { suffix: '!' }; + return
    {items.map((item) => {item.text + extra.suffix})}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains("_map("), + "Expected captured .map() to lower through _map(...).\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for successful rewrite: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_transform_map_to_each_with_key_capture() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const prefix = { value: 'item:' }; + return
    {items.map((item) => {item.text})}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains("_map("), + "Expected captured key rewrite to lower through _map(...).\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for successful rewrite: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_transform_map_to_each_with_item_click_handler() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => ( + + ))}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains("Each"), + "Expected Each render in generated output.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for successful rewrite: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_not_transform_map_to_each_in_hoist_mode() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => {item.text})}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + entry_strategy: EntryStrategy::Hoist, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + !combined_code.contains("Each") && !combined_code.contains("_map("), + "Expected hoist mode to keep the original .map() call.\n{}", + combined_code + ); + assert!( + combined_code.contains(".map("), + "Expected original .map() call to remain in hoist mode.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for hoist-mode bailout: {:?}", + output.diagnostics + ); +} + +#[test] +fn snapshot_map_to_each_outside_jsx_children_is_not_rewritten() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const rendered = items.map((item) => {item.text}); + return
    {rendered}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_inside_normal_function_is_not_rewritten() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const renderItems = () => items.map((item) => {item.text}); + return
    {renderItems()}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_outside_jsx_children_without_jsx_transpile_is_not_rewritten() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const rendered = items.map((item) => {item.text}); + return
    {rendered}
    ; +}); +"# + .to_string(), + transpile_ts: false, + transpile_jsx: false, + snapshot: true, + ..TestInput::default() + }); +} + +#[test] +fn should_not_transform_map_to_each_when_disabled_by_comment() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    { + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item.text}) + }
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + !combined_code.contains("Each"), + "Expected map callback rewrite to be disabled.\n{}", + combined_code + ); + assert!( + combined_code.contains(".map("), + "Expected original .map() call to remain.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings when map-to-each is disabled: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_silence_map_to_each_warnings_when_disabled_by_comment() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    { + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item}) + }
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + combined_code.contains(".map("), + "Expected original .map() call to remain.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings when map-to-each is disabled: {:?}", + output.diagnostics + ); +} + +#[test] +fn should_disable_map_to_each_with_sibling_jsx_comment() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    + {/* @qwik-disable-next-line map-to-each */} + {items.map((item) => {item.text})} +
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + !combined_code.contains("Each"), + "Expected map callback rewrite to be disabled by sibling JSX comment.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings when map-to-each is disabled by sibling JSX comment: {:?}", + output.diagnostics + ); +} + +#[test] +fn snapshot_map_to_each_skips_local_function_component() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + function Filter(props: { filter: string }) { + return
  • {props.filter}
  • ; + } + + return
      {['all', 'active'].map((filter) => )}
    ; + }); + "# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_skips_dynamic_item_component() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const logos = [{ title: 'Qwik', alt: 'Qwik', Logo, downloadHref: '/logos/qwik.svg' }]; + return
    {logos.map((item) => ( + + + + ))}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn should_warn_when_map_key_uses_second_param() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    {items.map((item, idx) =>
    {item}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + assert!( + output + .diagnostics + .iter() + .any(|d| d.category == DiagnosticCategory::Warning + && d.code.as_deref() == Some("map-to-each") + && d.message.contains("index parameter")), + "Expected second-parameter warning, got {:?}", + output.diagnostics + ); +} + +#[test] +fn should_warn_when_map_key_comes_from_call() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) =>
    {item.text}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + assert!( + output + .diagnostics + .iter() + .any(|d| d.category == DiagnosticCategory::Warning + && d.message.contains("function call")), + "Expected call-derived-key warning, got {:?}", + output.diagnostics + ); +} + +#[test] +fn should_warn_when_map_callback_does_not_return_single_jsx_node() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => [
    {item.text}
    ])}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + assert!( + output + .diagnostics + .iter() + .any(|d| d.category == DiagnosticCategory::Warning + && d.message.contains("single JSX node")), + "Expected single-node warning, got {:?}", + output.diagnostics + ); +} + +#[test] +fn should_not_transform_map_to_each_when_component_type_comes_from_item() { + let res = test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const logos = [{ title: 'Qwik', alt: 'Qwik', Logo, downloadHref: '/logos/qwik.svg' }]; + return
    {logos.map((item) => ( + + + + ))}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + snapshot: false, + ..TestInput::default() + }); + + assert!(res.is_ok(), "Transform should succeed"); + let output = res.unwrap(); + let combined_code = combined_modules_code(&output); + + assert!( + !combined_code.contains("Each"), + "Expected dynamic item component reference to keep the original .map().\n{}", + combined_code + ); + assert!( + combined_code.contains(".map("), + "Expected original .map() call to remain.\n{}", + combined_code + ); + assert!( + output.diagnostics.is_empty(), + "Did not expect warnings for dynamic component bailout: {:?}", + output.diagnostics + ); +} + +#[test] +fn snapshot_map_to_each_simple() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = { value: [{ key: 'a', text: 'A' }] }; + return
    {items.value.map(item =>
    {item.text}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_component_root() { + test_input!(TestInput { + code: r#" +import { component$, Each } from '@qwik.dev/core'; + +const Row = component$((props: { text: string }) => { + return

    {props.text}

    ; +}); + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map(item => )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_without_jsx_transpile() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ key: 'a', text: 'A' }]; + return
    {items.map(item => )}
    ; +}); +"# + .to_string(), + transpile_ts: false, + transpile_jsx: false, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_shared_alias() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ data: { id: 'a', label: 'A' } }]; + return
    {items.map((item) => { + const data = item.data; + return
    {data.label}
    ; + })}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_with_outer_capture() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const extra = { suffix: '!' }; + return
    {items.map((item) => {item.text + extra.suffix})}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_with_key_capture() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + const prefix = { value: 'item:' }; + return
    {items.map((item) => {item.text})}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_with_item_click_handler() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => ( + + ))}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_disabled_by_comment() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    { + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item.text}) + }
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_warning_disabled_by_comment() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    { + /* @qwik-disable-next-line map-to-each */ + items.map((item) => {item}) + }
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_disabled_by_sibling_jsx_comment() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    + {/* @qwik-disable-next-line map-to-each */} + {items.map((item) => {item.text})} +
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_nested_control_flow() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', altId: 'b', vip: true, name: 'A' }]; + return
    {items.map((item, idx) => { + let label = item.name; + if (item.vip) { + label = label + ':' + idx; + } + + let key = item.id; + if (item.altId) { + key = item.altId; + } + + return
    {label}
    ; + })}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_to_each_with_loop_slicing() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', parts: ['A', 'B'] }]; + return
    {items.map((item) => { + let label = ''; + for (const part of item.parts) { + label += part; + } + const key = item.id; + return
    {label}
    ; + })}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_warn_when_map_has_no_key() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    {items.map(item =>
    {item}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_warn_when_map_key_uses_second_param_alias() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a']; + return
    {items.map((item, position) => { + const key = position; + return
    {item}
    ; + })}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_warn_when_map_key_comes_from_call() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) =>
    {item.text}
    )}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_warn_when_map_callback_returns_array() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = [{ id: 'a', text: 'A' }]; + return
    {items.map((item) => [
    {item.text}
    ])}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + +#[test] +fn snapshot_map_non_candidate_has_no_warning() { + test_input!(TestInput { + code: r#" +import { component$ } from '@qwik.dev/core'; + +export const App = component$(() => { + const items = ['a', 'b']; + return
    {items.map((item) => item.toUpperCase())}
    ; +}); +"# + .to_string(), + transpile_ts: true, + transpile_jsx: true, + ..TestInput::default() + }); +} + impl TestInput { pub fn default() -> Self { Self { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index 096b6deb1b9..a5c2e83b3ed 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -6,6 +6,7 @@ use crate::entry_strategy::EntryPolicy; use crate::inlined_fn::{convert_inlined_fn, render_expr}; use crate::is_const::is_const_expr; use crate::parse::{EmitMode, PathData}; +use crate::utils::{Diagnostic, DiagnosticCategory, DiagnosticScope, SourceLocation}; use crate::words::*; use crate::{errors, EntryStrategy}; use base64::Engine; @@ -19,9 +20,9 @@ use std::hash::Hasher; // import without risk of name clashing use std::iter; use std::str; use swc_atoms::{atom, Atom}; -use swc_common::comments::{Comments, SingleThreadedComments}; +use swc_common::comments::{Comment, Comments, SingleThreadedComments}; use swc_common::SyntaxContext; -use swc_common::{errors::HANDLER, sync::Lrc, SourceMap, Span, Spanned, DUMMY_SP}; +use swc_common::{errors::HANDLER, sync::Lrc, SourceMap, SourceMapper, Span, Spanned, DUMMY_SP}; use swc_ecmascript::ast::{self, SpreadElement}; use swc_ecmascript::utils::{private_ident, quote_ident, ExprFactory}; use swc_ecmascript::visit::{noop_fold_type, noop_visit_type, Fold, FoldWith, Visit, VisitWith}; @@ -42,6 +43,10 @@ macro_rules! id_eq { }; } +mod each_transform; + +const QWIK_DISABLE_NEXT_LINE_DIRECTIVE: &str = "qwik-disable-next-line"; + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum SegmentKind { @@ -105,6 +110,7 @@ struct ImportQrlName { #[allow(clippy::module_name_repetitions)] pub struct QwikTransform<'a> { pub segments: Vec, + pub diagnostics: Vec, pub options: QwikTransformOptions<'a>, segment_names: HashMap, @@ -129,6 +135,7 @@ pub struct QwikTransform<'a> { file_hash: u64, jsx_key_counter: u32, root_jsx_mode: bool, + jsx_children_expr_depth: usize, jsx_element_is_native: Vec, hoisted_fn_signals: HashMap, hoisted_fn_counter: u32, @@ -148,6 +155,8 @@ pub struct QwikTransform<'a> { hoisted_segment_idents: HashSet, /// Pending expression replacement for fold_expr (to return non-CallExpr from fold_call_expr) pending_expr_replacement: Option, + /// Rules disabled by a preceding JSX comment sibling, keyed by the next expression's span.lo. + jsx_disabled_rules_by_pos: HashMap>, } pub struct QwikTransformOptions<'a> { @@ -256,6 +265,7 @@ impl<'a> QwikTransform<'a> { stack_ctxt: Vec::with_capacity(16), decl_stack: Vec::with_capacity(32), segments: Vec::with_capacity(16), + diagnostics: Vec::new(), segment_stack: Vec::with_capacity(16), extra_top_items: BTreeMap::new(), extra_bottom_items: BTreeMap::new(), @@ -286,6 +296,7 @@ impl<'a> QwikTransform<'a> { jsx_functions, immutable_function_cmp, root_jsx_mode: true, + jsx_children_expr_depth: 0, jsx_mutable: false, jsx_element_is_native: Vec::new(), hoisted_fn_signals: HashMap::new(), @@ -299,6 +310,7 @@ impl<'a> QwikTransform<'a> { ref_assignments: Vec::new(), hoisted_segment_idents: HashSet::new(), pending_expr_replacement: None, + jsx_disabled_rules_by_pos: HashMap::new(), options, } } @@ -310,6 +322,231 @@ impl<'a> QwikTransform<'a> { ) } + fn has_disabled_optimizer_rule(&self, span: Span, rule: &str) -> bool { + let has_registered_disable = self + .jsx_disabled_rules_by_pos + .get(&span.lo.0) + .is_some_and(|rules| rules.contains("all") || rules.contains(rule)); + if has_registered_disable { + return true; + } + + let has_leading_comment_disable = self.options.comments.is_some_and(|comments| { + comments.with_leading(span.lo, |comments| { + comments + .iter() + .any(|comment| optimizer_comment_disables_rule(comment.text.as_ref(), rule)) + }) + }); + if has_leading_comment_disable { + return true; + } + + self.has_disabled_optimizer_rule_on_previous_line(span, rule) + } + + fn has_disabled_optimizer_rule_on_previous_line(&self, span: Span, rule: &str) -> bool { + if span.lo.0 == 0 { + return false; + } + let loc = self.options.cm.lookup_char_pos(span.lo); + let src = &loc.file.src; + let Some(previous_line) = src.lines().nth(loc.line.saturating_sub(2)) else { + return false; + }; + optimizer_comment_disables_rule(previous_line, rule) + } + + fn register_jsx_disabled_rules(&mut self, span: Span, rules: &HashSet) { + if rules.is_empty() { + return; + } + self.jsx_disabled_rules_by_pos + .entry(span.lo.0) + .or_default() + .extend(rules.iter().cloned()); + } + + fn disabled_rules_from_jsx_comment_container( + &self, + container: &ast::JSXExprContainer, + empty: &ast::JSXEmptyExpr, + ) -> HashSet { + let mut rules = HashSet::new(); + if let Ok(snippet) = self.options.cm.span_to_snippet(container.span) { + extend_optimizer_disabled_rules_from_comment_text(&snippet, &mut rules); + } + let Some(comments) = self.options.comments else { + return rules; + }; + for comment in comments.get_leading(empty.span.lo).into_iter().flatten() { + extend_optimizer_disabled_rules_from_comment(&comment, &mut rules); + } + for comment in comments.get_trailing(empty.span.lo).into_iter().flatten() { + extend_optimizer_disabled_rules_from_comment(&comment, &mut rules); + } + for comment in comments.get_leading(empty.span.hi).into_iter().flatten() { + extend_optimizer_disabled_rules_from_comment(&comment, &mut rules); + } + for comment in comments.get_trailing(empty.span.hi).into_iter().flatten() { + extend_optimizer_disabled_rules_from_comment(&comment, &mut rules); + } + rules + } + + fn fold_jsx_children_with_disable_directives( + &mut self, + children: Vec, + ) -> Vec { + let mut pending_rules = HashSet::new(); + children + .into_iter() + .map(|child| match &child { + ast::JSXElementChild::JSXExprContainer(container) => match &container.expr { + ast::JSXExpr::JSXEmptyExpr(empty) => { + pending_rules.extend( + self.disabled_rules_from_jsx_comment_container(container, empty), + ); + child.fold_with(self) + } + ast::JSXExpr::Expr(expr) => { + if !pending_rules.is_empty() { + self.register_jsx_disabled_rules(expr.span(), &pending_rules); + pending_rules.clear(); + } + self.fold_jsx_child_expr(child) + } + }, + ast::JSXElementChild::JSXText(text) => { + if !text.value.trim().is_empty() { + pending_rules.clear(); + } + child.fold_with(self) + } + _ => { + pending_rules.clear(); + child.fold_with(self) + } + }) + .collect() + } + + fn fold_boxed_expr_in_jsx_children_context(&mut self, expr: Box) -> Box { + self.jsx_children_expr_depth += 1; + let folded = expr.fold_with(self); + self.jsx_children_expr_depth -= 1; + folded + } + + fn fold_jsx_child_expr(&mut self, child: ast::JSXElementChild) -> ast::JSXElementChild { + self.jsx_children_expr_depth += 1; + let folded = child.fold_with(self); + self.jsx_children_expr_depth -= 1; + folded + } + + fn collect_expr_scoped_idents(&self, expr: &ast::Expr) -> Vec { + let descendent_idents = { + let mut collector = IdentCollector::new(); + expr.visit_with(&mut collector); + collector.get_words() + }; + let decl_collect: Vec<_> = self.decl_stack.iter().flat_map(|v| v.iter()).cloned().collect(); + let (scoped, _) = compute_scoped_idents(&descendent_idents, &decl_collect); + scoped + } + + fn create_deferred_capture_qsegment( + &mut self, + first_arg: ast::Expr, + ctx_kind: SegmentKind, + ctx_name: Atom, + display_name_seed: &str, + shared_scoped_idents: Vec, + ) -> ast::Expr { + let can_capture = can_capture_scope(&first_arg); + let first_arg_span = first_arg.span(); + let (symbol_name, display_name, hash, segment_hash) = self.register_context_name( + None, + Some(display_name_seed), + None, + ); + + self.segment_stack.push(symbol_name.clone()); + let span = first_arg.span(); + let folded = first_arg.fold_with(self); + self.segment_stack.pop(); + + let param_idents = get_function_params(&folded); + let mut scoped_idents = shared_scoped_idents; + scoped_idents.retain(|id| !param_idents.contains(id)); + + if !can_capture && !scoped_idents.is_empty() { + HANDLER.with(|handler| { + let ids: Vec<_> = scoped_idents.iter().map(|id| id.0.as_ref()).collect(); + handler + .struct_span_err_with_code( + first_arg_span, + &format!( + "Qrl($) scope is not a function, but it's capturing local identifiers: {}", + ids.join(", ") + ), + errors::get_diagnostic_id(errors::Error::CanNotCapture), + ) + .emit(); + }); + scoped_idents = vec![]; + } + + let folded = if !scoped_idents.is_empty() { + let new_local = self.ensure_core_import(&_CAPTURES); + transform_function_expr(folded, &new_local, &scoped_idents) + } else { + folded + }; + + let segment_data = SegmentData { + extension: self.options.extension.clone(), + local_idents: self.get_local_idents(&folded), + scoped_idents: vec![], + parent_segment: self.segment_stack.last().cloned(), + ctx_kind, + ctx_name, + origin: self.options.path_data.rel_path.to_slash_lossy().into(), + path: self.options.path_data.rel_dir.to_slash_lossy().into(), + display_name, + need_transform: !matches!(self.options.mode, EmitMode::Lib), + hash, + migrated_root_vars: Vec::new(), + }; + + let should_emit = self.should_emit_segment(&segment_data); + if !should_emit { + return ast::Expr::Call(self.create_noop_qrl(&symbol_name, segment_data)); + } + + let folded = if self.should_reg_segment(&segment_data.ctx_name) { + ast::Expr::Call(self.create_internal_call( + &_REG_SYMBOL, + vec![ + folded, + ast::Expr::Lit(ast::Lit::Str(ast::Str::from(segment_data.hash.clone()))), + ], + true, + )) + } else { + folded + }; + + let _ = segment_hash; + ast::Expr::Call(self.create_inline_qrl_without_capture_array( + segment_data, + folded, + symbol_name, + span, + )) + } + fn get_dev_location(&self, span: Span) -> ast::ExprOrSpread { let loc = self.options.cm.lookup_char_pos(span.lo); let file_name = self @@ -2026,6 +2263,70 @@ impl<'a> QwikTransform<'a> { self.create_internal_call(&fn_callee, args, true) } + fn create_inline_qrl_without_capture_array( + &mut self, + segment_data: SegmentData, + expr: ast::Expr, + symbol_name: Atom, + span: Span, + ) -> ast::CallExpr { + let should_inline = matches!(self.options.entry_strategy, EntryStrategy::Inline) + || matches!(self.options.mode, EmitMode::Lib) + || matches!(expr, ast::Expr::Ident(_)); + let param_names = Self::extract_param_names(&expr); + let inlined_expr = if should_inline { + expr + } else { + let new_ident = private_ident!(symbol_name.clone()); + let qrl_id: Id = ( + Atom::from(format!("q_{}", symbol_name)), + SyntaxContext::empty(), + ); + self.hoisted_segment_idents.insert(id!(new_ident)); + self.segments.push(Segment { + entry: None, + span, + canonical_filename: get_canonical_filename( + &segment_data.display_name, + &symbol_name, + ), + name: symbol_name.clone(), + data: segment_data.clone(), + expr: Box::new(expr), + hash: new_ident.ctxt.as_u32() as u64, + param_names, + qrl_id: Some(qrl_id), + }); + ast::Expr::Ident(new_ident) + }; + + let mut args = vec![ + inlined_expr, + ast::Expr::Lit(ast::Lit::Str(ast::Str { + span: DUMMY_SP, + value: symbol_name, + raw: None, + })), + ]; + + let fn_callee = if matches!(self.options.mode, EmitMode::Dev | EmitMode::Hmr) { + args.push(get_qrl_dev_obj( + Atom::from( + self.options + .dev_path + .unwrap_or(&self.options.path_data.abs_path.to_slash_lossy()), + ), + &segment_data, + &span, + )); + _INLINED_QRL_DEV.clone() + } else { + _INLINED_QRL.clone() + }; + + self.create_internal_call(&fn_callee, args, true) + } + pub fn create_internal_call( &mut self, fn_name: &Atom, @@ -2336,12 +2637,14 @@ impl<'a> QwikTransform<'a> { // input, textarea etc if is_text_only { self.jsx_mutable = true; - folded.fold_with(self) + self.fold_boxed_expr_in_jsx_children_context(folded) } else { - Box::new(new_children.fold_with(self)) + self.fold_boxed_expr_in_jsx_children_context(Box::new( + new_children, + )) } } else { - folded.fold_with(self) + self.fold_boxed_expr_in_jsx_children_context(folded) }; if self.jsx_mutable { static_subtree = false; @@ -3307,6 +3610,34 @@ impl<'a> QwikTransform<'a> { } } +fn optimizer_comment_disables_rule(comment_text: &str, rule: &str) -> bool { + let mut rules = HashSet::new(); + extend_optimizer_disabled_rules_from_comment_text(comment_text, &mut rules); + rules.contains("all") || rules.contains(rule) +} + +fn extend_optimizer_disabled_rules_from_comment(comment: &Comment, rules: &mut HashSet) { + extend_optimizer_disabled_rules_from_comment_text(comment.text.as_ref(), rules); +} + +fn extend_optimizer_disabled_rules_from_comment_text( + comment_text: &str, + rules: &mut HashSet, +) { + for line in comment_text.lines() { + let line = line.trim_start_matches(['*', ' ', '/', '{', '}']).trim(); + let line = line.strip_prefix('@').unwrap_or(line); + let Some(rest) = line.strip_prefix(QWIK_DISABLE_NEXT_LINE_DIRECTIVE) else { + continue; + }; + rules.extend( + rest.split(|c: char| c == ',' || c.is_whitespace()) + .filter(|token| !token.is_empty()) + .map(str::to_string), + ); + } +} + impl<'a> Fold for QwikTransform<'a> { noop_fold_type!(); @@ -3829,14 +4160,25 @@ impl<'a> Fold for QwikTransform<'a> { } else { (false, false) }; - let o = node.fold_children_with(self); + let mut node = node; + node.opening = node.opening.fold_with(self); + node.children = self.fold_jsx_children_with_disable_directives(node.children); + node.closing = node.closing.fold_with(self); if stacked { self.stack_ctxt.pop(); } if is_native { self.jsx_element_is_native.pop(); } - o + node + } + + fn fold_jsx_fragment(&mut self, node: ast::JSXFragment) -> ast::JSXFragment { + let mut node = node; + node.opening = node.opening.fold_with(self); + node.children = self.fold_jsx_children_with_disable_directives(node.children); + node.closing = node.closing.fold_with(self); + node } fn fold_export_default_expr(&mut self, node: ast::ExportDefaultExpr) -> ast::ExportDefaultExpr { @@ -3928,6 +4270,11 @@ impl<'a> Fold for QwikTransform<'a> { let mut replace_callee = None; let mut ctx_name: Atom = QSEGMENT.clone(); + if let Some(replacement) = self.try_rewrite_map_to_each(&node) { + self.pending_expr_replacement = Some(replacement); + return Default::default(); + } + // Check if this is an array iteration method call (e.g., .map(), .filter(), etc.) let is_iteration_method = if let ast::Callee::Expr(box ast::Expr::Member(member)) = &node.callee { diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs new file mode 100644 index 00000000000..cf26eec820a --- /dev/null +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -0,0 +1,841 @@ +use super::*; + +const MAP_TO_EACH_DIRECTIVE: &str = "map-to-each"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EachCandidateWarning { + MissingKey, + UsesSecondParamForKey, + CallDerivedKey, + NotSingleJsxNode, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EachSliceError { + UsesSecondParamForKey, + CallDerivedKey, + UnsafeSlice, +} + +#[derive(Clone)] +enum EachCallbackBody { + Expr(Box), + Block { + stmts: Vec, + return_expr: Box, + }, +} + +#[derive(Clone)] +struct EachCallbackInfo { + params: Vec, + second_param_ids: Vec, + body: EachCallbackBody, +} + +impl<'a> QwikTransform<'a> { + fn push_each_candidate_warning(&mut self, span: Span, warning: EachCandidateWarning) { + let (message, suggestion) = match warning { + EachCandidateWarning::MissingKey => ( + "This .map() was not optimized to Each because the returned JSX node is missing a key.", + "Add a stable key to the returned JSX node.", + ), + EachCandidateWarning::UsesSecondParamForKey => ( + "This .map() was not optimized to Each because the key uses the callback's index parameter.", + "Use a stable key derived from the item instead of the callback's index parameter.", + ), + EachCandidateWarning::CallDerivedKey => ( + "This .map() was not optimized to Each because the key is derived from a function call.", + "Use a stable key expression without function calls.", + ), + EachCandidateWarning::NotSingleJsxNode => ( + "This .map() was not optimized to Each because the callback does not return a single JSX node.", + "Return a single JSX node from the .map() callback.", + ), + }; + self.diagnostics.push(Diagnostic { + category: DiagnosticCategory::Warning, + code: Some(MAP_TO_EACH_DIRECTIVE.to_string()), + file: self + .options + .path_data + .rel_path + .to_slash_lossy() + .to_string() + .into(), + message: message.into(), + highlights: Some(vec![SourceLocation::from(&self.options.cm, span)]), + suggestions: Some(vec![suggestion.into()]), + scope: DiagnosticScope::Optimizer, + }); + } + + fn analyze_each_callback(&self, expr: &ast::Expr) -> Option { + // Normalize arrow/function callbacks into one shape so the rewrite logic can + // reason about params and the returned JSX without caring which syntax was used. + match expr { + ast::Expr::Arrow(arrow) => { + let params = arrow.params.clone(); + let second_param_ids = params.get(1).map_or_else(Vec::new, collect_ids_from_pat); + let body = match &*arrow.body { + ast::BlockStmtOrExpr::Expr(expr) => EachCallbackBody::Expr(expr.clone()), + ast::BlockStmtOrExpr::BlockStmt(block) => { + let (last, prefix) = block.stmts.split_last()?; + let ast::Stmt::Return(ast::ReturnStmt { + arg: Some(return_expr), + .. + }) = last + else { + return None; + }; + EachCallbackBody::Block { + stmts: prefix.to_vec(), + return_expr: return_expr.clone(), + } + } + }; + Some(EachCallbackInfo { + params, + second_param_ids, + body, + }) + } + ast::Expr::Fn(fn_expr) => { + let params: Vec<_> = fn_expr + .function + .params + .iter() + .map(|param| param.pat.clone()) + .collect(); + let second_param_ids = params.get(1).map_or_else(Vec::new, collect_ids_from_pat); + let body = { + let block = fn_expr.function.body.as_ref()?; + let (last, prefix) = block.stmts.split_last()?; + let ast::Stmt::Return(ast::ReturnStmt { + arg: Some(return_expr), + .. + }) = last + else { + return None; + }; + EachCallbackBody::Block { + stmts: prefix.to_vec(), + return_expr: return_expr.clone(), + } + }; + Some(EachCallbackInfo { + params, + second_param_ids, + body, + }) + } + _ => None, + } + } + + pub(super) fn try_rewrite_map_to_each(&mut self, node: &ast::CallExpr) -> Option { + if self.jsx_children_expr_depth == 0 { + return None; + } + // Hoist mode currently lowers JSX prop QRLs to module-local noop QRLs. That is fine + // for many internal uses, but `Each` persists `key$` / `item$` into serialized output, + // where resumability needs chunk-backed QRLs. Until the hoist path can emit those + // props safely, leave authored `.map()` loops unchanged in hoist builds. + if matches!(self.options.entry_strategy, EntryStrategy::Hoist) { + return None; + } + + let ast::Callee::Expr(box ast::Expr::Member(member)) = &node.callee else { + return None; + }; + let directive_span = if node.span.lo.0 == 0 { + member.span + } else { + node.span + }; + if self.has_disabled_optimizer_rule(directive_span, MAP_TO_EACH_DIRECTIVE) { + return None; + } + if prop_to_string(&member.prop).as_deref() != Some("map") { + return None; + } + + let callback = node.args.first()?.expr.as_ref(); + let callback_info = match self.analyze_each_callback(callback) { + Some(info) if !info.params.is_empty() => info, + _ => return None, + }; + + let (return_expr, stmts) = match &callback_info.body { + EachCallbackBody::Expr(expr) => (expr.as_ref(), None), + EachCallbackBody::Block { stmts, return_expr } => (return_expr.as_ref(), Some(stmts)), + }; + + // Support both optimizer entry paths: + // - raw JSX when `transpile_jsx` is false + // - lowered `jsx/jsxs/jsxDEV(...)` calls when JSX was already transpiled by SWC + let (key_expr, item_expr, raw_mode) = if let Some(jsx) = get_jsx_element_expr(return_expr) { + let mut jsx = jsx.clone(); + let Some(key_expr) = remove_key_from_jsx_element(&mut jsx) else { + self.push_each_candidate_warning(node.span, EachCandidateWarning::MissingKey); + return None; + }; + ( + Box::new(key_expr), + Box::new(ast::Expr::JSXElement(Box::new(jsx))), + true, + ) + } else if let Some(call) = get_transpiled_jsx_call(return_expr, &self.jsx_functions) { + let mut jsx_call = call.clone(); + let Some(key_expr) = remove_key_from_transpiled_jsx_call(&mut jsx_call) else { + self.push_each_candidate_warning(node.span, EachCandidateWarning::MissingKey); + return None; + }; + ( + Box::new(key_expr), + Box::new(ast::Expr::Call(jsx_call)), + false, + ) + } else if is_non_single_jsx_like(return_expr, &self.jsx_functions) { + self.push_each_candidate_warning(node.span, EachCandidateWarning::NotSingleJsxNode); + return None; + } else { + return None; + }; + + if contains_any_ident(key_expr.as_ref(), &callback_info.second_param_ids) { + self.push_each_candidate_warning( + node.span, + EachCandidateWarning::UsesSecondParamForKey, + ); + return None; + } + if expr_contains_call(key_expr.as_ref()) { + self.push_each_candidate_warning(node.span, EachCandidateWarning::CallDerivedKey); + return None; + } + if item_expr_has_dynamic_component_reference( + item_expr.as_ref(), + &callback_info.params, + &self.jsx_functions, + ) { + return None; + } + + let key_fn_expr = match stmts { + Some(stmts) => { + // Build the minimal control-flow slice needed to compute the key. This + // keeps us from blindly duplicating the whole callback prefix into `key$`. + let key_stmts = match slice_statements_for_target( + stmts, + key_expr.as_ref(), + &callback_info.second_param_ids, + true, + ) { + Ok(stmts) => stmts, + Err(err) => { + if let Some(warning) = slice_error_to_warning(err) { + self.push_each_candidate_warning(node.span, warning); + } + return None; + } + }; + build_each_arrow_expr(&callback_info.params, Some(key_stmts), key_expr) + } + None => build_each_arrow_expr(&callback_info.params, None, key_expr), + }; + + let item_fn_expr = match stmts { + Some(stmts) => { + // Build the minimal slice needed to render the item body. Shared, simple + // aliases can appear in both generated functions when both depend on them. + let item_stmts = + match slice_statements_for_target(stmts, item_expr.as_ref(), &[], false) { + Ok(stmts) => stmts, + Err(err) => { + if let Some(warning) = slice_error_to_warning(err) { + self.push_each_candidate_warning(node.span, warning); + } + return None; + } + }; + build_each_arrow_expr(&callback_info.params, Some(item_stmts), item_expr) + } + None => build_each_arrow_expr(&callback_info.params, None, item_expr), + }; + + let mut shared_scoped_idents = self.collect_expr_scoped_idents(&key_fn_expr); + for id in self.collect_expr_scoped_idents(&item_fn_expr) { + if !shared_scoped_idents.contains(&id) { + shared_scoped_idents.push(id); + } + } + + if !shared_scoped_idents.is_empty() { + // Captured values can still use the Each fast path when they are serializable, so + // lower to the internal `_map(...)` helper and let runtime decide between `Each` + // and the original `.map(...)` callback. + let items_expr = member.obj.as_ref().clone().fold_with(self); + let fallback_fn = callback.clone().fold_with(self); + let key_qrl = self.create_deferred_capture_qsegment( + key_fn_expr, + SegmentKind::JSXProp, + Atom::from("key$"), + "map_key", + shared_scoped_idents.clone(), + ); + let item_qrl = self.create_deferred_capture_qsegment( + item_fn_expr, + SegmentKind::JSXProp, + Atom::from("item$"), + "map_item", + shared_scoped_idents.clone(), + ); + let captures_expr = ast::Expr::Array(ast::ArrayLit { + span: DUMMY_SP, + elems: shared_scoped_idents + .into_iter() + .map(|id| { + Some(ast::ExprOrSpread { + spread: None, + expr: Box::new(ast::Expr::Ident(new_ident_from_id(&id))), + }) + }) + .collect(), + }); + return Some(ast::Expr::Call(self.create_internal_call( + &_MAP, + vec![items_expr, fallback_fn, key_qrl, item_qrl, captures_expr], + true, + ))); + } + + let each_id = self.ensure_core_import(&Atom::from("Each")); + let replacement = if raw_mode { + build_each_jsx_element( + &each_id, + member.obj.as_ref().clone(), + key_fn_expr, + item_fn_expr, + ) + } else { + let template = get_transpiled_jsx_call(return_expr, &self.jsx_functions) + .expect("checked transpiled jsx call"); + build_each_transpiled_call( + template, + &each_id, + member.obj.as_ref().clone(), + key_fn_expr, + item_fn_expr, + ) + }; + + Some(replacement.fold_with(self)) + } +} + +fn collect_ids_from_pat(pat: &ast::Pat) -> Vec { + let mut identifiers = Vec::new(); + collect_from_pat(pat, &mut identifiers); + identifiers.into_iter().map(|(id, _)| id).collect() +} + +fn collect_expr_used_idents(expr: &ast::Expr) -> HashSet { + let mut collector = IdentCollector::new(); + expr.visit_with(&mut collector); + collector.get_words().into_iter().collect() +} + +fn collect_stmt_used_idents(stmt: &ast::Stmt) -> HashSet { + let mut collector = AnyIdentCollector::new(); + stmt.visit_with(&mut collector); + collector.local_idents +} + +struct DefinedIdCollector { + defines: HashSet, +} + +impl DefinedIdCollector { + fn new() -> Self { + Self { + defines: HashSet::new(), + } + } +} + +impl Visit for DefinedIdCollector { + noop_visit_type!(); + + fn visit_var_declarator(&mut self, node: &ast::VarDeclarator) { + let mut identifiers = Vec::new(); + collect_from_pat(&node.name, &mut identifiers); + self.defines + .extend(identifiers.into_iter().map(|(id, _)| id)); + node.visit_children_with(self); + } + + fn visit_assign_expr(&mut self, node: &ast::AssignExpr) { + match &node.left { + ast::AssignTarget::Simple(simple) => match simple { + ast::SimpleAssignTarget::Ident(ident) => { + self.defines.insert(id!(&ident.id)); + } + ast::SimpleAssignTarget::Paren(paren) => { + paren.visit_children_with(self); + } + _ => {} + }, + ast::AssignTarget::Pat(pat) => { + pat.visit_children_with(self); + } + } + node.visit_children_with(self); + } + + fn visit_update_expr(&mut self, node: &ast::UpdateExpr) { + if let ast::Expr::Ident(ident) = &*node.arg { + self.defines.insert(id!(ident)); + } + node.visit_children_with(self); + } +} + +fn collect_stmt_defined_idents(stmt: &ast::Stmt) -> HashSet { + let mut collector = DefinedIdCollector::new(); + stmt.visit_with(&mut collector); + collector.defines +} + +struct DeclaredIdCollector { + declared: HashSet, +} + +impl DeclaredIdCollector { + fn new() -> Self { + Self { + declared: HashSet::new(), + } + } +} + +impl Visit for DeclaredIdCollector { + noop_visit_type!(); + + fn visit_var_declarator(&mut self, node: &ast::VarDeclarator) { + let mut identifiers = Vec::new(); + collect_from_pat(&node.name, &mut identifiers); + self.declared + .extend(identifiers.into_iter().map(|(id, _)| id)); + node.visit_children_with(self); + } + + fn visit_fn_decl(&mut self, node: &ast::FnDecl) { + self.declared.insert(id!(&node.ident)); + node.visit_children_with(self); + } + + fn visit_class_decl(&mut self, node: &ast::ClassDecl) { + self.declared.insert(id!(&node.ident)); + node.visit_children_with(self); + } +} + +fn collect_stmt_declared_idents(stmt: &ast::Stmt) -> HashSet { + let mut collector = DeclaredIdCollector::new(); + stmt.visit_with(&mut collector); + collector.declared +} + +fn stmt_uses_any_ident(stmt: &ast::Stmt, ids: &[Id]) -> bool { + ids.iter().any(|id| { + let mut checker = StatementIdentChecker { + target_id: id.clone(), + found: false, + }; + stmt.visit_with(&mut checker); + checker.found + }) +} + +fn contains_any_ident(expr: &ast::Expr, ids: &[Id]) -> bool { + ids.iter().any(|id| expr_uses_ident(expr, id)) +} + +struct StatementIdentChecker { + target_id: Id, + found: bool, +} + +impl Visit for StatementIdentChecker { + noop_visit_type!(); + + fn visit_ident(&mut self, ident: &ast::Ident) { + if id!(ident) == self.target_id { + self.found = true; + } + } +} + +struct CallExprChecker { + found: bool, +} + +impl Visit for CallExprChecker { + noop_visit_type!(); + + fn visit_call_expr(&mut self, _: &ast::CallExpr) { + self.found = true; + } + + fn visit_opt_chain_expr(&mut self, node: &ast::OptChainExpr) { + if matches!(*node.base, ast::OptChainBase::Call(_)) { + self.found = true; + } + node.visit_children_with(self); + } +} + +fn expr_contains_call(expr: &ast::Expr) -> bool { + let mut checker = CallExprChecker { found: false }; + expr.visit_with(&mut checker); + checker.found +} + +fn stmt_contains_call(stmt: &ast::Stmt) -> bool { + let mut checker = CallExprChecker { found: false }; + stmt.visit_with(&mut checker); + checker.found +} + +fn collect_declared_ids_from_stmts(stmts: &[ast::Stmt]) -> HashSet { + let mut out = HashSet::new(); + for stmt in stmts { + out.extend(collect_stmt_defined_idents(stmt)); + } + out +} + +fn slice_statements_for_target( + stmts: &[ast::Stmt], + target_expr: &ast::Expr, + second_param_ids: &[Id], + is_key_slice: bool, +) -> Result, EachSliceError> { + // Walk backwards from the final expression, pulling in only statements that define + // values still needed by that expression. This lets `key$` and `item$` preserve + // relevant branches/loops independently instead of copying the entire callback body. + let mut needed = collect_expr_used_idents(target_expr); + let local_declared = collect_declared_ids_from_stmts(stmts); + let mut selected = Vec::new(); + + for stmt in stmts.iter().rev() { + let defines = collect_stmt_defined_idents(stmt); + let declared = collect_stmt_declared_idents(stmt); + if defines.is_empty() || defines.is_disjoint(&needed) { + continue; + } + let contains_call = stmt_contains_call(stmt); + let uses_second_param = stmt_uses_any_ident(stmt, second_param_ids); + if is_key_slice && (contains_call || uses_second_param) { + return Err(if contains_call { + EachSliceError::CallDerivedKey + } else { + EachSliceError::UsesSecondParamForKey + }); + } + + let mut uses = collect_stmt_used_idents(stmt); + for defined in &defines { + needed.remove(defined); + if declared.contains(defined) { + uses.remove(defined); + } + } + needed.extend(uses); + selected.push(stmt.clone()); + } + + if needed.iter().any(|id| local_declared.contains(id)) { + return Err(EachSliceError::UnsafeSlice); + } + + selected.reverse(); + Ok(selected) +} + +fn slice_error_to_warning(err: EachSliceError) -> Option { + match err { + EachSliceError::UsesSecondParamForKey => Some(EachCandidateWarning::UsesSecondParamForKey), + EachSliceError::CallDerivedKey => Some(EachCandidateWarning::CallDerivedKey), + EachSliceError::UnsafeSlice => None, + } +} + +fn get_jsx_element_expr(expr: &ast::Expr) -> Option<&ast::JSXElement> { + match expr { + ast::Expr::Paren(paren) => get_jsx_element_expr(&paren.expr), + ast::Expr::JSXElement(element) => Some(element), + _ => None, + } +} + +fn get_transpiled_jsx_call<'a>( + expr: &'a ast::Expr, + jsx_functions: &HashSet, +) -> Option<&'a ast::CallExpr> { + match expr { + ast::Expr::Paren(paren) => get_transpiled_jsx_call(&paren.expr, jsx_functions), + ast::Expr::Call(call) => { + let ast::Callee::Expr(box ast::Expr::Ident(ident)) = &call.callee else { + return None; + }; + jsx_functions.contains(&id!(ident)).then_some(call) + } + _ => None, + } +} + +fn is_non_single_jsx_like(expr: &ast::Expr, jsx_functions: &HashSet) -> bool { + match expr { + ast::Expr::Paren(paren) => is_non_single_jsx_like(&paren.expr, jsx_functions), + ast::Expr::JSXFragment(_) | ast::Expr::Array(_) => true, + ast::Expr::Call(call) => { + let ast::Callee::Expr(box ast::Expr::Ident(ident)) = &call.callee else { + return false; + }; + jsx_functions.contains(&id!(ident)) + } + _ => false, + } +} + +fn remove_key_from_jsx_element(element: &mut ast::JSXElement) -> Option { + let key_index = element.opening.attrs.iter().position(|attr| { + matches!( + attr, + ast::JSXAttrOrSpread::JSXAttr(ast::JSXAttr { + name: ast::JSXAttrName::Ident(ident), + .. + }) if ident.sym == *"key" + ) + })?; + let ast::JSXAttrOrSpread::JSXAttr(attr) = element.opening.attrs.remove(key_index) else { + return None; + }; + match attr.value { + Some(ast::JSXAttrValue::Lit(ast::Lit::Str(str))) => { + Some(ast::Expr::Lit(ast::Lit::Str(str))) + } + Some(ast::JSXAttrValue::JSXExprContainer(container)) => match container.expr { + ast::JSXExpr::Expr(expr) => Some(*expr), + ast::JSXExpr::JSXEmptyExpr(_) => None, + }, + _ => None, + } +} + +fn remove_key_from_transpiled_jsx_call(call: &mut ast::CallExpr) -> Option { + if call.args.len() < 3 { + return None; + } + // SWC's automatic JSX runtime lowers `
    ` to `jsx(type, props, key, ...)`, + // so the key lives outside the props object and must be cleared from arg slot 2. + let key_expr = (*call.args[2].expr).clone(); + call.args[2] = get_null_arg(); + Some(key_expr) +} + +fn build_each_arrow_expr( + params: &[ast::Pat], + stmts: Option>, + return_expr: Box, +) -> ast::Expr { + let params = params.to_vec(); + match stmts { + Some(mut stmts) => { + stmts.push(ast::Stmt::Return(ast::ReturnStmt { + span: DUMMY_SP, + arg: Some(return_expr), + })); + ast::Expr::Arrow(ast::ArrowExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + params, + body: Box::new(ast::BlockStmtOrExpr::BlockStmt(ast::BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts, + })), + is_async: false, + is_generator: false, + type_params: None, + return_type: None, + }) + } + None => ast::Expr::Arrow(ast::ArrowExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + params, + body: Box::new(ast::BlockStmtOrExpr::Expr(return_expr)), + is_async: false, + is_generator: false, + type_params: None, + return_type: None, + }), + } +} + +fn build_each_jsx_attr(name: &str, expr: ast::Expr) -> ast::JSXAttrOrSpread { + ast::JSXAttrOrSpread::JSXAttr(ast::JSXAttr { + span: DUMMY_SP, + name: ast::JSXAttrName::Ident(ast::IdentName::new(name.into(), DUMMY_SP)), + value: Some(ast::JSXAttrValue::JSXExprContainer(ast::JSXExprContainer { + span: DUMMY_SP, + expr: ast::JSXExpr::Expr(Box::new(expr)), + })), + }) +} + +fn build_each_jsx_element( + each_id: &Id, + items_expr: ast::Expr, + key_expr: ast::Expr, + item_expr: ast::Expr, +) -> ast::Expr { + ast::Expr::JSXElement(Box::new(ast::JSXElement { + span: DUMMY_SP, + opening: ast::JSXOpeningElement { + name: ast::JSXElementName::Ident(ast::Ident::new( + each_id.0.clone(), + DUMMY_SP, + each_id.1, + )), + span: DUMMY_SP, + attrs: vec![ + build_each_jsx_attr("items", items_expr), + build_each_jsx_attr("key$", key_expr), + build_each_jsx_attr("item$", item_expr), + ], + self_closing: true, + type_args: None, + }, + children: vec![], + closing: None, + })) +} + +fn build_each_prop(name: &str, expr: ast::Expr) -> ast::PropOrSpread { + ast::PropOrSpread::Prop(Box::new(ast::Prop::KeyValue(ast::KeyValueProp { + key: ast::PropName::Ident(ast::IdentName::new(name.into(), DUMMY_SP)), + value: Box::new(expr), + }))) +} + +fn build_each_transpiled_call( + template: &ast::CallExpr, + each_id: &Id, + items_expr: ast::Expr, + key_expr: ast::Expr, + item_expr: ast::Expr, +) -> ast::Expr { + let mut args = vec![ + ast::ExprOrSpread { + spread: None, + expr: Box::new(ast::Expr::Ident(new_ident_from_id(each_id))), + }, + ast::ExprOrSpread { + spread: None, + expr: Box::new(ast::Expr::Object(ast::ObjectLit { + span: DUMMY_SP, + props: vec![ + build_each_prop("items", items_expr), + build_each_prop("key$", key_expr), + build_each_prop("item$", item_expr), + ], + })), + }, + get_null_arg(), + ]; + // Preserve any extra runtime arguments (for example dev-location metadata) from the + // original lowered JSX call so the surrounding optimizer pipeline keeps behaving the same. + args.extend(template.args.iter().skip(3).cloned()); + ast::Expr::Call(ast::CallExpr { + callee: template.callee.clone(), + args, + ..template.clone() + }) +} + +fn item_expr_has_dynamic_component_reference( + expr: &ast::Expr, + params: &[ast::Pat], + jsx_functions: &HashSet, +) -> bool { + let param_ids: Vec<_> = params.iter().flat_map(collect_ids_from_pat).collect(); + if param_ids.is_empty() { + return false; + } + + let mut checker = DynamicComponentReferenceChecker { + param_ids: ¶m_ids, + jsx_functions, + found: false, + }; + expr.visit_with(&mut checker); + checker.found +} + +struct DynamicComponentReferenceChecker<'a> { + param_ids: &'a [Id], + jsx_functions: &'a HashSet, + found: bool, +} + +impl Visit for DynamicComponentReferenceChecker<'_> { + noop_visit_type!(); + + fn visit_jsx_opening_element(&mut self, node: &ast::JSXOpeningElement) { + if jsx_element_name_uses_any_ident(&node.name, self.param_ids) { + self.found = true; + return; + } + node.visit_children_with(self); + } + + fn visit_call_expr(&mut self, node: &ast::CallExpr) { + let ast::Callee::Expr(box ast::Expr::Ident(ident)) = &node.callee else { + node.visit_children_with(self); + return; + }; + + if self.jsx_functions.contains(&id!(ident)) + && node + .args + .first() + .is_some_and(|arg| match arg.expr.as_ref() { + ast::Expr::Lit(ast::Lit::Str(_)) => false, + expr => contains_any_ident(expr, self.param_ids), + }) { + self.found = true; + return; + } + + node.visit_children_with(self); + } +} + +fn jsx_element_name_uses_any_ident(name: &ast::JSXElementName, ids: &[Id]) -> bool { + match name { + ast::JSXElementName::Ident(ident) => ids.iter().any(|id| id!(ident) == *id), + ast::JSXElementName::JSXMemberExpr(member) => jsx_member_expr_uses_any_ident(member, ids), + ast::JSXElementName::JSXNamespacedName(_) => false, + } +} + +fn jsx_member_expr_uses_any_ident(member: &ast::JSXMemberExpr, ids: &[Id]) -> bool { + match &member.obj { + ast::JSXObject::Ident(ident) => ids.iter().any(|id| id!(ident) == *id), + ast::JSXObject::JSXMemberExpr(member) => jsx_member_expr_uses_any_ident(member, ids), + } +} diff --git a/packages/optimizer/core/src/utils.rs b/packages/optimizer/core/src/utils.rs index 4851a347790..938d9486ee6 100644 --- a/packages/optimizer/core/src/utils.rs +++ b/packages/optimizer/core/src/utils.rs @@ -41,7 +41,7 @@ impl PartialOrd for SourceLocation { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Diagnostic { pub category: DiagnosticCategory, @@ -53,7 +53,7 @@ pub struct Diagnostic { pub scope: DiagnosticScope, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub enum DiagnosticCategory { /// Fails the build with an error. @@ -64,7 +64,7 @@ pub enum DiagnosticCategory { SourceError, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub enum DiagnosticScope { Optimizer, diff --git a/packages/optimizer/core/src/words.rs b/packages/optimizer/core/src/words.rs index 41082a8b554..ef18931fabb 100644 --- a/packages/optimizer/core/src/words.rs +++ b/packages/optimizer/core/src/words.rs @@ -24,6 +24,7 @@ lazy_static! { pub static ref BUILDER_IO_QWIK_JSX_DEV: Atom = Atom::from("@qwik.dev/core/jsx-dev-runtime"); pub static ref QCOMPONENT: Atom = Atom::from("component$"); pub static ref _CAPTURES: Atom = Atom::from("_captures"); + pub static ref _MAP: Atom = Atom::from("_map"); pub static ref H: Atom = Atom::from("h"); pub static ref FRAGMENT: Atom = Atom::from("Fragment"); pub static ref _INLINED_FN: Atom = Atom::from("_fnSignal"); diff --git a/packages/qwik-vite/src/plugins/rollup.ts b/packages/qwik-vite/src/plugins/rollup.ts index 4ddf5b368ce..93b06e20bc9 100644 --- a/packages/qwik-vite/src/plugins/rollup.ts +++ b/packages/qwik-vite/src/plugins/rollup.ts @@ -283,6 +283,7 @@ export function createRollupError(id: string, diagnostic: Diagnostic) { id, plugin: 'qwik', loc: loc && { + file: id, column: loc.startCol, line: loc.startLine, }, diff --git a/packages/qwik/src/core/control-flow/each.ts b/packages/qwik/src/core/control-flow/each.ts index 4ef72c573a1..b05f928abeb 100644 --- a/packages/qwik/src/core/control-flow/each.ts +++ b/packages/qwik/src/core/control-flow/each.ts @@ -1,7 +1,8 @@ import type { PublicProps } from '../shared/component.public'; -import type { DevJSX, JSXOutput } from '../shared/jsx/types/jsx-node'; +import type { DevJSX, JSXNode, JSXNodeInternal, JSXOutput } from '../shared/jsx/types/jsx-node'; import type { QRL } from '../shared/qrl/qrl.public'; import { inlinedQrl } from '../shared/qrl/qrl'; +import type { QRLInternal } from '../shared/qrl/qrl-class'; import { tryGetInvokeContext } from '../use/use-core'; import { markVNodeDirty } from '../shared/vnode/vnode-dirty'; import { ChoreBits } from '../shared/vnode/enums/chore-bits.enum'; @@ -11,6 +12,11 @@ import { type TaskCtx, useTaskQrl } from '../use/use-task'; import { isServer } from '@qwik.dev/core/build'; import { SkipRender } from '../shared/jsx/utils.public'; import { _captures } from '../shared/qrl/qrl-class'; +import { _getProps, type PropsProxy } from '../shared/jsx/props-proxy'; +import { isStore } from '../reactive-primitives/impl/store'; +import { isSignal } from '../internal'; +import { _jsxSorted } from '../shared/jsx/jsx-internal'; +import { canSerialize } from '../shared/serdes/index'; export interface EachProps { items: readonly T[]; @@ -27,8 +33,13 @@ export type EachComponent = ( /** @internal */ export const eachCmpTask = async ({ track }: TaskCtx) => { - const props = _captures![0] as EachProps; - track(() => props.items); + const props = _captures![0] as PropsProxy; + const items = _getProps(props, 'items') as any; + if (isSignal(items) || isStore(items)) { + track(items); + } else { + track(() => items); + } const context = tryGetInvokeContext()!; const host = context.$hostElement$!; const container = context.$container$!; @@ -45,6 +56,32 @@ export const eachCmp = (props: EachProps) => { return SkipRender; }; +/** @internal */ +export const _map = ( + items: readonly T[], + fallbackFn: (item: T, index: number) => ITEM, + key$: QRL<(item: T, index: number) => string>, + item$: QRL<(item: T, index: number) => ITEM>, + captures: Readonly | null +): JSXNode => { + if (!Array.isArray(items) || !canSerialize(captures)) { + return (items as any).map(fallbackFn); + } + + return _jsxSorted( + Each, + { + item$: (item$ as QRLInternal<(item: T, index: number) => ITEM>).w(captures), + items, + key$: (key$ as QRLInternal<(item: T, index: number) => string>).w(captures), + }, + null, + null, + 3, + null + ) as JSXNodeInternal; +}; + /** @public */ export const Each = /*#__PURE__*/ componentQrl>( /*#__PURE__*/ inlinedQrl(eachCmp, '_eaC') diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index d6e784dc274..2263284bb62 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -180,7 +180,7 @@ export type { ComputedOptions } from './reactive-primitives/types'; ////////////////////////////////////////////////////////////////////////////////////////// // Control flow ////////////////////////////////////////////////////////////////////////////////////////// -export { eachCmpTask as _eaT, eachCmp as _eaC } from './control-flow/each'; +export { eachCmpTask as _eaT, eachCmp as _eaC, _map } from './control-flow/each'; export { Each } from './control-flow/each'; ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index ceea9e20143..8161d098b2b 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -607,6 +607,9 @@ export type JSXTagName = keyof HTMLElementTagNameMap | Omit; +// @internal (undocumented) +export const _map: (items: readonly T[], fallbackFn: (item: T, index: number) => ITEM, key$: QRL<(item: T, index: number) => string>, item$: QRL<(item: T, index: number) => ITEM>, captures: Readonly | null) => JSXNode; + // @internal (undocumented) export const _mapApp_findIndx: (array: (T | null)[], key: string, start: number) => number; diff --git a/packages/qwik/src/core/tests/component.spec.tsx b/packages/qwik/src/core/tests/component.spec.tsx index 8671914501e..e4ed78afab7 100644 --- a/packages/qwik/src/core/tests/component.spec.tsx +++ b/packages/qwik/src/core/tests/component.spec.tsx @@ -1030,6 +1030,7 @@ describe.each([ <> + {/* @qwik-disable-next-line map-to-each */} {children.map((value) => { return ; })} @@ -1101,6 +1102,7 @@ describe.each([ return (
    (keys.value = ['B', 'C', 'A'])}> + {/* @qwik-disable-next-line map-to-each */} {keys.value.map((key) => ( {key} @@ -1670,6 +1672,7 @@ describe.each([ }} >
    + {/* @qwik-disable-next-line map-to-each */} {data.items.map((item, index) => { return ( diff --git a/packages/qwik/src/core/tests/each.spec.tsx b/packages/qwik/src/core/tests/each.spec.tsx index 18704939416..d24efd83b65 100644 --- a/packages/qwik/src/core/tests/each.spec.tsx +++ b/packages/qwik/src/core/tests/each.spec.tsx @@ -1,7 +1,15 @@ import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; import { describe, expect, it } from 'vitest'; -import { component$, Fragment, Fragment as Component, useSignal, useStore } from '@qwik.dev/core'; -import { Each } from '../control-flow/each'; +import { + component$, + Fragment, + Fragment as Component, + inlinedQrl, + noSerialize, + useSignal, + useStore, +} from '@qwik.dev/core'; +import { _map, Each } from '../control-flow/each'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -202,6 +210,74 @@ describe.each([ ); }); + it('should use Each fast path when _map captures are serializable', () => { + const captures = ['serializable']; + const result = _map( + [ + { id: 'a', label: 'Hello a' }, + { id: 'b', label: 'Hello b' }, + ], + (item) =>
    {item.label}
    , + inlinedQrl((item) => item.id, 's_mapKeySerializable'), + inlinedQrl((item) =>
    {item.label}
    , 's_mapItemSerializable'), + captures + ) as any; + + expect(result.type).toBe(Each); + expect(result.props.key$.getCaptured()).toEqual(captures); + expect(result.props.item$.getCaptured()).toEqual(captures); + }); + + it('should fall back to .map() when _map captures are not serializable', async () => { + const Cmp = component$(() => { + const items = useSignal([ + { id: 'a', label: 'Hello a' }, + { id: 'b', label: 'Hello b' }, + ]); + const captures = [noSerialize({ kind: 'non-serializable' })]; + return ( + <> +
    + {_map( + items.value, + (item) => ( +
    {item.label}
    + ), + inlinedQrl((item) => item.id, 's_mapKeyFallback'), + inlinedQrl((item) =>
    {item.label}
    , 's_mapItemFallback'), + captures + )} +
    + + + ); + }); + + const { document } = await render(, { debug }); + await expect(document.getElementById('loop')).toMatchDOM( +
    +
    Hello a
    +
    Hello b
    +
    + ); + await trigger(document.body, 'button', 'click'); + await expect(document.getElementById('loop')).toMatchDOM( +
    +
    Hello a
    +
    Updated b
    +
    + ); + }); + it('should swap items without re-rendering the rest', async () => { (globalThis as any).testCount = 0; const Cmp = component$(() => { @@ -278,6 +354,55 @@ describe.each([ }); describe('store', async () => { + it('should update when a store-backed array shrinks in place', async () => { + const Cmp = component$(() => { + const items = useStore([ + { id: 1, label: 'Hello a' }, + { id: 2, label: 'Hello b' }, + { id: 3, label: 'Hello c' }, + ]); + return ( + <> +
    + String(item.id)} + item$={(item) =>
    {item.label}
    } + /> +
    + + + ); + }); + + const { document } = await render(, { debug }); + await expect(document.getElementById('loop')).toMatchDOM( +
    +
    Hello a
    +
    Hello b
    +
    Hello c
    +
    + ); + + await trigger(document.body, 'button', 'click'); + await expect(document.getElementById('loop')).toMatchDOM( +
    +
    Hello a
    +
    Hello b
    +
    + ); + + await trigger(document.body, 'button', 'click'); + await expect(document.getElementById('loop')).toMatchDOM( +
    +
    Hello a
    +
    + ); + + await trigger(document.body, 'button', 'click'); + await expect(document.getElementById('loop')).toMatchDOM(
    ); + }); + it('should update each item', async () => { const Cmp = component$(() => { const items = useStore({ diff --git a/packages/qwik/src/core/tests/use-computed.spec.tsx b/packages/qwik/src/core/tests/use-computed.spec.tsx index abd677a3cb8..f9a9beb93b0 100644 --- a/packages/qwik/src/core/tests/use-computed.spec.tsx +++ b/packages/qwik/src/core/tests/use-computed.spec.tsx @@ -350,6 +350,7 @@ describe.each([ return ( <> + {/* @qwik-disable-next-line map-to-each */} {[count.value].map((o) => ( ))} diff --git a/scripts/submodule-optimizer.ts b/scripts/submodule-optimizer.ts index 12286041a8a..6130a7e8e3a 100644 --- a/scripts/submodule-optimizer.ts +++ b/scripts/submodule-optimizer.ts @@ -83,9 +83,15 @@ export async function submoduleOptimizer(config: BuildConfig) { const optimizerScopeDir = join(config.rootDir, 'node_modules', '@qwik.dev'); const optimizerLinkPath = join(optimizerScopeDir, 'optimizer'); + const optimizerTargetPath = + process.platform === 'win32' + ? config.optimizerPkgDir + : join('..', '..', 'packages', 'optimizer'); + const optimizerLinkType = process.platform === 'win32' ? 'junction' : 'dir'; mkdirSync(optimizerScopeDir, { recursive: true }); rmSync(optimizerLinkPath, { force: true, recursive: true }); - symlinkSync(join('..', '..', 'packages', 'optimizer'), optimizerLinkPath, 'dir'); + // Windows commonly blocks directory symlinks unless Developer Mode or elevated privileges are enabled. + symlinkSync(optimizerTargetPath, optimizerLinkPath, optimizerLinkType); console.log('🐹', submodule); }