Skip to content

Commit 49d50f4

Browse files
committed
feat(@angular-devkit/build-webpack): use a Host for webpacks input file system
1 parent 53333e6 commit 49d50f4

File tree

2 files changed

+181
-15
lines changed

2 files changed

+181
-15
lines changed

packages/angular_devkit/build_webpack/src/browser/index.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
BuilderConfiguration,
1313
BuilderContext,
1414
} from '@angular-devkit/architect';
15-
import { Path, getSystemPath, normalize, resolve } from '@angular-devkit/core';
15+
import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core';
16+
import * as fs from 'fs';
1617
import { Observable } from 'rxjs/Observable';
17-
import { of } from 'rxjs/observable/of';
18-
import { concat, concatMap } from 'rxjs/operators';
18+
import { concat as concatObservable } from 'rxjs/observable/concat';
19+
import { empty } from 'rxjs/observable/empty';
20+
import { ignoreElements, switchMap } from 'rxjs/operators';
1921
import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies
2022
import * as webpack from 'webpack';
2123
import {
@@ -33,6 +35,7 @@ import {
3335
statsToString,
3436
statsWarningsToString,
3537
} from '../angular-cli-files/utilities/stats';
38+
import { WebpackFileSystemHostAdapter } from '../utils/webpack-file-system-host-adapter';
3639
const webpackMerge = require('webpack-merge');
3740

3841

@@ -128,12 +131,11 @@ export class BrowserBuilder implements Builder<BrowserBuilderOptions> {
128131
const root = this.context.workspace.root;
129132
const projectRoot = resolve(root, builderConfig.root);
130133

131-
// TODO: verify using of(null) to kickstart things is a pattern.
132-
return of(null).pipe(
133-
concatMap(() => options.deleteOutputPath
134+
return concatObservable(
135+
options.deleteOutputPath
134136
? this._deleteOutputDir(root, normalize(options.outputPath))
135-
: of(null)),
136-
concatMap(() => new Observable(obs => {
137+
: empty<BuildEvent>(),
138+
new Observable(obs => {
137139
// Ensure Build Optimizer is only used with AOT.
138140
if (options.buildOptimizer && !options.aot) {
139141
throw new Error('The `--build-optimizer` option cannot be used without `--aot`.');
@@ -150,6 +152,12 @@ export class BrowserBuilder implements Builder<BrowserBuilderOptions> {
150152
return;
151153
}
152154
const webpackCompiler = webpack(webpackConfig);
155+
156+
// TODO: fix webpack typings.
157+
// tslint:disable-next-line:no-any
158+
(webpackCompiler as any).inputFileSystem = new WebpackFileSystemHostAdapter(
159+
this.context.host as virtualFs.Host<fs.Stats>,
160+
);
153161
const statsConfig = getWebpackStatsConfig(options.verbose);
154162

155163
const callback: webpack.compiler.CompilerCallback = (err, stats) => {
@@ -204,7 +212,7 @@ export class BrowserBuilder implements Builder<BrowserBuilderOptions> {
204212
}
205213
throw err;
206214
}
207-
})),
215+
}),
208216
);
209217
}
210218

@@ -277,18 +285,21 @@ export class BrowserBuilder implements Builder<BrowserBuilderOptions> {
277285
return webpackMerge(webpackConfigs);
278286
}
279287

280-
private _deleteOutputDir(root: Path, outputPath: Path) {
288+
private _deleteOutputDir(root: Path, outputPath: Path): Observable<void> {
281289
const resolvedOutputPath = resolve(root, outputPath);
282290
if (resolvedOutputPath === root) {
283291
throw new Error('Output path MUST not be project root directory!');
284292
}
285293

286294
return this.context.host.exists(resolvedOutputPath).pipe(
287-
concatMap(exists => exists
288-
// TODO: remove this concat once host ops emit an event.
289-
? this.context.host.delete(resolvedOutputPath).pipe(concat(of(null)))
290-
// ? of(null)
291-
: of(null)),
295+
switchMap(exists => {
296+
if (exists) {
297+
return this.context.host.delete(resolvedOutputPath);
298+
} else {
299+
return empty<void>();
300+
}
301+
}),
302+
ignoreElements(),
292303
);
293304
}
294305
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { FileDoesNotExistException, JsonObject, normalize, virtualFs } from '@angular-devkit/core';
9+
import { Callback, InputFileSystem } from '@ngtools/webpack/src/webpack';
10+
import { Stats } from 'fs';
11+
import { Observable } from 'rxjs/Observable';
12+
import { of } from 'rxjs/observable/of';
13+
import { map, mergeMap, switchMap } from 'rxjs/operators';
14+
15+
16+
export class WebpackFileSystemHostAdapter implements InputFileSystem {
17+
protected _syncHost: virtualFs.SyncDelegateHost<Stats> | null = null;
18+
19+
constructor(protected _host: virtualFs.Host<Stats>) {}
20+
21+
private _doHostCall<T>(o: Observable<T>, callback: Callback<T>) {
22+
const token = Symbol();
23+
let value: T | typeof token = token;
24+
let error = false;
25+
26+
try {
27+
o.subscribe({
28+
error(err) {
29+
error = true;
30+
callback(err);
31+
},
32+
next(v) {
33+
value = v;
34+
},
35+
complete() {
36+
if (value !== token) {
37+
callback(null, value);
38+
} else {
39+
callback(new Error('Unknown error happened.'));
40+
}
41+
},
42+
});
43+
} catch (err) {
44+
// In some occasions, the error handler above will be called, then an exception will be
45+
// thrown (by design in observable constructors in RxJS 5). Don't call the callback
46+
// twice.
47+
if (!error) {
48+
callback(err);
49+
}
50+
}
51+
}
52+
53+
stat(path: string, callback: Callback<virtualFs.Stats>): void {
54+
const p = normalize('/' + path);
55+
const result = this._host.stat(p);
56+
57+
if (result === null) {
58+
const o = this._host.exists(p).pipe(
59+
switchMap(exists => {
60+
if (!exists) {
61+
throw new FileDoesNotExistException(p);
62+
}
63+
64+
return this._host.isDirectory(p).pipe(
65+
mergeMap(isDirectory => {
66+
return (isDirectory ? of(0) : this._host.read(p).pipe(
67+
map(content => content.byteLength),
68+
)).pipe(
69+
map(size => [isDirectory, size]),
70+
);
71+
}),
72+
);
73+
}),
74+
map(([isDirectory, size]) => {
75+
return {
76+
isFile() { return !isDirectory; },
77+
isDirectory() { return isDirectory; },
78+
size,
79+
atime: new Date(),
80+
mtime: new Date(),
81+
ctime: new Date(),
82+
birthtime: new Date(),
83+
};
84+
}),
85+
);
86+
87+
this._doHostCall(o, callback);
88+
} else {
89+
this._doHostCall(result, callback);
90+
}
91+
}
92+
93+
readdir(path: string, callback: Callback<string[]>): void {
94+
return this._doHostCall(this._host.list(normalize('/' + path)), callback);
95+
}
96+
97+
readFile(path: string, callback: Callback<Buffer>): void {
98+
const o = this._host.read(normalize('/' + path)).pipe(
99+
map(content => new Buffer(content)),
100+
);
101+
102+
return this._doHostCall(o, callback);
103+
}
104+
105+
readJson(path: string, callback: Callback<JsonObject>): void {
106+
const o = this._host.read(normalize('/' + path)).pipe(
107+
map(content => JSON.parse(virtualFs.fileBufferToString(content))),
108+
);
109+
110+
return this._doHostCall(o, callback);
111+
}
112+
113+
readlink(path: string, callback: Callback<string>): void {
114+
const err: NodeJS.ErrnoException = new Error('Not a symlink.');
115+
err.code = 'EINVAL';
116+
callback(err);
117+
}
118+
119+
statSync(path: string): Stats {
120+
if (!this._syncHost) {
121+
this._syncHost = new virtualFs.SyncDelegateHost(this._host);
122+
}
123+
124+
const result = this._syncHost.stat(normalize('/' + path));
125+
if (result) {
126+
return result;
127+
} else {
128+
return {} as Stats;
129+
}
130+
}
131+
readdirSync(path: string): string[] {
132+
if (!this._syncHost) {
133+
this._syncHost = new virtualFs.SyncDelegateHost(this._host);
134+
}
135+
136+
return this._syncHost.list(normalize('/' + path));
137+
}
138+
readFileSync(path: string): string {
139+
if (!this._syncHost) {
140+
this._syncHost = new virtualFs.SyncDelegateHost(this._host);
141+
}
142+
143+
return virtualFs.fileBufferToString(this._syncHost.read(normalize('/' + path)));
144+
}
145+
readJsonSync(path: string): string {
146+
return JSON.parse(this.readFileSync(path));
147+
}
148+
readlinkSync(path: string): string {
149+
const err: NodeJS.ErrnoException = new Error('Not a symlink.');
150+
err.code = 'EINVAL';
151+
throw err;
152+
}
153+
154+
purge(_changes?: string[] | string): void {}
155+
}

0 commit comments

Comments
 (0)