Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,2 @@
declare function _default(): Promise<import("vue-bundle-renderer").Manifest>;
export default _default;

View file

@ -0,0 +1,4 @@
// @ts-check
import { viteNodeFetch } from './vite-node-shared.mjs'
export default () => viteNodeFetch.getManifest()

View file

@ -0,0 +1,10 @@
/** @typedef {import('node:net').Socket} Socket */
/** @typedef {import('../plugins/vite-node').ViteNodeFetch} ViteNodeFetch */
/** @type {import('../plugins/vite-node').ViteNodeServerOptions} */
export const viteNodeOptions: import("../plugins/vite-node").ViteNodeServerOptions;
/**
* @type {ViteNodeFetch}
*/
export const viteNodeFetch: ViteNodeFetch;
export type Socket = import("node:net").Socket;
export type ViteNodeFetch = import("../plugins/vite-node").ViteNodeFetch;

View file

@ -0,0 +1,322 @@
// @ts-check
import process from 'node:process'
import net from 'node:net'
import { Buffer } from 'node:buffer'
import { isTest } from 'std-env'
/** @typedef {import('node:net').Socket} Socket */
/** @typedef {import('../plugins/vite-node').ViteNodeFetch} ViteNodeFetch */
/** @type {import('../plugins/vite-node').ViteNodeServerOptions} */
export const viteNodeOptions = JSON.parse(process.env.NUXT_VITE_NODE_OPTIONS || '{}')
/** @type {Map<number, { resolve: (value: any) => void, reject: (reason?: any) => void }>} */
const pendingRequests = new Map()
let requestIdCounter = 0
/** @type {Socket | undefined} */
let clientSocket
/** @type {Promise<Socket> | undefined} */
let currentConnectPromise
const MAX_RETRY_ATTEMPTS = viteNodeOptions.maxRetryAttempts ?? 5
const BASE_RETRY_DELAY_MS = viteNodeOptions.baseRetryDelay ?? 100
const MAX_RETRY_DELAY_MS = viteNodeOptions.maxRetryDelay ?? 2000
const REQUEST_TIMEOUT_MS = viteNodeOptions.requestTimeout ?? 60000
/**
* Calculates exponential backoff delay with jitter.
* @param {number} attempt - The current attempt number (0-based).
* @returns {number} Delay in milliseconds.
*/
function calculateRetryDelay (attempt) {
const exponentialDelay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt)
const jitter = Math.random() * 0.1 * exponentialDelay // Add 10% jitter
return Math.min(exponentialDelay + jitter, MAX_RETRY_DELAY_MS)
}
/**
* Establishes or returns an existing IPC socket connection with retry logic.
* @returns {Promise<Socket>} A promise that resolves with the connected socket.
*/
function connectSocket () {
if (clientSocket && !clientSocket.destroyed) {
return Promise.resolve(clientSocket)
}
if (currentConnectPromise) {
return currentConnectPromise
}
const thisPromise = new Promise((resolve, reject) => {
if (!viteNodeOptions.socketPath) {
console.error('vite-node-shared: NUXT_VITE_NODE_OPTIONS.socketPath is not defined.')
return reject(new Error('Vite Node IPC socket path not configured.'))
}
const attemptConnection = (attempt = 0) => {
const socket = net.createConnection(viteNodeOptions.socketPath)
const INITIAL_BUFFER_SIZE = 64 * 1024 // 64KB
const MAX_BUFFER_SIZE = 1024 * 1024 * 1024 // 1GB
let buffer = Buffer.alloc(INITIAL_BUFFER_SIZE)
let writeOffset = 0
let readOffset = 0
// optimize socket for high-frequency IPC
socket.setNoDelay(true)
socket.setKeepAlive(true, 30000) // 30s
const cleanup = () => {
socket.off('connect', onConnect)
socket.off('data', onData)
socket.off('error', onError)
socket.off('close', onClose)
}
const resetBuffer = () => {
writeOffset = 0
readOffset = 0
}
const compactBuffer = () => {
if (readOffset > 0) {
const remainingData = writeOffset - readOffset
if (remainingData > 0) {
buffer.copy(buffer, 0, readOffset, writeOffset)
}
writeOffset = remainingData
readOffset = 0
}
}
/**
* @param {number} additionalBytes
*/
const ensureBufferCapacity = (additionalBytes) => {
const requiredSize = writeOffset + additionalBytes
if (requiredSize > MAX_BUFFER_SIZE) {
throw new Error(`Buffer size limit exceeded: ${requiredSize} > ${MAX_BUFFER_SIZE}`)
}
if (requiredSize > buffer.length) {
// Try compacting first
compactBuffer()
// ... then if we still need more space, grow the buffer
if (writeOffset + additionalBytes > buffer.length) {
const newSize = Math.min(
Math.max(buffer.length * 2, requiredSize),
MAX_BUFFER_SIZE,
)
const newBuffer = Buffer.alloc(newSize)
buffer.copy(newBuffer, 0, 0, writeOffset)
buffer = newBuffer
}
}
}
const onConnect = () => {
clientSocket = socket
resolve(socket)
}
/** @param {Buffer} data */
const onData = (data) => {
try {
ensureBufferCapacity(data.length)
data.copy(buffer, writeOffset)
writeOffset += data.length
while (writeOffset - readOffset >= 4) {
const messageLength = buffer.readUInt32BE(readOffset)
if (writeOffset - readOffset < 4 + messageLength) {
return // Wait for more data
}
const message = buffer.subarray(readOffset + 4, readOffset + 4 + messageLength).toString('utf-8')
readOffset += 4 + messageLength
try {
const response = JSON.parse(message)
const requestHandlers = pendingRequests.get(response.id)
if (requestHandlers) {
const { resolve: resolveRequest, reject: rejectRequest } = requestHandlers
if (response.type === 'error') {
const err = new Error(response.error.message)
// @ts-ignore We are augmenting the error object
err.stack = response.error.stack
// @ts-ignore
err.data = response.error.data
// @ts-ignore
err.statusCode = response.error.statusCode
rejectRequest(err)
} else {
resolveRequest(response.data)
}
pendingRequests.delete(response.id)
}
} catch (parseError) {
console.warn('vite-node-shared: Failed to parse IPC response:', parseError)
// ignore malformed messages
}
}
// compact buffer periodically to prevent memory waste
if (readOffset > buffer.length / 2) {
compactBuffer()
}
} catch (error) {
socket.destroy(error instanceof Error ? error : new Error('Buffer management error'))
}
}
/** @param {Error} err */
const onError = (err) => {
cleanup()
resetBuffer()
if (attempt < MAX_RETRY_ATTEMPTS) {
const delay = calculateRetryDelay(attempt)
setTimeout(() => attemptConnection(attempt + 1), delay)
} else {
if (currentConnectPromise === thisPromise) {
reject(err)
}
for (const { reject: rejectRequest } of pendingRequests.values()) {
rejectRequest(err)
}
pendingRequests.clear()
if (clientSocket === socket) { clientSocket = undefined }
if (currentConnectPromise === thisPromise) { currentConnectPromise = undefined }
}
}
const onClose = () => {
cleanup()
resetBuffer()
for (const { reject: rejectRequest } of pendingRequests.values()) {
rejectRequest(new Error('IPC connection closed'))
}
pendingRequests.clear()
if (clientSocket === socket) { clientSocket = undefined }
if (currentConnectPromise === thisPromise) { currentConnectPromise = undefined }
}
socket.on('connect', onConnect)
socket.on('data', onData)
socket.on('error', onError)
socket.on('close', onClose)
}
attemptConnection()
})
currentConnectPromise = thisPromise
return currentConnectPromise
}
/**
* Sends a request over the IPC socket with automatic reconnection.
* @template {keyof import('../plugins/vite-node').ViteNodeRequestMap} T
* @param {T} type - The type of the request.
* @param {import('../plugins/vite-node').ViteNodeRequestMap[T]['request']} [payload] - The payload for the request.
* @returns {Promise<import('../plugins/vite-node').ViteNodeRequestMap[T]['response']>} A promise that resolves with the response data.
*/
async function sendRequest (type, payload) {
const requestId = requestIdCounter++
let lastError
// retry the entire request (including reconnection) up to MAX_RETRY_ATTEMPTS times
for (let requestAttempt = 0; requestAttempt <= MAX_RETRY_ATTEMPTS; requestAttempt++) {
try {
const socket = await connectSocket()
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
pendingRequests.delete(requestId)
reject(new Error(`Request timeout after ${REQUEST_TIMEOUT_MS}ms for type: ${type}`))
}, REQUEST_TIMEOUT_MS)
pendingRequests.set(requestId, {
resolve: (value) => {
clearTimeout(timeoutId)
resolve(value)
},
reject: (reason) => {
clearTimeout(timeoutId)
reject(reason)
},
})
const message = JSON.stringify({ id: requestId, type, payload })
const messageBuffer = Buffer.from(message, 'utf-8')
const messageLength = messageBuffer.length
// pre-allocate single buffer for length + message to avoid Buffer.concat()
const fullMessage = Buffer.alloc(4 + messageLength)
fullMessage.writeUInt32BE(messageLength, 0)
messageBuffer.copy(fullMessage, 4)
try {
socket.write(fullMessage)
} catch (error) {
clearTimeout(timeoutId)
pendingRequests.delete(requestId)
reject(error)
}
})
} catch (error) {
lastError = error
if (requestAttempt < MAX_RETRY_ATTEMPTS) {
const delay = calculateRetryDelay(requestAttempt)
await new Promise(resolve => setTimeout(resolve, delay))
// clear current connection state to force reconnection
if (clientSocket) {
clientSocket.destroy()
clientSocket = undefined
}
currentConnectPromise = undefined
}
}
}
throw lastError || new Error('Request failed after all retry attempts')
}
/**
* @type {ViteNodeFetch}
*/
export const viteNodeFetch = {
getManifest () {
return sendRequest('manifest')
},
getInvalidates () {
return sendRequest('invalidates')
},
resolveId (id, importer) {
return sendRequest('resolve', { id, importer })
},
fetchModule (moduleId) {
return sendRequest('module', { moduleId })
},
ensureConnected () {
return connectSocket()
},
}
// attempt to pre-establish the IPC connection to reduce latency on first request
let preConnectAttempted = false
function preConnect () {
if (preConnectAttempted || !viteNodeOptions.socketPath) {
return
}
preConnectAttempted = true
return connectSocket().catch(() => {})
}
if (typeof process !== 'undefined' && !isTest) {
setTimeout(preConnect, 100)
}

View file

@ -0,0 +1,2 @@
declare function _default(ssrContext: any): Promise<any>;
export default _default;

View file

@ -0,0 +1,112 @@
// @ts-check
import process from 'node:process'
import { performance } from 'node:perf_hooks'
import { createError } from 'h3'
import { ViteNodeRunner } from 'vite-node/client'
import { consola } from 'consola'
import { viteNodeFetch, viteNodeOptions } from './vite-node-shared.mjs'
const runner = createRunner()
/** @type {(ssrContext: import('#app').NuxtSSRContext) => Promise<any>} */
let render
/** @param {import('#app').NuxtSSRContext} ssrContext */
export default async (ssrContext) => {
// Workaround for stub mode
// https://github.com/nuxt/framework/pull/3983
// eslint-disable-next-line nuxt/prefer-import-meta,@typescript-eslint/no-deprecated
process.server = true
import.meta.server = true
// Invalidate cache for files changed since last rendering
const invalidates = await viteNodeFetch.getInvalidates()
const updates = runner.moduleCache.invalidateDepTree(invalidates)
// Execute SSR bundle on demand
const start = performance.now()
render = (updates.has(viteNodeOptions.entryPath) || !render) ? (await runner.executeFile(viteNodeOptions.entryPath)).default : render
if (updates.size) {
const time = Math.round((performance.now() - start) * 1000) / 1000
consola.success(`Vite server hmr ${updates.size} files`, time ? `in ${time}ms` : '')
}
const result = await render(ssrContext)
return result
}
function createRunner () {
return new ViteNodeRunner({
root: viteNodeOptions.root, // Equals to Nuxt `srcDir`
base: viteNodeOptions.base,
async resolveId (id, importer) {
return await viteNodeFetch.resolveId(id, importer)
},
async fetchModule (id) {
id = id.replace(/\/\//g, '/') // TODO: fix in vite-node
return await viteNodeFetch.fetchModule(id).catch((err) => {
const errorData = err?.data?.data
if (!errorData) {
throw err
}
let _err
try {
const { message, stack } = formatViteError(errorData, id)
_err = createError({
statusMessage: 'Vite Error',
message,
stack,
})
} catch (formatError) {
consola.warn('Internal nuxt error while formatting vite-node error. Please report this!', formatError)
const message = `[vite-node] [TransformError] ${errorData?.message || '-'}`
consola.error(message, errorData)
throw createError({
statusMessage: 'Vite Error',
message,
stack: `${message}\nat ${id}\n` + (errorData?.stack || ''),
})
}
throw _err
})
},
})
}
/**
* @param {any} errorData
* @param {string} id
*/
function formatViteError (errorData, id) {
const errorCode = errorData.name || errorData.reasonCode || errorData.code
const frame = errorData.frame || errorData.source || errorData.pluginCode
/** @param {{ file?: string, id?: string, url?: string }} locObj */
const getLocId = (locObj = {}) => locObj.file || locObj.id || locObj.url || id || ''
/** @param {{ line?: string, column?: string }} locObj */
const getLocPos = (locObj = {}) => locObj.line ? `${locObj.line}:${locObj.column || 0}` : ''
const locId = getLocId(errorData.loc) || getLocId(errorData.location) || getLocId(errorData.input) || getLocId(errorData)
const locPos = getLocPos(errorData.loc) || getLocPos(errorData.location) || getLocPos(errorData.input) || getLocPos(errorData)
const loc = locId.replace(process.cwd(), '.') + (locPos ? `:${locPos}` : '')
const message = [
'[vite-node]',
errorData.plugin && `[plugin:${errorData.plugin}]`,
errorCode && `[${errorCode}]`,
loc,
errorData.reason && `: ${errorData.reason}`,
frame && `<br><pre>${frame.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre><br>`,
].filter(Boolean).join(' ')
const stack = [
message,
`at ${loc}`,
errorData.stack,
].filter(Boolean).join('\n')
return {
message,
stack,
}
}