@@ -12,6 +12,7 @@ import {
1212import * as compilerCli from '@angular/compiler-cli' ;
1313import { createRequire } from 'node:module' ;
1414import * as ts from 'typescript' ;
15+ import { type createAngularCompilation as createAngularCompilationType } from '@angular/build/private' ;
1516
1617import * as ngCompiler from '@angular/compiler' ;
1718import {
@@ -40,9 +41,10 @@ import {
4041
4142import { angularVitestPlugins } from './angular-vitest-plugin.js' ;
4243import {
43- angularMajor ,
44+ createAngularCompilation ,
4445 createJitResourceTransformer ,
4546 SourceFileCache ,
47+ angularFullVersion ,
4648} from './utils/devkit.js' ;
4749
4850const require = createRequire ( import . meta. url ) ;
@@ -58,6 +60,15 @@ import {
5860 replaceFiles ,
5961} from './plugins/file-replacements.plugin.js' ;
6062import { routerPlugin } from './router-plugin.js' ;
63+ import { createHash } from 'node:crypto' ;
64+
65+ export enum DiagnosticModes {
66+ None = 0 ,
67+ Option = 1 << 0 ,
68+ Syntactic = 1 << 1 ,
69+ Semantic = 1 << 2 ,
70+ All = Option | Syntactic | Semantic ,
71+ }
6172
6273export interface PluginOptions {
6374 tsconfig ?: string | ( ( ) => string ) ;
@@ -80,6 +91,9 @@ export interface PluginOptions {
8091 liveReload ?: boolean ;
8192 disableTypeChecking ?: boolean ;
8293 fileReplacements ?: FileReplacement [ ] ;
94+ experimental ?: {
95+ useAngularCompilationAPI ?: boolean ;
96+ } ;
8397}
8498
8599/**
@@ -120,6 +134,8 @@ export function angular(options?: PluginOptions): Plugin[] {
120134 liveReload : options ?. liveReload ?? false ,
121135 disableTypeChecking : options ?. disableTypeChecking ?? true ,
122136 fileReplacements : options ?. fileReplacements ?? [ ] ,
137+ useAngularCompilationAPI :
138+ options ?. experimental ?. useAngularCompilationAPI ?? false ,
123139 } ;
124140
125141 let resolvedConfig : ResolvedConfig ;
@@ -157,14 +173,38 @@ export function angular(options?: PluginOptions): Plugin[] {
157173 } ;
158174 let initialCompilation = false ;
159175 const declarationFiles : DeclarationFile [ ] = [ ] ;
176+ let compilation : Awaited < ReturnType < typeof createAngularCompilationType > > ;
160177
161178 function angularPlugin ( ) : Plugin {
162179 let isProd = false ;
163180
164- if ( angularMajor < 19 || isTest ) {
181+ if ( angularFullVersion < 190000 || isTest ) {
165182 pluginOptions . liveReload = false ;
166183 }
167184
185+ if ( pluginOptions . useAngularCompilationAPI ) {
186+ if ( angularFullVersion < 200100 ) {
187+ pluginOptions . useAngularCompilationAPI = false ;
188+ console . warn (
189+ '[@analogjs/vite-plugin-angular]: The Angular Compilation API is only available with Angular v20.1 and later' ,
190+ ) ;
191+ }
192+
193+ if ( pluginOptions . liveReload ) {
194+ pluginOptions . liveReload = false ;
195+ console . warn (
196+ '[@analogjs-vite-plugin-angular]: Live reload is currently not compatible with the Angular Compilation API option' ,
197+ ) ;
198+ }
199+
200+ if ( pluginOptions . fileReplacements ) {
201+ pluginOptions . fileReplacements = [ ] ;
202+ console . warn (
203+ '[@analogjs-vite-plugin-angular]: File replacements are currently not compatible with the Angular Compilation API option' ,
204+ ) ;
205+ }
206+ }
207+
168208 return {
169209 name : '@analogjs/vite-plugin-angular' ,
170210 async config ( config , { command } ) {
@@ -226,6 +266,11 @@ export function angular(options?: PluginOptions): Plugin[] {
226266 configResolved ( config ) {
227267 resolvedConfig = config ;
228268
269+ if ( pluginOptions . useAngularCompilationAPI ) {
270+ externalComponentStyles = new Map ( ) ;
271+ inlineComponentStyles = new Map ( ) ;
272+ }
273+
229274 // resolve the tsconfig path after config is fully resolved
230275 if ( tsConfigResolutionContext ) {
231276 const tsconfigValue = pluginOptions . tsconfigGetter ( ) ;
@@ -672,7 +717,113 @@ export function angular(options?: PluginOptions): Plugin[] {
672717 return resolvedPath ;
673718 }
674719
720+ async function performAngularCompilation ( config : ResolvedConfig ) {
721+ compilation = await (
722+ createAngularCompilation as typeof createAngularCompilationType
723+ ) ( ! ! pluginOptions . jit , false ) ;
724+ const compilationResult = await compilation . initialize (
725+ resolvedTsConfigPath ,
726+ {
727+ async transformStylesheet (
728+ data ,
729+ containingFile ,
730+ resourceFile ,
731+ order ,
732+ className ,
733+ ) {
734+ if ( pluginOptions . liveReload ) {
735+ const id = createHash ( 'sha256' )
736+ . update ( containingFile )
737+ . update ( className as string )
738+ . update ( String ( order ) )
739+ . update ( data )
740+ . digest ( 'hex' ) ;
741+ const filename = id + '.' + pluginOptions . inlineStylesExtension ;
742+ inlineComponentStyles ! . set ( filename , data ) ;
743+ return filename ;
744+ }
745+
746+ const filename =
747+ resourceFile ??
748+ containingFile . replace ( '.ts' , `.${ options ?. inlineStylesExtension } ` ) ;
749+
750+ let stylesheetResult ;
751+
752+ try {
753+ stylesheetResult = await preprocessCSS (
754+ data ,
755+ `${ filename } ?direct` ,
756+ resolvedConfig ,
757+ ) ;
758+ } catch ( e ) {
759+ console . error ( `${ e } ` ) ;
760+ }
761+
762+ return stylesheetResult ?. code || '' ;
763+ } ,
764+ processWebWorker ( workerFile , containingFile ) {
765+ return '' ;
766+ } ,
767+ } ,
768+ ( tsCompilerOptions ) => {
769+ if ( pluginOptions . liveReload && watchMode ) {
770+ tsCompilerOptions [ '_enableHmr' ] = true ;
771+ tsCompilerOptions [ 'externalRuntimeStyles' ] = true ;
772+ // Workaround for https://github.com/angular/angular/issues/59310
773+ // Force extra instructions to be generated for HMR w/defer
774+ tsCompilerOptions [ 'supportTestBed' ] = true ;
775+ }
776+
777+ if ( tsCompilerOptions . compilationMode === 'partial' ) {
778+ // These options can't be false in partial mode
779+ tsCompilerOptions [ 'supportTestBed' ] = true ;
780+ tsCompilerOptions [ 'supportJitMode' ] = true ;
781+ }
782+
783+ if ( ! isTest && config . build ?. lib ) {
784+ tsCompilerOptions [ 'declaration' ] = true ;
785+ tsCompilerOptions [ 'declarationMap' ] = watchMode ;
786+ tsCompilerOptions [ 'inlineSources' ] = true ;
787+ }
788+
789+ if ( isTest ) {
790+ // Allow `TestBed.overrideXXX()` APIs.
791+ tsCompilerOptions [ 'supportTestBed' ] = true ;
792+ }
793+
794+ return tsCompilerOptions ;
795+ } ,
796+ ) ;
797+
798+ compilationResult . externalStylesheets ?. forEach ( ( value , key ) => {
799+ externalComponentStyles ?. set ( `${ value } .css` , key ) ;
800+ } ) ;
801+
802+ const diagnostics = await compilation . diagnoseFiles (
803+ pluginOptions . disableTypeChecking
804+ ? DiagnosticModes . All & ~ DiagnosticModes . Semantic
805+ : DiagnosticModes . All ,
806+ ) ;
807+
808+ const errors = diagnostics . errors ?. length ? diagnostics . errors : [ ] ;
809+ const warnings = diagnostics . warnings ?. length ? diagnostics . warnings : [ ] ;
810+
811+ for ( const file of await compilation . emitAffectedFiles ( ) ) {
812+ outputFiles . set ( file . filename , {
813+ content : file . contents ,
814+ dependencies : [ ] ,
815+ errors : errors . map ( ( error ) => error . text || '' ) ,
816+ warnings : warnings . map ( ( warning ) => warning . text || '' ) ,
817+ } ) ;
818+ }
819+ }
820+
675821 async function performCompilation ( config : ResolvedConfig , ids ?: string [ ] ) {
822+ if ( pluginOptions . useAngularCompilationAPI ) {
823+ await performAngularCompilation ( config ) ;
824+ return { host : { } } ;
825+ }
826+
676827 const isProd = config . mode === 'production' ;
677828 const includeFiles = findIncludes ( ) ;
678829
0 commit comments