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

Add transform support for JSON modules imports #16172

Merged
merged 8 commits into from Feb 27, 2024
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
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}))
Copy link
Contributor

Choose a reason for hiding this comment

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

The fs Promises API is available on Node.js 10. If we would like to support older versions, we should manually promisify the callback-style fs.readFile call. Ideally we can output the most compatible form based on the input target.

.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