halfcode-compiler.xnl 0.1.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 +19 -0
- package/dist/application-assembly.d.ts +2 -0
- package/dist/application-assembly.js +2 -0
- package/dist/authoring-runtime.d.ts +2 -0
- package/dist/authoring-runtime.js +2 -0
- package/dist/contract-schema.d.ts +45 -0
- package/dist/contract-schema.js +297 -0
- package/dist/dist-Bkv7YeVi.js +491 -0
- package/dist/index-Ba5Gt9iW.d.ts +67 -0
- package/dist/index-D-cBhQVw.d.ts +38 -0
- package/dist/index-Dg0imcjf.d.ts +31 -0
- package/dist/index-DuRG4TQo.d.ts +12 -0
- package/dist/index-FdimP_SL.d.ts +236 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/kind-definition.d.ts +14 -0
- package/dist/kind-definition.js +8 -0
- package/dist/resource-core.d.ts +2 -0
- package/dist/resource-core.js +2 -0
- package/dist/resource-mapping.d.ts +43 -0
- package/dist/resource-mapping.js +2 -0
- package/dist/resource-projection.d.ts +2 -0
- package/dist/resource-projection.js +8 -0
- package/dist/skill-capsule.d.ts +2 -0
- package/dist/skill-capsule.js +2 -0
- package/dist/src-B87eJQat.js +39 -0
- package/dist/src-BlKlpg0J.js +180 -0
- package/dist/src-COEMEqZw.js +431 -0
- package/dist/src-DEuFHizY.js +561 -0
- package/dist/src-Dv8bfFUo.js +607 -0
- package/dist/testing.d.ts +30 -0
- package/dist/testing.js +57 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# halfcode-compiler.xnl
|
|
2
|
+
|
|
3
|
+
The single public npm package for compiling XNL-described applications into runtime-ready skill capsules.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install halfcode-compiler.xnl
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use the package root for the high-level application assembly and skill capsule APIs:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
resolveApplicationAssembly,
|
|
14
|
+
compileResourceSkillCapsule,
|
|
15
|
+
type PageObjectResource,
|
|
16
|
+
} from "halfcode-compiler.xnl"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Lower-level APIs are available through explicit subpaths such as `halfcode-compiler.xnl/resource-core` and `halfcode-compiler.xnl/authoring-runtime`.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as ObjectTargets, B as assertExecuteObjectOperationRequest, C as ObjectOperationDefinition, D as ObjectOperationTargetDeclaration, E as ObjectOperationOwnerKind, F as ResourceSource, G as resolveApplicationAssembly, H as assertObjectOperationCatalog, I as SkillCapsuleResource, K as resourceRefToFqn, L as TextMaterial, M as ResolveApplicationAssemblyOptions, N as ResolvedAssemblyPortBinding, O as ObjectTargetKindDefinition, P as ResourceInclude, R as TextResource, S as ObjectOperationContractError, T as ObjectOperationHandler, U as assertObjectOperationDefinition, V as assertObjectOperationCall, W as loadApplicationAssembly, _ as ObjectInvocationMode, a as AssemblyResource, b as ObjectOperationCatalog, c as BusinessObjectSopResource, d as ExecuteObjectOperationRequest, f as JsonObject, g as ObjectInvocation, h as LoadApplicationAssemblyOptions, i as AssemblyPortBinding, j as PageObjectResource, k as ObjectTargetRef, l as CodeBinding, m as JsonValue, n as ApplicationScope, o as AuthoringModuleDescriptor, p as JsonPrimitive, r as AssemblyPort, s as BusinessObjectResource, t as ApplicationAssembly, u as ContractedCallableResource, v as ObjectOperationBehavior, w as ObjectOperationEffect, x as ObjectOperationConfig, y as ObjectOperationCall, z as applicationAssemblyPackage } from "./index-FdimP_SL.js";
|
|
2
|
+
export { type ApplicationAssembly, type ApplicationScope, type AssemblyPort, type AssemblyPortBinding, type AssemblyResource, type AuthoringModuleDescriptor, type BusinessObjectResource, type BusinessObjectSopResource, type CodeBinding, type ContractedCallableResource, type ExecuteObjectOperationRequest, type JsonObject, type JsonPrimitive, type JsonValue, type LoadApplicationAssemblyOptions, type ObjectInvocation, type ObjectInvocationMode, type ObjectOperationBehavior, type ObjectOperationCall, type ObjectOperationCatalog, type ObjectOperationConfig, ObjectOperationContractError, type ObjectOperationDefinition, type ObjectOperationEffect, type ObjectOperationHandler, type ObjectOperationOwnerKind, type ObjectOperationTargetDeclaration, type ObjectTargetKindDefinition, type ObjectTargetRef, type ObjectTargets, type PageObjectResource, type ResolveApplicationAssemblyOptions, type ResolvedAssemblyPortBinding, type ResourceInclude, type ResourceSource, type SkillCapsuleResource, type TextMaterial, type TextResource, applicationAssemblyPackage, assertExecuteObjectOperationRequest, assertObjectOperationCall, assertObjectOperationCatalog, assertObjectOperationDefinition, loadApplicationAssembly, resolveApplicationAssembly, resourceRefToFqn };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as assertObjectOperationCatalog, c as resolveApplicationAssembly, i as assertObjectOperationCall, l as resourceRefToFqn, n as applicationAssemblyPackage, o as assertObjectOperationDefinition, r as assertExecuteObjectOperationRequest, s as loadApplicationAssembly, t as ObjectOperationContractError } from "./src-Dv8bfFUo.js";
|
|
2
|
+
export { ObjectOperationContractError, applicationAssemblyPackage, assertExecuteObjectOperationRequest, assertObjectOperationCall, assertObjectOperationCatalog, assertObjectOperationDefinition, loadApplicationAssembly, resolveApplicationAssembly, resourceRefToFqn };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as createAuthoringBindings, c as logger, i as RuntimeEffects, l as runWithRuntime, n as AuthoringRuntime, o as currentRuntime, r as Logger, s as invokeEffect, t as AuthoringBindings, u as runtimeAuthoringPackage } from "./index-Dg0imcjf.js";
|
|
2
|
+
export { AuthoringBindings, AuthoringRuntime, Logger, RuntimeEffects, createAuthoringBindings, currentRuntime, invokeEffect, logger, runWithRuntime, runtimeAuthoringPackage };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as runWithRuntime, i as logger, n as currentRuntime, o as runtimeAuthoringPackage, r as invokeEffect, t as createAuthoringBindings } from "./src-B87eJQat.js";
|
|
2
|
+
export { createAuthoringBindings, currentRuntime, invokeEffect, logger, runWithRuntime, runtimeAuthoringPackage };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region ../contract-schema-compiler/src/index.d.ts
|
|
2
|
+
type JsonSchema = Record<string, unknown>;
|
|
3
|
+
interface ContractSideFact {
|
|
4
|
+
source: string;
|
|
5
|
+
export: string;
|
|
6
|
+
schemaId: string;
|
|
7
|
+
}
|
|
8
|
+
interface ContractFact {
|
|
9
|
+
kind: string;
|
|
10
|
+
fqn: string;
|
|
11
|
+
input: ContractSideFact;
|
|
12
|
+
output: ContractSideFact;
|
|
13
|
+
}
|
|
14
|
+
interface ContractSchemaCompilerOptions {
|
|
15
|
+
packageRoot: string;
|
|
16
|
+
sourceRoot?: string;
|
|
17
|
+
contractFactName?: string;
|
|
18
|
+
checkOnly?: boolean;
|
|
19
|
+
expectedKind?: string | readonly string[];
|
|
20
|
+
categoryCatalog?: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface ContractSchemaCompilerReport {
|
|
23
|
+
facts: readonly ContractFactRecord[];
|
|
24
|
+
files: readonly GeneratedFile[];
|
|
25
|
+
checkOnly: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface ContractFactRecord {
|
|
28
|
+
directory: string;
|
|
29
|
+
factPath: string;
|
|
30
|
+
category: string;
|
|
31
|
+
capabilityName: string;
|
|
32
|
+
fact: ContractFact;
|
|
33
|
+
}
|
|
34
|
+
interface GeneratedFile {
|
|
35
|
+
path: string;
|
|
36
|
+
content: string;
|
|
37
|
+
}
|
|
38
|
+
declare function compileContractSchemas(options: ContractSchemaCompilerOptions): Promise<ContractSchemaCompilerReport>;
|
|
39
|
+
declare const contractSchemaCompilerPackage: {
|
|
40
|
+
readonly role: "framework";
|
|
41
|
+
readonly area: "contract-schema-compiler";
|
|
42
|
+
readonly owns: "TypeScript type-system driven contract schema projections";
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { ContractFact, ContractFactRecord, ContractSchemaCompilerOptions, ContractSchemaCompilerReport, ContractSideFact, GeneratedFile, JsonSchema, compileContractSchemas, contractSchemaCompilerPackage };
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
//#region ../contract-schema-compiler/src/index.ts
|
|
5
|
+
const defaultCompilerOptions = {
|
|
6
|
+
module: ts.ModuleKind.ESNext,
|
|
7
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
8
|
+
target: ts.ScriptTarget.ES2022,
|
|
9
|
+
strict: true,
|
|
10
|
+
skipLibCheck: true
|
|
11
|
+
};
|
|
12
|
+
async function compileContractSchemas(options) {
|
|
13
|
+
const packageRoot = resolve(options.packageRoot);
|
|
14
|
+
const sourceRoot = resolve(packageRoot, options.sourceRoot ?? "src");
|
|
15
|
+
const factName = options.contractFactName ?? "contract.json";
|
|
16
|
+
const checkOnly = options.checkOnly ?? false;
|
|
17
|
+
const facts = await findContractFacts(sourceRoot, factName, options.expectedKind);
|
|
18
|
+
if (facts.length === 0) throw new Error(`No ${factName} facts found under ${relative(packageRoot, sourceRoot)}`);
|
|
19
|
+
const sourcePaths = facts.flatMap((record) => [resolve(dirname(record.factPath), normalizeFactSource(record.fact.input.source)), resolve(dirname(record.factPath), normalizeFactSource(record.fact.output.source))]);
|
|
20
|
+
const program = ts.createProgram(sourcePaths, defaultCompilerOptions);
|
|
21
|
+
assertNoProgramDiagnostics(program);
|
|
22
|
+
const files = buildGeneratedFiles({
|
|
23
|
+
packageRoot,
|
|
24
|
+
sourceRoot,
|
|
25
|
+
facts,
|
|
26
|
+
program,
|
|
27
|
+
checker: program.getTypeChecker()
|
|
28
|
+
});
|
|
29
|
+
await reconcileGeneratedFiles({
|
|
30
|
+
packageRoot,
|
|
31
|
+
files,
|
|
32
|
+
obsoletePaths: await findObsoleteGeneratedPaths(sourceRoot, files),
|
|
33
|
+
checkOnly
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
facts,
|
|
37
|
+
files,
|
|
38
|
+
checkOnly
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function findContractFacts(sourceRoot, factName, expectedKind) {
|
|
42
|
+
const records = [];
|
|
43
|
+
await walk(sourceRoot, async (path, name) => {
|
|
44
|
+
if (name !== factName) return;
|
|
45
|
+
const fact = parseContractFact(path, await readFile(path, "utf8"), expectedKind);
|
|
46
|
+
const directory = dirname(path);
|
|
47
|
+
const segments = relative(sourceRoot, directory).split(/[\\/]/).join("/").split("/").filter(Boolean);
|
|
48
|
+
records.push({
|
|
49
|
+
directory,
|
|
50
|
+
factPath: path,
|
|
51
|
+
category: segments[0] ?? "",
|
|
52
|
+
capabilityName: segments.at(-1) ?? "",
|
|
53
|
+
fact
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return records.sort((left, right) => left.factPath.localeCompare(right.factPath));
|
|
57
|
+
}
|
|
58
|
+
async function walk(dir, visit) {
|
|
59
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
60
|
+
const full = join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) await walk(full, visit);
|
|
62
|
+
else if (entry.isFile()) await visit(full, entry.name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseContractFact(factPath, source, expectedKind) {
|
|
66
|
+
const fact = JSON.parse(source);
|
|
67
|
+
const kinds = typeof expectedKind === "string" ? [expectedKind] : expectedKind;
|
|
68
|
+
if (typeof fact.kind !== "string" || !fact.kind.trim()) throw new Error(`${factPath} must declare a non-empty kind`);
|
|
69
|
+
if (kinds && !kinds.includes(fact.kind)) throw new Error(`${factPath} kind must be one of ${kinds.join(", ")}, received ${fact.kind}`);
|
|
70
|
+
if (typeof fact.fqn !== "string" || !fact.fqn.trim()) throw new Error(`${factPath} must declare a non-empty fqn`);
|
|
71
|
+
for (const direction of ["input", "output"]) {
|
|
72
|
+
const side = fact[direction];
|
|
73
|
+
if (!side || typeof side.source !== "string" || typeof side.export !== "string" || typeof side.schemaId !== "string") throw new Error(`${factPath} requires ${direction}.source/export/schemaId`);
|
|
74
|
+
normalizeFactSource(side.source);
|
|
75
|
+
}
|
|
76
|
+
return fact;
|
|
77
|
+
}
|
|
78
|
+
function normalizeFactSource(value) {
|
|
79
|
+
if (!value.trim() || value.includes("\\") || value.startsWith("/") || value.split("/").includes("..")) throw new Error(`Contract source must be a safe relative path: ${value}`);
|
|
80
|
+
return value.replace(/^\.\//, "");
|
|
81
|
+
}
|
|
82
|
+
function assertNoProgramDiagnostics(program) {
|
|
83
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
84
|
+
if (diagnostics.length === 0) return;
|
|
85
|
+
const message = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
86
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
87
|
+
getCurrentDirectory: () => process.cwd(),
|
|
88
|
+
getNewLine: () => "\n"
|
|
89
|
+
});
|
|
90
|
+
throw new Error(`TypeScript contract program has diagnostics:\n${message}`);
|
|
91
|
+
}
|
|
92
|
+
function buildGeneratedFiles(input) {
|
|
93
|
+
const files = [];
|
|
94
|
+
for (const record of input.facts) {
|
|
95
|
+
const inputSchema = schemaForContractSide(input.program, input.checker, record, record.fact.input);
|
|
96
|
+
const outputSchema = schemaForContractSide(input.program, input.checker, record, record.fact.output);
|
|
97
|
+
files.push({
|
|
98
|
+
path: join(record.directory, "input.schema.generated.ts"),
|
|
99
|
+
content: schemaModule("inputSchema", inputSchema)
|
|
100
|
+
});
|
|
101
|
+
files.push({
|
|
102
|
+
path: join(record.directory, "output.schema.generated.ts"),
|
|
103
|
+
content: schemaModule("outputSchema", outputSchema)
|
|
104
|
+
});
|
|
105
|
+
files.push({
|
|
106
|
+
path: join(record.directory, "index.ts"),
|
|
107
|
+
content: capabilityIndex(record.fact)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const record of input.facts) {
|
|
112
|
+
const bucket = byCategory.get(record.category) ?? [];
|
|
113
|
+
bucket.push(record);
|
|
114
|
+
byCategory.set(record.category, bucket);
|
|
115
|
+
}
|
|
116
|
+
for (const [category, records] of [...byCategory.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
117
|
+
if (!category) continue;
|
|
118
|
+
files.push({
|
|
119
|
+
path: join(input.sourceRoot, category, "schema-catalog.generated.ts"),
|
|
120
|
+
content: schemaCatalog(category, records, input.sourceRoot)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
124
|
+
}
|
|
125
|
+
function schemaForContractSide(program, checker, record, side) {
|
|
126
|
+
const sourcePath = resolve(record.directory, normalizeFactSource(side.source));
|
|
127
|
+
const sourceFile = program.getSourceFile(sourcePath);
|
|
128
|
+
if (!sourceFile) throw new Error(`Unable to read contract source ${sourcePath}`);
|
|
129
|
+
const declaration = exportedType(sourceFile, side.export);
|
|
130
|
+
return {
|
|
131
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
132
|
+
$id: side.schemaId,
|
|
133
|
+
title: side.export,
|
|
134
|
+
...schemaForType(checker, checker.getTypeAtLocation(declaration), /* @__PURE__ */ new Set())
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function exportedType(sourceFile, name) {
|
|
138
|
+
for (const statement of sourceFile.statements) if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && statement.name.text === name && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) return statement;
|
|
139
|
+
throw new Error(`Exported TypeScript type ${name} not found in ${sourceFile.fileName}`);
|
|
140
|
+
}
|
|
141
|
+
function schemaForType(checker, type, seen) {
|
|
142
|
+
const primitive = primitiveSchema(checker, type);
|
|
143
|
+
if (primitive) return primitive;
|
|
144
|
+
if (seen.has(type)) return {};
|
|
145
|
+
if (checker.isTupleType(type)) {
|
|
146
|
+
const elements = checker.getTypeArguments(type);
|
|
147
|
+
return {
|
|
148
|
+
type: "array",
|
|
149
|
+
prefixItems: elements.map((element) => schemaForType(checker, element, seen)),
|
|
150
|
+
minItems: elements.length,
|
|
151
|
+
maxItems: elements.length
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (checker.isArrayType(type)) {
|
|
155
|
+
const element = checker.getTypeArguments(type)[0];
|
|
156
|
+
return {
|
|
157
|
+
type: "array",
|
|
158
|
+
items: element ? schemaForType(checker, element, seen) : {}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (type.isUnion()) {
|
|
162
|
+
const schemas = type.types.filter((member) => !(member.flags & ts.TypeFlags.Undefined)).map((member) => schemaForType(checker, member, seen));
|
|
163
|
+
if (schemas.length === 1) return schemas[0] ?? {};
|
|
164
|
+
if (schemas.every((schema) => schema.type === "string" && typeof schema.const === "string")) return {
|
|
165
|
+
type: "string",
|
|
166
|
+
enum: schemas.map((schema) => schema.const)
|
|
167
|
+
};
|
|
168
|
+
return { anyOf: schemas };
|
|
169
|
+
}
|
|
170
|
+
if (type.isIntersection()) {
|
|
171
|
+
const schemas = type.types.map((member) => schemaForType(checker, member, seen));
|
|
172
|
+
const objectSchemas = schemas.filter((schema) => schema.type === "object");
|
|
173
|
+
if (objectSchemas.length === schemas.length) return mergeObjectSchemas(objectSchemas);
|
|
174
|
+
return { allOf: schemas };
|
|
175
|
+
}
|
|
176
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
177
|
+
seen.add(type);
|
|
178
|
+
const properties = {};
|
|
179
|
+
const required = [];
|
|
180
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
181
|
+
const declaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
182
|
+
if (!declaration) continue;
|
|
183
|
+
properties[property.name] = schemaForType(checker, checker.getTypeOfSymbolAtLocation(property, declaration), seen);
|
|
184
|
+
if (!(property.flags & ts.SymbolFlags.Optional)) required.push(property.name);
|
|
185
|
+
}
|
|
186
|
+
seen.delete(type);
|
|
187
|
+
const stringIndex = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
188
|
+
return {
|
|
189
|
+
type: "object",
|
|
190
|
+
additionalProperties: stringIndex ? schemaForType(checker, stringIndex.type, seen) : false,
|
|
191
|
+
...Object.keys(properties).length > 0 ? {
|
|
192
|
+
properties,
|
|
193
|
+
required
|
|
194
|
+
} : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
function primitiveSchema(checker, type) {
|
|
200
|
+
if (type.flags & ts.TypeFlags.StringLiteral) return {
|
|
201
|
+
type: "string",
|
|
202
|
+
const: type.value
|
|
203
|
+
};
|
|
204
|
+
if (type.flags & ts.TypeFlags.NumberLiteral) return {
|
|
205
|
+
type: "number",
|
|
206
|
+
const: type.value
|
|
207
|
+
};
|
|
208
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) return {
|
|
209
|
+
type: "boolean",
|
|
210
|
+
const: checker.typeToString(type) === "true"
|
|
211
|
+
};
|
|
212
|
+
if (type.flags & ts.TypeFlags.StringLike) return { type: "string" };
|
|
213
|
+
if (type.flags & ts.TypeFlags.NumberLike) return { type: "number" };
|
|
214
|
+
if (type.flags & ts.TypeFlags.BooleanLike) return { type: "boolean" };
|
|
215
|
+
if (type.flags & ts.TypeFlags.Null) return { type: "null" };
|
|
216
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return {};
|
|
217
|
+
}
|
|
218
|
+
function mergeObjectSchemas(schemas) {
|
|
219
|
+
return {
|
|
220
|
+
type: "object",
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
properties: Object.assign({}, ...schemas.map((schema) => schema.properties)),
|
|
223
|
+
required: [...new Set(schemas.flatMap((schema) => Array.isArray(schema.required) ? schema.required : []))]
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function schemaModule(exportName, schema) {
|
|
227
|
+
return [
|
|
228
|
+
"/* eslint-disable */",
|
|
229
|
+
"// This file is generated by contract facts. Do not edit by hand.",
|
|
230
|
+
"",
|
|
231
|
+
`export const ${exportName} = ${JSON.stringify(schema, null, 2)} as const`,
|
|
232
|
+
""
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
235
|
+
function capabilityIndex(fact) {
|
|
236
|
+
return [
|
|
237
|
+
"/* eslint-disable */",
|
|
238
|
+
"// This file is generated by contract facts. Do not edit by hand.",
|
|
239
|
+
"",
|
|
240
|
+
`export type { ${fact.input.export} } from ${JSON.stringify(`./${stripTsExtension(normalizeFactSource(fact.input.source))}`)}`,
|
|
241
|
+
`export type { ${fact.output.export} } from ${JSON.stringify(`./${stripTsExtension(normalizeFactSource(fact.output.source))}`)}`,
|
|
242
|
+
"export { inputSchema } from \"./input.schema.generated\"",
|
|
243
|
+
"export { outputSchema } from \"./output.schema.generated\"",
|
|
244
|
+
""
|
|
245
|
+
].join("\n");
|
|
246
|
+
}
|
|
247
|
+
function schemaCatalog(category, records, sourceRoot) {
|
|
248
|
+
return [
|
|
249
|
+
"/* eslint-disable */",
|
|
250
|
+
"// This file is generated by contract facts. Do not edit by hand.",
|
|
251
|
+
"",
|
|
252
|
+
...records.map((record, index) => {
|
|
253
|
+
const dir = relative(join(sourceRoot, category), record.directory).split(/[\\/]/).join("/");
|
|
254
|
+
return `import { inputSchema as input${index}, outputSchema as output${index} } from ${JSON.stringify(`./${dir}`)}`;
|
|
255
|
+
}),
|
|
256
|
+
"",
|
|
257
|
+
`export const schemaCatalog = [${records.flatMap((_, index) => [`input${index}`, `output${index}`]).join(", ")}] as const`,
|
|
258
|
+
""
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
function stripTsExtension(source) {
|
|
262
|
+
return source.replace(/\.ts$/, "");
|
|
263
|
+
}
|
|
264
|
+
async function findObsoleteGeneratedPaths(sourceRoot, expectedFiles) {
|
|
265
|
+
const expected = new Set(expectedFiles.map((file) => resolve(file.path)));
|
|
266
|
+
const obsolete = [];
|
|
267
|
+
await walk(sourceRoot, async (path, name) => {
|
|
268
|
+
if (name === "input.schema.generated.ts" || name === "output.schema.generated.ts" || name === "schema-catalog.generated.ts") {
|
|
269
|
+
if (!expected.has(resolve(path))) obsolete.push(path);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (name !== "index.ts" || expected.has(resolve(path))) return;
|
|
273
|
+
if ((await readFile(path, "utf8")).includes("This file is generated by contract facts. Do not edit by hand.")) obsolete.push(path);
|
|
274
|
+
});
|
|
275
|
+
return obsolete.sort();
|
|
276
|
+
}
|
|
277
|
+
async function reconcileGeneratedFiles(input) {
|
|
278
|
+
if (input.checkOnly) {
|
|
279
|
+
const stale = [];
|
|
280
|
+
for (const file of input.files) if (await readFile(file.path, "utf8").catch(() => void 0) !== file.content) stale.push(relative(input.packageRoot, file.path));
|
|
281
|
+
for (const obsoletePath of input.obsoletePaths) if (await readFile(obsoletePath, "utf8").then(() => true).catch(() => false)) stale.push(relative(input.packageRoot, obsoletePath));
|
|
282
|
+
if (stale.length > 0) throw new Error(`Generated contract schema files are stale:\n${stale.join("\n")}`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const file of input.files) {
|
|
286
|
+
await mkdir(dirname(file.path), { recursive: true });
|
|
287
|
+
await writeFile(file.path, file.content, "utf8");
|
|
288
|
+
}
|
|
289
|
+
await Promise.all(input.obsoletePaths.map((path) => rm(path, { force: true })));
|
|
290
|
+
}
|
|
291
|
+
const contractSchemaCompilerPackage = {
|
|
292
|
+
role: "framework",
|
|
293
|
+
area: "contract-schema-compiler",
|
|
294
|
+
owns: "TypeScript type-system driven contract schema projections"
|
|
295
|
+
};
|
|
296
|
+
//#endregion
|
|
297
|
+
export { compileContractSchemas, contractSchemaCompilerPackage };
|