This commit is contained in:
2025-05-24 01:47:40 +09:00
commit 09d97cbb0b
1594 changed files with 184634 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import type { ResolvedConfig, InlineConfig } from 'vite'
import colors from 'picocolors'
import { mergeConfig, build as viteBuild } from 'vite'
export async function buildApp({
config,
viteConfig,
outStatic,
outServer,
}: {
config: ResolvedConfig
viteConfig: InlineConfig
outStatic: string
outServer: string
}) {
config.logger.info(colors.green('[SSG] Build for client...'))
await viteBuild(
mergeConfig(viteConfig, {
define: {
__VUERO_SSR_BUILD__: true,
},
build: {
ssrManifest: true,
outDir: outStatic,
},
mode: config.mode,
}),
)
// server
config.logger.info(colors.green('[SSG] Build for server...'))
await viteBuild(
mergeConfig(viteConfig, {
define: {
__VUERO_SSR_BUILD__: 'true',
},
build: {
ssr: 'src/entry-server.ts',
outDir: outServer,
},
mode: config.mode,
}),
)
}

146
server/generate/populate.ts Normal file
View File

@@ -0,0 +1,146 @@
import type { ResolvedConfig } from 'vite'
import colors from 'picocolors'
import { generateStaticParams } from '../config'
export const routeParamRe = /(\[.*?\])/g
interface PrerenderPage {
url: string
logPrefix: string
}
export async function populateRouteParams({
routes,
config,
}: {
config: ResolvedConfig
routes: string[]
}) {
const staticParams = generateStaticParams()
const pages: PrerenderPage[] = []
for (const index in routes) {
const url: string
= routes[index] === '/' ? '/' : routes[index].replace(/\/$/, '') // remove trailing slash
const logCount = `${1 + parseInt(index, 10)}/${routes.length}`
if (url.includes('[')) {
const routeStaticParamsFn
= url in staticParams ? staticParams[url as keyof typeof staticParams] : undefined
if (!routeStaticParamsFn) {
config.logger.warn(
`dynamic route (${logCount}) ${colors.yellow(
url,
)} - missing static config - update ${colors.cyan(
'./build-ssg.config.ts',
)} to generate static params for this route.`,
)
continue
}
// extract route params from url (e.g. /[id] or /[[slug]] or /[...all])
const params = (url.match(routeParamRe) || []).map((p: string) => {
const required = !p.includes('[[')
const array = p.includes('...')
const name = p.replaceAll(/\[/g, '').replaceAll(/\]/g, '').replaceAll(/\./g, '')
return {
required,
array,
name,
param: p,
}
})
const routeStaticParams = await staticParams[url as keyof typeof staticParams]()
if (!routeStaticParams || !Array.isArray(routeStaticParams)) {
config.logger.warn(
`dynamic route (${logCount}) ${colors.yellow(
url,
)} - static params must be an array`,
)
continue
}
// check if static params are valid
const invalidParams = routeStaticParams.filter((param) => {
return params.some((p) => {
if (p.required && !(p.name in param)) {
config.logger.warn(
`dynamic route (${logCount}) ${colors.yellow(
url,
)} - missing required param ${colors.cyan(p.name)}`,
)
return true
}
if (p.array && p.name in param) {
const value = param[p.name as keyof typeof param]
const valid = Array.isArray(value)
if (!valid) {
config.logger.warn(
`dynamic route (${logCount}) ${colors.yellow(url)} - param ${colors.cyan(
p.name,
)} must be an array, got string "${colors.cyan(value)}"`,
)
return true
}
}
else if (!p.array && p.name in param) {
const value = param[p.name as keyof typeof param]
const valid = !Array.isArray(value)
if (!valid) {
const values = `[${value.join(', ')}]`
config.logger.warn(
`dynamic route (${logCount}) ${colors.yellow(url)} - param ${colors.cyan(
p.name,
)} must be string, got array ${colors.cyan(values)}`,
)
return true
}
}
})
})
if (invalidParams.length) {
continue
}
// render each static param
for (const subindex in routeStaticParams) {
const logSubCount = `${1 + parseInt(subindex, 10)}/${routeStaticParams.length}`
const param = routeStaticParams[subindex]
const paramUrl = params.reduce((url, p) => {
if (p.name in param) {
const value = param[p.name as keyof typeof param]
if (Array.isArray(value)) {
return url.replace(p.param, value.join('/'))
}
else {
return url.replace(p.param, value)
}
}
else {
return url.replace(p.param, '')
}
}, url)
pages.push({
url: paramUrl,
logPrefix: logSubCount,
})
}
continue
}
pages.push({
url,
logPrefix: logCount,
})
}
return pages
}

View File

@@ -0,0 +1,61 @@
import { ServerResponse, IncomingMessage } from 'node:http'
import fs from 'node:fs'
import path from 'node:path'
import { Socket } from 'node:net'
import { H3Event } from 'h3'
import type { VueroServerRender } from '../types'
import { resolve } from '../utils'
export async function renderToFile(render: VueroServerRender, {
url,
template,
manifest,
outStatic,
}: {
url: string
template: string
manifest: Record<string, string[]>
outStatic: string
}) {
const sock = new Socket()
const req = new IncomingMessage(sock)
const res = new ServerResponse(req)
const event = new H3Event(req, res)
const html = await render({
event,
manifest,
template,
})
const base = url.endsWith('/') ? `${url}` : `${url}/`
const file = `${base}index.html`
const filePath = path.join(outStatic, file)
const dirname = path.dirname(filePath)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
if (typeof html === 'string') {
fs.writeFileSync(resolve(filePath), html)
}
else {
const stream = fs.createWriteStream(resolve(filePath))
await html.pipeTo(new WritableStream({
write(chunk) {
stream.write(chunk)
},
close() {
stream.end()
},
abort() {
stream.end()
},
}))
}
return filePath
}

View File

@@ -0,0 +1,30 @@
import fsp from 'node:fs/promises'
import path from 'node:path'
import type { VueroServerRender } from '../types'
export async function createRenderer({
outServer,
outStatic,
}: {
outServer: string
outStatic: string
}) {
const template = await fsp.readFile(path.join(outStatic, './index.html'), 'utf-8')
const manifest = JSON.parse(
await fsp.readFile(path.join(outStatic, './.vite/ssr-manifest.json'), 'utf-8'),
)
const prefix = process.platform === 'win32' ? 'file://' : ''
const entryServer = path.join(prefix, outServer, 'entry-server.mjs')
// const _require = createRequire(import.meta.url)
const render: VueroServerRender = (await import(entryServer)).render
return {
manifest,
template,
render,
}
}

19
server/generate/scan.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from 'node:path'
import fg from 'fast-glob'
export async function scanRoutes(cwd: string) {
const files = await fg([path.resolve(cwd, 'src/pages/**/*.vue').replace(/\\/g, '/')])
return files
.filter(path => !path.includes('src/pages/[...all].vue')) // ignore root catch-all route
.map((file) => {
const name = file
.replace(/\.vue$/, '')
.replace(cwd.replace(/\\/g, '/'), '')
.replace(/\/+/g, '/')
.replace('/src/pages/', '')
.toLowerCase()
return '/' + name.replace(/index$/, '')
})
}