@ -1,37 +1,63 @@
/ * *
* translations - check — validate the texts / idlangref discipline across UIX .
*
* Runs f our independent checks and exits non - zero if any of them fails :
* Runs f ive independent checks and exits non - zero if any of them fails :
*
* 1 . * * Morfo ` texts: ` shape * * : every value is an absolute idlangref
* ( ` '#?…|fallback' ` ) . Component - scoped refs ( ` #?components.{kebab}.… ` ) must
* match the morfo ' s own ` kebab ` .
*
* 2 . * * Component catalog coverage * * : every component - scoped idlangref points
* into ` src/uix/langs/components/{kebab}.ts ` . The dotted path must resolve
* to a leaf ` LangRecord ` ( object whose values are strings ) .
* 2 . * * Compiled ref coverage * * : every ` v.translationRef ` / ` v.commonRef ` /
* ` v.langRef ` in a morfo ' s aria / data contract — as NORMALIZED by the real
* compiler ( ` compileMorfo ` , the same path the runtime resolves ) — points to
* a catalog leaf . This is the check that catches the silent - fallback class :
* ` v.translationRef('prevMonth') ` with a catalog key ` prev-month ` compiles ,
* runs , and ships the English fallback on every locale ( measured 2026 - 08 - 10
* in chronos : five labels ) . ` texts: ` is a declaration , never a lookup table ,
* so only the compiled key can be validated .
*
* 3 . * * Common catalog coverage * * : every ` #?common.… ` idlangref points into
* 3 . * * Component catalog coverage * * : every component - scoped idlangref in
* source points into ` src/uix/langs/components/{kebab}.ts ` . The dotted path
* must resolve to a leaf ` LangRecord ` ( object whose values are strings ) .
*
* 4 . * * Common catalog coverage * * : every ` #?common.… ` idlangref points into
* ` src/uix/langs/common.ts ` .
*
* 4 . * * Orphan detection * * : every leaf in the component catalogs is referenced
* by at least one ` morfo.texts ` entry OR a soma / eidos source file . Orphan
* 5 . * * Orphan detection * * : every leaf in the component catalogs is referenced
* by a ` morfo.texts ` entry , a compiled morfo ref , or a source file . Orphan
* entries are surfaced as warnings ( not errors ) so consumers can remove
* them safely .
*
* Also enforces :
* - No raw ` translations: ` field survives anywhere under ` src/uix/ morfo/` .
* - No raw ` translations: ` field survives on any morfo.
* - No source file ( . ts / . svelte under src / uix ) declares an idlangref to a
* nonexistent path .
*
* The implementation evaluates each ` src/uix/langs/components/*.ts ` and the
* common catalog via a controlled ` Function(...) ` call ; those files are pure
* data so this is safe in CI .
* The catalogs and morfos are IMPORTED ( they are pure data module s ) and morfos
* are compiled with the real ` compileMorfo ` — never re - parsed with regex . The
* previous implementation regex - evaluated the catalog sources and its comment
* stripper corrupted any string containing ` // ` ( a ` https:// ` URL crashed the
* whole run ) , while the class it was credited with catching — translationRef
* call sites — was never scanned at all : a ` v.translationRef('key') ` contains
* no ` #? ` literal until the compiler normalizes it . Validating the compiled
* contract closes both holes at once .
*
* ` .ts ` sources are scanned via the TypeScript AST ( string literals only , so a
* ` // ` inside a string can never eat the rest of the line and comments never
* produce false references ) . ` .svelte ` sources get their ` <script> ` blocks
* AST - scanned the same way ; the remaining template only has HTML comments
* stripped — line comments are left alone there on purpose , because stripping
* them is exactly what turned URLs into silent false negatives .
* /
import { readdirSync , readFileSync , statSync } from 'node:fs' ;
import { join , dirname , basename } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import { join , dirname } from 'node:path' ;
import { fileURLToPath , pathToFileURL } from 'node:url' ;
import ts from 'typescript' ;
import { compileMorfo } from '../src/uix/morfo/compile' ;
import type { SourcePlan } from '../src/uix/morfo/compile' ;
import type { Morfo } from '../src/uix/morfo/types' ;
const HERE = dirname ( fileURLToPath ( import . meta . url ) ) ;
const REPO = join ( HERE , '..' ) ;
@ -57,44 +83,83 @@ function warn(source: string, message: string) {
findings . push ( { severity : 'warn' , source , message } ) ;
}
// ── Catalog evaluation ────────────────────────────────────────────────────
function evalCatalogFile ( file : string , exportName : string ) : LangBranch {
const source = readFileSync ( file , 'utf8' ) ;
const stripped = source
. replace ( /\/\/[^\n]*/g , '' )
. replace ( /\/\*[\s\S]*?\*\//g , '' )
. replace ( /,(\s*[}\]])/g , '$1' ) ;
const re = new RegExp ( ` export \\ s+const \\ s+ ${ exportName } \\ s*= \\ s*( \\ {[ \\ s \\ S]*? \\ }) \\ s+satisfies ` ) ;
const match = stripped . match ( re ) ;
if ( ! match ) throw new Error ( ` No export const ${ exportName } found in ${ file } ` ) ;
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const fn = new Function ( ` return ( ${ match [ 1 ] } ); ` ) ;
return fn ( ) as LangBranch ;
// ── Module loading ────────────────────────────────────────────────────────
async function importModule ( file : string ) : Promise < Record < string , unknown > | null > {
try {
return ( await import ( pathToFileURL ( file ) . href ) ) as Record < string , unknown > ;
} catch ( e ) {
err ( file , ` Failed to import: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
return null ;
}
}
function kebabToCamel ( kebab : string ) : string {
return kebab . replace ( /-([a-z])/g , ( _ , c ) = > c . toUpperCase ( ) ) ;
}
function loadCommonCatalog ( ) : LangBranch {
return evalCatalogFile ( LANGS_COMMON , 'commonLangs' ) ;
async function loadCommonCatalog ( ) : Promise < LangBranch > {
const mod = await importModule ( LANGS_COMMON ) ;
const branch = mod ? . commonLangs as LangBranch | undefined ;
if ( ! branch ) {
err ( LANGS_COMMON , 'No commonLangs export found.' ) ;
return { } ;
}
return branch ;
}
function loadComponentCatalogs ( ) : Map < string , LangBranch > {
async function loadComponentCatalogs ( ) : Promise< Map < string, LangBranch > > {
const map = new Map < string , LangBranch > ( ) ;
const files = readdirSync ( LANGS_COMPONENTS ) . filter (
( f ) = > f . endsWith ( '.ts' ) && f !== 'index.ts'
) ;
const files = readdirSync ( LANGS_COMPONENTS ) . filter ( ( f ) = > f . endsWith ( '.ts' ) && f !== 'index.ts' ) ;
for ( const file of files ) {
const kebab = file . replace ( /\.ts$/ , '' ) ;
const exportName = ` ${ kebabToCamel ( kebab ) } Langs ` ;
const data = evalCatalogFile ( join ( LANGS_COMPONENTS , file ) , exportName ) ;
const mod = await importModule ( join ( LANGS_COMPONENTS , file ) ) ;
if ( ! mod ) continue ;
const data = mod [ exportName ] as LangBranch | undefined ;
if ( ! data ) {
err ( join ( LANGS_COMPONENTS , file ) , ` No export const ${ exportName } found. ` ) ;
continue ;
}
map . set ( kebab , data ) ;
}
return map ;
}
interface LoadedMorfo {
file : string ;
morfo : Morfo ;
}
function looksLikeMorfo ( x : unknown ) : x is Morfo {
return (
! ! x &&
typeof x === 'object' &&
typeof ( x as Morfo ) . kebab === 'string' &&
Array . isArray ( ( x as Morfo ) . parts )
) ;
}
async function loadMorfos ( ) : Promise < LoadedMorfo [ ] > {
const out : LoadedMorfo [ ] = [ ] ;
// Dedupe by identity: a module that re-exports another component's morfo
// must not double-count it (compileMorfo caches by identity for the same
// reason).
const seen = new Set < unknown > ( ) ;
const files = readdirSync ( MORFO_DIR ) . filter ( ( f ) = > f . endsWith ( '.ts' ) && ! f . endsWith ( '.test.ts' ) ) ;
for ( const file of files ) {
const path = join ( MORFO_DIR , file ) ;
const mod = await importModule ( path ) ;
if ( ! mod ) continue ;
for ( const exported of Object . values ( mod ) ) {
if ( ! looksLikeMorfo ( exported ) || seen . has ( exported ) ) continue ;
seen . add ( exported ) ;
out . push ( { file : path , morfo : exported } ) ;
}
}
return out ;
}
// ── Path resolution inside a catalog branch ───────────────────────────────
function resolvePath ( node : LangBranch , path : string [ ] ) : unknown {
@ -121,7 +186,7 @@ function collectLeafPaths(node: unknown, prefix: string[] = []): string[][] {
return Object . entries ( node ) . flatMap ( ( [ k , v ] ) = > collectLeafPaths ( v , [ . . . prefix , k ] ) ) ;
}
// ── Morfo file scanning ──────── ───────────────────────────────────────────
// ── Morfo texts + compiled refs ───────────────────────────────────────────
interface MorfoTextEntry {
file : string ;
@ -130,55 +195,60 @@ interface MorfoTextEntry {
rawRef : string ;
}
function extractKebab ( source : string ) : string | null {
const m = source . match ( /kebab:\s*'([^']+)'/ ) ;
return m ? . [ 1 ] ? ? null ;
function morfoTextEntries ( loaded : LoadedMorfo [ ] ) : MorfoTextEntry [ ] {
const out : MorfoTextEntry [ ] = [ ] ;
for ( const { file , morfo } of loaded ) {
// Hard ban: no legacy `translations:` field.
if ( 'translations' in ( morfo as object ) ) {
err ( file , 'Legacy `translations:` field present. Replace with `texts:` idlangrefs.' ) ;
}
const texts = ( morfo as { texts? : Record < string , string > } ) . texts ? ? { } ;
for ( const [ textKey , rawRef ] of Object . entries ( texts ) ) {
out . push ( { file , kebab : morfo.kebab , textKey , rawRef } ) ;
}
}
return out ;
}
function extractTextsBlock ( source : string ) : string | null {
const m = source . match ( /^\ttexts:\s*\{/m ) ;
if ( ! m ) return null ;
const open = m . index ! + m [ 0 ] . length - 1 ;
let depth = 1 ;
let i = open + 1 ;
while ( i < source . length && depth > 0 ) {
if ( source [ i ] === '{' ) depth ++ ;
else if ( source [ i ] === '}' ) depth -- ;
i ++ ;
}
if ( depth !== 0 ) return null ;
return source . slice ( open + 1 , i - 1 ) ;
interface CompiledRefEntry {
file : string ;
kebab : string ;
part : string ;
attr : string ;
/** Normalized idlangref as the runtime will resolve it: `#?path|fallback`. */
ref : string ;
}
function scanMorfoTexts ( ) : MorfoTextEntry [ ] {
const out : MorfoTextEntry [ ] = [ ] ;
const files = readdirSync ( MORFO_DIR ) . filter ( ( f ) = > f . endsWith ( '.ts' ) && ! f . endsWith ( '.test.ts' ) ) ;
for ( const file of files ) {
const path = join ( MORFO_DIR , file ) ;
const source = readFileSync ( path , 'utf8' ) ;
function visitSource ( source : SourcePlan , onRef : ( ref : string ) = > void ) : void {
if ( source . kind === 'mapRef' ) {
visitSource ( source . source , onRef ) ;
return ;
}
if ( source . kind === 'translationRef' ) onRef ( source . key ) ;
}
// Hard ban: no `translations:` field at root indent.
if ( /^\ttranslations:\s*\{/m . test ( source ) ) {
err ( path , 'Legacy `translations:` field present. Replace with `texts:` idlangrefs.' ) ;
function collectCompiledRefs ( loaded : LoadedMorfo [ ] ) : CompiledRefEntry [ ] {
const out : CompiledRefEntry [ ] = [ ] ;
for ( const { file , morfo } of loaded ) {
let compiled ;
try {
compiled = compileMorfo ( morfo ) ;
} catch ( e ) {
err ( file , ` compileMorfo failed: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
continue ;
}
const kebab = extractKebab ( source ) ;
if ( ! kebab ) continue ;
const block = extractTextsBlock ( source ) ;
if ( ! block ) continue ;
// Walk key: '#?…' or "key": '#?…' lines. The value is always a single
// quoted string per the migrated layout.
const lineRe = /['"]?([\w-]+(?:\.[\w-]+)*)['"]?\s*:\s*['"`](#\?[^'"`]+)['"`]/g ;
let lm : RegExpExecArray | null ;
while ( ( lm = lineRe . exec ( block ) ) ) {
out . push ( { file : path , kebab , textKey : lm [ 1 ] , rawRef : lm [ 2 ] } ) ;
for ( const [ partKebab , part ] of compiled . parts . byKebab ) {
for ( const plan of part . dynamicAttrs ) {
visitSource ( plan . source , ( ref ) = > {
out . push ( { file , kebab : compiled.kebab , part : partKebab , attr : plan.attr , ref } ) ;
} ) ;
}
}
}
return out ;
}
// ── Cross-source idlangref scanning ───────────────────────────────────────
// ── Source idlangref scanning ─────────────────────────────────────────────
function listSourceFiles ( root : string ) : string [ ] {
if ( ! statSync ( root ) . isDirectory ( ) ) return [ ] ;
@ -199,32 +269,95 @@ function listSourceFiles(root: string): string[] {
return out ;
}
function stripCommentsAndStrings ( source : string ) : string {
// Drop // line comments, /* */ block comments, and `<!-- -->` HTML comments
// inside .svelte. Templates with `{var}` placeholders inside comments would
// otherwise produce false-positive idlangref scans.
return source
. replace ( /\/\/[^\n]*/g , '' )
. replace ( /\/\*[\s\S]*?\*\//g , '' )
. replace ( /<!--[\s\S]*?-->/g , '' ) ;
// Only match refs with no `{` `}` `:` whitespace — those are real idlangrefs,
// not doc placeholders like `#?components.{kebab}.{key}`.
const IDLANGREF_RE = /#\?([A-Za-z][\w.-]*)(?:\|([^'"`\n]*))?/g ;
/ * *
* A text chunk extracted from a source file . ` truncated ` marks template - literal
* heads / middle spans : their text ends where a ` ${ … } ` substitution begins , so
* a ref match that reaches the chunk ' s end is INCOMPLETE — a dynamic ref like
* ` ` ` #?components.x. ${ key } |… ` ` ` must not be validated as the literal path
* ` components.x. ` ; it contributes a dynamic PREFIX instead .
* /
interface SourceChunk {
text : string ;
truncated : boolean ;
}
function scanIdlangrefsInSource ( ) : Map < string , Set < string > > {
const occurrences = new Map < string , Set < string > > ( ) ; // ref → files
const files = listSourceFiles ( UIX ) ;
// Only match refs with no `{` `}` `:` whitespace — those are real idlangrefs,
// not doc placeholders like `#?components.{kebab}.{key}`.
const re = /#\?([A-Za-z][\w.-]*)(?:\|([^'"`\n]*))?/g ;
for ( const file of files ) {
const source = stripCommentsAndStrings ( readFileSync ( file , 'utf8' ) ) ;
let m : RegExpExecArray | null ;
while ( ( m = re . exec ( source ) ) ) {
const path = m [ 1 ] ;
if ( ! occurrences . has ( path ) ) occurrences . set ( path , new Set ( ) ) ;
occurrences . get ( path ) ! . add ( file ) ;
/** Every string-literal text in a TS source, via the real parser. */
function stringChunksOf ( fileName : string , source : string ) : SourceChunk [ ] {
const sf = ts . createSourceFile ( fileName , source , ts . ScriptTarget . Latest , false ) ;
const out : SourceChunk [ ] = [ ] ;
const visit = ( node : ts.Node ) : void = > {
if ( ts . isStringLiteralLike ( node ) ) {
out . push ( { text : node.text , truncated : false } ) ;
} else if ( ts . isTemplateExpression ( node ) ) {
out . push ( { text : node.head.text , truncated : true } ) ;
for ( let i = 0 ; i < node . templateSpans . length ; i ++ ) {
// Every span's literal but the last is followed by another `${…}`.
const last = i === node . templateSpans . length - 1 ;
out . push ( { text : node.templateSpans [ i ] . literal . text , truncated : ! last } ) ;
}
}
ts . forEachChild ( node , visit ) ;
} ;
visit ( sf ) ;
return out ;
}
interface SourceScan {
/** ref path → files that reference it. */
occurrences : Map < string , Set < string > > ;
/ * *
* Path prefixes of refs built dynamically ( template substitution right
* after the prefix ) . A catalog leaf under one of these cannot be proven
* orphan — the concrete key only exists at runtime .
* /
dynamicPrefixes : Set < string > ;
}
function scanChunk ( chunk : SourceChunk , file : string , scan : SourceScan ) : void {
IDLANGREF_RE . lastIndex = 0 ;
let m : RegExpExecArray | null ;
while ( ( m = IDLANGREF_RE . exec ( chunk . text ) ) ) {
const reachesEnd = m . index + m [ 0 ] . length === chunk . text . length ;
if ( chunk . truncated && reachesEnd ) {
scan . dynamicPrefixes . add ( m [ 1 ] . replace ( /\.$/ , '' ) ) ;
continue ;
}
if ( ! scan . occurrences . has ( m [ 1 ] ) ) scan . occurrences . set ( m [ 1 ] , new Set ( ) ) ;
scan . occurrences . get ( m [ 1 ] ) ! . add ( file ) ;
}
return occurrences ;
}
function refChunksOfFile ( path : string ) : SourceChunk [ ] {
const source = readFileSync ( path , 'utf8' ) ;
if ( ! path . endsWith ( '.svelte' ) ) return stringChunksOf ( path , source ) ;
const chunks : SourceChunk [ ] = [ ] ;
const scriptRe = /<script[^>]*>([\s\S]*?)<\/script>/g ;
let m : RegExpExecArray | null ;
while ( ( m = scriptRe . exec ( source ) ) ) {
chunks . push ( . . . stringChunksOf ( path , m [ 1 ] ) ) ;
}
// Template remainder: strip HTML comments only. `//` stays — it lives
// inside URLs in quoted strings, and eating the rest of the line is how
// real refs went unscanned.
chunks . push ( {
text : source.replace ( scriptRe , '' ) . replace ( /<!--[\s\S]*?-->/g , '' ) ,
truncated : false
} ) ;
return chunks ;
}
function scanIdlangrefsInSource ( ) : SourceScan {
const scan : SourceScan = { occurrences : new Map ( ) , dynamicPrefixes : new Set ( ) } ;
for ( const file of listSourceFiles ( UIX ) ) {
for ( const chunk of refChunksOfFile ( file ) ) {
scanChunk ( chunk , file , scan ) ;
}
}
return scan ;
}
// ── Checks ────────────────────────────────────────────────────────────────
@ -262,10 +395,7 @@ function checkMorfoTextsShape(
}
const branch = components . get ( kebab ) ;
if ( ! branch ) {
err (
file ,
` texts. ${ textKey } : no catalog found at src/uix/langs/components/ ${ kebab } .ts. `
) ;
err ( file , ` texts. ${ textKey } : no catalog found at src/uix/langs/components/ ${ kebab } .ts. ` ) ;
continue ;
}
const leaf = resolvePath ( branch , segments . slice ( 2 ) ) ;
@ -293,28 +423,89 @@ function checkMorfoTextsShape(
}
}
function checkCompiledRefs (
entries : CompiledRefEntry [ ] ,
components : Map < string , LangBranch > ,
common : LangBranch
) : void {
for ( const { file , kebab , part , attr , ref } of entries ) {
const body = ref . startsWith ( '#?' ) ? ref . slice ( 2 ) : ref ;
const pipe = body . indexOf ( '|' ) ;
const refPath = pipe === - 1 ? body : body.slice ( 0 , pipe ) ;
const segments = refPath . split ( '.' ) ;
const where = ` ${ kebab } . ${ part } ${ attr } ` ;
if ( segments [ 0 ] === 'components' ) {
if ( segments [ 1 ] !== kebab ) {
err (
file ,
` ${ where } : compiled ref ${ refPath } points into components. ${ segments [ 1 ] } but the morfo kebab is ${ kebab } . `
) ;
continue ;
}
const branch = components . get ( kebab ) ;
if ( ! branch ) {
err (
file ,
` ${ where } : no catalog at src/uix/langs/components/ ${ kebab } .ts for compiled ref ${ refPath } . `
) ;
continue ;
}
const leaf = resolvePath ( branch , segments . slice ( 2 ) ) ;
if ( ! isLeafRecord ( leaf ) ) {
err (
file ,
` ${ where } : compiled ref ${ refPath } does not resolve to a leaf in components/ ${ kebab } .ts — ` +
'translationRef takes the CATALOG key verbatim; an alias dies to the English fallback.'
) ;
}
} else if ( segments [ 0 ] === 'common' ) {
const leaf = resolvePath ( common , segments . slice ( 1 ) ) ;
if ( ! isLeafRecord ( leaf ) ) {
err (
file ,
` ${ where } : compiled ref ${ refPath } does not resolve to a leaf in langs/common.ts — ` +
'commonRef takes the CATALOG group verbatim; an alias dies to the English fallback.'
) ;
}
} else {
warn (
file ,
` ${ where } : compiled ref ${ refPath } is outside common/components; not validated by UIX. `
) ;
}
}
}
function checkComponentCatalogOrphans (
components : Map < string , LangBranch > ,
morfoEntries : MorfoTextEntry [ ] ,
sourceOccurrences : Map < string , Set < string > >
compiledRefs : CompiledRefEntry [ ] ,
scan : SourceScan
) : void {
const referenced = new Set < string > ( ) ;
for ( const e of morfoEntries ) {
const refPath = e . rawRef . slice ( 2 ) . split ( '|' ) [ 0 ] ;
referenced . add ( refPath ) ;
}
for ( const path of sourceOccurrences . keys ( ) ) referenced . add ( path ) ;
for ( const e of compiledRefs ) {
const body = e . ref . startsWith ( '#?' ) ? e . ref . slice ( 2 ) : e . ref ;
referenced . add ( body . split ( '|' ) [ 0 ] ) ;
}
for ( const path of scan . occurrences . keys ( ) ) referenced . add ( path ) ;
const dynamicPrefixes = [ . . . scan . dynamicPrefixes ] ;
for ( const [ kebab , branch ] of components ) {
const leaves = collectLeafPaths ( branch ) ;
for ( const leafPath of leaves ) {
const full = ` components. ${ kebab } . ${ leafPath . join ( '.' ) } ` ;
if ( ! referenced . has ( full ) ) {
warn (
join ( LANGS_COMPONENTS , ` ${ kebab } .ts ` ) ,
` Orphan: ${ full } is not referenced by any morfo.texts entry or source file. `
) ;
}
if ( referenced . has ( full ) ) continue ;
// A ref built at runtime (`#?components.x.${key}`) can reach any leaf
// under its prefix — those cannot be proven orphan.
if ( dynamicPrefixes . some ( ( p ) = > full === p || full . startsWith ( ` ${ p } . ` ) ) ) continue ;
warn (
join ( LANGS_COMPONENTS , ` ${ kebab } .ts ` ) ,
` Orphan: ${ full } is not referenced by any morfo.texts entry, compiled ref or source file. `
) ;
}
}
}
@ -349,15 +540,18 @@ function checkSourceIdlangrefs(
// ── Main ──────────────────────────────────────────────────────────────────
function main() {
const common = loadCommonCatalog ( ) ;
const components = loadComponentCatalogs ( ) ;
const morfoEntries = scanMorfoTexts ( ) ;
const sourceOccurrences = scanIdlangrefsInSource ( ) ;
async function main() {
const common = await loadCommonCatalog ( ) ;
const components = await loadComponentCatalogs ( ) ;
const morfos = await loadMorfos ( ) ;
const morfoEntries = morfoTextEntries ( morfos ) ;
const compiledRefs = collectCompiledRefs ( morfos ) ;
const scan = scanIdlangrefsInSource ( ) ;
checkMorfoTextsShape ( morfoEntries , components , common ) ;
checkSourceIdlangrefs ( sourceOccurrences , components , common ) ;
checkComponentCatalogOrphans ( components , morfoEntries , sourceOccurrences ) ;
checkCompiledRefs ( compiledRefs , components , common ) ;
checkSourceIdlangrefs ( scan . occurrences , components , common ) ;
checkComponentCatalogOrphans ( components , morfoEntries , compiledRefs , scan ) ;
const errors = findings . filter ( ( f ) = > f . severity === 'error' ) ;
const warnings = findings . filter ( ( f ) = > f . severity === 'warn' ) ;
@ -370,10 +564,15 @@ function main() {
console . log ( ) ;
console . log (
` Summary: ${ morfoEntries . length } morfo text refs · ${ components . size } component catalogs · ${ errors . length } errors · ${ warnings . length } warnings. `
` Summary: ${ morfos . length } morfos · ${ morfoEntries . length } texts refs · ` +
` ${ compiledRefs . length } compiled refs · ${ components . size } component catalogs · ` +
` ${ errors . length } errors · ${ warnings . length } warnings. `
) ;
if ( errors . length > 0 ) process . exit ( 1 ) ;
}
main ( ) ;
main ( ) . catch ( ( e ) = > {
console . error ( e ) ;
process . exit ( 2 ) ;
} ) ;