@solidjs/vite-plugin 3.0.0-next.36 → 3.0.0-next.38

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.
@@ -1,12 +1,12 @@
1
1
  import * as babel from '@babel/core';
2
2
  import remapping from '@ampproject/remapping';
3
3
  import solid from '@solidjs/babel-plugin';
4
- import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
4
+ import fs, { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
5
5
  import { mergeAndConcat } from 'merge-anything';
6
6
  import { createRequire } from 'module';
7
7
  import path from 'path';
8
8
  import { Readable } from 'node:stream';
9
- import { createFilter, normalizePath, loadEnv, runnerImport, defaultClientConditions, defaultServerConditions, defaultExternalConditions } from 'vite';
9
+ import { createFilter, normalizePath, loadEnv, runnerImport, transformWithOxc, defaultClientConditions, defaultServerConditions, defaultExternalConditions } from 'vite';
10
10
  import { pathToFileURL, fileURLToPath } from 'node:url';
11
11
  import { crawlFrameworkPkgs } from 'vitefu';
12
12
 
@@ -63,7 +63,16 @@ function webRequestFromNode(req, urlPath, res) {
63
63
  signal = controller.signal;
64
64
  }
65
65
  const method = req.method || 'GET';
66
- const body = method === 'GET' || method === 'HEAD' ? undefined : Readable.toWeb(req);
66
+ // Only attach a body when the request actually carries one. A web Request
67
+ // built by the browser for a bodyless POST has `body === null`, and the
68
+ // runtime keys off that (a present body that decodes to nothing is a 400
69
+ // since @solidjs/web 2.0.0-rc.5) — so an unconditionally attached (empty)
70
+ // stream misparses bodyless calls. HTTP/1 signals a body via
71
+ // Content-Length/Transfer-Encoding (RFC 9112 §6); the h2 compat API sets
72
+ // `stream.endAfterHeaders` when END_STREAM rode the headers frame.
73
+ const h2Stream = req.stream;
74
+ const hasBody = method !== 'GET' && method !== 'HEAD' && (h2Stream ? !h2Stream.endAfterHeaders : req.headers['transfer-encoding'] !== undefined || req.headers['content-length'] !== undefined && req.headers['content-length'] !== '0');
75
+ const body = hasBody ? Readable.toWeb(req) : undefined;
67
76
  return new Request(url, {
68
77
  method,
69
78
  headers,
@@ -137,6 +146,75 @@ function joinBase(base, pathname) {
137
146
  return (base.endsWith('/') ? base.slice(0, -1) : base) + pathname;
138
147
  }
139
148
 
149
+ const TSRX_CSS_QUERY = '?solid-tsrx-css&lang.css';
150
+ const NULL_BYTE_PLACEHOLDER$1 = '/@id/__x00__';
151
+ function cleanModuleId(id) {
152
+ const query = id.indexOf('?');
153
+ return query === -1 ? id : id.slice(0, query);
154
+ }
155
+ function isTsrxModule(id) {
156
+ return cleanModuleId(id).toLowerCase().endsWith('.tsrx');
157
+ }
158
+ function isTsrxCssModule(id) {
159
+ const unwrapped = id.startsWith('\0') ? id.slice(1) : id.startsWith(NULL_BYTE_PLACEHOLDER$1) ? id.slice(NULL_BYTE_PLACEHOLDER$1.length) : id;
160
+ const queryIndex = unwrapped.indexOf('?');
161
+ if (queryIndex === -1 || !unwrapped.slice(0, queryIndex).toLowerCase().endsWith('.tsrx')) {
162
+ return false;
163
+ }
164
+ const params = unwrapped.slice(queryIndex + 1).split('&');
165
+ return params.includes('solid-tsrx-css') && params.includes('lang.css');
166
+ }
167
+ function resolveTsrxCssModule(id) {
168
+ if (!isTsrxCssModule(id)) return null;
169
+ if (id.startsWith('\0')) return id;
170
+ if (id.startsWith(NULL_BYTE_PLACEHOLDER$1)) {
171
+ return '\0' + id.slice(NULL_BYTE_PLACEHOLDER$1.length);
172
+ }
173
+ return '\0' + id;
174
+ }
175
+ function tsrxCssModuleId(id) {
176
+ return cleanModuleId(id) + TSRX_CSS_QUERY;
177
+ }
178
+ function resolvedTsrxCssModuleId(id) {
179
+ return '\0' + tsrxCssModuleId(id);
180
+ }
181
+ function tsrxCssSourceId(id) {
182
+ if (!id.startsWith('\0') || !isTsrxCssModule(id)) return null;
183
+ return cleanModuleId(id.slice(1));
184
+ }
185
+ function updateTsrxCss(cache, id, css) {
186
+ const key = cleanModuleId(id);
187
+ if (css) {
188
+ cache.set(key, css);
189
+ } else {
190
+ cache.delete(key);
191
+ }
192
+ }
193
+ function prependTsrxCssImport(code, id) {
194
+ return `import ${JSON.stringify(tsrxCssModuleId(id))};\n${code}`;
195
+ }
196
+ function offsetSourceMapLine(map) {
197
+ if (map && typeof map === 'object' && 'mappings' in map && typeof map.mappings === 'string') {
198
+ return {
199
+ ...map,
200
+ mappings: ';' + map.mappings
201
+ };
202
+ }
203
+ if (map && typeof map === 'object' && 'sections' in map && Array.isArray(map.sections)) {
204
+ return {
205
+ ...map,
206
+ sections: map.sections.map(section => ({
207
+ ...section,
208
+ offset: {
209
+ ...section.offset,
210
+ line: section.offset.line + 1
211
+ }
212
+ }))
213
+ };
214
+ }
215
+ return map;
216
+ }
217
+
140
218
  /**
141
219
  * Dev-mode asset resolution: the `virtual:solid-manifest` module exports a
142
220
  * resolver function in dev (instead of the static object a build produces),
@@ -246,6 +324,9 @@ const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
246
324
  // importer controls them — so they must not be SSR'd as style tags.
247
325
  const nonAmbientQueryRegExp = /[?&](url|inline|raw)\b/;
248
326
  const NULL_BYTE_PLACEHOLDER = '/@id/__x00__';
327
+ function isCssModuleUrl(url) {
328
+ return cssFileRegExp.test(url.split('?')[0]) || isTsrxCssModule(url);
329
+ }
249
330
 
250
331
  // Per Vite's convention virtual module ids are prefixed with `\0`, which
251
332
  // cannot appear in an HTML attribute (the parser replaces it). Serialize the
@@ -302,7 +383,7 @@ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, impor
302
383
  const node = await getModuleNode(env, file, importer);
303
384
  if (!node?.id || deps.has(node)) return;
304
385
  deps.add(node);
305
- const isCss = cssFileRegExp.test(node.url.split('?')[0]);
386
+ const isCss = isCssModuleUrl(node.url);
306
387
  if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
307
388
  if (node.file) onFile?.(node.file);
308
389
  if (isCss) return;
@@ -334,8 +415,7 @@ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleF
334
415
  const seen = new Set();
335
416
  for (const node of deps) {
336
417
  if (!node.id) continue;
337
- const cleanUrl = node.url.split('?')[0];
338
- if (!cssFileRegExp.test(cleanUrl) || nonAmbientQueryRegExp.test(node.url)) continue;
418
+ if (!isCssModuleUrl(node.url) || nonAmbientQueryRegExp.test(node.url)) continue;
339
419
  const id = wrapId(node.id);
340
420
  if (seen.has(id)) continue;
341
421
  seen.add(id);
@@ -549,7 +629,11 @@ function boundaryModules() {
549
629
  }
550
630
 
551
631
  /**
552
- * Agent diagnostics surface (`diagnostics: true`, dev serve only).
632
+ * Agent diagnostics surface (dev serve only).
633
+ *
634
+ * Enabled automatically when the app declares `@solidjs/diagnostics` in
635
+ * its package.json (the `diagnostics` option overrides: `true` forces it
636
+ * on and errors if the package is missing, `false` opts out entirely).
553
637
  *
554
638
  * Three pieces:
555
639
  * - an injected client module (virtual, imported by index.html or the
@@ -577,6 +661,36 @@ const METHODS = ['begin', 'end', 'active', 'whyDidRun', 'costs'];
577
661
 
578
662
  /** How long the endpoint waits for a page to answer before failing the call. */
579
663
  const RESPONSE_TIMEOUT_MS = 10_000;
664
+
665
+ /**
666
+ * Whether the app *declares* `@solidjs/diagnostics` — the auto-enable
667
+ * signal. Declaration in the nearest package.json (walking up from the
668
+ * Vite root, so a `client/` root still finds the app manifest) rather
669
+ * than node_modules presence: presence-based detection escapes the app
670
+ * into ancestor installs, which surprise-enables the surface for every
671
+ * fixture app inside a monorepo that happens to have the package
672
+ * somewhere above it (this broke the plugin's own example suites). A
673
+ * declared dependency is unambiguous intent, and resolution then works
674
+ * regardless of hoisting. `diagnostics: true` remains the override for
675
+ * setups the heuristic can't see.
676
+ */
677
+ function detectDiagnosticsPackage(root) {
678
+ let dir = path.resolve(root);
679
+ while (true) {
680
+ const manifestPath = path.join(dir, 'package.json');
681
+ if (fs.existsSync(manifestPath)) {
682
+ try {
683
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
684
+ return !!(manifest.dependencies?.[DIAGNOSTICS_PACKAGE] ?? manifest.devDependencies?.[DIAGNOSTICS_PACKAGE] ?? manifest.optionalDependencies?.[DIAGNOSTICS_PACKAGE]);
685
+ } catch {
686
+ return false;
687
+ }
688
+ }
689
+ const parent = path.dirname(dir);
690
+ if (parent === dir) return false;
691
+ dir = parent;
692
+ }
693
+ }
580
694
  function diagnosticsClientModuleCode() {
581
695
  // Runtime imports resolve to the APP's diagnostics package (see the
582
696
  // resolveId assist below) — the page speaks its own package's protocol.
@@ -603,18 +717,26 @@ function readJsonBody(req) {
603
717
  req.on('error', reject);
604
718
  });
605
719
  }
606
- function solidDiagnostics() {
720
+ function solidDiagnostics(mode = 'auto') {
607
721
  let root = process.cwd();
608
722
  let base = '/';
723
+ // Resolved at configResolved: explicit `true` is unconditional (missing
724
+ // package becomes a hard error at bridge resolution); `'auto'` enables
725
+ // only when the app has the package installed.
726
+ let enabled = mode === true;
609
727
  return {
610
728
  name: 'solid:diagnostics',
611
729
  // Dev-serve only: the channels this fronts exist in dev builds only.
730
+ // Test mode excluded — vitest (including browser mode) runs a dev
731
+ // serve, and injecting the bridge into test pages perturbs suites
732
+ // that never asked for it.
612
733
  apply(_config, env) {
613
- return env.command === 'serve' && !env.isPreview;
734
+ return env.command === 'serve' && !env.isPreview && env.mode !== 'test';
614
735
  },
615
736
  configResolved(config) {
616
737
  root = config.root;
617
738
  base = config.base;
739
+ if (mode === 'auto') enabled = detectDiagnosticsPackage(root);
618
740
  },
619
741
  async resolveId(source, importer) {
620
742
  if (source === DIAGNOSTICS_CLIENT_ID) {
@@ -630,7 +752,7 @@ function solidDiagnostics() {
630
752
  skipSelf: true
631
753
  });
632
754
  if (!resolved || resolved.id.startsWith('__vite-optional-peer-dep:')) {
633
- this.error(`[@solidjs/vite-plugin] the diagnostics option requires ${DIAGNOSTICS_PACKAGE} ` + 'installed in the app (it provides the in-page bridge). Install it as a ' + 'development dependency or remove `diagnostics: true`.');
755
+ this.error(`[@solidjs/vite-plugin] the diagnostics surface requires ${DIAGNOSTICS_PACKAGE} ` + 'installed in the app (it provides the in-page bridge). Install it as a ' + 'development dependency, or set `diagnostics: false` to opt out.');
634
756
  }
635
757
  return resolved;
636
758
  }
@@ -643,6 +765,7 @@ function solidDiagnostics() {
643
765
  // Plain (index.html) apps get the client module injected here;
644
766
  // start-mode apps import it from the generated client entry instead.
645
767
  transformIndexHtml() {
768
+ if (!enabled) return undefined;
646
769
  return [{
647
770
  tag: 'script',
648
771
  attrs: {
@@ -653,6 +776,11 @@ function solidDiagnostics() {
653
776
  }];
654
777
  },
655
778
  configureServer(server) {
779
+ // The whole surface (announcement, middleware, bridge injection) only
780
+ // exists when enabled, so the discovery breadcrumb never lies about
781
+ // a dead endpoint.
782
+ if (!enabled) return;
783
+
656
784
  // Announce the surface in the startup block. This is a discovery
657
785
  // channel: agents watching dev-server output learn the endpoint and
658
786
  // the skill documents without any project-level pointer (AGENTS.md).
@@ -661,7 +789,7 @@ function solidDiagnostics() {
661
789
  originalPrintUrls();
662
790
  const local = server.resolvedUrls?.local[0];
663
791
  const endpoint = local ? new URL(DIAGNOSTICS_ENDPOINT, local).href : DIAGNOSTICS_ENDPOINT;
664
- server.config.logger.info(` ➜ Solid diagnostics: ${endpoint} ` + `(GET status; POST {"method":"begin"|"end"|"whyDidRun"|"costs"})\n` + ` ➜ Agent skills: node_modules/${DIAGNOSTICS_PACKAGE}/skills/agent-loops/SKILL.md, ` + `node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`);
792
+ server.config.logger.info(` ➜ Solid diagnostics: ${endpoint} ` + `(GET status; POST {"method":"begin"|"end"|"whyDidRun"|"costs"})` + (mode === 'auto' ? ' — auto-enabled; `diagnostics: false` opts out' : '') + `\n ➜ Agent skills: node_modules/${DIAGNOSTICS_PACKAGE}/skills/agent-loops/SKILL.md, ` + `node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`);
665
793
  };
666
794
  const pending = new Map();
667
795
  let nextId = 1;
@@ -672,6 +800,11 @@ function solidDiagnostics() {
672
800
  clearTimeout(entry.timer);
673
801
  entry.resolve(data);
674
802
  });
803
+
804
+ // No host/origin validation here: on all supported Vite versions
805
+ // (peer range ^8) Vite's own DNS-rebinding host check runs ahead of
806
+ // plugin middleware — verified: requests with a disallowed Host
807
+ // header get Vite's 403 before reaching this handler.
675
808
  server.middlewares.use(DIAGNOSTICS_ENDPOINT, async (req, res) => {
676
809
  // The middleware mounts on the exact path; anything deeper is 404.
677
810
  if (req.url && req.url !== '/' && req.url !== '') {
@@ -794,7 +927,7 @@ async function compile(id, code, options) {
794
927
  mode: options.mode,
795
928
  env: options.env,
796
929
  directive: options.directive,
797
- sourceMap: true,
930
+ sourceMap: options.sourceMap !== false,
798
931
  register: options.definitions.register,
799
932
  create: options.definitions.create
800
933
  });
@@ -920,11 +1053,11 @@ function xxHash32(buffer, seed = 0) {
920
1053
  * root — not the invocation directory — so running `vite` from outside the
921
1054
  * project keeps compiling the same files. Absolute patterns are used as-is.
922
1055
  *
923
- * @default include "src/**\/*.{jsx,tsx,ts,js,mjs,cjs}", exclude "node_modules/**\/*.{jsx,tsx,ts,js,mjs,cjs}"
1056
+ * @default include "src/**\/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}", exclude "node_modules/**\/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}"
924
1057
  */
925
1058
 
926
- const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,ts,js,mjs,cjs}';
927
- const DEFAULT_EXCLUDE = 'node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}';
1059
+ const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}';
1060
+ const DEFAULT_EXCLUDE = 'node_modules/**/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}';
928
1061
  const DEFAULT_MANIFEST = 'virtual:solid-server-function-manifest';
929
1062
  const DEFAULT_DIRECTIVE = 'use server';
930
1063
  const DEFAULT_RUNTIME = '@solidjs/web/server-functions';
@@ -1167,9 +1300,11 @@ function serverFunctions(options = {}, internal = {}) {
1167
1300
  `export function handleServerFunctionRequest(request, options) {`, ` const { event: eventInit, ...rest } = options || {};`, ` return handle(request, {`, ` provideEvent: provideRequestEvent,`, ` ...(eventInit ? { createEvent: (req) => ({ request: req, locals: {}, ...eventInit }) } : {}),`, ` ...rest,`, ` });`, `}`].join('\n');
1168
1301
  }
1169
1302
 
1170
- // Function IDs are `xxHash32(root-relative path)-<count>` (see compile.ts),
1171
- // so the hash segment maps an incoming ID back to its module. Rebuilt
1172
- // whenever a transform has grown the manifest.
1303
+ // Function IDs are `<name>-<xxHash32(root-relative path)>[-<ordinal>]`
1304
+ // (identity-keyed, solidjs/solid#3109). The name is a JS identifier and
1305
+ // never contains `-`, so the hash is always the second segment and maps
1306
+ // an incoming ID back to its module. Rebuilt whenever a transform has
1307
+ // grown the manifest.
1173
1308
  const hashIndex = new Map();
1174
1309
  let hashIndexSize = -1;
1175
1310
  function moduleForFunctionId(functionId) {
@@ -1181,7 +1316,7 @@ function serverFunctions(options = {}, internal = {}) {
1181
1316
  }
1182
1317
  hashIndexSize = manifest.server.size;
1183
1318
  }
1184
- return hashIndex.get(functionId.split('-', 1)[0]);
1319
+ return hashIndex.get(functionId.split('-')[1]);
1185
1320
  }
1186
1321
  function moduleDevUrl(entry) {
1187
1322
  const relative = path.relative(root, entry).split(path.sep).join('/');
@@ -1219,10 +1354,11 @@ function serverFunctions(options = {}, internal = {}) {
1219
1354
  if (internal.externalDevServer || !isRunnableEnvironment(ssrEnvironment)) {
1220
1355
  return;
1221
1356
  }
1222
- // A call's address is `<endpoint>/<id>` (solidjs/solid#3076)the
1223
- // mount plus exactly one path segment. Bare-mount requests still
1224
- // reach the runtime handler (it answers 404), so misdirected posts
1225
- // fail through the endpoint rather than falling through to SSR.
1357
+ // A call's address is `<endpoint>/<id>` plain HTTP or
1358
+ // `<endpoint>/data/<id>` the scripted transport's own path
1359
+ // (solidjs/solid#3076, #3094). Bare-mount requests still reach the
1360
+ // runtime handler (it answers 404), so misdirected posts fail
1361
+ // through the endpoint rather than falling through to SSR.
1226
1362
  const underMount = (pathname, mount) => pathname === mount || pathname.startsWith(mount + '/');
1227
1363
  server.middlewares.use((req, res, next) => {
1228
1364
  const url = new URL(req.url || '/', 'http://localhost');
@@ -1241,9 +1377,15 @@ function serverFunctions(options = {}, internal = {}) {
1241
1377
  // Make sure the referenced module has been evaluated in the SSR
1242
1378
  // environment so its registration exists — functions only client
1243
1379
  // code references are never loaded by the SSR render itself.
1244
- // The id lives in the path segment after the mount.
1380
+ // The id lives in the path segment after the mount — behind a
1381
+ // literal `data` segment on the scripted transport's address
1382
+ // (solidjs/solid#3094). Segment count keeps the two apart: an id
1383
+ // occupies exactly one segment, so `data/<id>` is only ever a
1384
+ // data address, and a function id spelled `data` still parses at
1385
+ // the bare one.
1245
1386
  const mount = basePrefixed ? resolvedEndpoint : endpoint;
1246
- const segment = url.pathname.slice(mount.length + 1);
1387
+ let segment = url.pathname.slice(mount.length + 1);
1388
+ if (segment.startsWith('data/')) segment = segment.slice(5);
1247
1389
  let functionId = null;
1248
1390
  if (segment && !segment.includes('/')) {
1249
1391
  try {
@@ -1252,14 +1394,6 @@ function serverFunctions(options = {}, internal = {}) {
1252
1394
  // not an address; the runtime handler answers the 404
1253
1395
  }
1254
1396
  }
1255
- if (!functionId) {
1256
- // TRANSITIONAL (remove before 3.0 stable): the retired header
1257
- // and `?id=` addressing, kept only for the RC window where this
1258
- // plugin meets a @solidjs/web older than the path-addressing
1259
- // change (solidjs/solid#3076).
1260
- const headerId = req.headers['x-server-function-id'];
1261
- functionId = (typeof headerId === 'string' ? headerId.split('#')[0] : undefined) || url.searchParams.get('id');
1262
- }
1263
1397
  if (functionId) {
1264
1398
  const entry = moduleForFunctionId(functionId);
1265
1399
  if (entry) await ssrEnvironment.runner.import(moduleDevUrl(entry));
@@ -1288,6 +1422,46 @@ function serverFunctions(options = {}, internal = {}) {
1288
1422
  }
1289
1423
  });
1290
1424
  }
1425
+ async function transformModule(ctx, code, fileId, opts, tsrx) {
1426
+ const mode = getEnvironmentConsumer(ctx.environment, opts);
1427
+ const [id] = fileId.split('?');
1428
+ if (!id || !filter(id) || isTsrxModule(id) !== tsrx) return null;
1429
+
1430
+ // The directive has to appear literally, so anything without the
1431
+ // substring can skip the native parse entirely.
1432
+ if (!code.includes(directive)) return null;
1433
+ const result = await compile(id, code, {
1434
+ ...(mode === 'server' ? serverOptions : clientOptions),
1435
+ mode,
1436
+ env,
1437
+ root,
1438
+ sourceMap: !tsrx || !!internal.tsrxSourceMap
1439
+ });
1440
+ if (!result.valid) return null;
1441
+ const preloader = preload[mode];
1442
+ if (preloader) preloader.defer();
1443
+ invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1444
+ return {
1445
+ // Appended (not prepended) so the source map for the compiled module
1446
+ // stays valid; imports hoist and the endpoint is only read at call time.
1447
+ code: (result.code || '') + endpointConfigureSnippet(mode),
1448
+ map: result.map
1449
+ };
1450
+ }
1451
+ const compilerPlugin = {
1452
+ name: 'solid:server-functions/compiler',
1453
+ enforce: 'pre',
1454
+ transform(code, fileId, opts) {
1455
+ return transformModule(this, code, fileId, opts, false);
1456
+ }
1457
+ };
1458
+ const tsrxCompilerPlugin = {
1459
+ name: 'solid:server-functions/tsrx-compiler',
1460
+ enforce: 'pre',
1461
+ transform(code, fileId, opts) {
1462
+ return transformModule(this, code, fileId, opts, true);
1463
+ }
1464
+ };
1291
1465
  return [{
1292
1466
  name: 'solid:server-functions/setup',
1293
1467
  enforce: 'pre',
@@ -1364,44 +1538,7 @@ function serverFunctions(options = {}, internal = {}) {
1364
1538
  }
1365
1539
  return null;
1366
1540
  }
1367
- }, {
1368
- name: 'solid:server-functions/compiler',
1369
- enforce: 'pre',
1370
- async transform(code, fileId, opts) {
1371
- const mode = getEnvironmentConsumer(this.environment, opts);
1372
- const [id] = fileId.split('?');
1373
- if (!filter(id)) {
1374
- return null;
1375
- }
1376
-
1377
- // Fast path: the directive has to appear literally, so anything
1378
- // without the substring can skip the native parse entirely.
1379
- if (!code.includes(directive)) {
1380
- return null;
1381
- }
1382
- const result = await compile(id, code, {
1383
- ...(mode === 'server' ? serverOptions : clientOptions),
1384
- mode,
1385
- env,
1386
- root
1387
- });
1388
- if (result.valid) {
1389
- const preloader = preload[mode];
1390
- if (preloader) {
1391
- preloader.defer();
1392
- }
1393
- invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1394
- return {
1395
- // Appended (not prepended) so the source map for the compiled
1396
- // module stays valid; imports hoist and the endpoint is only
1397
- // read at call time, never during module evaluation.
1398
- code: (result.code || '') + endpointConfigureSnippet(mode),
1399
- map: result.map
1400
- };
1401
- }
1402
- return null;
1403
- }
1404
- }, ...startPlugins];
1541
+ }, compilerPlugin, ...(internal.tsrxAfterSolid ? [tsrxCompilerPlugin] : []), ...startPlugins];
1405
1542
  }
1406
1543
 
1407
1544
  const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
@@ -1496,9 +1633,9 @@ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1496
1633
  const MANIFEST_ID = 'virtual:solid-manifest';
1497
1634
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1498
1635
  const STORAGE_SOURCE = '@solidjs/web/storage';
1499
- const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs'];
1500
- const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
1501
- const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx'];
1636
+ const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs', '.tsrx'];
1637
+ const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.tsrx'];
1638
+ const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx', '.tsrx'];
1502
1639
  function probe(root, stem, extensions) {
1503
1640
  for (const ext of extensions) {
1504
1641
  if (existsSync(path.resolve(root, stem + ext))) return stem + ext;
@@ -1606,7 +1743,9 @@ function startServe(options, internal = {}) {
1606
1743
  const serverComponents = !!internal.serverComponents;
1607
1744
  const errorBoundary = options.errorBoundary !== false;
1608
1745
  const styleFilter = internal.styleFilter;
1609
- const diagnostics = !!internal.diagnostics;
1746
+ // `'auto'` resolves against the project root in configResolved, before
1747
+ // any of the (lazy) uses in entry codegen and the entry transform.
1748
+ let diagnostics = internal.diagnostics === true;
1610
1749
  let devtoolsEnabled = false;
1611
1750
  let devtoolsResolutions = {};
1612
1751
  let devtoolsIds = {};
@@ -1898,7 +2037,8 @@ function startServe(options, internal = {}) {
1898
2037
  lines.push(``, `async function dispatchRequest(request, event, options) {`);
1899
2038
  if (composeServerFunctions) {
1900
2039
  lines.push(
1901
- // A call's address is `<endpoint>/<id>` (solidjs/solid#3076); the
2040
+ // A call's address is `<endpoint>/<id>` or `<endpoint>/data/<id>`
2041
+ // (solidjs/solid#3076, #3094); the prefix gate covers both, and the
1902
2042
  // bare mount still routes so a misaddressed request 404s through the
1903
2043
  // runtime handler instead of rendering a page at it.
1904
2044
  ` const requestPath = new URL(request.url).pathname;`, ` if (requestPath === endpoint || requestPath.startsWith(endpoint + '/')) {`,
@@ -1971,6 +2111,7 @@ function startServe(options, internal = {}) {
1971
2111
  devtoolsResolutions = {};
1972
2112
  devtoolsIds = {};
1973
2113
  entries = resolveEntries(root, options, clientMode);
2114
+ internal.onDocumentResolved?.(entries.document);
1974
2115
  middlewarePath = options.middleware ? path.resolve(root, normalizeUserPath(root, options.middleware, 'middleware')) : null;
1975
2116
  // Server-mode only, like `entryServer`/`external` (a documented
1976
2117
  // no-op in client mode so configs survive the `ssr` boolean flip).
@@ -2114,6 +2255,12 @@ function startServe(options, internal = {}) {
2114
2255
  root = config.root;
2115
2256
  base = config.base;
2116
2257
  isBuild = config.command === 'build';
2258
+ // Test mode excluded for the same reason as the surface plugin's
2259
+ // `apply`: vitest runs a dev serve, and test pages should not get
2260
+ // the bridge import injected into their client entries.
2261
+ if (internal.diagnostics === 'auto' && !isBuild && config.mode !== 'test') {
2262
+ diagnostics = detectDiagnosticsPackage(root);
2263
+ }
2117
2264
  },
2118
2265
  resolveId(source, importer, opts) {
2119
2266
  if (source === HANDLER_ID) {
@@ -2964,6 +3111,11 @@ const LAZY_PLACEHOLDER_PREFIX = '__SOLID_LAZY_MODULE__:';
2964
3111
  * solid-refresh#85 — is no longer used at all).
2965
3112
  */
2966
3113
  const REFRESH_RUNTIME_SOURCE = 'solid-js/refresh';
3114
+
3115
+ // Appended to the document shell's client compile instead of a refresh
3116
+ // boundary (see documentModuleId in solidPlugin): self-accept, then
3117
+ // invalidate — Vite's spelling for "this module cannot hot-update, reload".
3118
+ const DOCUMENT_HMR_DECLINE = '\nif (import.meta.hot) {\n import.meta.hot.accept(() => import.meta.hot.invalidate());\n}\n';
2967
3119
  const DEFAULT_STYLE_EXCLUDE = /node_modules/;
2968
3120
  const VIRTUAL_MANIFEST_ID = 'virtual:solid-manifest';
2969
3121
  const RESOLVED_VIRTUAL_MANIFEST_ID = '\0' + VIRTUAL_MANIFEST_ID;
@@ -3225,7 +3377,8 @@ function solidPlugin(options = {}) {
3225
3377
  // resolve against the Vite root, not process.cwd() — running `vite` from
3226
3378
  // outside the project would otherwise change what the filter matches.
3227
3379
  let filter = createFilter(options.include, options.exclude);
3228
- const serverComponents = typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
3380
+ const serverComponentsOption = typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
3381
+ const serverComponents = !!serverComponentsOption;
3229
3382
  // `start: true` is sugar for the empty options bag — one start mode,
3230
3383
  // two spellings — so normalize here and let everything downstream see a
3231
3384
  // single shape (`false` behaves exactly like omission).
@@ -3255,6 +3408,15 @@ function solidPlugin(options = {}) {
3255
3408
  const externalDevServer = !!options.ssr && !!startOptions?.external;
3256
3409
  let needHmr = false;
3257
3410
  let replaceDev = false;
3411
+ // Resolved absolute path of the start-mode document shell (normalized to
3412
+ // forward slashes, matching Vite ids), reported back by the start plugin's
3413
+ // config hook. The document is the one module whose client compile must
3414
+ // decline HMR instead of taking a refresh boundary: it hydrates the whole
3415
+ // `document`, and no component swap can re-claim `document.documentElement`
3416
+ // — an accepted update would be absorbed with nothing visibly changing
3417
+ // (solidjs/solid#3151). Declining makes a save invalidate the module, so
3418
+ // Vite falls back to a full page reload: the honest cost.
3419
+ let documentModuleId = null;
3258
3420
  // The live dev server, kept so the dev manifest module can bake the bridge
3259
3421
  // endpoint URL in when its code is generated (see devManifestBridgeUrl).
3260
3422
  let devServer = null;
@@ -3266,6 +3428,7 @@ function solidPlugin(options = {}) {
3266
3428
  let base = '/';
3267
3429
  let clientOutDir = null;
3268
3430
  let solidPkgsConfig;
3431
+ const tsrxCss = new Map();
3269
3432
 
3270
3433
  // The client build's manifest, read back by SSR builds. In builder-mode
3271
3434
  // (single process, e.g. SolidStart's nitro plugin) the client build runs
@@ -3360,6 +3523,44 @@ function solidPlugin(options = {}) {
3360
3523
  const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
3361
3524
  return code + `\nexport const $$moduleUrl = ${JSON.stringify(relativeId)};\n`;
3362
3525
  }
3526
+ function nativeTsrxCss(result) {
3527
+ const css = result.css;
3528
+ return typeof css === 'string' ? css : '';
3529
+ }
3530
+ function babelTsrxCss(result) {
3531
+ const css = result.metadata?.css;
3532
+ return typeof css === 'string' ? css : '';
3533
+ }
3534
+ async function compileTsrxCss(source, id) {
3535
+ const solidOptions = getSolidOptions(options, false, replaceDev, isTestMode);
3536
+ if (options.compiler === 'babel') {
3537
+ const babelUserOptions = await getBabelUserOptions(options, source, id, false);
3538
+ const babelOptions = mergeAndConcat(babelUserOptions, {
3539
+ root: projectRoot,
3540
+ // Keep .tsrx: the Babel plugin uses it to select its TSRX parser.
3541
+ filename: id,
3542
+ sourceFileName: id,
3543
+ ast: false,
3544
+ code: false,
3545
+ sourceMaps: false,
3546
+ configFile: false,
3547
+ babelrc: false,
3548
+ parserOpts: {
3549
+ plugins: ['jsx', 'decorators', 'typescript']
3550
+ },
3551
+ plugins: [[solid, solidOptions]]
3552
+ });
3553
+ const result = await babel.transformAsync(source, babelOptions);
3554
+ return result ? babelTsrxCss(result) : '';
3555
+ }
3556
+ const compiler = await loadNativeCompiler();
3557
+ const result = await compiler.transformAsync(source, {
3558
+ ...solidOptions,
3559
+ filename: id,
3560
+ sourceMap: false
3561
+ });
3562
+ return nativeTsrxCss(result);
3563
+ }
3363
3564
  const mainPlugin = {
3364
3565
  name: 'solid',
3365
3566
  enforce: 'pre',
@@ -3450,6 +3651,7 @@ function solidPlugin(options = {}) {
3450
3651
  dedupe: nestedDeps
3451
3652
  },
3452
3653
  optimizeDeps: {
3654
+ extensions: ['.tsrx'],
3453
3655
  include: [...nestedDeps,
3454
3656
  // Dev refresh wrappers import the solid-js/refresh runtime in
3455
3657
  // every mode; pre-bundle it up front so its discovery doesn't
@@ -3470,7 +3672,28 @@ function solidPlugin(options = {}) {
3470
3672
  jsx: {
3471
3673
  runtime: 'classic'
3472
3674
  }
3473
- }
3675
+ },
3676
+ plugins: [{
3677
+ name: 'solid:tsrx-dep-scan',
3678
+ async transform(source, id) {
3679
+ if (!isTsrxModule(id) || isTsrxCssModule(id)) return null;
3680
+ const compiler = await loadNativeCompiler();
3681
+ const result = await compiler.transformAsync(source, {
3682
+ ...getSolidOptions(options, false, replaceDev, isTestMode),
3683
+ filename: cleanModuleId(id),
3684
+ sourceMap: false
3685
+ });
3686
+ const stripped = await transformWithOxc(result.code, cleanModuleId(id) + '.tsx', {
3687
+ lang: 'tsx',
3688
+ sourcemap: false,
3689
+ target: 'esnext'
3690
+ });
3691
+ return {
3692
+ code: stripped.code,
3693
+ map: null
3694
+ };
3695
+ }
3696
+ }]
3474
3697
  }
3475
3698
  },
3476
3699
  ...(Object.keys(test).length ? {
@@ -3529,8 +3752,13 @@ function solidPlugin(options = {}) {
3529
3752
  resolve: projectRoot
3530
3753
  });
3531
3754
  styleFilter = createStyleFilter(projectRoot);
3532
- if (serverComponents && !(options.start && options.ssr)) {
3533
- config.logger.warn('[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' + 'option with `ssr: true`), so the plugin only installs the endpoint response transform ' + '(server functions returning components stream correctly). The document wiring — render ' + 'plugin, bootstrap script, and the client-side installServerComponents() call is ' + "emitted by SSR start mode's generated entries; without it, server components only mount " + 'from post-boot streams and your client code must call installServerComponents() itself.');
3755
+ // `components: 'external'` is the acknowledgement that a composing
3756
+ // host (e.g. the Astro adapter or TanStack Start's Solid integration)
3757
+ // owns the document wiring itself — behavior is identical to `true`,
3758
+ // only this warning is skipped. Under SSR start mode it's redundant
3759
+ // but harmless (treated exactly as `true`).
3760
+ if (serverComponents && serverComponentsOption !== 'external' && !(options.start && options.ssr)) {
3761
+ config.logger.warn('[@solidjs/vite-plugin] serverFunctions.components is set without SSR start mode (the `start` ' + 'option with `ssr: true`), so the plugin only installs the endpoint response transform ' + '(server functions returning components stream correctly). The document wiring — the ' + 'render plugin (with the direct-call transform) and the client-side ' + "installServerComponents() call — is emitted by SSR start mode's generated entries; " + 'without it, server components only mount from post-boot streams and your client code ' + 'must call installServerComponents() itself. If a composing host owns that wiring, set ' + "`components: 'external'` to acknowledge it and silence this warning.");
3534
3762
  }
3535
3763
  needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false && !options.refresh?.disabled;
3536
3764
  },
@@ -3568,9 +3796,21 @@ function solidPlugin(options = {}) {
3568
3796
  return origSend(...args);
3569
3797
  };
3570
3798
  },
3571
- hotUpdate({
3572
- modules
3799
+ async hotUpdate({
3800
+ file,
3801
+ modules,
3802
+ read
3573
3803
  }) {
3804
+ if (isTsrxModule(file) && this.environment.name === 'client') {
3805
+ updateTsrxCss(tsrxCss, file, await compileTsrxCss(await read(), file));
3806
+ const cssModule = this.environment.moduleGraph.getModuleById(resolvedTsrxCssModuleId(file));
3807
+ if (cssModule) {
3808
+ this.environment.moduleGraph.invalidateModule(cssModule);
3809
+ if (!modules.includes(cssModule)) modules = [...modules, cssModule];
3810
+ return modules;
3811
+ }
3812
+ }
3813
+
3574
3814
  // solid-refresh only injects HMR boundaries into client modules, so
3575
3815
  // non-client environments have no accept handlers. Without this, Vite
3576
3816
  // would see no boundaries and send full-reload messages that race with
@@ -3589,11 +3829,26 @@ function solidPlugin(options = {}) {
3589
3829
  this.environment.hot.send({
3590
3830
  type: 'full-reload'
3591
3831
  });
3832
+ // Server-only modules are the exception to the suppression: a file
3833
+ // with no modules in the client graph has no browser HMR path at
3834
+ // all — nothing client-side accepts it, so staying silent leaves
3835
+ // the browser rendering stale server output until a manual refresh
3836
+ // (e.g. the document shell, which only the server ever imports;
3837
+ // solidjs/solid#3151). Reload the page: the honest cost, and there
3838
+ // is no client update to race with by construction.
3839
+ const clientEnv = devServer?.environments.client;
3840
+ if (clientEnv && !clientEnv.moduleGraph.getModulesByFile(file)?.size) {
3841
+ clientEnv.hot.send({
3842
+ type: 'full-reload'
3843
+ });
3844
+ }
3592
3845
  }
3593
3846
  return [];
3594
3847
  }
3595
3848
  },
3596
3849
  resolveId(id) {
3850
+ const tsrxCssId = resolveTsrxCssModule(id);
3851
+ if (tsrxCssId) return tsrxCssId;
3597
3852
  if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
3598
3853
  },
3599
3854
  moduleParsed(info) {
@@ -3606,7 +3861,7 @@ function solidPlugin(options = {}) {
3606
3861
  for (const depId of info.dynamicallyImportedIds || []) {
3607
3862
  const cleanId = depId.split('?')[0];
3608
3863
  if (/node_modules/.test(cleanId) || cleanId.startsWith('\0')) continue;
3609
- if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue;
3864
+ if (!(/\.[mc]?[tj]sx?$/i.test(cleanId) || isTsrxModule(cleanId))) continue;
3610
3865
  if (emittedLazyChunks.has(depId)) continue;
3611
3866
  emittedLazyChunks.add(depId);
3612
3867
  emittedLazyChunkRefs.push(this.emitFile({
@@ -3617,6 +3872,8 @@ function solidPlugin(options = {}) {
3617
3872
  }
3618
3873
  },
3619
3874
  load(id) {
3875
+ const tsrxSource = tsrxCssSourceId(id);
3876
+ if (tsrxSource) return tsrxCss.get(tsrxSource) ?? '';
3620
3877
  if (id === RESOLVED_VIRTUAL_MANIFEST_ID) {
3621
3878
  if (!isBuild) {
3622
3879
  return devManifestCode(projectRoot, base, devServer ? devManifestBridgeUrl(devServer) : null);
@@ -3658,6 +3915,7 @@ function solidPlugin(options = {}) {
3658
3915
  }
3659
3916
  },
3660
3917
  async transform(source, id, transformOptions) {
3918
+ if (isTsrxCssModule(id)) return null;
3661
3919
  const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3662
3920
  const currentFileExtension = getExtension(id);
3663
3921
  const extensionsToWatch = options.extensions || [];
@@ -3673,14 +3931,15 @@ function solidPlugin(options = {}) {
3673
3931
  // while the transform pipeline below works on the clean file path.
3674
3932
  const moduleId = id;
3675
3933
  id = id.replace(/\?.*$/, '');
3676
- if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
3934
+ const isTsrx = isTsrxModule(id);
3935
+ if (!(/\.[mc]?[tj]sx$/i.test(id) || isTsrx || allExtensions.includes(currentFileExtension))) {
3677
3936
  return null;
3678
3937
  }
3679
3938
  const inNodeModules = /node_modules/.test(id);
3680
3939
  const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, isTestMode);
3681
3940
 
3682
3941
  // We need to know if the current file extension has a typescript options tied to it
3683
- const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || extensionsToWatch.some(extension => {
3942
+ const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || isTsrx || extensionsToWatch.some(extension => {
3684
3943
  if (typeof extension === 'string') {
3685
3944
  return extension.includes('tsx');
3686
3945
  }
@@ -3692,14 +3951,19 @@ function solidPlugin(options = {}) {
3692
3951
  if (shouldBeProcessedWithTypescript) {
3693
3952
  plugins.push('typescript');
3694
3953
  }
3695
- const needRefresh = needHmr && !isSsr && !inNodeModules;
3954
+
3955
+ // See the documentModuleId declaration: the document shell declines HMR
3956
+ // (no refresh boundary, explicit self-invalidation) so edits full-reload.
3957
+ const isDocumentShell = documentModuleId !== null && id === documentModuleId;
3958
+ const needRefresh = needHmr && !isSsr && !inNodeModules && !isDocumentShell;
3959
+ const declineHmr = isDocumentShell && needHmr && !isSsr;
3696
3960
  const babelUserOptions = await getBabelUserOptions(options, source, id, !!isSsr);
3697
3961
 
3698
3962
  // The native compiler picks its parser dialect from the file
3699
3963
  // extension; custom extensions registered through `options.extensions`
3700
3964
  // are unknown to it, so borrow a standard one matching the configured
3701
3965
  // TypeScript-ness.
3702
- const nativeFilename = /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3966
+ const nativeFilename = isTsrx || /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3703
3967
 
3704
3968
  // Shared native prelude for every mode: the lazy() module-URL pass,
3705
3969
  // then (dev/client/non-node_modules) the solid-refresh HMR pass, both
@@ -3709,6 +3973,97 @@ function solidPlugin(options = {}) {
3709
3973
  const compiler = await loadNativeCompiler();
3710
3974
  let code = source;
3711
3975
  const maps = [];
3976
+ if (isTsrx) {
3977
+ // Solid lowering preserves authored TypeScript annotations; secondary
3978
+ // passes therefore parse the generated module as TSX even though no
3979
+ // template syntax remains.
3980
+ const generatedFilename = id + '.tsx';
3981
+ const babelBaseOptions = {
3982
+ root: projectRoot,
3983
+ filename: id,
3984
+ sourceFileName: id,
3985
+ ast: false,
3986
+ sourceMaps: true,
3987
+ configFile: false,
3988
+ babelrc: false,
3989
+ parserOpts: {
3990
+ plugins
3991
+ }
3992
+ };
3993
+ let css = '';
3994
+ if (options.compiler !== 'babel') {
3995
+ const result = await compiler.transformAsync(code, {
3996
+ ...solidOptions,
3997
+ filename: id,
3998
+ sourceMap: true
3999
+ });
4000
+ code = result.code || '';
4001
+ css = nativeTsrxCss(result);
4002
+ maps.push(result.map);
4003
+ if (options.babel) {
4004
+ // The support pass cannot parse authored TSRX. On this route it
4005
+ // intentionally sees the lowered ordinary JavaScript instead.
4006
+ const supportOptions = mergeAndConcat(babelUserOptions, babelBaseOptions);
4007
+ // This pass sees native-lowered ordinary JavaScript, so do not
4008
+ // route it back through Babel's TSRX parser.
4009
+ supportOptions.filename = generatedFilename;
4010
+ const supportResult = await babel.transformAsync(code, supportOptions);
4011
+ if (!supportResult) return undefined;
4012
+ code = supportResult.code || '';
4013
+ maps.push(supportResult.map);
4014
+ }
4015
+ } else {
4016
+ const babelOptions = mergeAndConcat(babelUserOptions, {
4017
+ ...babelBaseOptions,
4018
+ plugins: [[solid, solidOptions]]
4019
+ });
4020
+ const result = await babel.transformAsync(code, babelOptions);
4021
+ if (!result) return undefined;
4022
+ code = result.code || '';
4023
+ css = babelTsrxCss(result);
4024
+ maps.push(result.map);
4025
+ }
4026
+ const lazyResult = await compiler.transformLazyAsync(code, {
4027
+ filename: generatedFilename,
4028
+ sourceMap: true
4029
+ });
4030
+ code = lazyResult.code;
4031
+ maps.push(lazyResult.map);
4032
+ if (needRefresh) {
4033
+ const refreshResult = await compiler.transformRefreshAsync(code, {
4034
+ filename: generatedFilename,
4035
+ bundler: 'vite',
4036
+ fixRender: true,
4037
+ ...(typeof options.refresh?.granular === 'boolean' ? {
4038
+ granular: options.refresh.granular
4039
+ } : {}),
4040
+ jsx: false,
4041
+ importSource: REFRESH_RUNTIME_SOURCE,
4042
+ sourceMap: true
4043
+ });
4044
+ code = refreshResult.code;
4045
+ maps.push(refreshResult.map);
4046
+ }
4047
+ code = injectSsrModuleId(await resolveLazyModuleUrls(this, code, id), moduleId, !!isSsr);
4048
+ let map = options.compiler === 'babel' ? combineSourcemaps(maps) : null;
4049
+ updateTsrxCss(tsrxCss, id, css);
4050
+ if (css) {
4051
+ code = prependTsrxCssImport(code, id);
4052
+ map = offsetSourceMapLine(map);
4053
+ }
4054
+ // Vite selects its TypeScript stripping by file extension. Since the
4055
+ // real module identity remains `.tsrx`, strip the annotations here
4056
+ // after Solid lowering instead of handing typed JavaScript to Rollup.
4057
+ const stripped = await transformWithOxc(code, generatedFilename, {
4058
+ lang: 'tsx',
4059
+ sourcemap: map != null,
4060
+ target: 'esnext'
4061
+ }, map ?? undefined);
4062
+ return {
4063
+ code: stripped.code,
4064
+ map: map == null ? null : stripped.map
4065
+ };
4066
+ }
3712
4067
  const lazyResult = await compiler.transformLazyAsync(code, {
3713
4068
  filename: nativeFilename,
3714
4069
  sourceMap: true
@@ -3764,7 +4119,7 @@ function solidPlugin(options = {}) {
3764
4119
  maps.push(result.map);
3765
4120
  const finalCode = injectSsrModuleId(await resolveLazyModuleUrls(this, result.code || '', id), moduleId, !!isSsr);
3766
4121
  return {
3767
- code: finalCode,
4122
+ code: declineHmr ? finalCode + DOCUMENT_HMR_DECLINE : finalCode,
3768
4123
  map: combineSourcemaps(maps)
3769
4124
  };
3770
4125
  }
@@ -3785,26 +4140,30 @@ function solidPlugin(options = {}) {
3785
4140
  maps.push(result.map);
3786
4141
  const finalCode = injectSsrModuleId(await resolveLazyModuleUrls(this, result.code || '', id), moduleId, !!isSsr);
3787
4142
  return {
3788
- code: finalCode,
4143
+ code: declineHmr ? finalCode + DOCUMENT_HMR_DECLINE : finalCode,
3789
4144
  map: combineSourcemaps(maps)
3790
4145
  };
3791
4146
  }
3792
4147
  };
3793
4148
 
3794
- // The directive transform must run before the JSX transform (it operates
3795
- // on raw directives, and client-mode module-level extraction must happen
3796
- // before templates are generated), so its sub-plugins go first. The
3797
- // boundary markers (`server-only` / `client-only`) are always on.
3798
- const plugins = options.serverFunctions ? [boundaryModules(), ...serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
4149
+ // Ordinary modules need the directive transform before JSX. Authored TSRX
4150
+ // cannot be parsed by that standalone pass, so its companion compiler runs
4151
+ // after mainPlugin has lowered the file to ordinary JavaScript while keeping
4152
+ // the original .tsrx id for stable server-function hashes.
4153
+ const serverFunctionPlugins = options.serverFunctions ? serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
3799
4154
  devMiddleware: true,
3800
4155
  externalDevServer,
4156
+ tsrxAfterSolid: true,
4157
+ tsrxSourceMap: options.compiler === 'babel',
3801
4158
  // With start mode on (either variant), the dev middleware dispatches
3802
4159
  // the endpoint through the SSR handler so user middleware and the
3803
4160
  // stub-backed request event front it exactly like page SSR.
3804
4161
  ...(startOptions ? {
3805
4162
  ssrHandler: SSR_HANDLER_ID
3806
4163
  } : {})
3807
- }), mainPlugin] : [boundaryModules(), mainPlugin];
4164
+ }) : [];
4165
+ const tsrxServerFunctionPlugin = serverFunctionPlugins.find(plugin => plugin.name === 'solid:server-functions/tsrx-compiler');
4166
+ const plugins = [boundaryModules(), ...serverFunctionPlugins.filter(plugin => plugin !== tsrxServerFunctionPlugin), mainPlugin, ...(tsrxServerFunctionPlugin ? [tsrxServerFunctionPlugin] : [])];
3808
4167
 
3809
4168
  // The `start` option opts into start-mode serving on top of the transforms;
3810
4169
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
@@ -3819,14 +4178,20 @@ function solidPlugin(options = {}) {
3819
4178
  serverComponents,
3820
4179
  ssr: !!options.ssr,
3821
4180
  styleFilter: filterDevStyles,
3822
- diagnostics: !!options.diagnostics
4181
+ diagnostics: options.diagnostics ?? 'auto',
4182
+ onDocumentResolved(documentPath) {
4183
+ // Normalize to forward slashes to match Vite's transform ids.
4184
+ documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
4185
+ }
3823
4186
  }));
3824
4187
  }
3825
4188
 
3826
4189
  // Agent diagnostics endpoint + injected bridge (dev serve only — the
3827
- // plugin no-ops itself for builds and preview via `apply`).
3828
- if (options.diagnostics) {
3829
- plugins.push(solidDiagnostics());
4190
+ // plugin no-ops itself for builds and preview via `apply`, and in the
4191
+ // default auto mode additionally disables itself unless the app has
4192
+ // `@solidjs/diagnostics` installed).
4193
+ if (options.diagnostics !== false) {
4194
+ plugins.push(solidDiagnostics(options.diagnostics === true ? true : 'auto'));
3830
4195
  }
3831
4196
 
3832
4197
  // Builder-mode (environments API) client-before-server build ordering.