Skip to content

Commit c39ef3e

Browse files
committed
fix(@angular-devkit/build-angular): serve assets
1 parent e230d47 commit c39ef3e

File tree

3 files changed

+93
-10
lines changed

3 files changed

+93
-10
lines changed

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

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ExecutionTransformer } from '../../transforms';
2727
import { OutputHashing } from '../browser-esbuild/schema';
2828
import { findTests } from './find-tests';
2929
import { Schema as KarmaBuilderOptions } from './schema';
30+
import { IncomingMessage, ServerResponse } from 'http';
3031

3132
interface BuildOptions extends ApplicationBuilderInternalOptions {
3233
// We know that it's always a string since we set it.
@@ -40,6 +41,68 @@ 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+
return;
80+
} else if (file?.origin === 'memory') {
81+
// Include pathname to help with Content-Type headers.
82+
this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents, true);
83+
return;
84+
}
85+
} catch (e) {
86+
err = e;
87+
}
88+
next(err);
89+
}
90+
91+
static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef {
92+
return {
93+
[LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }],
94+
95+
[`middleware:${AngularAssetsMiddleware.NAME}`]: [
96+
'factory',
97+
Object.assign((...args: ConstructorParameters<typeof AngularAssetsMiddleware>) => {
98+
const inst = new AngularAssetsMiddleware(...args);
99+
return inst.handle.bind(inst);
100+
}, AngularAssetsMiddleware),
101+
],
102+
};
103+
}
104+
}
105+
43106
function injectKarmaReporter(
44107
context: BuilderContext,
45108
buildOptions: BuildOptions,
@@ -58,9 +121,12 @@ function injectKarmaReporter(
58121
}
59122

60123
class ProgressNotifierReporter {
61-
static $inject = ['emitter'];
124+
static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN];
62125

63-
constructor(private readonly emitter: KarmaEmitter) {
126+
constructor(
127+
private readonly emitter: KarmaEmitter,
128+
private readonly latestBuildFiles: LatestBuildFiles,
129+
) {
64130
this.startWatchingBuild();
65131
}
66132

@@ -81,6 +147,14 @@ function injectKarmaReporter(
81147
buildOutput.kind === ResultKind.Incremental ||
82148
buildOutput.kind === ResultKind.Full
83149
) {
150+
if (buildOutput.kind === ResultKind.Full) {
151+
this.latestBuildFiles.files = buildOutput.files;
152+
} else {
153+
this.latestBuildFiles.files = {
154+
...this.latestBuildFiles.files,
155+
...buildOutput.files,
156+
};
157+
}
84158
await writeTestFiles(buildOutput.files, buildOptions.outputPath);
85159
this.emitter.refreshFiles();
86160
}
@@ -237,6 +311,7 @@ async function initializeApplication(
237311
: undefined;
238312

239313
const buildOptions: BuildOptions = {
314+
assets: options.assets,
240315
entryPoints,
241316
tsConfig: options.tsConfig,
242317
outputPath,
@@ -293,7 +368,6 @@ async function initializeApplication(
293368
},
294369
);
295370
}
296-
297371
karmaOptions.files.push(
298372
// Serve remaining JS on page load, these are the test entrypoints.
299373
{ pattern: `${outputPath}/*.js`, type: 'module', watched: false },
@@ -313,8 +387,9 @@ async function initializeApplication(
313387
// Remove the webpack plugin/framework:
314388
// Alternative would be to make the Karma plugin "smart" but that's a tall order
315389
// with managing unneeded imports etc..
316-
const pluginLengthBefore = (parsedKarmaConfig.plugins ?? []).length;
317-
parsedKarmaConfig.plugins = (parsedKarmaConfig.plugins ?? []).filter(
390+
parsedKarmaConfig.plugins ??= [];
391+
const pluginLengthBefore = parsedKarmaConfig.plugins.length;
392+
parsedKarmaConfig.plugins = parsedKarmaConfig.plugins.filter(
318393
(plugin: string | InlinePluginDef) => {
319394
if (typeof plugin === 'string') {
320395
return plugin !== 'framework:@angular-devkit/build-angular';
@@ -323,16 +398,21 @@ async function initializeApplication(
323398
return !plugin['framework:@angular-devkit/build-angular'];
324399
},
325400
);
326-
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks?.filter(
401+
parsedKarmaConfig.frameworks ??= [];
402+
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks.filter(
327403
(framework: string) => framework !== '@angular-devkit/build-angular',
328404
);
329-
const pluginLengthAfter = (parsedKarmaConfig.plugins ?? []).length;
405+
const pluginLengthAfter = parsedKarmaConfig.plugins.length;
330406
if (pluginLengthBefore !== pluginLengthAfter) {
331407
context.logger.warn(
332408
`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.`,
333409
);
334410
}
335411

412+
parsedKarmaConfig.plugins.push(AngularAssetsMiddleware.createPlugin(buildOutput));
413+
parsedKarmaConfig.middleware ??= [];
414+
parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME);
415+
336416
// When using code-coverage, auto-add karma-coverage.
337417
// This was done as part of the karma plugin for webpack.
338418
if (

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

Lines changed: 2 additions & 1 deletion
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
});

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
3838
declarations: [AppComponent]
3939
}));
4040
41-
it('should not contain text that is hidden via css', () => {
41+
it('should not contain text that is hidden via css', async () => {
4242
const fixture = TestBed.createComponent(AppComponent);
43+
await fixture.whenStable();
4344
expect(fixture.nativeElement.innerText).not.toContain('Hello World');
4445
});
4546
});`,
@@ -101,8 +102,9 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
101102
imports: [HttpClientModule],
102103
declarations: [AppComponent]
103104
}));
104-
it('should create the app', () => {
105+
it('should create the app', async () => {
105106
const fixture = TestBed.createComponent(AppComponent);
107+
await fixture.whenStable();
106108
const app = fixture.debugElement.componentInstance;
107109
expect(app).toBeTruthy();
108110
});

0 commit comments

Comments
 (0)