/** * @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 {dirname, relative} from 'canonical-path'; import * as ts from 'typescript'; import MagicString from 'magic-string'; import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; import {UmdReflectionHost} from '../host/umd_host'; import {Esm5RenderingFormatter} from './esm5_rendering_formatter'; import {stripExtension} from './utils'; import {Reexport} from '../../../src/ngtsc/imports'; type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; /** * A RenderingFormatter that works with UMD files, instead of `import` and `export` statements * the module is an IIFE with a factory function call with dependencies, which are defined in a * wrapper function for AMD, CommonJS and global module formats. */ export class UmdRenderingFormatter extends Esm5RenderingFormatter { constructor(protected umdHost: UmdReflectionHost, isCore: boolean) { super(umdHost, isCore); } /** * Add the imports to the UMD module IIFE. */ addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { if (imports.length === 0) { return; } // Assume there is only one UMD module in the file const umdModule = this.umdHost.getUmdModule(file); if (!umdModule) { return; } const wrapperFunction = umdModule.wrapperFn; // We need to add new `require()` calls for each import in the CommonJS initializer renderCommonJsDependencies(output, wrapperFunction, imports); renderAmdDependencies(output, wrapperFunction, imports); renderGlobalDependencies(output, wrapperFunction, imports); renderFactoryParameters(output, wrapperFunction, imports); } /** * Add the exports to the bottom of the UMD module factory function. */ addExports( output: MagicString, entryPointBasePath: string, exports: ExportInfo[], importManager: ImportManager, file: ts.SourceFile): void { const umdModule = this.umdHost.getUmdModule(file); if (!umdModule) { return; } const factoryFunction = umdModule.factoryFn; const lastStatement = factoryFunction.body.statements[factoryFunction.body.statements.length - 1]; const insertionPoint = lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; exports.forEach(e => { const basePath = stripExtension(e.from); const relativePath = './' + relative(dirname(entryPointBasePath), basePath); const namedImport = entryPointBasePath !== basePath ? importManager.generateNamedImport(relativePath, e.identifier) : {symbol: e.identifier, moduleImport: null}; const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; output.appendRight(insertionPoint, exportStr); }); } addDirectExports( output: MagicString, exports: Reexport[], importManager: ImportManager, file: ts.SourceFile): void { const umdModule = this.umdHost.getUmdModule(file); if (!umdModule) { return; } const factoryFunction = umdModule.factoryFn; const lastStatement = factoryFunction.body.statements[factoryFunction.body.statements.length - 1]; const insertionPoint = lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; for (const e of exports) { const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName); const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`; output.appendRight(insertionPoint, exportStr); } } /** * Add the constants to the top of the UMD factory function. */ addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { if (constants === '') { return; } const umdModule = this.umdHost.getUmdModule(file); if (!umdModule) { return; } const factoryFunction = umdModule.factoryFn; const firstStatement = factoryFunction.body.statements[0]; const insertionPoint = firstStatement ? firstStatement.getStart() : factoryFunction.body.getStart() + 1; output.appendLeft(insertionPoint, '\n' + constants + '\n'); } } /** * Add dependencies to the CommonJS part of the UMD wrapper function. */ function renderCommonJsDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional); if (!conditional) { return; } const factoryCall = conditional.whenTrue; // Backup one char to account for the closing parenthesis on the call const injectionPoint = factoryCall.getEnd() - 1; const importString = imports.map(i => `require('${i.specifier}')`).join(','); output.appendLeft(injectionPoint, (factoryCall.arguments.length > 0 ? ',' : '') + importString); } /** * Add dependencies to the AMD part of the UMD wrapper function. */ function renderAmdDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const conditional = find(wrapperFunction.body.statements[0], isAmdConditional); if (!conditional) { return; } const amdDefineCall = conditional.whenTrue; const importString = imports.map(i => `'${i.specifier}'`).join(','); // The dependency array (if it exists) is the second to last argument // `define(id?, dependencies?, factory);` const factoryIndex = amdDefineCall.arguments.length - 1; const dependencyArray = amdDefineCall.arguments[factoryIndex - 1]; if (dependencyArray === undefined || !ts.isArrayLiteralExpression(dependencyArray)) { // No array provided: `define(factory)` or `define(id, factory)`. // Insert a new array in front the `factory` call. const injectionPoint = amdDefineCall.arguments[factoryIndex].getFullStart(); output.appendLeft(injectionPoint, `[${importString}],`); } else { // Already an array, add imports to the end of the array. // Backup one char to account for the closing square bracket on the array const injectionPoint = dependencyArray.getEnd() - 1; output.appendLeft( injectionPoint, (dependencyArray.elements.length > 0 ? ',' : '') + importString); } } /** * Add dependencies to the global part of the UMD wrapper function. */ function renderGlobalDependencies( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall); if (!globalFactoryCall) { return; } // Backup one char to account for the closing parenthesis after the argument list of the call. const injectionPoint = globalFactoryCall.getEnd() - 1; const importString = imports.map(i => `global.${getGlobalIdentifier(i)}`).join(','); output.appendLeft( injectionPoint, (globalFactoryCall.arguments.length > 0 ? ',' : '') + importString); } /** * Add dependency parameters to the UMD factory function. */ function renderFactoryParameters( output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { const wrapperCall = wrapperFunction.parent as ts.CallExpression; const secondArgument = wrapperCall.arguments[1]; if (!secondArgument) { return; } // Be resilient to the factory being inside parentheses const factoryFunction = ts.isParenthesizedExpression(secondArgument) ? secondArgument.expression : secondArgument; if (!ts.isFunctionExpression(factoryFunction)) { return; } const parameters = factoryFunction.parameters; const parameterString = imports.map(i => i.qualifier).join(','); if (parameters.length > 0) { const injectionPoint = parameters[parameters.length - 1].getEnd(); output.appendLeft(injectionPoint, ',' + parameterString); } else { // If there are no parameters then the factory function will look like: // function () { ... } // The AST does not give us a way to find the insertion point - between the two parentheses. // So we must use a regular expression on the text of the function. const injectionPoint = factoryFunction.getStart() + factoryFunction.getText().indexOf('()') + 1; output.appendLeft(injectionPoint, parameterString); } } /** * Is this node the CommonJS conditional expression in the UMD wrapper? */ function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { if (!ts.isConditionalExpression(value)) { return false; } if (!ts.isBinaryExpression(value.condition) || value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { return false; } if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'exports', 'module'))) { return false; } if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { return false; } return value.whenTrue.expression.text === 'factory'; } /** * Is this node the AMD conditional expression in the UMD wrapper? */ function isAmdConditional(value: ts.Node): value is AmdConditional { if (!ts.isConditionalExpression(value)) { return false; } if (!ts.isBinaryExpression(value.condition) || value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { return false; } if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'define'))) { return false; } if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { return false; } return value.whenTrue.expression.text === 'define'; } /** * Is this node the call to setup the global dependencies in the UMD wrapper? */ function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression { if (ts.isCallExpression(value) && !!value.parent) { // Be resilient to the value being part of a comma list value = isCommaExpression(value.parent) ? value.parent : value; // Be resilient to the value being inside parentheses value = ts.isParenthesizedExpression(value.parent) ? value.parent : value; return !!value.parent && ts.isConditionalExpression(value.parent) && value.parent.whenFalse === value; } else { return false; } } function isCommaExpression(value: ts.Node): value is ts.BinaryExpression { return ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.CommaToken; } /** * Compute a global identifier for the given import (`i`). * * The identifier used to access a package when using the "global" form of a UMD bundle usually * follows a special format where snake-case is conveted to camelCase and path separators are * converted to dots. In addition there are special cases such as `@angular` is mapped to `ng`. * * For example * * * `@ns/package/entry-point` => `ns.package.entryPoint` * * `@angular/common/testing` => `ng.common.testing` * * `@angular/platform-browser-dynamic` => `ng.platformBrowserDynamic` * * It is possible for packages to specify completely different identifiers for attaching the package * to the global, and so there is no guaranteed way to compute this. * Currently, this approach appears to work for the known scenarios; also it is not known how common * it is to use globals for importing packages. * * If it turns out that there are packages that are being used via globals, where this approach * fails, we should consider implementing a configuration based solution, similar to what would go * in a rollup configuration for mapping import paths to global indentifiers. */ function getGlobalIdentifier(i: Import): string { return i.specifier.replace(/^@angular\//, 'ng.') .replace(/^@/, '') .replace(/\//g, '.') .replace(/[-_]+(.?)/g, (_, c) => c.toUpperCase()) .replace(/^./, c => c.toLowerCase()); } function find(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T|undefined { return test(node) ? node : node.forEachChild(child => find(child, test)); } function oneOfBinaryConditions( node: ts.BinaryExpression, test: (expression: ts.Expression) => boolean) { return test(node.left) || test(node.right); } function isTypeOf(node: ts.Expression, ...types: string[]): boolean { return ts.isBinaryExpression(node) && ts.isTypeOfExpression(node.left) && ts.isIdentifier(node.left.expression) && types.indexOf(node.left.expression.text) !== -1; }