mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-06-15 11:24:47 +02:00
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
import YAML from 'yaml'
|
|
|
|
export interface MeshforgeTagsConfig {
|
|
/** JS regex tested against each tag name. Tags not matching are hidden. */
|
|
include?: string
|
|
}
|
|
|
|
export interface MeshforgeTargetsConfig {
|
|
/** JS regex tested against each env name. Non-matching envs are hidden. */
|
|
include?: string
|
|
/**
|
|
* Template string that becomes a regex after substituting captures from the tag matched
|
|
* against tags.include. Supported placeholders: ${captureName}, ${captureName_snake},
|
|
* ${captureName_camel}, ${captureName_pascal}, ${1}, ${2}, …
|
|
* Each substituted segment is regex-escaped before insertion.
|
|
* When the current tag matches tags.include and this field is set, the expanded pattern
|
|
* replaces targets.include for filtering. Falls back to targets.include when the tag
|
|
* does not match or this field is absent.
|
|
*/
|
|
include_template?: string
|
|
/** AND-filter: every listed capability must be present on the env (e.g. ["wifi"]). */
|
|
require_capabilities?: string[]
|
|
}
|
|
|
|
/** Per-platform overlay (submodule name → profile fragment). */
|
|
export interface MeshforgePlatformFragment {
|
|
tags?: MeshforgeTagsConfig
|
|
targets?: MeshforgeTargetsConfig
|
|
/** Same semantics as meshforge root `require_capabilities`; merged into effective targets filter. */
|
|
require_capabilities?: string[]
|
|
}
|
|
|
|
export interface MeshforgeConfig {
|
|
tags?: MeshforgeTagsConfig
|
|
targets?: MeshforgeTargetsConfig
|
|
/** MeshForge root-level capability filter (merged into effective `targets.require_capabilities`). */
|
|
require_capabilities?: string[]
|
|
/** Submodule directory names → overlay merged on top of root when that platform is selected. */
|
|
platforms?: Record<string, MeshforgePlatformFragment>
|
|
}
|
|
|
|
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
return x !== null && typeof x === 'object' && !Array.isArray(x)
|
|
}
|
|
|
|
function readTags(obj: Record<string, unknown>): MeshforgeTagsConfig | undefined {
|
|
const t = obj.tags
|
|
if (!isRecord(t)) return undefined
|
|
const tags: MeshforgeTagsConfig = {}
|
|
if (typeof t.include === 'string') tags.include = t.include
|
|
return Object.keys(tags).length ? tags : undefined
|
|
}
|
|
|
|
function readTargets(obj: Record<string, unknown>): MeshforgeTargetsConfig | undefined {
|
|
const t = obj.targets
|
|
if (!isRecord(t)) return undefined
|
|
const targets: MeshforgeTargetsConfig = {}
|
|
if (typeof t.include === 'string') targets.include = t.include
|
|
if (typeof t.include_template === 'string') targets.include_template = t.include_template
|
|
if (Array.isArray(t.require_capabilities)) {
|
|
const caps = t.require_capabilities.filter((c): c is string => typeof c === 'string')
|
|
if (caps.length) targets.require_capabilities = caps
|
|
}
|
|
return Object.keys(targets).length ? targets : undefined
|
|
}
|
|
|
|
function readPlatformFragment(obj: Record<string, unknown>): MeshforgePlatformFragment {
|
|
const frag: MeshforgePlatformFragment = {}
|
|
const tags = readTags(obj)
|
|
const targets = readTargets(obj)
|
|
if (tags) frag.tags = tags
|
|
if (targets) frag.targets = targets
|
|
if (Array.isArray(obj.require_capabilities)) {
|
|
const caps = obj.require_capabilities.filter((c): c is string => typeof c === 'string')
|
|
if (caps.length) frag.require_capabilities = caps
|
|
}
|
|
return frag
|
|
}
|
|
|
|
/**
|
|
* Parse meshforge.yaml using a real YAML parser (nested `platforms:` and root keys).
|
|
*/
|
|
export function parseMeshforgeYaml(raw: string): MeshforgeConfig | null {
|
|
let doc: unknown
|
|
try {
|
|
doc = YAML.parse(raw)
|
|
} catch {
|
|
return null
|
|
}
|
|
if (!isRecord(doc)) return null
|
|
const mf = doc.meshforge
|
|
if (!isRecord(mf)) return null
|
|
|
|
const config: MeshforgeConfig = {}
|
|
const tags = readTags(mf)
|
|
const targets = readTargets(mf)
|
|
if (tags) config.tags = tags
|
|
if (targets) config.targets = targets
|
|
|
|
if (Array.isArray(mf.require_capabilities)) {
|
|
const caps = mf.require_capabilities.filter((c): c is string => typeof c === 'string')
|
|
if (caps.length) config.require_capabilities = caps
|
|
}
|
|
|
|
if (isRecord(mf.platforms)) {
|
|
const platforms: Record<string, MeshforgePlatformFragment> = {}
|
|
for (const [name, fragRaw] of Object.entries(mf.platforms)) {
|
|
const key = name.trim()
|
|
if (!key) continue
|
|
if (fragRaw === null || fragRaw === undefined) {
|
|
platforms[key] = {}
|
|
} else if (isRecord(fragRaw)) {
|
|
platforms[key] = readPlatformFragment(fragRaw)
|
|
}
|
|
}
|
|
if (Object.keys(platforms).length) config.platforms = platforms
|
|
}
|
|
|
|
if (!config.tags && !config.targets && !config.platforms && !config.require_capabilities) return null
|
|
return config
|
|
}
|
|
|
|
function rootCapabilityUnion(config: MeshforgeConfig): string[] {
|
|
const a = config.targets?.require_capabilities ?? []
|
|
const b = config.require_capabilities ?? []
|
|
return [...new Set([...a, ...b])]
|
|
}
|
|
|
|
/** Sorted submodule / platform keys from `meshforge.platforms`. */
|
|
export function meshforgePlatformKeys(config: MeshforgeConfig | null | undefined): string[] {
|
|
if (!config?.platforms) return []
|
|
return Object.keys(config.platforms)
|
|
.map(k => k.trim())
|
|
.filter(Boolean)
|
|
.sort((x, y) => x.localeCompare(y))
|
|
}
|
|
|
|
/**
|
|
* Root meshforge plus optional platform overlay (for tag/target filtering in UI and CI context).
|
|
* Strips `platforms` from the result — callers use {@link meshforgePlatformKeys} for the menu.
|
|
*/
|
|
export function mergeEffectiveMeshforgeConfig(
|
|
config: MeshforgeConfig | null,
|
|
platformKey: string | null
|
|
): MeshforgeConfig | null {
|
|
if (!config) return null
|
|
const baseCaps = rootCapabilityUnion(config)
|
|
const stripPlatforms = (): MeshforgeConfig => {
|
|
const { platforms: _p, ...rest } = config
|
|
const out: MeshforgeConfig = { ...rest }
|
|
if (baseCaps.length) {
|
|
out.targets = { ...out.targets, require_capabilities: baseCaps }
|
|
}
|
|
return out
|
|
}
|
|
|
|
if (!platformKey || !config.platforms?.[platformKey]) {
|
|
return stripPlatforms()
|
|
}
|
|
|
|
const frag = config.platforms[platformKey]
|
|
const fragCaps = [...(frag.targets?.require_capabilities ?? []), ...(frag.require_capabilities ?? [])]
|
|
const mergedCaps = [...new Set([...baseCaps, ...fragCaps])]
|
|
|
|
return {
|
|
tags: { ...config.tags, ...frag.tags },
|
|
targets: {
|
|
...config.targets,
|
|
...frag.targets,
|
|
...(mergedCaps.length ? { require_capabilities: mergedCaps } : {}),
|
|
},
|
|
}
|
|
}
|