This commit is contained in:
pooya parsa 2020-11-20 01:16:31 +01:00 committed by GitHub
parent 047761f8b7
commit c06f09e9ab
32 changed files with 735 additions and 506 deletions

View File

@ -1,48 +1,97 @@
import { resolve } from 'path'
import { resolve, join } from 'upath'
import consola from 'consola'
import { rollup } from 'rollup'
import Hookable from 'hookable'
import { readFile, emptyDir } from 'fs-extra'
import { rollup, watch as rollupWatch } from 'rollup'
import ora from 'ora'
import { readFile, emptyDir, copy } from 'fs-extra'
import { printFSTree } from './utils/tree'
import { getRollupConfig } from './rollup/config'
import { hl, serializeTemplate, writeFile } from './utils'
import { SLSOptions } from './config'
import { SigmaContext } from './context'
export async function build (options: SLSOptions) {
consola.info(`Generating bundle for ${hl(options.target)}`)
export async function build (sigmaContext: SigmaContext) {
consola.info(`Sigma preset is ${hl(sigmaContext.preset)}`)
const hooks = new Hookable()
hooks.addHooks(options.hooks)
if (options.cleanTargetDir) {
await emptyDir(options.targetDir)
}
// Cleanup output dir
await emptyDir(sigmaContext.output.dir)
// Compile html template
const htmlSrc = resolve(options.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`)
const htmlSrc = resolve(sigmaContext._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 hooks.callHook('template:document', htmlTemplate)
await sigmaContext._internal.hooks.callHook('sigma:template:document', htmlTemplate)
await writeFile(htmlTemplate.dst, htmlTemplate.compiled)
options.rollupConfig = getRollupConfig(options)
await generate(sigmaContext)
await hooks.callHook('rollup:before', options)
sigmaContext.rollupConfig = getRollupConfig(sigmaContext)
const build = await rollup(options.rollupConfig).catch((error) => {
error.message = '[serverless] Rollup Error: ' + error.message
await sigmaContext._internal.hooks.callHook('sigma:rollup:before', sigmaContext)
return sigmaContext._nuxt.dev ? _watch(sigmaContext) : _build(sigmaContext)
}
export async function generate (sigmaContext: SigmaContext) {
await copy(
resolve(sigmaContext._nuxt.buildDir, 'dist/client'),
join(sigmaContext.output.publicDir, sigmaContext._nuxt.publicPath)
)
await copy(
resolve(sigmaContext._nuxt.rootDir, sigmaContext._nuxt.staticDir),
sigmaContext.output.publicDir
)
}
async function _build (sigmaContext: SigmaContext) {
const spinner = ora()
spinner.start('Building server...')
const build = await rollup(sigmaContext.rollupConfig).catch((error) => {
spinner.fail('Rollup error: ' + error.messsage)
throw error
})
await build.write(options.rollupConfig.output)
spinner.start('Wrting Sigma bundle...')
await build.write(sigmaContext.rollupConfig.output)
await printFSTree(options.targetDir)
await hooks.callHook('done', options)
spinner.succeed('Sigma built')
await printFSTree(sigmaContext.output.serverDir)
await sigmaContext._internal.hooks.callHook('sigma:compiled', sigmaContext)
return {
entry: resolve(options.rollupConfig.output.dir, options.rollupConfig.output.entryFileNames)
entry: resolve(sigmaContext.rollupConfig.output.dir, sigmaContext.rollupConfig.output.entryFileNames)
}
}
function _watch (sigmaContext: SigmaContext) {
const spinner = ora()
const watcher = rollupWatch(sigmaContext.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()
spinner.start('Building Sigma...')
return
// Finished building all bundles
case 'END':
sigmaContext._internal.hooks.callHook('sigma:compiled', sigmaContext)
return spinner.succeed(`Sigma built in ${Date.now() - start} ms`)
// Encountered an error while bundling
case 'ERROR':
spinner.fail('Rollup error: ' + event.error)
// consola.error(event.error)
}
})
}

View File

@ -1,108 +0,0 @@
import { resolve } from 'path'
import defu from 'defu'
import type { NuxtOptions } from '@nuxt/types'
import Hookable, { configHooksT } from 'hookable'
import { tryImport, resolvePath, detectTarget, extendTarget } from './utils'
import * as TARGETS from './targets'
// eslint-disable-next-line
export type UnresolvedPath = string | ((config: SLSOptions) => string)
export interface Nuxt extends Hookable{
options: NuxtOptions
}
export interface ServerMiddleware {
route: string
handle: string
lazy?: boolean
}
export interface SLSOptions {
hooks: configHooksT
nuxtHooks: configHooksT
rootDir: string
buildDir: string
publicDir: string
routerBase: string
publicPath: string
fullStatic: boolean
staticAssets: any
entry: UnresolvedPath
outName: string
node: false | true
target: string
minify: boolean
externals: boolean
rollupConfig?: any
timing: boolean
inlineChunks: boolean
renderer: string
analyze: boolean
cleanTargetDir: boolean
runtimeDir: string
slsDir: string
targetDir: string
serverMiddleware: ServerMiddleware[],
static: string[]
generateIgnore: string[]
}
export interface SLSConfig extends Omit<Partial<SLSOptions>, 'targetDir'> {
targetDir?: UnresolvedPath
}
export type SLSTargetFn = (config: SLSConfig) => SLSConfig
export type SLSTarget = SLSConfig | SLSTargetFn
export function getoptions (nuxtOptions: Nuxt['options'], serverless: SLSConfig): SLSOptions {
const defaults: SLSConfig = {
rootDir: nuxtOptions.rootDir,
buildDir: nuxtOptions.buildDir,
publicDir: nuxtOptions.generate.dir,
routerBase: nuxtOptions.router.base,
publicPath: nuxtOptions.build.publicPath,
fullStatic: nuxtOptions.target === 'static' && !nuxtOptions._legacyGenerate,
// @ts-ignore
staticAssets: nuxtOptions.generate.staticAssets,
outName: '_nuxt.js',
timing: true,
inlineChunks: true,
minify: false,
externals: false,
cleanTargetDir: true,
runtimeDir: resolve(__dirname, '../runtime'),
slsDir: '{{ rootDir }}/.nuxt/serverless',
targetDir: '{{ slsDir }}/{{ target }}',
serverMiddleware: serverless.serverMiddleware || [],
static: [],
generateIgnore: []
}
const target = serverless.target || process.env.NUXT_SLS_TARGET || detectTarget()
let targetDefaults = TARGETS[target] || tryImport(nuxtOptions.rootDir, target)
if (!targetDefaults) {
throw new Error('Cannot resolve target: ' + target)
}
targetDefaults = targetDefaults.default || targetDefaults
const _defaults = defu(defaults, { target })
const _targetInput = defu(nuxtOptions.serverless, _defaults)
const _target = extendTarget(nuxtOptions.serverless, targetDefaults)(_targetInput)
const options: SLSOptions = defu(nuxtOptions.serverless, _target, _defaults)
options.slsDir = resolvePath(options, options.slsDir)
options.targetDir = resolvePath(options, options.targetDir)
options.publicDir = resolvePath(options, options.publicDir)
return options
}

View File

@ -0,0 +1,110 @@
import { resolve } from 'upath'
import defu from 'defu'
import type { NuxtOptions } from '@nuxt/types'
import Hookable, { configHooksT } from 'hookable'
import { tryImport, resolvePath, detectTarget, extendPreset } from './utils'
import * as PRESETS from './presets'
export interface ServerMiddleware {
route: string
handle: string
lazy?: boolean
}
export interface SigmaContext {
timing: boolean
inlineChunks: boolean
minify: boolean
externals: boolean
analyze: boolean
entry: string
node: boolean
preset: string
rollupConfig?: any
renderer: string
middleware: ServerMiddleware[]
hooks: configHooksT
ignore: string[]
output: {
dir: string
serverDir: string
publicDir: string
}
_nuxt: {
dev: boolean
rootDir: string
buildDir: string
staticDir: string
routerBase: string
publicPath: string
fullStatic: boolean
staticAssets: any
}
_internal: {
runtimeDir: string
hooks: Hookable
}
}
export interface SigmaInput extends Partial<SigmaContext> {}
export type SigmaPreset = SigmaInput | ((input: SigmaInput) => SigmaInput)
export function getsigmaContext (nuxtOptions: NuxtOptions, input: SigmaInput): SigmaContext {
const defaults: SigmaContext = {
timing: true,
inlineChunks: true,
minify: true,
externals: false,
analyze: false,
entry: undefined,
node: undefined,
preset: undefined,
rollupConfig: undefined,
renderer: undefined,
middleware: [],
ignore: [],
hooks: {},
output: {
dir: '{{ _nuxt.rootDir }}/.output',
serverDir: '{{ output.dir }}/server',
publicDir: '{{ output.dir }}/public'
},
_nuxt: {
dev: nuxtOptions.dev,
rootDir: nuxtOptions.rootDir,
buildDir: nuxtOptions.buildDir,
staticDir: nuxtOptions.dir.static,
routerBase: nuxtOptions.router.base,
publicPath: nuxtOptions.build.publicPath,
fullStatic: nuxtOptions.preset === 'static' && !nuxtOptions._legacyGenerate,
// @ts-ignore
staticAssets: nuxtOptions.generate.staticAssets
},
_internal: {
runtimeDir: resolve(__dirname, '../runtime'),
hooks: undefined
}
}
defaults.preset = input.preset || process.env.SIGMA_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 sigmaContext: SigmaContext = defu(input, _preset, defaults) as any
sigmaContext.output.dir = resolvePath(sigmaContext, sigmaContext.output.dir)
sigmaContext.output.publicDir = resolvePath(sigmaContext, sigmaContext.output.publicDir)
sigmaContext.output.serverDir = resolvePath(sigmaContext, sigmaContext.output.serverDir)
// console.log(sigmaContext)
// process.exit(1)
return sigmaContext
}

View File

@ -1,85 +1,6 @@
import type { Module } from '@nuxt/types'
import { build } from './build'
import { getoptions } from './config'
import nuxt2 from './module/nuxt2'
export default <Module> function slsModule () {
export default function () {
const { nuxt } = this
if (nuxt.options.dev) {
return
}
// Config
const options = getoptions(nuxt.options, nuxt.options.serverless || {})
// Tune webpack config
nuxt.options.build._minifyServer = false
nuxt.options.build.standalone = false
// Tune generator
nuxt.options.generate.crawler = false
if (Array.isArray(nuxt.options.generate.routes)) {
nuxt.options.generate.routes = Array.from(new Set([
...nuxt.options.generate.routes,
...options.static
]))
}
nuxt.options.generate.dir = options.publicDir
// serverMiddleware
// TODO: render:setupMiddleware hook
// TODO: support m.prefix and m.route
nuxt.hook('modules:done', () => {
const unsupported = []
for (let m of nuxt.options.serverMiddleware) {
if (typeof m === 'string') {
m = { handler: m }
}
const route = m.path || m.route || '/'
const handle = nuxt.resolver.resolvePath(m.handler || m.handle)
if (typeof handle !== 'string' || typeof route !== 'string') {
unsupported.push(m)
continue
}
options.serverMiddleware.push({ ...m, route, handle })
}
if (unsupported.length) {
console.warn('[serverless] Unsupported Server middleware used: ', unsupported)
console.info('Supported format is `{ path: string, handler: string }` and handler should export `(req, res) => {}`')
}
})
if (options.nuxtHooks) {
nuxt.addHooks(options.nuxtHooks)
}
nuxt.hook('generate:cache:ignore', (ignore: string[]) => {
ignore.push(options.slsDir)
ignore.push(options.targetDir)
ignore.push(...options.generateIgnore)
})
nuxt.hook('generate:page', (page) => {
// TODO: Use ssrContext
if (!options.static.includes(page.route)) {
page.exclude = true
}
})
nuxt.hook('generate:before', async () => {
console.info('Building light version for `nuxt generate`')
const { entry } = await build(getoptions(nuxt.options, {
target: 'cjs',
serverMiddleware: options.serverMiddleware
}))
console.info('Loading lambda')
require(entry)
})
nuxt.hook('generate:done', async () => {
await build(options)
})
return nuxt2(nuxt)
}

View File

@ -0,0 +1,126 @@
import fetch from 'node-fetch'
import { resolve } from 'upath'
import { build } from '../build'
import { getsigmaContext, SigmaContext } from '../context'
import { createDevServer } from '../server'
import wpfs from '../utils/wpfs'
export default function (nuxt) {
// Build in node_modules/.cache/nuxt
const oldBuildDir = nuxt.options.buildDir
nuxt.options.buildDir = resolve(nuxt.options.rootDir, 'node_modules/.cache/nuxt')
nuxt.options.build.transpile = nuxt.options.build.transpile || []
nuxt.options.build.transpile.push(nuxt.options.buildDir)
nuxt.options.appTemplatePath = nuxt.options.appTemplatePath
.replace(oldBuildDir, nuxt.options.buildDir)
// Create contexts
const sigmaContext = getsigmaContext(nuxt.options, nuxt.options.sigma || {})
const sigmaDevContext = getsigmaContext(nuxt.options, { preset: 'dev' })
// Use nuxt as main hooks host
sigmaContext._internal.hooks = nuxt
sigmaDevContext._internal.hooks = nuxt
nuxt.addHooks(sigmaContext.hooks)
// Replace nuxt server
if (nuxt.server) {
nuxt.server.__closed = true
nuxt.server = createNuxt2DevServer(sigmaDevContext)
nuxt.addHooks(sigmaDevContext.hooks)
}
// serverMiddleware bridge
// TODO: render:setupMiddleware hook
// TODO: support m.prefix and m.route
nuxt.hook('modules:done', () => {
const unsupported = []
for (let m of nuxt.options.serverMiddleware) {
if (typeof m === 'string') { m = { handler: m } }
const route = m.path || m.route || '/'
let handle = m.handler || m.handle
if (typeof handle !== 'string' || typeof route !== 'string') {
if (route === '/_loading') {
nuxt.server.setLoadingMiddleware(handle)
continue
}
unsupported.push(m)
continue
}
handle = nuxt.resolver.resolvePath(handle)
sigmaContext.middleware.push({ ...m, route, handle })
sigmaDevContext.middleware.push({ ...m, route, handle })
}
nuxt.options.serverMiddleware = [...unsupported]
if (unsupported.length) {
console.warn('[sigma] Unsupported Server middleware used: \n', ...unsupported)
console.info('Supported format is `{ path: string, handler: string }` and handler should export `(req, res) => {}`')
}
})
// nuxt build/dev
nuxt.options.build._minifyServer = false
nuxt.options.build.standalone = false
nuxt.hook('build:done', async () => {
await build(nuxt.options.dev ? sigmaDevContext : sigmaContext)
})
// nude dev
if (nuxt.options.dev) {
nuxt.hook('sigma:compiled', () => { nuxt.server.watch() })
nuxt.hook('build:compile', ({ compiler }) => { compiler.outputFileSystem = wpfs })
nuxt.hook('server:devMiddleware', (m) => { nuxt.server.setDevMiddleware(m) })
}
// nuxt generate
nuxt.hook('generate:cache:ignore', (ignore: string[]) => {
ignore.push(sigmaContext.output.dir)
ignore.push(sigmaContext.output.serverDir)
ignore.push(sigmaContext.output.publicDir)
ignore.push(...sigmaContext.ignore)
})
// generate:bfore is before webpack build that we need!
nuxt.hook('generate:extendRoutes', async () => {
await build(sigmaDevContext)
await nuxt.server.reload()
})
}
function createNuxt2DevServer (sigmaContext: SigmaContext) {
const server = createDevServer(sigmaContext)
const listeners = []
async function listen (port) {
const listener = await server.listen(port)
listeners.push(listener)
return listeners
}
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,40 @@
import { writeFile } from 'fs-extra'
import { resolve } from 'upath'
import consola from 'consola'
import { extendPreset, prettyPath } from '../utils'
import { SigmaPreset, SigmaContext, SigmaInput } from '../context'
import { worker } from './worker'
export const browser: SigmaPreset = extendPreset(worker, (input: SigmaInput) => {
const script = `<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('${input._nuxt.routerBase}index.js');
});
}
</script>`
return <SigmaInput> {
entry: '{{ _internal.runtimeDir }}/entries/service-worker',
output: {
dir: '{{ _nuxt.rootDir }}/.output/public',
publicDir: '{{ output.dir }}',
serverDir: '{{ output.dir }}'
},
hooks: {
'vue-renderer:ssr:templateParams' (params) {
params.APP += script
},
'vue-renderer:spa:templateParams' (params) {
params.APP += script
},
'sigma:template:document' (tmpl) {
tmpl.compiled = tmpl.compiled.replace('</body>', script + '</body>')
},
async 'sigma:compiled' ({ output }: SigmaContext) {
await writeFile(resolve(output.publicDir, 'index.html'), script) // TODO
consola.info('Ready to deploy to static hosting:', prettyPath(output.publicDir))
}
}
}
})

View File

@ -0,0 +1,15 @@
import consola from 'consola'
import { extendPreset, prettyPath } from '../utils'
import { SigmaPreset, SigmaContext } from '../context'
import { node } from './node'
export const cli: SigmaPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/cli',
externals: true,
inlineChunks: true,
hooks: {
'sigma:compiled' ({ output }: SigmaContext) {
consola.info('Run with `node ' + prettyPath(output.serverDir) + ' [route]`')
}
}
})

View File

@ -0,0 +1,19 @@
import { resolve } from 'upath'
import consola from 'consola'
import { extendPreset, writeFile, prettyPath } from '../utils'
import { SigmaContext, SigmaPreset } from '../context'
import { worker } from './worker'
export const cloudflare: SigmaPreset = extendPreset(worker, {
entry: '{{ _internal.runtimeDir }}/entries/cloudflare',
ignore: [
'wrangler.toml'
],
hooks: {
async 'sigma:compiled' ({ output, _nuxt }: SigmaContext) {
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))
consola.success('Ready to run `wrangler publish` in', prettyPath(_nuxt.rootDir))
}
}
})

View File

@ -0,0 +1,11 @@
import { extendPreset } from '../utils'
import { SigmaPreset } from '../context'
import { node } from './node'
export const dev: SigmaPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/dev',
minify: false,
externals: true,
inlineChunks: true,
timing: true
})

View File

@ -3,6 +3,8 @@ export * from './cloudflare'
export * from './lambda'
export * from './netlify'
export * from './node'
export * from './cjs'
export * from './dev'
export * from './server'
export * from './cli'
export * from './vercel'
export * from './worker'

View File

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

View File

@ -0,0 +1,10 @@
import { extendPreset } from '../utils'
import { SigmaPreset } from '../context'
import { lambda } from './lambda'
export const netlify: SigmaPreset = extendPreset(lambda, {
ignore: [
'netlify.toml',
'_redirects'
]
})

View File

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

View File

@ -0,0 +1,16 @@
import consola from 'consola'
import { extendPreset, hl, prettyPath } from '../utils'
import { SigmaPreset, SigmaContext } from '../context'
import { node } from './node'
export const server: SigmaPreset = extendPreset(node, {
entry: '{{ _internal.runtimeDir }}/entries/server',
externals: false,
inlineChunks: false,
timing: true,
hooks: {
'sigma:compiled' ({ output }: SigmaContext) {
consola.success('Ready to run', hl('node ' + prettyPath(output.serverDir)))
}
}
})

View File

@ -0,0 +1,48 @@
import { resolve } from 'upath'
import { extendPreset, writeFile } from '../utils'
import { SigmaPreset, SigmaContext } from '../context'
import { node } from './node'
export const vercel: SigmaPreset = extendPreset(node, {
output: {
dir: '{{ _nuxt.rootDir }}/.vercel_build_output',
serverDir: '{{ output.dir }}/functions/node/_nuxt/index.js',
publicDir: '{{ output.dir }}/static'
},
ignore: [
'vercel.json'
],
hooks: {
async 'sigma:compiled' (ctx: SigmaContext) {
await writeRoutes(ctx)
}
}
})
async function writeRoutes ({ output }: SigmaContext) {
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/_nuxt/index'
}
]
await writeFile(resolve(output.dir, 'config/routes.json'), JSON.stringify(routes, null, 2))
}

View File

@ -0,0 +1,11 @@
import { SigmaPreset, SigmaContext } from '../context'
export const worker: SigmaPreset = {
entry: null, // Abstract
node: false,
hooks: {
'sigma:rollup:before' ({ rollupConfig }: SigmaContext) {
rollupConfig.output.format = 'iife'
}
}
}

View File

@ -1,5 +1,5 @@
import Module from 'module'
import { dirname, join, relative, resolve } from 'path'
import { dirname, join, relative, resolve } from 'upath'
import { InputOptions, OutputOptions } from 'rollup'
import { terser } from 'rollup-plugin-terser'
import commonjs from '@rollup/plugin-commonjs'
@ -10,110 +10,64 @@ 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 * as un from '@nuxt/un'
import hasha from 'hasha'
import { SLSOptions } from '../config'
import { SigmaContext } from '../context'
import { resolvePath, MODULE_DIR } from '../utils'
import { dynamicRequire } from './dynamic-require'
import { externals } from './externals'
import { timing } from './timing'
const mapArrToVal = (val, arr) => arr.reduce((p, c) => ({ ...p, [c]: val }), {})
export type RollupConfig = InputOptions & { output: OutputOptions }
export const getRollupConfig = (options: SLSOptions) => {
const extensions: string[] = ['.ts', '.mjs', '.js', '.json', '.node']
export const getRollupConfig = (sigmaContext: SigmaContext) => {
const extensions: string[] = ['.ts', '.js', '.json', '.node']
const external: InputOptions['external'] = []
const injects:{ [key: string]: string| string[] } = {}
const presets = []
const aliases: { [key: string]: string } = {}
Object.assign(aliases, mapArrToVal('~mocks/generic', [
// @nuxt/devalue
'consola',
// vue2
'encoding',
'stream',
'he',
'resolve',
'source-map',
'lodash.template',
'serialize-javascript',
// vue3
'@babel/parser',
'@vue/compiler-core',
'@vue/compiler-dom',
'@vue/compiler-ssr'
]))
// Uses eval 😈
aliases.depd = '~mocks/custom/depd'
if (options.node === false) {
// Globals
// injects.Buffer = ['buffer', 'Buffer'] <-- TODO: Make it opt-in
injects.process = '~mocks/node/process'
// Aliases
Object.assign(aliases, {
// Node
...mapArrToVal('~mocks/generic', Module.builtinModules),
http: '~mocks/node/http',
fs: '~mocks/node/fs',
process: '~mocks/node/process',
'node-process': require.resolve('process/browser.js'),
// buffer: require.resolve('buffer/index.js'),
util: require.resolve('util/util.js'),
events: require.resolve('events/events.js'),
inherits: require.resolve('inherits/inherits_browser.js'),
// Custom
'node-fetch': '~mocks/custom/node-fetch',
etag: '~mocks/generic/noop',
// Express
...mapArrToVal('~mocks/generic', [
'serve-static',
'iconv-lite'
]),
// Mime
'mime-db': '~mocks/custom/mime-db',
'mime/lite': require.resolve('mime/lite'),
mime: '~mocks/custom/mime'
})
if (sigmaContext.node === false) {
presets.push(un.nodeless)
} else {
presets.push(un.node)
external.push(...Module.builtinModules)
}
const chunksDirName = join(dirname(options.outName), 'chunks')
const serverDir = join(options.buildDir, 'dist/server')
const env = un.env(...presets, {
alias: {
depd: require.resolve('@nuxt/un/runtime/npm/depd')
}
})
const buildServerDir = join(sigmaContext._nuxt.buildDir, 'dist/server')
const runtimeAppDir = join(sigmaContext._internal.runtimeDir, 'app')
const rollupConfig: RollupConfig = {
input: resolvePath(options, options.entry),
input: resolvePath(sigmaContext, sigmaContext.entry),
output: {
dir: options.targetDir,
entryFileNames: options.outName,
dir: sigmaContext.output.serverDir,
entryFileNames: 'index.js',
chunkFileNames (chunkInfo) {
let prefix = ''
const modules = Object.keys(chunkInfo.modules)
const lastModule = modules[modules.length - 1]
if (lastModule.startsWith(serverDir)) {
prefix = join('ssr', relative(serverDir, dirname(lastModule)))
} else if (lastModule.startsWith(options.buildDir)) {
prefix = 'ssr'
} else if (lastModule.startsWith(options.runtimeDir)) {
prefix = 'runtime'
} else if (!prefix && options.serverMiddleware.find(m => lastModule.startsWith(m.handle))) {
if (lastModule.startsWith(buildServerDir)) {
prefix = join('app', relative(buildServerDir, dirname(lastModule)))
} else if (lastModule.startsWith(runtimeAppDir)) {
prefix = 'app'
} else if (lastModule.startsWith(sigmaContext._nuxt.buildDir)) {
prefix = 'nuxt'
} else if (lastModule.startsWith(sigmaContext._internal.runtimeDir)) {
prefix = 'sigma'
} else if (!prefix && sigmaContext.middleware.find(m => lastModule.startsWith(m.handle))) {
prefix = 'middleware'
}
return join(chunksDirName, prefix, '[name].js')
return join('chunks', prefix, '[name].js')
},
inlineDynamicImports: options.inlineChunks,
inlineDynamicImports: sigmaContext.inlineChunks,
format: 'cjs',
exports: 'auto',
intro: '',
@ -124,28 +78,28 @@ export const getRollupConfig = (options: SLSOptions) => {
plugins: []
}
if (options.timing) {
if (sigmaContext.timing) {
rollupConfig.plugins.push(timing())
}
// https://github.com/rollup/plugins/tree/master/packages/replace
rollupConfig.plugins.push(replace({
values: {
'process.env.NODE_ENV': '"production"',
'process.env.NODE_ENV': sigmaContext._nuxt.dev ? '"development"' : '"production"',
'typeof window': '"undefined"',
'process.env.ROUTER_BASE': JSON.stringify(options.routerBase),
'process.env.PUBLIC_PATH': JSON.stringify(options.publicPath),
'process.env.NUXT_STATIC_BASE': JSON.stringify(options.staticAssets.base),
'process.env.NUXT_STATIC_VERSION': JSON.stringify(options.staticAssets.version),
'process.env.ROUTER_BASE': JSON.stringify(sigmaContext._nuxt.routerBase),
'process.env.PUBLIC_PATH': JSON.stringify(sigmaContext._nuxt.publicPath),
'process.env.NUXT_STATIC_BASE': JSON.stringify(sigmaContext._nuxt.staticAssets.base),
'process.env.NUXT_STATIC_VERSION': JSON.stringify(sigmaContext._nuxt.staticAssets.version),
// @ts-ignore
'process.env.NUXT_FULL_STATIC': options.fullStatic
'process.env.NUXT_FULL_STATIC': sigmaContext.fullStatic
}
}))
// Dynamic Require Support
rollupConfig.plugins.push(dynamicRequire({
dir: resolve(options.buildDir, 'dist/server'),
inline: options.node === false || options.inlineChunks,
dir: resolve(sigmaContext._nuxt.buildDir, 'dist/server'),
inline: sigmaContext.node === false || sigmaContext.inlineChunks,
globbyOptions: {
ignore: [
'server.js'
@ -166,36 +120,39 @@ export const getRollupConfig = (options: SLSOptions) => {
const getImportId = p => '_' + hasha(p).substr(0, 6)
rollupConfig.plugins.push(virtual({
'~serverMiddleware': `
${options.serverMiddleware.filter(m => !m.lazy).map(m => `import ${getImportId(m.handle)} from '${m.handle}';`).join('\n')}
${sigmaContext.middleware.filter(m => !m.lazy).map(m => `import ${getImportId(m.handle)} from '${m.handle}';`).join('\n')}
${options.serverMiddleware.filter(m => m.lazy).map(m => `const ${getImportId(m.handle)} = () => import('${m.handle}');`).join('\n')}
${sigmaContext.middleware.filter(m => m.lazy).map(m => `const ${getImportId(m.handle)} = () => import('${m.handle}');`).join('\n')}
export default [
${options.serverMiddleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)}, lazy: ${m.lazy || false} }`).join(',\n')}
${sigmaContext.middleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)}, lazy: ${m.lazy || false} }`).join(',\n')}
];
`
}))
// Polyfill
rollupConfig.plugins.push(virtual({
'~polyfill': env.polyfill.map(p => `require('${p}');`).join('\n')
}))
// https://github.com/rollup/plugins/tree/master/packages/alias
const renderer = options.renderer || 'vue2'
const renderer = sigmaContext.renderer || 'vue2'
rollupConfig.plugins.push(alias({
entries: {
'~runtime': options.runtimeDir,
'~mocks': resolve(options.runtimeDir, 'mocks'),
'~renderer': require.resolve(resolve(options.runtimeDir, 'ssr', renderer)),
'~build': options.buildDir,
'~mock': require.resolve(resolve(options.runtimeDir, 'mocks/generic')),
...aliases
'~runtime': sigmaContext._internal.runtimeDir,
'~renderer': require.resolve(resolve(sigmaContext._internal.runtimeDir, 'app', renderer)),
'~build': sigmaContext._nuxt.buildDir,
...env.alias
}
}))
// External Plugin
if (options.externals) {
if (sigmaContext.externals) {
rollupConfig.plugins.push(externals({
relativeTo: options.targetDir,
relativeTo: sigmaContext.output.serverDir,
include: [
options.runtimeDir,
...options.serverMiddleware.map(m => m.handle)
sigmaContext._internal.runtimeDir,
...sigmaContext.middleware.map(m => m.handle)
]
}))
}
@ -204,12 +161,12 @@ export const getRollupConfig = (options: SLSOptions) => {
rollupConfig.plugins.push(nodeResolve({
extensions,
preferBuiltins: true,
rootDir: options.rootDir,
rootDir: sigmaContext._nuxt.rootDir,
// https://www.npmjs.com/package/resolve
customResolveOptions: {
basedir: options.rootDir,
basedir: sigmaContext._nuxt.rootDir,
paths: [
resolve(options.rootDir, 'node_modukes'),
resolve(sigmaContext._nuxt.rootDir, 'node_modules'),
resolve(MODULE_DIR, 'node_modules')
]
},
@ -225,16 +182,16 @@ export const getRollupConfig = (options: SLSOptions) => {
rollupConfig.plugins.push(json())
// https://github.com/rollup/plugins/tree/master/packages/inject
rollupConfig.plugins.push(inject(injects))
rollupConfig.plugins.push(inject(env.inject))
if (options.analyze) {
if (sigmaContext.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-options
if (options.minify !== false) {
// https://github.com/terser/terser#minify-sigmaContext
if (sigmaContext.minify) {
rollupConfig.plugins.push(terser({
mangle: {
keep_fnames: true,

View File

@ -1,4 +1,4 @@
import { resolve } from 'path'
import { resolve } from 'upath'
import globby, { GlobbyOptions } from 'globby'
import type { Plugin } from 'rollup'

View File

@ -1,4 +1,4 @@
import { isAbsolute, relative } from 'path'
import { isAbsolute, relative } from 'upath'
export function externals ({ include = [], relativeTo }) {
return {

View File

@ -1,4 +1,4 @@
import { extname } from 'path'
import { extname } from 'upath'
import type { Plugin, RenderedChunk } from 'rollup'
export interface Options { }
@ -20,7 +20,7 @@ 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.log('◈', id, t, 'ms'); };
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 };
`)
@ -30,7 +30,8 @@ export function timing (_opts: Options = {}): Plugin {
renderChunk (code, chunk: RenderedChunk) {
let name = chunk.fileName || ''
name = name.replace(extname(name), '')
return "'use strict';" + (chunk.isEntry ? HELPER : '') + `${TIMING}.logStart('import:${name}');` + code + `;${TIMING}.logEnd('import:${name}');`
const logName = name === 'index' ? 'entry' : name
return "'use strict';" + (chunk.isEntry ? HELPER : '') + `${TIMING}.logStart('${logName}');` + code + `;${TIMING}.logEnd('${logName}');`
}
}
}

View File

@ -0,0 +1,137 @@
import { Worker } from 'worker_threads'
import { Server } from 'http'
import { resolve } from 'upath'
import debounce from 'debounce'
import connect from 'connect'
import getPort from 'get-port-please'
import chokidar from 'chokidar'
import serveStatic from 'serve-static'
import { createProxy } from 'http-proxy'
import { stat } from 'fs-extra'
import type { SigmaContext } from './context'
export function createDevServer (sigmaContext: SigmaContext) {
// Worker
const workerEntry = resolve(sigmaContext.output.dir, sigmaContext.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 = connect()
// _nuxt and static
app.use(sigmaContext._nuxt.publicPath, serveStatic(resolve(sigmaContext._nuxt.buildDir, 'dist/client')))
app.use(sigmaContext._nuxt.routerBase, serveStatic(resolve(sigmaContext._nuxt.staticDir)))
// Dev Middleware
let loadingMiddleware, devMiddleware
const setLoadingMiddleware = (m) => { loadingMiddleware = m }
const setDevMiddleware = (m) => { devMiddleware = m }
app.use((req, res, next) => {
if (loadingMiddleware && req.url.startsWith('/_loading')) {
req.url = req.url.replace('/_loading', '')
return loadingMiddleware(req, res)
}
if (devMiddleware) {
return devMiddleware(req, res, next)
}
return next()
})
// SSR Proxy
const proxy = createProxy()
app.use((req, res) => {
if (workerAddress) {
proxy.web(req, res, { target: workerAddress })
} else if (loadingMiddleware) {
// TODO:serverIndex method is not exposed
// loadingMiddleware(req, res)
sigmaContext._internal.hooks.callHook('server:nuxt:renderLoading', req, res)
} else {
res.end('Worker not ready!')
}
})
// Listen
const listeners: Server[] = []
async function listen (port) {
port = await getPort({ name: 'nuxt' })
const listener = await new Promise<Server>((resolve, reject) => {
const l = app.listen(port, err => err ? reject(err) : resolve(l))
})
listeners.push(listener)
return {
url: 'http://localhost:' + port
}
}
// 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(sigmaContext.output.serverDir, pattern),
resolve(sigmaContext._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 => new Promise((resolve, reject) => {
l.close(err => err ? reject(err) : resolve())
})))
}
sigmaContext._internal.hooks.hook('close', close)
return {
reload,
listen,
close,
watch,
setLoadingMiddleware,
setDevMiddleware
}
}

View File

@ -1,44 +0,0 @@
import { resolve, relative } from 'path'
import { existsSync, copy } from 'fs-extra'
import consola from 'consola'
import { extendTarget } from '../utils'
import { SLSTarget } from '../config'
import { worker } from './worker'
export const browser: SLSTarget = extendTarget(worker, (options) => {
const script = `<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('${options.routerBase}_nuxt.js');
});
}
</script>`.replace(/\n| +/g, '')
return {
entry: '{{ runtimeDir }}/targets/service-worker',
targetDir: '{{ publicDir }}',
outName: '_nuxt.js',
cleanTargetDir: false,
nuxtHooks: {
'vue-renderer:ssr:templateParams' (params) {
params.APP += script
},
'vue-renderer:spa:templateParams' (params) {
params.APP += script
}
},
hooks: {
'template:document' (tmpl) {
tmpl.compiled = tmpl.compiled.replace('</body>', script + '</body>')
},
async done ({ rootDir, publicDir }) {
const fallback200 = resolve(publicDir, '200.html')
const fallback404 = resolve(publicDir, '404.html')
if (!existsSync(fallback404) && existsSync(fallback200)) {
await copy(fallback200, fallback404)
}
consola.info(`Try with \`nuxt start ${relative(process.cwd(), rootDir)}\``)
}
}
}
})

View File

@ -1,10 +0,0 @@
import { extendTarget } from '../utils'
import { SLSTarget } from '../config'
import { node } from './node'
export const cjs: SLSTarget = extendTarget(node, {
entry: '{{ runtimeDir }}/targets/cjs',
minify: false,
externals: true,
inlineChunks: true
})

View File

@ -1,21 +0,0 @@
import { resolve } from 'path'
import consola from 'consola'
import { extendTarget, writeFile } from '../utils'
import { SLSOptions, SLSTarget } from '../config'
import { worker } from './worker'
export const cloudflare: SLSTarget = extendTarget(worker, {
entry: '{{ runtimeDir }}/targets/cloudflare',
outName: '_nuxt.js',
generateIgnore: [
'wrangler.toml'
],
hooks: {
async done ({ targetDir }: SLSOptions) {
await writeFile(resolve(targetDir, 'package.json'), JSON.stringify({ private: true, main: './_nuxt.js' }, null, 2))
await writeFile(resolve(targetDir, 'package-lock.json'), JSON.stringify({ lockfileVersion: 1 }, null, 2))
consola.success('Ready to run `wrangler publish`')
}
}
})

View File

@ -1,8 +0,0 @@
import { SLSTarget } from '../config'
export const lambda: SLSTarget = {
entry: '{{ runtimeDir }}/targets/lambda',
outName: '_nuxt.js',
inlineChunks: false
}

View File

@ -1,11 +0,0 @@
import { extendTarget } from '../utils'
import { SLSTarget } from '../config'
import { lambda } from './lambda'
export const netlify: SLSTarget = extendTarget(lambda, {
outName: '_nuxt.js',
generateIgnore: [
'netlify.toml',
'_redirects'
]
})

View File

@ -1,8 +0,0 @@
import { SLSTarget } from '../config'
export const node: SLSTarget = {
entry: '{{ runtimeDir }}/targets/node',
outName: 'index.js',
inlineChunks: false,
minify: true
}

View File

@ -1,47 +0,0 @@
import { resolve } from 'path'
import { extendTarget, writeFile } from '../utils'
import { SLSTarget } from '../config'
import { node } from './node'
export const vercel: SLSTarget = extendTarget(node, {
targetDir: '{{ rootDir }}/.vercel_build_output',
outName: 'functions/node/_nuxt/index.js',
publicDir: '{{ targetDir }}/static',
cleanTargetDir: false,
generateIgnore: [
'vercel.json'
],
hooks: {
async done ({ targetDir }) {
await writeRoutes({ targetDir })
}
}
})
async function writeRoutes ({ targetDir }) {
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/_nuxt/index'
}
]
await writeFile(resolve(targetDir, 'config/routes.json'), JSON.stringify(routes, null, 2))
}

View File

@ -1,12 +0,0 @@
import { SLSTarget } from '../config'
export const worker: SLSTarget = {
entry: null, // Abstract
node: false,
minify: true,
hooks: {
'rollup:before' ({ rollupConfig }) {
rollupConfig.output.format = 'iife'
}
}
}

View File

@ -1,15 +1,17 @@
import { relative, dirname, resolve } from 'path'
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 type { SLSOptions, UnresolvedPath, SLSTarget, SLSTargetFn, SLSConfig } from '../config'
import chalk from 'chalk'
import { get } from 'dot-prop'
import type { SigmaPreset, SigmaInput } from '../context'
export const MODULE_DIR = resolve(__dirname, '..')
export function hl (str: string) {
return '`' + str + '`'
return chalk.cyan(str)
}
export function prettyPath (p: string, highlight = true) {
@ -18,7 +20,13 @@ export function prettyPath (p: string, highlight = true) {
}
export function compileTemplate (contents: string) {
return (params: Record<string, any>) => contents.replace(/{{ ?(\w+) ?}}/g, (_, match) => params[match] || '')
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) {
@ -42,16 +50,16 @@ export async function writeFile (file, contents) {
consola.info('Generated', prettyPath(file))
}
export function resolvePath (options: SLSOptions, path: UnresolvedPath, resolveBase: string = '') {
export function resolvePath (sigmaContext: SigmaInput, path: string | ((sigmaContext) => string), resolveBase: string = ''): string {
if (typeof path === 'function') {
path = path(options)
path = path(sigmaContext)
}
if (typeof path !== 'string') {
throw new TypeError('Invalid path: ' + path)
}
path = compileTemplate(path)(options)
path = compileTemplate(path)(sigmaContext)
return resolve(resolveBase, path)
}
@ -64,22 +72,19 @@ export function detectTarget () {
if (process.env.NOW_BUILDER) {
return 'vercel'
}
return 'node'
}
export function extendTarget (base: SLSTarget, target: SLSTarget): SLSTargetFn {
return (config: SLSConfig) => {
if (typeof target === 'function') {
target = target(config)
export function extendPreset (base: SigmaPreset, preset: SigmaPreset): SigmaPreset {
return (config: SigmaInput) => {
if (typeof preset === 'function') {
preset = preset(config)
}
if (typeof base === 'function') {
base = base(config)
}
return defu({
hooks: Hookable.mergeHooks(base.hooks, target.hooks),
nuxtHooks: Hookable.mergeHooks(base.nuxtHooks as any, target.nuxtHooks as any)
}, target, base)
hooks: Hookable.mergeHooks(base.hooks, preset.hooks)
}, preset, base)
}
}

View File

@ -1,4 +1,4 @@
import { resolve, dirname, relative } from 'path'
import { resolve, dirname, relative } from 'upath'
import globby from 'globby'
import prettyBytes from 'pretty-bytes'
import gzipSize from 'gzip-size'
@ -30,5 +30,5 @@ export async function printFSTree (dir) {
totalGzip += item.gzip
})
process.stdout.write(`${chalk.cyan('λ Total size:')} ${prettyBytes(totalSize)} (${prettyBytes(totalGzip)} gzip)\n`)
process.stdout.write(`${chalk.cyan('Σ Total size:')} ${prettyBytes(totalSize)} (${prettyBytes(totalGzip)} gzip)\n`)
}

View File

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