Skip to content

Commit

Permalink
Support injecting polyfill imports in a specific order (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo authored Jan 18, 2024
1 parent 3a657bd commit 7bac97b
Show file tree
Hide file tree
Showing 68 changed files with 1,038 additions and 994 deletions.
10 changes: 7 additions & 3 deletions docs/polyfill-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,16 @@ A `meta` object describes the statement or expression which triggered the call t
When calling a provider function (e.g. `usageGlobal`), `@babel/helper-define-polyfill-provider` will provide it a few utilities to easily inject the necessary `import` statements or `require` calls, depending on the source type. Polyfill providers shouldn't worry about which AST represents an import, or about the source type of the file being transpiled.

- `utils.injectGlobalImport(url: string)` can be used to inject side-effectful global imports. It is usually called when injecting global polyfills.
- `utils.injectGlobalImport(url: string, polyfillName?: string)` can be used to inject side-effectful global imports. It is usually called when injecting global polyfills.
For example, `utils.injectGlobalImport("my-polyfill")` would generate this code:

```js
import "my-polyfill";
```

- `utils.injectNamedImport(url: string, name: string, hint?: string)` and `utils.injectDefaultImport(url: string, hint?: string)` are used to inject named or defaults import. They both return an identifier referencing the imported value.
If `polyfillName` is specified, imports are injected respecting the order defined in [`provider.polyfills`](#providerpolyfills-string---name-string-support-).

- `utils.injectNamedImport(url: string, name: string, hint?: string, polyfillName?: string)` and `utils.injectDefaultImport(url: string, hint?: string, polyfillName?: string)` are used to inject named or defaults import. They both return an identifier referencing the imported value.
The optional `hint` parameter can be used to generate a nice-looking alias for the import.
For example, `utils.injectNamedImport("array-polyfills", "from, "Array.from")` would generate this code:

Expand All @@ -222,6 +224,8 @@ When calling a provider function (e.g. `usageGlobal`), `@babel/helper-define-pol
}
```

If `polyfillName` is specified, imports are injected respecting the order defined in [`provider.polyfills`](#providerpolyfills-string---name-string-support-).

## The `ProviderApi` parameter

While some utilities are provided in the `utils` object, some of them are provided in the `api` object. The main different is that `utils` methods act on a specific input source file, while `api` methods provide info about how the plugin was configured and are not directly related to the transformed source code.
Expand Down Expand Up @@ -265,7 +269,7 @@ Sometimes you might need to inject an import outside of the `entryGlobal`/`usage
You can use this method to create a new `utils` object with all its utilities, attached to the file the current `NodePath` belongs to.
```js
export default function({ getUtils }) {
export default function ({ getUtils }) {
return {
// ...
visitor: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ import { types as t } from "@babel/core";

type StrMap<K> = Map<string, K>;

export default class ImportsCache {
export default class ImportsCachedInjector {
_imports: WeakMap<NodePath<t.Program>, StrMap<string>>;
_anonymousImports: WeakMap<NodePath<t.Program>, Set<string>>;
_lastImports: WeakMap<NodePath<t.Program>, NodePath<t.Node>>;
_lastImports: WeakMap<
NodePath<t.Program>,
Array<{ path: NodePath<t.Node>; index: number }>
>;
_resolver: (url: string) => string;
_getPreferredIndex: (url: string) => number;

constructor(resolver: (url: string) => string) {
constructor(
resolver: (url: string) => string,
getPreferredIndex: (url: string) => number,
) {
this._imports = new WeakMap();
this._anonymousImports = new WeakMap();
this._lastImports = new WeakMap();
this._resolver = resolver;
this._getPreferredIndex = getPreferredIndex;
}

storeAnonymous(
programPath: NodePath<t.Program>,
url: string,
// eslint-disable-next-line no-undef
moduleName: string,
getVal: (isScript: boolean, source: t.StringLiteral) => t.Node,
) {
const key = this._normalizeKey(programPath, url);
Expand All @@ -36,13 +44,14 @@ export default class ImportsCache {
t.stringLiteral(this._resolver(url)),
);
imports.add(key);
this._injectImport(programPath, node);
this._injectImport(programPath, node, moduleName);
}

storeNamed(
programPath: NodePath<t.Program>,
url: string,
name: string,
moduleName: string,
getVal: (
isScript: boolean,
// eslint-disable-next-line no-undef
Expand All @@ -65,51 +74,56 @@ export default class ImportsCache {
t.identifier(name),
);
imports.set(key, id);
this._injectImport(programPath, node);
this._injectImport(programPath, node, moduleName);
}

return t.identifier(imports.get(key));
}

_injectImport(programPath: NodePath<t.Program>, node: t.Node) {
const lastImport = this._lastImports.get(programPath);
let newNodes: [NodePath];
if (
lastImport &&
lastImport.node &&
_injectImport(
programPath: NodePath<t.Program>,
node: t.Node,
moduleName: string,
) {
const newIndex = this._getPreferredIndex(moduleName);
const lastImports = this._lastImports.get(programPath) ?? [];

const isPathStillValid = (path: NodePath) =>
path.node &&
// Sometimes the AST is modified and the "last import"
// we have has been replaced
lastImport.parent === programPath.node &&
lastImport.container === programPath.node.body
) {
newNodes = lastImport.insertAfter(node);
} else {
newNodes = programPath.unshiftContainer("body", node);
}
const newNode = newNodes[newNodes.length - 1];
this._lastImports.set(programPath, newNode);

/*
let lastImport;
programPath.get("body").forEach(path => {
if (path.isImportDeclaration()) lastImport = path;
if (
path.isExpressionStatement() &&
isRequireCall(path.get("expression"))
) {
lastImport = path;
path.parent === programPath.node &&
path.container === programPath.node.body;

let last: NodePath;

if (newIndex === Infinity) {
// Fast path: we can always just insert at the end if newIndex is `Infinity`
if (lastImports.length > 0) {
last = lastImports[lastImports.length - 1].path;
if (!isPathStillValid(last)) last = undefined;
}
if (
path.isVariableDeclaration() &&
path.get("declarations").length === 1 &&
(isRequireCall(path.get("declarations.0.init")) ||
(path.get("declarations.0.init").isMemberExpression() &&
isRequireCall(path.get("declarations.0.init.object"))))
) {
lastImport = path;
} else {
for (const [i, data] of lastImports.entries()) {
const { path, index } = data;
if (isPathStillValid(path)) {
if (newIndex < index) {
const [newPath] = path.insertBefore(node);
lastImports.splice(i, 0, { path: newPath, index: newIndex });
return;
}
last = path;
}
}
});*/
}

if (last) {
const [newPath] = last.insertAfter(node);
lastImports.push({ path: newPath, index: newIndex });
} else {
const [newPath] = programPath.unshiftContainer("body", node);
this._lastImports.set(programPath, [{ path: newPath, index: newIndex }]);
}
}

_ensure<C extends Map<string, any> | Set<string>>(
Expand Down
27 changes: 16 additions & 11 deletions packages/babel-helper-define-polyfill-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import _getTargets, {
const getTargets = _getTargets.default || _getTargets;

import { createUtilsGetter } from "./utils";
import ImportsCache from "./imports-cache";
import ImportsCachedInjector from "./imports-injector";
import {
stringifyTargetsMultiline,
presetEnvSilentDebugHeader,
Expand Down Expand Up @@ -163,18 +163,19 @@ function instantiateProvider<Options>(
absoluteImports,
} = resolveOptions<Options>(options, babelApi);

const getUtils = createUtilsGetter(
new ImportsCache(moduleName =>
deps.resolve(dirname, moduleName, absoluteImports),
),
);

// eslint-disable-next-line prefer-const
let include, exclude;
let polyfillsSupport;
let polyfillsNames;
let polyfillsNames: Map<string, number> | undefined;
let filterPolyfills;

const getUtils = createUtilsGetter(
new ImportsCachedInjector(
moduleName => deps.resolve(dirname, moduleName, absoluteImports),
(name: string) => polyfillsNames?.get(name) ?? Infinity,
),
);

const depsCache = new Map();

const api: ProviderApi = {
Expand Down Expand Up @@ -256,14 +257,18 @@ function instantiateProvider<Options>(
}

if (Array.isArray(provider.polyfills)) {
polyfillsNames = new Set(provider.polyfills);
polyfillsNames = new Map(
provider.polyfills.map((name, index) => [name, index]),
);
filterPolyfills = provider.filterPolyfills;
} else if (provider.polyfills) {
polyfillsNames = new Set(Object.keys(provider.polyfills));
polyfillsNames = new Map(
Object.keys(provider.polyfills).map((name, index) => [name, index]),
);
polyfillsSupport = provider.polyfills;
filterPolyfills = provider.filterPolyfills;
} else {
polyfillsNames = new Set();
polyfillsNames = new Map();
}

({ include, exclude } = validateIncludeExclude(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function buldDuplicatesError(duplicates) {

export function validateIncludeExclude(
provider: string,
polyfills: Set<string>,
polyfills: Map<string, unknown>,
includePatterns: Pattern[],
excludePatterns: Pattern[],
) {
Expand All @@ -43,7 +43,7 @@ export function validateIncludeExclude(
if (!regexp) return false;

let matched = false;
for (const polyfill of polyfills) {
for (const polyfill of polyfills.keys()) {
if (regexp.test(polyfill)) {
matched = true;
current.add(polyfill);
Expand Down
15 changes: 12 additions & 3 deletions packages/babel-helper-define-polyfill-provider/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,18 @@ export type ProviderApi = {
};

export type Utils = {
injectGlobalImport(url: string): void;
injectNamedImport(url: string, name: string, hint?: string): t.Identifier;
injectDefaultImport(url: string, hint?: string): t.Identifier;
injectGlobalImport(url: string, moduleName?: string): void;
injectNamedImport(
url: string,
name: string,
hint?: string,
moduleName?: string,
): t.Identifier;
injectDefaultImport(
url: string,
hint?: string,
moduleName?: string,
): t.Identifier;
};

export type ProviderResult = {
Expand Down
60 changes: 36 additions & 24 deletions packages/babel-helper-define-polyfill-provider/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { types as t, template } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import type { Utils } from "./types";
import type ImportsCache from "./imports-cache";
import type ImportsCachedInjector from "./imports-injector";

export function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
const result = new Set<T>();
Expand Down Expand Up @@ -126,41 +126,53 @@ function hoist(node: t.Node) {
return node;
}

export function createUtilsGetter(cache: ImportsCache) {
export function createUtilsGetter(cache: ImportsCachedInjector) {
return (path: NodePath): Utils => {
const prog = path.findParent(p => p.isProgram()) as NodePath<t.Program>;

return {
injectGlobalImport(url) {
cache.storeAnonymous(prog, url, (isScript, source) => {
injectGlobalImport(url, moduleName) {
cache.storeAnonymous(prog, url, moduleName, (isScript, source) => {
return isScript
? template.statement.ast`require(${source})`
: t.importDeclaration([], source);
});
},
injectNamedImport(url, name, hint = name) {
return cache.storeNamed(prog, url, name, (isScript, source, name) => {
const id = prog.scope.generateUidIdentifier(hint);
return {
node: isScript
? hoist(template.statement.ast`
injectNamedImport(url, name, hint = name, moduleName) {
return cache.storeNamed(
prog,
url,
name,
moduleName,
(isScript, source, name) => {
const id = prog.scope.generateUidIdentifier(hint);
return {
node: isScript
? hoist(template.statement.ast`
var ${id} = require(${source}).${name}
`)
: t.importDeclaration([t.importSpecifier(id, name)], source),
name: id.name,
};
});
: t.importDeclaration([t.importSpecifier(id, name)], source),
name: id.name,
};
},
);
},
injectDefaultImport(url, hint = url) {
return cache.storeNamed(prog, url, "default", (isScript, source) => {
const id = prog.scope.generateUidIdentifier(hint);
return {
node: isScript
? hoist(template.statement.ast`var ${id} = require(${source})`)
: t.importDeclaration([t.importDefaultSpecifier(id)], source),
name: id.name,
};
});
injectDefaultImport(url, hint = url, moduleName) {
return cache.storeNamed(
prog,
url,
"default",
moduleName,
(isScript, source) => {
const id = prog.scope.generateUidIdentifier(hint);
return {
node: isScript
? hoist(template.statement.ast`var ${id} = require(${source})`)
: t.importDeclaration([t.importDefaultSpecifier(id)], source),
name: id.name,
};
},
);
},
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin-polyfill-corejs3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default defineProvider<Options>(function (
function maybeInjectGlobalImpl(name: string, utils) {
if (shouldInjectPolyfill(name)) {
debug(name);
utils.injectGlobalImport(coreJSModule(name));
utils.injectGlobalImport(coreJSModule(name), name);
return true;
}
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "core-js/modules/es.object.from-entries.js";
import "core-js/modules/esnext.set.add-all.js";
import "core-js/modules/esnext.set.delete-all.js";
import "core-js/modules/esnext.set.difference.js";
Expand All @@ -16,5 +17,4 @@ import "core-js/modules/esnext.set.reduce.js";
import "core-js/modules/esnext.set.some.js";
import "core-js/modules/esnext.set.symmetric-difference.js";
import "core-js/modules/esnext.set.union.js";
import "core-js/modules/es.object.from-entries.js";
import "core-js/modules/esnext.string.replace-all.js";
Loading

0 comments on commit 7bac97b

Please sign in to comment.