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/dev-false-production-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

`dev: false` now serves Solid's production builds under `vite dev`, as documented (#62). The plugin already dropped its own `development` condition, but Vite's default `development|production` condition resolves to `development` in any non-production mode, so the client still pre-bundled `solid.dev.js` / `web.dev.js` and SSR loaded `server.dev.js`. With `dev: false` the plugin now rewrites that condition to `production` under `vite dev`; vitest runs and `vite build` keep Vite's default resolution. Vite conditions apply per environment, so other packages that ship a `development` export also get their production builds in that case. It also stops emitting HMR refresh wrappers and pre-bundling `solid-js/refresh`, because the production refresh runtime is an inert stub that only warns; edits fall back to a full reload. The diagnostics bridge also stays off with `dev: false` unless `observe` is on, since its channels exist only in the dev and observe builds.
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 && node test/scan.mjs && node test/server-components-define.mjs && node test/ts-module-url.mjs"
"test": "node test/run.mjs && node test/boundary.mjs && node test/scan.mjs && node test/server-components-define.mjs && node test/ts-module-url.mjs && node test/dev-false.mjs"
},
"devDependencies": {
"vite": "^8.2.1",
Expand Down
222 changes: 222 additions & 0 deletions examples/ssr/test/dev-false.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Regression for #62. With `dev: false`, `vite dev` must resolve Solid's
// production builds: the plugin drops its own `development` condition, but
// Vite's default `development|production` token still resolves to
// `development` in any non-production mode, so the client was pre-bundling
// `dist/solid.dev.js` and SSR was loading `dist/server.dev.js`.
//
// Asserts, for client and SSR:
// - `dev: false` during `vite dev` (`command === 'serve'`, mode not
// `test`) rewrites that token to `production` and `resolveId('solid-js')`
// ends at `dist/solid.js` / `dist/server.js`,
// - default serve (`dev` unset and `dev: true`), `mode: 'test'` (including
// `dev: false`, which must not opt tests into the rewrite), and
// `vite build` leave the token alone. Resolution then follows Vite:
// `development` under serve (test mode also injects `browser`, so SSR
// lands on the browser dev build) and `production` under build.
//
// Serve goes through `pluginContainer.resolveId`. When the client optimizer
// rewrites that id to a prebundle, the recorded `metadata.src` is the Solid
// file it resolved. Build uses Vite's `createIdResolver` (`resolveId` against
// the build config; `vite build` pins NODE_ENV=production). SSR externalizes
// `solid-js` during build, so that case sets `ssr.noExternal` to ask the same
// resolver for the file path. No browser.
// Requires the plugin built (pnpm build at the repo root).
// Usage: node test/dev-false.mjs

import { fileURLToPath } from 'node:url';
import { mkdtempSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createBuilder, createIdResolver, createServer } from 'vite';
import solidPlugin from '@solidjs/vite-plugin';

const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const TOKEN = 'development|production';
const CLIENT_PROD = '/dist/solid.js';
const CLIENT_DEV = '/dist/solid.dev.js';
const SSR_PROD = '/dist/server.js';
const SSR_DEV = '/dist/server.dev.js';

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

function inlineConfig(solidOptions, { mode } = {}) {
return {
root: exampleDir,
configFile: false,
logLevel: 'silent',
...(mode ? { mode } : {}),
plugins: [solidPlugin({ ssr: true, ...solidOptions })],
server: { middlewareMode: true, hmr: false, watch: null },
optimizeDeps: { noDiscovery: true },
};
}

function fileName(id) {
return String(id ?? '')
.split('?')[0]
.replace(/\\/g, '/');
}

/** Package file `resolveId` selected, following an optimized dep back to its src. */
function packagePath(environment, resolved) {
const id = fileName(typeof resolved === 'string' ? resolved : resolved?.id);
if (id.includes('/node_modules/solid-js/dist/')) return id;
const meta = environment.depsOptimizer?.metadata;
const info = meta?.optimized?.['solid-js'] ?? meta?.discovered?.['solid-js'];
return fileName(info?.src || '');
}

function conditionsFollow(conditions, force) {
return force
? conditions.includes('production') && !conditions.includes(TOKEN)
: conditions.includes(TOKEN) && !conditions.includes('production');
}

async function withNodeEnv(nodeEnv, fn) {
const before = process.env.NODE_ENV;
process.env.NODE_ENV = nodeEnv;
try {
return await fn();
} finally {
if (before === undefined) delete process.env.NODE_ENV;
else process.env.NODE_ENV = before;
}
}

async function resolveServe(solidOptions, { mode } = {}) {
// `vite dev` leaves NODE_ENV unset, and resolveConfig then pins development.
// A fresh cache so a previous case's prebundle can't answer this resolve.
return withNodeEnv('development', async () => {
const server = await createServer({
...inlineConfig(solidOptions, { mode }),
cacheDir: mkdtempSync(path.join(os.tmpdir(), 'solid-dev-false-')),
});
try {
const [client, ssr] = await Promise.all([
server.environments.client.pluginContainer.resolveId('solid-js'),
server.environments.ssr.pluginContainer.resolveId('solid-js'),
]);
return {
clientConditions: server.environments.client.config.resolve.conditions,
ssrConditions: server.environments.ssr.config.resolve.conditions,
client: packagePath(server.environments.client, client),
ssr: packagePath(server.environments.ssr, ssr),
};
} finally {
await server.close();
}
});
}

async function resolveBuild(solidOptions) {
// `vite build` pins NODE_ENV=production, which is what expands the token.
// `ssr.noExternal` only so the externalized SSR id is the file path; it
// does not rewrite conditions.
return withNodeEnv('production', async () => {
const builder = await createBuilder({
...inlineConfig(solidOptions),
ssr: { noExternal: ['solid-js'] },
});
const resolveId = createIdResolver(builder.config);
const [client, ssr] = await Promise.all([
resolveId(builder.environments.client, 'solid-js'),
resolveId(builder.environments.ssr, 'solid-js'),
]);
return {
clientConditions: builder.environments.client.config.resolve.conditions,
ssrConditions: builder.environments.ssr.config.resolve.conditions,
client: packagePath(builder.environments.client, client),
ssr: packagePath(builder.environments.ssr, ssr),
};
});
}

function check(name, result, { force, client, ssr }) {
const listed = `client [${result.clientConditions.join(', ')}] -> ${result.client}; ssr [${result.ssrConditions.join(', ')}] -> ${result.ssr}`;
record(
`${name}: client conditions ${force ? 'force production' : 'keep the token'}`,
conditionsFollow(result.clientConditions, force),
listed,
);
record(
`${name}: ssr conditions ${force ? 'force production' : 'keep the token'}`,
conditionsFollow(result.ssrConditions, force),
listed,
);
record(
`${name}: client resolveId ends at ${client}`,
result.client.endsWith(client),
result.client,
);
record(`${name}: ssr resolveId ends at ${ssr}`, result.ssr.endsWith(ssr), result.ssr);
}

const cases = [
{
name: 'vite dev with dev: false',
run: () => resolveServe({ dev: false }),
force: true,
client: CLIENT_PROD,
ssr: SSR_PROD,
},
{
name: 'vite dev (dev unset)',
run: () => resolveServe({}),
force: false,
client: CLIENT_DEV,
ssr: SSR_DEV,
},
{
name: 'vite dev with dev: true',
run: () => resolveServe({ dev: true }),
force: false,
client: CLIENT_DEV,
ssr: SSR_DEV,
},
{
name: "vite dev in mode 'test'",
run: () => resolveServe({}, { mode: 'test' }),
force: false,
client: CLIENT_DEV,
ssr: CLIENT_DEV,
},
{
name: "vite dev in mode 'test' with dev: false",
run: () => resolveServe({ dev: false }, { mode: 'test' }),
force: false,
client: CLIENT_DEV,
ssr: CLIENT_DEV,
},
{
name: 'vite build (dev unset)',
run: () => resolveBuild({}),
force: false,
client: CLIENT_PROD,
ssr: SSR_PROD,
},
{
name: 'vite build with dev: false',
run: () => resolveBuild({ dev: false }),
force: false,
client: CLIENT_PROD,
ssr: SSR_PROD,
},
];

for (const entry of cases) {
check(entry.name, await entry.run(), entry);
}

const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} dev-false assertions passed`);
if (failed.length) {
console.log('\nFailures:');
for (const f of failed) console.log(` ${f.name} — ${f.detail}`);
}
process.exit(failed.length ? 1 : 0);
24 changes: 20 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,9 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// `start: true` is sugar for the empty options bag — one start mode,
// two spellings — so normalize here and let everything downstream see a
// single shape (`false` behaves exactly like omission).
// The diagnostics channels exist only in Solid's dev and observe builds.
const diagnosticsEnabled =
options.diagnostics !== false && (options.dev !== false || options.observe === true);
const startOptions: StartOptions | null =
options.start === true ? {} : options.start || null;
const styleFilterOptions = startOptions?.css?.filter;
Expand Down Expand Up @@ -1177,6 +1180,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {

let needHmr = false;
let replaceDev = false;
let forceProduction = false;
let observe = false;
// Resolved absolute path of the start-mode document shell (normalized to
// forward slashes, matching Vite ids), reported back by the start plugin's
Expand Down Expand Up @@ -1382,6 +1386,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
observe = options.observe === true;
projectRoot = userConfig.root || projectRoot;
isTestMode = userConfig.mode === 'test';
forceProduction = options.dev === false && command === 'serve' && !isTestMode;
// Per-vitest-project posture: the client posture (browser conditions,
// dom codegen, jsdom default) is right for DOM component tests but
// wrong for server-runtime unit tests. A project that explicitly opts
Expand Down Expand Up @@ -1583,7 +1588,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// Dev refresh wrappers import the solid-js/refresh runtime in
// every mode; pre-bundle it up front so its discovery doesn't
// trigger a re-optimize + full reload on first use.
...(command === 'serve' && options.hot !== false && !options.refresh?.disabled
...(command === 'serve' &&
options.dev !== false &&
options.hot !== false &&
!options.refresh?.disabled
? [REFRESH_RUNTIME_SOURCE]
: []),
// The server-components client runtime is imported by the
Expand Down Expand Up @@ -1665,7 +1673,13 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// keep the default server conditions instead, so the framework's
// real server build resolves (isServer true).
...(isTestMode && !serverTestPosture && !opts.isSsrTargetWebworker ? ['browser'] : []),
...config.resolve.conditions,
// Vite's `development|production` resolves to `development` in any
// non-production mode, which would undo `dev: false` under `vite dev`.
...(forceProduction
? config.resolve.conditions.map((c) =>
c === 'development|production' ? 'production' : c,
)
: config.resolve.conditions),
];

// Vite seeds only the `client` environment from the top-level
Expand Down Expand Up @@ -1842,6 +1856,8 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
needHmr =
config.command === 'serve' &&
config.mode !== 'production' &&
// The production `solid-js/refresh` runtime is an inert stub.
options.dev !== false &&
options.hot !== false &&
!options.refresh?.disabled;
},
Expand Down Expand Up @@ -2499,7 +2515,7 @@ export default manifest;`;
serverComponents,
ssr: !!options.ssr,
styleFilter: filterDevStyles,
diagnostics: options.diagnostics ?? 'auto',
diagnostics: diagnosticsEnabled ? (options.diagnostics ?? 'auto') : false,
performanceTracks: performanceTracksOptions !== null,
onDocumentResolved(documentPath) {
// Normalize to forward slashes to match Vite's transform ids.
Expand All @@ -2516,7 +2532,7 @@ export default manifest;`;
// plugin no-ops itself for builds and preview via `apply`, and in the
// default auto mode additionally disables itself unless the app has
// `@solidjs/diagnostics` installed).
if (options.diagnostics !== false) {
if (diagnosticsEnabled) {
plugins.push(solidDiagnostics(options.diagnostics === true ? true : 'auto'));
}

Expand Down
Loading