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
17 changes: 17 additions & 0 deletions .changeset/dev-toolbar-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@solidjs/vite-plugin': minor
---

New `start.devtools` option: a development toolbar with runtime errors and a
server function inspector, backed by the new optional-peer package
`@solidjs/start-devtools`. By default the toolbar turns on in `vite dev`
whenever the package resolves (install it as a dev dependency) and stays off
otherwise; `start: { devtools: true }` makes the package required (a missing
install becomes an error) and `start: { devtools: false }` opts out entirely.
Generated client entries wrap the app in the toolbar's `DevToolbar` component,
authored client entries get an injected mount import instead, and either way
the wiring is dev-serve-only codegen — production builds and previews contain
none of it. The package itself is resolved from the app graph first and from
the plugin's own location as a fallback, and the virtual toolbar modules
delegate their imports to that captured resolution, so pnpm-isolated installs
work without the package being hoisted to the app root.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,19 @@ same server functions.
The object form carries the options (`start: true` is pure sugar for
`start: {}` — both mean the identical start mode with defaults, and
`false`/absent means off): `app`, `document`, `entryServer`, `entryClient`,
`middleware`, `setup`, `env`, `errorBoundary`, `css`, `external`, all
documented below.
`middleware`, `setup`, `env`, `devtools`, `errorBoundary`, `css`, `external`,
all documented below.

Install `@solidjs/start-devtools` as a development dependency to add the
development toolbar with runtime errors and server function calls:

```sh
pnpm add -D @solidjs/start-devtools@next
```

Start mode detects the package automatically. Set `start: { devtools: true }`
to require it or `start: { devtools: false }` to disable it. The package is an
optional peer and the toolbar is not included in production builds.

```tsx
// src/App.tsx — the entire app: a plain content component
Expand Down
36 changes: 36 additions & 0 deletions examples/start-client/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,31 @@ async function runBrowserChecks(mode, origin) {
`getComputedStyle(document.querySelector("#title")).color === ${JSON.stringify(APP_CSS_COLOR)}`,
),
);
record(
mode,
'browser',
mode === 'dev' ? 'development toolbar mounted' : 'development toolbar omitted',
mode === 'dev'
? await cdp.waitFor('document.querySelector("[data-solid-dev-toolbar]")')
: !(await cdp.evalJs('document.querySelector("[data-solid-dev-toolbar]")')),
);
if (mode === 'dev') {
const message = 'DEV_TOOLBAR_TEST_ERROR';
await cdp.evalJs(
`window.dispatchEvent(new ErrorEvent("error", { error: new Error(${JSON.stringify(message)}) }))`,
);
record(
mode,
'browser',
'runtime error shown in toolbar',
await cdp.waitFor(
`document.querySelector("[data-solid-error-viewer-error-info-message]")?.textContent === ${JSON.stringify(message)}`,
),
);
for (let i = cdp.exceptions.length - 1; i >= 0; i--) {
if (cdp.exceptions[i].includes(message)) cdp.exceptions.splice(i, 1);
}
}

// Deep-link boot: the same shell must boot the app on a non-root path.
await cdp.send('Page.navigate', { url: origin + '/deep/link' });
Expand Down Expand Up @@ -288,6 +313,17 @@ async function devMode() {
"entry graph CSS inlined (App.css style tag)",
/<style[^>]*data-vite-dev-id="[^"]*App\.css"/.test(html),
);
const entry = await fetch(origin + '/@id/virtual:solid-ssr-entry-client.tsx').then((res) =>
res.text(),
);
record('dev', 'entry', 'toolbar wraps the generated app', entry.includes('DevToolbar'));
const devtools = await fetch(origin + '/@id/virtual:solid-devtools').then((res) => res.text());
record(
'dev',
'entry',
'server function observer connected',
devtools.includes('observeServerFunctionCalls'),
);

await runBrowserChecks('dev', origin);

Expand Down
104 changes: 101 additions & 3 deletions examples/start-ssr/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ async function runHmrChecks(mode, cdp, origin, { expectCompiler } = {}) {
}
}

async function runBrowserChecks(mode, origin, { hmr, devCss, expectCompiler } = {}) {
async function runBrowserChecks(mode, origin, { hmr, devCss, expectCompiler, devtools } = {}) {
const chrome = startProcess(CHROME, [
'--headless=new',
`--remote-debugging-port=${CDP_PORT}`,
Expand Down Expand Up @@ -743,6 +743,25 @@ async function runBrowserChecks(mode, origin, { hmr, devCss, expectCompiler } =
);
}

// `devtools: true` (dev, auto-detected package): the toolbar mounts in
// the live DOM. `devtools: false` (prod): it must not exist. Undefined
// skips the check — other dev-server modes don't re-assert the default.
if (devtools === true) {
record(
mode,
'devtools',
'development toolbar mounted in the DOM',
await cdp.waitFor('document.querySelector("[data-solid-dev-toolbar]") !== null'),
);
} else if (devtools === false) {
record(
mode,
'devtools',
'no development toolbar in the DOM',
(await cdp.evalJs('document.querySelector("[data-solid-dev-toolbar]")')) === null,
);
}

const errs = cdp.exceptions.filter((e) => !/favicon/i.test(e));
record(mode, 'browser', 'no page errors', errs.length === 0, errs.join(' | '));

Expand Down Expand Up @@ -872,11 +891,34 @@ async function runDevMode() {
!generatedEntry.includes('@solidjs/web/frames'),
);

// Devtools default-on: the workspace's @solidjs/start-devtools install is
// auto-detected, so the generated client entry must wrap the app in the
// toolbar and pull the virtual devtools module (whose transformed source
// wires the server-function observer).
record(
mode,
'devtools',
'generated client entry wraps the app in DevToolbar',
generatedEntry.includes('DevToolbar') && generatedEntry.includes('virtual:solid-devtools'),
);
const devtoolsModule = await (await fetch(origin + '/@id/virtual:solid-devtools')).text();
record(
mode,
'devtools',
'server function observer wired in the devtools module',
devtoolsModule.includes('observeServerFunctionCalls'),
);

await runHttpChecks(mode, origin);

await runLazyAssetChecks(mode, origin, { dev: true });

await runBrowserChecks(mode, origin, { hmr: true, devCss: true, expectCompiler: 'native' });
await runBrowserChecks(mode, origin, {
hmr: true,
devCss: true,
expectCompiler: 'native',
devtools: true,
});

// ---- Cold-start dep scan (boundary-guard false positive) -------------
// Counterpart: the ssr example's boundary.mjs proves the guard still
Expand All @@ -894,6 +936,41 @@ async function runDevMode() {
'dependency pre-bundling wrote its metadata',
existsSync(path.join(exampleDir, 'node_modules/.vite/deps/_metadata.json')),
);

// ---- Devtools opt-out sub-run (SSR_DEVTOOLS=0 → devtools: false) -----
// The package still resolves, but the option must win: no toolbar wrap in
// the generated entry and the virtual devtools module stays unclaimed.
const offPort = 3176;
const offOrigin = `http://localhost:${offPort}`;
const offServer = startProcess(
'pnpm',
['exec', 'vite', '--port', String(offPort), '--strictPort'],
{ cwd: exampleDir, env: { ...process.env, SSR_DEVTOOLS: '0' } },
);
try {
await waitForHttp(offOrigin + '/src/api.ts', 30000);
const offEntry = await (
await fetch(offOrigin + '/@id/virtual:solid-ssr-entry-client.tsx')
).text();
record(
mode,
'devtools',
'devtools: false strips the toolbar from the generated entry',
!offEntry.includes('DevToolbar') && !offEntry.includes('virtual:solid-devtools'),
);
const offModule = await fetch(offOrigin + '/@id/virtual:solid-devtools');
record(
mode,
'devtools',
'devtools: false leaves the virtual devtools module unserved',
!offModule.ok,
`status ${offModule.status}`,
);
} finally {
try {
process.kill(-offServer.pid, 'SIGTERM');
} catch {}
}
} catch (e) {
record(
mode,
Expand Down Expand Up @@ -1001,6 +1078,27 @@ async function runProdMode() {
'no server-components transform in server bundle (option off)',
!serverBundle.includes('@solidjs/web/frames') && !serverBundle.includes('frameTransformResult'),
);
// Dev-serve-only guarantee for `start.devtools`: production output carries
// none of it — client assets checked via the toolbar's minification-proof
// DOM marker and the package name, the (unminified) server bundle via the
// package name and the virtual module id.
const devtoolsLeaks = readdirSync(assetsDir).filter((f) => {
const source = readFileSync(path.join(assetsDir, f), 'utf-8');
return source.includes('data-solid-dev-toolbar') || source.includes('start-devtools');
});
record(
mode,
'dce',
'no devtools code in client assets',
devtoolsLeaks.length === 0,
devtoolsLeaks.join(', '),
);
record(
mode,
'dce',
'no devtools wiring in server bundle',
!serverBundle.includes('start-devtools') && !serverBundle.includes('virtual:solid-devtools'),
);

const server = startProcess('node', ['server.js'], {
cwd: exampleDir,
Expand Down Expand Up @@ -1076,7 +1174,7 @@ async function runProdMode() {

await runLazyAssetChecks(mode, origin, { dev: false });

await runBrowserChecks(mode, origin);
await runBrowserChecks(mode, origin, { devtools: false });
} catch (e) {
record(
mode,
Expand Down
8 changes: 8 additions & 0 deletions examples/start-ssr/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import solidPlugin from '@solidjs/vite-plugin';
// - SERVER_FN_DEV_MIDDLEWARE=0 disables the built-in dev middleware via
// `serverFunctions.devMiddleware` (no-middleware mode) — endpoint dispatch
// becomes the host's job, like a Cloudflare-style environment plugin.
// - SSR_DEVTOOLS=0 disables the development toolbar via `start.devtools`
// (dev-mode off sub-run); by default the workspace's @solidjs/start-devtools
// install is auto-detected and the toolbar mounts in dev.
// - BUILD_SSR_FIRST installs an adversarial `builder.buildApp` that builds
// the ssr environment before the client (builder-order mode) — mimicking
// host orchestrators like @cloudflare/vite-plugin; the plugin's
Expand Down Expand Up @@ -137,6 +140,11 @@ export default defineConfig({
? { css: { filter: { include: /App\.tsx$/, exclude: /App\.tsx$/ } } }
: {}),
...(process.env.CSS_FILTER === 'default' ? { app: 'src/CssLibApp.tsx' } : {}),
// SSR_DEVTOOLS=0 (dev-mode sub-run) opts out of the development
// toolbar via `start.devtools`. Without the knob the workspace's
// @solidjs/start-devtools install is auto-detected, so plain dev
// runs double as coverage for the default-on wiring.
...(process.env.SSR_DEVTOOLS === '0' ? { devtools: false } : {}),
// SSR_MIDDLEWARE=1 (middleware/preview modes): a fetch-style
// chain fronting every dispatch path — page SSR, /_server,
// preview — with getRequestEvent() live inside it.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@skypack/package-check": "^0.2.2",
"@solidjs/start-devtools": "^1.0.0-next.0",
"@types/node": "^18.18.4",
"cypress": "^14.0.0",
"cypress-visual-regression": "^5.2.2",
Expand All @@ -85,12 +86,16 @@
"vite": "^7.0.0"
},
"peerDependencies": {
"@solidjs/start-devtools": "^1.0.0-next.0",
"@solidjs/web": "^2.0.0-rc.0",
"@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*",
"solid-js": "^2.0.0-rc.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"@solidjs/start-devtools": {
"optional": true
},
"@testing-library/jest-dom": {
"optional": true
}
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ignoredBuiltDependencies:

minimumReleaseAgeExclude:
- '@solidjs/signals@2.0.0-beta.30 || 2.0.0-beta.31 || 2.0.0-beta.32 || 2.0.0-rc.0'
- '@solidjs/start-devtools@1.0.0-next.0'
- '@solidjs/web@2.0.0-beta.30 || 2.0.0-beta.31 || 2.0.0-beta.32 || 2.0.0-rc.0'
- babel-preset-solid@2.0.0-beta.30 || 2.0.0-beta.31 || 2.0.0-beta.32 || 2.0.0-rc.0
- solid-js@2.0.0-beta.30 || 2.0.0-beta.31 || 2.0.0-beta.32 || 2.0.0-rc.0
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const external = [
'babel-preset-solid',
'merge-anything',
'vitefu',
'vite'
'vite',
];

/**
Expand Down
21 changes: 21 additions & 0 deletions src/devtools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const DEVTOOLS_PACKAGE = '@solidjs/start-devtools';
export const DEVTOOLS_ID = 'virtual:solid-devtools';
export const DEVTOOLS_MOUNT_ID = 'virtual:solid-devtools/mount';

export function devtoolsModuleCode(): string {
return [
`import * as serverFunctions from '@solidjs/web/server-functions';`,
`import { DevToolbar, pushServerFunctionCall } from '${DEVTOOLS_PACKAGE}';`,
`const observe = Reflect.get(serverFunctions, 'observeServerFunctionCalls');`,
`if (typeof observe === 'function') observe(pushServerFunctionCall);`,
`export { DevToolbar };`,
].join('\n');
}

export function devtoolsMountModuleCode(): string {
return [
`import ${JSON.stringify(DEVTOOLS_ID)};`,
`import { mountDevToolbar } from '${DEVTOOLS_PACKAGE}';`,
`mountDevToolbar();`,
].join('\n');
}
Loading
Loading