@analogjs/vite-plugin-angular 3.0.0-alpha.24 → 3.0.0-alpha.26

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