@@ -2,8 +2,11 @@ import { mergeConfig, type UserConfig, type Plugin } from 'vite';
22import path from 'path' ;
33import { createRequire } from 'node:module' ;
44import angular from '@analogjs/vite-plugin-angular' ;
5- import { angularLinkerVitePlugin , angularLinkerVitePluginPost } from '../helpers/angular-linker.js' ;
5+ import { angularLinkerVitePlugin , angularLinkerVitePluginPost } from '../helpers/angular/angular-linker.js' ;
6+ import { ensureSharedAngularLinker } from '../helpers/angular/shared-linker.js' ;
7+ import { containsRealNgDeclare } from '../helpers/angular/util.js' ;
68import { baseConfig } from './base.js' ;
9+ import { getCliFlags } from '../helpers/cli-flags.js' ;
710
811// Rollup-level linker to guarantee Angular libraries are linked when included in the bundle graph.
912function angularRollupLinker ( projectRoot ?: string ) : Plugin {
@@ -101,6 +104,10 @@ function angularRollupLinker(projectRoot?: string): Plugin {
101104 } ;
102105}
103106
107+ const cliFlags = getCliFlags ( ) ;
108+ const isDevEnv = process . env . NODE_ENV !== 'production' ;
109+ const hmrActive = isDevEnv && ! ! cliFlags . hmr ;
110+
104111const plugins = [
105112 // Allow external html template changes to trigger hot reload: Make .ts files depend on their .html templates
106113 {
@@ -151,6 +158,17 @@ const plugins = [
151158 } ,
152159 } as any ;
153160 } ,
161+ configResolved ( resolved ) {
162+ const deps = ( resolved . optimizeDeps ||= { } as any ) ;
163+ deps . noDiscovery = true ;
164+ deps . entries = [ ] ;
165+ deps . include = [ ] ;
166+ const exclude = new Set < string > ( Array . isArray ( deps . exclude ) ? deps . exclude : [ ] ) ;
167+ [ '@nativescript/core' , 'rxjs' , '@valor/nativescript-websockets' , 'set-value' , 'react' , 'react-reconciler' , 'react-nativescript' ] . forEach ( ( x ) => exclude . add ( x ) ) ;
168+ deps . exclude = Array . from ( exclude ) ;
169+ const esbuildOptions = ( deps . esbuildOptions ||= { } ) ;
170+ esbuildOptions . plugins = [ ] ;
171+ } ,
154172 } ,
155173] ;
156174
@@ -166,86 +184,12 @@ export const angularConfig = ({ mode }): UserConfig => {
166184 apply : 'build' as const ,
167185 enforce : 'post' as const ,
168186 async generateBundle ( _options , bundle ) {
169- function containsRealNgDeclare ( src : string ) : boolean {
170- // scan while skipping strings and comments; detect ɵɵngDeclare outside them
171- let inStr = false ,
172- strCh = '' ,
173- esc = false ,
174- inBlk = false ,
175- inLine = false ;
176- for ( let i = 0 ; i < src . length ; i ++ ) {
177- const ch = src [ i ] ;
178- const next = src [ i + 1 ] ;
179- if ( inLine ) {
180- if ( ch === '\n' ) inLine = false ;
181- continue ;
182- }
183- if ( inBlk ) {
184- if ( ch === '*' && next === '/' ) {
185- inBlk = false ;
186- i ++ ;
187- }
188- continue ;
189- }
190- if ( inStr ) {
191- if ( esc ) {
192- esc = false ;
193- continue ;
194- }
195- if ( ch === '\\' ) {
196- esc = true ;
197- continue ;
198- }
199- if ( ch === strCh ) {
200- inStr = false ;
201- strCh = '' ;
202- }
203- continue ;
204- }
205- if ( ch === '/' && next === '/' ) {
206- inLine = true ;
207- i ++ ;
208- continue ;
209- }
210- if ( ch === '/' && next === '*' ) {
211- inBlk = true ;
212- i ++ ;
213- continue ;
214- }
215- if ( ch === '"' || ch === "'" || ch === '`' ) {
216- inStr = true ;
217- strCh = ch ;
218- continue ;
219- }
220- // fast path for ɵ + call-like ngDeclare pattern (e.g., ɵɵngDeclareDirective(...))
221- if ( ch === 'ɵ' && next === 'ɵ' && src . startsWith ( 'ɵɵngDeclare' , i ) ) {
222- let j = i + 'ɵɵngDeclare' . length ;
223- // consume identifier tail (e.g., ClassMetadata, Directive, Component, Factory, Injectable, etc.)
224- while ( j < src . length ) {
225- const cj = src . charCodeAt ( j ) ;
226- // [A-Za-z0-9_$]
227- if ( ( cj >= 65 && cj <= 90 ) || ( cj >= 97 && cj <= 122 ) || ( cj >= 48 && cj <= 57 ) || cj === 95 || cj === 36 ) j ++ ;
228- else break ;
229- }
230- // skip whitespace
231- while ( j < src . length && / \s / . test ( src [ j ] ) ) j ++ ;
232- if ( src [ j ] === '(' ) return true ; // it's a call site
233- }
234- }
235- return false ;
236- }
237- // Lazy load linker deps to avoid hard coupling
238- let babel : any = null ;
239- let createLinker : any = null ;
240- try {
241- babel = await import ( '@babel/core' ) ;
242- const linkerMod : any = await import ( '@angular/compiler-cli/linker/babel' ) ;
243- createLinker = linkerMod . createLinkerPlugin || linkerMod . createEs2015LinkerPlugin ;
244- } catch {
245- return ; // no linker available
187+ function isNsAngularPolyfillsChunk ( chunk : any ) : boolean {
188+ if ( ! chunk || ! ( chunk as any ) . modules ) return false ;
189+ return Object . keys ( ( chunk as any ) . modules ) . some ( ( m ) => m . includes ( 'node_modules/@nativescript/angular/fesm2022/nativescript-angular-polyfills.mjs' ) ) ;
246190 }
247- if ( ! babel || ! createLinker ) return ;
248- const plugin = createLinker ( { sourceMapping : false } ) ;
191+ const { babel, linkerPlugin } = await ensureSharedAngularLinker ( process . cwd ( ) ) ;
192+ if ( ! babel || ! linkerPlugin ) return ;
249193 const strict = process . env . NS_STRICT_NG_LINK === '1' || process . env . NS_STRICT_NG_LINK === 'true' ;
250194 const enforceStrict = process . env . NS_STRICT_NG_LINK_ENFORCE === '1' || process . env . NS_STRICT_NG_LINK_ENFORCE === 'true' ;
251195 const debug = process . env . VITE_DEBUG_LOGS === '1' || process . env . VITE_DEBUG_LOGS === 'true' ;
@@ -255,36 +199,34 @@ export const angularConfig = ({ mode }): UserConfig => {
255199 if ( chunk && ( chunk as any ) . type === 'chunk' ) {
256200 const code = ( chunk as any ) . code as string ;
257201 if ( ! code ) continue ;
202+ const isNsPolyfills = isNsAngularPolyfillsChunk ( chunk ) ;
258203 try {
259204 const res = await ( babel as any ) . transformAsync ( code , {
260205 filename : fileName ,
261206 configFile : false ,
262207 babelrc : false ,
263208 sourceMaps : false ,
264209 compact : false ,
265- plugins : [ plugin ] ,
210+ plugins : [ linkerPlugin ] ,
266211 } ) ;
267- if ( res ?. code && res . code !== code ) {
268- ( chunk as any ) . code = res . code ;
212+ const finalCode = res ?. code && res . code !== code ? res . code : code ;
213+ if ( finalCode !== code ) {
214+ ( chunk as any ) . code = finalCode ;
269215 if ( debug ) {
270216 try {
271- console . log ( '[ns-angular-linker][post] linked' , fileName ) ;
217+ console . log ( '[ns-angular-linker][post] linked' , fileName , isNsPolyfills ? '(polyfills)' : '' ) ;
272218 } catch { }
273219 }
274- } else if ( strict ) {
275- // Only flag if we still detect actual Ivy partial declarations (ɵɵngDeclare*) outside strings/comments
276- if ( containsRealNgDeclare ( code ) ) {
277- unlinked . push ( fileName ) ;
278- }
220+ }
221+ if ( strict && ! isNsPolyfills && containsRealNgDeclare ( finalCode ) ) {
222+ unlinked . push ( fileName ) ;
279223 }
280224 } catch {
281- // best effort; keep original code
282225 if ( strict ) unlinked . push ( fileName ) ;
283226 }
284227 }
285228 }
286229 if ( strict && unlinked . length ) {
287- // Provide extra diagnostics: list a few module ids from bundle to locate offending sources
288230 const details : string [ ] = [ ] ;
289231 for ( const fname of unlinked ) {
290232 const chunk : any = ( bundle as any ) [ fname ] ;
@@ -293,7 +235,8 @@ export const angularConfig = ({ mode }): UserConfig => {
293235 . filter ( ( m ) => / n o d e _ m o d u l e s \/ (?: @ a n g u l a r | @ n a t i v e s c r i p t \/ a n g u l a r ) \/ / . test ( m ) )
294236 . slice ( 0 , 8 )
295237 : [ ] ;
296- // Add a small code excerpt around the first occurrence for easier inspection
238+ const isOnlyPolyfills = modIds . length > 0 && modIds . every ( ( m ) => m . includes ( 'node_modules/@nativescript/angular/fesm2022/nativescript-angular-polyfills.mjs' ) ) ;
239+ if ( isOnlyPolyfills ) continue ;
297240 let snippet = '' ;
298241 try {
299242 const code = ( chunk as any ) . code as string ;
@@ -306,6 +249,7 @@ export const angularConfig = ({ mode }): UserConfig => {
306249 } catch { }
307250 details . push ( ` - ${ fname } ${ modIds . length ? `\n from: ${ modIds . join ( '\n ' ) } ` : '' } ${ snippet } ` ) ;
308251 }
252+ if ( ! details . length ) return ;
309253 const message = `Angular linker strict mode: found unlinked partial declarations in emitted chunks: \n` + details . join ( '\n' ) + `\nSet NS_STRICT_NG_LINK=0 to disable this check. Set NS_STRICT_NG_LINK_ENFORCE=1 to make this a hard error.` ;
310254 if ( enforceStrict ) {
311255 throw new Error ( message ) ;
@@ -326,93 +270,40 @@ export const angularConfig = ({ mode }): UserConfig => {
326270 async renderChunk ( code : string , chunk : any ) {
327271 try {
328272 if ( ! code ) return null ;
329- // quick guard: look for real ɵɵngDeclare call outside strings/comments
330- const hasReal = ( ( ) => {
331- let inStr = false ,
332- strCh = '' ,
333- esc = false ,
334- inBlk = false ,
335- inLine = false ;
336- for ( let i = 0 ; i < code . length ; i ++ ) {
337- const ch = code [ i ] ,
338- n = code [ i + 1 ] ;
339- if ( inLine ) {
340- if ( ch === '\n' ) inLine = false ;
341- continue ;
342- }
343- if ( inBlk ) {
344- if ( ch === '*' && n === '/' ) {
345- inBlk = false ;
346- i ++ ;
347- }
348- continue ;
349- }
350- if ( inStr ) {
351- if ( esc ) {
352- esc = false ;
353- continue ;
354- }
355- if ( ch === '\\' ) {
356- esc = true ;
357- continue ;
358- }
359- if ( ch === strCh ) {
360- inStr = false ;
361- strCh = '' ;
362- }
363- continue ;
364- }
365- if ( ch === '/' && n === '/' ) {
366- inLine = true ;
367- i ++ ;
368- continue ;
369- }
370- if ( ch === '/' && n === '*' ) {
371- inBlk = true ;
372- i ++ ;
373- continue ;
374- }
375- if ( ch === '"' || ch === "'" || ch === '`' ) {
376- inStr = true ;
377- strCh = ch ;
378- continue ;
379- }
380- if ( ch === 'ɵ' && n === 'ɵ' && code . startsWith ( 'ɵɵngDeclare' , i ) ) {
381- let j = i + 'ɵɵngDeclare' . length ;
382- while ( j < code . length ) {
383- const cj = code . charCodeAt ( j ) ;
384- if ( ( cj >= 65 && cj <= 90 ) || ( cj >= 97 && cj <= 122 ) || ( cj >= 48 && cj <= 57 ) || cj === 95 || cj === 36 ) j ++ ;
385- else break ;
386- }
387- while ( j < code . length && / \s / . test ( code [ j ] ) ) j ++ ;
388- if ( code [ j ] === '(' ) return true ;
389- }
273+ if ( ! containsRealNgDeclare ( code ) ) return null ;
274+ const { babel, linkerPlugin } = await ensureSharedAngularLinker ( process . cwd ( ) ) ;
275+ if ( ! babel || ! linkerPlugin ) return null ;
276+ const filename = chunk . fileName || chunk . name || 'chunk.mjs' ;
277+ const debug = process . env . VITE_DEBUG_LOGS === '1' || process . env . VITE_DEBUG_LOGS === 'true' ;
278+ const runLink = async ( input : string ) => {
279+ const result = await ( babel as any ) . transformAsync ( input , {
280+ filename,
281+ configFile : false ,
282+ babelrc : false ,
283+ sourceMaps : false ,
284+ compact : false ,
285+ plugins : [ linkerPlugin ] ,
286+ } ) ;
287+ return result ?. code ?? input ;
288+ } ;
289+ let transformed = await runLink ( code ) ;
290+ if ( containsRealNgDeclare ( transformed ) ) {
291+ transformed = await runLink ( transformed ) ;
292+ }
293+ if ( transformed !== code ) {
294+ if ( debug ) {
295+ try {
296+ console . log ( '[ns-angular-linker][render] linked' , filename ) ;
297+ } catch { }
390298 }
391- return false ;
392- } ) ( ) ;
393- if ( ! hasReal ) return null ;
394- const babel : any = await import ( '@babel/core' ) ;
395- const linkerMod : any = await import ( '@angular/compiler-cli/linker/babel' ) ;
396- const createLinker = linkerMod . createLinkerPlugin || linkerMod . createEs2015LinkerPlugin ;
397- if ( ! babel || ! createLinker ) return null ;
398- const plugin = createLinker ( { sourceMapping : false } ) ;
399- const result = await ( babel as any ) . transformAsync ( code , {
400- filename : chunk . fileName || chunk . name || 'chunk.mjs' ,
401- configFile : false ,
402- babelrc : false ,
403- sourceMaps : false ,
404- compact : false ,
405- plugins : [ plugin ] ,
406- } ) ;
407- if ( result ?. code && result . code !== code ) {
408- return { code : result . code , map : null } ;
299+ return { code : transformed , map : null } ;
409300 }
410301 } catch { }
411302 return null ;
412303 } ,
413304 } ;
414305
415- const enableRollupLinker = process . env . NS_ENABLE_ROLLUP_LINKER === '1' || process . env . NS_ENABLE_ROLLUP_LINKER === 'true' ;
306+ const enableRollupLinker = process . env . NS_ENABLE_ROLLUP_LINKER === '1' || process . env . NS_ENABLE_ROLLUP_LINKER === 'true' || hmrActive ;
416307
417308 return mergeConfig ( baseConfig ( { mode } ) , {
418309 plugins : [ ...plugins , ...( enableRollupLinker ? [ angularRollupLinker ( process . cwd ( ) ) ] : [ ] ) , renderChunkLinker , postLinker ] ,
0 commit comments