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
5 changes: 5 additions & 0 deletions .changeset/source-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/vite-plugin": patch
---

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.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<Home>`)
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
(`<Home>`, `span.textContent`, `count`) survive minification. Under `vite dev` the dev build
still wins.

#### options.hot

Expand Down Expand Up @@ -935,6 +936,40 @@ 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 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. Three kinds:

- `components` emits the tag name (`createComponent(Home, props, "Home")` → owners read
`<Home>`).
- `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

- Type: [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript)
Expand Down
193 changes: 174 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,38 @@
}

export type Compiler = 'babel' | 'native';
export type SolidOptions = Omit<JsxCompilerOptions, 'filename' | 'sourceMap'>;
/**
* 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 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 `<Home>`. */
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 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;
}

export type SolidOptions = Omit<JsxCompilerOptions, 'filename' | 'sourceMap' | 'sourceNames'> & {
/**
* Source names in output: `true`/`false` for every kind, or per kind.
* 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;
};
type NativeCompiler = typeof import('@solidjs/compiler');
let nativeCompilerPromise: Promise<NativeCompiler> | undefined;

Expand Down Expand Up @@ -239,8 +270,9 @@
* 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
Expand Down Expand Up @@ -592,23 +624,112 @@
// 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 (`<Home>`) 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
// (`<Home>`) 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.
//
// 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,
...(dev || observe ? { componentNames: true } : {}),
...(options.solid || {}),
sourceNames: components || bindings ? { components, bindings } : false,
...userSolidOptions,
};
}

/**
* 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<Options>,
dev: boolean,
observe: boolean,
): Required<SourceNamesOptions> {
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') {

Check failure on line 716 in src/index.ts

View workflow job for this annotation

GitHub Actions / ⚡️ Continuous Releases

Property 'transformSourceNamesAsync' does not exist on type 'typeof import("/home/runner/work/solid-vite-plugin/solid-vite-plugin/node_modules/.pnpm/@solidjs+compiler@2.0.0-rc.9/node_modules/@solidjs/compiler/types")'. Did you mean 'transformRefreshAsync'?

Check failure on line 716 in src/index.ts

View workflow job for this annotation

GitHub Actions / E2E tests

Property 'transformSourceNamesAsync' does not exist on type 'typeof import("/home/runner/work/solid-vite-plugin/solid-vite-plugin/node_modules/.pnpm/@solidjs+compiler@2.0.0-rc.9/node_modules/@solidjs/compiler/types")'. Did you mean 'transformRefreshAsync'?
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 });

Check failure on line 727 in src/index.ts

View workflow job for this annotation

GitHub Actions / ⚡️ Continuous Releases

Property 'transformSourceNamesAsync' does not exist on type 'typeof import("/home/runner/work/solid-vite-plugin/solid-vite-plugin/node_modules/.pnpm/@solidjs+compiler@2.0.0-rc.9/node_modules/@solidjs/compiler/types")'. Did you mean 'transformRefreshAsync'?

Check failure on line 727 in src/index.ts

View workflow job for this annotation

GitHub Actions / E2E tests

Property 'transformSourceNamesAsync' does not exist on type 'typeof import("/home/runner/work/solid-vite-plugin/solid-vite-plugin/node_modules/.pnpm/@solidjs+compiler@2.0.0-rc.9/node_modules/@solidjs/compiler/types")'. Did you mean 'transformRefreshAsync'?
// 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<Options>,
source: string,
Expand Down Expand Up @@ -1661,12 +1782,27 @@
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
Expand Down Expand Up @@ -1709,15 +1845,26 @@
? 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
Expand Down Expand Up @@ -1774,6 +1921,14 @@
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,
Expand Down
Loading