diff --git a/.github/contributing.md b/.github/contributing.md index 9c385f1bcc9..afdae671193 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -17,7 +17,11 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before ## Pull Request Guidelines -- Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. +- Vue core has two primary work branches: `main` and `minor`. + + - If your pull request is a feature that adds new API surface, it should be submitted against the `minor` branch. + + - Otherwise, it should be submitted against the `main` branch. - [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time. @@ -181,7 +185,7 @@ Shortcut for starting the SFC Playground in local dev mode. This provides the fa ### `nr dev-esm` -Builds and watches `vue/dist/vue-runtime.esm-bundler.js` with all deps inlined using esbuild. This is useful when debugging the ESM build in a reproductions that require real build setups: link `packages/vue` globally, then link it into the project being debugged. +Builds and watches `vue/dist/vue-runtime.esm-bundler.js` with all deps inlined using esbuild. This is useful when debugging the ESM build in a reproduction that requires real build setups: link `packages/vue` globally, then link it into the project being debugged. ### `nr dev-compiler` diff --git a/.github/git-branch-workflow.excalidraw b/.github/git-branch-workflow.excalidraw new file mode 100644 index 00000000000..dd9127938da --- /dev/null +++ b/.github/git-branch-workflow.excalidraw @@ -0,0 +1,1746 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "arrow", + "version": 799, + "versionNonce": 529220601, + "isDeleted": false, + "id": "Gao2krnDddLMCj468JSWD", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 860.0129225738813, + "y": 663.9911710635109, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 133.75296854079784, + "height": 149.58016791936518, + "seed": 1415631543, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "hDC6an14QljktaZCUhcPF", + "focus": 0.09950793234484598, + "gap": 1.2432497743127229 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 25.209039386719837, + 85.96948921803892 + ], + [ + 133.75296854079784, + 149.58016791936518 + ] + ] + }, + { + "type": "arrow", + "version": 563, + "versionNonce": 290881303, + "isDeleted": false, + "id": "N3wyyEU7TQ8BsOQgxCmlR", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 292.88008929085873, + "y": 660.7027503334302, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 936.9972134376155, + "height": 1.3184243543457796, + "seed": 534235417, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 936.9972134376155, + -1.3184243543457796 + ] + ] + }, + { + "type": "arrow", + "version": 302, + "versionNonce": 883286489, + "isDeleted": false, + "id": "nRDWQs5nQa37yzCWTBiXC", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 293.1231624544633, + "y": 820.6017661012943, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 790.7091601354882, + "height": 0.35284814071621895, + "seed": 515907671, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "ggogfJT7E_bbfEog7Hjnp", + "focus": -0.14000162237652433, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 790.7091601354882, + -0.35284814071621895 + ] + ] + }, + { + "type": "text", + "version": 36, + "versionNonce": 981763127, + "isDeleted": false, + "id": "ZPdMAnEUq5Jgj1W07Zqiw", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 292.0450153578305, + "y": 619.3959946602608, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 46.875, + "height": 24, + "seed": 1311694519, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "main", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "main", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "text", + "version": 94, + "versionNonce": 18759353, + "isDeleted": false, + "id": "g9IkEIfu4vA8Qkwtw01Hi", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 290.88990199912035, + "y": 779.1760596323645, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 58.59375, + "height": 24, + "seed": 329886135, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "minor", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "minor", + "lineHeight": 1.2, + "baseline": 20 + }, + { + "type": "ellipse", + "version": 50, + "versionNonce": 1442112855, + "isDeleted": false, + "id": "RrdEQ7hwgGGDPhzDnuZj1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 361.55609907891005, + "y": 649.8742329483416, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 2077639991, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 79, + "versionNonce": 1547173785, + "isDeleted": false, + "id": "Zmp49FKWxGSzKnVKomjQc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 427.3015090315691, + "y": 650.256485100784, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 372652121, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 76, + "versionNonce": 586949239, + "isDeleted": false, + "id": "UOl9nLBksM7RPdH9mzjJa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 490.9435520120701, + "y": 651.2601420343765, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 508667545, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 120, + "versionNonce": 874947705, + "isDeleted": false, + "id": "oMC55V0VO_hOXoZ1se8Kl", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 555.4481126698772, + "y": 650.7975189165487, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1914963513, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 66, + "versionNonce": 39762839, + "isDeleted": false, + "id": "DZY5DC5uVP7-U5c3ngIZ4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 622.5167031502219, + "y": 649.3743647489936, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 165914713, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 107, + "versionNonce": 1689103705, + "isDeleted": false, + "id": "Vsw6oIiTM3fQypkiCic3f", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 690.330195260967, + "y": 650.6681412649529, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 280044345, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "lwYvAs-7FTjcwxKjcx0KV", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 148, + "versionNonce": 1986194201, + "isDeleted": false, + "id": "D14w9erv_2l53mINe2nSt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 361.004283792179, + "y": 810.2809579853473, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1203257975, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 179, + "versionNonce": 1172811511, + "isDeleted": false, + "id": "6WO8xOpG0rf673b_bT0m7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 426.74969374483805, + "y": 810.6632101377896, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 2056706967, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "mE8Mu0qKfFaWPCC5vmF_f", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 173, + "versionNonce": 820518905, + "isDeleted": false, + "id": "VB9U8oH-78hf530hIb_mG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 490.391736725339, + "y": 811.6668670713822, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1149587639, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 218, + "versionNonce": 1227143191, + "isDeleted": false, + "id": "Bxv1hcS0VmxUwI0JLFH97", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 554.8962973831461, + "y": 811.2042439535543, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1864901079, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "M14Q0Uo1DBy2Ss2SOFSgW", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 167, + "versionNonce": 1387509977, + "isDeleted": false, + "id": "4v23gkfhy-hzk18YdkfLz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 621.9648878634908, + "y": 809.7810897859994, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 462671607, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "vEF1cIIYYWKm84KLKqEz3", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 200, + "versionNonce": 774085943, + "isDeleted": false, + "id": "AtEf7o4WZQn4Zxq8EN5fH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 689.7783799742359, + "y": 811.0748663019584, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1414322199, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "3heKY3vfe3-6ni4dX7Uqo", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 199, + "versionNonce": 1834563001, + "isDeleted": false, + "id": "ugDby5sBv4NKdNt8eC1sg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 762.6179978227377, + "y": 810.2986003923828, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1598537015, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 211, + "versionNonce": 407428695, + "isDeleted": false, + "id": "Fwe4F2sB_0jptOZGYsusj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 837.1081608628116, + "y": 810.859236882632, + "strokeColor": "#f08c00", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1340669527, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "M14Q0Uo1DBy2Ss2SOFSgW", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 57, + "versionNonce": 335287961, + "isDeleted": false, + "id": "mE8Mu0qKfFaWPCC5vmF_f", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 437.60867586595543, + "y": 830.4227236701945, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 0.5232394659406623, + "height": 33.25787987764363, + "seed": 482155929, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6WO8xOpG0rf673b_bT0m7", + "focus": -0.1727591064041787, + "gap": 1.046152088903881 + }, + "endBinding": { + "elementId": "JALHBtowuh3_a86loej2x", + "focus": 0.015156451076917701, + "gap": 15.586906139714472 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -0.5232394659406623, + 33.25787987764363 + ] + ] + }, + { + "type": "arrow", + "version": 59, + "versionNonce": 1248394103, + "isDeleted": false, + "id": "AI-_jSAuzesxTqwRvpk0s", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 501.2878833373983, + "y": 652.3088851192829, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "width": 0, + "height": 40.40111211199792, + "seed": 1052632343, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -40.40111211199792 + ] + ] + }, + { + "type": "arrow", + "version": 261, + "versionNonce": 693099385, + "isDeleted": false, + "id": "lwYvAs-7FTjcwxKjcx0KV", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 786.7392304423553, + "y": 649.6016935672433, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffc9c9", + "width": 0, + "height": 40.40111211199792, + "seed": 1233043511, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": { + "elementId": "s0PKxsWTJSDbQeEl_WI-C", + "focus": 0.016372633695398757, + "gap": 1 + }, + "endBinding": { + "elementId": "9ia1Uwc5X0fRw5iaahmcT", + "focus": 0.025318405829282714, + "gap": 14.862364635333904 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -40.40111211199792 + ] + ] + }, + { + "type": "text", + "version": 121, + "versionNonce": 952661143, + "isDeleted": false, + "id": "qWW8uxDIcV3Bkj28uvRLr", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 454.32425448306674, + "y": 537.8854189061962, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 93.75, + "height": 57.599999999999994, + "seed": 809847769, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "patch\nrelease\ne.g. 3.3.8", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "patch\nrelease\ne.g. 3.3.8", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "text", + "version": 257, + "versionNonce": 1838679129, + "isDeleted": false, + "id": "9ia1Uwc5X0fRw5iaahmcT", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 741.0510307156029, + "y": 536.7382168199114, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 93.75, + "height": 57.599999999999994, + "seed": 213765431, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "lwYvAs-7FTjcwxKjcx0KV", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "patch\nrelease\ne.g. 3.3.9", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "patch\nrelease\ne.g. 3.3.9", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "text", + "version": 222, + "versionNonce": 1528547767, + "isDeleted": false, + "id": "JALHBtowuh3_a86loej2x", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 350.7264132088442, + "y": 879.2675096875524, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 168.75, + "height": 57.599999999999994, + "seed": 41180921, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "mE8Mu0qKfFaWPCC5vmF_f", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "pre minor\nrelease\ne.g. 3.4.0-alpha.1", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "pre minor\nrelease\ne.g. 3.4.0-alpha.1", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "arrow", + "version": 345, + "versionNonce": 1286082873, + "isDeleted": false, + "id": "3heKY3vfe3-6ni4dX7Uqo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 699.5281288163526, + "y": 831.0290882554708, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 0.5502191262773977, + "height": 33.25154356841597, + "seed": 627698359, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613071, + "link": null, + "locked": false, + "startBinding": { + "elementId": "AtEf7o4WZQn4Zxq8EN5fH", + "focus": -0.05612657009295625, + "gap": 1.1451322685712295 + }, + "endBinding": { + "elementId": "9t6qH-tAxVUexkHHi2pd2", + "focus": 0.015156451076917755, + "gap": 15.586906139714358 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -0.5502191262773977, + 33.25154356841597 + ] + ] + }, + { + "type": "text", + "version": 365, + "versionNonce": 1049066199, + "isDeleted": false, + "id": "9t6qH-tAxVUexkHHi2pd2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 617.3409291322284, + "y": 879.8675379636011, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 159.375, + "height": 57.599999999999994, + "seed": 1013545943, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "3heKY3vfe3-6ni4dX7Uqo", + "type": "arrow" + } + ], + "updated": 1698927613071, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "pre minor\nrelease\ne.g. 3.4.0-beta.1", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "pre minor\nrelease\ne.g. 3.4.0-beta.1", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "arrow", + "version": 788, + "versionNonce": 1810072089, + "isDeleted": false, + "id": "vEF1cIIYYWKm84KLKqEz3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 630.3597332113623, + "y": 667.2735668205443, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 2.258228100583324, + "height": 140.75112333166828, + "seed": 2091697367, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "4v23gkfhy-hzk18YdkfLz", + "focus": 0.13930391883256707, + "gap": 1.8256906627890626 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1.8426514015177418, + 69.09942755691065 + ], + [ + 2.258228100583324, + 140.75112333166828 + ] + ] + }, + { + "type": "arrow", + "version": 687, + "versionNonce": 2017318649, + "isDeleted": false, + "id": "M14Q0Uo1DBy2Ss2SOFSgW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 370.5976915356099, + "y": 667.5155013947814, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 1.5329291446666957, + "height": 145.39303664953377, + "seed": 361678233, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -0.34892760581925586, + 83.56228079137543 + ], + [ + 1.1840015388474399, + 145.39303664953377 + ] + ] + }, + { + "type": "text", + "version": 537, + "versionNonce": 342487319, + "isDeleted": false, + "id": "CHAOOJMz7tNaG1VsG_uzT", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 384.81046417498214, + "y": 725.4677076298137, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 131.25, + "height": 57.599999999999994, + "seed": 1656007289, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "merge main\ninto minor\nbefore release", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "merge main\ninto minor\nbefore release", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "ellipse", + "version": 202, + "versionNonce": 876253145, + "isDeleted": false, + "id": "hDC6an14QljktaZCUhcPF", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 993.0386151813434, + "y": 810.335845473903, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1433430105, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "Gao2krnDddLMCj468JSWD", + "type": "arrow" + } + ], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 1525, + "versionNonce": 777631287, + "isDeleted": false, + "id": "ces8IwHCpQlTnELpjFDIn", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1092.5386800881793, + "y": 827.5114796878765, + "strokeColor": "#f08c00", + "backgroundColor": "#ffc9c9", + "width": 0.3315362017829102, + "height": 49.45191086419197, + "seed": 225867737, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "8rWUxp-jRNGrGRmhHHfm4", + "focus": -0.2047594653982401, + "gap": 10.392197401393389 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -0.3315362017829102, + 49.45191086419197 + ] + ] + }, + { + "type": "text", + "version": 894, + "versionNonce": 1173171385, + "isDeleted": false, + "id": "8rWUxp-jRNGrGRmhHHfm4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1047.251646167428, + "y": 887.3555879534618, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 112.5, + "height": 57.599999999999994, + "seed": 1600918713, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "ces8IwHCpQlTnELpjFDIn", + "type": "arrow" + } + ], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "stable minor\nrelease\ne.g. 3.4.0", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "stable minor\nrelease\ne.g. 3.4.0", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "ellipse", + "version": 201, + "versionNonce": 78435447, + "isDeleted": false, + "id": "3RHuRn_evSK0YUe02B4MY", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 909.9742423218671, + "y": 810.4142561718397, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 1199705047, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 371, + "versionNonce": 2093872087, + "isDeleted": false, + "id": "9h2Cu__8owLUgUGjGcWDe", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 848.4414471158692, + "y": 650.826922928275, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 603147257, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 361, + "versionNonce": 1981618457, + "isDeleted": false, + "id": "s0PKxsWTJSDbQeEl_WI-C", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 777.1778842958995, + "y": 650.2466837635417, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 326722777, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "lwYvAs-7FTjcwxKjcx0KV", + "type": "arrow" + } + ], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 871, + "versionNonce": 1528156247, + "isDeleted": false, + "id": "3JAdSa7kqqSDSom5ZFDoE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 904.3603861670398, + "y": 707.2413714353705, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 140.625, + "height": 57.599999999999994, + "seed": 1011049431, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "final merge\nmain into minor\nbefore release", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "final merge\nmain into minor\nbefore release", + "lineHeight": 1.2, + "baseline": 53 + }, + { + "type": "arrow", + "version": 591, + "versionNonce": 1714373785, + "isDeleted": false, + "id": "7kFBLq2Iczmj0lVnVk8Ad", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1100.7141458557703, + "y": 814.2034531496416, + "strokeColor": "#2f9e44", + "backgroundColor": "#ffffff", + "width": 127.38209933342364, + "height": 144.5383600420214, + "seed": 25829591, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "Y7VXnuc9QEz2N2l9i0xrc", + "focus": 0.3932764551319699, + "gap": 5.928572790502042 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 88.94909573964219, + -43.721805169626464 + ], + [ + 127.38209933342364, + -144.5383600420214 + ] + ] + }, + { + "type": "text", + "version": 1208, + "versionNonce": 1254600055, + "isDeleted": false, + "id": "gwFWlPLabuYhxCOweJjWz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1223.0464288187204, + "y": 725.1565933898091, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 150, + "height": 38.4, + "seed": 51102743, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "main merge minor\n(fast forward)", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "main merge minor\n(fast forward)", + "lineHeight": 1.2, + "baseline": 34 + }, + { + "type": "ellipse", + "version": 597, + "versionNonce": 1760381305, + "isDeleted": false, + "id": "Y7VXnuc9QEz2N2l9i0xrc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1227.4473966637659, + "y": 647.6689320688656, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 412038615, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "7kFBLq2Iczmj0lVnVk8Ad", + "type": "arrow" + } + ], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 547, + "versionNonce": 1585505943, + "isDeleted": false, + "id": "ggogfJT7E_bbfEog7Hjnp", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 1083.7911569735343, + "y": 809.5203742153592, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 18.814646969963974, + "height": 18.814646969963974, + "seed": 741463161, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "nRDWQs5nQa37yzCWTBiXC", + "type": "arrow" + } + ], + "updated": 1698927613072, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 229, + "versionNonce": 1935127129, + "isDeleted": false, + "id": "eU-EgpwDD42CLYUEIDLaD", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 305.8405004265049, + "y": 389.31989430571576, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 581.25, + "height": 19.2, + "seed": 1086231577, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "- merge feature PRs into, and release minors from minor branch", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- merge feature PRs into, and release minors from minor branch", + "lineHeight": 1.2, + "baseline": 15 + }, + { + "type": "text", + "version": 397, + "versionNonce": 116088535, + "isDeleted": false, + "id": "Kt6VBAVD4sLM4IexsRGoX", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 305.4136207977353, + "y": 358.61173442109686, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 618.75, + "height": 19.2, + "seed": 273353945, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927617946, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "- merge fix / chore PRs into, and release patches from main branch", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- merge fix / chore PRs into, and release patches from main branch", + "lineHeight": 1.2, + "baseline": 15 + }, + { + "type": "text", + "version": 459, + "versionNonce": 440532793, + "isDeleted": false, + "id": "JwKEdnU6H_Nu74WbEAX5M", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 305.6723761009271, + "y": 418.3724478537203, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 459.375, + "height": 19.2, + "seed": 1001222329, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "- merge main into minor before each minor release", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- merge main into minor before each minor release", + "lineHeight": 1.2, + "baseline": 15 + }, + { + "type": "text", + "version": 602, + "versionNonce": 1108720119, + "isDeleted": false, + "id": "mb9ZoP803MiH7MTO8wH-2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 305.0895924262568, + "y": 447.44321411383333, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 534.375, + "height": 19.2, + "seed": 264651479, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "- fast forward main to minor after a stable minor release", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- fast forward main to minor after a stable minor release", + "lineHeight": 1.2, + "baseline": 15 + }, + { + "type": "text", + "version": 612, + "versionNonce": 1588872441, + "isDeleted": false, + "id": "IfJPOFiwrCibpaBQqc5g-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 2, + "opacity": 100, + "angle": 0, + "x": 646.7131179044119, + "y": 724.4984335940012, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "width": 131.25, + "height": 57.599999999999994, + "seed": 1301100087, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698927613072, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "merge main\ninto minor\nbefore release", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "merge main\ninto minor\nbefore release", + "lineHeight": 1.2, + "baseline": 53 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/.github/git-branch-workflow.png b/.github/git-branch-workflow.png new file mode 100644 index 00000000000..6c8ee07d484 Binary files /dev/null and b/.github/git-branch-workflow.png differ diff --git a/.github/issue-workflow.png b/.github/issue-workflow.png new file mode 100644 index 00000000000..92b1de0633c Binary files /dev/null and b/.github/issue-workflow.png differ diff --git a/.github/maintenance.md b/.github/maintenance.md new file mode 100644 index 00000000000..8d4317c6b01 --- /dev/null +++ b/.github/maintenance.md @@ -0,0 +1,122 @@ +# Vue Core Maintenance Handbook + +Unlike [contributing.md](./contributing.md), which targets external contributors, this document is mainly intended for team members responsible for maintaining the project. It provides guidelines on how to triage issues, review & merge PRs, and publish releases. However, it should also be valuable to external contributors even if you are not a maintainer, as it gives you a better idea of how the maintainers operate, and how you can better collaborate with them. And who knows - maybe one day you will join as a maintainer as well! + +- [Issue Triage Workflow](#issue-triage-workflow) +- [Pull Request Review Guidelines](#pull-request-review-guidelines) + - [Reviewing a Fix](#reviewing-a-fix) + - [Reviewing a Refactor](#reviewing-a-refactor) + - [Reviewing a Feature](#reviewing-a-feature) + - [Common Considerations for All PRs](#common-considerations-for-all-prs) +- [PR Merge Rules for Team Members](#pr-merge-rules-for-team-members) +- [Git Branch and Release Workflow](#git-branch-and-release-workflow) + +## Issue Triage Workflow + +![Workflow](./issue-workflow.png) + +## Pull Request Review Guidelines + +The first step of reviewing a PR is to identify its purpose. We can usually put a PR in one of these categories: + +- **Fix**: fixes some wrong behavior. Usually associated with an issue that has a reproduction of the behavior being fixed. +- **Refactor**: improves performance or code quality, but does not affect behavior. +- **Feature**: implements something that increases the public API surface. + +Depending on the type of the PR, different considerations need to be taken into account. + +### Reviewing a Fix + +- Is the PR fixing a well defined issue / bug report? + - If not, ask to clarify context / provide reproduction or failing test case +- In most cases, a fix PR should include a test case that fails without the fix. +- Is it the right fix? + - If not, guide user to rework the PR. + - If the needed change is small and obvious, can directly push to the PR or add inline suggestions to reduce the back-and-forth. +- Is the cost justified? + - Sometimes the fix for a rare edge case might be introducing disproportionately large overhead (perf or code size). We should try our best to reduce the overhead to make the fix a reasonable tradeoff. +- If the reviewer is not sure about a fix, try to leave a comment explaining the concerns / reservations so the contributor at least gets some feedback. + +#### Verifying a Fix + +- **Always locally verify that the fix indeed fixes the original behavior, either through a reproduction or a failing test case.** +- We will run [ecosystem-ci](https://github.com/vuejs/ecosystem-ci) before every release, but if you are concerned about the potential impact of a change, it never hurts to manually run ecosystem-ci by leaving a `/ecosystem-ci run` comment (only works for team members). +- Take extra caution with snapshot tests! The CI can be "passing" even if the code generated in the snapshot contains bugs. It's best to always accompany a snapshot test with extra `expect(code).toMatch(...)` assertions. + +### Reviewing a Refactor + +- Performance: if a refactor PR claims to improve performance, there should be benchmarks showcasing said performance unless the improvement is self-explanatory. + +- Code quality / stylistic PRs: we should be conservative on merging this type PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it. + + - For PRs in this category that are approved, do not merge immediately. Group them before releasing a new minor, after all feature-oriented PRs are merged. + +### Reviewing a Feature + +- Feature PRs should always have clear context and explanation on why the feature should be added, ideally in the form of an RFC. If the PR doesn't explain what real-world problem it is solving, ask the contributor to clarify. + +- Decide if the feature should require an RFC process. The line isn't always clear, but a rough criteria is whether it is augmenting an existing API vs. adding a new API. Some examples: + + - Adding a new built-in component or directive is "significant" and definitely requires an RFC. + - Template syntax additions like adding a new `v-on` modifier or a new `v-bind` syntax sugar are "substantial". It would be nice to have an RFC for it, but a detailed explanation on the use case and reasoning behind the design directly in the PR itself can be acceptable. + - Small, low-impact additions like exposing a new utility type or adding a new app config option can be self-explanatory, but should still provide enough context in the PR. + +- Always ask if the use case can be solved with existing APIs. Vue already has a pretty large API surface, so we want to make sure every new addition either solves something that wasn't possible before, or significantly improves the DX of a common task. + +### Common Considerations for All PRs + +- Scope: a PR should only contain changes directly related to the problem being addressed. It should not contain unnecessary code style changes. + +- Implementation: code style should be consistent with the rest of the codebase, follow common best practices. Prefer code that is boring but easy to understand over "clever" code. + +- Size: bundle size matters. We have a GitHub action that compares the size change for every PR. We should always aim to realize the desired changes with the smallest amount of code size increase. + + - Sometimes we need to compare the size increase vs. perceived benefits to decide whether a change is justifiable. Also take extra care to make sure added code can be tree-shaken if not needed. + + - Make sure to put dev-only code in `__DEV__` branches so they are tree-shakable. + + - Runtime code is more sensitive to size increase than compiler code. + + - Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in @vue/shared are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`. + +- Performance + - Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code. + +- Potential Breakage + - avoiding runtime behavior breakage is the highest priority + - if not sure, use `ecosystem-ci` to verify! + - some fix inevitably cause behavior change, these must be discussed case-by-case + - type level breakage (e.g upgrading TS) is possible between minors + +## PR Merge Rules for Team Members + +Given that the PR meets the review requirements: + +- Chore / dependencies bumps: can merge directly. +- Fixes / refactors: can merge with two or more approvals from team members. + - If you believe a PR looks good but you are not 100% confident to merge, label with "ready for merge" and Evan will provide a final review before merging. +- Features: if approved by two or more team members, label with "ready to merge". Evan will review periodically, or they can be raised and discussed at team meetings. + +## Git Branch and Release Workflow + +We use two primary work branches: `main` and `minor`. + +- The `main` branch is for stable releases. Changes that are bug fixes or refactors that do not affect the public API surface should land in this branch. We periodically release patch releases from the `main` branch. + +- The `minor` branch is the WIP branch for the next minor release. Changes that are new features or those that affect public API behavior should land in this branch. We will periodically release pre-releases (alpha / beta) for the next minor from this branch. + +Before each release, we merge latest `main` into `minor` so it would include the latest bug fixes. + +When the minor is ready, we do a final merge of `main` into `minor`, and then release a stable minor from this branch (e.g. `3.4.0`). After that, the `main` branch is fast-forwarded to the release commit, so the two branches are synced at each stable minor release. + +![Workflow](./git-branch-workflow.png) + +### Reasoning Behind the Workflow + +The reason behind this workflow is to allow merging and releasing of fixes and features in parallel. In the past, we used a linear trunk-based development model. While the linear model results in a clean git history, the downside is that we need to be careful about when to merge patches vs. features. + +Vue typically groups a number of features with the same scope in a minor release. We don't want to release a minor just because we happened to merge a feature PR along with a bunch of small bug fixes. So we usually "wait" until we feel we are ready to start working on a minor release before merging feature PRs. + +But in reality, there are always bugs to fix and patch release to work on - this caused the intervals between minors to drag on longer than we had hoped, and many feature PRs were left waiting for a long period of time. + +This is why we decided to separate bug fixes and feature PRs into separate branches. With this two-branch model, we are able to merge and release both types of changes in parallel. diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 1c81ece394d..1392de2e080 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -17,7 +17,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Set node version to 18 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: pnpm diff --git a/.github/workflows/canary-minor.yml b/.github/workflows/canary-minor.yml index 2aa6db12b36..27fbd42c90c 100644 --- a/.github/workflows/canary-minor.yml +++ b/.github/workflows/canary-minor.yml @@ -20,7 +20,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Set node version to 18 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 61265c2d0e6..61490232f66 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -18,7 +18,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db874b1240e..493ab295000 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' @@ -45,7 +45,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' @@ -74,13 +74,13 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' - run: pnpm install - - run: node node_modules/puppeteer/install.js + - run: node node_modules/puppeteer/install.mjs - name: Run e2e tests run: pnpm run test-e2e @@ -97,7 +97,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/size-data.yml b/.github/workflows/size-data.yml index 83141e242f8..bb82aa18d58 100644 --- a/.github/workflows/size-data.yml +++ b/.github/workflows/size-data.yml @@ -25,7 +25,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: pnpm diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index cdfce9a979f..78ae44bb7ea 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -27,7 +27,7 @@ jobs: uses: pnpm/action-setup@v2 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.node-version' cache: pnpm diff --git a/CHANGELOG.md b/CHANGELOG.md index 12be7dc8a3a..bf0c8591f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## [3.3.8](https://github.com/vuejs/core/compare/v3.3.7...v3.3.8) (2023-11-06) + + +### Bug Fixes + +* **compile-sfc:** support `Error` type in `defineProps` ([#5955](https://github.com/vuejs/core/issues/5955)) ([a989345](https://github.com/vuejs/core/commit/a9893458ec519aae442e1b99e64e6d74685cd22c)) +* **compiler-core:** known global should be shadowed by local variables in expression rewrite ([#9492](https://github.com/vuejs/core/issues/9492)) ([a75d1c5](https://github.com/vuejs/core/commit/a75d1c5c6242e91a73cc5ba01e6da620dea0b3d9)), closes [#9482](https://github.com/vuejs/core/issues/9482) +* **compiler-sfc:** fix dynamic directive arguments usage check for slots ([#9495](https://github.com/vuejs/core/issues/9495)) ([b39fa1f](https://github.com/vuejs/core/commit/b39fa1f8157647859331ce439c42ae016a49b415)), closes [#9493](https://github.com/vuejs/core/issues/9493) +* **deps:** update dependency @vue/repl to ^2.6.2 ([#9536](https://github.com/vuejs/core/issues/9536)) ([5cef325](https://github.com/vuejs/core/commit/5cef325f41e3b38657c72fa1a38dedeee1c7a60a)) +* **deps:** update dependency @vue/repl to ^2.6.3 ([#9540](https://github.com/vuejs/core/issues/9540)) ([176d590](https://github.com/vuejs/core/commit/176d59058c9aecffe9da4d4311e98496684f06d4)) +* **hydration:** fix tagName access eeror on comment/text node hydration mismatch ([dd8a0cf](https://github.com/vuejs/core/commit/dd8a0cf5dcde13d2cbd899262a0e07f16e14e489)), closes [#9531](https://github.com/vuejs/core/issues/9531) +* **types:** avoid exposing lru-cache types in generated dts ([462aeb3](https://github.com/vuejs/core/commit/462aeb3b600765e219ded2ee9a0ed1e74df61de0)), closes [#9521](https://github.com/vuejs/core/issues/9521) +* **warn:** avoid warning on empty children with Suspense ([#3962](https://github.com/vuejs/core/issues/3962)) ([405f345](https://github.com/vuejs/core/commit/405f34587a63a5f1e3d147b9848219ea98acc22d)) + + + +## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24) + + +### Bug Fixes + +* **compiler-sfc:** avoid gen useCssVars when targeting SSR ([#6979](https://github.com/vuejs/core/issues/6979)) ([c568778](https://github.com/vuejs/core/commit/c568778ea3265d8e57f788b00864c9509bf88a4e)), closes [#6926](https://github.com/vuejs/core/issues/6926) +* **compiler-ssr:** proper scope analysis for ssr vnode slot fallback ([#7184](https://github.com/vuejs/core/issues/7184)) ([e09c26b](https://github.com/vuejs/core/commit/e09c26bc9bc4394c2c2d928806d382515c2676f3)), closes [#7095](https://github.com/vuejs/core/issues/7095) +* correctly resolve types from relative paths on Windows ([#9446](https://github.com/vuejs/core/issues/9446)) ([089d36d](https://github.com/vuejs/core/commit/089d36d167dc7834065b03ca689f9b6a44eead8a)), closes [#8671](https://github.com/vuejs/core/issues/8671) +* **hmr:** fix hmr error for hoisted children array in v-for ([7334376](https://github.com/vuejs/core/commit/733437691f70ebca8dd6cc3bc8356f5b57d4d5d8)), closes [#6978](https://github.com/vuejs/core/issues/6978) [#7114](https://github.com/vuejs/core/issues/7114) +* **reactivity:** assigning array.length while observing a symbol property ([#7568](https://github.com/vuejs/core/issues/7568)) ([e9e2778](https://github.com/vuejs/core/commit/e9e2778e9ec5cca07c1df5f0c9b7b3595a1a3244)) +* **scheduler:** ensure jobs are in the correct order ([#7748](https://github.com/vuejs/core/issues/7748)) ([a8f6638](https://github.com/vuejs/core/commit/a8f663867b8cd2736b82204bc58756ef02441276)), closes [#7576](https://github.com/vuejs/core/issues/7576) +* **ssr:** fix hydration mismatch for disabled teleport at component root ([#9399](https://github.com/vuejs/core/issues/9399)) ([d8990fc](https://github.com/vuejs/core/commit/d8990fc6182d1c2cf0a8eab7b35a9d04df668507)), closes [#6152](https://github.com/vuejs/core/issues/6152) +* **Suspense:** calling hooks before the transition finishes ([#9388](https://github.com/vuejs/core/issues/9388)) ([00de3e6](https://github.com/vuejs/core/commit/00de3e61ed7a55e7d6c2e1987551d66ad0f909ff)), closes [#5844](https://github.com/vuejs/core/issues/5844) [#5952](https://github.com/vuejs/core/issues/5952) +* **transition/ssr:** make transition appear work with SSR ([#8859](https://github.com/vuejs/core/issues/8859)) ([5ea8a8a](https://github.com/vuejs/core/commit/5ea8a8a4fab4e19a71e123e4d27d051f5e927172)), closes [#6951](https://github.com/vuejs/core/issues/6951) +* **types:** fix ComponentCustomProps augmentation ([#9468](https://github.com/vuejs/core/issues/9468)) ([7374e93](https://github.com/vuejs/core/commit/7374e93f0281f273b90ab5a6724cc47332a01d6c)), closes [#8376](https://github.com/vuejs/core/issues/8376) +* **types:** improve `h` overload to support union of string and component ([#5432](https://github.com/vuejs/core/issues/5432)) ([16ecb44](https://github.com/vuejs/core/commit/16ecb44c89cd8299a3b8de33cccc2e2cc36f065b)), closes [#5431](https://github.com/vuejs/core/issues/5431) + + + ## [3.3.6](https://github.com/vuejs/core/compare/v3.3.5...v3.3.6) (2023-10-20) diff --git a/README.md b/README.md index 17d9abc6bae..cbc05311ae7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Vue.js is an MIT-licensed open source project with its ongoing development made

- sponsors + sponsors

diff --git a/package.json b/package.json index d903294ab1e..6dbd935809b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, - "version": "3.3.6", - "packageManager": "pnpm@8.9.2", + "version": "3.3.8", + "packageManager": "pnpm@8.10.3", "type": "module", "scripts": { "dev": "node scripts/dev.js", @@ -27,9 +27,9 @@ "dev-esm": "node scripts/dev.js -if esm-bundler-runtime", "dev-compiler": "run-p \"dev template-explorer\" serve", "dev-sfc": "run-s dev-sfc-prepare dev-sfc-run", - "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-compiler-cjs", + "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-all-cjs", "dev-sfc-serve": "vite packages/sfc-playground --host", - "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", + "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", "serve": "serve", "open": "open http://localhost:3000/packages/template-explorer/local.html", "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self", @@ -57,25 +57,25 @@ "node": ">=18.12.0" }, "devDependencies": { - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", "@rollup/plugin-alias": "^5.0.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.4", "@rollup/plugin-terser": "^0.4.4", - "@types/hash-sum": "^1.0.1", - "@types/node": "^18.18.6", - "@typescript-eslint/parser": "^6.8.0", - "@vitest/coverage-istanbul": "^0.34.4", + "@types/hash-sum": "^1.0.2", + "@types/node": "^20.9.0", + "@typescript-eslint/parser": "^6.10.0", + "@vitest/coverage-istanbul": "^0.34.6", "@vue/consolidate": "0.17.3", "conventional-changelog-cli": "^4.1.0", "enquirer": "^2.4.1", "esbuild": "^0.19.5", "esbuild-plugin-polyfill-node": "^0.3.0", - "eslint": "^8.51.0", - "eslint-plugin-jest": "^27.4.2", + "eslint": "^8.53.0", + "eslint-plugin-jest": "^27.6.0", "estree-walker": "^2.0.2", "execa": "^8.0.1", "jsdom": "^22.1.0", @@ -83,16 +83,16 @@ "lodash": "^4.17.21", "magic-string": "^0.30.5", "markdown-table": "^3.0.3", - "marked": "^9.1.2", + "marked": "^9.1.6", "minimist": "^1.2.8", "npm-run-all": "^4.1.5", "picocolors": "^1.0.0", "prettier": "^3.0.3", "pretty-bytes": "^6.1.1", "pug": "^3.0.2", - "puppeteer": "~21.2.1", + "puppeteer": "~21.5.1", "rimraf": "^5.0.5", - "rollup": "^3.29.4", + "rollup": "^4.1.4", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-polyfill-node": "^0.12.0", @@ -100,11 +100,11 @@ "serve": "^14.2.1", "simple-git-hooks": "^2.9.0", "terser": "^5.22.0", - "todomvc-app-css": "^2.4.2", + "todomvc-app-css": "^2.4.3", "tslib": "^2.6.2", "tsx": "^3.14.0", - "typescript": "^5.1.6", - "vite": "^4.3.0", - "vitest": "^0.34.4" + "typescript": "^5.2.2", + "vite": "^4.5.0", + "vitest": "^0.34.6" } } diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap index c72e0229832..434ebcbcf2f 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/transformExpressions.spec.ts.snap @@ -2,7 +2,7 @@ exports[`compiler: expression transform > bindingMetadata > inline mode 1`] = ` "(_ctx, _cache) => { - return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.props) + \\" \\" + _toDisplayString(_unref(setup)) + \\" \\" + _toDisplayString(setupConst) + \\" \\" + _toDisplayString(_ctx.data) + \\" \\" + _toDisplayString(_ctx.options), 1 /* TEXT */)) + return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.props) + \\" \\" + _toDisplayString(_unref(setup)) + \\" \\" + _toDisplayString(setupConst) + \\" \\" + _toDisplayString(_ctx.data) + \\" \\" + _toDisplayString(_ctx.options) + \\" \\" + _toDisplayString(isNaN.value), 1 /* TEXT */)) }" `; @@ -10,6 +10,48 @@ exports[`compiler: expression transform > bindingMetadata > non-inline mode 1`] "const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { - return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString($props.props) + \\" \\" + _toDisplayString($setup.setup) + \\" \\" + _toDisplayString($data.data) + \\" \\" + _toDisplayString($options.options), 1 /* TEXT */)) + return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString($props.props) + \\" \\" + _toDisplayString($setup.setup) + \\" \\" + _toDisplayString($data.data) + \\" \\" + _toDisplayString($options.options) + \\" \\" + _toDisplayString($setup.isNaN), 1 /* TEXT */)) +}" +`; + +exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for loop 1`] = ` +"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +return function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock(\\"div\\", { + onClick: () => { + for (let i = 0; i < _ctx.list.length; i++) { + _ctx.log(i) + } + } + }, null, 8 /* PROPS */, [\\"onClick\\"])) +}" +`; + +exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...in 1`] = ` +"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +return function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock(\\"div\\", { + onClick: () => { + for (const x in _ctx.list) { + _ctx.log(x) + } + } + }, null, 8 /* PROPS */, [\\"onClick\\"])) +}" +`; + +exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...of 1`] = ` +"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue + +return function render(_ctx, _cache, $props, $setup, $data, $options) { + return (_openBlock(), _createElementBlock(\\"div\\", { + onClick: () => { + for (const x of _ctx.list) { + _ctx.log(x) + } + } + }, null, 8 /* PROPS */, [\\"onClick\\"])) }" `; diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index e59df2d5458..983fe1223aa 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -85,7 +85,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock(\\"input\\", { \\"foo-value\\": model, \\"onUpdate:fooValue\\": $event => ((model) = $event) - }, null, 40 /* PROPS, HYDRATE_EVENTS */, [\\"foo-value\\", \\"onUpdate:fooValue\\"])) + }, null, 40 /* PROPS, NEED_HYDRATION */, [\\"foo-value\\", \\"onUpdate:fooValue\\"])) } }" `; diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts index eec5a76d363..49ad7ad8982 100644 --- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -593,5 +593,17 @@ describe('compiler: hoistStatic transform', () => { expect(root.hoists.length).toBe(2) expect(generate(root).code).toMatchSnapshot() }) + + test('clone hoisted array children in HMR mode', () => { + const root = transformWithHoist(`
`, { + hmr: true + }) + expect(root.hoists.length).toBe(2) + expect(root.codegenNode).toMatchObject({ + children: { + content: '[..._hoisted_2]' + } + }) + }) }) }) diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index a1ae013a830..97559369d8a 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -1089,7 +1089,7 @@ describe('compiler: element transform', () => { }) }) - test('HYDRATE_EVENTS', () => { + test('NEED_HYDRATION for v-on', () => { // ignore click events (has dedicated fast path) const { node } = parseWithElementTransform(`
`, { directiveTransforms: { @@ -1108,12 +1108,24 @@ describe('compiler: element transform', () => { } ) expect(node2.patchFlag).toBe( - genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) + genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]) + ) + }) + + test('NEED_HYDRATION for v-bind.prop', () => { + const { node } = parseWithBind(`
`) + expect(node.patchFlag).toBe( + genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]) + ) + + const { node: node2 } = parseWithBind(`
`) + expect(node2.patchFlag).toBe( + genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]) ) }) // #5870 - test('HYDRATE_EVENTS on dynamic component', () => { + test('NEED_HYDRATION on dynamic component', () => { const { node } = parseWithElementTransform( ``, { @@ -1123,7 +1135,7 @@ describe('compiler: element transform', () => { } ) expect(node.patchFlag).toBe( - genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) + genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]) ) }) }) diff --git a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts index 686794c23ab..16229113656 100644 --- a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts @@ -506,7 +506,8 @@ describe('compiler: expression transform', () => { data: BindingTypes.DATA, options: BindingTypes.OPTIONS, reactive: BindingTypes.SETUP_REACTIVE_CONST, - literal: BindingTypes.LITERAL_CONST + literal: BindingTypes.LITERAL_CONST, + isNaN: BindingTypes.SETUP_REF } function compileWithBindingMetadata( @@ -522,19 +523,56 @@ describe('compiler: expression transform', () => { test('non-inline mode', () => { const { code } = compileWithBindingMetadata( - `
{{ props }} {{ setup }} {{ data }} {{ options }}
` + `
{{ props }} {{ setup }} {{ data }} {{ options }} {{ isNaN }}
` ) expect(code).toMatch(`$props.props`) expect(code).toMatch(`$setup.setup`) + expect(code).toMatch(`$setup.isNaN`) expect(code).toMatch(`$data.data`) expect(code).toMatch(`$options.options`) expect(code).toMatch(`_ctx, _cache, $props, $setup, $data, $options`) expect(code).toMatchSnapshot() }) + test('should not prefix temp variable of for...in', () => { + const { code } = compileWithBindingMetadata( + `
` + ) + expect(code).not.toMatch(`_ctx.x`) + expect(code).toMatchSnapshot() + }) + + test('should not prefix temp variable of for...of', () => { + const { code } = compileWithBindingMetadata( + `
` + ) + expect(code).not.toMatch(`_ctx.x`) + expect(code).toMatchSnapshot() + }) + + test('should not prefix temp variable of for loop', () => { + const { code } = compileWithBindingMetadata( + `
` + ) + expect(code).not.toMatch(`_ctx.i`) + expect(code).toMatchSnapshot() + }) + test('inline mode', () => { const { code } = compileWithBindingMetadata( - `
{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }}
`, + `
{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}
`, { inline: true } ) expect(code).toMatch(`__props.props`) @@ -542,6 +580,7 @@ describe('compiler: expression transform', () => { expect(code).toMatch(`_toDisplayString(setupConst)`) expect(code).toMatch(`_ctx.data`) expect(code).toMatch(`_ctx.options`) + expect(code).toMatch(`isNaN.value`) expect(code).toMatchSnapshot() }) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index f885e2046e1..948861435d2 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -32,12 +32,12 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { - "@babel/parser": "^7.23.0", - "@vue/shared": "3.3.6", + "@babel/parser": "^7.23.3", + "@vue/shared": "3.3.8", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, "devDependencies": { - "@babel/types": "^7.23.0" + "@babel/types": "^7.23.3" } } diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts index b52ce28990a..1f1e3896a1e 100644 --- a/packages/compiler-core/src/babelUtils.ts +++ b/packages/compiler-core/src/babelUtils.ts @@ -165,6 +165,19 @@ export function walkBlockDeclarations( ) { if (stmt.declare || !stmt.id) continue onIdent(stmt.id) + } else if ( + stmt.type === 'ForOfStatement' || + stmt.type === 'ForInStatement' || + stmt.type === 'ForStatement' + ) { + const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left + if (variable && variable.type === 'VariableDeclaration') { + for (const decl of variable.declarations) { + for (const id of extractIdentifiers(decl.id)) { + onIdent(id) + } + } + } } } } diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 65bbcb36dd6..abfba98e35c 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -256,6 +256,12 @@ export interface TransformOptions * needed to render inline CSS variables on component root */ ssrCssVars?: string + /** + * Whether to compile the template assuming it needs to handle HMR. + * Some edge cases may need to generate different code for HMR to work + * correctly, e.g. #6938, #7138 + */ + hmr?: boolean } export interface CodegenOptions extends SharedTransformCodegenOptions { diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index b72ad028b4b..b1b7c079ced 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -1063,7 +1063,7 @@ function parseTextData( ) { return rawText } else { - // DATA or RCDATA containing "&"". Entity decoding required. + // DATA or RCDATA containing "&". Entity decoding is required. return context.options.decodeEntities( rawText, mode === TextModes.ATTRIBUTE_VALUE diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index d26c11bba20..04f85679cae 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -129,6 +129,7 @@ export function createTransformContext( filename = '', prefixIdentifiers = false, hoistStatic = false, + hmr = false, cacheHandlers = false, nodeTransforms = [], directiveTransforms = {}, @@ -155,6 +156,7 @@ export function createTransformContext( selfName: nameMatch && capitalize(camelize(nameMatch[1])), prefixIdentifiers, hoistStatic, + hmr, cacheHandlers, nodeTransforms, directiveTransforms, diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 5526163c6f9..fd443496ca7 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -140,9 +140,16 @@ function walk( node.codegenNode.type === NodeTypes.VNODE_CALL && isArray(node.codegenNode.children) ) { - node.codegenNode.children = context.hoist( + const hoisted = context.hoist( createArrayExpression(node.codegenNode.children) ) + // #6978, #7138, #7114 + // a hoisted children array inside v-for can caused HMR errors since + // it might be mutated when mounting the v-for list + if (context.hmr) { + hoisted.content = `[...${hoisted.content}]` + } + node.codegenNode.children = hoisted } } diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 253b6be5efa..fd61f011051 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -550,7 +550,7 @@ export function buildProps( ) } else { // directives - const { name, arg, exp, loc } = prop + const { name, arg, exp, loc, modifiers } = prop const isVBind = name === 'bind' const isVOn = name === 'on' @@ -678,6 +678,11 @@ export function buildProps( continue } + // force hydration for v-bind with .prop modifier + if (isVBind && modifiers.includes('prop')) { + patchFlag |= PatchFlags.NEED_HYDRATION + } + const directiveTransform = context.directiveTransforms[name] if (directiveTransform) { // has built-in directive transform. @@ -743,12 +748,12 @@ export function buildProps( patchFlag |= PatchFlags.PROPS } if (hasHydrationEventBinding) { - patchFlag |= PatchFlags.HYDRATE_EVENTS + patchFlag |= PatchFlags.NEED_HYDRATION } } if ( !shouldUseBlock && - (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && + (patchFlag === 0 || patchFlag === PatchFlags.NEED_HYDRATION) && (hasRef || hasVnodeHook || runtimeDirectives.length > 0) ) { patchFlag |= PatchFlags.NEED_PATCH diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index eab6b237f13..52d1fb42a13 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -227,10 +227,15 @@ export function processExpression( const isScopeVarReference = context.identifiers[rawExp] const isAllowedGlobal = isGloballyAllowed(rawExp) const isLiteral = isLiteralWhitelisted(rawExp) - if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) { + if ( + !asParams && + !isScopeVarReference && + !isLiteral && + (!isAllowedGlobal || bindingMetadata[rawExp]) + ) { // const bindings exposed from setup can be skipped for patching but // cannot be hoisted to module scope - if (isConst(bindingMetadata[node.content])) { + if (isConst(bindingMetadata[rawExp])) { node.constType = ConstantTypes.CAN_SKIP_PATCH } node.content = rewriteIdentifier(rawExp) diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index a44532c0d58..56144dcd284 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -37,7 +37,8 @@ import { isTemplateNode, isSlotOutlet, injectProp, - findDir + findDir, + forAliasRE } from '../utils' import { RENDER_LIST, @@ -308,7 +309,6 @@ export function processFor( } } -const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ // This regex doesn't cover the case if key or index aliases have destructuring, // but those do not make sense in the first place, so this works in practice. const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index c4416dd45f7..ffa90ea1171 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => { export type SlotFnBuilder = ( slotProps: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, slotChildren: TemplateChildNode[], loc: SourceLocation ) => FunctionExpression -const buildClientSlotFn: SlotFnBuilder = (props, children, loc) => +const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => createFunctionExpression( props, children, @@ -149,7 +150,7 @@ export function buildSlots( slotsProperties.push( createObjectProperty( arg || createSimpleExpression('default', true), - buildSlotFn(exp, children, loc) + buildSlotFn(exp, undefined, children, loc) ) ) } @@ -201,11 +202,17 @@ export function buildSlots( hasDynamicSlots = true } - const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc) + const vFor = findDir(slotElement, 'for') + const slotFunction = buildSlotFn( + slotProps, + vFor?.exp, + slotChildren, + slotLoc + ) + // check if this slot is conditional (v-if/v-for) let vIf: DirectiveNode | undefined let vElse: DirectiveNode | undefined - let vFor: DirectiveNode | undefined if ((vIf = findDir(slotElement, 'if'))) { hasDynamicSlots = true dynamicSlots.push( @@ -257,7 +264,7 @@ export function buildSlots( createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc) ) } - } else if ((vFor = findDir(slotElement, 'for'))) { + } else if (vFor) { hasDynamicSlots = true const parseResult = vFor.parseResult || @@ -306,7 +313,7 @@ export function buildSlots( props: ExpressionNode | undefined, children: TemplateChildNode[] ) => { - const fn = buildSlotFn(props, children, loc) + const fn = buildSlotFn(props, undefined, children, loc) if (__COMPAT__ && context.compatConfig) { fn.isNonScopedSlot = true } diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index bd2882b09e2..baffe5086f6 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -519,3 +519,5 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) { return node } } + +export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 85efeafb8c9..90eb25c72ef 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -20,10 +20,7 @@ describe('stringify static html', () => { } function repeat(code: string, n: number): string { - return new Array(n) - .fill(0) - .map(() => code) - .join('') + return code.repeat(n) } test('should bail on non-eligible static trees', () => { diff --git a/packages/compiler-dom/__tests__/transforms/vModel.spec.ts b/packages/compiler-dom/__tests__/transforms/vModel.spec.ts index dce8f09b02c..a67ca5d691f 100644 --- a/packages/compiler-dom/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/vModel.spec.ts @@ -137,6 +137,27 @@ describe('compiler: transform v-model', () => { }) ) }) + + test('should error on dynamic value binding alongside v-model', () => { + const onError = vi.fn() + transformWithModel(``, { + onError + }) + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ + code: DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE + }) + ) + }) + + // #3596 + test('should NOT error on static value binding alongside v-model', () => { + const onError = vi.fn() + transformWithModel(``, { + onError + }) + expect(onError).not.toHaveBeenCalled() + }) }) describe('modifiers', () => { diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts index efc7fee374f..79ffcdef03c 100644 --- a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts @@ -272,7 +272,7 @@ describe('compiler-dom: transform v-on', () => { // should not treat cached handler as dynamicProp, so it should have no // dynamicProps flags and only the hydration flag expect((root as any).children[0].codegenNode.patchFlag).toBe( - genFlagText(PatchFlags.HYDRATE_EVENTS) + genFlagText(PatchFlags.NEED_HYDRATION) ) expect(prop).toMatchObject({ key: { diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 8bde209e292..c2bed3837a1 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.6", - "@vue/compiler-core": "3.3.6" + "@vue/shared": "3.3.8", + "@vue/compiler-core": "3.3.8" } } diff --git a/packages/compiler-dom/src/transforms/vModel.ts b/packages/compiler-dom/src/transforms/vModel.ts index 5dff390d3d8..bc1f6fcd27b 100644 --- a/packages/compiler-dom/src/transforms/vModel.ts +++ b/packages/compiler-dom/src/transforms/vModel.ts @@ -4,7 +4,9 @@ import { ElementTypes, findProp, NodeTypes, - hasDynamicKeyVBind + hasDynamicKeyVBind, + findDir, + isStaticArgOf } from '@vue/compiler-core' import { createDOMCompilerError, DOMErrorCodes } from '../errors' import { @@ -32,8 +34,8 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { } function checkDuplicatedValue() { - const value = findProp(node, 'value') - if (value) { + const value = findDir(node, 'bind') + if (value && isStaticArgOf(value.arg, 'value')) { context.onError( createDOMCompilerError( DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE, diff --git a/packages/compiler-sfc/README.md b/packages/compiler-sfc/README.md index c76618e4a4d..4f8ff3ac8b3 100644 --- a/packages/compiler-sfc/README.md +++ b/packages/compiler-sfc/README.md @@ -4,7 +4,7 @@ **Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/compiler-sfc`. This means you no longer need to explicitly install this package and ensure its version match that of `vue`'s. Just use the main `vue/compiler-sfc` deep import instead.** -This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader), [rollup-plugin-vue](https://github.com/vuejs/rollup-plugin-vue) and [vite](https://github.com/vitejs/vite). +This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader) and [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue). ## API @@ -77,4 +77,4 @@ export default script Options needed for these APIs can be passed via the query string. -For detailed API references and options, check out the source type definitions. For actual usage of these APIs, check out [rollup-plugin-vue](https://github.com/vuejs/rollup-plugin-vue/tree/next) or [vue-loader](https://github.com/vuejs/vue-loader/tree/next). +For detailed API references and options, check out the source type definitions. For actual usage of these APIs, check out [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue) or [vue-loader](https://github.com/vuejs/vue-loader/tree/next). diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 949c9946d9f..073874363b8 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -696,14 +696,14 @@ return { get vMyDir() { return vMyDir } } exports[`SFC compile `) expect(content).toMatch( `return { get FooBar() { return FooBar }, get foo() { return foo }, ` + - `get bar() { return bar } }` + `get bar() { return bar }, get baz() { return baz } }` ) assertCode(content) }) diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap index 1e851cbf4df..78a9834d288 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap @@ -81,6 +81,24 @@ return { emit } })" `; +exports[`defineEmits > w/ type (interface w/ extends) 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Base { (e: 'foo'): void } + interface Emits extends Base { (e: 'bar'): void } + +export default /*#__PURE__*/_defineComponent({ + emits: [\\"bar\\", \\"foo\\"], + setup(__props, { expose: __expose, emit: __emit }) { + __expose(); + + const emit = __emit + +return { emit } +} + +})" +`; + exports[`defineEmits > w/ type (interface) 1`] = ` "import { defineComponent as _defineComponent } from 'vue' interface Emits { (e: 'foo' | 'bar'): void } diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap index 30e00e518b2..158b5c8f555 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap @@ -46,6 +46,51 @@ export default /*#__PURE__*/_defineComponent({ const { foo } = __props +return { } +} + +})" +`; + +exports[`defineProps > should escape names w/ special symbols 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + props: { + \\"spa ce\\": { type: null, required: true }, + \\"exclamation!mark\\": { type: null, required: true }, + \\"double\\\\\\"quote\\": { type: null, required: true }, + \\"hash#tag\\": { type: null, required: true }, + \\"dollar$sign\\": { type: null, required: true }, + \\"percentage%sign\\": { type: null, required: true }, + \\"amper&sand\\": { type: null, required: true }, + \\"single'quote\\": { type: null, required: true }, + \\"round(brack)ets\\": { type: null, required: true }, + \\"aste*risk\\": { type: null, required: true }, + \\"pl+us\\": { type: null, required: true }, + \\"com,ma\\": { type: null, required: true }, + \\"do.t\\": { type: null, required: true }, + \\"sla/sh\\": { type: null, required: true }, + \\"co:lon\\": { type: null, required: true }, + \\"semi;colon\\": { type: null, required: true }, + \\"angleets\\": { type: null, required: true }, + \\"equal=sign\\": { type: null, required: true }, + \\"question?mark\\": { type: null, required: true }, + \\"at@sign\\": { type: null, required: true }, + \\"square[brack]ets\\": { type: null, required: true }, + \\"back\\\\\\\\slash\\": { type: null, required: true }, + \\"ca^ret\\": { type: null, required: true }, + \\"back\`tick\\": { type: null, required: true }, + \\"curly{bra}ces\\": { type: null, required: true }, + \\"pi|pe\\": { type: null, required: true }, + \\"til~de\\": { type: null, required: true }, + \\"da-sh\\": { type: null, required: true } + }, + setup(__props: any, { expose: __expose }) { + __expose(); + + + return { } } @@ -232,6 +277,7 @@ export default /*#__PURE__*/_defineComponent({ alias: { type: Array, required: true }, method: { type: Function, required: true }, symbol: { type: Symbol, required: true }, + error: { type: Error, required: true }, extract: { type: Number, required: true }, exclude: { type: [Number, Boolean], required: true }, uppercase: { type: String, required: true }, diff --git a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts index 13b981db3d7..0d1a41e0f2b 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts @@ -80,6 +80,18 @@ const emit = defineEmits(['a', 'b']) expect(content).toMatch(`emits: ["foo", "bar"]`) }) + test('w/ type (interface w/ extends)', () => { + const { content } = compile(` + + `) + assertCode(content) + expect(content).toMatch(`emits: ["bar", "foo"]`) + }) + test('w/ type (exported interface)', () => { const { content } = compile(` `) + assertCode(content) + expect(content).toMatch(`"spa ce": { type: null, required: true }`) + expect(content).toMatch( + `"exclamation!mark": { type: null, required: true }` + ) + expect(content).toMatch(`"double\\"quote": { type: null, required: true }`) + expect(content).toMatch(`"hash#tag": { type: null, required: true }`) + expect(content).toMatch(`"dollar$sign": { type: null, required: true }`) + expect(content).toMatch(`"percentage%sign": { type: null, required: true }`) + expect(content).toMatch(`"amper&sand": { type: null, required: true }`) + expect(content).toMatch(`"single'quote": { type: null, required: true }`) + expect(content).toMatch(`"round(brack)ets": { type: null, required: true }`) + expect(content).toMatch(`"aste*risk": { type: null, required: true }`) + expect(content).toMatch(`"pl+us": { type: null, required: true }`) + expect(content).toMatch(`"com,ma": { type: null, required: true }`) + expect(content).toMatch(`"do.t": { type: null, required: true }`) + expect(content).toMatch(`"sla/sh": { type: null, required: true }`) + expect(content).toMatch(`"co:lon": { type: null, required: true }`) + expect(content).toMatch(`"semi;colon": { type: null, required: true }`) + expect(content).toMatch(`"angleets": { type: null, required: true }`) + expect(content).toMatch(`"equal=sign": { type: null, required: true }`) + expect(content).toMatch(`"question?mark": { type: null, required: true }`) + expect(content).toMatch(`"at@sign": { type: null, required: true }`) + expect(content).toMatch( + `"square[brack]ets": { type: null, required: true }` + ) + expect(content).toMatch(`"back\\\\slash": { type: null, required: true }`) + expect(content).toMatch(`"ca^ret": { type: null, required: true }`) + expect(content).toMatch(`"back\`tick": { type: null, required: true }`) + expect(content).toMatch(`"curly{bra}ces": { type: null, required: true }`) + expect(content).toMatch(`"pi|pe": { type: null, required: true }`) + expect(content).toMatch(`"til~de": { type: null, required: true }`) + expect(content).toMatch(`"da-sh": { type: null, required: true }`) + expect(bindings).toStrictEqual({ + 'spa ce': BindingTypes.PROPS, + 'exclamation!mark': BindingTypes.PROPS, + 'double"quote': BindingTypes.PROPS, + 'hash#tag': BindingTypes.PROPS, + dollar$sign: BindingTypes.PROPS, + 'percentage%sign': BindingTypes.PROPS, + 'amper&sand': BindingTypes.PROPS, + "single'quote": BindingTypes.PROPS, + 'round(brack)ets': BindingTypes.PROPS, + 'aste*risk': BindingTypes.PROPS, + 'pl+us': BindingTypes.PROPS, + 'com,ma': BindingTypes.PROPS, + 'do.t': BindingTypes.PROPS, + 'sla/sh': BindingTypes.PROPS, + 'co:lon': BindingTypes.PROPS, + 'semi;colon': BindingTypes.PROPS, + 'angleets': BindingTypes.PROPS, + 'equal=sign': BindingTypes.PROPS, + 'question?mark': BindingTypes.PROPS, + 'at@sign': BindingTypes.PROPS, + 'square[brack]ets': BindingTypes.PROPS, + 'back\\slash': BindingTypes.PROPS, + 'ca^ret': BindingTypes.PROPS, + 'back`tick': BindingTypes.PROPS, + 'curly{bra}ces': BindingTypes.PROPS, + 'pi|pe': BindingTypes.PROPS, + 'til~de': BindingTypes.PROPS, + 'da-sh': BindingTypes.PROPS + }) + }) }) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index fc600f1a518..5f421708af0 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -481,25 +481,28 @@ describe('resolveType', () => { test.runIf(process.platform === 'win32')('relative ts on Windows', () => { const files = { - 'C:\\Test\\foo.ts': 'export type P = { foo: number }', - 'C:\\Test\\bar.d.ts': + 'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }', + 'C:\\Test\\FolderA\\bar.d.ts': 'type X = { bar: string }; export { X as Y };' + // verify that we can parse syntax that is only valid in d.ts - 'export const baz: boolean' + 'export const baz: boolean', + 'C:\\Test\\FolderB\\buz.ts': 'export type Z = { buz: string }' } const { props, deps } = resolve( ` import { P } from './foo' import { Y as PP } from './bar' - defineProps

() + import { Z as PPP } from '../FolderB/buz' + defineProps

() `, files, {}, - 'C:\\Test\\Test.vue' + 'C:\\Test\\FolderA\\Test.vue' ) expect(props).toStrictEqual({ foo: ['Number'], - bar: ['String'] + bar: ['String'], + buz: ['String'] }) expect(deps && [...deps].map(normalize)).toStrictEqual( Object.keys(files).map(normalize) diff --git a/packages/compiler-sfc/__tests__/compileStyle.spec.ts b/packages/compiler-sfc/__tests__/compileStyle.spec.ts index b33dabfd2ce..66fc201f547 100644 --- a/packages/compiler-sfc/__tests__/compileStyle.spec.ts +++ b/packages/compiler-sfc/__tests__/compileStyle.spec.ts @@ -85,6 +85,16 @@ describe('SFC scoped CSS', () => { ".baz .qux[data-v-test] .foo .bar { color: red; }" `) + expect(compileScoped(`:is(.foo :deep(.bar)) { color: red; }`)) + .toMatchInlineSnapshot(` + ":is(.foo[data-v-test] .bar) { color: red; + }" + `) + expect(compileScoped(`:where(.foo :deep(.bar)) { color: red; }`)) + .toMatchInlineSnapshot(` + ":where(.foo[data-v-test] .bar) { color: red; + }" + `) }) test('::v-slotted', () => { diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 6a1fc8bba3a..4d55ffb0820 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -32,27 +32,27 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { - "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.6", - "@vue/compiler-dom": "3.3.6", - "@vue/compiler-ssr": "3.3.6", - "@vue/reactivity-transform": "3.3.6", - "@vue/shared": "3.3.6", + "@babel/parser": "^7.23.3", + "@vue/compiler-core": "3.3.8", + "@vue/compiler-dom": "3.3.8", + "@vue/compiler-ssr": "3.3.8", + "@vue/reactivity-transform": "3.3.8", + "@vue/shared": "3.3.8", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", "postcss": "^8.4.31", "source-map-js": "^1.0.2" }, "devDependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.3", "@vue/consolidate": "^0.17.3", "hash-sum": "^2.0.0", - "lru-cache": "^10.0.1", + "lru-cache": "^10.0.2", "merge-source-map": "^1.1.0", "minimatch": "^9.0.3", "postcss-modules": "^4.3.1", "postcss-selector-parser": "^6.0.13", "pug": "^3.0.2", - "sass": "^1.69.4" + "sass": "^1.69.5" } } diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index fbd100c9784..b036619c794 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -212,6 +212,7 @@ function doCompileTemplate({ slotted, sourceMap: true, ...compilerOptions, + hmr: !isProd, nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []), filename, onError: e => errors.push(e), diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 76b4900d46d..c63e302e0c3 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -1,13 +1,17 @@ export const version = __VERSION__ // API -export { parse, parseCache } from './parse' +export { parse } from './parse' export { compileTemplate } from './compileTemplate' export { compileStyle, compileStyleAsync } from './compileStyle' export { compileScript } from './compileScript' export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' export { resolveTypeElements, inferRuntimeType } from './script/resolveType' +import { SFCParseResult, parseCache as _parseCache } from './parse' +// #9521 export parseCache as a simple map to avoid exposing LRU types +export const parseCache = _parseCache as Map + // TODO remove in 3.4 export { shouldTransform as shouldTransformRef, diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 692eab3ab9e..b05b8d910ee 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -164,7 +164,7 @@ export function resolveParserPlugins( } if (lang === 'ts' || lang === 'tsx') { plugins.push(['typescript', { dts }]) - if (!plugins.includes('decorators')) { + if (!userPlugins || !userPlugins.includes('decorators')) { plugins.push('decorators-legacy') } } diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 5004e314da1..9de15b92b76 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -17,7 +17,7 @@ import { isCallOf, unwrapTSNode, toRuntimeTypeString, - getEscapedKey + getEscapedPropName } from './utils' import { genModelProps } from './defineModel' import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings' @@ -135,7 +135,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { const defaults: string[] = [] for (const key in ctx.propsDestructuredBindings) { const d = genDestructuredDefaultValue(ctx, key) - const finalKey = getEscapedKey(key) + const finalKey = getEscapedPropName(key) if (d) defaults.push( `${finalKey}: ${d.valueString}${ @@ -251,7 +251,7 @@ function genRuntimePropFromType( } } - const finalKey = getEscapedKey(key) + const finalKey = getEscapedPropName(key) if (!ctx.options.isProd) { return `${finalKey}: { ${concatStrings([ `type: ${toRuntimeTypeString(type)}`, diff --git a/packages/compiler-sfc/src/script/importUsageCheck.ts b/packages/compiler-sfc/src/script/importUsageCheck.ts index f3c3932d829..34e95f1918b 100644 --- a/packages/compiler-sfc/src/script/importUsageCheck.ts +++ b/packages/compiler-sfc/src/script/importUsageCheck.ts @@ -4,6 +4,7 @@ import { NodeTypes, SimpleExpressionNode, createRoot, + forAliasRE, parserOptions, transform, walkIdentifiers @@ -50,12 +51,14 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) { if (!isBuiltInDirective(prop.name)) { code += `,v${capitalize(camelize(prop.name))}` } + + // process dynamic directive arguments if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) { - code += `,${processExp( - (prop.arg as SimpleExpressionNode).content, - prop.name + code += `,${stripStrings( + (prop.arg as SimpleExpressionNode).content )}` } + if (prop.exp) { code += `,${processExp( (prop.exp as SimpleExpressionNode).content, @@ -85,8 +88,6 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) { return code } -const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ - function processExp(exp: string, dir?: string): string { if (/ as\s+\w|<.*>|:/.test(exp)) { if (dir === 'slot') { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 215081dc0b7..898ec38fec2 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -39,8 +39,9 @@ import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' import { createCache } from '../cache' import type TS from 'typescript' -import { extname, dirname } from 'path' +import { extname, dirname, join } from 'path' import { minimatch as isMatch } from 'minimatch' +import * as process from 'process' /** * TypeResolveContext is compatible with ScriptCompileContext @@ -334,12 +335,15 @@ function resolveInterfaceMembers( continue } try { - const { props } = resolveTypeElements(ctx, ext, scope) + const { props, calls } = resolveTypeElements(ctx, ext, scope) for (const key in props) { if (!hasOwn(base.props, key)) { base.props[key] = props[key] } } + if (calls) { + ;(base.calls || (base.calls = [])).push(...calls) + } } catch (e) { ctx.error( `Failed to resolve extends base type.\nIf this previously worked in 3.2, ` + @@ -776,7 +780,12 @@ function importSourceToScope( let resolved: string | undefined = scope.resolvedImportSources[source] if (!resolved) { - if (source.startsWith('.')) { + if (source.startsWith('..')) { + const osSpecificJoinFn = process.platform === 'win32' ? join : joinPaths + + const filename = osSpecificJoinFn(dirname(scope.filename), source) + resolved = resolveExt(filename, fs) + } else if (source.startsWith('.')) { // relative import - fast path const filename = joinPaths(dirname(scope.filename), source) resolved = resolveExt(filename, fs) @@ -1391,6 +1400,7 @@ export function inferRuntimeType( case 'WeakMap': case 'Date': case 'Promise': + case 'Error': return [node.typeName.name] // TS built-in utility types diff --git a/packages/compiler-sfc/src/script/utils.ts b/packages/compiler-sfc/src/script/utils.ts index 42c4718e3a8..8afadf63ee4 100644 --- a/packages/compiler-sfc/src/script/utils.ts +++ b/packages/compiler-sfc/src/script/utils.ts @@ -113,8 +113,14 @@ export const joinPaths = (path.posix || path).join * key may contain symbols * e.g. onUpdate:modelValue -> "onUpdate:modelValue" */ -export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g +export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/ -export function getEscapedKey(key: string) { - return escapeSymbolsRE.test(key) ? JSON.stringify(key) : key +export function getEscapedPropName(key: string) { + return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key +} + +export const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g + +export function getEscapedCssVarName(key: string) { + return key.replace(cssVarNameEscapeSymbolsRE, s => `\\${s}`) } diff --git a/packages/compiler-sfc/src/style/cssVars.ts b/packages/compiler-sfc/src/style/cssVars.ts index 2380959b819..9fe727bc5dc 100644 --- a/packages/compiler-sfc/src/style/cssVars.ts +++ b/packages/compiler-sfc/src/style/cssVars.ts @@ -8,7 +8,7 @@ import { BindingMetadata } from '@vue/compiler-dom' import { SFCDescriptor } from '../parse' -import { escapeSymbolsRE } from '../script/utils' +import { getEscapedCssVarName } from '../script/utils' import { PluginCreator } from 'postcss' import hash from 'hash-sum' @@ -32,7 +32,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string { return hash(id + raw) } else { // escape ASCII Punctuation & Symbols - return `${id}-${raw.replace(escapeSymbolsRE, s => `\\${s}`)}` + return `${id}-${getEscapedCssVarName(raw)}` } } diff --git a/packages/compiler-sfc/src/style/pluginScoped.ts b/packages/compiler-sfc/src/style/pluginScoped.ts index f6e9be2fde7..5394b77743f 100644 --- a/packages/compiler-sfc/src/style/pluginScoped.ts +++ b/packages/compiler-sfc/src/style/pluginScoped.ts @@ -173,6 +173,11 @@ function rewriteSelector( if (n.type !== 'pseudo' && n.type !== 'combinator') { node = n } + + if (n.type === 'pseudo' && (n.value === ':is' || n.value === ':where')) { + rewriteSelector(id, n.nodes[0], selectorRoot, slotted) + shouldInject = false + } }) if (node) { diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 9391c01e37e..a8ea08a5349 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -181,11 +181,14 @@ describe('ssr: components', () => { }) test('v-for slot', () => { - expect( - compile(` - - `).code - ).toMatchInlineSnapshot(` + const { code } = compile(` + + `) + expect(code).not.toMatch(`_ctx.msg`) + expect(code).not.toMatch(`_ctx.key`) + expect(code).not.toMatch(`_ctx.index`) + expect(code).toMatch(`_ctx.bar`) + expect(code).toMatchInlineSnapshot(` "const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\") const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\") @@ -193,15 +196,15 @@ describe('ssr: components', () => { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [ - _renderList(_ctx.names, (key) => { + _renderList(_ctx.names, (key, index) => { return { name: key, fn: _withCtx(({ msg }, _push, _parent, _scopeId) => { if (_push) { - _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`) + _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`) } else { return [ - _createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */) + _createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */) ] } }) diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts new file mode 100644 index 00000000000..319b3902239 --- /dev/null +++ b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts @@ -0,0 +1,25 @@ +import { compile } from '../src' + +describe('transition', () => { + test('basic', () => { + expect(compile(`

foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`foo
\`) + }" + `) + }) + + test('with appear', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index dc4933349f0..6ba15970340 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.6", - "@vue/compiler-dom": "3.3.6" + "@vue/shared": "3.3.8", + "@vue/compiler-dom": "3.3.8" } } diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index 1775af52bfc..1e5f9055064 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -72,7 +72,7 @@ export function compile( // reusing core v-bind bind: transformBind, on: transformOn, - // model and show has dedicated SSR handling + // model and show have dedicated SSR handling model: ssrTransformModel, show: ssrTransformShow, // the following are ignored during SSR diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index dc8c6a4ae4f..7a12cb29009 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -56,6 +56,10 @@ import { } from './ssrTransformTransitionGroup' import { isSymbol, isObject, isArray } from '@vue/shared' import { buildSSRProps } from './ssrTransformElement' +import { + ssrProcessTransition, + ssrTransformTransition +} from './ssrTransformTransition' // We need to construct the slot functions in the 1st pass to ensure proper // scope tracking, but the children of each slot cannot be processed until @@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { if (isSymbol(component)) { if (component === SUSPENSE) { return ssrTransformSuspense(node, context) - } - if (component === TRANSITION_GROUP) { + } else if (component === TRANSITION_GROUP) { return ssrTransformTransitionGroup(node, context) + } else if (component === TRANSITION) { + return ssrTransformTransition(node, context) } return // other built-in components: fallthrough } @@ -120,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { // fallback in case the child is render-fn based). Store them in an array // for later use. if (clonedNode.children.length) { - buildSlots(clonedNode, context, (props, children) => { - vnodeBranches.push(createVNodeSlotBranch(props, children, context)) + buildSlots(clonedNode, context, (props, vFor, children) => { + vnodeBranches.push( + createVNodeSlotBranch(props, vFor, children, context) + ) return createFunctionExpression(undefined) }) } @@ -145,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { const wipEntries: WIPSlotEntry[] = [] wipMap.set(node, wipEntries) - const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { + const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => { const param0 = (props && stringifyExpression(props)) || `_` const fn = createFunctionExpression( [param0, `_push`, `_parent`, `_scopeId`], @@ -216,9 +223,8 @@ export function ssrProcessComponent( if ((parent as WIPSlotEntry).type === WIP_SLOT) { context.pushStringPart(``) } - // #5351: filter out comment children inside transition if (component === TRANSITION) { - node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + return ssrProcessTransition(node, context) } processChildren(node, context) } @@ -273,6 +279,7 @@ const vnodeDirectiveTransforms = { function createVNodeSlotBranch( props: ExpressionNode | undefined, + vForExp: ExpressionNode | undefined, children: TemplateChildNode[], parentContext: TransformContext ): ReturnStatement { @@ -299,8 +306,8 @@ function createVNodeSlotBranch( tag: 'template', tagType: ElementTypes.TEMPLATE, isSelfClosing: false, - // important: provide v-slot="props" on the wrapper for proper - // scope analysis + // important: provide v-slot="props" and v-for="exp" on the wrapper for + // proper scope analysis props: [ { type: NodeTypes.DIRECTIVE, @@ -309,6 +316,14 @@ function createVNodeSlotBranch( arg: undefined, modifiers: [], loc: locStub + }, + { + type: NodeTypes.DIRECTIVE, + name: 'for', + exp: vForExp, + arg: undefined, + modifiers: [], + loc: locStub } ], children, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts index 207e9348eef..e7efbe1fb73 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts @@ -36,20 +36,24 @@ export function ssrTransformSuspense( wipSlots: [] } wipMap.set(node, wipEntry) - wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => { - const fn = createFunctionExpression( - [], - undefined, // no return, assign body later - true, // newline - false, // suspense slots are not treated as normal slots - loc - ) - wipEntry.wipSlots.push({ - fn, - children - }) - return fn - }).slots + wipEntry.slotsExp = buildSlots( + node, + context, + (_props, _vForExp, children, loc) => { + const fn = createFunctionExpression( + [], + undefined, // no return, assign body later + true, // newline + false, // suspense slots are not treated as normal slots + loc + ) + wipEntry.wipSlots.push({ + fn, + children + }) + return fn + } + ).slots } } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts new file mode 100644 index 00000000000..d09a806f7b0 --- /dev/null +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts @@ -0,0 +1,36 @@ +import { + ComponentNode, + findProp, + NodeTypes, + TransformContext +} from '@vue/compiler-dom' +import { processChildren, SSRTransformContext } from '../ssrCodegenTransform' + +const wipMap = new WeakMap() + +export function ssrTransformTransition( + node: ComponentNode, + context: TransformContext +) { + return () => { + const appear = findProp(node, 'appear', false, true) + wipMap.set(node, !!appear) + } +} + +export function ssrProcessTransition( + node: ComponentNode, + context: SSRTransformContext +) { + // #5351: filter out comment children inside transition + node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + + const appear = wipMap.get(node) + if (appear) { + context.pushStringPart(``) + } else { + processChildren(node, context, false, true) + } +} diff --git a/packages/dts-built-test/README.md b/packages/dts-built-test/README.md new file mode 100644 index 00000000000..8191d66e32e --- /dev/null +++ b/packages/dts-built-test/README.md @@ -0,0 +1,5 @@ +# dts built-package test + +This package is private and for testing only. It is used to verify edge cases for external libraries that build their types using Vue core types - e.g. Vuetify as in [#8376](https://github.com/vuejs/core/issues/8376). + +When running the `build-dts` task, this package's types are built alongside other packages. Then, during `test-dts-only` it is imported and used in [`packages/dts-test/built.test-d.ts`](https://github.com/vuejs/core/blob/main/packages/dts-test/built.test-d.ts) to verify that the built types work correctly. diff --git a/packages/dts-built-test/package.json b/packages/dts-built-test/package.json new file mode 100644 index 00000000000..427bd9c0c4b --- /dev/null +++ b/packages/dts-built-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vue/dts-built-test", + "private": true, + "types": "dist/dts-built-test.d.ts", + "dependencies": { + "@vue/shared": "workspace:*", + "@vue/reactivity": "workspace:*", + "vue": "workspace:*" + }, + "version": "3.3.8" +} diff --git a/packages/dts-built-test/src/index.ts b/packages/dts-built-test/src/index.ts new file mode 100644 index 00000000000..2d9d4033254 --- /dev/null +++ b/packages/dts-built-test/src/index.ts @@ -0,0 +1,12 @@ +import { defineComponent } from 'vue' + +const _CustomPropsNotErased = defineComponent({ + props: {}, + setup() {} +}) + +// #8376 +export const CustomPropsNotErased = + _CustomPropsNotErased as typeof _CustomPropsNotErased & { + foo: string + } diff --git a/packages/dts-test/built.test-d.ts b/packages/dts-test/built.test-d.ts new file mode 100644 index 00000000000..8ac3e333f99 --- /dev/null +++ b/packages/dts-test/built.test-d.ts @@ -0,0 +1,13 @@ +import { CustomPropsNotErased } from '@vue/dts-built-test' +import { expectType, describe } from './utils' + +declare module 'vue' { + interface ComponentCustomProps { + custom?: number + } +} + +// #8376 - custom props should not be erased +describe('Custom Props not erased', () => { + expectType(new CustomPropsNotErased().$props.custom) +}) diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index abb1d592e89..f19121c77fe 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -1476,6 +1476,31 @@ describe('slots', () => { expectType(new comp2().$slots) }) +// #5885 +describe('should work when props type is incompatible with setup returned type ', () => { + type SizeType = 'small' | 'big' + const Comp = defineComponent({ + props: { + size: { + type: String as PropType, + required: true + } + }, + setup(props) { + expectType(props.size) + return { + size: 1 + } + } + }) + type CompInstance = InstanceType + + const CompA = {} as CompInstance + expectType(CompA) + expectType(CompA.size) + expectType(CompA.$props.size) +}) + // #3367 expose components types describe('expose component types', () => { const child = defineComponent({ diff --git a/packages/dts-test/defineCustomElement.test-d.ts b/packages/dts-test/defineCustomElement.test-d.ts index 4e7cf228372..f4f238641db 100644 --- a/packages/dts-test/defineCustomElement.test-d.ts +++ b/packages/dts-test/defineCustomElement.test-d.ts @@ -1,5 +1,9 @@ -import { defineCustomElement } from 'vue' -import { expectType, describe } from './utils' +import { + defineCustomElement, + defineComponent, + type VueElementConstructor +} from 'vue' +import { expectType, describe, test } from './utils' describe('inject', () => { // with object inject @@ -62,3 +66,20 @@ describe('inject', () => { } }) }) + +describe('defineCustomElement using defineComponent return type', () => { + test('with emits', () => { + const Comp1Vue = defineComponent({ + props: { + a: String + }, + emits: { + click: () => true + } + }) + const Comp = defineCustomElement(Comp1Vue) + expectType(Comp) + + expectType(new Comp().a) + }) +}) diff --git a/packages/dts-test/h.test-d.ts b/packages/dts-test/h.test-d.ts index 5c700800e94..f2e984b49b8 100644 --- a/packages/dts-test/h.test-d.ts +++ b/packages/dts-test/h.test-d.ts @@ -1,6 +1,7 @@ import { h, defineComponent, + DefineComponent, ref, Fragment, Teleport, @@ -231,3 +232,18 @@ describe('resolveComponent should work', () => { message: '1' }) }) + +// #5431 +describe('h should work with multiple types', () => { + const serializers = { + Paragraph: 'p', + Component: {} as Component, + DefineComponent: {} as DefineComponent + } + + const sampleComponent = serializers['' as keyof typeof serializers] + + h(sampleComponent) + h(sampleComponent, {}) + h(sampleComponent, {}, []) +}) diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index da8424e254c..07710bcc8d5 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -2,7 +2,8 @@ "name": "dts-test", "private": true, "dependencies": { - "vue": "workspace:*" + "vue": "workspace:*", + "@vue/dts-built-test": "workspace:*" }, - "version": "3.3.6" + "version": "3.3.8" } diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index bbcde45ddda..542d9d6a9ef 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -15,9 +15,10 @@ import { MaybeRef, MaybeRefOrGetter, ComputedRef, - computed + computed, + ShallowRef } from 'vue' -import { expectType, describe } from './utils' +import { expectType, describe, IsUnion } from './utils' function plainType(arg: number | Ref) { // ref coercing @@ -174,6 +175,27 @@ if (refStatus.value === 'initial') { refStatus.value = 'invalidating' } +{ + const shallow = shallowRef(1) + expectType>(shallow) + expectType>(shallow) +} + +{ + //#7852 + type Steps = { step: '1' } | { step: '2' } + const shallowUnionGenParam = shallowRef({ step: '1' }) + const shallowUnionAsCast = shallowRef({ step: '1' } as Steps) + + expectType>(false) + expectType>(false) +} + +describe('shallowRef with generic', () => { + const r = ref({}) as MaybeRef + expectType | Ref>(shallowRef(r)) +}) + // proxyRefs: should return `reactive` directly const r1 = reactive({ k: 'v' diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index feb4085dea0..51f95c00944 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -8,7 +8,8 @@ import { defineSlots, VNode, Ref, - defineModel + defineModel, + toRefs } from 'vue' import { describe, expectType } from './utils' import { defineComponent } from 'vue' @@ -20,6 +21,7 @@ describe('defineProps w/ type declaration', () => { foo: string bool?: boolean boolAndUndefined: boolean | undefined + file?: File | File[] }>() // explicitly declared type should be refined expectType(props.foo) @@ -108,6 +110,7 @@ describe('defineProps w/ generic type declaration + withDefaults', (res.generic1) expectType<{ x: T }>(res.generic2) @@ -328,3 +335,11 @@ describe('useSlots', () => { const slots = useSlots() expectType(slots) }) + +// #6420 +describe('toRefs w/ type declaration', () => { + const props = defineProps<{ + file?: File | File[] + }>() + expectType>(toRefs(props).file) +}) diff --git a/packages/dts-test/tsx.test-d.tsx b/packages/dts-test/tsx.test-d.tsx index 04915a9673f..4b4a0dbf9df 100644 --- a/packages/dts-test/tsx.test-d.tsx +++ b/packages/dts-test/tsx.test-d.tsx @@ -17,6 +17,59 @@ expectType(
) +// allow undefined, string, object, array and nested array classes +expectType(
) +expectType(
) +expectType(
) +expectType(
) +expectType(
) +expectType(
) +expectType(
) +expectType( +
+) +expectType( +
+) +expectType( +
+) + +// #7955 +expectType(
) + +expectType(
) + +expectType(
) + +expectType(
) + +expectType(
) + +// @ts-expect-error +;
+ +// @ts-expect-error +;
+ // @ts-expect-error unknown prop ;
diff --git a/packages/dts-test/watch.test-d.ts b/packages/dts-test/watch.test-d.ts index 9d727999bcc..cc631d37acb 100644 --- a/packages/dts-test/watch.test-d.ts +++ b/packages/dts-test/watch.test-d.ts @@ -1,4 +1,4 @@ -import { ref, computed, watch, defineComponent } from 'vue' +import { ref, computed, watch, defineComponent, shallowRef } from 'vue' import { expectType } from './utils' const source = ref('foo') @@ -92,3 +92,17 @@ defineComponent({ ) } }) + +{ + //#7852 + type Steps = { step: '1' } | { step: '2' } + const shallowUnionGenParam = shallowRef({ step: '1' }) + const shallowUnionAsCast = shallowRef({ step: '1' } as Steps) + + watch(shallowUnionGenParam, value => { + expectType(value) + }) + watch(shallowUnionAsCast, value => { + expectType(value) + }) +} diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index 887fedabc6e..48749f0bb31 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -28,14 +28,14 @@ }, "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { - "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.6", - "@vue/shared": "3.3.6", + "@babel/parser": "^7.23.3", + "@vue/compiler-core": "3.3.8", + "@vue/shared": "3.3.8", "estree-walker": "^2.0.2", "magic-string": "^0.30.5" }, "devDependencies": { - "@babel/core": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/core": "^7.23.3", + "@babel/types": "^7.23.3" } } diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index d0c91f0fb9f..448419d3176 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -275,6 +275,14 @@ describe('reactivity/readonly', () => { expect(isReactive(value)).toBe(true) } }) + + test('should return undefined from Map.clear() call', () => { + const wrapped = readonly(new Collection()) + expect(wrapped.clear()).toBeUndefined() + expect( + `Clear operation failed: target is readonly.` + ).toHaveBeenWarned() + }) } }) }) @@ -332,6 +340,14 @@ describe('reactivity/readonly', () => { expect(isReadonly(v2)).toBe(true) } }) + + test('should return undefined from Set.clear() call', () => { + const wrapped = readonly(new Collection()) + expect(wrapped.clear()).toBeUndefined() + expect( + `Clear operation failed: target is readonly.` + ).toHaveBeenWarned() + }) } }) }) diff --git a/packages/reactivity/__tests__/shallowReadonly.spec.ts b/packages/reactivity/__tests__/shallowReadonly.spec.ts index 79d4376cc01..b6736f4a595 100644 --- a/packages/reactivity/__tests__/shallowReadonly.spec.ts +++ b/packages/reactivity/__tests__/shallowReadonly.spec.ts @@ -113,6 +113,12 @@ describe('reactivity/shallowReadonly', () => { ).not.toHaveBeenWarned() }) }) + + test('should return undefined from Map.clear() call', () => { + const sroMap = shallowReadonly(new Map()) + expect(sroMap.clear()).toBeUndefined() + expect(`Clear operation failed: target is readonly.`).toHaveBeenWarned() + }) }) describe('collection/Set', () => { @@ -197,5 +203,11 @@ describe('reactivity/shallowReadonly', () => { ).not.toHaveBeenWarned() }) }) + + test('should return undefined from Set.clear() call', () => { + const sroSet = shallowReadonly(new Set()) + expect(sroSet.clear()).toBeUndefined() + expect(`Clear operation failed: target is readonly.`).toHaveBeenWarned() + }) }) }) diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index ac49b4ec7d2..6582c7683b7 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.6", + "version": "3.3.8", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.6" + "@vue/shared": "3.3.8" } } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 1d07af3be8c..fe7d13d1841 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -223,7 +223,11 @@ function createReadonlyMethod(type: TriggerOpTypes): Function { toRaw(this) ) } - return type === TriggerOpTypes.DELETE ? false : this + return type === TriggerOpTypes.DELETE + ? false + : type === TriggerOpTypes.CLEAR + ? undefined + : this } } diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 915f5760878..61e0bf6c13d 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,3 +1,4 @@ +import type { ComputedRef } from './computed' import { activeEffect, getDepFromReactive, @@ -115,9 +116,8 @@ export type ShallowRef = Ref & { [ShallowRefMarker]?: true } * @param value - The "inner value" for the shallow ref. * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref} */ -export function shallowRef( - value: T -): T extends Ref ? T : ShallowRef +export function shallowRef(value: MaybeRef): Ref | ShallowRef +export function shallowRef(value: T): T export function shallowRef(value: T): ShallowRef export function shallowRef(): ShallowRef export function shallowRef(value?: unknown) { @@ -211,7 +211,7 @@ export type MaybeRefOrGetter = MaybeRef | (() => T) * @param ref - Ref or plain value to be converted into the plain value. * @see {@link https://vuejs.org/api/reactivity-utilities.html#unref} */ -export function unref(ref: MaybeRef): T { +export function unref(ref: MaybeRef | ComputedRef): T { return isRef(ref) ? ref.value : ref } @@ -231,7 +231,7 @@ export function unref(ref: MaybeRef): T { * @param source - A getter, an existing ref, or a non-function value. * @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue} */ -export function toValue(source: MaybeRefOrGetter): T { +export function toValue(source: MaybeRefOrGetter | ComputedRef): T { return isFunction(source) ? source() : unref(source) } diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index ca712e0d3ac..94f9db1b028 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -1,7 +1,7 @@ /** * @vitest-environment jsdom */ -import { vi, type Mock } from 'vitest' +import { type Mock } from 'vitest' import { h, nodeOps, diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index f24ce80b9df..48fdd2888ec 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -91,7 +91,7 @@ describe('api: watch', () => { array.push(1) await nextTick() expect(spy).toBeCalledTimes(1) - expect(spy).toBeCalledWith([1], expect.anything(), expect.anything()) + expect(spy).toBeCalledWith([1], [1], expect.anything()) }) it('should not fire if watched getter result did not change', async () => { @@ -1205,4 +1205,39 @@ describe('api: watch', () => { expect(countWE).toBe(3) expect(countW).toBe(2) }) + + // #5151 + test('OnCleanup also needs to be cleaned,', async () => { + const spy1 = vi.fn() + const spy2 = vi.fn() + const num = ref(0) + + watch(num, (value, oldValue, onCleanup) => { + if (value > 1) { + return + } + spy1() + onCleanup(() => { + // OnCleanup also needs to be cleaned + spy2() + }) + }) + + num.value++ + await nextTick() + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(0) + + num.value++ + await nextTick() + + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(1) + + num.value++ + await nextTick() + // would not be calld when value>1 + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 89112f2ad49..885e80090a1 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -336,7 +336,8 @@ describe('component props', () => { obj: { type: Object }, cls: { type: MyClass }, fn: { type: Function }, - skipCheck: { type: [Boolean, Function], skipCheck: true } + skipCheck: { type: [Boolean, Function], skipCheck: true }, + empty: { type: [] } }, setup() { return () => null @@ -351,7 +352,8 @@ describe('component props', () => { obj: 'false', cls: {}, fn: true, - skipCheck: 'foo' + skipCheck: 'foo', + empty: [1, 2, 3] }), nodeOps.createElement('div') ) @@ -379,6 +381,9 @@ describe('component props', () => { expect( `Invalid prop: type check failed for prop "skipCheck". Expected Boolean | Function, got String with value "foo".` ).not.toHaveBeenWarned() + expect( + `Prop type [] for prop "empty" won't match anything. Did you mean to use type Array instead?` + ).toHaveBeenWarned() }) // #3495 diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 065898048c3..d822a992816 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -17,9 +17,12 @@ import { onUnmounted, onErrorCaptured, shallowRef, + SuspenseProps, + resolveDynamicComponent, Fragment } from '@vue/runtime-test' import { createApp, defineComponent } from 'vue' +import { type RawSlots } from 'packages/runtime-core/src/componentSlots' describe('Suspense', () => { const deps: Promise[] = [] @@ -1523,4 +1526,75 @@ describe('Suspense', () => { expected = `
outerB
innerB
` expect(serializeInner(root)).toBe(expected) }) + + describe('warnings', () => { + // base function to check if a combination of slots warns or not + function baseCheckWarn( + shouldWarn: boolean, + children: RawSlots, + props: SuspenseProps | null = null + ) { + const Comp = { + setup() { + return () => h(Suspense, props, children) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + + if (shouldWarn) { + expect(` slots expect a single root node.`).toHaveBeenWarned() + } else { + expect( + ` slots expect a single root node.` + ).not.toHaveBeenWarned() + } + } + + // actual function that we use in tests + const checkWarn = baseCheckWarn.bind(null, true) + const checkNoWarn = baseCheckWarn.bind(null, false) + + test('does not warn on single child', async () => { + checkNoWarn({ + default: h('div'), + fallback: h('div') + }) + }) + + test('does not warn on null', async () => { + checkNoWarn({ + default: null, + fallback: null + }) + }) + + test('does not warn on ', async () => { + checkNoWarn({ + default: () => [resolveDynamicComponent(null)], + fallback: () => null + }) + }) + + test('does not warn on empty array', async () => { + checkNoWarn({ + default: [], + fallback: () => [] + }) + }) + + test('warns on multiple children in default', async () => { + checkWarn({ + default: [h('div'), h('div')] + }) + }) + + test('warns on multiple children in fallback', async () => { + checkWarn({ + default: h('div'), + fallback: [h('div'), h('div')] + }) + }) + }) }) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index db713a3f276..a5ec90ad7d3 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -20,7 +20,7 @@ const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ registerRuntimeCompiler(compileToFunction) function compileToFunction(template: string) { - const { code } = baseCompile(template) + const { code } = baseCompile(template, { hoistStatic: true, hmr: true }) const render = new Function('Vue', code)( runtimeTest ) as InternalRenderFunction @@ -218,6 +218,75 @@ describe('hot module replacement', () => { expect(deactiveSpy).toHaveBeenCalledTimes(1) }) + // #7121 + test('reload KeepAlive slot in Transition', async () => { + const root = nodeOps.createElement('div') + const childId = 'test-transition-keep-alive-reload' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() + const activeSpy = vi.fn() + const deactiveSpy = vi.fn() + + const Child: ComponentOptions = { + __hmrId: childId, + data() { + return { count: 0 } + }, + unmounted: unmountSpy, + render: compileToFunction(`
{{ count }}
`) + } + createRecord(childId, Child) + + const Parent: ComponentOptions = { + components: { Child }, + data() { + return { toggle: true } + }, + render: compileToFunction( + `` + ) + } + + render(h(Parent), root) + expect(serializeInner(root)).toBe(`
0
`) + + reload(childId, { + __hmrId: childId, + data() { + return { count: 1 } + }, + mounted: mountSpy, + unmounted: unmountSpy, + activated: activeSpy, + deactivated: deactiveSpy, + render: compileToFunction(`
{{ count }}
`) + }) + await nextTick() + expect(serializeInner(root)).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactiveSpy).toHaveBeenCalledTimes(0) + + // should not unmount when toggling + triggerEvent(root.children[1] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(``) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactiveSpy).toHaveBeenCalledTimes(1) + + // should not mount when toggling + triggerEvent(root.children[1] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(2) + expect(deactiveSpy).toHaveBeenCalledTimes(1) + }) + test('reload class component', async () => { const root = nodeOps.createElement('div') const childId = 'test4-child' @@ -567,4 +636,40 @@ describe('hot module replacement', () => { rerender(parentId, compileToFunction(`2`)) expect(serializeInner(root)).toBe(`2`) }) + + // #6978, #7138, #7114 + test('hoisted children array inside v-for', () => { + const root = nodeOps.createElement('div') + const appId = 'test-app-id' + const App: ComponentOptions = { + __hmrId: appId, + render: compileToFunction( + `
+
1
+
+

2

+

3

` + ) + } + createRecord(appId, App) + + render(h(App), root) + expect(serializeInner(root)).toBe( + `
1
1

2

3

` + ) + + // move the

3

into the
1
+ rerender( + appId, + compileToFunction( + `
+
1

3

+
+

2

` + ) + ) + expect(serializeInner(root)).toBe( + `
1

3

1

3

2

` + ) + }) }) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index f0a3a9333a7..7ea607d3380 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -18,10 +18,14 @@ import { createVNode, withDirectives, vModelCheckbox, - renderSlot + renderSlot, + Transition, + createCommentVNode, + vShow } from '@vue/runtime-dom' import { renderToString, SSRContext } from '@vue/server-renderer' -import { PatchFlags } from '../../shared/src' +import { PatchFlags } from '@vue/shared' +import { vShowOldKey } from '../../runtime-dom/src/directives/vShow' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -931,6 +935,18 @@ describe('SSR hydration', () => { ) }) + test('force hydrate prop with `.prop` modifier', () => { + const { container } = mountWithHydration( + '', + () => + h('input', { + type: 'checkbox', + '.indeterminate': true + }) + ) + expect((container.firstChild! as any).indeterminate).toBe(true) + }) + test('force hydrate input v-model with non-string value bindings', () => { const { container } = mountWithHydration( '', @@ -949,6 +965,20 @@ describe('SSR hydration', () => { expect((container.firstChild as any)._trueValue).toBe(true) }) + test('force hydrate checkbox with indeterminate', () => { + const { container } = mountWithHydration( + '', + () => + createVNode( + 'input', + { type: 'checkbox', indeterminate: '' }, + null, + PatchFlags.HOISTED + ) + ) + expect((container.firstChild as any).indeterminate).toBe(true) + }) + test('force hydrate select option with non-string value bindings', () => { const { container } = mountWithHydration( '', @@ -1016,6 +1046,74 @@ describe('SSR hydration', () => { expect(`mismatch`).not.toHaveBeenWarned() }) + test('transition appear', () => { + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => h('div', 'foo') + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` +
+ foo +
+ `) + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-if', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => (show ? h('div', 'foo') : createCommentVNode('')) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-show', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => + withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` + + `) + expect((container.firstChild as any)[vShowOldKey]).toBe('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') @@ -1105,5 +1203,21 @@ describe('SSR hydration', () => { expect(teleportContainer.innerHTML).toBe(`value`) expect(`Hydration children mismatch`).toHaveBeenWarned() }) + + test('comment mismatch (element)', () => { + const { container } = mountWithHydration(`
`, () => + h('div', [createCommentVNode('hi')]) + ) + expect(container.innerHTML).toBe('
') + expect(`Hydration node mismatch`).toHaveBeenWarned() + }) + + test('comment mismatch (text)', () => { + const { container } = mountWithHydration(`
foobar
`, () => + h('div', [createCommentVNode('hi')]) + ) + expect(container.innerHTML).toBe('
') + expect(`Hydration node mismatch`).toHaveBeenWarned() + }) }) }) diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts index 6f1d0288b21..37daafa9725 100644 --- a/packages/runtime-core/__tests__/rendererComponent.spec.ts +++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts @@ -354,4 +354,25 @@ describe('renderer: component', () => { expect(serializeInner(root)).toBe(`

1

`) expect(spy).toHaveBeenCalledTimes(2) }) + + it('should warn accessing `this` in a