Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change history

## 0.3.2

This version fixes a bug, so it is a patch version.

- The Babel preset does not change the server code of React Server Components: a file or a function with the directive `"use server"`, `"use cache"` or `"use cache: <kind>"`. Next.js accepts only native async functions there. Before, the preset changed them into functions that are not async and that use `this` and `arguments`. Next.js then stopped the build, also when a Turbopack rule gave only the files of the browser to the preset. The other presets and plugins of the configuration still change this code. The Vite plugin uses the preset, so it gets the same fix.
- The guide "Build tools" recommends a Turbopack rule with `babel-loader` for Next.js. It also tells the pnpm settings with which a dependency finds the runtime, and how to make Babel do less work. CI builds the rule with a server action and a `"use cache"` function.
- The Babel configuration file for Next.js with webpack gives the default browsers of Next.js to `@babel/preset-env` in `next/babel`. Without targets, preset-env changes the async functions of a server action, and Next.js stops the build.

## 0.3.1

This version fixes a bug, so it is a patch version.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ A test in CI (`pnpm test:bundlers`) builds a page with each of these setups and
| webpack 5, Rspack 2 | `babel-loader` with the preset. Do not exclude `node_modules`. | All |
| Rollup 4 | `@rollup/plugin-babel` with the preset | All |
| esbuild | A small plugin that applies the preset | All |
| Next.js 16, Turbopack or webpack | `babel.config.js` with `next/babel` and the preset, and `transpilePackages` for the dependencies | All |
| Next.js 16, Turbopack | A `turbopack.rules` rule: `babel-loader` with only the preset, for the files of the browser | All |
| Next.js 16, webpack | `babel.config.js` with `next/babel` (with the browsers of Next.js as `preset-env` targets) and the preset, and `transpilePackages` for the dependencies | All |
| SWC, esbuild, Next.js without Babel | The target ES2016: the compiler changes async functions into generators | All except rule C5 |

With the target ES2016, the patch of `then` keeps the context through `await`. A generator body then gets the context of each `next()` call, and the callback hooks get no names.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "async-browser-context",
"version": "0.3.1",
"version": "0.3.2",
"description": "AsyncLocalStorage and the TC39 AsyncContext API (Variable, Snapshot) for browsers. The context stays through await, promises, timers, events and generators. A Babel preset and a Vite plugin transform async functions. On Node.js, the package uses the native AsyncLocalStorage.",
"license": "MIT",
"author": "Mark Russell",
Expand Down
2 changes: 1 addition & 1 deletion site/content/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The plugin applies the Babel preset to each JavaScript and TypeScript module, an
| `names` | `true` | Give the name of each async function to the callback hooks. The option of the preset tells more. |
| `ssr` | `false` | Also transform the modules of server-side rendering. On Node.js, the native `AsyncLocalStorage` keeps the context, so the default is `false`. Set it to `true` for tests in the Node.js environment of Vitest. |

The plugin does not change a module that has no async function, no generator and no `for await` loop. It does not transform the runtime of the library. The plugin skips the files in the `dist` folder of the package, and the preset skips the functions with the directive of the runtime.
The plugin does not change a module that has no async function, no generator and no `for await` loop. It does not transform the runtime of the library. The plugin skips the files in the `dist` folder of the package, and the preset skips the functions with the directive of the runtime. The preset also does not change the server code of React Server Components: a file or a function with the directive `"use server"`, `"use cache"` or `"use cache: <kind>"`.

The plugin gives Babel the source type "module", because Vite serves each module as an ES module, also a worker. Only a `.cjs` or `.cts` file, or a file with CommonJS marks and no static `import` or `export`, can be a script. The plugin does not read an input source map, because the map of Vite describes the code that the plugin gets. It keeps the format of large modules (`compact: false`).

Expand Down
2 changes: 2 additions & 0 deletions site/content/docs/concepts/transform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The preset `async-browser-context/babel-preset` contains nine Babel plugins, in
8. `@babel/plugin-transform-async-generator-functions` changes the async generators and the `for await` loops. The preset makes its own traversal also skip the functions with the directive of the runtime.
9. `@babel/plugin-transform-async-to-generator` changes each async function into a generator function. Its `module` and `method` options make the code use `coroutine` from `async-browser-context/runtime`.

Steps 2 to 9 do not change the server code of React Server Components: a file or a function with the directive `"use server"`, `"use cache"` or `"use cache: <kind>"`. Next.js accepts only native async functions there, and the server keeps the context with the native `AsyncLocalStorage`. These plugins do not skip the code in the shared traversal of Babel. Thus the other presets, for example `@babel/preset-typescript`, still change it. The [Next.js](/docs/guides/build-tools#server-actions-and-use-cache) section of the build tools guide tells more.

The Babel transforms of steps 8 and 9 already handle destructuring, `var`, labels, closures, sync iterables and the `return()` use after `break`. The library adds no custom code for `await`.

The plugin of step 5 sets the helpers in the `Program` visitor. Thus the preset also operates with `@babel/plugin-transform-runtime`, which gives the other helpers of Babel from `@babel/runtime`.
Expand Down
120 changes: 112 additions & 8 deletions site/content/docs/guides/build-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Use the Babel preset if your build tool can use Babel. Use the target ES2016 if

## The tested setups

The command `pnpm test:bundlers` builds the same page with each setup, and opens the page in Chromium. The page examines the context after `await`, in a callback of a dependency in `node_modules`, in a generator and in an event of the browser. CI starts this test for each change.
The command `pnpm test:bundlers` builds the same page with each setup, and opens the page in Chromium. The page examines the context after `await`, in a callback of a dependency in `node_modules`, in a generator and in an event of the browser. The Next.js page also starts a server action and a `"use cache"` function. CI starts this test for each change.

| Tool | Setup | Rules |
| --- | --- | --- |
Expand All @@ -26,11 +26,12 @@ The command `pnpm test:bundlers` builds the same page with each setup, and opens
| Rollup 4 | `@rollup/plugin-babel` with the preset | All rules |
| esbuild 0.28 | A plugin that applies the preset | All rules |
| esbuild 0.28 | The target `es2016` | All rules except rule C5 |
| Next.js 16, Turbopack | A rule with `babel-loader` and the preset | All rules |
| Next.js 16, Turbopack | A Babel configuration file | All rules |
| Next.js 16, webpack | A Babel configuration file | All rules |
| Next.js 16, Turbopack | A `browserslist` query with a browser without async functions | All rules except rule C5 |

The configurations of the test are in `test/bundlers/fixture/build.mjs` and `test/bundlers/fixture/next/`.
The configurations of the test are in `test/bundlers/fixture/build.mjs`, `test/bundlers/fixture/next/` and `test/bundlers/fixture/next-rule/`.

## Vite

Expand Down Expand Up @@ -146,12 +147,107 @@ Without the plugin, set the target `es2016`. esbuild then changes each async fun

## Next.js

1. Add a Babel configuration file next to `package.json`, with the preset of Next.js and the preset of the library:
CI builds and examines three setups with Next.js 16. The page of the test also starts a server action, which starts a `"use cache"` function.

| Setup | Rules | The code that changes | Use it |
| --- | --- | --- | --- |
| [A Turbopack rule](#a-turbopack-rule) | All rules | The files of the browser with async functions or generators, also in `node_modules` | With Turbopack. This is the recommended setup. |
| [A Babel configuration file](#a-babel-configuration-file) | All rules | All your code, on the server and in the browser, and the packages in `transpilePackages` | With webpack (`next build --webpack`) |
| [SWC with a `browserslist` query](#swc-with-a-browserslist-query) | All rules except rule C5 | All the code of the browser, also in `node_modules` | If you do not want Babel |

### A Turbopack rule

Install `babel-loader`. Then add a rule for each kind of file to `next.config.mjs`:

```js
// next.config.mjs
/** The options of babel-loader for one kind of file. */
function babel(syntax) {
return {
loader : "babel-loader",
options : {
babelrc : false,
configFile : false,
sourceType : "unambiguous",
parserOpts : { plugins : syntax },
presets : ["async-browser-context/babel-preset"],
},
};
}

/** The files of the browser with async functions or generators. */
const condition = { all : ["browser", { content : /\basync\b|\byield\b|function\s*\*/ }] };

export default {
turbopack : {
rules : {
"*.{js,jsx,mjs,cjs}" : { condition, loaders : [babel(["jsx"])] },
"*.{ts,mts,cts}" : { condition, loaders : [babel(["typescript"])] },
"*.tsx" : { condition, loaders : [babel(["typescript", "jsx"])] },
},
},
};
```

- The loader applies only the preset of the library. Babel reads the TypeScript and JSX syntax (`parserOpts`), but it does not change it. After the loader, SWC compiles the file as usual.
- A `.ts` file cannot have JSX. Thus TypeScript files and TSX files have different rules.
- Turbopack gives the options of a loader as JSON. Thus use only names, strings, numbers and lists in `options`. Do not use functions or regular expressions there.
- The `browser` condition selects the code of the browser. On the server, the `node` export condition selects the Node.js entry of the library, which uses the native `AsyncLocalStorage`. The server code thus does not need the transform.
- The rule also applies to the packages in `node_modules`. To exclude a file or a package, add a condition to the `all` list, for example `{ not : { path : /node_modules\/some-package\// } }`. `{ not : "foreign" }` excludes all of `node_modules`. A callback that a package starts after its own `await` then gets the root context (refer to [the dependencies](#the-dependencies)).

The configuration of the test is in `test/bundlers/fixture/next-rule/next.config.mjs`.

### Server Actions and `"use cache"`

The preset does not change the server code of React Server Components: a file or a function with the directive `"use server"`, `"use cache"` or `"use cache: <kind>"`. Next.js accepts only native async functions there. Thus you do not exclude these files from a rule. In the bundle of the browser, Next.js replaces a server action with a reference to the server. The server keeps the context with the native `AsyncLocalStorage`.

The preset changes the other functions of the file. For example, an async page function can have a server action in it. The page function changes, and the server action stays a native async function.

The preset of version 0.3.1 and earlier changed these functions, and Next.js then stopped the build.

### pnpm

The preset adds an import of `async-browser-context/runtime` to each file that it changes, also to the files of dependencies. Thus each dependency that the rule changes must find the library from its own folder. With pnpm, this depends on the settings:

| pnpm setting | A dependency finds the runtime | What to do |
| --- | --- | --- |
| The default `hoistPattern` (`*`) | Yes, in `node_modules/.pnpm/node_modules` | Nothing |
| `hoist: false`, or a `hoistPattern` without `async-browser-context` | No | Add `async-browser-context` to `publicHoistPattern`, or add the alias below |
| `enableGlobalVirtualStore: true` | No | Add the alias below |

The alias sends each import of the runtime in the browser to one file. The path goes from the folder of `next.config.mjs` to the `node_modules` folder with the library. In a monorepo, this can be `../../node_modules`.

```js
// next.config.mjs
export default {
turbopack : {
resolveAlias : {
"async-browser-context/runtime" : { browser : "./node_modules/async-browser-context/dist/runtime.js" },
},
rules : { /* the rules above */ },
},
};
```

### Performance

Babel is slower than SWC. The rule above gives Babel only the files of the browser with the words `async`, `yield` or `function*` (the `content` condition). SWC compiles all the other files without Babel. The Vite plugin of the library uses the same filter.

A file that Babel changes gets generators in place of async functions, and it uses the runtime. Thus the bundle gets larger. The cost depends on the number of files with async functions in your code and in your dependencies. Compare the time of `next build` and the size of the bundle with and without the rule. To make Babel do less work, exclude packages with a `path` condition. Exclude only the packages that do not start your code after an `await`.

The build tool test writes the time of each Next.js build to its output. These times come from one build of a small page, so they are not a benchmark.

### A Babel configuration file

1. Add a Babel configuration file next to `package.json`, with the preset of Next.js and the preset of the library. Give the browsers of Next.js to `@babel/preset-env` in `next/babel`:

```js
// babel.config.js
module.exports = {
presets : ["next/babel", "async-browser-context/babel-preset"],
presets : [
["next/babel", { "preset-env" : { targets : "chrome 111, edge 111, firefox 111, safari 16.4" } }],
"async-browser-context/babel-preset",
],
};
```

Expand All @@ -162,11 +258,19 @@ Without the plugin, set the target `es2016`. esbuild then changes each async fun
export default { transpilePackages : ["some-dependency"] };
```

With a Babel configuration file, Next.js uses Babel in place of SWC for your code, with Turbopack and with webpack. The build takes more time. Next.js does not apply the Babel configuration to the other packages in `node_modules`.
With webpack, `next/babel` also compiles the code of the browser with `@babel/preset-env`. Next.js does not give it the browsers of its SWC configuration. Without `targets` and without a `browserslist` query, preset-env changes all the code for old browsers. It also changes the async functions of each server action that a client component imports. Next.js then stops the build with the error "Server Actions must be async functions". This occurs also without the preset of the library.

The `targets` above are the default browsers of Next.js 16. A `browserslist` query in `package.json` can replace them, but each browser in the query must have native async functions. On the server, `next/babel` uses the version of Node.js that builds the app.

With a Babel configuration file, Next.js uses Babel in place of SWC for all your code, with Turbopack and with webpack. The build takes more time. Next.js does not apply the Babel configuration to the other packages in `node_modules`. With Turbopack, use [the rule](#a-turbopack-rule): SWC then compiles all the other files.

The server code also gets the transform. On the server, the `node` export condition selects the Node.js entry of the library, which uses the native `AsyncLocalStorage`. The preset does not change server actions and `"use cache"` functions (refer to [Server Actions](#server-actions-and-use-cache)).

### SWC with a `browserslist` query

The server code also gets the transform. On the server, the `node` export condition selects the Node.js entry of the library, which uses the native `AsyncLocalStorage`.
Next.js can also keep SWC for all files. Add a browser without async functions to the `browserslist` query of `package.json`, for example `"chrome 54"`. SWC then changes each async function into a generator. Turbopack applies the query also to `node_modules`. The webpack mode of Next.js does not, so it needs `transpilePackages`.

Next.js can also keep SWC. Add a browser without async functions to the `browserslist` query of `package.json`, for example `"chrome 54"`. SWC then changes each async function into a generator. Turbopack applies the query also to `node_modules`. The webpack mode of Next.js does not, so it needs `transpilePackages`. The query also changes all newer syntax, so the bundles get larger.
This setup keeps all rules except rule C5, and the callback hooks get no names (refer to [what the target ES2016 does not keep](#what-the-target-es2016-does-not-keep)). The query also changes all newer syntax, so the bundles get larger. The library has no SWC plugin.

## SWC

Expand All @@ -192,7 +296,7 @@ The setups on this page minify after the transform of each module, so they are c

## The dependencies

A dependency with native async functions continues after its own `await` without the context. A callback that the dependency starts after that `await` gets the root context (rule C7). It does not get the context of a different operation. Each setup on this page also transforms `node_modules`, except Next.js, which needs `transpilePackages`.
A dependency with native async functions continues after its own `await` without the context. A callback that the dependency starts after that `await` gets the root context (rule C7). It does not get the context of a different operation. Each setup on this page also transforms `node_modules`, except the Babel configuration file of Next.js, which needs `transpilePackages`.

A dependency that its authors compiled to generators before the publish needs nothing. Examples are the `__awaiter` helper of TypeScript and `_asyncToGenerator` of Babel. The helpers continue in `then` callbacks, and the patch of `then` keeps the context.

Expand Down
6 changes: 3 additions & 3 deletions src/babel/async-generator-resources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NodePath, PluginObject, PluginPass, types } from "@babel/core";
import { isRuntimeFunction } from "./runtime-calls.js";
import { isUnchangedFunction } from "./server-code.js";
import type { PluginFactory } from "./skip-runtime.js";

/**
Expand All @@ -11,7 +11,7 @@ import type { PluginFactory } from "./skip-runtime.js";
* This function gives a plugin that changes the `using` declarations of each async generator
* in `Program` enter, with the visitor of `resources`. The preset puts it after the plugin of
* `using` declarations and before the plugin of async generators. The functions with the
* directive of the runtime stay as they are.
* directive of the runtime and the server functions (server-code.ts) stay as they are.
*/
export function asyncGeneratorResources(resources : PluginFactory) : PluginFactory {
return function asyncGeneratorResourcesPlugin(api, options, dirname) : PluginObject<PluginPass> {
Expand All @@ -23,7 +23,7 @@ export function asyncGeneratorResources(resources : PluginFactory) : PluginFacto
Program(programPath, state) {
programPath.traverse({
Function(functionPath : NodePath<types.Function>) {
if (isRuntimeFunction(functionPath.node)) {
if (isUnchangedFunction(functionPath.node)) {
functionPath.skip();
return;
}
Expand Down
Loading
Loading