From 7eae5d429d884dd28b3589ddb798c0b19257c06c Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 12:49:36 +0100 Subject: [PATCH 1/9] feat: implement transformation from map to Each for JSX elements --- packages/optimizer/core/src/parse.rs | 7 +- ...nent_with_event_listeners_inside_loop.snap | 44 +- ...ple_derived_signals_complext_children.snap | 24 +- ..._test__example_functional_component_2.snap | 24 +- ...ore__test__example_qwik_router_client.snap | 44 +- .../qwik_core__test__example_spread_jsx.snap | 24 +- ...core__test__hoisted_fn_signal_in_loop.snap | 96 ++- .../qwik_core__test__issue_5008.snap | 45 +- ...test__root_level_self_referential_qrl.snap | 24 +- ...oot_level_self_referential_qrl_inline.snap | 24 +- ...act_multiple_qrls_with_item_and_index.snap | 24 +- ...s_with_item_and_index_and_capture_ref.snap | 24 +- ...core__test__should_extract_single_qrl.snap | 24 +- ...re__test__should_extract_single_qrl_2.snap | 44 +- ..._should_extract_single_qrl_with_index.snap | 24 +- ...act_single_qrl_with_nested_components.snap | 24 +- ...d_to_iteration_variables_to_var_props.snap | 24 +- ..._not_transform_events_on_non_elements.snap | 24 +- ...oped_variables_and_item_index_in_loop.snap | 24 +- ...nsform_block_scoped_variables_in_loop.snap | 24 +- ...nsform_component_with_normal_function.snap | 24 +- ...capturing_cross_scope_in_nested_loops.snap | 44 +- ...tiple_handler_with_different_captures.snap | 24 +- ...oped_variables_and_item_index_in_loop.snap | 26 +- ...ltiple_block_scoped_variables_in_loop.snap | 26 +- ...uld_transform_multiple_event_handlers.snap | 26 +- ...ansform_multiple_event_handlers_case2.snap | 26 +- ...__test__should_transform_nested_loops.snap | 46 +- ...ops_handler_captures_only_inner_scope.snap | 46 +- ...one_handler_with_captures_one_without.snap | 26 +- ...ted_loops_handler_captures_outer_only.snap | 66 +- ...pturing_different_block_scope_in_loop.snap | 26 +- ...shot_map_non_candidate_has_no_warning.snap | 62 ++ ...__snapshot_map_to_each_component_root.snap | 181 +++++ ...pshot_map_to_each_nested_control_flow.snap | 158 ++++ ...st__snapshot_map_to_each_shared_alias.snap | 146 ++++ ...re__test__snapshot_map_to_each_simple.snap | 137 ++++ ...t_map_to_each_with_item_click_handler.snap | 176 +++++ ...napshot_map_to_each_with_loop_slicing.snap | 151 ++++ ...apshot_map_to_each_with_outer_capture.snap | 153 ++++ ...hot_map_to_each_without_jsx_transpile.snap | 126 ++++ ..._warn_when_map_callback_returns_array.snap | 88 +++ ...st__snapshot_warn_when_map_has_no_key.snap | 82 +++ ...hot_warn_when_map_key_comes_from_call.snap | 86 +++ ..._when_map_key_uses_second_param_alias.snap | 88 +++ packages/optimizer/core/src/test.rs | 559 +++++++++++++++ packages/optimizer/core/src/transform.rs | 10 + .../core/src/transform/each_transform.rs | 677 ++++++++++++++++++ packages/optimizer/core/src/utils.rs | 6 +- 49 files changed, 3823 insertions(+), 85 deletions(-) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_non_candidate_has_no_warning.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_component_root.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_nested_control_flow.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_shared_alias.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_simple.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_item_click_handler.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_loop_slicing.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_without_jsx_transpile.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_callback_returns_array.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_has_no_key.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_comes_from_call.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_key_uses_second_param_alias.snap create mode 100644 packages/optimizer/core/src/transform/each_transform.rs 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..0d26e2ff7a9 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": null, + "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": null, + "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..bc5c4147db9 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": null, + "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..b400030d406 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": null, + "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..caa591ffe4e 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": null, + "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..d4d39af3b63 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": null, + "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..42ae2b8b46e 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": null, + "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..43f3b31762a 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": null, + "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": null, + "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..a33ffa6b2c1 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": null, + "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..e5063ffa227 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": null, + "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..3ed62a10720 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": null, + "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..95fd1baa510 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": null, + "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..a1779c91ee5 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": null, + "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..cf5e6a0f933 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": null, + "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..bc0aa003131 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": null, + "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..759ab8b3394 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": null, + "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": null, + "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..fad8b61d5dd 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": null, + "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..8dfbb27b3b3 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": null, + "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..e511ae24c7f 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": null, + "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..7dc81efd2ba 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": null, + "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..1a8920e2f68 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": null, + "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..2f2193a7e9e 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": null, + "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": null, + "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..6006b780b1a 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": null, + "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": null, + "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..f7494326800 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": null, + "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..79ea1622e7a 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": null, + "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": null, + "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": null, + "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..69aab953fcb 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": null, + "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_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_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_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_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..bcfba0cd2d7 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap @@ -0,0 +1,153 @@ +--- +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' }]; + 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_App_component_div_Each_item_f829DjfZibU.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 App_component_div_Each_item_f829DjfZibU = (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;;wDAAtD;;yBAAS,WAAC\"}") +/* +{ + "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": true, + "loc": [ + 0, + 0 + ], + "paramNames": [ + "item" + ], + "captureNames": [ + "extra" + ] +} +*/ +============================= 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' + } + ]; + const extra = { + suffix: '!' + }; + return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { + item$: q_App_component_div_Each_item_f829DjfZibU.w([ + extra + ]), + items: items + }, { + key$: q_App_component_div_Each_key_07Oo5ReT59I + }, null, 2, 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,MAAM,QAAQ;QAAE,QAAQ;IAAI;IAC5B,qBAAO,WAAC,mBAAyB;;;;eAApB;;;;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, + 260 + ] +} +*/ +============================= 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\":\"uDAM0B,OAAoB,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_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..0f548dd43ba --- /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": null, + "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..592c818f911 --- /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": null, + "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..3664820ed5d --- /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": null, + "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..c11ce9e27d8 --- /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": null, + "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..c45b217313e 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,564 @@ 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("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_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_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.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 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_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_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..dd06db91ce3 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; @@ -42,6 +43,8 @@ macro_rules! id_eq { }; } +mod each_transform; + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum SegmentKind { @@ -105,6 +108,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, @@ -256,6 +260,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(), @@ -3928,6 +3933,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..af130f1655e --- /dev/null +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -0,0 +1,677 @@ +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EachCandidateWarning { + MissingKey, + UsesSecondParamForKey, + CallDerivedKey, + NotSingleJsxNode, + 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.", + ), + EachCandidateWarning::UnsafeSlice => ( + "This .map() was not optimized to Each because the callback body could not be safely sliced.", + "Simplify the callback body so the key and rendered item can be derived independently.", + ), + }; + self.diagnostics.push(Diagnostic { + category: DiagnosticCategory::Warning, + code: None, + 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 { + let ast::Callee::Expr(box ast::Expr::Member(member)) = &node.callee else { + 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; + } + + 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(warning) => { + self.push_each_candidate_warning(node.span, warning); + return None; + } + }; + build_each_arrow_expr(&callback_info.params, Some(key_stmts), key_expr.clone()) + } + None => build_each_arrow_expr(&callback_info.params, None, key_expr.clone()), + }; + + 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(warning) => { + self.push_each_candidate_warning(node.span, warning); + return None; + } + }; + build_each_arrow_expr(&callback_info.params, Some(item_stmts), item_expr.clone()) + } + None => build_each_arrow_expr(&callback_info.params, None, item_expr.clone()), + }; + + 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, EachCandidateWarning> { + // 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; + } + if is_key_slice { + if stmt_contains_call(stmt) || stmt_uses_any_ident(stmt, second_param_ids) { + return Err(if stmt_contains_call(stmt) { + EachCandidateWarning::CallDerivedKey + } else { + EachCandidateWarning::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(EachCandidateWarning::UnsafeSlice); + } + + selected.reverse(); + Ok(selected) +} + +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() + }) +} 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, From d128ddfbdc3ad5dcd5c6e2da5b9c727798233685 Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 13:29:03 +0100 Subject: [PATCH 2/9] feat: add support for disabling map-to-each optimization via comments --- ...nent_with_event_listeners_inside_loop.snap | 4 +- ...ple_derived_signals_complext_children.snap | 2 +- ..._test__example_functional_component_2.snap | 2 +- ...ore__test__example_qwik_router_client.snap | 4 +- .../qwik_core__test__example_spread_jsx.snap | 2 +- .../qwik_core__test__issue_5008.snap | 4 +- ...test__root_level_self_referential_qrl.snap | 2 +- ...oot_level_self_referential_qrl_inline.snap | 2 +- ...act_multiple_qrls_with_item_and_index.snap | 2 +- ...s_with_item_and_index_and_capture_ref.snap | 2 +- ...core__test__should_extract_single_qrl.snap | 2 +- ...re__test__should_extract_single_qrl_2.snap | 4 +- ..._should_extract_single_qrl_with_index.snap | 2 +- ...act_single_qrl_with_nested_components.snap | 2 +- ...d_to_iteration_variables_to_var_props.snap | 2 +- ..._not_transform_events_on_non_elements.snap | 2 +- ...oped_variables_and_item_index_in_loop.snap | 2 +- ...nsform_block_scoped_variables_in_loop.snap | 2 +- ...nsform_component_with_normal_function.snap | 2 +- ...capturing_cross_scope_in_nested_loops.snap | 4 +- ...tiple_handler_with_different_captures.snap | 2 +- ...oped_variables_and_item_index_in_loop.snap | 2 +- ...ltiple_block_scoped_variables_in_loop.snap | 2 +- ...uld_transform_multiple_event_handlers.snap | 2 +- ...ansform_multiple_event_handlers_case2.snap | 2 +- ...__test__should_transform_nested_loops.snap | 4 +- ...ops_handler_captures_only_inner_scope.snap | 4 +- ...one_handler_with_captures_one_without.snap | 2 +- ...ted_loops_handler_captures_outer_only.snap | 6 +- ...pturing_different_block_scope_in_loop.snap | 2 +- ...pshot_map_to_each_disabled_by_comment.snap | 69 +++++++ ..._each_disabled_by_sibling_jsx_comment.snap | 69 +++++++ ...p_to_each_warning_disabled_by_comment.snap | 65 +++++++ ..._warn_when_map_callback_returns_array.snap | 2 +- ...st__snapshot_warn_when_map_has_no_key.snap | 2 +- ...hot_warn_when_map_key_comes_from_call.snap | 2 +- ..._when_map_key_uses_second_param_alias.snap | 2 +- packages/optimizer/core/src/test.rs | 180 ++++++++++++++++++ packages/optimizer/core/src/transform.rs | 170 ++++++++++++++++- .../core/src/transform/each_transform.rs | 22 ++- 40 files changed, 604 insertions(+), 57 deletions(-) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_comment.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_disabled_by_sibling_jsx_comment.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_warning_disabled_by_comment.snap diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap index b6304978df0..6917ec1caba 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap @@ -488,7 +488,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -508,7 +508,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap index 60c45162328..2f42fefd3df 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_derived_signals_complext_children.snap @@ -56,7 +56,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_functional_component_2.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_functional_component_2.snap index 34dc8204575..d3b34ad2169 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_functional_component_2.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_functional_component_2.snap @@ -193,7 +193,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap index c3879877af6..868e1537b90 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap @@ -4217,7 +4217,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind [ { "category": "warning", - "code": null, + "code": "map-to-each", "file": "../node_modules/@qwik.dev/router/index.qwik.mjs", "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", "highlights": [ @@ -4237,7 +4237,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind }, { "category": "warning", - "code": null, + "code": "map-to-each", "file": "../node_modules/@qwik.dev/router/index.qwik.mjs", "message": "This .map() was not optimized to Each because the returned JSX node is missing a key.", "highlights": [ diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_spread_jsx.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_spread_jsx.snap index 4034b7116b2..e8fcc144a11 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_spread_jsx.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_spread_jsx.snap @@ -122,7 +122,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 0d26e2ff7a9..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 @@ -127,7 +127,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -147,7 +147,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 bc5c4147db9..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 @@ -74,7 +74,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 b400030d406..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 @@ -64,7 +64,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/node_modules/qwik-tree/index. [ { "category": "warning", - "code": null, + "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": [ 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 caa591ffe4e..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 @@ -144,7 +144,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 d4d39af3b63..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 @@ -172,7 +172,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 42ae2b8b46e..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 @@ -227,7 +227,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 43f3b31762a..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 @@ -181,7 +181,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -201,7 +201,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 a33ffa6b2c1..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 @@ -237,7 +237,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 e5063ffa227..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 @@ -147,7 +147,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 3ed62a10720..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 @@ -107,7 +107,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 95fd1baa510..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 @@ -127,7 +127,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 a1779c91ee5..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 @@ -116,7 +116,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 cf5e6a0f933..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 @@ -110,7 +110,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 bc0aa003131..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 @@ -147,7 +147,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 759ab8b3394..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 @@ -197,7 +197,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -217,7 +217,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 fad8b61d5dd..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 @@ -162,7 +162,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 8dfbb27b3b3..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 @@ -120,7 +120,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 e511ae24c7f..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 @@ -116,7 +116,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 7dc81efd2ba..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 @@ -170,7 +170,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 1a8920e2f68..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 @@ -174,7 +174,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 2f2193a7e9e..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 @@ -166,7 +166,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -186,7 +186,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 6006b780b1a..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 @@ -124,7 +124,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -144,7 +144,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 f7494326800..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 @@ -150,7 +150,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 79ea1622e7a..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 @@ -148,7 +148,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ @@ -168,7 +168,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ @@ -188,7 +188,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma }, { "category": "warning", - "code": null, + "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": [ 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 69aab953fcb..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 @@ -155,7 +155,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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_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_warn_when_map_callback_returns_array.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_warn_when_map_callback_returns_array.snap index 0f548dd43ba..26593f901a9 100644 --- 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 @@ -67,7 +67,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 index 592c818f911..2f5cb682d58 100644 --- 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 @@ -61,7 +61,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 index 3664820ed5d..3ac1dc25160 100644 --- 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 @@ -65,7 +65,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ 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 index c11ce9e27d8..d7092a476aa 100644 --- 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 @@ -67,7 +67,7 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma [ { "category": "warning", - "code": null, + "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": [ diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index c45b217313e..44c6aec6636 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7208,6 +7208,122 @@ export const App = component$(() => { ); } +#[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 should_warn_when_map_key_uses_second_param() { let res = test_input!(TestInput { @@ -7233,6 +7349,7 @@ export const App = component$(() => { .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 @@ -7421,6 +7538,69 @@ export const App = component$(() => { }); } +#[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 { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index dd06db91ce3..b8448619c16 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -20,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}; @@ -45,6 +45,8 @@ 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 { @@ -152,6 +154,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> { @@ -302,11 +306,12 @@ impl<'a> QwikTransform<'a> { in_callback: false, const_initializers: HashMap::new(), ref_assignments: Vec::new(), - hoisted_segment_idents: HashSet::new(), - pending_expr_replacement: None, - options, + hoisted_segment_idents: HashSet::new(), + pending_expr_replacement: None, + jsx_disabled_rules_by_pos: HashMap::new(), + options, + } } - } const fn is_inline(&self) -> bool { matches!( @@ -315,6 +320,116 @@ 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(); + } + child.fold_with(self) + } + }, + 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 get_dev_location(&self, span: Span) -> ast::ExprOrSpread { let loc = self.options.cm.lookup_char_pos(span.lo); let file_name = self @@ -3312,6 +3427,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!(); @@ -3834,14 +3977,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 { diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index af130f1655e..6b7abfc1463 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -1,5 +1,7 @@ use super::*; +const MAP_TO_EACH_DIRECTIVE: &str = "map-to-each"; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum EachCandidateWarning { MissingKey, @@ -53,12 +55,12 @@ impl<'a> QwikTransform<'a> { "Simplify the callback body so the key and rendered item can be derived independently.", ), }; - self.diagnostics.push(Diagnostic { - category: DiagnosticCategory::Warning, - code: None, - file: self - .options - .path_data + 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() @@ -141,6 +143,14 @@ impl<'a> QwikTransform<'a> { 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; } From b29618e8e40b32ffd7f41b459dff16fbd06e392b Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 15:40:33 +0100 Subject: [PATCH 3/9] feat: enhance map-to-each optimization with local function handling and disable comments --- package.json | 4 +- ...o_each_skips_local_function_component.snap | 91 +++++++++++++++++++ packages/optimizer/core/src/test.rs | 21 +++++ packages/optimizer/core/src/transform.rs | 17 ++++ .../core/src/transform/each_transform.rs | 38 ++++++-- packages/qwik-vite/src/plugins/rollup.ts | 1 + packages/qwik/src/core/control-flow/each.ts | 12 ++- .../qwik/src/core/tests/component.spec.tsx | 3 + packages/qwik/src/core/tests/each.spec.tsx | 49 ++++++++++ .../qwik/src/core/tests/use-computed.spec.tsx | 1 + 10 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_local_function_component.snap 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/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..d578d24e1df --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_local_function_component.snap @@ -0,0 +1,91 @@ +--- +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_App_component_ckEPmXZlub0.js (ENTRY POINT)== + +import { _jsxSorted } from "@qwik.dev/core"; +import { _wrapProp } from "@qwik.dev/core"; +// +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, [ + 'all', + 'active' + ].map((filter)=>/*#__PURE__*/ _jsxSorted(Filter, { + filter: filter + }, null, null, 3, 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,kBAAI;QAAC;QAAO;KAAS,CAAC,GAAG,CAAC,CAAC,uBAAW,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 + ] +} +*/ +== DIAGNOSTICS == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the generated Each callback would capture a local function or class.", + "highlights": [ + { + "lo": 188, + "hi": 262, + "startLine": 9, + "startCol": 15, + "endLine": 9, + "endCol": 88 + } + ], + "suggestions": [ + "Move the referenced function or component out of the parent scope, or keep the original .map() render loop." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index 44c6aec6636..016c64bebb7 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7324,6 +7324,27 @@ export const App = component$(() => { ); } +#[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 should_warn_when_map_key_uses_second_param() { let res = test_input!(TestInput { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index b8448619c16..c60981b3b95 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -430,6 +430,23 @@ impl<'a> QwikTransform<'a> { .collect() } + fn has_invalid_qrl_function_reference(&self, expr: &ast::Expr) -> bool { + let descendent_idents = { + let mut collector = IdentCollector::new(); + expr.visit_with(&mut collector); + collector.get_words() + }; + let (_decl_collect, invalid_decl): (_, Vec<_>) = self + .decl_stack + .iter() + .flat_map(|v| v.iter()) + .cloned() + .partition(|(_, t)| matches!(t, IdentType::Var(_))); + descendent_idents + .iter() + .any(|ident| invalid_decl.iter().any(|entry| entry.0 == *ident)) + } + fn get_dev_location(&self, span: Span) -> ast::ExprOrSpread { let loc = self.options.cm.lookup_char_pos(span.lo); let file_name = self diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index 6b7abfc1463..7d5b572ea43 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -9,6 +9,7 @@ enum EachCandidateWarning { CallDerivedKey, NotSingleJsxNode, UnsafeSlice, + LocalFunctionReference, } #[derive(Clone)] @@ -54,17 +55,21 @@ impl<'a> QwikTransform<'a> { "This .map() was not optimized to Each because the callback body could not be safely sliced.", "Simplify the callback body so the key and rendered item can be derived independently.", ), + EachCandidateWarning::LocalFunctionReference => ( + "This .map() was not optimized to Each because the generated Each callback would capture a local function or class.", + "Move the referenced function or component out of the parent scope, or keep the original .map() render loop.", + ), }; - 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(), + 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()]), @@ -237,6 +242,19 @@ impl<'a> QwikTransform<'a> { None => build_each_arrow_expr(&callback_info.params, None, item_expr.clone()), }; + // `item$` / `key$` become QRL-backed JSX props. If either generated callback would + // capture a local function or class declaration from the parent scope, keep the + // original `.map()` so we don't surface a later FunctionReference optimizer error. + if self.has_invalid_qrl_function_reference(&key_fn_expr) + || self.has_invalid_qrl_function_reference(&item_fn_expr) + { + self.push_each_candidate_warning( + directive_span, + EachCandidateWarning::LocalFunctionReference, + ); + return None; + } + let each_id = self.ensure_core_import(&Atom::from("Each")); let replacement = if raw_mode { build_each_jsx_element( 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..aa7c4b358fd 100644 --- a/packages/qwik/src/core/control-flow/each.ts +++ b/packages/qwik/src/core/control-flow/each.ts @@ -11,6 +11,9 @@ 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'; export interface EachProps { items: readonly T[]; @@ -27,8 +30,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$!; 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..0cd9a5478ec 100644 --- a/packages/qwik/src/core/tests/each.spec.tsx +++ b/packages/qwik/src/core/tests/each.spec.tsx @@ -278,6 +278,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) => ( ))} From 4a6d7b79409e0639178e85b655e9f7dd4f33f78d Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 16:02:25 +0100 Subject: [PATCH 4/9] docs: update Each documentation and menu reference for clarity and optimization guidance --- .../routes/docs/(qwik)/core/each/index.mdx | 36 ++++--------- .../docs/(qwik)/core/rendering/index.mdx | 53 ++++++++++++++++++- packages/docs/src/routes/docs/menu.md | 2 +- .../core/src/transform/each_transform.rs | 24 ++++----- 4 files changed, 75 insertions(+), 40 deletions(-) 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..59385a9f5a3 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. + +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 -
      - {todos.value.map((todo) => ( -
    • {todo.label}
    • - ))} -
    -``` +## 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..abdc0926bb8 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,55 @@ 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 an internal [`Each`](../each/index.mdx)-style keyed list 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 to the same keyed-list machinery used by manual `Each`. + +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/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index 7d5b572ea43..ff96bb32696 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -220,9 +220,9 @@ impl<'a> QwikTransform<'a> { return None; } }; - build_each_arrow_expr(&callback_info.params, Some(key_stmts), key_expr.clone()) + build_each_arrow_expr(&callback_info.params, Some(key_stmts), key_expr) } - None => build_each_arrow_expr(&callback_info.params, None, key_expr.clone()), + None => build_each_arrow_expr(&callback_info.params, None, key_expr), }; let item_fn_expr = match stmts { @@ -237,9 +237,9 @@ impl<'a> QwikTransform<'a> { return None; } }; - build_each_arrow_expr(&callback_info.params, Some(item_stmts), item_expr.clone()) + build_each_arrow_expr(&callback_info.params, Some(item_stmts), item_expr) } - None => build_each_arrow_expr(&callback_info.params, None, item_expr.clone()), + None => build_each_arrow_expr(&callback_info.params, None, item_expr), }; // `item$` / `key$` become QRL-backed JSX props. If either generated callback would @@ -480,14 +480,14 @@ fn slice_statements_for_target( if defines.is_empty() || defines.is_disjoint(&needed) { continue; } - if is_key_slice { - if stmt_contains_call(stmt) || stmt_uses_any_ident(stmt, second_param_ids) { - return Err(if stmt_contains_call(stmt) { - EachCandidateWarning::CallDerivedKey - } else { - EachCandidateWarning::UsesSecondParamForKey - }); - } + 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 { + EachCandidateWarning::CallDerivedKey + } else { + EachCandidateWarning::UsesSecondParamForKey + }); } let mut uses = collect_stmt_used_idents(stmt); From ef8a2aa36713b8fcd6895f3fc29dfe2b353c3ec8 Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 21:55:47 +0100 Subject: [PATCH 5/9] fix: windows optimizer build --- scripts/submodule-optimizer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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); } From 795c51fb0ec3bce112f5b9b21fa31b4b43526666 Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 22:05:31 +0100 Subject: [PATCH 6/9] fix(optimizer): only rewrite map-to-Each inside JSX children --- ...nent_with_event_listeners_inside_loop.snap | 21 +----- ...side_normal_function_is_not_rewritten.snap | 68 +++++++++++++++++++ ...outside_jsx_children_is_not_rewritten.snap | 68 +++++++++++++++++++ ...ithout_jsx_transpile_is_not_rewritten.snap | 65 ++++++++++++++++++ packages/optimizer/core/src/test.rs | 66 ++++++++++++++++-- packages/optimizer/core/src/transform.rs | 43 ++++++++---- .../core/src/transform/each_transform.rs | 43 +++++++----- 7 files changed, 319 insertions(+), 55 deletions(-) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_inside_normal_function_is_not_rewritten.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_is_not_rewritten.snap create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_outside_jsx_children_without_jsx_transpile_is_not_rewritten.snap diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap index 6917ec1caba..98ae1f82854 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_component_with_event_listeners_inside_loop.snap @@ -1,5 +1,6 @@ --- source: packages/optimizer/core/src/test.rs +assertion_line: 3979 expression: output --- ==INPUT== @@ -486,26 +487,6 @@ 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": 257, - "hi": 432, - "startLine": 7, - "startCol": 16, - "endLine": 15, - "endCol": 10 - } - ], - "suggestions": [ - "Add a stable key to the returned JSX node." - ], - "scope": "optimizer" - }, { "category": "warning", "code": "map-to-each", 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_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/test.rs b/packages/optimizer/core/src/test.rs index 016c64bebb7..7d13a3848f0 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7043,14 +7043,12 @@ export const App = component$(() => { let combined_code = combined_modules_code(&output); assert!( - combined_code.contains("_jsxSorted(Each") - || combined_code.contains("_jsxSplit(Each"), + 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=>"), + !combined_code.contains(".map((item)=>") && !combined_code.contains(".map(item=>"), "Expected map callback to be rewritten.\n{}", combined_code ); @@ -7208,6 +7206,66 @@ export const App = component$(() => { ); } +#[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 { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index c60981b3b95..cd7973eb55a 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -135,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, @@ -295,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(), @@ -306,12 +308,12 @@ impl<'a> QwikTransform<'a> { in_callback: false, const_initializers: HashMap::new(), ref_assignments: Vec::new(), - hoisted_segment_idents: HashSet::new(), - pending_expr_replacement: None, - jsx_disabled_rules_by_pos: HashMap::new(), - options, - } + hoisted_segment_idents: HashSet::new(), + pending_expr_replacement: None, + jsx_disabled_rules_by_pos: HashMap::new(), + options, } + } const fn is_inline(&self) -> bool { matches!( @@ -402,10 +404,9 @@ impl<'a> QwikTransform<'a> { .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, - )); + pending_rules.extend( + self.disabled_rules_from_jsx_comment_container(container, empty), + ); child.fold_with(self) } ast::JSXExpr::Expr(expr) => { @@ -413,7 +414,7 @@ impl<'a> QwikTransform<'a> { self.register_jsx_disabled_rules(expr.span(), &pending_rules); pending_rules.clear(); } - child.fold_with(self) + self.fold_jsx_child_expr(child) } }, ast::JSXElementChild::JSXText(text) => { @@ -430,6 +431,20 @@ impl<'a> QwikTransform<'a> { .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 has_invalid_qrl_function_reference(&self, expr: &ast::Expr) -> bool { let descendent_idents = { let mut collector = IdentCollector::new(); @@ -2473,12 +2488,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; diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index ff96bb32696..007e936fb96 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -29,11 +29,7 @@ struct EachCallbackInfo { } impl<'a> QwikTransform<'a> { - fn push_each_candidate_warning( - &mut self, - span: Span, - warning: EachCandidateWarning, - ) { + 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.", @@ -66,10 +62,10 @@ impl<'a> QwikTransform<'a> { file: self .options .path_data - .rel_path - .to_slash_lossy() - .to_string() - .into(), + .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()]), @@ -83,9 +79,7 @@ impl<'a> QwikTransform<'a> { 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 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) => { @@ -116,9 +110,7 @@ impl<'a> QwikTransform<'a> { .iter() .map(|param| param.pat.clone()) .collect(); - let second_param_ids = params - .get(1) - .map_or_else(Vec::new, collect_ids_from_pat); + 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()?; @@ -145,6 +137,10 @@ impl<'a> QwikTransform<'a> { } pub(super) fn try_rewrite_map_to_each(&mut self, node: &ast::CallExpr) -> Option { + if self.jsx_children_expr_depth == 0 { + return None; + } + let ast::Callee::Expr(box ast::Expr::Member(member)) = &node.callee else { return None; }; @@ -180,14 +176,22 @@ impl<'a> QwikTransform<'a> { self.push_each_candidate_warning(node.span, EachCandidateWarning::MissingKey); return None; }; - (Box::new(key_expr), Box::new(ast::Expr::JSXElement(Box::new(jsx))), true) + ( + 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) + ( + 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; @@ -196,7 +200,10 @@ impl<'a> QwikTransform<'a> { }; if contains_any_ident(key_expr.as_ref(), &callback_info.second_param_ids) { - self.push_each_candidate_warning(node.span, EachCandidateWarning::UsesSecondParamForKey); + self.push_each_candidate_warning( + node.span, + EachCandidateWarning::UsesSecondParamForKey, + ); return None; } if expr_contains_call(key_expr.as_ref()) { From 7b4bf49908018e17394d31ce30fd2df30e4379d6 Mon Sep 17 00:00:00 2001 From: Varixo Date: Wed, 25 Mar 2026 22:56:18 +0100 Subject: [PATCH 7/9] fix(optimizer): skip map-to-Each for item-derived component types --- ..._to_each_skips_dynamic_item_component.snap | 97 +++++++++++++++++++ packages/optimizer/core/src/test.rs | 71 ++++++++++++++ .../core/src/transform/each_transform.rs | 89 +++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_dynamic_item_component.snap 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..d116ebb12d3 --- /dev/null +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_skips_dynamic_item_component.snap @@ -0,0 +1,97 @@ +--- +source: packages/optimizer/core/src/test.rs +assertion_line: 7408 +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 == + +[ + { + "category": "warning", + "code": "map-to-each", + "file": "test.tsx", + "message": "This .map() was not optimized to Each because the rendered component type depends on the callback item.", + "highlights": [ + { + "lo": 190, + "hi": 310, + "startLine": 6, + "startCol": 16, + "endLine": 10, + "endCol": 4 + } + ], + "suggestions": [ + "Keep the original .map() render loop, or render a statically referenced component type." + ], + "scope": "optimizer" + } +] diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index 7d13a3848f0..cda0c8e13cd 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7403,6 +7403,28 @@ export const App = component$(() => { }); } +#[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 { @@ -7497,6 +7519,55 @@ export const App = component$(() => { ); } +#[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 + .iter() + .any(|d| d.category == DiagnosticCategory::Warning + && d.code.as_deref() == Some("map-to-each") + && d.message + .contains("component type depends on the callback item")), + "Expected dynamic-component warning, got {:?}", + output.diagnostics + ); +} + #[test] fn snapshot_map_to_each_simple() { test_input!(TestInput { diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index 007e936fb96..62c6cbde944 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -10,6 +10,7 @@ enum EachCandidateWarning { NotSingleJsxNode, UnsafeSlice, LocalFunctionReference, + DynamicComponentReference, } #[derive(Clone)] @@ -55,6 +56,10 @@ impl<'a> QwikTransform<'a> { "This .map() was not optimized to Each because the generated Each callback would capture a local function or class.", "Move the referenced function or component out of the parent scope, or keep the original .map() render loop.", ), + EachCandidateWarning::DynamicComponentReference => ( + "This .map() was not optimized to Each because the rendered component type depends on the callback item.", + "Keep the original .map() render loop, or render a statically referenced component type.", + ), }; self.diagnostics.push(Diagnostic { category: DiagnosticCategory::Warning, @@ -210,6 +215,17 @@ impl<'a> QwikTransform<'a> { 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, + ) { + self.push_each_candidate_warning( + node.span, + EachCandidateWarning::DynamicComponentReference, + ); + return None; + } let key_fn_expr = match stmts { Some(stmts) => { @@ -710,3 +726,76 @@ fn build_each_transpiled_call( ..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), + } +} From 7da9d143ddcc0d24cf975f56ba6087123d7817b1 Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 26 Mar 2026 16:09:10 +0100 Subject: [PATCH 8/9] feat: introduce _map helper --- .../routes/docs/(qwik)/core/each/index.mdx | 2 +- .../docs/(qwik)/core/rendering/index.mdx | 8 +- ..._to_each_skips_dynamic_item_component.snap | 24 +-- ...o_each_skips_local_function_component.snap | 109 ++++++++--- ...snapshot_map_to_each_with_key_capture.snap | 146 ++++++++++++++ ...apshot_map_to_each_with_outer_capture.snap | 66 ++++--- packages/optimizer/core/src/test.rs | 68 ++++++- packages/optimizer/core/src/transform.rs | 178 +++++++++++++++++- .../core/src/transform/each_transform.rs | 113 +++++++---- packages/optimizer/core/src/words.rs | 1 + packages/qwik/src/core/control-flow/each.ts | 31 ++- packages/qwik/src/core/index.ts | 2 +- packages/qwik/src/core/qwik.core.api.md | 3 + packages/qwik/src/core/tests/each.spec.tsx | 80 +++++++- 14 files changed, 688 insertions(+), 143 deletions(-) create mode 100644 packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_key_capture.snap 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 59385a9f5a3..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,7 @@ import CodeSandbox from '../../../../../components/code-sandbox/index.tsx'; `Each` is a built-in Qwik component for rendering keyed lists. -For most list rendering, write normal `items.map()` code and let Qwik optimize compatible keyed loops automatically. +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). 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 abdc0926bb8..f44ffa9f276 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx @@ -124,7 +124,7 @@ export const Parent = component$(() => { ## Automatic `.map()` optimization -For compatible render loops, Qwik optimizes keyed `items.map()` calls into an internal [`Each`](../each/index.mdx)-style keyed list automatically during compilation. +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: @@ -141,7 +141,11 @@ For example, this authoring pattern: ))} ``` -can be optimized to the same keyed-list machinery used by manual `Each`. +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. 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 index d116ebb12d3..8edad67abc8 100644 --- 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 @@ -1,6 +1,5 @@ --- source: packages/optimizer/core/src/test.rs -assertion_line: 7408 expression: output --- ==INPUT== @@ -73,25 +72,4 @@ 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 rendered component type depends on the callback item.", - "highlights": [ - { - "lo": 190, - "hi": 310, - "startLine": 6, - "startCol": 16, - "endLine": 10, - "endCol": 4 - } - ], - "suggestions": [ - "Keep the original .map() render loop, or render a statically referenced component type." - ], - "scope": "optimizer" - } -] +[] 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 index d578d24e1df..76744794472 100644 --- 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 @@ -26,25 +26,69 @@ 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, [ + return /*#__PURE__*/ _jsxSorted("ul", null, null, /*#__PURE__*/ _map([ 'all', 'active' - ].map((filter)=>/*#__PURE__*/ _jsxSorted(Filter, { + ], (filter)=>_jsxSorted(Filter, { filter: filter - }, null, null, 3, filter)), 1, "u6_1"); + }, 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,kBAAI;QAAC;QAAO;KAAS,CAAC,GAAG,CAAC,CAAC,uBAAW,WAAC;YAAO,QAAQ;0BAAa;AAC5E\"}") +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", @@ -65,27 +109,40 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma ] } */ +============================= 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 == -[ - { - "category": "warning", - "code": "map-to-each", - "file": "test.tsx", - "message": "This .map() was not optimized to Each because the generated Each callback would capture a local function or class.", - "highlights": [ - { - "lo": 188, - "hi": 262, - "startLine": 9, - "startCol": 15, - "endLine": 9, - "endCol": 88 - } - ], - "suggestions": [ - "Move the referenced function or component out of the parent scope, or keep the original .map() render loop." - ], - "scope": "optimizer" - } -] +[] 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_outer_capture.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__snapshot_map_to_each_with_outer_capture.snap index bcfba0cd2d7..7af7ae3bb6a 100644 --- 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 @@ -1,5 +1,5 @@ --- -source: packages/qwik/src/optimizer/core/src/test.rs +source: packages/optimizer/core/src/test.rs expression: output --- ==INPUT== @@ -24,7 +24,7 @@ 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_div_Each_item_f829DjfZibU.js (ENTRY POINT)== +============================= test.tsx_map_item_nf66XgXHgFs.js (ENTRY POINT)== import { _captures } from "@qwik.dev/core"; import { _fnSignal } from "@qwik.dev/core"; @@ -32,7 +32,7 @@ import { _jsxSorted } from "@qwik.dev/core"; // const _hf0 = (p0, p1)=>p1.text + p0.suffix; const _hf0_str = "p1.text+p0.suffix"; -export const App_component_div_Each_item_f829DjfZibU = (item)=>{ +export const map_item_nf66XgXHgFs = (item)=>{ const extra = _captures[0]; return /*#__PURE__*/ _jsxSorted("span", null, null, _fnSignal(_hf0, [ extra, @@ -41,41 +41,42 @@ export const App_component_div_Each_item_f829DjfZibU = (item)=>{ }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;uBAMwD,GAAK,IAAI,GAAG,GAAM,MAAM;;wDAAtD;;yBAAS,WAAC\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;uBAMwD,GAAK,IAAI,GAAG,GAAM,MAAM;;qCAAtD;;yBAAS,WAAC\"}") /* { "origin": "test.tsx", - "name": "App_component_div_Each_item_f829DjfZibU", + "name": "map_item_nf66XgXHgFs", "entry": null, - "displayName": "test.tsx_App_component_div_Each_item", - "hash": "f829DjfZibU", - "canonicalFilename": "test.tsx_App_component_div_Each_item_f829DjfZibU", + "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": true, + "captures": false, "loc": [ 0, 0 ], "paramNames": [ "item" - ], - "captureNames": [ - "extra" ] } */ ============================= test.tsx_App_component_ckEPmXZlub0.js (ENTRY POINT)== -import { Each } from "@qwik.dev/core"; +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 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"); +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 = [ @@ -87,18 +88,16 @@ export const App_component_ckEPmXZlub0 = ()=>{ const extra = { suffix: '!' }; - return /*#__PURE__*/ _jsxSorted("div", null, null, _jsxSorted(Each, { - item$: q_App_component_div_Each_item_f829DjfZibU.w([ - extra - ]), - items: items - }, { - key$: q_App_component_div_Each_key_07Oo5ReT59I - }, null, 2, null), 1, "u6_0"); + 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\":\";;;;;;;yCAG8B;IAC5B,MAAM,QAAQ;QAAC;YAAE,IAAI;YAAK,MAAM;QAAI;KAAE;IACtC,MAAM,QAAQ;QAAE,QAAQ;IAAI;IAC5B,qBAAO,WAAC,mBAAyB;;;;eAApB;;;;AACf\"}") +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", @@ -119,20 +118,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma ] } */ -============================= test.tsx_App_component_div_Each_key_07Oo5ReT59I.js (ENTRY POINT)== +============================= test.tsx_map_key_bn60zrX6S0A.js (ENTRY POINT)== -export const App_component_div_Each_key_07Oo5ReT59I = (item)=>item.id; +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\":\"uDAM0B,OAAoB,KAAK,EAAE\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;oCAM0B;;WAAoB,KAAK,EAAE\"}") /* { "origin": "test.tsx", - "name": "App_component_div_Each_key_07Oo5ReT59I", + "name": "map_key_bn60zrX6S0A", "entry": null, - "displayName": "test.tsx_App_component_div_Each_key", - "hash": "07Oo5ReT59I", - "canonicalFilename": "test.tsx_App_component_div_Each_key_07Oo5ReT59I", + "displayName": "test.tsx_map_key", + "hash": "bn60zrX6S0A", + "canonicalFilename": "test.tsx_map_key_bn60zrX6S0A", "path": "", "extension": "js", "parent": "App_component_ckEPmXZlub0", diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index cda0c8e13cd..697746d93f3 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7157,8 +7157,43 @@ export const App = component$(() => { let combined_code = combined_modules_code(&output); assert!( - combined_code.contains("Each"), - "Expected Each render in generated output.\n{}", + 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!( @@ -7556,14 +7591,8 @@ export const App = component$(() => { combined_code ); assert!( - output - .diagnostics - .iter() - .any(|d| d.category == DiagnosticCategory::Warning - && d.code.as_deref() == Some("map-to-each") - && d.message - .contains("component type depends on the callback item")), - "Expected dynamic-component warning, got {:?}", + output.diagnostics.is_empty(), + "Did not expect warnings for dynamic component bailout: {:?}", output.diagnostics ); } @@ -7666,6 +7695,25 @@ export const App = component$(() => { }); } +#[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 { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index cd7973eb55a..37038f3a949 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -445,21 +445,115 @@ impl<'a> QwikTransform<'a> { folded } - fn has_invalid_qrl_function_reference(&self, expr: &ast::Expr) -> bool { + 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, invalid_decl): (_, Vec<_>) = self - .decl_stack - .iter() - .flat_map(|v| v.iter()) - .cloned() - .partition(|(_, t)| matches!(t, IdentType::Var(_))); - descendent_idents - .iter() - .any(|ident| invalid_decl.iter().any(|entry| entry.0 == *ident)) + 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 + }; + + if self.is_inline() || matches!(self.options.mode, EmitMode::Lib) { + ast::Expr::Call(self.create_inline_qrl_without_capture_array( + segment_data, + folded, + symbol_name, + span, + )) + } else { + ast::Expr::Call(self.create_segment( + segment_data, + folded, + symbol_name, + span, + segment_hash, + )) + } } fn get_dev_location(&self, span: Span) -> ast::ExprOrSpread { @@ -2178,6 +2272,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, diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index 62c6cbde944..2d88a952447 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -8,9 +8,13 @@ enum EachCandidateWarning { UsesSecondParamForKey, CallDerivedKey, NotSingleJsxNode, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EachSliceError { + UsesSecondParamForKey, + CallDerivedKey, UnsafeSlice, - LocalFunctionReference, - DynamicComponentReference, } #[derive(Clone)] @@ -48,18 +52,6 @@ impl<'a> QwikTransform<'a> { "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.", ), - EachCandidateWarning::UnsafeSlice => ( - "This .map() was not optimized to Each because the callback body could not be safely sliced.", - "Simplify the callback body so the key and rendered item can be derived independently.", - ), - EachCandidateWarning::LocalFunctionReference => ( - "This .map() was not optimized to Each because the generated Each callback would capture a local function or class.", - "Move the referenced function or component out of the parent scope, or keep the original .map() render loop.", - ), - EachCandidateWarning::DynamicComponentReference => ( - "This .map() was not optimized to Each because the rendered component type depends on the callback item.", - "Keep the original .map() render loop, or render a statically referenced component type.", - ), }; self.diagnostics.push(Diagnostic { category: DiagnosticCategory::Warning, @@ -220,10 +212,6 @@ impl<'a> QwikTransform<'a> { &callback_info.params, &self.jsx_functions, ) { - self.push_each_candidate_warning( - node.span, - EachCandidateWarning::DynamicComponentReference, - ); return None; } @@ -238,8 +226,10 @@ impl<'a> QwikTransform<'a> { true, ) { Ok(stmts) => stmts, - Err(warning) => { - self.push_each_candidate_warning(node.span, warning); + Err(err) => { + if let Some(warning) = slice_error_to_warning(err) { + self.push_each_candidate_warning(node.span, warning); + } return None; } }; @@ -255,8 +245,10 @@ impl<'a> QwikTransform<'a> { let item_stmts = match slice_statements_for_target(stmts, item_expr.as_ref(), &[], false) { Ok(stmts) => stmts, - Err(warning) => { - self.push_each_candidate_warning(node.span, warning); + Err(err) => { + if let Some(warning) = slice_error_to_warning(err) { + self.push_each_candidate_warning(node.span, warning); + } return None; } }; @@ -265,17 +257,58 @@ impl<'a> QwikTransform<'a> { None => build_each_arrow_expr(&callback_info.params, None, item_expr), }; - // `item$` / `key$` become QRL-backed JSX props. If either generated callback would - // capture a local function or class declaration from the parent scope, keep the - // original `.map()` so we don't surface a later FunctionReference optimizer error. - if self.has_invalid_qrl_function_reference(&key_fn_expr) - || self.has_invalid_qrl_function_reference(&item_fn_expr) - { - self.push_each_candidate_warning( - directive_span, - EachCandidateWarning::LocalFunctionReference, + 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(), ); - return None; + let item_qrl = self.create_deferred_capture_qsegment( + item_fn_expr, + SegmentKind::JSXProp, + Atom::from("item$"), + "map_item", + shared_scoped_idents.clone(), + ); + let key_qrl = match key_qrl { + ast::Expr::Call(call) => self.hoist_qrl_to_module_scope(call), + expr => expr, + }; + let item_qrl = match item_qrl { + ast::Expr::Call(call) => self.hoist_qrl_to_module_scope(call), + expr => expr, + }; + 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")); @@ -489,7 +522,7 @@ fn slice_statements_for_target( target_expr: &ast::Expr, second_param_ids: &[Id], is_key_slice: bool, -) -> Result, EachCandidateWarning> { +) -> 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. @@ -507,9 +540,9 @@ fn slice_statements_for_target( 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 { - EachCandidateWarning::CallDerivedKey + EachSliceError::CallDerivedKey } else { - EachCandidateWarning::UsesSecondParamForKey + EachSliceError::UsesSecondParamForKey }); } @@ -525,13 +558,21 @@ fn slice_statements_for_target( } if needed.iter().any(|id| local_declared.contains(id)) { - return Err(EachCandidateWarning::UnsafeSlice); + 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), 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/src/core/control-flow/each.ts b/packages/qwik/src/core/control-flow/each.ts index aa7c4b358fd..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'; @@ -14,6 +15,8 @@ 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[]; @@ -53,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/each.spec.tsx b/packages/qwik/src/core/tests/each.spec.tsx index 0cd9a5478ec..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$(() => { From b85623a59fd1c8d5fe8f1ab0d74d9f4de8ed1cdc Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 26 Mar 2026 16:09:56 +0100 Subject: [PATCH 9/9] fix(optimizer): skip map-to-each rewrite in hoist builds --- packages/optimizer/core/src/test.rs | 40 +++++++++++++++++++ packages/optimizer/core/src/transform.rs | 23 ++++------- .../core/src/transform/each_transform.rs | 15 ++++--- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/packages/optimizer/core/src/test.rs b/packages/optimizer/core/src/test.rs index 697746d93f3..394eb3897eb 100644 --- a/packages/optimizer/core/src/test.rs +++ b/packages/optimizer/core/src/test.rs @@ -7241,6 +7241,46 @@ export const App = component$(() => { ); } +#[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 { diff --git a/packages/optimizer/core/src/transform.rs b/packages/optimizer/core/src/transform.rs index 37038f3a949..a5c2e83b3ed 100644 --- a/packages/optimizer/core/src/transform.rs +++ b/packages/optimizer/core/src/transform.rs @@ -538,22 +538,13 @@ impl<'a> QwikTransform<'a> { folded }; - if self.is_inline() || matches!(self.options.mode, EmitMode::Lib) { - ast::Expr::Call(self.create_inline_qrl_without_capture_array( - segment_data, - folded, - symbol_name, - span, - )) - } else { - ast::Expr::Call(self.create_segment( - segment_data, - folded, - symbol_name, - span, - segment_hash, - )) - } + 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 { diff --git a/packages/optimizer/core/src/transform/each_transform.rs b/packages/optimizer/core/src/transform/each_transform.rs index 2d88a952447..cf26eec820a 100644 --- a/packages/optimizer/core/src/transform/each_transform.rs +++ b/packages/optimizer/core/src/transform/each_transform.rs @@ -137,6 +137,13 @@ impl<'a> QwikTransform<'a> { 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; @@ -284,14 +291,6 @@ impl<'a> QwikTransform<'a> { "map_item", shared_scoped_idents.clone(), ); - let key_qrl = match key_qrl { - ast::Expr::Call(call) => self.hoist_qrl_to_module_scope(call), - expr => expr, - }; - let item_qrl = match item_qrl { - ast::Expr::Call(call) => self.hoist_qrl_to_module_scope(call), - expr => expr, - }; let captures_expr = ast::Expr::Array(ast::ArrayLit { span: DUMMY_SP, elems: shared_scoped_idents