@analogjs/vite-plugin-angular 2.0.0-alpha.1 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
- import { dirname, resolve } from 'node:path';
1
+ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
3
  import * as compilerCli from '@angular/compiler-cli';
3
- import * as ts from 'typescript';
4
4
  import { createRequire } from 'node:module';
5
5
  import { normalizePath, preprocessCSS, } from 'vite';
6
+ import * as ngCompiler from '@angular/compiler';
6
7
  import { createCompilerPlugin } from './compiler-plugin.js';
7
8
  import { StyleUrlsResolver, TemplateUrlsResolver, } from './component-resolvers.js';
8
9
  import { augmentHostWithCaching, augmentHostWithResources, augmentProgramWithVersioning, mergeTransformers, } from './host.js';
9
10
  import { jitPlugin } from './angular-jit-plugin.js';
10
11
  import { buildOptimizerPlugin } from './angular-build-optimizer-plugin.js';
11
- import { createJitResourceTransformer, SourceFileCache, } from './utils/devkit.js';
12
+ import { createJitResourceTransformer, SourceFileCache, angularMajor, } from './utils/devkit.js';
12
13
  import { angularVitestPlugins } from './angular-vitest-plugin.js';
13
14
  import { angularStorybookPlugin } from './angular-storybook-plugin.js';
14
15
  const require = createRequire(import.meta.url);
@@ -16,22 +17,22 @@ import { getFrontmatterMetadata } from './authoring/frontmatter.js';
16
17
  import { defaultMarkdownTemplateTransforms, } from './authoring/markdown-transform.js';
17
18
  import { routerPlugin } from './router-plugin.js';
18
19
  import { pendingTasksPlugin } from './angular-pending-tasks.plugin.js';
20
+ import { analyzeFileUpdates } from './utils/hmr-candidates.js';
19
21
  /**
20
22
  * TypeScript file extension regex
21
23
  * Match .(c or m)ts, .ts extensions with an optional ? for query params
22
24
  * Ignore .tsx extensions
23
25
  */
24
26
  const TS_EXT_REGEX = /\.[cm]?(ts|analog|ag)[^x]?\??/;
27
+ const ANGULAR_COMPONENT_PREFIX = '/@ng/component';
28
+ const classNames = new Map();
25
29
  export function angular(options) {
26
30
  /**
27
31
  * Normalize plugin options so defaults
28
32
  * are used for values not provided.
29
33
  */
30
34
  const pluginOptions = {
31
- tsconfig: options?.tsconfig ??
32
- (process.env['NODE_ENV'] === 'test'
33
- ? './tsconfig.spec.json'
34
- : './tsconfig.app.json'),
35
+ tsconfig: options?.tsconfig || '',
35
36
  workspaceRoot: options?.workspaceRoot ?? process.cwd(),
36
37
  inlineStylesExtension: options?.inlineStylesExtension ?? 'css',
37
38
  advanced: {
@@ -50,20 +51,19 @@ export function angular(options) {
50
51
  : defaultMarkdownTemplateTransforms,
51
52
  include: options?.include ?? [],
52
53
  additionalContentDirs: options?.additionalContentDirs ?? [],
54
+ liveReload: options?.liveReload ?? false,
55
+ disableTypeChecking: options?.disableTypeChecking ?? true,
53
56
  };
54
- // The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
55
- let fileEmitter;
56
- let compilerOptions = {};
57
- const ts = require('typescript');
58
57
  let resolvedConfig;
59
- let rootNames;
60
- let host;
61
58
  let nextProgram;
62
59
  let builderProgram;
63
60
  let watchMode = false;
64
- let testWatchMode = false;
61
+ let testWatchMode = isTestWatchMode();
62
+ let inlineComponentStyles;
63
+ let externalComponentStyles;
65
64
  const sourceFileCache = new SourceFileCache();
66
65
  const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST'];
66
+ const isVitestVscode = !!process.env['VITEST_VSCODE'];
67
67
  const isStackBlitz = !!process.versions['webcontainer'];
68
68
  const isAstroIntegration = process.env['ANALOG_ASTRO'] === 'true';
69
69
  const isStorybook = process.env['npm_lifecycle_script']?.includes('storybook') ||
@@ -72,28 +72,27 @@ export function angular(options) {
72
72
  process.env['ANALOG_STORYBOOK'] === 'true';
73
73
  const jit = typeof pluginOptions?.jit !== 'undefined' ? pluginOptions.jit : isTest;
74
74
  let viteServer;
75
- let styleTransform;
76
75
  const styleUrlsResolver = new StyleUrlsResolver();
77
76
  const templateUrlsResolver = new TemplateUrlsResolver();
77
+ const outputFiles = new Map();
78
+ const fileEmitter = (file) => {
79
+ return outputFiles.get(normalizePath(file));
80
+ };
81
+ let initialCompilation = false;
82
+ const declarationFiles = [];
78
83
  function angularPlugin() {
79
84
  let isProd = false;
85
+ if (angularMajor < 19 || isTest) {
86
+ pluginOptions.liveReload = false;
87
+ }
80
88
  return {
81
89
  name: '@analogjs/vite-plugin-angular',
82
- async watchChange() {
83
- if (isTest) {
84
- await buildAndAnalyze();
85
- }
86
- },
87
90
  async config(config, { command }) {
88
91
  watchMode = command === 'serve';
89
92
  isProd =
90
93
  config.mode === 'production' ||
91
94
  process.env['NODE_ENV'] === 'production';
92
- pluginOptions.tsconfig =
93
- options?.tsconfig ??
94
- resolve(config.root || '.', process.env['NODE_ENV'] === 'test'
95
- ? './tsconfig.spec.json'
96
- : './tsconfig.app.json');
95
+ pluginOptions.tsconfig = getTsConfigPath(config.root || '.', pluginOptions, isProd, isTest, !!config?.build?.lib);
97
96
  return {
98
97
  esbuild: config.esbuild ?? false,
99
98
  optimizeDeps: {
@@ -123,44 +122,94 @@ export function angular(options) {
123
122
  },
124
123
  configResolved(config) {
125
124
  resolvedConfig = config;
126
- // set test watch mode
127
- // - vite override from vitest-angular
128
- // - @nx/vite executor set server.watch explicitly to undefined (watch)/null (watch=false)
129
- // - vite config for test.watch variable
130
- testWatchMode =
131
- !(config.server.watch === null) ||
132
- config.test?.watch === true;
125
+ if (isTest) {
126
+ // set test watch mode
127
+ // - vite override from vitest-angular
128
+ // - @nx/vite executor set server.watch explicitly to undefined (watch)/null (watch=false)
129
+ // - vite config for test.watch variable
130
+ // - vitest watch mode detected from the command line
131
+ testWatchMode =
132
+ !(config.server.watch === null) ||
133
+ config.test?.watch === true ||
134
+ testWatchMode;
135
+ }
133
136
  },
134
137
  configureServer(server) {
135
138
  viteServer = server;
136
139
  server.watcher.on('add', async () => {
137
- setupCompilation(resolvedConfig);
138
- await buildAndAnalyze();
140
+ await performCompilation(resolvedConfig);
139
141
  });
140
142
  server.watcher.on('unlink', async () => {
141
- setupCompilation(resolvedConfig);
142
- await buildAndAnalyze();
143
+ await performCompilation(resolvedConfig);
143
144
  });
145
+ if (pluginOptions.liveReload) {
146
+ const angularComponentMiddleware = async (req, res, next) => {
147
+ if (req.url === undefined || res.writableEnded) {
148
+ return;
149
+ }
150
+ if (!req.url.includes(ANGULAR_COMPONENT_PREFIX)) {
151
+ next();
152
+ return;
153
+ }
154
+ const requestUrl = new URL(req.url, 'http://localhost');
155
+ const componentId = requestUrl.searchParams.get('c');
156
+ if (!componentId) {
157
+ res.statusCode = 400;
158
+ res.end();
159
+ return;
160
+ }
161
+ const [fileId] = decodeURIComponent(componentId).split('@');
162
+ const resolvedId = resolve(process.cwd(), fileId);
163
+ const invalidated = !!server.moduleGraph.getModuleById(resolvedId)
164
+ ?.lastInvalidationTimestamp && classNames.get(resolvedId);
165
+ // don't send an HMR update until the file has been invalidated
166
+ if (!invalidated) {
167
+ res.setHeader('Content-Type', 'text/javascript');
168
+ res.setHeader('Cache-Control', 'no-cache');
169
+ res.end('');
170
+ return;
171
+ }
172
+ const result = fileEmitter(resolvedId);
173
+ res.setHeader('Content-Type', 'text/javascript');
174
+ res.setHeader('Cache-Control', 'no-cache');
175
+ res.end(`${result?.hmrUpdateCode || ''}`);
176
+ };
177
+ viteServer.middlewares.use(angularComponentMiddleware);
178
+ }
144
179
  },
145
180
  async buildStart() {
146
- setupCompilation(resolvedConfig);
147
- // Only store cache if in watch mode
148
- if (watchMode) {
149
- augmentHostWithCaching(host, sourceFileCache);
181
+ // Defer the first compilation in test mode
182
+ if (!isVitestVscode) {
183
+ const { host } = await performCompilation(resolvedConfig);
184
+ initialCompilation = true;
185
+ // Only store cache if in watch mode
186
+ if (watchMode) {
187
+ augmentHostWithCaching(host, sourceFileCache);
188
+ }
150
189
  }
151
- await buildAndAnalyze();
152
190
  },
153
191
  async handleHotUpdate(ctx) {
154
- // The `handleHotUpdate` hook may be called before the `buildStart`,
155
- // which sets the compilation. As a result, the `host` may not be available
156
- // yet for use, leading to build errors such as "cannot read properties of undefined"
157
- // (because `host` is undefined).
158
- if (!host) {
159
- return;
160
- }
161
192
  if (TS_EXT_REGEX.test(ctx.file)) {
162
- sourceFileCache.invalidate([ctx.file.replace(/\?(.*)/, '')]);
163
- await buildAndAnalyze();
193
+ let [fileId] = ctx.file.split('?');
194
+ if (pluginOptions.supportAnalogFormat &&
195
+ ['ag', 'analog', 'agx'].some((ext) => fileId.endsWith(ext))) {
196
+ fileId += '.ts';
197
+ }
198
+ sourceFileCache.invalidate([fileId]);
199
+ await performCompilation(resolvedConfig, [fileId]);
200
+ const result = fileEmitter(fileId);
201
+ if (pluginOptions.liveReload &&
202
+ !!result?.hmrEligible &&
203
+ classNames.get(fileId)) {
204
+ const relativeFileId = `${relative(process.cwd(), fileId)}@${classNames.get(fileId)}`;
205
+ sendHMRComponentUpdate(ctx.server, relativeFileId);
206
+ return ctx.modules.map((mod) => {
207
+ if (mod.id === ctx.file) {
208
+ return markModuleSelfAccepting(mod);
209
+ }
210
+ return mod;
211
+ });
212
+ }
164
213
  }
165
214
  if (/\.(html|htm|css|less|sass|scss)$/.test(ctx.file)) {
166
215
  /**
@@ -169,28 +218,116 @@ export function angular(options) {
169
218
  */
170
219
  const isDirect = ctx.modules.find((mod) => ctx.file === mod.file && mod.id?.includes('?direct'));
171
220
  if (isDirect) {
221
+ if (pluginOptions.liveReload && isDirect?.id && isDirect.file) {
222
+ const isComponentStyle = isDirect.type === 'css' && isComponentStyleSheet(isDirect.id);
223
+ if (isComponentStyle) {
224
+ const { encapsulation } = getComponentStyleSheetMeta(isDirect.id);
225
+ // Track if the component uses ShadowDOM encapsulation
226
+ // Shadow DOM components currently require a full reload.
227
+ // Vite's CSS hot replacement does not support shadow root searching.
228
+ if (encapsulation !== 'shadow') {
229
+ ctx.server.ws.send({
230
+ type: 'update',
231
+ updates: [
232
+ {
233
+ type: 'css-update',
234
+ timestamp: Date.now(),
235
+ path: isDirect.url,
236
+ acceptedPath: isDirect.file,
237
+ },
238
+ ],
239
+ });
240
+ return ctx.modules
241
+ .filter((mod) => {
242
+ // Component stylesheets will have 2 modules (*.component.scss and *.component.scss?direct&ngcomp=xyz&e=x)
243
+ // We remove the module with the query params to prevent vite double logging the stylesheet name "hmr update *.component.scss, *.component.scss?direct&ngcomp=xyz&e=x"
244
+ return mod.file !== ctx.file || mod.id !== isDirect.id;
245
+ })
246
+ .map((mod) => {
247
+ if (mod.file === ctx.file) {
248
+ return markModuleSelfAccepting(mod);
249
+ }
250
+ return mod;
251
+ });
252
+ }
253
+ }
254
+ }
172
255
  return ctx.modules;
173
256
  }
174
257
  const mods = [];
258
+ const updates = [];
175
259
  ctx.modules.forEach((mod) => {
176
260
  mod.importers.forEach((imp) => {
177
261
  sourceFileCache.invalidate([imp.id]);
178
262
  ctx.server.moduleGraph.invalidateModule(imp);
179
- mods.push(imp);
263
+ if (pluginOptions.liveReload && classNames.get(imp.id)) {
264
+ updates.push(imp.id);
265
+ }
266
+ else {
267
+ mods.push(imp);
268
+ }
180
269
  });
181
270
  });
182
- await buildAndAnalyze();
271
+ await performCompilation(resolvedConfig, updates);
272
+ if (updates.length > 0) {
273
+ updates.forEach((updateId) => {
274
+ const impRelativeFileId = `${relative(process.cwd(), updateId)}@${classNames.get(updateId)}`;
275
+ sendHMRComponentUpdate(ctx.server, impRelativeFileId);
276
+ });
277
+ return ctx.modules.map((mod) => {
278
+ if (mod.id === ctx.file) {
279
+ return markModuleSelfAccepting(mod);
280
+ }
281
+ return mod;
282
+ });
283
+ }
183
284
  return mods;
184
285
  }
286
+ // clear HMR updates with a full reload
287
+ classNames.clear();
185
288
  return ctx.modules;
186
289
  },
187
- resolveId(id, importer) {
290
+ resolveId(id, importer, options) {
188
291
  if (id.startsWith('angular:jit:')) {
189
292
  const path = id.split(';')[1];
190
293
  return `${normalizePath(resolve(dirname(importer), path))}?raw`;
191
294
  }
295
+ // Map angular external styleUrls to the source file
296
+ if (isComponentStyleSheet(id)) {
297
+ const componentStyles = externalComponentStyles?.get(getFilenameFromPath(id));
298
+ if (componentStyles) {
299
+ return componentStyles + new URL(id, 'http://localhost').search;
300
+ }
301
+ }
302
+ if (options?.ssr && id.includes(ANGULAR_COMPONENT_PREFIX)) {
303
+ const requestUrl = new URL(id.slice(1), 'http://localhost');
304
+ const componentId = requestUrl.searchParams.get('c');
305
+ const res = resolve(process.cwd(), decodeURIComponent(componentId).split('@')[0]);
306
+ return res;
307
+ }
192
308
  return undefined;
193
309
  },
310
+ async load(id, options) {
311
+ // Map angular inline styles to the source text
312
+ if (isComponentStyleSheet(id)) {
313
+ const componentStyles = inlineComponentStyles?.get(getFilenameFromPath(id));
314
+ if (componentStyles) {
315
+ return componentStyles;
316
+ }
317
+ }
318
+ if (pluginOptions.liveReload &&
319
+ options?.ssr &&
320
+ id.includes(ANGULAR_COMPONENT_PREFIX)) {
321
+ const requestUrl = new URL(id.slice(1), 'http://localhost');
322
+ const componentId = requestUrl.searchParams.get('c');
323
+ if (!componentId) {
324
+ return;
325
+ }
326
+ const result = fileEmitter(resolve(process.cwd(), decodeURIComponent(componentId).split('@')[0]));
327
+ return result?.hmrUpdateCode || '';
328
+ }
329
+ return;
330
+ },
194
331
  async transform(code, id) {
195
332
  // Skip transforming node_modules
196
333
  if (id.includes('node_modules')) {
@@ -220,6 +357,19 @@ export function angular(options) {
220
357
  if (id.includes('analog-content-')) {
221
358
  return;
222
359
  }
360
+ /**
361
+ * Encapsulate component stylesheets that use emulated encapsulation
362
+ */
363
+ if (pluginOptions.liveReload && isComponentStyleSheet(id)) {
364
+ const { encapsulation, componentId } = getComponentStyleSheetMeta(id);
365
+ if (encapsulation === 'emulated' && componentId) {
366
+ const encapsulated = ngCompiler.encapsulateStyle(code, componentId);
367
+ return {
368
+ code: encapsulated,
369
+ map: null,
370
+ };
371
+ }
372
+ }
223
373
  if (TS_EXT_REGEX.test(id)) {
224
374
  if (id.includes('.ts?')) {
225
375
  // Strip the query string off the ID
@@ -231,11 +381,17 @@ export function angular(options) {
231
381
  * for test(Vitest)
232
382
  */
233
383
  if (isTest) {
384
+ if (isVitestVscode && !initialCompilation) {
385
+ // Do full initial compilation
386
+ await performCompilation(resolvedConfig);
387
+ initialCompilation = true;
388
+ }
234
389
  const tsMod = viteServer?.moduleGraph.getModuleById(id);
235
390
  if (tsMod) {
236
- sourceFileCache.invalidate([id]);
237
- if (testWatchMode) {
238
- await buildAndAnalyze();
391
+ const invalidated = tsMod.lastInvalidationTimestamp;
392
+ if (testWatchMode && invalidated) {
393
+ sourceFileCache.invalidate([id]);
394
+ await performCompilation(resolvedConfig, [id]);
239
395
  }
240
396
  }
241
397
  }
@@ -250,7 +406,7 @@ export function angular(options) {
250
406
  this.addWatchFile(absoluteFileUrl);
251
407
  }
252
408
  }
253
- const typescriptResult = await fileEmitter?.(id);
409
+ const typescriptResult = fileEmitter(id);
254
410
  if (typescriptResult?.warnings &&
255
411
  typescriptResult?.warnings.length > 0) {
256
412
  this.warn(`${typescriptResult.warnings.join('\n')}`);
@@ -283,7 +439,7 @@ export function angular(options) {
283
439
  pluginOptions.supportAnalogFormat &&
284
440
  fileEmitter) {
285
441
  sourceFileCache.invalidate([`${id}.ts`]);
286
- const ngFileResult = await fileEmitter(`${id}.ts`);
442
+ const ngFileResult = fileEmitter(`${id}.ts`);
287
443
  data = ngFileResult?.content || '';
288
444
  if (id.includes('.agx')) {
289
445
  const metadata = await getFrontmatterMetadata(code, id, pluginOptions.markdownTemplateTransforms || []);
@@ -297,6 +453,12 @@ export function angular(options) {
297
453
  }
298
454
  return undefined;
299
455
  },
456
+ closeBundle() {
457
+ declarationFiles.forEach(({ declarationFileDir, declarationPath, data }) => {
458
+ mkdirSync(declarationFileDir, { recursive: true });
459
+ writeFileSync(declarationPath, data, 'utf-8');
460
+ });
461
+ },
300
462
  };
301
463
  }
302
464
  return [
@@ -331,7 +493,7 @@ export function angular(options) {
331
493
  const globs = [
332
494
  `${appRoot}/**/*.{analog,agx,ag}`,
333
495
  ...extraGlobs.map((glob) => `${workspaceRoot}${glob}.{analog,agx,ag}`),
334
- ...(pluginOptions.additionalContentDirs || [])?.map((glob) => `${workspaceRoot}${glob}/**/*.agx`),
496
+ ...(pluginOptions.additionalContentDirs || []).map((glob) => `${workspaceRoot}${glob}/**/*.agx`),
335
497
  ...pluginOptions.include.map((glob) => `${workspaceRoot}${glob}`.replace(/\.ts$/, '.analog')),
336
498
  ];
337
499
  return fg
@@ -350,11 +512,29 @@ export function angular(options) {
350
512
  dot: true,
351
513
  });
352
514
  }
353
- function setupCompilation(config, context) {
515
+ function getTsConfigPath(root, options, isProd, isTest, isLib) {
516
+ if (options.tsconfig && isAbsolute(options.tsconfig)) {
517
+ return options.tsconfig;
518
+ }
519
+ let tsconfigFilePath = './tsconfig.app.json';
520
+ if (isLib) {
521
+ tsconfigFilePath = isProd
522
+ ? './tsconfig.lib.prod.json'
523
+ : './tsconfig.lib.json';
524
+ }
525
+ if (isTest) {
526
+ tsconfigFilePath = './tsconfig.spec.json';
527
+ }
528
+ if (options.tsconfig) {
529
+ tsconfigFilePath = options.tsconfig;
530
+ }
531
+ return resolve(root, tsconfigFilePath);
532
+ }
533
+ async function performCompilation(config, ids) {
354
534
  const isProd = config.mode === 'production';
355
535
  const analogFiles = findAnalogFiles(config);
356
536
  const includeFiles = findIncludes();
357
- const { options: tsCompilerOptions, rootNames: rn } = compilerCli.readConfiguration(pluginOptions.tsconfig, {
537
+ let { options: tsCompilerOptions, rootNames } = compilerCli.readConfiguration(pluginOptions.tsconfig, {
358
538
  suppressOutputPathCheck: true,
359
539
  outDir: undefined,
360
540
  sourceMap: false,
@@ -377,89 +557,262 @@ export function angular(options) {
377
557
  // AOT and virtually compiled .analog files.
378
558
  tsCompilerOptions.compilationMode = 'experimental-local';
379
559
  }
380
- rootNames = rn.concat(analogFiles, includeFiles);
381
- compilerOptions = tsCompilerOptions;
382
- host = ts.createIncrementalCompilerHost(compilerOptions);
383
- styleTransform = (code, filename) => preprocessCSS(code, filename, config);
560
+ if (pluginOptions.liveReload && watchMode) {
561
+ tsCompilerOptions['_enableHmr'] = true;
562
+ tsCompilerOptions['externalRuntimeStyles'] = true;
563
+ // Workaround for https://github.com/angular/angular/issues/59310
564
+ // Force extra instructions to be generated for HMR w/defer
565
+ tsCompilerOptions['supportTestBed'] = true;
566
+ }
567
+ if (tsCompilerOptions.compilationMode === 'partial') {
568
+ // These options can't be false in partial mode
569
+ tsCompilerOptions['supportTestBed'] = true;
570
+ tsCompilerOptions['supportJitMode'] = true;
571
+ }
572
+ if (!isTest && config.build?.lib) {
573
+ tsCompilerOptions['declaration'] = true;
574
+ tsCompilerOptions['declarationMap'] = watchMode;
575
+ tsCompilerOptions['inlineSources'] = true;
576
+ }
577
+ rootNames = rootNames.concat(analogFiles, includeFiles);
578
+ const ts = require('typescript');
579
+ const host = ts.createIncrementalCompilerHost(tsCompilerOptions);
384
580
  if (!jit) {
581
+ const styleTransform = (code, filename) => preprocessCSS(code, filename, config);
582
+ inlineComponentStyles = tsCompilerOptions['externalRuntimeStyles']
583
+ ? new Map()
584
+ : undefined;
585
+ externalComponentStyles = tsCompilerOptions['externalRuntimeStyles']
586
+ ? new Map()
587
+ : undefined;
385
588
  augmentHostWithResources(host, styleTransform, {
386
589
  inlineStylesExtension: pluginOptions.inlineStylesExtension,
387
590
  supportAnalogFormat: pluginOptions.supportAnalogFormat,
388
591
  isProd,
389
592
  markdownTemplateTransforms: pluginOptions.markdownTemplateTransforms,
593
+ inlineComponentStyles,
594
+ externalComponentStyles,
390
595
  });
391
596
  }
392
- }
393
- /**
394
- * Creates a new NgtscProgram to analyze/re-analyze
395
- * the source files and create a file emitter.
396
- * This is shared between an initial build and a hot update.
397
- */
398
- async function buildAndAnalyze() {
597
+ /**
598
+ * Creates a new NgtscProgram to analyze/re-analyze
599
+ * the source files and create a file emitter.
600
+ * This is shared between an initial build and a hot update.
601
+ */
399
602
  let builder;
400
603
  let typeScriptProgram;
401
604
  let angularCompiler;
402
605
  if (!jit) {
403
606
  // Create the Angular specific program that contains the Angular compiler
404
- const angularProgram = new compilerCli.NgtscProgram(rootNames, compilerOptions, host, nextProgram);
607
+ const angularProgram = new compilerCli.NgtscProgram(ids && ids.length > 0 ? ids : rootNames, tsCompilerOptions, host, nextProgram);
405
608
  angularCompiler = angularProgram.compiler;
406
609
  typeScriptProgram = angularProgram.getTsProgram();
407
610
  augmentProgramWithVersioning(typeScriptProgram);
408
- builder = builderProgram =
409
- ts.createEmitAndSemanticDiagnosticsBuilderProgram(typeScriptProgram, host, builderProgram);
611
+ builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(typeScriptProgram, host, builderProgram);
410
612
  await angularCompiler.analyzeAsync();
411
613
  nextProgram = angularProgram;
614
+ builderProgram =
615
+ builder;
412
616
  }
413
617
  else {
414
- builder = builderProgram =
415
- ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, compilerOptions, host, nextProgram);
618
+ builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, tsCompilerOptions, host, nextProgram);
416
619
  typeScriptProgram = builder.getProgram();
417
- nextProgram = builderProgram;
418
620
  }
419
621
  if (!watchMode) {
420
622
  // When not in watch mode, the startup cost of the incremental analysis can be avoided by
421
623
  // using an abstract builder that only wraps a TypeScript program.
422
624
  builder = ts.createAbstractBuilder(typeScriptProgram, host);
423
625
  }
424
- const getTypeChecker = () => builder.getProgram().getTypeChecker();
425
- fileEmitter = createFileEmitter(builder, mergeTransformers({
426
- before: [
427
- ...(jit
428
- ? [
429
- compilerCli.constructorParametersDownlevelTransform(builder.getProgram()),
430
- createJitResourceTransformer(getTypeChecker),
431
- ]
432
- : []),
433
- ...pluginOptions.advanced.tsTransformers.before,
434
- ],
435
- after: pluginOptions.advanced.tsTransformers.after,
436
- afterDeclarations: pluginOptions.advanced.tsTransformers.afterDeclarations,
437
- }, jit ? {} : angularCompiler.prepareEmit().transformers), () => [], angularCompiler);
626
+ const beforeTransformers = jit
627
+ ? [
628
+ compilerCli.constructorParametersDownlevelTransform(builder.getProgram()),
629
+ createJitResourceTransformer(() => builder.getProgram().getTypeChecker()),
630
+ ]
631
+ : [];
632
+ const transformers = mergeTransformers({ before: beforeTransformers }, jit ? {} : angularCompiler.prepareEmit().transformers);
633
+ const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.liveReload, pluginOptions.disableTypeChecking);
634
+ const writeFileCallback = (_filename, content, _a, _b, sourceFiles) => {
635
+ if (!sourceFiles?.length) {
636
+ return;
637
+ }
638
+ const filename = normalizePath(sourceFiles[0].fileName);
639
+ if (filename.includes('ngtypecheck.ts') || filename.includes('.d.')) {
640
+ return;
641
+ }
642
+ const metadata = watchMode
643
+ ? fileMetadata(filename, sourceFileCache.get(filename))
644
+ : {};
645
+ outputFiles.set(filename, {
646
+ content,
647
+ dependencies: [],
648
+ errors: metadata.errors,
649
+ warnings: metadata.warnings,
650
+ hmrUpdateCode: metadata.hmrUpdateCode,
651
+ hmrEligible: metadata.hmrEligible,
652
+ });
653
+ };
654
+ const writeOutputFile = (id) => {
655
+ const sourceFile = builder.getSourceFile(id);
656
+ if (!sourceFile) {
657
+ return;
658
+ }
659
+ let content = '';
660
+ builder.emit(sourceFile, (filename, data) => {
661
+ if (/\.[cm]?js$/.test(filename)) {
662
+ content = data;
663
+ }
664
+ if (!watchMode &&
665
+ !isTest &&
666
+ /\.d\.ts/.test(filename) &&
667
+ !filename.includes('.ngtypecheck.')) {
668
+ // output to library root instead /src
669
+ const declarationPath = resolve(config.root, config.build.outDir, relative(config.root, filename)).replace('/src/', '/');
670
+ const declarationFileDir = declarationPath
671
+ .replace(basename(filename), '')
672
+ .replace('/src/', '/');
673
+ declarationFiles.push({
674
+ declarationFileDir,
675
+ declarationPath,
676
+ data,
677
+ });
678
+ }
679
+ }, undefined /* cancellationToken */, undefined /* emitOnlyDtsFiles */, transformers);
680
+ writeFileCallback(id, content, false, undefined, [sourceFile]);
681
+ };
682
+ if (!watchMode) {
683
+ for (const sf of builder.getSourceFiles()) {
684
+ const id = sf.fileName;
685
+ writeOutputFile(id);
686
+ }
687
+ }
688
+ else {
689
+ if (ids && ids.length > 0) {
690
+ ids.forEach((id) => writeOutputFile(id));
691
+ }
692
+ else {
693
+ // TypeScript will loop until there are no more affected files in the program
694
+ while (builder.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)) {
695
+ /* empty */
696
+ }
697
+ }
698
+ }
699
+ return { host };
438
700
  }
439
701
  }
440
- export function createFileEmitter(program, transformers = {}, onAfterEmit, angularCompiler) {
441
- return async (file) => {
702
+ function sendHMRComponentUpdate(server, id) {
703
+ server.ws.send('angular:component-update', {
704
+ id: encodeURIComponent(id),
705
+ timestamp: Date.now(),
706
+ });
707
+ classNames.delete(id);
708
+ }
709
+ export function getFileMetadata(program, angularCompiler, liveReload, disableTypeChecking) {
710
+ const ts = require('typescript');
711
+ return (file, stale) => {
442
712
  const sourceFile = program.getSourceFile(file);
443
713
  if (!sourceFile) {
444
- return undefined;
714
+ return {};
445
715
  }
446
- const diagnostics = angularCompiler
447
- ? angularCompiler.getDiagnosticsForFile(sourceFile, 1)
448
- : [];
716
+ const hmrEligible = liveReload && stale
717
+ ? !!analyzeFileUpdates(stale, sourceFile, angularCompiler)
718
+ : false;
719
+ const diagnostics = getDiagnosticsForSourceFile(sourceFile, !!disableTypeChecking, program, angularCompiler);
449
720
  const errors = diagnostics
450
721
  .filter((d) => d.category === ts.DiagnosticCategory?.Error)
451
- .map((d) => d.messageText);
722
+ .map((d) => typeof d.messageText === 'object'
723
+ ? d.messageText.messageText
724
+ : d.messageText);
452
725
  const warnings = diagnostics
453
726
  .filter((d) => d.category === ts.DiagnosticCategory?.Warning)
454
727
  .map((d) => d.messageText);
455
- let content;
456
- program.emit(sourceFile, (filename, data) => {
457
- if (/\.[cm]?js$/.test(filename)) {
458
- content = data;
728
+ let hmrUpdateCode = undefined;
729
+ if (liveReload) {
730
+ for (const node of sourceFile.statements) {
731
+ if (ts.isClassDeclaration(node) && node.name != null) {
732
+ hmrUpdateCode = angularCompiler?.emitHmrUpdateModule(node);
733
+ !!hmrUpdateCode && classNames.set(file, node.name.getText());
734
+ }
459
735
  }
460
- }, undefined /* cancellationToken */, undefined /* emitOnlyDtsFiles */, transformers);
461
- onAfterEmit?.(sourceFile);
462
- return { content, dependencies: [], errors, warnings };
736
+ }
737
+ return { errors, warnings, hmrUpdateCode, hmrEligible };
463
738
  };
464
739
  }
740
+ function getDiagnosticsForSourceFile(sourceFile, disableTypeChecking, program, angularCompiler) {
741
+ const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
742
+ if (disableTypeChecking) {
743
+ // Syntax errors are cheap to compute and the app will not run if there are any
744
+ // So always show these types of errors regardless if type checking is disabled
745
+ return syntacticDiagnostics;
746
+ }
747
+ const semanticDiagnostics = program.getSemanticDiagnostics(sourceFile);
748
+ const angularDiagnostics = angularCompiler
749
+ ? angularCompiler.getDiagnosticsForFile(sourceFile, 1)
750
+ : [];
751
+ return [
752
+ ...syntacticDiagnostics,
753
+ ...semanticDiagnostics,
754
+ ...angularDiagnostics,
755
+ ];
756
+ }
757
+ function markModuleSelfAccepting(mod) {
758
+ // support Vite 6
759
+ if ('_clientModule' in mod) {
760
+ mod['_clientModule'].isSelfAccepting = true;
761
+ }
762
+ return {
763
+ ...mod,
764
+ isSelfAccepting: true,
765
+ };
766
+ }
767
+ function isComponentStyleSheet(id) {
768
+ return id.includes('ngcomp=');
769
+ }
770
+ function getComponentStyleSheetMeta(id) {
771
+ const params = new URL(id, 'http://localhost').searchParams;
772
+ const encapsulationMapping = {
773
+ '0': 'emulated',
774
+ '2': 'none',
775
+ '3': 'shadow',
776
+ };
777
+ return {
778
+ componentId: params.get('ngcomp'),
779
+ encapsulation: encapsulationMapping[params.get('e')],
780
+ };
781
+ }
782
+ /**
783
+ * Removes leading / and query string from a url path
784
+ * e.g. /foo.scss?direct&ngcomp=ng-c3153525609&e=0 returns foo.scss
785
+ * @param id
786
+ */
787
+ function getFilenameFromPath(id) {
788
+ return new URL(id, 'http://localhost').pathname.replace(/^\//, '');
789
+ }
790
+ /**
791
+ * Checks for vitest run from the command line
792
+ * @returns boolean
793
+ */
794
+ export function isTestWatchMode(args = process.argv) {
795
+ // vitest --run
796
+ const hasRun = args.find((arg) => arg.includes('--run'));
797
+ if (hasRun) {
798
+ return false;
799
+ }
800
+ // vitest --no-run
801
+ const hasNoRun = args.find((arg) => arg.includes('--no-run'));
802
+ if (hasNoRun) {
803
+ return true;
804
+ }
805
+ // check for --watch=false or --no-watch
806
+ const hasWatch = args.find((arg) => arg.includes('watch'));
807
+ if (hasWatch && ['false', 'no'].some((neg) => hasWatch.includes(neg))) {
808
+ return false;
809
+ }
810
+ // check for --watch false
811
+ const watchIndex = args.findIndex((arg) => arg.includes('watch'));
812
+ const watchArg = args[watchIndex + 1];
813
+ if (watchArg && watchArg === 'false') {
814
+ return false;
815
+ }
816
+ return true;
817
+ }
465
818
  //# sourceMappingURL=angular-vite-plugin.js.map