diff --git a/docs/content/en/1.getting-started/3.build.md b/docs/content/en/1.getting-started/3.build.md new file mode 100644 index 0000000..b49b836 --- /dev/null +++ b/docs/content/en/1.getting-started/3.build.md @@ -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! diff --git a/package.json b/package.json index 4af081f..064e44a 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/client.ts b/src/client.ts index 9d9e4b5..cb4f138 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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' @@ -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 }, @@ -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 @@ -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) diff --git a/src/index.ts b/src/index.ts index a4e31f0..70e7467 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 } @@ -92,6 +94,7 @@ declare module '@nuxt/types/config/index' { */ vite?: ViteOptions & { ssr: false | ViteOptions['ssr'], + build: boolean | ViteOptions['build'], experimentWarning: boolean } } diff --git a/src/server.ts b/src/server.ts index 6322e56..73bbbd9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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' @@ -16,9 +17,7 @@ const DEFAULT_APP_TEMPLATE = ` {{ HEAD }} -
{{ APP }}
- - + {{ APP }} ` @@ -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"', @@ -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'), @@ -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 }}', '
{{ APP }}
') - .replace( - '', - '' - ) : 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 }}', '
{{ APP }}
') + .replace( + '', + '' + ) + 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 (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\./)) } diff --git a/src/utils/wpfs.ts b/src/utils/wpfs.ts index ed03373..a1966ca 100644 --- a/src/utils/wpfs.ts +++ b/src/utils/wpfs.ts @@ -1,7 +1,7 @@ import { join } from 'upath' import fsExtra from 'fs-extra' -export const wpfs = { +export const wpfs: any = { ...fsExtra, join } diff --git a/src/vite.ts b/src/vite.ts index 70645c6..d63a521 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -78,12 +78,14 @@ async function bundle (nuxt: Nuxt, builder: any) { } await ctx.nuxt.callHook('vite:extend', ctx) - ctx.nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => { - const start = Date.now() - warmupViteServer(server, ['/client.js']).then(() => { - consola.info(`Vite warmed up in ${Date.now() - start}ms`) - }).catch(consola.error) - }) + if (nuxt.options.dev) { + ctx.nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer) => { + const start = Date.now() + warmupViteServer(server, ['/client.js']).then(() => { + consola.info(`Vite warmed up in ${Date.now() - start}ms`) + }).catch(consola.error) + }) + } await buildClient(ctx) await buildServer(ctx) diff --git a/test/fixture/nuxt.config.js b/test/fixture/nuxt.config.js index 5ea34cb..cc87378 100644 --- a/test/fixture/nuxt.config.js +++ b/test/fixture/nuxt.config.js @@ -14,7 +14,8 @@ export default { '~/plugins/capi', '~/plugins/plugin.client', '~/plugins/plugin.server', - '~/plugins/no-export.js' + '~/plugins/no-export.js', + '~/plugins/style' ], hooks: { 'vue-renderer:context' (ssrContext) { @@ -25,6 +26,7 @@ export default { }, vite: { ssr: true, + build: true, server: { fs: { strict: false diff --git a/test/fixture/plugins/style.js b/test/fixture/plugins/style.js new file mode 100644 index 0000000..9d51ffc --- /dev/null +++ b/test/fixture/plugins/style.js @@ -0,0 +1 @@ +import '../styles/main.css' diff --git a/test/fixture/styles/main.css b/test/fixture/styles/main.css new file mode 100644 index 0000000..103f212 --- /dev/null +++ b/test/fixture/styles/main.css @@ -0,0 +1,3 @@ +h2 { + color: #00c58e; +} diff --git a/yarn.lock b/yarn.lock index 3347043..24c5df8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1203,6 +1203,11 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/standalone@^7.14.9": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.15.3.tgz#60f74273202ffcc6bb1428918053449fe477227c" + integrity sha512-Bst2YWEyQ2ROyO0+jxPVnnkSmUh44/x54+LSbe5M4N5LGfOkxpajEUKVE4ndXtIVrLlHCyuiqCPwv3eC1ItnCg== + "@babel/template@^7.12.13", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -2210,6 +2215,13 @@ dependencies: "@types/webpack" "^4" +"@types/fs-extra@^9.0.12": + version "9.0.12" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf" + integrity sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw== + dependencies: + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -2644,6 +2656,17 @@ "@typescript-eslint/types" "4.29.1" eslint-visitor-keys "^2.0.0" +"@vitejs/plugin-legacy@^1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-1.5.1.tgz#fef2a11c05d83f5ab13d2d04e52d75bac13c6e6c" + integrity sha512-g+0iy0X3NJRUSKZK+OCeSxNWnCuuE/6lsmr2WLWPOEt1vp6LdfHuNCYRooCm6s0ccTZ/SiumVk8vt9DWSYs+8A== + dependencies: + "@babel/standalone" "^7.14.9" + core-js "^3.16.0" + magic-string "^0.25.7" + regenerator-runtime "^0.13.9" + systemjs "^6.10.2" + "@vue/babel-helper-vue-jsx-merge-props@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" @@ -4859,6 +4882,11 @@ core-js@^2.4.0, core-js@^2.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.16.0: + version "3.16.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.1.tgz#f4485ce5c9f3c6a7cb18fa80488e08d362097249" + integrity sha512-AAkP8i35EbefU+JddyWi12AWE9f2N/qr/pwnDtWz4nyUIBGMJPX99ANFFRSw6FefM374lDujdtLDyhN2A/btHw== + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -10751,6 +10779,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== +regenerator-runtime@^0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" @@ -11870,6 +11903,11 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +systemjs@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.10.2.tgz#c9870217bddf9cfd25d12d4fcd1989541ef1207c" + integrity sha512-PwaC0Z6Y1E6gFekY2u38EC5+5w2M65jYVrD1aAcOptpHVhCwPIwPFJvYJyryQKUyeuQ5bKKI3PBHWNjdE9aizg== + table@^6.0.9: version "6.7.1" resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"