Skip to content

Type checker replacements #12419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

// TODO: cleanup this file, it's copied as is from Angular CLI.

// tslint:disable-next-line:no-implicit-dependencies
import * as ts from 'typescript';
import { logging } from '@angular-devkit/core';
import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies
import {
AssetPatternObject,
Budget,
Expand Down Expand Up @@ -74,6 +74,7 @@ export interface WebpackTestOptions extends BuildOptions {

export interface WebpackConfigOptions<T = BuildOptions> {
root: string;
logger: logging.Logger;
projectRoot: string;
sourceRoot?: string;
buildOptions: T;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function _createAotPlugin(
nameLazyFiles: buildOptions.namedChunks,
forkTypeChecker: buildOptions.forkTypeChecker,
contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'),
logger: wco.logger,
...options,
};
return new AngularCompilerPlugin(pluginOptions);
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export class BrowserBuilder implements Builder<BrowserBuilderSchema> {

wco = {
root: getSystemPath(root),
logger: this.context.logger,
projectRoot: getSystemPath(projectRoot),
buildOptions: options,
tsConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class ExtractI18nBuilder implements Builder<ExtractI18nBuilderOptions> {

wco = {
root: getSystemPath(root),
logger: this.context.logger,
projectRoot: getSystemPath(projectRoot),
// TODO: use only this.options, it contains all flags and configs items already.
buildOptions: options,
Expand Down
5 changes: 5 additions & 0 deletions packages/angular_devkit/build_angular/src/karma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export class KarmaBuilder implements Builder<KarmaBuilderSchema> {
// Pass onto Karma to emit BuildEvents.
successCb: () => obs.next({ success: true }),
failureCb: () => obs.next({ success: false }),
// Workaround for https://github.com/karma-runner/karma/issues/3154
// When this workaround is removed, user projects need to be updated to use a Karma
// version that has a fix for this issue.
toJSON: () => { },
};

// TODO: inside the configs, always use the project root and not the workspace root.
Expand Down Expand Up @@ -139,6 +143,7 @@ export class KarmaBuilder implements Builder<KarmaBuilderSchema> {

wco = {
root: getSystemPath(root),
logger: this.context.logger,
projectRoot: getSystemPath(projectRoot),
sourceRoot: sourceRoot && getSystemPath(sourceRoot),
// TODO: use only this.options, it contains all flags and configs items already.
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class ServerBuilder implements Builder<BuildWebpackServerSchema> {

wco = {
root: getSystemPath(root),
logger: this.context.logger,
projectRoot: getSystemPath(projectRoot),
// TODO: use only this.options, it contains all flags and configs items already.
buildOptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

import { runTargetSpec } from '@angular-devkit/architect/testing';
import { DefaultTimeout, TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { takeWhile, tap } from 'rxjs/operators';
import { of, race } from 'rxjs';
import { delay, filter, map, take, takeUntil, takeWhile, tap } from 'rxjs/operators';
import { browserTargetSpec, host } from '../utils';


Expand Down Expand Up @@ -154,4 +155,53 @@ describe('Browser Builder file replacements', () => {
);
});

it('file replacements work with forked type checker on watch mode', async () => {
host.writeMultipleFiles({
'src/file-replaced.ts': 'export var obj = { one: 1, two: 2 };',
'src/file.ts': `export var obj = { one: 1 };`,
'src/main.ts': `
import { obj } from './file';
console.log(obj.two);
`,
});

const overrides = {
fileReplacements: [{
replace: normalize('/src/file.ts'),
with: normalize('/src/file-replaced.ts'),
}],
watch: true,
};

const unexpectedError = `Property 'two' does not exist on type '{ one: number; }'`;
const expectedError = `Property 'prop' does not exist on type '{}'`;
const logger = new TestLogger('rebuild-type-errors');

// Race between a timeout and the expected log entry.
const stop$ = race<null | string>(
Copy link
Collaborator

@alan-agius4 alan-agius4 Oct 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the below result in the same test?

let errorAdded = false;
runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout, logger)
  .pipe(
    tap((buildEvent) => expect(buildEvent.success).toBe(true, 'build should succeed')),
    tap(() => {
      // Introduce a known type error to detect in the logger filter.
      if (!errorAdded) {
        host.appendToFile('src/main.ts', 'console.log({}.prop);');
        errorAdded = true;
      } else {
        expect(logger).toContain(expectedError);
      }
    }),
    tap(() => {
      expect(logger).not.toContain(unexpectedError);
      logger.clear();
    }),
    take(2),
  ).toPromise().then(done, done.fail);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because the type checker logs are not synchronized with builds. Since the type checker runs separately, it will finish rebuilds separately from the main webpack process.

In this specific case, the type checker rebuild will take longer. We need to start a build and and rebuild on the main compilation process, then wait for the forked process to send logs messages.

Copy link
Collaborator

@alan-agius4 alan-agius4 Oct 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cause it runs in a forked process. 👍 Cool test with the race :)

of(null).pipe(delay(DefaultTimeout * 2 / 3)),
logger.pipe(
filter(entry => entry.message.includes(expectedError)),
map(entry => entry.message),
take(1),
),
);

let errorAdded = false;
runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true, 'build should succeed')),
tap(() => {
// Introduce a known type error to detect in the logger filter.
if (!errorAdded) {
host.appendToFile('src/main.ts', 'console.log({}.prop);');
errorAdded = true;
}
}),
takeUntil(stop$),
).subscribe();

const res = await stop$.toPromise();
expect(res).not.toBe(null, 'Test timed out.');
expect(res).not.toContain(unexpectedError);
});
});
54 changes: 47 additions & 7 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Path, dirname, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core';
import {
Path,
dirname,
getSystemPath,
logging,
normalize,
resolve,
virtualFs,
} from '@angular-devkit/core';
import { createConsoleLogger } from '@angular-devkit/core/node';
import {
CompilerHost,
CompilerOptions,
Expand Down Expand Up @@ -45,7 +54,15 @@ import {
replaceServerBootstrap,
} from './transformers';
import { collectDeepNodes } from './transformers/ast_helpers';
import { AUTO_START_ARG, InitMessage, UpdateMessage } from './type_checker';
import {
AUTO_START_ARG,
} from './type_checker';
import {
InitMessage,
LogMessage,
MESSAGE_KIND,
UpdateMessage,
} from './type_checker_messages';
import {
VirtualFileSystemDecorator,
VirtualWatchFileSystemDecorator,
Expand All @@ -59,7 +76,7 @@ import { WebpackInputHost } from './webpack-input-host';

const treeKill = require('tree-kill');

export interface ContextElementDependency {}
export interface ContextElementDependency { }

export interface ContextElementDependencyConstructor {
new(modulePath: string, name: string): ContextElementDependency;
Expand All @@ -85,6 +102,7 @@ export interface AngularCompilerPluginOptions {
missingTranslation?: string;
platform?: PLATFORM;
nameLazyFiles?: boolean;
logger?: logging.Logger;

// added to the list of lazy routes
additionalLazyModules?: { [module: string]: string };
Expand Down Expand Up @@ -141,6 +159,9 @@ export class AngularCompilerPlugin {
private _typeCheckerProcess: ChildProcess | null;
private _forkedTypeCheckerInitialized = false;

// Logging.
private _logger: logging.Logger;

private get _ngCompilerSupportsNewApi() {
if (this._JitMode) {
return false;
Expand Down Expand Up @@ -179,6 +200,8 @@ export class AngularCompilerPlugin {

private _setupOptions(options: AngularCompilerPluginOptions) {
time('AngularCompilerPlugin._setupOptions');
this._logger = options.logger || createConsoleLogger();

// Fill in the missing options.
if (!options.hasOwnProperty('tsConfigPath')) {
throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.');
Expand Down Expand Up @@ -415,7 +438,7 @@ export class AngularCompilerPlugin {
}),
// TODO: fix compiler-cli typings; entryModule should not be string, but also optional.
// tslint:disable-next-line:no-non-null-assertion
entryModule: this._entryModule !,
entryModule: this._entryModule!,
});
timeEnd('AngularCompilerPlugin._getLazyRoutesFromNgtools');

Expand Down Expand Up @@ -542,6 +565,18 @@ export class AngularCompilerPlugin {
forkArgs,
forkOptions);

// Handle child messages.
this._typeCheckerProcess.on('message', message => {
switch (message.kind) {
case MESSAGE_KIND.Log:
const logMessage = message as LogMessage;
this._logger.log(logMessage.level, logMessage.message);
break;
default:
throw new Error(`TypeChecker: Unexpected message received: ${message}.`);
}
});

// Handle child process exit.
this._typeCheckerProcess.once('exit', (_, signal) => {
this._typeCheckerProcess = null;
Expand All @@ -567,8 +602,13 @@ export class AngularCompilerPlugin {
private _updateForkedTypeChecker(rootNames: string[], changedCompilationFiles: string[]) {
if (this._typeCheckerProcess) {
if (!this._forkedTypeCheckerInitialized) {
let hostReplacementPaths = {};
if (this._options.hostReplacementPaths
&& typeof this._options.hostReplacementPaths != 'function') {
hostReplacementPaths = this._options.hostReplacementPaths;
}
this._typeCheckerProcess.send(new InitMessage(this._compilerOptions, this._basePath,
this._JitMode, this._rootNames));
this._JitMode, this._rootNames, hostReplacementPaths));
this._forkedTypeCheckerInitialized = true;
}
this._typeCheckerProcess.send(new UpdateMessage(rootNames, changedCompilationFiles));
Expand Down Expand Up @@ -748,10 +788,10 @@ export class AngularCompilerPlugin {
const name = request.request;
const issuer = request.contextInfo.issuer;
if (name.endsWith('.ts') || name.endsWith('.tsx')
|| (issuer && /\.ts|ngfactory\.js$/.test(issuer))) {
|| (issuer && /\.ts|ngfactory\.js$/.test(issuer))) {
try {
await this.done;
} catch {}
} catch { }
}
}

Expand Down
56 changes: 24 additions & 32 deletions packages/ngtools/webpack/src/type_checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { terminal } from '@angular-devkit/core';
import { normalize, resolve, virtualFs } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import {
CompilerHost,
Expand All @@ -19,37 +19,10 @@ import * as ts from 'typescript';
import { time, timeEnd } from './benchmark';
import { WebpackCompilerHost } from './compiler_host';
import { CancellationToken, gatherDiagnostics } from './gather_diagnostics';
import { LogMessage, TypeCheckerMessage } from './type_checker_messages';


// This file should run in a child process with the AUTO_START_ARG argument


export enum MESSAGE_KIND {
Init,
Update,
}

export abstract class TypeCheckerMessage {
constructor(public kind: MESSAGE_KIND) { }
}

export class InitMessage extends TypeCheckerMessage {
constructor(
public compilerOptions: ts.CompilerOptions,
public basePath: string,
public jitMode: boolean,
public rootNames: string[],
) {
super(MESSAGE_KIND.Init);
}
}

export class UpdateMessage extends TypeCheckerMessage {
constructor(public rootNames: string[], public changedCompilationFiles: string[]) {
super(MESSAGE_KIND.Update);
}
}

export const AUTO_START_ARG = '9d93e901-158a-4cf9-ba1b-2f0582ffcfeb';

export class TypeChecker {
Expand All @@ -61,12 +34,25 @@ export class TypeChecker {
_basePath: string,
private _JitMode: boolean,
private _rootNames: string[],
hostReplacementPaths: { [path: string]: string },
) {
time('TypeChecker.constructor');
const host = new virtualFs.AliasHost(new NodeJsSyncHost());

// Add file replacements.
for (const from in hostReplacementPaths) {
const normalizedFrom = resolve(normalize(_basePath), normalize(from));
const normalizedWith = resolve(
normalize(_basePath),
normalize(hostReplacementPaths[from]),
);
host.aliases.set(normalizedFrom, normalizedWith);
}

const compilerHost = new WebpackCompilerHost(
_compilerOptions,
_basePath,
new NodeJsSyncHost(),
host,
);
// We don't set a async resource loader on the compiler host because we only support
// html templates, which are the only ones that can throw errors, and those can be loaded
Expand Down Expand Up @@ -124,19 +110,25 @@ export class TypeChecker {

if (errors.length > 0) {
const message = formatDiagnostics(errors);
console.error(terminal.bold(terminal.red('ERROR in ' + message)));
this.sendMessage(new LogMessage('error', 'ERROR in ' + message));
} else {
// Reset the changed file tracker only if there are no errors.
this._compilerHost.resetChangedFileTracker();
}

if (warnings.length > 0) {
const message = formatDiagnostics(warnings);
console.error(terminal.bold(terminal.yellow('WARNING in ' + message)));
this.sendMessage(new LogMessage('warn', 'WARNING in ' + message));
}
}
}

private sendMessage(msg: TypeCheckerMessage) {
if (process.send) {
process.send(msg);
}
}

public update(rootNames: string[], changedCompilationFiles: string[],
cancellationToken: CancellationToken) {
this._update(rootNames, changedCompilationFiles);
Expand Down
Loading