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 15 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 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
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
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
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
206 changes: 160 additions & 46 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,178 @@ 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: [],
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(r('index.ssr.html'), SSR_TEMPLATE)
await writeFile(r('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 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 = ctx.nuxt.options.app.assetsPath // Default: /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'))

// 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}");`)
.join('\n')
const clientEntryHash = createHash('sha256')
.update(clientEntryCode)
.digest('hex')
.substr(0, 8)
const clientEntryName = 'entry.' + clientEntryHash + '.js'

const clientManifest = {
publicPath,
all: uniq([
polyfillName,
clientEntryName,
...Object.entries(viteClientManifest)
.flatMap(getModuleIds)
]).filter(i => i),
initial: [
'client.js'
polyfillName,
clientEntryName,
...initialAssets
],
async: [
// we move initial entries to the client entry
...initialJs,
...asyncEntries
],
async: [],
modules: {},
assetsMapping: {}
}, null, 2))
await writeFile(resolve(serverDist, 'server.manifest.json'), JSON.stringify({
}

const serverManifest = {
entry: 'server.js',
files: {
'server.js': 'server.js'
'server.js': 'server.js',
...Object.fromEntries(Object.entries(viteClientManifest).map((i: any) => [i[0], i[1].file]))
},
maps: {}
}, null, 2))
}

const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)
await writeFile(join(clientDist, clientEntryName), clientEntryCode, 'utf-8')

if (!ctx.nuxt.options.ssr) {
await onBuild()
return
}
const clientManifestJSON = JSON.stringify(clientManifest, null, 2)
await writeFile(r('client.manifest.json'), clientManifestJSON, 'utf-8')
await writeFile(r('client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')

const build = debounce(async () => {
const start = Date.now()
await vite.build(serverConfig)
await onBuild()
consola.info(`Server built in ${Date.now() - start}ms`)
}, 300)
const serverManifestJSON = JSON.stringify(serverManifest, null, 2)
await writeFile(r('server.manifest.json'), serverManifestJSON, 'utf-8')
await writeFile(r('server.manifest.mjs'), `export default ${serverManifestJSON}`, 'utf-8')
}

await build()
// 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)

const watcher = watch([
ctx.nuxt.options.buildDir,
ctx.nuxt.options.srcDir,
ctx.nuxt.options.rootDir
], {
ignored: [
'**/dist/server/**'
]
})
const clientManifest = {
publicPath: '',
all: [
'client.js'
],
initial: [
'client.js'
],
async: [],
modules: {},
assetsMapping: {}
}
const serverManifest = {
entry: 'server.js',
files: {
'server.js': 'server.js'
},
maps: {}
}

watcher.on('change', () => build())
const clientManifestJSON = JSON.stringify(clientManifest, null, 2)
await writeFile(r('client.manifest.json'), clientManifestJSON, 'utf-8')
await writeFile(r('client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')
await writeJSON(r('server.manifest.json'), serverManifest, { spaces: 2 })
}

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