@aelionsdk/material-sdk 0.1.0-beta.1 → 1.1.0-rc.1
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.
- package/README.md +51 -4
- package/dist/cli-lib.d.ts +34 -0
- package/dist/cli-lib.d.ts.map +1 -0
- package/dist/cli-lib.js +367 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +57 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@aelionsdk/material-sdk`
|
|
2
2
|
|
|
3
|
-
Type-safe authoring, validation and
|
|
3
|
+
Type-safe authoring, validation, packaging, signing and trust tools for Aelion
|
|
4
|
+
Material Protocol v1.
|
|
4
5
|
|
|
5
|
-
Install
|
|
6
|
+
## Install
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
```bash
|
|
9
|
+
npm install @aelionsdk/material-sdk@next
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Author CLI
|
|
13
|
+
|
|
14
|
+
The package installs `aelion-material`:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm exec aelion-material init ./my-pack
|
|
18
|
+
pnpm exec aelion-material build ./my-pack
|
|
19
|
+
pnpm exec aelion-material validate ./my-pack
|
|
20
|
+
pnpm exec aelion-material types ./my-pack
|
|
21
|
+
pnpm exec aelion-material preview ./my-pack
|
|
22
|
+
pnpm exec aelion-material prepublish ./my-pack
|
|
23
|
+
pnpm exec aelion-material pack ./my-pack --out ./dist/my-pack.aelionmat
|
|
24
|
+
pnpm exec aelion-material golden actual.rgba expected.rgba --tolerance 2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`build` refreshes declared payload sizes/hashes and generates types plus the
|
|
28
|
+
preview report. `prepublish` requires valid payload hashes and schemas, no error diagnostics,
|
|
29
|
+
strict determinism, and WebGL2/WebGPU compiler parity for declarative Graph
|
|
30
|
+
materials. The `.aelionmat` archive is deterministic.
|
|
31
|
+
|
|
32
|
+
Signing keys are deliberately not read by the generic CLI. A host that owns a
|
|
33
|
+
publisher key should obtain a `CryptoKey` from its own key system and call
|
|
34
|
+
`signMaterialPackage()` explicitly.
|
|
35
|
+
|
|
36
|
+
Node build systems can import the same operations from
|
|
37
|
+
`@aelionsdk/material-sdk/author-cli`.
|
|
38
|
+
|
|
39
|
+
## TypeScript API
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import {
|
|
43
|
+
MaterialLabSession,
|
|
44
|
+
materialDefinition,
|
|
45
|
+
materialGraph,
|
|
46
|
+
packMaterialPackage,
|
|
47
|
+
} from '@aelionsdk/material-sdk';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See the [Material guide](https://foyonaczy.github.io/AelionSDK/guides/materials/)
|
|
51
|
+
and [AMP v1 reference](https://foyonaczy.github.io/AelionSDK/reference/material-protocol-v1/).
|
|
52
|
+
|
|
53
|
+
Version `1.1.0-rc.1` is a prerelease. Public changes before 1.0 are recorded
|
|
54
|
+
in the repository changelog and migration documentation. Licensed under MIT.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MaterialLabSession, compareMaterialGolden } from './lab.js';
|
|
2
|
+
export interface MaterialAuthorInspection {
|
|
3
|
+
readonly directory: string;
|
|
4
|
+
readonly packageId: string;
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly integrity: string;
|
|
7
|
+
readonly files: number;
|
|
8
|
+
readonly materials: readonly {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
readonly parameters: number;
|
|
12
|
+
readonly webgl2: boolean;
|
|
13
|
+
readonly webgpu: boolean;
|
|
14
|
+
readonly budget: ReturnType<MaterialLabSession['analyze']>['budget'];
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export declare function validateMaterialAuthorPackage(directory: string): Promise<MaterialAuthorInspection>;
|
|
18
|
+
export declare function synchronizeMaterialAuthorManifest(directory: string): Promise<string>;
|
|
19
|
+
export declare function generateMaterialTypes(directory: string, outputPath?: string): Promise<string>;
|
|
20
|
+
export declare function writeMaterialPreviewReport(directory: string, outputPath?: string): Promise<string>;
|
|
21
|
+
export declare function packMaterialAuthorPackage(directory: string, outputPath: string): Promise<{
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly integrity: string;
|
|
24
|
+
readonly bytes: number;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function buildMaterialAuthorPackage(directory: string): Promise<{
|
|
27
|
+
readonly inspection: MaterialAuthorInspection;
|
|
28
|
+
readonly typesPath: string;
|
|
29
|
+
readonly previewPath: string;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function compareMaterialGoldenFiles(actualPath: string, expectedPath: string, tolerance?: number): Promise<ReturnType<typeof compareMaterialGolden>>;
|
|
32
|
+
export declare function prepublishMaterialAuthorPackage(directory: string): Promise<MaterialAuthorInspection>;
|
|
33
|
+
export declare function initializeMaterialAuthorPackage(directory: string): Promise<string>;
|
|
34
|
+
//# sourceMappingURL=cli-lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-lib.d.ts","sourceRoot":"","sources":["../src/cli-lib.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAmBrE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS;QAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACtE,EAAE,CAAC;CACL;AAsID,wBAAsB,6BAA6B,CACjD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,wBAAwB,CAAC,CAEnC;AAED,wBAAsB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1F;AAYD,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,SAAgD,GACzD,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,EACjB,UAAU,SAA8C,GACvD,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAUxF;AAED,wBAAsB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3E,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC,CAQD;AAED,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,SAAS,SAAI,GACZ,OAAO,CAAC,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAGnD;AAED,wBAAsB,+BAA+B,CACnD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,wBAAwB,CAAC,CAenC;AAED,wBAAsB,+BAA+B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA6HxF"}
|
package/dist/cli-lib.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
import { canonicalMaterialBytes, sha256Hex } from './canonical.js';
|
|
4
|
+
import { MaterialLabSession, compareMaterialGolden } from './lab.js';
|
|
5
|
+
import { packMaterialPackage, verifyMaterialPackage } from './package.js';
|
|
6
|
+
import { assertMaterialDefinitionSchema, assertMaterialGraphSchema, assertMaterialManifestSchema, } from './schema-validation.js';
|
|
7
|
+
import { createDeterministicMaterialArchive } from './zip.js';
|
|
8
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
function safePath(root, path) {
|
|
11
|
+
const absolute = resolve(root, path);
|
|
12
|
+
const fromRoot = relative(root, absolute).replaceAll('\\', '/');
|
|
13
|
+
if (fromRoot.length === 0 ||
|
|
14
|
+
fromRoot === '..' ||
|
|
15
|
+
fromRoot.startsWith('../') ||
|
|
16
|
+
fromRoot.startsWith('/') ||
|
|
17
|
+
path.includes('\\')) {
|
|
18
|
+
throw new TypeError(`Unsafe Material path ${path}`);
|
|
19
|
+
}
|
|
20
|
+
return absolute;
|
|
21
|
+
}
|
|
22
|
+
function json(bytes, path) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(decoder.decode(bytes));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new TypeError(`Invalid JSON in ${path}`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function exists(path) {
|
|
31
|
+
try {
|
|
32
|
+
await access(path);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function loadAuthorPackage(directory) {
|
|
40
|
+
const root = resolve(directory);
|
|
41
|
+
const manifestPath = resolve(root, 'manifest.json');
|
|
42
|
+
const manifestSource = new Uint8Array(await readFile(manifestPath));
|
|
43
|
+
const manifestValue = json(manifestSource, manifestPath);
|
|
44
|
+
assertMaterialManifestSchema(manifestValue);
|
|
45
|
+
const manifest = manifestValue;
|
|
46
|
+
const manifestBytes = canonicalMaterialBytes(manifest);
|
|
47
|
+
const files = new Map([['manifest.json', manifestBytes]]);
|
|
48
|
+
for (const entry of manifest.files) {
|
|
49
|
+
const bytes = new Uint8Array(await readFile(safePath(root, entry.path)));
|
|
50
|
+
files.set(entry.path, bytes);
|
|
51
|
+
}
|
|
52
|
+
const archiveBytes = createDeterministicMaterialArchive(files);
|
|
53
|
+
const integrity = `sha256:${await sha256Hex(manifestBytes)}`;
|
|
54
|
+
const packed = {
|
|
55
|
+
manifest,
|
|
56
|
+
manifestBytes,
|
|
57
|
+
files,
|
|
58
|
+
archiveBytes,
|
|
59
|
+
integrity,
|
|
60
|
+
};
|
|
61
|
+
await verifyMaterialPackage(packed, integrity);
|
|
62
|
+
const definitionPaths = new Set(manifest.materials.map(material => material.definition));
|
|
63
|
+
const graphPaths = new Set();
|
|
64
|
+
const authored = manifest.materials.map(material => {
|
|
65
|
+
const definitionBytes = files.get(material.definition);
|
|
66
|
+
if (definitionBytes === undefined) {
|
|
67
|
+
throw new TypeError(`Definition ${material.definition} is missing`);
|
|
68
|
+
}
|
|
69
|
+
const definitionValue = json(definitionBytes, material.definition);
|
|
70
|
+
assertMaterialDefinitionSchema(definitionValue);
|
|
71
|
+
const definition = definitionValue;
|
|
72
|
+
if (definition.id !== material.id || definition.kind !== material.kind) {
|
|
73
|
+
throw new TypeError(`Definition identity differs for ${material.id}`);
|
|
74
|
+
}
|
|
75
|
+
const graphImplementation = definition.implementations.find(value => value.type === 'graph');
|
|
76
|
+
if (graphImplementation === undefined)
|
|
77
|
+
return { definition };
|
|
78
|
+
graphPaths.add(graphImplementation.graph);
|
|
79
|
+
const graphBytes = files.get(graphImplementation.graph);
|
|
80
|
+
if (graphBytes === undefined) {
|
|
81
|
+
throw new TypeError(`Graph ${graphImplementation.graph} is missing`);
|
|
82
|
+
}
|
|
83
|
+
const graphValue = json(graphBytes, graphImplementation.graph);
|
|
84
|
+
assertMaterialGraphSchema(graphValue);
|
|
85
|
+
return {
|
|
86
|
+
definition,
|
|
87
|
+
graph: graphValue,
|
|
88
|
+
definitionPath: material.definition,
|
|
89
|
+
graphPath: graphImplementation.graph,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const extraFiles = manifest.files.flatMap(entry => {
|
|
93
|
+
if (definitionPaths.has(entry.path) || graphPaths.has(entry.path))
|
|
94
|
+
return [];
|
|
95
|
+
const data = files.get(entry.path);
|
|
96
|
+
if (data === undefined)
|
|
97
|
+
throw new TypeError(`Package file ${entry.path} is missing`);
|
|
98
|
+
return [{ path: entry.path, mediaType: entry.mediaType, data }];
|
|
99
|
+
});
|
|
100
|
+
// Re-run the same authoring validation/compilation path used by package
|
|
101
|
+
// creation. The stored manifest verification above remains authoritative for
|
|
102
|
+
// exact payload bytes and transport integrity.
|
|
103
|
+
await packMaterialPackage({
|
|
104
|
+
metadata: manifest.package,
|
|
105
|
+
materials: authored,
|
|
106
|
+
files: extraFiles,
|
|
107
|
+
});
|
|
108
|
+
return { directory: root, manifest, packed, authored, extraFiles };
|
|
109
|
+
}
|
|
110
|
+
function inspection(loaded) {
|
|
111
|
+
return {
|
|
112
|
+
directory: loaded.directory,
|
|
113
|
+
packageId: loaded.manifest.package.id,
|
|
114
|
+
version: loaded.manifest.package.version,
|
|
115
|
+
integrity: loaded.packed.integrity,
|
|
116
|
+
files: loaded.manifest.files.length + 1,
|
|
117
|
+
materials: loaded.authored.map(material => {
|
|
118
|
+
const report = new MaterialLabSession(material).analyze();
|
|
119
|
+
return {
|
|
120
|
+
id: material.definition.id,
|
|
121
|
+
kind: material.definition.kind,
|
|
122
|
+
parameters: material.definition.parameters.length,
|
|
123
|
+
webgl2: report.webgl2.available,
|
|
124
|
+
webgpu: report.webgpu.available,
|
|
125
|
+
budget: report.budget,
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function validateMaterialAuthorPackage(directory) {
|
|
131
|
+
return inspection(await loadAuthorPackage(directory));
|
|
132
|
+
}
|
|
133
|
+
export async function synchronizeMaterialAuthorManifest(directory) {
|
|
134
|
+
const root = resolve(directory);
|
|
135
|
+
const manifestPath = resolve(root, 'manifest.json');
|
|
136
|
+
const manifestValue = json(new Uint8Array(await readFile(manifestPath)), manifestPath);
|
|
137
|
+
assertMaterialManifestSchema(manifestValue);
|
|
138
|
+
const manifest = manifestValue;
|
|
139
|
+
const files = await Promise.all(manifest.files.map(async (entry) => {
|
|
140
|
+
const bytes = new Uint8Array(await readFile(safePath(root, entry.path)));
|
|
141
|
+
return {
|
|
142
|
+
...entry,
|
|
143
|
+
bytes: bytes.byteLength,
|
|
144
|
+
sha256: await sha256Hex(bytes),
|
|
145
|
+
};
|
|
146
|
+
}));
|
|
147
|
+
await writeFile(manifestPath, `${JSON.stringify({ ...manifest, files }, null, 2)}\n`, 'utf8');
|
|
148
|
+
return manifestPath;
|
|
149
|
+
}
|
|
150
|
+
function parameterType(type) {
|
|
151
|
+
if (type === 'boolean')
|
|
152
|
+
return 'boolean';
|
|
153
|
+
if (type === 'enum' || type === 'string')
|
|
154
|
+
return 'string';
|
|
155
|
+
if (['integer', 'float', 'angle', 'duration'].includes(type))
|
|
156
|
+
return 'number';
|
|
157
|
+
if (type === 'vec2')
|
|
158
|
+
return 'readonly [number, number]';
|
|
159
|
+
if (type === 'vec3')
|
|
160
|
+
return 'readonly [number, number, number]';
|
|
161
|
+
if (type === 'vec4' || type === 'color')
|
|
162
|
+
return 'readonly [number, number, number, number]';
|
|
163
|
+
return 'JsonValue';
|
|
164
|
+
}
|
|
165
|
+
export async function generateMaterialTypes(directory, outputPath = resolve(directory, 'material.generated.d.ts')) {
|
|
166
|
+
const loaded = await loadAuthorPackage(directory);
|
|
167
|
+
const needsJsonValue = loaded.authored.some(material => material.definition.parameters.some(parameter => ['gradient', 'curve'].includes(parameter.type)));
|
|
168
|
+
const sections = loaded.authored.map(material => {
|
|
169
|
+
const name = `${material.definition.id
|
|
170
|
+
.split(/[^A-Za-z0-9]+/u)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map(part => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
|
|
173
|
+
.join('')}Parameters`;
|
|
174
|
+
const members = material.definition.parameters.map(parameter => ` readonly ${JSON.stringify(parameter.id)}: ${parameterType(parameter.type)};`);
|
|
175
|
+
return `export interface ${name} {\n${members.join('\n')}\n}`;
|
|
176
|
+
});
|
|
177
|
+
const source = `${needsJsonValue ? "import type { JsonValue } from '@aelionsdk/core';\n\n" : ''}${sections.join('\n\n')}\n`;
|
|
178
|
+
await mkdir(dirname(resolve(outputPath)), { recursive: true });
|
|
179
|
+
await writeFile(resolve(outputPath), source, 'utf8');
|
|
180
|
+
return resolve(outputPath);
|
|
181
|
+
}
|
|
182
|
+
export async function writeMaterialPreviewReport(directory, outputPath = resolve(directory, 'material-preview.json')) {
|
|
183
|
+
const loaded = await loadAuthorPackage(directory);
|
|
184
|
+
const report = {
|
|
185
|
+
reportVersion: '1.0.0',
|
|
186
|
+
package: {
|
|
187
|
+
id: loaded.manifest.package.id,
|
|
188
|
+
version: loaded.manifest.package.version,
|
|
189
|
+
integrity: loaded.packed.integrity,
|
|
190
|
+
},
|
|
191
|
+
materials: loaded.authored.map(material => ({
|
|
192
|
+
id: material.definition.id,
|
|
193
|
+
display: material.definition.display,
|
|
194
|
+
execution: material.definition.execution,
|
|
195
|
+
analysis: new MaterialLabSession(material).analyze(),
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
await mkdir(dirname(resolve(outputPath)), { recursive: true });
|
|
199
|
+
await writeFile(resolve(outputPath), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
200
|
+
return resolve(outputPath);
|
|
201
|
+
}
|
|
202
|
+
export async function packMaterialAuthorPackage(directory, outputPath) {
|
|
203
|
+
await synchronizeMaterialAuthorManifest(directory);
|
|
204
|
+
const loaded = await loadAuthorPackage(directory);
|
|
205
|
+
await mkdir(dirname(resolve(outputPath)), { recursive: true });
|
|
206
|
+
await writeFile(resolve(outputPath), loaded.packed.archiveBytes);
|
|
207
|
+
return {
|
|
208
|
+
path: resolve(outputPath),
|
|
209
|
+
integrity: loaded.packed.integrity,
|
|
210
|
+
bytes: loaded.packed.archiveBytes.byteLength,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export async function buildMaterialAuthorPackage(directory) {
|
|
214
|
+
await synchronizeMaterialAuthorManifest(directory);
|
|
215
|
+
const inspection = await validateMaterialAuthorPackage(directory);
|
|
216
|
+
const [typesPath, previewPath] = await Promise.all([
|
|
217
|
+
generateMaterialTypes(directory),
|
|
218
|
+
writeMaterialPreviewReport(directory),
|
|
219
|
+
]);
|
|
220
|
+
return { inspection, typesPath, previewPath };
|
|
221
|
+
}
|
|
222
|
+
export async function compareMaterialGoldenFiles(actualPath, expectedPath, tolerance = 2) {
|
|
223
|
+
const [actual, expected] = await Promise.all([readFile(actualPath), readFile(expectedPath)]);
|
|
224
|
+
return compareMaterialGolden(actual, expected, tolerance);
|
|
225
|
+
}
|
|
226
|
+
export async function prepublishMaterialAuthorPackage(directory) {
|
|
227
|
+
const loaded = await loadAuthorPackage(directory);
|
|
228
|
+
for (const material of loaded.authored) {
|
|
229
|
+
const report = new MaterialLabSession(material).analyze();
|
|
230
|
+
if (report.diagnostics.some(value => value.severity === 'error')) {
|
|
231
|
+
throw new TypeError(`Material ${material.definition.id} has error diagnostics`);
|
|
232
|
+
}
|
|
233
|
+
if (material.graph !== undefined && (!report.webgl2.available || !report.webgpu.available)) {
|
|
234
|
+
throw new TypeError(`Material ${material.definition.id} lacks WebGL2/WebGPU compiler parity`);
|
|
235
|
+
}
|
|
236
|
+
if (material.definition.execution.determinism === 'non-deterministic') {
|
|
237
|
+
throw new TypeError(`Material ${material.definition.id} is non-deterministic`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return inspection(loaded);
|
|
241
|
+
}
|
|
242
|
+
export async function initializeMaterialAuthorPackage(directory) {
|
|
243
|
+
const root = resolve(directory);
|
|
244
|
+
if ((await exists(root)) && (await readdir(root)).length > 0) {
|
|
245
|
+
throw new TypeError(`Material directory is not empty: ${root}`);
|
|
246
|
+
}
|
|
247
|
+
const definition = {
|
|
248
|
+
$schema: 'https://schemas.aelion.dev/material/definition/v1.json',
|
|
249
|
+
protocolVersion: '1.0.0',
|
|
250
|
+
id: 'starter-filter',
|
|
251
|
+
kind: 'visual-filter',
|
|
252
|
+
display: { name: 'Starter Filter', category: 'starter' },
|
|
253
|
+
scopes: ['source', 'item', 'track', 'sequence'],
|
|
254
|
+
ports: [
|
|
255
|
+
{
|
|
256
|
+
id: 'source',
|
|
257
|
+
direction: 'input',
|
|
258
|
+
type: 'visual-frame',
|
|
259
|
+
role: 'source',
|
|
260
|
+
binding: 'host',
|
|
261
|
+
required: true,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'result',
|
|
265
|
+
direction: 'output',
|
|
266
|
+
type: 'visual-frame',
|
|
267
|
+
role: 'result',
|
|
268
|
+
binding: 'host',
|
|
269
|
+
required: true,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
parameters: [
|
|
273
|
+
{
|
|
274
|
+
id: 'intensity',
|
|
275
|
+
type: 'float',
|
|
276
|
+
default: 1,
|
|
277
|
+
range: { min: 0, max: 1, step: 0.01 },
|
|
278
|
+
unit: 'ratio',
|
|
279
|
+
animatable: true,
|
|
280
|
+
interpolation: 'linear',
|
|
281
|
+
affects: 'uniform',
|
|
282
|
+
ui: { control: 'slider', group: 'main', order: 0, label: 'Intensity' },
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
bundledResources: [],
|
|
286
|
+
resourceSlots: [],
|
|
287
|
+
execution: {
|
|
288
|
+
color: { input: 'working-linear', output: 'working-linear' },
|
|
289
|
+
alpha: {
|
|
290
|
+
input: 'premultiplied',
|
|
291
|
+
output: 'premultiplied',
|
|
292
|
+
preservesTransparency: true,
|
|
293
|
+
},
|
|
294
|
+
resolution: { policy: 'same-as-host' },
|
|
295
|
+
spatialPadding: { mode: 'none' },
|
|
296
|
+
temporal: { pastUs: 0, futureUs: 0, stateful: false, seekPolicy: 'stateless' },
|
|
297
|
+
determinism: 'strict',
|
|
298
|
+
supports: { realtime: true, offline: true, alpha: true, hdr: false, tiled: true },
|
|
299
|
+
},
|
|
300
|
+
implementations: [
|
|
301
|
+
{
|
|
302
|
+
type: 'graph',
|
|
303
|
+
graph: 'graphs/starter-filter.graph.json',
|
|
304
|
+
nodeSet: 'aelion.visual.nodes/1.0.0',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
splitPolicy: 'copy',
|
|
308
|
+
};
|
|
309
|
+
const graph = {
|
|
310
|
+
$schema: 'https://schemas.aelion.dev/material/graph/v1.json',
|
|
311
|
+
graphVersion: '1.0.0',
|
|
312
|
+
nodeSet: 'aelion.visual.nodes/1.0.0',
|
|
313
|
+
nodes: [
|
|
314
|
+
{
|
|
315
|
+
id: 'mix',
|
|
316
|
+
type: 'composite.mix',
|
|
317
|
+
typeVersion: '1.0.0',
|
|
318
|
+
inputs: {
|
|
319
|
+
a: { inputPort: 'source' },
|
|
320
|
+
b: { inputPort: 'source' },
|
|
321
|
+
amount: { parameter: 'intensity' },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
outputs: { result: { node: 'mix', output: 'frame' } },
|
|
326
|
+
};
|
|
327
|
+
const definitionPath = 'materials/starter-filter.material.json';
|
|
328
|
+
const graphPath = 'graphs/starter-filter.graph.json';
|
|
329
|
+
const definitionSource = `${JSON.stringify(definition, null, 2)}\n`;
|
|
330
|
+
const graphSource = `${JSON.stringify(graph, null, 2)}\n`;
|
|
331
|
+
const manifest = {
|
|
332
|
+
$schema: 'https://schemas.aelion.dev/material/package/v1.json',
|
|
333
|
+
protocolVersion: '1.0.0',
|
|
334
|
+
package: {
|
|
335
|
+
id: 'dev.example.starter',
|
|
336
|
+
version: '0.1.0',
|
|
337
|
+
displayName: 'Starter Material',
|
|
338
|
+
publisher: { id: 'dev.example', name: 'Example Publisher' },
|
|
339
|
+
license: 'MIT',
|
|
340
|
+
engines: { aelion: '>=1.0.0-rc.1 <2.0.0', nodeSet: 'aelion.visual.nodes/1.0.0' },
|
|
341
|
+
trust: 'declarative',
|
|
342
|
+
},
|
|
343
|
+
materials: [{ id: definition.id, kind: definition.kind, definition: definitionPath }],
|
|
344
|
+
files: [
|
|
345
|
+
{
|
|
346
|
+
path: graphPath,
|
|
347
|
+
mediaType: 'application/vnd.aelion.material-graph+json',
|
|
348
|
+
bytes: encoder.encode(graphSource).byteLength,
|
|
349
|
+
sha256: await sha256Hex(encoder.encode(graphSource)),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
path: definitionPath,
|
|
353
|
+
mediaType: 'application/vnd.aelion.material+json',
|
|
354
|
+
bytes: encoder.encode(definitionSource).byteLength,
|
|
355
|
+
sha256: await sha256Hex(encoder.encode(definitionSource)),
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
await mkdir(resolve(root, 'materials'), { recursive: true });
|
|
360
|
+
await mkdir(resolve(root, 'graphs'), { recursive: true });
|
|
361
|
+
await writeFile(resolve(root, definitionPath), definitionSource, 'utf8');
|
|
362
|
+
await writeFile(resolve(root, graphPath), graphSource, 'utf8');
|
|
363
|
+
await writeFile(resolve(root, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
364
|
+
await validateMaterialAuthorPackage(root);
|
|
365
|
+
await generateMaterialTypes(root);
|
|
366
|
+
return root;
|
|
367
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { buildMaterialAuthorPackage, compareMaterialGoldenFiles, generateMaterialTypes, initializeMaterialAuthorPackage, packMaterialAuthorPackage, prepublishMaterialAuthorPackage, validateMaterialAuthorPackage, writeMaterialPreviewReport, } from './cli-lib.js';
|
|
4
|
+
function option(name) {
|
|
5
|
+
const index = process.argv.indexOf(name);
|
|
6
|
+
return index < 0 ? undefined : process.argv[index + 1];
|
|
7
|
+
}
|
|
8
|
+
function required(value, description) {
|
|
9
|
+
if (value === undefined || value.length === 0)
|
|
10
|
+
throw new TypeError(`${description} is required`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
async function main() {
|
|
14
|
+
const command = process.argv[2];
|
|
15
|
+
const target = process.argv[3];
|
|
16
|
+
let result;
|
|
17
|
+
if (command === 'init') {
|
|
18
|
+
result = { directory: await initializeMaterialAuthorPackage(required(target, 'directory')) };
|
|
19
|
+
}
|
|
20
|
+
else if (command === 'build') {
|
|
21
|
+
result = await buildMaterialAuthorPackage(required(target, 'directory'));
|
|
22
|
+
}
|
|
23
|
+
else if (command === 'validate' || command === 'inspect') {
|
|
24
|
+
result = await validateMaterialAuthorPackage(required(target, 'directory'));
|
|
25
|
+
}
|
|
26
|
+
else if (command === 'types') {
|
|
27
|
+
result = {
|
|
28
|
+
path: await generateMaterialTypes(required(target, 'directory'), option('--out') ?? resolve(required(target, 'directory'), 'material.generated.d.ts')),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
else if (command === 'preview') {
|
|
32
|
+
result = {
|
|
33
|
+
path: await writeMaterialPreviewReport(required(target, 'directory'), option('--out') ?? resolve(required(target, 'directory'), 'material-preview.json')),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else if (command === 'pack') {
|
|
37
|
+
result = await packMaterialAuthorPackage(required(target, 'directory'), required(option('--out'), '--out'));
|
|
38
|
+
}
|
|
39
|
+
else if (command === 'golden') {
|
|
40
|
+
const tolerance = Number.parseInt(option('--tolerance') ?? '2', 10);
|
|
41
|
+
const comparison = await compareMaterialGoldenFiles(required(target, 'actual file'), required(process.argv[4], 'expected file'), tolerance);
|
|
42
|
+
result = comparison;
|
|
43
|
+
if (!comparison.passed)
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
else if (command === 'prepublish') {
|
|
47
|
+
result = await prepublishMaterialAuthorPackage(required(target, 'directory'));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new TypeError('Usage: aelion-material <init|build|validate|inspect|types|preview|pack|golden|prepublish> ...');
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
53
|
+
}
|
|
54
|
+
void main().catch((error) => {
|
|
55
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aelionsdk/material-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0-rc.1",
|
|
4
4
|
"description": "Type-safe authoring, validation and packaging tools for Aelion materials",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,27 +20,35 @@
|
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./author-cli": {
|
|
25
|
+
"types": "./dist/cli-lib.d.ts",
|
|
26
|
+
"import": "./dist/cli-lib.js"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"aelion-material": "./dist/cli.js"
|
|
31
|
+
},
|
|
25
32
|
"files": [
|
|
26
33
|
"dist",
|
|
27
34
|
"!dist/.tsbuildinfo"
|
|
28
35
|
],
|
|
29
36
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
37
|
+
"node": ">=24 <25"
|
|
31
38
|
},
|
|
32
39
|
"publishConfig": {
|
|
33
40
|
"access": "public",
|
|
34
41
|
"provenance": true
|
|
35
42
|
},
|
|
36
43
|
"dependencies": {
|
|
37
|
-
"@aelionsdk/core": "
|
|
38
|
-
"@aelionsdk/material-compiler": "
|
|
39
|
-
"ajv": "8.
|
|
44
|
+
"@aelionsdk/core": "1.1.0-rc.1",
|
|
45
|
+
"@aelionsdk/material-compiler": "1.1.0-rc.1",
|
|
46
|
+
"ajv": "8.18.0",
|
|
40
47
|
"ajv-formats": "3.0.1"
|
|
41
48
|
},
|
|
42
49
|
"scripts": {
|
|
43
50
|
"build": "tsc -b",
|
|
51
|
+
"cli": "corepack pnpm run build && node dist/cli.js",
|
|
44
52
|
"typecheck": "tsc -b --pretty false"
|
|
45
53
|
}
|
|
46
54
|
}
|