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

Commit

Permalink
feat: production build with vite (#179)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: Pooya Parsa <pyapar@gmail.com>
  • Loading branch information
3 people authored Aug 24, 2021
1 parent f0044db commit 19bf4b2
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 56 deletions.
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 disabled by default and you can enable it by setting `vite.build: true` in config.

```js
// nuxt.config
export default {
buildModules: [
'nuxt-vite'
],
vite: {
build: true
}
}
```

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",
"dev": "yarn fixture:dev",
"fixture:dev": "nuxt dev test/fixture",
"fixture:build": "nuxt build test/fixture",
"fixture: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
16 changes: 14 additions & 2 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,6 +48,13 @@ export async function buildClient (ctx: ViteBuildContext) {

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

// Production build
if (!ctx.nuxt.options.dev) {
await vite.build(clientConfig)
return
}

// Create development server
const viteServer = await vite.createServer(clientConfig)
await ctx.nuxt.callHook('vite:serverCreated', viteServer)

Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import type { ViteOptions } from './types'

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

if (!nuxt.options.dev) {
// Only enable for development or production if `build: true` is set
if (!nuxt.options.dev && !viteOptions.build) {
return
}

Expand Down Expand Up @@ -92,6 +94,7 @@ declare module '@nuxt/types/config/index' {
*/
vite?: ViteOptions & {
ssr: false | ViteOptions['ssr'],
build: boolean | ViteOptions['build'],
experimentWarning: boolean
}
}
Expand Down
213 changes: 169 additions & 44 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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, remove } from 'fs-extra'
import debounce from 'p-debounce'
import consola from 'consola'
import { ViteBuildContext, ViteOptions } from './types'
Expand All @@ -16,9 +17,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 +40,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 +61,7 @@ export async function buildServer (ctx: ViteBuildContext) {
},
build: {
outDir: 'dist/server',
assetsDir: ctx.nuxt.options.app.assetsPath.replace(/^\/|\/$/, ''),
ssr: true,
rollupOptions: {
input: resolve(ctx.nuxt.options.buildDir, 'server.js'),
Expand All @@ -79,68 +80,192 @@ export async function buildServer (ctx: ViteBuildContext) {

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

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

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({
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

await writeFile(rDist('server/index.ssr.html'), SSR_TEMPLATE)
await writeFile(rDist('server/index.spa.html'), SPA_TEMPLATE)

if (ctx.nuxt.options.dev) {
await stubManifest(ctx)
} else {
await generateBuildManifest(ctx)
}

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`)
}

const debouncedBuild = debounce(build, 300)

await build()

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 rDist = (...args: string[]): string => resolve(ctx.nuxt.options.buildDir, 'dist', ...args)

const publicPath = ctx.nuxt.options.app.assetsPath // Default: /nuxt/
const viteClientManifest = await readJSON(rDist('client/manifest.json'))
const clientEntries = Object.entries(viteClientManifest)

const asyncEntries = uniq(clientEntries.filter((id: any) => id[1].isDynamicEntry).flatMap(getModuleIds)).filter(Boolean)
const initialEntries = uniq(clientEntries.filter((id: any) => !id[1].isDynamicEntry).flatMap(getModuleIds)).filter(Boolean)
const initialJs = initialEntries.filter(isJS)
const initialAssets = initialEntries.filter(isCSS)

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

// @vitejs/plugin-legacy uses SystemJS which need to call `System.import` to load modules
const clientImports = initialJs.filter(id => id !== polyfillName).map(id => publicPath + id)
const clientEntryCode = `var imports = ${JSON.stringify(clientImports)}\nimports.reduce((p, id) => p.then(() => System.import(id)), Promise.resolve())`
const clientEntryName = 'entry-legacy.' + hash(clientEntryCode) + '.js'

const clientManifest = {
publicPath,
all: uniq([
polyfillName,
clientEntryName,
...clientEntries.flatMap(getModuleIds)
]).filter(Boolean),
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(clientEntries.map(([id, entry]) => [id, (entry as any).file]))
},
maps: {}
}

await writeFile(rDist('client', clientEntryName), clientEntryCode, 'utf-8')

const clientManifestJSON = JSON.stringify(clientManifest, null, 2)
await writeFile(rDist('server/client.manifest.json'), clientManifestJSON, 'utf-8')
await writeFile(rDist('server/client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')

const serverManifestJSON = JSON.stringify(serverManifest, null, 2)
await writeFile(rDist('server/server.manifest.json'), serverManifestJSON, 'utf-8')
await writeFile(rDist('server/server.manifest.mjs'), `export default ${serverManifestJSON}`, 'utf-8')

// Remove SSR manifest from public client dir
await remove(rDist('client/manifest.json'))
await remove(rDist('client/ssr-manifest.json'))
}

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

const clientManifest = {
publicPath: '',
all: [],
all: [
'client.js'
],
initial: [
'client.js'
],
async: [],
modules: {},
assetsMapping: {}
}, null, 2))
await writeFile(resolve(serverDist, 'server.manifest.json'), JSON.stringify({
}
const serverManifest = {
entry: 'server.js',
files: {
'server.js': 'server.js'
},
maps: {}
}, null, 2))
}

const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
const clientManifestJSON = JSON.stringify(clientManifest, null, 2)
await writeFile(rDist('server/client.manifest.json'), clientManifestJSON, 'utf-8')
await writeFile(rDist('server/client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')

if (!ctx.nuxt.options.ssr) {
await onBuild()
return
}
const serverManifestJSON = JSON.stringify(serverManifest, null, 2)
await writeFile(rDist('server/server.manifest.json'), serverManifestJSON)
await writeFile(rDist('server/server.manifest.mjs'), serverManifestJSON)
}

const build = debounce(async () => {
const start = Date.now()
await vite.build(serverConfig)
await onBuild()
consola.info(`Server built in ${Date.now() - start}ms`)
}, 300)
function hash (input: string, length = 8) {
return createHash('sha256')
.update(input)
.digest('hex')
.substr(0, length)
}

await build()
function uniq<T> (arr:T[]): T[] {
return Array.from(new Set(arr))
}

const watcher = watch([
ctx.nuxt.options.buildDir,
ctx.nuxt.options.srcDir,
ctx.nuxt.options.rootDir
], {
ignored: [
'**/dist/server/**'
]
})
// Copied from vue-bundle-renderer utils
const IS_JS_RE = /\.[cm]?js(\?[^.]+)?$/
const IS_MODULE_RE = /\.mjs(\?[^.]+)?$/
const HAS_EXT_RE = /[^./]+\.[^./]+$/
const IS_CSS_RE = /\.css(\?[^.]+)?$/

export function isJS (file: string) {
return IS_JS_RE.test(file) || !HAS_EXT_RE.test(file)
}

export function isModule (file: string) {
return IS_MODULE_RE.test(file) || !HAS_EXT_RE.test(file)
}

watcher.on('change', () => build())
export function isCSS (file: string) {
return IS_CSS_RE.test(file)
}

ctx.nuxt.hook('close', async () => {
await watcher.close()
})
function getModuleIds ([, value]: [string, any]) {
if (!value) { return [] }
// Only include legacy and css ids
return [value.file, ...value.css || []].filter(id => isCSS(id) || id.match(/-legacy\./))
}
2 changes: 1 addition & 1 deletion src/utils/wpfs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from 'upath'
import fsExtra from 'fs-extra'

export const wpfs = {
export const wpfs: any = {
...fsExtra,
join
}
Loading

0 comments on commit 19bf4b2

Please sign in to comment.