Skip to content
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

Implement import defer proposal transform support #15878

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 36 additions & 14 deletions packages/babel-helper-module-transforms/src/index.ts
Expand Up @@ -13,10 +13,10 @@ import normalizeModuleAndLoadMetadata, {
import type {
ImportInterop,
InteropType,
Lazy,
ModuleMetadata,
SourceModuleMetadata,
} from "./normalize-and-load-metadata.ts";
import * as Lazy from "./lazy-modules.ts";
import type { NodePath } from "@babel/traverse";

const {
Expand Down Expand Up @@ -57,7 +57,13 @@ export interface RewriteModuleStatementsAndPrepareHeaderOptions {
loose?: boolean;
importInterop?: ImportInterop;
noInterop?: boolean;
lazy?: Lazy;
lazy?: Lazy.Lazy;
getWrapperPayload?: (
source: string,
metadata: SourceModuleMetadata,
importNodes: t.Node[],
) => unknown;
wrapReference?: (ref: t.Expression, payload: unknown) => t.Expression | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we make this interface generic? So that TS knows that the payload passed into wrapReference must conforms to type of the return of getWrapperPayload.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, payload might be unknown because it might be generated by the hook of another plugin. The best we can do is

interface T {
  wrapReference?: (ref: t.Expression, payload: T | unknown) => t.Expression | null;
}

which does not improve types (T | unknown is unknown) but it might improve autocompletion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be generated by the hook of another plugin

Good point. That seems to be similar to the PluginPass's behaviour: Any plugin can augment the PluginPass but there is no guarantee that the PluginPass must not be changed by other plugins within the same pass.

esNamespaceOnly?: boolean;
filename: string | undefined;
constantReexports?: boolean | void;
Expand All @@ -80,7 +86,11 @@ export function rewriteModuleStatementsAndPrepareHeader(
strictMode,
noInterop,
importInterop = noInterop ? "none" : "babel",
// TODO(Babel 8): After that `lazy` implementation is moved to the CJS
// transform, remove this parameter.
lazy,
getWrapperPayload = Lazy.toGetWrapperPayload(lazy ?? false),
wrapReference = Lazy.wrapReference,
esNamespaceOnly,
filename,

Expand All @@ -100,7 +110,7 @@ export function rewriteModuleStatementsAndPrepareHeader(
const meta = normalizeModuleAndLoadMetadata(path, exportName, {
importInterop,
initializeReexports: constantReexports,
lazy,
getWrapperPayload,
esNamespaceOnly,
filename,
});
Expand All @@ -109,7 +119,7 @@ export function rewriteModuleStatementsAndPrepareHeader(
rewriteThis(path);
}

rewriteLiveReferences(path, meta);
rewriteLiveReferences(path, meta, wrapReference);

if (strictMode !== false) {
const hasStrict = path.node.directives.some(directive => {
Expand Down Expand Up @@ -140,6 +150,7 @@ export function rewriteModuleStatementsAndPrepareHeader(
...buildExportInitializationStatements(
path,
meta,
wrapReference,
constantReexports,
noIncompleteNsImportDetection,
),
Expand Down Expand Up @@ -204,6 +215,10 @@ export function buildNamespaceInitStatements(
metadata: ModuleMetadata,
sourceMetadata: SourceModuleMetadata,
constantReexports: boolean | void = false,
wrapReference: (
ref: t.Identifier,
payload: unknown,
) => t.Expression | null = Lazy.wrapReference,
) {
const statements = [];

Expand All @@ -221,17 +236,18 @@ export function buildNamespaceInitStatements(
);
}

const srcNamespace = sourceMetadata.lazy
? callExpression(srcNamespaceId, [])
: srcNamespaceId;
const srcNamespace =
wrapReference(srcNamespaceId, sourceMetadata.wrap) ?? srcNamespaceId;

if (constantReexports) {
statements.push(...buildReexportsFromMeta(metadata, sourceMetadata, true));
statements.push(
...buildReexportsFromMeta(metadata, sourceMetadata, true, wrapReference),
);
}
for (const exportName of sourceMetadata.reexportNamespace) {
// Assign export to namespace object.
statements.push(
(sourceMetadata.lazy
(!t.isIdentifier(srcNamespace)
? template.statement`
Object.defineProperty(EXPORTS, "NAME", {
enumerable: true,
Expand Down Expand Up @@ -278,10 +294,10 @@ function buildReexportsFromMeta(
meta: ModuleMetadata,
metadata: SourceModuleMetadata,
constantReexports: boolean,
wrapReference: (ref: t.Expression, payload: unknown) => t.Expression | null,
) {
const namespace = metadata.lazy
? callExpression(identifier(metadata.name), [])
: identifier(metadata.name);
let namespace: t.Expression = identifier(metadata.name);
namespace = wrapReference(namespace, metadata.wrap) ?? namespace;

const { stringSpecifiers } = meta;
return Array.from(metadata.reexports, ([exportName, importName]) => {
Expand Down Expand Up @@ -342,7 +358,7 @@ function buildESModuleHeader(
*/
function buildNamespaceReexport(
metadata: ModuleMetadata,
namespace: t.Identifier | t.CallExpression,
namespace: t.Expression,
constantReexports: boolean | void,
) {
return (
Expand Down Expand Up @@ -436,6 +452,7 @@ function buildExportNameListDeclaration(
function buildExportInitializationStatements(
programPath: NodePath,
metadata: ModuleMetadata,
wrapReference: (ref: t.Expression, payload: unknown) => t.Expression | null,
constantReexports: boolean | void = false,
noIncompleteNsImportDetection: boolean | void = false,
) {
Expand All @@ -460,7 +477,12 @@ function buildExportInitializationStatements(

for (const data of metadata.source.values()) {
if (!constantReexports) {
const reexportsStatements = buildReexportsFromMeta(metadata, data, false);
const reexportsStatements = buildReexportsFromMeta(
metadata,
data,
false,
wrapReference,
);
const reexports = [...data.reexports.keys()];
for (let i = 0; i < reexportsStatements.length; i++) {
initStatements.push([reexports[i], reexportsStatements[i]]);
Expand Down
37 changes: 37 additions & 0 deletions packages/babel-helper-module-transforms/src/lazy-modules.ts
@@ -0,0 +1,37 @@
// TODO: Move `lazy` implementation logic into the CommonJS plugin, since other
// modules systems do not support `lazy`.

import { types as t } from "@babel/core";
import {
type SourceModuleMetadata,
isSideEffectImport,
} from "./normalize-and-load-metadata.ts";

export type Lazy = boolean | string[] | ((source: string) => boolean);

export function toGetWrapperPayload(lazy: Lazy) {
return (source: string, metadata: SourceModuleMetadata): null | "lazy" => {
if (lazy === false) return null;
if (isSideEffectImport(metadata) || metadata.reexportAll) return null;
if (lazy === true) {
// 'true' means that local relative files are eagerly loaded and
// dependency modules are loaded lazily.
return /\./.test(source) ? null : "lazy";
}
if (Array.isArray(lazy)) {
return lazy.indexOf(source) === -1 ? null : "lazy";
}
if (typeof lazy === "function") {
return lazy(source) ? "lazy" : null;
}
throw new Error(`.lazy must be a boolean, string array, or function`);
};
}

export function wrapReference(
ref: t.Identifier,
payload: unknown,
): t.Expression | null {
if (payload === "lazy") return t.callExpression(ref, []);
return null;
}
Expand Up @@ -34,8 +34,6 @@ export type ImportInterop =
| "node"
| ((source: string, filename?: string) => "none" | "babel" | "node");

export type Lazy = boolean | string[] | ((source: string) => boolean);

export interface SourceModuleMetadata {
// A unique variable name to use for this namespace object. Centralized for simplicity.
name: string;
Expand All @@ -53,7 +51,7 @@ export interface SourceModuleMetadata {
reexportAll: null | {
loc: t.SourceLocation | undefined | null;
};
lazy?: Lazy;
wrap?: unknown;
referenced: boolean;
}

Expand Down Expand Up @@ -119,13 +117,17 @@ export default function normalizeModuleAndLoadMetadata(
{
importInterop,
initializeReexports = false,
lazy = false,
getWrapperPayload,
esNamespaceOnly = false,
filename,
}: {
importInterop: ImportInterop;
initializeReexports: boolean | void;
lazy: Lazy;
getWrapperPayload?: (
source: string,
metadata: SourceModuleMetadata,
importNodes: t.Node[],
) => unknown;
esNamespaceOnly: boolean;
filename: string;
},
Expand All @@ -139,7 +141,7 @@ export default function normalizeModuleAndLoadMetadata(

const { local, sources, hasExports } = getModuleMetadata(
programPath,
{ initializeReexports, lazy },
{ initializeReexports, getWrapperPayload },
stringSpecifiers,
);

Expand Down Expand Up @@ -231,11 +233,14 @@ function assertExportSpecifier(
function getModuleMetadata(
programPath: NodePath<t.Program>,
{
lazy,
getWrapperPayload,
initializeReexports,
}: {
// todo(flow-ts) changed from boolean, to match expected usage inside the function
lazy: boolean | string[] | ((source: string) => boolean);
getWrapperPayload?: (
source: string,
metadata: SourceModuleMetadata,
importNodes: t.Node[],
) => unknown;
initializeReexports: boolean | void;
},
stringSpecifiers: Set<string>,
Expand All @@ -246,8 +251,9 @@ function getModuleMetadata(
stringSpecifiers,
);

const importNodes = new Map<string, t.Node[]>();
const sourceData = new Map<string, SourceModuleMetadata>();
const getData = (sourceNode: t.StringLiteral) => {
const getData = (sourceNode: t.StringLiteral, node: t.Node) => {
const source = sourceNode.value;

let data = sourceData.get(source);
Expand All @@ -270,18 +276,29 @@ function getModuleMetadata(
reexportNamespace: new Set(),
reexportAll: null,

lazy: false,
wrap: null,

// @ts-expect-error lazy is not listed in the type.
// This is needed for compatibility with older version of the commonjs
// plusing.
// TODO(Babel 8): Remove this
get lazy() {
return this.wrap === "lazy";
},

referenced: false,
};
sourceData.set(source, data);
importNodes.set(source, [node]);
} else {
importNodes.get(source).push(node);
}
return data;
};
let hasExports = false;
programPath.get("body").forEach(child => {
if (child.isImportDeclaration()) {
const data = getData(child.node.source);
const data = getData(child.node.source, child.node);
if (!data.loc) data.loc = child.node.loc;

child.get("specifiers").forEach(spec => {
Expand Down Expand Up @@ -334,7 +351,7 @@ function getModuleMetadata(
});
} else if (child.isExportAllDeclaration()) {
hasExports = true;
const data = getData(child.node.source);
const data = getData(child.node.source, child.node);
if (!data.loc) data.loc = child.node.loc;

data.reexportAll = {
Expand All @@ -343,7 +360,7 @@ function getModuleMetadata(
data.referenced = true;
} else if (child.isExportNamedDeclaration() && child.node.source) {
hasExports = true;
const data = getData(child.node.source);
const data = getData(child.node.source, child.node);
if (!data.loc) data.loc = child.node.loc;

child.get("specifiers").forEach(spec => {
Expand Down Expand Up @@ -404,22 +421,13 @@ function getModuleMetadata(
}
}

for (const [source, metadata] of sourceData) {
if (
lazy !== false &&
!(isSideEffectImport(metadata) || metadata.reexportAll)
) {
if (lazy === true) {
// 'true' means that local relative files are eagerly loaded and
// dependency modules are loaded lazily.
metadata.lazy = !/\./.test(source);
} else if (Array.isArray(lazy)) {
metadata.lazy = lazy.indexOf(source) !== -1;
} else if (typeof lazy === "function") {
metadata.lazy = lazy(source);
} else {
throw new Error(`.lazy must be a boolean, string array, or function`);
}
if (getWrapperPayload) {
for (const [source, metadata] of sourceData) {
metadata.wrap = getWrapperPayload(
source,
metadata,
importNodes.get(source),
);
}
}

Expand Down
Expand Up @@ -7,7 +7,6 @@ import type { ModuleMetadata } from "./normalize-and-load-metadata.ts";

const {
assignmentExpression,
callExpression,
cloneNode,
expressionStatement,
getOuterBindingIdentifiers,
Expand Down Expand Up @@ -72,6 +71,7 @@ function isInType(path: NodePath) {
export default function rewriteLiveReferences(
programPath: NodePath<t.Program>,
metadata: ModuleMetadata,
wrapReference: (ref: t.Expression, payload: unknown) => null | t.Expression,
) {
const imported = new Map();
const exported = new Map();
Expand Down Expand Up @@ -135,23 +135,22 @@ export default function rewriteLiveReferences(
scope: programPath.scope,
imported, // local / import
exported, // local name => exported name list
buildImportReference: ([source, importName, localName], identNode) => {
buildImportReference([source, importName, localName], identNode) {
const meta = metadata.source.get(source);
meta.referenced = true;

if (localName) {
if (meta.lazy) {
identNode = callExpression(
// @ts-expect-error Fixme: we should handle the case when identNode is a JSXIdentifier
identNode,
[],
);
if (meta.wrap) {
// @ts-expect-error Fixme: we should handle the case when identNode is a JSXIdentifier
identNode = wrapReference(identNode, meta.wrap) ?? identNode;
}
return identNode;
}

let namespace: t.Expression = identifier(meta.name);
if (meta.lazy) namespace = callExpression(namespace, []);
if (meta.wrap) {
namespace = wrapReference(namespace, meta.wrap) ?? namespace;
}

if (importName === "default" && meta.interop === "node-default") {
return namespace;
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-helpers/src/helpers-generated.ts
Expand Up @@ -65,6 +65,10 @@ export default Object.freeze({
"7.22.0",
'function dispose_SuppressedError(r,e){return"undefined"!=typeof SuppressedError?dispose_SuppressedError=SuppressedError:(dispose_SuppressedError=function(r,e){this.suppressed=r,this.error=e,this.stack=(new Error).stack},dispose_SuppressedError.prototype=Object.create(Error.prototype,{constructor:{value:dispose_SuppressedError,writable:!0,configurable:!0}})),new dispose_SuppressedError(r,e)}export default function _dispose(r,e,s){function next(){for(;r.length>0;)try{var o=r.pop(),p=o.d.call(o.v);if(o.a)return Promise.resolve(p).then(next,err)}catch(r){return err(r)}if(s)throw e}function err(r){return e=s?new dispose_SuppressedError(r,e):r,s=!0,next()}return next()}',
),
importDeferProxy: helper(
"7.22.0",
"export default function _importDeferProxy(e){var t=null,constValue=function(e){return function(){return e}},proxy=function(r){return function(n,o,f){return null===t&&(t=e()),r(t,o,f)}};return new Proxy({},{defineProperty:constValue(!1),deleteProperty:constValue(!1),get:proxy(Reflect.get),getOwnPropertyDescriptor:proxy(Reflect.getOwnPropertyDescriptor),getPrototypeOf:constValue(null),isExtensible:constValue(!1),has:proxy(Reflect.has),ownKeys:proxy(Reflect.ownKeys),preventExtensions:constValue(!0),set:constValue(!1),setPrototypeOf:constValue(!1)})}",
),
iterableToArrayLimit: helper(
"7.0.0-beta.0",
'export default function _iterableToArrayLimit(r,l){var t=null==r?null:"undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(null!=t){var e,n,i,u,a=[],f=!0,o=!1;try{if(i=(t=t.call(r)).next,0===l){if(Object(t)!==t)return;f=!1}else for(;!(f=(e=i.call(t)).done)&&(a.push(e.value),a.length!==l);f=!0);}catch(r){o=!0,n=r}finally{try{if(!f&&null!=t.return&&(u=t.return(),Object(u)!==u))return}finally{if(o)throw n}}return a}}',
Expand Down