cli: add orca plugin create, validate and inspect (#36280)

* cli: add orca plugin create, validate and inspect

* build: ship orca plugin validate's main checks as a CLI main entry
This commit is contained in:
Neil
2026-10-30 01:00:15 -07:00
committed by GitHub
parent 5dd43d0146
commit 1963ac0ea8
27 changed files with 1154 additions and 77 deletions
@@ -0,0 +1,126 @@
import { z } from 'zod'
import {
isPlausiblePluginIconSvg,
parsePluginIconThemeArtifact
} from '../artifacts/plugin-icon-theme-artifact'
import { parsePluginLanguagePackArtifact } from '../artifacts/plugin-language-pack-artifact'
import {
parsePluginGrammarArtifact,
parsePluginLanguageConfiguration
} from '../artifacts/plugin-syntax-artifact'
import { parsePluginTerminalThemeArtifact } from '../artifacts/plugin-terminal-theme-artifact'
import { parsePluginAppThemeArtifact } from '../artifacts/plugin-theme-artifact'
import type { PluginManifest } from '../manifest/plugin-manifest'
import { formatPluginSchemaIssues } from '../manifest/plugin-schema-issues'
/** Reads a plugin file by its manifest path (always `/`); null when it does not exist. */
export type PluginArtifactTextReader = (manifestPath: string) => string | null
const THEME_POINTS = ['themes', 'terminalThemes', 'iconThemes'] as const
const THEME_PARSERS: Record<(typeof THEME_POINTS)[number], (raw: string) => unknown> = {
themes: (raw) => parsePluginAppThemeArtifact(raw, 'strict'),
terminalThemes: (raw) => parsePluginTerminalThemeArtifact(raw, 'strict'),
iconThemes: (raw) => parsePluginIconThemeArtifact(raw, 'strict')
}
// Why: Orca drops an icon theme whose SVG is missing or not an SVG only once a user picks it.
function iconSvgErrors(read: PluginArtifactTextReader, themeText: string | null): string[] {
if (themeText === null) {
return []
}
let artifact: ReturnType<typeof parsePluginIconThemeArtifact>
try {
artifact = parsePluginIconThemeArtifact(themeText, 'strict')
} catch {
return []
}
const paths = new Set([
...Object.values(artifact.icons),
...Object.values(artifact.fileNames),
...Object.values(artifact.fileExtensions)
])
return [...paths].flatMap((path) => {
const svg = read(path)
if (svg === null) {
return [`${path}: file not found`]
}
return isPlausiblePluginIconSvg(svg) ? [] : [`${path}: not an SVG image`]
})
}
function detail(error: unknown, text: string | null): string {
if (error instanceof z.ZodError) {
let input: unknown = null
try {
input = text === null ? null : JSON.parse(text)
} catch {
input = null
}
return formatPluginSchemaIssues(error.issues, input)
}
return error instanceof Error ? error.message : String(error)
}
function check(read: PluginArtifactTextReader, path: string, parse: (text: string) => void) {
const text = read(path)
if (text === null) {
return 'file not found'
}
try {
parse(text)
return null
} catch (error) {
return detail(error, text)
}
}
/**
* Author-time checks of every content artifact, strictly. Why: Orca skips unknown theme tokens,
* icon slots, protected language-pack copy and unsafe syntax settings at load, so authors would
* only learn about a typo here.
*/
export function strictPluginArtifactErrors(
manifest: PluginManifest,
read: PluginArtifactTextReader
): string[] {
const c = manifest.contributes
const themeErrors = THEME_POINTS.flatMap((point) =>
c[point].flatMap((contribution) => {
const error = check(read, contribution.path, THEME_PARSERS[point])
return error
? [`contributes.${point} "${contribution.id}" (${contribution.path}): ${error}`]
: []
})
)
const packErrors = c.languagePacks.flatMap((pack) => {
const error = check(read, pack.path, (text) => {
const parsed = parsePluginLanguagePackArtifact(text, 'strict')
if (!parsed.ok) {
throw new Error(parsed.error)
}
})
return error ? [`contributes.languagePacks "${pack.locale}" (${pack.path}): ${error}`] : []
})
const syntaxErrors = c.syntaxes.flatMap((syntax) => {
const grammar = check(read, syntax.path, (text) =>
parsePluginGrammarArtifact(text, syntax.scopeName)
)
const configuration = syntax.configuration
? check(read, syntax.configuration, (text) =>
parsePluginLanguageConfiguration(text, 'strict')
)
: null
return [
...(grammar ? [`contributes.syntaxes "${syntax.id}" (${syntax.path}): ${grammar}`] : []),
...(configuration
? [`contributes.syntaxes "${syntax.id}" (${syntax.configuration}): ${configuration}`]
: [])
]
})
const svgErrors = c.iconThemes.flatMap((theme) =>
iconSvgErrors(read, read(theme.path)).map(
(error) => `contributes.iconThemes "${theme.id}" (${theme.path}): ${error}`
)
)
return [...themeErrors, ...svgErrors, ...packErrors, ...syntaxErrors]
}