1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
import { createHash } from 'node:crypto'
import {
mkdirSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { dirname, join, relative, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseYaml } from 'yaml'
import type {
ContentManifest,
ManifestEntry
} from '@hollowdark/content-system/manifest/schema'
const HERE = dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = join(HERE, '..')
const CONTENT_DIR = join(PROJECT_ROOT, 'content')
const OUT_CONTENT_DIR = join(PROJECT_ROOT, 'static', 'content')
const OUT_MANIFEST_PATH = join(PROJECT_ROOT, 'static', 'content-manifest.json')
function readPackageVersion(): string {
const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json'), 'utf8')) as {
version: string
}
return pkg.version
}
function walkYaml(dir: string): readonly string[] {
const out: string[] = []
const walk = (current: string): void => {
for (const entry of readdirSync(current)) {
const full = join(current, entry)
const stat = statSync(full)
if (stat.isDirectory()) {
walk(full)
} else if (stat.isFile() && entry.endsWith('.yaml')) {
out.push(full)
}
}
}
walk(dir)
return out
}
function toChunkId(absPath: string): string {
const rel = relative(CONTENT_DIR, absPath)
const withoutExt = rel.slice(0, -'.yaml'.length)
return withoutExt.split(sep).join('/')
}
function shortHash(bytes: Buffer | string): string {
return createHash('sha256').update(bytes).digest('hex').slice(0, 16)
}
function writeChunk(chunkId: string, data: unknown, version: string): ManifestEntry {
const jsonBody = JSON.stringify(data)
const hash = shortHash(jsonBody)
const segments = chunkId.split('/')
const leaf = segments[segments.length - 1]
if (leaf === undefined) {
throw new Error(`Empty chunk id from compilation input`)
}
const parentSegments = segments.slice(0, -1)
const finalName = `${leaf}.v${version}.${hash}.json`
const outPath = join(OUT_CONTENT_DIR, ...parentSegments, finalName)
mkdirSync(dirname(outPath), { recursive: true })
writeFileSync(outPath, jsonBody, 'utf8')
const urlPath = [...parentSegments, finalName].join('/')
return {
version,
hash,
url: `/content/${urlPath}`,
size_bytes: Buffer.byteLength(jsonBody, 'utf8')
}
}
function compile(): void {
const version = readPackageVersion()
rmSync(OUT_CONTENT_DIR, { recursive: true, force: true })
mkdirSync(OUT_CONTENT_DIR, { recursive: true })
const yamlFiles = walkYaml(CONTENT_DIR)
const chunks: Record<string, ManifestEntry> = {}
for (const absPath of yamlFiles) {
const raw = readFileSync(absPath, 'utf8')
const parsed = parseYaml(raw) as unknown
const chunkId = toChunkId(absPath)
chunks[chunkId] = writeChunk(chunkId, parsed, version)
}
const manifest: ContentManifest = {
content_version: version,
app_version: version,
generated_at: new Date().toISOString(),
chunks
}
mkdirSync(dirname(OUT_MANIFEST_PATH), { recursive: true })
writeFileSync(OUT_MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf8')
const chunkCount = Object.keys(chunks).length
process.stdout.write(`Compiled ${chunkCount} content chunk${chunkCount === 1 ? '' : 's'} to ${OUT_CONTENT_DIR}\n`)
}
compile()
|