feat: support server directory (#132)

* feat: support server directory

* fix sorting and global

* lazy load api

* pretty print opts

* fix: hide table when no middleware
This commit is contained in:
pooya parsa 2021-02-18 17:06:58 +01:00 committed by GitHub
parent 27aef1489f
commit 85da52d390
10 changed files with 210 additions and 66 deletions

View File

@ -1,12 +1,12 @@
import { resolve, join } from 'upath' import { resolve, join } from 'upath'
import consola from 'consola' import consola from 'consola'
import { rollup, watch as rollupWatch } from 'rollup' import { rollup, watch as rollupWatch } from 'rollup'
import ora from 'ora'
import { readFile, emptyDir, copy } from 'fs-extra' import { readFile, emptyDir, copy } from 'fs-extra'
import { printFSTree } from './utils/tree' import { printFSTree } from './utils/tree'
import { getRollupConfig } from './rollup/config' import { getRollupConfig } from './rollup/config'
import { hl, prettyPath, serializeTemplate, writeFile, isDirectory } from './utils' import { hl, prettyPath, serializeTemplate, writeFile, isDirectory } from './utils'
import { NitroContext } from './context' import { NitroContext } from './context'
import { scanMiddleware } from './server/middleware'
export async function prepare (nitroContext: NitroContext) { export async function prepare (nitroContext: NitroContext) {
consola.info(`Nitro preset is ${hl(nitroContext.preset)}`) consola.info(`Nitro preset is ${hl(nitroContext.preset)}`)
@ -28,8 +28,7 @@ async function cleanupDir (dir: string) {
} }
export async function generate (nitroContext: NitroContext) { export async function generate (nitroContext: NitroContext) {
const spinner = ora() consola.start('Generating public...')
spinner.start('Generating public...')
const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client') const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client')
if (await isDirectory(clientDist)) { if (await isDirectory(clientDist)) {
@ -41,7 +40,7 @@ export async function generate (nitroContext: NitroContext) {
await copy(staticDir, nitroContext.output.publicDir) 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) { export async function build (nitroContext: NitroContext) {
@ -60,18 +59,18 @@ export async function build (nitroContext: NitroContext) {
} }
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) => { const build = await rollup(nitroContext.rollupConfig).catch((error) => {
spinner.fail('Rollup error: ' + error.message) consola.error('Rollup error: ' + error.message)
throw error throw error
}) })
spinner.start('Writing server bundle...') consola.start('Writing server bundle...')
await build.write(nitroContext.rollupConfig.output) await build.write(nitroContext.rollupConfig.output)
spinner.succeed('Server built') consola.success('Server built')
await printFSTree(nitroContext.output.serverDir) await printFSTree(nitroContext.output.serverDir)
await nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext) await nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext)
@ -80,11 +79,16 @@ async function _build (nitroContext: NitroContext) {
} }
} }
function _watch (nitroContext: NitroContext) { async function _watch (nitroContext: NitroContext) {
const spinner = ora()
const watcher = rollupWatch(nitroContext.rollupConfig) const watcher = rollupWatch(nitroContext.rollupConfig)
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir,
(middleware, event, file) => {
nitroContext.scannedMiddleware = middleware
watcher.emit(event, file)
}
)
let start let start
watcher.on('event', (event) => { watcher.on('event', (event) => {
@ -96,17 +100,17 @@ function _watch (nitroContext: NitroContext) {
// Building an individual bundle // Building an individual bundle
case 'BUNDLE_START': case 'BUNDLE_START':
start = Date.now() start = Date.now()
spinner.start('Building Nitro...')
return return
// Finished building all bundles // Finished building all bundles
case 'END': case 'END':
nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext) 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 // Encountered an error while bundling
case 'ERROR': case 'ERROR':
spinner.fail('Rollup error: ' + event.error) consola.error('Rollup error: ' + event.error)
// consola.error(event.error) // consola.error(event.error)
} }
}) })

View File

@ -2,9 +2,9 @@ import fetch from 'node-fetch'
import { resolve } from 'upath' import { resolve } from 'upath'
import { build, generate, prepare } from './build' import { build, generate, prepare } from './build'
import { getNitroContext, NitroContext } from './context' import { getNitroContext, NitroContext } from './context'
import { createDevServer } from './server' import { createDevServer } from './server/dev'
import { wpfs } from './utils/wpfs' import { wpfs } from './utils/wpfs'
import { resolveMiddleware } from './middleware' import { resolveMiddleware } from './server/middleware'
export default function nuxt2CompatModule () { export default function nuxt2CompatModule () {
const { nuxt } = this const { nuxt } = this

View File

@ -6,13 +6,7 @@ import type { Preset } from '@nuxt/un'
import { tryImport, resolvePath, detectTarget, extendPreset } from './utils' import { tryImport, resolvePath, detectTarget, extendPreset } from './utils'
import * as PRESETS from './presets' import * as PRESETS from './presets'
import type { NodeExternalsOptions } from './rollup/plugins/externals' import type { NodeExternalsOptions } from './rollup/plugins/externals'
import type { ServerMiddleware } from './server/middleware'
export interface ServerMiddleware {
route: string
handle: string
lazy?: boolean // Default is true
promisify?: boolean // Default is true
}
export interface NitroContext { export interface NitroContext {
timing: boolean timing: boolean
@ -28,6 +22,7 @@ export interface NitroContext {
renderer: string renderer: string
serveStatic: boolean serveStatic: boolean
middleware: ServerMiddleware[] middleware: ServerMiddleware[]
scannedMiddleware: ServerMiddleware[]
hooks: configHooksT hooks: configHooksT
nuxtHooks: configHooksT nuxtHooks: configHooksT
ignore: string[] ignore: string[]
@ -45,6 +40,7 @@ export interface NitroContext {
buildDir: string buildDir: string
generateDir: string generateDir: string
staticDir: string staticDir: string
serverDir: string
routerBase: string routerBase: string
publicPath: string publicPath: string
isStatic: boolean isStatic: boolean
@ -79,6 +75,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
renderer: undefined, renderer: undefined,
serveStatic: false, serveStatic: false,
middleware: [], middleware: [],
scannedMiddleware: [],
ignore: [], ignore: [],
env: {}, env: {},
hooks: {}, hooks: {},
@ -96,6 +93,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
buildDir: nuxtOptions.buildDir, buildDir: nuxtOptions.buildDir,
generateDir: nuxtOptions.generate.dir, generateDir: nuxtOptions.generate.dir,
staticDir: nuxtOptions.dir.static, staticDir: nuxtOptions.dir.static,
serverDir: resolve(nuxtOptions.srcDir, (nuxtOptions.dir as any).server || 'server'),
routerBase: nuxtOptions.router.base, routerBase: nuxtOptions.router.base,
publicPath: nuxtOptions.build.publicPath, publicPath: nuxtOptions.build.publicPath,
isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev, isStatic: nuxtOptions.target === 'static' && !nuxtOptions.dev,

View File

@ -1,6 +1,6 @@
export * from './build' export * from './build'
export * from './context' export * from './context'
export * from './middleware' export * from './server/middleware'
export * from './server' export * from './server/dev'
export * from './types' export * from './types'
export { wpfs } from './utils/wpfs' export { wpfs } from './utils/wpfs'

View File

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

View File

@ -148,11 +148,16 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
} }
// Middleware // Middleware
const _middleware = [...nitroContext.middleware] rollupConfig.plugins.push(middleware(() => {
if (nitroContext.serveStatic) { const _middleware = [
_middleware.unshift({ route: '/', handle: '~runtime/server/static' }) ...nitroContext.scannedMiddleware,
} ...nitroContext.middleware
rollupConfig.plugins.push(middleware(_middleware)) ]
if (nitroContext.serveStatic) {
_middleware.unshift({ route: '/', handle: '~runtime/server/static' })
}
return _middleware
}))
// Polyfill // Polyfill
rollupConfig.plugins.push(virtual({ rollupConfig.plugins.push(virtual({
@ -187,7 +192,8 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
ignore: [ ignore: [
nitroContext._internal.runtimeDir, nitroContext._internal.runtimeDir,
...(nitroContext._nuxt.dev ? [] : [nitroContext._nuxt.buildDir]), ...(nitroContext._nuxt.dev ? [] : [nitroContext._nuxt.buildDir]),
...nitroContext.middleware.map(m => m.handle) ...nitroContext.middleware.map(m => m.handle),
nitroContext._nuxt.serverDir
], ],
traceOptions: { traceOptions: {
base: nitroContext._nuxt.rootDir base: nitroContext._nuxt.rootDir

View File

@ -1,12 +1,26 @@
import hasha from 'hasha' import hasha from 'hasha'
import virtual from '@rollup/plugin-virtual' import { relative } from 'upath'
import type { ServerMiddleware } from '../../context' 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) const getImportId = p => '_' + hasha(p).substr(0, 6)
let lastDump = ''
return virtual({ 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 => `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')} ${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 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(', ')
}

View File

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

View File

@ -8,7 +8,7 @@ import serveStatic from 'serve-static'
import servePlaceholder from 'serve-placeholder' import servePlaceholder from 'serve-placeholder'
import { createProxy } from 'http-proxy' import { createProxy } from 'http-proxy'
import { stat } from 'fs-extra' import { stat } from 'fs-extra'
import type { NitroContext } from './context' import type { NitroContext } from '../context'
export function createDevServer (nitroContext: NitroContext) { export function createDevServer (nitroContext: NitroContext) {
// Worker // Worker

View File

@ -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>): 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<ServerMiddleware[]> {
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
}
}