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
7 changes: 7 additions & 0 deletions .changeset/dev-artifact-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"solid-js": patch
"@solidjs/web": patch
"@solidjs/universal": patch
---

Rename the three legacy client dev artifacts to the `<entry>.dev.{js,cjs}` convention every other dev build already uses: `solid-js/dist/dev.*` → `dist/solid.dev.*`, `@solidjs/web/dist/dev.*` → `dist/web.dev.*`, `@solidjs/universal/dist/dev.*` → `dist/universal.dev.*`. With server dev builds now shipping as `dist/server.dev.*`, a bare `dev.js` no longer says which entry it is the dev build of. The `exports` maps are updated; only code deep-importing `dist/dev.js` directly (bypassing `exports`) is affected.
12 changes: 12 additions & 0 deletions .changeset/server-dev-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"solid-js": patch
"@solidjs/web": patch
---

Add development server builds — `dist/server.dev.*` for `solid-js` and `@solidjs/web`, and `frames/dist/server.dev.*` — selected by the `development` export condition nested under `node`/`worker`/`deno` (nesting is required: those conditions precede the top-level `development` key, so a top-level entry never matched on a server). Until now SSR had no dev build: the only server artifact was built with `_SOLID_DEV_` stripped, so the server runtime's dev checks (head/preload descriptor validation, `useHead` warnings, the committed-response header guard) never ran outside the test suite.

The server entries now gate their public dev flags on the same `_SOLID_DEV_` replace as their internals instead of hard-coding them: `solid-js`'s server `DEV` is `@solidjs/signals`' `DEV` object in the dev artifact (so `DEV.diagnostics.subscribe`/`capture` work server-side) and `undefined` in prod; `@solidjs/web`'s server `isDev` is `true` in the dev artifact and `false` in prod.

Behavior change for dev SSR hosts that pass the `development` condition (Vite dev does by default): a header write after the response has committed now **throws** with the offending header named, where the production artifact continues to `console.error` and drop the write.

Also runs `replaceDev(false)` on `solid-js`'s production server build so a future `_SOLID_DEV_` gate in `src/server/` cannot constant-fold into the dev branch in production.
5 changes: 5 additions & 0 deletions .changeset/signals-dev-cjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

Add a development CJS build, `dist/node.dev.cjs`, selected by the `development` condition on the `require` branch of `exports`. Previously `require` always resolved to the production `dist/node.cjs` (`__DEV__` false), so a CJS host that resolved `solid-js`'s dev server artifact would get `DEV === undefined` from its `@solidjs/signals` dependency — a dev server runtime whose diagnostics channel was silently absent. Dev CJS is the unmangled twin of `dist/dev.js`; the production CJS is unchanged.
7 changes: 5 additions & 2 deletions .cursor/rules/web.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ Browser-safe stubs that log errors if server APIs are accidentally used in brows
## Build Outputs

- `dist/web.js` — production browser
- `dist/dev.js` — development browser (with `_SOLID_DEV_` enabled)
- `dist/server.js` — server (Node/Deno/Worker)
- `dist/web.dev.js` — development browser (with `_SOLID_DEV_` enabled)
- `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. Dev-ness is a build mode, not an entry.

Rollup with Babel for JSX transformation, `@rollup/plugin-replace` for dev/prod flags.
285 changes: 285 additions & 0 deletions documentation/plans/server-dev-build-plan.md

Large diffs are not rendered by default.

740 changes: 740 additions & 0 deletions documentation/proposals/production-observability-sketch.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@
},
"require": {
"types": "./dist/types-cjs/index.d.cts",
"development": "./dist/node.dev.cjs",
"default": "./dist/node.cjs"
}
},
"./package.json": "./package.json"
},
"scripts": {
"build": "npm-run-all -nl build:* && pnpm types",
"build:clean": "rimraf dist/dev dist/prod dist/node dist/dev.js dist/prod.js dist/node.cjs",
"build:clean": "rimraf dist/dev dist/prod dist/node dist/dev.js dist/prod.js dist/node.cjs dist/node.dev.cjs",
"build:js": "rollup -c && node ./scripts/mangle-props.mjs dist/prod dist/node.cjs && node ./scripts/check-pure.mjs dist/prod",
"types": "tsc -p tsconfig.build.json && node ../../scripts/sync-dual-types.mjs ./dist/types ./dist/types-cjs",
"test": "vitest run",
Expand Down
35 changes: 34 additions & 1 deletion packages/signals/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import prettier from "rollup-plugin-prettier";
// consumed exclusively by bundlers, which can drop whole feature modules —
// including their top-level GlobalQueue hook installs, which statement-level
// shaking of a flat file can never remove (#2883) — and scope-hoist the rest
// back into one module. Dev and node stay flat single files: dev bundle size
// back into one module. Dev and node (prod + dev CJS) stay flat single files: dev bundle size
// doesn't matter (and vitest's per-module SSR transform makes a chunked tree
// ~2x slower in the flush hot path, poisoning CI benches), and CJS `require`
// can't tree-shake, so a tree would charge unbundled SSR the per-module-
Expand Down Expand Up @@ -95,5 +95,38 @@ export default [
parser: "typescript"
})
]
},
{
// Dev CJS — the `require` twin of dist/dev.js, selected by the
// `development` condition on the `require` branch. Without it a CJS host
// that resolved solid-js's `dist/server.dev.cjs` would `require` the prod
// `dist/node.cjs` (`__DEV__` false) and get `DEV === undefined` from a dev
// artifact — a lie that goes unnoticed until something emits into
// `DEV.diagnostics`. Flat single file like node.cjs; not mangled (dev
// outputs never are — see build:js).
input: "src/index.ts",
output: {
file: "dist/node.dev.cjs",
format: "cjs",
exports: "named"
},
plugins: [
replace({
__DEV__: "true",
__TEST__: "false",
preventAssignment: true
}),
typescript({
declaration: false,
outDir: "dist",
module: "esnext",
target: "esnext",
moduleResolution: "bundler",
verbatimModuleSyntax: true
}),
prettier({
parser: "typescript"
})
]
}
];
35 changes: 35 additions & 0 deletions packages/signals/tests/dist-cjs-artifacts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Direct coverage of the built CJS artifacts — dist/node.cjs (prod, the
* `require` default) and dist/node.dev.cjs (the `development` condition on
* the `require` branch). The suite otherwise imports source under vitest's
* `__DEV__: true` define, so it cannot see whether each artifact's replace
* ran. `DEV` is the observable: defined in dev, `undefined` in prod. The dev
* CJS exists so a CJS host that resolves solid-js's `dist/server.dev.cjs`
* also gets a signals with a live `DEV.diagnostics` — one dev flag per
* process, not one per module format. Requires a prior build (the turbo
* `test` task depends on `build`).
*/
import { createRequire } from "node:module";
import { describe, expect, test } from "vitest";

const require = createRequire(import.meta.url);

describe("@solidjs/signals CJS artifacts", () => {
test("dist/node.cjs (prod) exports DEV as undefined", () => {
const prod = require("../dist/node.cjs");
expect(prod.DEV).toBeUndefined();
});

test("dist/node.dev.cjs exports a live DEV with the diagnostics channel", () => {
const dev = require("../dist/node.dev.cjs");
expect(dev.DEV).toBeDefined();
expect(typeof dev.DEV.diagnostics.subscribe).toBe("function");
expect(typeof dev.DEV.diagnostics.capture).toBe("function");
});

test("the two artifacts export the same surface", () => {
const prod = require("../dist/node.cjs");
const dev = require("../dist/node.dev.cjs");
expect(Object.keys(dev).sort()).toEqual(Object.keys(prod).sort());
});
});
38 changes: 34 additions & 4 deletions packages/solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
"exports": {
".": {
"worker": {
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/server.dev.cjs"
}
},
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.js"
Expand All @@ -41,11 +51,11 @@
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/dev.js"
"default": "./dist/solid.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/dev.cjs"
"default": "./dist/solid.dev.cjs"
}
},
"import": {
Expand All @@ -58,6 +68,16 @@
}
},
"deno": {
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/server.dev.cjs"
}
},
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.js"
Expand All @@ -68,6 +88,16 @@
}
},
"node": {
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/server.dev.cjs"
}
},
"import": {
"types": "./types/index.d.ts",
"default": "./dist/server.js"
Expand All @@ -80,11 +110,11 @@
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/dev.js"
"default": "./dist/solid.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/dev.cjs"
"default": "./dist/solid.dev.cjs"
}
},
"import": {
Expand Down
30 changes: 27 additions & 3 deletions packages/solid/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export default [
plugins: [replaceDev(false)].concat(plugins)
},
{
// Prod server build. `src/server/` has no `"_SOLID_DEV_"` gates today, but
// the replace must run anyway: without it babel constant-folds the truthy
// string literal and the first gate anyone adds takes the dev branch in
// production (the #2982 failure @solidjs/web's server entry shipped with).
input: "src/server/index.ts",
output: [
{
Expand All @@ -56,17 +60,37 @@ export default [
}
],
external: ["@solidjs/signals", "stream"],
plugins
plugins: [replaceDev(false)].concat(plugins)
},
{
// Dev server build, selected by the `development` condition nested under
// `node`/`worker`/`deno` in package.json exports (nested on purpose: at the
// top level `node` precedes `development` and would win). Until this
// existed, SSR had no dev build at all — server-side dev diagnostics had
// nowhere to run. Mirrors `@solidjs/web/server-functions`'s server.dev.
input: "src/server/index.ts",
output: [
{
file: "dist/server.dev.cjs",
format: "cjs"
},
{
file: "dist/server.dev.js",
format: "es"
}
],
external: ["@solidjs/signals", "stream"],
plugins: [replaceDev(true)].concat(plugins)
},
{
input: "src/index.ts",
output: [
{
file: "dist/dev.cjs",
file: "dist/solid.dev.cjs",
format: "cjs"
},
{
file: "dist/dev.js",
file: "dist/solid.dev.js",
format: "es"
}
],
Expand Down
12 changes: 10 additions & 2 deletions packages/solid/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DEV as _DEV, type Dev } from "@solidjs/signals";

// From mock signals (same exports that index.ts pulls from @solidjs/signals)
export {
$PROXY,
Expand Down Expand Up @@ -126,5 +128,11 @@ export function materializeContainerTrace(marker: unknown): unknown {
return marker;
}

// Dev — no dev mode on server
export const DEV = undefined;
// Dev — same shape as the client entry. `"_SOLID_DEV_"` is replaced per build
// (dist/server.dev.* → true, dist/server.* → false), so the dev artifact
// exposes @solidjs/signals' DEV object — its `diagnostics` channel is the bus
// server-side dev findings report through — and prod exports `undefined`.
// The server reimplements reactivity, so `DEV.attribution`/graph helpers
// have nothing to introspect here; the channel is what's shared.
const IS_DEV = "_SOLID_DEV_" as string | boolean;
export const DEV: Dev | undefined = IS_DEV ? _DEV : undefined;
39 changes: 39 additions & 0 deletions packages/solid/test/server/dist-server-artifact.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @vitest-environment node */
/**
* Direct coverage of the built server artifacts — dist/server.js (prod) and
* dist/server.dev.js (the `development` export condition, nested under
* node/worker/deno). The rest of the suite imports source, where the
* `"_SOLID_DEV_"` literal is truthy, so it cannot see whether the replace
* ran per artifact. `DEV` is the observable: the server entry gates it the
* same way the client does, so the dev artifact must expose @solidjs/signals'
* DEV object (the diagnostics channel server-side findings report through)
* and the prod artifact must export `undefined`. A string scan can't pin
* this — babel's constant folding erases the marker either way.
* Requires a prior `pnpm build`.
*/
import { describe, expect, test } from "vitest";
// Relative imports on purpose: they bypass the `solid-js` → source alias in
// vite.config.mjs so the built artifacts themselves are under test.
// @ts-ignore — dist files have no adjacent type declarations.
import * as prod from "../../dist/server.js";
// @ts-ignore
import * as dev from "../../dist/server.dev.js";
import { DEV as signalsDEV } from "@solidjs/signals";

describe("solid-js server artifacts", () => {
test("dist/server.js (prod) exports DEV as undefined", () => {
expect(prod.DEV).toBeUndefined();
});

test("dist/server.dev.js exports @solidjs/signals' DEV object", () => {
expect(dev.DEV).toBeDefined();
// Not a copy or a stub: the very object signals exports, so a subscriber
// on `DEV.diagnostics` sees server findings without a second channel.
expect(dev.DEV).toBe(signalsDEV);
expect(typeof dev.DEV.diagnostics.subscribe).toBe("function");
});

test("the two artifacts export the same surface", () => {
expect(Object.keys(dev).sort()).toEqual(Object.keys(prod).sort());
});
});
9 changes: 7 additions & 2 deletions packages/solid/test/server/export-parity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ describe("Export parity: server mirrors client", () => {
expect(missingFromServer).toEqual([]);
});

test("all function exports are functions (or undefined for DEV)", () => {
test("all function exports are functions (DEV is the same signals object as the client's)", () => {
for (const [key, value] of Object.entries(server)) {
if (key === "DEV") {
expect(value).toBeUndefined();
// Both entries gate on `"_SOLID_DEV_"` (truthy here — source, not
// dist) and re-export @solidjs/signals' DEV, so the server's dev
// diagnostics channel IS the client's. dist/server.js exports
// `undefined`; the dist artifact specs pin that.
expect(value).toBeDefined();
expect(value).toBe(client.DEV);
continue;
}
if (typeof (client as any)[key] === "function") {
Expand Down
4 changes: 2 additions & 2 deletions packages/universal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
"development": {
"import": {
"types": "./types/index.d.ts",
"default": "./dist/dev.js"
"default": "./dist/universal.dev.js"
},
"require": {
"types": "./types-cjs/index.d.cts",
"default": "./dist/dev.cjs"
"default": "./dist/universal.dev.cjs"
}
},
"import": {
Expand Down
4 changes: 2 additions & 2 deletions packages/universal/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export default [
input: "src/index.ts",
output: [
{
file: "dist/dev.cjs",
file: "dist/universal.dev.cjs",
format: "cjs"
},
{
file: "dist/dev.js",
file: "dist/universal.dev.js",
format: "es"
}
],
Expand Down
10 changes: 10 additions & 0 deletions packages/web/frames/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
"default": "./dist/client.cjs"
}
},
"development": {
"import": {
"types": "../types/frames/server.d.ts",
"default": "./dist/server.dev.js"
},
"require": {
"types": "../types-cjs/frames/server.d.cts",
"default": "./dist/server.dev.cjs"
}
},
"import": {
"types": "../types/frames/server.d.ts",
"default": "./dist/server.js"
Expand Down
Loading
Loading