Skip to content

Commit

Permalink
Add transform support for JSON modules imports (babel#16172)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo authored and liuxingbaoyu committed Mar 5, 2024
1 parent 8a77f2c commit 89b73d6
Show file tree
Hide file tree
Showing 93 changed files with 862 additions and 255 deletions.
3 changes: 3 additions & 0 deletions packages/babel-helper-import-to-platform-api/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-helper-import-to-platform-api/README.md
@@ -0,0 +1,19 @@
# @babel/helper-import-to-platform-api

> Helper function to transform import statements to platform-specific APIs
See our website [@babel/helper-import-to-platform-api](https://babeljs.io/docs/babel-helper-import-to-platform-api) for more information.

## Install

Using npm:

```sh
npm install --save @babel/helper-import-to-platform-api
```

or using yarn:

```sh
yarn add @babel/helper-import-to-platform-api
```
55 changes: 55 additions & 0 deletions packages/babel-helper-import-to-platform-api/package.json
@@ -0,0 +1,55 @@
{
"name": "@babel/helper-import-to-platform-api",
"version": "7.22.5",
"description": "Helper function to transform import statements to platform-specific APIs",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-helper-import-to-platform-api"
},
"homepage": "https://babel.dev/docs/en/next/babel-helper-import-to-platform-api",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"dependencies": {
"@babel/helper-compilation-targets": "workspace:^",
"@babel/helper-module-imports": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
},
"TODO": "The @babel/traverse dependency is only needed for the NodePath TS type. We can consider exporting it from @babel/core.",
"devDependencies": {
"@babel/core": "workspace:^",
"@babel/traverse": "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"
}
237 changes: 237 additions & 0 deletions packages/babel-helper-import-to-platform-api/src/index.ts
@@ -0,0 +1,237 @@
import { types as t, template } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import type { Targets } from "@babel/helper-compilation-targets";
import { addNamed } from "@babel/helper-module-imports";

import getSupport from "./platforms-support.ts";

function imp(path: NodePath, name: string, module: string) {
return addNamed(path, name, module, { importedType: "es6" });
}

export interface Pieces {
webFetch: (fetch: t.Expression) => t.Expression;
nodeFsSync: (read: t.Expression) => t.Expression;
nodeFsAsync: () => t.Expression;
}

export interface Builders {
buildFetch: (specifier: t.Expression, path: NodePath) => t.Expression;
buildFetchAsync: (specifier: t.Expression, path: NodePath) => t.Expression;
needsAwait: boolean;
}

const imr = (s: t.Expression) => template.expression.ast`
import.meta.resolve(${s})
`;
const imrWithFallback = (s: t.Expression) => template.expression.ast`
import.meta.resolve?.(${s}) ?? new URL(${t.cloneNode(s)}, import.meta.url)
`;

export function importToPlatformApi(
targets: Targets,
transformers: Pieces,
toCommonJS: boolean,
) {
const {
needsNodeSupport,
needsWebSupport,
nodeSupportsIMR,
webSupportsIMR,
nodeSupportsFsPromises,
} = getSupport(targets);

let buildFetchAsync: (
specifier: t.Expression,
path: NodePath,
) => t.Expression;
let buildFetchSync: typeof buildFetchAsync;

// "p" stands for pattern matching :)
const p = ({
web: w,
node: n,
nodeFSP: nF = nodeSupportsFsPromises,
webIMR: wI = webSupportsIMR,
nodeIMR: nI = nodeSupportsIMR,
toCJS: c = toCommonJS,
}: {
web: boolean;
node: boolean;
nodeFSP?: boolean;
webIMR?: boolean;
nodeIMR?: boolean;
toCJS?: boolean;
preferSync?: boolean;
}) => +w + (+n << 1) + (+wI << 2) + (+nI << 3) + (+c << 4) + (+nF << 5);

const readFileP = (fs: t.Expression, arg: t.Expression) => {
if (nodeSupportsFsPromises) {
return template.expression.ast`${fs}.promises.readFile(${arg})`;
}
return template.expression.ast`
new Promise(
(a =>
(r, j) => ${fs}.readFile(a, (e, d) => e ? j(e) : r(d))
)(${arg})
)`;
};

switch (
p({
web: needsWebSupport,
node: needsNodeSupport,
webIMR: webSupportsIMR,
nodeIMR: nodeSupportsIMR,
toCJS: toCommonJS,
})
) {
case p({ web: true, node: true }):
buildFetchAsync = specifier => {
const web = transformers.webFetch(
t.callExpression(t.identifier("fetch"), [
(webSupportsIMR ? imr : imrWithFallback)(t.cloneNode(specifier)),
]),
);
const node = nodeSupportsIMR
? template.expression.ast`
import("fs").then(
fs => ${readFileP(
t.identifier("fs"),
template.expression.ast`new URL(${imr(specifier)})`,
)}
).then(${transformers.nodeFsAsync()})
`
: template.expression.ast`
Promise.all([import("fs"), import("module")])
.then(([fs, module]) =>
${readFileP(
t.identifier("fs"),
template.expression.ast`
module.createRequire(import.meta.url).resolve(${specifier})
`,
)}
)
.then(${transformers.nodeFsAsync()})
`;

return template.expression.ast`
typeof process === "object" && process.versions?.node
? ${node}
: ${web}
`;
};
break;
case p({ web: true, node: false, webIMR: true }):
buildFetchAsync = specifier =>
transformers.webFetch(
t.callExpression(t.identifier("fetch"), [imr(specifier)]),
);
break;
case p({ web: true, node: false, webIMR: false }):
buildFetchAsync = specifier =>
transformers.webFetch(
t.callExpression(t.identifier("fetch"), [imrWithFallback(specifier)]),
);
break;
case p({ web: false, node: true, toCJS: true }):
buildFetchSync = specifier =>
transformers.nodeFsSync(template.expression.ast`
require("fs").readFileSync(require.resolve(${specifier}))
`);
buildFetchAsync = specifier => template.expression.ast`
require("fs").promises.readFile(require.resolve(${specifier}))
.then(${transformers.nodeFsAsync()})
`;
break;
case p({ web: false, node: true, toCJS: false, nodeIMR: true }):
buildFetchSync = (specifier, path) =>
transformers.nodeFsSync(template.expression.ast`
${imp(path, "readFileSync", "fs")}(
new URL(${imr(specifier)})
)
`);
buildFetchAsync = (specifier, path) =>
template.expression.ast`
${imp(path, "promises", "fs")}
.readFile(new URL(${imr(specifier)}))
.then(${transformers.nodeFsAsync()})
`;
break;
case p({ web: false, node: true, toCJS: false, nodeIMR: false }):
buildFetchSync = (specifier, path) =>
transformers.nodeFsSync(template.expression.ast`
${imp(path, "readFileSync", "fs")}(
${imp(path, "createRequire", "module")}(import.meta.url)
.resolve(${specifier})
)
`);
buildFetchAsync = (specifier, path) =>
transformers.webFetch(template.expression.ast`
${imp(path, "promises", "fs")}
.readFile(
${imp(path, "createRequire", "module")}(import.meta.url)
.resolve(${specifier})
)
`);
break;
default:
throw new Error("Internal Babel error: unreachable code.");
}

buildFetchAsync ??= buildFetchSync;
const buildFetchAsyncWrapped: typeof buildFetchAsync = (expression, path) => {
if (t.isStringLiteral(expression)) {
return template.expression.ast`
Promise.resolve().then(() => ${buildFetchAsync(expression, path)})
`;
} else {
return template.expression.ast`
Promise.resolve(\`\${${expression}}\`).then((s) => ${buildFetchAsync(
t.identifier("s"),
path,
)})
`;
}
};

return {
buildFetch: buildFetchSync || buildFetchAsync,
buildFetchAsync: buildFetchAsyncWrapped,
needsAwait: !buildFetchSync,
};
}

export function buildParallelStaticImports(
data: Array<{ id: t.Identifier; fetch: t.Expression }>,
needsAwait: boolean,
): t.VariableDeclaration | null {
if (data.length === 0) return null;

const declarators: t.VariableDeclarator[] = [];

if (data.length === 1) {
let rhs = data[0].fetch;
if (needsAwait) rhs = t.awaitExpression(rhs);
declarators.push(t.variableDeclarator(data[0].id, rhs));
} else if (needsAwait) {
const ids = data.map(({ id }) => id);
const fetches = data.map(({ fetch }) => fetch);
declarators.push(
t.variableDeclarator(
t.arrayPattern(ids),
t.awaitExpression(
template.expression.ast`
Promise.all(${t.arrayExpression(fetches)})
`,
),
),
);
} else {
for (const { id, fetch } of data) {
declarators.push(t.variableDeclarator(id, fetch));
}
}

return t.variableDeclaration("const", declarators);
}
@@ -0,0 +1,70 @@
import { isRequired, type Targets } from "@babel/helper-compilation-targets";

function isEmpty(obj: object) {
return Object.keys(obj).length === 0;
}

const isRequiredOptions = {
compatData: {
// `import.meta.resolve` compat data.
// Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve#browser_compatibility
// Once Node.js implements `fetch` of local files, we can re-use the web implementation for it
// similarly to how we do for Deno.
webIMR: {
chrome: "105.0.0",
edge: "105.0.0",
firefox: "106.0.0",
opera: "91.0.0",
safari: "16.4.0",
opera_mobile: "72.0.0",
ios: "16.4.0",
samsung: "20.0",
deno: "1.24.0",
},
nodeIMR: {
node: "20.6.0",
},
// Node.js require("fs").promises compat data.
nodeFSP: {
node: "10.0.0",
},
},
};

interface Support {
needsNodeSupport: boolean;
needsWebSupport: boolean;
nodeSupportsIMR: boolean;
webSupportsIMR: boolean;
nodeSupportsFsPromises: boolean;
}

const SUPPORT_CACHE = new WeakMap<Targets, Support>();
export default function getSupport(targets: Targets): Support {
if (SUPPORT_CACHE.has(targets)) return SUPPORT_CACHE.get(targets)!;

const { node: nodeTarget, ...webTargets } = targets;
const emptyNodeTarget = nodeTarget == null;
const emptyWebTargets = isEmpty(webTargets);
const needsNodeSupport = !emptyNodeTarget || emptyWebTargets;
const needsWebSupport = !emptyWebTargets || emptyNodeTarget;

const webSupportsIMR =
!emptyWebTargets && !isRequired("webIMR", webTargets, isRequiredOptions);
const nodeSupportsIMR =
!emptyNodeTarget &&
!isRequired("nodeIMR", { node: nodeTarget }, isRequiredOptions);
const nodeSupportsFsPromises =
!emptyNodeTarget &&
!isRequired("nodeFSP", { node: nodeTarget }, isRequiredOptions);

const result = {
needsNodeSupport,
needsWebSupport,
nodeSupportsIMR,
webSupportsIMR,
nodeSupportsFsPromises,
};
SUPPORT_CACHE.set(targets, result);
return result;
}
Expand Up @@ -29,8 +29,7 @@
"stage-3"
],
"dependencies": {
"@babel/helper-compilation-targets": "workspace:^",
"@babel/helper-module-imports": "workspace:^",
"@babel/helper-import-to-platform-api": "workspace:^",
"@babel/helper-plugin-utils": "workspace:^",
"@babel/plugin-syntax-import-source": "workspace:^"
},
Expand Down

0 comments on commit 89b73d6

Please sign in to comment.