From 35c0651fcb6936a9cf166c689260623ad3a67484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 18 Sep 2023 17:23:31 +0200 Subject: [PATCH] Implement `import defer` proposal transform support (#15878) --- .../src/index.ts | 50 ++++++-- .../src/lazy-modules.ts | 37 ++++++ .../src/normalize-and-load-metadata.ts | 68 +++++----- .../src/rewrite-live-references.ts | 17 ++- .../babel-helpers/src/helpers-generated.ts | 4 + .../src/helpers/importDeferProxy.js | 31 +++++ .../.npmignore | 3 + .../README.md | 19 +++ .../package.json | 57 +++++++++ .../src/index.ts | 120 ++++++++++++++++++ .../integration/input.mjs | 7 + .../integration/options.json | 6 + .../integration/output.js | 17 +++ .../exec/get-own-property-names/dep.cjs | 4 + .../exec/get-own-property-names/exec.js | 14 ++ .../get-own-property-names/side-channel.cjs | 1 + .../test/fixtures/exec/not-referenced/dep.cjs | 1 + .../test/fixtures/exec/not-referenced/exec.js | 4 + .../exec/not-referenced/side-channel.cjs | 1 + .../test/fixtures/exec/options.json | 4 + .../exec/reference-plain-only/dep.cjs | 4 + .../exec/reference-plain-only/exec.js | 13 ++ .../reference-plain-only/side-channel.cjs | 1 + .../exec/reference-property-only/dep.cjs | 4 + .../exec/reference-property-only/exec.js | 9 ++ .../reference-property-only/side-channel.cjs | 1 + .../transform/not-referenced/input.mjs | 1 + .../transform/not-referenced/output.js | 1 + .../test/fixtures/transform/options.json | 3 + .../reference-plain-and-property/input.mjs | 6 + .../reference-plain-and-property/output.js | 7 + .../transform/reference-plain-only/input.mjs | 5 + .../transform/reference-plain-only/output.js | 6 + .../reference-property-and-plain/input.mjs | 6 + .../reference-property-and-plain/output.js | 7 + .../reference-property-only/input.mjs | 5 + .../reference-property-only/output.js | 9 ++ .../with-full-import-after/input.mjs | 7 + .../with-full-import-after/output.js | 8 ++ .../with-full-import-before/input.mjs | 7 + .../with-full-import-before/output.js | 8 ++ .../test/index.js | 3 + .../test/package.json | 1 + .../README.md | 19 +++ .../package.json | 54 ++++++++ .../src/index.ts | 13 ++ .../src/hooks.ts | 65 ++++++++++ .../src/index.ts | 46 ++++--- .../src/lazy.ts | 41 ++++++ packages/babel-runtime-corejs2/package.json | 9 ++ packages/babel-runtime-corejs3/package.json | 9 ++ packages/babel-runtime/package.json | 9 ++ packages/babel-standalone/package.json | 1 + .../scripts/pluginConfig.json | 3 +- .../babel-standalone/src/generated/plugins.ts | 3 + tsconfig.json | 8 ++ yarn.lock | 26 ++++ 57 files changed, 819 insertions(+), 74 deletions(-) create mode 100644 packages/babel-helper-module-transforms/src/lazy-modules.ts create mode 100644 packages/babel-helpers/src/helpers/importDeferProxy.js create mode 100644 packages/babel-plugin-proposal-import-defer/.npmignore create mode 100644 packages/babel-plugin-proposal-import-defer/README.md create mode 100644 packages/babel-plugin-proposal-import-defer/package.json create mode 100644 packages/babel-plugin-proposal-import-defer/src/index.ts create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/options.json create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/dep.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/exec.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/side-channel.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/dep.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/exec.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/side-channel.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/options.json create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/dep.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/exec.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/side-channel.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/dep.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/exec.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/side-channel.cjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/options.json create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/input.mjs create mode 100644 packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/output.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/index.js create mode 100644 packages/babel-plugin-proposal-import-defer/test/package.json create mode 100644 packages/babel-plugin-syntax-import-defer/README.md create mode 100644 packages/babel-plugin-syntax-import-defer/package.json create mode 100644 packages/babel-plugin-syntax-import-defer/src/index.ts create mode 100644 packages/babel-plugin-transform-modules-commonjs/src/hooks.ts create mode 100644 packages/babel-plugin-transform-modules-commonjs/src/lazy.ts diff --git a/packages/babel-helper-module-transforms/src/index.ts b/packages/babel-helper-module-transforms/src/index.ts index 0f6560be9f0a..5afadb10322b 100644 --- a/packages/babel-helper-module-transforms/src/index.ts +++ b/packages/babel-helper-module-transforms/src/index.ts @@ -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 { @@ -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; esNamespaceOnly?: boolean; filename: string | undefined; constantReexports?: boolean | void; @@ -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, @@ -100,7 +110,7 @@ export function rewriteModuleStatementsAndPrepareHeader( const meta = normalizeModuleAndLoadMetadata(path, exportName, { importInterop, initializeReexports: constantReexports, - lazy, + getWrapperPayload, esNamespaceOnly, filename, }); @@ -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 => { @@ -140,6 +150,7 @@ export function rewriteModuleStatementsAndPrepareHeader( ...buildExportInitializationStatements( path, meta, + wrapReference, constantReexports, noIncompleteNsImportDetection, ), @@ -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 = []; @@ -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, @@ -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]) => { @@ -342,7 +358,7 @@ function buildESModuleHeader( */ function buildNamespaceReexport( metadata: ModuleMetadata, - namespace: t.Identifier | t.CallExpression, + namespace: t.Expression, constantReexports: boolean | void, ) { return ( @@ -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, ) { @@ -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]]); diff --git a/packages/babel-helper-module-transforms/src/lazy-modules.ts b/packages/babel-helper-module-transforms/src/lazy-modules.ts new file mode 100644 index 000000000000..c926736c6a5e --- /dev/null +++ b/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; +} diff --git a/packages/babel-helper-module-transforms/src/normalize-and-load-metadata.ts b/packages/babel-helper-module-transforms/src/normalize-and-load-metadata.ts index fb574db29fa6..d21e606338b8 100644 --- a/packages/babel-helper-module-transforms/src/normalize-and-load-metadata.ts +++ b/packages/babel-helper-module-transforms/src/normalize-and-load-metadata.ts @@ -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; @@ -53,7 +51,7 @@ export interface SourceModuleMetadata { reexportAll: null | { loc: t.SourceLocation | undefined | null; }; - lazy?: Lazy; + wrap?: unknown; referenced: boolean; } @@ -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; }, @@ -139,7 +141,7 @@ export default function normalizeModuleAndLoadMetadata( const { local, sources, hasExports } = getModuleMetadata( programPath, - { initializeReexports, lazy }, + { initializeReexports, getWrapperPayload }, stringSpecifiers, ); @@ -231,11 +233,14 @@ function assertExportSpecifier( function getModuleMetadata( programPath: NodePath, { - 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, @@ -246,8 +251,9 @@ function getModuleMetadata( stringSpecifiers, ); + const importNodes = new Map(); const sourceData = new Map(); - const getData = (sourceNode: t.StringLiteral) => { + const getData = (sourceNode: t.StringLiteral, node: t.Node) => { const source = sourceNode.value; let data = sourceData.get(source); @@ -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 => { @@ -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 = { @@ -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 => { @@ -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), + ); } } diff --git a/packages/babel-helper-module-transforms/src/rewrite-live-references.ts b/packages/babel-helper-module-transforms/src/rewrite-live-references.ts index a9b7cce49c6c..a7e55896d94c 100644 --- a/packages/babel-helper-module-transforms/src/rewrite-live-references.ts +++ b/packages/babel-helper-module-transforms/src/rewrite-live-references.ts @@ -7,7 +7,6 @@ import type { ModuleMetadata } from "./normalize-and-load-metadata.ts"; const { assignmentExpression, - callExpression, cloneNode, expressionStatement, getOuterBindingIdentifiers, @@ -72,6 +71,7 @@ function isInType(path: NodePath) { export default function rewriteLiveReferences( programPath: NodePath, metadata: ModuleMetadata, + wrapReference: (ref: t.Expression, payload: unknown) => null | t.Expression, ) { const imported = new Map(); const exported = new Map(); @@ -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; diff --git a/packages/babel-helpers/src/helpers-generated.ts b/packages/babel-helpers/src/helpers-generated.ts index a9c37f955b97..fb1f14d8d959 100644 --- a/packages/babel-helpers/src/helpers-generated.ts +++ b/packages/babel-helpers/src/helpers-generated.ts @@ -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}}', diff --git a/packages/babel-helpers/src/helpers/importDeferProxy.js b/packages/babel-helpers/src/helpers/importDeferProxy.js new file mode 100644 index 000000000000..d14b5e7776a3 --- /dev/null +++ b/packages/babel-helpers/src/helpers/importDeferProxy.js @@ -0,0 +1,31 @@ +/* @minVersion 7.22.0 */ +export default function _importDeferProxy(init) { + var ns = null; + var constValue = function (v) { + return function () { + return v; + }; + }; + var proxy = function (run) { + return function (arg1, arg2, arg3) { + if (ns === null) ns = init(); + return run(ns, arg2, arg3); + }; + }; + return new Proxy( + {}, + { + defineProperty: constValue(false), + deleteProperty: constValue(false), + get: proxy(Reflect.get), + getOwnPropertyDescriptor: proxy(Reflect.getOwnPropertyDescriptor), + getPrototypeOf: constValue(null), + isExtensible: constValue(false), + has: proxy(Reflect.has), + ownKeys: proxy(Reflect.ownKeys), + preventExtensions: constValue(true), + set: constValue(false), + setPrototypeOf: constValue(false), + } + ); +} diff --git a/packages/babel-plugin-proposal-import-defer/.npmignore b/packages/babel-plugin-proposal-import-defer/.npmignore new file mode 100644 index 000000000000..f9806945836e --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-proposal-import-defer/README.md b/packages/babel-plugin-proposal-import-defer/README.md new file mode 100644 index 000000000000..fa338796e480 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/README.md @@ -0,0 +1,19 @@ +# @babel/plugin-proposal-import-defer + +> Support `import defer` when compiling to CommonJS + +See our website [@babel/plugin-proposal-import-defer](https://babeljs.io/docs/babel-plugin-proposal-import-defer) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/plugin-proposal-import-defer +``` + +or using yarn: + +```sh +yarn add @babel/plugin-proposal-import-defer --dev +``` diff --git a/packages/babel-plugin-proposal-import-defer/package.json b/packages/babel-plugin-proposal-import-defer/package.json new file mode 100644 index 000000000000..7acac2dfe69c --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/package.json @@ -0,0 +1,57 @@ +{ + "name": "@babel/plugin-proposal-import-defer", + "version": "7.22.5", + "description": "Support `import defer` when compiling to CommonJS", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-plugin-proposal-import-defer" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "./lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": { + "@babel/helper-plugin-utils": "workspace:^", + "@babel/plugin-syntax-import-defer": "workspace:^", + "@babel/plugin-transform-modules-commonjs": "workspace:^" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "devDependencies": { + "@babel/core": "workspace:^", + "@babel/helper-plugin-test-runner": "workspace:^" + }, + "engines": { + "node": ">=6.9.0" + }, + "author": "The Babel Team (https://babel.dev/team)", + "conditions": { + "BABEL_8_BREAKING": [ + { + "engines": { + "node": "^16.20.0 || ^18.16.0 || >=20.0.0" + } + }, + { + "exports": null + } + ], + "USE_ESM": [ + { + "type": "module" + }, + null + ] + }, + "exports": { + ".": "./lib/index.js", + "./package.json": "./package.json" + }, + "type": "commonjs" +} diff --git a/packages/babel-plugin-proposal-import-defer/src/index.ts b/packages/babel-plugin-proposal-import-defer/src/index.ts new file mode 100644 index 000000000000..7a8775d24c61 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/src/index.ts @@ -0,0 +1,120 @@ +import { declare } from "@babel/helper-plugin-utils"; +import type { types as t } from "@babel/core"; +import type { Scope } from "@babel/traverse"; +import { defineCommonJSHook } from "@babel/plugin-transform-modules-commonjs"; + +import syntaxImportDefer from "@babel/plugin-syntax-import-defer"; + +export default declare(api => { + api.assertVersion(7); + // We need the explicit type annotation otherwise when using t.assert* ts + // reports that 'Assertions require every name in the call target to be + // declared with an explicit type annotation' + const t: typeof api.types = api.types; + const { template } = api; + + function allReferencesAreProps(scope: Scope, node: t.ImportDeclaration) { + const specifier = node.specifiers[0]; + t.assertImportNamespaceSpecifier(specifier); + + const binding = scope.getOwnBinding(specifier.local.name); + return !!binding?.referencePaths.every(path => + path.parentPath.isMemberExpression({ object: path.node }), + ); + } + + return { + name: "proposal-import-defer", + + inherits: syntaxImportDefer, + + pre() { + const { file } = this; + + defineCommonJSHook(file, { + name: PACKAGE_JSON.name, + version: PACKAGE_JSON.version, + getWrapperPayload(source, metadata, importNodes) { + let needsProxy = false; + for (const node of importNodes) { + if (!t.isImportDeclaration(node)) return null; + if (node.phase !== "defer") return null; + if (!allReferencesAreProps(file.scope, node)) needsProxy = true; + } + return needsProxy ? "defer/proxy" : "defer/function"; + }, + buildRequireWrapper(name, init, payload, referenced) { + if (payload === "defer/proxy") { + if (!referenced) return false; + return template.statement.ast` + var ${name} = ${file.addHelper("importDeferProxy")}( + () => ${init} + ) + `; + } + if (payload === "defer/function") { + if (!referenced) return false; + return template.statement.ast` + function ${name}(data) { + ${name} = () => data; + return data = ${init}; + } + `; + } + }, + wrapReference(ref, payload) { + if (payload === "defer/function") return t.callExpression(ref, []); + }, + }); + }, + + visitor: { + Program(path) { + if (this.file.get("@babel/plugin-transform-modules-*") !== "commonjs") { + throw new Error( + `@babel/plugin-proposal-import-defer can only be used when` + + ` transpiling modules to CommonJS.`, + ); + } + + // Move all deferred imports to the end, so that in case of + // import defer * as a from "a" + // import "b" + // import "a" + // we have the correct evaluation order + + const eagerImports = new Set(); + + for (const child of path.get("body")) { + if ( + (child.isImportDeclaration() && child.node.phase == null) || + (child.isExportNamedDeclaration() && child.node.source !== null) || + child.isExportAllDeclaration() + ) { + const specifier = child.node.source!.value; + if (!eagerImports.has(specifier)) { + eagerImports.add(specifier); + } + } + } + + const importsToPush = []; + for (const child of path.get("body")) { + if (child.isImportDeclaration({ phase: "defer" })) { + const specifier = child.node.source.value; + if (!eagerImports.has(specifier)) continue; + + child.node.phase = null; + importsToPush.push(child.node); + child.remove(); + } + } + if (importsToPush.length) { + path.pushContainer("body", importsToPush); + // Re-collect references to moved imports + path.scope.crawl(); + } + }, + }, + }; +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/input.mjs new file mode 100644 index 000000000000..ebdb14cadb74 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/input.mjs @@ -0,0 +1,7 @@ +import defer * as a from "a"; +import defer * as b from "b"; +import * as c from "lazy"; + +later(() => { + use(a.x, b, c); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/options.json b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/options.json new file mode 100644 index 000000000000..be5f10f7bf53 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + "proposal-import-defer", + ["transform-modules-commonjs", { "lazy": ["lazy"] }] + ] +} diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/output.js new file mode 100644 index 000000000000..8ec667a39032 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/esm-to-commonjs-lazy/integration/output.js @@ -0,0 +1,17 @@ +"use strict"; + +function a(data) { + a = () => data; + return data = babelHelpers.interopRequireWildcard(require("a")); +} +var b = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("b"))); +function c() { + const data = babelHelpers.interopRequireWildcard(require("lazy")); + c = function () { + return data; + }; + return data; +} +later(() => { + use(a().x, b, c()); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/dep.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/dep.cjs new file mode 100644 index 000000000000..8c54d2a33bf4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/dep.cjs @@ -0,0 +1,4 @@ +require("./side-channel.cjs").executed = true; + +exports.__esModule = true; +exports.prop = 3; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/exec.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/exec.js new file mode 100644 index 000000000000..a02ccc444a2b --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/exec.js @@ -0,0 +1,14 @@ +import defer * as ns from "./dep.cjs"; +import sideChannel from "./side-channel.cjs"; + +expect(sideChannel.executed).toBe(false); + +// NOTE: In the current proposal, Object.getOwnPropertyNames does +// not trigger evaluation. However, this behavior is impossible +// to emulate given that we are importing a CommonJS module. +// In the proposal, Object.keys does tirgger evaluation because +// it internally performs .[[Get]] on the namespace object. + +const names = Object.getOwnPropertyNames(ns); +expect(sideChannel.executed).toBe(true); +expect(names).toEqual(["__esModule", "prop"]); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/side-channel.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/side-channel.cjs new file mode 100644 index 000000000000..175559f973c4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/get-own-property-names/side-channel.cjs @@ -0,0 +1 @@ +exports.executed = false; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/dep.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/dep.cjs new file mode 100644 index 000000000000..c0a7f229f3ba --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/dep.cjs @@ -0,0 +1 @@ +require("./side-channel.cjs").executed = true; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/exec.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/exec.js new file mode 100644 index 000000000000..9e94f0b5956c --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/exec.js @@ -0,0 +1,4 @@ +import defer * as ns from "./dep.cjs"; +import sideChannel from "./side-channel.cjs"; + +expect(sideChannel.executed).toBe(false); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/side-channel.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/side-channel.cjs new file mode 100644 index 000000000000..175559f973c4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/not-referenced/side-channel.cjs @@ -0,0 +1 @@ +exports.executed = false; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/options.json b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/options.json new file mode 100644 index 000000000000..f3112f15306b --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["proposal-import-defer", "transform-modules-commonjs"] +} diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/dep.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/dep.cjs new file mode 100644 index 000000000000..8c54d2a33bf4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/dep.cjs @@ -0,0 +1,4 @@ +require("./side-channel.cjs").executed = true; + +exports.__esModule = true; +exports.prop = 3; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/exec.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/exec.js new file mode 100644 index 000000000000..023d30684f8a --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/exec.js @@ -0,0 +1,13 @@ +import defer * as ns from "./dep.cjs"; +import sideChannel from "./side-channel.cjs"; + +expect(sideChannel.executed).toBe(false); + +const copy = ns; +expect(typeof copy).toBe("object"); +expect(sideChannel.executed).toBe(false); + +const val = Reflect.get(ns, "prop"); +expect(val).toBe(3); + +expect(sideChannel.executed).toBe(true); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/side-channel.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/side-channel.cjs new file mode 100644 index 000000000000..175559f973c4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-plain-only/side-channel.cjs @@ -0,0 +1 @@ +exports.executed = false; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/dep.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/dep.cjs new file mode 100644 index 000000000000..8c54d2a33bf4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/dep.cjs @@ -0,0 +1,4 @@ +require("./side-channel.cjs").executed = true; + +exports.__esModule = true; +exports.prop = 3; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/exec.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/exec.js new file mode 100644 index 000000000000..8c03595de4b1 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/exec.js @@ -0,0 +1,9 @@ +import defer * as ns from "./dep.cjs"; +import sideChannel from "./side-channel.cjs"; + +expect(sideChannel.executed).toBe(false); + +const val = ns.prop; +expect(val).toBe(3); + +expect(sideChannel.executed).toBe(true); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/side-channel.cjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/side-channel.cjs new file mode 100644 index 000000000000..175559f973c4 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/exec/reference-property-only/side-channel.cjs @@ -0,0 +1 @@ +exports.executed = false; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/input.mjs new file mode 100644 index 000000000000..806ae2bb7690 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/input.mjs @@ -0,0 +1 @@ +import defer * as ns from "x"; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/output.js new file mode 100644 index 000000000000..3918c74e4463 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/not-referenced/output.js @@ -0,0 +1 @@ +"use strict"; diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/options.json b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/options.json new file mode 100644 index 000000000000..36c31a6bafe9 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-import-defer", "transform-modules-commonjs"] +} diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/input.mjs new file mode 100644 index 000000000000..1941f1f70683 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/input.mjs @@ -0,0 +1,6 @@ +import defer * as ns from "x"; + +later(() => { + use(ns); + ns.prop; +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/output.js new file mode 100644 index 000000000000..2758d4ab4c5e --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-and-property/output.js @@ -0,0 +1,7 @@ +"use strict"; + +var ns = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("x"))); +later(() => { + use(ns); + ns.prop; +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/input.mjs new file mode 100644 index 000000000000..fe4157905a95 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/input.mjs @@ -0,0 +1,5 @@ +import defer * as ns from "x"; + +later(() => { + use(ns); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/output.js new file mode 100644 index 000000000000..33ec4d205ea1 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-plain-only/output.js @@ -0,0 +1,6 @@ +"use strict"; + +var ns = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("x"))); +later(() => { + use(ns); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/input.mjs new file mode 100644 index 000000000000..35c45201d2fc --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/input.mjs @@ -0,0 +1,6 @@ +import defer * as ns from "x"; + +later(() => { + ns.prop; + use(ns); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/output.js new file mode 100644 index 000000000000..90a7346d042e --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-and-plain/output.js @@ -0,0 +1,7 @@ +"use strict"; + +var ns = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("x"))); +later(() => { + ns.prop; + use(ns); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/input.mjs new file mode 100644 index 000000000000..ed468db4a4d1 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/input.mjs @@ -0,0 +1,5 @@ +import defer * as ns from "x"; + +later(() => { + ns.prop; +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/output.js new file mode 100644 index 000000000000..de49e8cfbd0f --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/reference-property-only/output.js @@ -0,0 +1,9 @@ +"use strict"; + +function ns(data) { + ns = () => data; + return data = babelHelpers.interopRequireWildcard(require("x")); +} +later(() => { + ns().prop; +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/input.mjs new file mode 100644 index 000000000000..0783441e664a --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/input.mjs @@ -0,0 +1,7 @@ +import defer * as x1 from "x"; +import * as y from "y"; +import * as x2 from "x"; + +later(() => { + use(x1, x2, y); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/output.js new file mode 100644 index 000000000000..268da2faaa1d --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-after/output.js @@ -0,0 +1,8 @@ +"use strict"; + +var y = babelHelpers.interopRequireWildcard(require("y")); +var x2 = babelHelpers.interopRequireWildcard(require("x")); +var x1 = x2; +later(() => { + use(x1, x2, y); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/input.mjs b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/input.mjs new file mode 100644 index 000000000000..464bbe629e01 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/input.mjs @@ -0,0 +1,7 @@ +import * as x1 from "x"; +import * as y from "y"; +import defer * as x2 from "x"; + +later(() => { + use(x1, x2, y); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/output.js b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/output.js new file mode 100644 index 000000000000..f0888785b98c --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/fixtures/transform/with-full-import-before/output.js @@ -0,0 +1,8 @@ +"use strict"; + +var x1 = babelHelpers.interopRequireWildcard(require("x")); +var x2 = x1; +var y = babelHelpers.interopRequireWildcard(require("y")); +later(() => { + use(x1, x2, y); +}); diff --git a/packages/babel-plugin-proposal-import-defer/test/index.js b/packages/babel-plugin-proposal-import-defer/test/index.js new file mode 100644 index 000000000000..21a55ce6b5e7 --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/index.js @@ -0,0 +1,3 @@ +import runner from "@babel/helper-plugin-test-runner"; + +runner(import.meta.url); diff --git a/packages/babel-plugin-proposal-import-defer/test/package.json b/packages/babel-plugin-proposal-import-defer/test/package.json new file mode 100644 index 000000000000..5ffd9800b97c --- /dev/null +++ b/packages/babel-plugin-proposal-import-defer/test/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/packages/babel-plugin-syntax-import-defer/README.md b/packages/babel-plugin-syntax-import-defer/README.md new file mode 100644 index 000000000000..98b68aea356c --- /dev/null +++ b/packages/babel-plugin-syntax-import-defer/README.md @@ -0,0 +1,19 @@ +# @babel/plugin-syntax-import-defer + +> Allow parsing of the `import defer` syntax in import statement + +See our website [@babel/plugin-syntax-import-defer](https://babeljs.io/docs/babel-plugin-syntax-import-defer) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/plugin-syntax-import-defer +``` + +or using yarn: + +```sh +yarn add @babel/plugin-syntax-import-defer --dev +``` diff --git a/packages/babel-plugin-syntax-import-defer/package.json b/packages/babel-plugin-syntax-import-defer/package.json new file mode 100644 index 000000000000..e1a99166e8ae --- /dev/null +++ b/packages/babel-plugin-syntax-import-defer/package.json @@ -0,0 +1,54 @@ +{ + "name": "@babel/plugin-syntax-import-defer", + "version": "7.22.5", + "description": "Allow parsing of the `import defer` syntax in import statement", + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-plugin-syntax-import-defer" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "./lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": { + "@babel/helper-plugin-utils": "workspace:^" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "devDependencies": { + "@babel/core": "workspace:^" + }, + "engines": { + "node": ">=6.9.0" + }, + "author": "The Babel Team (https://babel.dev/team)", + "conditions": { + "BABEL_8_BREAKING": [ + { + "engines": { + "node": "^16.20.0 || ^18.16.0 || >=20.0.0" + } + }, + { + "exports": null + } + ], + "USE_ESM": [ + { + "type": "module" + }, + null + ] + }, + "exports": { + ".": "./lib/index.js", + "./package.json": "./package.json" + }, + "type": "commonjs" +} diff --git a/packages/babel-plugin-syntax-import-defer/src/index.ts b/packages/babel-plugin-syntax-import-defer/src/index.ts new file mode 100644 index 000000000000..792aa52a4f25 --- /dev/null +++ b/packages/babel-plugin-syntax-import-defer/src/index.ts @@ -0,0 +1,13 @@ +import { declare } from "@babel/helper-plugin-utils"; + +export default declare(api => { + api.assertVersion(7); + + return { + name: "syntax-import-defer", + + manipulateOptions(_, parserOpts) { + parserOpts.plugins.push("deferredImportEvaluation"); + }, + }; +}); diff --git a/packages/babel-plugin-transform-modules-commonjs/src/hooks.ts b/packages/babel-plugin-transform-modules-commonjs/src/hooks.ts new file mode 100644 index 000000000000..05767ace1264 --- /dev/null +++ b/packages/babel-plugin-transform-modules-commonjs/src/hooks.ts @@ -0,0 +1,65 @@ +import type { types as t, File } from "@babel/core"; +import type { isSideEffectImport } from "@babel/helper-module-transforms"; + +const commonJSHooksKey = + "@babel/plugin-transform-modules-commonjs/customWrapperPlugin"; + +type SourceMetadata = Parameters[0]; + +// A hook exposes a set of function that can customize how `require()` calls and +// references to the imported bindings are handled. These functions can either +// return a result, or return `null` to delegate to the next hook. +export interface CommonJSHook { + name: string; + version: string; + wrapReference?(ref: t.Expression, payload: unknown): t.CallExpression | null; + // Optionally wrap a `require` call. If this function returns `false`, the + // `require` call is removed from the generated code. + buildRequireWrapper?( + name: string, + init: t.Expression, + payload: unknown, + referenced: boolean, + ): t.Statement | false | null; + getWrapperPayload?( + source: string, + metadata: SourceMetadata, + importNodes: t.Node[], + ): string | null; +} + +export function defineCommonJSHook(file: File, hook: CommonJSHook) { + let hooks = file.get(commonJSHooksKey); + if (!hooks) file.set(commonJSHooksKey, (hooks = [])); + hooks.push(hook); +} + +function findMap(arr: T[] | null, cb: (el: T) => U): U | null { + if (arr) { + for (const el of arr) { + const res = cb(el); + if (res != null) return res; + } + } +} + +export function makeInvokers( + file: File, +): Pick< + CommonJSHook, + "wrapReference" | "getWrapperPayload" | "buildRequireWrapper" +> { + const hooks: CommonJSHook[] | null = file.get(commonJSHooksKey); + + return { + getWrapperPayload(...args) { + return findMap(hooks, hook => hook.getWrapperPayload?.(...args)); + }, + wrapReference(...args) { + return findMap(hooks, hook => hook.wrapReference?.(...args)); + }, + buildRequireWrapper(...args) { + return findMap(hooks, hook => hook.buildRequireWrapper?.(...args)); + }, + }; +} diff --git a/packages/babel-plugin-transform-modules-commonjs/src/index.ts b/packages/babel-plugin-transform-modules-commonjs/src/index.ts index 6dba2e9b1816..80d45a6db180 100644 --- a/packages/babel-plugin-transform-modules-commonjs/src/index.ts +++ b/packages/babel-plugin-transform-modules-commonjs/src/index.ts @@ -15,6 +15,10 @@ import type { PluginOptions } from "@babel/helper-module-transforms"; import type { Visitor, Scope, NodePath } from "@babel/traverse"; import { transformDynamicImport } from "./dynamic-import.ts"; +import { lazyImportsHook } from "./lazy.ts"; + +import { defineCommonJSHook, makeInvokers } from "./hooks.ts"; +export { defineCommonJSHook }; export interface Options extends PluginOptions { allowCommonJSExports?: boolean; @@ -166,6 +170,8 @@ export default declare((api, options: Options) => { pre() { this.file.set("@babel/plugin-transform-modules-*", "commonjs"); + + if (lazy) defineCommonJSHook(this.file, lazyImportsHook(lazy)); }, visitor: { @@ -216,6 +222,8 @@ export default declare((api, options: Options) => { // @ts-expect-error todo(flow->ts): do not reuse variables if (moduleName) moduleName = t.stringLiteral(moduleName); + const hooks = makeInvokers(this.file); + const { meta, headers } = rewriteModuleStatementsAndPrepareHeader( path, { @@ -227,7 +235,8 @@ export default declare((api, options: Options) => { allowTopLevelThis, noInterop, importInterop, - lazy, + wrapReference: hooks.wrapReference, + getWrapperPayload: hooks.getWrapperPayload, esNamespaceOnly: typeof state.filename === "string" && /\.mjs$/.test(state.filename) @@ -245,32 +254,28 @@ export default declare((api, options: Options) => { let header: t.Statement; if (isSideEffectImport(metadata)) { - if (metadata.lazy) throw new Error("Assertion failure"); + if (lazy && metadata.wrap === "function") { + throw new Error("Assertion failure"); + } header = t.expressionStatement(loadExpr); } else { - // A lazy import that is never referenced can be safely - // omitted, since it wouldn't be executed anyway. - if (metadata.lazy && !metadata.referenced) { - continue; - } - const init = wrapInterop(path, loadExpr, metadata.interop) || loadExpr; - if (metadata.lazy) { - header = template.statement.ast` - function ${metadata.name}() { - const data = ${init}; - ${metadata.name} = function(){ return data; }; - return data; - } - `; - } else { - header = template.statement.ast` - var ${metadata.name} = ${init}; - `; + if (metadata.wrap) { + const res = hooks.buildRequireWrapper( + metadata.name, + init, + metadata.wrap, + metadata.referenced, + ); + if (res === false) continue; + else header = res; } + header ??= template.statement.ast` + var ${metadata.name} = ${init}; + `; } header.loc = metadata.loc; @@ -280,6 +285,7 @@ export default declare((api, options: Options) => { meta, metadata, constantReexports, + hooks.wrapReference, ), ); } diff --git a/packages/babel-plugin-transform-modules-commonjs/src/lazy.ts b/packages/babel-plugin-transform-modules-commonjs/src/lazy.ts new file mode 100644 index 000000000000..d20b2b51b15b --- /dev/null +++ b/packages/babel-plugin-transform-modules-commonjs/src/lazy.ts @@ -0,0 +1,41 @@ +import { template, types as t } from "@babel/core"; +import { isSideEffectImport } from "@babel/helper-module-transforms"; +import type { CommonJSHook } from "./hooks.ts"; + +type Lazy = boolean | string[] | ((source: string) => boolean); + +export const lazyImportsHook = (lazy: Lazy): CommonJSHook => ({ + name: `${PACKAGE_JSON.name}/lazy`, + version: PACKAGE_JSON.version, + getWrapperPayload(source, metadata) { + 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/function"; + } + if (Array.isArray(lazy)) { + return lazy.indexOf(source) === -1 ? null : "lazy/function"; + } + if (typeof lazy === "function") { + return lazy(source) ? "lazy/function" : null; + } + }, + buildRequireWrapper(name, init, payload, referenced) { + if (payload === "lazy/function") { + if (!referenced) return false; + return template.statement.ast` + function ${name}() { + const data = ${init}; + ${name} = function(){ return data; }; + return data; + } + `; + } + }, + wrapReference(ref, payload) { + if (payload === "lazy/function") return t.callExpression(ref, []); + }, +}); diff --git a/packages/babel-runtime-corejs2/package.json b/packages/babel-runtime-corejs2/package.json index d470f6a29f31..9fa2a5b36826 100644 --- a/packages/babel-runtime-corejs2/package.json +++ b/packages/babel-runtime-corejs2/package.json @@ -135,6 +135,15 @@ "./helpers/dispose.js" ], "./helpers/esm/dispose": "./helpers/esm/dispose.js", + "./helpers/importDeferProxy": [ + { + "node": "./helpers/importDeferProxy.js", + "import": "./helpers/esm/importDeferProxy.js", + "default": "./helpers/importDeferProxy.js" + }, + "./helpers/importDeferProxy.js" + ], + "./helpers/esm/importDeferProxy": "./helpers/esm/importDeferProxy.js", "./helpers/iterableToArrayLimit": [ { "node": "./helpers/iterableToArrayLimit.js", diff --git a/packages/babel-runtime-corejs3/package.json b/packages/babel-runtime-corejs3/package.json index 760b7ef1c2ed..6f8f142bd393 100644 --- a/packages/babel-runtime-corejs3/package.json +++ b/packages/babel-runtime-corejs3/package.json @@ -134,6 +134,15 @@ "./helpers/dispose.js" ], "./helpers/esm/dispose": "./helpers/esm/dispose.js", + "./helpers/importDeferProxy": [ + { + "node": "./helpers/importDeferProxy.js", + "import": "./helpers/esm/importDeferProxy.js", + "default": "./helpers/importDeferProxy.js" + }, + "./helpers/importDeferProxy.js" + ], + "./helpers/esm/importDeferProxy": "./helpers/esm/importDeferProxy.js", "./helpers/iterableToArrayLimit": [ { "node": "./helpers/iterableToArrayLimit.js", diff --git a/packages/babel-runtime/package.json b/packages/babel-runtime/package.json index b5b3a5c81b6a..7a759a5a3314 100644 --- a/packages/babel-runtime/package.json +++ b/packages/babel-runtime/package.json @@ -134,6 +134,15 @@ "./helpers/dispose.js" ], "./helpers/esm/dispose": "./helpers/esm/dispose.js", + "./helpers/importDeferProxy": [ + { + "node": "./helpers/importDeferProxy.js", + "import": "./helpers/esm/importDeferProxy.js", + "default": "./helpers/importDeferProxy.js" + }, + "./helpers/importDeferProxy.js" + ], + "./helpers/esm/importDeferProxy": "./helpers/esm/importDeferProxy.js", "./helpers/iterableToArrayLimit": [ { "node": "./helpers/iterableToArrayLimit.js", diff --git a/packages/babel-standalone/package.json b/packages/babel-standalone/package.json index a78cdb056fe4..af987e2c1288 100644 --- a/packages/babel-standalone/package.json +++ b/packages/babel-standalone/package.json @@ -20,6 +20,7 @@ "@babel/plugin-proposal-export-default-from": "workspace:^", "@babel/plugin-proposal-function-bind": "workspace:^", "@babel/plugin-proposal-function-sent": "workspace:^", + "@babel/plugin-proposal-import-defer": "workspace:^", "@babel/plugin-proposal-pipeline-operator": "workspace:^", "@babel/plugin-proposal-record-and-tuple": "workspace:^", "@babel/plugin-proposal-regexp-modifiers": "workspace:^", diff --git a/packages/babel-standalone/scripts/pluginConfig.json b/packages/babel-standalone/scripts/pluginConfig.json index cec59ae0f0cc..762c7a47ea29 100644 --- a/packages/babel-standalone/scripts/pluginConfig.json +++ b/packages/babel-standalone/scripts/pluginConfig.json @@ -115,6 +115,7 @@ "transform-typescript", "transform-unicode-escapes", "transform-unicode-regex", - "proposal-explicit-resource-management" + "proposal-explicit-resource-management", + "proposal-import-defer" ] } diff --git a/packages/babel-standalone/src/generated/plugins.ts b/packages/babel-standalone/src/generated/plugins.ts index fa6c00041d33..6e6e99d3d7af 100644 --- a/packages/babel-standalone/src/generated/plugins.ts +++ b/packages/babel-standalone/src/generated/plugins.ts @@ -100,6 +100,7 @@ import transformTypescript from "@babel/plugin-transform-typescript"; import transformUnicodeEscapes from "@babel/plugin-transform-unicode-escapes"; import transformUnicodeRegex from "@babel/plugin-transform-unicode-regex"; import proposalExplicitResourceManagement from "@babel/plugin-proposal-explicit-resource-management"; +import proposalImportDefer from "@babel/plugin-proposal-import-defer"; export const syntaxAsyncGenerators = makeNoopPlugin(), syntaxClassProperties = makeNoopPlugin(), syntaxClassStaticBlock = makeNoopPlugin(), @@ -205,6 +206,7 @@ export { transformUnicodeEscapes, transformUnicodeRegex, proposalExplicitResourceManagement, + proposalImportDefer, }; export const all: { [k: string]: any } = { "syntax-async-generators": syntaxAsyncGenerators, @@ -313,4 +315,5 @@ export const all: { [k: string]: any } = { "transform-unicode-escapes": transformUnicodeEscapes, "transform-unicode-regex": transformUnicodeRegex, "proposal-explicit-resource-management": proposalExplicitResourceManagement, + "proposal-import-defer": proposalImportDefer, }; diff --git a/tsconfig.json b/tsconfig.json index b2a6d0dc8f5f..9145aa192015 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,6 +51,7 @@ "./packages/babel-plugin-proposal-function-bind/src/**/*.ts", "./packages/babel-plugin-proposal-function-sent/src/**/*.ts", "./packages/babel-plugin-proposal-import-attributes-to-assertions/src/**/*.ts", + "./packages/babel-plugin-proposal-import-defer/src/**/*.ts", "./packages/babel-plugin-proposal-partial-application/src/**/*.ts", "./packages/babel-plugin-proposal-pipeline-operator/src/**/*.ts", "./packages/babel-plugin-proposal-record-and-tuple/src/**/*.ts", @@ -68,6 +69,7 @@ "./packages/babel-plugin-syntax-function-sent/src/**/*.ts", "./packages/babel-plugin-syntax-import-assertions/src/**/*.ts", "./packages/babel-plugin-syntax-import-attributes/src/**/*.ts", + "./packages/babel-plugin-syntax-import-defer/src/**/*.ts", "./packages/babel-plugin-syntax-import-reflection/src/**/*.ts", "./packages/babel-plugin-syntax-jsx/src/**/*.ts", "./packages/babel-plugin-syntax-module-blocks/src/**/*.ts", @@ -323,6 +325,9 @@ "@babel/plugin-proposal-import-attributes-to-assertions": [ "./packages/babel-plugin-proposal-import-attributes-to-assertions/src" ], + "@babel/plugin-proposal-import-defer": [ + "./packages/babel-plugin-proposal-import-defer/src" + ], "@babel/plugin-proposal-partial-application": [ "./packages/babel-plugin-proposal-partial-application/src" ], @@ -374,6 +379,9 @@ "@babel/plugin-syntax-import-attributes": [ "./packages/babel-plugin-syntax-import-attributes/src" ], + "@babel/plugin-syntax-import-defer": [ + "./packages/babel-plugin-syntax-import-defer/src" + ], "@babel/plugin-syntax-import-reflection": [ "./packages/babel-plugin-syntax-import-reflection/src" ], diff --git a/yarn.lock b/yarn.lock index c3281609284e..608c5c9224bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1562,6 +1562,20 @@ __metadata: languageName: unknown linkType: soft +"@babel/plugin-proposal-import-defer@workspace:^, @babel/plugin-proposal-import-defer@workspace:packages/babel-plugin-proposal-import-defer": + version: 0.0.0-use.local + resolution: "@babel/plugin-proposal-import-defer@workspace:packages/babel-plugin-proposal-import-defer" + dependencies: + "@babel/core": "workspace:^" + "@babel/helper-plugin-test-runner": "workspace:^" + "@babel/helper-plugin-utils": "workspace:^" + "@babel/plugin-syntax-import-defer": "workspace:^" + "@babel/plugin-transform-modules-commonjs": "workspace:^" + peerDependencies: + "@babel/core": ^7.0.0-0 + languageName: unknown + linkType: soft + "@babel/plugin-proposal-partial-application@workspace:packages/babel-plugin-proposal-partial-application": version: 0.0.0-use.local resolution: "@babel/plugin-proposal-partial-application@workspace:packages/babel-plugin-proposal-partial-application" @@ -1911,6 +1925,17 @@ __metadata: languageName: unknown linkType: soft +"@babel/plugin-syntax-import-defer@workspace:^, @babel/plugin-syntax-import-defer@workspace:packages/babel-plugin-syntax-import-defer": + version: 0.0.0-use.local + resolution: "@babel/plugin-syntax-import-defer@workspace:packages/babel-plugin-syntax-import-defer" + dependencies: + "@babel/core": "workspace:^" + "@babel/helper-plugin-utils": "workspace:^" + peerDependencies: + "@babel/core": ^7.0.0-0 + languageName: unknown + linkType: soft + "@babel/plugin-syntax-import-meta-BABEL_8_BREAKING-false@npm:@babel/plugin-syntax-import-meta@^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" @@ -4106,6 +4131,7 @@ __metadata: "@babel/plugin-proposal-export-default-from": "workspace:^" "@babel/plugin-proposal-function-bind": "workspace:^" "@babel/plugin-proposal-function-sent": "workspace:^" + "@babel/plugin-proposal-import-defer": "workspace:^" "@babel/plugin-proposal-pipeline-operator": "workspace:^" "@babel/plugin-proposal-record-and-tuple": "workspace:^" "@babel/plugin-proposal-regexp-modifiers": "workspace:^"