@analogjs/vite-plugin-angular 3.0.0-alpha.23 → 3.0.0-alpha.25

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,10 +1,11 @@
1
1
  import { angularFullVersion, cjt, createAngularCompilation, sourceFileCache } from "./utils/devkit.js";
2
2
  import { getJsTransformConfigKey, isRolldown } from "./utils/rolldown.js";
3
3
  import { buildOptimizerPlugin } from "./angular-build-optimizer-plugin.js";
4
+ import { activateDeferredDebug, applyDebugOption, debugCompilationApi, debugCompiler, debugCompilerV, debugHmr, debugHmrV, debugStyles, debugStylesV, debugTailwind, debugTailwindV } from "./utils/debug.js";
4
5
  import { jitPlugin } from "./angular-jit-plugin.js";
5
6
  import { createCompilerPlugin, createRolldownCompilerPlugin } from "./compiler-plugin.js";
6
- import { StyleUrlsResolver, TemplateUrlsResolver } from "./component-resolvers.js";
7
- import { activateDeferredDebug, applyDebugOption, debugCompilationApi, debugCompiler, debugHmr, debugStyles, debugTailwind } from "./utils/debug.js";
7
+ import { StyleUrlsResolver, TemplateUrlsResolver, getAngularComponentMetadata } from "./component-resolvers.js";
8
+ import { AnalogStylesheetRegistry, preprocessStylesheet, registerStylesheetContent, rewriteRelativeCssImports } from "./stylesheet-registry.js";
8
9
  import { augmentHostWithCaching, augmentHostWithResources, augmentProgramWithVersioning, mergeTransformers } from "./host.js";
9
10
  import { angularVitestPlugins } from "./angular-vitest-plugin.js";
10
11
  import { pendingTasksPlugin } from "./angular-pending-tasks.plugin.js";
@@ -12,14 +13,15 @@ import { liveReloadPlugin } from "./live-reload-plugin.js";
12
13
  import { nxFolderPlugin } from "./nx-folder-plugin.js";
13
14
  import { replaceFiles } from "./plugins/file-replacements.plugin.js";
14
15
  import { routerPlugin } from "./router-plugin.js";
15
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
16
+ import { union } from "es-toolkit";
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
16
19
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
17
20
  import * as compilerCli from "@angular/compiler-cli";
18
21
  import { createRequire } from "node:module";
19
22
  import * as ngCompiler from "@angular/compiler";
20
23
  import { globSync } from "tinyglobby";
21
24
  import { defaultClientConditions, normalizePath, preprocessCSS } from "vite";
22
- import { createHash } from "node:crypto";
23
25
  //#region packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
24
26
  var require = createRequire(import.meta.url);
25
27
  var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
@@ -30,6 +32,13 @@ var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
30
32
  DiagnosticModes[DiagnosticModes["All"] = 7] = "All";
31
33
  return DiagnosticModes;
32
34
  }({});
35
+ function normalizeIncludeGlob(workspaceRoot, glob) {
36
+ const normalizedWorkspaceRoot = normalizePath(resolve(workspaceRoot));
37
+ const normalizedGlob = normalizePath(glob);
38
+ if (normalizedGlob === normalizedWorkspaceRoot || normalizedGlob.startsWith(`${normalizedWorkspaceRoot}/`)) return normalizedGlob;
39
+ if (normalizedGlob.startsWith("/")) return `${normalizedWorkspaceRoot}${normalizedGlob}`;
40
+ return normalizePath(resolve(normalizedWorkspaceRoot, normalizedGlob));
41
+ }
33
42
  /**
34
43
  * TypeScript file extension regex
35
44
  * Match .(c or m)ts, .ts extensions with an optional ? for query params
@@ -37,10 +46,33 @@ var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
37
46
  */
38
47
  var TS_EXT_REGEX = /\.[cm]?(ts)[^x]?\??/;
39
48
  var classNames = /* @__PURE__ */ new Map();
49
+ function evictDeletedFileMetadata(file, { removeActiveGraphMetadata, removeStyleOwnerMetadata, classNamesMap, fileTransformMap }) {
50
+ const normalizedFile = normalizePath(file.split("?")[0]);
51
+ removeActiveGraphMetadata(normalizedFile);
52
+ removeStyleOwnerMetadata(normalizedFile);
53
+ classNamesMap.delete(normalizedFile);
54
+ fileTransformMap.delete(normalizedFile);
55
+ }
56
+ function injectViteIgnoreForHmrMetadata(code) {
57
+ let patched = code.replace(/\bimport\(([a-zA-Z_$][\w$]*\.\u0275\u0275getReplaceMetadataURL)/g, "import(/* @vite-ignore */ $1");
58
+ if (patched === code) patched = patched.replace(/import\((\S+getReplaceMetadataURL)/g, "import(/* @vite-ignore */ $1");
59
+ return patched;
60
+ }
61
+ function isIgnoredHmrFile(file) {
62
+ return file.endsWith(".tsbuildinfo");
63
+ }
40
64
  /**
41
65
  * Builds a resolved stylePreprocessor function from plugin options.
42
- * If `tailwindCss` is provided, creates an auto-reference injector.
43
- * If `stylePreprocessor` is also provided, chains them (tailwind first, then user).
66
+ *
67
+ * When `tailwindCss` is configured, creates an injector that prepends
68
+ * `@reference "<rootStylesheet>"` into component CSS that uses Tailwind
69
+ * utilities. Uses absolute paths because Angular's externalRuntimeStyles
70
+ * serves component CSS as virtual modules (hash-based IDs) with no
71
+ * meaningful directory — relative paths can't resolve from a hash.
72
+ *
73
+ * If both `tailwindCss` and `stylePreprocessor` are provided, they are
74
+ * chained: Tailwind reference injection runs first, then the user's
75
+ * custom preprocessor.
44
76
  */
45
77
  function buildStylePreprocessor(options) {
46
78
  const userPreprocessor = options?.stylePreprocessor;
@@ -54,17 +86,18 @@ function buildStylePreprocessor(options) {
54
86
  rootStylesheet,
55
87
  prefixes
56
88
  });
89
+ if (!existsSync(rootStylesheet)) console.warn(`[@analogjs/vite-plugin-angular] tailwindCss.rootStylesheet not found at "${rootStylesheet}". @reference directives will point to a non-existent file, which will cause Tailwind CSS errors. Ensure the path is absolute and the file exists.`);
57
90
  tailwindPreprocessor = (code, filename) => {
58
91
  if (code.includes("@reference") || code.includes("@import \"tailwindcss\"") || code.includes("@import 'tailwindcss'")) {
59
- debugTailwind("skip (already has @reference or is root)", { filename });
92
+ debugTailwindV("skip (already has @reference or is root)", { filename });
60
93
  return code;
61
94
  }
62
95
  if (!(prefixes ? prefixes.some((prefix) => code.includes(prefix)) : code.includes("@apply"))) {
63
- debugTailwind("skip (no Tailwind usage detected)", { filename });
96
+ debugTailwindV("skip (no Tailwind usage detected)", { filename });
64
97
  return code;
65
98
  }
66
- debugTailwind("injected @reference", { filename });
67
- return `@reference "${relative(dirname(filename), rootStylesheet)}";\n${code}`;
99
+ debugTailwind("injected @reference via preprocessor", { filename });
100
+ return `@reference "${rootStylesheet}";\n${code}`;
68
101
  };
69
102
  }
70
103
  if (tailwindPreprocessor && userPreprocessor) {
@@ -76,7 +109,7 @@ function buildStylePreprocessor(options) {
76
109
  return tailwindPreprocessor ?? userPreprocessor;
77
110
  }
78
111
  function angular(options) {
79
- applyDebugOption(options?.debug);
112
+ applyDebugOption(options?.debug, options?.workspaceRoot);
80
113
  /**
81
114
  * Normalize plugin options so defaults
82
115
  * are used for values not provided.
@@ -94,10 +127,12 @@ function angular(options) {
94
127
  jit: options?.jit,
95
128
  include: options?.include ?? [],
96
129
  additionalContentDirs: options?.additionalContentDirs ?? [],
97
- liveReload: options?.liveReload ?? false,
130
+ hmr: options?.hmr ?? options?.liveReload ?? true,
98
131
  disableTypeChecking: options?.disableTypeChecking ?? true,
99
132
  fileReplacements: options?.fileReplacements ?? [],
100
133
  useAngularCompilationAPI: options?.experimental?.useAngularCompilationAPI ?? false,
134
+ hasTailwindCss: !!options?.tailwindCss,
135
+ tailwindCss: options?.tailwindCss,
101
136
  stylePreprocessor: buildStylePreprocessor(options)
102
137
  };
103
138
  let resolvedConfig;
@@ -119,8 +154,134 @@ function angular(options) {
119
154
  }
120
155
  let watchMode = false;
121
156
  let testWatchMode = isTestWatchMode();
122
- let inlineComponentStyles;
123
- let externalComponentStyles;
157
+ const activeGraphComponentMetadata = /* @__PURE__ */ new Map();
158
+ const selectorOwners = /* @__PURE__ */ new Map();
159
+ const classNameOwners = /* @__PURE__ */ new Map();
160
+ const transformedStyleOwnerMetadata = /* @__PURE__ */ new Map();
161
+ const styleSourceOwners = /* @__PURE__ */ new Map();
162
+ function shouldEnableHmr() {
163
+ return !!((isTest ? testWatchMode : watchMode) && pluginOptions.hmr);
164
+ }
165
+ /**
166
+ * Determines whether Angular should externalize component styles.
167
+ *
168
+ * When true, Angular emits style references (hash-based IDs) instead of
169
+ * inlining CSS strings. Vite's resolveId → load → transform pipeline
170
+ * then serves these virtual modules, allowing @tailwindcss/vite to
171
+ * process @reference directives.
172
+ *
173
+ * Required for TWO independent use-cases:
174
+ * 1. HMR — Vite needs external modules for hot replacement
175
+ * 2. Tailwind CSS (hasTailwindCss) — styles must pass through Vite's
176
+ * CSS pipeline so @tailwindcss/vite can resolve @apply directives
177
+ *
178
+ * In production builds (!watchMode), styles are NOT externalized — they
179
+ * are inlined after preprocessCSS runs eagerly in transformStylesheet.
180
+ */
181
+ function shouldExternalizeStyles() {
182
+ if (!(isTest ? testWatchMode : watchMode)) return false;
183
+ return !!(shouldEnableHmr() || pluginOptions.hasTailwindCss);
184
+ }
185
+ /**
186
+ * Validates the Tailwind CSS integration configuration and emits actionable
187
+ * warnings for common misconfigurations that cause silent failures.
188
+ *
189
+ * Called once during `configResolved` when `tailwindCss` is configured.
190
+ */
191
+ function validateTailwindConfig(config, isWatchMode) {
192
+ const PREFIX = "[@analogjs/vite-plugin-angular]";
193
+ const tw = pluginOptions.tailwindCss;
194
+ if (!tw) return;
195
+ if (!isAbsolute(tw.rootStylesheet)) console.warn(`${PREFIX} tailwindCss.rootStylesheet must be an absolute path. Got: "${tw.rootStylesheet}". Use path.resolve(__dirname, '...') in your vite.config to convert it.`);
196
+ const resolvedPlugins = config.plugins;
197
+ const hasTailwindPlugin = resolvedPlugins.some((p) => p.name.startsWith("@tailwindcss/vite") || p.name.startsWith("tailwindcss"));
198
+ if (isWatchMode && !hasTailwindPlugin) throw new Error(`${PREFIX} tailwindCss is configured but no @tailwindcss/vite plugin was found. Component CSS with @apply directives will not be processed.\n\n Fix: npm install @tailwindcss/vite --save-dev\n Then add tailwindcss() to your vite.config plugins array.\n`);
199
+ if (isWatchMode && tw.rootStylesheet) {
200
+ const projectRoot = config.root;
201
+ if (!tw.rootStylesheet.startsWith(projectRoot)) {
202
+ if (!(config.server?.fs?.allow ?? []).some((allowed) => tw.rootStylesheet.startsWith(allowed))) console.warn(`${PREFIX} tailwindCss.rootStylesheet is outside the Vite project root. The dev server may reject it with 403.\n\n Root: ${projectRoot}\n Stylesheet: ${tw.rootStylesheet}\n\n Fix: server.fs.allow: ['${dirname(tw.rootStylesheet)}']\n`);
203
+ }
204
+ }
205
+ if (tw.prefixes !== void 0 && tw.prefixes.length === 0) console.warn(`${PREFIX} tailwindCss.prefixes is an empty array. No component stylesheets will receive @reference injection. Either remove the prefixes option (to use @apply detection) or specify your prefixes: ['tw:']\n`);
206
+ const analogInstances = resolvedPlugins.filter((p) => p.name === "@analogjs/vite-plugin-angular");
207
+ if (analogInstances.length > 1) throw new Error(`${PREFIX} analog() is registered ${analogInstances.length} times. Each instance creates separate style maps, causing component styles to be lost. Remove duplicate registrations.`);
208
+ if (existsSync(tw.rootStylesheet)) try {
209
+ const rootContent = readFileSync(tw.rootStylesheet, "utf-8");
210
+ if (!rootContent.includes("@import \"tailwindcss\"") && !rootContent.includes("@import 'tailwindcss'")) console.warn(`${PREFIX} tailwindCss.rootStylesheet does not contain @import "tailwindcss". The @reference directive will point to a file without Tailwind configuration.\n\n File: ${tw.rootStylesheet}\n`);
211
+ } catch {}
212
+ }
213
+ function isLikelyPageOnlyComponent(id) {
214
+ return id.includes("/pages/") || /\.page\.[cm]?[jt]sx?$/i.test(id) || /\([^/]+\)\.page\.[cm]?[jt]sx?$/i.test(id);
215
+ }
216
+ function removeActiveGraphMetadata(file) {
217
+ const previous = activeGraphComponentMetadata.get(file);
218
+ if (!previous) return;
219
+ for (const record of previous) {
220
+ const location = `${record.file}#${record.className}`;
221
+ if (record.selector) {
222
+ const selectorSet = selectorOwners.get(record.selector);
223
+ selectorSet?.delete(location);
224
+ if (selectorSet?.size === 0) selectorOwners.delete(record.selector);
225
+ }
226
+ const classNameSet = classNameOwners.get(record.className);
227
+ classNameSet?.delete(location);
228
+ if (classNameSet?.size === 0) classNameOwners.delete(record.className);
229
+ }
230
+ activeGraphComponentMetadata.delete(file);
231
+ }
232
+ function registerActiveGraphMetadata(file, records) {
233
+ removeActiveGraphMetadata(file);
234
+ if (records.length === 0) return;
235
+ activeGraphComponentMetadata.set(file, records);
236
+ for (const record of records) {
237
+ const location = `${record.file}#${record.className}`;
238
+ if (record.selector) {
239
+ let selectorSet = selectorOwners.get(record.selector);
240
+ if (!selectorSet) {
241
+ selectorSet = /* @__PURE__ */ new Set();
242
+ selectorOwners.set(record.selector, selectorSet);
243
+ }
244
+ selectorSet.add(location);
245
+ }
246
+ let classNameSet = classNameOwners.get(record.className);
247
+ if (!classNameSet) {
248
+ classNameSet = /* @__PURE__ */ new Set();
249
+ classNameOwners.set(record.className, classNameSet);
250
+ }
251
+ classNameSet.add(location);
252
+ }
253
+ }
254
+ function removeStyleOwnerMetadata(file) {
255
+ const previous = transformedStyleOwnerMetadata.get(file);
256
+ if (!previous) return;
257
+ for (const record of previous) {
258
+ const owners = styleSourceOwners.get(record.sourcePath);
259
+ owners?.delete(record.ownerFile);
260
+ if (owners?.size === 0) styleSourceOwners.delete(record.sourcePath);
261
+ }
262
+ transformedStyleOwnerMetadata.delete(file);
263
+ }
264
+ function registerStyleOwnerMetadata(file, styleUrls) {
265
+ removeStyleOwnerMetadata(file);
266
+ const records = styleUrls.map((urlSet) => {
267
+ const [, absoluteFileUrl] = urlSet.split("|");
268
+ return absoluteFileUrl ? {
269
+ ownerFile: file,
270
+ sourcePath: normalizePath(absoluteFileUrl)
271
+ } : void 0;
272
+ }).filter((record) => !!record);
273
+ if (records.length === 0) return;
274
+ transformedStyleOwnerMetadata.set(file, records);
275
+ for (const record of records) {
276
+ let owners = styleSourceOwners.get(record.sourcePath);
277
+ if (!owners) {
278
+ owners = /* @__PURE__ */ new Set();
279
+ styleSourceOwners.set(record.sourcePath, owners);
280
+ }
281
+ owners.add(record.ownerFile);
282
+ }
283
+ }
284
+ let stylesheetRegistry;
124
285
  const sourceFileCache$1 = new sourceFileCache();
125
286
  const isTest = process.env.NODE_ENV === "test" || !!process.env["VITEST"];
126
287
  const isVitestVscode = !!process.env["VITEST_VSCODE"];
@@ -145,9 +306,17 @@ function angular(options) {
145
306
  let angularCompilation;
146
307
  function angularPlugin() {
147
308
  let isProd = false;
148
- if (angularFullVersion < 19e4 || isTest) {
149
- pluginOptions.liveReload = false;
150
- debugHmr("liveReload disabled", {
309
+ if (angularFullVersion < 19e4 && pluginOptions.hmr) {
310
+ debugHmr("hmr disabled: Angular version does not support HMR APIs", {
311
+ angularVersion: angularFullVersion,
312
+ isTest
313
+ });
314
+ console.warn("[@analogjs/vite-plugin-angular]: HMR was disabled because Angular v19+ is required for externalRuntimeStyles/_enableHmr support. Detected Angular version: %s.", angularFullVersion);
315
+ pluginOptions.hmr = false;
316
+ }
317
+ if (isTest) {
318
+ pluginOptions.hmr = false;
319
+ debugHmr("hmr disabled", {
151
320
  angularVersion: angularFullVersion,
152
321
  isTest
153
322
  });
@@ -200,7 +369,11 @@ function angular(options) {
200
369
  return {
201
370
  [jsTransformConfigKey]: jsTransformConfigValue,
202
371
  optimizeDeps: {
203
- include: ["rxjs/operators", "rxjs"],
372
+ include: [
373
+ "rxjs/operators",
374
+ "rxjs",
375
+ "tslib"
376
+ ],
204
377
  exclude: ["@angular/platform-server"],
205
378
  ...useRolldown ? { rolldownOptions } : { esbuildOptions }
206
379
  },
@@ -209,10 +382,10 @@ function angular(options) {
209
382
  },
210
383
  configResolved(config) {
211
384
  resolvedConfig = config;
385
+ if (pluginOptions.hasTailwindCss) validateTailwindConfig(config, watchMode);
212
386
  if (pluginOptions.useAngularCompilationAPI) {
213
- externalComponentStyles = /* @__PURE__ */ new Map();
214
- inlineComponentStyles = /* @__PURE__ */ new Map();
215
- debugStyles("style maps initialized (Angular Compilation API)");
387
+ stylesheetRegistry = new AnalogStylesheetRegistry();
388
+ debugStyles("stylesheet registry initialized (Angular Compilation API)");
216
389
  }
217
390
  if (!jit) styleTransform = (code, filename) => preprocessCSS(code, filename, config);
218
391
  if (isTest) testWatchMode = !(config.server.watch === null) || config.test?.watch === true || testWatchMode;
@@ -221,7 +394,15 @@ function angular(options) {
221
394
  viteServer = server;
222
395
  const invalidateCompilationOnFsChange = createFsWatcherCacheInvalidator(invalidateFsCaches, invalidateTsconfigCaches, () => performCompilation(resolvedConfig));
223
396
  server.watcher.on("add", invalidateCompilationOnFsChange);
224
- server.watcher.on("unlink", invalidateCompilationOnFsChange);
397
+ server.watcher.on("unlink", (file) => {
398
+ evictDeletedFileMetadata(file, {
399
+ removeActiveGraphMetadata,
400
+ removeStyleOwnerMetadata,
401
+ classNamesMap: classNames,
402
+ fileTransformMap
403
+ });
404
+ return invalidateCompilationOnFsChange();
405
+ });
225
406
  server.watcher.on("change", (file) => {
226
407
  if (file.includes("tsconfig")) invalidateTsconfigCaches();
227
408
  });
@@ -234,6 +415,10 @@ function angular(options) {
234
415
  }
235
416
  },
236
417
  async handleHotUpdate(ctx) {
418
+ if (isIgnoredHmrFile(ctx.file)) {
419
+ debugHmr("ignored file change", { file: ctx.file });
420
+ return [];
421
+ }
237
422
  if (TS_EXT_REGEX.test(ctx.file)) {
238
423
  const [fileId] = ctx.file.split("?");
239
424
  debugHmr("TS file changed", {
@@ -242,7 +427,7 @@ function angular(options) {
242
427
  });
243
428
  pendingCompilation = performCompilation(resolvedConfig, [fileId]);
244
429
  let result;
245
- if (pluginOptions.liveReload) {
430
+ if (shouldEnableHmr()) {
246
431
  await pendingCompilation;
247
432
  pendingCompilation = null;
248
433
  result = fileEmitter(fileId);
@@ -251,10 +436,28 @@ function angular(options) {
251
436
  hmrEligible: !!result?.hmrEligible,
252
437
  hasClassName: !!classNames.get(fileId)
253
438
  });
439
+ debugHmrV("ts hmr evaluation", {
440
+ file: ctx.file,
441
+ fileId,
442
+ hasResult: !!result,
443
+ hmrEligible: !!result?.hmrEligible,
444
+ hasClassName: !!classNames.get(fileId),
445
+ className: classNames.get(fileId),
446
+ updateCode: result?.hmrUpdateCode ? describeStylesheetContent(result.hmrUpdateCode) : void 0,
447
+ errors: result?.errors?.length ?? 0,
448
+ warnings: result?.warnings?.length ?? 0,
449
+ hint: result?.hmrEligible ? "A TS-side component change, including inline template edits, produced an Angular HMR payload." : "No Angular HMR payload was emitted for this TS change; the change may not affect component template state."
450
+ });
254
451
  }
255
- if (pluginOptions.liveReload && result?.hmrEligible && classNames.get(fileId)) {
452
+ if (shouldEnableHmr() && result?.hmrEligible && classNames.get(fileId)) {
256
453
  const relativeFileId = `${normalizePath(relative(process.cwd(), fileId))}@${classNames.get(fileId)}`;
257
454
  debugHmr("sending component update", { relativeFileId });
455
+ debugHmrV("ts hmr component update payload", {
456
+ file: ctx.file,
457
+ fileId,
458
+ relativeFileId,
459
+ className: classNames.get(fileId)
460
+ });
258
461
  sendHMRComponentUpdate(ctx.server, relativeFileId);
259
462
  return ctx.modules.map((mod) => {
260
463
  if (mod.id === ctx.file) return markModuleSelfAccepting(mod);
@@ -265,51 +468,224 @@ function angular(options) {
265
468
  if (/\.(html|htm|css|less|sass|scss)$/.test(ctx.file)) {
266
469
  debugHmr("resource file changed", { file: ctx.file });
267
470
  fileTransformMap.delete(ctx.file.split("?")[0]);
471
+ if (/\.(css|less|sass|scss)$/.test(ctx.file)) refreshStylesheetRegistryForFile(ctx.file, stylesheetRegistry, pluginOptions.stylePreprocessor);
472
+ if (/\.(css|less|sass|scss)$/.test(ctx.file) && existsSync(ctx.file)) try {
473
+ const rawResource = readFileSync(ctx.file, "utf-8");
474
+ debugHmrV("resource source snapshot", {
475
+ file: ctx.file,
476
+ mtimeMs: safeStatMtimeMs(ctx.file),
477
+ ...describeStylesheetContent(rawResource)
478
+ });
479
+ } catch (error) {
480
+ debugHmrV("resource source snapshot failed", {
481
+ file: ctx.file,
482
+ error: String(error)
483
+ });
484
+ }
485
+ const fileModules = await getModulesForChangedFile(ctx.server, ctx.file, ctx.modules, stylesheetRegistry);
486
+ debugHmrV("resource modules resolved", {
487
+ file: ctx.file,
488
+ eventModuleCount: ctx.modules.length,
489
+ fileModuleCount: fileModules.length,
490
+ modules: fileModules.map((mod) => ({
491
+ id: mod.id,
492
+ file: mod.file,
493
+ type: mod.type,
494
+ url: mod.url
495
+ }))
496
+ });
268
497
  /**
269
498
  * Check to see if this was a direct request
270
499
  * for an external resource (styles, html).
271
500
  */
272
- const isDirect = ctx.modules.find((mod) => ctx.file === mod.file && mod.id?.includes("?direct"));
273
- const isInline = ctx.modules.find((mod) => ctx.file === mod.file && mod.id?.includes("?inline"));
501
+ const isDirect = fileModules.find((mod) => !!mod.id && mod.id.includes("?direct") && isModuleForChangedResource(mod, ctx.file, stylesheetRegistry));
502
+ const isInline = fileModules.find((mod) => !!mod.id && mod.id.includes("?inline") && isModuleForChangedResource(mod, ctx.file, stylesheetRegistry));
503
+ debugHmrV("resource direct/inline detection", {
504
+ file: ctx.file,
505
+ hasDirect: !!isDirect,
506
+ directId: isDirect?.id,
507
+ hasInline: !!isInline,
508
+ inlineId: isInline?.id
509
+ });
274
510
  if (isDirect || isInline) {
275
- if (pluginOptions.liveReload && isDirect?.id && isDirect.file) {
276
- if (isDirect.type === "css" && isComponentStyleSheet(isDirect.id)) {
511
+ if (shouldExternalizeStyles() && isDirect?.id && isDirect.file) {
512
+ const isComponentStyle = isDirect.type === "css" && isComponentStyleSheet(isDirect.id);
513
+ debugHmrV("resource direct branch", {
514
+ file: ctx.file,
515
+ directId: isDirect.id,
516
+ directType: isDirect.type,
517
+ shouldExternalize: shouldExternalizeStyles(),
518
+ isComponentStyle
519
+ });
520
+ if (isComponentStyle) {
277
521
  const { encapsulation } = getComponentStyleSheetMeta(isDirect.id);
278
- debugStyles("HMR: component stylesheet changed", {
522
+ const wrapperModules = await findComponentStylesheetWrapperModules(ctx.server, ctx.file, isDirect, fileModules, stylesheetRegistry);
523
+ const stylesheetDiagnosis = diagnoseComponentStylesheetPipeline(ctx.file, isDirect, stylesheetRegistry, wrapperModules, pluginOptions.stylePreprocessor);
524
+ debugStylesV("HMR: component stylesheet changed", {
279
525
  file: isDirect.file,
280
526
  encapsulation
281
527
  });
282
- if (encapsulation !== "shadow") {
283
- ctx.server.ws.send({
284
- type: "update",
285
- updates: [{
286
- type: "css-update",
287
- timestamp: Date.now(),
288
- path: isDirect.url,
289
- acceptedPath: isDirect.file
290
- }]
528
+ debugHmrV("component stylesheet wrapper modules", {
529
+ file: ctx.file,
530
+ wrapperCount: wrapperModules.length,
531
+ wrapperIds: wrapperModules.map((mod) => mod.id),
532
+ availableModuleIds: fileModules.map((mod) => mod.id)
533
+ });
534
+ debugHmrV("component stylesheet pipeline diagnosis", stylesheetDiagnosis);
535
+ ctx.server.moduleGraph.invalidateModule(isDirect);
536
+ debugHmrV("component stylesheet direct module invalidated", {
537
+ file: ctx.file,
538
+ directModuleId: isDirect.id,
539
+ directModuleUrl: isDirect.url,
540
+ reason: "Ensure Vite drops stale direct CSS transform results before wrapper or fallback handling continues."
541
+ });
542
+ const trackedWrapperRequestIds = stylesheetDiagnosis.trackedRequestIds.filter((id) => id.includes("?ngcomp="));
543
+ if (encapsulation !== "shadow" && (wrapperModules.length > 0 || trackedWrapperRequestIds.length > 0)) {
544
+ wrapperModules.forEach((mod) => ctx.server.moduleGraph.invalidateModule(mod));
545
+ debugHmrV("sending css-update for component stylesheet", {
546
+ file: ctx.file,
547
+ path: isDirect.url,
548
+ acceptedPath: isDirect.file,
549
+ wrapperCount: wrapperModules.length,
550
+ trackedWrapperRequestIds,
551
+ hint: wrapperModules.length > 0 ? "Live wrapper modules were found and invalidated before sending the CSS update." : "No live wrapper ModuleNode was available, but the wrapper request id is already tracked, so Analog is trusting the browser-visible wrapper identity and patching the direct stylesheet instead of forcing a reload."
552
+ });
553
+ sendCssUpdate(ctx.server, {
554
+ path: isDirect.url,
555
+ acceptedPath: isDirect.file
556
+ });
557
+ logComponentStylesheetHmrOutcome({
558
+ file: ctx.file,
559
+ encapsulation,
560
+ diagnosis: stylesheetDiagnosis,
561
+ outcome: "css-update",
562
+ directModuleId: isDirect.id,
563
+ wrapperIds: wrapperModules.map((mod) => mod.id)
291
564
  });
292
- return ctx.modules.filter((mod) => {
565
+ return union(fileModules.filter((mod) => {
293
566
  return mod.file !== ctx.file || mod.id !== isDirect.id;
294
567
  }).map((mod) => {
295
568
  if (mod.file === ctx.file) return markModuleSelfAccepting(mod);
296
569
  return mod;
570
+ }), wrapperModules.map((mod) => markModuleSelfAccepting(mod)));
571
+ }
572
+ debugHmrV("component stylesheet hmr fallback: full reload", {
573
+ file: ctx.file,
574
+ encapsulation,
575
+ reason: trackedWrapperRequestIds.length === 0 ? "missing-wrapper-module" : encapsulation === "shadow" ? "shadow-encapsulation" : "tracked-wrapper-still-not-patchable",
576
+ directId: isDirect.id,
577
+ trackedRequestIds: stylesheetRegistry?.getRequestIdsForSource(ctx.file) ?? []
578
+ });
579
+ const ownerModules = findStyleOwnerModules(ctx.server, ctx.file, styleSourceOwners);
580
+ debugHmrV("component stylesheet owner fallback lookup", {
581
+ file: ctx.file,
582
+ ownerCount: ownerModules.length,
583
+ ownerIds: ownerModules.map((mod) => mod.id),
584
+ ownerFiles: [...styleSourceOwners.get(normalizePath(ctx.file)) ?? []]
585
+ });
586
+ if (ownerModules.length > 0) {
587
+ pendingCompilation = performCompilation(resolvedConfig, [...ownerModules.map((mod) => mod.id).filter(Boolean)]);
588
+ await pendingCompilation;
589
+ pendingCompilation = null;
590
+ const updates = ownerModules.map((mod) => mod.id).filter((id) => !!id && !!classNames.get(id));
591
+ const derivedUpdates = ownerModules.map((mod) => mod.id).filter((id) => !!id).flatMap((ownerId) => resolveComponentClassNamesForStyleOwner(ownerId, ctx.file).map((className) => ({
592
+ ownerId,
593
+ className,
594
+ via: "raw-component-metadata"
595
+ })));
596
+ debugHmrV("component stylesheet owner fallback compilation", {
597
+ file: ctx.file,
598
+ ownerIds: ownerModules.map((mod) => mod.id),
599
+ updateIds: updates,
600
+ classNames: updates.map((id) => ({
601
+ id,
602
+ className: classNames.get(id)
603
+ })),
604
+ derivedUpdates
605
+ });
606
+ if (derivedUpdates.length > 0) debugHmrV("component stylesheet owner fallback derived updates", {
607
+ file: ctx.file,
608
+ updates: derivedUpdates,
609
+ hint: "Angular did not repopulate classNames during CSS-only owner recompilation, so Analog derived component identities from raw component metadata."
297
610
  });
298
611
  }
612
+ logComponentStylesheetHmrOutcome({
613
+ file: ctx.file,
614
+ encapsulation,
615
+ diagnosis: stylesheetDiagnosis,
616
+ outcome: "full-reload",
617
+ directModuleId: isDirect.id,
618
+ wrapperIds: wrapperModules.map((mod) => mod.id),
619
+ ownerIds: ownerModules.map((mod) => mod.id)
620
+ });
621
+ sendFullReload(ctx.server, {
622
+ file: ctx.file,
623
+ encapsulation,
624
+ reason: wrapperModules.length === 0 ? "missing-wrapper-module-and-no-owner-updates" : "shadow-encapsulation",
625
+ directId: isDirect.id,
626
+ trackedRequestIds: stylesheetRegistry?.getRequestIdsForSource(ctx.file) ?? []
627
+ });
628
+ return [];
629
+ }
630
+ }
631
+ return fileModules;
632
+ }
633
+ if (shouldEnableHmr() && /\.(html|htm)$/.test(ctx.file) && fileModules.length === 0) {
634
+ const ownerModules = findTemplateOwnerModules(ctx.server, ctx.file);
635
+ debugHmrV("template owner lookup", {
636
+ file: ctx.file,
637
+ ownerCount: ownerModules.length,
638
+ ownerIds: ownerModules.map((mod) => mod.id),
639
+ hint: ownerModules.length > 0 ? "The external template has candidate TS owner modules that can be recompiled for HMR." : "No TS owner modules were visible for this external template change; HMR will fall through to the generic importer path."
640
+ });
641
+ if (ownerModules.length > 0) {
642
+ const ownerIds = ownerModules.map((mod) => mod.id).filter(Boolean);
643
+ ownerModules.forEach((mod) => ctx.server.moduleGraph.invalidateModule(mod));
644
+ pendingCompilation = performCompilation(resolvedConfig, ownerIds);
645
+ await pendingCompilation;
646
+ pendingCompilation = null;
647
+ const updates = ownerIds.filter((id) => classNames.get(id));
648
+ debugHmrV("template owner recompilation result", {
649
+ file: ctx.file,
650
+ ownerIds,
651
+ updates,
652
+ updateClassNames: updates.map((id) => ({
653
+ id,
654
+ className: classNames.get(id)
655
+ })),
656
+ hint: updates.length > 0 ? "External template recompilation produced Angular component update targets." : "External template recompilation completed, but no Angular component update targets were surfaced."
657
+ });
658
+ if (updates.length > 0) {
659
+ debugHmr("template owner module invalidation", {
660
+ file: ctx.file,
661
+ ownerIds,
662
+ updateCount: updates.length
663
+ });
664
+ updates.forEach((updateId) => {
665
+ const relativeFileId = `${normalizePath(relative(process.cwd(), updateId))}@${classNames.get(updateId)}`;
666
+ sendHMRComponentUpdate(ctx.server, relativeFileId);
667
+ });
668
+ return ownerModules.map((mod) => markModuleSelfAccepting(mod));
299
669
  }
300
670
  }
301
- return ctx.modules;
302
671
  }
303
672
  const mods = [];
304
673
  const updates = [];
305
- ctx.modules.forEach((mod) => {
674
+ fileModules.forEach((mod) => {
306
675
  mod.importers.forEach((imp) => {
307
676
  ctx.server.moduleGraph.invalidateModule(imp);
308
- if (pluginOptions.liveReload && classNames.get(imp.id)) updates.push(imp.id);
677
+ if (shouldExternalizeStyles() && classNames.get(imp.id)) updates.push(imp.id);
309
678
  else mods.push(imp);
310
679
  });
311
680
  });
312
- pendingCompilation = performCompilation(resolvedConfig, [...mods.map((mod) => mod.id), ...updates]);
681
+ debugHmrV("resource importer analysis", {
682
+ file: ctx.file,
683
+ fileModuleCount: fileModules.length,
684
+ importerCount: fileModules.reduce((count, mod) => count + mod.importers.size, 0),
685
+ updates,
686
+ mods: mods.map((mod) => mod.id)
687
+ });
688
+ pendingCompilation = performCompilation(resolvedConfig, [...mods.map((mod) => mod.id).filter(Boolean), ...updates]);
313
689
  if (updates.length > 0) {
314
690
  await pendingCompilation;
315
691
  pendingCompilation = null;
@@ -321,7 +697,7 @@ function angular(options) {
321
697
  const impRelativeFileId = `${normalizePath(relative(process.cwd(), updateId))}@${classNames.get(updateId)}`;
322
698
  sendHMRComponentUpdate(ctx.server, impRelativeFileId);
323
699
  });
324
- return ctx.modules.map((mod) => {
700
+ return fileModules.map((mod) => {
325
701
  if (mod.id === ctx.file) return markModuleSelfAccepting(mod);
326
702
  return mod;
327
703
  });
@@ -339,24 +715,42 @@ function angular(options) {
339
715
  }
340
716
  if (isComponentStyleSheet(id)) {
341
717
  const filename = getFilenameFromPath(id);
342
- const componentStyles = externalComponentStyles?.get(filename);
718
+ if (stylesheetRegistry?.hasServed(filename)) {
719
+ debugStylesV("resolveId: kept preprocessed ID", { filename });
720
+ return id;
721
+ }
722
+ const componentStyles = stylesheetRegistry?.resolveExternalSource(filename);
343
723
  if (componentStyles) {
344
- debugStyles("resolveId: mapped external stylesheet", {
724
+ debugStylesV("resolveId: mapped external stylesheet", {
345
725
  filename,
346
726
  resolvedPath: componentStyles
347
727
  });
348
728
  return componentStyles + new URL(id, "http://localhost").search;
349
729
  }
730
+ debugStyles("resolveId: component stylesheet NOT FOUND in either map", {
731
+ filename,
732
+ inlineMapSize: stylesheetRegistry?.servedCount ?? 0,
733
+ externalMapSize: stylesheetRegistry?.externalCount ?? 0
734
+ });
350
735
  }
351
736
  },
352
737
  async load(id) {
353
738
  if (isComponentStyleSheet(id)) {
354
739
  const filename = getFilenameFromPath(id);
355
- const componentStyles = inlineComponentStyles?.get(filename);
740
+ const componentStyles = stylesheetRegistry?.getServedContent(filename);
356
741
  if (componentStyles) {
357
- debugStyles("load: served inline component stylesheet", {
742
+ stylesheetRegistry?.registerActiveRequest(id);
743
+ debugHmrV("stylesheet active request registered", {
744
+ requestId: id,
358
745
  filename,
359
- length: componentStyles.length
746
+ sourcePath: stylesheetRegistry?.resolveExternalSource(filename) ?? stylesheetRegistry?.resolveExternalSource(filename.replace(/^\//, "")),
747
+ trackedRequestIds: stylesheetRegistry?.getRequestIdsForSource(stylesheetRegistry?.resolveExternalSource(filename) ?? stylesheetRegistry?.resolveExternalSource(filename.replace(/^\//, "")) ?? "") ?? []
748
+ });
749
+ debugStylesV("load: served inline component stylesheet", {
750
+ filename,
751
+ length: componentStyles.length,
752
+ requestId: id,
753
+ ...describeStylesheetContent(componentStyles)
360
754
  });
361
755
  return componentStyles;
362
756
  }
@@ -387,12 +781,15 @@ function angular(options) {
387
781
  */
388
782
  if (id.includes("?") && id.includes("analog-content-")) return;
389
783
  /**
390
- * Encapsulate component stylesheets that use emulated encapsulation
784
+ * Encapsulate component stylesheets that use emulated encapsulation.
785
+ * Must run whenever styles are externalized (not just HMR), because
786
+ * Angular's externalRuntimeStyles skips its own encapsulation when
787
+ * styles are external — the build tool is expected to handle it.
391
788
  */
392
- if (pluginOptions.liveReload && isComponentStyleSheet(id)) {
789
+ if (shouldExternalizeStyles() && isComponentStyleSheet(id)) {
393
790
  const { encapsulation, componentId } = getComponentStyleSheetMeta(id);
394
791
  if (encapsulation === "emulated" && componentId) {
395
- debugStyles("applying emulated view encapsulation", {
792
+ debugStylesV("applying emulated view encapsulation", {
396
793
  stylesheet: id.split("?")[0],
397
794
  componentId
398
795
  });
@@ -420,7 +817,7 @@ function angular(options) {
420
817
  }
421
818
  }
422
819
  const hasComponent = code.includes("@Component");
423
- debugCompiler("transform", {
820
+ debugCompilerV("transform", {
424
821
  id,
425
822
  codeLength: code.length,
426
823
  hasComponent
@@ -450,6 +847,19 @@ function angular(options) {
450
847
  data = data.replace(`angular:jit:style:file;${styleFile}`, `${resolvedStyleUrl}?inline`);
451
848
  });
452
849
  }
850
+ if (data.includes("HmrLoad")) {
851
+ const hasMetaUrl = data.includes("getReplaceMetadataURL");
852
+ debugHmrV("vite-ignore injection", {
853
+ id,
854
+ dataLength: data.length,
855
+ hasMetaUrl
856
+ });
857
+ if (hasMetaUrl) {
858
+ const patched = injectViteIgnoreForHmrMetadata(data);
859
+ if (patched !== data && !patched.includes("@vite-ignore")) debugHmrV("vite-ignore regex fallback", { id });
860
+ data = patched;
861
+ }
862
+ }
453
863
  return {
454
864
  code: data,
455
865
  map: null
@@ -468,8 +878,103 @@ function angular(options) {
468
878
  }
469
879
  return [
470
880
  replaceFiles(pluginOptions.fileReplacements, pluginOptions.workspaceRoot),
881
+ {
882
+ name: "@analogjs/vite-plugin-angular:template-class-binding-guard",
883
+ enforce: "pre",
884
+ transform(code, id) {
885
+ if (id.includes("node_modules")) return;
886
+ const cleanId = id.split("?")[0];
887
+ if (/\.(html|htm)$/i.test(cleanId)) {
888
+ const staticClassIssue = findStaticClassAndBoundClassConflicts(code)[0];
889
+ if (staticClassIssue) throwTemplateClassBindingConflict(cleanId, staticClassIssue);
890
+ const mixedClassIssue = findBoundClassAndNgClassConflicts(code)[0];
891
+ if (mixedClassIssue) this.warn([
892
+ "[Analog Angular] Conflicting class composition.",
893
+ `File: ${cleanId}:${mixedClassIssue.line}:${mixedClassIssue.column}`,
894
+ "This element mixes `[class]` and `[ngClass]`.",
895
+ "Prefer a single class-binding strategy so class merging stays predictable.",
896
+ "Use one `[ngClass]` expression or explicit `[class.foo]` bindings.",
897
+ `Snippet: ${mixedClassIssue.snippet}`
898
+ ].join("\n"));
899
+ return;
900
+ }
901
+ if (TS_EXT_REGEX.test(cleanId)) {
902
+ const rawStyleUrls = styleUrlsResolver.resolve(code, cleanId);
903
+ registerStyleOwnerMetadata(cleanId, rawStyleUrls);
904
+ debugHmrV("component stylesheet owner metadata registered", {
905
+ file: cleanId,
906
+ styleUrlCount: rawStyleUrls.length,
907
+ styleUrls: rawStyleUrls,
908
+ ownerSources: [...transformedStyleOwnerMetadata.get(cleanId)?.map((record) => record.sourcePath) ?? []]
909
+ });
910
+ const components = getAngularComponentMetadata(code);
911
+ const inlineTemplateIssue = components.flatMap((component) => component.inlineTemplates.flatMap((template) => findStaticClassAndBoundClassConflicts(template)))[0];
912
+ if (inlineTemplateIssue) throwTemplateClassBindingConflict(cleanId, inlineTemplateIssue);
913
+ const mixedInlineClassIssue = components.flatMap((component) => component.inlineTemplates.flatMap((template) => findBoundClassAndNgClassConflicts(template)))[0];
914
+ if (mixedInlineClassIssue) this.warn([
915
+ "[Analog Angular] Conflicting class composition.",
916
+ `File: ${cleanId}:${mixedInlineClassIssue.line}:${mixedInlineClassIssue.column}`,
917
+ "This element mixes `[class]` and `[ngClass]`.",
918
+ "Prefer a single class-binding strategy so class merging stays predictable.",
919
+ "Use one `[ngClass]` expression or explicit `[class.foo]` bindings.",
920
+ `Snippet: ${mixedInlineClassIssue.snippet}`
921
+ ].join("\n"));
922
+ registerActiveGraphMetadata(cleanId, components.map((component) => ({
923
+ file: cleanId,
924
+ className: component.className,
925
+ selector: component.selector
926
+ })));
927
+ for (const component of components) {
928
+ if (!component.selector && !isLikelyPageOnlyComponent(cleanId)) throw new Error([
929
+ "[Analog Angular] Selectorless component detected.",
930
+ `File: ${cleanId}`,
931
+ `Component: ${component.className}`,
932
+ "This component has no `selector`, so Angular will render it as `ng-component`.",
933
+ "That increases the chance of component ID collisions and makes diagnostics harder to interpret.",
934
+ "Add an explicit selector for reusable components.",
935
+ "Selectorless components are only supported for page and route-only files."
936
+ ].join("\n"));
937
+ if (component.selector) {
938
+ const selectorEntries = selectorOwners.get(component.selector);
939
+ if (selectorEntries && selectorEntries.size > 1) throw new Error([
940
+ "[Analog Angular] Duplicate component selector detected.",
941
+ `Selector: ${component.selector}`,
942
+ "Multiple components in the active application graph use the same selector.",
943
+ "Selectors must be unique within the active graph to avoid ambiguous rendering and confusing diagnostics.",
944
+ `Locations:\n${formatActiveGraphLocations(selectorEntries)}`
945
+ ].join("\n"));
946
+ }
947
+ const classNameEntries = classNameOwners.get(component.className);
948
+ if (classNameEntries && classNameEntries.size > 1) this.warn([
949
+ "[Analog Angular] Duplicate component class name detected.",
950
+ `Class name: ${component.className}`,
951
+ "Two or more Angular components in the active graph share the same exported class name.",
952
+ "Rename one of them to keep HMR, stack traces, and compiler diagnostics unambiguous.",
953
+ `Locations:\n${formatActiveGraphLocations(classNameEntries)}`
954
+ ].join("\n"));
955
+ }
956
+ }
957
+ }
958
+ },
959
+ pluginOptions.hasTailwindCss && {
960
+ name: "@analogjs/vite-plugin-angular:tailwind-reference",
961
+ enforce: "pre",
962
+ transform(code, id) {
963
+ const tw = pluginOptions.tailwindCss;
964
+ if (!tw || !id.includes(".css")) return;
965
+ if (id.split("?")[0] === tw.rootStylesheet) return;
966
+ if (code.includes("@reference") || code.includes("@import \"tailwindcss\"") || code.includes("@import 'tailwindcss'")) return;
967
+ const rootBasename = basename(tw.rootStylesheet);
968
+ if (code.includes(rootBasename)) return;
969
+ const prefixes = tw.prefixes;
970
+ if (prefixes ? prefixes.some((p) => code.includes(p)) : code.includes("@apply")) {
971
+ debugTailwind("injected @reference via pre-transform", { id: id.split("/").slice(-2).join("/") });
972
+ return `@reference "${tw.rootStylesheet}";\n${code}`;
973
+ }
974
+ }
975
+ },
471
976
  angularPlugin(),
472
- pluginOptions.liveReload && liveReloadPlugin({
977
+ pluginOptions.hmr && liveReloadPlugin({
473
978
  classNames,
474
979
  fileEmitter
475
980
  }),
@@ -484,8 +989,7 @@ function angular(options) {
484
989
  nxFolderPlugin()
485
990
  ].filter(Boolean);
486
991
  function findIncludes() {
487
- const workspaceRoot = normalizePath(resolve(pluginOptions.workspaceRoot));
488
- return globSync([...pluginOptions.include.map((glob) => `${workspaceRoot}${glob}`)], {
992
+ return globSync(pluginOptions.include.map((glob) => normalizeIncludeGlob(pluginOptions.workspaceRoot, glob)), {
489
993
  dot: true,
490
994
  absolute: true
491
995
  });
@@ -540,17 +1044,30 @@ function angular(options) {
540
1044
  modifiedFiles,
541
1045
  async transformStylesheet(data, containingFile, resourceFile, order, className) {
542
1046
  const filename = resourceFile ?? containingFile.replace(".ts", `.${pluginOptions.inlineStylesExtension}`);
543
- const preprocessedData = pluginOptions.stylePreprocessor ? pluginOptions.stylePreprocessor(data, filename) ?? data : data;
544
- if (pluginOptions.liveReload && watchMode) {
545
- const stylesheetId = createHash("sha256").update(containingFile).update(className).update(String(order)).update(preprocessedData).digest("hex") + "." + pluginOptions.inlineStylesExtension;
546
- inlineComponentStyles.set(stylesheetId, preprocessedData);
547
- debugStyles("stylesheet deferred to Vite pipeline (liveReload)", {
1047
+ const preprocessedData = preprocessStylesheet(data, filename, pluginOptions.stylePreprocessor);
1048
+ if (shouldEnableHmr() && className && containingFile) classNames.set(normalizePath(containingFile), className);
1049
+ if (shouldExternalizeStyles()) {
1050
+ const stylesheetId = registerStylesheetContent(stylesheetRegistry, {
1051
+ code: preprocessedData,
1052
+ containingFile,
1053
+ className,
1054
+ order,
1055
+ inlineStylesExtension: pluginOptions.inlineStylesExtension,
1056
+ resourceFile: resourceFile ?? void 0
1057
+ });
1058
+ debugStyles("stylesheet deferred to Vite pipeline", {
548
1059
  stylesheetId,
549
1060
  resourceFile: resourceFile ?? "(inline)"
550
1061
  });
1062
+ debugStylesV("stylesheet deferred content snapshot", {
1063
+ stylesheetId,
1064
+ filename,
1065
+ resourceFile: resourceFile ?? "(inline)",
1066
+ ...describeStylesheetContent(preprocessedData)
1067
+ });
551
1068
  return stylesheetId;
552
1069
  }
553
- debugStyles("stylesheet processed inline via preprocessCSS (no liveReload)", {
1070
+ debugStyles("stylesheet processed inline via preprocessCSS", {
554
1071
  filename,
555
1072
  resourceFile: resourceFile ?? "(inline)",
556
1073
  dataLength: preprocessedData.length
@@ -559,7 +1076,11 @@ function angular(options) {
559
1076
  try {
560
1077
  stylesheetResult = await preprocessCSS(preprocessedData, `${filename}?direct`, resolvedConfig);
561
1078
  } catch (e) {
562
- console.error(`${e}`);
1079
+ debugStyles("preprocessCSS error", {
1080
+ filename,
1081
+ resourceFile: resourceFile ?? "(inline)",
1082
+ error: String(e)
1083
+ });
563
1084
  }
564
1085
  return stylesheetResult?.code || "";
565
1086
  },
@@ -567,21 +1088,24 @@ function angular(options) {
567
1088
  return "";
568
1089
  }
569
1090
  }, (tsCompilerOptions) => {
570
- if (pluginOptions.liveReload && watchMode) {
1091
+ if (shouldExternalizeStyles()) tsCompilerOptions["externalRuntimeStyles"] = true;
1092
+ if (shouldEnableHmr()) {
571
1093
  tsCompilerOptions["_enableHmr"] = true;
572
- tsCompilerOptions["externalRuntimeStyles"] = true;
573
1094
  tsCompilerOptions["supportTestBed"] = true;
574
1095
  }
575
1096
  debugCompiler("tsCompilerOptions (compilation API)", {
576
- liveReload: pluginOptions.liveReload,
1097
+ hmr: pluginOptions.hmr,
1098
+ hasTailwindCss: pluginOptions.hasTailwindCss,
577
1099
  watchMode,
1100
+ shouldExternalize: shouldExternalizeStyles(),
578
1101
  externalRuntimeStyles: !!tsCompilerOptions["externalRuntimeStyles"],
579
- hmr: !!tsCompilerOptions["_enableHmr"]
1102
+ hmrEnabled: !!tsCompilerOptions["_enableHmr"]
580
1103
  });
581
1104
  if (tsCompilerOptions.compilationMode === "partial") {
582
1105
  tsCompilerOptions["supportTestBed"] = true;
583
1106
  tsCompilerOptions["supportJitMode"] = true;
584
1107
  }
1108
+ if (angularFullVersion >= 2e5) tsCompilerOptions["_enableSelectorless"] = true;
585
1109
  if (!isTest && config.build?.lib) {
586
1110
  tsCompilerOptions["declaration"] = true;
587
1111
  tsCompilerOptions["declarationMap"] = watchMode;
@@ -590,13 +1114,80 @@ function angular(options) {
590
1114
  if (isTest) tsCompilerOptions["supportTestBed"] = true;
591
1115
  return tsCompilerOptions;
592
1116
  });
1117
+ debugStyles("external stylesheets from compilation API", {
1118
+ count: compilationResult.externalStylesheets?.size ?? 0,
1119
+ hasPreprocessor: !!pluginOptions.stylePreprocessor,
1120
+ hasInlineMap: !!stylesheetRegistry
1121
+ });
1122
+ const preprocessStats = {
1123
+ total: 0,
1124
+ injected: 0,
1125
+ skipped: 0,
1126
+ errors: 0
1127
+ };
593
1128
  compilationResult.externalStylesheets?.forEach((value, key) => {
594
- externalComponentStyles?.set(`${value}.css`, key);
595
- debugStyles("external stylesheet registered for resolveId mapping", {
596
- filename: `${value}.css`,
1129
+ preprocessStats.total++;
1130
+ const angularHash = `${value}.css`;
1131
+ stylesheetRegistry?.registerExternalRequest(angularHash, key);
1132
+ if (stylesheetRegistry && pluginOptions.stylePreprocessor && existsSync(key)) try {
1133
+ const rawCss = readFileSync(key, "utf-8");
1134
+ let preprocessed = preprocessStylesheet(rawCss, key, pluginOptions.stylePreprocessor);
1135
+ debugStylesV("external stylesheet raw snapshot", {
1136
+ angularHash,
1137
+ resolvedPath: key,
1138
+ mtimeMs: safeStatMtimeMs(key),
1139
+ ...describeStylesheetContent(rawCss)
1140
+ });
1141
+ preprocessed = rewriteRelativeCssImports(preprocessed, key);
1142
+ stylesheetRegistry.registerServedStylesheet({
1143
+ publicId: angularHash,
1144
+ sourcePath: key,
1145
+ originalCode: rawCss,
1146
+ normalizedCode: preprocessed
1147
+ }, [
1148
+ key,
1149
+ normalizePath(key),
1150
+ basename(key),
1151
+ key.replace(/^\//, "")
1152
+ ]);
1153
+ if (preprocessed && preprocessed !== rawCss) {
1154
+ preprocessStats.injected++;
1155
+ debugStylesV("preprocessed external stylesheet for Tailwind @reference", {
1156
+ angularHash,
1157
+ resolvedPath: key,
1158
+ mtimeMs: safeStatMtimeMs(key),
1159
+ raw: describeStylesheetContent(rawCss),
1160
+ served: describeStylesheetContent(preprocessed)
1161
+ });
1162
+ } else {
1163
+ preprocessStats.skipped++;
1164
+ debugStylesV("external stylesheet unchanged after preprocessing", {
1165
+ angularHash,
1166
+ resolvedPath: key,
1167
+ mtimeMs: safeStatMtimeMs(key),
1168
+ raw: describeStylesheetContent(rawCss),
1169
+ served: describeStylesheetContent(preprocessed),
1170
+ hint: "Registry mapping is still registered so Angular component stylesheet HMR can track and refresh this file even when preprocessing makes no textual changes."
1171
+ });
1172
+ }
1173
+ } catch (e) {
1174
+ preprocessStats.errors++;
1175
+ console.warn(`[@analogjs/vite-plugin-angular] failed to preprocess external stylesheet: ${key}: ${e}`);
1176
+ }
1177
+ else {
1178
+ preprocessStats.skipped++;
1179
+ debugStylesV("external stylesheet preprocessing skipped", {
1180
+ filename: angularHash,
1181
+ resolvedPath: key,
1182
+ reason: !stylesheetRegistry ? "no stylesheetRegistry" : !pluginOptions.stylePreprocessor ? "no stylePreprocessor" : "file not found on disk"
1183
+ });
1184
+ }
1185
+ debugStylesV("external stylesheet registered for resolveId mapping", {
1186
+ filename: angularHash,
597
1187
  resolvedPath: key
598
1188
  });
599
1189
  });
1190
+ debugStyles("external stylesheet preprocessing complete", preprocessStats);
600
1191
  const diagnostics = await angularCompilation.diagnoseFiles(pluginOptions.disableTypeChecking ? DiagnosticModes.All & ~DiagnosticModes.Semantic : DiagnosticModes.All);
601
1192
  const errors = diagnostics.errors?.length ? diagnostics.errors : [];
602
1193
  const warnings = diagnostics.warnings?.length ? diagnostics.warnings : [];
@@ -652,7 +1243,9 @@ function angular(options) {
652
1243
  resolvedTsConfigPath,
653
1244
  isProd ? "prod" : "dev",
654
1245
  isTest ? "test" : "app",
655
- config.build?.lib ? "lib" : "nolib"
1246
+ config.build?.lib ? "lib" : "nolib",
1247
+ pluginOptions.hmr ? "hmr" : "nohmr",
1248
+ pluginOptions.hasTailwindCss ? "tw" : "notw"
656
1249
  ].join("|");
657
1250
  let cached = tsconfigOptionsCache.get(tsconfigKey);
658
1251
  if (!cached) {
@@ -681,19 +1274,22 @@ function angular(options) {
681
1274
  }
682
1275
  const tsCompilerOptions = { ...cached.options };
683
1276
  let rootNames = [...cached.rootNames];
684
- if (pluginOptions.liveReload && watchMode) {
1277
+ if (shouldExternalizeStyles()) tsCompilerOptions["externalRuntimeStyles"] = true;
1278
+ if (shouldEnableHmr()) {
685
1279
  tsCompilerOptions["_enableHmr"] = true;
686
- tsCompilerOptions["externalRuntimeStyles"] = true;
687
1280
  tsCompilerOptions["supportTestBed"] = true;
688
1281
  }
689
1282
  debugCompiler("tsCompilerOptions (NgtscProgram path)", {
1283
+ hmr: pluginOptions.hmr,
1284
+ shouldExternalize: shouldExternalizeStyles(),
690
1285
  externalRuntimeStyles: !!tsCompilerOptions["externalRuntimeStyles"],
691
- hmr: !!tsCompilerOptions["_enableHmr"]
1286
+ hmrEnabled: !!tsCompilerOptions["_enableHmr"]
692
1287
  });
693
1288
  if (tsCompilerOptions["compilationMode"] === "partial") {
694
1289
  tsCompilerOptions["supportTestBed"] = true;
695
1290
  tsCompilerOptions["supportJitMode"] = true;
696
1291
  }
1292
+ if (angularFullVersion >= 2e5) tsCompilerOptions["_enableSelectorless"] = true;
697
1293
  if (!isTest && config.build?.lib) {
698
1294
  tsCompilerOptions["declaration"] = true;
699
1295
  tsCompilerOptions["declarationMap"] = watchMode;
@@ -701,11 +1297,7 @@ function angular(options) {
701
1297
  }
702
1298
  if (isTest) tsCompilerOptions["supportTestBed"] = true;
703
1299
  const replacements = pluginOptions.fileReplacements.map((rp) => join(pluginOptions.workspaceRoot, rp.ssr || rp.with));
704
- rootNames = [...new Set([
705
- ...rootNames,
706
- ...includeCache,
707
- ...replacements
708
- ])];
1300
+ rootNames = union(rootNames, includeCache, replacements);
709
1301
  const hostKey = JSON.stringify(tsCompilerOptions);
710
1302
  let host;
711
1303
  if (cachedHost && cachedHostKey === hostKey) host = cachedHost;
@@ -725,14 +1317,12 @@ function angular(options) {
725
1317
  }
726
1318
  if (!jit) {
727
1319
  const externalizeStyles = !!tsCompilerOptions["externalRuntimeStyles"];
728
- inlineComponentStyles = externalizeStyles ? /* @__PURE__ */ new Map() : void 0;
729
- externalComponentStyles = externalizeStyles ? /* @__PURE__ */ new Map() : void 0;
730
- debugStyles("style maps initialized (NgtscProgram path)", { externalizeStyles });
1320
+ stylesheetRegistry = externalizeStyles ? new AnalogStylesheetRegistry() : void 0;
1321
+ debugStyles("stylesheet registry initialized (NgtscProgram path)", { externalizeStyles });
731
1322
  augmentHostWithResources(host, styleTransform, {
732
1323
  inlineStylesExtension: pluginOptions.inlineStylesExtension,
733
1324
  isProd,
734
- inlineComponentStyles,
735
- externalComponentStyles,
1325
+ stylesheetRegistry,
736
1326
  sourceFileCache: sourceFileCache$1,
737
1327
  stylePreprocessor: pluginOptions.stylePreprocessor
738
1328
  });
@@ -759,7 +1349,7 @@ function angular(options) {
759
1349
  if (!watchMode) builder = ts.createAbstractBuilder(typeScriptProgram, host, oldBuilder);
760
1350
  if (angularCompiler) await angularCompiler.analyzeAsync();
761
1351
  const transformers = mergeTransformers({ before: jit ? [compilerCli.constructorParametersDownlevelTransform(builder.getProgram()), cjt(() => builder.getProgram().getTypeChecker())] : [] }, jit ? {} : angularCompiler.prepareEmit().transformers);
762
- const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.liveReload, pluginOptions.disableTypeChecking);
1352
+ const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.hmr, pluginOptions.disableTypeChecking);
763
1353
  const writeFileCallback = (_filename, content, _a, _b, sourceFiles) => {
764
1354
  if (!sourceFiles?.length) return;
765
1355
  const filename = normalizePath(sourceFiles[0].fileName);
@@ -853,14 +1443,377 @@ function mapTemplateUpdatesToFiles(templateUpdates) {
853
1443
  });
854
1444
  return updatesByFile;
855
1445
  }
1446
+ /**
1447
+ * Returns every live Vite module that can legitimately represent a changed
1448
+ * Angular resource file.
1449
+ *
1450
+ * For normal files, `getModulesByFile()` is enough. For Angular component
1451
+ * stylesheets, it is not: the browser often holds virtual hashed requests
1452
+ * (`/abc123.css?direct&ngcomp=...` and `/abc123.css?ngcomp=...`) that are no
1453
+ * longer discoverable from the original source path alone. We therefore merge:
1454
+ * - watcher event modules
1455
+ * - module-graph modules by source file
1456
+ * - registry-tracked live request ids resolved back through the module graph
1457
+ */
1458
+ async function getModulesForChangedFile(server, file, eventModules = [], stylesheetRegistry) {
1459
+ const normalizedFile = normalizePath(file.split("?")[0]);
1460
+ const modules = /* @__PURE__ */ new Map();
1461
+ for (const mod of eventModules) if (mod.id) modules.set(mod.id, mod);
1462
+ server.moduleGraph.getModulesByFile(normalizedFile)?.forEach((mod) => {
1463
+ if (mod.id) modules.set(mod.id, mod);
1464
+ });
1465
+ const stylesheetRequestIds = stylesheetRegistry?.getRequestIdsForSource(normalizedFile) ?? [];
1466
+ const requestIdHits = [];
1467
+ for (const requestId of stylesheetRequestIds) {
1468
+ const candidates = [requestId, requestId.startsWith("/") ? requestId : `/${requestId}`];
1469
+ for (const candidate of candidates) {
1470
+ const mod = await server.moduleGraph.getModuleByUrl(candidate) ?? server.moduleGraph.getModuleById(candidate);
1471
+ requestIdHits.push({
1472
+ requestId,
1473
+ candidate,
1474
+ via: mod?.url === candidate ? "url" : "id",
1475
+ moduleId: mod?.id
1476
+ });
1477
+ if (mod?.id) modules.set(mod.id, mod);
1478
+ }
1479
+ }
1480
+ debugHmrV("getModulesForChangedFile registry lookup", {
1481
+ file: normalizedFile,
1482
+ stylesheetRequestIds,
1483
+ requestIdHits,
1484
+ resolvedModuleIds: [...modules.keys()]
1485
+ });
1486
+ return [...modules.values()];
1487
+ }
1488
+ function isModuleForChangedResource(mod, changedFile, stylesheetRegistry) {
1489
+ const normalizedChangedFile = normalizePath(changedFile.split("?")[0]);
1490
+ if (normalizePath((mod.file ?? "").split("?")[0]) === normalizedChangedFile) return true;
1491
+ if (!mod.id) return false;
1492
+ const requestPath = getFilenameFromPath(mod.id);
1493
+ return normalizePath((stylesheetRegistry?.resolveExternalSource(requestPath) ?? stylesheetRegistry?.resolveExternalSource(requestPath.replace(/^\//, "")) ?? "").split("?")[0]) === normalizedChangedFile;
1494
+ }
1495
+ function describeStylesheetContent(code) {
1496
+ return {
1497
+ length: code.length,
1498
+ digest: createHash("sha256").update(code).digest("hex").slice(0, 12),
1499
+ preview: code.replace(/\s+/g, " ").trim().slice(0, 160)
1500
+ };
1501
+ }
1502
+ function safeStatMtimeMs(file) {
1503
+ try {
1504
+ return statSync(file).mtimeMs;
1505
+ } catch {
1506
+ return;
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Refreshes any already-served stylesheet records that map back to a changed
1511
+ * source file.
1512
+ *
1513
+ * This is the critical bridge for externalized Angular component styles during
1514
+ * HMR. Angular's resource watcher can notice that `/src/...component.css`
1515
+ * changed before Angular recompilation has had a chance to repopulate the
1516
+ * stylesheet registry. If we emit a CSS update against the existing virtual
1517
+ * stylesheet id without first refreshing the registry content, the browser gets
1518
+ * a hot update containing stale CSS. By rewriting the existing served records
1519
+ * from disk up front, HMR always pushes the latest source content.
1520
+ */
1521
+ function refreshStylesheetRegistryForFile(file, stylesheetRegistry, stylePreprocessor) {
1522
+ const normalizedFile = normalizePath(file.split("?")[0]);
1523
+ if (!stylesheetRegistry || !existsSync(normalizedFile)) return;
1524
+ const publicIds = stylesheetRegistry.getPublicIdsForSource(normalizedFile);
1525
+ if (publicIds.length === 0) return;
1526
+ const rawCss = readFileSync(normalizedFile, "utf-8");
1527
+ let servedCss = preprocessStylesheet(rawCss, normalizedFile, stylePreprocessor);
1528
+ servedCss = rewriteRelativeCssImports(servedCss, normalizedFile);
1529
+ for (const publicId of publicIds) stylesheetRegistry.registerServedStylesheet({
1530
+ publicId,
1531
+ sourcePath: normalizedFile,
1532
+ originalCode: rawCss,
1533
+ normalizedCode: servedCss
1534
+ }, [
1535
+ normalizedFile,
1536
+ normalizePath(normalizedFile),
1537
+ basename(normalizedFile),
1538
+ normalizedFile.replace(/^\//, "")
1539
+ ]);
1540
+ debugStylesV("stylesheet registry refreshed from source file", {
1541
+ file: normalizedFile,
1542
+ publicIds,
1543
+ source: describeStylesheetContent(rawCss),
1544
+ served: describeStylesheetContent(servedCss)
1545
+ });
1546
+ }
1547
+ function diagnoseComponentStylesheetPipeline(changedFile, directModule, stylesheetRegistry, wrapperModules, stylePreprocessor) {
1548
+ const normalizedFile = normalizePath(changedFile.split("?")[0]);
1549
+ const sourceExists = existsSync(normalizedFile);
1550
+ const sourceCode = sourceExists ? readFileSync(normalizedFile, "utf-8") : void 0;
1551
+ const directRequestPath = directModule.id ? getFilenameFromPath(directModule.id) : void 0;
1552
+ const sourcePath = directRequestPath ? stylesheetRegistry?.resolveExternalSource(directRequestPath) ?? stylesheetRegistry?.resolveExternalSource(directRequestPath.replace(/^\//, "")) : normalizedFile;
1553
+ const registryCode = directRequestPath ? stylesheetRegistry?.getServedContent(directRequestPath) : void 0;
1554
+ const trackedRequestIds = stylesheetRegistry?.getRequestIdsForSource(sourcePath ?? "") ?? [];
1555
+ const anomalies = [];
1556
+ const hints = [];
1557
+ if (!sourceExists) {
1558
+ anomalies.push("source_file_missing");
1559
+ hints.push("The stylesheet watcher fired for a file that no longer exists on disk.");
1560
+ }
1561
+ if (!registryCode) {
1562
+ anomalies.push("registry_content_missing");
1563
+ hints.push("The stylesheet registry has no served content for the direct module request path.");
1564
+ }
1565
+ if (sourceCode && registryCode) {
1566
+ let expectedRegistryCode = preprocessStylesheet(sourceCode, normalizedFile, stylePreprocessor);
1567
+ expectedRegistryCode = rewriteRelativeCssImports(expectedRegistryCode, normalizedFile);
1568
+ if (describeStylesheetContent(expectedRegistryCode).digest !== describeStylesheetContent(registryCode).digest) {
1569
+ anomalies.push("source_registry_mismatch");
1570
+ hints.push("The source file changed, but the served stylesheet content in the registry is still stale.");
1571
+ }
1572
+ }
1573
+ if (trackedRequestIds.length === 0) {
1574
+ anomalies.push("no_tracked_requests");
1575
+ hints.push("No live stylesheet requests are tracked for this source file, so HMR has no browser-facing target.");
1576
+ }
1577
+ if (trackedRequestIds.some((id) => id.includes("?ngcomp=")) && wrapperModules.length === 0) {
1578
+ anomalies.push("tracked_wrapper_missing_from_module_graph");
1579
+ hints.push("A wrapper request id is known, but Vite did not expose a live wrapper module during this HMR pass.");
1580
+ }
1581
+ if (trackedRequestIds.every((id) => !id.includes("?ngcomp=")) && wrapperModules.length === 0) {
1582
+ anomalies.push("wrapper_not_yet_tracked");
1583
+ hints.push("Only direct stylesheet requests were tracked during this HMR pass; the wrapper request may be appearing too late.");
1584
+ }
1585
+ return {
1586
+ file: changedFile,
1587
+ sourcePath,
1588
+ source: sourceCode ? describeStylesheetContent(rewriteRelativeCssImports(preprocessStylesheet(sourceCode, normalizedFile, stylePreprocessor), normalizedFile)) : void 0,
1589
+ registry: registryCode ? describeStylesheetContent(registryCode) : void 0,
1590
+ directModuleId: directModule.id,
1591
+ directModuleUrl: directModule.url,
1592
+ trackedRequestIds,
1593
+ wrapperCount: wrapperModules.length,
1594
+ anomalies,
1595
+ hints
1596
+ };
1597
+ }
1598
+ async function findComponentStylesheetWrapperModules(server, changedFile, directModule, fileModules, stylesheetRegistry) {
1599
+ const wrapperModules = /* @__PURE__ */ new Map();
1600
+ for (const mod of fileModules) if (mod.id && mod.type === "js" && isComponentStyleSheet(mod.id) && isModuleForChangedResource(mod, changedFile, stylesheetRegistry)) wrapperModules.set(mod.id, mod);
1601
+ const directRequestIds = /* @__PURE__ */ new Set();
1602
+ if (directModule.id) directRequestIds.add(directModule.id);
1603
+ if (directModule.url) directRequestIds.add(directModule.url);
1604
+ const requestPath = directModule.id ? getFilenameFromPath(directModule.id) : void 0;
1605
+ const sourcePath = requestPath ? stylesheetRegistry?.resolveExternalSource(requestPath) ?? stylesheetRegistry?.resolveExternalSource(requestPath.replace(/^\//, "")) : void 0;
1606
+ for (const requestId of stylesheetRegistry?.getRequestIdsForSource(sourcePath ?? "") ?? []) if (requestId.includes("?ngcomp=")) directRequestIds.add(requestId);
1607
+ const candidateWrapperIds = [...directRequestIds].filter((id) => id.includes("?direct&ngcomp=")).map((id) => id.replace("?direct&ngcomp=", "?ngcomp="));
1608
+ const lookupHits = [];
1609
+ for (const candidate of candidateWrapperIds) {
1610
+ const mod = await server.moduleGraph.getModuleByUrl(candidate) ?? server.moduleGraph.getModuleById(candidate);
1611
+ lookupHits.push({
1612
+ candidate,
1613
+ via: mod?.url === candidate ? "url" : mod ? "id" : void 0,
1614
+ moduleId: mod?.id,
1615
+ moduleType: mod?.type
1616
+ });
1617
+ if (mod?.id && mod.type === "js" && isComponentStyleSheet(mod.id) && isModuleForChangedResource(mod, changedFile, stylesheetRegistry)) wrapperModules.set(mod.id, mod);
1618
+ }
1619
+ debugHmrV("component stylesheet wrapper lookup", {
1620
+ file: changedFile,
1621
+ sourcePath,
1622
+ directModuleId: directModule.id,
1623
+ directModuleUrl: directModule.url,
1624
+ candidateWrapperIds,
1625
+ lookupHits
1626
+ });
1627
+ if (wrapperModules.size === 0) debugHmrV("component stylesheet wrapper lookup empty", {
1628
+ file: changedFile,
1629
+ sourcePath,
1630
+ directModuleId: directModule.id,
1631
+ directModuleUrl: directModule.url,
1632
+ candidateWrapperIds
1633
+ });
1634
+ return [...wrapperModules.values()];
1635
+ }
856
1636
  function sendHMRComponentUpdate(server, id) {
1637
+ debugHmrV("ws send: angular component update", {
1638
+ id,
1639
+ timestamp: Date.now()
1640
+ });
857
1641
  server.ws.send("angular:component-update", {
858
1642
  id: encodeURIComponent(id),
859
1643
  timestamp: Date.now()
860
1644
  });
861
1645
  classNames.delete(id);
862
1646
  }
863
- function getFileMetadata(program, angularCompiler, liveReload, disableTypeChecking) {
1647
+ function sendCssUpdate(server, update) {
1648
+ const timestamp = Date.now();
1649
+ debugHmrV("ws send: css-update", {
1650
+ ...update,
1651
+ timestamp
1652
+ });
1653
+ server.ws.send({
1654
+ type: "update",
1655
+ updates: [{
1656
+ type: "css-update",
1657
+ timestamp,
1658
+ path: update.path,
1659
+ acceptedPath: update.acceptedPath
1660
+ }]
1661
+ });
1662
+ }
1663
+ function sendFullReload(server, details) {
1664
+ debugHmrV("ws send: full-reload", details);
1665
+ server.ws.send("analog:debug-full-reload", details);
1666
+ server.ws.send({ type: "full-reload" });
1667
+ }
1668
+ function resolveComponentClassNamesForStyleOwner(ownerFile, sourcePath) {
1669
+ if (!existsSync(ownerFile)) return [];
1670
+ const components = getAngularComponentMetadata(readFileSync(ownerFile, "utf-8"));
1671
+ const normalizedSourcePath = normalizePath(sourcePath);
1672
+ return components.filter((component) => component.styleUrls.some((styleUrl) => normalizePath(resolve(dirname(ownerFile), styleUrl)) === normalizedSourcePath)).map((component) => component.className);
1673
+ }
1674
+ function findStaticClassAndBoundClassConflicts(template) {
1675
+ const issues = [];
1676
+ for (const { index, snippet } of findOpeningTagSnippets(template)) {
1677
+ if (!snippet.includes("[class]")) continue;
1678
+ const hasStaticClass = /\sclass\s*=\s*(['"])(?:(?!\1)[\s\S])*\1/.test(snippet);
1679
+ const hasBoundClass = /\s\[class\]\s*=\s*(['"])(?:(?!\1)[\s\S])*\1/.test(snippet);
1680
+ if (hasStaticClass && hasBoundClass) {
1681
+ const prefix = template.slice(0, index);
1682
+ const line = prefix.split("\n").length;
1683
+ const column = index - prefix.lastIndexOf("\n");
1684
+ issues.push({
1685
+ line,
1686
+ column,
1687
+ snippet: snippet.replace(/\s+/g, " ").trim()
1688
+ });
1689
+ }
1690
+ }
1691
+ return issues;
1692
+ }
1693
+ function throwTemplateClassBindingConflict(id, issue) {
1694
+ throw new Error([
1695
+ "[Analog Angular] Invalid template class binding.",
1696
+ `File: ${id}:${issue.line}:${issue.column}`,
1697
+ "The same element uses both a static `class=\"...\"` attribute and a whole-element `[class]=\"...\"` binding.",
1698
+ "That pattern can replace or conflict with static Tailwind classes, which makes styles appear to stop applying.",
1699
+ "Use `[ngClass]` or explicit `[class.foo]` bindings instead of `[class]` when the element also has static classes.",
1700
+ `Snippet: ${issue.snippet}`
1701
+ ].join("\n"));
1702
+ }
1703
+ function findBoundClassAndNgClassConflicts(template) {
1704
+ const issues = [];
1705
+ if (!/\[class\]\s*=/.test(template) || !template.includes("[ngClass]")) return issues;
1706
+ for (const { index, snippet } of findOpeningTagSnippets(template)) {
1707
+ if (!/\[class\]\s*=/.test(snippet) || !snippet.includes("[ngClass]")) continue;
1708
+ const prefix = template.slice(0, index);
1709
+ const line = prefix.split("\n").length;
1710
+ const column = index - prefix.lastIndexOf("\n");
1711
+ issues.push({
1712
+ line,
1713
+ column,
1714
+ snippet: snippet.replace(/\s+/g, " ").trim()
1715
+ });
1716
+ }
1717
+ return issues;
1718
+ }
1719
+ function findOpeningTagSnippets(template) {
1720
+ const matches = [];
1721
+ for (let index = 0; index < template.length; index++) {
1722
+ if (template[index] !== "<") continue;
1723
+ const tagStart = template[index + 1];
1724
+ if (!tagStart || !/[a-zA-Z]/.test(tagStart)) continue;
1725
+ let quote = null;
1726
+ for (let end = index + 1; end < template.length; end++) {
1727
+ const char = template[end];
1728
+ if (quote) {
1729
+ if (char === quote) quote = null;
1730
+ continue;
1731
+ }
1732
+ if (char === "\"" || char === "'") {
1733
+ quote = char;
1734
+ continue;
1735
+ }
1736
+ if (char === ">") {
1737
+ matches.push({
1738
+ index,
1739
+ snippet: template.slice(index, end + 1)
1740
+ });
1741
+ index = end;
1742
+ break;
1743
+ }
1744
+ }
1745
+ }
1746
+ return matches;
1747
+ }
1748
+ function formatActiveGraphLocations(entries) {
1749
+ return [...entries].sort().map((entry) => `- ${entry}`).join("\n");
1750
+ }
1751
+ function logComponentStylesheetHmrOutcome(details) {
1752
+ const pitfalls = [];
1753
+ const rejectedPreferredPaths = [];
1754
+ const hints = [];
1755
+ if (details.encapsulation === "shadow") {
1756
+ pitfalls.push("shadow-encapsulation");
1757
+ rejectedPreferredPaths.push("css-update");
1758
+ rejectedPreferredPaths.push("owner-component-update");
1759
+ hints.push("Shadow DOM styles cannot rely on Vite CSS patching because Angular applies them inside a shadow root.");
1760
+ }
1761
+ if (details.diagnosis.anomalies.includes("wrapper_not_yet_tracked")) {
1762
+ pitfalls.push("wrapper-not-yet-tracked");
1763
+ rejectedPreferredPaths.push("css-update");
1764
+ hints.push("The direct stylesheet module exists, but the browser-visible Angular wrapper module was not available in the live graph during this HMR pass.");
1765
+ }
1766
+ if (details.diagnosis.anomalies.includes("tracked_wrapper_missing_from_module_graph")) {
1767
+ pitfalls.push("tracked-wrapper-missing-from-module-graph");
1768
+ rejectedPreferredPaths.push("css-update");
1769
+ hints.push("A wrapper request id is known, but Vite could not resolve a live wrapper module for targeted CSS HMR.");
1770
+ }
1771
+ if ((details.ownerIds?.filter(Boolean).length ?? 0) === 0) {
1772
+ pitfalls.push("no-owner-modules");
1773
+ if (details.outcome === "full-reload") {
1774
+ rejectedPreferredPaths.push("owner-component-update");
1775
+ hints.push("No owning TS component modules were available in the module graph for owner-based fallback.");
1776
+ }
1777
+ } else if ((details.updateIds?.length ?? 0) === 0) {
1778
+ pitfalls.push("owner-modules-without-class-identities");
1779
+ if (details.outcome === "full-reload") {
1780
+ rejectedPreferredPaths.push("owner-component-update");
1781
+ hints.push("Owner modules were found, but Angular did not expose component class identities after recompilation, so no targeted component update could be sent.");
1782
+ }
1783
+ }
1784
+ debugHmrV("component stylesheet hmr outcome", {
1785
+ file: details.file,
1786
+ outcome: details.outcome,
1787
+ encapsulation: details.encapsulation,
1788
+ directModuleId: details.directModuleId,
1789
+ wrapperIds: details.wrapperIds ?? [],
1790
+ ownerIds: details.ownerIds ?? [],
1791
+ updateIds: details.updateIds ?? [],
1792
+ preferredPath: details.encapsulation === "shadow" ? "full-reload" : "css-update",
1793
+ rejectedPreferredPaths: [...new Set(rejectedPreferredPaths)],
1794
+ pitfalls: [...new Set(pitfalls)],
1795
+ anomalies: details.diagnosis.anomalies,
1796
+ hints: [...new Set([...details.diagnosis.hints, ...hints])]
1797
+ });
1798
+ }
1799
+ function findTemplateOwnerModules(server, resourceFile) {
1800
+ const candidateTsFiles = [normalizePath(resourceFile.split("?")[0]).replace(/\.(html|htm)$/i, ".ts")];
1801
+ const modules = /* @__PURE__ */ new Map();
1802
+ for (const candidate of candidateTsFiles) server.moduleGraph.getModulesByFile(candidate)?.forEach((mod) => {
1803
+ if (mod.id) modules.set(mod.id, mod);
1804
+ });
1805
+ return [...modules.values()];
1806
+ }
1807
+ function findStyleOwnerModules(server, resourceFile, styleSourceOwners) {
1808
+ const normalizedResourceFile = normalizePath(resourceFile.split("?")[0]);
1809
+ const candidateOwnerFiles = [...styleSourceOwners.get(normalizedResourceFile) ?? []];
1810
+ const modules = /* @__PURE__ */ new Map();
1811
+ for (const ownerFile of candidateOwnerFiles) server.moduleGraph.getModulesByFile(ownerFile)?.forEach((mod) => {
1812
+ if (mod.id) modules.set(mod.id, mod);
1813
+ });
1814
+ return [...modules.values()];
1815
+ }
1816
+ function getFileMetadata(program, angularCompiler, hmrEnabled, disableTypeChecking) {
864
1817
  const ts = require("typescript");
865
1818
  return (file) => {
866
1819
  const sourceFile = program.getSourceFile(file);
@@ -870,7 +1823,7 @@ function getFileMetadata(program, angularCompiler, liveReload, disableTypeChecki
870
1823
  const warnings = diagnostics.filter((d) => d.category === ts.DiagnosticCategory?.Warning).map((d) => d.messageText);
871
1824
  let hmrUpdateCode = void 0;
872
1825
  let hmrEligible = false;
873
- if (liveReload) {
1826
+ if (hmrEnabled) {
874
1827
  for (const node of sourceFile.statements) if (ts.isClassDeclaration(node) && node.name != null) {
875
1828
  hmrUpdateCode = angularCompiler?.emitHmrUpdateModule(node);
876
1829
  if (hmrUpdateCode) {
@@ -930,7 +1883,12 @@ function getComponentStyleSheetMeta(id) {
930
1883
  * @param id
931
1884
  */
932
1885
  function getFilenameFromPath(id) {
933
- return new URL(id, "http://localhost").pathname.replace(/^\//, "");
1886
+ try {
1887
+ return new URL(id, "http://localhost").pathname.replace(/^\//, "");
1888
+ } catch {
1889
+ const queryIndex = id.indexOf("?");
1890
+ return (queryIndex >= 0 ? id.slice(0, queryIndex) : id).replace(/^\//, "");
1891
+ }
934
1892
  }
935
1893
  /**
936
1894
  * Checks for vitest run from the command line