@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.
@@ -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
 
@@ -146,6 +146,75 @@ function joinBase(base, pathname) {
146
146
  return (base.endsWith('/') ? base.slice(0, -1) : base) + pathname;
147
147
  }
148
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
+
149
218
  /**
150
219
  * Dev-mode asset resolution: the `virtual:solid-manifest` module exports a
151
220
  * resolver function in dev (instead of the static object a build produces),
@@ -255,6 +324,9 @@ const cssFileRegExp = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
255
324
  // importer controls them — so they must not be SSR'd as style tags.
256
325
  const nonAmbientQueryRegExp = /[?&](url|inline|raw)\b/;
257
326
  const NULL_BYTE_PLACEHOLDER = '/@id/__x00__';
327
+ function isCssModuleUrl(url) {
328
+ return cssFileRegExp.test(url.split('?')[0]) || isTsrxCssModule(url);
329
+ }
258
330
 
259
331
  // Per Vite's convention virtual module ids are prefixed with `\0`, which
260
332
  // cannot appear in an HTML attribute (the parser replaces it). Serialize the
@@ -311,7 +383,7 @@ async function collectModuleDeps(env, file, deps, crawled, filter, onFile, impor
311
383
  const node = await getModuleNode(env, file, importer);
312
384
  if (!node?.id || deps.has(node)) return;
313
385
  deps.add(node);
314
- const isCss = cssFileRegExp.test(node.url.split('?')[0]);
386
+ const isCss = isCssModuleUrl(node.url);
315
387
  if (!isCss && node.file && !node.id.startsWith('\0') && !filter(node.file)) return;
316
388
  if (node.file) onFile?.(node.file);
317
389
  if (isCss) return;
@@ -343,8 +415,7 @@ async function collectDevStyleSources(env, files, onFile, filter = defaultStyleF
343
415
  const seen = new Set();
344
416
  for (const node of deps) {
345
417
  if (!node.id) continue;
346
- const cleanUrl = node.url.split('?')[0];
347
- if (!cssFileRegExp.test(cleanUrl) || nonAmbientQueryRegExp.test(node.url)) continue;
418
+ if (!isCssModuleUrl(node.url) || nonAmbientQueryRegExp.test(node.url)) continue;
348
419
  const id = wrapId(node.id);
349
420
  if (seen.has(id)) continue;
350
421
  seen.add(id);
@@ -558,7 +629,11 @@ function boundaryModules() {
558
629
  }
559
630
 
560
631
  /**
561
- * 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).
562
637
  *
563
638
  * Three pieces:
564
639
  * - an injected client module (virtual, imported by index.html or the
@@ -586,6 +661,36 @@ const METHODS = ['begin', 'end', 'active', 'whyDidRun', 'costs'];
586
661
 
587
662
  /** How long the endpoint waits for a page to answer before failing the call. */
588
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
+ }
589
694
  function diagnosticsClientModuleCode() {
590
695
  // Runtime imports resolve to the APP's diagnostics package (see the
591
696
  // resolveId assist below) — the page speaks its own package's protocol.
@@ -612,18 +717,26 @@ function readJsonBody(req) {
612
717
  req.on('error', reject);
613
718
  });
614
719
  }
615
- function solidDiagnostics() {
720
+ function solidDiagnostics(mode = 'auto') {
616
721
  let root = process.cwd();
617
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;
618
727
  return {
619
728
  name: 'solid:diagnostics',
620
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.
621
733
  apply(_config, env) {
622
- return env.command === 'serve' && !env.isPreview;
734
+ return env.command === 'serve' && !env.isPreview && env.mode !== 'test';
623
735
  },
624
736
  configResolved(config) {
625
737
  root = config.root;
626
738
  base = config.base;
739
+ if (mode === 'auto') enabled = detectDiagnosticsPackage(root);
627
740
  },
628
741
  async resolveId(source, importer) {
629
742
  if (source === DIAGNOSTICS_CLIENT_ID) {
@@ -639,7 +752,7 @@ function solidDiagnostics() {
639
752
  skipSelf: true
640
753
  });
641
754
  if (!resolved || resolved.id.startsWith('__vite-optional-peer-dep:')) {
642
- 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.');
643
756
  }
644
757
  return resolved;
645
758
  }
@@ -652,6 +765,7 @@ function solidDiagnostics() {
652
765
  // Plain (index.html) apps get the client module injected here;
653
766
  // start-mode apps import it from the generated client entry instead.
654
767
  transformIndexHtml() {
768
+ if (!enabled) return undefined;
655
769
  return [{
656
770
  tag: 'script',
657
771
  attrs: {
@@ -662,6 +776,11 @@ function solidDiagnostics() {
662
776
  }];
663
777
  },
664
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
+
665
784
  // Announce the surface in the startup block. This is a discovery
666
785
  // channel: agents watching dev-server output learn the endpoint and
667
786
  // the skill documents without any project-level pointer (AGENTS.md).
@@ -670,7 +789,7 @@ function solidDiagnostics() {
670
789
  originalPrintUrls();
671
790
  const local = server.resolvedUrls?.local[0];
672
791
  const endpoint = local ? new URL(DIAGNOSTICS_ENDPOINT, local).href : DIAGNOSTICS_ENDPOINT;
673
- 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`);
674
793
  };
675
794
  const pending = new Map();
676
795
  let nextId = 1;
@@ -681,6 +800,11 @@ function solidDiagnostics() {
681
800
  clearTimeout(entry.timer);
682
801
  entry.resolve(data);
683
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.
684
808
  server.middlewares.use(DIAGNOSTICS_ENDPOINT, async (req, res) => {
685
809
  // The middleware mounts on the exact path; anything deeper is 404.
686
810
  if (req.url && req.url !== '/' && req.url !== '') {
@@ -803,7 +927,7 @@ async function compile(id, code, options) {
803
927
  mode: options.mode,
804
928
  env: options.env,
805
929
  directive: options.directive,
806
- sourceMap: true,
930
+ sourceMap: options.sourceMap !== false,
807
931
  register: options.definitions.register,
808
932
  create: options.definitions.create
809
933
  });
@@ -929,11 +1053,11 @@ function xxHash32(buffer, seed = 0) {
929
1053
  * root — not the invocation directory — so running `vite` from outside the
930
1054
  * project keeps compiling the same files. Absolute patterns are used as-is.
931
1055
  *
932
- * @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}"
933
1057
  */
934
1058
 
935
- const DEFAULT_INCLUDE = 'src/**/*.{jsx,tsx,ts,js,mjs,cjs}';
936
- 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}';
937
1061
  const DEFAULT_MANIFEST = 'virtual:solid-server-function-manifest';
938
1062
  const DEFAULT_DIRECTIVE = 'use server';
939
1063
  const DEFAULT_RUNTIME = '@solidjs/web/server-functions';
@@ -1298,6 +1422,46 @@ function serverFunctions(options = {}, internal = {}) {
1298
1422
  }
1299
1423
  });
1300
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
+ };
1301
1465
  return [{
1302
1466
  name: 'solid:server-functions/setup',
1303
1467
  enforce: 'pre',
@@ -1374,44 +1538,7 @@ function serverFunctions(options = {}, internal = {}) {
1374
1538
  }
1375
1539
  return null;
1376
1540
  }
1377
- }, {
1378
- name: 'solid:server-functions/compiler',
1379
- enforce: 'pre',
1380
- async transform(code, fileId, opts) {
1381
- const mode = getEnvironmentConsumer(this.environment, opts);
1382
- const [id] = fileId.split('?');
1383
- if (!filter(id)) {
1384
- return null;
1385
- }
1386
-
1387
- // Fast path: the directive has to appear literally, so anything
1388
- // without the substring can skip the native parse entirely.
1389
- if (!code.includes(directive)) {
1390
- return null;
1391
- }
1392
- const result = await compile(id, code, {
1393
- ...(mode === 'server' ? serverOptions : clientOptions),
1394
- mode,
1395
- env,
1396
- root
1397
- });
1398
- if (result.valid) {
1399
- const preloader = preload[mode];
1400
- if (preloader) {
1401
- preloader.defer();
1402
- }
1403
- invalidateModules(currentServer, mergeManifestRecord(manifest.server, new Set([id])), manifestId);
1404
- return {
1405
- // Appended (not prepended) so the source map for the compiled
1406
- // module stays valid; imports hoist and the endpoint is only
1407
- // read at call time, never during module evaluation.
1408
- code: (result.code || '') + endpointConfigureSnippet(mode),
1409
- map: result.map
1410
- };
1411
- }
1412
- return null;
1413
- }
1414
- }, ...startPlugins];
1541
+ }, compilerPlugin, ...(internal.tsrxAfterSolid ? [tsrxCompilerPlugin] : []), ...startPlugins];
1415
1542
  }
1416
1543
 
1417
1544
  const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
@@ -1506,9 +1633,9 @@ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1506
1633
  const MANIFEST_ID = 'virtual:solid-manifest';
1507
1634
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1508
1635
  const STORAGE_SOURCE = '@solidjs/web/storage';
1509
- const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs'];
1510
- const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
1511
- 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'];
1512
1639
  function probe(root, stem, extensions) {
1513
1640
  for (const ext of extensions) {
1514
1641
  if (existsSync(path.resolve(root, stem + ext))) return stem + ext;
@@ -1616,7 +1743,9 @@ function startServe(options, internal = {}) {
1616
1743
  const serverComponents = !!internal.serverComponents;
1617
1744
  const errorBoundary = options.errorBoundary !== false;
1618
1745
  const styleFilter = internal.styleFilter;
1619
- 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;
1620
1749
  let devtoolsEnabled = false;
1621
1750
  let devtoolsResolutions = {};
1622
1751
  let devtoolsIds = {};
@@ -2126,6 +2255,12 @@ function startServe(options, internal = {}) {
2126
2255
  root = config.root;
2127
2256
  base = config.base;
2128
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
+ }
2129
2264
  },
2130
2265
  resolveId(source, importer, opts) {
2131
2266
  if (source === HANDLER_ID) {
@@ -3242,7 +3377,8 @@ function solidPlugin(options = {}) {
3242
3377
  // resolve against the Vite root, not process.cwd() — running `vite` from
3243
3378
  // outside the project would otherwise change what the filter matches.
3244
3379
  let filter = createFilter(options.include, options.exclude);
3245
- const serverComponents = typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
3380
+ const serverComponentsOption = typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
3381
+ const serverComponents = !!serverComponentsOption;
3246
3382
  // `start: true` is sugar for the empty options bag — one start mode,
3247
3383
  // two spellings — so normalize here and let everything downstream see a
3248
3384
  // single shape (`false` behaves exactly like omission).
@@ -3292,6 +3428,7 @@ function solidPlugin(options = {}) {
3292
3428
  let base = '/';
3293
3429
  let clientOutDir = null;
3294
3430
  let solidPkgsConfig;
3431
+ const tsrxCss = new Map();
3295
3432
 
3296
3433
  // The client build's manifest, read back by SSR builds. In builder-mode
3297
3434
  // (single process, e.g. SolidStart's nitro plugin) the client build runs
@@ -3386,6 +3523,44 @@ function solidPlugin(options = {}) {
3386
3523
  const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
3387
3524
  return code + `\nexport const $$moduleUrl = ${JSON.stringify(relativeId)};\n`;
3388
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
+ }
3389
3564
  const mainPlugin = {
3390
3565
  name: 'solid',
3391
3566
  enforce: 'pre',
@@ -3476,6 +3651,7 @@ function solidPlugin(options = {}) {
3476
3651
  dedupe: nestedDeps
3477
3652
  },
3478
3653
  optimizeDeps: {
3654
+ extensions: ['.tsrx'],
3479
3655
  include: [...nestedDeps,
3480
3656
  // Dev refresh wrappers import the solid-js/refresh runtime in
3481
3657
  // every mode; pre-bundle it up front so its discovery doesn't
@@ -3496,7 +3672,28 @@ function solidPlugin(options = {}) {
3496
3672
  jsx: {
3497
3673
  runtime: 'classic'
3498
3674
  }
3499
- }
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
+ }]
3500
3697
  }
3501
3698
  },
3502
3699
  ...(Object.keys(test).length ? {
@@ -3555,8 +3752,13 @@ function solidPlugin(options = {}) {
3555
3752
  resolve: projectRoot
3556
3753
  });
3557
3754
  styleFilter = createStyleFilter(projectRoot);
3558
- if (serverComponents && !(options.start && options.ssr)) {
3559
- 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.");
3560
3762
  }
3561
3763
  needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false && !options.refresh?.disabled;
3562
3764
  },
@@ -3594,10 +3796,21 @@ function solidPlugin(options = {}) {
3594
3796
  return origSend(...args);
3595
3797
  };
3596
3798
  },
3597
- hotUpdate({
3799
+ async hotUpdate({
3800
+ file,
3598
3801
  modules,
3599
- file
3802
+ read
3600
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
+
3601
3814
  // solid-refresh only injects HMR boundaries into client modules, so
3602
3815
  // non-client environments have no accept handlers. Without this, Vite
3603
3816
  // would see no boundaries and send full-reload messages that race with
@@ -3634,6 +3847,8 @@ function solidPlugin(options = {}) {
3634
3847
  }
3635
3848
  },
3636
3849
  resolveId(id) {
3850
+ const tsrxCssId = resolveTsrxCssModule(id);
3851
+ if (tsrxCssId) return tsrxCssId;
3637
3852
  if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
3638
3853
  },
3639
3854
  moduleParsed(info) {
@@ -3646,7 +3861,7 @@ function solidPlugin(options = {}) {
3646
3861
  for (const depId of info.dynamicallyImportedIds || []) {
3647
3862
  const cleanId = depId.split('?')[0];
3648
3863
  if (/node_modules/.test(cleanId) || cleanId.startsWith('\0')) continue;
3649
- if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue;
3864
+ if (!(/\.[mc]?[tj]sx?$/i.test(cleanId) || isTsrxModule(cleanId))) continue;
3650
3865
  if (emittedLazyChunks.has(depId)) continue;
3651
3866
  emittedLazyChunks.add(depId);
3652
3867
  emittedLazyChunkRefs.push(this.emitFile({
@@ -3657,6 +3872,8 @@ function solidPlugin(options = {}) {
3657
3872
  }
3658
3873
  },
3659
3874
  load(id) {
3875
+ const tsrxSource = tsrxCssSourceId(id);
3876
+ if (tsrxSource) return tsrxCss.get(tsrxSource) ?? '';
3660
3877
  if (id === RESOLVED_VIRTUAL_MANIFEST_ID) {
3661
3878
  if (!isBuild) {
3662
3879
  return devManifestCode(projectRoot, base, devServer ? devManifestBridgeUrl(devServer) : null);
@@ -3698,6 +3915,7 @@ function solidPlugin(options = {}) {
3698
3915
  }
3699
3916
  },
3700
3917
  async transform(source, id, transformOptions) {
3918
+ if (isTsrxCssModule(id)) return null;
3701
3919
  const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3702
3920
  const currentFileExtension = getExtension(id);
3703
3921
  const extensionsToWatch = options.extensions || [];
@@ -3713,14 +3931,15 @@ function solidPlugin(options = {}) {
3713
3931
  // while the transform pipeline below works on the clean file path.
3714
3932
  const moduleId = id;
3715
3933
  id = id.replace(/\?.*$/, '');
3716
- 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))) {
3717
3936
  return null;
3718
3937
  }
3719
3938
  const inNodeModules = /node_modules/.test(id);
3720
3939
  const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, isTestMode);
3721
3940
 
3722
3941
  // We need to know if the current file extension has a typescript options tied to it
3723
- const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || extensionsToWatch.some(extension => {
3942
+ const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || isTsrx || extensionsToWatch.some(extension => {
3724
3943
  if (typeof extension === 'string') {
3725
3944
  return extension.includes('tsx');
3726
3945
  }
@@ -3744,7 +3963,7 @@ function solidPlugin(options = {}) {
3744
3963
  // extension; custom extensions registered through `options.extensions`
3745
3964
  // are unknown to it, so borrow a standard one matching the configured
3746
3965
  // TypeScript-ness.
3747
- 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');
3748
3967
 
3749
3968
  // Shared native prelude for every mode: the lazy() module-URL pass,
3750
3969
  // then (dev/client/non-node_modules) the solid-refresh HMR pass, both
@@ -3754,6 +3973,97 @@ function solidPlugin(options = {}) {
3754
3973
  const compiler = await loadNativeCompiler();
3755
3974
  let code = source;
3756
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
+ }
3757
4067
  const lazyResult = await compiler.transformLazyAsync(code, {
3758
4068
  filename: nativeFilename,
3759
4069
  sourceMap: true
@@ -3836,20 +4146,24 @@ function solidPlugin(options = {}) {
3836
4146
  }
3837
4147
  };
3838
4148
 
3839
- // The directive transform must run before the JSX transform (it operates
3840
- // on raw directives, and client-mode module-level extraction must happen
3841
- // before templates are generated), so its sub-plugins go first. The
3842
- // boundary markers (`server-only` / `client-only`) are always on.
3843
- 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, {
3844
4154
  devMiddleware: true,
3845
4155
  externalDevServer,
4156
+ tsrxAfterSolid: true,
4157
+ tsrxSourceMap: options.compiler === 'babel',
3846
4158
  // With start mode on (either variant), the dev middleware dispatches
3847
4159
  // the endpoint through the SSR handler so user middleware and the
3848
4160
  // stub-backed request event front it exactly like page SSR.
3849
4161
  ...(startOptions ? {
3850
4162
  ssrHandler: SSR_HANDLER_ID
3851
4163
  } : {})
3852
- }), 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] : [])];
3853
4167
 
3854
4168
  // The `start` option opts into start-mode serving on top of the transforms;
3855
4169
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
@@ -3864,7 +4178,7 @@ function solidPlugin(options = {}) {
3864
4178
  serverComponents,
3865
4179
  ssr: !!options.ssr,
3866
4180
  styleFilter: filterDevStyles,
3867
- diagnostics: !!options.diagnostics,
4181
+ diagnostics: options.diagnostics ?? 'auto',
3868
4182
  onDocumentResolved(documentPath) {
3869
4183
  // Normalize to forward slashes to match Vite's transform ids.
3870
4184
  documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
@@ -3873,9 +4187,11 @@ function solidPlugin(options = {}) {
3873
4187
  }
3874
4188
 
3875
4189
  // Agent diagnostics endpoint + injected bridge (dev serve only — the
3876
- // plugin no-ops itself for builds and preview via `apply`).
3877
- if (options.diagnostics) {
3878
- 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'));
3879
4195
  }
3880
4196
 
3881
4197
  // Builder-mode (environments API) client-before-server build ordering.