Skip to content

Commit

Permalink
feat: Add ssr.noExternal = true option (#4490)
Browse files Browse the repository at this point in the history
  • Loading branch information
jplhomer committed Aug 16, 2021
1 parent 89e7a41 commit 963387a
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 9 deletions.
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

0 comments on commit 963387a

Please sign in to comment.