Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/angular/build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ts_library(
"//packages/angular_devkit/architect",
"@npm//@ampproject/remapping",
"@npm//@angular/common",
"@npm//@angular/compiler",
"@npm//@angular/compiler-cli",
"@npm//@angular/core",
"@npm//@angular/localize",
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"watchpack": "2.4.2"
},
"peerDependencies": {
"@angular/compiler": "^19.0.0-next.0",
"@angular/compiler-cli": "^19.0.0-next.0",
"@angular/localize": "^19.0.0-next.0",
"@angular/platform-server": "^19.0.0-next.0",
Expand Down
31 changes: 29 additions & 2 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export async function* serveWithVite(
implicitServer: [],
explicit: [],
};
const usedComponentStyles = new Map<string, string[]>();

// Add cleanup logic via a builder teardown.
let deferred: () => void;
Expand Down Expand Up @@ -262,7 +263,14 @@ export async function* serveWithVite(
// This is a workaround for: https://github.com/vitejs/vite/issues/14896
await server.restart();
} else {
await handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger);
await handleUpdate(
normalizePath,
generatedFiles,
server,
serverOptions,
context.logger,
usedComponentStyles,
);
}
} else {
const projectName = context.target?.project;
Expand Down Expand Up @@ -302,6 +310,7 @@ export async function* serveWithVite(
prebundleTransformer,
target,
isZonelessApp(polyfills),
usedComponentStyles,
browserOptions.loader as EsbuildLoaderOption | undefined,
extensions?.middleware,
transformers?.indexHtml,
Expand Down Expand Up @@ -359,6 +368,7 @@ async function handleUpdate(
server: ViteDevServer,
serverOptions: NormalizedDevServerOptions,
logger: BuilderContext['logger'],
usedComponentStyles: Map<string, string[]>,
): Promise<void> {
const updatedFiles: string[] = [];
let isServerFileUpdated = false;
Expand Down Expand Up @@ -394,7 +404,22 @@ async function handleUpdate(
const timestamp = Date.now();
server.hot.send({
type: 'update',
updates: updatedFiles.map((filePath) => {
updates: updatedFiles.flatMap((filePath) => {
// For component styles, an HMR update must be sent for each one with the corresponding
// component identifier search parameter (`ngcomp`). The Vite client code will not keep
// the existing search parameters when it performs an update and each one must be
// specified explicitly. Typically, there is only one each though as specific style files
// are not typically reused across components.
const componentIds = usedComponentStyles.get(filePath);
if (componentIds) {
return componentIds.map((id) => ({
type: 'css-update',
timestamp,
path: `${filePath}?ngcomp` + (id ? `=${id}` : ''),
acceptedPath: filePath,
}));
}

return {
type: 'css-update',
timestamp,
Expand Down Expand Up @@ -499,6 +524,7 @@ export async function setupServer(
prebundleTransformer: JavaScriptTransformer,
target: string[],
zoneless: boolean,
usedComponentStyles: Map<string, string[]>,
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
extensionMiddleware?: Connect.NextHandleFunction[],
indexHtmlTransformer?: (content: string) => Promise<string>,
Expand Down Expand Up @@ -607,6 +633,7 @@ export async function setupServer(
indexHtmlTransformer,
extensionMiddleware,
normalizePath,
usedComponentStyles,
}),
createRemoveIdPrefixPlugin(externalMetadata.explicit),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface AngularMemoryPluginOptions {
extensionMiddleware?: Connect.NextHandleFunction[];
indexHtmlTransformer?: (content: string) => Promise<string>;
normalizePath: (path: string) => string;
usedComponentStyles: Map<string, string[]>;
}

export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Plugin {
Expand All @@ -42,6 +43,7 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
extensionMiddleware,
indexHtmlTransformer,
normalizePath,
usedComponentStyles,
} = options;

return {
Expand Down Expand Up @@ -113,7 +115,9 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
};

// Assets and resources get handled first
server.middlewares.use(createAngularAssetsMiddleware(server, assets, outputFiles));
server.middlewares.use(
createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles),
);

if (extensionMiddleware?.length) {
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
import { lookup as lookupMimeType } from 'mrmime';
import { extname } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
import { loadEsmModule } from '../../../utils/load-esm';
import {
AngularMemoryOutputFiles,
appendServerConfiguredHeaders,
pathnameWithoutBasePath,
} from '../utils';

const COMPONENT_REGEX = /%COMP%/g;

export function createAngularAssetsMiddleware(
server: ViteDevServer,
assets: Map<string, string>,
outputFiles: AngularMemoryOutputFiles,
usedComponentStyles: Map<string, string[]>,
): Connect.NextHandleFunction {
return function (req, res, next) {
if (req.url === undefined || res.writableEnded) {
Expand Down Expand Up @@ -69,13 +73,51 @@ export function createAngularAssetsMiddleware(
if (extension !== '.js' && extension !== '.html') {
const outputFile = outputFiles.get(pathname);
if (outputFile?.servable) {
const data = outputFile.contents;
if (extension === '.css') {
// Inject component ID for view encapsulation if requested
const componentId = new URL(req.url, 'http://localhost').searchParams.get('ngcomp');
if (componentId !== null) {
// Record the component style usage for HMR updates
const usedIds = usedComponentStyles.get(pathname);
if (usedIds === undefined) {
usedComponentStyles.set(pathname, [componentId]);
} else {
usedIds.push(componentId);
}
// Shim the stylesheet if a component ID is provided
if (componentId.length > 0) {
// Validate component ID
if (/[_.-A-Za-z0-9]+-c\d{9}$/.test(componentId)) {
loadEsmModule<typeof import('@angular/compiler')>('@angular/compiler')
.then((compilerModule) => {
const encapsulatedData = compilerModule
.encapsulateStyle(new TextDecoder().decode(data))
.replaceAll(COMPONENT_REGEX, componentId);

res.setHeader('Content-Type', 'text/css');
res.setHeader('Cache-Control', 'no-cache');
appendServerConfiguredHeaders(server, res);
res.end(encapsulatedData);
})
.catch((e) => next(e));

return;
} else {
// eslint-disable-next-line no-console
console.error('Invalid component stylesheet ID request: ' + componentId);
}
}
}
}

const mimeType = lookupMimeType(extension);
if (mimeType) {
res.setHeader('Content-Type', mimeType);
}
res.setHeader('Cache-Control', 'no-cache');
appendServerConfiguredHeaders(server, res);
res.end(outputFile.contents);
res.end(data);

return;
}
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ __metadata:
vite: "npm:5.4.3"
watchpack: "npm:2.4.2"
peerDependencies:
"@angular/compiler": ^19.0.0-next.0
"@angular/compiler-cli": ^19.0.0-next.0
"@angular/localize": ^19.0.0-next.0
"@angular/platform-server": ^19.0.0-next.0
Expand Down