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
10 changes: 10 additions & 0 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ interface InternalOptions {
* @default false
*/
disableFullServerManifestGeneration?: boolean;

/**
* Enables the use of AOT compiler emitted external runtime styles.
* External runtime styles use `link` elements instead of embedded style content in the output JavaScript.
* This option is only intended to be used with a development server that can process and serve component
* styles.
*/
externalRuntimeStyles?: boolean;
}

/** Full set of options for `application` builder. */
Expand Down Expand Up @@ -375,6 +383,7 @@ export async function normalizeOptions(
clearScreen,
define,
disableFullServerManifestGeneration = false,
externalRuntimeStyles,
} = options;

// Return all the normalized options
Expand Down Expand Up @@ -436,6 +445,7 @@ export async function normalizeOptions(
clearScreen,
define,
disableFullServerManifestGeneration,
externalRuntimeStyles,
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugi
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
import { createRemoveIdPrefixPlugin } from '../../tools/vite/id-prefix-plugin';
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
import { useComponentStyleHmr } from '../../utils/environment-options';
import { loadEsmModule } from '../../utils/load-esm';
import { Result, ResultFile, ResultKind } from '../application/results';
import {
Expand Down Expand Up @@ -130,6 +131,9 @@ export async function* serveWithVite(
process.setSourceMapsEnabled(true);
}

// TODO: Enable by default once full support across CLI and FW is integrated
browserOptions.externalRuntimeStyles = useComponentStyleHmr;

// Setup the prebundling transformer that will be shared across Vite prebundling requests
const prebundleTransformer = new JavaScriptTransformer(
// Always enable JIT linking to support applications built with and without AOT.
Expand Down
29 changes: 29 additions & 0 deletions packages/angular/build/src/tools/angular/angular-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type ng from '@angular/compiler-cli';
import assert from 'node:assert';
import { createHash } from 'node:crypto';
import nodePath from 'node:path';
import type ts from 'typescript';
Expand All @@ -18,10 +19,12 @@ export interface AngularHostOptions {
fileReplacements?: Record<string, string>;
sourceFileCache?: Map<string, ts.SourceFile>;
modifiedFiles?: Set<string>;
externalStylesheets?: Map<string, string>;
transformStylesheet(
data: string,
containingFile: string,
stylesheetFile?: string,
order?: number,
): Promise<string | null>;
processWebWorker(workerFile: string, containingFile: string): string;
}
Expand Down Expand Up @@ -180,6 +183,11 @@ export function createAngularCompilerHost(
return null;
}

assert(
!context.resourceFile || !hostOptions.externalStylesheets?.has(context.resourceFile),
'External runtime stylesheets should not be transformed: ' + context.resourceFile,
);

// No transformation required if the resource is empty
if (data.trim().length === 0) {
return { content: '' };
Expand All @@ -189,11 +197,32 @@ export function createAngularCompilerHost(
data,
context.containingFile,
context.resourceFile ?? undefined,
// TODO: Remove once available in compiler-cli types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(context as any).order,
);

return typeof result === 'string' ? { content: result } : null;
};

host.resourceNameToFileName = function (resourceName, containingFile) {
const resolvedPath = nodePath.join(nodePath.dirname(containingFile), resourceName);

// All resource names that have HTML file extensions are assumed to be templates
if (resourceName.endsWith('.html') || !hostOptions.externalStylesheets) {
return resolvedPath;
}

// For external stylesheets, create a unique identifier and store the mapping
let externalId = hostOptions.externalStylesheets.get(resolvedPath);
if (externalId === undefined) {
externalId = createHash('sha256').update(resolvedPath).digest('hex');
hostOptions.externalStylesheets.set(resolvedPath, externalId);
}

return externalId + '.css';
};

// Allow the AOT compiler to request the set of changed templates and styles
host.getModifiedResourceFiles = function () {
return hostOptions.modifiedFiles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export abstract class AngularCompilation {
affectedFiles: ReadonlySet<ts.SourceFile>;
compilerOptions: ng.CompilerOptions;
referencedFiles: readonly string[];
externalStylesheets?: ReadonlyMap<string, string>;
}>;

abstract emitAffectedFiles(): Iterable<EmitFileResult> | Promise<Iterable<EmitFileResult>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class AotCompilation extends AngularCompilation {
affectedFiles: ReadonlySet<ts.SourceFile>;
compilerOptions: ng.CompilerOptions;
referencedFiles: readonly string[];
externalStylesheets?: ReadonlyMap<string, string>;
}> {
// Dynamically load the Angular compiler CLI package
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
Expand All @@ -59,6 +60,10 @@ export class AotCompilation extends AngularCompilation {
const compilerOptions =
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;

if (compilerOptions.externalRuntimeStyles) {
hostOptions.externalStylesheets ??= new Map();
}

// Create Angular compiler host
const host = createAngularCompilerHost(ts, compilerOptions, hostOptions);

Expand Down Expand Up @@ -121,7 +126,12 @@ export class AotCompilation extends AngularCompilation {
this.#state?.diagnosticCache,
);

return { affectedFiles, compilerOptions, referencedFiles };
return {
affectedFiles,
compilerOptions,
referencedFiles,
externalStylesheets: hostOptions.externalStylesheets,
};
}

*collectDiagnostics(modes: DiagnosticModes): Iterable<ts.Diagnostic> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class ParallelCompilation extends AngularCompilation {
affectedFiles: ReadonlySet<SourceFile>;
compilerOptions: CompilerOptions;
referencedFiles: readonly string[];
externalStylesheets?: ReadonlyMap<string, string>;
}> {
const stylesheetChannel = new MessageChannel();
// The request identifier is required because Angular can issue multiple concurrent requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function initialize(request: InitRequest) {
}
});

const { compilerOptions, referencedFiles } = await compilation.initialize(
const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize(
request.tsconfig,
{
fileReplacements: request.fileReplacements,
Expand Down Expand Up @@ -93,6 +93,7 @@ export async function initialize(request: InitRequest) {
);

return {
externalStylesheets,
referencedFiles,
// TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
compilerOptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
PluginBuild,
} from 'esbuild';
import assert from 'node:assert';
import { createHash } from 'node:crypto';
import * as path from 'node:path';
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
import { AngularHostOptions } from '../../angular/angular-host';
Expand Down Expand Up @@ -48,6 +49,7 @@ export interface CompilerPluginOptions {
sourceFileCache?: SourceFileCache;
loadResultCache?: LoadResultCache;
incremental: boolean;
externalRuntimeStyles?: boolean;
}

// eslint-disable-next-line max-lines-per-function
Expand Down Expand Up @@ -152,6 +154,7 @@ export function createCompilerPlugin(
// Angular compiler which does not have direct knowledge of transitive resource
// dependencies or web worker processing.
let modifiedFiles;
let invalidatedStylesheetEntries;
if (
pluginOptions.sourceFileCache?.modifiedFiles.size &&
referencedFileTracker &&
Expand All @@ -160,7 +163,7 @@ export function createCompilerPlugin(
// TODO: Differentiate between changed input files and stale output files
modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles);
pluginOptions.sourceFileCache.invalidate(modifiedFiles);
stylesheetBundler.invalidate(modifiedFiles);
invalidatedStylesheetEntries = stylesheetBundler.invalidate(modifiedFiles);
}

if (
Expand All @@ -176,7 +179,7 @@ export function createCompilerPlugin(
fileReplacements: pluginOptions.fileReplacements,
modifiedFiles,
sourceFileCache: pluginOptions.sourceFileCache,
async transformStylesheet(data, containingFile, stylesheetFile) {
async transformStylesheet(data, containingFile, stylesheetFile, order) {
let stylesheetResult;

// Stylesheet file only exists for external stylesheets
Expand All @@ -188,6 +191,16 @@ export function createCompilerPlugin(
containingFile,
// Inline stylesheets from a template style element are always CSS
containingFile.endsWith('.html') ? 'css' : styleOptions.inlineStyleLanguage,
// When external runtime styles are enabled, an identifier for the style that does not change
// based on the content is required to avoid emitted JS code changes. Any JS code changes will
// invalid the output and force a full page reload for HMR cases. The containing file and order
// of the style within the containing file is used.
pluginOptions.externalRuntimeStyles
? createHash('sha-256')
.update(containingFile)
.update((order ?? 0).toString())
.digest('hex')
: undefined,
);
}

Expand Down Expand Up @@ -266,6 +279,7 @@ export function createCompilerPlugin(
// Initialize the Angular compilation for the current build.
// In watch mode, previous build state will be reused.
let referencedFiles;
let externalStylesheets;
try {
const initializationResult = await compilation.initialize(
pluginOptions.tsconfig,
Expand All @@ -280,6 +294,7 @@ export function createCompilerPlugin(
!!initializationResult.compilerOptions.sourceMap ||
!!initializationResult.compilerOptions.inlineSourceMap;
referencedFiles = initializationResult.referencedFiles;
externalStylesheets = initializationResult.externalStylesheets;
} catch (error) {
(result.errors ??= []).push({
text: 'Angular compilation initialization failed.',
Expand All @@ -304,6 +319,32 @@ export function createCompilerPlugin(
return result;
}

if (externalStylesheets) {
// Process any new external stylesheets
for (const [stylesheetFile, externalId] of externalStylesheets) {
await bundleExternalStylesheet(
stylesheetBundler,
stylesheetFile,
externalId,
result,
additionalResults,
);
}
// Process any updated stylesheets
if (invalidatedStylesheetEntries) {
for (const stylesheetFile of invalidatedStylesheetEntries) {
// externalId is already linked in the bundler context so only enabling is required here
await bundleExternalStylesheet(
stylesheetBundler,
stylesheetFile,
true,
result,
additionalResults,
);
}
}
}

// Update TypeScript file output cache for all affected files
try {
await profileAsync('NG_EMIT_TS', async () => {
Expand Down Expand Up @@ -500,6 +541,30 @@ export function createCompilerPlugin(
};
}

async function bundleExternalStylesheet(
stylesheetBundler: ComponentStylesheetBundler,
stylesheetFile: string,
externalId: string | boolean,
result: OnStartResult,
additionalResults: Map<
string,
{ outputFiles?: OutputFile[]; metafile?: Metafile; errors?: PartialMessage[] }
>,
) {
const { outputFiles, metafile, errors, warnings } = await stylesheetBundler.bundleFile(
stylesheetFile,
externalId,
);
if (errors) {
(result.errors ??= []).push(...errors);
}
(result.warnings ??= []).push(...warnings);
additionalResults.set(stylesheetFile, {
outputFiles,
metafile,
});
}

function createCompilerOptionsTransformer(
setupWarnings: PartialMessage[] | undefined,
pluginOptions: CompilerPluginOptions,
Expand Down Expand Up @@ -572,6 +637,7 @@ function createCompilerOptionsTransformer(
mapRoot: undefined,
sourceRoot: undefined,
preserveSymlinks,
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
};
};
}
Expand Down
Loading