Skip to content

Commit 1e37b59

Browse files
committed
fix(@angular-devkit/build-angular): serve assets
(cherry picked from commit 1278112)
1 parent 43e7aae commit 1e37b59

File tree

2 files changed

+92
-8
lines changed

2 files changed

+92
-8
lines changed

packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts

+90-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
1919
import { randomUUID } from 'crypto';
2020
import glob from 'fast-glob';
2121
import * as fs from 'fs/promises';
22+
import { IncomingMessage, ServerResponse } from 'http';
2223
import type { Config, ConfigOptions, InlinePluginDef } from 'karma';
2324
import * as path from 'path';
2425
import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap } from 'rxjs';
@@ -40,6 +41,71 @@ class ApplicationBuildError extends Error {
4041
}
4142
}
4243

44+
interface ServeFileFunction {
45+
(
46+
filepath: string,
47+
rangeHeader: string | string[] | undefined,
48+
response: ServerResponse,
49+
transform?: (c: string | Uint8Array) => string | Uint8Array,
50+
content?: string | Uint8Array,
51+
doNotCache?: boolean,
52+
): void;
53+
}
54+
55+
interface LatestBuildFiles {
56+
files: Record<string, ResultFile | undefined>;
57+
}
58+
59+
const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles';
60+
61+
class AngularAssetsMiddleware {
62+
static readonly $inject = ['serveFile', LATEST_BUILD_FILES_TOKEN];
63+
64+
static readonly NAME = 'angular-test-assets';
65+
66+
constructor(
67+
private readonly serveFile: ServeFileFunction,
68+
private readonly latestBuildFiles: LatestBuildFiles,
69+
) {}
70+
71+
handle(req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => unknown) {
72+
let err = null;
73+
try {
74+
const url = new URL(`http://${req.headers['host']}${req.url}`);
75+
const file = this.latestBuildFiles.files[url.pathname.slice(1)];
76+
77+
if (file?.origin === 'disk') {
78+
this.serveFile(file.inputPath, undefined, res);
79+
80+
return;
81+
} else if (file?.origin === 'memory') {
82+
// Include pathname to help with Content-Type headers.
83+
this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents, true);
84+
85+
return;
86+
}
87+
} catch (e) {
88+
err = e;
89+
}
90+
next(err);
91+
}
92+
93+
static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef {
94+
return {
95+
[LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }],
96+
97+
[`middleware:${AngularAssetsMiddleware.NAME}`]: [
98+
'factory',
99+
Object.assign((...args: ConstructorParameters<typeof AngularAssetsMiddleware>) => {
100+
const inst = new AngularAssetsMiddleware(...args);
101+
102+
return inst.handle.bind(inst);
103+
}, AngularAssetsMiddleware),
104+
],
105+
};
106+
}
107+
}
108+
43109
function injectKarmaReporter(
44110
context: BuilderContext,
45111
buildOptions: BuildOptions,
@@ -58,9 +124,12 @@ function injectKarmaReporter(
58124
}
59125

60126
class ProgressNotifierReporter {
61-
static $inject = ['emitter'];
127+
static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN];
62128

63-
constructor(private readonly emitter: KarmaEmitter) {
129+
constructor(
130+
private readonly emitter: KarmaEmitter,
131+
private readonly latestBuildFiles: LatestBuildFiles,
132+
) {
64133
this.startWatchingBuild();
65134
}
66135

@@ -81,6 +150,14 @@ function injectKarmaReporter(
81150
buildOutput.kind === ResultKind.Incremental ||
82151
buildOutput.kind === ResultKind.Full
83152
) {
153+
if (buildOutput.kind === ResultKind.Full) {
154+
this.latestBuildFiles.files = buildOutput.files;
155+
} else {
156+
this.latestBuildFiles.files = {
157+
...this.latestBuildFiles.files,
158+
...buildOutput.files,
159+
};
160+
}
84161
await writeTestFiles(buildOutput.files, buildOptions.outputPath);
85162
this.emitter.refreshFiles();
86163
}
@@ -237,6 +314,7 @@ async function initializeApplication(
237314
: undefined;
238315

239316
const buildOptions: BuildOptions = {
317+
assets: options.assets,
240318
entryPoints,
241319
tsConfig: options.tsConfig,
242320
outputPath,
@@ -293,7 +371,6 @@ async function initializeApplication(
293371
},
294372
);
295373
}
296-
297374
karmaOptions.files.push(
298375
// Serve remaining JS on page load, these are the test entrypoints.
299376
{ pattern: `${outputPath}/*.js`, type: 'module', watched: false },
@@ -313,8 +390,9 @@ async function initializeApplication(
313390
// Remove the webpack plugin/framework:
314391
// Alternative would be to make the Karma plugin "smart" but that's a tall order
315392
// with managing unneeded imports etc..
316-
const pluginLengthBefore = (parsedKarmaConfig.plugins ?? []).length;
317-
parsedKarmaConfig.plugins = (parsedKarmaConfig.plugins ?? []).filter(
393+
parsedKarmaConfig.plugins ??= [];
394+
const pluginLengthBefore = parsedKarmaConfig.plugins.length;
395+
parsedKarmaConfig.plugins = parsedKarmaConfig.plugins.filter(
318396
(plugin: string | InlinePluginDef) => {
319397
if (typeof plugin === 'string') {
320398
return plugin !== 'framework:@angular-devkit/build-angular';
@@ -323,16 +401,21 @@ async function initializeApplication(
323401
return !plugin['framework:@angular-devkit/build-angular'];
324402
},
325403
);
326-
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks?.filter(
404+
parsedKarmaConfig.frameworks ??= [];
405+
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks.filter(
327406
(framework: string) => framework !== '@angular-devkit/build-angular',
328407
);
329-
const pluginLengthAfter = (parsedKarmaConfig.plugins ?? []).length;
408+
const pluginLengthAfter = parsedKarmaConfig.plugins.length;
330409
if (pluginLengthBefore !== pluginLengthAfter) {
331410
context.logger.warn(
332411
`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.`,
333412
);
334413
}
335414

415+
parsedKarmaConfig.plugins.push(AngularAssetsMiddleware.createPlugin(buildOutput));
416+
parsedKarmaConfig.middleware ??= [];
417+
parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME);
418+
336419
// When using code-coverage, auto-add karma-coverage.
337420
// This was done as part of the karma plugin for webpack.
338421
if (

packages/angular_devkit/build_angular/src/builders/karma/tests/options/assets_spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
7474
declarations: [AppComponent]
7575
}));
7676
77-
it('should create the app', () => {
77+
it('should create the app', async () => {
7878
const fixture = TestBed.createComponent(AppComponent);
79+
await fixture.whenStable();
7980
const app = fixture.debugElement.componentInstance;
8081
expect(app).toBeTruthy();
8182
});

0 commit comments

Comments
 (0)