@analogjs/vite-plugin-angular 3.0.0-alpha.24 → 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";
@@ -13,14 +14,14 @@ 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
16
  import { union } from "es-toolkit";
16
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
17
19
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
18
20
  import * as compilerCli from "@angular/compiler-cli";
19
21
  import { createRequire } from "node:module";
20
22
  import * as ngCompiler from "@angular/compiler";
21
23
  import { globSync } from "tinyglobby";
22
24
  import { defaultClientConditions, normalizePath, preprocessCSS } from "vite";
23
- import { createHash } from "node:crypto";
24
25
  //#region packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
25
26
  var require = createRequire(import.meta.url);
26
27
  var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
@@ -31,6 +32,13 @@ var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
31
32
  DiagnosticModes[DiagnosticModes["All"] = 7] = "All";
32
33
  return DiagnosticModes;
33
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
+ }
34
42
  /**
35
43
  * TypeScript file extension regex
36
44
  * Match .(c or m)ts, .ts extensions with an optional ? for query params
@@ -38,10 +46,33 @@ var DiagnosticModes = /* @__PURE__ */ function(DiagnosticModes) {
38
46
  */
39
47
  var TS_EXT_REGEX = /\.[cm]?(ts)[^x]?\??/;
40
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
+ }
41
64
  /**
42
65
  * Builds a resolved stylePreprocessor function from plugin options.
43
- * If `tailwindCss` is provided, creates an auto-reference injector.
44
- * 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.
45
76
  */
46
77
  function buildStylePreprocessor(options) {
47
78
  const userPreprocessor = options?.stylePreprocessor;
@@ -55,17 +86,18 @@ function buildStylePreprocessor(options) {
55
86
  rootStylesheet,
56
87
  prefixes
57
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.`);
58
90
  tailwindPreprocessor = (code, filename) => {
59
91
  if (code.includes("@reference") || code.includes("@import \"tailwindcss\"") || code.includes("@import 'tailwindcss'")) {
60
- debugTailwind("skip (already has @reference or is root)", { filename });
92
+ debugTailwindV("skip (already has @reference or is root)", { filename });
61
93
  return code;
62
94
  }
63
95
  if (!(prefixes ? prefixes.some((prefix) => code.includes(prefix)) : code.includes("@apply"))) {
64
- debugTailwind("skip (no Tailwind usage detected)", { filename });
96
+ debugTailwindV("skip (no Tailwind usage detected)", { filename });
65
97
  return code;
66
98
  }
67
- debugTailwind("injected @reference", { filename });
68
- return `@reference "${relative(dirname(filename), rootStylesheet)}";\n${code}`;
99
+ debugTailwind("injected @reference via preprocessor", { filename });
100
+ return `@reference "${rootStylesheet}";\n${code}`;
69
101
  };
70
102
  }
71
103
  if (tailwindPreprocessor && userPreprocessor) {
@@ -95,10 +127,12 @@ function angular(options) {
95
127
  jit: options?.jit,
96
128
  include: options?.include ?? [],
97
129
  additionalContentDirs: options?.additionalContentDirs ?? [],
98
- liveReload: options?.liveReload ?? false,
130
+ hmr: options?.hmr ?? options?.liveReload ?? true,
99
131
  disableTypeChecking: options?.disableTypeChecking ?? true,
100
132
  fileReplacements: options?.fileReplacements ?? [],
101
133
  useAngularCompilationAPI: options?.experimental?.useAngularCompilationAPI ?? false,
134
+ hasTailwindCss: !!options?.tailwindCss,
135
+ tailwindCss: options?.tailwindCss,
102
136
  stylePreprocessor: buildStylePreprocessor(options)
103
137
  };
104
138
  let resolvedConfig;
@@ -120,8 +154,134 @@ function angular(options) {
120
154
  }
121
155
  let watchMode = false;
122
156
  let testWatchMode = isTestWatchMode();
123
- let inlineComponentStyles;
124
- 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;
125
285
  const sourceFileCache$1 = new sourceFileCache();
126
286
  const isTest = process.env.NODE_ENV === "test" || !!process.env["VITEST"];
127
287
  const isVitestVscode = !!process.env["VITEST_VSCODE"];
@@ -146,9 +306,17 @@ function angular(options) {
146
306
  let angularCompilation;
147
307
  function angularPlugin() {
148
308
  let isProd = false;
149
- if (angularFullVersion < 19e4 || isTest) {
150
- pluginOptions.liveReload = false;
151
- 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", {
152
320
  angularVersion: angularFullVersion,
153
321
  isTest
154
322
  });
@@ -201,7 +369,11 @@ function angular(options) {
201
369
  return {
202
370
  [jsTransformConfigKey]: jsTransformConfigValue,
203
371
  optimizeDeps: {
204
- include: ["rxjs/operators", "rxjs"],
372
+ include: [
373
+ "rxjs/operators",
374
+ "rxjs",
375
+ "tslib"
376
+ ],
205
377
  exclude: ["@angular/platform-server"],
206
378
  ...useRolldown ? { rolldownOptions } : { esbuildOptions }
207
379
  },
@@ -210,10 +382,10 @@ function angular(options) {
210
382
  },
211
383
  configResolved(config) {
212
384
  resolvedConfig = config;
385
+ if (pluginOptions.hasTailwindCss) validateTailwindConfig(config, watchMode);
213
386
  if (pluginOptions.useAngularCompilationAPI) {
214
- externalComponentStyles = /* @__PURE__ */ new Map();
215
- inlineComponentStyles = /* @__PURE__ */ new Map();
216
- debugStyles("style maps initialized (Angular Compilation API)");
387
+ stylesheetRegistry = new AnalogStylesheetRegistry();
388
+ debugStyles("stylesheet registry initialized (Angular Compilation API)");
217
389
  }
218
390
  if (!jit) styleTransform = (code, filename) => preprocessCSS(code, filename, config);
219
391
  if (isTest) testWatchMode = !(config.server.watch === null) || config.test?.watch === true || testWatchMode;
@@ -222,7 +394,15 @@ function angular(options) {
222
394
  viteServer = server;
223
395
  const invalidateCompilationOnFsChange = createFsWatcherCacheInvalidator(invalidateFsCaches, invalidateTsconfigCaches, () => performCompilation(resolvedConfig));
224
396
  server.watcher.on("add", invalidateCompilationOnFsChange);
225
- 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
+ });
226
406
  server.watcher.on("change", (file) => {
227
407
  if (file.includes("tsconfig")) invalidateTsconfigCaches();
228
408
  });
@@ -235,6 +415,10 @@ function angular(options) {
235
415
  }
236
416
  },
237
417
  async handleHotUpdate(ctx) {
418
+ if (isIgnoredHmrFile(ctx.file)) {
419
+ debugHmr("ignored file change", { file: ctx.file });
420
+ return [];
421
+ }
238
422
  if (TS_EXT_REGEX.test(ctx.file)) {
239
423
  const [fileId] = ctx.file.split("?");
240
424
  debugHmr("TS file changed", {
@@ -243,7 +427,7 @@ function angular(options) {
243
427
  });
244
428
  pendingCompilation = performCompilation(resolvedConfig, [fileId]);
245
429
  let result;
246
- if (pluginOptions.liveReload) {
430
+ if (shouldEnableHmr()) {
247
431
  await pendingCompilation;
248
432
  pendingCompilation = null;
249
433
  result = fileEmitter(fileId);
@@ -252,10 +436,28 @@ function angular(options) {
252
436
  hmrEligible: !!result?.hmrEligible,
253
437
  hasClassName: !!classNames.get(fileId)
254
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
+ });
255
451
  }
256
- if (pluginOptions.liveReload && result?.hmrEligible && classNames.get(fileId)) {
452
+ if (shouldEnableHmr() && result?.hmrEligible && classNames.get(fileId)) {
257
453
  const relativeFileId = `${normalizePath(relative(process.cwd(), fileId))}@${classNames.get(fileId)}`;
258
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
+ });
259
461
  sendHMRComponentUpdate(ctx.server, relativeFileId);
260
462
  return ctx.modules.map((mod) => {
261
463
  if (mod.id === ctx.file) return markModuleSelfAccepting(mod);
@@ -266,51 +468,224 @@ function angular(options) {
266
468
  if (/\.(html|htm|css|less|sass|scss)$/.test(ctx.file)) {
267
469
  debugHmr("resource file changed", { file: ctx.file });
268
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
+ });
269
497
  /**
270
498
  * Check to see if this was a direct request
271
499
  * for an external resource (styles, html).
272
500
  */
273
- const isDirect = ctx.modules.find((mod) => ctx.file === mod.file && mod.id?.includes("?direct"));
274
- 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
+ });
275
510
  if (isDirect || isInline) {
276
- if (pluginOptions.liveReload && isDirect?.id && isDirect.file) {
277
- 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) {
278
521
  const { encapsulation } = getComponentStyleSheetMeta(isDirect.id);
279
- 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", {
280
525
  file: isDirect.file,
281
526
  encapsulation
282
527
  });
283
- if (encapsulation !== "shadow") {
284
- ctx.server.ws.send({
285
- type: "update",
286
- updates: [{
287
- type: "css-update",
288
- timestamp: Date.now(),
289
- path: isDirect.url,
290
- acceptedPath: isDirect.file
291
- }]
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)
292
564
  });
293
- return ctx.modules.filter((mod) => {
565
+ return union(fileModules.filter((mod) => {
294
566
  return mod.file !== ctx.file || mod.id !== isDirect.id;
295
567
  }).map((mod) => {
296
568
  if (mod.file === ctx.file) return markModuleSelfAccepting(mod);
297
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."
298
610
  });
299
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));
300
669
  }
301
670
  }
302
- return ctx.modules;
303
671
  }
304
672
  const mods = [];
305
673
  const updates = [];
306
- ctx.modules.forEach((mod) => {
674
+ fileModules.forEach((mod) => {
307
675
  mod.importers.forEach((imp) => {
308
676
  ctx.server.moduleGraph.invalidateModule(imp);
309
- if (pluginOptions.liveReload && classNames.get(imp.id)) updates.push(imp.id);
677
+ if (shouldExternalizeStyles() && classNames.get(imp.id)) updates.push(imp.id);
310
678
  else mods.push(imp);
311
679
  });
312
680
  });
313
- 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]);
314
689
  if (updates.length > 0) {
315
690
  await pendingCompilation;
316
691
  pendingCompilation = null;
@@ -322,7 +697,7 @@ function angular(options) {
322
697
  const impRelativeFileId = `${normalizePath(relative(process.cwd(), updateId))}@${classNames.get(updateId)}`;
323
698
  sendHMRComponentUpdate(ctx.server, impRelativeFileId);
324
699
  });
325
- return ctx.modules.map((mod) => {
700
+ return fileModules.map((mod) => {
326
701
  if (mod.id === ctx.file) return markModuleSelfAccepting(mod);
327
702
  return mod;
328
703
  });
@@ -340,24 +715,42 @@ function angular(options) {
340
715
  }
341
716
  if (isComponentStyleSheet(id)) {
342
717
  const filename = getFilenameFromPath(id);
343
- 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);
344
723
  if (componentStyles) {
345
- debugStyles("resolveId: mapped external stylesheet", {
724
+ debugStylesV("resolveId: mapped external stylesheet", {
346
725
  filename,
347
726
  resolvedPath: componentStyles
348
727
  });
349
728
  return componentStyles + new URL(id, "http://localhost").search;
350
729
  }
730
+ debugStyles("resolveId: component stylesheet NOT FOUND in either map", {
731
+ filename,
732
+ inlineMapSize: stylesheetRegistry?.servedCount ?? 0,
733
+ externalMapSize: stylesheetRegistry?.externalCount ?? 0
734
+ });
351
735
  }
352
736
  },
353
737
  async load(id) {
354
738
  if (isComponentStyleSheet(id)) {
355
739
  const filename = getFilenameFromPath(id);
356
- const componentStyles = inlineComponentStyles?.get(filename);
740
+ const componentStyles = stylesheetRegistry?.getServedContent(filename);
357
741
  if (componentStyles) {
358
- debugStyles("load: served inline component stylesheet", {
742
+ stylesheetRegistry?.registerActiveRequest(id);
743
+ debugHmrV("stylesheet active request registered", {
744
+ requestId: id,
359
745
  filename,
360
- 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)
361
754
  });
362
755
  return componentStyles;
363
756
  }
@@ -388,12 +781,15 @@ function angular(options) {
388
781
  */
389
782
  if (id.includes("?") && id.includes("analog-content-")) return;
390
783
  /**
391
- * 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.
392
788
  */
393
- if (pluginOptions.liveReload && isComponentStyleSheet(id)) {
789
+ if (shouldExternalizeStyles() && isComponentStyleSheet(id)) {
394
790
  const { encapsulation, componentId } = getComponentStyleSheetMeta(id);
395
791
  if (encapsulation === "emulated" && componentId) {
396
- debugStyles("applying emulated view encapsulation", {
792
+ debugStylesV("applying emulated view encapsulation", {
397
793
  stylesheet: id.split("?")[0],
398
794
  componentId
399
795
  });
@@ -421,7 +817,7 @@ function angular(options) {
421
817
  }
422
818
  }
423
819
  const hasComponent = code.includes("@Component");
424
- debugCompiler("transform", {
820
+ debugCompilerV("transform", {
425
821
  id,
426
822
  codeLength: code.length,
427
823
  hasComponent
@@ -451,6 +847,19 @@ function angular(options) {
451
847
  data = data.replace(`angular:jit:style:file;${styleFile}`, `${resolvedStyleUrl}?inline`);
452
848
  });
453
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
+ }
454
863
  return {
455
864
  code: data,
456
865
  map: null
@@ -469,8 +878,103 @@ function angular(options) {
469
878
  }
470
879
  return [
471
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
+ },
472
976
  angularPlugin(),
473
- pluginOptions.liveReload && liveReloadPlugin({
977
+ pluginOptions.hmr && liveReloadPlugin({
474
978
  classNames,
475
979
  fileEmitter
476
980
  }),
@@ -485,8 +989,7 @@ function angular(options) {
485
989
  nxFolderPlugin()
486
990
  ].filter(Boolean);
487
991
  function findIncludes() {
488
- const workspaceRoot = normalizePath(resolve(pluginOptions.workspaceRoot));
489
- return globSync([...pluginOptions.include.map((glob) => `${workspaceRoot}${glob}`)], {
992
+ return globSync(pluginOptions.include.map((glob) => normalizeIncludeGlob(pluginOptions.workspaceRoot, glob)), {
490
993
  dot: true,
491
994
  absolute: true
492
995
  });
@@ -541,17 +1044,30 @@ function angular(options) {
541
1044
  modifiedFiles,
542
1045
  async transformStylesheet(data, containingFile, resourceFile, order, className) {
543
1046
  const filename = resourceFile ?? containingFile.replace(".ts", `.${pluginOptions.inlineStylesExtension}`);
544
- const preprocessedData = pluginOptions.stylePreprocessor ? pluginOptions.stylePreprocessor(data, filename) ?? data : data;
545
- if (pluginOptions.liveReload && watchMode) {
546
- const stylesheetId = createHash("sha256").update(containingFile).update(className).update(String(order)).update(preprocessedData).digest("hex") + "." + pluginOptions.inlineStylesExtension;
547
- inlineComponentStyles.set(stylesheetId, preprocessedData);
548
- 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", {
549
1059
  stylesheetId,
550
1060
  resourceFile: resourceFile ?? "(inline)"
551
1061
  });
1062
+ debugStylesV("stylesheet deferred content snapshot", {
1063
+ stylesheetId,
1064
+ filename,
1065
+ resourceFile: resourceFile ?? "(inline)",
1066
+ ...describeStylesheetContent(preprocessedData)
1067
+ });
552
1068
  return stylesheetId;
553
1069
  }
554
- debugStyles("stylesheet processed inline via preprocessCSS (no liveReload)", {
1070
+ debugStyles("stylesheet processed inline via preprocessCSS", {
555
1071
  filename,
556
1072
  resourceFile: resourceFile ?? "(inline)",
557
1073
  dataLength: preprocessedData.length
@@ -560,7 +1076,11 @@ function angular(options) {
560
1076
  try {
561
1077
  stylesheetResult = await preprocessCSS(preprocessedData, `${filename}?direct`, resolvedConfig);
562
1078
  } catch (e) {
563
- console.error(`${e}`);
1079
+ debugStyles("preprocessCSS error", {
1080
+ filename,
1081
+ resourceFile: resourceFile ?? "(inline)",
1082
+ error: String(e)
1083
+ });
564
1084
  }
565
1085
  return stylesheetResult?.code || "";
566
1086
  },
@@ -568,21 +1088,24 @@ function angular(options) {
568
1088
  return "";
569
1089
  }
570
1090
  }, (tsCompilerOptions) => {
571
- if (pluginOptions.liveReload && watchMode) {
1091
+ if (shouldExternalizeStyles()) tsCompilerOptions["externalRuntimeStyles"] = true;
1092
+ if (shouldEnableHmr()) {
572
1093
  tsCompilerOptions["_enableHmr"] = true;
573
- tsCompilerOptions["externalRuntimeStyles"] = true;
574
1094
  tsCompilerOptions["supportTestBed"] = true;
575
1095
  }
576
1096
  debugCompiler("tsCompilerOptions (compilation API)", {
577
- liveReload: pluginOptions.liveReload,
1097
+ hmr: pluginOptions.hmr,
1098
+ hasTailwindCss: pluginOptions.hasTailwindCss,
578
1099
  watchMode,
1100
+ shouldExternalize: shouldExternalizeStyles(),
579
1101
  externalRuntimeStyles: !!tsCompilerOptions["externalRuntimeStyles"],
580
- hmr: !!tsCompilerOptions["_enableHmr"]
1102
+ hmrEnabled: !!tsCompilerOptions["_enableHmr"]
581
1103
  });
582
1104
  if (tsCompilerOptions.compilationMode === "partial") {
583
1105
  tsCompilerOptions["supportTestBed"] = true;
584
1106
  tsCompilerOptions["supportJitMode"] = true;
585
1107
  }
1108
+ if (angularFullVersion >= 2e5) tsCompilerOptions["_enableSelectorless"] = true;
586
1109
  if (!isTest && config.build?.lib) {
587
1110
  tsCompilerOptions["declaration"] = true;
588
1111
  tsCompilerOptions["declarationMap"] = watchMode;
@@ -591,13 +1114,80 @@ function angular(options) {
591
1114
  if (isTest) tsCompilerOptions["supportTestBed"] = true;
592
1115
  return tsCompilerOptions;
593
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
+ };
594
1128
  compilationResult.externalStylesheets?.forEach((value, key) => {
595
- externalComponentStyles?.set(`${value}.css`, key);
596
- debugStyles("external stylesheet registered for resolveId mapping", {
597
- 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,
598
1187
  resolvedPath: key
599
1188
  });
600
1189
  });
1190
+ debugStyles("external stylesheet preprocessing complete", preprocessStats);
601
1191
  const diagnostics = await angularCompilation.diagnoseFiles(pluginOptions.disableTypeChecking ? DiagnosticModes.All & ~DiagnosticModes.Semantic : DiagnosticModes.All);
602
1192
  const errors = diagnostics.errors?.length ? diagnostics.errors : [];
603
1193
  const warnings = diagnostics.warnings?.length ? diagnostics.warnings : [];
@@ -653,7 +1243,9 @@ function angular(options) {
653
1243
  resolvedTsConfigPath,
654
1244
  isProd ? "prod" : "dev",
655
1245
  isTest ? "test" : "app",
656
- config.build?.lib ? "lib" : "nolib"
1246
+ config.build?.lib ? "lib" : "nolib",
1247
+ pluginOptions.hmr ? "hmr" : "nohmr",
1248
+ pluginOptions.hasTailwindCss ? "tw" : "notw"
657
1249
  ].join("|");
658
1250
  let cached = tsconfigOptionsCache.get(tsconfigKey);
659
1251
  if (!cached) {
@@ -682,19 +1274,22 @@ function angular(options) {
682
1274
  }
683
1275
  const tsCompilerOptions = { ...cached.options };
684
1276
  let rootNames = [...cached.rootNames];
685
- if (pluginOptions.liveReload && watchMode) {
1277
+ if (shouldExternalizeStyles()) tsCompilerOptions["externalRuntimeStyles"] = true;
1278
+ if (shouldEnableHmr()) {
686
1279
  tsCompilerOptions["_enableHmr"] = true;
687
- tsCompilerOptions["externalRuntimeStyles"] = true;
688
1280
  tsCompilerOptions["supportTestBed"] = true;
689
1281
  }
690
1282
  debugCompiler("tsCompilerOptions (NgtscProgram path)", {
1283
+ hmr: pluginOptions.hmr,
1284
+ shouldExternalize: shouldExternalizeStyles(),
691
1285
  externalRuntimeStyles: !!tsCompilerOptions["externalRuntimeStyles"],
692
- hmr: !!tsCompilerOptions["_enableHmr"]
1286
+ hmrEnabled: !!tsCompilerOptions["_enableHmr"]
693
1287
  });
694
1288
  if (tsCompilerOptions["compilationMode"] === "partial") {
695
1289
  tsCompilerOptions["supportTestBed"] = true;
696
1290
  tsCompilerOptions["supportJitMode"] = true;
697
1291
  }
1292
+ if (angularFullVersion >= 2e5) tsCompilerOptions["_enableSelectorless"] = true;
698
1293
  if (!isTest && config.build?.lib) {
699
1294
  tsCompilerOptions["declaration"] = true;
700
1295
  tsCompilerOptions["declarationMap"] = watchMode;
@@ -722,14 +1317,12 @@ function angular(options) {
722
1317
  }
723
1318
  if (!jit) {
724
1319
  const externalizeStyles = !!tsCompilerOptions["externalRuntimeStyles"];
725
- inlineComponentStyles = externalizeStyles ? /* @__PURE__ */ new Map() : void 0;
726
- externalComponentStyles = externalizeStyles ? /* @__PURE__ */ new Map() : void 0;
727
- debugStyles("style maps initialized (NgtscProgram path)", { externalizeStyles });
1320
+ stylesheetRegistry = externalizeStyles ? new AnalogStylesheetRegistry() : void 0;
1321
+ debugStyles("stylesheet registry initialized (NgtscProgram path)", { externalizeStyles });
728
1322
  augmentHostWithResources(host, styleTransform, {
729
1323
  inlineStylesExtension: pluginOptions.inlineStylesExtension,
730
1324
  isProd,
731
- inlineComponentStyles,
732
- externalComponentStyles,
1325
+ stylesheetRegistry,
733
1326
  sourceFileCache: sourceFileCache$1,
734
1327
  stylePreprocessor: pluginOptions.stylePreprocessor
735
1328
  });
@@ -756,7 +1349,7 @@ function angular(options) {
756
1349
  if (!watchMode) builder = ts.createAbstractBuilder(typeScriptProgram, host, oldBuilder);
757
1350
  if (angularCompiler) await angularCompiler.analyzeAsync();
758
1351
  const transformers = mergeTransformers({ before: jit ? [compilerCli.constructorParametersDownlevelTransform(builder.getProgram()), cjt(() => builder.getProgram().getTypeChecker())] : [] }, jit ? {} : angularCompiler.prepareEmit().transformers);
759
- const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.liveReload, pluginOptions.disableTypeChecking);
1352
+ const fileMetadata = getFileMetadata(builder, angularCompiler, pluginOptions.hmr, pluginOptions.disableTypeChecking);
760
1353
  const writeFileCallback = (_filename, content, _a, _b, sourceFiles) => {
761
1354
  if (!sourceFiles?.length) return;
762
1355
  const filename = normalizePath(sourceFiles[0].fileName);
@@ -850,14 +1443,377 @@ function mapTemplateUpdatesToFiles(templateUpdates) {
850
1443
  });
851
1444
  return updatesByFile;
852
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
+ }
853
1636
  function sendHMRComponentUpdate(server, id) {
1637
+ debugHmrV("ws send: angular component update", {
1638
+ id,
1639
+ timestamp: Date.now()
1640
+ });
854
1641
  server.ws.send("angular:component-update", {
855
1642
  id: encodeURIComponent(id),
856
1643
  timestamp: Date.now()
857
1644
  });
858
1645
  classNames.delete(id);
859
1646
  }
860
- 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) {
861
1817
  const ts = require("typescript");
862
1818
  return (file) => {
863
1819
  const sourceFile = program.getSourceFile(file);
@@ -867,7 +1823,7 @@ function getFileMetadata(program, angularCompiler, liveReload, disableTypeChecki
867
1823
  const warnings = diagnostics.filter((d) => d.category === ts.DiagnosticCategory?.Warning).map((d) => d.messageText);
868
1824
  let hmrUpdateCode = void 0;
869
1825
  let hmrEligible = false;
870
- if (liveReload) {
1826
+ if (hmrEnabled) {
871
1827
  for (const node of sourceFile.statements) if (ts.isClassDeclaration(node) && node.name != null) {
872
1828
  hmrUpdateCode = angularCompiler?.emitHmrUpdateModule(node);
873
1829
  if (hmrUpdateCode) {
@@ -927,7 +1883,12 @@ function getComponentStyleSheetMeta(id) {
927
1883
  * @param id
928
1884
  */
929
1885
  function getFilenameFromPath(id) {
930
- 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
+ }
931
1892
  }
932
1893
  /**
933
1894
  * Checks for vitest run from the command line