@sembl/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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sembl contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @sembl/compiler
2
+
3
+ Schema compiler for SEMBL. Walks decorated TypeScript classes and emits the
4
+ runtime schemas [`@sembl/core`](https://github.com/nickrunner/sembl/tree/main/packages/core)
5
+ coerces against.
6
+
7
+ Extraction reads the source AST with ts-morph. Decorators are no-ops at
8
+ runtime — nothing is reflected or evaluated — so a value that isn't written out
9
+ in source can't be read, and the compiler says so rather than guessing.
10
+
11
+ See the [project README](https://github.com/nickrunner/sembl#readme) for the
12
+ full walkthrough.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pnpm add -D @sembl/compiler
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```sh
23
+ sembl extract --input src/schemas --output src/generated
24
+ ```
25
+
26
+ Emits one `<Name>.schema.ts` per decorated class plus an `index.ts` exporting a
27
+ `SchemaBundle`. Treat the output as a build artifact: gitignore it, and run
28
+ extraction before your build.
29
+
30
+ | Flag | Notes |
31
+ | ----------------- | ------------------------------------------------------------ |
32
+ | `-i, --input` | Directory of decorated schema classes. Required. |
33
+ | `-o, --output` | Directory for generated files. Required. |
34
+ | `--tsconfig` | Path to a tsconfig, when type resolution needs your settings. |
35
+ | `--strict` | Exit non-zero on warnings. Recommended in CI. |
36
+
37
+ ## Warnings
38
+
39
+ Warnings print to stderr and are advisory by default. They are worth reading —
40
+ each one marks a place where the emitted schema is narrower or vaguer than the
41
+ type you wrote:
42
+
43
+ - a `@Constrain` entry that isn't a compile-time literal, so it can't be read
44
+ - `@ValuesFrom` on a field that isn't a string or `string[]`
45
+ - a type with no `FieldType` equivalent (a `Date`, a `Map`, an index-signature
46
+ object), which falls back to a string so the rest of the schema still extracts
47
+
48
+ Inline anonymous object types don't warn — they get a synthesized nested schema
49
+ registered in the bundle, named after the class and property path.
@@ -0,0 +1,464 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/extractor/decorator-parser.ts
4
+ import {
5
+ Node,
6
+ SyntaxKind
7
+ } from "ts-morph";
8
+ var CONSTRAINT_KEYS = {
9
+ maxLength: "number",
10
+ minLength: "number",
11
+ minimum: "number",
12
+ maximum: "number",
13
+ minItems: "number",
14
+ maxItems: "number",
15
+ pattern: "string"
16
+ };
17
+ function getDecoratorStringArg(decorator) {
18
+ if (!decorator.isDecoratorFactory()) {
19
+ return void 0;
20
+ }
21
+ const args = decorator.getArguments();
22
+ if (args.length === 0) {
23
+ return void 0;
24
+ }
25
+ const arg = args[0];
26
+ const text = arg.getText();
27
+ if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
28
+ return text.slice(1, -1);
29
+ }
30
+ if (text.startsWith("`") && text.endsWith("`")) {
31
+ return text.slice(1, -1);
32
+ }
33
+ return void 0;
34
+ }
35
+ function parseSchemaDecorator(classDecl) {
36
+ const decorator = classDecl.getDecorator("Schema");
37
+ if (!decorator) {
38
+ return void 0;
39
+ }
40
+ return getDecoratorStringArg(decorator);
41
+ }
42
+ function parseDescribeDecorator(propDecl) {
43
+ const decorator = propDecl.getDecorator("Describe");
44
+ if (!decorator) {
45
+ return void 0;
46
+ }
47
+ return getDecoratorStringArg(decorator);
48
+ }
49
+ function readNumberLiteral(node) {
50
+ if (Node.isNumericLiteral(node)) {
51
+ return node.getLiteralValue();
52
+ }
53
+ if (Node.isPrefixUnaryExpression(node)) {
54
+ const operand = node.getOperand();
55
+ if (Node.isNumericLiteral(operand)) {
56
+ const operator = node.getOperatorToken();
57
+ if (operator === SyntaxKind.MinusToken) {
58
+ return -operand.getLiteralValue();
59
+ }
60
+ if (operator === SyntaxKind.PlusToken) {
61
+ return operand.getLiteralValue();
62
+ }
63
+ }
64
+ }
65
+ return void 0;
66
+ }
67
+ function readStringLiteral(node) {
68
+ if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
69
+ return node.getLiteralValue();
70
+ }
71
+ return void 0;
72
+ }
73
+ function readConstraintEntry(property, constraints, scope) {
74
+ if (!Node.isPropertyAssignment(property)) {
75
+ scope.context.warn(
76
+ scope,
77
+ `@Constrain entry \`${property.getText()}\` is not a \`key: value\` pair of compile-time constants and cannot be read from source. Skipping it.`
78
+ );
79
+ return false;
80
+ }
81
+ const nameNode = property.getNameNode();
82
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
83
+ scope.context.warn(
84
+ scope,
85
+ `@Constrain key \`${nameNode.getText()}\` is computed and cannot be read from source. Skipping it.`
86
+ );
87
+ return false;
88
+ }
89
+ const key = nameNode.getText().replace(/^["']|["']$/g, "");
90
+ if (!Object.prototype.hasOwnProperty.call(CONSTRAINT_KEYS, key)) {
91
+ scope.context.warn(
92
+ scope,
93
+ `@Constrain key "${key}" is not a FieldConstraints property. Expected one of: ${Object.keys(CONSTRAINT_KEYS).join(", ")}. Skipping it.`
94
+ );
95
+ return false;
96
+ }
97
+ const expected = CONSTRAINT_KEYS[key];
98
+ const initializer = property.getInitializerOrThrow();
99
+ const value = expected === "number" ? readNumberLiteral(initializer) : readStringLiteral(initializer);
100
+ if (value === void 0) {
101
+ scope.context.warn(
102
+ scope,
103
+ `@Constrain value for "${key}" is \`${initializer.getText()}\`, which is not a ${expected} literal the compiler can read from source. Skipping it.`
104
+ );
105
+ return false;
106
+ }
107
+ constraints[key] = value;
108
+ return true;
109
+ }
110
+ function parseConstrainDecorator(propDecl, scope) {
111
+ const decorator = propDecl.getDecorator("Constrain");
112
+ if (!decorator) {
113
+ return void 0;
114
+ }
115
+ const args = decorator.isDecoratorFactory() ? decorator.getArguments() : [];
116
+ const argument = args[0];
117
+ if (argument === void 0 || !Node.isObjectLiteralExpression(argument)) {
118
+ scope.context.warn(
119
+ scope,
120
+ `@Constrain expects an inline object literal of compile-time constants, but ${argument === void 0 ? "it was called with no argument" : `was given \`${argument.getText()}\``}. Ignoring the decorator.`
121
+ );
122
+ return void 0;
123
+ }
124
+ const constraints = {};
125
+ let accepted = 0;
126
+ for (const property of argument.getProperties()) {
127
+ if (readConstraintEntry(property, constraints, scope)) {
128
+ accepted += 1;
129
+ }
130
+ }
131
+ return accepted > 0 ? constraints : void 0;
132
+ }
133
+ function parseValuesFromDecorator(propDecl, scope) {
134
+ const decorator = propDecl.getDecorator("ValuesFrom");
135
+ if (!decorator) {
136
+ return void 0;
137
+ }
138
+ const sourceId = getDecoratorStringArg(decorator);
139
+ if (sourceId === void 0) {
140
+ scope.context.warn(
141
+ scope,
142
+ `@ValuesFrom expects a string literal naming the enum source, which the caller resolves at coercion time. Ignoring the decorator.`
143
+ );
144
+ return void 0;
145
+ }
146
+ return sourceId;
147
+ }
148
+
149
+ // src/extractor/extraction-context.ts
150
+ import { createHash } from "crypto";
151
+ function createExtractionContext() {
152
+ const warnings = [];
153
+ const synthesizedSchemas = {};
154
+ const nestedReferences = [];
155
+ return {
156
+ warnings,
157
+ synthesizedSchemas,
158
+ warn(scope, message) {
159
+ warnings.push(`${scope.className}.${scope.propertyPath}: ${message}`);
160
+ },
161
+ registerSynthesizedSchema(schema) {
162
+ synthesizedSchemas[schema.id] = schema;
163
+ },
164
+ recordNestedReference(scope, nestedSchemaId, typeText) {
165
+ nestedReferences.push({
166
+ className: scope.className,
167
+ propertyPath: scope.propertyPath,
168
+ nestedSchemaId,
169
+ typeText
170
+ });
171
+ },
172
+ reportUnresolvedNestedSchemas(knownSchemaIds) {
173
+ for (const reference of nestedReferences) {
174
+ if (knownSchemaIds.has(reference.nestedSchemaId)) {
175
+ continue;
176
+ }
177
+ warnings.push(
178
+ `${reference.className}.${reference.propertyPath}: type \`${reference.typeText}\` resolves to nested schema "${reference.nestedSchemaId}", which is not a @Schema-decorated class in this extraction. It will emit as an object with no properties. Decorate it with @Schema if it is yours to change, or use a type the contract supports \u2014 a \`Date\`, for instance, extracts as an ISO-8601 string.`
179
+ );
180
+ }
181
+ }
182
+ };
183
+ }
184
+ function synthesizedSchemaId(scope, structuralSignature) {
185
+ const path = `${scope.className}_${scope.propertyPath}`.replace(
186
+ /[^A-Za-z0-9_]/g,
187
+ "_"
188
+ );
189
+ const digest = createHash("sha256").update(structuralSignature).digest("hex").slice(0, 8);
190
+ return `${path}__${digest}`;
191
+ }
192
+
193
+ // src/extractor/type-resolver.ts
194
+ function reportUnsupported(type, scope, reason) {
195
+ scope.context.warn(
196
+ scope,
197
+ `unsupported type \`${type.getText(scope.node)}\` \u2014 ${reason}. Falling back to string.`
198
+ );
199
+ return { kind: "string" };
200
+ }
201
+ function resolveInlineObjectType(type, scope) {
202
+ const properties = type.getProperties();
203
+ if (properties.length === 0) {
204
+ const isMap = type.getStringIndexType() !== void 0 || type.getNumberIndexType() !== void 0;
205
+ return reportUnsupported(
206
+ type,
207
+ scope,
208
+ isMap ? "a map with an index signature has no FieldType equivalent, so its entries cannot be described to the model; declare a @Schema class with the keys you expect, or take the value as a JSON string and parse it yourself" : "an object type with no properties has nothing to extract"
209
+ );
210
+ }
211
+ const fields = properties.map((property) => {
212
+ const name = property.getName();
213
+ return {
214
+ name,
215
+ // Members of an inline type carry no @Describe, so the owning field's
216
+ // description is the only semantics the model gets for them.
217
+ description: "",
218
+ type: resolveFieldType(property.getTypeAtLocation(scope.node), {
219
+ ...scope,
220
+ propertyPath: `${scope.propertyPath}.${name}`
221
+ }),
222
+ required: !property.isOptional()
223
+ };
224
+ });
225
+ const id = synthesizedSchemaId(
226
+ scope,
227
+ JSON.stringify(fields.map((f) => [f.name, f.required, f.type]))
228
+ );
229
+ scope.context.registerSynthesizedSchema({
230
+ id,
231
+ description: `Inline object type declared at ${scope.className}.${scope.propertyPath}.`,
232
+ fields
233
+ });
234
+ return { kind: "object", nestedSchemaId: id };
235
+ }
236
+ function resolveFieldType(type, scope) {
237
+ if (type.isUnion()) {
238
+ const members = type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull());
239
+ if (members.length === 1) {
240
+ return resolveFieldType(members[0], scope);
241
+ }
242
+ if (members.length === 0) {
243
+ return reportUnsupported(type, scope, "there is no value type to extract");
244
+ }
245
+ if (members.every((t) => t.isStringLiteral())) {
246
+ return {
247
+ kind: "enum",
248
+ values: members.map((t) => t.getLiteralValue())
249
+ };
250
+ }
251
+ if (members.every((t) => t.isBoolean() || t.isBooleanLiteral())) {
252
+ return { kind: "boolean" };
253
+ }
254
+ if (members.every((t) => t.isNumber() || t.isNumberLiteral())) {
255
+ return { kind: "number" };
256
+ }
257
+ return reportUnsupported(
258
+ type,
259
+ scope,
260
+ "a union mixing several kinds of value has no single FieldType; split it into separate fields, or narrow it to one kind"
261
+ );
262
+ }
263
+ if (type.isString() || type.isStringLiteral()) {
264
+ return { kind: "string" };
265
+ }
266
+ if (type.isNumber() || type.isNumberLiteral()) {
267
+ return { kind: "number" };
268
+ }
269
+ if (type.isBoolean() || type.isBooleanLiteral()) {
270
+ return { kind: "boolean" };
271
+ }
272
+ if (type.isArray()) {
273
+ const elementType = type.getArrayElementTypeOrThrow();
274
+ return { kind: "array", items: resolveFieldType(elementType, scope) };
275
+ }
276
+ if (type.isEnum()) {
277
+ const members = type.getUnionTypes().map((t) => t.getLiteralValue()).filter((v) => typeof v === "string");
278
+ if (members.length > 0) {
279
+ return { kind: "enum", values: members };
280
+ }
281
+ return reportUnsupported(
282
+ type,
283
+ scope,
284
+ "its members are not string values, so they cannot be offered to the model as an enum"
285
+ );
286
+ }
287
+ if (type.isObject()) {
288
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
289
+ const typeName = symbol?.getName();
290
+ if (typeName && typeName !== "__type" && typeName !== "Object") {
291
+ scope.context.recordNestedReference(
292
+ scope,
293
+ typeName,
294
+ type.getText(scope.node)
295
+ );
296
+ return { kind: "object", nestedSchemaId: typeName };
297
+ }
298
+ return resolveInlineObjectType(type, scope);
299
+ }
300
+ return reportUnsupported(
301
+ type,
302
+ scope,
303
+ "it maps to none of string, number, boolean, array, enum, or a @Schema class"
304
+ );
305
+ }
306
+
307
+ // src/extractor/class-visitor.ts
308
+ function describeKind(type) {
309
+ return type.kind === "array" ? `${describeKind(type.items)}[]` : type.kind;
310
+ }
311
+ function applyValuesFrom(type, sourceId, scope) {
312
+ if (type.kind === "string") {
313
+ return { kind: "dynamicEnum", sourceId };
314
+ }
315
+ if (type.kind === "array" && type.items.kind === "string") {
316
+ return { kind: "array", items: { kind: "dynamicEnum", sourceId } };
317
+ }
318
+ scope.context.warn(
319
+ scope,
320
+ `@ValuesFrom("${sourceId}") applies to a string or string[] field, but this field resolved to ${describeKind(type)}. Leaving the type unchanged.`
321
+ );
322
+ return type;
323
+ }
324
+ function visitClass(classDecl, context) {
325
+ const description = parseSchemaDecorator(classDecl);
326
+ if (description === void 0) {
327
+ return void 0;
328
+ }
329
+ const className = classDecl.getName();
330
+ if (!className) {
331
+ return void 0;
332
+ }
333
+ const fields = [];
334
+ for (const prop of classDecl.getProperties()) {
335
+ const fieldDescription = parseDescribeDecorator(prop);
336
+ if (fieldDescription === void 0) {
337
+ continue;
338
+ }
339
+ const name = prop.getName();
340
+ const isOptional = prop.hasQuestionToken();
341
+ const scope = {
342
+ className,
343
+ propertyPath: name,
344
+ node: prop,
345
+ context
346
+ };
347
+ let type = resolveFieldType(prop.getType(), scope);
348
+ const sourceId = parseValuesFromDecorator(prop, scope);
349
+ if (sourceId !== void 0) {
350
+ type = applyValuesFrom(type, sourceId, scope);
351
+ }
352
+ const constraints = parseConstrainDecorator(prop, scope);
353
+ fields.push({
354
+ name,
355
+ description: fieldDescription,
356
+ type,
357
+ required: !isOptional,
358
+ ...constraints !== void 0 ? { constraints } : {}
359
+ });
360
+ }
361
+ return {
362
+ id: className,
363
+ description,
364
+ fields
365
+ };
366
+ }
367
+
368
+ // src/extractor/ast-extractor.ts
369
+ import { Project, ScriptTarget } from "ts-morph";
370
+ function extractSchemas(options) {
371
+ const project = new Project({
372
+ tsConfigFilePath: options.tsconfigPath,
373
+ skipAddingFilesFromTsConfig: true,
374
+ compilerOptions: {
375
+ experimentalDecorators: true,
376
+ strict: true,
377
+ // Without a target the default lib is ES5, so anything newer — `Map`,
378
+ // `Set` — resolves to `any` and lands in the unsupported-type warning as
379
+ // "any", naming a type the author never wrote.
380
+ target: ScriptTarget.ES2022
381
+ }
382
+ });
383
+ for (const pattern of options.filePatterns) {
384
+ project.addSourceFilesAtPaths(pattern);
385
+ }
386
+ const context = createExtractionContext();
387
+ const schemas = {};
388
+ for (const sourceFile of project.getSourceFiles()) {
389
+ for (const classDecl of sourceFile.getClasses()) {
390
+ const schema = visitClass(classDecl, context);
391
+ if (schema) {
392
+ schemas[schema.id] = schema;
393
+ }
394
+ }
395
+ }
396
+ for (const [id, schema] of Object.entries(context.synthesizedSchemas)) {
397
+ schemas[id] = schema;
398
+ }
399
+ context.reportUnresolvedNestedSchemas(new Set(Object.keys(schemas)));
400
+ return { schemas, warnings: [...context.warnings] };
401
+ }
402
+
403
+ // src/generator/schema-emitter.ts
404
+ import { mkdirSync, writeFileSync } from "fs";
405
+ import { join } from "path";
406
+ function schemaToSource(schema) {
407
+ const json = JSON.stringify(schema, null, 2);
408
+ return `// Auto-generated by sembl extract \u2014 do not edit
409
+ import type { RuntimeSchema } from "@sembl/core";
410
+
411
+ export const ${schema.id}Schema: RuntimeSchema = ${json};
412
+ `;
413
+ }
414
+ function emitSchemas(bundle, outputDir) {
415
+ mkdirSync(outputDir, { recursive: true });
416
+ const emittedFiles = [];
417
+ const schemaIds = [];
418
+ for (const [id, schema] of Object.entries(bundle.schemas)) {
419
+ const fileName = `${id}.schema.ts`;
420
+ const filePath = join(outputDir, fileName);
421
+ writeFileSync(filePath, schemaToSource(schema), "utf-8");
422
+ emittedFiles.push(filePath);
423
+ schemaIds.push(id);
424
+ }
425
+ const indexLines = [
426
+ "// Auto-generated by sembl extract \u2014 do not edit",
427
+ 'import type { SchemaBundle } from "@sembl/core";',
428
+ ""
429
+ ];
430
+ for (const id of schemaIds) {
431
+ indexLines.push(`import { ${id}Schema } from "./${id}.schema.js";`);
432
+ }
433
+ indexLines.push("");
434
+ indexLines.push("export const bundle: SchemaBundle = {");
435
+ indexLines.push(" schemas: {");
436
+ for (const id of schemaIds) {
437
+ indexLines.push(` ${id}: ${id}Schema,`);
438
+ }
439
+ indexLines.push(" },");
440
+ indexLines.push("};");
441
+ indexLines.push("");
442
+ for (const id of schemaIds) {
443
+ indexLines.push(`export { ${id}Schema } from "./${id}.schema.js";`);
444
+ }
445
+ indexLines.push("");
446
+ const indexPath = join(outputDir, "index.ts");
447
+ writeFileSync(indexPath, indexLines.join("\n"), "utf-8");
448
+ emittedFiles.push(indexPath);
449
+ return emittedFiles;
450
+ }
451
+
452
+ export {
453
+ parseSchemaDecorator,
454
+ parseDescribeDecorator,
455
+ parseConstrainDecorator,
456
+ parseValuesFromDecorator,
457
+ createExtractionContext,
458
+ synthesizedSchemaId,
459
+ resolveFieldType,
460
+ visitClass,
461
+ extractSchemas,
462
+ emitSchemas
463
+ };
464
+ //# sourceMappingURL=chunk-Y4FKH2ZU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/extractor/decorator-parser.ts","../src/extractor/extraction-context.ts","../src/extractor/type-resolver.ts","../src/extractor/class-visitor.ts","../src/extractor/ast-extractor.ts","../src/generator/schema-emitter.ts"],"sourcesContent":["import {\n Node,\n SyntaxKind,\n type ClassDeclaration,\n type PropertyDeclaration,\n type Decorator,\n} from \"ts-morph\";\nimport type { FieldConstraints } from \"@sembl/core\";\nimport type { FieldScope } from \"./extraction-context.js\";\n\n/**\n * Every key `FieldConstraints` allows, with the literal kind its value must be.\n *\n * Typed against `FieldConstraints` on purpose: if core adds or renames a\n * constraint, this table stops compiling instead of silently rejecting the new\n * key as unknown.\n */\nconst CONSTRAINT_KEYS: Record<\n keyof Required<FieldConstraints>,\n \"number\" | \"string\"\n> = {\n maxLength: \"number\",\n minLength: \"number\",\n minimum: \"number\",\n maximum: \"number\",\n minItems: \"number\",\n maxItems: \"number\",\n pattern: \"string\",\n};\n\n/**\n * Extract the string argument from a decorator call expression.\n * e.g. @Schema(\"some description\") → \"some description\"\n */\nfunction getDecoratorStringArg(decorator: Decorator): string | undefined {\n if (!decorator.isDecoratorFactory()) {\n return undefined;\n }\n const args = decorator.getArguments();\n if (args.length === 0) {\n return undefined;\n }\n const arg = args[0];\n // Strip quotes from string literal\n const text = arg.getText();\n if (\n (text.startsWith('\"') && text.endsWith('\"')) ||\n (text.startsWith(\"'\") && text.endsWith(\"'\"))\n ) {\n return text.slice(1, -1);\n }\n // Handle template literals\n if (text.startsWith(\"`\") && text.endsWith(\"`\")) {\n return text.slice(1, -1);\n }\n return undefined;\n}\n\n/**\n * Extract the @Schema description from a class declaration.\n * Returns undefined if the class doesn't have a @Schema decorator.\n */\nexport function parseSchemaDecorator(\n classDecl: ClassDeclaration,\n): string | undefined {\n const decorator = classDecl.getDecorator(\"Schema\");\n if (!decorator) {\n return undefined;\n }\n return getDecoratorStringArg(decorator);\n}\n\n/**\n * Extract the @Describe description from a property declaration.\n * Returns undefined if the property doesn't have a @Describe decorator.\n */\nexport function parseDescribeDecorator(\n propDecl: PropertyDeclaration,\n): string | undefined {\n const decorator = propDecl.getDecorator(\"Describe\");\n if (!decorator) {\n return undefined;\n }\n return getDecoratorStringArg(decorator);\n}\n\n/**\n * Read a number written directly in source.\n *\n * A negative bound is a prefix minus applied to a numeric literal rather than\n * a literal of its own, so it needs unwrapping.\n */\nfunction readNumberLiteral(node: Node): number | undefined {\n if (Node.isNumericLiteral(node)) {\n return node.getLiteralValue();\n }\n if (Node.isPrefixUnaryExpression(node)) {\n const operand = node.getOperand();\n if (Node.isNumericLiteral(operand)) {\n const operator = node.getOperatorToken();\n if (operator === SyntaxKind.MinusToken) {\n return -operand.getLiteralValue();\n }\n if (operator === SyntaxKind.PlusToken) {\n return operand.getLiteralValue();\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read a string written directly in source.\n */\nfunction readStringLiteral(node: Node): string | undefined {\n if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {\n return node.getLiteralValue();\n }\n return undefined;\n}\n\n/**\n * Read one `key: value` pair of a @Constrain object literal into `constraints`.\n * Returns true if a value was accepted.\n */\nfunction readConstraintEntry(\n property: Node,\n constraints: FieldConstraints,\n scope: FieldScope,\n): boolean {\n if (!Node.isPropertyAssignment(property)) {\n // A shorthand (`{ maxLength }`), a spread (`{ ...shared }`), or a method\n // all resolve through a binding the compiler never evaluates.\n scope.context.warn(\n scope,\n `@Constrain entry \\`${property.getText()}\\` is not a \\`key: value\\` pair of ` +\n `compile-time constants and cannot be read from source. Skipping it.`,\n );\n return false;\n }\n\n const nameNode = property.getNameNode();\n if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {\n scope.context.warn(\n scope,\n `@Constrain key \\`${nameNode.getText()}\\` is computed and cannot be read from source. Skipping it.`,\n );\n return false;\n }\n\n const key = nameNode.getText().replace(/^[\"']|[\"']$/g, \"\");\n if (!Object.prototype.hasOwnProperty.call(CONSTRAINT_KEYS, key)) {\n scope.context.warn(\n scope,\n `@Constrain key \"${key}\" is not a FieldConstraints property. ` +\n `Expected one of: ${Object.keys(CONSTRAINT_KEYS).join(\", \")}. Skipping it.`,\n );\n return false;\n }\n\n const expected = CONSTRAINT_KEYS[key as keyof typeof CONSTRAINT_KEYS];\n const initializer = property.getInitializerOrThrow();\n const value =\n expected === \"number\"\n ? readNumberLiteral(initializer)\n : readStringLiteral(initializer);\n\n if (value === undefined) {\n scope.context.warn(\n scope,\n `@Constrain value for \"${key}\" is \\`${initializer.getText()}\\`, which is not ` +\n `a ${expected} literal the compiler can read from source. Skipping it.`,\n );\n return false;\n }\n\n (constraints as Record<string, number | string>)[key] = value;\n return true;\n}\n\n/**\n * Extract the @Constrain bounds from a property declaration.\n *\n * The decorator's argument has to be an inline object literal: decorators are\n * never evaluated, so a value that is not written out in source cannot be\n * resolved. Unreadable and unknown entries are warned about and skipped\n * individually, so one bad bound does not discard the rest.\n *\n * Returns undefined if there is no @Constrain decorator, or if nothing in it\n * could be read.\n */\nexport function parseConstrainDecorator(\n propDecl: PropertyDeclaration,\n scope: FieldScope,\n): FieldConstraints | undefined {\n const decorator = propDecl.getDecorator(\"Constrain\");\n if (!decorator) {\n return undefined;\n }\n\n const args = decorator.isDecoratorFactory() ? decorator.getArguments() : [];\n const argument = args[0];\n if (argument === undefined || !Node.isObjectLiteralExpression(argument)) {\n scope.context.warn(\n scope,\n `@Constrain expects an inline object literal of compile-time constants, but ` +\n `${argument === undefined ? \"it was called with no argument\" : `was given \\`${argument.getText()}\\``}. ` +\n `Ignoring the decorator.`,\n );\n return undefined;\n }\n\n const constraints: FieldConstraints = {};\n let accepted = 0;\n for (const property of argument.getProperties()) {\n if (readConstraintEntry(property, constraints, scope)) {\n accepted += 1;\n }\n }\n\n return accepted > 0 ? constraints : undefined;\n}\n\n/**\n * Extract the enum source id from a property's @ValuesFrom decorator.\n *\n * Returns undefined if there is no @ValuesFrom decorator, or if its argument\n * is not a string literal.\n */\nexport function parseValuesFromDecorator(\n propDecl: PropertyDeclaration,\n scope: FieldScope,\n): string | undefined {\n const decorator = propDecl.getDecorator(\"ValuesFrom\");\n if (!decorator) {\n return undefined;\n }\n\n const sourceId = getDecoratorStringArg(decorator);\n if (sourceId === undefined) {\n scope.context.warn(\n scope,\n `@ValuesFrom expects a string literal naming the enum source, which the caller ` +\n `resolves at coercion time. Ignoring the decorator.`,\n );\n return undefined;\n }\n return sourceId;\n}\n","import { createHash } from \"node:crypto\";\nimport type { Node } from \"ts-morph\";\nimport type { RuntimeSchema } from \"@sembl/core\";\n\n/**\n * Where a field sits in the source, carried through type resolution and\n * decorator parsing.\n *\n * Every diagnostic names the class and the property, because a warning that\n * only says \"unsupported type\" sends the reader hunting through the whole\n * input directory for it.\n */\nexport interface FieldScope {\n /** Name of the @Schema class that owns the field. */\n className: string;\n /** Property name, dotted through synthesized inline objects. */\n propertyPath: string;\n /**\n * Declaration the type was read from. Members of an anonymous object type\n * have no declaration of their own to resolve against, so they are typed\n * relative to this node.\n */\n node: Node;\n /** Collector for diagnostics and synthesized schemas. */\n context: ExtractionContext;\n}\n\n/**\n * An `object` field type pointing at another schema by id, remembered so the\n * id can be checked once every source file has been visited.\n */\ninterface NestedReference {\n className: string;\n propertyPath: string;\n nestedSchemaId: string;\n /** Source text of the type, for a diagnostic the reader can act on. */\n typeText: string;\n}\n\n/**\n * Accumulates everything an extraction produces beyond the schemas themselves:\n * diagnostics, schemas synthesized for anonymous object types, and the nested\n * schema references that can only be validated once all files are visited.\n */\nexport interface ExtractionContext {\n /** Diagnostics raised so far, in discovery order. */\n readonly warnings: readonly string[];\n /** Schemas synthesized for anonymous inline object types, keyed by id. */\n readonly synthesizedSchemas: Readonly<Record<string, RuntimeSchema>>;\n /** Record a diagnostic against a field. */\n warn(scope: FieldScope, message: string): void;\n /** Add a schema synthesized for an inline object type. */\n registerSynthesizedSchema(schema: RuntimeSchema): void;\n /** Note that a field points at `nestedSchemaId`, to be checked later. */\n recordNestedReference(\n scope: FieldScope,\n nestedSchemaId: string,\n typeText: string,\n ): void;\n /**\n * Warn about every recorded reference whose target is not among\n * `knownSchemaIds`. Call once, after all classes have been visited.\n */\n reportUnresolvedNestedSchemas(knownSchemaIds: ReadonlySet<string>): void;\n}\n\n/**\n * Create an empty {@link ExtractionContext} for a single extraction run.\n */\nexport function createExtractionContext(): ExtractionContext {\n const warnings: string[] = [];\n const synthesizedSchemas: Record<string, RuntimeSchema> = {};\n const nestedReferences: NestedReference[] = [];\n\n return {\n warnings,\n synthesizedSchemas,\n\n warn(scope, message) {\n warnings.push(`${scope.className}.${scope.propertyPath}: ${message}`);\n },\n\n registerSynthesizedSchema(schema) {\n synthesizedSchemas[schema.id] = schema;\n },\n\n recordNestedReference(scope, nestedSchemaId, typeText) {\n nestedReferences.push({\n className: scope.className,\n propertyPath: scope.propertyPath,\n nestedSchemaId,\n typeText,\n });\n },\n\n reportUnresolvedNestedSchemas(knownSchemaIds) {\n for (const reference of nestedReferences) {\n if (knownSchemaIds.has(reference.nestedSchemaId)) {\n continue;\n }\n // A named object type that is not a @Schema class — a `Date`, a `Map`,\n // a plain interface — resolves to an id nothing in the bundle answers\n // to, and emits as an object with no properties. Nothing fails; the\n // field just comes back empty at runtime.\n warnings.push(\n `${reference.className}.${reference.propertyPath}: type \\`${reference.typeText}\\` ` +\n `resolves to nested schema \"${reference.nestedSchemaId}\", which is not a ` +\n `@Schema-decorated class in this extraction. It will emit as an object with ` +\n `no properties. Decorate it with @Schema if it is yours to change, or use a type ` +\n `the contract supports — a \\`Date\\`, for instance, extracts as an ISO-8601 string.`,\n );\n }\n },\n };\n}\n\n/**\n * Derive the id of a schema synthesized from an anonymous object type.\n *\n * The id becomes a filename and an exported binding in the generated output\n * (`<id>.schema.ts`, `<id>Schema`), so it has to be a valid identifier. The\n * path prefix keeps generated files traceable back to the declaration they\n * came from; the digest of the resolved shape makes the id collision-resistant\n * against a hand-written @Schema class and stable across runs and machines —\n * it is derived from the resolved fields, never from absolute source paths.\n */\nexport function synthesizedSchemaId(\n scope: FieldScope,\n structuralSignature: string,\n): string {\n const path = `${scope.className}_${scope.propertyPath}`.replace(\n /[^A-Za-z0-9_]/g,\n \"_\",\n );\n const digest = createHash(\"sha256\")\n .update(structuralSignature)\n .digest(\"hex\")\n .slice(0, 8);\n return `${path}__${digest}`;\n}\n","import type { Type } from \"ts-morph\";\nimport type { FieldDescriptor, FieldType } from \"@sembl/core\";\nimport { synthesizedSchemaId, type FieldScope } from \"./extraction-context.js\";\n\n/**\n * Report a type the schema contract cannot express and fall back to a string.\n *\n * The fallback keeps the rest of the extraction going, but the warning is the\n * point: a field silently mistyped as a string still builds, still validates,\n * and only shows up as a wrong extraction at runtime.\n */\nfunction reportUnsupported(\n type: Type,\n scope: FieldScope,\n reason: string,\n): FieldType {\n scope.context.warn(\n scope,\n `unsupported type \\`${type.getText(scope.node)}\\` — ${reason}. Falling back to string.`,\n );\n return { kind: \"string\" };\n}\n\n/**\n * Build a schema for an anonymous object type (`{ description: string }`) and\n * register it so it is emitted like any hand-written one.\n *\n * Without this an inline type resolves to an id no schema answers to, and the\n * field reaches the model as an object with no properties.\n */\nfunction resolveInlineObjectType(type: Type, scope: FieldScope): FieldType {\n const properties = type.getProperties();\n if (properties.length === 0) {\n // An index signature declares no properties to extract, and FieldType has\n // no map kind to express one. The contract is frozen, so there is nothing\n // correct to emit — only something to say.\n const isMap =\n type.getStringIndexType() !== undefined ||\n type.getNumberIndexType() !== undefined;\n return reportUnsupported(\n type,\n scope,\n isMap\n ? \"a map with an index signature has no FieldType equivalent, so its entries cannot be \" +\n \"described to the model; declare a @Schema class with the keys you expect, or take \" +\n \"the value as a JSON string and parse it yourself\"\n : \"an object type with no properties has nothing to extract\",\n );\n }\n\n const fields: FieldDescriptor[] = properties.map((property) => {\n const name = property.getName();\n return {\n name,\n // Members of an inline type carry no @Describe, so the owning field's\n // description is the only semantics the model gets for them.\n description: \"\",\n type: resolveFieldType(property.getTypeAtLocation(scope.node), {\n ...scope,\n propertyPath: `${scope.propertyPath}.${name}`,\n }),\n required: !property.isOptional(),\n };\n });\n\n // Hash the resolved fields rather than the source text: type text can embed\n // absolute import paths, which would make the id differ between machines.\n const id = synthesizedSchemaId(\n scope,\n JSON.stringify(fields.map((f) => [f.name, f.required, f.type])),\n );\n scope.context.registerSynthesizedSchema({\n id,\n description: `Inline object type declared at ${scope.className}.${scope.propertyPath}.`,\n fields,\n });\n return { kind: \"object\", nestedSchemaId: id };\n}\n\n/**\n * Resolve a TypeScript type to a FieldType descriptor.\n *\n * Maps TS types to the schema type system: string, number, boolean, array,\n * object, enum. Anything the contract cannot express is reported through\n * `scope.context` rather than quietly coerced.\n */\nexport function resolveFieldType(type: Type, scope: FieldScope): FieldType {\n // Optional and nullable fields arrive as unions with undefined/null. The\n // question mark already carries optionality, so resolve the value type.\n if (type.isUnion()) {\n const members = type\n .getUnionTypes()\n .filter((t) => !t.isUndefined() && !t.isNull());\n\n if (members.length === 1) {\n return resolveFieldType(members[0], scope);\n }\n\n if (members.length === 0) {\n return reportUnsupported(type, scope, \"there is no value type to extract\");\n }\n\n if (members.every((t) => t.isStringLiteral())) {\n return {\n kind: \"enum\",\n values: members.map((t) => t.getLiteralValue() as string),\n };\n }\n\n // `boolean` is modelled as `true | false`, so an optional boolean would\n // otherwise look like a mixed union and fall through to the fallback.\n if (members.every((t) => t.isBoolean() || t.isBooleanLiteral())) {\n return { kind: \"boolean\" };\n }\n\n // A numeric enum, or a union of number literals, widens to number:\n // FieldType has no numeric enum kind to narrow it to.\n if (members.every((t) => t.isNumber() || t.isNumberLiteral())) {\n return { kind: \"number\" };\n }\n\n return reportUnsupported(\n type,\n scope,\n \"a union mixing several kinds of value has no single FieldType; split it into \" +\n \"separate fields, or narrow it to one kind\",\n );\n }\n\n if (type.isString() || type.isStringLiteral()) {\n return { kind: \"string\" };\n }\n\n if (type.isNumber() || type.isNumberLiteral()) {\n return { kind: \"number\" };\n }\n\n if (type.isBoolean() || type.isBooleanLiteral()) {\n return { kind: \"boolean\" };\n }\n\n if (type.isArray()) {\n const elementType = type.getArrayElementTypeOrThrow();\n return { kind: \"array\", items: resolveFieldType(elementType, scope) };\n }\n\n if (type.isEnum()) {\n const members = type\n .getUnionTypes()\n .map((t) => t.getLiteralValue())\n .filter((v): v is string => typeof v === \"string\");\n if (members.length > 0) {\n return { kind: \"enum\", values: members };\n }\n return reportUnsupported(\n type,\n scope,\n \"its members are not string values, so they cannot be offered to the model as an enum\",\n );\n }\n\n if (type.isObject()) {\n const symbol = type.getSymbol() ?? type.getAliasSymbol();\n const typeName = symbol?.getName();\n if (typeName && typeName !== \"__type\" && typeName !== \"Object\") {\n // Assume a named object type is another @Schema class. Whether it really\n // is one cannot be known until every file has been visited, so record it\n // for the check in reportUnresolvedNestedSchemas.\n scope.context.recordNestedReference(\n scope,\n typeName,\n type.getText(scope.node),\n );\n return { kind: \"object\", nestedSchemaId: typeName };\n }\n return resolveInlineObjectType(type, scope);\n }\n\n return reportUnsupported(\n type,\n scope,\n \"it maps to none of string, number, boolean, array, enum, or a @Schema class\",\n );\n}\n","import type { ClassDeclaration } from \"ts-morph\";\nimport type { RuntimeSchema, FieldDescriptor, FieldType } from \"@sembl/core\";\nimport {\n parseSchemaDecorator,\n parseDescribeDecorator,\n parseConstrainDecorator,\n parseValuesFromDecorator,\n} from \"./decorator-parser.js\";\nimport { resolveFieldType } from \"./type-resolver.js\";\nimport type { ExtractionContext, FieldScope } from \"./extraction-context.js\";\n\n/**\n * Render a FieldType as something readable in a diagnostic.\n */\nfunction describeKind(type: FieldType): string {\n return type.kind === \"array\" ? `${describeKind(type.items)}[]` : type.kind;\n}\n\n/**\n * Point a resolved field type at a runtime-resolved enum source.\n *\n * Only a string, or an array of strings, has values for a source to constrain.\n * Anywhere else the decorator is on the wrong field — a mistake worth hearing\n * about at build time rather than discovering as an annotation that did\n * nothing, so the type is left alone and the field still extracts.\n */\nfunction applyValuesFrom(\n type: FieldType,\n sourceId: string,\n scope: FieldScope,\n): FieldType {\n if (type.kind === \"string\") {\n return { kind: \"dynamicEnum\", sourceId };\n }\n if (type.kind === \"array\" && type.items.kind === \"string\") {\n return { kind: \"array\", items: { kind: \"dynamicEnum\", sourceId } };\n }\n scope.context.warn(\n scope,\n `@ValuesFrom(\"${sourceId}\") applies to a string or string[] field, but this field ` +\n `resolved to ${describeKind(type)}. Leaving the type unchanged.`,\n );\n return type;\n}\n\n/**\n * Visit a class declaration and extract a RuntimeSchema if it has @Schema decorator.\n * Returns undefined if the class is not decorated with @Schema.\n *\n * Diagnostics and schemas synthesized for inline object types are collected\n * into `context` rather than thrown, so one questionable field does not stop\n * the rest of the extraction.\n */\nexport function visitClass(\n classDecl: ClassDeclaration,\n context: ExtractionContext,\n): RuntimeSchema | undefined {\n const description = parseSchemaDecorator(classDecl);\n if (description === undefined) {\n return undefined;\n }\n\n const className = classDecl.getName();\n if (!className) {\n return undefined;\n }\n\n const fields: FieldDescriptor[] = [];\n\n for (const prop of classDecl.getProperties()) {\n const fieldDescription = parseDescribeDecorator(prop);\n if (fieldDescription === undefined) {\n continue;\n }\n\n const name = prop.getName();\n const isOptional = prop.hasQuestionToken();\n const scope: FieldScope = {\n className,\n propertyPath: name,\n node: prop,\n context,\n };\n\n let type = resolveFieldType(prop.getType(), scope);\n\n // @ValuesFrom rewrites the resolved type, so it has to run after the type\n // is known — it is the declared TS type that decides whether the source\n // can apply at all.\n const sourceId = parseValuesFromDecorator(prop, scope);\n if (sourceId !== undefined) {\n type = applyValuesFrom(type, sourceId, scope);\n }\n\n const constraints = parseConstrainDecorator(prop, scope);\n\n fields.push({\n name,\n description: fieldDescription,\n type,\n required: !isOptional,\n ...(constraints !== undefined ? { constraints } : {}),\n });\n }\n\n return {\n id: className,\n description,\n fields,\n };\n}\n","import { Project, ScriptTarget } from \"ts-morph\";\nimport type { RuntimeSchema } from \"@sembl/core\";\nimport { visitClass } from \"./class-visitor.js\";\nimport { createExtractionContext } from \"./extraction-context.js\";\nimport type { ExtractionResult } from \"../types.js\";\n\nexport interface ExtractOptions {\n /** Glob patterns for source files */\n filePatterns: string[];\n /** Optional tsconfig.json path */\n tsconfigPath?: string;\n}\n\n/**\n * Extract all @Schema-decorated classes from the given source files.\n *\n * Returns the schemas alongside any warnings raised while resolving them —\n * unsupported field types, annotations that could not be read. Extraction\n * never throws on a bad field, so the warnings are the only signal that the\n * emitted schemas are not what the source described.\n */\nexport function extractSchemas(options: ExtractOptions): ExtractionResult {\n const project = new Project({\n tsConfigFilePath: options.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n compilerOptions: {\n experimentalDecorators: true,\n strict: true,\n // Without a target the default lib is ES5, so anything newer — `Map`,\n // `Set` — resolves to `any` and lands in the unsupported-type warning as\n // \"any\", naming a type the author never wrote.\n target: ScriptTarget.ES2022,\n },\n });\n\n // Add source files matching the patterns\n for (const pattern of options.filePatterns) {\n project.addSourceFilesAtPaths(pattern);\n }\n\n const context = createExtractionContext();\n const schemas: Record<string, RuntimeSchema> = {};\n\n for (const sourceFile of project.getSourceFiles()) {\n for (const classDecl of sourceFile.getClasses()) {\n const schema = visitClass(classDecl, context);\n if (schema) {\n schemas[schema.id] = schema;\n }\n }\n }\n\n // Schemas synthesized for inline object types are emitted like any other, so\n // the prompt builder and the JSON Schema converter can resolve them by id.\n for (const [id, schema] of Object.entries(context.synthesizedSchemas)) {\n schemas[id] = schema;\n }\n\n // Only now is the full set of ids known, so a nested reference to a class\n // declared in a file visited later is not mistaken for a dangling one.\n context.reportUnresolvedNestedSchemas(new Set(Object.keys(schemas)));\n\n return { schemas, warnings: [...context.warnings] };\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { RuntimeSchema, SchemaBundle } from \"@sembl/core\";\n\n/**\n * Serialize a RuntimeSchema to a TypeScript source string.\n */\nfunction schemaToSource(schema: RuntimeSchema): string {\n const json = JSON.stringify(schema, null, 2);\n return `// Auto-generated by sembl extract — do not edit\nimport type { RuntimeSchema } from \"@sembl/core\";\n\nexport const ${schema.id}Schema: RuntimeSchema = ${json};\n`;\n}\n\n/**\n * Emit a SchemaBundle as a set of individual .schema.ts files\n * plus a bundle index file.\n */\nexport function emitSchemas(bundle: SchemaBundle, outputDir: string): string[] {\n mkdirSync(outputDir, { recursive: true });\n\n const emittedFiles: string[] = [];\n const schemaIds: string[] = [];\n\n for (const [id, schema] of Object.entries(bundle.schemas)) {\n const fileName = `${id}.schema.ts`;\n const filePath = join(outputDir, fileName);\n writeFileSync(filePath, schemaToSource(schema), \"utf-8\");\n emittedFiles.push(filePath);\n schemaIds.push(id);\n }\n\n // Emit bundle index file\n const indexLines = [\n \"// Auto-generated by sembl extract — do not edit\",\n 'import type { SchemaBundle } from \"@sembl/core\";',\n \"\",\n ];\n\n for (const id of schemaIds) {\n indexLines.push(`import { ${id}Schema } from \"./${id}.schema.js\";`);\n }\n\n indexLines.push(\"\");\n indexLines.push(\"export const bundle: SchemaBundle = {\");\n indexLines.push(\" schemas: {\");\n for (const id of schemaIds) {\n indexLines.push(` ${id}: ${id}Schema,`);\n }\n indexLines.push(\" },\");\n indexLines.push(\"};\");\n indexLines.push(\"\");\n\n // Re-export individual schemas\n for (const id of schemaIds) {\n indexLines.push(`export { ${id}Schema } from \"./${id}.schema.js\";`);\n }\n indexLines.push(\"\");\n\n const indexPath = join(outputDir, \"index.ts\");\n writeFileSync(indexPath, indexLines.join(\"\\n\"), \"utf-8\");\n emittedFiles.push(indexPath);\n\n return emittedFiles;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ emitSchemas,
4
+ extractSchemas
5
+ } from "../chunk-Y4FKH2ZU.js";
6
+
7
+ // src/cli/index.ts
8
+ import { Command } from "commander";
9
+
10
+ // src/cli/commands/extract.ts
11
+ import { resolve } from "path";
12
+ import { glob } from "glob";
13
+ async function extractCommand(options) {
14
+ const inputDir = resolve(options.input);
15
+ const outputDir = resolve(options.output);
16
+ const files = await glob("**/*.ts", {
17
+ cwd: inputDir,
18
+ absolute: true,
19
+ ignore: ["**/*.d.ts", "**/*.schema.ts", "**/node_modules/**"]
20
+ });
21
+ if (files.length === 0) {
22
+ console.error(`sembl extract: no TypeScript files found in ${inputDir}`);
23
+ return { schemaCount: 0, emittedFiles: [], warnings: [], exitCode: 1 };
24
+ }
25
+ console.log(`Found ${files.length} source file(s) in ${inputDir}`);
26
+ const result = extractSchemas({
27
+ filePatterns: files,
28
+ tsconfigPath: options.tsconfig
29
+ });
30
+ for (const warning of result.warnings) {
31
+ console.error(`sembl extract: warning: ${warning}`);
32
+ }
33
+ const schemaCount = Object.keys(result.schemas).length;
34
+ if (schemaCount === 0) {
35
+ console.error(
36
+ `sembl extract: no @Schema-decorated classes found in ${inputDir}`
37
+ );
38
+ return {
39
+ schemaCount: 0,
40
+ emittedFiles: [],
41
+ warnings: result.warnings,
42
+ exitCode: 1
43
+ };
44
+ }
45
+ const emitted = emitSchemas(result, outputDir);
46
+ console.log(
47
+ `Extracted ${schemaCount} schema(s), emitted ${emitted.length} file(s) to ${outputDir}`
48
+ );
49
+ const failOnWarnings = options.strict === true && result.warnings.length > 0;
50
+ if (result.warnings.length > 0) {
51
+ console.error(
52
+ `sembl extract: ${result.warnings.length} warning(s).` + (failOnWarnings ? " Failing because --strict is set." : "")
53
+ );
54
+ }
55
+ return {
56
+ schemaCount,
57
+ emittedFiles: emitted,
58
+ warnings: result.warnings,
59
+ exitCode: failOnWarnings ? 1 : 0
60
+ };
61
+ }
62
+
63
+ // src/cli/index.ts
64
+ var program = new Command();
65
+ program.name("sembl").description("SEMBL schema compiler \u2014 extract runtime schemas from decorated TypeScript classes").version("0.1.0");
66
+ program.command("extract").description("Extract @Schema-decorated classes into RuntimeSchema files").requiredOption("-i, --input <path>", "Input directory containing decorated schema classes").requiredOption("-o, --output <path>", "Output directory for generated .schema.ts files").option("--tsconfig <path>", "Path to tsconfig.json").option("--strict", "Exit non-zero if extraction produced any warnings").action(async (options) => {
67
+ const result = await extractCommand({
68
+ input: options.input,
69
+ output: options.output,
70
+ tsconfig: options.tsconfig,
71
+ strict: options.strict
72
+ });
73
+ process.exitCode = result.exitCode;
74
+ });
75
+ program.parse();
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/extract.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { extractCommand } from \"./commands/extract.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"sembl\")\n .description(\"SEMBL schema compiler — extract runtime schemas from decorated TypeScript classes\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"extract\")\n .description(\"Extract @Schema-decorated classes into RuntimeSchema files\")\n .requiredOption(\"-i, --input <path>\", \"Input directory containing decorated schema classes\")\n .requiredOption(\"-o, --output <path>\", \"Output directory for generated .schema.ts files\")\n .option(\"--tsconfig <path>\", \"Path to tsconfig.json\")\n .option(\"--strict\", \"Exit non-zero if extraction produced any warnings\")\n .action(async (options) => {\n const result = await extractCommand({\n input: options.input,\n output: options.output,\n tsconfig: options.tsconfig,\n strict: options.strict,\n });\n // Set the code rather than exiting, so buffered stdout/stderr still flush.\n process.exitCode = result.exitCode;\n });\n\nprogram.parse();\n","import { resolve } from \"node:path\";\nimport { glob } from \"glob\";\nimport { extractSchemas } from \"../../extractor/ast-extractor.js\";\nimport { emitSchemas } from \"../../generator/schema-emitter.js\";\n\nexport interface ExtractCommandOptions {\n input: string;\n output: string;\n tsconfig?: string;\n /** Treat extraction warnings as errors. For CI and pre-build hooks. */\n strict?: boolean;\n}\n\n/**\n * What the command did. Returned rather than exited on, so the process owns\n * its exit code in one place and tests can assert without spawning.\n */\nexport interface ExtractCommandResult {\n /** Number of schemas emitted, including any synthesized for inline types. */\n schemaCount: number;\n /** Absolute paths of the files written. */\n emittedFiles: string[];\n /** Warnings raised during extraction, in discovery order. */\n warnings: string[];\n /** 0 on success; 1 on a misconfiguration, or on any warning under --strict. */\n exitCode: number;\n}\n\nexport async function extractCommand(\n options: ExtractCommandOptions,\n): Promise<ExtractCommandResult> {\n const inputDir = resolve(options.input);\n const outputDir = resolve(options.output);\n\n // Find all .ts files in the input directory\n const files = await glob(\"**/*.ts\", {\n cwd: inputDir,\n absolute: true,\n ignore: [\"**/*.d.ts\", \"**/*.schema.ts\", \"**/node_modules/**\"],\n });\n\n if (files.length === 0) {\n // Extract runs ahead of the build, and the build imports the bundle this\n // step writes. Succeeding quietly here just moves the failure to a later,\n // less legible \"cannot find ./generated/index.js\".\n console.error(`sembl extract: no TypeScript files found in ${inputDir}`);\n return { schemaCount: 0, emittedFiles: [], warnings: [], exitCode: 1 };\n }\n\n console.log(`Found ${files.length} source file(s) in ${inputDir}`);\n\n const result = extractSchemas({\n filePatterns: files,\n tsconfigPath: options.tsconfig,\n });\n\n // Warnings go to stderr so they survive a pipeline that captures stdout, and\n // are printed before the summary so the last line is the outcome.\n for (const warning of result.warnings) {\n console.error(`sembl extract: warning: ${warning}`);\n }\n\n const schemaCount = Object.keys(result.schemas).length;\n if (schemaCount === 0) {\n console.error(\n `sembl extract: no @Schema-decorated classes found in ${inputDir}`,\n );\n return {\n schemaCount: 0,\n emittedFiles: [],\n warnings: result.warnings,\n exitCode: 1,\n };\n }\n\n const emitted = emitSchemas(result, outputDir);\n console.log(\n `Extracted ${schemaCount} schema(s), emitted ${emitted.length} file(s) to ${outputDir}`,\n );\n\n const failOnWarnings = options.strict === true && result.warnings.length > 0;\n if (result.warnings.length > 0) {\n console.error(\n `sembl extract: ${result.warnings.length} warning(s).` +\n (failOnWarnings ? \" Failing because --strict is set.\" : \"\"),\n );\n }\n\n return {\n schemaCount,\n emittedFiles: emitted,\n warnings: result.warnings,\n exitCode: failOnWarnings ? 1 : 0,\n };\n}\n"],"mappings":";;;;;;;AAAA,SAAS,eAAe;;;ACAxB,SAAS,eAAe;AACxB,SAAS,YAAY;AA2BrB,eAAsB,eACpB,SAC+B;AAC/B,QAAM,WAAW,QAAQ,QAAQ,KAAK;AACtC,QAAM,YAAY,QAAQ,QAAQ,MAAM;AAGxC,QAAM,QAAQ,MAAM,KAAK,WAAW;AAAA,IAClC,KAAK;AAAA,IACL,UAAU;AAAA,IACV,QAAQ,CAAC,aAAa,kBAAkB,oBAAoB;AAAA,EAC9D,CAAC;AAED,MAAI,MAAM,WAAW,GAAG;AAItB,YAAQ,MAAM,+CAA+C,QAAQ,EAAE;AACvE,WAAO,EAAE,aAAa,GAAG,cAAc,CAAC,GAAG,UAAU,CAAC,GAAG,UAAU,EAAE;AAAA,EACvE;AAEA,UAAQ,IAAI,SAAS,MAAM,MAAM,sBAAsB,QAAQ,EAAE;AAEjE,QAAM,SAAS,eAAe;AAAA,IAC5B,cAAc;AAAA,IACd,cAAc,QAAQ;AAAA,EACxB,CAAC;AAID,aAAW,WAAW,OAAO,UAAU;AACrC,YAAQ,MAAM,2BAA2B,OAAO,EAAE;AAAA,EACpD;AAEA,QAAM,cAAc,OAAO,KAAK,OAAO,OAAO,EAAE;AAChD,MAAI,gBAAgB,GAAG;AACrB,YAAQ;AAAA,MACN,wDAAwD,QAAQ;AAAA,IAClE;AACA,WAAO;AAAA,MACL,aAAa;AAAA,MACb,cAAc,CAAC;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,QAAM,UAAU,YAAY,QAAQ,SAAS;AAC7C,UAAQ;AAAA,IACN,aAAa,WAAW,uBAAuB,QAAQ,MAAM,eAAe,SAAS;AAAA,EACvF;AAEA,QAAM,iBAAiB,QAAQ,WAAW,QAAQ,OAAO,SAAS,SAAS;AAC3E,MAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,YAAQ;AAAA,MACN,kBAAkB,OAAO,SAAS,MAAM,kBACrC,iBAAiB,sCAAsC;AAAA,IAC5D;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,cAAc;AAAA,IACd,UAAU,OAAO;AAAA,IACjB,UAAU,iBAAiB,IAAI;AAAA,EACjC;AACF;;;AD3FA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ,YAAY,wFAAmF,EAC/F,QAAQ,OAAO;AAElB,QACG,QAAQ,SAAS,EACjB,YAAY,4DAA4D,EACxE,eAAe,sBAAsB,qDAAqD,EAC1F,eAAe,uBAAuB,iDAAiD,EACvF,OAAO,qBAAqB,uBAAuB,EACnD,OAAO,YAAY,mDAAmD,EACtE,OAAO,OAAO,YAAY;AACzB,QAAM,SAAS,MAAM,eAAe;AAAA,IAClC,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,EAClB,CAAC;AAED,UAAQ,WAAW,OAAO;AAC5B,CAAC;AAEH,QAAQ,MAAM;","names":[]}
@@ -0,0 +1,181 @@
1
+ import { SchemaBundle, RuntimeSchema, FieldConstraints, FieldType } from '@sembl/core';
2
+ import { Node, ClassDeclaration, PropertyDeclaration, Type } from 'ts-morph';
3
+
4
+ /**
5
+ * Annotation extracted from a @Schema decorator.
6
+ */
7
+ interface SchemaAnnotation {
8
+ /** Class name */
9
+ className: string;
10
+ /** Description string from @Schema(description) */
11
+ description: string;
12
+ }
13
+ /**
14
+ * Annotation extracted from a @Describe decorator on a property.
15
+ */
16
+ interface FieldAnnotation {
17
+ /** Property name */
18
+ name: string;
19
+ /** Description string from @Describe(description) */
20
+ description: string;
21
+ /** Raw TypeScript type text */
22
+ rawType: string;
23
+ /** Whether the property is optional (?:) */
24
+ optional: boolean;
25
+ }
26
+ /**
27
+ * Configuration for the compiler.
28
+ */
29
+ interface CompilerConfig {
30
+ /** Glob patterns for input source files */
31
+ inputPatterns: string[];
32
+ /** Output directory for generated schema files */
33
+ outputDir: string;
34
+ /** Optional tsconfig path */
35
+ tsconfigPath?: string;
36
+ }
37
+ /**
38
+ * Result of extracting schemas from source files.
39
+ *
40
+ * Extends `SchemaBundle` so it can be handed straight to the emitter, and adds
41
+ * the diagnostics raised on the way — a field whose type the schema contract
42
+ * cannot express still emits, so the warnings are what tell the caller the
43
+ * output does not match the source.
44
+ */
45
+ interface ExtractionResult extends SchemaBundle {
46
+ /** Any warnings generated during extraction, in discovery order */
47
+ warnings: string[];
48
+ }
49
+
50
+ interface ExtractOptions {
51
+ /** Glob patterns for source files */
52
+ filePatterns: string[];
53
+ /** Optional tsconfig.json path */
54
+ tsconfigPath?: string;
55
+ }
56
+ /**
57
+ * Extract all @Schema-decorated classes from the given source files.
58
+ *
59
+ * Returns the schemas alongside any warnings raised while resolving them —
60
+ * unsupported field types, annotations that could not be read. Extraction
61
+ * never throws on a bad field, so the warnings are the only signal that the
62
+ * emitted schemas are not what the source described.
63
+ */
64
+ declare function extractSchemas(options: ExtractOptions): ExtractionResult;
65
+
66
+ /**
67
+ * Emit a SchemaBundle as a set of individual .schema.ts files
68
+ * plus a bundle index file.
69
+ */
70
+ declare function emitSchemas(bundle: SchemaBundle, outputDir: string): string[];
71
+
72
+ /**
73
+ * Where a field sits in the source, carried through type resolution and
74
+ * decorator parsing.
75
+ *
76
+ * Every diagnostic names the class and the property, because a warning that
77
+ * only says "unsupported type" sends the reader hunting through the whole
78
+ * input directory for it.
79
+ */
80
+ interface FieldScope {
81
+ /** Name of the @Schema class that owns the field. */
82
+ className: string;
83
+ /** Property name, dotted through synthesized inline objects. */
84
+ propertyPath: string;
85
+ /**
86
+ * Declaration the type was read from. Members of an anonymous object type
87
+ * have no declaration of their own to resolve against, so they are typed
88
+ * relative to this node.
89
+ */
90
+ node: Node;
91
+ /** Collector for diagnostics and synthesized schemas. */
92
+ context: ExtractionContext;
93
+ }
94
+ /**
95
+ * Accumulates everything an extraction produces beyond the schemas themselves:
96
+ * diagnostics, schemas synthesized for anonymous object types, and the nested
97
+ * schema references that can only be validated once all files are visited.
98
+ */
99
+ interface ExtractionContext {
100
+ /** Diagnostics raised so far, in discovery order. */
101
+ readonly warnings: readonly string[];
102
+ /** Schemas synthesized for anonymous inline object types, keyed by id. */
103
+ readonly synthesizedSchemas: Readonly<Record<string, RuntimeSchema>>;
104
+ /** Record a diagnostic against a field. */
105
+ warn(scope: FieldScope, message: string): void;
106
+ /** Add a schema synthesized for an inline object type. */
107
+ registerSynthesizedSchema(schema: RuntimeSchema): void;
108
+ /** Note that a field points at `nestedSchemaId`, to be checked later. */
109
+ recordNestedReference(scope: FieldScope, nestedSchemaId: string, typeText: string): void;
110
+ /**
111
+ * Warn about every recorded reference whose target is not among
112
+ * `knownSchemaIds`. Call once, after all classes have been visited.
113
+ */
114
+ reportUnresolvedNestedSchemas(knownSchemaIds: ReadonlySet<string>): void;
115
+ }
116
+ /**
117
+ * Create an empty {@link ExtractionContext} for a single extraction run.
118
+ */
119
+ declare function createExtractionContext(): ExtractionContext;
120
+ /**
121
+ * Derive the id of a schema synthesized from an anonymous object type.
122
+ *
123
+ * The id becomes a filename and an exported binding in the generated output
124
+ * (`<id>.schema.ts`, `<id>Schema`), so it has to be a valid identifier. The
125
+ * path prefix keeps generated files traceable back to the declaration they
126
+ * came from; the digest of the resolved shape makes the id collision-resistant
127
+ * against a hand-written @Schema class and stable across runs and machines —
128
+ * it is derived from the resolved fields, never from absolute source paths.
129
+ */
130
+ declare function synthesizedSchemaId(scope: FieldScope, structuralSignature: string): string;
131
+
132
+ /**
133
+ * Visit a class declaration and extract a RuntimeSchema if it has @Schema decorator.
134
+ * Returns undefined if the class is not decorated with @Schema.
135
+ *
136
+ * Diagnostics and schemas synthesized for inline object types are collected
137
+ * into `context` rather than thrown, so one questionable field does not stop
138
+ * the rest of the extraction.
139
+ */
140
+ declare function visitClass(classDecl: ClassDeclaration, context: ExtractionContext): RuntimeSchema | undefined;
141
+
142
+ /**
143
+ * Extract the @Schema description from a class declaration.
144
+ * Returns undefined if the class doesn't have a @Schema decorator.
145
+ */
146
+ declare function parseSchemaDecorator(classDecl: ClassDeclaration): string | undefined;
147
+ /**
148
+ * Extract the @Describe description from a property declaration.
149
+ * Returns undefined if the property doesn't have a @Describe decorator.
150
+ */
151
+ declare function parseDescribeDecorator(propDecl: PropertyDeclaration): string | undefined;
152
+ /**
153
+ * Extract the @Constrain bounds from a property declaration.
154
+ *
155
+ * The decorator's argument has to be an inline object literal: decorators are
156
+ * never evaluated, so a value that is not written out in source cannot be
157
+ * resolved. Unreadable and unknown entries are warned about and skipped
158
+ * individually, so one bad bound does not discard the rest.
159
+ *
160
+ * Returns undefined if there is no @Constrain decorator, or if nothing in it
161
+ * could be read.
162
+ */
163
+ declare function parseConstrainDecorator(propDecl: PropertyDeclaration, scope: FieldScope): FieldConstraints | undefined;
164
+ /**
165
+ * Extract the enum source id from a property's @ValuesFrom decorator.
166
+ *
167
+ * Returns undefined if there is no @ValuesFrom decorator, or if its argument
168
+ * is not a string literal.
169
+ */
170
+ declare function parseValuesFromDecorator(propDecl: PropertyDeclaration, scope: FieldScope): string | undefined;
171
+
172
+ /**
173
+ * Resolve a TypeScript type to a FieldType descriptor.
174
+ *
175
+ * Maps TS types to the schema type system: string, number, boolean, array,
176
+ * object, enum. Anything the contract cannot express is reported through
177
+ * `scope.context` rather than quietly coerced.
178
+ */
179
+ declare function resolveFieldType(type: Type, scope: FieldScope): FieldType;
180
+
181
+ export { type CompilerConfig, type ExtractOptions, type ExtractionContext, type ExtractionResult, type FieldAnnotation, type FieldScope, type SchemaAnnotation, createExtractionContext, emitSchemas, extractSchemas, parseConstrainDecorator, parseDescribeDecorator, parseSchemaDecorator, parseValuesFromDecorator, resolveFieldType, synthesizedSchemaId, visitClass };
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createExtractionContext,
4
+ emitSchemas,
5
+ extractSchemas,
6
+ parseConstrainDecorator,
7
+ parseDescribeDecorator,
8
+ parseSchemaDecorator,
9
+ parseValuesFromDecorator,
10
+ resolveFieldType,
11
+ synthesizedSchemaId,
12
+ visitClass
13
+ } from "./chunk-Y4FKH2ZU.js";
14
+ export {
15
+ createExtractionContext,
16
+ emitSchemas,
17
+ extractSchemas,
18
+ parseConstrainDecorator,
19
+ parseDescribeDecorator,
20
+ parseSchemaDecorator,
21
+ parseValuesFromDecorator,
22
+ resolveFieldType,
23
+ synthesizedSchemaId,
24
+ visitClass
25
+ };
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@sembl/compiler",
3
+ "version": "0.1.0",
4
+ "description": "Schema compiler for SEMBL: extracts runtime schemas from decorated TypeScript classes.",
5
+ "keywords": [
6
+ "llm",
7
+ "structured-output",
8
+ "json-schema",
9
+ "typescript",
10
+ "decorators",
11
+ "ast",
12
+ "compiler"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Sembl contributors",
16
+ "homepage": "https://github.com/nickrunner/sembl#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/nickrunner/sembl/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/nickrunner/sembl.git",
23
+ "directory": "packages/compiler"
24
+ },
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "sideEffects": false,
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "bin": {
44
+ "sembl": "./dist/cli/index.js"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "dependencies": {
51
+ "commander": "^12.0.0",
52
+ "glob": "^11.0.0",
53
+ "ts-morph": "^24.0.0",
54
+ "@sembl/core": "0.1.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^25.5.0",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.5.0"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "dev": "tsup --watch"
64
+ }
65
+ }