Skip to content

refactor(@ngtools/webpack): use ts transforms in refactor #7134

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

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions packages/@angular/cli/models/webpack-configs/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
locale: buildOptions.locale,
replaceExport: appConfig.platform === 'server',
hostReplacementPaths,
sourceMap: buildOptions.sourcemaps,
// If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`.
exclude: []
}, options));
Expand Down
1 change: 1 addition & 0 deletions packages/@ngtools/webpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The loader works with the webpack plugin to compile your TypeScript. It's import
* `skipCodeGeneration`. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources.
* `typeChecking`. Optional, defaults to true. Enable type checking through your application. This will slow down compilation, but show syntactic and semantic errors in webpack.
* `exclude`. Optional. Extra files to exclude from TypeScript compilation.
* `sourceMap`. Optional. Include sourcemaps.
* `compilerOptions`. Optional. Override options in `tsconfig.json`.

## Features
Expand Down
5 changes: 3 additions & 2 deletions packages/@ngtools/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
"dependencies": {
"loader-utils": "^1.0.2",
"magic-string": "^0.22.3",
"source-map": "^0.5.6"
"source-map": "^0.5.6",
"semver": "^5.3.0"
},
"peerDependencies": {
"enhanced-resolve": "^3.1.0",
"typescript": "^2.0.2",
"typescript": "^2.1.0",
"webpack": "^2.2.0 || ^3.0.0"
}
}
33 changes: 18 additions & 15 deletions packages/@ngtools/webpack/src/entry_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import * as fs from 'fs';
import {join} from 'path';
import * as ts from 'typescript';

import {TypeScriptFileRefactor} from './refactor';
import {TypeScriptFileRefactor, getTypeScriptFileRefactor} from './refactor/refactor';
import {ProgramManager} from './program_manager';


function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
symbolName: string,
host: ts.CompilerHost,
program: ts.Program): string | null {
programManager: ProgramManager): string | null {
// Check this file.
const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
Expand All @@ -29,15 +30,16 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,

const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text;
const resolvedModule = ts.resolveModuleName(
modulePath, refactor.fileName, program.getCompilerOptions(), host);
modulePath, refactor.fileName, programManager.program.getCompilerOptions(), host);
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
return null;
}

const module = resolvedModule.resolvedModule.resolvedFileName;
if (!decl.exportClause) {
const moduleRefactor = new TypeScriptFileRefactor(module, host, program);
const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program);
const moduleRefactor = getTypeScriptFileRefactor(module, host, programManager);
const maybeModule = _recursiveSymbolExportLookup(
moduleRefactor, symbolName, host, programManager);
if (maybeModule) {
return maybeModule;
}
Expand All @@ -51,17 +53,17 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
if (fs.statSync(module).isDirectory()) {
const indexModule = join(module, 'index.ts');
if (fs.existsSync(indexModule)) {
const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program);
const indexRefactor = getTypeScriptFileRefactor(indexModule, host, programManager);
const maybeModule = _recursiveSymbolExportLookup(
indexRefactor, symbolName, host, program);
indexRefactor, symbolName, host, programManager);
if (maybeModule) {
return maybeModule;
}
}
}

// Create the source and verify that the symbol is at least a class.
const source = new TypeScriptFileRefactor(module, host, program);
const source = getTypeScriptFileRefactor(module, host, programManager);
const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
.some((cd: ts.ClassDeclaration) => {
return cd.name && cd.name.text == symbolName;
Expand All @@ -80,7 +82,7 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
function _symbolImportLookup(refactor: TypeScriptFileRefactor,
symbolName: string,
host: ts.CompilerHost,
program: ts.Program): string | null {
programManager: ProgramManager): string | null {
// We found the bootstrap variable, now we just need to get where it's imported.
const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration)
.map(node => node as ts.ImportDeclaration);
Expand All @@ -95,7 +97,7 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor,

const resolvedModule = ts.resolveModuleName(
(decl.moduleSpecifier as ts.StringLiteral).text,
refactor.fileName, program.getCompilerOptions(), host);
refactor.fileName, programManager.program.getCompilerOptions(), host);
if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) {
continue;
}
Expand All @@ -112,8 +114,9 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor,
for (const specifier of binding.elements) {
if (specifier.name.text == symbolName) {
// Create the source and recursively lookup the import.
const source = new TypeScriptFileRefactor(module, host, program);
const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program);
const source = getTypeScriptFileRefactor(module, host, programManager);
const maybeModule = _recursiveSymbolExportLookup(
source, symbolName, host, programManager);
if (maybeModule) {
return maybeModule;
}
Expand All @@ -127,8 +130,8 @@ function _symbolImportLookup(refactor: TypeScriptFileRefactor,

export function resolveEntryModuleFromMain(mainPath: string,
host: ts.CompilerHost,
program: ts.Program) {
const source = new TypeScriptFileRefactor(mainPath, host, program);
programManager: ProgramManager) {
const source = getTypeScriptFileRefactor(mainPath, host, programManager);

const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true)
.map(node => node as ts.CallExpression)
Expand All @@ -148,7 +151,7 @@ export function resolveEntryModuleFromMain(mainPath: string,
+ 'to the plugins options.');
}
const bootstrapSymbolName = bootstrap[0].text;
const module = _symbolImportLookup(source, bootstrapSymbolName, host, program);
const module = _symbolImportLookup(source, bootstrapSymbolName, host, programManager);
if (module) {
return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`;
}
Expand Down
10 changes: 6 additions & 4 deletions packages/@ngtools/webpack/src/lazy_routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {dirname, join} from 'path';
import * as ts from 'typescript';

import {TypeScriptFileRefactor} from './refactor';
import {getTypeScriptFileRefactor} from './refactor/refactor';
import {ProgramManager} from './program_manager';


function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string {
Expand All @@ -21,9 +22,9 @@ export interface LazyRouteMap {


export function findLazyRoutes(filePath: string,
program: ts.Program,
programManager: ProgramManager,
host: ts.CompilerHost): LazyRouteMap {
const refactor = new TypeScriptFileRefactor(filePath, host, program);
const refactor = getTypeScriptFileRefactor(filePath, host, programManager);

return refactor
// Find all object literals in the file.
Expand All @@ -50,7 +51,8 @@ export function findLazyRoutes(filePath: string,
? ({
resolvedModule: { resolvedFileName: join(dirname(filePath), moduleName) + '.ts' }
} as any)
: ts.resolveModuleName(moduleName, filePath, program.getCompilerOptions(), host);
: ts.resolveModuleName(
moduleName, filePath, programManager.program.getCompilerOptions(), host);
if (resolvedModuleName.resolvedModule
&& resolvedModuleName.resolvedModule.resolvedFileName
&& host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) {
Expand Down
40 changes: 22 additions & 18 deletions packages/@ngtools/webpack/src/loader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ts from 'typescript';
import {removeModuleIdOnlyForTesting} from './loader';
import {WebpackCompilerHost} from './compiler_host';
import {TypeScriptFileRefactor} from './refactor';
import {getTypeScriptFileRefactor} from './refactor/refactor';
import {ProgramManager} from './program_manager';

describe('@ngtools/webpack', () => {
describe('loader', () => {
Expand All @@ -20,25 +20,28 @@ describe('@ngtools/webpack', () => {
@SomeDecorator({ otherValue4: 4, moduleId: 123 }) class CLS4 {}
`, false);

const program = ts.createProgram(['/file.ts', '/file2.ts'], {}, host);
const programManager = new ProgramManager(['/file.ts', '/file2.ts'], {}, host);

const refactor = new TypeScriptFileRefactor('/file.ts', host, program);
const refactor = getTypeScriptFileRefactor('/file.ts', host, programManager);
removeModuleIdOnlyForTesting(refactor);
expect(refactor.sourceText).not.toMatch(/obj = \{\s+};/);
expect(refactor.sourceText).not.toMatch(/\{\s*otherValue: 1\s*};/);

const refactor2 = new TypeScriptFileRefactor('/file2.ts', host, program);
const outputText = refactor.transpile().outputText;
expect(outputText).not.toMatch(/obj = \{\s+};/);
expect(outputText).not.toMatch(/\{\s*otherValue: 1\s*};/);

const refactor2 = getTypeScriptFileRefactor('/file2.ts', host, programManager);
removeModuleIdOnlyForTesting(refactor2);
expect(refactor2.sourceText).toMatch(/\(\{\s+}\)/);
expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/);
expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/);
expect(refactor2.sourceText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/);
const outputText2 = refactor2.transpile().outputText;
expect(outputText2).toMatch(/\(\{\s*}\)/);
expect(outputText2).toMatch(/\(\{\s*otherValue1: 1\s*}\)/);
expect(outputText2).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/);
expect(outputText2).toMatch(/\(\{\s*otherValue4: 4\s*}\)/);
});

it('should work without a root name', () => {
const host = new WebpackCompilerHost({}, '');
host.writeFile('/file.ts', `
import './file2.ts';
import './file2';
`, false);
host.writeFile('/file2.ts', `
@SomeDecorator({ moduleId: 123 }) class CLS {}
Expand All @@ -47,13 +50,14 @@ describe('@ngtools/webpack', () => {
@SomeDecorator({ otherValue4: 4, moduleId: 123 }) class CLS4 {}
`, false);

const program = ts.createProgram(['/file.ts'], {}, host);
const refactor = new TypeScriptFileRefactor('/file2.ts', host, program);
const programManager = new ProgramManager(['/file.ts'], {}, host);
const refactor = getTypeScriptFileRefactor('/file2.ts', host, programManager);
removeModuleIdOnlyForTesting(refactor);
expect(refactor.sourceText).toMatch(/\(\{\s+}\)/);
expect(refactor.sourceText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/);
expect(refactor.sourceText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/);
expect(refactor.sourceText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/);
const outputText = refactor.transpile().outputText;
expect(outputText).toMatch(/\(\{\s*}\)/);
expect(outputText).toMatch(/\(\{\s*otherValue1: 1\s*}\)/);
expect(outputText).toMatch(/\(\{\s*otherValue2: 2\s*,\s*otherValue3: 3\s*}\)/);
expect(outputText).toMatch(/\(\{\s*otherValue4: 4\s*}\)/);
});
});
});
Expand Down
50 changes: 27 additions & 23 deletions packages/@ngtools/webpack/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path';
import * as ts from 'typescript';
import {AotPlugin} from './plugin';
import {TypeScriptFileRefactor} from './refactor';
import {getTypeScriptFileRefactor, TypeScriptFileRefactor} from './refactor/refactor';
import {LoaderContext, ModuleReason} from './webpack';

interface Platform {
Expand Down Expand Up @@ -150,7 +150,11 @@ function _addCtorParameters(classNode: ts.ClassDeclaration,
});

const ctorParametersDecl = `static ctorParameters() { return [ ${params.join(', ')} ]; }`;
refactor.prependBefore(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl);
// ts.createIdentifier() with complex declarations is a hack to not have to manually
// create the node with TS primitives.
// TODO: replace with primitives over time.
const ctorParametersNode = ts.createIdentifier(ctorParametersDecl);
refactor.prependNode(classNode.getLastToken(refactor.sourceFile), ctorParametersNode);
}


Expand Down Expand Up @@ -214,7 +218,7 @@ function _replacePlatform(
const platform = changeMap[(call.expression as ts.Identifier).text];

// Replace with mapped replacement
refactor.replaceNode(call.expression, platform.name);
refactor.replaceNode(call.expression, ts.createIdentifier(platform.name));

// Add the appropriate import
refactor.insertImport(platform.name, platform.importLocation);
Expand Down Expand Up @@ -247,7 +251,7 @@ function _replaceBootstrapOrRender(refactor: TypeScriptFileRefactor, call: ts.Ca
}

if (identifier && identifier.text === replacementTarget) {
refactor.replaceNode(identifier, replacementTarget + 'Factory');
refactor.replaceNode(identifier, ts.createIdentifier(replacementTarget + 'Factory'));
}
}

Expand Down Expand Up @@ -280,7 +284,7 @@ function _replaceEntryModule(plugin: AotPlugin, refactor: TypeScriptFileRefactor

modules
.forEach(reference => {
refactor.replaceNode(reference, factoryClassName);
refactor.replaceNode(reference, ts.createIdentifier(factoryClassName));
const caller = _getCaller(reference);
_replaceBootstrapOrRender(refactor, caller);
});
Expand Down Expand Up @@ -325,7 +329,7 @@ function _removeModuleId(refactor: TypeScriptFileRefactor) {
// Get the trailing comma.
const moduleIdCommaProp = moduleIdProp.parent
? moduleIdProp.parent.getChildAt(1).getChildren()[1] : null;
refactor.removeNodes(moduleIdProp, moduleIdCommaProp);
refactor.removeNodes([moduleIdProp, moduleIdCommaProp]);
});
}

Expand All @@ -349,8 +353,9 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void {
const key = _getContentOfKeyLiteral(sourceFile, node.name);

if (key == 'templateUrl') {
refactor.replaceNode(node,
`template: require(${_getResourceRequest(node.initializer, sourceFile)})`);
refactor.replaceNode(node, ts.createIdentifier(
`template: require(${_getResourceRequest(node.initializer, sourceFile)})`
));
} else if (key == 'styleUrls') {
const arr = <ts.ArrayLiteralExpression[]>(
refactor.findAstNodes(node, ts.SyntaxKind.ArrayLiteralExpression, false));
Expand All @@ -361,7 +366,9 @@ function _replaceResources(refactor: TypeScriptFileRefactor): void {
const initializer = arr[0].elements.map((element: ts.Expression) => {
return _getResourceRequest(element, sourceFile);
});
refactor.replaceNode(node, `styles: [require(${initializer.join('), require(')})]`);
refactor.replaceNode(node, ts.createIdentifier(
`styles: [require(${initializer.join('), require(')})]`
));
}
});
}
Expand Down Expand Up @@ -460,7 +467,7 @@ export function _replaceExport(plugin: AotPlugin, refactor: TypeScriptFileRefact
const factoryPath = _getNgFactoryPath(plugin, refactor);
const factoryClassName = plugin.entryModule.className + 'NgFactory';
const exportStatement = `export \{ ${factoryClassName} \} from '${factoryPath}'`;
refactor.appendAfter(node, exportStatement);
refactor.appendNode(node, ts.createIdentifier(exportStatement));
});
}

Expand Down Expand Up @@ -497,15 +504,19 @@ export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefa

modules.forEach((module, index) => {
const relativePath = path.relative(dirName, module.modulePath).replace(/\\/g, '/');
refactor.prependBefore(node, `import * as __lazy_${index}__ from './${relativePath}'`);
refactor.prependNode(node, ts.createIdentifier(
`import * as __lazy_${index}__ from './${relativePath}'`
));
});

const jsonContent: string = modules
.map((module, index) =>
`"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`)
.join();

refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`);
refactor.prependNode(node, ts.createIdentifier(
`export const LAZY_MODULE_MAP = {${jsonContent}};`
));
});
}

Expand All @@ -531,8 +542,8 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
// extra TS loaders and there's no need to do any trickery.
source = null;
}
const refactor = new TypeScriptFileRefactor(
sourceFileName, plugin.compilerHost, plugin.program, source);
const refactor = getTypeScriptFileRefactor(
sourceFileName, plugin.compilerHost, plugin.programManager, source);

Promise.resolve()
.then(() => {
Expand Down Expand Up @@ -586,14 +597,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
}
}

// Force a few compiler options to make sure we get the result we want.
const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, {
inlineSources: true,
inlineSourceMap: false,
sourceRoot: plugin.basePath
});

const result = refactor.transpile(compilerOptions);
const result = refactor.transpile();
cb(null, result.outputText, result.sourceMap);
})
.catch(err => cb(err));
Expand Down Expand Up @@ -621,7 +625,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
compilerOptions[key] = options[key];
}
const compilerHost = ts.createCompilerHost(compilerOptions);
const refactor = new TypeScriptFileRefactor(sourceFileName, compilerHost);
const refactor = getTypeScriptFileRefactor(sourceFileName, compilerHost);
_replaceResources(refactor);

const result = refactor.transpile(compilerOptions);
Expand Down
Loading