@scenoco-three/compiler
v0.4.0
Published
SceNoCo build-time compiler, CLI and watcher
Maintainers
Readme
@scenoco-three/compiler
The build-time toolchain for SceNoCo: the XML parser, the
validator, the scene compiler, the optimized bundle encoder, the XSD/docs exporters,
component discovery, a file watcher, and the scenoco CLI. None of this ships to the
browser — the runtime (@scenoco-three/core) loads only the compiled bundle.
npm i -D @scenoco-three/compilerCLI
scenoco init [dir] Scaffold a runnable project
scenoco new component <Name> Generate assets/components/<Name>.ts
scenoco new scene <name> Generate assets/scenes/<name>.scene.xml
scenoco check Pre-flight: scans components, flags physics setup
scenoco validate <file...> "tsc for scenes": file:line:col diagnostics
scenoco bundle <file> [-o out] Build the runtime bundle (.bundle.json)
scenoco decompile <bundle.json> [-o out] Inverse of bundle: back to canonical XML
scenoco watch <file...> [--out dir] Rebuild bundles on XML/resource changes
scenoco export [--out dir] Write scene/prefab/material .xsd + scene-api.json (default ./schema)
--components <dirs> Comma-separated dirs scanned for @component classes (default "assets")
--packages <pkgs> Comma-separated tag packages to register (default: every
@scenoco-three/* dependency in the project's package.json)Configuration is zero-config by convention — components/scenes under assets/, tag packages
read from package.json. The exit code is non-zero when validation fails — wire it straight
into an edit→check→fix loop.
scenoco init my-app
scenoco validate assets/scenes/hello.scene.xml # all problems, with fix-its
scenoco bundle assets/scenes/hello.scene.xml -o hello.bundle.json
scenoco export --out schema # XSDs for IDE autocomplete + scene-api.jsonLibrary API
import {
compileSceneXml, compilePrefabXml, // XML → validated CompiledScene
bundleScene, bundlePrefab, // CompiledScene → optimized Bundle
serializeScene, // CompiledScene → canonical XML (round-trip)
patchSceneXml, // surgical, position-preserving edits to XML text
validateScene, parseScene, // lower-level passes
generateXsd, generateApi, generateApiJson, // exporters (from the registry)
scanComponents, loadComponents, // discover + register project @component files
watchSceneFiles, // rebuild on source/resource change
parseUniforms, glslMatches, // shader uniform parsing/validation helpers
formatDiagnostic,
} from '@scenoco-three/compiler';Compile a scene
const result = compileSceneXml(xml, { path, resolve });
if (result.ok) {
const bundle = bundleScene(result.scene);
writeFileSync('scene.bundle.json', JSON.stringify(bundle));
} else {
for (const d of result.diagnostics) console.error(formatDiagnostic(d, path));
}CompileOptions: path? (referrer for relative src= paths) and resolve?
(ResourceResolver = (request, referrer) => { path, contents } | null) — required only for
scenes that pull in resource files via src=. CompileResult is
{ ok: true; scene } | { ok: false; diagnostics }.
Discover project components
const scan = await loadComponents(['assets'], undefined, { packages: ['@scenoco-three/box2d'] });
// scan.byName: component name → file · scan.byTag: tag → module · scan.files: watched filesloadComponents compiles project .ts (via esbuild) and registers the decorated classes
in this Node process, so the validator sees the full vocabulary. scanComponents is the
static (non-executing) scan.
Watch
const handle = watchSceneFiles({ files: ['scenes/level.scene.xml'], componentRoots: ['assets'] });
handle.on?.((event) => { /* 'diagnostics' | 'success' | 'error' */ });Exporters
writeFileSync('schema/scene.xsd', generateXsd()); // IDE autocomplete + inline validation
writeFileSync('schema/scene-api.json', generateApiJson()); // machine-readable API surface
const api = generateApi(); // { nodes, attachments, components, systems, … } — derived from the registryThe compile pipeline
XML ──parse(saxes)──► expand prefabs ──► VALIDATE ──► compile (resolve refs/resources) ──► bundle- parse —
saxes, element-only (no text content); positions tracked for diagnostics. - validate — never fail-fasts: one pass returns every problem. Unknown tag/attr,
bad value, dangling
#ref, cardinality, etc. — each withfile:line:coland a fix-it. - compile — resolves references and
src=resources, inlines shader GLSL, validates<Uniform>s against the shader; produces a flatCompiledScene. - bundle — interns a string pool, elides default attrs via bitmasks, integer-encodes
references and transforms (~73% smaller than naive JSON). Decode is in
core.
The validator, compiler, and exporters are all generated views of the same registry as the runtime, so the schema cannot drift.
Prefabs
A prefab file has a <Prefab> root wrapping one node; a scene places an instance of it
with <PrefabInstance src="…prefab.xml" id="…">. Expansion happens before validation — the
<PrefabInstance> is replaced by the prefab's node subtree, so the validator/compiler/bundle
see ordinary nodes. During expansion:
- the prefab file is parsed, expanded, and validated standalone (a prefab is self-contained, which is what makes it dynamically spawnable);
- internal ids are namespaced into the instance scope (
osc→t1/osc; nested composes tot1/inner/osc) and internal#refsare rewritten to match — so the scene can address an instance internal by its scoped id (<Follow source="#t1/osc"/>); - instance transform attrs (
position/rotation/scale) override the prefab root's.
Per-instance customization uses two children:
<PrefabInstance src="turbine.prefab.xml" id="t2" position="5 0 0">
<Override target="#osc" amplitude="1.5" /> <!-- patch an internal by prefab-local id -->
<Components><Highlight color="#f00" /></Components> <!-- extra components on the instance root -->
</PrefabInstance><Override> values are validated against the target's schema, so mistakes point at the
<Override> you wrote, not into the prefab file.
Wiring a prefab internal to a scene reference (the cross-boundary case)
Because a prefab is validated standalone, it can't name a scene object. Leave the reference unset in the prefab and wire it from the instance — the override value is applied after id-scoping, so a scene id passes through unchanged and resolves in the normal scene reference pass:
<Mesh id="hud"><Components><GameManager id="gm" /></Components></Mesh>
<PrefabInstance src="player.prefab.xml" id="p1">
<Override target="#controller" gameManager="#gm" /> <!-- #gm is a SCENE id -->
</PrefabInstance>This connects the spawned player's controller.gameManager to the scene's <GameManager>.
For a prefab spawned at runtime (engine.instantiate / PrefabRef.spawn, no scene context
at author time), wire it in code instead — see the
@scenoco-three/core README (spawn → getComponent → set field,
or the prefab's script resolving the scene object via getComponentsOfType / findComponent).
Diagnostics
type SceDiagnostic = { code: string; message: string; line: number; column: number; … };
formatDiagnostic(d, filePath); // "level.scene.xml:12:7 unknown attribute 'colour' on <Mesh> — did you mean 'color'?"Standalone & dependencies
Depends on @scenoco-three/core (it imports the registry to know every tag/attr) plus
saxes (XML parse), esbuild (load project component .ts for discovery), and chokidar
(watch). It needs only core — no vite or editor. Use it as a library or as the
scenoco CLI; the Vite plugin and the editor are thin consumers of this package. Nothing
here is meant for the browser bundle.
See the repository and ARCHITECTURE.md.
