@solidjs/vite-plugin 3.0.0-next.37 → 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.
@@ -170,6 +170,75 @@ function joinBase(base, pathname) {
170
170
  return (base.endsWith('/') ? base.slice(0, -1) : base) + pathname;
171
171
  }
172
172
 
173
+ const TSRX_CSS_QUERY = '?solid-tsrx-css&lang.css';
174
+ const NULL_BYTE_PLACEHOLDER$1 = '/@id/__x00__';
175
+ function cleanModuleId(id) {
176
+ const query = id.indexOf('?');
177
+ return query === -1 ? id : id.slice(0, query);
178
+ }
179
+ function isTsrxModule(id) {
180
+ return cleanModuleId(id).toLowerCase().endsWith('.tsrx');
181
+ }
182
+ function isTsrxCssModule(id) {
183
+ const unwrapped = id.startsWith('\0') ? id.slice(1) : id.startsWith(NULL_BYTE_PLACEHOLDER$1) ? id.slice(NULL_BYTE_PLACEHOLDER$1.length) : id;
184
+ const queryIndex = unwrapped.indexOf('?');
185
+ if (queryIndex === -1 || !unwrapped.slice(0, queryIndex).toLowerCase().endsWith('.tsrx')) {
186
+ return false;
187
+ }
188
+ const params = unwrapped.slice(queryIndex + 1).split('&');
189
+ return params.includes('solid-tsrx-css') && params.includes('lang.css');
190
+ }
191
+ function resolveTsrxCssModule(id) {
192
+ if (!isTsrxCssModule(id)) return null;
193
+ if (id.startsWith('\0')) return id;
194
+ if (id.startsWith(NULL_BYTE_PLACEHOLDER$1)) {
195
+ return '\0' + id.slice(NULL_BYTE_PLACEHOLDER$1.length);
196
+ }
197
+ return '\0' + id;
198
+ }
199
+ function tsrxCssModuleId(id) {
200
+ return cleanModuleId(id) + TSRX_CSS_QUERY;
201
+ }
202
+ function resolvedTsrxCssModuleId(id) {
203
+ return '\0' + tsrxCssModuleId(id);
204
+ }
205
+ function tsrxCssSourceId(id) {
206
+ if (!id.startsWith('\0') || !isTsrxCssModule(id)) return null;
207
+ return cleanModuleId(id.slice(1));
208
+ }
209
+ function updateTsrxCss(cache, id, css) {
210
+ const key = cleanModuleId(id);
211
+ if (css) {
212
+ cache.set(key, css);
213
+ } else {
214
+ cache.delete(key);
215
+ }
216
+ }
217
+ function prependTsrxCssImport(code, id) {
218
+ return `import ${JSON.stringify(tsrxCssModuleId(id))};\n${code}`;
219
+ }
220
+ function offsetSourceMapLine(map) {
221
+ if (map && typeof map === 'object' && 'mappings' in map && typeof map.mappings === 'string') {
222
+ return {
223
+ ...map,
224
+ mappings: ';' + map.mappings
225
+ };
226
+ }
227
+ if (map && typeof map === 'object' && 'sections' in map && Array.isArray(map.sections)) {
228
+ return {
229
+ ...map,
230
+ sections: map.sections.map(section => ({
231
+ ...section,
232
+ offset: {
233
+ ...section.offset,
234
+ line: section.offset.line + 1
235
+ }
236
+ }))
237
+ };
238
+ }
239
+ return map;
240
+ }
241
+
173
242
  /**
174
243
  * Dev-mode asset resolution: the `virtual:solid-manifest` module exports a
175
244
  * resolver function in dev (instead of the static object a build produces),
@@ -279,6 +348,9 @@ const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
279
348
  // importer controls them — so they must not be SSR'd as style tags.
280
349
  const nonAmbientQueryRegExp = /[?&](url|inline|raw)\b/;
281
350
  const NULL_BYTE_PLACEHOLDER = '/@id/__x00__';
351
+ function isCssModuleUrl(url) {
352
+ return cssFileRegExp.test(url.split('?')[0]) || isTsrxCssModule(url);
353
+ }
282
354
 
283
355
  // Per Vite's convention virtual module ids are prefixed with `\0`, which
284
356
  // cannot appear in an HTML attribute (the parser replaces it). Serialize the
@@ -335,7 +407,7 @@ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, impor
335
407
  const node = await getModuleNode(env, file, importer);
336
408
  if (!node?.id || deps.has(node)) return;
337
409
  deps.add(node);
338
- const isCss = cssFileRegExp.test(node.url.split('?')[0]);
410
+ const isCss = isCssModuleUrl(node.url);
339
411
  if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
340
412
  if (node.file) onFile?.(node.file);
341
413
  if (isCss) return;
@@ -367,8 +439,7 @@ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleF
367
439
  const seen = new Set();
368
440
  for (const node of deps) {
369
441
  if (!node.id) continue;
370
- const cleanUrl = node.url.split('?')[0];
371
- if (!cssFileRegExp.test(cleanUrl) || nonAmbientQueryRegExp.test(node.url)) continue;
442
+ if (!isCssModuleUrl(node.url) || nonAmbientQueryRegExp.test(node.url)) continue;
372
443
  const id = wrapId(node.id);
373
444
  if (seen.has(id)) continue;
374
445
  seen.add(id);
@@ -582,7 +653,11 @@ function boundaryModules() {
582
653
  }
583
654
 
584
655
  /**
585
- * Agent diagnostics surface (`diagnostics: true`, dev serve only).
656
+ * Agent diagnostics surface (dev serve only).
657
+ *
658
+ * Enabled automatically when the app declares `@solidjs/diagnostics` in
659
+ * its package.json (the `diagnostics` option overrides: `true` forces it
660
+ * on and errors if the package is missing, `false` opts out entirely).
586
661
  *
587
662
  * Three pieces:
588
663
  * - an injected client module (virtual, imported by index.html or the
@@ -610,6 +685,36 @@ const METHODS = ['begin', 'end', 'active', 'whyDidRun', 'costs'];
610
685
 
611
686
  /** How long the endpoint waits for a page to answer before failing the call. */
612
687
  const RESPONSE_TIMEOUT_MS = 10_000;
688
+
689
+ /**
690
+ * Whether the app *declares* `@solidjs/diagnostics` — the auto-enable
691
+ * signal. Declaration in the nearest package.json (walking up from the
692
+ * Vite root, so a `client/` root still finds the app manifest) rather
693
+ * than node_modules presence: presence-based detection escapes the app
694
+ * into ancestor installs, which surprise-enables the surface for every
695
+ * fixture app inside a monorepo that happens to have the package
696
+ * somewhere above it (this broke the plugin's own example suites). A
697
+ * declared dependency is unambiguous intent, and resolution then works
698
+ * regardless of hoisting. `diagnostics: true` remains the override for
699
+ * setups the heuristic can't see.
700
+ */
701
+ function detectDiagnosticsPackage(root) {
702
+ let dir = path.resolve(root);
703
+ while (true) {
704
+ const manifestPath = path.join(dir, 'package.json');
705
+ if (fs.existsSync(manifestPath)) {
706
+ try {
707
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
708
+ return !!(manifest.dependencies?.[DIAGNOSTICS_PACKAGE] ?? manifest.devDependencies?.[DIAGNOSTICS_PACKAGE] ?? manifest.optionalDependencies?.[DIAGNOSTICS_PACKAGE]);
709
+ } catch {
710
+ return false;
711
+ }
712
+ }
713
+ const parent = path.dirname(dir);
714
+ if (parent === dir) return false;
715
+ dir = parent;
716
+ }
717
+ }
613
718
  function diagnosticsClientModuleCode() {
614
719
  // Runtime imports resolve to the APP's diagnostics package (see the
615
720
  // resolveId assist below) — the page speaks its own package's protocol.
@@ -636,18 +741,26 @@ function readJsonBody(req) {
636
741
  req.on('error', reject);
637
742
  });
638
743
  }
639
- function solidDiagnostics() {
744
+ function solidDiagnostics(mode = 'auto') {
640
745
  let root = process.cwd();
641
746
  let base = '/';
747
+ // Resolved at configResolved: explicit `true` is unconditional (missing
748
+ // package becomes a hard error at bridge resolution); `'auto'` enables
749
+ // only when the app has the package installed.
750
+ let enabled = mode === true;
642
751
  return {
643
752
  name: 'solid:diagnostics',
644
753
  // Dev-serve only: the channels this fronts exist in dev builds only.
754
+ // Test mode excluded — vitest (including browser mode) runs a dev
755
+ // serve, and injecting the bridge into test pages perturbs suites
756
+ // that never asked for it.
645
757
  apply(_config, env) {
646
- return env.command === 'serve' && !env.isPreview;
758
+ return env.command === 'serve' && !env.isPreview && env.mode !== 'test';
647
759
  },
648
760
  configResolved(config) {
649
761
  root = config.root;
650
762
  base = config.base;
763
+ if (mode === 'auto') enabled = detectDiagnosticsPackage(root);
651
764
  },
652
765
  async resolveId(source, importer) {
653
766
  if (source === DIAGNOSTICS_CLIENT_ID) {
@@ -663,7 +776,7 @@ function solidDiagnostics() {
663
776
  skipSelf: true
664
777
  });
665
778
  if (!resolved || resolved.id.startsWith('__vite-optional-peer-dep:')) {
666
- 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`.');
779
+ 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.');
667
780
  }
668
781
  return resolved;
669
782
  }
@@ -676,6 +789,7 @@ function solidDiagnostics() {
676
789
  // Plain (index.html) apps get the client module injected here;
677
790
  // start-mode apps import it from the generated client entry instead.
678
791
  transformIndexHtml() {
792
+ if (!enabled) return undefined;
679
793
  return [{
680
794
  tag: 'script',
681
795
  attrs: {
@@ -686,6 +800,11 @@ function solidDiagnostics() {
686
800
  }];
687
801
  },
688
802
  configureServer(server) {
803
+ // The whole surface (announcement, middleware, bridge injection) only
804
+ // exists when enabled, so the discovery breadcrumb never lies about
805
+ // a dead endpoint.
806
+ if (!enabled) return;
807
+
689
808
  // Announce the surface in the startup block. This is a discovery
690
809
  // channel: agents watching dev-server output learn the endpoint and
691
810
  // the skill documents without any project-level pointer (AGENTS.md).
@@ -694,7 +813,7 @@ function solidDiagnostics() {
694
813
  originalPrintUrls();
695
814
  const local = server.resolvedUrls?.local[0];
696
815
  const endpoint = local ? new URL(DIAGNOSTICS_ENDPOINT, local).href : DIAGNOSTICS_ENDPOINT;
697
- 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`);
816
+ 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`);
698
817
  };
699
818
  const pending = new Map();
700
819
  let nextId = 1;
@@ -705,6 +824,11 @@ function solidDiagnostics() {
705
824
  clearTimeout(entry.timer);
706
825
  entry.resolve(data);
707
826
  });
827
+
828
+ // No host/origin validation here: on all supported Vite versions
829
+ // (peer range ^8) Vite's own DNS-rebinding host check runs ahead of
830
+ // plugin middleware — verified: requests with a disallowed Host
831
+ // header get Vite's 403 before reaching this handler.
708
832
  server.middlewares.use(DIAGNOSTICS_ENDPOINT, async (req, res) => {
709
833
  // The middleware mounts on the exact path; anything deeper is 404.
710
834
  if (req.url && req.url !== '/' && req.url !== '') {
@@ -827,7 +951,7 @@ async function compile(id, code, options) {
827
951
  mode: options.mode,
828
952
  env: options.env,
829
953
  directive: options.directive,
830
- sourceMap: true,
954
+ sourceMap: options.sourceMap !== false,
831
955
  register: options.definitions.register,
832
956
  create: options.definitions.create
833
957
  });
@@ -953,11 +1077,11 @@ function xxHash32(buffer, seed = 0) {
953
1077
  * root — not the invocation directory — so running `vite` from outside the
954
1078
  * project keeps compiling the same files. Absolute patterns are used as-is.
955
1079
  *
956
- * @default include "src/**\/*.{jsx,tsx,ts,js,mjs,cjs}", exclude "node_modules/**\/*.{jsx,tsx,ts,js,mjs,cjs}"
1080
+ * @default include "src/**\/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}", exclude "node_modules/**\/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}"
957
1081
  */
958
1082
 
959
- const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,ts,js,mjs,cjs}';
960
- const DEFAULT_EXCLUDE = 'node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}';
1083
+ const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}';
1084
+ const DEFAULT_EXCLUDE = 'node_modules/**/*.{jsx,tsx,tsrx,ts,js,mjs,cjs}';
961
1085
  const DEFAULT_MANIFEST = 'virtual:solid-server-function-manifest';
962
1086
  const DEFAULT_DIRECTIVE = 'use server';
963
1087
  const DEFAULT_RUNTIME = '@solidjs/web/server-functions';
@@ -1322,6 +1446,46 @@ function serverFunctions(options = {}, internal = {}) {
1322
1446
  }
1323
1447
  });
1324
1448
  }
1449
+ async function transformModule(ctx, code, fileId, opts, tsrx) {
1450
+ const mode = getEnvironmentConsumer(ctx.environment, opts);
1451
+ const [id] = fileId.split('?');
1452
+ if (!id || !filter(id) || isTsrxModule(id) !== tsrx) return null;
1453
+
1454
+ // The directive has to appear literally, so anything without the
1455
+ // substring can skip the native parse entirely.
1456
+ if (!code.includes(directive)) return null;
1457
+ const result = await compile(id, code, {
1458
+ ...(mode === 'server' ? serverOptions : clientOptions),
1459
+ mode,
1460
+ env,
1461
+ root,
1462
+ sourceMap: !tsrx || !!internal.tsrxSourceMap
1463
+ });
1464
+ if (!result.valid) return null;
1465
+ const preloader = preload[mode];
1466
+ if (preloader) preloader.defer();
1467
+ invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1468
+ return {
1469
+ // Appended (not prepended) so the source map for the compiled module
1470
+ // stays valid; imports hoist and the endpoint is only read at call time.
1471
+ code: (result.code || '') + endpointConfigureSnippet(mode),
1472
+ map: result.map
1473
+ };
1474
+ }
1475
+ const compilerPlugin = {
1476
+ name: 'solid:server-functions/compiler',
1477
+ enforce: 'pre',
1478
+ transform(code, fileId, opts) {
1479
+ return transformModule(this, code, fileId, opts, false);
1480
+ }
1481
+ };
1482
+ const tsrxCompilerPlugin = {
1483
+ name: 'solid:server-functions/tsrx-compiler',
1484
+ enforce: 'pre',
1485
+ transform(code, fileId, opts) {
1486
+ return transformModule(this, code, fileId, opts, true);
1487
+ }
1488
+ };
1325
1489
  return [{
1326
1490
  name: 'solid:server-functions/setup',
1327
1491
  enforce: 'pre',
@@ -1398,44 +1562,7 @@ function serverFunctions(options = {}, internal = {}) {
1398
1562
  }
1399
1563
  return null;
1400
1564
  }
1401
- }, {
1402
- name: 'solid:server-functions/compiler',
1403
- enforce: 'pre',
1404
- async transform(code, fileId, opts) {
1405
- const mode = getEnvironmentConsumer(this.environment, opts);
1406
- const [id] = fileId.split('?');
1407
- if (!filter(id)) {
1408
- return null;
1409
- }
1410
-
1411
- // Fast path: the directive has to appear literally, so anything
1412
- // without the substring can skip the native parse entirely.
1413
- if (!code.includes(directive)) {
1414
- return null;
1415
- }
1416
- const result = await compile(id, code, {
1417
- ...(mode === 'server' ? serverOptions : clientOptions),
1418
- mode,
1419
- env,
1420
- root
1421
- });
1422
- if (result.valid) {
1423
- const preloader = preload[mode];
1424
- if (preloader) {
1425
- preloader.defer();
1426
- }
1427
- invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1428
- return {
1429
- // Appended (not prepended) so the source map for the compiled
1430
- // module stays valid; imports hoist and the endpoint is only
1431
- // read at call time, never during module evaluation.
1432
- code: (result.code || '') + endpointConfigureSnippet(mode),
1433
- map: result.map
1434
- };
1435
- }
1436
- return null;
1437
- }
1438
- }, ...startPlugins];
1565
+ }, compilerPlugin, ...(internal.tsrxAfterSolid ? [tsrxCompilerPlugin] : []), ...startPlugins];
1439
1566
  }
1440
1567
 
1441
1568
  const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
@@ -1530,9 +1657,9 @@ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1530
1657
  const MANIFEST_ID = 'virtual:solid-manifest';
1531
1658
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1532
1659
  const STORAGE_SOURCE = '@solidjs/web/storage';
1533
- const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs'];
1534
- const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
1535
- const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx'];
1660
+ const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs', '.tsrx'];
1661
+ const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.tsrx'];
1662
+ const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx', '.tsrx'];
1536
1663
  function probe(root, stem, extensions) {
1537
1664
  for (const ext of extensions) {
1538
1665
  if (fs.existsSync(path.resolve(root, stem + ext))) return stem + ext;
@@ -1640,7 +1767,9 @@ function startServe(options, internal = {}) {
1640
1767
  const serverComponents = !!internal.serverComponents;
1641
1768
  const errorBoundary = options.errorBoundary !== false;
1642
1769
  const styleFilter = internal.styleFilter;
1643
- const diagnostics = !!internal.diagnostics;
1770
+ // `'auto'` resolves against the project root in configResolved, before
1771
+ // any of the (lazy) uses in entry codegen and the entry transform.
1772
+ let diagnostics = internal.diagnostics === true;
1644
1773
  let devtoolsEnabled = false;
1645
1774
  let devtoolsResolutions = {};
1646
1775
  let devtoolsIds = {};
@@ -2150,6 +2279,12 @@ function startServe(options, internal = {}) {
2150
2279
  root = config.root;
2151
2280
  base = config.base;
2152
2281
  isBuild = config.command === 'build';
2282
+ // Test mode excluded for the same reason as the surface plugin's
2283
+ // `apply`: vitest runs a dev serve, and test pages should not get
2284
+ // the bridge import injected into their client entries.
2285
+ if (internal.diagnostics === 'auto' && !isBuild && config.mode !== 'test') {
2286
+ diagnostics = detectDiagnosticsPackage(root);
2287
+ }
2153
2288
  },
2154
2289
  resolveId(source, importer, opts) {
2155
2290
  if (source === HANDLER_ID) {
@@ -3266,7 +3401,8 @@ function solidPlugin(options = {}) {
3266
3401
  // resolve against the Vite root, not process.cwd() — running `vite` from
3267
3402
  // outside the project would otherwise change what the filter matches.
3268
3403
  let filter = vite.createFilter(options.include, options.exclude);
3269
- const serverComponents = typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
3404
+ const serverComponentsOption = typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
3405
+ const serverComponents = !!serverComponentsOption;
3270
3406
  // `start: true` is sugar for the empty options bag — one start mode,
3271
3407
  // two spellings — so normalize here and let everything downstream see a
3272
3408
  // single shape (`false` behaves exactly like omission).
@@ -3316,6 +3452,7 @@ function solidPlugin(options = {}) {
3316
3452
  let base = '/';
3317
3453
  let clientOutDir = null;
3318
3454
  let solidPkgsConfig;
3455
+ const tsrxCss = new Map();
3319
3456
 
3320
3457
  // The client build's manifest, read back by SSR builds. In builder-mode
3321
3458
  // (single process, e.g. SolidStart's nitro plugin) the client build runs
@@ -3410,6 +3547,44 @@ function solidPlugin(options = {}) {
3410
3547
  const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
3411
3548
  return code + `\nexport const $$moduleUrl = ${JSON.stringify(relativeId)};\n`;
3412
3549
  }
3550
+ function nativeTsrxCss(result) {
3551
+ const css = result.css;
3552
+ return typeof css === 'string' ? css : '';
3553
+ }
3554
+ function babelTsrxCss(result) {
3555
+ const css = result.metadata?.css;
3556
+ return typeof css === 'string' ? css : '';
3557
+ }
3558
+ async function compileTsrxCss(source, id) {
3559
+ const solidOptions = getSolidOptions(options, false, replaceDev, isTestMode);
3560
+ if (options.compiler === 'babel') {
3561
+ const babelUserOptions = await getBabelUserOptions(options, source, id, false);
3562
+ const babelOptions = mergeAnything.mergeAndConcat(babelUserOptions, {
3563
+ root: projectRoot,
3564
+ // Keep .tsrx: the Babel plugin uses it to select its TSRX parser.
3565
+ filename: id,
3566
+ sourceFileName: id,
3567
+ ast: false,
3568
+ code: false,
3569
+ sourceMaps: false,
3570
+ configFile: false,
3571
+ babelrc: false,
3572
+ parserOpts: {
3573
+ plugins: ['jsx', 'decorators', 'typescript']
3574
+ },
3575
+ plugins: [[solid, solidOptions]]
3576
+ });
3577
+ const result = await babel__namespace.transformAsync(source, babelOptions);
3578
+ return result ? babelTsrxCss(result) : '';
3579
+ }
3580
+ const compiler = await loadNativeCompiler();
3581
+ const result = await compiler.transformAsync(source, {
3582
+ ...solidOptions,
3583
+ filename: id,
3584
+ sourceMap: false
3585
+ });
3586
+ return nativeTsrxCss(result);
3587
+ }
3413
3588
  const mainPlugin = {
3414
3589
  name: 'solid',
3415
3590
  enforce: 'pre',
@@ -3500,6 +3675,7 @@ function solidPlugin(options = {}) {
3500
3675
  dedupe: nestedDeps
3501
3676
  },
3502
3677
  optimizeDeps: {
3678
+ extensions: ['.tsrx'],
3503
3679
  include: [...nestedDeps,
3504
3680
  // Dev refresh wrappers import the solid-js/refresh runtime in
3505
3681
  // every mode; pre-bundle it up front so its discovery doesn't
@@ -3520,7 +3696,28 @@ function solidPlugin(options = {}) {
3520
3696
  jsx: {
3521
3697
  runtime: 'classic'
3522
3698
  }
3523
- }
3699
+ },
3700
+ plugins: [{
3701
+ name: 'solid:tsrx-dep-scan',
3702
+ async transform(source, id) {
3703
+ if (!isTsrxModule(id) || isTsrxCssModule(id)) return null;
3704
+ const compiler = await loadNativeCompiler();
3705
+ const result = await compiler.transformAsync(source, {
3706
+ ...getSolidOptions(options, false, replaceDev, isTestMode),
3707
+ filename: cleanModuleId(id),
3708
+ sourceMap: false
3709
+ });
3710
+ const stripped = await vite.transformWithOxc(result.code, cleanModuleId(id) + '.tsx', {
3711
+ lang: 'tsx',
3712
+ sourcemap: false,
3713
+ target: 'esnext'
3714
+ });
3715
+ return {
3716
+ code: stripped.code,
3717
+ map: null
3718
+ };
3719
+ }
3720
+ }]
3524
3721
  }
3525
3722
  },
3526
3723
  ...(Object.keys(test).length ? {
@@ -3579,8 +3776,13 @@ function solidPlugin(options = {}) {
3579
3776
  resolve: projectRoot
3580
3777
  });
3581
3778
  styleFilter = createStyleFilter(projectRoot);
3582
- if (serverComponents && !(options.start && options.ssr)) {
3583
- 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.');
3779
+ // `components: 'external'` is the acknowledgement that a composing
3780
+ // host (e.g. the Astro adapter or TanStack Start's Solid integration)
3781
+ // owns the document wiring itself — behavior is identical to `true`,
3782
+ // only this warning is skipped. Under SSR start mode it's redundant
3783
+ // but harmless (treated exactly as `true`).
3784
+ if (serverComponents && serverComponentsOption !== 'external' && !(options.start && options.ssr)) {
3785
+ 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.");
3584
3786
  }
3585
3787
  needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false && !options.refresh?.disabled;
3586
3788
  },
@@ -3618,10 +3820,21 @@ function solidPlugin(options = {}) {
3618
3820
  return origSend(...args);
3619
3821
  };
3620
3822
  },
3621
- hotUpdate({
3823
+ async hotUpdate({
3824
+ file,
3622
3825
  modules,
3623
- file
3826
+ read
3624
3827
  }) {
3828
+ if (isTsrxModule(file) && this.environment.name === 'client') {
3829
+ updateTsrxCss(tsrxCss, file, await compileTsrxCss(await read(), file));
3830
+ const cssModule = this.environment.moduleGraph.getModuleById(resolvedTsrxCssModuleId(file));
3831
+ if (cssModule) {
3832
+ this.environment.moduleGraph.invalidateModule(cssModule);
3833
+ if (!modules.includes(cssModule)) modules = [...modules, cssModule];
3834
+ return modules;
3835
+ }
3836
+ }
3837
+
3625
3838
  // solid-refresh only injects HMR boundaries into client modules, so
3626
3839
  // non-client environments have no accept handlers. Without this, Vite
3627
3840
  // would see no boundaries and send full-reload messages that race with
@@ -3658,6 +3871,8 @@ function solidPlugin(options = {}) {
3658
3871
  }
3659
3872
  },
3660
3873
  resolveId(id) {
3874
+ const tsrxCssId = resolveTsrxCssModule(id);
3875
+ if (tsrxCssId) return tsrxCssId;
3661
3876
  if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
3662
3877
  },
3663
3878
  moduleParsed(info) {
@@ -3670,7 +3885,7 @@ function solidPlugin(options = {}) {
3670
3885
  for (const depId of info.dynamicallyImportedIds || []) {
3671
3886
  const cleanId = depId.split('?')[0];
3672
3887
  if (/node_modules/.test(cleanId) || cleanId.startsWith('\0')) continue;
3673
- if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue;
3888
+ if (!(/\.[mc]?[tj]sx?$/i.test(cleanId) || isTsrxModule(cleanId))) continue;
3674
3889
  if (emittedLazyChunks.has(depId)) continue;
3675
3890
  emittedLazyChunks.add(depId);
3676
3891
  emittedLazyChunkRefs.push(this.emitFile({
@@ -3681,6 +3896,8 @@ function solidPlugin(options = {}) {
3681
3896
  }
3682
3897
  },
3683
3898
  load(id) {
3899
+ const tsrxSource = tsrxCssSourceId(id);
3900
+ if (tsrxSource) return tsrxCss.get(tsrxSource) ?? '';
3684
3901
  if (id === RESOLVED_VIRTUAL_MANIFEST_ID) {
3685
3902
  if (!isBuild) {
3686
3903
  return devManifestCode(projectRoot, base, devServer ? devManifestBridgeUrl(devServer) : null);
@@ -3722,6 +3939,7 @@ function solidPlugin(options = {}) {
3722
3939
  }
3723
3940
  },
3724
3941
  async transform(source, id, transformOptions) {
3942
+ if (isTsrxCssModule(id)) return null;
3725
3943
  const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3726
3944
  const currentFileExtension = getExtension(id);
3727
3945
  const extensionsToWatch = options.extensions || [];
@@ -3737,14 +3955,15 @@ function solidPlugin(options = {}) {
3737
3955
  // while the transform pipeline below works on the clean file path.
3738
3956
  const moduleId = id;
3739
3957
  id = id.replace(/\?.*$/, '');
3740
- if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
3958
+ const isTsrx = isTsrxModule(id);
3959
+ if (!(/\.[mc]?[tj]sx$/i.test(id) || isTsrx || allExtensions.includes(currentFileExtension))) {
3741
3960
  return null;
3742
3961
  }
3743
3962
  const inNodeModules = /node_modules/.test(id);
3744
3963
  const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, isTestMode);
3745
3964
 
3746
3965
  // We need to know if the current file extension has a typescript options tied to it
3747
- const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || extensionsToWatch.some(extension => {
3966
+ const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || isTsrx || extensionsToWatch.some(extension => {
3748
3967
  if (typeof extension === 'string') {
3749
3968
  return extension.includes('tsx');
3750
3969
  }
@@ -3768,7 +3987,7 @@ function solidPlugin(options = {}) {
3768
3987
  // extension; custom extensions registered through `options.extensions`
3769
3988
  // are unknown to it, so borrow a standard one matching the configured
3770
3989
  // TypeScript-ness.
3771
- const nativeFilename = /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3990
+ const nativeFilename = isTsrx || /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3772
3991
 
3773
3992
  // Shared native prelude for every mode: the lazy() module-URL pass,
3774
3993
  // then (dev/client/non-node_modules) the solid-refresh HMR pass, both
@@ -3778,6 +3997,97 @@ function solidPlugin(options = {}) {
3778
3997
  const compiler = await loadNativeCompiler();
3779
3998
  let code = source;
3780
3999
  const maps = [];
4000
+ if (isTsrx) {
4001
+ // Solid lowering preserves authored TypeScript annotations; secondary
4002
+ // passes therefore parse the generated module as TSX even though no
4003
+ // template syntax remains.
4004
+ const generatedFilename = id + '.tsx';
4005
+ const babelBaseOptions = {
4006
+ root: projectRoot,
4007
+ filename: id,
4008
+ sourceFileName: id,
4009
+ ast: false,
4010
+ sourceMaps: true,
4011
+ configFile: false,
4012
+ babelrc: false,
4013
+ parserOpts: {
4014
+ plugins
4015
+ }
4016
+ };
4017
+ let css = '';
4018
+ if (options.compiler !== 'babel') {
4019
+ const result = await compiler.transformAsync(code, {
4020
+ ...solidOptions,
4021
+ filename: id,
4022
+ sourceMap: true
4023
+ });
4024
+ code = result.code || '';
4025
+ css = nativeTsrxCss(result);
4026
+ maps.push(result.map);
4027
+ if (options.babel) {
4028
+ // The support pass cannot parse authored TSRX. On this route it
4029
+ // intentionally sees the lowered ordinary JavaScript instead.
4030
+ const supportOptions = mergeAnything.mergeAndConcat(babelUserOptions, babelBaseOptions);
4031
+ // This pass sees native-lowered ordinary JavaScript, so do not
4032
+ // route it back through Babel's TSRX parser.
4033
+ supportOptions.filename = generatedFilename;
4034
+ const supportResult = await babel__namespace.transformAsync(code, supportOptions);
4035
+ if (!supportResult) return undefined;
4036
+ code = supportResult.code || '';
4037
+ maps.push(supportResult.map);
4038
+ }
4039
+ } else {
4040
+ const babelOptions = mergeAnything.mergeAndConcat(babelUserOptions, {
4041
+ ...babelBaseOptions,
4042
+ plugins: [[solid, solidOptions]]
4043
+ });
4044
+ const result = await babel__namespace.transformAsync(code, babelOptions);
4045
+ if (!result) return undefined;
4046
+ code = result.code || '';
4047
+ css = babelTsrxCss(result);
4048
+ maps.push(result.map);
4049
+ }
4050
+ const lazyResult = await compiler.transformLazyAsync(code, {
4051
+ filename: generatedFilename,
4052
+ sourceMap: true
4053
+ });
4054
+ code = lazyResult.code;
4055
+ maps.push(lazyResult.map);
4056
+ if (needRefresh) {
4057
+ const refreshResult = await compiler.transformRefreshAsync(code, {
4058
+ filename: generatedFilename,
4059
+ bundler: 'vite',
4060
+ fixRender: true,
4061
+ ...(typeof options.refresh?.granular === 'boolean' ? {
4062
+ granular: options.refresh.granular
4063
+ } : {}),
4064
+ jsx: false,
4065
+ importSource: REFRESH_RUNTIME_SOURCE,
4066
+ sourceMap: true
4067
+ });
4068
+ code = refreshResult.code;
4069
+ maps.push(refreshResult.map);
4070
+ }
4071
+ code = injectSsrModuleId(await resolveLazyModuleUrls(this, code, id), moduleId, !!isSsr);
4072
+ let map = options.compiler === 'babel' ? combineSourcemaps(maps) : null;
4073
+ updateTsrxCss(tsrxCss, id, css);
4074
+ if (css) {
4075
+ code = prependTsrxCssImport(code, id);
4076
+ map = offsetSourceMapLine(map);
4077
+ }
4078
+ // Vite selects its TypeScript stripping by file extension. Since the
4079
+ // real module identity remains `.tsrx`, strip the annotations here
4080
+ // after Solid lowering instead of handing typed JavaScript to Rollup.
4081
+ const stripped = await vite.transformWithOxc(code, generatedFilename, {
4082
+ lang: 'tsx',
4083
+ sourcemap: map != null,
4084
+ target: 'esnext'
4085
+ }, map ?? undefined);
4086
+ return {
4087
+ code: stripped.code,
4088
+ map: map == null ? null : stripped.map
4089
+ };
4090
+ }
3781
4091
  const lazyResult = await compiler.transformLazyAsync(code, {
3782
4092
  filename: nativeFilename,
3783
4093
  sourceMap: true
@@ -3860,20 +4170,24 @@ function solidPlugin(options = {}) {
3860
4170
  }
3861
4171
  };
3862
4172
 
3863
- // The directive transform must run before the JSX transform (it operates
3864
- // on raw directives, and client-mode module-level extraction must happen
3865
- // before templates are generated), so its sub-plugins go first. The
3866
- // boundary markers (`server-only` / `client-only`) are always on.
3867
- const plugins = options.serverFunctions ? [boundaryModules(), ...serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
4173
+ // Ordinary modules need the directive transform before JSX. Authored TSRX
4174
+ // cannot be parsed by that standalone pass, so its companion compiler runs
4175
+ // after mainPlugin has lowered the file to ordinary JavaScript while keeping
4176
+ // the original .tsrx id for stable server-function hashes.
4177
+ const serverFunctionPlugins = options.serverFunctions ? serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
3868
4178
  devMiddleware: true,
3869
4179
  externalDevServer,
4180
+ tsrxAfterSolid: true,
4181
+ tsrxSourceMap: options.compiler === 'babel',
3870
4182
  // With start mode on (either variant), the dev middleware dispatches
3871
4183
  // the endpoint through the SSR handler so user middleware and the
3872
4184
  // stub-backed request event front it exactly like page SSR.
3873
4185
  ...(startOptions ? {
3874
4186
  ssrHandler: SSR_HANDLER_ID
3875
4187
  } : {})
3876
- }), mainPlugin] : [boundaryModules(), mainPlugin];
4188
+ }) : [];
4189
+ const tsrxServerFunctionPlugin = serverFunctionPlugins.find(plugin => plugin.name === 'solid:server-functions/tsrx-compiler');
4190
+ const plugins = [boundaryModules(), ...serverFunctionPlugins.filter(plugin => plugin !== tsrxServerFunctionPlugin), mainPlugin, ...(tsrxServerFunctionPlugin ? [tsrxServerFunctionPlugin] : [])];
3877
4191
 
3878
4192
  // The `start` option opts into start-mode serving on top of the transforms;
3879
4193
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
@@ -3888,7 +4202,7 @@ function solidPlugin(options = {}) {
3888
4202
  serverComponents,
3889
4203
  ssr: !!options.ssr,
3890
4204
  styleFilter: filterDevStyles,
3891
- diagnostics: !!options.diagnostics,
4205
+ diagnostics: options.diagnostics ?? 'auto',
3892
4206
  onDocumentResolved(documentPath) {
3893
4207
  // Normalize to forward slashes to match Vite's transform ids.
3894
4208
  documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
@@ -3897,9 +4211,11 @@ function solidPlugin(options = {}) {
3897
4211
  }
3898
4212
 
3899
4213
  // Agent diagnostics endpoint + injected bridge (dev serve only — the
3900
- // plugin no-ops itself for builds and preview via `apply`).
3901
- if (options.diagnostics) {
3902
- plugins.push(solidDiagnostics());
4214
+ // plugin no-ops itself for builds and preview via `apply`, and in the
4215
+ // default auto mode additionally disables itself unless the app has
4216
+ // `@solidjs/diagnostics` installed).
4217
+ if (options.diagnostics !== false) {
4218
+ plugins.push(solidDiagnostics(options.diagnostics === true ? true : 'auto'));
3903
4219
  }
3904
4220
 
3905
4221
  // Builder-mode (environments API) client-before-server build ordering.