Skip to content
This repository has been archived by the owner on Jan 4, 2023. It is now read-only.

feat: production build with vite #179

Merged
merged 26 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/content/en/1.getting-started/3.build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Build

From v0.2.0, we shipped experimental build support with Vite. It's disable by default and you can enable it by setting `vite.build: true` in config.
antfu marked this conversation as resolved.
Show resolved Hide resolved

```js
// nuxt.config
export default {
buildModules: [
'nuxt-vite'
],
vite: {
build: true
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0 Should we make it default to true and for users to opt-out? Or maybe in next breaking?

Copy link
Member

@pi0 pi0 Aug 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote for next major (0.3.x) so that we can fix leftover issues like modern mode mapping, speeding up and better legacy.

}
}
```

Then run `nuxt build` with the power of Vite!
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
"scripts": {
"build": "siroc build && mkdist --src src/runtime --dist dist/runtime",
"prepublishOnly": "yarn build",
"dev": "nuxt dev test/fixture",
"play": "yarn play:dev",
"play:dev": "nuxt dev test/fixture",
"play:build": "nuxt build test/fixture",
"play:start": "nuxt start test/fixture",
"lint": "eslint --ext .ts .",
"release": "yarn test && standard-version && git push --follow-tags && npm publish",
"test": "yarn lint && yarn jest"
},
"dependencies": {
"@nuxt/http": "^0.6.4",
"@vitejs/plugin-legacy": "^1.5.1",
"chokidar": "^3.5.2",
"consola": "^2.15.3",
"fs-extra": "^10.0.0",
Expand All @@ -39,6 +43,7 @@
"@nuxt/types": "^2.15.8",
"@nuxtjs/composition-api": "^0.26.0",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.0",
"eslint": "^7.32.0",
"jest": "^27.0.6",
Expand Down
41 changes: 25 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { resolve } from 'path'
import * as vite from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import PluginLegacy from '@vitejs/plugin-legacy'
import { jsxPlugin } from './plugins/jsx'
import { replace } from './plugins/replace'
import { ViteBuildContext, ViteOptions } from './types'
Expand All @@ -17,6 +18,7 @@ export async function buildClient (ctx: ViteBuildContext) {
define: {
'process.server': false,
'process.client': true,
'process.static': false,
global: 'window',
'module.hot': false
},
Expand All @@ -29,12 +31,15 @@ export async function buildClient (ctx: ViteBuildContext) {
assetsDir: '.',
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'client.js')
}
},
manifest: true,
ssrManifest: true
},
plugins: [
replace({ 'process.env': 'import.meta.env' }),
jsxPlugin(),
createVuePlugin(ctx.config.vue)
createVuePlugin(ctx.config.vue),
PluginLegacy()
],
server: {
middlewareMode: true
Expand All @@ -43,23 +48,27 @@ export async function buildClient (ctx: ViteBuildContext) {

await ctx.nuxt.callHook('vite:extendConfig', clientConfig, { isClient: true, isServer: false })

const viteServer = await vite.createServer(clientConfig)
await ctx.nuxt.callHook('vite:serverCreated', viteServer)
if (ctx.nuxt.options.dev) {
const viteServer = await vite.createServer(clientConfig)
await ctx.nuxt.callHook('vite:serverCreated', viteServer)

const viteMiddleware = (req, res, next) => {
const viteMiddleware = (req, res, next) => {
// Workaround: vite devmiddleware modifies req.url
const originalURL = req.url
if (req.url === '/_nuxt/client.js') {
return res.end('')
const originalURL = req.url
if (req.url === '/_nuxt/client.js') {
return res.end('')
}
viteServer.middlewares.handle(req, res, (err) => {
req.url = originalURL
next(err)
})
}
viteServer.middlewares.handle(req, res, (err) => {
req.url = originalURL
next(err)
await ctx.nuxt.callHook('server:devMiddleware', viteMiddleware)

ctx.nuxt.hook('close', async () => {
await viteServer.close()
})
} else {
await vite.build(clientConfig)
}
await ctx.nuxt.callHook('server:devMiddleware', viteMiddleware)

ctx.nuxt.hook('close', async () => {
await viteServer.close()
})
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import type { ViteOptions } from './types'

function nuxtVite () {
const { nuxt } = this
const viteOptions = nuxt.options.vite || {}

if (!nuxt.options.dev) {
return
// default false
if (!viteOptions.build) {
return
}
}

// Check nuxt version
Expand Down Expand Up @@ -92,6 +96,7 @@ declare module '@nuxt/types/config/index' {
*/
vite?: ViteOptions & {
ssr: false | ViteOptions['ssr'],
build: boolean | ViteOptions['build'],
experimentWarning: boolean
}
}
Expand Down
198 changes: 150 additions & 48 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { resolve } from 'path'
import { createHash } from 'crypto'
import * as vite from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import { watch } from 'chokidar'
import { exists, readFile, mkdirp, writeFile } from 'fs-extra'
import { existsSync, readFile, mkdirp, writeFile, readJSON, writeJSON } from 'fs-extra'
import debounce from 'p-debounce'
import consola from 'consola'
import { join } from 'upath'
import { ViteBuildContext, ViteOptions } from './types'
import { wpfs } from './utils/wpfs'
import { jsxPlugin } from './plugins/jsx'
Expand All @@ -16,9 +18,7 @@ const DEFAULT_APP_TEMPLATE = `
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
<div id="__nuxt">{{ APP }}</div>
<script type="module" src="/@vite/client"></script>
<script type="module" src="/client.js"></script>
{{ APP }}
</body>
</html>
`
Expand All @@ -41,6 +41,7 @@ export async function buildServer (ctx: ViteBuildContext) {
define: {
'process.server': true,
'process.client': false,
'process.static': false,
'typeof window': '"undefined"',
'typeof document': '"undefined"',
'typeof navigator': '"undefined"',
Expand All @@ -61,6 +62,7 @@ export async function buildServer (ctx: ViteBuildContext) {
},
build: {
outDir: 'dist/server',
assetsDir: '_nuxt',
ssr: true,
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'server.js'),
Expand All @@ -81,66 +83,166 @@ export async function buildServer (ctx: ViteBuildContext) {

const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
await mkdirp(serverDist)
const r = (...args: string[]): string => resolve(serverDist, ...args)

const customAppTemplateFile = resolve(ctx.nuxt.options.srcDir, 'app.html')
const APP_TEMPLATE = await exists(customAppTemplateFile)
const APP_TEMPLATE = existsSync(customAppTemplateFile)
? (await readFile(customAppTemplateFile, 'utf-8'))
.replace('{{ APP }}', '<div id="__nuxt">{{ APP }}</div>')
.replace(
'</body>',
'<script type="module" src="/@vite/client"></script><script type="module" src="/client.js"></script></body>'
)
: DEFAULT_APP_TEMPLATE

await writeFile(resolve(serverDist, 'index.ssr.html'), APP_TEMPLATE)
await writeFile(resolve(serverDist, 'index.spa.html'), APP_TEMPLATE)
await writeFile(resolve(serverDist, 'client.manifest.json'), JSON.stringify({
publicPath: '',
all: [],
initial: [
'client.js'
],
async: [],
modules: {},
assetsMapping: {}
}, null, 2))
await writeFile(resolve(serverDist, 'server.manifest.json'), JSON.stringify({
entry: 'server.js',
files: {
'server.js': 'server.js'
},
maps: {}
}, null, 2))
const SPA_TEMPLATE = APP_TEMPLATE
.replace('{{ APP }}', '<div id="__nuxt">{{ APP }}</div>')
.replace(
'</body>',
'<script type="module" src="/@vite/client"></script><script type="module" src="/client.js"></script></body>'
)
const SSR_TEMPLATE = ctx.nuxt.options.dev ? SPA_TEMPLATE : APP_TEMPLATE

const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
await writeFile(r('index.ssr.html'), SSR_TEMPLATE)
await writeFile(r('index.spa.html'), SPA_TEMPLATE)

if (!ctx.nuxt.options.ssr) {
await onBuild()
return
if (ctx.nuxt.options.dev) {
await stubManifest(ctx)
} else {
await generateBuildManifest(ctx)
}

const build = debounce(async () => {
const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
const build = async () => {
const start = Date.now()
await vite.build(serverConfig)
await onBuild()
consola.info(`Server built in ${Date.now() - start}ms`)
}, 300)
}

const debouncedBuild = debounce(build, 300)

await build()

const watcher = watch([
ctx.nuxt.options.buildDir,
ctx.nuxt.options.srcDir,
ctx.nuxt.options.rootDir
], {
ignored: [
'**/dist/server/**'
]
})
if (ctx.nuxt.options.dev) {
const watcher = watch([
ctx.nuxt.options.buildDir,
ctx.nuxt.options.srcDir,
ctx.nuxt.options.rootDir
], {
ignored: [
'**/dist/server/**'
]
})

watcher.on('change', () => debouncedBuild())

ctx.nuxt.hook('close', async () => {
await watcher.close()
})
}
}

// convert vite's manifest to webpack style
async function generateBuildManifest (ctx: ViteBuildContext) {
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
const r = (...args: string[]): string => resolve(serverDist, ...args)

const publicPath = '/_nuxt/'
const viteClientManifest = await readJSON(join(clientDist, 'manifest.json'))

function getModuleIds ([, value]: [string, any]) {
if (!value) {
return []
}
return [value.file, ...value.css || []]
.filter(i => !i.endsWith('.js') || i.match(/-legacy\./)) // only use legacy build
}

const asyncEntries = uniq(
Object.entries(viteClientManifest)
.filter((i: any) => i[1].isDynamicEntry)
.flatMap(getModuleIds)
).filter(i => i)
const initialEntries = uniq(
Object.entries(viteClientManifest)
.filter((i: any) => !i[1].isDynamicEntry)
.flatMap(getModuleIds)
).filter(i => i)
const initialJs = initialEntries.filter(i => i.endsWith('.js'))
const initialAssets = initialEntries.filter(i => !i.endsWith('.js'))
pi0 marked this conversation as resolved.
Show resolved Hide resolved

// search for polyfill file, we don't include it in the client entry
const polyfillName = initialEntries.find(i => i.startsWith('polyfills-legacy.'))

// @vitejs/plugin-legacy uses SystemJS which need to call `System.import` to load modules
const clientEntryCode = initialJs
.filter(i => !i.startsWith('polyfills-legacy.'))
.map(i => `System.import("${publicPath}${i}");`)
pi0 marked this conversation as resolved.
Show resolved Hide resolved
.join('\n')
const clientEntryHash = createHash('sha256')
.update(clientEntryCode)
.digest('hex')
.substr(0, 8)
const clientEntryName = 'entry.' + clientEntryHash + '.js'

watcher.on('change', () => build())
const clientManifest = {
publicPath,
all: uniq([
polyfillName,
clientEntryName,
...Object.entries(viteClientManifest)
.flatMap(getModuleIds)
]).filter(i => i),
initial: [
polyfillName,
clientEntryName,
...initialAssets
],
async: [
// we move initial entries to the client entry
...initialJs,
...asyncEntries
],
modules: {},
assetsMapping: {}
}
const serverManifest = {
entry: 'server.js',
files: {
'server.js': 'server.js',
...Object.fromEntries(Object.entries(viteClientManifest).map((i: any) => [i[0], i[1].file]))
},
maps: {}
}

await writeFile(join(clientDist, clientEntryName), clientEntryCode, 'utf-8')
await writeJSON(r('client.manifest.json'), clientManifest, { spaces: 2 })
await writeJSON(r('server.manifest.json'), serverManifest, { spaces: 2 })
}

// stub manifest on dev
async function stubManifest (ctx: ViteBuildContext) {
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
const r = (...args: string[]): string => resolve(serverDist, ...args)

await writeJSON(r('client.manifest.json'), {
antfu marked this conversation as resolved.
Show resolved Hide resolved
publicPath: '',
all: [
'client.js'
],
initial: [
'client.js'
],
async: [],
modules: {},
assetsMapping: {}
}, { spaces: 2 })
await writeJSON(r('server.manifest.json'), {
entry: 'server.js',
files: {
'server.js': 'server.js'
},
maps: {}
}, { spaces: 2 })
}

ctx.nuxt.hook('close', async () => {
await watcher.close()
})
function uniq<T> (arr:T[]): T[] {
return Array.from(new Set(arr))
}
Loading