Skip to content

feat(@ngtools/webpack): show warning when a TS compilations contains … #15061

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 1 commit into from
Jul 25, 2019
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
@@ -0,0 +1,165 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 { Architect } from '@angular-devkit/architect';
import { TestLogger } from '@angular-devkit/architect/testing';
import { debounceTime, take, tap } from 'rxjs/operators';
import { BrowserBuilderOutput } from '../../src/browser/index';
import { createArchitect, host, ivyEnabled } from '../utils';


describe('Browser Builder unused files warnings', () => {
const warningMessageSuffix = `is part of the TypeScript compilation but it's unused`;
const targetSpec = { project: 'app', target: 'build' };
let architect: Architect;

beforeEach(async () => {
await host.initialize().toPromise();

architect = (await createArchitect(host.root())).architect;
});
afterEach(async () => host.restore().toPromise());

it('should not show warning when all files are used', async () => {
if (!ivyEnabled) {
// TODO: https://github.com/angular/angular-cli/issues/15056
pending('Only supported in Ivy.');

return;
}

const logger = new TestLogger('unused-files-warnings');
const run = await architect.scheduleTarget(targetSpec, undefined, { logger });
const output = await run.result as BrowserBuilderOutput;
expect(output.success).toBe(true);
expect(logger.includes(warningMessageSuffix)).toBe(false);
logger.clear();

await run.stop();
});

it('should show warning when some files are unused', async () => {
if (!ivyEnabled) {
// TODO: https://github.com/angular/angular-cli/issues/15056
pending('Only supported in Ivy.');

return;
}

host.replaceInFile(
'src/tsconfig.app.json',
'"main.ts"',
'"main.ts", "environments/environment.prod.ts"',
);

const logger = new TestLogger('unused-files-warnings');
const run = await architect.scheduleTarget(targetSpec, undefined, { logger });
const output = await run.result as BrowserBuilderOutput;
expect(output.success).toBe(true);
expect(logger.includes(`environment.prod.ts ${warningMessageSuffix}`)).toBe(true);
logger.clear();

await run.stop();
});

it('should not show warning when type files are used', async () => {
if (!ivyEnabled) {
// TODO: https://github.com/angular/angular-cli/issues/15056
pending('Only supported in Ivy.');

return;
}

host.writeMultipleFiles({
'src/app/type.ts': 'export type MyType = number;'
});

host.replaceInFile(
'src/app/app.component.ts',
`'@angular/core';`,
`'@angular/core';\nimport { MyType } from './type';\n`
);

const logger = new TestLogger('unused-files-warnings');
const run = await architect.scheduleTarget(targetSpec, undefined, { logger });
const output = await run.result as BrowserBuilderOutput;
expect(output.success).toBe(true);
expect(logger.includes(warningMessageSuffix)).toBe(false);
logger.clear();

await run.stop();
});

it('works for rebuilds', async () => {
if (!ivyEnabled) {
// TODO: https://github.com/angular/angular-cli/issues/15056
pending('Only supported in Ivy.');

return;
}

host.replaceInFile(
'src/tsconfig.app.json',
'"**/*.d.ts"',
'"**/*.d.ts", "testing/**/*.ts"',
);

const logger = new TestLogger('unused-files-warnings');
let buildNumber = 0;
const run = await architect.scheduleTarget(targetSpec, { watch: true }, { logger });

await run.output
.pipe(
debounceTime(1000),
tap(buildEvent => {
expect(buildEvent.success).toBe(true);

buildNumber ++;
switch (buildNumber) {
case 1:
// The first should not have unused files
expect(logger.includes(warningMessageSuffix)).toBe(false, `Case ${buildNumber} failed.`);

// Write a used file
host.writeMultipleFiles({
'src/testing/type.ts': 'export type MyType = number;'
});

// touch file to trigger build
host.replaceInFile(
'src/app/app.component.ts',
`'@angular/core';`,
`'@angular/core';\n`
);
break;

case 2:
// The second should have type.ts as unused
expect(logger.includes(`type.ts ${warningMessageSuffix}`)).toBe(true, `Case ${buildNumber} failed.`);

host.replaceInFile(
'src/app/app.component.ts',
`'@angular/core';`,
`'@angular/core';\nimport { MyType } from '../testing/type';`
);
break;

case 3:
// The third should not have any unused files
expect(logger.includes(warningMessageSuffix)).toBe(false, `Case ${buildNumber} failed.`);
break;
}

logger.clear();
}),
take(3),
)
.toPromise();
await run.stop();
});

});
51 changes: 48 additions & 3 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,50 @@ export class AngularCompilerPlugin {
}
}

private _warnOnUnusedFiles(compilation: compilation.Compilation) {
// Only do the unused TS files checks when under Ivy
// since previously we did include unused files in the compilation
// See: https://github.com/angular/angular-cli/pull/15030
if (!this._compilerOptions.enableIvy) {
return;
}

const program = this._getTsProgram();
if (!program) {
return;
}

// Exclude the following files from unused checks
// - ngfactories & ngstyle might not have a correspondent
// JS file example `@angular/core/core.ngfactory.ts`.
// - .d.ts files might not have a correspondent JS file due to bundling.
// - __ng_typecheck__.ts will never be requested.
const fileExcludeRegExp = /(\.(d|ngfactory|ngstyle)\.ts|ng_typecheck__\.ts)$/;

const usedFiles = new Set<string>();
for (const compilationModule of compilation.modules) {
if (!compilationModule.resource) {
continue;
}

usedFiles.add(forwardSlashPath(compilationModule.resource));

// We need the below for dependencies which
// are not emitted such as type only TS files
for (const dependency of compilationModule.buildInfo.fileDependencies) {
usedFiles.add(forwardSlashPath(dependency));
}
}

const unusedFilesWarning = program.getSourceFiles()
.filter(({ fileName }) => !fileExcludeRegExp.test(fileName) && !usedFiles.has(fileName))
.map(({ fileName }) => `${fileName} is part of the TypeScript compilation but it's unused.`);

if (unusedFilesWarning.length) {
compilation.warnings.push(...unusedFilesWarning);
}
}

// Registration hook for webpack plugin.
// tslint:disable-next-line:no-big-function
apply(compiler: Compiler & { watchMode?: boolean, parentCompilation?: compilation.Compilation }) {
Expand All @@ -609,6 +653,8 @@ export class AngularCompilerPlugin {
// cleanup if not watching
compiler.hooks.thisCompilation.tap('angular-compiler', compilation => {
compilation.hooks.finishModules.tap('angular-compiler', () => {
this._warnOnUnusedFiles(compilation);

let rootCompiler = compiler;
while (rootCompiler.parentCompilation) {
// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -1174,7 +1220,7 @@ export class AngularCompilerPlugin {
return null;
}
})
.filter(x => x);
.filter(x => x) as string[];

const resourceImports = findResources(sourceFile)
.map(resourcePath => resolve(dirname(resolvedFileName), normalize(resourcePath)));
Expand All @@ -1186,8 +1232,7 @@ export class AngularCompilerPlugin {
...this.getResourceDependencies(this._compilerHost.denormalizePath(resolvedFileName)),
].map((p) => p && this._compilerHost.denormalizePath(p)));

return [...uniqueDependencies]
.filter(x => !!x) as string[];
return [...uniqueDependencies];
}

getResourceDependencies(fileName: string): string[] {
Expand Down