Skip to content

Commit 61be34f

Browse files
authored
Add capability to use Babel runtime (#32)
* Add capability to use Babel runtime * Pull test runtime version from manifest * Change option to runtime and add docs * Use snapshot tests for debug logs
1 parent e31be96 commit 61be34f

12 files changed

+782
-113
lines changed

.yarnrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defaultSemverRangePrefix: ""
2+
3+
pnpEnableEsmLoader: true

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## How it works
1010

11-
The plugin works by transforming async modules using [`@babel/preset-env`](https://babeljs.io/docs/babel-preset-env) right before they are ready to be written to a chunk. It is expected that modules are already transpiled using [`babel-loader`](https://www.npmjs.com/package/babel-loader), so the primary transformations occurring are simply to the `async function`:
11+
The plugin works by transforming async modules using [Babel](https://babeljs.io) right before they are ready to be written to a chunk. It is expected that modules are already transpiled (e.g. by using [`babel-loader`](https://www.npmjs.com/package/babel-loader)), so the primary transformations occurring are simply to the `async function`:
1212

1313
1. [`@babel/plugin-transform-async-to-generator`](https://babeljs.io/docs/babel-plugin-transform-async-to-generator) to convert to a generator function, and
1414
2. [`@babel/plugin-transform-regenerator`](https://babeljs.io/docs/babel-plugin-transform-regenerator) to convert the ES2015 generator
@@ -47,12 +47,30 @@ export default {
4747

4848
## Options
4949

50-
The options passed to the plugin are optional. Properties are a subset of [Babel options to specify targets](https://babeljs.io/docs/options#output-targets), and are passed directly to the transform function.
50+
The plugin takes the following options, all of which are optional:
5151

5252
```ts
5353
interface TransformAsyncModulesPluginOptions {
54-
targets?: Targets; // see Babel docs for details
54+
targets?: Targets;
5555
browserslistConfigFile?: boolean;
5656
browserslistEnv?: string;
57+
runtime?: boolean | RuntimeOptions;
5758
}
5859
```
60+
61+
### targets, browserslistConfigFile, and browserslistEnv
62+
63+
Controls how the async modules will be transpiled. These properties are a subset of [Babel options to specify targets](https://babeljs.io/docs/options#output-targets), and are passed directly to Babel.
64+
65+
### runtime
66+
67+
Allows importing helpers and regenerator from `@babel/runtime` instead of repeating them for each async module. If it is falsey, the runtime will not be used. This option takes a subset of relevant [options for `@babel/plugin-transform-runtime`](https://babeljs.io/docs/babel-plugin-transform-runtime#options):
68+
69+
```ts
70+
interface RuntimeOptions {
71+
absoluteRuntime?: boolean | string;
72+
version?: string;
73+
}
74+
```
75+
76+
The default for `version` is the minimum required version for the plugin, so it is recommended this property be specified as the version installed when using the runtime.

mocha.config.cjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module.exports = {
2-
require: "ts-node/register",
2+
require: ["ts-node/register", "./test/snapshot-setup.ts"],
33
loader: ["ts-node/esm", "./.pnp.loader.mjs"],
44
asyncOnly: true,
55
forbidOnly: true,
6-
spec: ["test/**/*.spec.*"],
6+
enableSourceMaps: true,
7+
spec: ["test/**/*.spec.ts"],
78
watchFiles: ["src"],
89
};

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
],
3737
"exports": "./dist/index.cjs",
3838
"types": "./dist/index.d.cts",
39-
"sideEffects": false,
4039
"scripts": {
4140
"lint:js": "eslint --cache-location=node_modules/.cache/eslint/.eslintcache .",
4241
"lint:format": "prettier --cache --check .",
@@ -52,18 +51,25 @@
5251
},
5352
"dependencies": {
5453
"@babel/core": "^7.13.0",
54+
"@babel/plugin-transform-runtime": "^7.13.0",
5555
"@babel/preset-env": "^7.13.0"
5656
},
5757
"peerDependencies": {
5858
"@babel/core": "^7.13.0",
59+
"@babel/plugin-transform-runtime": "^7.13.0",
5960
"@babel/preset-env": "^7.13.0",
61+
"@babel/runtime": "^7.13.0",
6062
"webpack": "^5.0.0"
6163
},
6264
"devDependencies": {
65+
"@babel/runtime": "7.24.1",
6366
"@eslint/eslintrc": "3.0.2",
6467
"@eslint/js": "8.57.0",
6568
"@types/babel__core": "7.20.5",
69+
"@types/babel__plugin-transform-runtime": "7.9.5",
70+
"@types/babel__preset-env": "7.9.6",
6671
"@types/chai": "4.3.14",
72+
"@types/chai-jest-snapshot": "1.3.8",
6773
"@types/eslint-config-prettier": "6.11.3",
6874
"@types/eslint__eslintrc": "2.1.1",
6975
"@types/eslint__js": "8.42.3",
@@ -72,6 +78,7 @@
7278
"@typescript-eslint/eslint-plugin": "7.5.0",
7379
"@typescript-eslint/parser": "7.5.0",
7480
"chai": "5.1.0",
81+
"chai-jest-snapshot": "2.0.0",
7582
"eslint": "8.57.0",
7683
"eslint-config-prettier": "9.1.0",
7784
"globals": "15.0.0",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { types as t, type PluginObj, type PluginPass } from "@babel/core";
2+
3+
export interface RuntimeDependencyOptions {
4+
map: Map<string, string>;
5+
record?: boolean;
6+
}
7+
8+
interface ThisPluginPass extends PluginPass {
9+
opts: RuntimeDependencyOptions;
10+
counter: number;
11+
}
12+
13+
const ID_PREFIX = "__TAMP";
14+
15+
// This plugin is designed to run after transform-runtime, and operates in one
16+
// of two modes:
17+
// - In record mode, it just records the source request and a mangled
18+
// identifier for Webpack to inject in the provided map.
19+
// - Not in record mode, it expects that Webpack has injected the recorded
20+
// identifiers as variable bindings, and essentially swaps the Babel import
21+
// for a renamed Webpack variable.
22+
export const runtimeDependencyPlugin = (): PluginObj<ThisPluginPass> => ({
23+
name: "runtime-dependency",
24+
pre() {
25+
this.counter = 0;
26+
},
27+
visitor: {
28+
ImportDeclaration(path) {
29+
// Make sure we only have a single default specifier.
30+
if (
31+
path.node.specifiers.length !== 1 ||
32+
!t.isImportDefaultSpecifier(path.node.specifiers[0])
33+
) {
34+
throw Error(
35+
`Expected only a default import specifier:\n${path.getSource()}`,
36+
);
37+
}
38+
const localID = path.node.specifiers[0].local.name;
39+
// In record mode, just save the request and exit.
40+
if (this.opts.record) {
41+
const dependencyID = ID_PREFIX + localID;
42+
this.opts.map.set(path.node.source.value, dependencyID);
43+
return;
44+
}
45+
// Find the identifier that Webpack injected for this source.
46+
const dependencyID = this.opts.map.get(path.node.source.value);
47+
if (!dependencyID) {
48+
throw Error(
49+
`Encountered a dependency that was not recorded:\n${path.getSource()}`,
50+
);
51+
}
52+
// Check that the dependency was declared at the top scope, rename it,
53+
// and delete the import.
54+
if (!path.scope.hasOwnBinding(dependencyID)) {
55+
throw Error(`Identifier ${dependencyID} was not injected by Webpack`);
56+
}
57+
path.scope.rename(dependencyID, localID);
58+
path.remove();
59+
this.counter++;
60+
},
61+
},
62+
post() {
63+
// Check that each recorded/declared dependency was renamed.
64+
const nDeps = this.opts.map.size;
65+
if (!this.opts.record && this.counter !== nDeps) {
66+
throw Error(
67+
`Injected ${nDeps} dependencies but only swapped ${this.counter} imports`,
68+
);
69+
}
70+
},
71+
});

0 commit comments

Comments
 (0)