diff --git a/packages/nitro/src/context.ts b/packages/nitro/src/context.ts index e0a917a95a..1827ecdbea 100644 --- a/packages/nitro/src/context.ts +++ b/packages/nitro/src/context.ts @@ -106,7 +106,7 @@ export function getsigmaContext (nuxtOptions: NuxtOptions, input: SigmaInput): S } }, _internal: { - runtimeDir: resolve(__dirname, '../runtime'), + runtimeDir: resolve(__dirname, './runtime'), hooks: new Hookable() } } diff --git a/packages/nitro/src/runtime/app/config.ts b/packages/nitro/src/runtime/app/config.ts new file mode 100644 index 0000000000..a98105f3d4 --- /dev/null +++ b/packages/nitro/src/runtime/app/config.ts @@ -0,0 +1,19 @@ +import destr from 'destr' + +const runtimeConfig = process.env.RUNTIME_CONFIG + +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 +} diff --git a/packages/nitro/src/runtime/app/render.ts b/packages/nitro/src/runtime/app/render.ts new file mode 100644 index 0000000000..1c7552bd5f --- /dev/null +++ b/packages/nitro/src/runtime/app/render.ts @@ -0,0 +1,71 @@ +import { createRenderer } from 'vue-bundle-renderer' +import devalue from '@nuxt/devalue' +import config from './config' +import { renderToString } from '~renderer' +import server from '~build/dist/server/server' +import clientManifest from '~build/dist/server/client.manifest.json' +import htmlTemplate from '~build/views/document.template.js' + +const renderer = createRenderer(server, { + 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) + const payload = ssrContext.nuxt /* nuxt 2 */ || ssrContext.payload /* nuxt 3 */ + + 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 = `` + const _html = rendered.html + + return htmlTemplate({ + HTML_ATTRS: '', + HEAD_ATTRS: '', + BODY_ATTRS: '', + HEAD: rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''), + APP: _html + state + rendered.renderScripts() + }) +} + +function renderPayload (payload, url) { + return `__NUXT_JSONP__("${url}", ${devalue(payload)})` +} diff --git a/packages/nitro/src/runtime/app/sigma.client.js b/packages/nitro/src/runtime/app/sigma.client.js new file mode 100644 index 0000000000..4212c21438 --- /dev/null +++ b/packages/nitro/src/runtime/app/sigma.client.js @@ -0,0 +1,7 @@ +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 diff --git a/packages/nitro/src/runtime/app/vue2.basic.ts b/packages/nitro/src/runtime/app/vue2.basic.ts new file mode 100644 index 0000000000..cdae5c872c --- /dev/null +++ b/packages/nitro/src/runtime/app/vue2.basic.ts @@ -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) + }) + }) +} diff --git a/packages/nitro/src/runtime/app/vue2.ts b/packages/nitro/src/runtime/app/vue2.ts new file mode 100644 index 0000000000..7c03d09c0d --- /dev/null +++ b/packages/nitro/src/runtime/app/vue2.ts @@ -0,0 +1,14 @@ +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) + }) + }) +} diff --git a/packages/nitro/src/runtime/app/vue3.ts b/packages/nitro/src/runtime/app/vue3.ts new file mode 100644 index 0000000000..0d6b6c4190 --- /dev/null +++ b/packages/nitro/src/runtime/app/vue3.ts @@ -0,0 +1 @@ +export { renderToString } from '@vue/server-renderer' diff --git a/packages/nitro/src/runtime/entries/azure.ts b/packages/nitro/src/runtime/entries/azure.ts new file mode 100644 index 0000000000..eb309aa7f3 --- /dev/null +++ b/packages/nitro/src/runtime/entries/azure.ts @@ -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 + } +} diff --git a/packages/nitro/src/runtime/entries/cli.ts b/packages/nitro/src/runtime/entries/cli.ts new file mode 100644 index 0000000000..f412abf5bf --- /dev/null +++ b/packages/nitro/src/runtime/entries/cli.ts @@ -0,0 +1,23 @@ +import '~polyfill' +import { localCall } from '../server/call' + +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) + 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) + }) +} diff --git a/packages/nitro/src/runtime/entries/cloudflare.ts b/packages/nitro/src/runtime/entries/cloudflare.ts new file mode 100644 index 0000000000..cd38d749fd --- /dev/null +++ b/packages/nitro/src/runtime/entries/cloudflare.ts @@ -0,0 +1,46 @@ +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) => { + 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, + 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 + }) +} + +function assetsCacheControl (request) { + if (request.url.includes(PUBLIC_PATH) /* TODO: Check with routerBase */) { + return { + browserTTL: 31536000, + edgeTTL: 31536000 + } + } + return {} +} diff --git a/packages/nitro/src/runtime/entries/lambda.ts b/packages/nitro/src/runtime/entries/lambda.ts new file mode 100644 index 0000000000..41973392a8 --- /dev/null +++ b/packages/nitro/src/runtime/entries/lambda.ts @@ -0,0 +1,20 @@ +import '~polyfill' +import { localCall } from '../server' + +export async function handler (event, context) { + const r = await localCall({ + event, + url: event.path, + 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() + } +} diff --git a/packages/nitro/src/runtime/entries/local.ts b/packages/nitro/src/runtime/entries/local.ts new file mode 100644 index 0000000000..7386fb5f39 --- /dev/null +++ b/packages/nitro/src/runtime/entries/local.ts @@ -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 + }) +}) diff --git a/packages/nitro/src/runtime/entries/node.ts b/packages/nitro/src/runtime/entries/node.ts new file mode 100644 index 0000000000..5646685d25 --- /dev/null +++ b/packages/nitro/src/runtime/entries/node.ts @@ -0,0 +1,2 @@ +import '~polyfill' +export * from '../server' diff --git a/packages/nitro/src/runtime/entries/server.ts b/packages/nitro/src/runtime/entries/server.ts new file mode 100644 index 0000000000..301c872964 --- /dev/null +++ b/packages/nitro/src/runtime/entries/server.ts @@ -0,0 +1,18 @@ +import '~polyfill' +import { Server } from 'http' +import { handle } from '../server' + +const server = new Server(handle) + +const port = process.env.NUXT_PORT || process.env.PORT || 3000 +const host = process.env.NUXT_HOST || process.env.HOST || 'localhost' + +server.listen(port, host, (err) => { + if (err) { + console.error(err) + process.exit(1) + } + console.log(`Listening on http://${host}:${port}`) +}) + +export default {} diff --git a/packages/nitro/src/runtime/entries/service-worker.ts b/packages/nitro/src/runtime/entries/service-worker.ts new file mode 100644 index 0000000000..c23486b653 --- /dev/null +++ b/packages/nitro/src/runtime/entries/service-worker.ts @@ -0,0 +1,39 @@ +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()) +}) diff --git a/packages/nitro/src/runtime/entries/vercel.ts b/packages/nitro/src/runtime/entries/vercel.ts new file mode 100644 index 0000000000..3967889b40 --- /dev/null +++ b/packages/nitro/src/runtime/entries/vercel.ts @@ -0,0 +1,4 @@ +import '~polyfill' +import { handle } from '../server' + +export default handle diff --git a/packages/nitro/src/runtime/server/error.ts b/packages/nitro/src/runtime/server/error.ts new file mode 100644 index 0000000000..b01bf18998 --- /dev/null +++ b/packages/nitro/src/runtime/server/error.ts @@ -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 = ` + + + + + Nuxt Error + + + +
+
${req.method} ${req.url}

+

${error.toString()}

+
${stack.map(i =>
+        `${i.text}`
+  ).join('\n')
+    }
+
+ + +` + + res.statusCode = error.statusCode || 500 + res.statusMessage = error.statusMessage || 'Invernal Error' + res.end(html) +} diff --git a/packages/nitro/src/runtime/server/index.ts b/packages/nitro/src/runtime/server/index.ts new file mode 100644 index 0000000000..c42384a1b3 --- /dev/null +++ b/packages/nitro/src/runtime/server/index.ts @@ -0,0 +1,23 @@ +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' +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 }) diff --git a/packages/nitro/src/runtime/server/static.ts b/packages/nitro/src/runtime/server/static.ts new file mode 100644 index 0000000000..c1ef0b935f --- /dev/null +++ b/packages/nitro/src/runtime/server/static.ts @@ -0,0 +1,63 @@ +import { sendError } from 'h3' +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 = req.url.split('?')[0] + if (id.startsWith('/')) { + id = id.substr(1) + } + if (id.endsWith('/')) { + id = id.substr(0, id.length - 1) + } + + const asset = getAsset(id) || getAsset(id = id + '/index.html') + + if (!asset) { + if (id.startsWith(PUBLIC_PATH)) { + sendError(res, 'Asset not found: ' + id, false, 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) +} diff --git a/packages/nitro/src/runtime/server/timing.ts b/packages/nitro/src/runtime/server/timing.ts new file mode 100644 index 0000000000..6754a302bb --- /dev/null +++ b/packages/nitro/src/runtime/server/timing.ts @@ -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() +} diff --git a/packages/nitro/src/runtime/types.d.ts b/packages/nitro/src/runtime/types.d.ts new file mode 100644 index 0000000000..902e786658 --- /dev/null +++ b/packages/nitro/src/runtime/types.d.ts @@ -0,0 +1,6 @@ +declare module NodeJS { + interface Global { + __timing__: any + $config: any + } +}