Skip to content

Commit

Permalink
Implement import defer proposal transform support
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Sep 5, 2023
1 parent 95b4b22 commit 6571dc3
Show file tree
Hide file tree
Showing 44 changed files with 463 additions and 0 deletions.
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
31 changes: 31 additions & 0 deletions 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),
}
);
}
3 changes: 3 additions & 0 deletions packages/babel-plugin-proposal-import-defer/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions 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
```
57 changes: 57 additions & 0 deletions 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"
}
121 changes: 121 additions & 0 deletions packages/babel-plugin-proposal-import-defer/src/index.ts
@@ -0,0 +1,121 @@
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}() {
const data = ${init};
${name} = () => data;
return data;
}
`;
}
},
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 Map();

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.set(specifier, child);
}
}
}

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();
}
},
},
};
});
@@ -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);
});
@@ -0,0 +1,6 @@
{
"plugins": [
"proposal-import-defer",
["transform-modules-commonjs", { "lazy": ["lazy"] }]
]
}
@@ -0,0 +1,18 @@
"use strict";

function a() {
const data = babelHelpers.interopRequireWildcard(require("a"));
a = () => data;
return data;
}
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());
});
@@ -0,0 +1,4 @@
require("./side-channel.cjs").executed = true;

exports.__esModule = true;
exports.prop = 3;
@@ -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"]);
@@ -0,0 +1 @@
exports.executed = false;
@@ -0,0 +1 @@
require("./side-channel.cjs").executed = true;
@@ -0,0 +1,4 @@
import defer * as ns from "./dep.cjs";
import sideChannel from "./side-channel.cjs";

expect(sideChannel.executed).toBe(false);
@@ -0,0 +1 @@
exports.executed = false;
@@ -0,0 +1,4 @@
{
"sourceType": "module",
"plugins": ["proposal-import-defer", "transform-modules-commonjs"]
}
@@ -0,0 +1,4 @@
require("./side-channel.cjs").executed = true;

exports.__esModule = true;
exports.prop = 3;
@@ -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);
@@ -0,0 +1 @@
exports.executed = false;
@@ -0,0 +1,4 @@
require("./side-channel.cjs").executed = true;

exports.__esModule = true;
exports.prop = 3;
@@ -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);
@@ -0,0 +1 @@
exports.executed = false;
@@ -0,0 +1 @@
import defer * as ns from "x";
@@ -0,0 +1 @@
"use strict";
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-import-defer", "transform-modules-commonjs"]
}
@@ -0,0 +1,6 @@
import defer * as ns from "x";

later(() => {
use(ns);
ns.prop;
});
@@ -0,0 +1,7 @@
"use strict";

var ns = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("x")));
later(() => {
use(ns);
ns.prop;
});
@@ -0,0 +1,5 @@
import defer * as ns from "x";

later(() => {
use(ns);
});
@@ -0,0 +1,6 @@
"use strict";

var ns = babelHelpers.importDeferProxy(() => babelHelpers.interopRequireWildcard(require("x")));
later(() => {
use(ns);
});
@@ -0,0 +1,6 @@
import defer * as ns from "x";

later(() => {
ns.prop;
use(ns);
});

0 comments on commit 6571dc3

Please sign in to comment.