Skip to content
Draft
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
187 changes: 187 additions & 0 deletions docs/implementation-docs/handover-babel-to-typescript-eslint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Handover: Migrate ESLint Config from Babel to @typescript-eslint/parser

## Context

We are in the process of removing Babel from the CedarJS framework entirely. This
document covers the first and most self-contained step of that migration: replacing
`@babel/eslint-parser` with `@typescript-eslint/parser` in the ESLint configuration.

This is a good starting point because:

- It is self-contained — no other packages need to change for this step to be complete
- It removes `@cedarjs/babel-config` as a dependency of `packages/eslint-config`, which
is one of the early milestones toward eventually retiring the `babel-config` package
- The risk is low — if something goes wrong it only affects linting, not the build or runtime
- `@typescript-eslint/parser` is already installed and already used for TypeScript files
in both configs (see the `tseslint` usage in `shared.mjs` and `eslint.config.mjs`)

## Files to Change

There are two parallel ESLint configs to update. One is a legacy CJS config
(`shared.js` + `index.js`) and one is the modern flat config (`shared.mjs` +
`index.mjs`). Both need the same treatment. There is also the framework's own root-level
config (`eslint.config.mjs`) which needs updating independently.

### 1. `packages/eslint-config/shared.mjs` (flat config, project-facing)

**Current state:**

- Imports `babelParser` from `@babel/eslint-parser`
- Imports `babelPlugin` from `@babel/eslint-plugin`
- Sets `parser: babelParser` as the base parser for all files
- Registers `'@babel': babelPlugin` in plugins
- A separate block for config files (`.babelrc.js`, `babel.config.js`, etc.) also
uses `parser: babelParser`

**What to do:**

- Remove the `@babel/eslint-parser` and `@babel/eslint-plugin` imports
- The `@typescript-eslint/parser` is already used for `.ts`/`.tsx` files via
`tseslint.configs.base`. For `.js`/`.jsx` files you can either:
- Use `@typescript-eslint/parser` directly (it handles JSX and modern JS just fine
without a `tsconfig.json` when `project` is not set), or
- Use the `espree` parser (ESLint's built-in default) which is sufficient for plain JS
- The recommended approach is `@typescript-eslint/parser` for consistency, since it is
already a dependency
- Remove the `'@babel': babelPlugin` plugin registration and any rules prefixed with
`@babel/` (check for any `'@babel/...'` rule keys in the `rules` object — there do not
appear to be any currently, but verify)
- Remove `eslint-plugin-babel` from the plugins list and its associated rules (the
`shared.js` CJS version lists it; check for `'babel/...'` rule keys)
- The config file block that currently sets `parser: babelParser` for
`.babelrc.js` / `babel.config.js` etc. should be updated to use the default parser
or `@typescript-eslint/parser`. Once Babel is fully removed from the project,
the entries for `.babelrc.js` and `babel.config.js` in the `files` glob can also
be removed, but do not remove them yet — leave that for the final Babel cleanup PR.

### 2. `packages/eslint-config/index.mjs` (flat config, project-facing)

**Current state:**

- Imports `getCommonPlugins`, `getApiSideDefaultBabelConfig`,
`getWebSideDefaultBabelConfig` from `@cedarjs/babel-config`
- Calls those functions to build a `babelOptions` object which is passed into
`parserOptions.babelOptions` for the `.js`/`.jsx` file block

**What to do:**

- Remove the `@cedarjs/babel-config` import entirely
- Remove the `getProjectBabelOptions()` function and its call site
- Remove the `parserOptions.babelOptions` key from the JS/JSX file config block —
this is only needed when using `@babel/eslint-parser` and is meaningless to
`@typescript-eslint/parser`
- The `forJavaScriptLinting` flag in `getWebSideDefaultBabelConfig` was a special
case to enable `@babel/preset-react` for JS-only projects so the parser could
understand JSX. This is not needed with `@typescript-eslint/parser`, which
understands JSX natively via `parserOptions.ecmaFeatures.jsx: true`.

### 3. `packages/eslint-config/shared.js` (legacy CJS config, project-facing)

This is the CJS equivalent of `shared.mjs`. Apply the same changes as above:

- The `parser: '@babel/eslint-parser'` line at the top level should change to
`parser: '@typescript-eslint/parser'` (or be removed to fall back to espree for
JS files, while TS files keep their override)
- Remove `'@babel'` and `'babel'` from the `plugins` array
- Remove `eslint-plugin-babel` and `@babel/eslint-plugin` usage

### 4. `packages/eslint-config/index.js` (legacy CJS config, project-facing)

The CJS equivalent of `index.mjs`:

- Remove the `require('@cedarjs/babel-config')` call
- Remove `getProjectBabelOptions()` and the `parserOptions.babelOptions` key
- Same reasoning as `index.mjs` above

### 5. `eslint.config.mjs` (framework root config, not shipped to projects)

**Current state:**

- Imports `babelParser` from `@babel/eslint-parser` and `babelPlugin` from
`@babel/eslint-plugin`
- Has a `findBabelConfig()` helper that walks up the directory tree to find
`babel.config.js` and passes it as `babelOptions.configFile` to the parser
- Applies `parser: babelParser` to all `**/*.js`, `**/*.jsx`, `**/*.cjs`,
`**/*.mjs` files
- Registers `'@babel': babelPlugin` in the shared plugins block

**What to do:**

- Remove the `babelParser` and `babelPlugin` imports
- Remove the `findBabelConfig()` helper function entirely
- For the JS/JSX/CJS/MJS file block, switch to `@typescript-eslint/parser` (already
imported via `tseslint`) or remove the explicit parser override and let TypeScript
ESLint's base config handle it
- Remove `'@babel': babelPlugin` from the plugins object
- The config file block that lists `babel.config.js` etc. in its `files` glob —
same note as above, leave those globs for now and just fix the parser

## Dependency Changes

Once the code changes above are done, the following can be removed from
`packages/eslint-config/package.json`:

```json
"@babel/core": "...",
"@babel/eslint-parser": "...",
"@babel/eslint-plugin": "...",
"@babel/cli": "...",
"@cedarjs/babel-config": "workspace:*",
"eslint-import-resolver-babel-module": "...",
"eslint-plugin-babel": "..."
```

Note: `@babel/core` is a peer dependency of `@babel/eslint-parser`, so it can go too
once the parser is removed.

`eslint-import-resolver-babel-module` is used by `eslint-plugin-import` to resolve
module paths that go through Babel's `module-resolver` plugin (e.g. `src/` aliases).
Once `@babel/eslint-parser` is gone this resolver is no longer invoked. You will need
to verify that `eslint-plugin-import`'s `import/order` rule still resolves `src/`
aliases correctly. The `'import/internal-regex': '^src/'` setting in `shared.mjs`
controls what counts as "internal", and that should continue to work without the Babel
resolver. If module resolution becomes inaccurate you can switch to
`eslint-import-resolver-typescript` which is the idiomatic replacement.

## Verification

After making the changes, run the following to confirm linting still works end-to-end:

```sh
# From the repo root
yarn lint

# Also lint a JS-only Cedar project if you have one available, since the
# forJavaScriptLinting path is being removed
```

Pay particular attention to:

- **JS files** (`.js`, `.jsx`) in both `api/` and `web/` directories of a Cedar project —
these were previously parsed by `@babel/eslint-parser` and are the most likely to
surface regressions
- **`import/order` rule** — verify imports are still being sorted and that `src/`
aliases are still correctly classified as "internal"
- **JSX in `.js` files** — Cedar projects can write JSX in `.js` files (not just
`.jsx`). Confirm `@typescript-eslint/parser` handles this with
`parserOptions.ecmaFeatures.jsx: true`, which it does by default

## What This Does NOT Cover

This PR intentionally does not:

- Touch `packages/babel-config` itself
- Change any build pipeline (esbuild, Vite, Rollup)
- Remove `babel.config.js` from the repo root or any package
- Affect prerendering, the CLI `exec`/`console` commands, or code generation
- Change how Cedar projects configure their own Babel setup (user-land `babel.config.js`
files are still supported for now)

Those are tracked separately as subsequent steps in the broader Babel removal effort.

## Further Reading

- [`@typescript-eslint/parser` docs](https://typescript-eslint.io/packages/parser/)
- [`typescript-eslint` migration guide from `@babel/eslint-parser`](https://typescript-eslint.io/getting-started)
- [`eslint-import-resolver-typescript`](https://github.com/import-js/eslint-import-resolver-typescript)
as a replacement for `eslint-import-resolver-babel-module`
204 changes: 204 additions & 0 deletions docs/implementation-docs/plan-remove-babel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Plan: Remove Babel from CedarJS

This document gives a high-level overview of the steps needed to fully remove Babel
from the framework, and the order we want to tackle them in.

A detailed handover document for step 1 already exists at
`docs/handover-babel-to-typescript-eslint.md`.

## Out of Scope

The following areas have already been handled separately and do not need to be
addressed here:

- **Jest** — being replaced by Vitest
- **Vite RSC plugins** — being replaced as part of a separate RSC rework

---

## Step 1 — ESLint Config (start here)

**Packages:** `packages/eslint-config`, `eslint.config.mjs` (repo root)

Replace `@babel/eslint-parser` and `@babel/eslint-plugin` with
`@typescript-eslint/parser` (already installed) across both the flat config
(`shared.mjs`, `index.mjs`) and the legacy CJS config (`shared.js`, `index.js`).
This removes `@cedarjs/babel-config` as a dependency of `packages/eslint-config`.

This is the right place to start because it is fully self-contained, low-risk
(linting only), and represents the first time a package drops its `babel-config`
dependency entirely.

See `docs/handover-babel-to-typescript-eslint.md` for full details.

---

## Step 2 — AST Analysis Utilities

**Packages:** `packages/internal`, `packages/cli`, `packages/codemods`

Several packages share near-identical utility code that uses `@babel/parser` and
`@babel/traverse` to parse source files and walk their ASTs — extracting named
exports, default exports, GraphQL queries, JSX elements, and Cell metadata.

The key files are:

- `packages/internal/src/ast.ts` — canonical copy, used by the framework
- `packages/internal/src/jsx.ts` — JSX element extraction
- `packages/cli/src/testLib/cells.ts` — duplicate of the above for CLI use
- `packages/codemods/src/lib/cells.ts` — another near-duplicate

Replace `@babel/parser` + `@babel/traverse` with a parser that is already present
in the dependency graph or is a clear upgrade. The best candidate is
`@typescript-eslint/parser` (for pure parse + walk tasks) or `oxc-parser` (faster,
no traversal API needed for simple cases). For traversal, `es-tree` compatible
walkers like `estree-walker` are lightweight and parser-agnostic.

This step is worth doing early because `packages/internal` re-exports everything
from `@cedarjs/babel-config` (see `packages/internal/src/index.ts`), which means
every consumer of `@cedarjs/internal` transitively depends on Babel. Fixing this
cuts the blast radius of subsequent steps significantly.

The duplicate Cell analysis code in `cli` and `codemods` should be consolidated
onto the `internal` implementation at the same time.

---

## Step 3 — TypeScript-to-JavaScript Transform

**Packages:** `packages/internal`, `packages/codemods`, `packages/cli`,
`packages/create-cedar-app`

Several places use `@babel/core` with `@babel/plugin-transform-typescript` purely
to strip TypeScript types and produce JavaScript — with no other transforms applied.
This is one of Babel's most replaceable use cases.

The key files are:

- `packages/internal/src/ts2js.ts` — used by the CLI's `ts-to-js` command
- `packages/codemods/src/lib/ts2js.ts` — duplicate
- `packages/cli/src/lib/index.js` (`transformTSToJS`) — used during code generation
to convert `.ts` templates to `.js` when the user has a JS project
- `packages/create-cedar-app/scripts/tsToJS.js` — used to produce the JS starter
template from TypeScript sources

Replace all of these with either `oxc-transform` or the TypeScript compiler API
(`ts.transpileModule`), both of which can strip types without a full Babel pipeline.
The `ts2js` implementations in `internal` and `codemods` should be consolidated at
the same time.

Note that the Prettier `parser` strings of `'babel'` and `'babel-ts'` that appear
alongside these transforms (in `prettify` helpers across `internal`, `codemods`,
and `cli`) can be changed to `'babel'` → `'espree'` / `'babel-ts'` → `'typescript'`
as part of this step — they are trivial one-line changes.

---

## Step 4 — API Build Pipeline

**Packages:** `packages/internal`, `packages/vite`, `packages/cli-packages/dataMigrate`

The API build uses esbuild for bundling but threads every file through a Babel
transform first (via `transformWithBabel`) in order to apply the custom Cedar
plugins: context wrapping, OTel wrapping, job path injection, GraphQL options
extraction, and module resolution.

The key files are:

- `packages/internal/src/build/api.ts` — the esbuild plugin that calls
`transformWithBabel`
- `packages/vite/src/buildRouteHooks.ts` — uses the same pattern for route hook
builds
- `packages/babel-config/src/api.ts` — `transformWithBabel` itself, and
`getApiSideBabelPlugins`

The custom Babel plugins that are applied during this step need to be rewritten
as esbuild plugins (or a single combined esbuild plugin) so the Babel transform
layer can be removed entirely. The transforms themselves are not complex —
they do AST manipulation that can be reproduced with a lighter-weight approach.
Consider `oxc-transform` or direct string/regex transforms for the simpler ones
(e.g. `remove-dev-fatal-error-page`), and proper AST transforms for the more
complex ones (e.g. `otel-wrapping`, `context-wrapping`).

`packages/cli-packages/dataMigrate/src/commands/upHandler.ts` also calls
`registerApiSideBabelHook` to transpile data migration files on the fly. This
can be replaced with `tsx` or `@swc-node/register` once the custom plugins are
handled.

---

## Step 5 — CLI Runtime Hooks (`exec` and `console` commands)

**Package:** `packages/cli`

The `exec` and `console` commands use `@babel/register` (via
`registerApiSideBabelHook` / `registerWebSideBabelHook`) to patch Node's `require`
and transpile files on the fly when the user runs arbitrary scripts or opens the
REPL.

The key files are:

- `packages/cli/src/lib/execBabel.js` — `configureBabel()`, used by `exec`
- `packages/cli/src/commands/consoleHandler.ts` — REPL setup

Replace `@babel/register` with `tsx` (which registers a similar require hook using
esbuild under the hood) or `@swc-node/register`. Both support TypeScript, JSX,
and path aliases out of the box with minimal configuration.

The module-resolver aliases that are currently passed to `babel-plugin-module-resolver`
in these hooks will need to be replicated in the new approach — `tsx` supports this
via `tsconfig.json` path mappings, which Cedar already maintains.

---

## Step 6 — Prerender

**Package:** `packages/prerender`

Prerender is the most complex consumer of Babel in the codebase. It uses
`@babel/register` hooks for both the API side and the web side to transpile and
`require()` user code at render time, and it applies two Cedar-specific Babel
plugins on top of the standard config:

- `babelPluginRedwoodCell` — transforms Cell components for SSR
- `babelPluginRedwoodPrerenderMediaImports` — rewrites media imports to no-ops

The key file is `packages/prerender/src/runPrerender.tsx`. There is also a Rollup
plugin (`rollup-plugin-cedarjs-cell.ts`) that uses `@babel/parser`, `@babel/traverse`,
and `@babel/generator` for the same Cell transformation in the Rollup-based build
path.

This step is last because it has the most moving parts: both the on-the-fly
transpilation and the two custom plugin transforms need to be replaced at once for
prerender to remain functional. The `@babel/register` portion can follow the same
approach as step 5 (`tsx` / `@swc-node/register`), and the two Babel plugins should
be rewritten as Rollup/Vite transform hooks.

---

## Step 7 — Retire `packages/babel-config`

Once all the above steps are complete, `@cedarjs/babel-config` will have no
remaining consumers inside the framework. At that point:

- Delete `packages/babel-config` entirely
- Remove `@cedarjs/babel-config` from `packages/internal/src/index.ts` (which
currently re-exports everything from it)
- Clean up any remaining `babel.config.js` files in the repo and fixture projects
- Remove all remaining Babel-related entries from the root `package.json`

---

## Dependency Removal Tracker

As a rough guide, here is when each Babel dependency can be dropped:

| Dependency | Can be removed after step |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `@babel/eslint-parser`, `@babel/eslint-plugin`, `eslint-plugin-babel`, `eslint-import-resolver-babel-module` | 1 |
| `@babel/parser`, `@babel/traverse`, `@babel/types` (in `internal`, `cli`, `codemods`) | 2 |
| `@babel/core`, `@babel/plugin-transform-typescript` (ts2js usages) | 3 |
| `@babel/plugin-transform-react-jsx`, `@babel/preset-env`, `@babel/preset-typescript`, `babel-plugin-module-resolver` (API build) | 4 |
| `@babel/register`, `babel-plugin-module-resolver` (CLI hooks) | 5 |
| `@babel/core`, `@babel/register`, `babel-plugin-*` (prerender) | 6 |
| `@cedarjs/babel-config`, `@babel/cli`, `@babel/node`, `@babel/generator`, `babel-jest`, `babel-plugin-graphql-tag`, `babel-plugin-auto-import` | 7 |
Loading