@solidjs/vite-plugin 3.0.0-next.37 → 3.0.0-next.39

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';
@@ -954,29 +1078,50 @@ const HANDLER_ID$1 = 'virtual:solid-server-function-handler';
954
1078
  // (`vite build` then `vite build --ssr`) does not, so the client build
955
1079
  // persists its findings for the SSR build to merge (mirroring the plugin's
956
1080
  // dist/client/.vite/manifest.json convention).
1081
+ //
1082
+ // The file doubles as the build's statement of which server functions the
1083
+ // CLIENT can reach — every reference the client compile emitted, by wire id
1084
+ // — for build tooling that needs that set without re-deriving it from
1085
+ // compiled output (a static-site prerenderer checking that each reachable
1086
+ // function was captured, for example). Paths are root-relative, posix.
957
1087
  const PERSISTED_MANIFEST_PATH = '.vite/solid-server-functions.json';
1088
+
1089
+ /** The persisted manifest's on-disk shape (the array form is the pre-`functions` legacy). */
1090
+
958
1091
  function readPersistedManifest(root) {
959
1092
  const file = path.resolve(root, 'dist/client', PERSISTED_MANIFEST_PATH);
960
1093
  if (!existsSync(file)) return new Set();
961
1094
  try {
962
- const entries = JSON.parse(readFileSync(file, 'utf-8'));
1095
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'));
1096
+ const entries = Array.isArray(parsed) ? parsed : parsed.modules;
963
1097
  return new Set(entries.map(entry => path.resolve(root, entry)).filter(entry => existsSync(entry)));
964
1098
  } catch {
965
1099
  return new Set();
966
1100
  }
967
1101
  }
968
- function writePersistedManifest(root, outDir, entries) {
1102
+ function writePersistedManifest(root, outDir, entries, functions) {
969
1103
  const file = path.resolve(root, outDir, PERSISTED_MANIFEST_PATH);
970
1104
  mkdirSync(path.dirname(file), {
971
1105
  recursive: true
972
1106
  });
973
- const relative = [...entries].map(entry => path.relative(root, entry).split(path.sep).join('/'));
974
- writeFileSync(file, JSON.stringify(relative, null, 2));
1107
+ const relative = entry => path.relative(root, entry).split(path.sep).join('/');
1108
+ const manifest = {
1109
+ modules: [...entries].map(relative),
1110
+ functions: [...functions].map(([id, record]) => ({
1111
+ id,
1112
+ name: record.name,
1113
+ module: relative(record.module)
1114
+ }))
1115
+ };
1116
+ writeFileSync(file, JSON.stringify(manifest, null, 2));
975
1117
  }
976
1118
  function createManifest() {
977
1119
  return {
978
- server: new Set(),
979
- client: new Set()
1120
+ modules: {
1121
+ server: new Set(),
1122
+ client: new Set()
1123
+ },
1124
+ clientFunctions: new Map()
980
1125
  };
981
1126
  }
982
1127
  function createDeferredPromise() {
@@ -1184,13 +1329,13 @@ function serverFunctions(options = {}, internal = {}) {
1184
1329
  const hashIndex = new Map();
1185
1330
  let hashIndexSize = -1;
1186
1331
  function moduleForFunctionId(functionId) {
1187
- if (manifest.server.size !== hashIndexSize) {
1332
+ if (manifest.modules.server.size !== hashIndexSize) {
1188
1333
  hashIndex.clear();
1189
- for (const entry of manifest.server) {
1334
+ for (const entry of manifest.modules.server) {
1190
1335
  const relative = path.relative(root, entry).split(path.sep).join('/');
1191
1336
  hashIndex.set(xxHash32(relative).toString(16), entry);
1192
1337
  }
1193
- hashIndexSize = manifest.server.size;
1338
+ hashIndexSize = manifest.modules.server.size;
1194
1339
  }
1195
1340
  return hashIndex.get(functionId.split('-')[1]);
1196
1341
  }
@@ -1298,6 +1443,62 @@ function serverFunctions(options = {}, internal = {}) {
1298
1443
  }
1299
1444
  });
1300
1445
  }
1446
+ async function transformModule(ctx, code, fileId, opts, tsrx) {
1447
+ const mode = getEnvironmentConsumer(ctx.environment, opts);
1448
+ const [id] = fileId.split('?');
1449
+ if (!id || !filter(id) || isTsrxModule(id) !== tsrx) return null;
1450
+
1451
+ // The directive has to appear literally, so anything without the
1452
+ // substring can skip the native parse entirely.
1453
+ if (!code.includes(directive)) return null;
1454
+ const result = await compile(id, code, {
1455
+ ...(mode === 'server' ? serverOptions : clientOptions),
1456
+ mode,
1457
+ env,
1458
+ root,
1459
+ sourceMap: !tsrx || !!internal.tsrxSourceMap
1460
+ });
1461
+ if (!result.valid) return null;
1462
+
1463
+ // The client compile is the authority on what the browser can dispatch:
1464
+ // record every reference it emitted, by wire id, for the persisted
1465
+ // manifest. A module is re-transformed on change, so its previous ids
1466
+ // are dropped first (a renamed function must not linger as reachable).
1467
+ if (mode === 'client') {
1468
+ for (const [functionId, record] of manifest.clientFunctions) {
1469
+ if (record.module === id) manifest.clientFunctions.delete(functionId);
1470
+ }
1471
+ for (const fn of result.functions) {
1472
+ manifest.clientFunctions.set(fn.id, {
1473
+ name: fn.name,
1474
+ module: id
1475
+ });
1476
+ }
1477
+ }
1478
+ const preloader = preload[mode];
1479
+ if (preloader) preloader.defer();
1480
+ invalidateModules(currentServer, mergeManifestRecord(manifest.modules.server, new Set([id])), manifestId);
1481
+ return {
1482
+ // Appended (not prepended) so the source map for the compiled module
1483
+ // stays valid; imports hoist and the endpoint is only read at call time.
1484
+ code: (result.code || '') + endpointConfigureSnippet(mode),
1485
+ map: result.map
1486
+ };
1487
+ }
1488
+ const compilerPlugin = {
1489
+ name: 'solid:server-functions/compiler',
1490
+ enforce: 'pre',
1491
+ transform(code, fileId, opts) {
1492
+ return transformModule(this, code, fileId, opts, false);
1493
+ }
1494
+ };
1495
+ const tsrxCompilerPlugin = {
1496
+ name: 'solid:server-functions/tsrx-compiler',
1497
+ enforce: 'pre',
1498
+ transform(code, fileId, opts) {
1499
+ return transformModule(this, code, fileId, opts, true);
1500
+ }
1501
+ };
1301
1502
  return [{
1302
1503
  name: 'solid:server-functions/setup',
1303
1504
  enforce: 'pre',
@@ -1324,7 +1525,7 @@ function serverFunctions(options = {}, internal = {}) {
1324
1525
  // build discovered so the server manifest registers them even when
1325
1526
  // the SSR module graph never imports them.
1326
1527
  for (const entry of readPersistedManifest(root)) {
1327
- manifest.server.add(entry);
1528
+ manifest.modules.server.add(entry);
1328
1529
  }
1329
1530
  }
1330
1531
  },
@@ -1339,7 +1540,7 @@ function serverFunctions(options = {}, internal = {}) {
1339
1540
  const consumer = ctx.environment?.config?.consumer;
1340
1541
  const isClient = consumer ? consumer === 'client' : !isSsrBuild;
1341
1542
  if (isBuild && isClient) {
1342
- writePersistedManifest(root, outDir, manifest.server);
1543
+ writePersistedManifest(root, outDir, manifest.modules.server, manifest.clientFunctions);
1343
1544
  }
1344
1545
  }
1345
1546
  }, {
@@ -1364,54 +1565,17 @@ function serverFunctions(options = {}, internal = {}) {
1364
1565
  // configs resolve before the client build has written the file,
1365
1566
  // but this load runs once the SSR environment builds — after it.
1366
1567
  for (const entry of readPersistedManifest(root)) {
1367
- manifest.server.add(entry);
1568
+ manifest.modules.server.add(entry);
1368
1569
  }
1369
1570
  }
1370
- const current = new Debouncer(() => [...manifest[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1571
+ const current = new Debouncer(() => [...manifest.modules[mode]].map(entry => `import ${JSON.stringify(entry)};`).join('\n'));
1371
1572
  preload[mode] = current;
1372
1573
  const result = await current.promise.reference;
1373
1574
  return result;
1374
1575
  }
1375
1576
  return null;
1376
1577
  }
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];
1578
+ }, compilerPlugin, ...(internal.tsrxAfterSolid ? [tsrxCompilerPlugin] : []), ...startPlugins];
1415
1579
  }
1416
1580
 
1417
1581
  const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
@@ -1506,9 +1670,9 @@ const ERROR_BOUNDARY_ID = 'virtual:solid-ssr-error-boundary.tsx';
1506
1670
  const MANIFEST_ID = 'virtual:solid-manifest';
1507
1671
  const SERVER_FUNCTION_HANDLER_ID = 'virtual:solid-server-function-handler';
1508
1672
  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'];
1673
+ const ENTRY_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.mjs', '.tsrx'];
1674
+ const APP_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js', '.tsrx'];
1675
+ const DOCUMENT_EXTENSIONS = ['.tsx', '.jsx', '.tsrx'];
1512
1676
  function probe(root, stem, extensions) {
1513
1677
  for (const ext of extensions) {
1514
1678
  if (existsSync(path.resolve(root, stem + ext))) return stem + ext;
@@ -1616,7 +1780,9 @@ function startServe(options, internal = {}) {
1616
1780
  const serverComponents = !!internal.serverComponents;
1617
1781
  const errorBoundary = options.errorBoundary !== false;
1618
1782
  const styleFilter = internal.styleFilter;
1619
- const diagnostics = !!internal.diagnostics;
1783
+ // `'auto'` resolves against the project root in configResolved, before
1784
+ // any of the (lazy) uses in entry codegen and the entry transform.
1785
+ let diagnostics = internal.diagnostics === true;
1620
1786
  let devtoolsEnabled = false;
1621
1787
  let devtoolsResolutions = {};
1622
1788
  let devtoolsIds = {};
@@ -2126,6 +2292,12 @@ function startServe(options, internal = {}) {
2126
2292
  root = config.root;
2127
2293
  base = config.base;
2128
2294
  isBuild = config.command === 'build';
2295
+ // Test mode excluded for the same reason as the surface plugin's
2296
+ // `apply`: vitest runs a dev serve, and test pages should not get
2297
+ // the bridge import injected into their client entries.
2298
+ if (internal.diagnostics === 'auto' && !isBuild && config.mode !== 'test') {
2299
+ diagnostics = detectDiagnosticsPackage(root);
2300
+ }
2129
2301
  },
2130
2302
  resolveId(source, importer, opts) {
2131
2303
  if (source === HANDLER_ID) {
@@ -3242,7 +3414,8 @@ function solidPlugin(options = {}) {
3242
3414
  // resolve against the Vite root, not process.cwd() — running `vite` from
3243
3415
  // outside the project would otherwise change what the filter matches.
3244
3416
  let filter = createFilter(options.include, options.exclude);
3245
- const serverComponents = typeof options.serverFunctions === 'object' && !!options.serverFunctions.components;
3417
+ const serverComponentsOption = typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
3418
+ const serverComponents = !!serverComponentsOption;
3246
3419
  // `start: true` is sugar for the empty options bag — one start mode,
3247
3420
  // two spellings — so normalize here and let everything downstream see a
3248
3421
  // single shape (`false` behaves exactly like omission).
@@ -3292,6 +3465,7 @@ function solidPlugin(options = {}) {
3292
3465
  let base = '/';
3293
3466
  let clientOutDir = null;
3294
3467
  let solidPkgsConfig;
3468
+ const tsrxCss = new Map();
3295
3469
 
3296
3470
  // The client build's manifest, read back by SSR builds. In builder-mode
3297
3471
  // (single process, e.g. SolidStart's nitro plugin) the client build runs
@@ -3386,6 +3560,44 @@ function solidPlugin(options = {}) {
3386
3560
  const relativeId = path.relative(projectRoot, file).split(path.sep).join('/') + query;
3387
3561
  return code + `\nexport const $$moduleUrl = ${JSON.stringify(relativeId)};\n`;
3388
3562
  }
3563
+ function nativeTsrxCss(result) {
3564
+ const css = result.css;
3565
+ return typeof css === 'string' ? css : '';
3566
+ }
3567
+ function babelTsrxCss(result) {
3568
+ const css = result.metadata?.css;
3569
+ return typeof css === 'string' ? css : '';
3570
+ }
3571
+ async function compileTsrxCss(source, id) {
3572
+ const solidOptions = getSolidOptions(options, false, replaceDev, isTestMode);
3573
+ if (options.compiler === 'babel') {
3574
+ const babelUserOptions = await getBabelUserOptions(options, source, id, false);
3575
+ const babelOptions = mergeAndConcat(babelUserOptions, {
3576
+ root: projectRoot,
3577
+ // Keep .tsrx: the Babel plugin uses it to select its TSRX parser.
3578
+ filename: id,
3579
+ sourceFileName: id,
3580
+ ast: false,
3581
+ code: false,
3582
+ sourceMaps: false,
3583
+ configFile: false,
3584
+ babelrc: false,
3585
+ parserOpts: {
3586
+ plugins: ['jsx', 'decorators', 'typescript']
3587
+ },
3588
+ plugins: [[solid, solidOptions]]
3589
+ });
3590
+ const result = await babel.transformAsync(source, babelOptions);
3591
+ return result ? babelTsrxCss(result) : '';
3592
+ }
3593
+ const compiler = await loadNativeCompiler();
3594
+ const result = await compiler.transformAsync(source, {
3595
+ ...solidOptions,
3596
+ filename: id,
3597
+ sourceMap: false
3598
+ });
3599
+ return nativeTsrxCss(result);
3600
+ }
3389
3601
  const mainPlugin = {
3390
3602
  name: 'solid',
3391
3603
  enforce: 'pre',
@@ -3476,6 +3688,7 @@ function solidPlugin(options = {}) {
3476
3688
  dedupe: nestedDeps
3477
3689
  },
3478
3690
  optimizeDeps: {
3691
+ extensions: ['.tsrx'],
3479
3692
  include: [...nestedDeps,
3480
3693
  // Dev refresh wrappers import the solid-js/refresh runtime in
3481
3694
  // every mode; pre-bundle it up front so its discovery doesn't
@@ -3496,7 +3709,28 @@ function solidPlugin(options = {}) {
3496
3709
  jsx: {
3497
3710
  runtime: 'classic'
3498
3711
  }
3499
- }
3712
+ },
3713
+ plugins: [{
3714
+ name: 'solid:tsrx-dep-scan',
3715
+ async transform(source, id) {
3716
+ if (!isTsrxModule(id) || isTsrxCssModule(id)) return null;
3717
+ const compiler = await loadNativeCompiler();
3718
+ const result = await compiler.transformAsync(source, {
3719
+ ...getSolidOptions(options, false, replaceDev, isTestMode),
3720
+ filename: cleanModuleId(id),
3721
+ sourceMap: false
3722
+ });
3723
+ const stripped = await transformWithOxc(result.code, cleanModuleId(id) + '.tsx', {
3724
+ lang: 'tsx',
3725
+ sourcemap: false,
3726
+ target: 'esnext'
3727
+ });
3728
+ return {
3729
+ code: stripped.code,
3730
+ map: null
3731
+ };
3732
+ }
3733
+ }]
3500
3734
  }
3501
3735
  },
3502
3736
  ...(Object.keys(test).length ? {
@@ -3555,8 +3789,13 @@ function solidPlugin(options = {}) {
3555
3789
  resolve: projectRoot
3556
3790
  });
3557
3791
  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.');
3792
+ // `components: 'external'` is the acknowledgement that a composing
3793
+ // host (e.g. the Astro adapter or TanStack Start's Solid integration)
3794
+ // owns the document wiring itself — behavior is identical to `true`,
3795
+ // only this warning is skipped. Under SSR start mode it's redundant
3796
+ // but harmless (treated exactly as `true`).
3797
+ if (serverComponents && serverComponentsOption !== 'external' && !(options.start && options.ssr)) {
3798
+ 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
3799
  }
3561
3800
  needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false && !options.refresh?.disabled;
3562
3801
  },
@@ -3594,10 +3833,21 @@ function solidPlugin(options = {}) {
3594
3833
  return origSend(...args);
3595
3834
  };
3596
3835
  },
3597
- hotUpdate({
3836
+ async hotUpdate({
3837
+ file,
3598
3838
  modules,
3599
- file
3839
+ read
3600
3840
  }) {
3841
+ if (isTsrxModule(file) && this.environment.name === 'client') {
3842
+ updateTsrxCss(tsrxCss, file, await compileTsrxCss(await read(), file));
3843
+ const cssModule = this.environment.moduleGraph.getModuleById(resolvedTsrxCssModuleId(file));
3844
+ if (cssModule) {
3845
+ this.environment.moduleGraph.invalidateModule(cssModule);
3846
+ if (!modules.includes(cssModule)) modules = [...modules, cssModule];
3847
+ return modules;
3848
+ }
3849
+ }
3850
+
3601
3851
  // solid-refresh only injects HMR boundaries into client modules, so
3602
3852
  // non-client environments have no accept handlers. Without this, Vite
3603
3853
  // would see no boundaries and send full-reload messages that race with
@@ -3634,6 +3884,8 @@ function solidPlugin(options = {}) {
3634
3884
  }
3635
3885
  },
3636
3886
  resolveId(id) {
3887
+ const tsrxCssId = resolveTsrxCssModule(id);
3888
+ if (tsrxCssId) return tsrxCssId;
3637
3889
  if (id === VIRTUAL_MANIFEST_ID) return RESOLVED_VIRTUAL_MANIFEST_ID;
3638
3890
  },
3639
3891
  moduleParsed(info) {
@@ -3646,7 +3898,7 @@ function solidPlugin(options = {}) {
3646
3898
  for (const depId of info.dynamicallyImportedIds || []) {
3647
3899
  const cleanId = depId.split('?')[0];
3648
3900
  if (/node_modules/.test(cleanId) || cleanId.startsWith('\0')) continue;
3649
- if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue;
3901
+ if (!(/\.[mc]?[tj]sx?$/i.test(cleanId) || isTsrxModule(cleanId))) continue;
3650
3902
  if (emittedLazyChunks.has(depId)) continue;
3651
3903
  emittedLazyChunks.add(depId);
3652
3904
  emittedLazyChunkRefs.push(this.emitFile({
@@ -3657,6 +3909,8 @@ function solidPlugin(options = {}) {
3657
3909
  }
3658
3910
  },
3659
3911
  load(id) {
3912
+ const tsrxSource = tsrxCssSourceId(id);
3913
+ if (tsrxSource) return tsrxCss.get(tsrxSource) ?? '';
3660
3914
  if (id === RESOLVED_VIRTUAL_MANIFEST_ID) {
3661
3915
  if (!isBuild) {
3662
3916
  return devManifestCode(projectRoot, base, devServer ? devManifestBridgeUrl(devServer) : null);
@@ -3698,6 +3952,7 @@ function solidPlugin(options = {}) {
3698
3952
  }
3699
3953
  },
3700
3954
  async transform(source, id, transformOptions) {
3955
+ if (isTsrxCssModule(id)) return null;
3701
3956
  const isSsr = getEnvironmentConsumer(this.environment, transformOptions) === 'server';
3702
3957
  const currentFileExtension = getExtension(id);
3703
3958
  const extensionsToWatch = options.extensions || [];
@@ -3713,14 +3968,15 @@ function solidPlugin(options = {}) {
3713
3968
  // while the transform pipeline below works on the clean file path.
3714
3969
  const moduleId = id;
3715
3970
  id = id.replace(/\?.*$/, '');
3716
- if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
3971
+ const isTsrx = isTsrxModule(id);
3972
+ if (!(/\.[mc]?[tj]sx$/i.test(id) || isTsrx || allExtensions.includes(currentFileExtension))) {
3717
3973
  return null;
3718
3974
  }
3719
3975
  const inNodeModules = /node_modules/.test(id);
3720
3976
  const solidOptions = getSolidOptions(options, !!isSsr, replaceDev, isTestMode);
3721
3977
 
3722
3978
  // 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 => {
3979
+ const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || isTsrx || extensionsToWatch.some(extension => {
3724
3980
  if (typeof extension === 'string') {
3725
3981
  return extension.includes('tsx');
3726
3982
  }
@@ -3744,7 +4000,7 @@ function solidPlugin(options = {}) {
3744
4000
  // extension; custom extensions registered through `options.extensions`
3745
4001
  // are unknown to it, so borrow a standard one matching the configured
3746
4002
  // TypeScript-ness.
3747
- const nativeFilename = /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
4003
+ const nativeFilename = isTsrx || /\.(?:[mc]?[jt]s|[jt]sx)$/i.test(id) ? id : id + (shouldBeProcessedWithTypescript ? '.tsx' : '.jsx');
3748
4004
 
3749
4005
  // Shared native prelude for every mode: the lazy() module-URL pass,
3750
4006
  // then (dev/client/non-node_modules) the solid-refresh HMR pass, both
@@ -3754,6 +4010,97 @@ function solidPlugin(options = {}) {
3754
4010
  const compiler = await loadNativeCompiler();
3755
4011
  let code = source;
3756
4012
  const maps = [];
4013
+ if (isTsrx) {
4014
+ // Solid lowering preserves authored TypeScript annotations; secondary
4015
+ // passes therefore parse the generated module as TSX even though no
4016
+ // template syntax remains.
4017
+ const generatedFilename = id + '.tsx';
4018
+ const babelBaseOptions = {
4019
+ root: projectRoot,
4020
+ filename: id,
4021
+ sourceFileName: id,
4022
+ ast: false,
4023
+ sourceMaps: true,
4024
+ configFile: false,
4025
+ babelrc: false,
4026
+ parserOpts: {
4027
+ plugins
4028
+ }
4029
+ };
4030
+ let css = '';
4031
+ if (options.compiler !== 'babel') {
4032
+ const result = await compiler.transformAsync(code, {
4033
+ ...solidOptions,
4034
+ filename: id,
4035
+ sourceMap: true
4036
+ });
4037
+ code = result.code || '';
4038
+ css = nativeTsrxCss(result);
4039
+ maps.push(result.map);
4040
+ if (options.babel) {
4041
+ // The support pass cannot parse authored TSRX. On this route it
4042
+ // intentionally sees the lowered ordinary JavaScript instead.
4043
+ const supportOptions = mergeAndConcat(babelUserOptions, babelBaseOptions);
4044
+ // This pass sees native-lowered ordinary JavaScript, so do not
4045
+ // route it back through Babel's TSRX parser.
4046
+ supportOptions.filename = generatedFilename;
4047
+ const supportResult = await babel.transformAsync(code, supportOptions);
4048
+ if (!supportResult) return undefined;
4049
+ code = supportResult.code || '';
4050
+ maps.push(supportResult.map);
4051
+ }
4052
+ } else {
4053
+ const babelOptions = mergeAndConcat(babelUserOptions, {
4054
+ ...babelBaseOptions,
4055
+ plugins: [[solid, solidOptions]]
4056
+ });
4057
+ const result = await babel.transformAsync(code, babelOptions);
4058
+ if (!result) return undefined;
4059
+ code = result.code || '';
4060
+ css = babelTsrxCss(result);
4061
+ maps.push(result.map);
4062
+ }
4063
+ const lazyResult = await compiler.transformLazyAsync(code, {
4064
+ filename: generatedFilename,
4065
+ sourceMap: true
4066
+ });
4067
+ code = lazyResult.code;
4068
+ maps.push(lazyResult.map);
4069
+ if (needRefresh) {
4070
+ const refreshResult = await compiler.transformRefreshAsync(code, {
4071
+ filename: generatedFilename,
4072
+ bundler: 'vite',
4073
+ fixRender: true,
4074
+ ...(typeof options.refresh?.granular === 'boolean' ? {
4075
+ granular: options.refresh.granular
4076
+ } : {}),
4077
+ jsx: false,
4078
+ importSource: REFRESH_RUNTIME_SOURCE,
4079
+ sourceMap: true
4080
+ });
4081
+ code = refreshResult.code;
4082
+ maps.push(refreshResult.map);
4083
+ }
4084
+ code = injectSsrModuleId(await resolveLazyModuleUrls(this, code, id), moduleId, !!isSsr);
4085
+ let map = options.compiler === 'babel' ? combineSourcemaps(maps) : null;
4086
+ updateTsrxCss(tsrxCss, id, css);
4087
+ if (css) {
4088
+ code = prependTsrxCssImport(code, id);
4089
+ map = offsetSourceMapLine(map);
4090
+ }
4091
+ // Vite selects its TypeScript stripping by file extension. Since the
4092
+ // real module identity remains `.tsrx`, strip the annotations here
4093
+ // after Solid lowering instead of handing typed JavaScript to Rollup.
4094
+ const stripped = await transformWithOxc(code, generatedFilename, {
4095
+ lang: 'tsx',
4096
+ sourcemap: map != null,
4097
+ target: 'esnext'
4098
+ }, map ?? undefined);
4099
+ return {
4100
+ code: stripped.code,
4101
+ map: map == null ? null : stripped.map
4102
+ };
4103
+ }
3757
4104
  const lazyResult = await compiler.transformLazyAsync(code, {
3758
4105
  filename: nativeFilename,
3759
4106
  sourceMap: true
@@ -3836,20 +4183,24 @@ function solidPlugin(options = {}) {
3836
4183
  }
3837
4184
  };
3838
4185
 
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, {
4186
+ // Ordinary modules need the directive transform before JSX. Authored TSRX
4187
+ // cannot be parsed by that standalone pass, so its companion compiler runs
4188
+ // after mainPlugin has lowered the file to ordinary JavaScript while keeping
4189
+ // the original .tsrx id for stable server-function hashes.
4190
+ const serverFunctionPlugins = options.serverFunctions ? serverFunctions(options.serverFunctions === true ? {} : options.serverFunctions, {
3844
4191
  devMiddleware: true,
3845
4192
  externalDevServer,
4193
+ tsrxAfterSolid: true,
4194
+ tsrxSourceMap: options.compiler === 'babel',
3846
4195
  // With start mode on (either variant), the dev middleware dispatches
3847
4196
  // the endpoint through the SSR handler so user middleware and the
3848
4197
  // stub-backed request event front it exactly like page SSR.
3849
4198
  ...(startOptions ? {
3850
4199
  ssrHandler: SSR_HANDLER_ID
3851
4200
  } : {})
3852
- }), mainPlugin] : [boundaryModules(), mainPlugin];
4201
+ }) : [];
4202
+ const tsrxServerFunctionPlugin = serverFunctionPlugins.find(plugin => plugin.name === 'solid:server-functions/tsrx-compiler');
4203
+ const plugins = [boundaryModules(), ...serverFunctionPlugins.filter(plugin => plugin !== tsrxServerFunctionPlugin), mainPlugin, ...(tsrxServerFunctionPlugin ? [tsrxServerFunctionPlugin] : [])];
3853
4204
 
3854
4205
  // The `start` option opts into start-mode serving on top of the transforms;
3855
4206
  // the `ssr` boolean picks the mode (a bare `ssr: true` keeps the
@@ -3864,7 +4215,7 @@ function solidPlugin(options = {}) {
3864
4215
  serverComponents,
3865
4216
  ssr: !!options.ssr,
3866
4217
  styleFilter: filterDevStyles,
3867
- diagnostics: !!options.diagnostics,
4218
+ diagnostics: options.diagnostics ?? 'auto',
3868
4219
  onDocumentResolved(documentPath) {
3869
4220
  // Normalize to forward slashes to match Vite's transform ids.
3870
4221
  documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
@@ -3873,9 +4224,11 @@ function solidPlugin(options = {}) {
3873
4224
  }
3874
4225
 
3875
4226
  // 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());
4227
+ // plugin no-ops itself for builds and preview via `apply`, and in the
4228
+ // default auto mode additionally disables itself unless the app has
4229
+ // `@solidjs/diagnostics` installed).
4230
+ if (options.diagnostics !== false) {
4231
+ plugins.push(solidDiagnostics(options.diagnostics === true ? true : 'auto'));
3879
4232
  }
3880
4233
 
3881
4234
  // Builder-mode (environments API) client-before-server build ordering.