diff --git a/packages/cli/build.config.ts b/packages/cli/build.config.ts index 0a945810ee..ac63010e34 100644 --- a/packages/cli/build.config.ts +++ b/packages/cli/build.config.ts @@ -7,6 +7,6 @@ export default { 'src/index' ], externals: [ - 'nuxt3' + '@nuxt/kit' ] } diff --git a/packages/cli/package.json b/packages/cli/package.json index 657599d92b..ce7f20729a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,9 +17,17 @@ "dist" ], "devDependencies": { + "@nuxt/kit": "^0.4.0", + "@types/clear": "^0", + "@types/debounce-promise": "^3", "@types/mri": "^1.1.0", + "chokidar": "^3.5.1", + "clear": "^0.1.0", "colorette": "^1.2.2", - "listhen": "^0.2.3", + "debounce-promise": "^3.1.2", + "deep-object-diff": "^1.1.0", + "flat": "^5.0.2", + "listhen": "^0.2.4", "mri": "^1.1.6", "unbuild": "^0.1.12", "v8-compile-cache": "^2.3.0" diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 10c7cf54f0..732ba0853a 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,11 +1,13 @@ -import { buildNuxt, loadNuxt } from '../utils/nuxt' + +import { resolve } from 'path' +import { requireModule } from '../utils/cjs' export async function invoke (args) { - const nuxt = await loadNuxt({ - rootDir: args._[0], - for: 'build' - }) + const rootDir = resolve(args._[0] || '.') + const { loadNuxt, buildNuxt } = requireModule('@nuxt/kit', rootDir) + + const nuxt = await loadNuxt({ rootDir }) await buildNuxt(nuxt) } diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 3ef88898a9..71008a8cd2 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -1,20 +1,73 @@ -import { createServer } from '../utils/server' -import { buildNuxt, loadNuxt } from '../utils/nuxt' +import { resolve } from 'path' +import chokidar from 'chokidar' +import debounce from 'debounce-promise' +import { createServer, createLoadingHandler } from '../utils/server' +import { showBanner } from '../utils/banner' +import { requireModule } from '../utils/cjs' +import { error, info } from '../utils/log' +import { diff, printDiff } from '../utils/diff' export async function invoke (args) { const server = createServer() - const listenPromise = server.listen({ clipboard: true }) + const listener = await server.listen({ clipboard: true, open: true }) - const nuxt = await loadNuxt({ - rootDir: args._[0], - for: 'dev' + const rootDir = resolve(args._[0] || '.') + + const { loadNuxt, buildNuxt } = requireModule('@nuxt/kit', rootDir) + + const watcherFiles = new Set() + const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 0 }) + + let currentNuxt + const load = async () => { + try { + const newNuxt = await loadNuxt({ rootDir, dev: true, ready: false }) + watcherFiles.add(newNuxt.options.watch) + + let configChanges + if (currentNuxt) { + configChanges = diff(currentNuxt.options, newNuxt.options, [ + 'generate.staticAssets.version', + 'env.NITRO_PRESET' + ]) + server.setApp(createLoadingHandler('Restarting...', 1)) + await currentNuxt.close() + currentNuxt = newNuxt + } else { + currentNuxt = newNuxt + } + + showBanner(true) + listener.showURL() + + if (configChanges) { + if (configChanges.length) { + info('Nuxt config updated:') + printDiff(configChanges) + } else { + info('Restarted nuxt due to config changes') + } + } + + await currentNuxt.ready() + await buildNuxt(currentNuxt) + server.setApp(currentNuxt.server.app) + } catch (err) { + error('Cannot load nuxt.', err) + server.setApp(createLoadingHandler( + 'Error while loading nuxt. Please check console and fix errors.' + )) + } + } + + const dLoad = debounce(load, 250) + watcher.on('all', (_event, file) => { + if (watcherFiles.has(file) || file.includes('nuxt.config')) { + dLoad() + } }) - server.setApp(nuxt.server.app) - - await buildNuxt(nuxt) - - await listenPromise + await load() } export const meta = { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c04d1af44d..92232f5f85 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,9 +1,10 @@ import 'v8-compile-cache' import mri from 'mri' -import { red, cyan, green } from 'colorette' -import { version } from '../package.json' +import { red, cyan } from 'colorette' import { commands } from './commands' import { showHelp } from './utils/help' +import { showBanner } from './utils/banner' +import { error } from './utils/log' async function _main () { const _argv = process.argv.slice(2) @@ -11,7 +12,7 @@ async function _main () { // @ts-ignore let command = args._.shift() || 'usage' - console.log(green(`Nuxt CLI v${version}`)) + showBanner(command === 'dev') if (!(command in commands)) { console.log('\n' + red('Invalid command ' + command)) @@ -36,10 +37,13 @@ async function _main () { } function onFatalError (err) { - console.error(err) + error(err) process.exit(1) } +process.on('unhandledRejection', err => error('[unhandledRejection]', err)) +process.on('uncaughtException', err => error('[uncaughtException]', err)) + export function main () { _main().catch(onFatalError) } diff --git a/packages/cli/src/utils/banner.ts b/packages/cli/src/utils/banner.ts new file mode 100644 index 0000000000..753ac99f09 --- /dev/null +++ b/packages/cli/src/utils/banner.ts @@ -0,0 +1,8 @@ +import clear from 'clear' +import { green } from 'colorette' +import { version } from '../../package.json' + +export function showBanner (_clear: boolean) { + if (_clear) { clear() } + console.log(green(`Nuxt CLI v${version}`)) +} diff --git a/packages/cli/src/utils/cjs.ts b/packages/cli/src/utils/cjs.ts new file mode 100644 index 0000000000..8ac42d2b34 --- /dev/null +++ b/packages/cli/src/utils/cjs.ts @@ -0,0 +1,16 @@ +export function resolveModule (id, paths?) { + return require.resolve(id, { + paths: [].concat( + // @ts-ignore + global.__NUXT_PREPATHS__, + paths, + process.cwd(), + // @ts-ignore + global.__NUXT_PATHS__ + ).filter(Boolean) + }) +} + +export function requireModule (id, paths?) { + return require(resolveModule(id, paths)) +} diff --git a/packages/cli/src/utils/diff.ts b/packages/cli/src/utils/diff.ts new file mode 100644 index 0000000000..9b6ba04e76 --- /dev/null +++ b/packages/cli/src/utils/diff.ts @@ -0,0 +1,31 @@ +import flatten from 'flat' +import { detailedDiff } from 'deep-object-diff' +import { green, red, blue, cyan } from 'colorette' + +function normalizeDiff (diffObj, type, ignore) { + return Object.entries(flatten(diffObj)) + .map(([key, value]) => ({ key, value, type })) + .filter(item => !ignore.includes(item.key) && typeof item.value !== 'function') +} + +export function diff (a, b, ignore) { + const _diff: any = detailedDiff(a, b) + return [].concat( + normalizeDiff(_diff.added, 'added', ignore), + normalizeDiff(_diff.deleted, 'deleted', ignore), + normalizeDiff(_diff.updated, 'updated', ignore) + ) +} + +const typeMap = { + added: green('added'), + deleted: red('deleted'), + updated: blue('updated') +} + +export function printDiff (diff) { + for (const item of diff) { + console.log(' ', typeMap[item.type] || item.type, cyan(item.key), item.value ? `~> ${cyan(item.value)}` : '') + } + console.log() +} diff --git a/packages/cli/src/utils/log.ts b/packages/cli/src/utils/log.ts new file mode 100644 index 0000000000..ab3c895b07 --- /dev/null +++ b/packages/cli/src/utils/log.ts @@ -0,0 +1,5 @@ +import { red, yellow, cyan } from 'colorette' + +export const error = (...args) => console.error(red('[error]'), ...args) +export const warn = (...args) => console.warn(yellow('[warn]'), ...args) +export const info = (...args) => console.info(cyan('[info]'), ...args) diff --git a/packages/cli/src/utils/nuxt.ts b/packages/cli/src/utils/nuxt.ts deleted file mode 100644 index 1da22da115..0000000000 --- a/packages/cli/src/utils/nuxt.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function getNuxtPkg () { - return Promise.resolve(require('nuxt3')) -} - -export async function loadNuxt (opts) { - const { loadNuxt } = await getNuxtPkg() - return loadNuxt(opts) -} - -export async function buildNuxt (nuxt) { - const { build } = await getNuxtPkg() - return build(nuxt) -} diff --git a/packages/cli/src/utils/server.ts b/packages/cli/src/utils/server.ts index 2e23acb640..1fa2797a9f 100644 --- a/packages/cli/src/utils/server.ts +++ b/packages/cli/src/utils/server.ts @@ -1,10 +1,7 @@ import type { RequestListener } from 'http' export function createServer () { - const listener = createDynamicFunction ((_req, res) => { - res.setHeader('Content-Type', 'text/html; charset=UTF-8') - res.end('...') - }) + const listener = createDynamicFunction(createLoadingHandler('Loading...', 1)) async function listen (opts) { const { listen } = await import('listhen') @@ -17,6 +14,15 @@ export function createServer () { } } +export function createLoadingHandler (message: string, retryAfter = 60): RequestListener { + return (_req, res) => { + res.setHeader('Content-Type', 'text/html; charset=UTF-8') + res.statusCode = 503 /* Service Unavailable */ + res.setHeader('Retry-After', retryAfter) + res.end(`${message}`) + } +} + function createDynamicFunction any>(initialValue: T) { let fn: T = initialValue return { diff --git a/packages/kit/build.config.ts b/packages/kit/build.config.ts index cb563b1e0a..381bf45baf 100644 --- a/packages/kit/build.config.ts +++ b/packages/kit/build.config.ts @@ -13,5 +13,9 @@ export default { }, 'src/index' ], - externals: ['webpack'] + externals: [ + 'webpack', + 'nuxt', + 'nuxt3' + ] } diff --git a/packages/kit/src/config/load.ts b/packages/kit/src/config/load.ts index 22ba9cdd48..c605af1e74 100644 --- a/packages/kit/src/config/load.ts +++ b/packages/kit/src/config/load.ts @@ -1,4 +1,5 @@ import { resolve } from 'path' +import { existsSync } from 'fs' import defu from 'defu' import { applyDefaults } from 'untyped' import * as rc from 'rc9' @@ -22,7 +23,7 @@ export function loadNuxtConfig (opts: LoadNuxtConfigOptions): NuxtOptions { let nuxtConfig: any = {} - if (nuxtConfigFile) { + if (nuxtConfigFile && existsSync(nuxtConfigFile)) { nuxtConfig = requireModule(nuxtConfigFile, { clearCache: true }) nuxtConfig = { ...nuxtConfig } nuxtConfig._nuxtConfigFile = nuxtConfigFile diff --git a/packages/kit/src/config/schema/_common.ts b/packages/kit/src/config/schema/_common.ts index 2af687aa0d..caf8669aa0 100644 --- a/packages/kit/src/config/schema/_common.ts +++ b/packages/kit/src/config/schema/_common.ts @@ -500,7 +500,12 @@ export default { * ``` */ watch: { - $resolve: (val, get) => [].concat(val, get('_nuxtConfigFiles')).filter(Boolean) + $resolve: (val, get) => { + const rootDir = get('rootDir') + return Array.from(new Set([].concat(val, get('_nuxtConfigFiles')) + .filter(Boolean).map(p => resolve(rootDir, p)) + )) + } }, /** diff --git a/packages/kit/src/nuxt.ts b/packages/kit/src/nuxt.ts index 0a3f7a16cb..5a0c1b63ee 100644 --- a/packages/kit/src/nuxt.ts +++ b/packages/kit/src/nuxt.ts @@ -1,9 +1,12 @@ import { getContext } from 'unctx' import type { Nuxt } from './types/nuxt' import type { NuxtConfig } from './types/config' +import type { LoadNuxtConfigOptions } from './config/load' +import { requireModule } from './utils/cjs' /** Direct access to the Nuxt context - see https://github.com/unjs/unctx. */ export const nuxtCtx = getContext('nuxt') + /** * Get access to Nuxt (if run within the Nuxt context) - see https://github.com/unjs/unctx. * @@ -27,3 +30,45 @@ export const useNuxt = nuxtCtx.use export function defineNuxtConfig (config: NuxtConfig) { return config } + +export interface LoadNuxtOptions extends LoadNuxtConfigOptions { + rootDir: string + dev?: boolean + config?: NuxtConfig + version?: 2 | 3 + configFile?: string + ready?: boolean +} + +export async function loadNuxt (opts: LoadNuxtOptions): Promise { + // Nuxt 3 + if (opts.version !== 2) { + const { loadNuxt } = requireModule('nuxt3', { paths: opts.rootDir }) + const nuxt = await loadNuxt(opts) + return nuxt + } + + // Compat + // @ts-ignore + const { loadNuxt } = requireModule('nuxt', { paths: opts.rootDir }) + const nuxt = await loadNuxt({ + rootDir: opts.rootDir, + for: opts.dev ? 'dev' : 'build', + configOverrides: opts.config, + ready: opts.ready + }) + return nuxt as Nuxt +} + +export function buildNuxt (nuxt: Nuxt): Promise { + // Nuxt 3 + if (nuxt.options._majorVersion === 3) { + const { build } = requireModule('nuxt3', { paths: nuxt.options.rootDir }) + return build(nuxt) + } + + // Compat + // @ts-ignore + const { build } = requireModule('nuxt', { paths: nuxt.options.rootDir }) + return build(nuxt) +} diff --git a/packages/kit/src/types/nuxt.ts b/packages/kit/src/types/nuxt.ts index 3bfb51b245..8340f7348e 100644 --- a/packages/kit/src/types/nuxt.ts +++ b/packages/kit/src/types/nuxt.ts @@ -16,6 +16,9 @@ export interface Nuxt { hook: Nuxt['hooks']['hook'] callHook: Nuxt['hooks']['callHook'] + ready: () => Promise + close: () => Promise + /** The production or development server */ server?: any } diff --git a/packages/kit/src/utils/cjs.ts b/packages/kit/src/utils/cjs.ts index 8eba787c2b..f2593f22b6 100644 --- a/packages/kit/src/utils/cjs.ts +++ b/packages/kit/src/utils/cjs.ts @@ -5,7 +5,7 @@ import jiti from 'jiti' const _require = jiti(process.cwd()) export interface ResolveModuleOptions { - paths?: string[] + paths?: string | string[] } export interface RequireModuleOptions extends ResolveModuleOptions { @@ -82,7 +82,14 @@ export function requireModulePkg (id: string, opts: RequireModuleOptions = {}) { /** Resolve the path of a module. */ export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { return _require.resolve(id, { - paths: opts.paths + paths: [].concat( + // @ts-ignore + global.__NUXT_PREPATHS__, + opts.paths, + process.cwd(), + // @ts-ignore + global.__NUXT_PATHS__ + ).filter(Boolean) }) } diff --git a/packages/nuxt3/src/builder.ts b/packages/nuxt3/src/builder.ts index 74f2203ab7..4d7bc2ece1 100644 --- a/packages/nuxt3/src/builder.ts +++ b/packages/nuxt3/src/builder.ts @@ -43,7 +43,9 @@ export class Builder { async function _build (builder: Builder) { const { nuxt } = builder - await fsExtra.emptyDir(nuxt.options.buildDir) + if (!nuxt.options.dev) { + await fsExtra.emptyDir(nuxt.options.buildDir) + } await generate(builder) if (nuxt.options.dev) { diff --git a/packages/nuxt3/src/nuxt.ts b/packages/nuxt3/src/nuxt.ts index 576f4692bd..edd1fba38d 100644 --- a/packages/nuxt3/src/nuxt.ts +++ b/packages/nuxt3/src/nuxt.ts @@ -1,16 +1,20 @@ import Hookable from 'hookable' -import { loadNuxtConfig, LoadNuxtConfigOptions, Nuxt, NuxtOptions, installModule, NuxtConfig } from '@nuxt/kit' +import { loadNuxtConfig, LoadNuxtOptions, Nuxt, NuxtOptions, installModule, ModuleContainer } from '@nuxt/kit' import { initNitro } from './nitro' export function createNuxt (options: NuxtOptions): Nuxt { const hooks = new Hookable() as any as Nuxt['hooks'] - return { + const nuxt: Nuxt = { options, hooks, callHook: hooks.callHook, - hook: hooks.hook + hook: hooks.hook, + ready: () => initNuxt(nuxt), + close: () => Promise.resolve(hooks.callHook('close', nuxt)) } + + return nuxt } async function initNuxt (nuxt: Nuxt) { @@ -21,7 +25,7 @@ async function initNuxt (nuxt: Nuxt) { await initNitro(nuxt) // Init user modules - await nuxt.callHook('modules:before', nuxt) + await nuxt.callHook('modules:before', { nuxt } as ModuleContainer) const modulesToInstall = [ ...nuxt.options.buildModules, ...nuxt.options.modules, @@ -32,25 +36,13 @@ async function initNuxt (nuxt: Nuxt) { await installModule(nuxt, m) } - await nuxt.callHook('modules:done', nuxt) + await nuxt.callHook('modules:done', { nuxt } as ModuleContainer) await nuxt.callHook('ready', nuxt) } -export interface LoadNuxtOptions extends LoadNuxtConfigOptions { - for?: 'dev' | 'build' - rootDir?: string - config?: NuxtConfig -} - -export async function loadNuxt (loadOpts: LoadNuxtOptions = {}): Promise { - const options = loadNuxtConfig({ - config: { - dev: loadOpts.for === 'dev', - ...loadOpts.config - }, - ...loadOpts - }) +export async function loadNuxt (opts: LoadNuxtOptions): Promise { + const options = loadNuxtConfig(opts) // Temp const { appDir } = await import('@nuxt/app/meta') @@ -60,7 +52,9 @@ export async function loadNuxt (loadOpts: LoadNuxtOptions = {}): Promise { const nuxt = createNuxt(options) - await initNuxt(nuxt) + if (opts.ready !== false) { + await nuxt.ready() + } return nuxt } diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index c9eeec8317..479aea9a9f 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -104,7 +104,12 @@ async function buildServer (ctx: ViteBuildContext) { outDir: 'dist/server', ssr: true, rollupOptions: { - input: resolve(ctx.nuxt.options.buildDir, './entry.server.js') + input: resolve(ctx.nuxt.options.buildDir, './entry.server.js'), + onwarn (warning, rollupWarn) { + if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) { + rollupWarn(warning) + } + } } } } as vite.InlineConfig) diff --git a/yarn.lock b/yarn.lock index 9d8b9995da..9b3af00ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2158,6 +2158,13 @@ __metadata: languageName: node linkType: hard +"@types/clear@npm:^0": + version: 0.1.1 + resolution: "@types/clear@npm:0.1.1" + checksum: ded7ef18386cb110b1293e91640b11a03c1006a9c2c04336fad39b55a7bfff8f700468748432a3e6614cbeaaed8efc0d4608d003372784128129c230ac061cf3 + languageName: node + linkType: hard + "@types/connect@npm:*": version: 3.4.34 resolution: "@types/connect@npm:3.4.34" @@ -2167,6 +2174,13 @@ __metadata: languageName: node linkType: hard +"@types/debounce-promise@npm:^3": + version: 3.1.3 + resolution: "@types/debounce-promise@npm:3.1.3" + checksum: cdd735adf58d7c724cf3be35e799a657e0b9e71690fc2c64c660de1f8ba0f57fde3244ca515ae9c448ea47c293eef5b414e7f8aa1b2d782b78db8af0ca811ee6 + languageName: node + linkType: hard + "@types/debounce@npm:^1.2.0": version: 1.2.0 resolution: "@types/debounce@npm:1.2.0" @@ -4188,6 +4202,13 @@ __metadata: languageName: node linkType: hard +"clear@npm:^0.1.0": + version: 0.1.0 + resolution: "clear@npm:0.1.0" + checksum: 5bb327928ac437961b25b51c8ab637b9c6dd74e53ff1d76928c71900f27ef8774da778da81b6cc7d9aba3467da508be9bbd4968a347ba6467761643111d34948 + languageName: node + linkType: hard + "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -4968,6 +4989,13 @@ __metadata: languageName: node linkType: hard +"debounce-promise@npm:^3.1.2": + version: 3.1.2 + resolution: "debounce-promise@npm:3.1.2" + checksum: 7ae406b3daf581a9cb541d10400c2843057da57506737a36ca9b2e303b5515b0f786d392c678ef1fcfead4a2cf6075dbb16d9897166692cb3956a2ad835be5a4 + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -5064,6 +5092,13 @@ __metadata: languageName: node linkType: hard +"deep-object-diff@npm:^1.1.0": + version: 1.1.0 + resolution: "deep-object-diff@npm:1.1.0" + checksum: 2667c43932d14c908d03f813cd2846a626bda1d320f8f1dee3c7bd00ff8058713bb8f5d995fabd617e09229cf64bf16e53579233e29da870fd27bde23d5f5b20 + languageName: node + linkType: hard + "deepmerge@npm:^4.2.2": version: 4.2.2 resolution: "deepmerge@npm:4.2.2" @@ -6332,7 +6367,7 @@ __metadata: languageName: node linkType: hard -"flat@npm:^5.0.0": +"flat@npm:^5.0.0, flat@npm:^5.0.2": version: 5.0.2 resolution: "flat@npm:5.0.2" bin: @@ -8749,6 +8784,20 @@ __metadata: languageName: node linkType: hard +"listhen@npm:^0.2.4": + version: 0.2.4 + resolution: "listhen@npm:0.2.4" + dependencies: + clipboardy: ^2.3.0 + colorette: ^1.2.2 + get-port-please: ^2.1.0 + http-shutdown: ^1.2.2 + open: ^8.0.5 + selfsigned: ^1.10.8 + checksum: e297ecd7215f907b26ffbd5df03fbb25d92ef01a5d3f92e1134d0c599d76f98ab150453103c05800f23564df56d3aa7097dd01d3b6383990089f4c46bc51d754 + languageName: node + linkType: hard + "load-json-file@npm:^1.0.0": version: 1.1.0 resolution: "load-json-file@npm:1.1.0" @@ -9996,9 +10045,17 @@ __metadata: version: 0.0.0-use.local resolution: "nuxt-cli@workspace:packages/cli" dependencies: + "@nuxt/kit": ^0.4.0 + "@types/clear": ^0 + "@types/debounce-promise": ^3 "@types/mri": ^1.1.0 + chokidar: ^3.5.1 + clear: ^0.1.0 colorette: ^1.2.2 - listhen: ^0.2.3 + debounce-promise: ^3.1.2 + deep-object-diff: ^1.1.0 + flat: ^5.0.2 + listhen: ^0.2.4 mri: ^1.1.6 unbuild: ^0.1.12 v8-compile-cache: ^2.3.0