Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/scan-jsx-all-environments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

The dependency scanner settings now reach every environment, not just `client` (#387). Vite seeds only the `client` environment from the top-level `optimizeDeps`, so an `ssr` (or other server) environment with discovery turned back on — as `@cloudflare/vite-plugin` does for workerd SSR — scanned Solid TSX with Rolldown's default React automatic runtime. The scan failed on an unresolvable `react/jsx-dev-runtime` and pre-bundling was skipped for that environment, which showed up as duplicate Solid instances in the app. `configEnvironment` now gives non-client environments the classic scanner JSX runtime (unless the app set its own per-environment `transform.jsx`), the `.tsrx` extension, and the tsrx scan plugin.
2 changes: 1 addition & 1 deletion examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "NODE_ENV=production node server.js",
"test": "node test/run.mjs && node test/boundary.mjs"
"test": "node test/run.mjs && node test/boundary.mjs && node test/scan.mjs"
},
"devDependencies": {
"vite": "^8.2.1",
Expand Down
121 changes: 121 additions & 0 deletions examples/ssr/test/scan.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Dependency-scan test for non-client environments (#387). Vite seeds only
// the `client` environment from the top-level `optimizeDeps`, so the scanner
// JSX setting the plugin returns from `config` never reached `ssr`: with
// discovery turned back on (as @cloudflare/vite-plugin does for workerd
// SSR), Rolldown scanned Solid TSX with React's automatic runtime and the
// scan failed on an unresolvable `react/jsx-dev-runtime`, skipping
// pre-bundling for that environment. Asserts, in one process:
// - config: the ssr environment resolves the classic scanner JSX runtime,
// the `.tsrx` extension and the tsrx scan plugin; a per-environment
// `transform.jsx` set by the app is left alone,
// - scan: a cold dev server with ssr discovery on (entries
// src/entry-server.tsx, whose graph includes JSX and import.meta.glob)
// completes both the client and the ssr scans without a failure banner.
//
// Requires the plugin built (pnpm build at the repo root). No browser.
// Usage: node test/scan.mjs

import { fileURLToPath } from 'node:url';
import { mkdtempSync, rmSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createServer, resolveConfig } from 'vite';

const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));

const results = [];
function record(name, ok, detail = '') {
results.push({ name, ok, detail });
console.log(` [scan] ${ok ? 'PASS' : 'FAIL'} ${name}${detail && !ok ? ` — ${detail}` : ''}`);
}

const ssrDiscovery = { noDiscovery: false, entries: ['src/entry-server.tsx'] };

// ---- Resolved config -------------------------------------------------------
{
const config = await resolveConfig(
{ root: exampleDir, logLevel: 'silent', environments: { ssr: { optimizeDeps: ssrDiscovery } } },
'serve',
);
const ssr = config.environments.ssr.optimizeDeps;
record(
'ssr scanner uses the classic JSX runtime',
ssr.rolldownOptions?.transform?.jsx?.runtime === 'classic',
JSON.stringify(ssr.rolldownOptions?.transform),
);
record('ssr scanner crawls .tsrx', !!ssr.extensions?.includes('.tsrx'), JSON.stringify(ssr.extensions));
const plugins = [ssr.rolldownOptions?.plugins].flat(Infinity);
record(
'ssr scanner registers the tsrx scan plugin',
plugins.some((p) => p?.name === 'solid:tsrx-dep-scan'),
);
record(
'client scanner keeps the classic JSX runtime',
config.environments.client.optimizeDeps.rolldownOptions?.transform?.jsx?.runtime === 'classic',
);
}
{
const config = await resolveConfig(
{
root: exampleDir,
logLevel: 'silent',
environments: {
ssr: { optimizeDeps: { rolldownOptions: { transform: { jsx: { runtime: 'automatic' } } } } },
},
},
'serve',
);
record(
'an app-set per-environment scanner JSX option wins',
config.environments.ssr.optimizeDeps.rolldownOptions?.transform?.jsx?.runtime === 'automatic',
);
}

// ---- Cold scans ------------------------------------------------------------
{
const cacheDir = mkdtempSync(path.join(os.tmpdir(), 'solid-ssr-scan-'));
const failed = {};
const server = await createServer({
root: exampleDir,
cacheDir,
logLevel: 'silent',
server: { port: 0 },
environments: { ssr: { optimizeDeps: ssrDiscovery } },
});
try {
for (const [name, env] of Object.entries(server.environments)) {
const error = env.logger.error;
env.logger = {
...env.logger,
error(msg, opts) {
if (String(msg).includes('Failed to run dependency scan')) failed[name] = String(msg);
return error.call(this, msg, opts);
},
};
}
await server.listen();
for (const name of ['client', 'ssr']) {
const optimizer = server.environments[name]?.depsOptimizer;
if (!optimizer || optimizer.options.noDiscovery) {
record(`${name} scan ran`, false, 'discovery is off');
continue;
}
for (let i = 0; i < 100 && !optimizer.scanProcessing; i++) {
await new Promise((r) => setTimeout(r, 50));
}
await optimizer.scanProcessing;
record(`${name} scan completed (no failure banner)`, !failed[name], failed[name]?.slice(0, 300));
}
} finally {
await server.close();
rmSync(cacheDir, { recursive: true, force: true });
}
}

const failures = results.filter((r) => !r.ok);
console.log(`\n${results.length - failures.length}/${results.length} scan assertions passed`);
if (failures.length) {
console.log('\nFailures:');
for (const f of failures) console.log(` ${f.name} — ${f.detail}`);
}
process.exit(failures.length ? 1 : 0);
62 changes: 42 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,28 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
let projectRoot = process.cwd();
let isTestMode = false;
let serverTestPosture = false;

// Lets the dependency scanner crawl `.tsrx` imports. Registered on every
// environment's `optimizeDeps` (top-level for client, configEnvironment
// for the rest).
const tsrxDepScanPlugin = {
name: 'solid:tsrx-dep-scan',
async transform(source: string, id: string) {
if (!isTsrxModule(id) || isTsrxCssModule(id)) return null;
const compiler = await loadNativeCompiler();
const result = await compiler.transformAsync(source, {
...getSolidOptions(options, false, replaceDev, observe, isTestMode),
filename: cleanModuleId(id),
sourceMap: false,
});
const stripped = await transformWithOxc(result.code, cleanModuleId(id) + '.tsx', {
lang: 'tsx',
sourcemap: false,
target: 'esnext',
});
return { code: stripped.code, map: null };
},
};
let isBuild = false;
let isSsrBuild = false;
let base = '/';
Expand Down Expand Up @@ -1580,26 +1602,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// Keep Solid TSX from injecting React's automatic runtime during scanning.
rolldownOptions: {
transform: { jsx: { runtime: 'classic' as const } },
plugins: [
{
name: 'solid:tsrx-dep-scan',
async transform(source: string, id: string) {
if (!isTsrxModule(id) || isTsrxCssModule(id)) return null;
const compiler = await loadNativeCompiler();
const result = await compiler.transformAsync(source, {
...getSolidOptions(options, false, replaceDev, observe, isTestMode),
filename: cleanModuleId(id),
sourceMap: false,
});
const stripped = await transformWithOxc(result.code, cleanModuleId(id) + '.tsx', {
lang: 'tsx',
sourcemap: false,
target: 'esnext',
});
return { code: stripped.code, map: null };
},
},
],
plugins: [tsrxDepScanPlugin],
},
},
...(Object.keys(test).length ? { test } : {}),
Expand Down Expand Up @@ -1633,6 +1636,25 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
...config.resolve.conditions,
];

// Vite seeds only the `client` environment from the top-level
// `optimizeDeps` set in `config`, so every other environment would
// scan Solid TSX with Rolldown's default (React's automatic runtime)
// and fail on an unresolvable `react/jsx-dev-runtime`. Server
// environments default to `noDiscovery: true`, but hosts that run SSR
// outside Node turn discovery back on (@cloudflare/vite-plugin), #387.
if (name !== 'client') {
const optimizeDeps = (config.optimizeDeps ??= {});
if (!optimizeDeps.extensions?.includes('.tsrx')) {
optimizeDeps.extensions = [...(optimizeDeps.extensions ?? []), '.tsrx'];
}
const rolldownOptions = (optimizeDeps.rolldownOptions ??= {});
const transform = (rolldownOptions.transform ??= {});
transform.jsx ??= { runtime: 'classic' };
rolldownOptions.plugins = rolldownOptions.plugins
? [rolldownOptions.plugins, tsrxDepScanPlugin]
: [tsrxDepScanPlugin];
}

// `resolve.conditions` above only governs modules Vite inlines.
// Externalized server deps are resolved by `fetchModule` with
// `resolve.externalConditions` (default `['node', 'module-sync']`) and
Expand Down
Loading