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

feat: build dev server bundle using vite #201

Merged
merged 20 commits into from
Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"semver": "^7.3.5",
"ufo": "^0.7.9",
"upath": "^2.0.1",
"vite": "^2.5.4",
"vite": "^2.5.7",
"vite-plugin-vue2": "^1.8.2"
},
"devDependencies": {
Expand Down
5 changes: 0 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ export async function buildClient (ctx: ViteBuildContext) {
: `defaultexport:${p.src}`
}

// redirect '/_nuxt' to buildDir for dev
if (ctx.nuxt.options.dev) {
alias['/_nuxt'] = ctx.nuxt.options.buildDir
}

const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, {
define: {
'process.server': false,
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function nuxtVite () {
nuxt.options._modules = nuxt.options._modules
.filter(m => !(Array.isArray(m) && m[0] === '@nuxt/loading-screen'))

// TMP (ESM)
nuxt.options.render.bundleRenderer.runInNewContext = false

// Mask nuxt-vite to avoid other modules depending on it's existence
// TODO: Move to kit
const getModuleName = (m) => {
Expand Down
24 changes: 2 additions & 22 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { resolve } from 'path'
import { createHash } from 'crypto'
import { readJSON, remove, existsSync, readFile, writeFile, mkdirp } from 'fs-extra'
import { ViteBuildContext } from './types'
import { uniq, isJS, isCSS } from './utils'

const DEFAULT_APP_TEMPLATE = `
<!DOCTYPE html>
Expand All @@ -27,7 +28,7 @@ export async function prepareManifests (ctx: ViteBuildContext) {
const DEV_TEMPLATE = APP_TEMPLATE
.replace(
'</body>',
'<script type="module" src="/@vite/client"></script><script type="module" src="/_nuxt/client.js"></script></body>'
'<script type="module" src="/@vite/client"></script><script type="module" src="/.nuxt/client.js"></script></body>'
)
const SPA_TEMPLATE = ctx.nuxt.options.dev ? DEV_TEMPLATE : APP_TEMPLATE
const SSR_TEMPLATE = ctx.nuxt.options.dev ? DEV_TEMPLATE : APP_TEMPLATE
Expand Down Expand Up @@ -176,27 +177,6 @@ function hash (input: string, length = 8) {
.substr(0, length)
}

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

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

export function isCSS (file: string) {
return IS_CSS_RE.test(file)
}
function getModuleIds ([, value]: [string, any]) {
if (!value) { return [] }
// Only include legacy and css ids
Expand Down
214 changes: 182 additions & 32 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { resolve } from 'path'
import { builtinModules } from 'module'
import { createHash } from 'crypto'
import * as vite from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import consola from 'consola'
import { join } from 'upath'
import type { RollupWatcher } from 'rollup'
import { writeFile } from 'fs-extra'
import { ViteBuildContext, ViteOptions } from './types'
import { wpfs } from './utils/wpfs'
import { jsxPlugin } from './plugins/jsx'
import { generateDevSSRManifest } from './manifest'
import { uniq } from './utils'

export async function buildServer (ctx: ViteBuildContext) {
// Workaround to disable HMR
Expand Down Expand Up @@ -70,43 +72,191 @@ export async function buildServer (ctx: ViteBuildContext) {

const onBuild = () => ctx.nuxt.callHook('build:resources', wpfs)

// Production build
if (!ctx.nuxt.options.dev) {
const start = Date.now()
consola.info('Building server...')
await vite.build(serverConfig)
await onBuild()
consola.success(`Server built in ${Date.now() - start}ms`)
} else {
const watcher = await vite.build({
...serverConfig,
build: {
...serverConfig.build,
watch: {
include: [
join(ctx.nuxt.options.buildDir, '**/*'),
join(ctx.nuxt.options.srcDir, '**/*'),
join(ctx.nuxt.options.rootDir, '**/*')
],
exclude: [
'**/dist/server/**'
]
}
}
}) as RollupWatcher

let start = Date.now()
watcher.on('event', async (event) => {
if (event.code === 'BUNDLE_START') {
start = Date.now()
} else if (event.code === 'BUNDLE_END') {
await generateDevSSRManifest(ctx)
await onBuild()
consola.info(`Server rebuilt in ${Date.now() - start}ms`)
} else if (event.code === 'ERROR') {
consola.error(event.error)
return
}

// Start development server
const viteServer = await vite.createServer(serverConfig)
// Initialize plugins
await viteServer.pluginContainer.buildStart({})

const { code: esm } = await bundleRequest(viteServer, '/.nuxt/server.js')

// FIXME: vue-bundle-renderer does not support ESM
const cjs = `
module.exports = async (ctx) => {
// const server = await import('${resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs')}')
const server = require('jiti')()('${resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs')}')
const result = await server.default().then(i => i.default(ctx))
return result
}`

await writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), esm, 'utf-8')
await writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.js'), cjs, 'utf-8')

await writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/ssr-manifest.json'), JSON.stringify({}, null, 2), 'utf-8')

await generateDevSSRManifest(ctx)
await onBuild()
ctx.nuxt.hook('close', () => viteServer.close())
}

// ---- Vite Dev Bundler POC ----

interface TransformChunk {
id: string,
code: string,
deps: string[],
parents: string[]
}

interface SSRTransformResult {
code: string,
map: object,
deps: string[]
dynamicDeps: string[]
}

async function transformRequest (viteServer: vite.ViteDevServer, id) {
// Virtual modules start with `\0`
if (id && id.startsWith('/@id/__x00__')) {
id = '\0' + id.slice('/@id/__x00__'.length)
}

// Externals
if (builtinModules.includes(id) || (id.includes('node_modules') && !id.endsWith('.esm.js'))) {
if (id.startsWith('/@fs')) {
id = id.substr(4)
} else if (id.startsWith('/')) {
id = '.' + id // TODO
}
return {
code: `() => import('${id}')`,
deps: [],
dynamicDeps: []
}
}

// Transform
const res: SSRTransformResult = await viteServer.transformRequest(id, { ssr: true }).catch((err) => {
// eslint-disable-next-line no-console
console.warn(`[SSR] Error transforming ${id}: ${err}`)
// console.error(err)
}) as SSRTransformResult || { code: '', map: {}, deps: [], dynamicDeps: [] }

// Wrap into a vite module
const code = `async function () {
const __vite_ssr_exports__ = {};
const __vite_ssr_exportAll__ = __createViteSSRExportAll__(__vite_ssr_exports__)
${res.code || '/* empty */'};
return __vite_ssr_exports__;
}`
return { code, deps: res.deps || [], dynamicDeps: res.dynamicDeps || [] }
}

async function transformRequestRecursive (viteServer: vite.ViteDevServer, id, parent = '<entry>', chunks: Record<string, TransformChunk> = {}) {
if (chunks[id]) {
chunks[id].parents.push(parent)
return
}
const res = await transformRequest(viteServer, id)
const deps = uniq([...res.deps, ...res.dynamicDeps])

chunks[id] = {
id,
code: res.code,
deps,
parents: [parent]
} as TransformChunk
for (const dep of deps) {
await transformRequestRecursive(viteServer, dep, id, chunks)
}
return Object.values(chunks)
}

async function bundleRequest (viteServer: vite.ViteDevServer, id) {
const chunks = await transformRequestRecursive(viteServer, id)

const listIds = ids => ids.map(id => `// - ${id} (${hashId(id)})`).join('\n')
const chunksCode = chunks.map(chunk => `
// --------------------
// Request: ${chunk.id}
// Parents: \n${listIds(chunk.parents)}
// Dependencies: \n${listIds(chunk.deps)}
// --------------------
const ${hashId(chunk.id)} = ${chunk.code}
`).join('\n')

const manifestCode = 'const $chunks = {\n' +
chunks.map(chunk => ` '${chunk.id}': ${hashId(chunk.id)}`).join(',\n') + '\n}'

const dynamicImportCode = `
function __vite_ssr_import__ (id) {
return Promise.resolve($chunks[id]()).then(mod => {
if (mod && !('default' in mod))
mod.default = mod
return mod
})
}
function __vite_ssr_dynamic_import__(id) {
return __vite_ssr_import__(id)
}
`

// https://github.com/vitejs/vite/blob/fb406ce4c0fe6da3333c9d1c00477b2880d46352/packages/vite/src/node/ssr/ssrModuleLoader.ts#L121-L133
const helpers = `
function __createViteSSRExportAll__(ssrModule) {
return (sourceModule) => {
for (const key in sourceModule) {
if (key !== 'default') {
Object.defineProperty(sourceModule, key, {
enumerable: true,
configurable: true,
get() {
return sourceModule[key]
}
})
}
})
}
}
}
`

ctx.nuxt.hook('close', () => watcher.close())
// TODO: implement real HMR
const metaPolyfill = `
const __vite_ssr_import_meta__ = {
hot: {
accept() {}
}
}
`

const code = [
metaPolyfill,
chunksCode,
manifestCode,
dynamicImportCode,
helpers,
`export default ${hashId(id)}`
].join('\n\n')

return { code }
}

function hashId (id: string) {
return '$id_' + hash(id)
}

function hash (input: string, length = 8) {
return createHash('sha256')
.update(input)
.digest('hex')
.substr(0, length)
}
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function uniq<T> (arr: T[]): T[] {
return Array.from(new Set(arr))
}

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

export function isCSS (file: string) {
return IS_CSS_RE.test(file)
}
3 changes: 2 additions & 1 deletion src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async function bundle (nuxt: Nuxt, builder: any) {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
alias: {
...nuxt.options.alias,
'.nuxt': nuxt.options.buildDir,
'~': nuxt.options.srcDir,
'@': nuxt.options.srcDir,
'web-streams-polyfill/ponyfill/es2018': require.resolve('./runtime/mock/web-streams-polyfill.mjs'),
Expand Down Expand Up @@ -83,7 +84,7 @@ async function bundle (nuxt: Nuxt, builder: any) {
if (nuxt.options.dev) {
ctx.nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => {
const start = Date.now()
warmupViteServer(server, ['/_nuxt/client.js']).then(() => {
warmupViteServer(server, ['/.nuxt/client.js']).then(() => {
consola.info(`Vite warmed up in ${Date.now() - start}ms`)
}).catch(consola.error)
})
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12053,10 +12053,10 @@ vite-plugin-vue2@^1.8.2:
source-map "^0.7.3"
vue-template-es2015-compiler "^1.9.1"

vite@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.5.4.tgz#2dab98835be64c4157eba225e0becb9d373f608a"
integrity sha512-EAdbX8A6MJaqC6UuicZtoxY9+BdaBdcGVaROXvsu+vnlGyzZzm63QQgANJryIB0DvenveHAoCriN+dAnoS+TIQ==
vite@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.5.7.tgz#e495be9d8bcbf9d30c7141efdccacde746ee0125"
integrity sha512-hyUoWmRPhjN1aI+ZSBqDINKdIq7aokHE2ZXiztOg4YlmtpeQtMwMeyxv6X9YxHZmvGzg/js/eATM9Z1nwyakxg==
dependencies:
esbuild "^0.12.17"
postcss "^8.3.6"
Expand Down