feat: improve dev experience (#89)

This commit is contained in:
pooya parsa 2021-04-15 21:17:44 +02:00 committed by GitHub
parent ce72ce6b07
commit e224818395
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 311 additions and 68 deletions

View File

@ -7,6 +7,6 @@ export default <BuildConfig>{
'src/index' 'src/index'
], ],
externals: [ externals: [
'nuxt3' '@nuxt/kit'
] ]
} }

View File

@ -17,9 +17,17 @@
"dist" "dist"
], ],
"devDependencies": { "devDependencies": {
"@nuxt/kit": "^0.4.0",
"@types/clear": "^0",
"@types/debounce-promise": "^3",
"@types/mri": "^1.1.0", "@types/mri": "^1.1.0",
"chokidar": "^3.5.1",
"clear": "^0.1.0",
"colorette": "^1.2.2", "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", "mri": "^1.1.6",
"unbuild": "^0.1.12", "unbuild": "^0.1.12",
"v8-compile-cache": "^2.3.0" "v8-compile-cache": "^2.3.0"

View File

@ -1,11 +1,13 @@
import { buildNuxt, loadNuxt } from '../utils/nuxt'
import { resolve } from 'path'
import { requireModule } from '../utils/cjs'
export async function invoke (args) { export async function invoke (args) {
const nuxt = await loadNuxt({ const rootDir = resolve(args._[0] || '.')
rootDir: args._[0],
for: 'build'
})
const { loadNuxt, buildNuxt } = requireModule('@nuxt/kit', rootDir)
const nuxt = await loadNuxt({ rootDir })
await buildNuxt(nuxt) await buildNuxt(nuxt)
} }

View File

@ -1,20 +1,73 @@
import { createServer } from '../utils/server' import { resolve } from 'path'
import { buildNuxt, loadNuxt } from '../utils/nuxt' 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) { export async function invoke (args) {
const server = createServer() const server = createServer()
const listenPromise = server.listen({ clipboard: true }) const listener = await server.listen({ clipboard: true, open: true })
const nuxt = await loadNuxt({ const rootDir = resolve(args._[0] || '.')
rootDir: args._[0],
for: 'dev' 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 load()
await buildNuxt(nuxt)
await listenPromise
} }
export const meta = { export const meta = {

View File

@ -1,9 +1,10 @@
import 'v8-compile-cache' import 'v8-compile-cache'
import mri from 'mri' import mri from 'mri'
import { red, cyan, green } from 'colorette' import { red, cyan } from 'colorette'
import { version } from '../package.json'
import { commands } from './commands' import { commands } from './commands'
import { showHelp } from './utils/help' import { showHelp } from './utils/help'
import { showBanner } from './utils/banner'
import { error } from './utils/log'
async function _main () { async function _main () {
const _argv = process.argv.slice(2) const _argv = process.argv.slice(2)
@ -11,7 +12,7 @@ async function _main () {
// @ts-ignore // @ts-ignore
let command = args._.shift() || 'usage' let command = args._.shift() || 'usage'
console.log(green(`Nuxt CLI v${version}`)) showBanner(command === 'dev')
if (!(command in commands)) { if (!(command in commands)) {
console.log('\n' + red('Invalid command ' + command)) console.log('\n' + red('Invalid command ' + command))
@ -36,10 +37,13 @@ async function _main () {
} }
function onFatalError (err) { function onFatalError (err) {
console.error(err) error(err)
process.exit(1) process.exit(1)
} }
process.on('unhandledRejection', err => error('[unhandledRejection]', err))
process.on('uncaughtException', err => error('[uncaughtException]', err))
export function main () { export function main () {
_main().catch(onFatalError) _main().catch(onFatalError)
} }

View File

@ -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}`))
}

View File

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

View File

@ -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()
}

View File

@ -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)

View File

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

View File

@ -1,10 +1,7 @@
import type { RequestListener } from 'http' import type { RequestListener } from 'http'
export function createServer () { export function createServer () {
const listener = createDynamicFunction <RequestListener>((_req, res) => { const listener = createDynamicFunction(createLoadingHandler('Loading...', 1))
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.end('<!DOCTYPE html><html><head><meta http-equiv="refresh" content="1"><head><body>...')
})
async function listen (opts) { async function listen (opts) {
const { listen } = await import('listhen') 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(`<!DOCTYPE html><html><head><meta http-equiv="refresh" content="${retryAfter || 60}"><head><body>${message}`)
}
}
function createDynamicFunction<T extends (...args) => any>(initialValue: T) { function createDynamicFunction<T extends (...args) => any>(initialValue: T) {
let fn: T = initialValue let fn: T = initialValue
return { return {

View File

@ -13,5 +13,9 @@ export default <BuildConfig>{
}, },
'src/index' 'src/index'
], ],
externals: ['webpack'] externals: [
'webpack',
'nuxt',
'nuxt3'
]
} }

View File

@ -1,4 +1,5 @@
import { resolve } from 'path' import { resolve } from 'path'
import { existsSync } from 'fs'
import defu from 'defu' import defu from 'defu'
import { applyDefaults } from 'untyped' import { applyDefaults } from 'untyped'
import * as rc from 'rc9' import * as rc from 'rc9'
@ -22,7 +23,7 @@ export function loadNuxtConfig (opts: LoadNuxtConfigOptions): NuxtOptions {
let nuxtConfig: any = {} let nuxtConfig: any = {}
if (nuxtConfigFile) { if (nuxtConfigFile && existsSync(nuxtConfigFile)) {
nuxtConfig = requireModule(nuxtConfigFile, { clearCache: true }) nuxtConfig = requireModule(nuxtConfigFile, { clearCache: true })
nuxtConfig = { ...nuxtConfig } nuxtConfig = { ...nuxtConfig }
nuxtConfig._nuxtConfigFile = nuxtConfigFile nuxtConfig._nuxtConfigFile = nuxtConfigFile

View File

@ -500,7 +500,12 @@ export default {
* ``` * ```
*/ */
watch: { 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))
))
}
}, },
/** /**

View File

@ -1,9 +1,12 @@
import { getContext } from 'unctx' import { getContext } from 'unctx'
import type { Nuxt } from './types/nuxt' import type { Nuxt } from './types/nuxt'
import type { NuxtConfig } from './types/config' 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. */ /** Direct access to the Nuxt context - see https://github.com/unjs/unctx. */
export const nuxtCtx = getContext<Nuxt>('nuxt') export const nuxtCtx = getContext<Nuxt>('nuxt')
/** /**
* Get access to Nuxt (if run within the Nuxt context) - see https://github.com/unjs/unctx. * 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) { export function defineNuxtConfig (config: NuxtConfig) {
return config 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> {
// 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<any> {
// 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)
}

View File

@ -16,6 +16,9 @@ export interface Nuxt {
hook: Nuxt['hooks']['hook'] hook: Nuxt['hooks']['hook']
callHook: Nuxt['hooks']['callHook'] callHook: Nuxt['hooks']['callHook']
ready: () => Promise<void>
close: () => Promise<void>
/** The production or development server */ /** The production or development server */
server?: any server?: any
} }

View File

@ -5,7 +5,7 @@ import jiti from 'jiti'
const _require = jiti(process.cwd()) const _require = jiti(process.cwd())
export interface ResolveModuleOptions { export interface ResolveModuleOptions {
paths?: string[] paths?: string | string[]
} }
export interface RequireModuleOptions extends ResolveModuleOptions { export interface RequireModuleOptions extends ResolveModuleOptions {
@ -82,7 +82,14 @@ export function requireModulePkg (id: string, opts: RequireModuleOptions = {}) {
/** Resolve the path of a module. */ /** Resolve the path of a module. */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return _require.resolve(id, { 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)
}) })
} }

View File

@ -43,7 +43,9 @@ export class Builder {
async function _build (builder: Builder) { async function _build (builder: Builder) {
const { nuxt } = builder const { nuxt } = builder
if (!nuxt.options.dev) {
await fsExtra.emptyDir(nuxt.options.buildDir) await fsExtra.emptyDir(nuxt.options.buildDir)
}
await generate(builder) await generate(builder)
if (nuxt.options.dev) { if (nuxt.options.dev) {

View File

@ -1,16 +1,20 @@
import Hookable from 'hookable' 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' import { initNitro } from './nitro'
export function createNuxt (options: NuxtOptions): Nuxt { export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = new Hookable() as any as Nuxt['hooks'] const hooks = new Hookable() as any as Nuxt['hooks']
return { const nuxt: Nuxt = {
options, options,
hooks, hooks,
callHook: hooks.callHook, 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) { async function initNuxt (nuxt: Nuxt) {
@ -21,7 +25,7 @@ async function initNuxt (nuxt: Nuxt) {
await initNitro(nuxt) await initNitro(nuxt)
// Init user modules // Init user modules
await nuxt.callHook('modules:before', nuxt) await nuxt.callHook('modules:before', { nuxt } as ModuleContainer)
const modulesToInstall = [ const modulesToInstall = [
...nuxt.options.buildModules, ...nuxt.options.buildModules,
...nuxt.options.modules, ...nuxt.options.modules,
@ -32,25 +36,13 @@ async function initNuxt (nuxt: Nuxt) {
await installModule(nuxt, m) await installModule(nuxt, m)
} }
await nuxt.callHook('modules:done', nuxt) await nuxt.callHook('modules:done', { nuxt } as ModuleContainer)
await nuxt.callHook('ready', nuxt) await nuxt.callHook('ready', nuxt)
} }
export interface LoadNuxtOptions extends LoadNuxtConfigOptions { export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
for?: 'dev' | 'build' const options = loadNuxtConfig(opts)
rootDir?: string
config?: NuxtConfig
}
export async function loadNuxt (loadOpts: LoadNuxtOptions = {}): Promise<Nuxt> {
const options = loadNuxtConfig({
config: {
dev: loadOpts.for === 'dev',
...loadOpts.config
},
...loadOpts
})
// Temp // Temp
const { appDir } = await import('@nuxt/app/meta') const { appDir } = await import('@nuxt/app/meta')
@ -60,7 +52,9 @@ export async function loadNuxt (loadOpts: LoadNuxtOptions = {}): Promise<Nuxt> {
const nuxt = createNuxt(options) const nuxt = createNuxt(options)
await initNuxt(nuxt) if (opts.ready !== false) {
await nuxt.ready()
}
return nuxt return nuxt
} }

View File

@ -104,7 +104,12 @@ async function buildServer (ctx: ViteBuildContext) {
outDir: 'dist/server', outDir: 'dist/server',
ssr: true, ssr: true,
rollupOptions: { 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) } as vite.InlineConfig)

View File

@ -2158,6 +2158,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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:*": "@types/connect@npm:*":
version: 3.4.34 version: 3.4.34
resolution: "@types/connect@npm:3.4.34" resolution: "@types/connect@npm:3.4.34"
@ -2167,6 +2174,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/debounce@npm:^1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "@types/debounce@npm:1.2.0" resolution: "@types/debounce@npm:1.2.0"
@ -4188,6 +4202,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "cli-cursor@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "cli-cursor@npm:3.1.0" resolution: "cli-cursor@npm:3.1.0"
@ -4968,6 +4989,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "debounce@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "debounce@npm:1.2.1" resolution: "debounce@npm:1.2.1"
@ -5064,6 +5092,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "deepmerge@npm:^4.2.2":
version: 4.2.2 version: 4.2.2
resolution: "deepmerge@npm:4.2.2" resolution: "deepmerge@npm:4.2.2"
@ -6332,7 +6367,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"flat@npm:^5.0.0": "flat@npm:^5.0.0, flat@npm:^5.0.2":
version: 5.0.2 version: 5.0.2
resolution: "flat@npm:5.0.2" resolution: "flat@npm:5.0.2"
bin: bin:
@ -8749,6 +8784,20 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "load-json-file@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "load-json-file@npm:1.1.0" resolution: "load-json-file@npm:1.1.0"
@ -9996,9 +10045,17 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "nuxt-cli@workspace:packages/cli" resolution: "nuxt-cli@workspace:packages/cli"
dependencies: dependencies:
"@nuxt/kit": ^0.4.0
"@types/clear": ^0
"@types/debounce-promise": ^3
"@types/mri": ^1.1.0 "@types/mri": ^1.1.0
chokidar: ^3.5.1
clear: ^0.1.0
colorette: ^1.2.2 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 mri: ^1.1.6
unbuild: ^0.1.12 unbuild: ^0.1.12
v8-compile-cache: ^2.3.0 v8-compile-cache: ^2.3.0