fix(ngcc): handle UMD factories that do not use all params (#34660)

In some cases, where a module imports a dependency
but does not actually use it, UMD bundlers may remove
the dependency parameter from the UMD factory function
definition.

For example:

```
import * as x from 'x';
import * as z from 'z';
export const y = x;
```

may result in a UMD bundle including:

```
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ?
        factory(exports, require('x'), require('z')) :
    typeof define === 'function' && define.amd ?
        define(['exports', 'x', 'z'], factory) :
    (global = global || self, factory(global.myBundle = {}, global.x));
}(this, (function (exports, x) { 'use strict';
...
})));
```

Note that while the `z` dependency is provide in the call,
the factory itself only accepts `exports` and `x` as parameters.

Previously ngcc appended new dependencies to the end of the factory
function, but this breaks in the above scenario. Now the new
dependencies are prefixed at the front of parameters/arguments
already in place.

Fixes #34653

PR Close #34660
This commit is contained in:
Pete Bacon Darwin
2020-01-07 14:40:17 +00:00
committed by Alex Rickabaugh
parent 4def99ed38
commit 83868be713
2 changed files with 85 additions and 20 deletions

View File

@ -27,7 +27,24 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
constructor(protected umdHost: UmdReflectionHost, isCore: boolean) { super(umdHost, isCore); }
/**
* Add the imports to the UMD module IIFE.
* Add the imports to the UMD module IIFE.
*
* Note that imports at "prepended" to the start of the parameter list of the factory function,
* and so also to the arguments passed to it when it is called.
* This is because there are scenarios where the factory function does not accept as many
* parameters as are passed as argument in the call. For example:
*
* ```
* (function (global, factory) {
* typeof exports === 'object' && typeof module !== 'undefined' ?
* factory(exports,require('x'),require('z')) :
* typeof define === 'function' && define.amd ?
* define(['exports', 'x', 'z'], factory) :
* (global = global || self, factory(global.myBundle = {}, global.x));
* }(this, (function (exports, x) { ... }
* ```
*
* (See that the `z` import is not being used by the factory function.)
*/
addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
if (imports.length === 0) {
@ -125,10 +142,13 @@ function renderCommonJsDependencies(
return;
}
const factoryCall = conditional.whenTrue;
// Backup one char to account for the closing parenthesis on the call
const injectionPoint = factoryCall.getEnd() - 1;
const injectionPoint = factoryCall.arguments.length > 0 ?
// Add extra dependencies before the first argument
factoryCall.arguments[0].getFullStart() :
// Backup one char to account for the closing parenthesis on the call
factoryCall.getEnd() - 1;
const importString = imports.map(i => `require('${i.specifier}')`).join(',');
output.appendLeft(injectionPoint, (factoryCall.arguments.length > 0 ? ',' : '') + importString);
output.appendLeft(injectionPoint, importString + (factoryCall.arguments.length > 0 ? ',' : ''));
}
/**
@ -152,11 +172,14 @@ function renderAmdDependencies(
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;
// Already an array
const injectionPoint = dependencyArray.elements.length > 0 ?
// Add imports before the first item.
dependencyArray.elements[0].getFullStart() :
// Backup one char to account for the closing square bracket on the array
dependencyArray.getEnd() - 1;
output.appendLeft(
injectionPoint, (dependencyArray.elements.length > 0 ? ',' : '') + importString);
injectionPoint, importString + (dependencyArray.elements.length > 0 ? ',' : ''));
}
}
@ -169,11 +192,14 @@ function renderGlobalDependencies(
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 injectionPoint = globalFactoryCall.arguments.length > 0 ?
// Add extra dependencies before the first argument
globalFactoryCall.arguments[0].getFullStart() :
// Backup one char to account for the closing parenthesis on the call
globalFactoryCall.getEnd() - 1;
const importString = imports.map(i => `global.${getGlobalIdentifier(i)}`).join(',');
output.appendLeft(
injectionPoint, (globalFactoryCall.arguments.length > 0 ? ',' : '') + importString);
injectionPoint, importString + (globalFactoryCall.arguments.length > 0 ? ',' : ''));
}
/**
@ -197,8 +223,8 @@ function renderFactoryParameters(
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);
const injectionPoint = parameters[0].getFullStart();
output.appendLeft(injectionPoint, parameterString + ',');
} else {
// If there are no parameters then the factory function will look like:
// function () { ... }