@solidjs/compiler 2.0.0-rc.2 → 2.0.0-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +101 -8
  2. package/index.js +161 -17
  3. package/package.json +9 -9
  4. package/types.d.ts +157 -5
package/README.md CHANGED
@@ -78,17 +78,43 @@ const result = transform(source, {
78
78
  });
79
79
  ```
80
80
 
81
+ ### TSRX (experimental)
82
+
83
+ TSRX (TypeScript Render Extensions) is a syntax for declarative UI whose constructs (`@if`/`@else`, `@for … @empty`, `@switch`/`@case`, `@try`/`@catch`/`@pending`, and `@{}` statement containers) desugar to the Solid control-flow components. `.tsrx` filenames route through the TSRX frontend automatically and compile to the same output as `@solidjs/babel-plugin`'s TSRX support, byte for byte.
84
+
85
+ ```js
86
+ const result = transform(tsrxSource, { filename: "App.tsrx" });
87
+ // TSRX <style> blocks are extracted alongside the JavaScript:
88
+ result.css;
89
+ result.cssHash;
90
+ ```
91
+
92
+ Routing follows the filename by default (`syntax: "auto"`); pass `syntax: "tsrx"` or `syntax: "jsx"` to force a frontend regardless of filename. No extra install is needed — the shipped binaries include the frontend (Rust embedders can disable the default `tsrx` cargo feature).
93
+
94
+ Scoped `<style>` blocks are compile-time only. The compiler removes the style element, adds its `tsrx-<hash>` class to matching native and dynamic elements, scopes and prunes the CSS, and returns the stylesheet in `css` with its scope identifier in `cssHash`. Style expressions produce class-map objects, `<style ref={styles}>` initializes the requested class map, and `:global(...)` opts individual selectors out of scoping. A bundler integration must emit the returned CSS; the core compiler does not inject a runtime style helper.
95
+
96
+ Solid rejects authored TSRX lazy destructuring (`&{ … }` / `&[ … ]`). Keep accessor calls and reactive property reads explicit in Solid source.
97
+
98
+ Control-flow bindings pass through exactly as authored. `@for … index` and `@for … key` hand the callback the item as an accessor (and the index as an accessor under a custom key), and `@catch (err, reset)` receives Solid's `ErrorAccessor` — write `item()`, `i()`, `err()` as you would in JSX. Destructuring a binding in one of those positions is rejected with a diagnostic, since there is nothing to destructure; the default keyed `@for` item is a raw value and destructures as usual.
99
+
100
+ The frontend uses the community [oxc-tsrx](https://github.com/tsrx-org/oxc) parser at a pinned revision. Statement containers can be used as function bodies, statements, expressions (`const x = @{ … }`), and JSX children or expression containers. See `documentation/tsrx/frontend-notes.md` in the repository for the full frontend notes.
101
+
102
+ `projectTsrxForTypecheck(source, { filename })` is an experimental compiler-owned projection for editor and typecheck integrations. It returns independently typecheckable TSX without running the DOM/SSR/universal transforms, injecting collision-safe imports for generated Solid control-flow and dynamic-element helpers. The tooling-only path recovers common incomplete editor snapshots while normal compilation remains strict. The result also includes an authored `.tsrx` source map, exact equal-text `mappings`, processed `css`/`cssHash`, and parser-authored embedded CSS/raw-script regions. Mapping and embedded offsets use JavaScript UTF-16 string coordinates. The ranges deliberately omit generated-only text; host adapters such as Volar attach feature capabilities to them.
103
+
81
104
  ### Source maps
82
105
 
83
- Pass `sourceMap: true` to receive a JSON source map string in `result.map`.
106
+ Pass `sourceMap: true` to receive a JSON source map string in `result.map`. For TSRX, the compiler composes Oxc's generated-JavaScript map through the internal TSX text projection, returning the original `.tsrx` filename and source in `sources` and `sourcesContent`. Authored expressions and lazy/accessor rewrites map back to their TSRX locations; projection-only scaffolding remains explicitly unmapped rather than being attributed to nearby syntax.
84
107
 
85
108
  ### Options
86
109
 
87
110
  - `filename`
88
111
  - `moduleName` (default `"@solidjs/web"`)
112
+ - `syntax`: `"auto"`, `"jsx"`, or `"tsrx"` (default `"auto"` — routes `.tsrx` filenames through the TSRX frontend)
89
113
  - `generate`: `"dom"`, `"ssr"`, `"universal"`, or `"dynamic"` (default `"dom"`)
90
114
  - `hydratable`
91
115
  - `dev`
116
+ - `root` (`string`, default the working directory): the project root `sourceNames.locations` paths (and `transformSourceNames`'s `loc`) are made relative to — the same meaning as `transformDirectives`'s `root`.
117
+ - `sourceNames` (`boolean | { components?: boolean; bindings?: boolean; locations?: boolean }`, default follows `dev`): names as written in source, carried into output so dev/observe runtimes can label the reactive graph after minification. Unset, every kind is on when `dev: true` and off otherwise; `true`/`false` sets every kind (`sourceNames: false` opts a dev build out); the object form picks, and each kind it leaves unspecified follows `dev`. Production output (`dev: false`) is byte-identical with the option set or unset. Same shape and defaults as `@solidjs/babel-plugin`'s option. `components` emits the source tag name as `createComponent`'s third argument (`createComponent(Home, props, "Home")`) — DOM and SSR output (SSR keeps the `createComponent` call it otherwise inlines to `Comp(props)`); not universal or dynamic. `bindings` names every compiled binding effect by what it writes — `effect(…, { name: "span.textContent" })`, a hole `insert(el, v, undefined, undefined, { name: "div.children" })`, a spread `spread(el, props, false, undefined, "div")` (labelled `div.spread` / `div.children` by the runtime) — DOM output only. `locations` adds the source position beside each name the other two kinds emit, as `<path>:<line>:<column>` with the path relative to `root` (posix separators, `..` when the file is outside the root) and the column 1-based (the stack-trace and editor convention, so `http://localhost:5173/src/App.tsx:18:3` links to the right place): `createComponent(Home, props, "Home", "src/App.tsx:12:5")` — the start of the opening element — and `loc` right after `name` in a binding's options, `effect(…, { name: "div.class", loc: "src/App.tsx:14:7" })` — the attribute's start, a hole's `{`, a spread's `{...props}` (a spread's location rides as one more positional string after the tag). Nothing is emitted without a `filename`. The production runtimes ignore the names. These are the JSX-level kinds only: naming the primitives themselves is the separate `transformSourceNames` pass below, which the build tool runs on every module independently of the JSX compiler
92
118
  - `sourceMap`
93
119
  - `contextToCustomElements` (default `true`)
94
120
  - `delegateEvents`
@@ -105,12 +131,12 @@ Pass `sourceMap: true` to receive a JSON source map string in `result.map`.
105
131
  - `omitLastClosingTag`
106
132
  - `builtIns` (default `["For", "Show", "Switch", "Match", "Loading", "Reveal", "Portal", "Repeat", "Dynamic", "Errored"]`)
107
133
  - `requireImportSource`
108
- - `serverComponents`
134
+ - `hoistProps` (default `true`; SSR only)
109
135
  - `renderers`
110
136
 
111
137
  ### Server function directives (experimental)
112
138
 
113
- `transformDirectives(code, options)` is a second pass for `"use server"`. It applies to plain `.js`/`.ts` as well as JSX/TSX.
139
+ `transformDirectives(code, options)` is a second pass for `"use server"`. It accepts ordinary JavaScript/TypeScript, including JSX/TSX. For a `.tsrx` module, run `transform()` first, then pass its generated code to `transformDirectives()` with the same original `.tsrx` filename so function IDs use the manifest path. `transformDirectives()` does not parse raw TSRX syntax itself.
114
140
 
115
141
  ```js
116
142
  const { transformDirectives } = require("@solidjs/compiler");
@@ -126,23 +152,90 @@ result.code;
126
152
  result.functions; // [{ id, name, exports }] for manifest building
127
153
  ```
128
154
 
129
- The runtime module defaults to `@solidjs/web/server-functions`. Function IDs use `xxhash32(root-relative path)-<count>` (name-suffixed with `env: "development"`). There are also experimental `transformLazy` and `transformRefresh` passes.
155
+ A function-level `"use server"` function is extracted out of its lexical position, so it may only reference its own parameters and locals, module top-level bindings, and globals. Capturing anything else is a compile error. That includes `this` and `arguments` in a marked arrow, which an arrow takes from the function it was written in. Use a `function` if the server function needs its own `this` or `arguments`.
156
+
157
+ A function-level directive only works where the pass can extract the function: a function declaration (at any nesting depth), a function expression, or an arrow with a block body. Methods, getters, and setters are never extracted, so a directive on one is a compile error rather than a directive that silently does nothing. Assign a function to a property instead.
158
+
159
+ A module-level `"use server"` module can only export server functions. Its client build is rebuilt from those exports alone, so anything else would be missing from the browser bundle. Re-exports, `export *`, class and enum exports, destructured exports, and exports declared without an initializer are compile errors naming the export and its position. Type-only and `declare` exports are erased and are fine.
160
+
161
+ The runtime module defaults to `@solidjs/web/server-functions`. Function IDs are `<name>-<xxhash32(root-relative path)>`, the same in every env. The name is the function's dotted binding path, such as `handlers.save`, built from every named container on the way down (variable bindings, property keys, class names, class members, and named functions), so an id identifies a function by where it is bound rather than by its position in the file. Adding, removing, or reordering functions does not move the ids of the others. There are also experimental `transformLazy` and `transformRefresh` passes.
162
+
163
+ ### Source names for primitives
164
+
165
+ `transformSourceNames(code, { filename?, locations?, root?, sourceMap? })` is the pass behind `@solidjs/vite-plugin`'s `sourceNames.primitives` option: it names reactive primitives after the identifier they are declared as, so the dev and observe runtimes label graph nodes `count` / `doubled` / `todos.title` instead of `signal` / `computed` / `store.title`. With `locations` (default `true` — the pass only runs for the dev and observe postures) and a `filename`, the options literal also carries the call's position right after the name, `createSignal(0, { name: "count", loc: "src/store.ts:3:22" })` (`filename` relative to `root`, column 1-based); an explicit `name` is kept and `loc` still added, an explicit `loc` is never overridden. It is plain JavaScript in and out (JSX passes through untouched), which is why it is its own pass rather than a `transform()` option — primitives live in `.ts`/`.js` modules as much as in components. This standalone pass is the single owner of primitive naming: the build tool runs it on every module — `.ts`, `.js`, and JSX files alike — independently of which JSX compiler (this one or `@solidjs/babel-plugin`) handles the file's JSX, so there is no Babel counterpart and `transform()`'s `sourceNames` covers only the JSX-level kinds. `@solidjs/vite-plugin` (solid-vite-plugin #371, `solid: { sourceNames }`) runs it ahead of the JSX transform for the dev and `observe` postures.
166
+
167
+ ```js
168
+ const [count, setCount] = createSignal(0); // createSignal(0, { name: "count" })
169
+ const doubled = createMemo(() => count() * 2); // createMemo(…, { name: "doubled" })
170
+ const [todos, setTodos] = createStore({ list: [] }); // createStore(…, { name: "todos" })
171
+ export function createCounter() {
172
+ const [value, setValue] = createSignal(0); // { name: "createCounter.value" }
173
+ …
174
+ }
175
+ function Counter() {
176
+ const [n, setN] = createSignal(0); // { name: "n" } — no prefix in a component
177
+ }
178
+ ```
179
+
180
+ The name comes from the first element of the array pattern, the variable binding, the object-literal property key, or the class field the call initialises; a call in any other position (a hole in the pattern, an argument, an assignment) is left alone. Inside a non-component function — anything not PascalCase: `createCounter`, `useTheme`, a method — the name is prefixed with that function's, so a composed primitive's nodes fold under it (`createCounter.value`, `createCounter.twice`); components contribute no prefix, and anonymous callbacks inherit the nearest named function. Only calls that resolve to imports from `solid-js` or `@solidjs/signals` are named (aliases and namespace imports included; a shadowing local or another library's `createSignal` is not). The pass never overrides an explicit `name`, leaves a spread or non-literal options argument alone, fills omitted positional arguments with `void 0`, and reaches the call through `as`/`satisfies`/`!` and type arguments. Named: `createSignal`, `createMemo`, `createOptimistic`, `createStore`, `createOptimisticStore`, `createProjection`. A two-argument `createStore(x, y)` is only named when `y` is a non-empty object literal of option keys (`shallow`/`name`) — otherwise it may be a derive passed by reference with its seed.
130
181
 
131
182
  ## Rust compiler core
132
183
 
133
184
  The crate also exposes a host-independent Rust API. The crate name is `solidjs-compiler`; the Node `transform()` delegates to the same core.
134
185
 
135
186
  ```rust
136
- use solidjs_compiler::{compile, CompileOptions};
187
+ use solidjs_compiler::{
188
+ compile, project_tsrx_for_typecheck, CompileOptions,
189
+ TsrxTypecheckProjectionOptions,
190
+ };
137
191
 
138
192
  let output = compile(
139
193
  "const view = <div>{name()}</div>;",
140
194
  &CompileOptions::default(),
141
195
  )?;
196
+
197
+ let tsrx_source = "export function View() @{ <div /> }";
198
+ let virtual_tsx =
199
+ project_tsrx_for_typecheck(tsrx_source, &TsrxTypecheckProjectionOptions::default())?;
142
200
  ```
143
201
 
144
202
  `CompileOptions::default()` uses `module_name: "@solidjs/web"` and the same control-flow `built_ins` as the Babel plugin. Build with `--no-default-features` when embedding without the Node-API adapter.
145
203
 
204
+ A frontend that builds its own Oxc AST instead of source text hands the program to `compile_program`, which runs the same back ends `compile` reaches after parsing. The program must be allocated in the allocator passed beside it, and `source` must be its `source_text`: the authored text its spans index, so source maps and diagnostics land on authored positions (keep authored spans on carried-over nodes and leave synthesized nodes at `SPAN`). The caller must build against the same Oxc release as this crate.
205
+
206
+ ```rust
207
+ use oxc_allocator::Allocator;
208
+ use solidjs_compiler::{compile_program, CompileOptions};
209
+
210
+ let allocator = Allocator::default();
211
+ let program = my_frontend::lower(&allocator, authored_source)?; // oxc_ast::ast::Program
212
+ let output = compile_program(&allocator, program, authored_source, &CompileOptions::default())?;
213
+ ```
214
+
215
+ TSRX's scoped-CSS engine is exported as `solidjs_compiler::style` for other frontends, in every build (it does not need the `tsrx` feature). It has no dependency on TSRX syntax or Oxc: describe the elements a `<style>` block governs as `Element` values, and `compile_style` returns the scoped stylesheet, its hash, the class map and the ids of the elements to annotate, the same bytes a `.tsrx` file with that stylesheet at that `StyleLocation` gets.
216
+
217
+ ```rust
218
+ use solidjs_compiler::style::{compile_style, Element, ElementKind, StyleInput, StyleKind, StyleLocation};
219
+
220
+ let elements = [Element {
221
+ id: 1,
222
+ kind: ElementKind::Native("div".into()),
223
+ attributes: Vec::new(),
224
+ has_spread: false,
225
+ children: Vec::new(),
226
+ }];
227
+ let output = compile_style(StyleInput {
228
+ css: "div { color: red }",
229
+ location: StyleLocation { filename: "view.rx", line: 3, column: 4 },
230
+ elements: &elements,
231
+ kind: StyleKind::Block,
232
+ minify: false,
233
+ })?;
234
+ // output.css, output.hash, output.class_map, output.scoped_elements
235
+ ```
236
+
237
+ The unstable Rust typecheck projection reports embedded ranges in authored UTF-8 bytes. The N-API adapter converts those ranges to UTF-16 code units for JavaScript tooling.
238
+
146
239
  > **Stability:** the Rust API is unstable while the compiler is pre-1.0. Options, output, and error types may change in any release — pin an exact revision when embedding it.
147
240
 
148
241
  ## Performance
@@ -151,9 +244,9 @@ Compared against `@solidjs/babel-plugin` compiling identical sources under ident
151
244
 
152
245
  | Workload | babel-plugin | compiler | Speedup |
153
246
  | ----------------------------------------------- | -----------: | -------: | ------: |
154
- | Fixture corpus (88 files, 175 KB, all 10 modes) | 440 ms | 19 ms | 23x |
155
- | 129 KB single module | 545 ms | 9.4 ms | 58x |
156
- | 1 MB single module | 24,975 ms | 70 ms | 355x |
247
+ | Fixture corpus (88 files, 175 KB, all 10 modes) | 440 ms | 19 ms | 23x |
248
+ | 129 KB single module | 545 ms | 9.4 ms | 58x |
249
+ | 1 MB single module | 24,975 ms | 70 ms | 355x |
157
250
 
158
251
  Native throughput stays roughly flat as input grows, while Babel's per-file cost grows super-linearly.
159
252
 
package/index.js CHANGED
@@ -10,24 +10,68 @@ function transform(code, options) {
10
10
  throw new TypeError("@solidjs/compiler transform() expects source code as a string");
11
11
  }
12
12
 
13
- const nativeOptions = validateOptions(code, options);
13
+ const nativeOptions = hostRoot(validateOptions(code, options));
14
14
  const result = native.transform(code, nativeOptions);
15
- return {
15
+ const output = {
16
16
  code: result.code,
17
17
  map: result.map ?? null
18
18
  };
19
+ // Preserve the established JSX result shape. Native TSRX transforms always
20
+ // return a CSS string (including `""` when no styles are present), which
21
+ // makes the sidecar fields a route-specific extension.
22
+ if (result.css != null) {
23
+ output.css = result.css;
24
+ output.cssHash = result.cssHash ?? null;
25
+ }
26
+ return output;
19
27
  }
20
28
 
21
29
  function transformAsync(code, options) {
22
30
  return Promise.resolve().then(() => transform(code, options));
23
31
  }
24
32
 
33
+ function projectTsrxForTypecheck(code, options) {
34
+ if (typeof code !== "string") {
35
+ throw new TypeError(
36
+ "@solidjs/compiler projectTsrxForTypecheck() expects source code as a string"
37
+ );
38
+ }
39
+ const nativeOptions = validateTypecheckProjectionOptions(options);
40
+ const result = native.projectTsrxForTypecheck(code, nativeOptions);
41
+ return {
42
+ code: result.code,
43
+ map: result.map,
44
+ mappings: result.mappings,
45
+ css: result.css,
46
+ cssHash: result.cssHash ?? null,
47
+ embeddedRegions: result.embeddedRegions
48
+ };
49
+ }
50
+
51
+ function validateTypecheckProjectionOptions(options) {
52
+ if (options == null) return options;
53
+ if (typeof options !== "object" || Array.isArray(options)) {
54
+ throw new TypeError(
55
+ "@solidjs/compiler projectTsrxForTypecheck() expects options to be an object"
56
+ );
57
+ }
58
+ for (const key of Object.keys(options)) {
59
+ if (key !== "filename") {
60
+ throw new Error(`@solidjs/compiler received unknown option \`${key}\``);
61
+ }
62
+ }
63
+ if (options.filename !== undefined && typeof options.filename !== "string") {
64
+ throw new TypeError("@solidjs/compiler `filename` option must be a string");
65
+ }
66
+ return options;
67
+ }
68
+
25
69
  function transformDirectives(code, options) {
26
70
  if (typeof code !== "string") {
27
71
  throw new TypeError("@solidjs/compiler transformDirectives() expects source code as a string");
28
72
  }
29
73
 
30
- const nativeOptions = validateDirectivesOptions(options);
74
+ const nativeOptions = hostRoot(validateDirectivesOptions(options));
31
75
  const result = native.transformDirectives(code, nativeOptions);
32
76
  return {
33
77
  code: result.code,
@@ -75,6 +119,47 @@ function transformRefreshAsync(code, options) {
75
119
  return Promise.resolve().then(() => transformRefresh(code, options));
76
120
  }
77
121
 
122
+ function transformSourceNames(code, options) {
123
+ if (typeof code !== "string") {
124
+ throw new TypeError("@solidjs/compiler transformSourceNames() expects source code as a string");
125
+ }
126
+
127
+ const nativeOptions = hostRoot(validateSourceNamesOptions(options));
128
+ const result = native.transformSourceNames(code, nativeOptions);
129
+ return {
130
+ code: result.code,
131
+ map: result.map ?? null
132
+ };
133
+ }
134
+
135
+ function transformSourceNamesAsync(code, options) {
136
+ return Promise.resolve().then(() => transformSourceNames(code, options));
137
+ }
138
+
139
+ const sourceNamesOptionKeys = new Set(["filename", "locations", "root", "sourceMap"]);
140
+
141
+ function validateSourceNamesOptions(options) {
142
+ if (options == null) return options;
143
+ if (typeof options !== "object" || Array.isArray(options)) {
144
+ throw new TypeError("@solidjs/compiler transformSourceNames() expects options to be an object");
145
+ }
146
+
147
+ const nativeOptions = {};
148
+ for (const [key, value] of Object.entries(options)) {
149
+ if (!sourceNamesOptionKeys.has(key)) {
150
+ throw new Error(`@solidjs/compiler received unknown option \`${key}\``);
151
+ }
152
+ if ((key === "filename" || key === "root") && typeof value !== "string") {
153
+ throw new TypeError(`@solidjs/compiler \`${key}\` option must be a string`);
154
+ }
155
+ if ((key === "sourceMap" || key === "locations") && typeof value !== "boolean") {
156
+ throw new TypeError(`@solidjs/compiler \`${key}\` option must be boolean`);
157
+ }
158
+ nativeOptions[key] = value;
159
+ }
160
+ return nativeOptions;
161
+ }
162
+
78
163
  const lazyOptionKeys = new Set(["filename", "sourceMap"]);
79
164
 
80
165
  function validateLazyOptions(options) {
@@ -109,7 +194,8 @@ const refreshOptionKeys = new Set([
109
194
  "sourceMap"
110
195
  ]);
111
196
 
112
- const refreshBundlers = new Set(["esm", "vite", "webpack5", "rspack-esm", "standard"]);
197
+ // The runtime modes `solid-js/refresh` knows (its `RuntimeType`).
198
+ const refreshBundlers = new Set(["vite", "standard"]);
113
199
 
114
200
  function validateRefreshOptions(options) {
115
201
  if (options == null) return options;
@@ -132,9 +218,7 @@ function validateRefreshOptions(options) {
132
218
  throw new TypeError(`@solidjs/compiler \`${key}\` option must be boolean`);
133
219
  }
134
220
  if (key === "bundler" && !refreshBundlers.has(value)) {
135
- throw new TypeError(
136
- '@solidjs/compiler `bundler` option must be "esm", "vite", "webpack5", "rspack-esm" or "standard"'
137
- );
221
+ throw new TypeError('@solidjs/compiler `bundler` option must be "vite" or "standard"');
138
222
  }
139
223
  if (key === "jsx") {
140
224
  // The Babel plugin's JSX-granularity mode (its default!) is not
@@ -213,10 +297,12 @@ function validateImportDefinition(key, value) {
213
297
 
214
298
  const nativeOptionKeys = new Set([
215
299
  "filename",
300
+ "root",
216
301
  "moduleName",
217
302
  "generate",
218
303
  "hydratable",
219
304
  "dev",
305
+ "sourceNames",
220
306
  "sourceMap",
221
307
  "contextToCustomElements",
222
308
  "delegateEvents",
@@ -232,7 +318,7 @@ const nativeOptionKeys = new Set([
232
318
  "staticMarker",
233
319
  "omitNestedClosingTags",
234
320
  "omitLastClosingTag",
235
- "serverComponents",
321
+ "hoistProps",
236
322
  "builtIns",
237
323
  "renderers"
238
324
  ]);
@@ -277,6 +363,21 @@ function validateOptions(code, options) {
277
363
  nativeOptions.validate = value;
278
364
  continue;
279
365
  }
366
+ if (key === "sourceNames") {
367
+ validateSourceNames(value);
368
+ nativeOptions.sourceNames = value;
369
+ continue;
370
+ }
371
+ // Removed with binding slots (2026-10-16). Still accepted, and ignored,
372
+ // because @solidjs/vite-plugin passes it for server-component builds.
373
+ if (key === "serverComponents") continue;
374
+ if (key === "root") {
375
+ if (typeof value !== "string") {
376
+ throw new TypeError("@solidjs/compiler `root` option must be a string");
377
+ }
378
+ nativeOptions.root = value;
379
+ continue;
380
+ }
280
381
  if (nativeOptionKeys.has(key)) {
281
382
  if (key === "renderers") validateRenderers(value);
282
383
  nativeOptions[key] = value;
@@ -287,6 +388,23 @@ function validateOptions(code, options) {
287
388
  return nativeOptions;
288
389
  }
289
390
 
391
+ const sourceNameKinds = new Set(["components", "bindings", "locations"]);
392
+
393
+ function validateSourceNames(value) {
394
+ if (typeof value === "boolean") return;
395
+ if (typeof value !== "object" || value == null || Array.isArray(value)) {
396
+ throw new TypeError("@solidjs/compiler `sourceNames` option must be boolean or an object");
397
+ }
398
+ for (const [kind, enabled] of Object.entries(value)) {
399
+ if (!sourceNameKinds.has(kind)) {
400
+ throw new Error(`@solidjs/compiler received unknown \`sourceNames\` kind \`${kind}\``);
401
+ }
402
+ if (typeof enabled !== "boolean") {
403
+ throw new TypeError(`@solidjs/compiler \`sourceNames.${kind}\` must be boolean`);
404
+ }
405
+ }
406
+ }
407
+
290
408
  function validateRenderers(renderers) {
291
409
  if (renderers == null) return;
292
410
  if (!Array.isArray(renderers)) {
@@ -327,6 +445,23 @@ function tryPackage(packageName) {
327
445
  }
328
446
  }
329
447
 
448
+ // The binding makes `filename` relative to `root` (`loc` paths, directive
449
+ // hashes), resolving both against its own working directory. A WASI binding
450
+ // cannot see the host's (it starts at `/`), so hand it a `root` that needs
451
+ // none: absolute beside an absolute filename (the working directory when
452
+ // unset), relative beside a relative one. The result is unchanged on a native
453
+ // binding.
454
+ function hostRoot(nativeOptions) {
455
+ if (nativeOptions == null || typeof nativeOptions.filename !== "string") return nativeOptions;
456
+ const { filename, root } = nativeOptions;
457
+ if (path.isAbsolute(filename)) {
458
+ nativeOptions.root = path.resolve(root ?? ".");
459
+ } else if (root !== undefined && path.isAbsolute(root)) {
460
+ nativeOptions.root = path.relative(process.cwd(), root) || ".";
461
+ }
462
+ return nativeOptions;
463
+ }
464
+
330
465
  function requireBinding() {
331
466
  const explicit = process.env.SOLID_COMPILER_NATIVE;
332
467
  if (explicit) return require(explicit);
@@ -343,12 +478,10 @@ function requireBinding() {
343
478
  let nativeError;
344
479
  const suffix = platformArchSuffix();
345
480
 
346
- if (suffix) {
347
- const next = tryPackage(`@solidjs/compiler-${suffix}`);
348
- if (next.binding) return next.binding;
349
- if (next.error) nativeError = next.error;
350
- }
351
-
481
+ // Local builds first: the published package ships no local binary (see
482
+ // "files"), so a `compiler.node` next to this file can only be a
483
+ // development build — which must shadow the installed platform package,
484
+ // or the monorepo's own tests silently run against the last release.
352
485
  const localCandidates = [];
353
486
  if (suffix) localCandidates.push(`compiler.${suffix}.node`);
354
487
  localCandidates.push("compiler.node");
@@ -358,12 +491,20 @@ function requireBinding() {
358
491
  try {
359
492
  return require(full);
360
493
  } catch (error) {
361
- nativeError = error;
362
- break;
494
+ // A present-but-unloadable dev build is an error, not a fallback:
495
+ // degrading to the published binary would silently test stale code.
496
+ error.message += `\nA local development build (${file}) exists but could not be loaded. Rebuild with \`pnpm run build:debug\` or delete it to use the installed @solidjs/compiler-* package.`;
497
+ throw error;
363
498
  }
364
499
  }
365
500
  }
366
501
 
502
+ if (suffix) {
503
+ const next = tryPackage(`@solidjs/compiler-${suffix}`);
504
+ if (next.binding) return next.binding;
505
+ if (next.error) nativeError = next.error;
506
+ }
507
+
367
508
  const wasi = requireWasi();
368
509
  if (wasi) return wasi;
369
510
 
@@ -405,10 +546,13 @@ function isMissingPackage(error, packageName) {
405
546
  module.exports = {
406
547
  transform,
407
548
  transformAsync,
549
+ projectTsrxForTypecheck,
408
550
  transformDirectives,
409
551
  transformDirectivesAsync,
410
552
  transformLazy,
411
553
  transformLazyAsync,
412
554
  transformRefresh,
413
- transformRefreshAsync
555
+ transformRefreshAsync,
556
+ transformSourceNames,
557
+ transformSourceNamesAsync
414
558
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solidjs/compiler",
3
3
  "description": "Solid's native Oxc JSX compiler",
4
- "version": "2.0.0-rc.2",
4
+ "version": "2.0.0-rc.20",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "bench": "pnpm run build && node scripts/bench.mjs",
31
31
  "lint": "cargo clippy --manifest-path ./Cargo.toml -- -D warnings",
32
32
  "test": "pnpm run test:rust && pnpm run build:debug && vitest run",
33
- "test:rust": "cargo test --manifest-path ./Cargo.toml && cargo test --manifest-path ./Cargo.toml --no-default-features",
33
+ "test:rust": "cargo test --manifest-path ./Cargo.toml && cargo test --manifest-path ./Cargo.toml --no-default-features && cargo test --manifest-path ./Cargo.toml --no-default-features --features tsrx",
34
34
  "artifacts": "napi artifacts",
35
35
  "napi:version": "napi version && node ./sync-optional-deps.mjs",
36
36
  "create-npm-dirs": "napi create-npm-dirs"
@@ -57,16 +57,16 @@
57
57
  "devDependencies": {
58
58
  "@emnapi/core": "^1.11.3",
59
59
  "@emnapi/runtime": "^1.11.3",
60
- "@napi-rs/cli": "^3.7.2",
60
+ "@napi-rs/cli": "^3.10.6",
61
61
  "@napi-rs/wasm-runtime": "^1.1.6",
62
62
  "emnapi": "^1.11.3"
63
63
  },
64
64
  "optionalDependencies": {
65
- "@solidjs/compiler-darwin-x64": "2.0.0-rc.2",
66
- "@solidjs/compiler-darwin-arm64": "2.0.0-rc.2",
67
- "@solidjs/compiler-linux-x64-gnu": "2.0.0-rc.2",
68
- "@solidjs/compiler-linux-arm64-gnu": "2.0.0-rc.2",
69
- "@solidjs/compiler-win32-x64-msvc": "2.0.0-rc.2",
70
- "@solidjs/compiler-wasm32-wasi": "2.0.0-rc.2"
65
+ "@solidjs/compiler-darwin-x64": "2.0.0-rc.20",
66
+ "@solidjs/compiler-darwin-arm64": "2.0.0-rc.20",
67
+ "@solidjs/compiler-linux-x64-gnu": "2.0.0-rc.20",
68
+ "@solidjs/compiler-linux-arm64-gnu": "2.0.0-rc.20",
69
+ "@solidjs/compiler-win32-x64-msvc": "2.0.0-rc.20",
70
+ "@solidjs/compiler-wasm32-wasi": "2.0.0-rc.20"
71
71
  }
72
72
  }
package/types.d.ts CHANGED
@@ -1,10 +1,60 @@
1
+ /** The object form of `sourceNames`: which kinds of source name to carry into output. */
2
+ export interface SourceNamesOptions {
3
+ /** Component owner labels: the tag as written, as `createComponent`'s third argument. */
4
+ components?: boolean;
5
+ /** Binding effect labels: the element and attribute (or hole) each compiled effect writes. */
6
+ bindings?: boolean;
7
+ /**
8
+ * Source locations beside each emitted name: `file:line:col` (`filename`
9
+ * relative to `root`, column 1-based) as `createComponent`'s fourth
10
+ * argument and as `loc` after `name` in a binding's options. Only where
11
+ * the name is emitted, and only with a `filename`.
12
+ */
13
+ locations?: boolean;
14
+ }
15
+
1
16
  export interface TransformOptions {
2
17
  filename?: string;
18
+ /**
19
+ * Project root the `sourceNames.locations` paths are made relative to.
20
+ * Defaults to the working directory.
21
+ */
22
+ root?: string;
3
23
  /** Default `"@solidjs/web"`. */
4
24
  moduleName?: string;
25
+ /**
26
+ * Source syntax frontend, matching `@solidjs/babel-plugin`: `"auto"`
27
+ * (default) routes `.tsrx` filenames through the TSRX frontend and
28
+ * everything else through standard JSX; `"tsrx"` and `"jsx"` force a
29
+ * frontend regardless of filename. TSRX support is experimental.
30
+ */
31
+ syntax?: "auto" | "jsx" | "tsrx";
5
32
  generate?: "dom" | "ssr" | "universal" | "dynamic";
6
33
  hydratable?: boolean;
7
34
  dev?: boolean;
35
+ /**
36
+ * Names as written in source, carried into output so the dev and observe
37
+ * runtimes can label the reactive graph after minification.
38
+ * `components`: the tag as a third `createComponent` argument
39
+ * (`createComponent(Home, props, "Home")`) — DOM and SSR output (SSR
40
+ * keeps the `createComponent` call it would otherwise inline to
41
+ * `Comp(props)`); not universal or dynamic. `bindings`: every compiled
42
+ * binding effect named by what it writes — `span.textContent`,
43
+ * `div.class:active`, a hole `div.children`, a spread `div.spread` — as
44
+ * an options argument on `effect`/`insert`/`spread`; DOM output only.
45
+ * `locations`: the source position (`src/App.tsx:12:5`, `filename`
46
+ * relative to `root`) beside each emitted name — a fourth `createComponent`
47
+ * argument, `loc` after `name` in a binding's options — so a console
48
+ * report can point at the line; nothing without a `filename`.
49
+ * The production runtimes ignore the names. Defaults to `dev`: unset,
50
+ * every kind is on in dev and off otherwise; `true`/`false` sets every
51
+ * kind; an object picks, and each kind it leaves unspecified follows
52
+ * `dev`. Primitive names (`createSignal(0, { name: "count" })`) are not a
53
+ * kind here — they come from the standalone `transformSourceNames` pass,
54
+ * which the build tool runs on every module independently of this
55
+ * transform.
56
+ */
57
+ sourceNames?: boolean | SourceNamesOptions;
8
58
  sourceMap?: boolean;
9
59
  contextToCustomElements?: boolean;
10
60
  delegateEvents?: boolean;
@@ -19,7 +69,10 @@ export interface TransformOptions {
19
69
  validate?: boolean;
20
70
  omitNestedClosingTags?: boolean;
21
71
  omitLastClosingTag?: boolean;
72
+ /** @deprecated Ignored. Binding slots, the only thing it compiled for, are removed. */
22
73
  serverComponents?: boolean;
74
+ /** SSR-only, default `true`: component props literals with getters compile to module-level constructors with shared getters (#3511). */
75
+ hoistProps?: boolean;
23
76
  /** Default `["For", "Show", "Switch", "Match", "Loading", "Reveal", "Portal", "Repeat", "Dynamic", "Errored"]`. */
24
77
  builtIns?: string[];
25
78
  requireImportSource?: false | string;
@@ -35,6 +88,10 @@ export interface RendererOption {
35
88
  export interface TransformResult {
36
89
  code: string;
37
90
  map?: string | null;
91
+ /** Extracted scoped CSS for TSRX sources. */
92
+ css?: string | null;
93
+ /** Space-separated TSRX scope hashes. */
94
+ cssHash?: string | null;
38
95
  }
39
96
 
40
97
  export function transform(code: string, options?: TransformOptions | null): TransformResult;
@@ -43,6 +100,49 @@ export function transformAsync(
43
100
  options?: TransformOptions | null
44
101
  ): Promise<TransformResult>;
45
102
 
103
+ export interface ProjectTsrxForTypecheckOptions {
104
+ filename?: string;
105
+ }
106
+
107
+ export interface TsrxTypecheckEmbeddedRegion {
108
+ kind: "css" | "script";
109
+ /** Authored JavaScript string offset in UTF-16 code units. */
110
+ start: number;
111
+ /** Authored JavaScript string offset in UTF-16 code units. */
112
+ end: number;
113
+ content: string;
114
+ }
115
+
116
+ export interface TsrxTypecheckMapping {
117
+ /** Authored JavaScript string offset in UTF-16 code units. */
118
+ sourceStart: number;
119
+ /** Generated JavaScript string offset in UTF-16 code units. */
120
+ generatedStart: number;
121
+ sourceLength: number;
122
+ generatedLength: number;
123
+ }
124
+
125
+ export interface TsrxTypecheckProjectionResult {
126
+ /** Valid post-semantic-rewrite TypeScript/TSX. */
127
+ code: string;
128
+ /** JSON source map from virtual TSX back to the authored `.tsrx` source. */
129
+ map: string;
130
+ /** Exact equal-text ranges suitable for editor feature mappings. */
131
+ mappings: TsrxTypecheckMapping[];
132
+ css: string;
133
+ cssHash: string | null;
134
+ embeddedRegions: TsrxTypecheckEmbeddedRegion[];
135
+ }
136
+
137
+ /**
138
+ * Experimental compiler-owned TSRX projection for typechecking and editor
139
+ * tooling. This API is host-independent and does not run a runtime renderer.
140
+ */
141
+ export function projectTsrxForTypecheck(
142
+ code: string,
143
+ options?: ProjectTsrxForTypecheckOptions | null
144
+ ): TsrxTypecheckProjectionResult;
145
+
46
146
  export interface DirectiveImportDefinition {
47
147
  kind?: "named" | "default";
48
148
  name?: string;
@@ -77,8 +177,12 @@ export interface TransformDirectivesOptions {
77
177
 
78
178
  /** One extracted server function, for building a bundler manifest. */
79
179
  export interface ServerFunctionMeta {
80
- /** The wire ID (`<hash>-<count>[-<name>]`). */
180
+ /** The wire ID (`<name>-<hash>[-<ordinal>]`). */
81
181
  id: string;
182
+ /**
183
+ * The dotted binding path that names the function, such as
184
+ * `handlers.save`. `anonymous` when no enclosing binding applies.
185
+ */
82
186
  name: string;
83
187
  /** Export names bound to this function (module-level directives only). */
84
188
  exports: string[];
@@ -132,12 +236,12 @@ export interface TransformRefreshOptions {
132
236
  */
133
237
  filename?: string;
134
238
  /**
135
- * Selects the HMR API: `import.meta.hot` (esm/vite),
136
- * `import.meta.webpackHot` (webpack5/rspack-esm) or `module.hot`
137
- * (standard).
239
+ * Selects the HMR API — the runtime modes `solid-js/refresh` knows:
240
+ * `import.meta.hot` (vite), or `module.hot` / `import.meta.webpackHot`
241
+ * (standard: webpack, Rspack — the runtime probes for whichever exists).
138
242
  * @default "standard"
139
243
  */
140
- bundler?: "esm" | "vite" | "webpack5" | "rspack-esm" | "standard";
244
+ bundler?: "vite" | "standard";
141
245
  /**
142
246
  * Wrap top-level `render()`/`hydrate()` calls (imported from
143
247
  * `@solidjs/web`) with `hot.dispose` cleanup.
@@ -172,3 +276,51 @@ export function transformRefreshAsync(
172
276
  code: string,
173
277
  options?: TransformRefreshOptions | null
174
278
  ): Promise<TransformResult>;
279
+
280
+ /**
281
+ * Options for the `sourceNames.primitives` pass: reactive primitives named
282
+ * after the identifier they are declared as. `const [count, setCount] =
283
+ * createSignal(0)` becomes `createSignal(0, { name: "count" })`; inside a
284
+ * non-component function the name is prefixed with that function's
285
+ * (`createCounter.count`). Only calls resolving to imports from `solid-js` /
286
+ * `@solidjs/signals` are named, and an explicit `name` is never overridden.
287
+ * Plain JavaScript in and out, so it applies to `.ts`/`.js` modules too.
288
+ * This standalone pass is the single owner of primitive naming: the build
289
+ * tool (`@solidjs/vite-plugin`) runs it on every module — `.ts`, `.js` and
290
+ * JSX files alike — independently of which JSX compiler (this one or
291
+ * `@solidjs/babel-plugin`) handles the file's JSX. `transform()`'s
292
+ * `sourceNames` option covers only the JSX-level kinds.
293
+ */
294
+ export interface TransformSourceNamesOptions {
295
+ /**
296
+ * Picks the parser dialect (`.ts`, `.tsx`, `.js`, `.jsx`); TSX without
297
+ * one. Also the file the `loc` strings name: without one no `loc` is
298
+ * written.
299
+ */
300
+ filename?: string;
301
+ /**
302
+ * Write `loc` after `name` in each options literal: the call's
303
+ * `file:line:col` (`filename` relative to `root`, column 1-based), so
304
+ * `createSignal(0, { name: "count", loc: "src/store.ts:3:22" })`. An
305
+ * explicit `name` is kept and `loc` still added; an explicit `loc` is
306
+ * never overridden. The pass only runs for the dev and observe postures,
307
+ * so this is on by default.
308
+ * @default true
309
+ */
310
+ locations?: boolean;
311
+ /**
312
+ * Project root the `loc` paths are made relative to. Defaults to the
313
+ * working directory.
314
+ */
315
+ root?: string;
316
+ sourceMap?: boolean;
317
+ }
318
+
319
+ export function transformSourceNames(
320
+ code: string,
321
+ options?: TransformSourceNamesOptions | null
322
+ ): TransformResult;
323
+ export function transformSourceNamesAsync(
324
+ code: string,
325
+ options?: TransformSourceNamesOptions | null
326
+ ): Promise<TransformResult>;