diff --git a/docs/implementation-docs/handover-babel-to-typescript-eslint.md b/docs/implementation-docs/handover-babel-to-typescript-eslint.md new file mode 100644 index 0000000000..c4d95065a9 --- /dev/null +++ b/docs/implementation-docs/handover-babel-to-typescript-eslint.md @@ -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` diff --git a/docs/implementation-docs/plan-remove-babel.md b/docs/implementation-docs/plan-remove-babel.md new file mode 100644 index 0000000000..88c9e3e98e --- /dev/null +++ b/docs/implementation-docs/plan-remove-babel.md @@ -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 |