diff --git a/packages/nitro/src/build.ts b/packages/nitro/src/build.ts index 37161a602e..f6f34006a8 100644 --- a/packages/nitro/src/build.ts +++ b/packages/nitro/src/build.ts @@ -1,12 +1,12 @@ import { resolve, join } from 'upath' import consola from 'consola' import { rollup, watch as rollupWatch } from 'rollup' -import ora from 'ora' import { readFile, emptyDir, copy } from 'fs-extra' import { printFSTree } from './utils/tree' import { getRollupConfig } from './rollup/config' import { hl, prettyPath, serializeTemplate, writeFile, isDirectory } from './utils' import { NitroContext } from './context' +import { scanMiddleware } from './server/middleware' export async function prepare (nitroContext: NitroContext) { consola.info(`Nitro preset is ${hl(nitroContext.preset)}`) @@ -28,8 +28,7 @@ async function cleanupDir (dir: string) { } export async function generate (nitroContext: NitroContext) { - const spinner = ora() - spinner.start('Generating public...') + consola.start('Generating public...') const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client') if (await isDirectory(clientDist)) { @@ -41,7 +40,7 @@ export async function generate (nitroContext: NitroContext) { await copy(staticDir, nitroContext.output.publicDir) } - spinner.succeed('Generated public ' + prettyPath(nitroContext.output.publicDir)) + consola.success('Generated public ' + prettyPath(nitroContext.output.publicDir)) } export async function build (nitroContext: NitroContext) { @@ -60,18 +59,18 @@ export async function build (nitroContext: NitroContext) { } async function _build (nitroContext: NitroContext) { - const spinner = ora() + nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir) - spinner.start('Building server...') + consola.start('Building server...') const build = await rollup(nitroContext.rollupConfig).catch((error) => { - spinner.fail('Rollup error: ' + error.message) + consola.error('Rollup error: ' + error.message) throw error }) - spinner.start('Writing server bundle...') + consola.start('Writing server bundle...') await build.write(nitroContext.rollupConfig.output) - spinner.succeed('Server built') + consola.success('Server built') await printFSTree(nitroContext.output.serverDir) await nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext) @@ -80,11 +79,16 @@ async function _build (nitroContext: NitroContext) { } } -function _watch (nitroContext: NitroContext) { - const spinner = ora() - +async function _watch (nitroContext: NitroContext) { const watcher = rollupWatch(nitroContext.rollupConfig) + nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir, + (middleware, event, file) => { + nitroContext.scannedMiddleware = middleware + watcher.emit(event, file) + } + ) + let start watcher.on('event', (event) => { @@ -96,17 +100,17 @@ function _watch (nitroContext: NitroContext) { // Building an individual bundle case 'BUNDLE_START': start = Date.now() - spinner.start('Building Nitro...') return // Finished building all bundles case 'END': nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext) - return spinner.succeed(`Nitro built in ${Date.now() - start} ms`) + consola.success('Nitro built', start ? `in ${Date.now() - start} ms` : '') + return // Encountered an error while bundling case 'ERROR': - spinner.fail('Rollup error: ' + event.error) + consola.error('Rollup error: ' + event.error) // consola.error(event.error) } }) diff --git a/packages/nitro/src/compat.ts b/packages/nitro/src/compat.ts index 77d12e05c0..b5d7cb3aef 100644 --- a/packages/nitro/src/compat.ts +++ b/packages/nitro/src/compat.ts @@ -2,9 +2,9 @@ import fetch from 'node-fetch' import { resolve } from 'upath' import { build, generate, prepare } from './build' import { getNitroContext, NitroContext } from './context' -import { createDevServer } from './server' +import { createDevServer } from './server/dev' import { wpfs } from './utils/wpfs' -import { resolveMiddleware } from './middleware' +import { resolveMiddleware } from './server/middleware' export default function nuxt2CompatModule () { const { nuxt } = this diff --git a/packages/nitro/src/context.ts b/packages/nitro/src/context.ts index efcf184ada..f683e6b52f 100644 --- a/packages/nitro/src/context.ts +++ b/packages/nitro/src/context.ts @@ -6,13 +6,7 @@ import type { Preset } from '@nuxt/un' import { tryImport, resolvePath, detectTarget, extendPreset } from './utils' import * as PRESETS from './presets' import type { NodeExternalsOptions } from './rollup/plugins/externals' - -export interface ServerMiddleware { - route: string - handle: string - lazy?: boolean // Default is true - promisify?: boolean // Default is true -} +import type { ServerMiddleware } from './server/middleware' export interface NitroContext { timing: boolean @@ -28,6 +22,7 @@ export interface NitroContext { renderer: string serveStatic: boolean middleware: ServerMiddleware[] + scannedMiddleware: ServerMiddleware[] hooks: configHooksT nuxtHooks: configHooksT ignore: string[] @@ -45,6 +40,7 @@ export interface NitroContext { buildDir: string generateDir: string staticDir: string + serverDir: string routerBase: string publicPath: string isStatic: boolean @@ -79,6 +75,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N renderer: undefined, serveStatic: false, middleware: [], + scannedMiddleware: [], ignore: [], env: {}, hooks: {}, @@ -96,6 +93,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N buildDir: nuxtOptions.buildDir, generateDir: nuxtOptions.generate.dir, staticDir: nuxtOptions.dir.static, + serverDir: resolve(nuxtOptions.srcDir, (nuxtOptions.dir as any).server || 'server'), routerBase: nuxtOptions.router.base, publicPath: nuxtOptions.build.publicPath, isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev, diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 5fb5ca1103..338ed79d0a 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -1,6 +1,6 @@ export * from './build' export * from './context' -export * from './middleware' -export * from './server' +export * from './server/middleware' +export * from './server/dev' export * from './types' export { wpfs } from './utils/wpfs' diff --git a/packages/nitro/src/middleware.ts b/packages/nitro/src/middleware.ts deleted file mode 100644 index 84178c5342..0000000000 --- a/packages/nitro/src/middleware.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Middleware { - handle: string - route: string -} - -export function resolveMiddleware (serverMiddleware: any[], resolvePath: (string) => string) { - const middleware: Middleware[] = [] - const legacyMiddleware: Middleware[] = [] - - for (let m of serverMiddleware) { - if (typeof m === 'string') { m = { handler: m } } - const route = m.path || m.route || '/' - const handle = m.handler || m.handle - if (typeof handle !== 'string' || typeof route !== 'string') { - legacyMiddleware.push(m) - } else { - middleware.push({ - ...m, - handle: resolvePath(handle), - route - }) - } - } - - return { - middleware, - legacyMiddleware - } -} diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index 9f3172b57f..17e77e595a 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -148,11 +148,16 @@ export const getRollupConfig = (nitroContext: NitroContext) => { } // Middleware - const _middleware = [...nitroContext.middleware] - if (nitroContext.serveStatic) { - _middleware.unshift({ route: '/', handle: '~runtime/server/static' }) - } - rollupConfig.plugins.push(middleware(_middleware)) + rollupConfig.plugins.push(middleware(() => { + const _middleware = [ + ...nitroContext.scannedMiddleware, + ...nitroContext.middleware + ] + if (nitroContext.serveStatic) { + _middleware.unshift({ route: '/', handle: '~runtime/server/static' }) + } + return _middleware + })) // Polyfill rollupConfig.plugins.push(virtual({ @@ -187,7 +192,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => { ignore: [ nitroContext._internal.runtimeDir, ...(nitroContext._nuxt.dev ? [] : [nitroContext._nuxt.buildDir]), - ...nitroContext.middleware.map(m => m.handle) + ...nitroContext.middleware.map(m => m.handle), + nitroContext._nuxt.serverDir ], traceOptions: { base: nitroContext._nuxt.rootDir diff --git a/packages/nitro/src/rollup/plugins/middleware.ts b/packages/nitro/src/rollup/plugins/middleware.ts index fa78b98627..671a9c4221 100644 --- a/packages/nitro/src/rollup/plugins/middleware.ts +++ b/packages/nitro/src/rollup/plugins/middleware.ts @@ -1,12 +1,26 @@ import hasha from 'hasha' -import virtual from '@rollup/plugin-virtual' -import type { ServerMiddleware } from '../../context' +import { relative } from 'upath' +import { table, getBorderCharacters } from 'table' +import isPrimitive from 'is-primitive' +import type { ServerMiddleware } from '../../server/middleware' +import virtual from './virtual' -export function middleware (middleware: ServerMiddleware[]) { +export function middleware (getMiddleware: () => ServerMiddleware[]) { const getImportId = p => '_' + hasha(p).substr(0, 6) + let lastDump = '' + return virtual({ - '~serverMiddleware': ` + '~serverMiddleware': () => { + const middleware = getMiddleware() + const dumped = dumpMiddleware(middleware) + if (dumped !== lastDump) { + lastDump = dumped + if (middleware.length) { + console.log('\n\nNitro middleware:\n' + dumped) + } + } + return ` ${middleware.filter(m => m.lazy === false).map(m => `import ${getImportId(m.handle)} from '${m.handle}';`).join('\n')} ${middleware.filter(m => m.lazy !== false).map(m => `const ${getImportId(m.handle)} = () => import('${m.handle}');`).join('\n')} @@ -17,5 +31,31 @@ const middleware = [ export default middleware ` + } }) } + +function dumpMiddleware (middleware: ServerMiddleware[]) { + const data = middleware.map(({ route, handle, ...props }) => { + return [ + (route && route !== '/') ? route : '[global]', + relative(process.cwd(), handle), + dumpObject(props) + ] + }) + return table([ + ['Route', 'Handle', 'Options'], + ...data + ], { + border: getBorderCharacters('norc') + }) +} + +function dumpObject (obj: any) { + const items = [] + for (const key in obj) { + const val = obj[key] + items.push(`${key}: ${isPrimitive(val) ? val : JSON.stringify(val)}`) + } + return items.join(', ') +} diff --git a/packages/nitro/src/rollup/plugins/virtual.ts b/packages/nitro/src/rollup/plugins/virtual.ts new file mode 100644 index 0000000000..bf0e0a1c79 --- /dev/null +++ b/packages/nitro/src/rollup/plugins/virtual.ts @@ -0,0 +1,49 @@ +// Based on https://github.com/rollup/plugins/blob/master/packages/virtual/src/index.ts +import * as path from 'path' + +import { Plugin } from 'rollup' + +type UnresolvedModule = string | (() => string) +export interface RollupVirtualOptions { + [id: string]: UnresolvedModule; +} + +const PREFIX = '\0virtual:' + +const resolveModule = (m: UnresolvedModule) => typeof m === 'function' ? m() : m + +export default function virtual (modules: RollupVirtualOptions): Plugin { + const resolvedIds = new Map string)>() + + Object.keys(modules).forEach((id) => { + resolvedIds.set(path.resolve(id), modules[id]) + }) + + return { + name: 'virtual', + + resolveId (id, importer) { + if (id in modules) { return PREFIX + id } + + if (importer) { + const importerNoPrefix = importer.startsWith(PREFIX) + ? importer.slice(PREFIX.length) + : importer + const resolved = path.resolve(path.dirname(importerNoPrefix), id) + if (resolvedIds.has(resolved)) { return PREFIX + resolved } + } + + return null + }, + + load (id) { + if (!id.startsWith(PREFIX)) { + return null + } + const idNoPrefix = id.slice(PREFIX.length) + return idNoPrefix in modules + ? resolveModule(modules[idNoPrefix]) + : resolveModule(resolvedIds.get(idNoPrefix)) + } + } +} diff --git a/packages/nitro/src/server.ts b/packages/nitro/src/server/dev.ts similarity index 98% rename from packages/nitro/src/server.ts rename to packages/nitro/src/server/dev.ts index ba02b36960..3b7873678d 100644 --- a/packages/nitro/src/server.ts +++ b/packages/nitro/src/server/dev.ts @@ -8,7 +8,7 @@ import serveStatic from 'serve-static' import servePlaceholder from 'serve-placeholder' import { createProxy } from 'http-proxy' import { stat } from 'fs-extra' -import type { NitroContext } from './context' +import type { NitroContext } from '../context' export function createDevServer (nitroContext: NitroContext) { // Worker diff --git a/packages/nitro/src/server/middleware.ts b/packages/nitro/src/server/middleware.ts new file mode 100644 index 0000000000..9017a8af52 --- /dev/null +++ b/packages/nitro/src/server/middleware.ts @@ -0,0 +1,76 @@ +import { resolve, join, extname } from 'upath' +import { joinURL } from 'ufo' +import globby from 'globby' +import { watch } from 'chokidar' + +export interface ServerMiddleware { + route: string + handle: string + lazy?: boolean // Default is true + promisify?: boolean // Default is true +} + +function filesToMiddleware (files: string[], baseDir: string, basePath: string, overrides?: Partial): ServerMiddleware[] { + return files.map((file) => { + const route = joinURL(basePath, file.substr(0, file.length - extname(file).length)) + const handle = resolve(baseDir, file) + return { + route, + handle + } + }) + .sort((a, b) => a.route.localeCompare(b.route)) + .map(m => ({ ...m, ...overrides })) +} + +export function scanMiddleware (serverDir: string, onChange?: Function): Promise { + const pattern = '**/*.{js,ts}' + const globalDir = resolve(serverDir, 'middleware') + const apiDir = resolve(serverDir, 'api') + + const scan = async () => { + const globalFiles = await globby(pattern, { cwd: globalDir }) + const apiFiles = await globby(pattern, { cwd: apiDir }) + return [ + ...filesToMiddleware(globalFiles, globalDir, '/', { route: '/' }), + ...filesToMiddleware(apiFiles, apiDir, '/api', { lazy: true }) + ] + } + + if (typeof onChange === 'function') { + const watcher = watch([ + join(globalDir, pattern), + join(apiDir, pattern) + ], { ignoreInitial: true }) + watcher.on('all', async (event, file) => { + onChange(await scan(), event, file) + }) + } + + return scan() +} + +export function resolveMiddleware (serverMiddleware: any[], resolvePath: (string) => string) { + const middleware: ServerMiddleware[] = [] + const legacyMiddleware: ServerMiddleware[] = [] + + for (let m of serverMiddleware) { + if (typeof m === 'string') { m = { handler: m } } + const route = m.path || m.route || '/' + const handle = m.handler || m.handle + if (typeof handle !== 'string' || typeof route !== 'string') { + legacyMiddleware.push(m) + } else { + middleware.push({ + ...m, + handle: resolvePath(handle), + route + }) + } + } + + return { + middleware, + legacyMiddleware + } +}