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/webworker-async-hooks-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

Warn at build time when the server build targets a worker runtime (`ssr.target: 'webworker'`) and the plugin generates server code — SSR or client start mode, or `serverFunctions` on its own. That code imports `@solidjs/web/storage`, the one Solid module that needs `node:async_hooks` (`AsyncLocalStorage` keeps the request event live across `await`s; there is no sync-scope fallback), so on a worker without Node compat the deploy failed with a bare `No such module "node:async_hooks"` (solidjs/solid#3597, Shopify Oxygen — whose Vite plugin sets the webworker target). The warning names the requirement and both fixes: a `compatibility_date` of 2026-08-04 or later (Cloudflare enables `nodejs_compat` by default from that date, and Oxygen accepts the same date), or the `nodejs_compat` flag. It is emitted once per server build, from the `ssr` environment's `buildStart` (the default builder resolves the config once per environment, so a config-time warning would repeat), and never in dev, where the ssr environment runs in Node. No new option; the condition is the resolved `ssr.target`.
2 changes: 1 addition & 1 deletion examples/start-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite",
"build": "vite build",
"serve": "NODE_ENV=production node server.js",
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs"
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs && node test/webworker-warning.mjs"
},
"devDependencies": {
"jsdom": "^26.1.0",
Expand Down
202 changes: 202 additions & 0 deletions examples/start-ssr/test/webworker-warning.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Build-level test for the worker-target warning (solidjs/solid#3597). The
// generated server code (the start-mode handler and the server-function
// handler module) imports `@solidjs/web/storage`, which needs
// `node:async_hooks`. On a worker runtime without Node compat that fails at
// deploy with a bare module-not-found, so a build whose ssr environment
// targets a worker (`ssr.target: 'webworker'`, what Shopify Oxygen's Vite
// plugin sets) must warn at build time naming the requirement and both fixes
// (compatibility_date >= 2026-08-04, or the nodejs_compat flag). Asserts:
// - a full SSR start-mode `vite build` (client + ssr environments) with
// `ssr.target: 'webworker'` warns EXACTLY once — the default builder
// resolves the config once per environment on top of its own pass, so a
// configResolved-time warning would print three times; the text names
// node:async_hooks, the compatibility date, the nodejs_compat flag, and
// links the issue,
// - the same build with the default node target is silent,
// - a client start-mode build with `serverFunctions` (dist/server kept for
// the endpoint) warns once too,
// - a `serverFunctions`-only ssr build (no `start`) warns — its handler
// module carries the same storage import,
// - a transform-only `ssr: true` build (no generated server code) is silent,
// - `vite dev` with the webworker target is silent — the dev server runs
// the ssr environment in Node, so the warning is build-only.
//
// Real in-process builds into the example's dist/ (no browser); dev via a
// non-listening createServer. Requires the plugin built (pnpm build at the
// repo root). Usage: node test/webworker-warning.mjs

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

const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const NEEDLE = 'targets a worker runtime';

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

function capturingLogger(warnings) {
const logger = createLogger('warn', { allowClearScreen: false });
const originalWarn = logger.warn;
logger.warn = (msg, opts) => {
warnings.push(String(msg));
originalWarn(msg, opts);
};
return logger;
}

function inlineConfig(solidOptions, ssr, extra = {}) {
return {
root: exampleDir,
configFile: false,
logLevel: 'warn',
ssr,
plugins: [solidPlugin(solidOptions)],
...extra,
};
}

/** One plain `vite build` — every environment, like the CLI. */
async function buildAppWith(solidOptions, ssr) {
const warnings = [];
const builder = await createBuilder(
inlineConfig(solidOptions, ssr, {
customLogger: capturingLogger(warnings),
}),
);
await builder.buildApp();
return warnings.filter((w) => w.includes(NEEDLE));
}

/**
* The ssr environment alone, with an explicit input: the shape of a host
* that owns its server entry and only uses the plugin's transforms (and,
* optionally, its server-function handler module).
*/
async function buildSsrWith(solidOptions, ssr) {
const warnings = [];
const builder = await createBuilder(
inlineConfig(solidOptions, ssr, {
customLogger: capturingLogger(warnings),
environments: {
ssr: {
build: {
outDir: 'dist/webworker-warning-ssr',
rollupOptions: { input: path.join(exampleDir, 'src/api.ts') },
},
},
},
}),
);
await builder.build(builder.environments.ssr);
return warnings.filter((w) => w.includes(NEEDLE));
}

async function devWith(solidOptions, ssr) {
const warnings = [];
const server = await createServer(
inlineConfig(solidOptions, ssr, {
customLogger: capturingLogger(warnings),
server: { middlewareMode: true, hmr: false, watch: null },
}),
);
try {
// Environments (and their plugin containers' buildStart) initialize in
// createServer; nothing has to listen or be requested.
await Promise.all(Object.values(server.environments).map((env) => env.init?.()));
} finally {
await server.close();
}
return warnings.filter((w) => w.includes(NEEDLE));
}

const startSsr = { ssr: true, start: {}, serverFunctions: true };

// ---- SSR start-mode build, webworker target: warns once, full text --------
{
const warnings = await buildAppWith(startSsr, { target: 'webworker' });
record(
'start-mode build with ssr.target webworker warns exactly once',
warnings.length === 1,
`${warnings.length} warnings`,
);
const text = warnings[0] ?? '';
record('warning names ssr.target = "webworker"', text.includes('ssr.target = "webworker"'), text);
record(
'warning names node:async_hooks / AsyncLocalStorage',
text.includes('node:async_hooks') && text.includes('AsyncLocalStorage'),
text,
);
record(
'warning gives the compatibility_date route (>= 2026-08-04)',
text.includes('compatibility_date') && text.includes('2026-08-04'),
text,
);
record('warning gives the nodejs_compat flag route', text.includes('nodejs_compat'), text);
record('warning mentions Shopify Oxygen', text.includes('Shopify Oxygen'), text);
record(
'warning links solidjs/solid#3597',
text.includes('https://github.com/solidjs/solid/issues/3597'),
text,
);
}

// ---- Same build, default node target: silent -------------------------------
{
const warnings = await buildAppWith(startSsr, {});
record('start-mode build with the node target does not warn', warnings.length === 0, warnings);
}

// ---- Client start mode + serverFunctions: dist/server handler, warns once --
{
const warnings = await buildAppWith(
{ start: {}, serverFunctions: true },
{ target: 'webworker' },
);
record(
'client start mode with serverFunctions and webworker target warns once',
warnings.length === 1,
`${warnings.length} warnings`,
);
}

// ---- serverFunctions only (no start): the handler module imports storage --
{
const warnings = await buildSsrWith(
{ ssr: true, serverFunctions: true },
{ target: 'webworker' },
);
record(
'serverFunctions-only ssr build with webworker target warns once',
warnings.length === 1,
`${warnings.length} warnings`,
);
}

// ---- Transform-only ssr: true (no generated server code): silent ----------
{
const warnings = await buildSsrWith({ ssr: true }, { target: 'webworker' });
record(
'transform-only ssr build with webworker target does not warn',
warnings.length === 0,
warnings,
);
}

// ---- Dev with the webworker target: build-only, so silent -----------------
{
const warnings = await devWith(startSsr, { target: 'webworker' });
record('vite dev with ssr.target webworker does not warn', warnings.length === 0, warnings);
}

const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} assertions passed`);
if (failed.length) {
console.log('\nFailures:');
for (const f of failed) console.log(` ${f.name} — ${f.detail}`);
process.exit(1);
}
41 changes: 40 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type {
} from './server-functions/index.js';
export type { StartOptions };
import path from 'path';
import type { FilterPattern, Plugin, ViteDevServer } from 'vite';
import type { FilterPattern, Logger, Plugin, ViteDevServer } from 'vite';
import {
createFilter,
defaultClientConditions,
Expand Down Expand Up @@ -947,6 +947,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
let isBuild = false;
let isSsrBuild = false;
let base = '/';
let logger: Logger | null = null;
// Set in configResolved when a worker-targeted server build would carry
// the plugin's `@solidjs/web/storage` import; emitted once from buildStart.
let workerTargetWarning = false;
let clientOutDir: string | null = null;
// The client environment's resolved build options, for the configured
// entry input. Read off the resolved config so the SSR half of a
Expand Down Expand Up @@ -1433,6 +1437,24 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
"`components: 'external'` to acknowledge it and silence this warning.",
);
}
// The generated server code — the start-mode handler and the
// server-function handler module — imports `@solidjs/web/storage`,
// the one Solid module that needs `node:async_hooks`
// (AsyncLocalStorage keeps the request event live across `await`s;
// there is no sync-scope fallback, solidjs/solid#3597). A worker-
// targeted server build (`ssr.target: 'webworker'`, e.g. Shopify
// Oxygen's Vite plugin) only fails at deploy with a bare
// module-not-found, so name the requirement and the fix at build
// time instead. Detected here, emitted from the ssr environment's
// buildStart so it lands once per server build: the default builder
// resolves the config once per environment on top of its own pass,
// so a configResolved warning would print three times per build.
// Build-only — dev runs the ssr environment in Node.
workerTargetWarning =
isBuild &&
!!(startOptions || options.serverFunctions) &&
config.ssr?.target === 'webworker';
logger = config.logger;
needHmr =
config.command === 'serve' &&
config.mode !== 'production' &&
Expand Down Expand Up @@ -1481,6 +1503,23 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
} as typeof hot.send;
},

buildStart() {
// `ssr.target` is a property of the `ssr` environment specifically
// (Vite reads it as `environment.name === 'ssr'` too), and that is the
// one environment whose bundle carries the storage import. Each
// environment gets its own plugin instance under the default builder,
// so this runs exactly once for the server build — and not at all for
// the client build or a `vite build --ssr`-less client-only build.
if (!workerTargetWarning || this.environment?.name !== 'ssr') return;
logger!.warn(
'[@solidjs/vite-plugin] The server build targets a worker runtime (ssr.target = "webworker"). ' +
"Solid's server runtime requires Node's async context (node:async_hooks / AsyncLocalStorage). " +
'On Cloudflare Workers and compatible runtimes (e.g. Shopify Oxygen) set a compatibility_date ' +
'of 2026-08-04 or later (nodejs_compat is on by default from that date), or enable the ' +
'nodejs_compat flag. See https://github.com/solidjs/solid/issues/3597',
);
},

async hotUpdate({ file, modules, read }) {
if (isTsrxModule(file) && this.environment.name === 'client') {
updateTsrxCss(tsrxCss, file, await compileTsrxCss(await read(), file));
Expand Down
Loading