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 = ` + + +
+ +${stack.map(i =>
+ `${i.text}`
+ ).join('\n')
+ }
+