From e9afe8a518336ffb1345cc3a5df416323059703b Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 24 Sep 2026 00:15:45 -0700 Subject: [PATCH 1/2] feat: source names for the dev and observe postures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `componentNames` becomes the compiler's `sourceNames`, covering components, compiled binding effects (`span.textContent`, `div.children`) and primitives. Primitives are named by the compiler's `transformSourceNames` pass — `createSignal(0)` declared as `count` becomes `createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive — which the plugin runs ahead of the JSX transform, and alone on plain `.ts`/`.js` modules outside node_modules, so attribution chains, diagnostics owner paths and the Chrome performance tracks read source identifiers instead of `signal` / `computed` / `effect`. `solid.sourceNames` (`boolean | { components?, bindings?, primitives? }`) overrides the posture default (on for dev and observe, off for production). On a compiler without the pass, primitives keep their generic labels and the plugin warns once. Requires the @solidjs/compiler release carrying `sourceNames` and `transformSourceNamesAsync` (2.0.0-rc.10). Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- .changeset/source-names.md | 5 ++ README.md | 23 ++++- src/index.ts | 170 ++++++++++++++++++++++++++++++++----- 3 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 .changeset/source-names.md diff --git a/.changeset/source-names.md b/.changeset/source-names.md new file mode 100644 index 0000000..e3eb812 --- /dev/null +++ b/.changeset/source-names.md @@ -0,0 +1,5 @@ +--- +"@solidjs/vite-plugin": patch +--- + +Source names for the dev and `observe` postures: the compiler option `componentNames` is now `sourceNames` and covers components, compiled binding effects (`span.textContent`, `div.children`), and primitives. Primitives are named by the compiler's new `transformSourceNames` pass — `createSignal(0)` declared as `count` becomes `createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive — which the plugin now runs ahead of the JSX transform, and alone on plain `.ts`/`.js` modules (outside `node_modules`), so attribution chains, diagnostics owner paths, and the Chrome performance tracks read source identifiers instead of `signal` / `computed` / `effect`. `solid.sourceNames` (`boolean | { components?, bindings?, primitives? }`) overrides the posture default. Requires the `@solidjs/compiler` release carrying `sourceNames` and `transformSourceNames`; on an older compiler the primitives pass is skipped with a warning. diff --git a/README.md b/README.md index 68b757c..b8a5cec 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,9 @@ This is useful for extra logs and debug. Resolve Solid's observe builds in production: the production-speed runtime that keeps the diagnostics and attribution channels (`OBSERVE`) alive for observability tooling (error monitoring, performance tracing). Adds the `observe` export condition to every environment -and turns on the compiler's `componentNames` option, so component owner labels (``) -survive minification. Under `vite dev` the dev build still wins. +and turns on the compiler's source names (see `options.solid.sourceNames`), so graph labels +(``, `span.textContent`, `count`) survive minification. Under `vite dev` the dev build +still wins. #### options.hot @@ -935,6 +936,24 @@ defaults (`moduleName: "@solidjs/web"`, the control-flow built-ins, custom-element context, and conditional wrapping) internally; anything set here is merged over them and applied to whichever backend is selected. +##### options.solid.sourceNames + +- Type: `boolean | { components?: boolean; bindings?: boolean; primitives?: boolean }` +- Default: on for dev and `observe`, off for production builds + +Which names as written in source are carried into output so the dev and observe runtimes +can label the reactive graph after minification — in diagnostics, `whyDidRun` chains, error +owner paths, and the Chrome performance tracks. `components` emits the tag name +(`createComponent(Home, props, "Home")` → owners read ``); `bindings` names compiled +binding effects by what they write (`span.textContent`, `div.class:active`, a hole +`div.children`); `primitives` names `createSignal`/`createMemo`/`createStore`/… after the +identifier they are declared as (`count`, `doubled`, `todos.title`), prefixed with the enclosing +non-component function for composed primitives (`createCounter.value`). Primitives are named by +the compiler's `transformSourceNames` pass, which runs on `.ts`/`.js` modules as well as +components (never inside `node_modules`) and never overrides an explicit `name` option. The +production runtimes ignore the names, so production builds do not carry them. `true`/`false` sets +every kind; an object overrides per kind, the rest keeping the posture default. + #### options.typescript - Type: [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript) diff --git a/src/index.ts b/src/index.ts index f4d6687..2610d1d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,7 +191,33 @@ export interface ExtensionOptions { } export type Compiler = 'babel' | 'native'; -export type SolidOptions = Omit; +/** + * Which source names the compilers carry into output for the dev and + * observe runtimes to label the reactive graph with (see + * `Options.solid.sourceNames`). Each kind defaults to the posture: on for + * dev and `observe`, off for production. + */ +export interface SourceNamesOptions { + /** `createComponent(Home, props, "Home")` — owners labelled ``. */ + components?: boolean; + /** Binding effects named by what they write: `span.textContent`, `div.children`. */ + bindings?: boolean; + /** + * Primitives named after the identifier they are declared as — + * `createSignal(0, { name: "count" })`, `createCounter.value` inside a + * composed primitive — by the compiler's `transformSourceNames` pass. Runs + * on `.ts`/`.js` modules as well as components (outside node_modules). + */ + primitives?: boolean; +} + +export type SolidOptions = Omit & { + /** + * Source names in output: `true`/`false` for every kind, or per kind. + * Defaults to the posture — on for dev and `observe`, off for production. + */ + sourceNames?: boolean | SourceNamesOptions; +}; type NativeCompiler = typeof import('@solidjs/compiler'); let nativeCompilerPromise: Promise | undefined; @@ -239,8 +265,9 @@ export interface Options { * the diagnostics and attribution channels (`OBSERVE`) alive for * production observability tooling. Adds the `observe` export condition * to every environment (client and server, inlined and externalized) and - * turns on the compiler's `componentNames` option so owner labels survive - * minification. Applies to `vite build` and preview; under `vite dev` the + * turns on the compiler's `sourceNames` (components, bindings, and + * primitives) so graph labels survive minification. Applies to `vite + * build` and preview; under `vite dev` the * `development` condition still wins (the dev build is a superset). * * @default false @@ -592,23 +619,94 @@ function getSolidOptions( // builtIns, contextToCustomElements, wrapConditionals) are baked into both // backends — @solidjs/compiler and @solidjs/babel-plugin — so only the // posture this plugin actually decides is passed. - // Component labels: the dev and observe runtimes name each component's - // owner (``) for diagnostics and attribution paths. Without the - // compiler carrying the source tag name, a minified build labels owners by - // whatever the minifier left of `Comp.name`. Both generates emit it — the - // ssr generate from the compilers that carry solidjs/solid#3441 - // (2.0.0-rc.9), so server findings and boundary records locate by - // component too — and the production runtime ignores the argument, so it - // is only emitted for the postures whose runtime reads it. + // Source names: the dev and observe runtimes label each component's owner + // (``) and each compiled binding effect (`span.textContent`) for + // diagnostics and attribution paths. Without the compiler carrying the + // source names, a minified build labels owners by whatever the minifier + // left of `Comp.name` and bindings as `effect`. Both generates emit the + // component name — the ssr generate from the compilers that carry + // solidjs/solid#3441 (2.0.0-rc.9), so server findings and boundary + // records locate by component too — and the production runtime ignores + // the arguments, so they are only emitted for the postures whose runtime + // reads them. Primitive names are the separate transformSourceNames pass + // (see getSourceNames / the transform hook), not a JSX-compiler option. + const { sourceNames: _userSourceNames, ...userSolidOptions } = options.solid || {}; + const { components, bindings } = getSourceNames(options, dev, observe); return { ...solidOptions, ...(serverComponents && solidOptions.generate === 'ssr' ? { serverComponents: true } : {}), dev, - ...(dev || observe ? { componentNames: true } : {}), - ...(options.solid || {}), + ...(components || bindings ? { sourceNames: { components, bindings } } : {}), + ...userSolidOptions, }; } +/** + * The resolved `solid.sourceNames` posture: every kind defaults to on for + * dev and `observe` and off for production; `true`/`false` sets all three, + * an object overrides per kind. + */ +function getSourceNames( + options: Partial, + dev: boolean, + observe: boolean, +): Required { + const posture = dev || observe; + const user = options.solid?.sourceNames; + if (typeof user === 'boolean') return { components: user, bindings: user, primitives: user }; + return { + components: user?.components ?? posture, + bindings: user?.bindings ?? posture, + primitives: user?.primitives ?? posture, + }; +} + +/** + * The `sourceNames.primitives` pass is plain JavaScript in and out, so it + * also applies to the `.ts`/`.js` modules primitives are composed in — the + * ids the JSX transform gate below would otherwise return early for. A + * `.d.ts` has nothing to name. + */ +const PRIMITIVES_ONLY_MODULE = /\.[mc]?[jt]s$/i; +const DECLARATION_MODULE = /\.d\.[mc]?ts$/i; +/** + * Cheap pre-check ahead of the native call: the pass only names calls that + * resolve to imports from these modules, so source without either string + * cannot change. + */ +const PRIMITIVE_SOURCES = ['solid-js', '@solidjs/signals']; + +let warnedMissingSourceNamesPass = false; + +/** + * Run the compiler's `transformSourceNames` pass, or leave the code alone + * (warning once) on a compiler predating it — the pass is a default-on + * nicety for dev/observe, not something to fail a build over. + */ +async function transformPrimitiveNames( + ctx: { warn(message: string): void }, + compiler: NativeCompiler, + code: string, + filename: string, +): Promise<{ code: string; map: ChainableMap } | null> { + if (!PRIMITIVE_SOURCES.some((source) => code.includes(source))) return null; + if (typeof compiler.transformSourceNamesAsync !== 'function') { + if (!warnedMissingSourceNamesPass) { + warnedMissingSourceNamesPass = true; + ctx.warn( + '@solidjs/vite-plugin: the installed @solidjs/compiler has no transformSourceNames ' + + 'pass, so primitives keep their generic labels (`signal`, `computed`) in ' + + 'diagnostics. Update @solidjs/compiler, or set `solid.sourceNames.primitives: false`.', + ); + } + return null; + } + const result = await compiler.transformSourceNamesAsync(code, { filename, sourceMap: true }); + // Nothing to name: the pass hands the source back verbatim, with no map. + if (result.code === code) return null; + return { code: result.code, map: result.map }; +} + async function getBabelUserOptions( options: Partial, source: string, @@ -1661,12 +1759,27 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { const moduleId = id; id = id.replace(/\?.*$/, ''); const isTsrx = isTsrxModule(id); + const inNodeModules = /node_modules/.test(id); + // Primitive names stop at node_modules: a dependency's internals + // (Solid's own flow controls included) name what they mean to name, + // and a composed primitive from a library is identified by its + // package, not re-labelled by this app's build. + const namePrimitives = + !inNodeModules && getSourceNames(options, replaceDev, observe).primitives; if (!(/\.[mc]?[tj]sx$/i.test(id) || isTsrx || allExtensions.includes(currentFileExtension))) { + // Not a JSX module. The one pass that still applies is primitive + // naming — `createSignal` lives in `.ts`/`.js` as much as in + // components — and it runs alone: no lazy/refresh/JSX work. + if (namePrimitives && PRIMITIVES_ONLY_MODULE.test(id) && !DECLARATION_MODULE.test(id)) { + const compiler = await loadNativeCompiler(); + const named = await transformPrimitiveNames(this, compiler, source, id); + if (named === null) return null; + return { code: named.code, map: normalizeSourceMap(named.map) }; + } return null; } - const inNodeModules = /node_modules/.test(id); const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, observe, isTestMode); // We need to know if the current file extension has a typescript options tied to it @@ -1709,15 +1822,26 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx'); - // Shared native prelude for every mode: the lazy() module-URL pass, - // then (dev/client/non-node_modules) the solid-refresh HMR pass, both - // operating on pre-JSX source. Only the JSX transform itself differs - // between compiler backends. Sourcemaps are collected in application - // order and merged at the end. + // Shared native prelude for every mode: (dev/observe) the primitive + // naming pass, the lazy() module-URL pass, then (dev/client/ + // non-node_modules) the solid-refresh HMR pass, all operating on + // pre-JSX source. Only the JSX transform itself differs between + // compiler backends. Sourcemaps are collected in application order and + // merged at the end. const compiler = await loadNativeCompiler(); let code = source; const maps: ChainableMap[] = []; + // Authored TSRX cannot be parsed by a standalone pass; its primitives + // are named after Solid lowering, on the generated module, below. + if (namePrimitives && !isTsrx) { + const named = await transformPrimitiveNames(this, compiler, code, nativeFilename); + if (named !== null) { + code = named.code; + maps.push(named.map); + } + } + if (isTsrx) { // Solid lowering preserves authored TypeScript annotations; secondary // passes therefore parse the generated module as TSX even though no @@ -1774,6 +1898,14 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { maps.push(result.map); } + if (namePrimitives) { + const named = await transformPrimitiveNames(this, compiler, code, generatedFilename); + if (named !== null) { + code = named.code; + maps.push(named.map); + } + } + const lazyResult = await compiler.transformLazyAsync(code, { filename: generatedFilename, sourceMap: true, From 12fe90adc3dfac5ce7d2d3ad32b29d9a1ab22007 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 24 Sep 2026 15:01:00 -0700 Subject: [PATCH 2/2] feat: source names default on in dev, resolved by the plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `solid.sourceNames` resolves through one table: unset is on for dev (`options.dev`, the flag the compilers receive — on under `vite dev`) and `observe`, off for production builds; `true`/`false` sets every kind; the object form sets kinds individually with the rest at the posture default. The JSX compilers default their own `sourceNames` on under `dev` from rc.10, so `getSolidOptions` now always passes the resolved value — `false` when components and bindings are both off — instead of omitting the key. Otherwise `solid.sourceNames: false` would be a no-op in dev: the compiler default would turn the names back on. README and changeset describe the default and the split: component and binding names are the JSX compiler option, passed to whichever backend compiles the JSX; primitive names are the native compiler's standalone `transformSourceNames` pass, which runs on every module — babel apps included. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- .changeset/source-names.md | 2 +- README.md | 38 +++++++++++++++++++++++---------- src/index.ts | 43 +++++++++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/.changeset/source-names.md b/.changeset/source-names.md index e3eb812..d19f68e 100644 --- a/.changeset/source-names.md +++ b/.changeset/source-names.md @@ -2,4 +2,4 @@ "@solidjs/vite-plugin": patch --- -Source names for the dev and `observe` postures: the compiler option `componentNames` is now `sourceNames` and covers components, compiled binding effects (`span.textContent`, `div.children`), and primitives. Primitives are named by the compiler's new `transformSourceNames` pass — `createSignal(0)` declared as `count` becomes `createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive — which the plugin now runs ahead of the JSX transform, and alone on plain `.ts`/`.js` modules (outside `node_modules`), so attribution chains, diagnostics owner paths, and the Chrome performance tracks read source identifiers instead of `signal` / `computed` / `effect`. `solid.sourceNames` (`boolean | { components?, bindings?, primitives? }`) overrides the posture default. Requires the `@solidjs/compiler` release carrying `sourceNames` and `transformSourceNames`; on an older compiler the primitives pass is skipped with a warning. +Source names on by default in dev and under `observe`: the compiler option `componentNames` is now `sourceNames` and covers components, compiled binding effects (`span.textContent`, `div.children`), and primitives. Component and binding names are the JSX compiler's `sourceNames` option, passed to whichever backend compiles the JSX. Primitives are named by the native compiler's standalone `transformSourceNames` pass — `createSignal(0)` declared as `count` becomes `createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive — which the plugin runs ahead of the JSX transform and alone on plain `.ts`/`.js` modules (outside `node_modules`), for babel apps as well, so attribution chains, diagnostics owner paths, and the Chrome performance tracks read source identifiers instead of `signal` / `computed` / `effect`. Every kind defaults to on whenever the plugin compiles with `dev` (`vite dev`, or `dev: true`) or for `observe` builds, and to off for production builds, whose output is unchanged. `solid.sourceNames: false` opts out of every kind (in dev too — the plugin always passes the resolved value, so the compilers' own dev default does not re-enable it), `true` forces every kind on, and the object form `{ components?, bindings?, primitives? }` sets kinds individually with the rest at the posture default. Requires the `@solidjs/compiler` release carrying `sourceNames` and `transformSourceNames` (2.0.0-rc.10); on an older compiler the primitives pass is skipped with a one-time warning. diff --git a/README.md b/README.md index b8a5cec..ed607cf 100644 --- a/README.md +++ b/README.md @@ -939,20 +939,36 @@ here is merged over them and applied to whichever backend is selected. ##### options.solid.sourceNames - Type: `boolean | { components?: boolean; bindings?: boolean; primitives?: boolean }` -- Default: on for dev and `observe`, off for production builds +- Default: on in dev and under `observe`, off in production builds Which names as written in source are carried into output so the dev and observe runtimes can label the reactive graph after minification — in diagnostics, `whyDidRun` chains, error -owner paths, and the Chrome performance tracks. `components` emits the tag name -(`createComponent(Home, props, "Home")` → owners read ``); `bindings` names compiled -binding effects by what they write (`span.textContent`, `div.class:active`, a hole -`div.children`); `primitives` names `createSignal`/`createMemo`/`createStore`/… after the -identifier they are declared as (`count`, `doubled`, `todos.title`), prefixed with the enclosing -non-component function for composed primitives (`createCounter.value`). Primitives are named by -the compiler's `transformSourceNames` pass, which runs on `.ts`/`.js` modules as well as -components (never inside `node_modules`) and never overrides an explicit `name` option. The -production runtimes ignore the names, so production builds do not carry them. `true`/`false` sets -every kind; an object overrides per kind, the rest keeping the posture default. +owner paths, and the Chrome performance tracks. Three kinds: + +- `components` emits the tag name (`createComponent(Home, props, "Home")` → owners read + ``). +- `bindings` names compiled binding effects by what they write (`span.textContent`, + `div.class:active`, a hole `div.children`). +- `primitives` names `createSignal`/`createMemo`/`createStore`/… after the identifier they + are declared as (`count`, `doubled`, `todos.title`), prefixed with the enclosing + non-component function for composed primitives (`createCounter.value`). An explicit + `name` option is never overridden. + +The kinds come from two different places. `components` and `bindings` are the JSX compiler's +own `sourceNames` option, passed through to whichever backend compiles your JSX +(`@solidjs/compiler` or `@solidjs/babel-plugin`). `primitives` is the native compiler's +standalone `transformSourceNames` pass — plain JavaScript in and out — which the plugin runs +on every module it sees, `.ts`/`.js` files included, never inside `node_modules`. The plugin +always runs its non-JSX work through the native compiler, so babel apps get primitive names +too. + +The default follows the posture: on whenever the plugin compiles for the dev runtime (the +same `dev` flag it hands the compilers — `vite dev`, or `dev: true`) and for `observe` +builds; off for production builds, whose runtime ignores the names anyway, so production +output is unchanged. `sourceNames: false` turns every kind off, in dev too; `true` turns every +kind on, production builds included. The object form sets the listed kinds and leaves the +rest at the posture default — `{ primitives: false }` keeps component and binding names in +dev but skips the primitives pass. #### options.typescript diff --git a/src/index.ts b/src/index.ts index 2610d1d..e0850bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -192,10 +192,13 @@ export interface ExtensionOptions { export type Compiler = 'babel' | 'native'; /** - * Which source names the compilers carry into output for the dev and - * observe runtimes to label the reactive graph with (see + * Which source names are carried into output for the dev and observe + * runtimes to label the reactive graph with (see * `Options.solid.sourceNames`). Each kind defaults to the posture: on for - * dev and `observe`, off for production. + * dev and `observe`, off for production builds. `components` and `bindings` + * are the JSX compiler's `sourceNames` option; `primitives` is the native + * compiler's standalone `transformSourceNames` pass, which the plugin runs + * on every module — babel apps included. */ export interface SourceNamesOptions { /** `createComponent(Home, props, "Home")` — owners labelled ``. */ @@ -205,8 +208,9 @@ export interface SourceNamesOptions { /** * Primitives named after the identifier they are declared as — * `createSignal(0, { name: "count" })`, `createCounter.value` inside a - * composed primitive — by the compiler's `transformSourceNames` pass. Runs - * on `.ts`/`.js` modules as well as components (outside node_modules). + * composed primitive — by the native compiler's `transformSourceNames` + * pass. Runs on `.ts`/`.js` modules as well as components (outside + * node_modules), whichever JSX compiler the app uses. */ primitives?: boolean; } @@ -214,7 +218,8 @@ export interface SourceNamesOptions { export type SolidOptions = Omit & { /** * Source names in output: `true`/`false` for every kind, or per kind. - * Defaults to the posture — on for dev and `observe`, off for production. + * Defaults to the posture — on for dev and `observe`, off for production + * builds; `false` opts out of every kind, in dev too. */ sourceNames?: boolean | SourceNamesOptions; }; @@ -630,21 +635,39 @@ function getSolidOptions( // the arguments, so they are only emitted for the postures whose runtime // reads them. Primitive names are the separate transformSourceNames pass // (see getSourceNames / the transform hook), not a JSX-compiler option. + // + // The compilers default their own `sourceNames` on under `dev` (rc.10), so + // the resolved value is always passed — `false` when both kinds are off — + // and the plugin's table below, not the compiler default, decides. That is + // what makes `solid.sourceNames: false` an opt-out in dev rather than a + // no-op. const { sourceNames: _userSourceNames, ...userSolidOptions } = options.solid || {}; const { components, bindings } = getSourceNames(options, dev, observe); return { ...solidOptions, ...(serverComponents && solidOptions.generate === 'ssr' ? { serverComponents: true } : {}), dev, - ...(components || bindings ? { sourceNames: { components, bindings } } : {}), + sourceNames: components || bindings ? { components, bindings } : false, ...userSolidOptions, }; } /** - * The resolved `solid.sourceNames` posture: every kind defaults to on for - * dev and `observe` and off for production; `true`/`false` sets all three, - * an object overrides per kind. + * Resolve `solid.sourceNames` to one flag per kind — `components` and + * `bindings` go to the JSX compiler, `primitives` gates the standalone + * `transformSourceNames` pass in the transform hook. + * + * The default follows the posture. `dev` here is the same flag the compilers + * receive as `dev` (`options.dev`, which defaults to on under `vite dev` and + * off for `vite build`), and `observe` is `options.observe`; those are the + * two runtimes that read the names, and the production runtime ignores them. + * + * | `solid.sourceNames` | dev or observe | production | + * | ----------------------- | ------------------------- | -------------------------- | + * | (unset) | all on | all off | + * | `true` | all on | all on | + * | `false` | all off | all off | + * | `{ kind: true/false }` | as given; the rest on | as given; the rest off | */ function getSourceNames( options: Partial,