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
108 changes: 65 additions & 43 deletions packages/angular/build/src/builders/application/build-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import { BuilderContext } from '@angular-devkit/architect';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import {
BuildOutputAsset,
ExecutionResult,
RebuildState,
} from '../../tools/esbuild/bundler-execution-result';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
Expand Down Expand Up @@ -279,34 +283,15 @@ function* emitOutputResults(

// Use a full result if there is no rebuild state (no prior build result)
if (!rebuildState || !changes) {
const result: FullResult = {
kind: ResultKind.Full,
warnings: warnings as ResultMessage[],
files: {},
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
outputOptions,
},
};
for (const file of assetFiles) {
result.files[file.destination] = {
type: BuildOutputFileType.Browser,
inputPath: file.source,
origin: 'disk',
};
}
for (const file of outputFiles) {
result.files[file.path] = {
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
}

yield result;
yield createFullResult(
outputFiles,
assetFiles,
warnings,
outputOptions,
externalMetadata,
htmlIndexPath,
htmlBaseHref,
);

return;
}
Expand All @@ -326,7 +311,7 @@ function* emitOutputResults(
added: [],
removed: [],
modified: [],
files: {},
files: [],
detail: {
externalMetadata,
htmlIndexPath,
Expand All @@ -340,9 +325,10 @@ function* emitOutputResults(
// Initially assume all previous output files have been removed
const removedOutputFiles = new Map(previousOutputInfo);
for (const file of outputFiles) {
removedOutputFiles.delete(file.path);
const key = `${file.type}:${file.path}`;
removedOutputFiles.delete(key);

const previousHash = previousOutputInfo.get(file.path)?.hash;
const previousHash = previousOutputInfo.get(key)?.hash;
let needFile = false;
if (previousHash === undefined) {
needFile = true;
Expand All @@ -359,12 +345,13 @@ function* emitOutputResults(
incrementalResult.background = false;
}

incrementalResult.files[file.path] = {
incrementalResult.files.push({
path: file.path,
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
});
}
}

Expand All @@ -385,11 +372,12 @@ function* emitOutputResults(

hasCssUpdates ||= destination.endsWith('.css');

incrementalResult.files[destination] = {
incrementalResult.files.push({
path: destination,
type: BuildOutputFileType.Browser,
inputPath: source,
origin: 'disk',
};
});
}

// Do not remove stale files yet if there are template updates.
Expand All @@ -403,12 +391,12 @@ function* emitOutputResults(

// Include the removed output and asset files
incrementalResult.removed.push(
...Array.from(removedOutputFiles, ([file, { type }]) => ({
path: file,
...Array.from(removedOutputFiles.values(), ({ type, path }) => ({
path,
type,
})),
...Array.from(removedAssetFiles.values(), (file) => ({
path: file,
...Array.from(removedAssetFiles.values(), (path) => ({
path,
type: BuildOutputFileType.Browser,
})),
);
Expand All @@ -425,9 +413,7 @@ function* emitOutputResults(
added: incrementalResult.added.filter(isCssFilePath),
removed: incrementalResult.removed.filter(({ path }) => isCssFilePath(path)),
modified: incrementalResult.modified.filter(isCssFilePath),
files: Object.fromEntries(
Object.entries(incrementalResult.files).filter(([path]) => isCssFilePath(path)),
),
files: incrementalResult.files.filter((file) => isCssFilePath(file.path)),
};

yield styleResult;
Expand All @@ -446,6 +432,42 @@ function* emitOutputResults(
}
}

function createFullResult(
outputFiles: readonly BuildOutputFile[],
assetFiles: readonly BuildOutputAsset[],
warnings: readonly unknown[],
outputOptions: NormalizedApplicationBuildOptions['outputOptions'],
externalMetadata: unknown,
htmlIndexPath: unknown,
htmlBaseHref: unknown,
): FullResult {
return {
kind: ResultKind.Full,
warnings: warnings as ResultMessage[],
files: [
...assetFiles.map(({ source, destination }) => ({
path: destination,
type: BuildOutputFileType.Browser,
inputPath: source,
origin: 'disk' as const,
})),
...outputFiles.map((file) => ({
path: file.path,
type: file.type,
contents: file.contents,
origin: 'memory' as const,
hash: file.hash,
})),
],
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
outputOptions,
},
};
}

function isCssFilePath(filePath: string): boolean {
return /\.css(?:\.map)?$/i.test(filePath);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/build/src/builders/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export async function* buildApplication(
// Writes the output files to disk and ensures the containing directories are present
const directoryExists = new Set<string>();
try {
await emitFilesToDisk(Object.entries(result.files), async ([filePath, file]) => {
await emitFilesToDisk(result.files, async (file) => {
if (
outputOptions.ignoreServer &&
(file.type === BuildOutputFileType.ServerApplication ||
Expand All @@ -208,7 +208,7 @@ export async function* buildApplication(
return;
}

const fullFilePath = generateFullPath(filePath, file.type, outputOptions);
const fullFilePath = generateFullPath(file.path, file.type, outputOptions);

// Ensure output subdirectories exist
const fileBasePath = path.dirname(fullFilePath);
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/build/src/builders/application/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface FailureResult extends BaseResult {

export interface FullResult extends BaseResult {
kind: ResultKind.Full;
files: Record<string, ResultFile>;
files: ResultFile[];
}

export interface IncrementalResult extends BaseResult {
Expand All @@ -40,13 +40,14 @@ export interface IncrementalResult extends BaseResult {
added: string[];
removed: { path: string; type: BuildOutputFileType }[];
modified: string[];
files: Record<string, ResultFile>;
files: ResultFile[];
}

export type ResultFile = DiskFile | MemoryFile;

export interface BaseResultFile {
origin: 'memory' | 'disk';
path: string;
type: BuildOutputFileType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
name: 'app',
installMode: 'prefetch',
resources: {
files: ['/favicon.ico', '/index.html'],
files: ['/favicon.ico', '/index.html', '/*.css', '/*.js'],
},
},
{
Expand Down Expand Up @@ -88,5 +88,40 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
const config = await harness.readFile('dist/browser/ngsw.json');
expect(JSON.parse(config)).toEqual(jasmine.objectContaining({ index: '/index.csr.html' }));
});

it('should write JS-imported CSS chunk to browser dist when SSR is enabled', async () => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.files ??= [];
tsConfig.files.push('main.server.ts', 'server.ts', 'extra.d.ts');

return JSON.stringify(tsConfig);
});

await harness.writeFile('src/extra.d.ts', `declare module '*.css';`);
await harness.writeFile('src/server.ts', `console.log('Server!');`);
await harness.writeFile('src/extra.css', `body { color: red; }`);
await harness.modifyFile('src/main.ts', (content) => `import './extra.css';\n${content}`);

harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: { entry: 'src/server.ts' },
serviceWorker: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

const config = JSON.parse(harness.readFile('dist/browser/ngsw.json'));
const hashTable = config.hashTable as Record<string, string>;

const cssChunkUrls = Object.keys(hashTable).filter((url) => url.endsWith('.css'));
expect(cssChunkUrls.length).toBeGreaterThan(0);

for (const url of Object.keys(hashTable)) {
harness.expectFile(`dist/browser${url}`).toExist();
}
});
});
});
20 changes: 3 additions & 17 deletions packages/angular/build/src/builders/dev-server/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,8 @@ export async function* serveWithVite(
componentStyles.clear();
generatedFiles.clear();

for (const [outputPath, file] of Object.entries(result.files)) {
for (const file of result.files) {
updateResultRecord(
outputPath,
file,
normalizePath,
htmlIndexPath,
Expand Down Expand Up @@ -292,22 +291,9 @@ export async function* serveWithVite(
assetFiles.delete(filePath);
}

for (const modified of result.modified) {
for (const file of result.files) {
updateResultRecord(
modified,
result.files[modified],
normalizePath,
htmlIndexPath,
generatedFiles,
assetFiles,
componentStyles,
);
}

for (const added of result.added) {
updateResultRecord(
added,
result.files[added],
file,
normalizePath,
htmlIndexPath,
generatedFiles,
Expand Down
13 changes: 9 additions & 4 deletions packages/angular/build/src/builders/dev-server/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export interface DevServerExternalResultMetadata extends Omit<ExternalResultMeta
}

export function updateResultRecord(
outputPath: string,
file: ResultFile,
normalizePath: (id: string) => string,
htmlIndexPath: string,
Expand All @@ -40,7 +39,7 @@ export function updateResultRecord(
initial = false,
): void {
if (file.origin === 'disk') {
assetFiles.set('/' + normalizePath(outputPath), {
assetFiles.set('/' + normalizePath(file.path), {
source: normalizePath(file.inputPath),
updated: !initial,
});
Expand All @@ -49,12 +48,12 @@ export function updateResultRecord(
}

let filePath;
if (outputPath === htmlIndexPath) {
if (file.path === htmlIndexPath) {
// Convert custom index output path to standard index path for dev-server usage.
// This mimics the Webpack dev-server behavior.
filePath = '/index.html';
} else {
filePath = '/' + normalizePath(outputPath);
filePath = '/' + normalizePath(file.path);
}

const servable =
Expand All @@ -74,6 +73,12 @@ export function updateResultRecord(
return;
}

// Avoid overwriting a servable browser file with a non-servable server file of the same path (e.g. CSS chunks)
const existing = generatedFiles.get(filePath);
if (existing?.servable && !servable) {
return;
}

// New or updated file
generatedFiles.set(filePath, {
contents: file.contents,
Expand Down
Loading