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
21 changes: 21 additions & 0 deletions .changeset/remove-cjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@solidjs/signals": patch
"solid-js": patch
"@solidjs/web": patch
"@solidjs/universal": patch
"@solidjs/h": patch
"@solidjs/html": patch
---

**Breaking:** all runtime packages are ESM only and declare `engines.node >= 22.12`.

Every `.cjs` artifact, every `require` branch in the exports maps, and the `types-cjs/` declaration mirrors are gone. Node 22.12+ loads ESM through `require()` natively, so a CommonJS host resolves the same files through the same export conditions it always did (`browser`, `node`, `development`, `observe`, …) — there is one module graph per tier rather than two to keep in step. `main` now points at the ESM server entry.

For consumers:

- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later. `require("solid-js")` keeps working.
- TypeScript CommonJS projects: use `module: "NodeNext"` (TS 5.8+), which type-checks `require()` of ESM packages; `module: "Node16"` will report TS1479.
- Jest: needs Node 22.12+ for `require(esm)`; any preset that maps specifiers to `.cjs` paths (as `solid-jest` does for Solid 1.x) has nothing to map to and must be updated.

`@solidjs/signals` drops its flat `dist/node*.cjs` builds; its ESM entries (`dist/prod/`, `dist/observe/`, `dist/dev.js`) are the only ones. `@solidjs/babel-plugin` and `@solidjs/compiler` (build-time tooling loaded by Babel/Node) are unchanged.
2 changes: 1 addition & 1 deletion .cursor/rules/signals.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ alwaysApply: false
## Changing This Package — Checklist

1. **Bundle size is budgeted.** Scenarios and ratcheted caps live in `scripts/size/.size-limit.js` (run `npm run size` from `scripts/size/` after `pnpm build:js`). Every cap bump needs an audit note there. Restructuring golf is measured NO-WIN (brotli compresses repeats free; indirection adds unique tokens — 2026-09-04 notes). The winning move is **relocation into pay-for-use modules** (e.g. the optimistic module) so floor bundles don't pay.
2. **Two gates, three tiers.** Checks (strict reads, invariants, forbidden scopes, console reporting, dev-only error text) go under `__DEV__` — dev builds only. Wiring (attribution hook call sites, `_name` labels, graph edge counters, `emitDiagnostic`) goes under `__OBSERVE__` — dev AND the `dist/observe` tier, which production observability consumers select via the `observe` export condition. `__DEV__` implies `__OBSERVE__` (dev.ts asserts it). Prod folds both — free. The observe tier has its own size scenario; the prod scenarios must not move because of it. Public surface: `OBSERVE` (diagnostics channel + the attribution hook slot and interaction frame, observe-legal) vs `DEV` (devtools hooks, graph getters, console face). The attribution ENGINE is the separate `@solidjs/signals/attribution` entry (`src/attribution.ts`; prod resolves the inert `src/attribution.prod.ts`): nothing in the core may import `core/attribution.ts`, or the observe tier ships the engine to consumers that never enable it. Every build has two entries (`index` + `attribution`) sharing one module instance — the flat builds are code-split for exactly this. See `documentation/plans/observe-tier-plan.md`.
2. **Two gates, three tiers.** Checks (strict reads, invariants, forbidden scopes, console reporting, dev-only error text) go under `__DEV__` — dev builds only. Wiring (attribution hook call sites, `_name` labels, graph edge counters, `emitDiagnostic`) goes under `__OBSERVE__` — dev AND the `dist/observe` tier, which production observability consumers select via the `observe` export condition. `__DEV__` implies `__OBSERVE__` (dev.ts asserts it). Prod folds both — free. The observe tier has its own size scenario; the prod scenarios must not move because of it. Public surface: `OBSERVE` (diagnostics channel + the attribution hook slot and interaction frame, observe-legal) vs `DEV` (devtools hooks, graph getters, console face). The attribution ENGINE is the separate `@solidjs/signals/attribution` entry (`src/attribution.ts`; prod resolves the inert `src/attribution.prod.ts`): nothing in the core may import `core/attribution.ts`, or the observe tier ships the engine to consumers that never enable it. Every build has two entries (`index` + `attribution`) sharing one module instance — the flat dev build is code-split for exactly this. Artifacts are ESM only (`dist/prod/`, `dist/observe/`, `dist/dev*.js`; no `.cjs`, no `require` branch): Node >= 22.12 `require()`s ESM, so a CJS host gets the same files — which is why no shipped module may contain a top-level `await` (`tests/dist-artifacts.test.ts` scans for it and `require`s each tier). See `documentation/plans/observe-tier-plan.md`.
3. **Downstream tests consume built dist.** After changing signals, `pnpm --filter @solidjs/signals build` and `pnpm --filter solid-js build` BEFORE running `packages/solid` or `packages/web` tests — stale dist has caused false regression alarms repeatedly.
4. **Facades mirror types.** Store/signal API type changes must be applied in `packages/solid/src/client/hydration.ts` and `packages/solid/src/server/signals.ts` too.
5. Internal fields are mangled at build (`scripts/mangle-props.mjs`) in the prod and observe trees; keep the `_`-prefix convention. `_name` is the one reserved (unmangled) field — solid-js writes component labels onto signals' owners through it. `check-pure` verifies `/*#__PURE__*/` annotation count.
2 changes: 1 addition & 1 deletion .cursor/rules/web.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ Browser-safe stubs that log errors if server APIs are accidentally used in brows
- `dist/server.js` — production server (Node/Deno/Worker)
- `dist/server.dev.js` — development server (`development` export condition, nested under `node`/`worker`/`deno`)

Every dev artifact is `<entry>.dev.{js,cjs}` beside its prod `<entry>.{js,cjs}` — the same rule for `frames/`, `server-functions/`, and the other packages. Observe artifacts are `<entry>.observe.{js,cjs}` and exist only for entries that contain wiring (here: the browser entry); entries without wiring fall through to prod under `observe`. Tier-ness is a build mode, not an entry. Gate a site on `"_SOLID_OBSERVE_"` when production observability needs it (labels, attribution hooks), on `"_SOLID_DEV_"` when only a developer at a console does. See `documentation/plans/observe-tier-plan.md`.
Every dev artifact is `<entry>.dev.js` beside its prod `<entry>.js` — the same rule for `frames/`, `server-functions/`, and the other packages. All artifacts are ESM only (no `.cjs` twins, no `require` export branch, no `types-cjs/`): the published `engines.node >= 22.12` floor `require()`s ESM natively, so a CJS host resolves the same files through the same conditions. Nothing shipped may contain a top-level `await` (`require(esm)` throws on it; the signals dist test scans for it). Observe artifacts are `<entry>.observe.js` and exist only for entries that contain wiring (here: the browser entry); entries without wiring fall through to prod under `observe`. Tier-ness is a build mode, not an entry. Gate a site on `"_SOLID_OBSERVE_"` when production observability needs it (labels, attribution hooks), on `"_SOLID_DEV_"` when only a developer at a console does. See `documentation/plans/observe-tier-plan.md`.

Rollup with Babel for JSX transformation, `@rollup/plugin-replace` for dev/prod flags.
45 changes: 26 additions & 19 deletions documentation/plans/observe-tier-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ byte-identical to today under every bundler.
gates: `"_SOLID_OBSERVE_"` is a replaced literal like `"_SOLID_DEV_"`, and
packages ship an observe flavor only for entries that contain wiring:
- `@solidjs/signals`: `dist/observe/` (per-module tree like `prod/`,
mangled) and `dist/node.observe.cjs`.
- `solid-js`: `dist/solid.observe.{js,cjs}` — the component root + label
mangled).
- `solid-js`: `dist/solid.observe.js` — the component root + label
and the flow-control memo names (`<For>`, `<Repeat>`, `<Show>`'s
"condition value"/"condition"/"value", `<Match>`'s, `children`); and
`dist/server.observe.{js,cjs}`, which today differs from prod only in
`dist/server.observe.js`, which today differs from prod only in
exporting a live `OBSERVE` — it exists so `import { OBSERVE } from
"solid-js"` agrees with `@solidjs/signals` when both resolve under
`observe` in one process. P1's server labels give it content.
- `@solidjs/web`: `dist/web.observe.{js,cjs}` (3 `withInteraction` sites).
- `@solidjs/universal`: `dist/universal.observe.{js,cjs}` (renderer-effect
- `@solidjs/web`: `dist/web.observe.js` (3 `withInteraction` sites).
- `@solidjs/universal`: `dist/universal.observe.js` (renderer-effect
fallback names, "renderer render").
- frames, server-functions, storage, serialization, h, html, element: no
wiring → no flavor; under `observe` they fall through to prod (pinned by
Expand Down Expand Up @@ -76,9 +76,8 @@ format/formatOrigin` — is `@solidjs/signals/attribution` (re-exported as
that import it. Prod resolves an inert twin (`src/attribution.prod.ts`)
with the same `Attribution` surface, so the import needs no per-tier guard.
Every signals build therefore has two entries sharing one module instance:
the trees via `preserveModules`, the flat dev/CJS builds via code
splitting (`<name>.<ext>`, `<name>.attribution.<ext>`, `<name>-shared.<ext>`,
mangled as one domain). `InteractionRef` moved to `attribution-hooks.ts`;
the trees via `preserveModules`, the flat dev build via code splitting
(`dev.js`, `dev.attribution.js`, `dev-shared.js`). `InteractionRef` moved to `attribution-hooks.ts`;
the hooks gained `interactionStart(ref)`/`interactionEnd()`.

## PR A (this plan) — flags, split, flavors, caps, engine entry
Expand All @@ -87,10 +86,9 @@ format/formatOrigin` — is `@solidjs/signals/attribution` (re-exported as

Build: `globals.d.ts` declares `__OBSERVE__`. Rollup adds `dist/observe/`
(`__DEV__: "false", __OBSERVE__: "true"`, `preserveModules`, no prettier,
mangled) and `dist/node.observe.cjs`; `dev.js`/`node.dev.cjs` get
`__OBSERVE__: "true"`; `prod/`/`node.cjs` get `"false"`. `mangle-props.mjs`
reserves `_name`. `build:clean` covers the new outputs. Exports: `observe`
after `development` on both `import` and `require`.
mangled); `dev.js` gets `__OBSERVE__: "true"`; `prod/` gets `"false"`.
`mangle-props.mjs` reserves `_name`. `build:clean` covers the new outputs.
Exports: `observe` after `development`.

`dev.ts`: construct `OBSERVE` under `__OBSERVE__`, `DEV` under `__DEV__`;
`emitDiagnostic` (channel) is observe-tier; `reportDiagnostic`,
Expand Down Expand Up @@ -194,13 +192,22 @@ alongside server-dev-build-plan P1, which supplies the request half of

## Open questions

- **Client CJS.** Every client entry ships a `.cjs` twin (and its dev/observe
siblings) for one consumer: Jest with `solid-jest`. Everything Solid 2
documents or ships is Vitest/ESM, `require(esm)` is unflagged on 20.19+,
and Svelte 4, Vite 7, Angular and Lit are ESM-only. Dropping client CJS —
or all CJS, with `engines.node >= 20.19` — would delete roughly a third of
the build matrix and the whole dual-types pipeline. Its own PR, after this
lands.
- **CJS.** _Resolved: removed, across the board._ Every entry used to ship a
`.cjs` twin (and its dev/observe siblings) plus a `types-cjs/` mirror, for
one consumer: Jest with `solid-jest`. Everything Solid 2 documents or ships
is Vitest/ESM; Node 20 is EOL (2026-04) and Node 22.12+ `require()`s ESM
natively; the ESM-native runtimes (Bun, Deno, workers, Tauri, SolidTV's
bundled output) never read the CJS files. Runtime packages now publish
`engines.node >= 22.12` and ESM only — no `require` branch in any exports
map, so a CJS host resolves the very same files through the very same
conditions. That deleted roughly a third of the build matrix, the
dual-types pipeline (`sync-dual-types.mjs`), and signals' three flat CJS
builds. Two guards replaced them: the signals dist test `require()`s each
tier in a child Node and checks it is the same instance `import` yields,
and scans every shipped module for a top-level `await` (the one thing that
breaks `require(esm)`). Build-time tooling (`@solidjs/babel-plugin`,
`@solidjs/compiler`'s napi loader) is unchanged — those are loaded by Babel
and Node tooling, not by app code, and are outside the runtime tier story.

## Out of scope here

Expand Down
23 changes: 13 additions & 10 deletions documentation/plans/server-dev-build-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ attribution work has no server foothold.

1. **`solid-js` has no server dev build.** `packages/solid/rollup.config.js`
builds `src/server/index.ts` once, with _no_ `_SOLID_DEV_` replace at all;
`dist/server.{js,cjs}` is the only artifact for the `worker`/`deno`/`node`
`dist/server.js` is the only artifact for the `worker`/`deno`/`node`
conditions. `packages/solid/src/server/` contains **zero** `"_SOLID_DEV_"`
gates. Its 8 `console.warn`/`console.error` sites fire **unconditionally in
production**: `[SERVER_WRITE]` (`signals.ts:696`), nested `<Reveal>` in
Expand All @@ -38,7 +38,7 @@ attribution work has no server foothold.
these ever run outside the test suite (which runs from source, where the
string literal is truthy). `head.ts` and `cookies.ts` each have one more.
3. **The pattern already exists in the same package.**
`@solidjs/web/server-functions` ships `server.dev.{js,cjs}` via
`@solidjs/web/server-functions` ships `server.dev.js` via
`replaceDev(true)` and nests a `development` condition **inside**
`worker`/`deno`/`node` in its exports — the correct shape, because at the
top level `node` precedes `development` and would win. `./frames` server
Expand Down Expand Up @@ -71,7 +71,7 @@ attribution work has no server foothold.

**`solid-js`** (`packages/solid/rollup.config.js`, `package.json`):

- Add `dist/server.dev.{js,cjs}` from `src/server/index.ts` with
- Add `dist/server.dev.js` from `src/server/index.ts` with
`replaceDev(true)`.
- Add `replaceDev(false)` to the existing prod server build. Today it has no
replace because the source has no gates; the moment P1 adds one, an
Expand All @@ -83,9 +83,9 @@ attribution work has no server foothold.

**`@solidjs/web`** (`packages/web/rollup.config.js`, `package.json`):

- Add `dist/server.dev.{js,cjs}` from `src/index.server.ts` with
- Add `dist/server.dev.js` from `src/index.server.ts` with
`replaceDev(true)`; same external list as the prod server build.
- Add `frames/dist/server.dev.{js,cjs}` likewise; nest `development` under
- Add `frames/dist/server.dev.js` likewise; nest `development` under
the server conditions of `./frames` and add it to `./frames/server`.
- `./storage`, `./serialization`: no gates today; leave until one appears.

Expand Down Expand Up @@ -142,13 +142,16 @@ false`), `exports-server-conditions.spec.tsx`; solid
`prod/` (chunked dir for the mangle pass; a restructure, not a rename).
The resolution test now pins the client pairing under `browser` too.
- `@solidjs/signals`' `require` branch had no `development` condition
(`dist/node.cjs` is `__DEV__: false` only), so CJS hosts loading
(`dist/node.cjs` was `__DEV__: false` only), so CJS hosts loading
`server.dev.cjs` got signals' prod object and `DEV` came back `undefined` —
a dev artifact lying about the one export P1 will emit through. Fixed in
the same PR: `dist/node.dev.cjs` (unmangled twin of `dev.js`), selected by
`require.development`. The resolution test walks the CJS hops
(`@solidjs/web` → `solid-js` → `@solidjs/signals`) and pins all three
flipping together; a signals dist test pins `DEV` per CJS artifact.
the same PR with a `dist/node.dev.cjs`; then made moot when CJS was removed
across the board (observe-tier-plan, "CJS"): there is one module graph per
tier now and `require()` reaches it through the same conditions `import`
does. The resolution test still walks the `require` hops (`@solidjs/web` →
`solid-js` → `@solidjs/signals`) and pins all three landing on the same
ESM files per tier, and loads the chain through a real CJS `require` to
prove the graph is `require(esm)`-safe (no top-level `await`).
- `solid-js#test` now depends on `solid-js#build` in `turbo.json` — it had
no dist-based tests until this work, so it was the one package whose test
task didn't wait for its own build.
Expand Down
2 changes: 1 addition & 1 deletion packages/h/jsx-dev-runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solidjs/h/jsx-dev-runtime",
"main": "../jsx-runtime/dist/jsx.cjs",
"main": "../jsx-runtime/dist/jsx.js",
"module": "../jsx-runtime/dist/jsx.js",
"types": "../jsx-runtime/types/index.d.ts",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/h/jsx-runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solidjs/h/jsx-runtime",
"main": "./dist/jsx.cjs",
"main": "./dist/jsx.js",
"module": "./dist/jsx.js",
"types": "./types/index.d.ts",
"type": "module",
Expand Down
5 changes: 2 additions & 3 deletions packages/h/jsx-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ function Fragment(props: { children: JSX.Element }) {
}

// Explicit return annotation keeps tsc from inlining `import("../../types/hyperscript").HyperElement`
// in the emitted .d.ts. `sync-dual-types.mjs` rewrites `.d.ts` -> `.d.cts` extensions but does not
// remap the `types/` <-> `types-cjs/` directory, so a cross-folder inferred reference would break
// Node16 CJS type resolution from `@solidjs/h/jsx-runtime`'s CJS export.
// in the emitted .d.ts; the public type is `JSX.Element`, not a relative path into the parent
// package's declaration folder.
function jsx(type: any, props: any): JSX.Element {
return h(type, props) as unknown as JSX.Element;
}
Expand Down
Loading
Loading