@lincyaw/web 1.0.0-rc.0 → 1.0.0-rc.2
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/README.md +33 -4
- package/package.json +20 -19
- package/src/app/base.css +117 -0
- package/src/app/block-playback.ts +81 -0
- package/src/app/block-report.ts +104 -0
- package/src/app/document-modules.ts +30 -0
- package/src/app/env.d.ts +12 -0
- package/src/app/main.ts +198 -0
- package/src/app/page-app.ts +166 -0
- package/src/app/router.ts +109 -0
- package/src/app/server.ts +32 -0
- package/src/app/site/home.tsx +117 -0
- package/src/app/site/layout.tsx +92 -0
- package/src/app/site/search.tsx +22 -0
- package/src/app/site.css +1450 -0
- package/src/client/document.css +172 -0
- package/src/client/host.ts +22 -94
- package/src/client/preview-frame.ts +152 -0
- package/src/client/runtime.ts +159 -0
- package/src/domain/app-config.ts +91 -0
- package/src/domain/config.ts +17 -9
- package/src/domain/dev-updates.ts +12 -0
- package/src/domain/index.ts +17 -7
- package/src/domain/landing.ts +83 -0
- package/src/domain/links.ts +23 -18
- package/src/domain/messages.ts +74 -63
- package/src/domain/pages.ts +23 -41
- package/src/domain/themes.ts +59 -0
- package/src/domain/workspace-home.ts +12 -0
- package/src/node/client-modules.ts +92 -0
- package/src/node/document-bundle.ts +227 -0
- package/src/node/landing-template.ts +34 -0
- package/src/node/landing.ts +82 -0
- package/src/node/offline.ts +34 -11
- package/src/node/publish.ts +83 -15
- package/src/node/renderer.ts +78 -117
- package/src/node/site-assets.ts +17 -0
- package/src/node/site-build.ts +304 -0
- package/src/node/site-files.ts +87 -0
- package/src/node/site-model.ts +337 -0
- package/src/node/site-page.ts +82 -0
- package/src/node/site-server.ts +822 -0
- package/src/node/slides.ts +135 -0
- package/src/node/viewer-app.ts +504 -0
- package/src/node/viewer-build.ts +38 -0
- package/custom-nav-controls.vue +0 -17
- package/src/client/env.d.ts +0 -1
- package/src/client/home.ts +0 -182
- package/src/client/index.ts +0 -4
- package/src/client/page.ts +0 -38
- package/src/client/preview.ts +0 -71
- package/src/client/style.css +0 -427
- package/src/client/theme.ts +0 -322
- package/src/node/addons.ts +0 -76
- package/src/node/site.ts +0 -349
- package/src/node/sites.ts +0 -531
- package/src/node/vite.ts +0 -166
- package/src/node/vitepress.ts +0 -72
package/src/node/sites.ts
DELETED
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs'
|
|
2
|
-
import { createServer } from 'node:http'
|
|
3
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
-
import type { AddressInfo } from 'node:net'
|
|
5
|
-
import type { Duplex } from 'node:stream'
|
|
6
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { DEFAULT_LANG, deckLocale, docBlocks, errorMessage, isRecord, localRequestRefusal, nodeMdast } from '@lincyaw/kernel'
|
|
9
|
-
import type { DocumentLink, Finding, IRDocument, MdastNode, RenderContext, RenderOutput, SiteDevServer, SiteDocument, SiteSpec, SourcePos } from '@lincyaw/kernel'
|
|
10
|
-
import { FONT_LICENSE, fontLicense, optimizeDeps } from '@lincyaw/kernel/node'
|
|
11
|
-
import { createProxyServer } from 'http-proxy-3'
|
|
12
|
-
import { pageLink, searchDocuments, sectionLinks, siteShell, SITE_LABELS, toPages, translationPages, urlPath } from '../domain/index.ts'
|
|
13
|
-
import type { Neighbour, SearchDocument, WebPage } from '../domain/index.ts'
|
|
14
|
-
import { clientModules } from './addons.ts'
|
|
15
|
-
import type { ClientModules } from './addons.ts'
|
|
16
|
-
import { publishedFile } from './publish.ts'
|
|
17
|
-
import type { DocumentPage } from './publish.ts'
|
|
18
|
-
import { buildDocument, devDocument } from './renderer.ts'
|
|
19
|
-
import type { DocumentDevServer, DocumentSite } from './renderer.ts'
|
|
20
|
-
import { allowed, browserPackages, languageLabel, linkDependencies, OG_TITLE, presmiumRoot, SRC_EXCLUDE, write } from './site.ts'
|
|
21
|
-
import type { WorkspaceOptions } from './site.ts'
|
|
22
|
-
import { vitepressBuild, vitepressDev } from './vitepress.ts'
|
|
23
|
-
|
|
24
|
-
const HOME_THEME = fileURLToPath(new URL('../client/home.ts', import.meta.url))
|
|
25
|
-
const VITE_PLUGIN = fileURLToPath(new URL('./vite.ts', import.meta.url))
|
|
26
|
-
const LOOPBACK = '127.0.0.1'
|
|
27
|
-
const GENERATED = '// Generated by @lincyaw/web for a workspace site; do not edit.'
|
|
28
|
-
const LIST_PAGE = 'documents'
|
|
29
|
-
const WORKSPACE_FILE = 'presmium.workspace.yaml'
|
|
30
|
-
const DECK_ENTRY = 'lecture.md'
|
|
31
|
-
const REMOTE = /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i
|
|
32
|
-
const LINK_LANGUAGE = new WeakMap<DocumentLink, string>()
|
|
33
|
-
const SHELL_KEYS = ['nav', 'footer', 'head', 'sitemap']
|
|
34
|
-
|
|
35
|
-
function shellOptions(site: SiteSpec): Record<string, unknown> {
|
|
36
|
-
return Object.fromEntries(SHELL_KEYS.filter(key => key in site.config).map(key => [key, site.config[key]]))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function homeDocument(site: SiteSpec): SiteDocument | undefined {
|
|
40
|
-
return site.home === undefined ? undefined : site.documents.find(doc => doc.dir === site.home && doc.dir !== '')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function listLink(site: SiteSpec, base: string): string {
|
|
44
|
-
return homeDocument(site) ? `${base}${LIST_PAGE}.html` : base
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface HomeDocument {
|
|
48
|
-
dir: string
|
|
49
|
-
title: string
|
|
50
|
-
lang?: string
|
|
51
|
-
tags?: string[]
|
|
52
|
-
href: string
|
|
53
|
-
description?: string
|
|
54
|
-
pages?: number
|
|
55
|
-
problems?: { errors: number, warnings: number }
|
|
56
|
-
sections?: { id: string, title: string, href: string }[]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function documentBase(base: string, dir: string): string {
|
|
60
|
-
return dir ? `${base}${urlPath(dir)}/` : base
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function endpoints(base: string): WorkspaceOptions['endpoints'] {
|
|
64
|
-
return { ir: `${base}@lincyaw/ir`, source: `${base}@lincyaw/source` }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function homeDocuments(site: SiteSpec, base: string, compiled: ReadonlyMap<string, IRDocument> = new Map()): HomeDocument[] {
|
|
68
|
-
return site.documents.map(({ dir, title, lang, tags, doc }) => ({ dir, title, ...lang ? { lang } : {}, ...tags?.length ? { tags } : {}, href: documentBase(base, dir), ...cardOf(compiled.get(dir) ?? doc, documentBase(base, dir)) }))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function siteLanguages(site: SiteSpec, lang: string): string[] {
|
|
72
|
-
return [...new Set(site.documents.flatMap(entry => (entry.translations ?? []).map(t => t.lang)))].filter(other => other !== lang)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function translationOf(entry: SiteDocument, lang: string): { doc: IRDocument, pages: WebPage[] } | undefined {
|
|
76
|
-
const translated = entry.translations?.find(t => t.lang === lang)
|
|
77
|
-
return translated && entry.doc ? { doc: translated.doc, pages: translationPages(translated.doc, toPages(entry.doc)) } : undefined
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function localeDocuments(site: SiteSpec, base: string, lang: string): HomeDocument[] {
|
|
81
|
-
return site.documents.map((entry) => {
|
|
82
|
-
const translated = translationOf(entry, lang)
|
|
83
|
-
if (!translated) return homeDocuments({ ...site, documents: [entry] }, base)[0]!
|
|
84
|
-
const root = `${documentBase(base, entry.dir)}${lang}/`
|
|
85
|
-
return { dir: entry.dir, title: translated.doc.meta.title, lang, ...entry.tags?.length ? { tags: entry.tags } : {}, href: root, ...cardOf(translated.doc, root, translated.pages) }
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function cardOf(doc: IRDocument | undefined, root: string, pages = doc && toPages(doc)): Pick<HomeDocument, 'description' | 'pages' | 'problems' | 'sections'> {
|
|
90
|
-
if (!doc || !pages) return {}
|
|
91
|
-
const description = typeof doc.meta.description === 'string' ? doc.meta.description.trim() : ''
|
|
92
|
-
const count = (severity: Finding['severity']) => doc.diagnostics.filter(finding => finding.severity === severity).length
|
|
93
|
-
const links = sectionLinks(pages)
|
|
94
|
-
const sections = doc.sections.flatMap(section => (section.title && links[section.id] ? [{ id: section.id, title: section.title, href: `${root}${links[section.id]!.slice(1)}` }] : []))
|
|
95
|
-
return { ...description ? { description } : {}, pages: pages.length, problems: { errors: count('error'), warnings: count('warning') }, sections }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function packageDir(name: string, from: string): string | undefined {
|
|
99
|
-
for (let dir = from; ; dir = dirname(dir)) {
|
|
100
|
-
const candidate = join(dir, 'node_modules', name)
|
|
101
|
-
if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate)
|
|
102
|
-
if (dirname(dir) === dir) return undefined
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function siteModules(ctx: RenderContext): ClientModules {
|
|
107
|
-
const { components, plugins, viewers, vite } = clientModules({ ...ctx, deckDir: join(ctx.workDir, 'site-plugins') }, [])
|
|
108
|
-
return { components, plugins, viewers, vite, nodeVite: [] }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function decoded(text: string): string {
|
|
112
|
-
try {
|
|
113
|
-
return decodeURIComponent(text)
|
|
114
|
-
} catch {
|
|
115
|
-
return text
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function documentLinkOf(ctx: RenderContext): (source: SourcePos, url: string) => DocumentLink | undefined {
|
|
120
|
-
if (ctx.documentLink) return (source, url) => ctx.documentLink!(source, url)
|
|
121
|
-
return (source, url) => {
|
|
122
|
-
const path = url.replace(/[?#].*$/s, '')
|
|
123
|
-
if (REMOTE.test(url) || !path || path.startsWith('/')) return undefined
|
|
124
|
-
let target: string
|
|
125
|
-
try {
|
|
126
|
-
target = realpathSync(resolve(dirname(source.file), decoded(path)))
|
|
127
|
-
} catch {
|
|
128
|
-
return undefined
|
|
129
|
-
}
|
|
130
|
-
const stat = statSync(target)
|
|
131
|
-
const dir = stat.isFile() && basename(target) === DECK_ENTRY ? dirname(target) : stat.isDirectory() && existsSync(join(target, DECK_ENTRY)) ? target : undefined
|
|
132
|
-
const rel = dir === undefined ? undefined : relative(realpathSync(ctx.workspaceRoot), dir)
|
|
133
|
-
if (rel === undefined || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return undefined
|
|
134
|
-
const hash = url.indexOf('#')
|
|
135
|
-
const fragment = hash < 0 ? '' : decoded(url.slice(hash + 1))
|
|
136
|
-
return { dir: rel.split(sep).join('/'), ...fragment ? { fragment } : {} }
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function labelSection(doc: IRDocument, label: string): string | undefined {
|
|
141
|
-
const named = (node: MdastNode): boolean => node.identifier === label || node.label === label || Boolean(node.children?.some(named))
|
|
142
|
-
for (const { section, block } of docBlocks(doc)) {
|
|
143
|
-
if (block.id === label || nodeMdast(block.node).some(named)) return section.id
|
|
144
|
-
}
|
|
145
|
-
return undefined
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function documentPage(site: SiteSpec, base: string, docs: Map<string, IRDocument>): DocumentPage {
|
|
149
|
-
return (link) => {
|
|
150
|
-
const { dir, fragment } = link
|
|
151
|
-
const entry = site.documents.find(other => other.dir === dir)
|
|
152
|
-
if (!entry) return undefined
|
|
153
|
-
const lang = LINK_LANGUAGE.get(link)
|
|
154
|
-
const translated = lang ? translationOf(entry, lang) : undefined
|
|
155
|
-
const root = `${documentBase(base, dir)}${translated ? `${lang}/` : ''}`
|
|
156
|
-
const anchor = fragment ? `#${encodeURIComponent(fragment)}` : ''
|
|
157
|
-
const doc = docs.get(dir)
|
|
158
|
-
if (!doc || !fragment) return `${root}${anchor}`
|
|
159
|
-
const links = sectionLinks(translated?.pages ?? toPages(doc))
|
|
160
|
-
if (links[fragment]) return `${root}${links[fragment].slice(1)}`
|
|
161
|
-
const section = labelSection(doc, fragment)
|
|
162
|
-
return `${root}${(section ? links[section]!.split('#')[0]! : '/').slice(1)}${anchor}`
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function sourceFiles(doc: IRDocument): Set<string> {
|
|
167
|
-
return new Set([...doc.sections.map(section => section.source.file), ...[...docBlocks(doc)].map(({ block }) => block.source.file)])
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function translatedFiles(doc: IRDocument, translations: SiteDocument['translations'] = []): Map<string, string> {
|
|
171
|
-
const original = sourceFiles(doc)
|
|
172
|
-
const files = new Map<string, string>()
|
|
173
|
-
for (const { lang, doc: translated } of translations) {
|
|
174
|
-
for (const file of sourceFiles(translated)) if (!original.has(file)) files.set(file, lang)
|
|
175
|
-
}
|
|
176
|
-
return files
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function documentContext(ctx: RenderContext, site: SiteSpec, dir: string, doc: IRDocument, outDir: string, workDir: string): RenderContext {
|
|
180
|
-
const linkOf = documentLinkOf(ctx)
|
|
181
|
-
const entry = site.documents.find(other => other.dir === dir)
|
|
182
|
-
const translated = translatedFiles(doc, entry?.translations)
|
|
183
|
-
const documentLink = (source: SourcePos, url: string): DocumentLink | undefined => {
|
|
184
|
-
const link = linkOf(source, url)
|
|
185
|
-
const lang = translated.get(source.file)
|
|
186
|
-
if (link && lang) LINK_LANGUAGE.set(link, lang)
|
|
187
|
-
return link
|
|
188
|
-
}
|
|
189
|
-
const deckDir = join(ctx.workspaceRoot, dir)
|
|
190
|
-
const pluginDirs: Record<string, string> = {}
|
|
191
|
-
for (const name of doc.config.plugins) {
|
|
192
|
-
const found = isAbsolute(name) ? name : ctx.pluginDirs[name] ?? packageDir(name, deckDir)
|
|
193
|
-
if (found && existsSync(found)) pluginDirs[name] = found
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
...ctx,
|
|
197
|
-
deckDir,
|
|
198
|
-
outDir,
|
|
199
|
-
workDir,
|
|
200
|
-
config: shellOptions(site),
|
|
201
|
-
plugins: doc.config.plugins.filter(name => pluginDirs[name]),
|
|
202
|
-
pluginDirs,
|
|
203
|
-
lang: doc.meta.lang,
|
|
204
|
-
base: documentBase(ctx.base, dir),
|
|
205
|
-
vitePlugins: [],
|
|
206
|
-
translations: entry?.translations,
|
|
207
|
-
documentLink,
|
|
208
|
-
localAsset(source, url) {
|
|
209
|
-
if (documentLink(source, url)) return undefined
|
|
210
|
-
if (!url.startsWith('/') || url.startsWith('//')) {
|
|
211
|
-
const found = ctx.localAsset(source, url)
|
|
212
|
-
return found && basename(realpathSync(found.file)) === DECK_ENTRY ? undefined : found
|
|
213
|
-
}
|
|
214
|
-
const path = url.replace(/[?#].*$/s, '')
|
|
215
|
-
let file = join(deckDir, 'public', path)
|
|
216
|
-
try {
|
|
217
|
-
file = join(deckDir, 'public', decodeURIComponent(path))
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
if (!existsSync(file)) return undefined
|
|
221
|
-
const rel = relative(dirname(source.file), join(deckDir, 'public', path)).split(sep).join('/')
|
|
222
|
-
return ctx.localAsset(source, `${rel}${url.slice(path.length)}`)
|
|
223
|
-
},
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function documentWorkDir(ctx: RenderContext, dir: string): string {
|
|
228
|
-
return join(ctx.workDir, 'documents', dir ? encodeURIComponent(dir) : '_')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function firstPage(entry: SiteDocument, doc?: IRDocument): Neighbour {
|
|
232
|
-
const page = doc && toPages(doc)[0]
|
|
233
|
-
return { text: page?.title ?? entry.title, link: `${urlPath(entry.dir)}/` }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function lastPage(entry: SiteDocument, doc?: IRDocument): Neighbour {
|
|
237
|
-
const page = doc && toPages(doc).at(-1)
|
|
238
|
-
return page ? { text: page.title, link: `${urlPath(entry.dir)}${pageLink(page)}` } : { text: entry.title, link: `${urlPath(entry.dir)}/` }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function siteLinks(site: SiteSpec, base: string, dir: string, docs: Map<string, IRDocument>): Pick<WorkspaceOptions, 'neighbours' | 'search'> {
|
|
242
|
-
const listed = site.documents.filter(entry => entry.dir !== '')
|
|
243
|
-
const i = listed.findIndex(entry => entry.dir === dir)
|
|
244
|
-
const before = listed[i - 1]
|
|
245
|
-
const after = listed[i + 1]
|
|
246
|
-
const search: SearchDocument[] = listed.flatMap((entry) => {
|
|
247
|
-
const doc = docs.get(entry.dir)
|
|
248
|
-
return entry.dir === dir || !doc ? [] : searchDocuments(doc, documentBase(base, entry.dir), toPages(doc), [entry.title])
|
|
249
|
-
})
|
|
250
|
-
return {
|
|
251
|
-
neighbours: i < 0 ? {} : { ...before ? { prev: lastPage(before, docs.get(before.dir)) } : {}, ...after ? { next: firstPage(after, docs.get(after.dir)) } : {} },
|
|
252
|
-
search,
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function documentSite(ctx: RenderContext, site: SiteSpec, dir: string, extra: ClientModules, served: boolean, allow: string[], docs: Map<string, IRDocument>): DocumentSite {
|
|
257
|
-
const base = documentBase(ctx.base, dir)
|
|
258
|
-
const single = site.documents.length === 1 && site.documents[0]!.dir === ''
|
|
259
|
-
return {
|
|
260
|
-
documentPage: documentPage(site, ctx.base, docs),
|
|
261
|
-
workspace: { document: dir, home: single ? base : listLink(site, ctx.base), endpoints: endpoints(base), served, allow, ...siteLinks(site, ctx.base, dir, docs) },
|
|
262
|
-
extra,
|
|
263
|
-
config: join(ctx.workspaceRoot, WORKSPACE_FILE),
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function sitemapPages(site: SiteSpec): string[] {
|
|
268
|
-
return site.documents.flatMap((entry) => {
|
|
269
|
-
if (!entry.doc || !entry.dir) return []
|
|
270
|
-
const translated = (entry.translations ?? []).flatMap(({ lang }) => translationOf(entry, lang)?.pages.map(page => pageLink(page, `/${lang}`)) ?? [])
|
|
271
|
-
return [...toPages(entry.doc).map(page => pageLink(page)), ...translated].map(link => `${urlPath(entry.dir)}${link}`.replace(/^\//, ''))
|
|
272
|
-
})
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function writeHome(workDir: string, site: SiteSpec, ctx: RenderContext, modules: ClientModules, allow?: string[]): string {
|
|
276
|
-
const root = join(workDir, 'site')
|
|
277
|
-
const lang = typeof site.config.lang === 'string' ? site.config.lang : site.documents.find(doc => doc.lang)?.lang ?? ctx.lang ?? DEFAULT_LANG
|
|
278
|
-
const labels = SITE_LABELS[deckLocale(lang)]
|
|
279
|
-
const shell = siteShell(site.config)
|
|
280
|
-
const dataFile = join(workDir, 'home.json')
|
|
281
|
-
const home = homeDocument(site)
|
|
282
|
-
const target = home && documentBase(ctx.base, home.dir)
|
|
283
|
-
const titles = isRecord(site.config.locales) ? site.config.locales : {}
|
|
284
|
-
const locales = siteLanguages(site, lang).map((other) => {
|
|
285
|
-
const own = titles[other]
|
|
286
|
-
const translatedHome = home && translationOf(home, other) ? `${target}${other}/` : target
|
|
287
|
-
return {
|
|
288
|
-
lang: other,
|
|
289
|
-
label: languageLabel(other),
|
|
290
|
-
title: isRecord(own) && typeof own.title === 'string' ? own.title : site.title,
|
|
291
|
-
themeConfig: SITE_LABELS[deckLocale(other)].themeConfig,
|
|
292
|
-
documents: localeDocuments(site, ctx.base, other),
|
|
293
|
-
...translatedHome ? { redirect: translatedHome, list: `${ctx.base}${other}/${LIST_PAGE}.html` } : {},
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
write(dataFile, JSON.stringify({ title: site.title, label: languageLabel(lang), documents: homeDocuments(site, ctx.base), locales, sitemap: sitemapPages(site) }))
|
|
297
|
-
write(join(workDir, '.gitignore'), '*\n')
|
|
298
|
-
linkDependencies(root)
|
|
299
|
-
const optimize = optimizeDeps(root, browserPackages(modules))
|
|
300
|
-
const publicDir = join(root, 'public')
|
|
301
|
-
write(join(publicDir, FONT_LICENSE), fontLicense(import.meta.url))
|
|
302
|
-
const image = shell.head?.image && (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(shell.head.image)
|
|
303
|
-
? shell.head.image
|
|
304
|
-
: publishedFile(ctx, publicDir, ctx.base, { file: join(ctx.workspaceRoot, WORKSPACE_FILE), line: 1 }, shell.head.image))
|
|
305
|
-
const head: unknown[] = shell.head
|
|
306
|
-
? [['meta', { property: 'og:type', content: 'website' }], ...shell.head.description ? [['meta', { property: 'og:description', content: shell.head.description }]] : [], ...image ? [['meta', { property: 'og:image', content: shell.sitemap && image.startsWith('/') ? `${shell.sitemap.hostname}${image}` : image }]] : []]
|
|
307
|
-
: []
|
|
308
|
-
const config = {
|
|
309
|
-
lang,
|
|
310
|
-
base: ctx.base,
|
|
311
|
-
cleanUrls: false,
|
|
312
|
-
ignoreDeadLinks: true,
|
|
313
|
-
srcExclude: SRC_EXCLUDE,
|
|
314
|
-
...shell.head?.description ? { description: shell.head.description } : {},
|
|
315
|
-
head,
|
|
316
|
-
themeConfig: { ...labels.themeConfig, ...shell.nav ? { nav: shell.nav } : {}, ...shell.footer ? { footer: shell.footer } : {} },
|
|
317
|
-
}
|
|
318
|
-
const sitemap = shell.sitemap ? `site.sitemap = { hostname: ${JSON.stringify(`${shell.sitemap.hostname}${ctx.base}`)}, transformItems: items => [...items, ...data.sitemap.map(url => ({ url }))] }\n` : ''
|
|
319
|
-
const vite = modules.vite.map((file, i) => `import vite${i} from ${JSON.stringify(file)}\n`).join('')
|
|
320
|
-
write(join(root, '.vitepress/config.mjs'), `${GENERATED}
|
|
321
|
-
import { readFileSync } from 'node:fs'
|
|
322
|
-
import { presmiumReady } from ${JSON.stringify(VITE_PLUGIN)}
|
|
323
|
-
${vite}
|
|
324
|
-
const site = ${JSON.stringify(config, null, 2)}
|
|
325
|
-
const data = JSON.parse(readFileSync(${JSON.stringify(dataFile)}, 'utf8'))
|
|
326
|
-
site.title = data.title
|
|
327
|
-
site.themeConfig.presmium = { home: true, documents: data.documents${target ? `, redirect: ${JSON.stringify(target)}, list: ${JSON.stringify(`${ctx.base}${LIST_PAGE}.html`)}` : ''} }
|
|
328
|
-
// The languages of the documents' translations (P-123): a locale each, which lists the documents in its language.
|
|
329
|
-
if (data.locales.length > 0) {
|
|
330
|
-
site.locales = { root: { label: data.label, lang: site.lang } }
|
|
331
|
-
for (const locale of data.locales) {
|
|
332
|
-
const presmium = { ...site.themeConfig.presmium, documents: locale.documents, ...locale.redirect ? { redirect: locale.redirect, list: locale.list } : {} }
|
|
333
|
-
site.locales[locale.lang] = { label: locale.label, lang: locale.lang, title: locale.title, themeConfig: { ...locale.themeConfig, presmium } }
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
site.vite = { plugins: [presmiumReady(), ...[${modules.vite.map((_, i) => `vite${i}`).join(', ')}].flatMap(config => config?.plugins ?? [])], resolve: { dedupe: ['vue'] }${optimize.include.length + optimize.exclude.length > 0 ? `, optimizeDeps: ${JSON.stringify(optimize)}` : ''}${allow ? `, server: { fs: { allow: ${JSON.stringify(allow)} } }` : ''} }
|
|
337
|
-
${sitemap}${shell.head ? OG_TITLE : ''}
|
|
338
|
-
export default site
|
|
339
|
-
`)
|
|
340
|
-
const load = (file: string) => `() => import(${JSON.stringify(file)})`
|
|
341
|
-
write(join(root, '.vitepress/theme/index.ts'), `${GENERATED}
|
|
342
|
-
import { createSiteHomeTheme } from ${JSON.stringify(HOME_THEME)}
|
|
343
|
-
|
|
344
|
-
const ssr = import.meta.env.SSR
|
|
345
|
-
export default createSiteHomeTheme({
|
|
346
|
-
plugins: ssr ? [] : [
|
|
347
|
-
${modules.plugins.map(file => ` ${load(file)},`).join('\n')}
|
|
348
|
-
],
|
|
349
|
-
})
|
|
350
|
-
`)
|
|
351
|
-
writeIndex(root, home?.title, target)
|
|
352
|
-
const langs = new Set(locales.map(locale => locale.lang))
|
|
353
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
354
|
-
if (entry.isDirectory() && !entry.name.startsWith('.') && !['public', 'node_modules'].includes(entry.name) && !langs.has(entry.name)) rmSync(join(root, entry.name), { recursive: true, force: true })
|
|
355
|
-
}
|
|
356
|
-
for (const locale of locales) writeIndex(join(root, locale.lang), locale.documents.find(doc => doc.dir === home?.dir)?.title, locale.redirect, true)
|
|
357
|
-
return root
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function writeIndex(dir: string, title: string | undefined, target: string | undefined, clientOnly = false): void {
|
|
361
|
-
const list = `---\nlayout: page\n---\n\n${clientOnly ? '<ClientOnly><PresmiumSiteHome /></ClientOnly>' : '<PresmiumSiteHome />'}\n`
|
|
362
|
-
if (target) {
|
|
363
|
-
const refresh = JSON.stringify(['meta', { 'http-equiv': 'refresh', 'content': `0; url=${target}` }])
|
|
364
|
-
write(join(dir, 'index.md'), `---\nlayout: page\ntitle: ${JSON.stringify(title ?? '')}\nhead:\n - ${refresh}\n---\n\n<PresmiumSiteRedirect />\n`)
|
|
365
|
-
write(join(dir, `${LIST_PAGE}.md`), list)
|
|
366
|
-
} else {
|
|
367
|
-
write(join(dir, 'index.md'), list)
|
|
368
|
-
rmSync(join(dir, `${LIST_PAGE}.md`), { force: true })
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export async function buildSite(site: SiteSpec, ctx: RenderContext): Promise<RenderOutput> {
|
|
373
|
-
const extra = siteModules(ctx)
|
|
374
|
-
const single = site.documents.length === 1 && site.documents[0]!.dir === ''
|
|
375
|
-
if (!single && ctx.config.home !== false) {
|
|
376
|
-
const work = join(ctx.workDir, 'home')
|
|
377
|
-
const root = writeHome(work, site, ctx, siteModules(ctx))
|
|
378
|
-
const code = await vitepressBuild(root, ctx.outDir)
|
|
379
|
-
if (code !== 0) throw new Error(`vitepress build of the home page exited with code ${code}`)
|
|
380
|
-
}
|
|
381
|
-
const docs = new Map(site.documents.flatMap(entry => (entry.doc ? [[entry.dir, entry.doc] as const] : [])))
|
|
382
|
-
for (const entry of site.documents) {
|
|
383
|
-
if (!entry.doc) continue
|
|
384
|
-
const workDir = documentWorkDir(ctx, entry.dir)
|
|
385
|
-
const dctx = documentContext(ctx, site, entry.dir, entry.doc, join(ctx.outDir, entry.dir), workDir)
|
|
386
|
-
try {
|
|
387
|
-
await buildDocument(entry.doc, dctx, workDir, documentSite(ctx, site, entry.dir, extra, false, [], docs))
|
|
388
|
-
} catch (error) {
|
|
389
|
-
throw new Error(`document ${entry.dir || '.'}: ${errorMessage(error)}`)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return { outDir: ctx.outDir, skipped: [] }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export async function devSite(site: SiteSpec, ctx: RenderContext, compile: (dir: string) => Promise<IRDocument>): Promise<SiteDevServer> {
|
|
396
|
-
const extra = siteModules(ctx)
|
|
397
|
-
const shared = allowed(presmiumRoot(), ...Object.values(ctx.pluginDirs))
|
|
398
|
-
const latest = new Map<string, IRDocument>()
|
|
399
|
-
const documents = new Map<string, Promise<DocumentDevServer>>()
|
|
400
|
-
const linking = new Map<string, DocumentSite>()
|
|
401
|
-
let home: Promise<{ port: number, close(): Promise<void> }> | undefined
|
|
402
|
-
let closed = false
|
|
403
|
-
const stopping = new AbortController()
|
|
404
|
-
const base = ctx.base
|
|
405
|
-
|
|
406
|
-
const startDocument = (dir: string): Promise<DocumentDevServer> => {
|
|
407
|
-
let started = documents.get(dir)
|
|
408
|
-
if (!started) {
|
|
409
|
-
started = (async () => {
|
|
410
|
-
const doc = latest.get(dir) ?? await compile(dir)
|
|
411
|
-
latest.set(dir, doc)
|
|
412
|
-
const workDir = documentWorkDir(ctx, dir)
|
|
413
|
-
mkdirSync(join(workDir, 'site'), { recursive: true })
|
|
414
|
-
const dctx = documentContext(ctx, site, dir, doc, workDir, workDir)
|
|
415
|
-
const allow = allowed(join(workDir, 'site'), dctx.deckDir, ...Object.values(dctx.pluginDirs), ...shared)
|
|
416
|
-
const document = documentSite(ctx, site, dir, extra, true, allow, latest)
|
|
417
|
-
linking.set(dir, document)
|
|
418
|
-
return devDocument(doc, dctx, document, 0, { host: LOOPBACK, signal: stopping.signal })
|
|
419
|
-
})()
|
|
420
|
-
started.catch(() => documents.delete(dir))
|
|
421
|
-
documents.set(dir, started)
|
|
422
|
-
}
|
|
423
|
-
return started
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const startHome = () => {
|
|
427
|
-
if (home) return home
|
|
428
|
-
home = (async () => {
|
|
429
|
-
const work = join(ctx.workDir, 'home')
|
|
430
|
-
mkdirSync(join(work, 'site'), { recursive: true })
|
|
431
|
-
const root = writeHome(work, site, ctx, siteModules(ctx), allowed(join(work, 'site'), ...shared))
|
|
432
|
-
const server = await vitepressDev(root, 0, () => {}, { host: LOOPBACK, signal: stopping.signal })
|
|
433
|
-
return { port: server.port, close: () => server.close() }
|
|
434
|
-
})()
|
|
435
|
-
home.catch(() => {
|
|
436
|
-
home = undefined
|
|
437
|
-
})
|
|
438
|
-
return home
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const targetOf = async (path: string): Promise<number> => {
|
|
442
|
-
const listed = new Set(site.documents.map(doc => doc.dir))
|
|
443
|
-
for (const [dir, server] of documents) {
|
|
444
|
-
if (listed.has(dir)) continue
|
|
445
|
-
documents.delete(dir)
|
|
446
|
-
latest.delete(dir)
|
|
447
|
-
linking.delete(dir)
|
|
448
|
-
void server.then(s => s.close(), () => undefined)
|
|
449
|
-
}
|
|
450
|
-
let rest = path.startsWith(base) ? path.slice(base.length) : undefined
|
|
451
|
-
if (rest !== undefined) {
|
|
452
|
-
try {
|
|
453
|
-
rest = decodeURIComponent(rest)
|
|
454
|
-
} catch {
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const dir = rest === undefined
|
|
458
|
-
? undefined
|
|
459
|
-
: [...listed].filter(d => d === '' || rest === d || rest!.startsWith(`${d}/`)).sort((a, b) => b.length - a.length)[0]
|
|
460
|
-
if (dir !== undefined) return (await startDocument(dir)).port
|
|
461
|
-
return (await startHome()).port
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const proxy = createProxyServer({ ws: true, xfwd: false })
|
|
465
|
-
proxy.on('error', (error: Error, _req: unknown, res: unknown) => {
|
|
466
|
-
const response = res as ServerResponse | Duplex | undefined
|
|
467
|
-
if (response && 'writeHead' in response && !response.headersSent) {
|
|
468
|
-
response.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
469
|
-
response.end(`site proxy error: ${error.message}`)
|
|
470
|
-
} else {
|
|
471
|
-
response?.destroy?.()
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
const pathOf = (req: IncomingMessage) => new URL(req.url ?? '/', 'http://site').pathname
|
|
475
|
-
const server = createServer((req, res) => {
|
|
476
|
-
const refused = localRequestRefusal(req.headers)
|
|
477
|
-
if (refused) {
|
|
478
|
-
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
479
|
-
res.end(refused)
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
const path = pathOf(req)
|
|
483
|
-
targetOf(path).then(
|
|
484
|
-
port => proxy.web(req, res, { target: `http://${LOOPBACK}:${port}` }),
|
|
485
|
-
(error: unknown) => {
|
|
486
|
-
if (res.headersSent) return res.destroy()
|
|
487
|
-
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
488
|
-
res.end(`the site could not show ${path}: ${errorMessage(error)}`)
|
|
489
|
-
},
|
|
490
|
-
)
|
|
491
|
-
})
|
|
492
|
-
server.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
493
|
-
if (localRequestRefusal(req.headers)) return socket.destroy()
|
|
494
|
-
targetOf(pathOf(req)).then(
|
|
495
|
-
port => proxy.ws(req, socket, head, { target: `http://${LOOPBACK}:${port}` }),
|
|
496
|
-
() => socket.destroy(),
|
|
497
|
-
)
|
|
498
|
-
})
|
|
499
|
-
const host = typeof ctx.config.host === 'string' && ctx.config.host ? ctx.config.host : '127.0.0.1'
|
|
500
|
-
const port = await new Promise<number>((resolve, reject) => {
|
|
501
|
-
server.once('error', reject)
|
|
502
|
-
server.listen(Number(ctx.config.port) || 0, host, () => resolve((server.address() as AddressInfo).port))
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
url: `http://${host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host}:${port}${base}`,
|
|
507
|
-
async update(dir: string, doc: IRDocument) {
|
|
508
|
-
if (closed) return
|
|
509
|
-
latest.set(dir, doc)
|
|
510
|
-
const entry = site.documents.find(d => d.dir === dir)
|
|
511
|
-
const known: SiteDocument = { dir, title: doc.meta.title, ...doc.meta.lang ? { lang: doc.meta.lang } : {}, ...Array.isArray(doc.meta.tags) ? { tags: (doc.meta.tags as unknown[]).filter((t): t is string => typeof t === 'string') } : {} }
|
|
512
|
-
if (entry) Object.assign(entry, known)
|
|
513
|
-
else site.documents.push(known)
|
|
514
|
-
for (const [other, document] of linking) Object.assign(document.workspace!, siteLinks(site, base, other, latest))
|
|
515
|
-
const running = documents.get(dir)
|
|
516
|
-
if (running) void running.then(server => server.update(doc)).catch((error: unknown) => console.error(`[presmium] the site did not update ${dir}: ${errorMessage(error)}`))
|
|
517
|
-
},
|
|
518
|
-
async close() {
|
|
519
|
-
closed = true
|
|
520
|
-
stopping.abort()
|
|
521
|
-
proxy.close()
|
|
522
|
-
await new Promise<void>((done) => {
|
|
523
|
-
server.close(() => done())
|
|
524
|
-
server.closeAllConnections()
|
|
525
|
-
})
|
|
526
|
-
const all = [...documents.values(), ...home ? [home] : []]
|
|
527
|
-
documents.clear()
|
|
528
|
-
await Promise.all(all.map(started => Promise.race([started.then(s => s.close(), () => undefined), new Promise(done => setTimeout(done, 10_000).unref())])))
|
|
529
|
-
},
|
|
530
|
-
}
|
|
531
|
-
}
|