Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ssr.noExternal = true option #4490

Merged
merged 10 commits into from Aug 16, 2021
6 changes: 3 additions & 3 deletions docs/config/index.md
Expand Up @@ -415,7 +415,7 @@ export default defineConfig(async ({ command, mode }) => {
changeOrigin: true,
configure: (proxy, options) => {
// proxy will be an instance of 'http-proxy'
},
}
}
}
}
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions docs/guide/ssr.md
Expand Up @@ -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
48 changes: 48 additions & 0 deletions 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)
}
})
}
10 changes: 10 additions & 0 deletions 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')
})
15 changes: 15 additions & 0 deletions 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"
}
}
19 changes: 19 additions & 0 deletions 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(
`
<h1>hello from webworker</h1>
<p class="linked">${linkedMsg}</p>
<p class="external">${typeof React}</p>
`,
{
headers: {
'content-type': 'text/html'
}
}
)
)
})
12 changes: 12 additions & 0 deletions 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
}
}
26 changes: 26 additions & 0 deletions 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
4 changes: 2 additions & 2 deletions packages/vite/src/node/config.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/index.ts
Expand Up @@ -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(),
Expand Down
20 changes: 17 additions & 3 deletions packages/vite/src/node/plugins/resolve.ts
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -69,7 +69,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
root,
isProduction,
asSrc,
ssrTarget,
ssrConfig,
preferRelative = false
} = baseOptions
const requireOptions: InternalResolveOptions = {
Expand All @@ -78,6 +78,8 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
}
let server: ViteDevServer | undefined

const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {}

return {
name: 'vite:resolve',

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/node/ssr/ssrExternal.ts
Expand Up @@ -18,6 +18,10 @@ export function resolveSSRExternal(
ssrExternals: Set<string> = new Set(),
seen: Set<string> = new Set()
): string[] {
if (config.ssr?.noExternal === true) {
return []
}

const { root } = config
const pkgContent = lookupFile(root, ['package.json'])
if (!pkgContent) {
Expand Down