mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-20 05:24:57 +08:00
refactor(@angular-devkit/build-angular): improve accuracy of programmatic watch mode usage for esbuild builders
To better capture file changes after the initial build for the esbuild-based builders in a programmatic usage, the file watching initialization has been moved to before the first build results are yielded. This allows tests that execute code to change files with improved accuracy of the watch mode triggering. The application builder now also supports aborting the watch mode programmatically. This allows tests to gracefully stop the watch mode and more fully cleanup the test at completion.
This commit is contained in:
parent
03a1eaf01c
commit
9c4a6be2d1
@ -30,6 +30,7 @@ export async function* runEsBuildBuildAction(
|
|||||||
progress?: boolean;
|
progress?: boolean;
|
||||||
deleteOutputPath?: boolean;
|
deleteOutputPath?: boolean;
|
||||||
poll?: number;
|
poll?: number;
|
||||||
|
signal?: AbortSignal;
|
||||||
},
|
},
|
||||||
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
|
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
|
||||||
const {
|
const {
|
||||||
@ -75,22 +76,6 @@ export async function* runEsBuildBuildAction(
|
|||||||
let result: ExecutionResult;
|
let result: ExecutionResult;
|
||||||
try {
|
try {
|
||||||
result = await withProgress('Building...', () => action());
|
result = await withProgress('Building...', () => action());
|
||||||
|
|
||||||
if (writeToFileSystem) {
|
|
||||||
// Write output files
|
|
||||||
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
|
|
||||||
|
|
||||||
yield result.output;
|
|
||||||
} else {
|
|
||||||
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
yield result.outputWithFiles as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finish if watch mode is not enabled
|
|
||||||
if (!watch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
// Ensure Sass workers are shutdown if not watching
|
// Ensure Sass workers are shutdown if not watching
|
||||||
if (!watch) {
|
if (!watch) {
|
||||||
@ -98,13 +83,16 @@ export async function* runEsBuildBuildAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup watcher if watch mode enabled
|
||||||
|
let watcher: import('../../tools/esbuild/watcher').BuildWatcher | undefined;
|
||||||
|
if (watch) {
|
||||||
if (progress) {
|
if (progress) {
|
||||||
logger.info('Watch mode enabled. Watching for file changes...');
|
logger.info('Watch mode enabled. Watching for file changes...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup a watcher
|
// Setup a watcher
|
||||||
const { createWatcher } = await import('../../tools/esbuild/watcher');
|
const { createWatcher } = await import('../../tools/esbuild/watcher');
|
||||||
const watcher = createWatcher({
|
watcher = createWatcher({
|
||||||
polling: typeof poll === 'number',
|
polling: typeof poll === 'number',
|
||||||
interval: poll,
|
interval: poll,
|
||||||
ignored: [
|
ignored: [
|
||||||
@ -118,6 +106,9 @@ export async function* runEsBuildBuildAction(
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup abort support
|
||||||
|
options.signal?.addEventListener('abort', () => void watcher?.close());
|
||||||
|
|
||||||
// Temporarily watch the entire project
|
// Temporarily watch the entire project
|
||||||
watcher.add(projectRoot);
|
watcher.add(projectRoot);
|
||||||
|
|
||||||
@ -138,12 +129,36 @@ export async function* runEsBuildBuildAction(
|
|||||||
watcher.add(packageWatchFiles.map((file) => path.join(workspaceRoot, file)));
|
watcher.add(packageWatchFiles.map((file) => path.join(workspaceRoot, file)));
|
||||||
|
|
||||||
// Watch locations provided by the initial build result
|
// Watch locations provided by the initial build result
|
||||||
let previousWatchFiles = new Set(result.watchFiles);
|
|
||||||
watcher.add(result.watchFiles);
|
watcher.add(result.watchFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the first build results after setting up the watcher to ensure that any code executed
|
||||||
|
// higher in the iterator call stack will trigger the watcher. This is particularly relevant for
|
||||||
|
// unit tests which execute the builder and modify the file system programmatically.
|
||||||
|
if (writeToFileSystem) {
|
||||||
|
// Write output files
|
||||||
|
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
|
||||||
|
|
||||||
|
yield result.output;
|
||||||
|
} else {
|
||||||
|
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
yield result.outputWithFiles as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish if watch mode is not enabled
|
||||||
|
if (!watcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for changes and rebuild as needed
|
// Wait for changes and rebuild as needed
|
||||||
|
let previousWatchFiles = new Set(result.watchFiles);
|
||||||
try {
|
try {
|
||||||
for await (const changes of watcher) {
|
for await (const changes of watcher) {
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
logger.info(changes.toDebugString());
|
logger.info(changes.toDebugString());
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ import { Schema as ApplicationBuilderOptions } from './schema';
|
|||||||
|
|
||||||
export async function* buildApplicationInternal(
|
export async function* buildApplicationInternal(
|
||||||
options: ApplicationBuilderInternalOptions,
|
options: ApplicationBuilderInternalOptions,
|
||||||
context: BuilderContext,
|
// TODO: Integrate abort signal support into builder system
|
||||||
|
context: BuilderContext & { signal?: AbortSignal },
|
||||||
infrastructureSettings?: {
|
infrastructureSettings?: {
|
||||||
write?: boolean;
|
write?: boolean;
|
||||||
},
|
},
|
||||||
@ -73,6 +74,7 @@ export async function* buildApplicationInternal(
|
|||||||
progress: normalizedOptions.progress,
|
progress: normalizedOptions.progress,
|
||||||
writeToFileSystem: infrastructureSettings?.write,
|
writeToFileSystem: infrastructureSettings?.write,
|
||||||
logger: context.logger,
|
logger: context.logger,
|
||||||
|
signal: context.signal,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
|||||||
harness.expectFile('dist/main.js').content.toContain('color: indianred');
|
harness.expectFile('dist/main.js').content.toContain('color: indianred');
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('updates produced stylesheet in watch mode', async () => {
|
it('updates produced stylesheet in watch mode', async () => {
|
||||||
harness.useTarget('build', {
|
harness.useTarget('build', {
|
||||||
...BASE_OPTIONS,
|
...BASE_OPTIONS,
|
||||||
inlineStyleLanguage: InlineStyleLanguage.Scss,
|
inlineStyleLanguage: InlineStyleLanguage.Scss,
|
||||||
@ -87,8 +87,9 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
|||||||
content.replace('__STYLE_MARKER__', '$primary: indianred;\\nh1 { color: $primary; }'),
|
content.replace('__STYLE_MARKER__', '$primary: indianred;\\nh1 { color: $primary; }'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const builderAbort = new AbortController();
|
||||||
const buildCount = await harness
|
const buildCount = await harness
|
||||||
.execute()
|
.execute({ signal: builderAbort.signal })
|
||||||
.pipe(
|
.pipe(
|
||||||
timeout(30000),
|
timeout(30000),
|
||||||
concatMap(async ({ result }, index) => {
|
concatMap(async ({ result }, index) => {
|
||||||
@ -121,10 +122,12 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
|||||||
harness.expectFile('dist/main.js').content.not.toContain('color: indianred');
|
harness.expectFile('dist/main.js').content.not.toContain('color: indianred');
|
||||||
harness.expectFile('dist/main.js').content.not.toContain('color: aqua');
|
harness.expectFile('dist/main.js').content.not.toContain('color: aqua');
|
||||||
harness.expectFile('dist/main.js').content.toContain('color: blue');
|
harness.expectFile('dist/main.js').content.toContain('color: blue');
|
||||||
|
|
||||||
|
// Test complete - abort watch mode
|
||||||
|
builderAbort.abort();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
take(3),
|
|
||||||
count(),
|
count(),
|
||||||
)
|
)
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
@ -28,4 +28,8 @@ export const BASE_OPTIONS = Object.freeze<Schema>({
|
|||||||
|
|
||||||
// Disable optimizations
|
// Disable optimizations
|
||||||
optimization: false,
|
optimization: false,
|
||||||
|
|
||||||
|
// Enable polling (if a test enables watch mode).
|
||||||
|
// This is a workaround for bazel isolation file watch not triggering in tests.
|
||||||
|
poll: 100,
|
||||||
});
|
});
|
||||||
|
@ -51,6 +51,7 @@ export interface BuilderHarnessExecutionOptions {
|
|||||||
outputLogsOnFailure: boolean;
|
outputLogsOnFailure: boolean;
|
||||||
outputLogsOnException: boolean;
|
outputLogsOnException: boolean;
|
||||||
useNativeFileWatching: boolean;
|
useNativeFileWatching: boolean;
|
||||||
|
signal: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -235,6 +236,7 @@ export class BuilderHarness<T> {
|
|||||||
this.builderInfo,
|
this.builderInfo,
|
||||||
this.resolvePath('.'),
|
this.resolvePath('.'),
|
||||||
contextHost,
|
contextHost,
|
||||||
|
options.signal,
|
||||||
useNativeFileWatching ? undefined : this.watcherNotifier,
|
useNativeFileWatching ? undefined : this.watcherNotifier,
|
||||||
);
|
);
|
||||||
if (this.targetName !== undefined) {
|
if (this.targetName !== undefined) {
|
||||||
@ -389,6 +391,7 @@ class HarnessBuilderContext implements BuilderContext {
|
|||||||
public builder: BuilderInfo,
|
public builder: BuilderInfo,
|
||||||
basePath: string,
|
basePath: string,
|
||||||
private readonly contextHost: ContextHost,
|
private readonly contextHost: ContextHost,
|
||||||
|
public readonly signal: AbortSignal | undefined,
|
||||||
public readonly watcherFactory: BuilderWatcherFactory | undefined,
|
public readonly watcherFactory: BuilderWatcherFactory | undefined,
|
||||||
) {
|
) {
|
||||||
this.workspaceRoot = this.currentDirectory = basePath;
|
this.workspaceRoot = this.currentDirectory = basePath;
|
||||||
@ -442,6 +445,7 @@ class HarnessBuilderContext implements BuilderContext {
|
|||||||
info,
|
info,
|
||||||
this.workspaceRoot,
|
this.workspaceRoot,
|
||||||
this.contextHost,
|
this.contextHost,
|
||||||
|
this.signal,
|
||||||
this.watcherFactory,
|
this.watcherFactory,
|
||||||
);
|
);
|
||||||
context.target = target;
|
context.target = target;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user