Skip to content

Commit a957af4

Browse files
committed
Support Angular 20.1
1 parent 4a6ed9a commit a957af4

File tree

6 files changed

+601
-711
lines changed

6 files changed

+601
-711
lines changed

package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,30 @@
2727
"release": "release-it"
2828
},
2929
"devDependencies": {
30-
"@angular/compiler": "20.0.0",
30+
"@angular/compiler": "20.1.0",
3131
"@babel/code-frame": "7.27.1",
32-
"@babel/parser": "7.27.3",
33-
"@babel/types": "7.27.3",
32+
"@babel/parser": "7.28.0",
33+
"@babel/types": "7.28.1",
3434
"@types/babel__code-frame": "7.0.6",
35-
"@types/node": "22.15.23",
36-
"@vitest/coverage-v8": "3.1.4",
35+
"@types/node": "24.0.13",
36+
"@vitest/coverage-v8": "3.2.4",
3737
"del-cli": "6.0.0",
38-
"eslint": "9.27.0",
38+
"eslint": "9.31.0",
3939
"eslint-config-prettier": "10.1.5",
4040
"eslint-plugin-simple-import-sort": "12.1.1",
4141
"eslint-plugin-unicorn": "59.0.1",
42-
"globals": "16.2.0",
42+
"globals": "16.3.0",
4343
"jest-snapshot-serializer-raw": "2.0.0",
4444
"lines-and-columns": "2.0.4",
4545
"npm-run-all2": "8.0.4",
46-
"prettier": "3.5.3",
47-
"release-it": "19.0.2",
46+
"prettier": "3.6.2",
47+
"release-it": "19.0.3",
4848
"typescript": "5.8.3",
49-
"typescript-eslint": "8.33.0",
50-
"vitest": "3.1.4"
49+
"typescript-eslint": "8.36.0",
50+
"vitest": "3.2.4"
5151
},
5252
"peerDependencies": {
53-
"@angular/compiler": "^20.0.0"
53+
"@angular/compiler": "^20.1.0"
5454
},
5555
"engines": {
5656
"node": ">= 20"

src/angular-parser.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import {
22
type ASTWithSource,
33
Lexer,
4+
ParseLocation,
45
Parser,
6+
ParseSourceFile,
7+
ParseSourceSpan,
58
type TemplateBindingParseResult,
69
} from '@angular/compiler';
710

811
import { type CommentLine } from './types.js';
912
import { sourceSpanToLocationInformation } from './utils.js';
1013

14+
// https://github.com/angular/angular/blob/5e9707dc84e6590ec8c9d41e7d3be7deb2fa7c53/packages/compiler/test/expression_parser/utils/span.ts
15+
function getFakeSpan(fileName = 'test.html') {
16+
const file = new ParseSourceFile('', fileName);
17+
const location = new ParseLocation(file, 0, 0, 0);
18+
return new ParseSourceSpan(location, location);
19+
}
20+
1121
const getCommentStart = (text: string): number | null =>
1222
// @ts-expect-error -- need to call private _commentStart
1323
Parser.prototype._commentStart(text);
@@ -59,23 +69,23 @@ function createAngularParseFunction<
5969
}
6070

6171
export const parseBinding = createAngularParseFunction((text, parser) =>
62-
parser.parseBinding(text, '', 0),
72+
parser.parseBinding(text, getFakeSpan(), 0),
6373
);
6474

6575
export const parseSimpleBinding = createAngularParseFunction((text, parser) =>
66-
parser.parseSimpleBinding(text, '', 0),
76+
parser.parseSimpleBinding(text, getFakeSpan(), 0),
6777
);
6878

6979
export const parseAction = createAngularParseFunction((text, parser) =>
70-
parser.parseAction(text, '', 0),
80+
parser.parseAction(text, getFakeSpan(), 0),
7181
);
7282

7383
export const parseInterpolationExpression = createAngularParseFunction(
74-
(text, parser) => parser.parseInterpolationExpression(text, '', 0),
84+
(text, parser) => parser.parseInterpolationExpression(text, getFakeSpan(), 0),
7585
);
7686

7787
export const parseTemplateBindings = createAngularParseFunction(
78-
(text, parser) => parser.parseTemplateBindings('', text, '', 0, 0),
88+
(text, parser) => parser.parseTemplateBindings('', text, getFakeSpan(), 0, 0),
7989
/* shouldExtractComment */ false,
8090
);
8191

src/transform-node.ts

Lines changed: 28 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ type NodeTransformOptions = {
3939
parent?: angular.AST;
4040
};
4141

42+
const assignmentOperators = new Set([
43+
'=',
44+
// https://github.com/angular/angular/pull/62064
45+
'+=', // addition assignment
46+
'-=', // subtraction assignment
47+
'*=', // multiplication assignment
48+
'/=', // division assignment
49+
'%=', // remainder assignment
50+
'**=', // exponentiation assignment
51+
'&&=', // logical AND assignment
52+
'||=', // logical OR assignment
53+
'??=', // nullish coalescing assignment
54+
]);
55+
4256
class Transformer extends Source {
4357
#node;
4458
#text;
@@ -170,6 +184,19 @@ class Transformer extends Source {
170184
);
171185
}
172186

187+
if (assignmentOperators.has(operator)) {
188+
return this.#create<babel.AssignmentExpression>(
189+
{
190+
...properties,
191+
type: 'AssignmentExpression',
192+
left: left as babel.MemberExpression,
193+
operator,
194+
...node.sourceSpan,
195+
},
196+
{ hasParentParens: isInParentParens },
197+
);
198+
}
199+
173200
return this.#create<babel.BinaryExpression>(
174201
{
175202
...properties,
@@ -485,14 +512,11 @@ class Transformer extends Source {
485512
node instanceof angular.SafePropertyRead
486513
) {
487514
const { receiver, name } = node;
488-
const nameEnd =
489-
this.getCharacterLastIndex(/\S/, node.sourceSpan.end - 1) + 1;
490515
const tName = this.#create<babel.Identifier>(
491516
{
492517
type: 'Identifier',
493518
name,
494-
start: nameEnd - name.length,
495-
end: nameEnd,
519+
...node.nameSpan,
496520
},
497521
isImplicitThis(receiver, this.#text)
498522
? { hasParentParens: isInParentParens }
@@ -505,58 +529,6 @@ class Transformer extends Source {
505529
});
506530
}
507531

508-
if (node instanceof angular.KeyedWrite) {
509-
const key = this.#transform<babel.Expression>(node.key);
510-
const right = this.#transform<babel.Expression>(node.value);
511-
const left = this.#transformReceiverAndName(node.receiver, key, {
512-
computed: true,
513-
optional: false,
514-
end: this.getCharacterIndex(']', getOuterEnd(key)) + 1,
515-
});
516-
return this.#create<babel.AssignmentExpression>(
517-
{
518-
type: 'AssignmentExpression',
519-
left: left as babel.MemberExpression,
520-
operator: '=',
521-
right,
522-
start: getOuterStart(left),
523-
end: getOuterEnd(right),
524-
},
525-
{ hasParentParens: isInParentParens },
526-
);
527-
}
528-
529-
if (node instanceof angular.PropertyWrite) {
530-
const { receiver, name, value } = node;
531-
const tValue = this.#transform<babel.Expression>(value);
532-
const nameEnd =
533-
this.getCharacterLastIndex(
534-
/\S/,
535-
this.getCharacterLastIndex('=', getOuterStart(tValue) - 1) - 1,
536-
) + 1;
537-
const tName = this.#create<babel.Identifier>({
538-
type: 'Identifier',
539-
name,
540-
start: nameEnd - name.length,
541-
end: nameEnd,
542-
});
543-
const tReceiverAndName = this.#transformReceiverAndName(receiver, tName, {
544-
computed: false,
545-
optional: false,
546-
});
547-
return this.#create<babel.AssignmentExpression>(
548-
{
549-
type: 'AssignmentExpression',
550-
left: tReceiverAndName as babel.MemberExpression,
551-
operator: '=',
552-
right: tValue,
553-
start: getOuterStart(tReceiverAndName),
554-
end: getOuterEnd(tValue),
555-
},
556-
{ hasParentParens: isInParentParens },
557-
);
558-
}
559-
560532
if (node instanceof angular.TaggedTemplateLiteral) {
561533
return this.#create<babel.TaggedTemplateExpression>({
562534
type: 'TaggedTemplateExpression',
@@ -619,8 +591,6 @@ class Transformer extends Source {
619591
type SupportedNodes =
620592
| angular.ASTWithSource // Not handled
621593
| angular.PropertyRead
622-
| angular.PropertyWrite
623-
| angular.KeyedWrite
624594
| angular.Call
625595
| angular.LiteralPrimitive
626596
| angular.Unary

tests/helpers.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,12 @@ const KNOWN_AST_TYPES = [
157157
'KeyedRead',
158158
'SafeKeyedRead',
159159
'TypeofExpression',
160-
'KeyedWrite',
161160
'LiteralArray',
162161
'LiteralMap',
163162
'LiteralPrimitive',
164163
'NonNullAssert',
165164
'PrefixNot',
166165
'PropertyRead',
167-
'PropertyWrite',
168166
'SafeCall',
169167
'SafePropertyRead',
170168
'ThisReceiver',

tests/transform.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe.each`
5151
${'SafeKeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b ?. [ c ] '} | ${true} | ${true} | ${true} | ${true}
5252
${'KeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b () [ c ] '} | ${true} | ${true} | ${true} | ${true}
5353
${'SafeKeyedRead'} | ${'OptionalMemberExpression'} | ${' a ?. b () ?. [ c ] '} | ${true} | ${true} | ${true} | ${true}
54-
${'KeyedWrite'} | ${'AssignmentExpression'} | ${' a [ b ] = 1 '} | ${true} | ${true} | ${true} | ${true}
54+
${'Binary'} | ${'AssignmentExpression'} | ${' a [ b ] = 1 '} | ${true} | ${true} | ${true} | ${true}
5555
${'ImplicitReceiver'} | ${'ThisExpression'} | ${' this '} | ${true} | ${true} | ${true} | ${true}
5656
${'LiteralArray'} | ${'ArrayExpression'} | ${' [ 1 ] '} | ${true} | ${true} | ${true} | ${true}
5757
${'LiteralMap'} | ${'ObjectExpression'} | ${' ( { "a" : 1 } )'} | ${true} | ${true} | ${true} | ${true}
@@ -88,8 +88,8 @@ describe.each`
8888
${'PropertyRead'} | ${'MemberExpression'} | ${' this . a '} | ${true} | ${true} | ${true} | ${true}
8989
${'PropertyRead'} | ${'OptionalMemberExpression'} | ${' a ?. b . c '} | ${true} | ${true} | ${true} | ${true}
9090
${'PropertyRead'} | ${'OptionalMemberExpression'} | ${' a ?. b ( ) . c '} | ${true} | ${true} | ${true} | ${true}
91-
${'PropertyWrite'} | ${'AssignmentExpression'} | ${' a . b = 1 '} | ${true} | ${false} | ${false} | ${false}
92-
${'PropertyWrite'} | ${'AssignmentExpression'} | ${' a = 1 '} | ${true} | ${false} | ${false} | ${false}
91+
${'Binary'} | ${'AssignmentExpression'} | ${' a . b = 1 '} | ${true} | ${false} | ${false} | ${false}
92+
${'Binary'} | ${'AssignmentExpression'} | ${' a = 1 '} | ${true} | ${false} | ${false} | ${false}
9393
${'Call'} | ${'OptionalCallExpression'} | ${' a ?. b ( ) '} | ${true} | ${true} | ${true} | ${true}
9494
${'SafeCall'} | ${'OptionalCallExpression'} | ${' a ?. b ?. ( ) '} | ${true} | ${true} | ${true} | ${true}
9595
${'SafePropertyRead'} | ${'OptionalMemberExpression'} | ${' a ?. b '} | ${true} | ${true} | ${true} | ${true}
@@ -106,6 +106,7 @@ describe.each`
106106
${'TaggedTemplateLiteral'} | ${'TaggedTemplateExpression'} | ${' ( ( ( ( tag ) ) ` a ${ b } \\u0063 ` ) ) '} | ${true} | ${true} | ${true} | ${true}
107107
${'LiteralMap'} | ${'ObjectExpression'} | ${' ( ( {foo: ` a ${ b } ` } ) ) '} | ${true} | ${true} | ${true} | ${true}
108108
${'LiteralMap'} | ${'ObjectExpression'} | ${' ( ( {foo: tag ` a ${ b } ` } ) ) '} | ${true} | ${true} | ${true} | ${true}
109+
${'Binary'} | ${'AssignmentExpression'} | ${' a ??= b '} | ${true} | ${false} | ${false} | ${false}
109110
`('($expectedAngularType -> $expectedEstreeType)', (fields) => {
110111
for (const method of PARSE_METHODS) {
111112
testSection(method, fields);

0 commit comments

Comments
 (0)