Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run catalog:check
- run: npm run typecheck
- run: npm run lint
- run: npm run test:extractor
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"gen:catalog": "node scripts/gen-catalog.mjs",
"prebuild": "npm run gen:catalog",
"prebuild:renderer": "npm run gen:catalog",
"catalog:check": "node scripts/check-catalog-stale.mjs",
"test:extractor": "node scripts/test-extract-base-url.mjs",
"package": "npm run build && electron-builder"
},
Expand Down
42 changes: 42 additions & 0 deletions scripts/check-catalog-stale.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env -S node --no-warnings
// Verify that src/main/.../catalog.generated.ts is in sync with the JSON
// sources. Exits non-zero with a diff hint if the catalog sources change
// without running `npm run gen:catalog`. Part of CI.
//
// Reads the same JSON sources the generator does and runs the build helper
// from gen-catalog.mjs. Compares byte-by-byte against the on-disk file.
'use strict'

import { readFile } from 'node:fs/promises'
import { buildForSource, SOURCES } from './gen-catalog.mjs'

let stale = 0
for (const source of SOURCES) {
let onDisk
try {
onDisk = await readFile(source.out, 'utf8')
} catch (err) {
if (err.code === 'ENOENT') {
stale++
console.error(`✗ ${source.out}`)
console.error(` catalog.generated.ts is missing. Run: npm run gen:catalog`)
continue
}
throw err
}
const { body: expected } = await buildForSource(source)
if (onDisk !== expected) {
stale++
console.error(`✗ ${source.out}`)
console.error(` catalog.generated.ts is stale vs ${source.heading}.`)
console.error(` Run: npm run gen:catalog`)
} else {
console.log(`✓ ${source.out.replace(process.cwd() + '/', '')}`)
}
}

if (stale > 0) {
console.error(`\n${stale} catalog(s) out of date.`)
process.exit(1)
}
console.log('\nall catalog.generated.ts files are in sync with their sources')
64 changes: 53 additions & 11 deletions scripts/gen-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import { readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { createHash } from 'node:crypto'

const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const SOURCES = [

export const SOURCES = [
{
src: resolve(ROOT, 'src/main/providers/catalog.source.json'),
out: resolve(ROOT, 'src/main/providers/catalog.generated.ts'),
Expand Down Expand Up @@ -46,27 +48,67 @@ function jsonToTs(value, indent = 2) {
throw new Error(`Unsupported value type: ${typeof value}`)
}

async function generate(src, out, typeName, exportName, collection, heading) {
const raw = await readFile(src, 'utf8')
/**
* Stable JSON: sort the entries array by `id` and stringify with deterministic
* key ordering. The hash fingerprint is for cheap CI checks — `git diff`
* still shows every change.
*/
function stableStringify(entries) {
const sorted = [...entries].sort((a, b) => {
const idA = String(a.id ?? '')
const idB = String(b.id ?? '')
if (idA < idB) return -1
if (idA > idB) return 1
return 0
})
const sortKeys = (v) => {
if (Array.isArray(v)) return v.map(sortKeys)
if (v && typeof v === 'object') {
const out = {}
for (const k of Object.keys(v).sort()) out[k] = sortKeys(v[k])
return out
}
return v
}
return JSON.stringify(sortKeys(sorted))
}

function fingerprint(entries) {
const json = stableStringify(entries)
return createHash('sha256').update(json).digest('hex').slice(0, 16)
}

export async function buildForSource(source) {
const raw = await readFile(source.src, 'utf8')
const parsed = JSON.parse(raw)
const entries = parsed[collection]
const entries = parsed[source.collection]
const fp = fingerprint(entries)
const header = `/**
* GENERATED FILE — DO NOT EDIT BY HAND.
*
* To add or modify entries, edit:
* ${heading}
* ${source.heading}
* and run \`npm run gen:catalog\`.
*
* fingerprint: ${fp} (entries: ${entries.length})
*/

import type { ${typeName} } from './types'
import type { ${source.type} } from './types'

export const ${exportName}: readonly ${typeName}[] =
export const ${source.name}: readonly ${source.type}[] =
${jsonToTs(entries, 2)} as const
`
await writeFile(out, header, 'utf8')
console.log(`✓ ${out.replace(ROOT + '/', '')} (${entries.length} entries)`)
return { body: header, entries, fingerprint: fp }
}

for (const s of SOURCES) {
await generate(s.src, s.out, s.type, s.name, s.collection, s.heading)
export async function writeSource(source) {
const { body } = await buildForSource(source)
await writeFile(source.out, body, 'utf8')
console.log(`✓ ${source.out.replace(ROOT + '/', '')} (${(await readFile(source.src, 'utf8')).length} bytes source)`)
}

if (import.meta.url === `file://${process.argv[1]}`) {
for (const s of SOURCES) {
await writeSource(s)
}
}
2 changes: 2 additions & 0 deletions src/main/gateways/catalog.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* To add or modify entries, edit:
* src/main/gateways/catalog.source.json
* and run `npm run gen:catalog`.
*
* fingerprint: ce1c1cb45bbe308d (entries: 11)
*/

import type { GatewayEntry } from './types'
Expand Down
2 changes: 2 additions & 0 deletions src/main/providers/catalog.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* To add or modify entries, edit:
* src/main/providers/catalog.source.json
* and run `npm run gen:catalog`.
*
* fingerprint: 3d35ef2c7f6a58e0 (entries: 18)
*/

import type { ProviderEntry } from './types'
Expand Down
Loading