Skip to content

Commit

Permalink
refactor(@angular/build): support application incremental build resul…
Browse files Browse the repository at this point in the history
…t in dev-server

The dev-server will now leverage the incremental build result data from the application
builder. This removes the need to directly analyze all the newly built files within the
dev-server to determine what type of update is needed. Incremental build results also
only contain the files that are new and/or modified and removes the need to pass a potentially
large amount of file content between the application build and the dev-server.
  • Loading branch information
clydin committed Jan 6, 2025
1 parent ddae37f commit 0581c45
Showing 1 changed file with 92 additions and 76 deletions.
168 changes: 92 additions & 76 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export async function* serveWithVite(
browserOptions.templateUpdates =
serverOptions.liveReload && serverOptions.hmr && useComponentTemplateHmr;

browserOptions.incrementalResults = true;

// 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 Expand Up @@ -225,10 +227,25 @@ export async function* serveWithVite(
}

assetFiles.clear();
for (const [outputPath, file] of Object.entries(result.files)) {
componentStyles.clear();
generatedFiles.clear();
for (const entry of Object.entries(result.files)) {
const [outputPath, file] = entry;
if (file.origin === 'disk') {
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
continue;
}

updateResultRecord(
outputPath,
file,
normalizePath,
htmlIndexPath,
generatedFiles,
componentStyles,
// The initial build will not yet have a server setup
!server,
);
}

// Invalidate SSR module graph to ensure that only new rebuild is used and not stale component updates
Expand All @@ -239,18 +256,36 @@ export async function* serveWithVite(
// Clear stale template updates on code rebuilds
templateUpdates.clear();

// Analyze result files for changes
analyzeResultFiles(
normalizePath,
htmlIndexPath,
result.files,
generatedFiles,
componentStyles,
);
break;
case ResultKind.Incremental:
assert(server, 'Builder must provide an initial full build before incremental results.');
// TODO: Implement support -- application builder currently does not use

for (const removed of result.removed) {
const filePath = '/' + normalizePath(removed.path);
generatedFiles.delete(filePath);
assetFiles.delete(filePath);
}
for (const modified of result.modified) {
updateResultRecord(
modified,
result.files[modified],
normalizePath,
htmlIndexPath,
generatedFiles,
componentStyles,
);
}
for (const added of result.added) {
updateResultRecord(
added,
result.files[added],
normalizePath,
htmlIndexPath,
generatedFiles,
componentStyles,
);
}

break;
case ResultKind.ComponentUpdate:
assert(serverOptions.hmr, 'Component updates are only supported with HMR enabled.');
Expand Down Expand Up @@ -444,12 +479,13 @@ async function handleUpdate(
let destroyAngularServerAppCalled = false;

// Invalidate any updated files
for (const [file, { updated, type }] of generatedFiles) {
if (!updated) {
for (const [file, record] of generatedFiles) {
if (!record.updated) {
continue;
}
record.updated = false;

if (type === BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
if (record.type === BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
// Clear the server app cache
// This must be done before module invalidation.
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
Expand Down Expand Up @@ -541,85 +577,65 @@ async function handleUpdate(
}
}

function analyzeResultFiles(
function updateResultRecord(
outputPath: string,
file: ResultFile,
normalizePath: (id: string) => string,
htmlIndexPath: string,
resultFiles: Record<string, ResultFile>,
generatedFiles: Map<string, OutputFileRecord>,
componentStyles: Map<string, ComponentStyleRecord>,
) {
const seen = new Set<string>(['/index.html']);
for (const [outputPath, file] of Object.entries(resultFiles)) {
if (file.origin === 'disk') {
continue;
}
let filePath;
if (outputPath === 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);
}

seen.add(filePath);

const servable =
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media;

// Skip analysis of sourcemaps
if (filePath.endsWith('.map')) {
generatedFiles.set(filePath, {
contents: file.contents,
servable,
size: file.contents.byteLength,
hash: file.hash,
type: file.type,
updated: false,
});
initial = false,
): void {
if (file.origin === 'disk') {
return;
}

continue;
}
let filePath;
if (outputPath === 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);
}

const existingRecord = generatedFiles.get(filePath);
if (
existingRecord &&
existingRecord.size === file.contents.byteLength &&
existingRecord.hash === file.hash
) {
// Same file
existingRecord.updated = false;
continue;
}
const servable =
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media;

// New or updated file
// Skip analysis of sourcemaps
if (filePath.endsWith('.map')) {
generatedFiles.set(filePath, {
contents: file.contents,
servable,
size: file.contents.byteLength,
hash: file.hash,
updated: true,
type: file.type,
servable,
updated: false,
});

// Record any external component styles
if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
const componentStyle = componentStyles.get(filePath);
if (componentStyle) {
componentStyle.rawContent = file.contents;
} else {
componentStyles.set(filePath, {
rawContent: file.contents,
});
}
}
return;
}

// Clear stale output files
for (const file of generatedFiles.keys()) {
if (!seen.has(file)) {
generatedFiles.delete(file);
componentStyles.delete(file);
// New or updated file
generatedFiles.set(filePath, {
contents: file.contents,
size: file.contents.byteLength,
hash: file.hash,
// Consider the files updated except on the initial build result
updated: !initial,
type: file.type,
servable,
});

// Record any external component styles
if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
const componentStyle = componentStyles.get(filePath);
if (componentStyle) {
componentStyle.rawContent = file.contents;
} else {
componentStyles.set(filePath, {
rawContent: file.contents,
});
}
}
}
Expand Down

0 comments on commit 0581c45

Please sign in to comment.