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

Commit

Permalink
feat: build dev server bundle using vite (#201)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
pi0 and antfu authored Sep 30, 2021
1 parent 81114b4 commit 547ceb4
Show file tree
Hide file tree
Showing 14 changed files with 517 additions and 1,761 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: yarn build

- name: Test
run: yarn jest
run: yarn test

- name: Coverage
uses: codecov/codecov-action@v2
6 changes: 0 additions & 6 deletions jest.config.js

This file was deleted.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,35 @@
"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"
"test": "mocha -b -r jiti/register ./test/*.test.*"
},
"dependencies": {
"@vitejs/plugin-legacy": "^1.5.3",
"consola": "^2.15.3",
"fs-extra": "^10.0.0",
"p-debounce": "3",
"postcss-import": "^14.0.2",
"postcss-import-resolver": "^2.0.0",
"postcss-preset-env": "^6.7.0",
"postcss-url": "^10.1.3",
"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": {
"@babel/preset-typescript": "^7.15.0",
"@nuxt/test-utils": "^0.2.2",
"@nuxt/types": "^2.15.8",
"@nuxtjs/composition-api": "^0.28.0",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.1",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"eslint": "^7.32.0",
"jest": "^27.1.0",
"mkdist": "^0.3.3",
"mocha": "^9.1.2",
"nuxt": "^2.15.8",
"ohmyfetch": "^0.3.1",
"playwright": "^1.14.1",
"sass": "^1.39.0",
"siroc": "^0.16.0",
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
15 changes: 1 addition & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,12 @@ function nuxtVite () {
return
}

// Disable SSR by default
const ssrEnabled = nuxt.options.ssr && nuxt.options.vite?.ssr
if (!ssrEnabled) {
nuxt.options.ssr = false
nuxt.options.render.ssr = false
nuxt.options.build.ssr = false
nuxt.options.mode = 'spa'
}

nuxt.options.cli.badgeMessages.push(`⚡ Vite Mode Enabled (v${version})`)
// eslint-disable-next-line no-console
if (nuxt.options.vite?.experimentWarning !== false && !nuxt.options.test) {
consola.log(
'🧪 Vite mode is experimental and some nuxt modules might be incompatible\n',
' If found a bug, please report via https://github.com/nuxt/vite/issues with a minimal reproduction.' + (
ssrEnabled
? '\n Unstable server-side rendering is enabled'
: '\n You can enable unstable server-side rendering using `vite: { ssr: true }` in `nuxt.config`'
)
' If found a bug, please report via https://github.com/nuxt/vite/issues with a minimal reproduction.'
)
}

Expand Down
32 changes: 2 additions & 30 deletions src/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,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, hash } from './utils'

const DEFAULT_APP_TEMPLATE = `
<!DOCTYPE html>
Expand All @@ -27,7 +27,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 @@ -169,34 +169,6 @@ async function writeClientManifest (clientManifest: any, buildDir: string) {
await writeFile(resolve(buildDir, 'dist/server/client.manifest.mjs'), `export default ${clientManifestJSON}`, 'utf-8')
}

function hash (input: string, length = 8) {
return createHash('sha256')
.update(input)
.digest('hex')
.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
211 changes: 179 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 * 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 pDebounce from 'p-debounce'
import { ViteBuildContext, ViteOptions } from './types'
import { wpfs } from './utils/wpfs'
import { jsxPlugin } from './plugins/jsx'
import { generateDevSSRManifest } from './manifest'
import { uniq, hashId } from './utils'

export async function buildServer (ctx: ViteBuildContext) {
// Workaround to disable HMR
Expand Down Expand Up @@ -70,43 +72,188 @@ 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)
ctx.nuxt.hook('close', () => viteServer.close())

// Initialize plugins
await viteServer.pluginContainer.buildStart({})

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

// Build and watch
const _doBuild = async () => {
const start = Date.now()
const { code } = await bundleRequest(viteServer, '/.nuxt/server.js')
await writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.js'), code, 'utf-8')
const time = (Date.now() - start)
consola.info(`Server built in ${time}ms`)
await onBuild()
}
const doBuild = pDebounce(_doBuild, 300)

// Initial build
await _doBuild()

// Watch
viteServer.watcher.on('all', (_event, file) => {
if (file.indexOf(ctx.nuxt.options.buildDir) === 0) { return }
doBuild()
})
}

// ---- 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)) {
return {
code: `() => require('${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 exports = {}
const module = { exports }
const __vite_ssr_exports__ = exports;
const __vite_ssr_exportAll__ = __createViteSSRExportAll__(__vite_ssr_exports__)
${res.code || '/* empty */'};
return module.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 = `
const __vite_import_cache__ = Object.create({})
async function __vite_ssr_import__ (id) {
if (__vite_import_cache__[id]) {
return __vite_import_cache__[id]
}
const mod = await $chunks[id]()
if (mod && !('default' in mod)) {
mod.default = mod
}
__vite_import_cache__[id] = 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(ssrModule, 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,
`module.exports = function (...args) { return ${hashId(id)}().then(r => r.default(...args)) }`
].join('\n\n')

return { code }
}
Loading

0 comments on commit 547ceb4

Please sign in to comment.