11import { extname } from 'node:path' ;
22
3- import { TmplAstNode , TmplAstSwitchBlock } from '@angular/compiler' ;
3+ import { ParsedTemplate , parseTemplate , type TmplAstNode , TmplAstSwitchBlock } from '@angular/compiler' ;
44import { ScriptKind , tsquery } from '@phenomnomnominal/tsquery' ;
55import pkg , {
6- Node ,
7- Identifier ,
8- ClassDeclaration ,
9- ConstructorDeclaration ,
10- CallExpression ,
11- Expression ,
12- StringLiteral ,
13- SourceFile ,
14- PropertyDeclaration ,
15- PropertyAccessExpression ,
6+ type CallExpression ,
7+ type ClassDeclaration ,
8+ type ConstructorDeclaration ,
9+ type Expression ,
10+ type Identifier ,
11+ type Node ,
12+ type PropertyAccessExpression ,
13+ type PropertyAssignment ,
14+ type PropertyDeclaration ,
15+ type SourceFile ,
16+ type StringLiteral ,
1617} from 'typescript' ;
1718
19+ interface ParsedScriptSource {
20+ source : string ;
21+ parsedFile : SourceFile ;
22+ parsedTemplates : ParsedTemplate [ ] ;
23+ }
24+
25+ interface ParsedTemplateSource {
26+ source : string ;
27+ parsedFile : null ;
28+ parsedTemplates : [ ParsedTemplate ] ;
29+ }
30+
31+ type ParsedSource = ParsedScriptSource | ParsedTemplateSource ;
32+
1833// Importing non-type members from 'typescript' this way to prevent runtime errors such as:
1934// `SyntaxError: Named export 'isCallExpression' not found. The requested module 'typescript' is a CommonJS module,
2035// which may not support all module.exports as named exports.`
@@ -28,17 +43,76 @@ const {
2843 SyntaxKind,
2944} = pkg ;
3045
31- export function getAST ( source : string , fileName = '' ) : SourceFile {
32- const supportedScriptTypes : Record < string , ScriptKind > = {
33- '.js' : ScriptKind . JS ,
34- '.jsx' : ScriptKind . JSX ,
35- '.ts' : ScriptKind . TS ,
36- '.tsx' : ScriptKind . TSX ,
37- } ;
46+ const ANGULAR_TEMPLATE_KIND = ScriptKind . Unknown ; // Unknown for ts
47+
48+ const SCRIPT_TYPES = new Map ( [
49+ [ '.js' , ScriptKind . JS ] ,
50+ [ '.mjs' , ScriptKind . JS ] ,
51+ [ '.jsx' , ScriptKind . JSX ] ,
52+ [ '.ts' , ScriptKind . TS ] ,
53+ [ '.mts' , ScriptKind . TS ] ,
54+ [ '.tsx' , ScriptKind . TSX ] ,
55+ [ '.html' , ANGULAR_TEMPLATE_KIND ] ,
56+ ] ) ;
57+
58+ const AST_CACHE = new Map < string , ParsedSource > ( ) ;
3859
39- const scriptKind = supportedScriptTypes [ extname ( fileName ) ] ?? ScriptKind . TS ;
60+ export function clearAstCache ( ) : void {
61+ AST_CACHE . clear ( ) ;
62+ }
63+
64+ export function getAST ( source : string , fileName = '' ) : ParsedSource {
65+ // Skip cache if no fileName is provided
66+ if ( ! fileName ) {
67+ return parseSource ( source , fileName ) ;
68+ }
4069
41- return tsquery . ast ( source , fileName , scriptKind ) ;
70+ const cached = AST_CACHE . get ( fileName ) ;
71+ if ( cached && cached . source === source ) {
72+ return cached ;
73+ }
74+
75+ const result = parseSource ( source , fileName ) ;
76+ AST_CACHE . set ( fileName , result ) ;
77+
78+ return result ;
79+ }
80+
81+ export function parseSource ( source : string , fileName = '' ) : ParsedSource {
82+ const scriptKind = SCRIPT_TYPES . get ( extname ( fileName ) ) ?? ScriptKind . TS ;
83+
84+ // Angular template, pass to Angular compiler.
85+ if ( scriptKind === ANGULAR_TEMPLATE_KIND ) {
86+ return {
87+ source,
88+ parsedFile : null ,
89+ parsedTemplates : [ parseTemplate ( source , fileName , { collectCommentNodes : false } ) ] ,
90+ } ;
91+ }
92+
93+ const parsedFile = tsquery . ast ( source , fileName , scriptKind ) ;
94+ const parsedTemplates : ParsedTemplate [ ] = [ ] ;
95+
96+ // Check for possible inline templates that need to be processed by the Angular compiler.
97+ if ( source . includes ( '@Component(' ) && source . includes ( 'template:' ) ) {
98+ getComponentInlineTemplate ( parsedFile ) . forEach ( ( templateNode ) => {
99+ const tpl = templateNode . initializer ;
100+ if ( isStringLiteralLike ( tpl ) ) {
101+ parsedTemplates . push ( parseTemplate ( tpl . text , fileName , { collectCommentNodes : false } ) ) ;
102+ }
103+ } ) ;
104+ }
105+
106+ return { source, parsedFile, parsedTemplates } ;
107+ }
108+
109+ /**
110+ * Retrieves inline `template` property assignments from Angular `@Component` decorators.
111+ */
112+ export function getComponentInlineTemplate ( node : Node ) : PropertyAssignment [ ] {
113+ const query =
114+ 'Decorator > CallExpression:has(Identifier[name="Component"]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="template"])' ;
115+ return tsquery < PropertyAssignment > ( node , query ) ;
42116}
43117
44118/**
0 commit comments