chore: merge nitro into monorepo

This commit is contained in:
Pooya Parsa 2021-03-17 20:09:00 +01:00
commit 8d6b97a4ac
55 changed files with 2725 additions and 0 deletions

124
packages/nitro/src/build.ts Normal file
View File

@ -0,0 +1,124 @@
import { resolve, join } from 'upath'
import consola from 'consola'
import { rollup, watch as rollupWatch } from 'rollup'
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)}`)
await cleanupDir(nitroContext.output.dir)
if (!nitroContext.output.publicDir.startsWith(nitroContext.output.dir)) {
await cleanupDir(nitroContext.output.publicDir)
}
if (!nitroContext.output.serverDir.startsWith(nitroContext.output.dir)) {
await cleanupDir(nitroContext.output.serverDir)
}
}
async function cleanupDir (dir: string) {
consola.info('Cleaning up', prettyPath(dir))
await emptyDir(dir)
}
export async function generate (nitroContext: NitroContext) {
consola.start('Generating public...')
const clientDist = resolve(nitroContext._nuxt.buildDir, 'dist/client')
if (await isDirectory(clientDist)) {
await copy(clientDist, join(nitroContext.output.publicDir, nitroContext._nuxt.publicPath))
}
const staticDir = resolve(nitroContext._nuxt.srcDir, nitroContext._nuxt.staticDir)
if (await isDirectory(staticDir)) {
await copy(staticDir, nitroContext.output.publicDir)
}
consola.success('Generated public ' + prettyPath(nitroContext.output.publicDir))
}
export async function build (nitroContext: NitroContext) {
// Compile html template
const htmlSrc = resolve(nitroContext._nuxt.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`)
const htmlTemplate = { src: htmlSrc, contents: '', dst: '', compiled: '' }
htmlTemplate.dst = htmlTemplate.src.replace(/.html$/, '.js').replace('app.', 'document.')
htmlTemplate.contents = await readFile(htmlTemplate.src, 'utf-8')
htmlTemplate.compiled = 'module.exports = ' + serializeTemplate(htmlTemplate.contents)
await nitroContext._internal.hooks.callHook('nitro:template:document', htmlTemplate)
await writeFile(htmlTemplate.dst, htmlTemplate.compiled)
nitroContext.rollupConfig = getRollupConfig(nitroContext)
await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext)
return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext)
}
async function _build (nitroContext: NitroContext) {
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir)
consola.start('Building server...')
const build = await rollup(nitroContext.rollupConfig).catch((error) => {
consola.error('Rollup error: ' + error.message)
throw error
})
consola.start('Writing server bundle...')
await build.write(nitroContext.rollupConfig.output)
consola.success('Server built')
await printFSTree(nitroContext.output.serverDir)
await nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext)
return {
entry: resolve(nitroContext.rollupConfig.output.dir, nitroContext.rollupConfig.output.entryFileNames)
}
}
function startRollupWatcher (nitroContext: NitroContext) {
const watcher = rollupWatch(nitroContext.rollupConfig)
let start
watcher.on('event', (event) => {
switch (event.code) {
// The watcher is (re)starting
case 'START':
return
// Building an individual bundle
case 'BUNDLE_START':
start = Date.now()
return
// Finished building all bundles
case 'END':
nitroContext._internal.hooks.callHook('nitro:compiled', nitroContext)
consola.success('Nitro built', start ? `in ${Date.now() - start} ms` : '')
return
// Encountered an error while bundling
case 'ERROR':
consola.error('Rollup error: ' + event.error)
// consola.error(event.error)
}
})
return watcher
}
async function _watch (nitroContext: NitroContext) {
let watcher = startRollupWatcher(nitroContext)
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir,
(middleware, event) => {
nitroContext.scannedMiddleware = middleware
if (['add', 'addDir'].includes(event)) {
watcher.close()
watcher = startRollupWatcher(nitroContext)
}
}
)
}

View File

@ -0,0 +1,152 @@
import fetch from 'node-fetch'
import { resolve } from 'upath'
import { build, generate, prepare } from './build'
import { getNitroContext, NitroContext } from './context'
import { createDevServer } from './server/dev'
import { wpfs } from './utils/wpfs'
import { resolveMiddleware } from './server/middleware'
export default function nuxt2CompatModule () {
const { nuxt } = this
// Ensure we're not just building with 'static' target
if (!nuxt.options.dev && nuxt.options.target === 'static' && !nuxt.options._export && !nuxt.options._legacyGenerate) {
throw new Error('[nitro] Please use `nuxt generate` for static target')
}
// Disable loading-screen
nuxt.options.build.loadingScreen = false
nuxt.options.build.indicator = false
// Create contexts
const nitroContext = getNitroContext(nuxt.options, nuxt.options.nitro || {})
const nitroDevContext = getNitroContext(nuxt.options, { preset: 'dev' })
// Connect hooks
nuxt.addHooks(nitroContext.nuxtHooks)
nuxt.hook('close', () => nitroContext._internal.hooks.callHook('close'))
nuxt.addHooks(nitroDevContext.nuxtHooks)
nuxt.hook('close', () => nitroDevContext._internal.hooks.callHook('close'))
nitroDevContext._internal.hooks.hook('renderLoading',
(req, res) => nuxt.callHook('server:nuxt:renderLoading', req, res))
// Expose process.env.NITRO_PRESET
nuxt.options.env.NITRO_PRESET = nitroContext.preset
// .ts is supported for serverMiddleware
nuxt.options.extensions.push('ts')
// Replace nuxt server
if (nuxt.server) {
nuxt.server.__closed = true
nuxt.server = createNuxt2DevServer(nitroDevContext)
}
// Disable server sourceMap, esbuild will generate for it.
nuxt.hook('webpack:config', (webpackConfigs) => {
const serverConfig = webpackConfigs.find(config => config.name === 'server')
serverConfig.devtool = false
})
// Nitro client plugin
this.addPlugin({
fileName: 'nitro.client.js',
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.js')
})
// Resolve middleware
nuxt.hook('modules:done', () => {
const { middleware, legacyMiddleware } =
resolveMiddleware(nuxt.options.serverMiddleware, nuxt.resolver.resolvePath)
if (nuxt.server) {
nuxt.server.setLegacyMiddleware(legacyMiddleware)
}
nitroContext.middleware.push(...middleware)
nitroDevContext.middleware.push(...middleware)
})
// nuxt build/dev
nuxt.options.build._minifyServer = false
nuxt.options.build.standalone = false
nuxt.hook('build:done', async () => {
if (nuxt.options.dev) {
await build(nitroDevContext)
} else if (!nitroContext._nuxt.isStatic) {
await prepare(nitroContext)
await generate(nitroContext)
await build(nitroContext)
}
})
// nude dev
if (nuxt.options.dev) {
nitroDevContext._internal.hooks.hook('nitro:compiled', () => { nuxt.server.watch() })
nuxt.hook('build:compile', ({ compiler }) => { compiler.outputFileSystem = wpfs })
nuxt.hook('server:devMiddleware', (m) => { nuxt.server.setDevMiddleware(m) })
}
// nuxt generate
nuxt.options.generate.dir = nitroContext.output.publicDir
nuxt.options.generate.manifest = false
nuxt.hook('generate:cache:ignore', (ignore: string[]) => {
ignore.push(nitroContext.output.dir)
ignore.push(nitroContext.output.serverDir)
if (nitroContext.output.publicDir) {
ignore.push(nitroContext.output.publicDir)
}
ignore.push(...nitroContext.ignore)
})
nuxt.hook('generate:before', async () => {
await prepare(nitroContext)
})
nuxt.hook('generate:extendRoutes', async () => {
await build(nitroDevContext)
await nuxt.server.reload()
})
nuxt.hook('generate:done', async () => {
await nuxt.server.close()
await build(nitroContext)
})
}
function createNuxt2DevServer (nitroContext: NitroContext) {
const server = createDevServer(nitroContext)
const listeners = []
async function listen (port) {
const listener = await server.listen(port, {
showURL: false,
isProd: true
})
listeners.push(listener)
return listener
}
async function renderRoute (route = '/', renderContext = {}) {
const [listener] = listeners
if (!listener) {
throw new Error('There is no server listener to call `server.renderRoute()`')
}
const html = await fetch(listener.url + route, {
headers: { 'nuxt-render-context': encodeQuery(renderContext) }
}).then(r => r.text())
return { html }
}
return {
...server,
listeners,
renderRoute,
listen,
serverMiddlewarePaths () { return [] },
ready () { }
}
}
function encodeQuery (obj) {
return Object.entries(obj).map(
([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`
).join('&')
}

View File

@ -0,0 +1,136 @@
import { resolve } from 'upath'
import defu from 'defu'
import type { NuxtOptions } from '@nuxt/types'
import Hookable, { configHooksT } from 'hookable'
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'
import type { ServerMiddleware } from './server/middleware'
export interface NitroContext {
timing: boolean
inlineDynamicImports: boolean
minify: boolean
sourceMap: boolean
externals: boolean | NodeExternalsOptions
analyze: boolean
entry: string
node: boolean
preset: string
rollupConfig?: any
renderer: string
serveStatic: boolean
middleware: ServerMiddleware[]
scannedMiddleware: ServerMiddleware[]
hooks: configHooksT
nuxtHooks: configHooksT
ignore: string[]
env: Preset
output: {
dir: string
serverDir: string
publicDir: string
}
_nuxt: {
majorVersion: number
dev: boolean
rootDir: string
srcDir: string
buildDir: string
generateDir: string
staticDir: string
serverDir: string
routerBase: string
publicPath: string
isStatic: boolean
fullStatic: boolean
staticAssets: any
runtimeConfig: { public: any, private: any }
}
_internal: {
runtimeDir: string
hooks: Hookable
}
}
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> }
export interface NitroInput extends DeepPartial<NitroContext> {}
export type NitroPreset = NitroInput | ((input: NitroInput) => NitroInput)
export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): NitroContext {
const defaults: NitroContext = {
timing: undefined,
inlineDynamicImports: undefined,
minify: undefined,
sourceMap: undefined,
externals: undefined,
analyze: undefined,
entry: undefined,
node: undefined,
preset: undefined,
rollupConfig: undefined,
renderer: undefined,
serveStatic: undefined,
middleware: [],
scannedMiddleware: [],
ignore: [],
env: {},
hooks: {},
nuxtHooks: {},
output: {
dir: '{{ _nuxt.rootDir }}/.output',
serverDir: '{{ output.dir }}/server',
publicDir: '{{ output.dir }}/public'
},
_nuxt: {
majorVersion: nuxtOptions._majorVersion || 2,
dev: nuxtOptions.dev,
rootDir: nuxtOptions.rootDir,
srcDir: nuxtOptions.srcDir,
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,
fullStatic: nuxtOptions.target === 'static' && !nuxtOptions._legacyGenerate,
// @ts-ignore
staticAssets: nuxtOptions.generate.staticAssets,
runtimeConfig: {
public: nuxtOptions.publicRuntimeConfig,
private: nuxtOptions.privateRuntimeConfig
}
},
_internal: {
runtimeDir: resolve(__dirname, './runtime'),
hooks: new Hookable()
}
}
defaults.preset = input.preset || process.env.NITRO_PRESET || detectTarget() || 'server'
let presetDefaults = PRESETS[defaults.preset] || tryImport(nuxtOptions.rootDir, defaults.preset)
if (!presetDefaults) {
throw new Error('Cannot resolve preset: ' + defaults.preset)
}
presetDefaults = presetDefaults.default || presetDefaults
const _presetInput = defu(input, defaults)
// @ts-ignore
const _preset = extendPreset(input, presetDefaults)(_presetInput)
const nitroContext: NitroContext = defu(_preset, defaults) as any
nitroContext.output.dir = resolvePath(nitroContext, nitroContext.output.dir)
nitroContext.output.publicDir = resolvePath(nitroContext, nitroContext.output.publicDir)
nitroContext.output.serverDir = resolvePath(nitroContext, nitroContext.output.serverDir)
nitroContext._internal.hooks.addHooks(nitroContext.hooks)
// console.log(nitroContext)
// process.exit(1)
return nitroContext
}

View File

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

View File

@ -0,0 +1,105 @@
import consola from 'consola'
import fse from 'fs-extra'
import globby from 'globby'
import { join, resolve } from 'upath'
import { writeFile } from '../utils'
import { NitroPreset, NitroContext } from '../context'
export const azure: NitroPreset = {
entry: '{{ _internal.runtimeDir }}/entries/azure',
output: {
serverDir: '{{ output.dir }}/server/functions'
},
hooks: {
async 'nitro:compiled' (ctx: NitroContext) {
await writeRoutes(ctx)
}
}
}
async function writeRoutes ({ output: { serverDir, publicDir } }: NitroContext) {
const host = {
version: '2.0'
}
const routes = [
{
route: '/*',
serve: '/api/server'
}
]
const indexPath = resolve(publicDir, 'index.html')
const indexFileExists = fse.existsSync(indexPath)
if (!indexFileExists) {
routes.unshift(
{
route: '/',
serve: '/api/server'
},
{
route: '/index.html',
serve: '/api/server'
}
)
}
const folderFiles = await globby([
join(publicDir, 'index.html'),
join(publicDir, '**/index.html')
])
const prefix = publicDir.length
const suffix = '/index.html'.length
folderFiles.forEach(file =>
routes.unshift({
route: file.slice(prefix, -suffix) || '/',
serve: file.slice(prefix)
})
)
const otherFiles = await globby([join(publicDir, '**/*.html'), join(publicDir, '*.html')])
otherFiles.forEach((file) => {
if (file.endsWith('index.html')) {
return
}
const route = file.slice(prefix, -5)
const existingRouteIndex = routes.findIndex(_route => _route.route === route)
if (existingRouteIndex > -1) {
routes.splice(existingRouteIndex, 1)
}
routes.unshift(
{
route,
serve: file.slice(prefix)
}
)
})
const functionDefinition = {
entryPoint: 'handle',
bindings: [
{
authLevel: 'anonymous',
type: 'httpTrigger',
direction: 'in',
name: 'req',
route: '{*url}',
methods: ['delete', 'get', 'head', 'options', 'patch', 'post', 'put']
},
{
type: 'http',
direction: 'out',
name: 'res'
}
]
}
await writeFile(resolve(serverDir, 'function.json'), JSON.stringify(functionDefinition))
await writeFile(resolve(serverDir, '../host.json'), JSON.stringify(host))
await writeFile(resolve(publicDir, 'routes.json'), JSON.stringify({ routes }))
if (!indexFileExists) {
await writeFile(indexPath, '')
}
consola.success('Ready to deploy.')
}

View File

@ -0,0 +1,74 @@
import archiver from 'archiver'
import consola from 'consola'
import { createWriteStream } from 'fs-extra'
import { join, resolve } from 'upath'
import { prettyPath, writeFile } from '../utils'
import { NitroPreset, NitroContext } from '../context'
// eslint-disable-next-line
export const azure_functions: NitroPreset = {
serveStatic: true,
entry: '{{ _internal.runtimeDir }}/entries/azure_functions',
hooks: {
async 'nitro:compiled' (ctx: NitroContext) {
await writeRoutes(ctx)
}
}
}
function zipDirectory (dir: string, outfile: string): Promise<undefined> {
const archive = archiver('zip', { zlib: { level: 9 } })
const stream = createWriteStream(outfile)
return new Promise((resolve, reject) => {
archive
.directory(dir, false)
.on('error', (err: Error) => reject(err))
.pipe(stream)
stream.on('close', () => resolve(undefined))
archive.finalize()
})
}
async function writeRoutes ({ output: { dir, serverDir } }: NitroContext) {
const host = {
version: '2.0',
extensions: { http: { routePrefix: '' } }
}
const functionDefinition = {
entryPoint: 'handle',
bindings: [
{
authLevel: 'anonymous',
type: 'httpTrigger',
direction: 'in',
name: 'req',
route: '{*url}',
methods: [
'delete',
'get',
'head',
'options',
'patch',
'post',
'put'
]
},
{
type: 'http',
direction: 'out',
name: 'res'
}
]
}
await writeFile(resolve(serverDir, 'function.json'), JSON.stringify(functionDefinition))
await writeFile(resolve(dir, 'host.json'), JSON.stringify(host))
await zipDirectory(dir, join(dir, 'deploy.zip'))
const zipPath = prettyPath(resolve(dir, 'deploy.zip'))
consola.success(`Ready to run \`az functionapp deployment source config-zip -g <resource-group> -n <app-name> --src ${zipPath}\``)
}

View File

@ -0,0 +1,79 @@
import { writeFile } from 'fs-extra'
import { resolve } from 'upath'
import consola from 'consola'
import { extendPreset, prettyPath } from '../utils'
import { NitroPreset, NitroContext, NitroInput } from '../context'
import { worker } from './worker'
export const browser: NitroPreset = extendPreset(worker, (input: NitroInput) => {
const routerBase = input._nuxt.routerBase
const script = `<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('${routerBase}sw.js');
});
}
</script>`
// TEMP FIX
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="prefetch" href="${routerBase}sw.js">
<link rel="prefetch" href="${routerBase}_server/index.js">
<script>
async function register () {
const registration = await navigator.serviceWorker.register('${routerBase}sw.js')
await navigator.serviceWorker.ready
registration.active.addEventListener('statechange', (event) => {
if (event.target.state === 'activated') {
window.location.reload()
}
})
}
if (location.hostname !== 'localhost' && location.protocol === 'http:') {
location.replace(location.href.replace('http://', 'https://'))
} else {
register()
}
</script>
</head>
<body>
Loading...
</body>
</html>`
return <NitroInput> {
entry: '{{ _internal.runtimeDir }}/entries/service-worker',
output: {
serverDir: '{{ output.dir }}/public/_server'
},
nuxtHooks: {
'vue-renderer:ssr:templateParams' (params) {
params.APP += script
},
'vue-renderer:spa:templateParams' (params) {
params.APP += script
}
},
hooks: {
'nitro:template:document' (tmpl) {
tmpl.compiled = tmpl.compiled.replace('</body>', script + '</body>')
},
async 'nitro:compiled' ({ output }: NitroContext) {
await writeFile(resolve(output.publicDir, 'sw.js'), `self.importScripts('${input._nuxt.routerBase}_server/index.js');`)
// Temp fix
await writeFile(resolve(output.publicDir, 'index.html'), html)
await writeFile(resolve(output.publicDir, '200.html'), html)
await writeFile(resolve(output.publicDir, '404.html'), html)
consola.info('Ready to deploy to static hosting:', prettyPath(output.publicDir as string))
}
}
}
})

View File

@ -0,0 +1,13 @@
import consola from 'consola'
import { extendPreset, prettyPath } from '../utils'
import { NitroPreset, NitroContext } from '../context'
import { node } from './node'
export const cli: NitroPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/cli',
hooks: {
'nitro:compiled' ({ output }: NitroContext) {
consola.info('Run with `node ' + prettyPath(output.serverDir) + ' [route]`')
}
}
})

View File

@ -0,0 +1,23 @@
import { resolve } from 'upath'
import consola from 'consola'
import { extendPreset, writeFile, prettyPath } from '../utils'
import { NitroContext, NitroPreset } from '../context'
import { worker } from './worker'
export const cloudflare: NitroPreset = extendPreset(worker, {
entry: '{{ _internal.runtimeDir }}/entries/cloudflare',
ignore: [
'wrangler.toml'
],
hooks: {
async 'nitro:compiled' ({ output, _nuxt }: NitroContext) {
await writeFile(resolve(output.dir, 'package.json'), JSON.stringify({ private: true, main: './server/index.js' }, null, 2))
await writeFile(resolve(output.dir, 'package-lock.json'), JSON.stringify({ lockfileVersion: 1 }, null, 2))
let inDir = prettyPath(_nuxt.rootDir)
if (inDir) {
inDir = 'in ' + inDir
}
consola.success('Ready to run `wrangler publish`', inDir)
}
}
})

View File

@ -0,0 +1,13 @@
import { extendPreset } from '../utils'
import { NitroPreset } from '../context'
import { node } from './node'
export const dev: NitroPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/dev',
output: {
serverDir: '{{ _nuxt.buildDir }}/nitro'
},
externals: { trace: false },
inlineDynamicImports: true, // externals plugin limitation
sourceMap: true
})

View File

@ -0,0 +1,81 @@
import { join, relative, resolve } from 'upath'
import { existsSync, readJSONSync } from 'fs-extra'
import consola from 'consola'
import globby from 'globby'
import { writeFile } from '../utils'
import { NitroPreset, NitroContext } from '../context'
export const firebase: NitroPreset = {
entry: '{{ _internal.runtimeDir }}/entries/firebase',
hooks: {
async 'nitro:compiled' (ctx: NitroContext) {
await writeRoutes(ctx)
}
}
}
async function writeRoutes ({ output: { publicDir, serverDir }, _nuxt: { rootDir } }: NitroContext) {
if (!existsSync(join(rootDir, 'firebase.json'))) {
const firebase = {
functions: {
source: relative(rootDir, serverDir)
},
hosting: [
{
site: '<your_project_id>',
public: relative(rootDir, publicDir),
cleanUrls: true,
rewrites: [
{
source: '**',
function: 'server'
}
]
}
]
}
await writeFile(resolve(rootDir, 'firebase.json'), JSON.stringify(firebase))
}
const jsons = await globby(`${serverDir}/node_modules/**/package.json`)
const prefixLength = `${serverDir}/node_modules/`.length
const suffixLength = '/package.json'.length
const dependencies = jsons.reduce((obj, packageJson) => {
const dirname = packageJson.slice(prefixLength, -suffixLength)
if (!dirname.includes('node_modules')) {
obj[dirname] = require(packageJson).version
}
return obj
}, {} as Record<string, string>)
let nodeVersion = '12'
try {
const currentNodeVersion = readJSONSync(join(rootDir, 'package.json')).engines.node
if (['12', '10'].includes(currentNodeVersion)) {
nodeVersion = currentNodeVersion
}
} catch {}
await writeFile(
resolve(serverDir, 'package.json'),
JSON.stringify(
{
private: true,
main: './index.js',
dependencies,
devDependencies: {
'firebase-functions-test': 'latest',
'firebase-admin': require('firebase-admin/package.json').version,
'firebase-functions': require('firebase-functions/package.json')
.version
},
engines: { node: nodeVersion }
},
null,
2
)
)
consola.success('Ready to run `firebase deploy`')
}

View File

@ -0,0 +1,13 @@
export * from './azure_functions'
export * from './azure'
export * from './browser'
export * from './cloudflare'
export * from './firebase'
export * from './lambda'
export * from './netlify'
export * from './node'
export * from './dev'
export * from './server'
export * from './cli'
export * from './vercel'
export * from './worker'

View File

@ -0,0 +1,7 @@
import { NitroPreset } from '../context'
export const lambda: NitroPreset = {
entry: '{{ _internal.runtimeDir }}/entries/lambda',
externals: true
}

View File

@ -0,0 +1,13 @@
import { extendPreset } from '../utils'
import { NitroPreset } from '../context'
import { lambda } from './lambda'
export const netlify: NitroPreset = extendPreset(lambda, {
output: {
publicDir: '{{ _nuxt.rootDir }}/dist'
},
ignore: [
'netlify.toml',
'_redirects'
]
})

View File

@ -0,0 +1,6 @@
import { NitroPreset } from '../context'
export const node: NitroPreset = {
entry: '{{ _internal.runtimeDir }}/entries/node',
externals: true
}

View File

@ -0,0 +1,14 @@
import consola from 'consola'
import { extendPreset, hl, prettyPath } from '../utils'
import { NitroPreset, NitroContext } from '../context'
import { node } from './node'
export const server: NitroPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/server',
serveStatic: true,
hooks: {
'nitro:compiled' ({ output }: NitroContext) {
consola.success('Ready to run', hl('node ' + prettyPath(output.serverDir)))
}
}
})

View File

@ -0,0 +1,49 @@
import { resolve } from 'upath'
import { extendPreset, writeFile } from '../utils'
import { NitroPreset, NitroContext } from '../context'
import { node } from './node'
export const vercel: NitroPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/vercel',
output: {
dir: '{{ _nuxt.rootDir }}/.vercel_build_output',
serverDir: '{{ output.dir }}/functions/node/server',
publicDir: '{{ output.dir }}/static'
},
ignore: [
'vercel.json'
],
hooks: {
async 'nitro:compiled' (ctx: NitroContext) {
await writeRoutes(ctx)
}
}
})
async function writeRoutes ({ output }: NitroContext) {
const routes = [
{
src: '/sw.js',
headers: {
'cache-control': 'public, max-age=0, must-revalidate'
},
continue: true
},
{
src: '/_nuxt/(.*)',
headers: {
'cache-control': 'public,max-age=31536000,immutable'
},
continue: true
},
{
handle: 'filesystem'
},
{
src: '(.*)',
dest: '/.vercel/functions/server/index'
}
]
await writeFile(resolve(output.dir, 'config/routes.json'), JSON.stringify(routes, null, 2))
}

View File

@ -0,0 +1,13 @@
import { NitroPreset, NitroContext } from '../context'
export const worker: NitroPreset = {
entry: null, // Abstract
node: false,
minify: true,
inlineDynamicImports: true, // iffe does not support code-splitting
hooks: {
'nitro:rollup:before' ({ rollupConfig }: NitroContext) {
rollupConfig.output.format = 'iife'
}
}
}

View File

@ -0,0 +1,249 @@
import { dirname, join, relative, resolve } from 'upath'
import { InputOptions, OutputOptions } from 'rollup'
import defu from 'defu'
import { terser } from 'rollup-plugin-terser'
import commonjs from '@rollup/plugin-commonjs'
import nodeResolve from '@rollup/plugin-node-resolve'
import alias from '@rollup/plugin-alias'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
import virtual from '@rollup/plugin-virtual'
import inject from '@rollup/plugin-inject'
import analyze from 'rollup-plugin-analyzer'
import type { Preset } from '@nuxt/un'
import * as un from '@nuxt/un'
import { NitroContext } from '../context'
import { resolvePath, MODULE_DIR } from '../utils'
import { dynamicRequire } from './plugins/dynamic-require'
import { externals } from './plugins/externals'
import { timing } from './plugins/timing'
import { autoMock } from './plugins/automock'
import { staticAssets, dirnames } from './plugins/static'
import { middleware } from './plugins/middleware'
import { esbuild } from './plugins/esbuild'
export type RollupConfig = InputOptions & { output: OutputOptions }
export const getRollupConfig = (nitroContext: NitroContext) => {
const extensions: string[] = ['.ts', '.mjs', '.js', '.json', '.node']
const nodePreset = nitroContext.node === false ? un.nodeless : un.node
const builtinPreset: Preset = {
alias: {
// General
debug: 'un/npm/debug',
depd: 'un/npm/depd',
// Vue 2
encoding: 'un/mock/proxy',
he: 'un/mock/proxy',
resolve: 'un/mock/proxy',
'source-map': 'un/mock/proxy',
'lodash.template': 'un/mock/proxy',
'serialize-javascript': 'un/mock/proxy',
// Vue 3
'@babel/parser': 'un/mock/proxy',
'@vue/compiler-core': 'un/mock/proxy',
'@vue/compiler-dom': 'un/mock/proxy',
'@vue/compiler-ssr': 'un/mock/proxy'
}
}
const env = un.env(nodePreset, builtinPreset, nitroContext.env)
delete env.alias['node-fetch'] // FIX ME
if (nitroContext.sourceMap) {
env.polyfill.push('source-map-support/register')
}
const buildServerDir = join(nitroContext._nuxt.buildDir, 'dist/server')
const runtimeAppDir = join(nitroContext._internal.runtimeDir, 'app')
const rollupConfig: RollupConfig = {
input: resolvePath(nitroContext, nitroContext.entry),
output: {
dir: nitroContext.output.serverDir,
entryFileNames: 'index.js',
chunkFileNames (chunkInfo) {
let prefix = ''
const modules = Object.keys(chunkInfo.modules)
const lastModule = modules[modules.length - 1]
if (lastModule.startsWith(buildServerDir)) {
prefix = join('app', relative(buildServerDir, dirname(lastModule)))
} else if (lastModule.startsWith(runtimeAppDir)) {
prefix = 'app'
} else if (lastModule.startsWith(nitroContext._nuxt.buildDir)) {
prefix = 'nuxt'
} else if (lastModule.startsWith(nitroContext._internal.runtimeDir)) {
prefix = 'nitro'
} else if (!prefix && nitroContext.middleware.find(m => lastModule.startsWith(m.handle))) {
prefix = 'middleware'
}
return join('chunks', prefix, '[name].js')
},
inlineDynamicImports: nitroContext.inlineDynamicImports,
format: 'cjs',
exports: 'auto',
intro: '',
outro: '',
preferConst: true,
sourcemap: nitroContext.sourceMap,
sourcemapExcludeSources: true,
sourcemapPathTransform (relativePath, sourcemapPath) {
return resolve(dirname(sourcemapPath), relativePath)
}
},
external: env.external,
plugins: [],
onwarn (warning, rollupWarn) {
if (!['CIRCULAR_DEPENDENCY', 'EVAL'].includes(warning.code)) {
rollupWarn(warning)
}
}
}
if (nitroContext.timing) {
rollupConfig.plugins.push(timing())
}
// https://github.com/rollup/plugins/tree/master/packages/replace
rollupConfig.plugins.push(replace({
// @ts-ignore https://github.com/rollup/plugins/pull/810
preventAssignment: true,
values: {
'process.env.NODE_ENV': nitroContext._nuxt.dev ? '"development"' : '"production"',
'typeof window': '"undefined"',
'process.env.ROUTER_BASE': JSON.stringify(nitroContext._nuxt.routerBase),
'process.env.PUBLIC_PATH': JSON.stringify(nitroContext._nuxt.publicPath),
'process.env.NUXT_STATIC_BASE': JSON.stringify(nitroContext._nuxt.staticAssets.base),
'process.env.NUXT_STATIC_VERSION': JSON.stringify(nitroContext._nuxt.staticAssets.version),
'process.env.NUXT_FULL_STATIC': nitroContext._nuxt.fullStatic as unknown as string,
'process.env.NITRO_PRESET': JSON.stringify(nitroContext.preset),
'process.env.RUNTIME_CONFIG': JSON.stringify(nitroContext._nuxt.runtimeConfig),
'process.env.DEBUG': JSON.stringify(nitroContext._nuxt.dev)
}
}))
// ESBuild
rollupConfig.plugins.push(esbuild({
sourceMap: true
}))
// Dynamic Require Support
rollupConfig.plugins.push(dynamicRequire({
dir: resolve(nitroContext._nuxt.buildDir, 'dist/server'),
inline: nitroContext.node === false || nitroContext.inlineDynamicImports,
globbyOptions: {
ignore: [
'server.js'
]
}
}))
// Static
if (nitroContext.serveStatic) {
rollupConfig.plugins.push(dirnames())
rollupConfig.plugins.push(staticAssets(nitroContext))
}
// 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({
'~polyfill': env.polyfill.map(p => `import '${p}';`).join('\n')
}))
// https://github.com/rollup/plugins/tree/master/packages/alias
const renderer = nitroContext.renderer || (nitroContext._nuxt.majorVersion === 3 ? 'vue3' : 'vue2')
const vue2ServerRenderer = 'vue-server-renderer/' + (nitroContext._nuxt.dev ? 'build.dev.js' : 'build.prod.js')
rollupConfig.plugins.push(alias({
entries: {
'~runtime': nitroContext._internal.runtimeDir,
'~renderer': require.resolve(resolve(nitroContext._internal.runtimeDir, 'app', renderer)),
'~vueServerRenderer': vue2ServerRenderer,
'~build': nitroContext._nuxt.buildDir,
...env.alias
}
}))
const moduleDirectories = [
resolve(nitroContext._nuxt.rootDir, 'node_modules'),
resolve(MODULE_DIR, 'node_modules'),
resolve(MODULE_DIR, '../node_modules'),
'node_modules'
]
// Externals Plugin
if (nitroContext.externals) {
rollupConfig.plugins.push(externals(defu(nitroContext.externals as any, {
outDir: nitroContext.output.serverDir,
moduleDirectories,
ignore: [
nitroContext._internal.runtimeDir,
...(nitroContext._nuxt.dev ? [] : [nitroContext._nuxt.buildDir]),
...nitroContext.middleware.map(m => m.handle),
nitroContext._nuxt.serverDir
],
traceOptions: {
base: nitroContext._nuxt.rootDir
}
})))
}
// https://github.com/rollup/plugins/tree/master/packages/node-resolve
rollupConfig.plugins.push(nodeResolve({
extensions,
preferBuiltins: true,
rootDir: nitroContext._nuxt.rootDir,
moduleDirectories,
mainFields: ['main'] // Force resolve CJS (@vue/runtime-core ssrUtils)
}))
// Automatically mock unresolved externals
rollupConfig.plugins.push(autoMock())
// https://github.com/rollup/plugins/tree/master/packages/commonjs
rollupConfig.plugins.push(commonjs({
extensions: extensions.filter(ext => ext !== '.json')
}))
// https://github.com/rollup/plugins/tree/master/packages/json
rollupConfig.plugins.push(json())
// https://github.com/rollup/plugins/tree/master/packages/inject
rollupConfig.plugins.push(inject(env.inject))
if (nitroContext.analyze) {
// https://github.com/doesdev/rollup-plugin-analyzer
rollupConfig.plugins.push(analyze())
}
// https://github.com/TrySound/rollup-plugin-terser
// https://github.com/terser/terser#minify-nitroContext
if (nitroContext.minify) {
rollupConfig.plugins.push(terser({
mangle: {
keep_fnames: true,
keep_classnames: true
},
format: {
comments: false
}
}))
}
return rollupConfig
}

View File

@ -0,0 +1,13 @@
export function autoMock () {
return {
name: 'auto-mock',
resolveId (src: string) {
if (src && !src.startsWith('.') && !src.includes('?') && !src.includes('.js')) {
return {
id: require.resolve('@nuxt/un/runtime/mock/proxy')
}
}
return null
}
}
}

View File

@ -0,0 +1,136 @@
import { resolve } from 'upath'
import globby, { GlobbyOptions } from 'globby'
import type { Plugin } from 'rollup'
const PLUGIN_NAME = 'dynamic-require'
const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.js`
const DYNAMIC_REQUIRE_RE = /require\("\.\/" ?\+/g
interface Options {
dir: string
inline: boolean
globbyOptions: GlobbyOptions
outDir?: string
prefix?: string
}
interface Chunk {
id: string
src: string
name: string
meta?: {
id?: string
ids?: string[]
moduleIds?: string[]
}
}
interface TemplateContext {
chunks: Chunk[]
}
export function dynamicRequire ({ dir, globbyOptions, inline }: Options): Plugin {
return {
name: PLUGIN_NAME,
transform (code: string, _id: string) {
return {
code: code.replace(DYNAMIC_REQUIRE_RE, `require('${HELPER_DYNAMIC}')(`),
map: null
}
},
resolveId (id: string) {
return id === HELPER_DYNAMIC ? id : null
},
// TODO: Async chunk loading over netwrok!
// renderDynamicImport () {
// return {
// left: 'fetch(', right: ')'
// }
// },
async load (_id: string) {
if (_id !== HELPER_DYNAMIC) {
return null
}
// Scan chunks
const files = await globby('**/*.js', { cwd: dir, absolute: false, ...globbyOptions })
const chunks = files.map(id => ({
id,
src: resolve(dir, id).replace(/\\/g, '/'),
name: '_' + id.replace(/[^a-zA-Z0-9_]/g, '_'),
meta: getWebpackChunkMeta(resolve(dir, id))
}))
return inline ? TMPL_INLINE({ chunks }) : TMPL_LAZY({ chunks })
},
renderChunk (code) {
if (inline) {
return {
map: null,
code
}
}
return {
map: null,
code: code.replace(
/Promise.resolve\(\).then\(function \(\) \{ return require\('([^']*)' \/\* webpackChunk \*\/\); \}\).then\(function \(n\) \{ return n.([_a-zA-Z0-9]*); \}\)/g,
"require('$1').$2")
}
}
}
}
function getWebpackChunkMeta (src: string) {
const chunk = require(src) || {}
const { id, ids, modules } = chunk
return {
id,
ids,
moduleIds: Object.keys(modules)
}
}
function TMPL_INLINE ({ chunks }: TemplateContext) {
return `${chunks.map(i => `import ${i.name} from '${i.src}'`).join('\n')}
const dynamicChunks = {
${chunks.map(i => ` ['${i.id}']: ${i.name}`).join(',\n')}
};
export default function dynamicRequire(id) {
return dynamicChunks[id];
};`
}
function TMPL_LAZY ({ chunks }: TemplateContext) {
return `
function dynamicWebpackModule(id, getChunk) {
return function (module, exports, require) {
const r = getChunk()
if (r instanceof Promise) {
module.exports = r.then(r => {
const realModule = { exports: {}, require };
r.modules[id](realModule, realModule.exports, realModule.require);
return realModule.exports;
});
} else {
r.modules[id](module, exports, require);
}
};
};
function webpackChunk (meta, getChunk) {
const chunk = { ...meta, modules: {} };
for (const id of meta.moduleIds) {
chunk.modules[id] = dynamicWebpackModule(id, getChunk);
};
return chunk;
};
const dynamicChunks = {
${chunks.map(i => ` ['${i.id}']: () => webpackChunk(${JSON.stringify(i.meta)}, () => import('${i.src}' /* webpackChunk */))`).join(',\n')}
};
export default function dynamicRequire(id) {
return dynamicChunks[id]();
};`
}

View File

@ -0,0 +1,165 @@
// Based on https://github.com/egoist/rollup-plugin-esbuild (MIT)
import { extname, relative } from 'path'
import { Plugin, PluginContext } from 'rollup'
import { startService, Loader, Service, TransformResult } from 'esbuild'
import { createFilter, FilterPattern } from '@rollup/pluginutils'
const defaultLoaders: { [ext: string]: Loader } = {
'.ts': 'ts',
'.js': 'js'
}
export type Options = {
include?: FilterPattern
exclude?: FilterPattern
sourceMap?: boolean
minify?: boolean
target?: string | string[]
jsxFactory?: string
jsxFragment?: string
define?: {
[k: string]: string
}
/**
* Use this tsconfig file instead
* Disable it by setting to `false`
*/
tsconfig?: string | false
/**
* Map extension to esbuild loader
* Note that each entry (the extension) needs to start with a dot
*/
loaders?: {
[ext: string]: Loader | false
}
}
export function esbuild (options: Options = {}): Plugin {
let target: string | string[]
const loaders = {
...defaultLoaders
}
if (options.loaders) {
for (const key of Object.keys(options.loaders)) {
const value = options.loaders[key]
if (typeof value === 'string') {
loaders[key] = value
} else if (value === false) {
delete loaders[key]
}
}
}
const extensions: string[] = Object.keys(loaders)
const INCLUDE_REGEXP = new RegExp(
`\\.(${extensions.map(ext => ext.slice(1)).join('|')})$`
)
const EXCLUDE_REGEXP = /node_modules/
const filter = createFilter(
options.include || INCLUDE_REGEXP,
options.exclude || EXCLUDE_REGEXP
)
let service: Service | undefined
const stopService = () => {
if (service) {
service.stop()
service = undefined
}
}
return {
name: 'esbuild',
async buildStart () {
if (!service) {
service = await startService()
}
},
async transform (code, id) {
if (!filter(id)) {
return null
}
const ext = extname(id)
const loader = loaders[ext]
if (!loader || !service) {
return null
}
target = options.target || 'node12'
const result = await service.transform(code, {
loader,
target,
define: options.define,
sourcemap: options.sourceMap !== false,
sourcefile: id
})
printWarnings(id, result, this)
return (
result.code && {
code: result.code,
map: result.map || null
}
)
},
buildEnd (error) {
// Stop the service early if there's error
if (error && !this.meta.watchMode) {
stopService()
}
},
async renderChunk (code) {
if (options.minify && service) {
const result = await service.transform(code, {
loader: 'js',
minify: true,
target
})
if (result.code) {
return {
code: result.code,
map: result.map || null
}
}
}
return null
},
generateBundle () {
if (!this.meta.watchMode) {
stopService()
}
}
}
}
function printWarnings (
id: string,
result: TransformResult,
plugin: PluginContext
) {
if (result.warnings) {
for (const warning of result.warnings) {
let message = '[esbuild]'
if (warning.location) {
message += ` (${relative(process.cwd(), id)}:${warning.location.line}:${warning.location.column
})`
}
message += ` ${warning.text}`
plugin.warn(message)
}
}
}

View File

@ -0,0 +1,62 @@
import { isAbsolute, relative } from 'path'
import type { Plugin } from 'rollup'
import { resolve, dirname } from 'upath'
import { copyFile, mkdirp } from 'fs-extra'
import { nodeFileTrace, NodeFileTraceOptions } from '@vercel/nft'
export interface NodeExternalsOptions {
ignore?: string[]
outDir?: string
trace?: boolean
traceOptions?: NodeFileTraceOptions
moduleDirectories?: string[]
}
export function externals (opts: NodeExternalsOptions): Plugin {
const resolvedExternals = {}
return {
name: 'node-externals',
resolveId (id) {
// Internals
if (id.startsWith('\x00') || id.includes('?')) {
return null
}
// Resolve relative paths and exceptions
if (id.startsWith('.') || opts.ignore.find(i => id.startsWith(i))) {
return null
}
for (const dir of opts.moduleDirectories) {
if (id.startsWith(dir)) {
id = id.substr(dir.length + 1)
break
}
}
try {
resolvedExternals[id] = require.resolve(id, { paths: opts.moduleDirectories })
} catch (_err) { }
return {
id: isAbsolute(id) ? relative(opts.outDir, id) : id,
external: true
}
},
async buildEnd () {
if (opts.trace !== false) {
const { fileList } = await nodeFileTrace(Object.values(resolvedExternals), opts.traceOptions)
await Promise.all(fileList.map(async (file) => {
if (!file.startsWith('node_modules')) {
return
}
// TODO: Minify package.json
const src = resolve(opts.traceOptions.base, file)
const dst = resolve(opts.outDir, file)
await mkdirp(dirname(dst))
await copyFile(src, dst)
}))
}
}
}
}

View File

@ -0,0 +1,67 @@
import hasha from 'hasha'
import { relative } from 'upath'
import { table, getBorderCharacters } from 'table'
import isPrimitive from 'is-primitive'
import stdenv from 'std-env'
import type { ServerMiddleware } from '../../server/middleware'
import virtual from './virtual'
export function middleware (getMiddleware: () => ServerMiddleware[]) {
const getImportId = p => '_' + hasha(p).substr(0, 6)
let lastDump = ''
return virtual({
'~serverMiddleware': () => {
const middleware = getMiddleware()
if (!stdenv.test) {
const dumped = dumpMiddleware(middleware)
if (dumped !== lastDump) {
lastDump = dumped
if (middleware.length) {
console.log(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')}
const middleware = [
${middleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)}, lazy: ${m.lazy || true}, promisify: ${m.promisify !== undefined ? m.promisify : true} }`).join(',\n')}
];
export default middleware
`
}
})
}
function dumpMiddleware (middleware: ServerMiddleware[]) {
const data = middleware.map(({ route, handle, ...props }) => {
return [
(route && route !== '/') ? route : '*',
relative(process.cwd(), handle),
dumpObject(props)
]
})
return table([
['Route', 'Handle', 'Options'],
...data
], {
singleLine: true,
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,58 @@
import createEtag from 'etag'
import { readFileSync, statSync } from 'fs-extra'
import mime from 'mime'
import { relative, resolve } from 'upath'
import virtual from '@rollup/plugin-virtual'
import globby from 'globby'
import type { Plugin } from 'rollup'
import type { NitroContext } from '../../context'
export function staticAssets (context: NitroContext) {
const assets: Record<string, { type: string, etag: string, mtime: string, path: string }> = {}
const files = globby.sync('**/*.*', { cwd: context.output.publicDir, absolute: false })
for (const id of files) {
let type = mime.getType(id) || 'text/plain'
if (type.startsWith('text')) { type += '; charset=utf-8' }
const fullPath = resolve(context.output.publicDir, id)
const etag = createEtag(readFileSync(fullPath))
const stat = statSync(fullPath)
assets['/' + id] = {
type,
etag,
mtime: stat.mtime.toJSON(),
path: relative(context.output.serverDir, fullPath)
}
}
return virtual({
'~static-assets': `export default ${JSON.stringify(assets, null, 2)};`,
'~static': `
import { promises } from 'fs'
import { resolve } from 'path'
import assets from '~static-assets'
export function readAsset (id) {
return promises.readFile(resolve(mainDir, getAsset(id).path))
}
export function getAsset (id) {
return assets[id]
}
`
})
}
export function dirnames (): Plugin {
return {
name: 'dirnames',
renderChunk (code, chunk) {
return {
code: code + (chunk.isEntry ? 'global.mainDir="undefined"!=typeof __dirname?__dirname:require.main.filename;' : ''),
map: null
}
}
}
}

View File

@ -0,0 +1,40 @@
import { extname } from 'upath'
import type { Plugin, RenderedChunk } from 'rollup'
export interface Options { }
const TIMING = 'global.__timing__'
const iife = code => `(function() { ${code.trim()} })();`.replace(/\n/g, '')
// https://gist.github.com/pi0/1476085924f8a2eb1df85929c20cb43f
const POLYFILL = `const global="undefined"!=typeof globalThis?globalThis:void 0!==o?o:"undefined"!=typeof self?self:{};
global.process = global.process || {};
const o=Date.now(),t=()=>Date.now()-o;global.process.hrtime=global.process.hrtime||(o=>{const e=Math.floor(.001*(Date.now()-t())),a=.001*t();let l=Math.floor(a)+e,n=Math.floor(a%1*1e9);return o&&(l-=o[0],n-=o[1],n<0&&(l--,n+=1e9)),[l,n]});`
const HELPER = POLYFILL + iife(`
const hrtime = global.process.hrtime;
const start = () => hrtime();
const end = s => { const d = hrtime(s); return ((d[0] * 1e9) + d[1]) / 1e6; };
const _s = {};
const metrics = [];
const logStart = id => { _s[id] = hrtime(); };
const logEnd = id => { const t = end(_s[id]); delete _s[id]; metrics.push([id, t]); console.debug('>', id + ' (' + t + 'ms)'); };
${TIMING} = { hrtime, start, end, metrics, logStart, logEnd };
`)
export function timing (_opts: Options = {}): Plugin {
return {
name: 'timing',
renderChunk (code, chunk: RenderedChunk) {
let name = chunk.fileName || ''
name = name.replace(extname(name), '')
const logName = name === 'index' ? 'Cold Start' : ('Load ' + name)
return {
code: (chunk.isEntry ? HELPER : '') + `${TIMING}.logStart('${logName}');` + code + `;${TIMING}.logEnd('${logName}');`,
map: null
}
}
}
}

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

@ -0,0 +1,19 @@
import destr from 'destr'
const runtimeConfig = process.env.RUNTIME_CONFIG as any
for (const type of ['private', 'public']) {
for (const key in runtimeConfig[type]) {
runtimeConfig[type][key] = destr(process.env[key] || runtimeConfig[type][key])
}
}
const $config = global.$config = {
...runtimeConfig.public,
...runtimeConfig.private
}
export default {
public: runtimeConfig.public,
private: $config
}

View File

@ -0,0 +1,8 @@
import _global from '@nuxt/un/runtime/global'
import { $fetch } from 'ohmyfetch'
_global.process = _global.process || {};
(function () { const o = Date.now(); const t = () => Date.now() - o; _global.process.hrtime = _global.process.hrtime || ((o) => { const e = Math.floor(0.001 * (Date.now() - t())); const a = 0.001 * t(); let l = Math.floor(a) + e; let n = Math.floor(a % 1 * 1e9); return o && (l -= o[0], n -= o[1], n < 0 && (l--, n += 1e9)), [l, n] }) })()
global.$fetch = $fetch

View File

@ -0,0 +1,82 @@
import { createRenderer } from 'vue-bundle-renderer'
import devalue from '@nuxt/devalue'
import config from './config'
// @ts-ignore
import { renderToString } from '~renderer'
// @ts-ignore
import createApp from '~build/dist/server/server'
// @ts-ignore
import clientManifest from '~build/dist/server/client.manifest.json'
// @ts-ignore
import htmlTemplate from '~build/views/document.template.js'
function _interopDefault (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e }
const renderer = createRenderer(_interopDefault(createApp), {
clientManifest: _interopDefault(clientManifest),
renderToString
})
const STATIC_ASSETS_BASE = process.env.NUXT_STATIC_BASE + '/' + process.env.NUXT_STATIC_VERSION
const PAYLOAD_JS = '/payload.js'
export async function renderMiddleware (req, res) {
let url = req.url
// payload.json request detection
let isPayloadReq = false
if (url.startsWith(STATIC_ASSETS_BASE) && url.endsWith(PAYLOAD_JS)) {
isPayloadReq = true
url = url.substr(STATIC_ASSETS_BASE.length, url.length - STATIC_ASSETS_BASE.length - PAYLOAD_JS.length)
}
const ssrContext = {
url,
runtimeConfig: {
public: config.public,
private: config.private
},
...(req.context || {})
}
const rendered = await renderer.renderToString(ssrContext)
// TODO: nuxt3 should not reuse `nuxt` property for different purpose!
const payload = ssrContext.payload /* nuxt 3 */ || ssrContext.nuxt /* nuxt 2 */
if (process.env.NUXT_FULL_STATIC) {
payload.staticAssetsBase = STATIC_ASSETS_BASE
}
let data
if (isPayloadReq) {
data = renderPayload(payload, url)
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8')
} else {
data = renderHTML(payload, rendered, ssrContext)
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
}
const error = ssrContext.nuxt && ssrContext.nuxt.error
res.statusCode = error ? error.statusCode : 200
res.end(data, 'utf-8')
}
function renderHTML (payload, rendered, ssrContext) {
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
const _html = rendered.html
const { htmlAttrs = '', bodyAttrs = '', headTags = '', headAttrs = '' } =
(ssrContext.head && ssrContext.head()) || {}
return htmlTemplate({
HTML_ATTRS: htmlAttrs,
HEAD_ATTRS: headAttrs,
BODY_ATTRS: bodyAttrs,
HEAD: headTags +
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
APP: _html + state + rendered.renderScripts()
})
}
function renderPayload (payload, url) {
return `__NUXT_JSONP__("${url}", ${devalue(payload)})`
}

View File

@ -0,0 +1,12 @@
import _renderToString from 'vue-server-renderer/basic'
export function renderToString (component, context) {
return new Promise((resolve, reject) => {
_renderToString(component, context, (err, result) => {
if (err) {
return reject(err)
}
return resolve(result)
})
})
}

View File

@ -0,0 +1,15 @@
// @ts-ignore
import { createRenderer } from '~vueServerRenderer'
const _renderer = createRenderer({})
export function renderToString (component, context) {
return new Promise((resolve, reject) => {
_renderer.renderToString(component, context, (err, result) => {
if (err) {
return reject(err)
}
return resolve(result)
})
})
}

View File

@ -0,0 +1,2 @@
// @ts-ignore
export { renderToString } from '@vue/server-renderer'

View File

@ -0,0 +1,28 @@
import '~polyfill'
import { parseURL } from 'ufo'
import { localCall } from '../server'
export default async function handle (context, req) {
let url: string
if (req.headers['x-ms-original-url']) {
// This URL has been proxied as there was no static file matching it.
url = parseURL(req.headers['x-ms-original-url']).pathname
} else {
// Because Azure SWA handles /api/* calls differently they
// never hit the proxy and we have to reconstitute the URL.
url = '/api/' + (req.params.url || '')
}
const { body, status, statusText, headers } = await localCall({
url,
headers: req.headers,
method: req.method,
body: req.body
})
context.res = {
status,
headers,
body: body ? body.toString() : statusText
}
}

View File

@ -0,0 +1,19 @@
import '~polyfill'
import { localCall } from '../server'
export default async function handle (context, req) {
const url = '/' + (req.params.url || '')
const { body, status, statusText, headers } = await localCall({
url,
headers: req.headers,
method: req.method,
body: req.body
})
context.res = {
status,
headers,
body: body ? body.toString() : statusText
}
}

View File

@ -0,0 +1,24 @@
import '~polyfill'
import { localCall } from '../server'
async function cli () {
const url = process.argv[2] || '/'
const debug = (label, ...args) => console.debug(`> ${label}:`, ...args)
const r = await localCall({ url })
debug('URL', url)
debug('StatusCode', r.status)
debug('StatusMessage', r.statusText)
// @ts-ignore
for (const header of r.headers.entries()) {
debug(header[0], header[1])
}
console.log('\n', r.body.toString())
}
if (require.main === module) {
cli().catch((err) => {
console.error(err)
process.exit(1)
})
}

View File

@ -0,0 +1,47 @@
import '~polyfill'
import { getAssetFromKV } from '@cloudflare/kv-asset-handler'
import { localCall } from '../server'
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
addEventListener('fetch', (event: any) => {
event.respondWith(handleEvent(event))
})
async function handleEvent (event) {
try {
return await getAssetFromKV(event, { cacheControl: assetsCacheControl })
} catch (_err) {
// Ignore
}
const url = new URL(event.request.url)
const r = await localCall({
event,
url: url.pathname + url.search,
host: url.hostname,
protocol: url.protocol,
headers: event.request.headers,
method: event.request.method,
redirect: event.request.redirect,
body: event.request.body
})
return new Response(r.body, {
// @ts-ignore
headers: r.headers,
status: r.status,
statusText: r.statusText
})
}
function assetsCacheControl (request) {
if (request.url.includes(PUBLIC_PATH) /* TODO: Check with routerBase */) {
return {
browserTTL: 31536000,
edgeTTL: 31536000
}
}
return {}
}

View File

@ -0,0 +1,14 @@
import '~polyfill'
import { Server } from 'http'
import { parentPort } from 'worker_threads'
import type { AddressInfo } from 'net'
import { handle } from '../server'
const server = new Server(handle)
const netServer = server.listen(0, () => {
parentPort.postMessage({
event: 'listen',
port: (netServer.address() as AddressInfo).port
})
})

View File

@ -0,0 +1,7 @@
import '~polyfill'
import { handle } from '../server'
const functions = require('firebase-functions')
export const server = functions.https.onRequest(handle)

View File

@ -0,0 +1,21 @@
import '~polyfill'
import { withQuery } from 'ufo'
import { localCall } from '../server'
export async function handler (event, context) {
const r = await localCall({
event,
url: withQuery(event.path, event.queryStringParameters),
context,
headers: event.headers,
method: event.httpMethod,
query: event.queryStringParameters,
body: event.body // TODO: handle event.isBase64Encoded
})
return {
statusCode: r.status,
headers: r.headers,
body: r.body.toString()
}
}

View File

@ -0,0 +1,2 @@
import '~polyfill'
export * from '../server'

View File

@ -0,0 +1,20 @@
import '~polyfill'
import { Server } from 'http'
import destr from 'destr'
import { handle } from '../server'
const server = new Server(handle)
const port = (destr(process.env.NUXT_PORT || process.env.PORT) || 3000) as number
const hostname = process.env.NUXT_HOST || process.env.HOST || 'localhost'
// @ts-ignore
server.listen(port, hostname, (err) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log(`Listening on http://${hostname}:${port}`)
})
export default {}

View File

@ -0,0 +1,40 @@
// @ts-nocheck
import '~polyfill'
import { localCall } from '../server'
addEventListener('fetch', (event: any) => {
const url = new URL(event.request.url)
if (url.pathname.includes('.') /* is file */) {
return
}
event.respondWith(handleEvent(url, event))
})
async function handleEvent (url, event) {
const r = await localCall({
event,
url: url.pathname,
host: url.hostname,
protocol: url.protocol,
headers: event.request.headers,
method: event.request.method,
redirect: event.request.redirect,
body: event.request.body
})
return new Response(r.body, {
headers: r.headers,
status: r.status,
statusText: r.statusText
})
}
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})

View File

@ -0,0 +1,4 @@
import '~polyfill'
import { handle } from '../server'
export default handle

View File

@ -0,0 +1,67 @@
// import ansiHTML from 'ansi-html'
const cwd = process.cwd()
// TODO: Handle process.env.DEBUG
export function handleError (error, req, res) {
const stack = (error.stack || '')
.split('\n')
.splice(1)
.filter(line => line.includes('at '))
.map((line) => {
const text = line
.replace(cwd + '/', './')
.replace('webpack:/', '')
.replace('.vue', '.js') // TODO: Support sourcemap
.trim()
return {
text,
internal: (line.includes('node_modules') && !line.includes('.cache')) ||
line.includes('internal') ||
line.includes('new Promise')
}
})
console.error(error.message + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Nuxt Error</title>
<style>
html, body {
background: white;
color: red;
font-family: monospace;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
}
.stack {
padding-left: 2em;
}
.stack.internal {
color: grey;
}
</style>
</head>
<body>
<div>
<div>${req.method} ${req.url}</div><br>
<h1>${error.toString()}</h1>
<pre>${stack.map(i =>
`<span class="stack${i.internal ? ' internal' : ''}">${i.text}</span>`
).join('\n')
}</pre>
</div>
</body>
</html>
`
res.statusCode = error.statusCode || 500
res.statusMessage = error.statusMessage || 'Internal Error'
res.end(html)
}

View File

@ -0,0 +1,24 @@
import '../app/config'
import { createApp, useBase } from 'h3'
import { createFetch } from 'ohmyfetch'
import destr from 'destr'
import { createCall, createFetch as createLocalFetch } from '@nuxt/un/runtime/fetch'
import { timingMiddleware } from './timing'
import { handleError } from './error'
// @ts-ignore
import serverMiddleware from '~serverMiddleware'
const app = createApp({
debug: destr(process.env.DEBUG),
onError: handleError
})
app.use(timingMiddleware)
app.use(serverMiddleware)
app.use(() => import('../app/render').then(e => e.renderMiddleware), { lazy: true })
export const stack = app.stack
export const handle = useBase(process.env.ROUTER_BASE, app)
export const localCall = createCall(handle)
export const localFetch = createLocalFetch(localCall, global.fetch)
export const $fetch = global.$fetch = createFetch({ fetch: localFetch })

View File

@ -0,0 +1,71 @@
import { createError } from 'h3'
import { withoutTrailingSlash, withLeadingSlash, parseURL } from 'ufo'
// @ts-ignore
import { getAsset, readAsset } from '~static'
const METHODS = ['HEAD', 'GET']
const PUBLIC_PATH = process.env.PUBLIC_PATH // Default: /_nuxt/
const TWO_DAYS = 2 * 60 * 60 * 24
// eslint-disable-next-line
export default async function serveStatic(req, res) {
if (!METHODS.includes(req.method)) {
return
}
let id = withLeadingSlash(withoutTrailingSlash(parseURL(req.url).pathname))
let asset = getAsset(id)
// Try index.html
if (!asset) {
const _id = id + '/index.html'
const _asset = getAsset(_id)
if (_asset) {
asset = _asset
id = _id
}
}
if (!asset) {
if (id.startsWith(PUBLIC_PATH)) {
throw createError({
statusMessage: 'Cannot find static asset ' + id,
statusCode: 404
})
}
return
}
const ifNotMatch = req.headers['if-none-match'] === asset.etag
if (ifNotMatch) {
res.statusCode = 304
return res.end('Not Modified (etag)')
}
const ifModifiedSinceH = req.headers['if-modified-since']
if (ifModifiedSinceH && asset.mtime) {
if (new Date(ifModifiedSinceH) >= new Date(asset.mtime)) {
res.statusCode = 304
return res.end('Not Modified (mtime)')
}
}
if (asset.type) {
res.setHeader('Content-Type', asset.type)
}
if (asset.etag) {
res.setHeader('ETag', asset.etag)
}
if (asset.mtime) {
res.setHeader('Last-Modified', asset.mtime)
}
if (id.startsWith(PUBLIC_PATH)) {
res.setHeader('Cache-Control', `max-age=${TWO_DAYS}, immutable`)
}
const contents = await readAsset(id)
return res.end(contents)
}

View File

@ -0,0 +1,22 @@
export const globalTiming = global.__timing__ || {
start: () => 0,
end: () => 0,
metrics: []
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
export function timingMiddleware (_req, res, next) {
const start = globalTiming.start()
const _end = res.end
res.end = (data, encoding, callback) => {
const metrics = [['Generate', globalTiming.end(start)], ...globalTiming.metrics]
const serverTiming = metrics.map(m => `-;dur=${m[1]};desc="${encodeURIComponent(m[0])}"`).join(', ')
if (!res.headersSent) {
res.setHeader('Server-Timing', serverTiming)
}
_end.call(res, data, encoding, callback)
}
next()
}

6
packages/nitro/src/runtime/types.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module NodeJS {
interface Global {
__timing__: any
$config: any
}
}

View File

@ -0,0 +1,143 @@
import { Worker } from 'worker_threads'
import { createApp } from 'h3'
import { resolve } from 'upath'
import debounce from 'debounce'
import chokidar from 'chokidar'
import { listen, Listener } from 'listhen'
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'
export function createDevServer (nitroContext: NitroContext) {
// Worker
const workerEntry = resolve(nitroContext.output.dir, nitroContext.output.serverDir, 'index.js')
let pendingWorker: Worker
let activeWorker: Worker
let workerAddress: string
async function reload () {
if (pendingWorker) {
await pendingWorker.terminate()
workerAddress = null
pendingWorker = null
}
if (!(await stat(workerEntry)).isFile) {
throw new Error('Entry not found: ' + workerEntry)
}
return new Promise((resolve, reject) => {
const worker = pendingWorker = new Worker(workerEntry)
worker.once('exit', (code) => {
if (code) {
reject(new Error('[worker] exited with code: ' + code))
}
})
worker.on('error', (err) => {
err.message = '[worker] ' + err.message
reject(err)
})
worker.on('message', (event) => {
if (event && event.port) {
workerAddress = 'http://localhost:' + event.port
activeWorker = worker
pendingWorker = null
resolve(workerAddress)
}
})
})
}
// App
const app = createApp()
// _nuxt and static
app.use(nitroContext._nuxt.publicPath, serveStatic(resolve(nitroContext._nuxt.buildDir, 'dist/client')))
app.use(nitroContext._nuxt.routerBase, serveStatic(resolve(nitroContext._nuxt.staticDir)))
// Dynamic Middlwware
const legacyMiddleware = createDynamicMiddleware()
const devMiddleware = createDynamicMiddleware()
app.use(legacyMiddleware.middleware)
app.use(devMiddleware.middleware)
// serve placeholder 404 assets instead of hitting SSR
app.use(nitroContext._nuxt.publicPath, servePlaceholder())
app.use(nitroContext._nuxt.routerBase, servePlaceholder({ skipUnknown: true }))
// SSR Proxy
const proxy = createProxy()
app.use((req, res) => {
if (workerAddress) {
proxy.web(req, res, { target: workerAddress }, (_err) => {
// console.error('[proxy]', err)
})
} else {
res.end('Worker not ready!')
}
})
// Listen
let listeners: Listener[] = []
const _listen = async (port, opts?) => {
const listener = await listen(app, { port, ...opts })
listeners.push(listener)
return listener
}
// Watch for dist and reload worker
const pattern = '**/*.{js,json}'
const events = ['add', 'change']
let watcher
function watch () {
if (watcher) { return }
const dReload = debounce(() => reload().catch(console.warn), 200, true)
watcher = chokidar.watch([
resolve(nitroContext.output.serverDir, pattern),
resolve(nitroContext._nuxt.buildDir, 'dist/server', pattern)
]).on('all', event => events.includes(event) && dReload())
}
// Close handler
async function close () {
if (watcher) {
await watcher.close()
}
if (activeWorker) {
await activeWorker.terminate()
}
if (pendingWorker) {
await pendingWorker.terminate()
}
await Promise.all(listeners.map(l => l.close()))
listeners = []
}
nitroContext._internal.hooks.hook('close', close)
return {
reload,
listen: _listen,
close,
watch,
setLegacyMiddleware: legacyMiddleware.set,
setDevMiddleware: devMiddleware.set
}
}
function createDynamicMiddleware () {
let middleware
return {
set: (input) => {
if (!Array.isArray(input)) {
middleware = input
return
}
const app = require('connect')()
for (const m of input) {
app.use(m.path || m.route || '/', m.handler || m.handle)
}
middleware = app
},
middleware: (req, res, next) =>
middleware ? middleware(req, res, next) : next()
}
}

View File

@ -0,0 +1,78 @@
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?: (results: ServerMiddleware[], event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string) => void): 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 {
delete m.handler
delete m.path
middleware.push({
...m,
handle: resolvePath(handle),
route
})
}
}
return {
middleware,
legacyMiddleware
}
}

View File

@ -0,0 +1,11 @@
import type { $Fetch } from 'ohmyfetch'
declare global {
const $fetch: $Fetch
namespace NodeJS {
interface Global {
$fetch: $Fetch
}
}
}

View File

@ -0,0 +1,122 @@
import { relative, dirname, resolve } from 'upath'
import fse from 'fs-extra'
import jiti from 'jiti'
import defu from 'defu'
import Hookable from 'hookable'
import consola from 'consola'
import chalk from 'chalk'
import { get } from 'dot-prop'
import type { NitroPreset, NitroInput } from '../context'
export const MODULE_DIR = resolve(__dirname, '..')
export function hl (str: string) {
return chalk.cyan(str)
}
export function prettyPath (p: string, highlight = true) {
p = relative(process.cwd(), p)
return highlight ? hl(p) : p
}
export function compileTemplate (contents: string) {
return (params: Record<string, any>) => contents.replace(/{{ ?([\w.]+) ?}}/g, (_, match) => {
const val = get(params, match)
if (!val) {
consola.warn(`cannot resolve template param '${match}' in ${contents.substr(0, 20)}`)
}
return val as string || `${match}`
})
}
export function serializeTemplate (contents: string) {
// eslint-disable-next-line no-template-curly-in-string
return `(params) => \`${contents.replace(/{{ (\w+) }}/g, '${params.$1}')}\``
}
export function jitiImport (dir: string, path: string) {
return jiti(dir)(path)
}
export function tryImport (dir: string, path: string) {
try {
return jitiImport(dir, path)
} catch (_err) { }
}
export async function writeFile (file, contents, log = false) {
await fse.mkdirp(dirname(file))
await fse.writeFile(file, contents, 'utf-8')
if (log) {
consola.info('Generated', prettyPath(file))
}
}
export function resolvePath (nitroContext: NitroInput, path: string | ((nitroContext) => string), resolveBase: string = ''): string {
if (typeof path === 'function') {
path = path(nitroContext)
}
if (typeof path !== 'string') {
throw new TypeError('Invalid path: ' + path)
}
path = compileTemplate(path)(nitroContext)
return resolve(resolveBase, path)
}
export function detectTarget () {
if (process.env.NETLIFY) {
return 'netlify'
}
if (process.env.NOW_BUILDER) {
return 'vercel'
}
if (process.env.INPUT_AZURE_STATIC_WEB_APPS_API_TOKEN) {
return 'azure'
}
}
export async function isDirectory (path: string) {
try {
return (await fse.stat(path)).isDirectory()
} catch (_err) {
return false
}
}
export function extendPreset (base: NitroPreset, preset: NitroPreset): NitroPreset {
return (config: NitroInput) => {
if (typeof preset === 'function') {
preset = preset(config)
}
if (typeof base === 'function') {
base = base(config)
}
return defu({
hooks: Hookable.mergeHooks(base.hooks, preset.hooks)
}, preset, base)
}
}
const _getDependenciesMode = {
dev: ['devDependencies'],
prod: ['dependencies'],
all: ['devDependencies', 'dependencies']
}
export function getDependencies (dir: string, mode: keyof typeof _getDependenciesMode = 'all') {
const fields = _getDependenciesMode[mode]
const pkg = require(resolve(dir, 'package.json'))
const dependencies = []
for (const field of fields) {
if (pkg[field]) {
for (const name in pkg[field]) {
dependencies.push(name)
}
}
}
return dependencies
}

View File

@ -0,0 +1,50 @@
import { resolve, dirname, relative } from 'upath'
import globby from 'globby'
import prettyBytes from 'pretty-bytes'
import gzipSize from 'gzip-size'
import { readFile } from 'fs-extra'
import chalk from 'chalk'
import stdenv from 'std-env'
export async function printFSTree (dir) {
if (stdenv.test) {
return
}
const files = await globby('**/*.*', { cwd: dir })
const items = (await Promise.all(files.map(async (file) => {
const path = resolve(dir, file)
const src = await readFile(path)
const size = src.byteLength
const gzip = await gzipSize(src)
return { file, path, size, gzip }
}))).sort((a, b) => b.path.localeCompare(a.path))
let totalSize = 0
let totalGzip = 0
let totalNodeModulesSize = 0
let totalNodeModulesGzip = 0
items.forEach((item, index) => {
let dir = dirname(item.file)
if (dir === '.') { dir = '' }
const rpath = relative(process.cwd(), item.path)
const treeChar = index === items.length - 1 ? '└─' : '├─'
const isNodeModules = item.file.includes('node_modules')
if (isNodeModules) {
totalNodeModulesSize += item.size
totalNodeModulesGzip += item.gzip
return
}
process.stdout.write(chalk.gray(` ${treeChar} ${rpath} (${prettyBytes(item.size)}) (${prettyBytes(item.gzip)} gzip)\n`))
totalSize += item.size
totalGzip += item.gzip
})
process.stdout.write(`${chalk.cyan('Σ Total size:')} ${prettyBytes(totalSize + totalNodeModulesSize)} (${prettyBytes(totalGzip + totalNodeModulesGzip)} gzip)\n`)
}

View File

@ -0,0 +1,7 @@
import { join } from 'upath'
import fsExtra from 'fs-extra'
export const wpfs = {
...fsExtra,
join
}