Skip to content

Commit 094bc3d

Browse files
committed
test(@ngtools/webpack): add transformer tests
1 parent 3aed988 commit 094bc3d

File tree

10 files changed

+241
-25
lines changed

10 files changed

+241
-25
lines changed

packages/@ngtools/webpack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"loader-utils": "^1.0.2",
2929
"enhanced-resolve": "^3.1.0",
3030
"magic-string": "^0.22.3",
31+
"semver": "^5.3.0",
3132
"source-map": "^0.5.6"
3233
},
3334
"peerDependencies": {

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ import {
4141
SOURCE,
4242
Program,
4343
CompilerHost,
44-
createProgram as createProgramInterface,
45-
createCompilerHost as createCompilerHostInterface,
44+
CreateProgramInterface,
45+
CreateCompilerHostInterface,
4646
Diagnostics,
47-
formatDiagnostics as formatDiagnosticsInterface,
47+
FormatDiagnosticsInterface,
4848
CustomTransformers as CustomTransformers,
49-
} from './ngtools_api2.d';
49+
} from './ngtools_api2';
5050

51-
const createProgram: createProgramInterface = compilerCliNgtools.createProgram;
52-
const createCompilerHost: createCompilerHostInterface = compilerCliNgtools.createCompilerHost;
53-
const formatDiagnostics: formatDiagnosticsInterface = compilerCliNgtools.formatDiagnostics;
51+
const createProgram: CreateProgramInterface = compilerCliNgtools.createProgram;
52+
const createCompilerHost: CreateCompilerHostInterface = compilerCliNgtools.createCompilerHost;
53+
const formatDiagnostics: FormatDiagnosticsInterface = compilerCliNgtools.formatDiagnostics;
5454
const EmitFlags: any = compilerCliNgtools.EmitFlags;
5555

5656
/**
@@ -634,9 +634,6 @@ export class AngularCompilerPlugin implements Tapable {
634634

635635
getFile(fileName: string) {
636636
const outputFile = fileName.replace(/.ts$/, '.js');
637-
if (!this._compilerHost.fileExists(outputFile)) {
638-
return null;
639-
}
640637
return {
641638
outputText: this._compilerHost.readFile(outputFile),
642639
sourceMap: this._compilerHost.readFile(outputFile + '.map')

packages/@ngtools/webpack/src/loader.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -539,12 +539,6 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
539539
plugin.done
540540
.then(() => {
541541
const result = plugin.getFile(sourceFileName);
542-
if (result === null && !plugin.failedCompilation) {
543-
throw new Error(
544-
`${sourceFileName} is loaded but is not part of the TypeScript compilation. ` +
545-
'Please add it to your tsconfig file.'
546-
);
547-
}
548542
if (plugin.failedCompilation) {
549543
// Return an empty string if there is no result to prevent extra loader errors.
550544
// Plugin errors were already pushed to the compilation errors.

packages/@ngtools/webpack/src/ngtools_api2.d.ts renamed to packages/@ngtools/webpack/src/ngtools_api2.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/**
22
* This is a copy of @compiler-cli/src/ngtools_api.d.ts file.
33
*/
4-
import { ParseSourceSpan } from '@angular/compiler';
54
import * as ts from 'typescript';
65

76
export const DEFAULT_ERROR_CODE = 100;
87
export const UNKNOWN_ERROR_CODE = 500;
98
export const SOURCE = 'angular' as 'angular';
109
export interface Diagnostic {
1110
messageText: string;
12-
span?: ParseSourceSpan;
11+
span?: any;
1312
category: ts.DiagnosticCategory;
1413
code: number;
1514
source: 'angular';
@@ -99,20 +98,20 @@ export declare type Diagnostics = Array<ts.Diagnostic | Diagnostic>;
9998
export declare function formatDiagnostics(options: CompilerOptions, diags: Diagnostics): string;
10099

101100
// Interfaces for the function declarations.
102-
export interface createProgram {
101+
export interface CreateProgramInterface {
103102
({ rootNames, options, host, oldProgram }: {
104103
rootNames: string[];
105104
options: CompilerOptions;
106105
host: CompilerHost;
107106
oldProgram?: Program;
108107
}): Program;
109108
}
110-
export interface createCompilerHost {
109+
export interface CreateCompilerHostInterface {
111110
({ options, tsHost }: {
112111
options: CompilerOptions;
113112
tsHost?: ts.CompilerHost;
114113
}): CompilerHost;
115114
}
116-
export interface formatDiagnostics {
115+
export interface FormatDiagnosticsInterface {
117116
(options: CompilerOptions, diags: Diagnostics): string;
118117
}

packages/@ngtools/webpack/src/transformers/ast_helpers.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as ts from 'typescript';
2+
import { WebpackCompilerHost } from '../compiler_host';
3+
import { makeTransform, TransformOperation } from './make_transform';
24

35

46
/**
@@ -70,3 +72,50 @@ export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null {
7072
}
7173
return null;
7274
}
75+
76+
77+
export function transformTypescript(
78+
content: string,
79+
transformOpsCb: (SourceFile: ts.SourceFile) => TransformOperation[]
80+
) {
81+
82+
// Set compiler options.
83+
const compilerOptions: ts.CompilerOptions = {
84+
noEmitOnError: false,
85+
allowJs: true,
86+
newLine: ts.NewLineKind.LineFeed,
87+
target: ts.ScriptTarget.ESNext,
88+
skipLibCheck: true,
89+
sourceMap: false,
90+
importHelpers: true
91+
};
92+
93+
// Create compiler host.
94+
const basePath = '/project/src/';
95+
const compilerHost = new WebpackCompilerHost(compilerOptions, basePath);
96+
97+
// Add a dummy file to host content.
98+
const fileName = basePath + 'test-file.ts';
99+
compilerHost.writeFile(fileName, content, false);
100+
101+
// Create the TypeScript program.
102+
const program = ts.createProgram([fileName], compilerOptions, compilerHost);
103+
104+
// Get the transform operations.
105+
const sourceFile = program.getSourceFile(fileName);
106+
const transformOps = transformOpsCb(sourceFile);
107+
108+
// Emit.
109+
const { emitSkipped, diagnostics } = program.emit(
110+
undefined, undefined, undefined, undefined, { before: [makeTransform(transformOps)] }
111+
);
112+
113+
// Log diagnostics if emit wasn't successfull.
114+
if (emitSkipped) {
115+
console.log(diagnostics);
116+
return null;
117+
}
118+
119+
// Return the transpiled js.
120+
return compilerHost.readFile(fileName.replace(/\.ts$/, '.js'));
121+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as ts from 'typescript';
2+
import { oneLine, stripIndent } from 'common-tags';
3+
import { transformTypescript } from './ast_helpers';
4+
import { exportLazyModuleMap } from './export_lazy_module_map';
5+
6+
describe('@ngtools/webpack transformers', () => {
7+
describe('replace_resources', () => {
8+
it('should replace resources', () => {
9+
const input = stripIndent`
10+
export { AppModule } from './app/app.module';
11+
`;
12+
// tslint:disable:max-line-length
13+
const output = stripIndent`
14+
import * as __lazy_0__ from "app/lazy/lazy.module.ts";
15+
import * as __lazy_1__ from "app/lazy2/lazy2.module.ts";
16+
export { AppModule } from './app/app.module';
17+
export var LAZY_MODULE_MAP = { "./lazy/lazy.module#LazyModule": __lazy_0__.LazyModule, "./lazy2/lazy2.module#LazyModule2": __lazy_1__.LazyModule2 };
18+
`;
19+
// tslint:enable:max-line-length
20+
21+
const transformOpsCb = (sourceFile: ts.SourceFile) => exportLazyModuleMap(sourceFile, {
22+
'./lazy/lazy.module#LazyModule': '/project/src/app/lazy/lazy.module.ts',
23+
'./lazy2/lazy2.module#LazyModule2': '/project/src/app/lazy2/lazy2.module.ts',
24+
});
25+
const result = transformTypescript(input, transformOpsCb);
26+
27+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
28+
});
29+
});
30+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as ts from 'typescript';
2+
import { oneLine, stripIndent } from 'common-tags';
3+
import { transformTypescript } from './ast_helpers';
4+
import { exportNgFactory } from './export_ngfactory';
5+
6+
describe('@ngtools/webpack transformers', () => {
7+
describe('replace_resources', () => {
8+
it('should replace resources', () => {
9+
const input = stripIndent`
10+
export { AppModule } from './app/app.module';
11+
`;
12+
const output = stripIndent`
13+
export { AppModuleNgFactory } from "./app/app.module.ngfactory";
14+
export { AppModule } from './app/app.module';
15+
`;
16+
17+
const transformOpsCb = (sourceFile: ts.SourceFile) =>
18+
exportNgFactory(sourceFile, { path: '/app.module', className: 'AppModule' });
19+
const result = transformTypescript(input, transformOpsCb);
20+
21+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
22+
});
23+
});
24+
});

packages/@ngtools/webpack/src/transformers/make_transform.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as ts from 'typescript';
2+
import { satisfies } from 'semver';
23

34

5+
// Typescript below 2.5.0 needs a workaround.
6+
const visitEachChild = satisfies(ts.version, '^2.5.0')
7+
? ts.visitEachChild
8+
: visitEachChildWorkaround;
9+
410
export enum OPERATION_KIND {
511
Remove,
612
Add,
@@ -35,9 +41,6 @@ export class ReplaceNodeOperation extends TransformOperation {
3541
}
3642
}
3743

38-
// TODO: add symbol workaround for https://github.com/Microsoft/TypeScript/issues/17551 and
39-
// https://github.com/Microsoft/TypeScript/issues/17384
40-
4144
export function makeTransform(ops: TransformOperation[]): ts.TransformerFactory<ts.SourceFile> {
4245

4346
const sourceFiles = ops.reduce((prev, curr) =>
@@ -83,7 +86,7 @@ export function makeTransform(ops: TransformOperation[]): ts.TransformerFactory<
8386
return modifiedNodes;
8487
} else {
8588
// Otherwise return node as is and visit children.
86-
return ts.visitEachChild(node, visitor, context);
89+
return visitEachChild(node, visitor, context);
8790
}
8891
};
8992

@@ -94,3 +97,34 @@ export function makeTransform(ops: TransformOperation[]): ts.TransformerFactory<
9497
return transformer;
9598
};
9699
}
100+
101+
/**
102+
* This is a version of `ts.visitEachChild` that works that calls our version
103+
* of `updateSourceFileNode`, so that typescript doesn't lose type information
104+
* for property decorators.
105+
* See https://github.com/Microsoft/TypeScript/issues/17384 and
106+
* https://github.com/Microsoft/TypeScript/issues/17551, fixed by
107+
* https://github.com/Microsoft/TypeScript/pull/18051 and released on TS 2.5.0.
108+
*
109+
* @param sf
110+
* @param statements
111+
*/
112+
function visitEachChildWorkaround(node: ts.Node, visitor: ts.Visitor,
113+
context: ts.TransformationContext) {
114+
115+
if (node.kind === ts.SyntaxKind.SourceFile) {
116+
const sf = node as ts.SourceFile;
117+
const statements = ts.visitLexicalEnvironment(sf.statements, visitor, context);
118+
119+
if (statements === sf.statements) {
120+
return sf;
121+
}
122+
// Note: Need to clone the original file (and not use `ts.updateSourceFileNode`)
123+
// as otherwise TS fails when resolving types for decorators.
124+
const sfClone = ts.getMutableClone(sf);
125+
sfClone.statements = statements;
126+
return sfClone;
127+
}
128+
129+
return ts.visitEachChild(node, visitor, context);
130+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as ts from 'typescript';
2+
import { oneLine, stripIndent } from 'common-tags';
3+
import { transformTypescript } from './ast_helpers';
4+
import { replaceBootstrap } from './replace_bootstrap';
5+
6+
describe('@ngtools/webpack transformers', () => {
7+
describe('replace_bootstrap', () => {
8+
it('should replace bootstrap', () => {
9+
const input = stripIndent`
10+
import { enableProdMode } from '@angular/core';
11+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
12+
13+
import { AppModule } from './app/app.module';
14+
import { environment } from './environments/environment';
15+
16+
if (environment.production) {
17+
enableProdMode();
18+
}
19+
20+
platformBrowserDynamic().bootstrapModule(AppModule);
21+
`;
22+
const output = stripIndent`
23+
import { enableProdMode } from '@angular/core';
24+
import { environment } from './environments/environment';
25+
26+
import { AppModuleNgFactory } from "./app/app.module.ngfactory";
27+
import { platformBrowser } from "@angular/platform-browser";
28+
29+
if (environment.production) {
30+
enableProdMode();
31+
}
32+
33+
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
34+
`;
35+
36+
const transformOpsCb = (sourceFile: ts.SourceFile) =>
37+
replaceBootstrap(sourceFile, { path: '/app.module', className: 'AppModule' });
38+
const result = transformTypescript(input, transformOpsCb);
39+
40+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
41+
});
42+
});
43+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as ts from 'typescript';
2+
import { oneLine, stripIndent } from 'common-tags';
3+
import { transformTypescript } from './ast_helpers';
4+
import { replaceResources } from './replace_resources';
5+
6+
describe('@ngtools/webpack transformers', () => {
7+
describe('replace_resources', () => {
8+
it('should replace resources', () => {
9+
const input = stripIndent`
10+
import { Component } from '@angular/core';
11+
12+
@Component({
13+
selector: 'app-root',
14+
templateUrl: './app.component.html',
15+
styleUrls: ['./app.component.css', './app.component.2.css']
16+
})
17+
export class AppComponent {
18+
title = 'app';
19+
}
20+
`;
21+
const output = stripIndent`
22+
import * as tslib_1 from "tslib";
23+
import { Component } from '@angular/core';
24+
let AppComponent = class AppComponent {
25+
constructor() {
26+
this.title = 'app';
27+
}
28+
};
29+
AppComponent = tslib_1.__decorate([
30+
Component({
31+
selector: 'app-root',
32+
template: require("./app.component.html"),
33+
styles: [require("./app.component.css"), require("./app.component.2.css")]
34+
})
35+
], AppComponent);
36+
export { AppComponent };
37+
`;
38+
39+
const transformOpsCb = (sourceFile: ts.SourceFile) => replaceResources(sourceFile);
40+
const result = transformTypescript(input, transformOpsCb);
41+
42+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
43+
});
44+
});
45+
});

0 commit comments

Comments
 (0)