diff --git a/.changeset/deno-production-builds.md b/.changeset/deno-production-builds.md new file mode 100644 index 00000000000..8b7591b14ad --- /dev/null +++ b/.changeset/deno-production-builds.md @@ -0,0 +1,6 @@ +--- +'@builder.io/qwik': patch +'@builder.io/qwik-city': patch +--- + +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. diff --git a/packages/qwik-city/src/static/deno/index.ts b/packages/qwik-city/src/static/deno/index.ts deleted file mode 100644 index 1c62f3b6972..00000000000 --- a/packages/qwik-city/src/static/deno/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { StaticGenerateOptions } from '../types'; - -export async function generate(_opts: StaticGenerateOptions) { - console.error(`Deno not implemented`); - Deno.exit(1); -} - -declare const Deno: any; diff --git a/packages/qwik-city/src/static/index.ts b/packages/qwik-city/src/static/index.ts index 2da9bedae91..60b822dbd77 100644 --- a/packages/qwik-city/src/static/index.ts +++ b/packages/qwik-city/src/static/index.ts @@ -21,10 +21,7 @@ export async function generate(opts: StaticGenerateOptions) { export type { StaticGenerateOptions, StaticGenerateRenderOptions, StaticGenerateResult }; function getEntryModulePath() { - if (isDeno()) { - return './deno.mjs'; - } - if (isNode() || isBun()) { + if (isNode() || isBun() || isDeno()) { if (isCjs()) { return './node.cjs'; } diff --git a/packages/qwik/src/optimizer/src/plugins/vite.ts b/packages/qwik/src/optimizer/src/plugins/vite.ts index 3aed31f253e..c089eaf95bc 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite.ts @@ -7,6 +7,7 @@ import type { OptimizerOptions, OptimizerSystem, QwikManifest, + SystemEnvironment, TransformModule, } from '../types'; import { type BundleGraphAdder } from './bundle-graph'; @@ -191,7 +192,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { pluginOpts.input = viteConfig.build?.lib.entry; } } - if (sys.env === 'node' || sys.env === 'bun') { + if (hasNodeCompat(sys.env)) { const fs: typeof import('fs') = await sys.dynamicImport('node:fs'); try { @@ -551,7 +552,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { ); const sys = qwikPlugin.getSys(); - if (tmpClientManifestPath && (sys.env === 'node' || sys.env === 'bun')) { + if (tmpClientManifestPath && hasNodeCompat(sys.env)) { // Client build should write the manifest to a tmp dir const fs: typeof import('fs') = await sys.dynamicImport('node:fs'); await fs.promises.writeFile(tmpClientManifestPath, clientManifestStr); @@ -566,7 +567,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { // ssr build const sys = qwikPlugin.getSys(); - if (sys.env === 'node' || sys.env === 'bun') { + if (hasNodeCompat(sys.env)) { const outputs = Object.keys(rollupBundle); // In order to simplify executing the server script with a common script @@ -758,7 +759,7 @@ const findQwikRoots = async ( packageJsonDir: string ): Promise => { const paths = new Map(); - if (sys.env === 'node' || sys.env === 'bun') { + if (hasNodeCompat(sys.env)) { const fs: typeof import('fs') = await sys.dynamicImport('node:fs'); let prevPackageJsonDir: string | undefined; do { @@ -819,6 +820,9 @@ export const isNotNullable = (v: T): v is NonNullable => { return v != null; }; +/** Whether the runtime supports Node standard library APIs (node:fs, node:os, etc.). */ +const hasNodeCompat = (env: SystemEnvironment) => env === 'node' || env === 'bun' || env === 'deno'; + const VITE_CLIENT_MODULE = `@builder.io/qwik/vite-client`; const CLIENT_DEV_INPUT = 'entry.dev'; diff --git a/packages/qwik/src/optimizer/src/plugins/vite.unit.ts b/packages/qwik/src/optimizer/src/plugins/vite.unit.ts index 2813956b741..0887218699d 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite.unit.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite.unit.ts @@ -22,11 +22,11 @@ const chunkInfoMocks = [ }, ] as Rollup.PreRenderedChunk[]; -function mockOptimizerOptions(): OptimizerOptions { +function mockOptimizerOptions(env: 'node' | 'deno' = 'node'): OptimizerOptions { return { sys: { cwd: () => process.cwd(), - env: 'node', + env, os: process.platform, dynamicImport: async (path) => import(path), strictDynamicImport: async (path) => import(path), @@ -417,6 +417,51 @@ test('should use build.outDir config when assetsDir is _astro', async () => { assert.equal(c.build.outDir, normalizePath(resolve(cwd, `dist/`))); }); +test('command: build, mode: production (deno)', async () => { + const initOpts = { + optimizerOptions: mockOptimizerOptions('deno'), + }; + const plugin = getPlugin(initOpts); + const c = (await plugin.config.call( + configHookPluginContext, + {}, + { command: 'build', mode: 'production' } + ))!; + const opts = await plugin.api?.getOptions(); + + assert.deepEqual(opts.target, 'client'); + assert.deepEqual(opts.buildMode, 'production'); + assert.deepEqual(opts.resolveQwikBuild, true); + + // Deno should produce the same config shape as Node + const build = c.build!; + assert.deepEqual(build.outDir, normalizePath(resolve(cwd, 'dist'))); + assert.deepEqual(build.dynamicImportVarsOptions?.exclude, [/./]); + assert.deepEqual(build.ssr, undefined); +}); + +test('command: build, --ssr entry.server.tsx (deno)', async () => { + const initOpts = { + optimizerOptions: mockOptimizerOptions('deno'), + }; + const plugin = getPlugin(initOpts); + const c = (await plugin.config.call( + configHookPluginContext, + { build: { ssr: resolve(cwd, 'src', 'entry.server.tsx') } }, + { command: 'build', mode: '' } + ))!; + const opts = await plugin.api?.getOptions(); + + assert.deepEqual(opts.target, 'ssr'); + assert.deepEqual(opts.buildMode, 'development'); + assert.deepEqual(opts.entryStrategy, { type: 'hoist' }); + + const build = c.build!; + assert.deepEqual(build.outDir, normalizePath(resolve(cwd, 'server'))); + assert.deepEqual(build.ssr, true); + assert.deepEqual(c.publicDir, false); +}); + test('command: build, --mode lib', async () => { const initOpts = { optimizerOptions: mockOptimizerOptions(), diff --git a/scripts/qwik-city.ts b/scripts/qwik-city.ts index 406e3430df9..6060acc6f18 100644 --- a/scripts/qwik-city.ts +++ b/scripts/qwik-city.ts @@ -35,7 +35,6 @@ export async function buildQwikCity(config: BuildConfig) { buildMiddlewareFirebase(config), buildStatic(config), buildStaticNode(config), - buildStaticDeno(config), ]); await buildRuntime(config); @@ -637,19 +636,6 @@ async function buildStatic(config: BuildConfig) { }); } -async function buildStaticDeno(config: BuildConfig) { - const entryPoints = [join(config.srcQwikCityDir, 'static', 'deno', 'index.ts')]; - - await build({ - entryPoints, - outfile: join(config.distQwikCityPkgDir, 'static', 'deno.mjs'), - bundle: true, - platform: 'neutral', - format: 'esm', - plugins: [resolveRequestHandler('../middleware/request-handler/index.mjs')], - }); -} - async function buildStaticNode(config: BuildConfig) { const entryPoints = [join(config.srcQwikCityDir, 'static', 'node', 'index.ts')];