diff --git a/docs/config/index.md b/docs/config/index.md index e980f7ef1dd83a..e7043a5145cc30 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -415,7 +415,7 @@ export default defineConfig(async ({ command, mode }) => { changeOrigin: true, configure: (proxy, options) => { // proxy will be an instance of 'http-proxy' - }, + } } } } @@ -741,9 +741,9 @@ SSR options may be adjusted in minor releases. ### ssr.noExternal -- **Type:** `string | RegExp | (string | RegExp)[]` +- **Type:** `string | RegExp | (string | RegExp)[] | true` - Prevent listed dependencies from being externalized for SSR. + Prevent listed dependencies from being externalized for SSR. If `true`, no dependencies are externalized. ### ssr.target diff --git a/docs/guide/ssr.md b/docs/guide/ssr.md index 7a7b5673e7a1f3..3d65723a6bfc58 100644 --- a/docs/guide/ssr.md +++ b/docs/guide/ssr.md @@ -251,3 +251,10 @@ export function mySSRPlugin() { ## SSR Target The default target for the SSR build is a node environment, but you can also run the server in a Web Worker. Packages entry resolution is different for each platform. You can configure the target to be Web Worker using the `ssr.target` set to `'webworker'`. + +## SSR Bundle + +In some cases like `webworker` runtimes, you might want to bundle your SSR build into a single JavaScript file. You can enable this behavior by setting `ssr.noExternal` to `true`. This will do two things: + +- Treat all dependencies as `noExternal` +- Throw an error if any Node.js built-ins are imported diff --git a/packages/playground/ssr-webworker/__tests__/serve.js b/packages/playground/ssr-webworker/__tests__/serve.js new file mode 100644 index 00000000000000..f4f207b85026c6 --- /dev/null +++ b/packages/playground/ssr-webworker/__tests__/serve.js @@ -0,0 +1,48 @@ +// @ts-check +// this is automtically detected by scripts/jestPerTestSetup.ts and will replace +// the default e2e test serve behavior + +const path = require('path') + +const port = (exports.port = 9528) + +/** + * @param {string} root + * @param {boolean} isProd + */ +exports.serve = async function serve(root, isProd) { + // we build first, regardless of whether it's prod/build mode + // because Vite doesn't support the concept of a "webworker server" + const { build } = require('vite') + + // worker build + await build({ + root, + logLevel: 'silent', + build: { + target: 'esnext', + ssr: 'src/entry-worker.jsx', + outDir: 'dist/worker' + } + }) + + const { createServer } = require(path.resolve(root, 'worker.js')) + const { app } = await createServer(root, isProd) + + return new Promise((resolve, reject) => { + try { + const server = app.listen(port, () => { + resolve({ + // for test teardown + async close() { + await new Promise((resolve) => { + server.close(resolve) + }) + } + }) + }) + } catch (e) { + reject(e) + } + }) +} diff --git a/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts b/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts new file mode 100644 index 00000000000000..30d2bb93e495b1 --- /dev/null +++ b/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts @@ -0,0 +1,10 @@ +import { port } from './serve' + +const url = `http://localhost:${port}` + +test('/', async () => { + await page.goto(url + '/') + expect(await page.textContent('h1')).toMatch('hello from webworker') + expect(await page.textContent('.linked')).toMatch('dep from upper directory') + expect(await page.textContent('.external')).toMatch('object') +}) diff --git a/packages/playground/ssr-webworker/package.json b/packages/playground/ssr-webworker/package.json new file mode 100644 index 00000000000000..b37e403b658522 --- /dev/null +++ b/packages/playground/ssr-webworker/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ssr-webworker", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "DEV=1 node worker", + "build:worker": "vite build --ssr src/entry-worker.jsx --outDir dist/worker" + }, + "dependencies": { + "react": "^17.0.2" + }, + "devDependencies": { + "miniflare": "^1.3.3" + } +} diff --git a/packages/playground/ssr-webworker/src/entry-worker.jsx b/packages/playground/ssr-webworker/src/entry-worker.jsx new file mode 100644 index 00000000000000..c885657b18a6d3 --- /dev/null +++ b/packages/playground/ssr-webworker/src/entry-worker.jsx @@ -0,0 +1,19 @@ +import { msg as linkedMsg } from 'resolve-linked' +import React from 'react' + +addEventListener('fetch', function (event) { + return event.respondWith( + new Response( + ` +

hello from webworker

+

${linkedMsg}

+

${typeof React}

+ `, + { + headers: { + 'content-type': 'text/html' + } + } + ) + ) +}) diff --git a/packages/playground/ssr-webworker/vite.config.js b/packages/playground/ssr-webworker/vite.config.js new file mode 100644 index 00000000000000..d0e80179498685 --- /dev/null +++ b/packages/playground/ssr-webworker/vite.config.js @@ -0,0 +1,12 @@ +/** + * @type {import('vite').UserConfig} + */ +module.exports = { + build: { + minify: false + }, + ssr: { + target: 'webworker', + noExternal: true + } +} diff --git a/packages/playground/ssr-webworker/worker.js b/packages/playground/ssr-webworker/worker.js new file mode 100644 index 00000000000000..09725aaa9d71bb --- /dev/null +++ b/packages/playground/ssr-webworker/worker.js @@ -0,0 +1,26 @@ +// @ts-check +const path = require('path') +const { Miniflare } = require('miniflare') + +const isDev = process.env.DEV + +async function createServer(root = process.cwd()) { + const mf = new Miniflare({ + scriptPath: path.resolve(root, 'dist/worker/entry-worker.js') + }) + + const app = mf.createServer() + + return { app } +} + +if (isDev) { + createServer().then(({ app }) => + app.listen(3000, () => { + console.log('http://localhost:3000') + }) + ) +} + +// for test use +exports.createServer = createServer diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index fb8c3a5bba0aac..cd1d801fb8388d 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -187,7 +187,7 @@ export type SSRTarget = 'node' | 'webworker' export interface SSROptions { external?: string[] - noExternal?: string | RegExp | (string | RegExp)[] + noExternal?: string | RegExp | (string | RegExp)[] | true /** * Define the target for the ssr build. The browser field in package.json * is ignored for node but used if webworker is the target @@ -383,7 +383,7 @@ export async function resolveConfig( root: resolvedRoot, isProduction, isBuild: command === 'build', - ssrTarget: resolved.ssr?.target, + ssrConfig: resolved.ssr, asSrc: true, preferRelative: false, tryIndex: true, diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index a83ddae2b2bf2a..6c88978f437ccf 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -39,7 +39,7 @@ export async function resolvePlugins( root: config.root, isProduction: config.isProduction, isBuild, - ssrTarget: config.ssr?.target, + ssrConfig: config.ssr, asSrc: true }), htmlInlineScriptProxyPlugin(), diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index d9cb8b14b70980..dfaa270e79050f 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -25,7 +25,7 @@ import { cleanUrl, slash } from '../utils' -import { ViteDevServer, SSRTarget } from '..' +import { ViteDevServer, SSROptions } from '..' import { createFilter } from '@rollup/pluginutils' import { PartialResolvedId } from 'rollup' import { resolve as _resolveExports } from 'resolve.exports' @@ -50,7 +50,7 @@ export interface InternalResolveOptions extends ResolveOptions { root: string isBuild: boolean isProduction: boolean - ssrTarget?: SSRTarget + ssrConfig?: SSROptions /** * src code mode also attempts the following: * - resolving /xxx as URLs @@ -69,7 +69,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { root, isProduction, asSrc, - ssrTarget, + ssrConfig, preferRelative = false } = baseOptions const requireOptions: InternalResolveOptions = { @@ -78,6 +78,8 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { } let server: ViteDevServer | undefined + const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {} + return { name: 'vite:resolve', @@ -224,6 +226,18 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { // externalize if building for SSR, otherwise redirect to empty module if (isBuiltin(id)) { if (ssr) { + if (ssrNoExternal === true) { + let message = `Cannot bundle Node.js built-in "${id}"` + if (importer) { + message += ` imported from "${path.relative( + process.cwd(), + importer + )}"` + } + message += `. Consider disabling ssr.noExternal or remove the built-in dependency.` + this.error(message) + } + return { id, external: true diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index a3481283038789..4c3b40e1915a9a 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -18,6 +18,10 @@ export function resolveSSRExternal( ssrExternals: Set = new Set(), seen: Set = new Set() ): string[] { + if (config.ssr?.noExternal === true) { + return [] + } + const { root } = config const pkgContent = lookupFile(root, ['package.json']) if (!pkgContent) {