diff --git a/packages/nitro/package.json b/packages/nitro/package.json index 7e267ca741..6357535185 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -54,6 +54,7 @@ "node-fetch": "^3.0.0", "ohmyfetch": "^0.3.1", "ora": "^6.0.1", + "p-debounce": "^4.0.0", "pathe": "^0.2.0", "pretty-bytes": "^5.6.0", "rollup": "^2.58.0", diff --git a/packages/nuxt3/src/core/builder.ts b/packages/nuxt3/src/core/builder.ts index bf37985a3f..8a34fd02ad 100644 --- a/packages/nuxt3/src/core/builder.ts +++ b/packages/nuxt3/src/core/builder.ts @@ -49,7 +49,7 @@ function watch (nuxt: Nuxt) { } async function bundle (nuxt: Nuxt) { - const useVite = !!nuxt.options.vite + const useVite = nuxt.options.vite !== false const { bundle } = await (useVite ? import('@nuxt/vite-builder') : import('@nuxt/webpack-builder')) return bundle(nuxt) } diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index acd9f32042..f71185a96e 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -30,8 +30,7 @@ export async function buildClient (ctx: ViteBuildContext) { } }, manifest: true, - outDir: resolve(ctx.nuxt.options.buildDir, 'dist/client'), - assetsDir: '.' + outDir: resolve(ctx.nuxt.options.buildDir, 'dist/client') }, plugins: [ replace({ 'process.env': 'import.meta.env' }), diff --git a/packages/vite/src/dev-bundler.ts b/packages/vite/src/dev-bundler.ts new file mode 100644 index 0000000000..92acb2f7c3 --- /dev/null +++ b/packages/vite/src/dev-bundler.ts @@ -0,0 +1,184 @@ +import { builtinModules } from 'module' +import { createHash } from 'crypto' +import * as vite from 'vite' + +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) + } + if (id && id.startsWith('/@id/')) { + id = id.slice('/@id/'.length) + } + + // Externals + if (builtinModules.includes(id) || id.includes('node_modules')) { + return { + code: `(global, exports, importMeta, ssrImport, ssrDynamicImport, ssrExportAll) => import('${id.replace(/^\/@fs/, '')}').then(r => { ssrExportAll(r) })`, + 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 (global, __vite_ssr_exports__, __vite_ssr_import_meta__, __vite_ssr_import__, __vite_ssr_dynamic_import__, __vite_ssr_exportAll__) { +${res.code || '/* empty */'}; +}` + return { code, deps: res.deps || [], dynamicDeps: res.dynamicDeps || [] } +} + +async function transformRequestRecursive (viteServer: vite.ViteDevServer, id, parent = '', chunks: Record = {}) { + 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) +} + +export async function bundleRequest (viteServer: vite.ViteDevServer, entryURL) { + const chunks = await transformRequestRecursive(viteServer, entryURL) + + 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 __modules__ = {\n' + + chunks.map(chunk => ` '${chunk.id}': ${hashId(chunk.id)}`).join(',\n') + '\n}' + + // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/ssrModuleLoader.ts + const ssrModuleLoader = ` +const __pendingModules__ = new Map() +const __pendingImports__ = new Map() +const __ssrContext__ = { global: {} } + +function __ssrLoadModule__(url, urlStack = []) { + const pendingModule = __pendingModules__.get(url) + if (pendingModule) { return pendingModule } + const modulePromise = __instantiateModule__(url, urlStack) + __pendingModules__.set(url, modulePromise) + modulePromise.catch(() => { __pendingModules__.delete(url) }) + .finally(() => { __pendingModules__.delete(url) }) + return modulePromise +} + +async function __instantiateModule__(url, urlStack) { + const mod = __modules__[url] + if (mod.stubModule) { return mod.stubModule } + const stubModule = { [Symbol.toStringTag]: 'Module' } + Object.defineProperty(stubModule, '__esModule', { value: true }) + mod.stubModule = stubModule + const importMeta = { url, hot: { accept() {} } } + urlStack = urlStack.concat(url) + const isCircular = url => urlStack.includes(url) + const pendingDeps = [] + const ssrImport = async (dep) => { + // TODO: Handle externals if dep[0] !== '.' | '/' + if (!isCircular(dep) && !__pendingImports__.get(dep)?.some(isCircular)) { + pendingDeps.push(dep) + if (pendingDeps.length === 1) { + __pendingImports__.set(url, pendingDeps) + } + await __ssrLoadModule__(dep, urlStack) + if (pendingDeps.length === 1) { + __pendingImports__.delete(url) + } else { + pendingDeps.splice(pendingDeps.indexOf(dep), 1) + } + } + return __modules__[dep].stubModule + } + function ssrDynamicImport (dep) { + // TODO: Handle dynamic import starting with . relative to url + return ssrImport(dep) + } + + function ssrExportAll(sourceModule) { + for (const key in sourceModule) { + if (key !== 'default') { + try { + Object.defineProperty(stubModule, key, { + enumerable: true, + configurable: true, + get() { return sourceModule[key] } + }) + } catch (_err) { } + } + } + } + + await mod( + __ssrContext__.global, + stubModule, + importMeta, + ssrImport, + ssrDynamicImport, + ssrExportAll + ) + + return stubModule +} +` + + const code = [ + chunksCode, + manifestCode, + ssrModuleLoader, + `export default await __ssrLoadModule__('${entryURL}')` + ].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) +} + +export function uniq (arr: T[]): T[] { + return Array.from(new Set(arr)) +} diff --git a/packages/vite/src/server.ts b/packages/vite/src/server.ts index 17624a790c..9ddc4ba029 100644 --- a/packages/vite/src/server.ts +++ b/packages/vite/src/server.ts @@ -1,10 +1,13 @@ import { resolve } from 'pathe' import * as vite from 'vite' import vuePlugin from '@vitejs/plugin-vue' +import fse from 'fs-extra' +import pDebounce from 'p-debounce' import consola from 'consola' import { ViteBuildContext, ViteOptions } from './vite' import { wpfs } from './utils/wpfs' import { cacheDirPlugin } from './plugins/cache-dir' +import { bundleRequest } from './dev-bundler' export async function buildServer (ctx: ViteBuildContext) { const serverConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, { @@ -19,7 +22,14 @@ export async function buildServer (ctx: ViteBuildContext) { }, resolve: { alias: { - '#build/plugins': resolve(ctx.nuxt.options.buildDir, 'plugins/server') + '#build/plugins': resolve(ctx.nuxt.options.buildDir, 'plugins/server'), + // Alias vue + 'vue/server-renderer': 'vue/server-renderer', + 'vue/compiler-sfc': 'vue/compiler-sfc', + '@vue/reactivity': `@vue/reactivity/dist/reactivity.cjs${ctx.nuxt.options.dev ? '' : '.prod'}.js`, + '@vue/shared': `@vue/shared/dist/shared.cjs${ctx.nuxt.options.dev ? '' : '.prod'}.js`, + 'vue-router': `vue-router/dist/vue-router.cjs${ctx.nuxt.options.dev ? '' : '.prod'}.js`, + vue: `vue/dist/vue.cjs${ctx.nuxt.options.dev ? '' : '.prod'}.js` } }, ssr: { @@ -57,36 +67,49 @@ 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`) + return + } + if (!ctx.nuxt.options.ssr) { await onBuild() return } - let lastBuild = 0 - const build = async () => { - let start = Date.now() - // debounce - if (start - lastBuild < 300) { - await sleep(300 - (start - lastBuild) + 1) - start = Date.now() - if (start - lastBuild < 300) { - return - } - } - lastBuild = start - await vite.build(serverConfig) + // Start development server + const viteServer = await vite.createServer(serverConfig) + + // Close server on exit + ctx.nuxt.hook('close', () => viteServer.close()) + + // Initialize plugins + await viteServer.pluginContainer.buildStart({}) + + // Build and watch + const _doBuild = async () => { + const start = Date.now() + const { code } = await bundleRequest(viteServer, resolve(ctx.nuxt.options.appDir, 'entry')) + await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8') + const time = (Date.now() - start) + consola.success(`Vite server built in ${time}ms`) await onBuild() - consola.info(`Server built in ${Date.now() - start}ms`) } + const doBuild = pDebounce(_doBuild, 100) - await build() + // Initial build + await _doBuild() - ctx.nuxt.hook('builder:watch', () => build()) - ctx.nuxt.hook('app:templatesGenerated', () => build()) -} - -function sleep (ms:number) { - return new Promise((resolve) => { - setTimeout(resolve, ms) + // Watch + viteServer.watcher.on('all', (_event, file) => { + if (file.indexOf(ctx.nuxt.options.buildDir) === 0) { return } + doBuild() }) + // ctx.nuxt.hook('builder:watch', () => doBuild()) + ctx.nuxt.hook('app:templatesGenerated', () => doBuild()) } diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index e50dc842b0..854fc6bfc5 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,5 +1,4 @@ import { defineNuxtConfig } from '@nuxt/kit' export default defineNuxtConfig({ - // vite: true }) diff --git a/yarn.lock b/yarn.lock index b1a3903511..49c10cfb5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,6 +1566,7 @@ __metadata: node-fetch: ^3.0.0 ohmyfetch: ^0.3.1 ora: ^6.0.1 + p-debounce: ^4.0.0 pathe: ^0.2.0 pretty-bytes: ^5.6.0 rollup: ^2.58.0 @@ -10395,6 +10396,13 @@ fsevents@~2.3.2: languageName: node linkType: hard +"p-debounce@npm:^4.0.0": + version: 4.0.0 + resolution: "p-debounce@npm:4.0.0" + checksum: 7f796f6ed264cb964b83601e70c4c0d94dd52d54e1361400ee80df7527217a59074aa14ab746d4d3089b352181c4ffb5329158fd679cb4b4b52c208574e9e56d + languageName: node + linkType: hard + "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0"