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

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,21 @@ 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 { liveReloadPlugin } from './live-reload-plugin.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 classNames = new Map();
25
28
  export function angular(options) {
26
29
  /**
27
30
  * Normalize plugin options so defaults
28
31
  * are used for values not provided.
29
32
  */
30
33
  const pluginOptions = {
31
- tsconfig: options?.tsconfig ??
32
- (process.env['NODE_ENV'] === 'test'
33
- ? './tsconfig.spec.json'
34
- : './tsconfig.app.json'),
34
+ tsconfig: options?.tsconfig || '',
35
35
  workspaceRoot: options?.workspaceRoot ?? process.cwd(),
36
36
  inlineStylesExtension: options?.inlineStylesExtension ?? 'css',
37
37
  advanced: {
@@ -50,20 +50,19 @@ export function angular(options) {
50
50
  : defaultMarkdownTemplateTransforms,
51
51
  include: options?.include ?? [],
52
52
  additionalContentDirs: options?.additionalContentDirs ?? [],
53
+ liveReload: options?.liveReload ?? false,
54
+ disableTypeChecking: options?.disableTypeChecking ?? true,
53
55
  };
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
56
  let resolvedConfig;
59
- let rootNames;
60
- let host;
61
57
  let nextProgram;
62
58
  let builderProgram;
63
59
  let watchMode = false;
64
- let testWatchMode = false;
60
+ let testWatchMode = isTestWatchMode();
61
+ let inlineComponentStyles;
62
+ let externalComponentStyles;
65
63
  const sourceFileCache = new SourceFileCache();
66
64
  const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST'];
65
+ const isVitestVscode = !!process.env['VITEST_VSCODE'];
67
66
  const isStackBlitz = !!process.versions['webcontainer'];
68
67
  const isAstroIntegration = process.env['ANALOG_ASTRO'] === 'true';
69
68
  const isStorybook = process.env['npm_lifecycle_script']?.includes('storybook') ||
@@ -72,28 +71,27 @@ export function angular(options) {
72
71
  process.env['ANALOG_STORYBOOK'] === 'true';
73
72
  const jit = typeof pluginOptions?.jit !== 'undefined' ? pluginOptions.jit : isTest;
74
73
  let viteServer;
75
- let styleTransform;
76
74
  const styleUrlsResolver = new StyleUrlsResolver();
77
75
  const templateUrlsResolver = new TemplateUrlsResolver();
76
+ const outputFiles = new Map();
77
+ const fileEmitter = (file) => {
78
+ return outputFiles.get(normalizePath(file));
79
+ };
80
+ let initialCompilation = false;
81
+ const declarationFiles = [];
78
82
  function angularPlugin() {
79
83
  let isProd = false;
84
+ if (angularMajor < 19 || isTest) {
85
+ pluginOptions.liveReload = false;
86
+ }
80
87
  return {
81
88
  name: '@analogjs/vite-plugin-angular',
82
- async watchChange() {
83
- if (isTest) {
84
- await buildAndAnalyze();
85
- }
86
- },
87
89
  async config(config, { command }) {
88
90
  watchMode = command === 'serve';
89
91
  isProd =
90
92
  config.mode === 'production' ||
91
93
  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');
94
+ pluginOptions.tsconfig = getTsConfigPath(config.root || '.', pluginOptions, isProd, isTest, !!config?.build?.lib);
97
95
  return {
98
96
  esbuild: config.esbuild ?? false,
99
97
  optimizeDeps: {
@@ -123,44 +121,59 @@ export function angular(options) {
123
121
  },
124
122
  configResolved(config) {
125
123
  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;
124
+ if (isTest) {
125
+ // set test watch mode
126
+ // - vite override from vitest-angular
127
+ // - @nx/vite executor set server.watch explicitly to undefined (watch)/null (watch=false)
128
+ // - vite config for test.watch variable
129
+ // - vitest watch mode detected from the command line
130
+ testWatchMode =
131
+ !(config.server.watch === null) ||
132
+ config.test?.watch === true ||
133
+ testWatchMode;
134
+ }
133
135
  },
134
136
  configureServer(server) {
135
137
  viteServer = server;
136
138
  server.watcher.on('add', async () => {
137
- setupCompilation(resolvedConfig);
138
- await buildAndAnalyze();
139
+ await performCompilation(resolvedConfig);
139
140
  });
140
141
  server.watcher.on('unlink', async () => {
141
- setupCompilation(resolvedConfig);
142
- await buildAndAnalyze();
142
+ await performCompilation(resolvedConfig);
143
143
  });
144
144
  },
145
145
  async buildStart() {
146
- setupCompilation(resolvedConfig);
147
- // Only store cache if in watch mode
148
- if (watchMode) {
149
- augmentHostWithCaching(host, sourceFileCache);
146
+ // Defer the first compilation in test mode
147
+ if (!isVitestVscode) {
148
+ const { host } = await performCompilation(resolvedConfig);
149
+ initialCompilation = true;
150
+ // Only store cache if in watch mode
151
+ if (watchMode) {
152
+ augmentHostWithCaching(host, sourceFileCache);
153
+ }
150
154
  }
151
- await buildAndAnalyze();
152
155
  },
153
156
  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
157
  if (TS_EXT_REGEX.test(ctx.file)) {
162
- sourceFileCache.invalidate([ctx.file.replace(/\?(.*)/, '')]);
163
- await buildAndAnalyze();
158
+ let [fileId] = ctx.file.split('?');
159
+ if (pluginOptions.supportAnalogFormat &&
160
+ ['ag', 'analog', 'agx'].some((ext) => fileId.endsWith(ext))) {
161
+ fileId += '.ts';
162
+ }
163
+ await performCompilation(resolvedConfig, [fileId]);
164
+ const result = fileEmitter(fileId);
165
+ if (pluginOptions.liveReload &&
166
+ result?.hmrEligible &&
167
+ classNames.get(fileId)) {
168
+ const relativeFileId = `${relative(process.cwd(), fileId)}@${classNames.get(fileId)}`;
169
+ sendHMRComponentUpdate(ctx.server, relativeFileId);
170
+ return ctx.modules.map((mod) => {
171
+ if (mod.id === ctx.file) {
172
+ return markModuleSelfAccepting(mod);
173
+ }
174
+ return mod;
175
+ });
176
+ }
164
177
  }
165
178
  if (/\.(html|htm|css|less|sass|scss)$/.test(ctx.file)) {
166
179
  /**
@@ -169,19 +182,73 @@ export function angular(options) {
169
182
  */
170
183
  const isDirect = ctx.modules.find((mod) => ctx.file === mod.file && mod.id?.includes('?direct'));
171
184
  if (isDirect) {
185
+ if (pluginOptions.liveReload && isDirect?.id && isDirect.file) {
186
+ const isComponentStyle = isDirect.type === 'css' && isComponentStyleSheet(isDirect.id);
187
+ if (isComponentStyle) {
188
+ const { encapsulation } = getComponentStyleSheetMeta(isDirect.id);
189
+ // Track if the component uses ShadowDOM encapsulation
190
+ // Shadow DOM components currently require a full reload.
191
+ // Vite's CSS hot replacement does not support shadow root searching.
192
+ if (encapsulation !== 'shadow') {
193
+ ctx.server.ws.send({
194
+ type: 'update',
195
+ updates: [
196
+ {
197
+ type: 'css-update',
198
+ timestamp: Date.now(),
199
+ path: isDirect.url,
200
+ acceptedPath: isDirect.file,
201
+ },
202
+ ],
203
+ });
204
+ return ctx.modules
205
+ .filter((mod) => {
206
+ // Component stylesheets will have 2 modules (*.component.scss and *.component.scss?direct&ngcomp=xyz&e=x)
207
+ // 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"
208
+ return mod.file !== ctx.file || mod.id !== isDirect.id;
209
+ })
210
+ .map((mod) => {
211
+ if (mod.file === ctx.file) {
212
+ return markModuleSelfAccepting(mod);
213
+ }
214
+ return mod;
215
+ });
216
+ }
217
+ }
218
+ }
172
219
  return ctx.modules;
173
220
  }
174
221
  const mods = [];
222
+ const updates = [];
175
223
  ctx.modules.forEach((mod) => {
176
224
  mod.importers.forEach((imp) => {
177
225
  sourceFileCache.invalidate([imp.id]);
178
226
  ctx.server.moduleGraph.invalidateModule(imp);
179
- mods.push(imp);
227
+ if (pluginOptions.liveReload && classNames.get(imp.id)) {
228
+ updates.push(imp.id);
229
+ }
230
+ else {
231
+ mods.push(imp);
232
+ }
180
233
  });
181
234
  });
182
- await buildAndAnalyze();
235
+ await performCompilation(resolvedConfig, updates);
236
+ if (updates.length > 0) {
237
+ updates.forEach((updateId) => {
238
+ const impRelativeFileId = `${relative(process.cwd(), updateId)}@${classNames.get(updateId)}`;
239
+ sendHMRComponentUpdate(ctx.server, impRelativeFileId);
240
+ });
241
+ return ctx.modules.map((mod) => {
242
+ if (mod.id === ctx.file) {
243
+ return markModuleSelfAccepting(mod);
244
+ }
245
+ return mod;
246
+ });
247
+ }
183
248
  return mods;
184
249
  }
250
+ // clear HMR updates with a full reload
251
+ classNames.clear();
185
252
  return ctx.modules;
186
253
  },
187
254
  resolveId(id, importer) {
@@ -189,8 +256,25 @@ export function angular(options) {
189
256
  const path = id.split(';')[1];
190
257
  return `${normalizePath(resolve(dirname(importer), path))}?raw`;
191
258
  }
259
+ // Map angular external styleUrls to the source file
260
+ if (isComponentStyleSheet(id)) {
261
+ const componentStyles = externalComponentStyles?.get(getFilenameFromPath(id));
262
+ if (componentStyles) {
263
+ return componentStyles + new URL(id, 'http://localhost').search;
264
+ }
265
+ }
192
266
  return undefined;
193
267
  },
268
+ async load(id) {
269
+ // Map angular inline styles to the source text
270
+ if (isComponentStyleSheet(id)) {
271
+ const componentStyles = inlineComponentStyles?.get(getFilenameFromPath(id));
272
+ if (componentStyles) {
273
+ return componentStyles;
274
+ }
275
+ }
276
+ return;
277
+ },
194
278
  async transform(code, id) {
195
279
  // Skip transforming node_modules
196
280
  if (id.includes('node_modules')) {
@@ -220,6 +304,19 @@ export function angular(options) {
220
304
  if (id.includes('analog-content-')) {
221
305
  return;
222
306
  }
307
+ /**
308
+ * Encapsulate component stylesheets that use emulated encapsulation
309
+ */
310
+ if (pluginOptions.liveReload && isComponentStyleSheet(id)) {
311
+ const { encapsulation, componentId } = getComponentStyleSheetMeta(id);
312
+ if (encapsulation === 'emulated' && componentId) {
313
+ const encapsulated = ngCompiler.encapsulateStyle(code, componentId);
314
+ return {
315
+ code: encapsulated,
316
+ map: null,
317
+ };
318
+ }
319
+ }
223
320
  if (TS_EXT_REGEX.test(id)) {
224
321
  if (id.includes('.ts?')) {
225
322
  // Strip the query string off the ID
@@ -231,11 +328,17 @@ export function angular(options) {
231
328
  * for test(Vitest)
232
329
  */
233
330
  if (isTest) {
331
+ if (isVitestVscode && !initialCompilation) {
332
+ // Do full initial compilation
333
+ await performCompilation(resolvedConfig);
334
+ initialCompilation = true;
335
+ }
234
336
  const tsMod = viteServer?.moduleGraph.getModuleById(id);
235
337
  if (tsMod) {
236
- sourceFileCache.invalidate([id]);
237
- if (testWatchMode) {
238
- await buildAndAnalyze();
338
+ const invalidated = tsMod.lastInvalidationTimestamp;
339
+ if (testWatchMode && invalidated) {
340
+ sourceFileCache.invalidate([id]);
341
+ await performCompilation(resolvedConfig, [id]);
239
342
  }
240
343
  }
241
344
  }
@@ -250,7 +353,7 @@ export function angular(options) {
250
353
  this.addWatchFile(absoluteFileUrl);
251
354
  }
252
355
  }
253
- const typescriptResult = await fileEmitter?.(id);
356
+ const typescriptResult = fileEmitter(id);
254
357
  if (typescriptResult?.warnings &&
255
358
  typescriptResult?.warnings.length > 0) {
256
359
  this.warn(`${typescriptResult.warnings.join('\n')}`);
@@ -283,7 +386,7 @@ export function angular(options) {
283
386
  pluginOptions.supportAnalogFormat &&
284
387
  fileEmitter) {
285
388
  sourceFileCache.invalidate([`${id}.ts`]);
286
- const ngFileResult = await fileEmitter(`${id}.ts`);
389
+ const ngFileResult = fileEmitter(`${id}.ts`);
287
390
  data = ngFileResult?.content || '';
288
391
  if (id.includes('.agx')) {
289
392
  const metadata = await getFrontmatterMetadata(code, id, pluginOptions.markdownTemplateTransforms || []);
@@ -297,10 +400,17 @@ export function angular(options) {
297
400
  }
298
401
  return undefined;
299
402
  },
403
+ closeBundle() {
404
+ declarationFiles.forEach(({ declarationFileDir, declarationPath, data }) => {
405
+ mkdirSync(declarationFileDir, { recursive: true });
406
+ writeFileSync(declarationPath, data, 'utf-8');
407
+ });
408
+ },
300
409
  };
301
410
  }
302
411
  return [
303
412
  angularPlugin(),
413
+ pluginOptions.liveReload && liveReloadPlugin({ classNames, fileEmitter }),
304
414
  ...(isTest && !isStackBlitz ? angularVitestPlugins() : []),
305
415
  (jit &&
306
416
  jitPlugin({
@@ -331,7 +441,7 @@ export function angular(options) {
331
441
  const globs = [
332
442
  `${appRoot}/**/*.{analog,agx,ag}`,
333
443
  ...extraGlobs.map((glob) => `${workspaceRoot}${glob}.{analog,agx,ag}`),
334
- ...(pluginOptions.additionalContentDirs || [])?.map((glob) => `${workspaceRoot}${glob}/**/*.agx`),
444
+ ...(pluginOptions.additionalContentDirs || []).map((glob) => `${workspaceRoot}${glob}/**/*.agx`),
335
445
  ...pluginOptions.include.map((glob) => `${workspaceRoot}${glob}`.replace(/\.ts$/, '.analog')),
336
446
  ];
337
447
  return fg
@@ -350,11 +460,29 @@ export function angular(options) {
350
460
  dot: true,
351
461
  });
352
462
  }
353
- function setupCompilation(config, context) {
463
+ function getTsConfigPath(root, options, isProd, isTest, isLib) {
464
+ if (options.tsconfig && isAbsolute(options.tsconfig)) {
465
+ return options.tsconfig;
466
+ }
467
+ let tsconfigFilePath = './tsconfig.app.json';
468
+ if (isLib) {
469
+ tsconfigFilePath = isProd
470
+ ? './tsconfig.lib.prod.json'
471
+ : './tsconfig.lib.json';
472
+ }
473
+ if (isTest) {
474
+ tsconfigFilePath = './tsconfig.spec.json';
475
+ }
476
+ if (options.tsconfig) {
477
+ tsconfigFilePath = options.tsconfig;
478
+ }
479
+ return resolve(root, tsconfigFilePath);
480
+ }
481
+ async function performCompilation(config, ids) {
354
482
  const isProd = config.mode === 'production';
355
483
  const analogFiles = findAnalogFiles(config);
356
484
  const includeFiles = findIncludes();
357
- const { options: tsCompilerOptions, rootNames: rn } = compilerCli.readConfiguration(pluginOptions.tsconfig, {
485
+ let { options: tsCompilerOptions, rootNames } = compilerCli.readConfiguration(pluginOptions.tsconfig, {
358
486
  suppressOutputPathCheck: true,
359
487
  outDir: undefined,
360
488
  sourceMap: false,
@@ -377,89 +505,261 @@ export function angular(options) {
377
505
  // AOT and virtually compiled .analog files.
378
506
  tsCompilerOptions.compilationMode = 'experimental-local';
379
507
  }
380
- rootNames = rn.concat(analogFiles, includeFiles);
381
- compilerOptions = tsCompilerOptions;
382
- host = ts.createIncrementalCompilerHost(compilerOptions);
383
- styleTransform = (code, filename) => preprocessCSS(code, filename, config);
508
+ if (pluginOptions.liveReload && watchMode) {
509
+ tsCompilerOptions['_enableHmr'] = true;
510
+ tsCompilerOptions['externalRuntimeStyles'] = true;
511
+ // Workaround for https://github.com/angular/angular/issues/59310
512
+ // Force extra instructions to be generated for HMR w/defer
513
+ tsCompilerOptions['supportTestBed'] = true;
514
+ }
515
+ if (tsCompilerOptions.compilationMode === 'partial') {
516
+ // These options can't be false in partial mode
517
+ tsCompilerOptions['supportTestBed'] = true;
518
+ tsCompilerOptions['supportJitMode'] = true;
519
+ }
520
+ if (!isTest && config.build?.lib) {
521
+ tsCompilerOptions['declaration'] = true;
522
+ tsCompilerOptions['declarationMap'] = watchMode;
523
+ tsCompilerOptions['inlineSources'] = true;
524
+ }
525
+ rootNames = rootNames.concat(analogFiles, includeFiles);
526
+ const ts = require('typescript');
527
+ const host = ts.createIncrementalCompilerHost(tsCompilerOptions);
384
528
  if (!jit) {
529
+ const styleTransform = (code, filename) => preprocessCSS(code, filename, config);
530
+ inlineComponentStyles = tsCompilerOptions['externalRuntimeStyles']
531
+ ? new Map()
532
+ : undefined;
533
+ externalComponentStyles = tsCompilerOptions['externalRuntimeStyles']
534
+ ? new Map()
535
+ : undefined;
385
536
  augmentHostWithResources(host, styleTransform, {
386
537
  inlineStylesExtension: pluginOptions.inlineStylesExtension,
387
538
  supportAnalogFormat: pluginOptions.supportAnalogFormat,
388
539
  isProd,
389
540
  markdownTemplateTransforms: pluginOptions.markdownTemplateTransforms,
541
+ inlineComponentStyles,
542
+ externalComponentStyles,
390
543
  });
391
544
  }
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() {
545
+ /**
546
+ * Creates a new NgtscProgram to analyze/re-analyze
547
+ * the source files and create a file emitter.
548
+ * This is shared between an initial build and a hot update.
549
+ */
399
550
  let builder;
400
551
  let typeScriptProgram;
401
552
  let angularCompiler;
402
553
  if (!jit) {
403
554
  // Create the Angular specific program that contains the Angular compiler
404
- const angularProgram = new compilerCli.NgtscProgram(rootNames, compilerOptions, host, nextProgram);
555
+ const angularProgram = new compilerCli.NgtscProgram(ids && ids.length > 0 ? ids : rootNames, tsCompilerOptions, host, nextProgram);
405
556
  angularCompiler = angularProgram.compiler;
406
557
  typeScriptProgram = angularProgram.getTsProgram();
407
558
  augmentProgramWithVersioning(typeScriptProgram);
408
- builder = builderProgram =
409
- ts.createEmitAndSemanticDiagnosticsBuilderProgram(typeScriptProgram, host, builderProgram);
559
+ builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(typeScriptProgram, host, builderProgram);
410
560
  await angularCompiler.analyzeAsync();
411
561
  nextProgram = angularProgram;
562
+ builderProgram =
563
+ builder;
412
564
  }
413
565
  else {
414
- builder = builderProgram =
415
- ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, compilerOptions, host, nextProgram);
566
+ builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, tsCompilerOptions, host, nextProgram);
416
567
  typeScriptProgram = builder.getProgram();
417
- nextProgram = builderProgram;
418
568
  }
419
569
  if (!watchMode) {
420
570
  // When not in watch mode, the startup cost of the incremental analysis can be avoided by
421
571
  // using an abstract builder that only wraps a TypeScript program.
422
572
  builder = ts.createAbstractBuilder(typeScriptProgram, host);
423
573
  }
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);
574
+ const beforeTransformers = jit
575
+ ? [
576
+ compilerCli.constructorParametersDownlevelTransform(builder.getProgram()),
577
+ createJitResourceTransformer(() => builder.getProgram().getTypeChecker()),
578
+ ]
579
+ : [];
580
+ const transformers = mergeTransformers({ before: beforeTransformers }, jit ? {} : angularCompiler.prepareEmit().transformers);
581
+ const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.liveReload, pluginOptions.disableTypeChecking);
582
+ const writeFileCallback = (_filename, content, _a, _b, sourceFiles) => {
583
+ if (!sourceFiles?.length) {
584
+ return;
585
+ }
586
+ const filename = normalizePath(sourceFiles[0].fileName);
587
+ if (filename.includes('ngtypecheck.ts') || filename.includes('.d.')) {
588
+ return;
589
+ }
590
+ const metadata = watchMode ? fileMetadata(filename) : {};
591
+ outputFiles.set(filename, {
592
+ content,
593
+ dependencies: [],
594
+ errors: metadata.errors,
595
+ warnings: metadata.warnings,
596
+ hmrUpdateCode: metadata.hmrUpdateCode,
597
+ hmrEligible: metadata.hmrEligible,
598
+ });
599
+ };
600
+ const writeOutputFile = (id) => {
601
+ const sourceFile = builder.getSourceFile(id);
602
+ if (!sourceFile) {
603
+ return;
604
+ }
605
+ let content = '';
606
+ builder.emit(sourceFile, (filename, data) => {
607
+ if (/\.[cm]?js$/.test(filename)) {
608
+ content = data;
609
+ }
610
+ if (!watchMode &&
611
+ !isTest &&
612
+ /\.d\.ts/.test(filename) &&
613
+ !filename.includes('.ngtypecheck.')) {
614
+ // output to library root instead /src
615
+ const declarationPath = resolve(config.root, config.build.outDir, relative(config.root, filename)).replace('/src/', '/');
616
+ const declarationFileDir = declarationPath
617
+ .replace(basename(filename), '')
618
+ .replace('/src/', '/');
619
+ declarationFiles.push({
620
+ declarationFileDir,
621
+ declarationPath,
622
+ data,
623
+ });
624
+ }
625
+ }, undefined /* cancellationToken */, undefined /* emitOnlyDtsFiles */, transformers);
626
+ writeFileCallback(id, content, false, undefined, [sourceFile]);
627
+ };
628
+ if (!watchMode) {
629
+ for (const sf of builder.getSourceFiles()) {
630
+ const id = sf.fileName;
631
+ writeOutputFile(id);
632
+ }
633
+ }
634
+ else {
635
+ if (ids && ids.length > 0) {
636
+ ids.forEach((id) => writeOutputFile(id));
637
+ }
638
+ else {
639
+ // TypeScript will loop until there are no more affected files in the program
640
+ while (builder.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)) {
641
+ /* empty */
642
+ }
643
+ }
644
+ }
645
+ return { host };
438
646
  }
439
647
  }
440
- export function createFileEmitter(program, transformers = {}, onAfterEmit, angularCompiler) {
441
- return async (file) => {
648
+ function sendHMRComponentUpdate(server, id) {
649
+ server.ws.send('angular:component-update', {
650
+ id: encodeURIComponent(id),
651
+ timestamp: Date.now(),
652
+ });
653
+ classNames.delete(id);
654
+ }
655
+ export function getFileMetadata(program, angularCompiler, liveReload, disableTypeChecking) {
656
+ const ts = require('typescript');
657
+ return (file) => {
442
658
  const sourceFile = program.getSourceFile(file);
443
659
  if (!sourceFile) {
444
- return undefined;
660
+ return {};
445
661
  }
446
- const diagnostics = angularCompiler
447
- ? angularCompiler.getDiagnosticsForFile(sourceFile, 1)
448
- : [];
662
+ const diagnostics = getDiagnosticsForSourceFile(sourceFile, !!disableTypeChecking, program, angularCompiler);
449
663
  const errors = diagnostics
450
664
  .filter((d) => d.category === ts.DiagnosticCategory?.Error)
451
- .map((d) => d.messageText);
665
+ .map((d) => typeof d.messageText === 'object'
666
+ ? d.messageText.messageText
667
+ : d.messageText);
452
668
  const warnings = diagnostics
453
669
  .filter((d) => d.category === ts.DiagnosticCategory?.Warning)
454
670
  .map((d) => d.messageText);
455
- let content;
456
- program.emit(sourceFile, (filename, data) => {
457
- if (/\.[cm]?js$/.test(filename)) {
458
- content = data;
671
+ let hmrUpdateCode = undefined;
672
+ let hmrEligible = false;
673
+ if (liveReload) {
674
+ for (const node of sourceFile.statements) {
675
+ if (ts.isClassDeclaration(node) && node.name != null) {
676
+ hmrUpdateCode = angularCompiler?.emitHmrUpdateModule(node);
677
+ if (!!hmrUpdateCode) {
678
+ classNames.set(file, node.name.getText());
679
+ hmrEligible = true;
680
+ }
681
+ }
459
682
  }
460
- }, undefined /* cancellationToken */, undefined /* emitOnlyDtsFiles */, transformers);
461
- onAfterEmit?.(sourceFile);
462
- return { content, dependencies: [], errors, warnings };
683
+ }
684
+ return { errors, warnings, hmrUpdateCode, hmrEligible };
685
+ };
686
+ }
687
+ function getDiagnosticsForSourceFile(sourceFile, disableTypeChecking, program, angularCompiler) {
688
+ const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
689
+ if (disableTypeChecking) {
690
+ // Syntax errors are cheap to compute and the app will not run if there are any
691
+ // So always show these types of errors regardless if type checking is disabled
692
+ return syntacticDiagnostics;
693
+ }
694
+ const semanticDiagnostics = program.getSemanticDiagnostics(sourceFile);
695
+ const angularDiagnostics = angularCompiler
696
+ ? angularCompiler.getDiagnosticsForFile(sourceFile, 1)
697
+ : [];
698
+ return [
699
+ ...syntacticDiagnostics,
700
+ ...semanticDiagnostics,
701
+ ...angularDiagnostics,
702
+ ];
703
+ }
704
+ function markModuleSelfAccepting(mod) {
705
+ // support Vite 6
706
+ if ('_clientModule' in mod) {
707
+ mod['_clientModule'].isSelfAccepting = true;
708
+ }
709
+ return {
710
+ ...mod,
711
+ isSelfAccepting: true,
712
+ };
713
+ }
714
+ function isComponentStyleSheet(id) {
715
+ return id.includes('ngcomp=');
716
+ }
717
+ function getComponentStyleSheetMeta(id) {
718
+ const params = new URL(id, 'http://localhost').searchParams;
719
+ const encapsulationMapping = {
720
+ '0': 'emulated',
721
+ '2': 'none',
722
+ '3': 'shadow',
463
723
  };
724
+ return {
725
+ componentId: params.get('ngcomp'),
726
+ encapsulation: encapsulationMapping[params.get('e')],
727
+ };
728
+ }
729
+ /**
730
+ * Removes leading / and query string from a url path
731
+ * e.g. /foo.scss?direct&ngcomp=ng-c3153525609&e=0 returns foo.scss
732
+ * @param id
733
+ */
734
+ function getFilenameFromPath(id) {
735
+ return new URL(id, 'http://localhost').pathname.replace(/^\//, '');
736
+ }
737
+ /**
738
+ * Checks for vitest run from the command line
739
+ * @returns boolean
740
+ */
741
+ export function isTestWatchMode(args = process.argv) {
742
+ // vitest --run
743
+ const hasRun = args.find((arg) => arg.includes('--run'));
744
+ if (hasRun) {
745
+ return false;
746
+ }
747
+ // vitest --no-run
748
+ const hasNoRun = args.find((arg) => arg.includes('--no-run'));
749
+ if (hasNoRun) {
750
+ return true;
751
+ }
752
+ // check for --watch=false or --no-watch
753
+ const hasWatch = args.find((arg) => arg.includes('watch'));
754
+ if (hasWatch && ['false', 'no'].some((neg) => hasWatch.includes(neg))) {
755
+ return false;
756
+ }
757
+ // check for --watch false
758
+ const watchIndex = args.findIndex((arg) => arg.includes('watch'));
759
+ const watchArg = args[watchIndex + 1];
760
+ if (watchArg && watchArg === 'false') {
761
+ return false;
762
+ }
763
+ return true;
464
764
  }
465
765
  //# sourceMappingURL=angular-vite-plugin.js.map