@lincyaw/web 0.0.0-stage → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +24 -2
- package/custom-nav-controls.vue +17 -0
- package/package.json +56 -3
- package/src/client/env.d.ts +1 -0
- package/src/client/home.ts +182 -0
- package/src/client/host.ts +94 -0
- package/src/client/index.ts +4 -0
- package/src/client/page.ts +38 -0
- package/src/client/preview.ts +71 -0
- package/src/client/style.css +427 -0
- package/src/client/theme.ts +322 -0
- package/src/domain/config.ts +99 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/links.ts +62 -0
- package/src/domain/messages.ts +105 -0
- package/src/domain/pages.ts +108 -0
- package/src/node/addons.ts +76 -0
- package/src/node/offline.ts +130 -0
- package/src/node/presmium.ts +10 -0
- package/src/node/publish.ts +101 -0
- package/src/node/renderer.ts +147 -0
- package/src/node/site.ts +349 -0
- package/src/node/sites.ts +531 -0
- package/src/node/vite.ts +166 -0
- package/src/node/vitepress.ts +72 -0
package/src/node/vite.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import type { AddressInfo } from 'node:net'
|
|
4
|
+
import MiniSearch from 'minisearch'
|
|
5
|
+
import { localRequestRefusal, tokenize } from '@lincyaw/kernel'
|
|
6
|
+
import { IR_ENDPOINT, SOURCE_ENDPOINT } from '../domain/links.ts'
|
|
7
|
+
|
|
8
|
+
export const IR_MODULE = 'virtual:presmium/ir'
|
|
9
|
+
export const UPDATE_EVENT = 'presmium:update'
|
|
10
|
+
const RESOLVED = `\0${IR_MODULE}`
|
|
11
|
+
|
|
12
|
+
export function revisionOf(json: string): string {
|
|
13
|
+
return createHash('sha256').update(json).digest('hex').slice(0, 16)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UpdateEvent {
|
|
17
|
+
update: unknown
|
|
18
|
+
base: string
|
|
19
|
+
revision: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DevMessage =
|
|
23
|
+
| ({ type: 'presmium:update' } & UpdateEvent)
|
|
24
|
+
| { type: 'presmium:ready', port: number }
|
|
25
|
+
| { type: 'presmium:source', file: string, line: number }
|
|
26
|
+
| { type: 'presmium:translations' }
|
|
27
|
+
|
|
28
|
+
interface Request { url?: string, method?: string, headers: Record<string, string | string[] | undefined> }
|
|
29
|
+
interface Response { statusCode: number, setHeader(name: string, value: string): void, end(body?: string): void }
|
|
30
|
+
|
|
31
|
+
interface Module { id: string | null }
|
|
32
|
+
|
|
33
|
+
interface DevServer {
|
|
34
|
+
config: { server: { allowedHosts?: true | string[] } }
|
|
35
|
+
httpServer: { once(event: 'listening' | 'close', listener: () => void): unknown, address(): AddressInfo | string | null } | null
|
|
36
|
+
middlewares: { use(path: string, handler: (req: Request, res: Response, next?: () => void) => void): unknown }
|
|
37
|
+
moduleGraph: {
|
|
38
|
+
getModuleById(id: string): Module | undefined
|
|
39
|
+
invalidateModule(module: Module, seen?: Set<Module>, timestamp?: number, isHmr?: boolean): void
|
|
40
|
+
}
|
|
41
|
+
ws: { send(event: string, data: unknown): void }
|
|
42
|
+
reloadModule(module: Module): Promise<void>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SEARCH_INDEX = '/@localSearchIndex'
|
|
46
|
+
|
|
47
|
+
function searchIndex(documentsFile: string): string {
|
|
48
|
+
const index = new MiniSearch({ fields: ['title', 'titles', 'text'], storeFields: ['title', 'titles'], tokenize })
|
|
49
|
+
index.addAll(JSON.parse(readFileSync(documentsFile, 'utf8')))
|
|
50
|
+
return JSON.stringify(index)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function localeFile(file: string, lang: string): string {
|
|
54
|
+
return file.replace(/\.json$/, `.${lang}.json`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function presmiumFrame(policy: string) {
|
|
58
|
+
return {
|
|
59
|
+
name: 'presmium:frame',
|
|
60
|
+
configureServer(server: DevServer) {
|
|
61
|
+
server.middlewares.use('/', (_req, res, next) => {
|
|
62
|
+
res.setHeader('Content-Security-Policy', policy)
|
|
63
|
+
next?.()
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function presmiumReady() {
|
|
70
|
+
return {
|
|
71
|
+
name: 'presmium:ready',
|
|
72
|
+
configureServer(server: DevServer) {
|
|
73
|
+
server.httpServer?.once('listening', () => {
|
|
74
|
+
const address = server.httpServer?.address()
|
|
75
|
+
if (address && typeof address === 'object') process.send?.({ type: 'presmium:ready', port: address.port } satisfies DevMessage)
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function presmiumIR(irFile: string, searchDocuments: string, endpoints: { ir: string, source: string } = { ir: IR_ENDPOINT, source: SOURCE_ENDPOINT }, locales: string[] = []) {
|
|
82
|
+
let dev = false
|
|
83
|
+
const names = ['root', ...locales]
|
|
84
|
+
const documentsOf = (name: string) => (name === 'root' ? searchDocuments : localeFile(searchDocuments, name))
|
|
85
|
+
return {
|
|
86
|
+
name: 'presmium:ir',
|
|
87
|
+
enforce: 'pre' as const,
|
|
88
|
+
resolveId(id: string) {
|
|
89
|
+
if (id === IR_MODULE) return RESOLVED
|
|
90
|
+
return id.startsWith(`${IR_MODULE}/`) && locales.includes(id.slice(IR_MODULE.length + 1)) ? `\0${id}` : undefined
|
|
91
|
+
},
|
|
92
|
+
load(id: string) {
|
|
93
|
+
if (id === RESOLVED) {
|
|
94
|
+
const json = readFileSync(irFile, 'utf8')
|
|
95
|
+
const translations = locales.map(lang => `${JSON.stringify(lang)}: () => import(${JSON.stringify(`${IR_MODULE}/${lang}`)})`)
|
|
96
|
+
return `export default JSON.parse(${JSON.stringify(json)})\nexport const revision = ${JSON.stringify(revisionOf(json))}\nexport const translations = { ${translations.join(', ')} }`
|
|
97
|
+
}
|
|
98
|
+
if (id.startsWith(`${RESOLVED}/`)) return `export default JSON.parse(${JSON.stringify(readFileSync(localeFile(irFile, id.slice(RESOLVED.length + 1)), 'utf8'))})`
|
|
99
|
+
const module = id.split('?')[0]!
|
|
100
|
+
if (module === SEARCH_INDEX) {
|
|
101
|
+
const load = (name: string) => dev ? `import(/* @vite-ignore */ ${JSON.stringify(`.${SEARCH_INDEX}${name}?t=`)} + Date.now())` : `import(${JSON.stringify(`${SEARCH_INDEX.slice(1)}${name}`)})`
|
|
102
|
+
return `export default { ${names.map(name => `${JSON.stringify(name)}: () => ${load(name)}`).join(', ')} }`
|
|
103
|
+
}
|
|
104
|
+
const name = module.startsWith(SEARCH_INDEX) ? module.slice(SEARCH_INDEX.length) : undefined
|
|
105
|
+
if (name && names.includes(name)) return `export default ${JSON.stringify(searchIndex(documentsOf(name)))}`
|
|
106
|
+
return undefined
|
|
107
|
+
},
|
|
108
|
+
configureServer(server: DevServer) {
|
|
109
|
+
dev = true
|
|
110
|
+
const refreshSearch = () => {
|
|
111
|
+
const index = server.moduleGraph.getModuleById(SEARCH_INDEX)
|
|
112
|
+
if (!index) return
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const module = server.moduleGraph.getModuleById(`${SEARCH_INDEX}${name}`)
|
|
115
|
+
if (module) server.moduleGraph.invalidateModule(module, new Set(), Date.now(), true)
|
|
116
|
+
}
|
|
117
|
+
void server.reloadModule(index)
|
|
118
|
+
}
|
|
119
|
+
const receive = (message: DevMessage) => {
|
|
120
|
+
if (message?.type !== 'presmium:update' && message?.type !== 'presmium:translations') return
|
|
121
|
+
for (const id of [RESOLVED, ...locales.map(lang => `${RESOLVED}/${lang}`)]) {
|
|
122
|
+
const module = server.moduleGraph.getModuleById(id)
|
|
123
|
+
if (module) server.moduleGraph.invalidateModule(module)
|
|
124
|
+
}
|
|
125
|
+
if (message.type === 'presmium:update') server.ws.send(UPDATE_EVENT, { update: message.update, base: message.base, revision: message.revision } satisfies UpdateEvent)
|
|
126
|
+
refreshSearch()
|
|
127
|
+
}
|
|
128
|
+
process.on('message', receive)
|
|
129
|
+
server.httpServer?.once('close', () => process.off('message', receive))
|
|
130
|
+
const refused = (req: { headers: Record<string, string | string[] | undefined> }, res: { statusCode: number, end(body?: string): void }) => {
|
|
131
|
+
const reason = localRequestRefusal(req.headers, server.config.server.allowedHosts)
|
|
132
|
+
if (reason) {
|
|
133
|
+
res.statusCode = 403
|
|
134
|
+
res.end(reason)
|
|
135
|
+
}
|
|
136
|
+
return reason !== undefined
|
|
137
|
+
}
|
|
138
|
+
server.middlewares.use(endpoints.ir, (req, res) => {
|
|
139
|
+
if (refused(req, res)) return
|
|
140
|
+
const json = readFileSync(irFile, 'utf8')
|
|
141
|
+
res.statusCode = 200
|
|
142
|
+
res.setHeader('Content-Type', 'application/json')
|
|
143
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
144
|
+
res.end(`{"revision":${JSON.stringify(revisionOf(json))},"doc":${json}}`)
|
|
145
|
+
})
|
|
146
|
+
server.middlewares.use(endpoints.source, (req, res) => {
|
|
147
|
+
if (refused(req, res)) return
|
|
148
|
+
if (req.method !== 'POST' || req.headers['presmium-request'] !== 'source') {
|
|
149
|
+
res.statusCode = 405
|
|
150
|
+
res.end()
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const params = new URL(req.url ?? '', 'http://localhost').searchParams
|
|
154
|
+
const file = params.get('file')
|
|
155
|
+
const line = Number(params.get('line'))
|
|
156
|
+
if (file) process.send?.({ type: 'presmium:source', file, line: Number.isInteger(line) && line > 0 ? line : 1 } satisfies DevMessage)
|
|
157
|
+
res.statusCode = file ? 204 : 400
|
|
158
|
+
res.end()
|
|
159
|
+
})
|
|
160
|
+
server.httpServer?.once('listening', () => {
|
|
161
|
+
const address = server.httpServer?.address()
|
|
162
|
+
if (address && typeof address === 'object') process.send?.({ type: 'presmium:ready', port: address.port } satisfies DevMessage)
|
|
163
|
+
})
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
import type { ChildProcess } from 'node:child_process'
|
|
4
|
+
import { createServer } from 'node:net'
|
|
5
|
+
import type { AddressInfo } from 'node:net'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { TYPESCRIPT_IMPORT } from '@lincyaw/kernel/node'
|
|
9
|
+
import type { DevMessage } from './vite.ts'
|
|
10
|
+
|
|
11
|
+
function bin(): string {
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
const pkg = require.resolve('vitepress/package.json')
|
|
14
|
+
return join(dirname(pkg), (require(pkg) as { bin: Record<string, string> }).bin.vitepress!)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function vitepressBuild(root: string, outDir: string): Promise<number> {
|
|
18
|
+
return new Promise((done) => {
|
|
19
|
+
const child = spawn(process.execPath, [...TYPESCRIPT_IMPORT, bin(), 'build', realpathSync(root), '--outDir', outDir], { stdio: ['ignore', 2, 2] })
|
|
20
|
+
child.on('exit', code => done(code ?? 1))
|
|
21
|
+
child.on('error', () => done(1))
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function freePort(host: string): Promise<number> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const probe = createServer()
|
|
28
|
+
probe.once('error', reject)
|
|
29
|
+
probe.listen(0, host, () => {
|
|
30
|
+
const { port } = probe.address() as AddressInfo
|
|
31
|
+
probe.close(() => resolve(port))
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DevProcess {
|
|
37
|
+
port: number
|
|
38
|
+
send(message: DevMessage): void
|
|
39
|
+
close(): Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function vitepressDev(root: string, port: number | undefined, receive: (message: DevMessage) => void, options: { host?: string, signal?: AbortSignal } = {}): Promise<DevProcess> {
|
|
43
|
+
const listen = port === 0 ? await freePort(options.host ?? '127.0.0.1') : port
|
|
44
|
+
const args = [...TYPESCRIPT_IMPORT, bin(), 'dev', realpathSync(root), ...(listen ? ['--port', String(listen)] : []), ...(options.host ? ['--host', options.host] : [])]
|
|
45
|
+
const child: ChildProcess = spawn(process.execPath, args, { stdio: ['ignore', 'inherit', 'inherit', 'ipc'] })
|
|
46
|
+
const exited = new Promise<void>(resolve => child.once('exit', () => resolve()))
|
|
47
|
+
const stop = () => {
|
|
48
|
+
if (child.exitCode === null) child.kill('SIGTERM')
|
|
49
|
+
}
|
|
50
|
+
options.signal?.addEventListener('abort', stop, { once: true })
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
child.once('error', reject)
|
|
53
|
+
child.once('exit', code => reject(new Error(`vitepress dev exited with code ${code}`)))
|
|
54
|
+
child.on('message', (message: DevMessage) => {
|
|
55
|
+
if (message?.type !== 'presmium:ready') {
|
|
56
|
+
receive(message)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
resolve({
|
|
60
|
+
port: message.port,
|
|
61
|
+
send: m => void child.send(m),
|
|
62
|
+
async close() {
|
|
63
|
+
options.signal?.removeEventListener('abort', stop)
|
|
64
|
+
stop()
|
|
65
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 3000)
|
|
66
|
+
await exited
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
}
|