aboutsummaryrefslogtreecommitdiff
path: root/scripts/compile-content.ts
blob: b3e8989c9b6745b43c971fde0fb98b6bbdd08122 (plain)
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()