Skip to content

Commit fe6c528

Browse files
authored
fix(vite-plugin-angular): improve stylesheet registry compatibility with Tailwind (#2326)
1 parent 37280cc commit fe6c528

5 files changed

Lines changed: 161 additions & 18 deletions

File tree

packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,17 @@ export function angular(options?: PluginOptions): Plugin[] {
12231223
// Map angular external styleUrls to the source file
12241224
if (isComponentStyleSheet(id)) {
12251225
const filename = getFilenameFromPath(id);
1226+
const search = new URL(id, 'http://localhost').search;
1227+
const servedSourcePath =
1228+
stylesheetRegistry?.getServedSourcePath(filename);
1229+
1230+
if (servedSourcePath) {
1231+
debugStylesV('resolveId: mapped served stylesheet to source', {
1232+
filename,
1233+
resolvedPath: servedSourcePath,
1234+
});
1235+
return servedSourcePath + search;
1236+
}
12261237

12271238
if (stylesheetRegistry?.hasServed(filename)) {
12281239
debugStylesV('resolveId: kept preprocessed ID', { filename });
@@ -1286,13 +1297,21 @@ export function angular(options?: PluginOptions): Plugin[] {
12861297
stylesheetRegistry?.resolveExternalSource(filename) ??
12871298
stylesheetRegistry?.resolveExternalSource(
12881299
filename.replace(/^\//, ''),
1300+
) ??
1301+
stylesheetRegistry?.getServedSourcePath(filename) ??
1302+
stylesheetRegistry?.getServedSourcePath(
1303+
filename.replace(/^\//, ''),
12891304
),
12901305
trackedRequestIds:
12911306
stylesheetRegistry?.getRequestIdsForSource(
12921307
stylesheetRegistry?.resolveExternalSource(filename) ??
12931308
stylesheetRegistry?.resolveExternalSource(
12941309
filename.replace(/^\//, ''),
12951310
) ??
1311+
stylesheetRegistry?.getServedSourcePath(filename) ??
1312+
stylesheetRegistry?.getServedSourcePath(
1313+
filename.replace(/^\//, ''),
1314+
) ??
12961315
'',
12971316
) ?? [],
12981317
});
@@ -2053,7 +2072,9 @@ export function isModuleForChangedResource(
20532072
const requestPath = getFilenameFromPath(mod.id);
20542073
const sourcePath =
20552074
stylesheetRegistry?.resolveExternalSource(requestPath) ??
2056-
stylesheetRegistry?.resolveExternalSource(requestPath.replace(/^\//, ''));
2075+
stylesheetRegistry?.resolveExternalSource(requestPath.replace(/^\//, '')) ??
2076+
stylesheetRegistry?.getServedSourcePath(requestPath) ??
2077+
stylesheetRegistry?.getServedSourcePath(requestPath.replace(/^\//, ''));
20572078

20582079
return (
20592080
normalizePath((sourcePath ?? '').split('?')[0]) === normalizedChangedFile
@@ -2102,6 +2123,10 @@ function diagnoseComponentStylesheetPipeline(
21022123
? (stylesheetRegistry?.resolveExternalSource(directRequestPath) ??
21032124
stylesheetRegistry?.resolveExternalSource(
21042125
directRequestPath.replace(/^\//, ''),
2126+
) ??
2127+
stylesheetRegistry?.getServedSourcePath(directRequestPath) ??
2128+
stylesheetRegistry?.getServedSourcePath(
2129+
directRequestPath.replace(/^\//, ''),
21052130
))
21062131
: normalizedFile;
21072132
const registryCode = directRequestPath
@@ -2245,7 +2270,11 @@ export async function findComponentStylesheetWrapperModules(
22452270
: undefined;
22462271
const sourcePath = requestPath
22472272
? (stylesheetRegistry?.resolveExternalSource(requestPath) ??
2248-
stylesheetRegistry?.resolveExternalSource(requestPath.replace(/^\//, '')))
2273+
stylesheetRegistry?.resolveExternalSource(
2274+
requestPath.replace(/^\//, ''),
2275+
) ??
2276+
stylesheetRegistry?.getServedSourcePath(requestPath) ??
2277+
stylesheetRegistry?.getServedSourcePath(requestPath.replace(/^\//, '')))
22492278
: undefined;
22502279

22512280
// HMR timing matters here. On a pure CSS edit, the browser often already has

packages/vite-plugin-angular/src/lib/compilation-api/compilation-api-plugin.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,81 @@ describe('compilationAPIPlugin', () => {
203203
expect(emitAffectedFilesMock).toHaveBeenCalledOnce();
204204
});
205205

206+
it('resolves hashed component stylesheet ids to their source path', async () => {
207+
const containingFile = join(tempRoot, 'src/demo.component.ts');
208+
const resourceFile = join(tempRoot, 'src/demo.component.css');
209+
let stylesheetId = '';
210+
211+
const initializeMock = vi
212+
.fn()
213+
.mockImplementation(async (_tsconfig, host) => {
214+
stylesheetId = await host.transformStylesheet(
215+
'.demo { @apply sa:flex; }',
216+
containingFile,
217+
resourceFile,
218+
0,
219+
'DemoComponent',
220+
);
221+
222+
return {
223+
externalStylesheets: new Map(),
224+
templateUpdates: new Map(),
225+
};
226+
});
227+
const emitAffectedFilesMock = vi.fn().mockResolvedValue([]);
228+
229+
createAngularCompilationMock.mockResolvedValue({
230+
initialize: initializeMock,
231+
update: vi.fn(),
232+
diagnoseFiles: vi.fn().mockResolvedValue({ errors: [], warnings: [] }),
233+
emitAffectedFiles: emitAffectedFilesMock,
234+
});
235+
236+
const { compilationAPIPlugin } =
237+
await import('./compilation-api-plugin.js');
238+
const plugin = compilationAPIPlugin({
239+
tsconfigGetter: () => join(tempRoot, 'tsconfig.json'),
240+
workspaceRoot: tempRoot,
241+
inlineStylesExtension: 'css',
242+
jit: false,
243+
liveReload: false,
244+
disableTypeChecking: true,
245+
supportedBrowsers: ['safari 15'],
246+
fileReplacements: [],
247+
hasTailwindCss: true,
248+
isTest: false,
249+
isAstroIntegration: false,
250+
include: [],
251+
additionalContentDirs: [],
252+
});
253+
254+
await (plugin.config as any)(
255+
{ root: tempRoot, mode: 'development' },
256+
{ command: 'serve', mode: 'development' },
257+
);
258+
await (plugin.configResolved as any)({
259+
cacheDir: join(tempRoot, '.vite'),
260+
root: tempRoot,
261+
mode: 'development',
262+
build: {},
263+
server: { hmr: true },
264+
plugins: [],
265+
});
266+
await (plugin.buildStart as any).call({
267+
addWatchFile: vi.fn(),
268+
error: vi.fn(),
269+
warn: vi.fn(),
270+
});
271+
272+
expect(stylesheetId).toMatch(/^[a-f0-9]+\.css$/);
273+
expect((plugin.resolveId as any)(`/${stylesheetId}?ngcomp=ng-c1&e=0`)).toBe(
274+
`${resourceFile}?ngcomp=ng-c1&e=0`,
275+
);
276+
await expect(
277+
(plugin.load as any)(`${resourceFile}?ngcomp=ng-c1&e=0`),
278+
).resolves.toBe('.demo { @apply sa:flex; }');
279+
});
280+
206281
it('maps templateUpdates to HMR metadata', async () => {
207282
const testFile = join(tempRoot, 'src/app.component.ts');
208283
const initializeMock = vi.fn().mockResolvedValue({

packages/vite-plugin-angular/src/lib/compilation-api/compilation-api-plugin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
debugHmr,
2626
debugHmrV,
2727
debugStyles,
28+
debugStylesV,
2829
type DebugOption,
2930
} from '../utils/debug.js';
3031
import {
@@ -737,6 +738,17 @@ export function compilationAPIPlugin(
737738
// Map angular component stylesheets
738739
if (isComponentStyleSheet(id)) {
739740
const filename = getFilenameFromPath(id);
741+
const search = new URL(id, 'http://localhost').search;
742+
const servedSourcePath =
743+
stylesheetRegistry?.getServedSourcePath(filename);
744+
745+
if (servedSourcePath) {
746+
debugStylesV('resolveId: mapped served stylesheet to source', {
747+
filename,
748+
resolvedPath: servedSourcePath,
749+
});
750+
return servedSourcePath + search;
751+
}
740752

741753
if (stylesheetRegistry?.hasServed(filename)) {
742754
return id;

packages/vite-plugin-angular/src/lib/stylesheet-registry.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ describe('stylesheet-registry', () => {
6161
});
6262

6363
expect(stylesheetId).toMatch(/^[a-f0-9]+\.css$/);
64+
expect(registry.getServedSourcePath(stylesheetId)).toBe(
65+
'/project/src/app/demo.component.css',
66+
);
6467
expect(registry.getServedContent(stylesheetId)).toBe(
6568
'.demo { color: red; }',
6669
);
@@ -73,6 +76,26 @@ describe('stylesheet-registry', () => {
7376
expect(registry.getServedContent('demo.component.css')).toBeUndefined();
7477
});
7578

79+
it('registers inline stylesheet content under a stable synthetic source path', () => {
80+
const registry = new AnalogStylesheetRegistry();
81+
82+
const stylesheetId = registerStylesheetContent(registry, {
83+
code: '.demo { color: red; }',
84+
containingFile: '/project/src/app/demo.component.ts',
85+
className: 'DemoComponent',
86+
order: 0,
87+
inlineStylesExtension: 'css',
88+
});
89+
90+
expect(stylesheetId).toMatch(/^[a-f0-9]+\.css$/);
91+
expect(registry.getServedSourcePath(stylesheetId)).toBe(
92+
'/project/src/app/demo.component.css',
93+
);
94+
expect(
95+
registry.getServedContent('/project/src/app/demo.component.css'),
96+
).toBe('.demo { color: red; }');
97+
});
98+
7699
it('keeps structured transform metadata on the source stylesheet', () => {
77100
const registry = new AnalogStylesheetRegistry();
78101

packages/vite-plugin-angular/src/lib/stylesheet-registry.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export class AnalogStylesheetRegistry {
9595
return this.resolveServedRecord(requestId)?.normalizedCode;
9696
}
9797

98+
getServedSourcePath(requestId: string): string | undefined {
99+
return this.resolveServedRecord(requestId)?.sourcePath;
100+
}
101+
98102
resolveExternalSource(requestId: string): string | undefined {
99103
const normalizedRequestId = this.normalizeRequestId(requestId);
100104
return this.externalRequestToSource.get(normalizedRequestId);
@@ -136,7 +140,9 @@ export class AnalogStylesheetRegistry {
136140
const requestPath = normalizedRequestId.split('?')[0];
137141
const sourcePath =
138142
this.resolveExternalSource(requestPath) ??
139-
this.resolveExternalSource(requestPath.replace(/^\//, ''));
143+
this.resolveExternalSource(requestPath.replace(/^\//, '')) ??
144+
this.getServedSourcePath(requestPath) ??
145+
this.getServedSourcePath(requestPath.replace(/^\//, ''));
140146
if (!sourcePath) {
141147
return;
142148
}
@@ -295,25 +301,23 @@ export function registerStylesheetContent(
295301
.update(code)
296302
.digest('hex');
297303
const stylesheetId = `${id}.${inlineStylesExtension}`;
298-
299-
const aliases: string[] = [];
300-
301-
if (resourceFile) {
302-
const normalizedResourceFile = normalizePath(normalize(resourceFile));
303-
// Avoid basename-only aliases here: shared filenames like `index.css`
304-
// can collide across components and break HMR lookups.
305-
aliases.push(
306-
resourceFile,
307-
normalizedResourceFile,
308-
resourceFile.replace(/^\//, ''),
309-
normalizedResourceFile.replace(/^\//, ''),
310-
);
311-
}
304+
const sourcePath =
305+
resourceFile ?? containingFile.replace('.ts', `.${inlineStylesExtension}`);
306+
const normalizedSourcePath = normalizePath(normalize(sourcePath));
307+
308+
// Avoid basename-only aliases here: shared filenames like `index.css`
309+
// can collide across components and break HMR lookups.
310+
const aliases = [
311+
sourcePath,
312+
normalizedSourcePath,
313+
sourcePath.replace(/^\//, ''),
314+
normalizedSourcePath.replace(/^\//, ''),
315+
];
312316

313317
registry.registerServedStylesheet(
314318
{
315319
publicId: stylesheetId,
316-
sourcePath: resourceFile,
320+
sourcePath: normalizedSourcePath,
317321
normalizedCode: code,
318322
dependencies,
319323
diagnostics,

0 commit comments

Comments
 (0)