@@ -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 { globSync } from 'tinyglobby' ;
@@ -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 ;
@@ -156,14 +172,38 @@ export function angular(options?: PluginOptions): Plugin[] {
156172 } ;
157173 let initialCompilation = false ;
158174 const declarationFiles : DeclarationFile [ ] = [ ] ;
175+ let compilation : Awaited < ReturnType < typeof createAngularCompilationType > > ;
159176
160177 function angularPlugin ( ) : Plugin {
161178 let isProd = false ;
162179
163- if ( angularMajor < 19 || isTest ) {
180+ if ( angularFullVersion < 190000 || isTest ) {
164181 pluginOptions . liveReload = false ;
165182 }
166183
184+ if ( pluginOptions . useAngularCompilationAPI ) {
185+ if ( angularFullVersion < 200100 ) {
186+ pluginOptions . useAngularCompilationAPI = false ;
187+ console . warn (
188+ '[@analogjs/vite-plugin-angular]: The Angular Compilation API is only available with Angular v20.1 and later' ,
189+ ) ;
190+ }
191+
192+ if ( pluginOptions . liveReload ) {
193+ pluginOptions . liveReload = false ;
194+ console . warn (
195+ '[@analogjs-vite-plugin-angular]: Live reload is currently not compatible with the Angular Compilation API option' ,
196+ ) ;
197+ }
198+
199+ if ( pluginOptions . fileReplacements ) {
200+ pluginOptions . fileReplacements = [ ] ;
201+ console . warn (
202+ '[@analogjs-vite-plugin-angular]: File replacements are currently not compatible with the Angular Compilation API option' ,
203+ ) ;
204+ }
205+ }
206+
167207 return {
168208 name : '@analogjs/vite-plugin-angular' ,
169209 async config ( config , { command } ) {
@@ -219,6 +259,11 @@ export function angular(options?: PluginOptions): Plugin[] {
219259 configResolved ( config ) {
220260 resolvedConfig = config ;
221261
262+ if ( pluginOptions . useAngularCompilationAPI ) {
263+ externalComponentStyles = new Map ( ) ;
264+ inlineComponentStyles = new Map ( ) ;
265+ }
266+
222267 if ( isTest ) {
223268 // set test watch mode
224269 // - vite override from vitest-angular
@@ -665,7 +710,115 @@ export function angular(options?: PluginOptions): Plugin[] {
665710 ) ;
666711 }
667712
713+ async function performAngularCompilation ( config : ResolvedConfig ) {
714+ compilation = await (
715+ createAngularCompilation as typeof createAngularCompilationType
716+ ) ( ! ! pluginOptions . jit , false ) ;
717+
718+ const resolvedTsConfigPath = resolveTsConfigPath ( ) ;
719+ const compilationResult = await compilation . initialize (
720+ resolvedTsConfigPath ,
721+ {
722+ async transformStylesheet (
723+ data ,
724+ containingFile ,
725+ resourceFile ,
726+ order ,
727+ className ,
728+ ) {
729+ if ( pluginOptions . liveReload ) {
730+ const id = createHash ( 'sha256' )
731+ . update ( containingFile )
732+ . update ( className as string )
733+ . update ( String ( order ) )
734+ . update ( data )
735+ . digest ( 'hex' ) ;
736+ const filename = id + '.' + pluginOptions . inlineStylesExtension ;
737+ inlineComponentStyles ! . set ( filename , data ) ;
738+ return filename ;
739+ }
740+
741+ const filename =
742+ resourceFile ??
743+ containingFile . replace ( '.ts' , `.${ options ?. inlineStylesExtension } ` ) ;
744+
745+ let stylesheetResult ;
746+
747+ try {
748+ stylesheetResult = await preprocessCSS (
749+ data ,
750+ `${ filename } ?direct` ,
751+ resolvedConfig ,
752+ ) ;
753+ } catch ( e ) {
754+ console . error ( `${ e } ` ) ;
755+ }
756+
757+ return stylesheetResult ?. code || '' ;
758+ } ,
759+ processWebWorker ( workerFile , containingFile ) {
760+ return '' ;
761+ } ,
762+ } ,
763+ ( tsCompilerOptions ) => {
764+ if ( pluginOptions . liveReload && watchMode ) {
765+ tsCompilerOptions [ '_enableHmr' ] = true ;
766+ tsCompilerOptions [ 'externalRuntimeStyles' ] = true ;
767+ // Workaround for https://github.com/angular/angular/issues/59310
768+ // Force extra instructions to be generated for HMR w/defer
769+ tsCompilerOptions [ 'supportTestBed' ] = true ;
770+ }
771+
772+ if ( tsCompilerOptions . compilationMode === 'partial' ) {
773+ // These options can't be false in partial mode
774+ tsCompilerOptions [ 'supportTestBed' ] = true ;
775+ tsCompilerOptions [ 'supportJitMode' ] = true ;
776+ }
777+
778+ if ( ! isTest && config . build ?. lib ) {
779+ tsCompilerOptions [ 'declaration' ] = true ;
780+ tsCompilerOptions [ 'declarationMap' ] = watchMode ;
781+ tsCompilerOptions [ 'inlineSources' ] = true ;
782+ }
783+
784+ if ( isTest ) {
785+ // Allow `TestBed.overrideXXX()` APIs.
786+ tsCompilerOptions [ 'supportTestBed' ] = true ;
787+ }
788+
789+ return tsCompilerOptions ;
790+ } ,
791+ ) ;
792+
793+ compilationResult . externalStylesheets ?. forEach ( ( value , key ) => {
794+ externalComponentStyles ?. set ( `${ value } .css` , key ) ;
795+ } ) ;
796+
797+ const diagnostics = await compilation . diagnoseFiles (
798+ pluginOptions . disableTypeChecking
799+ ? DiagnosticModes . All & ~ DiagnosticModes . Semantic
800+ : DiagnosticModes . All ,
801+ ) ;
802+
803+ const errors = diagnostics . errors ?. length ? diagnostics . errors : [ ] ;
804+ const warnings = diagnostics . warnings ?. length ? diagnostics . warnings : [ ] ;
805+
806+ for ( const file of await compilation . emitAffectedFiles ( ) ) {
807+ outputFiles . set ( file . filename , {
808+ content : file . contents ,
809+ dependencies : [ ] ,
810+ errors : errors . map ( ( error ) => error . text || '' ) ,
811+ warnings : warnings . map ( ( warning ) => warning . text || '' ) ,
812+ } ) ;
813+ }
814+ }
815+
668816 async function performCompilation ( config : ResolvedConfig , ids ?: string [ ] ) {
817+ if ( pluginOptions . useAngularCompilationAPI ) {
818+ await performAngularCompilation ( config ) ;
819+ return { host : { } } ;
820+ }
821+
669822 const isProd = config . mode === 'production' ;
670823 const includeFiles = findIncludes ( ) ;
671824
0 commit comments