feat(nitro): server assets (#83)

This commit is contained in:
pooya parsa 2021-04-12 23:28:48 +02:00 committed by GitHub
parent 31f06e9f69
commit babb70a4bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 38 deletions

View File

@ -7,6 +7,7 @@ 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 { StorageOptions } from './rollup/plugins/storage' import type { StorageOptions } from './rollup/plugins/storage'
import type { AssetOptions } from './rollup/plugins/assets'
import type { ServerMiddleware } from './server/middleware' import type { ServerMiddleware } from './server/middleware'
export interface NitroContext { export interface NitroContext {
@ -34,6 +35,7 @@ export interface NitroContext {
publicDir: string publicDir: string
} }
storage: StorageOptions, storage: StorageOptions,
assets: AssetOptions,
_nuxt: { _nuxt: {
majorVersion: number majorVersion: number
dev: boolean dev: boolean
@ -88,6 +90,10 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
publicDir: '{{ output.dir }}/public' publicDir: '{{ output.dir }}/public'
}, },
storage: { mounts: { } }, storage: { mounts: { } },
assets: {
inline: !nuxtOptions.dev,
dirs: {}
},
_nuxt: { _nuxt: {
majorVersion: nuxtOptions._majorVersion || 2, majorVersion: nuxtOptions._majorVersion || 2,
dev: nuxtOptions.dev, dev: nuxtOptions.dev,
@ -148,6 +154,11 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N
} }
} }
// Assets
nitroContext.assets.dirs.server = {
dir: resolve(nitroContext._nuxt.rootDir, 'server/assets'), meta: true
}
// console.log(nitroContext) // console.log(nitroContext)
// process.exit(1) // process.exit(1)

View File

@ -21,6 +21,7 @@ import { externals } from './plugins/externals'
import { timing } from './plugins/timing' import { timing } from './plugins/timing'
import { autoMock } from './plugins/automock' import { autoMock } from './plugins/automock'
import { staticAssets, dirnames } from './plugins/static' import { staticAssets, dirnames } from './plugins/static'
import { assets } from './plugins/assets'
import { middleware } from './plugins/middleware' import { middleware } from './plugins/middleware'
import { esbuild } from './plugins/esbuild' import { esbuild } from './plugins/esbuild'
import { raw } from './plugins/raw' import { raw } from './plugins/raw'
@ -81,8 +82,10 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
prefix = 'nuxt' prefix = 'nuxt'
} else if (lastModule.startsWith(nitroContext._internal.runtimeDir)) { } else if (lastModule.startsWith(nitroContext._internal.runtimeDir)) {
prefix = 'nitro' prefix = 'nitro'
} else if (!prefix && nitroContext.middleware.find(m => lastModule.startsWith(m.handle))) { } else if (!prefix && nitroContext.middleware.find(m => lastModule.startsWith(m.handle as string))) {
prefix = 'middleware' prefix = 'middleware'
} else if (lastModule.includes('assets')) {
prefix = 'assets'
} }
return join('chunks', prefix, '[name].js') return join('chunks', prefix, '[name].js')
}, },
@ -148,7 +151,11 @@ export const getRollupConfig = (nitroContext: NitroContext) => {
} }
})) }))
// Assets
rollupConfig.plugins.push(assets(nitroContext.assets))
// Static // Static
// TODO: use assets plugin
if (nitroContext.serveStatic) { if (nitroContext.serveStatic) {
rollupConfig.plugins.push(dirnames()) rollupConfig.plugins.push(dirnames())
rollupConfig.plugins.push(staticAssets(nitroContext)) rollupConfig.plugins.push(staticAssets(nitroContext))

View File

@ -0,0 +1,103 @@
import { readFile, stat } from 'fs/promises'
import type { Plugin } from 'rollup'
import createEtag from 'etag'
import mime from 'mime'
import { resolve } from 'upath'
import globby from 'globby'
import virtual from './virtual'
export interface AssetOptions {
inline: Boolean
dirs: {
[assetdir: string]: {
dir: string
meta?: boolean
}
}
}
export function assets (opts: AssetOptions): Plugin {
type Asset = {
fsPath: string,
meta: {
type?: string,
etag?: string,
mtime?: string
}
}
const assetUtils = `
export function readAsset (id) {
return getAsset(id).read()
}
export function statAsset (id) {
return getAsset(id).meta
}
`
if (!opts.inline) {
return virtual({
'~nitro/assets': `
import { statSync, promises as fsp } from 'fs'
import { resolve } from 'path'
const dirs = ${JSON.stringify(opts.dirs)}
${assetUtils}
export function getAsset (id) {
for (const dirname in dirs) {
if (id.startsWith(dirname + '/')) {
const dirOpts = dirs[dirname]
const path = resolve(dirOpts.dir, id.substr(dirname.length + 1))
let stat = statSync(path)
const asset = {
read: () => fsp.readFile(path, 'utf-8'),
meta: {
mtime: stat.mtime
}
}
return asset
}
}
throw new Error('Asset dir not found: ' + id)
}
`
})
}
return virtual({
'~nitro/assets': {
async load () {
const assets: Record<string, Asset> = {}
for (const assetdir in opts.dirs) {
const dirOpts = opts.dirs[assetdir]
const files = globby.sync('**/*.*', { cwd: dirOpts.dir, absolute: false })
for (const _id of files) {
const fsPath = resolve(dirOpts.dir, _id)
const id = assetdir + '/' + _id
assets[id] = { fsPath, meta: {} }
if (dirOpts.meta) {
let type = mime.getType(id) || 'text/plain'
if (type.startsWith('text')) { type += '; charset=utf-8' }
const etag = createEtag(await readFile(fsPath))
const mtime = await stat(fsPath).then(s => s.mtime.toJSON())
assets[id].meta = { type, etag, mtime }
}
}
}
const inlineAssets = `const assets = {\n${Object.keys(assets).map(id =>
` ['${id}']: {\n read: () => import('${assets[id].fsPath}'),\n meta: ${JSON.stringify(assets[id].meta)}\n }`
).join(',\n')}\n}`
return `${inlineAssets}\n${assetUtils}
export function getAsset (id) {
if (!assets[id]) {
throw new Error('Asset not found : ' + id)
}
return assets[id]
}`
}
}
})
}

View File

@ -12,30 +12,32 @@ export function middleware (getMiddleware: () => ServerMiddleware[]) {
let lastDump = '' let lastDump = ''
return virtual({ return virtual({
'~serverMiddleware': () => { '~serverMiddleware': {
const middleware = getMiddleware() load: () => {
const middleware = getMiddleware()
if (!stdenv.test) { if (!stdenv.test) {
const dumped = dumpMiddleware(middleware) const dumped = dumpMiddleware(middleware)
if (dumped !== lastDump) { if (dumped !== lastDump) {
lastDump = dumped lastDump = dumped
if (middleware.length) { if (middleware.length) {
console.log(dumped) 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
`
} }
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
`
} }
}) })
} }

View File

@ -13,7 +13,10 @@ export function raw (opts: RawOptions = {}): Plugin {
name: 'raw', name: 'raw',
transform (code, id) { transform (code, id) {
if (id[0] !== '\0' && extensions.has(extname(id))) { if (id[0] !== '\0' && extensions.has(extname(id))) {
return `// ${id}\nexport default ${JSON.stringify(code)}` return {
code: `// ${id}\nexport default ${JSON.stringify(code)}`,
map: null
}
} }
} }
} }

View File

@ -3,21 +3,21 @@ import * as path from 'path'
import { Plugin } from 'rollup' import { Plugin } from 'rollup'
type UnresolvedModule = string | (() => string) type VirtualModule = string | { load: () => string | Promise<string> }
export interface RollupVirtualOptions { export interface RollupVirtualOptions {
[id: string]: UnresolvedModule; [id: string]: VirtualModule;
} }
const PREFIX = '\0virtual:' const PREFIX = '\0virtual:'
const resolveModule = (m: UnresolvedModule) => typeof m === 'function' ? m() : m
export default function virtual (modules: RollupVirtualOptions): Plugin { export default function virtual (modules: RollupVirtualOptions): Plugin {
const resolvedIds = new Map<string, string |(() => string)>() const _modules = new Map<string, VirtualModule>()
Object.keys(modules).forEach((id) => { for (const [id, mod] of Object.entries(modules)) {
resolvedIds.set(path.resolve(id), modules[id]) _modules.set(id, mod)
}) _modules.set(path.resolve(id), mod)
}
return { return {
name: 'virtual', name: 'virtual',
@ -30,20 +30,29 @@ export default function virtual (modules: RollupVirtualOptions): Plugin {
? importer.slice(PREFIX.length) ? importer.slice(PREFIX.length)
: importer : importer
const resolved = path.resolve(path.dirname(importerNoPrefix), id) const resolved = path.resolve(path.dirname(importerNoPrefix), id)
if (resolvedIds.has(resolved)) { return PREFIX + resolved } if (_modules.has(resolved)) { return PREFIX + resolved }
} }
return null return null
}, },
load (id) { async load (id) {
if (!id.startsWith(PREFIX)) { if (!id.startsWith(PREFIX)) { return null }
return null
}
const idNoPrefix = id.slice(PREFIX.length) const idNoPrefix = id.slice(PREFIX.length)
return idNoPrefix in modules if (!_modules.has(idNoPrefix)) { return null }
? resolveModule(modules[idNoPrefix])
: resolveModule(resolvedIds.get(idNoPrefix)) let m = _modules.get(idNoPrefix)
if (typeof m !== 'string' && typeof m.load === 'function') {
m = await m.load()
}
// console.log('[virtual]', idNoPrefix, '\n', m)
return {
code: m as string,
map: null
}
} }
} }
} }