From 881360b92b3cfa39ffa0c0ee03bb4319079849a6 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 10 Apr 2026 09:49:52 +0800 Subject: [PATCH 1/6] Support `default never` in Angular v21.2 (#19034) --- package.json | 2 +- .../print/angular-control-flow-block.js | 9 +++++++ .../__snapshots__/format.test.js.snap | 26 +++++++++++++++++++ .../angular/control-flow/default-never.html | 7 +++++ yarn.lock | 10 +++---- 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 tests/format/angular/control-flow/default-never.html diff --git a/package.json b/package.json index 8ea145a6d..d96c25aee 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "acorn": "8.15.0", "acorn-jsx": "5.3.2", "angular-estree-parser": "15.3.0", - "angular-html-parser": "10.3.0", + "angular-html-parser": "10.5.0", "camelcase": "9.0.0", "ci-info": "4.3.1", "cjk-regex": "3.4.0", diff --git a/src/language-html/print/angular-control-flow-block.js b/src/language-html/print/angular-control-flow-block.js index 5f9690403..d55fe5eb2 100644 --- a/src/language-html/print/angular-control-flow-block.js +++ b/src/language-html/print/angular-control-flow-block.js @@ -20,6 +20,11 @@ function printAngularControlFlowBlock(path, options, print) { docs.push("@", node.name); + if (isSwitchExhaustiveCheck(node)) { + docs.push(";"); + return docs; + } + if (node.parameters) { docs.push(" (", group(print("parameters")), ")"); } @@ -53,6 +58,10 @@ function shouldCloseBlock(node) { const isSwitchCaseBlock = (node) => node?.kind === "angularControlFlowBlock" && (node.name === "case" || node.name === "default"); + +const isSwitchExhaustiveCheck = (node) => + node?.kind === "angularControlFlowBlock" && node.name === "default never"; + // https://github.com/angular/angular/commit/0ad3adc7c6d4094f1e3432a3f2e3bdc9862cb4fa#diff-77a6f285c6ea13d6644ef635e7495e71134d1141af2650cb9ae5631ff1f38bf2R263 // https://github.com/prettier/prettier/issues/18563#issuecomment-3728354491 function isSwitchFallthroughCase(node) { diff --git a/tests/format/angular/control-flow/__snapshots__/format.test.js.snap b/tests/format/angular/control-flow/__snapshots__/format.test.js.snap index 57d672535..bc8ba2192 100644 --- a/tests/format/angular/control-flow/__snapshots__/format.test.js.snap +++ b/tests/format/angular/control-flow/__snapshots__/format.test.js.snap @@ -77,6 +77,32 @@ printWidth: 80 ================================================================================ `; +exports[`default-never.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +@switch ( cond.kind) { + @default never; +} + +@switch ( cond.kind) { + @default never ; +} + +=====================================output===================================== +@switch (cond.kind) { + @default never; +} + +@switch (cond.kind) { + @default never; +} + +================================================================================ +`; + exports[`defer.html format 1`] = ` ====================================options===================================== parsers: ["angular"] diff --git a/tests/format/angular/control-flow/default-never.html b/tests/format/angular/control-flow/default-never.html new file mode 100644 index 000000000..9f954fd91 --- /dev/null +++ b/tests/format/angular/control-flow/default-never.html @@ -0,0 +1,7 @@ +@switch ( cond.kind) { + @default never; +} + +@switch ( cond.kind) { + @default never ; +} diff --git a/yarn.lock b/yarn.lock index 889779b78..aafcc381b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2861,10 +2861,10 @@ __metadata: languageName: node linkType: hard -"angular-html-parser@npm:10.3.0": - version: 10.3.0 - resolution: "angular-html-parser@npm:10.3.0" - checksum: 10/97078879db8f53fc9645d2cd8c748c9eb3770790ec96f249bd96e0ab9293f65506b7facd201c17f2f18884e82550f3c362972e7b3a5448b0e1fb2a6938e9ef4d +"angular-html-parser@npm:10.5.0": + version: 10.5.0 + resolution: "angular-html-parser@npm:10.5.0" + checksum: 10/8544df535e0b111b44fbf8996141c63a2024f50912b08efaf28c91002260ad8b71af650c10a41aa6e3a0fd8c8f9750f40d4030c39587904aa816e0c88a108727 languageName: node linkType: hard @@ -7518,7 +7518,7 @@ __metadata: acorn: "npm:8.15.0" acorn-jsx: "npm:5.3.2" angular-estree-parser: "npm:15.3.0" - angular-html-parser: "npm:10.3.0" + angular-html-parser: "npm:10.5.0" base64-arraybuffer-es6: "npm:3.1.0" browserslist: "npm:4.28.0" browserslist-to-esbuild: "npm:2.1.1" From 96bbaeda0525bf758e464aed2f939d739a85c315 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 10 Apr 2026 10:03:27 +0800 Subject: [PATCH 2/6] Support Angular v21.2 (#18722) --- changelog_unreleased/angular/18722.md | 41 ++++++++++ package.json | 4 +- src/language-js/print/function-parameters.js | 5 +- .../__snapshots__/format.test.js.snap | 74 +++++++++++++++++++ .../__snapshots__/format.test.js.snap | 49 ++++++++++++ .../arrow-function-options/format.test.js | 20 +++++ .../angular/expression/arrow-function.html | 25 +++++++ .../expression/instanceof-operator.html | 4 + yarn.lock | 22 +++--- 9 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 changelog_unreleased/angular/18722.md create mode 100644 tests/format/angular/expression/arrow-function-options/__snapshots__/format.test.js.snap create mode 100644 tests/format/angular/expression/arrow-function-options/format.test.js create mode 100644 tests/format/angular/expression/arrow-function.html create mode 100644 tests/format/angular/expression/instanceof-operator.html diff --git a/changelog_unreleased/angular/18722.md b/changelog_unreleased/angular/18722.md new file mode 100644 index 000000000..61e69535e --- /dev/null +++ b/changelog_unreleased/angular/18722.md @@ -0,0 +1,41 @@ +#### Support Angular v21.2 (#18722, #19034 by @fisker) + +Exhaustive typechecking with `@default never;` + + +```html + +@switch (foo) { + @case (1) {} + @default never; +} + + +SyntaxError: Incomplete block "default never". If you meant to write the @ character, you should use the "@" HTML entity instead. (3:3) + + +@switch (foo) { + @case (1) {} + @default never; +} +``` + +`arrow function` and `instanceof` expressions. + + +```html + +@let fn = (a) => a? 1:2; + +{{ fn ( a instanceof b)}} + + +@let fn = (a) => a? 1:2; + +{{ fn ( a instanceof b)}} + + +@let fn = (a) => (a ? 1 : 2); + +{{ fn(a instanceof b) }} +``` diff --git a/package.json b/package.json index d96c25aee..9aa706f22 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "bin" ], "dependencies": { - "@angular/compiler": "21.1.0", + "@angular/compiler": "21.2.8", "@babel/code-frame": "8.0.0-beta.3", "@babel/parser": "8.0.0-beta.3", "@babel/types": "8.0.0-beta.3", @@ -55,7 +55,7 @@ "@typescript-eslint/visitor-keys": "8.48.0", "acorn": "8.15.0", "acorn-jsx": "5.3.2", - "angular-estree-parser": "15.3.0", + "angular-estree-parser": "15.4.3", "angular-html-parser": "10.5.0", "camelcase": "9.0.0", "ci-info": "4.3.1", diff --git a/src/language-js/print/function-parameters.js b/src/language-js/print/function-parameters.js index 2cfb316a8..e9bf9afc9 100644 --- a/src/language-js/print/function-parameters.js +++ b/src/language-js/print/function-parameters.js @@ -157,7 +157,10 @@ function printFunctionParameters( "(", indent([softline, ...printed]), ifBreak( - !hasRestParameter(functionNode) && shouldPrintComma(options, "all") + !hasRestParameter(functionNode) && + shouldPrintComma(options, "all") && + // Angular does not allow trailing comma + path.root.type !== "NGRoot" ? "," : "", ), diff --git a/tests/format/angular/expression/__snapshots__/format.test.js.snap b/tests/format/angular/expression/__snapshots__/format.test.js.snap index 65b9a0f52..4dabdf064 100644 --- a/tests/format/angular/expression/__snapshots__/format.test.js.snap +++ b/tests/format/angular/expression/__snapshots__/format.test.js.snap @@ -1,5 +1,63 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`arrow-function.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +@let fn = (a, b) => componentValue + a + b; +One: {{fn(0, 1)}} + +@if (true) { + Two: {{fn(1, 1)}} + + + +} + + +{{[(a) => a + 1][0](1000)}} +{{[(a ) => a + 1 + componentProp][0](1000)}} + + +@let a = 1; + +@if (true) { + @let b = 2; + + @if (true) { + @let c = 3; + {{(() => a + b + c)()}} + } +} + +=====================================output===================================== +@let fn = (a, b) => componentValue + a + b; One: {{ fn(0, 1) }} + +@if (true) { + Two: {{ fn(1, 1) }} + + + +} + +{{ [(a) => a + 1][0](1000) }} +{{ [(a) => a + 1 + componentProp][0](1000) }} + +@let a = 1; + +@if (true) { + @let b = 2; + + @if (true) { + @let c = 3; + {{ (() => a + b + c)() }} + } +} + +================================================================================ +`; + exports[`exponentiation-operators.html format 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -33,6 +91,22 @@ printWidth: 80 ================================================================================ `; +exports[`instanceof-operator.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +{{ + + ( (a instanceof (b))) +}} + +=====================================output===================================== +{{ a instanceof b }} + +================================================================================ +`; + exports[`spread-element.html format 1`] = ` ====================================options===================================== parsers: ["angular"] diff --git a/tests/format/angular/expression/arrow-function-options/__snapshots__/format.test.js.snap b/tests/format/angular/expression/arrow-function-options/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..71cf5f605 --- /dev/null +++ b/tests/format/angular/expression/arrow-function-options/__snapshots__/format.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`snippet: #0 - {"arrowParens":"avoid"} format 1`] = ` +====================================options===================================== +arrowParens: "avoid" +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +@let fn = ( oneArgument) => 1; +=====================================output===================================== +@let fn = oneArgument => 1; + +================================================================================ +`; + +exports[`snippet: #0 - {"printWidth":1,"trailingComma":"all"} format 1`] = ` +====================================options===================================== +parsers: ["angular"] +printWidth: 1 +trailingComma: "all" + | printWidth: 1 +=====================================input====================================== +@let fn = ( should, not, print,trailing, comma) => 1; +=====================================output===================================== +@let fn = + ( + should, + not, + print, + trailing, + comma + ) => + 1; + +================================================================================ +`; + +exports[`snippet: #1 - {"arrowParens":"avoid"} format 1`] = ` +====================================options===================================== +arrowParens: "avoid" +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +@let fn = oneArgument => 1; +=====================================output===================================== +@let fn = oneArgument => 1; + +================================================================================ +`; diff --git a/tests/format/angular/expression/arrow-function-options/format.test.js b/tests/format/angular/expression/arrow-function-options/format.test.js new file mode 100644 index 000000000..f0dd65135 --- /dev/null +++ b/tests/format/angular/expression/arrow-function-options/format.test.js @@ -0,0 +1,20 @@ +runFormatTest( + { + importMeta: import.meta, + snippets: ["@let fn = ( should, not, print,trailing, comma) => 1;"], + }, + ["angular"], + { printWidth: 1, trailingComma: "all" }, +); + +runFormatTest( + { + importMeta: import.meta, + snippets: [ + "@let fn = ( oneArgument) => 1;", + "@let fn = oneArgument => 1;", + ], + }, + ["angular"], + { arrowParens: "avoid" }, +); diff --git a/tests/format/angular/expression/arrow-function.html b/tests/format/angular/expression/arrow-function.html new file mode 100644 index 000000000..86c4b42d4 --- /dev/null +++ b/tests/format/angular/expression/arrow-function.html @@ -0,0 +1,25 @@ +@let fn = (a, b) => componentValue + a + b; +One: {{fn(0, 1)}} + +@if (true) { + Two: {{fn(1, 1)}} + + + +} + + +{{[(a) => a + 1][0](1000)}} +{{[(a ) => a + 1 + componentProp][0](1000)}} + + +@let a = 1; + +@if (true) { + @let b = 2; + + @if (true) { + @let c = 3; + {{(() => a + b + c)()}} + } +} diff --git a/tests/format/angular/expression/instanceof-operator.html b/tests/format/angular/expression/instanceof-operator.html new file mode 100644 index 000000000..4ed44ca48 --- /dev/null +++ b/tests/format/angular/expression/instanceof-operator.html @@ -0,0 +1,4 @@ +{{ + + ( (a instanceof (b))) +}} diff --git a/yarn.lock b/yarn.lock index aafcc381b..1f8257b82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,12 +5,12 @@ __metadata: version: 8 cacheKey: 10 -"@angular/compiler@npm:21.1.0": - version: 21.1.0 - resolution: "@angular/compiler@npm:21.1.0" +"@angular/compiler@npm:21.2.8": + version: 21.2.8 + resolution: "@angular/compiler@npm:21.2.8" dependencies: tslib: "npm:^2.3.0" - checksum: 10/1f409c20eb36079719e004f9e683a41d9d9fd975d814132faa28f941a0d491ba392c7cca92061f8dbe540bd922cac905f76ccab48a87c82c764b3b7cdb6b28dd + checksum: 10/5bcf866cf01fbc19b016df4c77aa662440ba5ea3b1615ea21b6c69cd52c5b6cf55fbf8003ee8bcfd9199deac7d33a37ccbf4b9feb4bcc21e65cae388d2fa6a88 languageName: node linkType: hard @@ -2852,12 +2852,12 @@ __metadata: languageName: node linkType: hard -"angular-estree-parser@npm:15.3.0": - version: 15.3.0 - resolution: "angular-estree-parser@npm:15.3.0" +"angular-estree-parser@npm:15.4.3": + version: 15.4.3 + resolution: "angular-estree-parser@npm:15.4.3" peerDependencies: - "@angular/compiler": ">=21.0.7 || >= 21.1.0-0" - checksum: 10/9266d6bf73fac7f2d96e6a7b306bef0a6b6563f4f11a36aa308d4ca2fda2868839c8831c2e95d5ea008636e061c1cdc0668c01fc9f9876eb1b5f6846217fc2ec + "@angular/compiler": ">=21.0.7 || >= 21.2.0-0" + checksum: 10/a31569c99bb89e20564c0caa77c8db28070170bbbb145fd5afdda77a0d535af71cd3bd13d1d25bd6fe6cffa81ae6ab57f37cd1a9ea1dcdb5039ac419945515ce languageName: node linkType: hard @@ -7496,7 +7496,7 @@ __metadata: version: 0.0.0-use.local resolution: "prettier@workspace:." dependencies: - "@angular/compiler": "npm:21.1.0" + "@angular/compiler": "npm:21.2.8" "@babel/code-frame": "npm:8.0.0-beta.3" "@babel/generator": "npm:8.0.0-beta.3" "@babel/parser": "npm:8.0.0-beta.3" @@ -7517,7 +7517,7 @@ __metadata: "@typescript-eslint/visitor-keys": "npm:8.48.0" acorn: "npm:8.15.0" acorn-jsx: "npm:5.3.2" - angular-estree-parser: "npm:15.3.0" + angular-estree-parser: "npm:15.4.3" angular-html-parser: "npm:10.5.0" base64-arraybuffer-es6: "npm:3.1.0" browserslist: "npm:4.28.0" From b31557cf331a02acf83e7e29d1001b070189a0d9 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 10 Apr 2026 10:28:07 +0800 Subject: [PATCH 3/6] Release 3.8.2 --- .github/ISSUE_TEMPLATE/formatting.md | 2 +- .github/ISSUE_TEMPLATE/integration.md | 2 +- CHANGELOG.md | 46 +++++++++++++++++++ package.json | 2 +- .../versioned_docs/version-stable/browser.md | 36 +++++++-------- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/formatting.md b/.github/ISSUE_TEMPLATE/formatting.md index 8a13880bf..eeb25ffd0 100644 --- a/.github/ISSUE_TEMPLATE/formatting.md +++ b/.github/ISSUE_TEMPLATE/formatting.md @@ -26,7 +26,7 @@ Don't fill the form below manually! Let a program create a report for you: --> -**Prettier 3.8.1** +**Prettier 3.8.2** [Playground link](https://prettier.io/playground/#.....) ```sh diff --git a/.github/ISSUE_TEMPLATE/integration.md b/.github/ISSUE_TEMPLATE/integration.md index 2278a95d6..84a3febc5 100644 --- a/.github/ISSUE_TEMPLATE/integration.md +++ b/.github/ISSUE_TEMPLATE/integration.md @@ -20,7 +20,7 @@ BEFORE SUBMITTING AN ISSUE: **Environments:** -- Prettier Version: 3.8.1 +- Prettier Version: 3.8.2 - Running Prettier via: - Runtime: - Operating System: diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f8d0c77..4ccd6026f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +# 3.8.2 + +[diff](https://github.com/prettier/prettier/compare/3.8.1...3.8.2) + +#### Angular: Support Angular v21.2 ([#18722](https://github.com/prettier/prettier/pull/18722), [#19034](https://github.com/prettier/prettier/pull/19034) by [@fisker](https://github.com/fisker)) + +Exhaustive typechecking with `@default never;` + + +```html + +@switch (foo) { + @case (1) {} + @default never; +} + + +SyntaxError: Incomplete block "default never". If you meant to write the @ character, you should use the "@" HTML entity instead. (3:3) + + +@switch (foo) { + @case (1) {} + @default never; +} +``` + +`arrow function` and `instanceof` expressions. + + +```html + +@let fn = (a) => a? 1:2; + +{{ fn ( a instanceof b)}} + + +@let fn = (a) => a? 1:2; + +{{ fn ( a instanceof b)}} + + +@let fn = (a) => (a ? 1 : 2); + +{{ fn(a instanceof b) }} +``` + # 3.8.1 [diff](https://github.com/prettier/prettier/compare/3.8.0...3.8.1) diff --git a/package.json b/package.json index 9aa706f22..8529cd77f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prettier", - "version": "3.9.0-dev", + "version": "3.8.2", "description": "Prettier is an opinionated code formatter", "bin": "./bin/prettier.cjs", "repository": "prettier/prettier", diff --git a/website/versioned_docs/version-stable/browser.md b/website/versioned_docs/version-stable/browser.md index 2c68aa954..614d523b8 100644 --- a/website/versioned_docs/version-stable/browser.md +++ b/website/versioned_docs/version-stable/browser.md @@ -21,7 +21,7 @@ Required options: - **[`parser`](options.md#parser) (or [`filepath`](options.md#file-path))**: One of these options has to be specified for Prettier to know which parser to use. -- **`plugins`**: Unlike the `format` function from the [Node.js-based API](api.md#prettierformatsource-options), this function doesn’t load plugins automatically. The `plugins` option is required because all the parsers included in the Prettier package come as plugins (for reasons of file size). These plugins are files in [https://unpkg.com/browse/prettier@3.8.1/plugins](https://unpkg.com/browse/prettier@3.8.1/plugins). Note that `estree` plugin should be loaded when printing JavaScript, TypeScript, Flow, or JSON. +- **`plugins`**: Unlike the `format` function from the [Node.js-based API](api.md#prettierformatsource-options), this function doesn’t load plugins automatically. The `plugins` option is required because all the parsers included in the Prettier package come as plugins (for reasons of file size). These plugins are files in [https://unpkg.com/browse/prettier@3.8.2/plugins](https://unpkg.com/browse/prettier@3.8.2/plugins). Note that `estree` plugin should be loaded when printing JavaScript, TypeScript, Flow, or JSON. You need to load the ones that you’re going to use and pass them to `prettier.format` using the `plugins` option. @@ -32,8 +32,8 @@ See below for examples. ### Global ```html - - + +