@scenoco-three/compiler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ import { Registry, allNodeDefs, allAttachmentDefs, attachmentsForNode, TRANSFORM_ATTRS, } from '@scenoco-three/core/internal';
2
+ import { codecs } from '../dsl/types.js';
3
+ /**
4
+ * Generates a no-namespace XSD 1.0 from the live registries (node tags +
5
+ * components). Point an editor's XML language server at it (via
6
+ * `xsi:noNamespaceSchemaLocation="scene.xsd"`) to get attribute autocomplete and
7
+ * inline validation for free — the same data the runtime Validator checks, in a
8
+ * form stock IDE tooling understands.
9
+ *
10
+ * The structure favors editor assistance over strict validation (e.g. it does
11
+ * not enforce "exactly one geometry per Mesh" — that's the Validator's job and
12
+ * would fight XSD 1.0's Unique Particle Attribution rule). Every element still
13
+ * carries a precise, typed attribute list.
14
+ */
15
+ export function generateXsd() {
16
+ const defs = allNodeDefs();
17
+ const byKind = (k) => defs.filter((d) => d.kind === k);
18
+ const out = [];
19
+ out.push(`<?xml version="1.0" encoding="UTF-8"?>`);
20
+ out.push(`<!-- Generated by scenoco. Do not edit by hand; run \`scenoco export\`. -->`);
21
+ out.push(`<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">`);
22
+ out.push('');
23
+ // --- Reusable simple types for math/reference attribute values -----------
24
+ out.push(` <!-- Value formats -->`);
25
+ for (const t of ['vec2', 'vec3', 'euler', 'color']) {
26
+ out.push(simpleType(t, codecs[t].xsd.pattern));
27
+ }
28
+ out.push(simpleType('ref', '#\\S+')); // a "#id" reference
29
+ out.push('');
30
+ // --- Shared transform attributes (every spatial node) --------------------
31
+ out.push(` <!-- Transform attributes shared by all spatial nodes -->`);
32
+ out.push(` <xs:attributeGroup name="transform">`);
33
+ out.push(` <xs:attribute name="id" type="xs:string"/>`);
34
+ for (const [name, meta] of Object.entries(TRANSFORM_ATTRS)) {
35
+ out.push(` ${nodeAttr(name, meta)}`);
36
+ }
37
+ out.push(` </xs:attributeGroup>`);
38
+ out.push('');
39
+ // --- Attachment elements (geometry/material/fog/…) -----------------------
40
+ out.push(` <!-- Attachments: leaves that configure a node (per-node allowed set below) -->`);
41
+ for (const def of allAttachmentDefs())
42
+ out.push(attachmentElement(def));
43
+ out.push('');
44
+ // --- Components -----------------------------------------------------------
45
+ out.push(` <!-- Components (children of <Components>) -->`);
46
+ for (const name of Registry.componentNames())
47
+ out.push(componentElement(name));
48
+ out.push('');
49
+ out.push(` <xs:element name="Components">`);
50
+ out.push(` <xs:complexType>`);
51
+ out.push(` <xs:choice minOccurs="0" maxOccurs="unbounded">`);
52
+ for (const name of Registry.componentNames())
53
+ out.push(` <xs:element ref="${name}"/>`);
54
+ out.push(` </xs:choice>`);
55
+ out.push(` </xs:complexType>`);
56
+ out.push(` </xs:element>`);
57
+ out.push('');
58
+ // --- Prefab instance + Override (DSL elements, not registered node tags) --
59
+ out.push(` <!-- Prefab instance: expands a .prefab.xml file in place -->`);
60
+ out.push(prefabElement());
61
+ out.push(overrideElement());
62
+ out.push('');
63
+ // --- Spatial nodes (Object3D family) -------------------------------------
64
+ const object3d = defs.filter((d) => d.kind === 'object3d');
65
+ out.push(choiceGroup('object3dNodes', [...object3d.map((d) => d.tag), 'PrefabInstance']));
66
+ out.push('');
67
+ out.push(` <!-- Spatial nodes -->`);
68
+ for (const def of [...byKind('scene'), ...object3d])
69
+ out.push(spatialElement(def));
70
+ out.push(`</xs:schema>`);
71
+ out.push('');
72
+ return out.join('\n');
73
+ }
74
+ function prefabElement() {
75
+ return [
76
+ ` <xs:element name="PrefabInstance"> <!-- &lt;PrefabInstance src="x.prefab.xml" id="t1" …/&gt; -->`,
77
+ ` <xs:complexType>`,
78
+ ` <xs:choice minOccurs="0" maxOccurs="unbounded">`,
79
+ ` <xs:element ref="Override"/>`,
80
+ ` <xs:element ref="Components"/>`,
81
+ ` </xs:choice>`,
82
+ ` <xs:attribute name="src" type="xs:string" use="required"/> <!-- the .prefab.xml file -->`,
83
+ ` <xs:attributeGroup ref="transform"/> <!-- incl. id (required by the validator) -->`,
84
+ ` </xs:complexType>`,
85
+ ` </xs:element>`,
86
+ ].join('\n');
87
+ }
88
+ function overrideElement() {
89
+ return [
90
+ ` <xs:element name="Override"> <!-- patch a prefab-internal node/component by id -->`,
91
+ ` <xs:complexType>`,
92
+ ` <xs:attribute name="target" type="ref" use="required"/> <!-- #&lt;prefab-local id&gt; -->`,
93
+ ` <xs:anyAttribute processContents="skip"/>`,
94
+ ` </xs:complexType>`,
95
+ ` </xs:element>`,
96
+ ].join('\n');
97
+ }
98
+ // ---- element builders -----------------------------------------------------
99
+ function spatialElement(def) {
100
+ const lines = [];
101
+ lines.push(` <xs:element name="${def.tag}">${comment(def.description)}`);
102
+ lines.push(` <xs:complexType>`);
103
+ const childRefs = [];
104
+ // Attachments this node accepts (geometry/material/fog/…).
105
+ for (const att of attachmentsForNode(def))
106
+ childRefs.push(`<xs:element ref="${att.tag}"/>`);
107
+ if (def.children.object3d)
108
+ childRefs.push(`<xs:group ref="object3dNodes"/>`);
109
+ if (def.children.components)
110
+ childRefs.push(`<xs:element ref="Components"/>`);
111
+ if (childRefs.length > 0) {
112
+ lines.push(` <xs:choice minOccurs="0" maxOccurs="unbounded">`);
113
+ for (const ref of childRefs)
114
+ lines.push(` ${ref}`);
115
+ lines.push(` </xs:choice>`);
116
+ }
117
+ lines.push(` <xs:attributeGroup ref="transform"/>`);
118
+ for (const [name, meta] of Object.entries(def.attrs))
119
+ lines.push(` ${nodeAttr(name, meta)}`);
120
+ lines.push(` </xs:complexType>`);
121
+ lines.push(` </xs:element>`);
122
+ return lines.join('\n');
123
+ }
124
+ function attachmentElement(def) {
125
+ const attrs = Object.entries(def.attrs).map(([name, meta]) => ` ${nodeAttr(name, meta)}`);
126
+ // Any attachment may pull defaults from a resource file via src= (e.g. .material.xml).
127
+ attrs.unshift(` <xs:attribute name="src" type="xs:string"/> <!-- load defaults from a resource file -->`);
128
+ return [
129
+ ` <xs:element name="${def.tag}">${comment(def.description)}`,
130
+ ` <xs:complexType>`,
131
+ ...attrs,
132
+ ` </xs:complexType>`,
133
+ ` </xs:element>`,
134
+ ].join('\n');
135
+ }
136
+ function componentElement(name) {
137
+ const meta = Registry.getComponent(name).meta;
138
+ const attrs = [` <xs:attribute name="id" type="xs:string"/>`];
139
+ for (const [attr, prop] of meta.properties)
140
+ attrs.push(` ${propertyAttr(attr, prop)}`);
141
+ return [
142
+ ` <xs:element name="${name}">${comment(meta.description)}`,
143
+ ` <xs:complexType>`,
144
+ ...attrs,
145
+ ` </xs:complexType>`,
146
+ ` </xs:element>`,
147
+ ].join('\n');
148
+ }
149
+ // ---- attribute / type helpers ---------------------------------------------
150
+ function nodeAttr(name, meta) {
151
+ const use = meta.required ? ` use="required"` : '';
152
+ return `<xs:attribute name="${name}" type="${xsdType(meta.type)}"${use}/>${comment(meta.description)}`;
153
+ }
154
+ function propertyAttr(name, meta) {
155
+ const use = meta.required ? ` use="required"` : '';
156
+ return `<xs:attribute name="${name}" type="${propertyXsdType(meta.type)}"${use}/>${comment(meta.description)}`;
157
+ }
158
+ /** XSD attribute type for a component property. */
159
+ function propertyXsdType(type) {
160
+ // A prefab prop is a file path; 'node' and Component-class refs are written "#id".
161
+ if (type === 'prefab')
162
+ return 'xs:string';
163
+ if (typeof type === 'string' && type !== 'node')
164
+ return xsdType(type);
165
+ return 'ref';
166
+ }
167
+ /** Map a scalar type to its XSD type name (named simpleType or xs: builtin). */
168
+ function xsdType(type) {
169
+ switch (type) {
170
+ case 'vec2':
171
+ case 'vec3':
172
+ case 'euler':
173
+ case 'color':
174
+ return type;
175
+ default:
176
+ return codecs[type].xsd.base;
177
+ }
178
+ }
179
+ function simpleType(name, pattern) {
180
+ return [
181
+ ` <xs:simpleType name="${name}">`,
182
+ ` <xs:restriction base="xs:string">`,
183
+ ` <xs:pattern value="${escapeXml(pattern)}"/>`,
184
+ ` </xs:restriction>`,
185
+ ` </xs:simpleType>`,
186
+ ].join('\n');
187
+ }
188
+ function choiceGroup(name, tags) {
189
+ const refs = tags.map((t) => ` <xs:element ref="${t}"/>`);
190
+ return [` <xs:group name="${name}">`, ` <xs:choice>`, ...refs, ` </xs:choice>`, ` </xs:group>`].join('\n');
191
+ }
192
+ const comment = (text) => (text ? ` <!-- ${escapeXml(text)} -->` : '');
193
+ function escapeXml(s) {
194
+ return s
195
+ .replace(/&/g, '&amp;')
196
+ .replace(/</g, '&lt;')
197
+ .replace(/>/g, '&gt;')
198
+ .replace(/"/g, '&quot;');
199
+ }
@@ -0,0 +1,15 @@
1
+ import '@scenoco-three/core/nodes';
2
+ export { parseScene, type SceElement, type ParseResult } from './dsl/SceneParser.js';
3
+ export { validateScene } from './dsl/Validator.js';
4
+ export { compileSceneXml, compilePrefabXml, type CompileResult, type CompileOptions, type ResourceResolver, type ResolvedFile, } from './dsl/compile.js';
5
+ export { bundleScene, bundlePrefab } from './dsl/bundle.js';
6
+ export { serializeScene } from './dsl/serialize.js';
7
+ export { watchSceneFiles, type WatchOptions, type WatchHandle, type WatchEvent, } from './watch.js';
8
+ export { scanComponents, loadComponents, type ComponentScan, type LoadOptions } from './components.js';
9
+ export { scaffoldFiles } from './cli/templates.js';
10
+ export { builtinNodeModules, builtinNodeModule } from '@scenoco-three/core/internal';
11
+ export { generateXsd } from './exporters/xsd.js';
12
+ export { generateDocs, type DocsOptions } from './exporters/docs.js';
13
+ export { formatDiagnostic, type SceDiagnostic } from './dsl/diagnostics.js';
14
+ export { codecs, type ScalarType, type TypeCodec } from './dsl/types.js';
15
+ export type { CompiledScene, CompiledNode, CompiledComponent, CompiledAsset, CompiledRef, CompiledValue, Bundle, } from '@scenoco-three/core/internal';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // `@scenoco-three/compiler` — the BUILD-TIME entry point: XML → validated → CompiledScene
2
+ // → optimized Bundle, plus the XSD/docs exporters. This is where the XML parser
3
+ // (saxes), validator, and compiler live; it never ships to the browser runtime.
4
+ //
5
+ // Build-time tooling needs the full built-in node set registered so validation,
6
+ // compilation and exporters see every tag. The runtime entry no longer pulls in
7
+ // node modules (for tree-shaking), so the compiler imports the barrel explicitly.
8
+ // This is the Node process only; it never reaches the browser bundle.
9
+ import '@scenoco-three/core/nodes';
10
+ export { parseScene } from './dsl/SceneParser.js';
11
+ export { validateScene } from './dsl/Validator.js';
12
+ export { compileSceneXml, compilePrefabXml, } from './dsl/compile.js';
13
+ export { bundleScene, bundlePrefab } from './dsl/bundle.js';
14
+ export { serializeScene } from './dsl/serialize.js';
15
+ export { watchSceneFiles, } from './watch.js';
16
+ export { scanComponents, loadComponents } from './components.js';
17
+ export { scaffoldFiles } from './cli/templates.js';
18
+ // Built-in node tag → package subpath that registers it, for usage-based shipping.
19
+ export { builtinNodeModules, builtinNodeModule } from '@scenoco-three/core/internal';
20
+ export { generateXsd } from './exporters/xsd.js';
21
+ export { generateDocs } from './exporters/docs.js';
22
+ export { formatDiagnostic } from './dsl/diagnostics.js';
23
+ export { codecs } from './dsl/types.js';
@@ -0,0 +1,33 @@
1
+ import { type ResourceResolver } from './dsl/compile.js';
2
+ import type { SceDiagnostic } from './dsl/diagnostics.js';
3
+ export type WatchEvent = {
4
+ type: 'start';
5
+ file: string;
6
+ } | {
7
+ type: 'success';
8
+ file: string;
9
+ outputPath: string;
10
+ durationMs: number;
11
+ } | {
12
+ type: 'diagnostics';
13
+ file: string;
14
+ diagnostics: SceDiagnostic[];
15
+ } | {
16
+ type: 'error';
17
+ file: string;
18
+ message: string;
19
+ };
20
+ export interface WatchOptions {
21
+ outDir?: string;
22
+ mode?: 'bundle' | 'compiled';
23
+ resolve?: ResourceResolver;
24
+ onEvent?: (event: WatchEvent) => void;
25
+ }
26
+ export interface WatchHandle {
27
+ close(): Promise<void>;
28
+ }
29
+ /**
30
+ * Watches scene XML files and writes updated outputs on every change.
31
+ * Also follows `src=` dependencies discovered during compilation.
32
+ */
33
+ export declare function watchSceneFiles(entries: string[], options?: WatchOptions): Promise<WatchHandle>;
package/dist/watch.js ADDED
@@ -0,0 +1,112 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import chokidar, {} from 'chokidar';
4
+ import { bundlePrefab, bundleScene } from './dsl/bundle.js';
5
+ import { compilePrefabXml, compileSceneXml } from './dsl/compile.js';
6
+ /**
7
+ * Watches scene XML files and writes updated outputs on every change.
8
+ * Also follows `src=` dependencies discovered during compilation.
9
+ */
10
+ export async function watchSceneFiles(entries, options = {}) {
11
+ const roots = entries.map((f) => resolve(f));
12
+ const outDir = options.outDir ? resolve(options.outDir) : undefined;
13
+ const mode = options.mode ?? 'bundle';
14
+ const onEvent = options.onEvent ?? (() => { });
15
+ const depToRoots = new Map();
16
+ const rootToDeps = new Map();
17
+ let watcher;
18
+ const updateDeps = (rootFile, deps) => {
19
+ const prev = rootToDeps.get(rootFile);
20
+ if (prev) {
21
+ for (const dep of prev) {
22
+ const owners = depToRoots.get(dep);
23
+ if (!owners)
24
+ continue;
25
+ owners.delete(rootFile);
26
+ if (owners.size === 0)
27
+ depToRoots.delete(dep);
28
+ }
29
+ }
30
+ rootToDeps.set(rootFile, deps);
31
+ for (const dep of deps) {
32
+ const owners = depToRoots.get(dep) ?? new Set();
33
+ owners.add(rootFile);
34
+ depToRoots.set(dep, owners);
35
+ }
36
+ };
37
+ const compileOne = (file) => {
38
+ const start = Date.now();
39
+ onEvent({ type: 'start', file });
40
+ const deps = new Set();
41
+ const resolver = createTrackingResolver(file, deps, options.resolve);
42
+ const isPrefab = file.endsWith('.prefab.xml');
43
+ const compile = isPrefab ? compilePrefabXml : compileSceneXml;
44
+ const result = compile(readFileSync(file, 'utf8'), {
45
+ path: file,
46
+ resolve: resolver,
47
+ });
48
+ updateDeps(file, deps);
49
+ watcher.add([...deps]);
50
+ if (!result.ok) {
51
+ onEvent({ type: 'diagnostics', file, diagnostics: result.diagnostics });
52
+ return;
53
+ }
54
+ const outputPath = outputFileFor(file, mode, outDir);
55
+ const payload = mode === 'bundle'
56
+ ? isPrefab
57
+ ? bundlePrefab(result.scene)
58
+ : bundleScene(result.scene)
59
+ : result.scene;
60
+ try {
61
+ mkdirSync(dirname(outputPath), { recursive: true });
62
+ writeFileSync(outputPath, JSON.stringify(payload));
63
+ }
64
+ catch (e) {
65
+ onEvent({ type: 'error', file, message: e instanceof Error ? e.message : String(e) });
66
+ return;
67
+ }
68
+ onEvent({ type: 'success', file, outputPath, durationMs: Date.now() - start });
69
+ };
70
+ watcher = chokidar.watch(roots, { ignoreInitial: false, awaitWriteFinish: true });
71
+ const onChange = (changedPath) => {
72
+ const abs = resolve(changedPath);
73
+ if (roots.includes(abs)) {
74
+ compileOne(abs);
75
+ return;
76
+ }
77
+ const dependents = depToRoots.get(abs);
78
+ if (!dependents)
79
+ return;
80
+ for (const rootFile of dependents)
81
+ compileOne(rootFile);
82
+ };
83
+ watcher.on('add', onChange);
84
+ watcher.on('change', onChange);
85
+ return {
86
+ async close() {
87
+ await watcher.close();
88
+ },
89
+ };
90
+ }
91
+ function outputFileFor(file, mode, outDir) {
92
+ const baseName = `${file}${mode === 'bundle' ? '.bundle.json' : '.json'}`;
93
+ if (!outDir)
94
+ return baseName;
95
+ const leaf = baseName.split('/').pop() ?? baseName;
96
+ return resolve(outDir, leaf);
97
+ }
98
+ function createTrackingResolver(rootFile, deps, fallback) {
99
+ return (request, referrer) => {
100
+ if (fallback) {
101
+ const resolved = fallback(request, referrer);
102
+ if (resolved)
103
+ deps.add(resolve(resolved.path));
104
+ return resolved;
105
+ }
106
+ const target = resolve(referrer ? dirname(referrer) : dirname(rootFile), request);
107
+ if (!existsSync(target))
108
+ return null;
109
+ deps.add(target);
110
+ return { path: target, contents: readFileSync(target, 'utf8') };
111
+ };
112
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@scenoco-three/compiler",
3
+ "version": "0.1.0",
4
+ "description": "SceNoCo build-time compiler, CLI and watcher",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "scenoco": "./dist/cli/index.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "keywords": ["three", "threejs", "scene", "components", "ecs", "xml", "dsl", "compiler", "gamedev"],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "typecheck": "npm --prefix ../core run build && tsc --noEmit -p tsconfig.json"
28
+ },
29
+ "dependencies": {
30
+ "@scenoco-three/core": "^0.1.0",
31
+ "chokidar": "^4.0.3",
32
+ "esbuild": "^0.25.12",
33
+ "saxes": "^6.0.0"
34
+ }
35
+ }