Skip to content

Commit

Permalink
refactor(@angular/build): support dev server direct component style s…
Browse files Browse the repository at this point in the history
…erving

The Vite-based development server now provides support for serving individual
component stylesheets both with and without emulated view encapsulation. This
capability is not yet used by the Angular runtime code. The ability to use
external stylesheets instead of bundling the style content is an enabling
capability primarily for automatic component style HMR features. Additionally,
it has potential future benefits for development mode deferred style processing
which may reduce the initial build time when using the development server. The
application build itself also does not yet generate external stylesheets.
  • Loading branch information
clydin authored and alan-agius4 committed Sep 12, 2024
1 parent 5bba61d commit 434979a
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 4 deletions.
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 @@ -151,6 +151,7 @@ export async function* serveWithVite(
explicitBrowser: [],
explicitServer: [],
};
const usedComponentStyles = new Map<string, string[]>();

// Add cleanup logic via a builder teardown.
let deferred: () => void;
Expand Down Expand Up @@ -271,7 +272,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 @@ -311,6 +319,7 @@ export async function* serveWithVite(
prebundleTransformer,
target,
isZonelessApp(polyfills),
usedComponentStyles,
browserOptions.loader as EsbuildLoaderOption | undefined,
extensions?.middleware,
transformers?.indexHtml,
Expand Down Expand Up @@ -368,6 +377,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 @@ -403,7 +413,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 @@ -508,6 +533,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 @@ -616,6 +642,7 @@ export async function setupServer(
indexHtmlTransformer,
extensionMiddleware,
normalizePath,
usedComponentStyles,
}),
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
],
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.4"
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

0 comments on commit 434979a

Please sign in to comment.