Shared lexicon schemas for long-form publishing on AT Protocol. Uses typescript to json via prototypey.
45
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add linting script to remove `required` properties

+138 -1
+2 -1
package.json
··· 4 4 "type": "module", 5 5 "private": true, 6 6 "scripts": { 7 - "lexicon:emit": "bunx prototypey gen-emit ./out ./src/lexicons/**/*.ts", 7 + "lexicon:emit": "bunx prototypey gen-emit ./out ./src/lexicons/**/*.ts && bun run scripts/lint.ts", 8 8 "lexicon:import": "bunx prototypey gen-from-json ./src/lexicons ./out/**/*.json", 9 + "lexicon:lint": "bun run scripts/lint.ts", 9 10 "lexicon:publish": "bun run scripts/publish.ts" 10 11 }, 11 12 "devDependencies": {
+136
scripts/lint.ts
··· 1 + import * as fs from 'fs' 2 + import * as path from 'path' 3 + import { glob } from 'tinyglobby' 4 + 5 + export type LexiconPatches = Record<string, Record<string, unknown>> 6 + 7 + /** 8 + * Get a nested value from an object using a dot-separated path. 9 + */ 10 + function getPath(obj: Record<string, unknown>, pathStr: string): unknown { 11 + return pathStr.split('.').reduce((acc: unknown, key) => { 12 + if (acc && typeof acc === 'object') { 13 + return (acc as Record<string, unknown>)[key] 14 + } 15 + return undefined 16 + }, obj) 17 + } 18 + 19 + /** 20 + * Load patches from lexicon source files. 21 + */ 22 + async function loadPatches(): Promise<Record<string, LexiconPatches>> { 23 + const srcDir = path.join(process.cwd(), 'src/lexicons') 24 + const files = await glob('**/*.ts', { cwd: srcDir, absolute: true }) 25 + 26 + const allPatches: Record<string, LexiconPatches> = {} 27 + 28 + for (const file of files) { 29 + try { 30 + const module = await import(file) 31 + if (!module.patches) continue 32 + 33 + const lexiconId = Object.values(module) 34 + .find((v): v is { json: { id: string } } => 35 + v !== null && typeof v === 'object' && 'json' in v && typeof (v as { 36 + json?: { id?: string } 37 + }).json?.id === 'string' 38 + )?.json.id 39 + 40 + if (!lexiconId) continue 41 + 42 + allPatches[lexiconId] = module.patches 43 + } catch { 44 + // Skip files that can't be imported 45 + } 46 + } 47 + 48 + return allPatches 49 + } 50 + 51 + /** 52 + * Apply patches to a lexicon object. 53 + */ 54 + function applyPatches(lexicon: Record<string, unknown>, patches: Record<string, LexiconPatches>): boolean { 55 + const id = lexicon.id as string 56 + const lexiconPatches = patches[id] 57 + if (!lexiconPatches) return false 58 + 59 + let applied = false 60 + for (const [pathStr, fields] of Object.entries(lexiconPatches)) { 61 + const target = getPath(lexicon, pathStr) as Record<string, unknown> | undefined 62 + if (!target || typeof target !== 'object') continue 63 + 64 + for (const [field, value] of Object.entries(fields)) { 65 + if (target[field] === value) continue 66 + 67 + target[field] = value 68 + applied = true 69 + } 70 + } 71 + return applied 72 + } 73 + 74 + /** 75 + * Recursively removes `"required": true` (boolean) from an object, 76 + * while preserving `"required": [...]` (arrays). 77 + */ 78 + function removeRequiredBooleans(obj: unknown): unknown { 79 + if (Array.isArray(obj)) { 80 + return obj.map(removeRequiredBooleans) 81 + } 82 + 83 + if (obj !== null && typeof obj === 'object') { 84 + const result: Record<string, unknown> = {} 85 + 86 + for (const [key, value] of Object.entries(obj)) { 87 + // Skip "required" if it's a boolean 88 + if (key === 'required' && typeof value === 'boolean') { 89 + continue 90 + } 91 + result[key] = removeRequiredBooleans(value) 92 + } 93 + 94 + return result 95 + } 96 + 97 + return obj 98 + } 99 + 100 + /** 101 + * Lint all JSON files in the out directory. 102 + */ 103 + async function lintLexicons() { 104 + const outDir = path.join(process.cwd(), 'out') 105 + 106 + const files = fs.readdirSync(outDir).filter((f) => f.endsWith('.json')) 107 + const patches = await loadPatches() 108 + 109 + let totalFixed = 0 110 + 111 + for (const file of files) { 112 + const filePath = path.join(outDir, file) 113 + const content = fs.readFileSync(filePath, 'utf8') 114 + const original = JSON.parse(content) 115 + const cleaned = removeRequiredBooleans(original) as Record<string, unknown> 116 + 117 + const originalStr = JSON.stringify(original) 118 + 119 + // Apply patches for features prototypey doesn't support 120 + applyPatches(cleaned, patches) 121 + 122 + const cleanedStr = JSON.stringify(cleaned, null, '\t') 123 + 124 + if (originalStr !== JSON.stringify(cleaned)) { 125 + fs.writeFileSync(filePath, cleanedStr + '\n') 126 + console.log(`Fixed: ${file}`) 127 + totalFixed++ 128 + } else { 129 + console.log(`OK: ${file}`) 130 + } 131 + } 132 + 133 + console.log(`\nLinted ${files.length} files, fixed ${totalFixed}`) 134 + } 135 + 136 + lintLexicons()