Skip to content
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
8 changes: 4 additions & 4 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import {
MESSAGE_KIND,
UpdateMessage,
} from './type_checker_messages';
import { flattenArray, workaroundResolve } from './utils';
import { flattenArray, forwardSlashPath, workaroundResolve } from './utils';
import {
VirtualFileSystemDecorator,
VirtualWatchFileSystemDecorator,
Expand Down Expand Up @@ -163,7 +163,7 @@ export class AngularCompilerPlugin {
throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.');
}
// TS represents paths internally with '/' and expects the tsconfig path to be in this format
this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/');
this._tsConfigPath = forwardSlashPath(options.tsConfigPath);

// Check the base path.
const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath);
Expand Down Expand Up @@ -482,7 +482,7 @@ export class AngularCompilerPlugin {
return;
}

const lazyRouteTSFile = discoveredLazyRoutes[lazyRouteKey].replace(/\\/g, '/');
const lazyRouteTSFile = forwardSlashPath(discoveredLazyRoutes[lazyRouteKey]);
let modulePath: string, moduleKey: string;

if (this._useFactories) {
Expand Down Expand Up @@ -928,7 +928,7 @@ export class AngularCompilerPlugin {
// Import ngfactory in loadChildren import syntax
if (this._useFactories) {
// Only transform imports to use factories with View Engine.
this._transformers.push(importFactory(msg => this._warnings.push(msg)));
this._transformers.push(importFactory(msg => this._warnings.push(msg), getTypeChecker));
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/ngtools/webpack/src/lazy_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { dirname, join } from 'path';
import * as ts from 'typescript';
import { findAstNodes, resolve } from './refactor';
import { forwardSlashPath } from './utils';


function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null {
Expand Down Expand Up @@ -38,7 +39,7 @@ export function findLazyRoutes(
}
compilerOptions = program.getCompilerOptions();
}
const fileName = resolve(filePath, host, compilerOptions).replace(/\\/g, '/');
const fileName = forwardSlashPath(resolve(filePath, host, compilerOptions));
let sourceFile: ts.SourceFile | undefined;
if (program) {
sourceFile = program.getSourceFile(fileName);
Expand Down
3 changes: 2 additions & 1 deletion packages/ngtools/webpack/src/refactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import * as path from 'path';
import * as ts from 'typescript';
import { forwardSlashPath } from './utils';


/**
Expand Down Expand Up @@ -95,7 +96,7 @@ export class TypeScriptFileRefactor {
let sourceFile: ts.SourceFile | null = null;

if (_program) {
fileName = resolve(fileName, _host, _program.getCompilerOptions()).replace(/\\/g, '/');
fileName = forwardSlashPath(resolve(fileName, _host, _program.getCompilerOptions()));
this._fileName = fileName;

if (source) {
Expand Down
43 changes: 37 additions & 6 deletions packages/ngtools/webpack/src/transformers/ast_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import { virtualFs } from '@angular-devkit/core';
import { readFileSync, readdirSync } from 'fs';
import { dirname, join } from 'path';
import * as ts from 'typescript';
import { WebpackCompilerHost } from '../compiler_host';

Expand Down Expand Up @@ -48,14 +50,20 @@ export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null {
// Test transform helpers.
const basePath = '/project/src/';
const fileName = basePath + 'test-file.ts';
const tsLibFiles = loadTsLibFiles();

export function createTypescriptContext(content: string, additionalFiles?: Record<string, string>) {
export function createTypescriptContext(
content: string,
additionalFiles?: Record<string, string>,
useLibs = false,
) {
// Set compiler options.
const compilerOptions: ts.CompilerOptions = {
noEmitOnError: false,
noEmitOnError: useLibs,
allowJs: true,
newLine: ts.NewLineKind.LineFeed,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
skipLibCheck: true,
sourceMap: false,
Expand All @@ -73,6 +81,15 @@ export function createTypescriptContext(content: string, additionalFiles?: Recor
// Add a dummy file to host content.
compilerHost.writeFile(fileName, content, false);

if (useLibs) {
// Write the default libs.
// These are needed for tests that use import(), because it relies on a Promise being there.
const compilerLibFolder = dirname(compilerHost.getDefaultLibFileName(compilerOptions));
for (const [k, v] of Object.entries(tsLibFiles)) {
compilerHost.writeFile(join(compilerLibFolder, k), v, false);
}
}

if (additionalFiles) {
for (const key in additionalFiles) {
compilerHost.writeFile(basePath + key, additionalFiles[key], false);
Expand Down Expand Up @@ -106,13 +123,27 @@ export function transformTypescript(
undefined, undefined, undefined, undefined, { before: transformers },
);

// Log diagnostics if emit wasn't successfull.
// Throw error with diagnostics if emit wasn't successfull.
if (emitSkipped) {
console.error(diagnostics);

return null;
throw new Error(ts.formatDiagnostics(diagnostics, compilerHost));
}

// Return the transpiled js.
return compilerHost.readFile(fileName.replace(/\.tsx?$/, '.js'));
}

function loadTsLibFiles() {
const libFolderPath = dirname(require.resolve('typescript/lib/lib.d.ts'));
const libFolderFiles = readdirSync(libFolderPath);
const libFileNames = libFolderFiles.filter(f => f.startsWith('lib.') && f.endsWith('.d.ts'));

// Return a map of the lib names to their content.
return libFileNames.reduce(
(map, f) => {
map[f] = readFileSync(join(libFolderPath, f), 'utf-8');

return map;
},
{} as { [k: string]: string },
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import * as path from 'path';
import * as ts from 'typescript';
import { LazyRouteMap } from '../lazy_routes';
import { forwardSlashPath } from '../utils';
import { getFirstNode, getLastNode } from './ast_helpers';
import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces';
import { makeTransform } from './make_transform';
Expand Down Expand Up @@ -46,7 +47,7 @@ export function exportLazyModuleMap(
return;
}

let relativePath = path.relative(dirName, modulePath).replace(/\\/g, '/');
let relativePath = forwardSlashPath(path.relative(dirName, modulePath));
if (!(relativePath.startsWith('./') || relativePath.startsWith('../'))) {
// 'a/b/c' is a relative path for Node but an absolute path for TS, so we must convert it.
relativePath = `./${relativePath}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { dirname, relative } from 'path';
import * as ts from 'typescript';
import { forwardSlashPath } from '../utils';
import { collectDeepNodes, getFirstNode } from './ast_helpers';
import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces';
import { makeTransform } from './make_transform';
Expand Down Expand Up @@ -35,7 +36,7 @@ export function exportNgFactory(
}

const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path);
const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/');
const normalizedEntryModulePath = forwardSlashPath(`./${relativeEntryModulePath}`);

// Get the module path from the import.
entryModuleIdentifiers.forEach((entryModuleIdentifier) => {
Expand Down
56 changes: 40 additions & 16 deletions packages/ngtools/webpack/src/transformers/import_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* 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 { dirname, relative } from 'path';
import * as ts from 'typescript';
import { forwardSlashPath } from '../utils';


/**
Expand Down Expand Up @@ -51,6 +53,7 @@ import * as ts from 'typescript';

export function importFactory(
warningCb: (warning: string) => void,
getTypeChecker: () => ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
// TODO(filipesilva): change the link to https://angular.io/guide/ivy once it is out.
Expand All @@ -59,17 +62,17 @@ export function importFactory(
Found 'loadChildren' with a non-string syntax in ${sourceFile.fileName} but could not transform it.
Make sure it matches the format below:

loadChildren: () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME)

Please note that only IMPORT_STRING and EXPORT_NAME can be replaced in this format.
Please note that only IMPORT_STRING, M, and EXPORT_NAME can be replaced in this format.

Visit https://next.angular.io/guide/ivy for more information on using Ivy.
`;

const emitWarning = () => warningCb(warning);
const visitVariableStatement: ts.Visitor = (node: ts.Node) => {
if (ts.isVariableDeclaration(node)) {
return replaceImport(node, context, emitWarning);
return replaceImport(node, context, emitWarning, sourceFile.fileName, getTypeChecker());
}

return ts.visitEachChild(node, visitVariableStatement, context);
Expand All @@ -95,16 +98,17 @@ function replaceImport(
node: ts.VariableDeclaration,
context: ts.TransformationContext,
emitWarning: () => void,
fileName: string,
typeChecker: ts.TypeChecker,
): ts.Node {
// This ONLY matches the original source code format below:
// loadChildren: () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
// loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME)
// And expects that source code to be transformed by NGC (see comment for importFactory).
// It will not match nor alter variations, for instance:
// - not using arrow functions
// - not using `m` as the module argument
// - using `await` instead of `then`
// - using a default export (https://github.com/angular/angular/issues/11402)
// The only parts that can change are the ones in caps: IMPORT_STRING and EXPORT_NAME.
// The only parts that can change are the ones in caps: IMPORT_STRING, M and EXPORT_NAME.

// Exit early if the structure is not what we expect.

Expand Down Expand Up @@ -158,16 +162,20 @@ function replaceImport(
return node;
}

// Now that we know it's both `ɵ0` (generated by NGC) and a `import()`, start emitting a warning
// if the structure isn't as expected to help users identify unusable syntax.
const warnAndBail = () => {
emitWarning();

return node;
};

// ɵ0 = () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
if (!(
topArrowFnBody.arguments.length === 1
&& ts.isArrowFunction(topArrowFnBody.arguments[0])
)) {
// Now that we know it's both `ɵ0` (generated by NGC) and a `import()`, start emitting a warning
// if the structure isn't as expected to help users identify unusable syntax.
emitWarning();

return node;
return warnAndBail();
}

const thenArrowFn = topArrowFnBody.arguments[0] as ts.ArrowFunction;
Expand All @@ -176,20 +184,36 @@ function replaceImport(
&& ts.isPropertyAccessExpression(thenArrowFn.body)
&& ts.isIdentifier(thenArrowFn.body.name)
)) {
emitWarning();

return node;
return warnAndBail();
}

// At this point we know what are the nodes we need to replace.
const importStringLit = importCall.arguments[0] as ts.StringLiteral;
const exportNameId = thenArrowFn.body.name;
const importStringLit = importCall.arguments[0] as ts.StringLiteral;

// Try to resolve the import. It might be a reexport from somewhere and the ngfactory will only
// be present next to the original module.
const exportedSymbol = typeChecker.getSymbolAtLocation(exportNameId);
if (!exportedSymbol) {
return warnAndBail();
}
const exportedSymbolDecl = exportedSymbol.getDeclarations();
if (!exportedSymbolDecl || exportedSymbolDecl.length === 0) {
return warnAndBail();
}

// Get the relative path from the containing module to the imported module.
const relativePath = relative(dirname(fileName), exportedSymbolDecl[0].getSourceFile().fileName);

// node's `relative` call doesn't actually add `./` so we add it here.
// Also replace the 'ts' extension with just 'ngfactory'.
const newImportString = `./${forwardSlashPath(relativePath)}`.replace(/ts$/, 'ngfactory');

// The easiest way to alter them is with a simple visitor.
const replacementVisitor: ts.Visitor = (node: ts.Node) => {
if (node === importStringLit) {
// Transform the import string.
return ts.createStringLiteral(importStringLit.text + '.ngfactory');
return ts.createStringLiteral(newImportString);
} else if (node === exportNameId) {
// Transform the export name.
return ts.createIdentifier(exportNameId.text + 'NgFactory');
Expand Down
Loading