Skip to content

Commit 03b590f

Browse files
ianletclaude
andcommitted
fix(qwik-city): support Deno as package manager for production builds
When using Deno to manage dependencies and run builds (e.g. `deno task build.client` + `deno task build.server`), two issues prevented a working production build: 1. The Vite plugin's temp file mechanism for passing the client manifest to the server build was gated on node/bun only, so Deno builds got null manifests and Code(31) errors. 2. SSG static generation stubbed out with Deno.exit(1) instead of delegating to the Node implementation, which works under Deno's Node compatibility layer. - Extract `hasNodeCompat()` predicate to include deno in all 4 env gates in the Vite plugin - Route Deno to the Node SSG module at runtime (same as Bun) - Remove dead static/deno stub and its build step Closes #8364 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20dc320 commit 03b590f

File tree

6 files changed

+62
-32
lines changed

6 files changed

+62
-32
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@builder.io/qwik': patch
3+
'@builder.io/qwik-city': patch
4+
---
5+
6+
FIX: support Deno as package manager for production builds. The Vite plugin now recognizes Deno as a Node-compatible runtime for manifest passing, and SSG delegates to the Node implementation instead of stubbing out.

packages/qwik-city/src/static/deno/index.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/qwik-city/src/static/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ export async function generate(opts: StaticGenerateOptions) {
2121
export type { StaticGenerateOptions, StaticGenerateRenderOptions, StaticGenerateResult };
2222

2323
function getEntryModulePath() {
24-
if (isDeno()) {
25-
return './deno.mjs';
26-
}
27-
if (isNode() || isBun()) {
24+
if (isNode() || isBun() || isDeno()) {
2825
if (isCjs()) {
2926
return './node.cjs';
3027
}

packages/qwik/src/optimizer/src/plugins/vite.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
OptimizerOptions,
88
OptimizerSystem,
99
QwikManifest,
10+
SystemEnvironment,
1011
TransformModule,
1112
} from '../types';
1213
import { type BundleGraphAdder } from './bundle-graph';
@@ -191,7 +192,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
191192
pluginOpts.input = viteConfig.build?.lib.entry;
192193
}
193194
}
194-
if (sys.env === 'node' || sys.env === 'bun') {
195+
if (hasNodeCompat(sys.env)) {
195196
const fs: typeof import('fs') = await sys.dynamicImport('node:fs');
196197

197198
try {
@@ -551,7 +552,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
551552
);
552553

553554
const sys = qwikPlugin.getSys();
554-
if (tmpClientManifestPath && (sys.env === 'node' || sys.env === 'bun')) {
555+
if (tmpClientManifestPath && hasNodeCompat(sys.env)) {
555556
// Client build should write the manifest to a tmp dir
556557
const fs: typeof import('fs') = await sys.dynamicImport('node:fs');
557558
await fs.promises.writeFile(tmpClientManifestPath, clientManifestStr);
@@ -566,7 +567,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
566567
// ssr build
567568

568569
const sys = qwikPlugin.getSys();
569-
if (sys.env === 'node' || sys.env === 'bun') {
570+
if (hasNodeCompat(sys.env)) {
570571
const outputs = Object.keys(rollupBundle);
571572

572573
// In order to simplify executing the server script with a common script
@@ -758,7 +759,7 @@ const findQwikRoots = async (
758759
packageJsonDir: string
759760
): Promise<QwikPackages[]> => {
760761
const paths = new Map<string, string>();
761-
if (sys.env === 'node' || sys.env === 'bun') {
762+
if (hasNodeCompat(sys.env)) {
762763
const fs: typeof import('fs') = await sys.dynamicImport('node:fs');
763764
let prevPackageJsonDir: string | undefined;
764765
do {
@@ -819,6 +820,9 @@ export const isNotNullable = <T>(v: T): v is NonNullable<T> => {
819820
return v != null;
820821
};
821822

823+
/** Whether the runtime supports Node standard library APIs (node:fs, node:os, etc.). */
824+
const hasNodeCompat = (env: SystemEnvironment) => env === 'node' || env === 'bun' || env === 'deno';
825+
822826
const VITE_CLIENT_MODULE = `@builder.io/qwik/vite-client`;
823827
const CLIENT_DEV_INPUT = 'entry.dev';
824828

packages/qwik/src/optimizer/src/plugins/vite.unit.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ const chunkInfoMocks = [
2222
},
2323
] as Rollup.PreRenderedChunk[];
2424

25-
function mockOptimizerOptions(): OptimizerOptions {
25+
function mockOptimizerOptions(env: 'node' | 'deno' = 'node'): OptimizerOptions {
2626
return {
2727
sys: {
2828
cwd: () => process.cwd(),
29-
env: 'node',
29+
env,
3030
os: process.platform,
3131
dynamicImport: async (path) => import(path),
3232
strictDynamicImport: async (path) => import(path),
@@ -417,6 +417,51 @@ test('should use build.outDir config when assetsDir is _astro', async () => {
417417
assert.equal(c.build.outDir, normalizePath(resolve(cwd, `dist/`)));
418418
});
419419

420+
test('command: build, mode: production (deno)', async () => {
421+
const initOpts = {
422+
optimizerOptions: mockOptimizerOptions('deno'),
423+
};
424+
const plugin = getPlugin(initOpts);
425+
const c = (await plugin.config.call(
426+
configHookPluginContext,
427+
{},
428+
{ command: 'build', mode: 'production' }
429+
))!;
430+
const opts = await plugin.api?.getOptions();
431+
432+
assert.deepEqual(opts.target, 'client');
433+
assert.deepEqual(opts.buildMode, 'production');
434+
assert.deepEqual(opts.resolveQwikBuild, true);
435+
436+
// Deno should produce the same config shape as Node
437+
const build = c.build!;
438+
assert.deepEqual(build.outDir, normalizePath(resolve(cwd, 'dist')));
439+
assert.deepEqual(build.dynamicImportVarsOptions?.exclude, [/./]);
440+
assert.deepEqual(build.ssr, undefined);
441+
});
442+
443+
test('command: build, --ssr entry.server.tsx (deno)', async () => {
444+
const initOpts = {
445+
optimizerOptions: mockOptimizerOptions('deno'),
446+
};
447+
const plugin = getPlugin(initOpts);
448+
const c = (await plugin.config.call(
449+
configHookPluginContext,
450+
{ build: { ssr: resolve(cwd, 'src', 'entry.server.tsx') } },
451+
{ command: 'build', mode: '' }
452+
))!;
453+
const opts = await plugin.api?.getOptions();
454+
455+
assert.deepEqual(opts.target, 'ssr');
456+
assert.deepEqual(opts.buildMode, 'development');
457+
assert.deepEqual(opts.entryStrategy, { type: 'hoist' });
458+
459+
const build = c.build!;
460+
assert.deepEqual(build.outDir, normalizePath(resolve(cwd, 'server')));
461+
assert.deepEqual(build.ssr, true);
462+
assert.deepEqual(c.publicDir, false);
463+
});
464+
420465
test('command: build, --mode lib', async () => {
421466
const initOpts = {
422467
optimizerOptions: mockOptimizerOptions(),

scripts/qwik-city.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export async function buildQwikCity(config: BuildConfig) {
3535
buildMiddlewareFirebase(config),
3636
buildStatic(config),
3737
buildStaticNode(config),
38-
buildStaticDeno(config),
3938
]);
4039

4140
await buildRuntime(config);
@@ -637,19 +636,6 @@ async function buildStatic(config: BuildConfig) {
637636
});
638637
}
639638

640-
async function buildStaticDeno(config: BuildConfig) {
641-
const entryPoints = [join(config.srcQwikCityDir, 'static', 'deno', 'index.ts')];
642-
643-
await build({
644-
entryPoints,
645-
outfile: join(config.distQwikCityPkgDir, 'static', 'deno.mjs'),
646-
bundle: true,
647-
platform: 'neutral',
648-
format: 'esm',
649-
plugins: [resolveRequestHandler('../middleware/request-handler/index.mjs')],
650-
});
651-
}
652-
653639
async function buildStaticNode(config: BuildConfig) {
654640
const entryPoints = [join(config.srcQwikCityDir, 'static', 'node', 'index.ts')];
655641

0 commit comments

Comments
 (0)