@toolproof-npm/schema 0.1.12
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 +58 -0
- package/dist/_lib/test.d.ts +1 -0
- package/dist/_lib/test.js +122 -0
- package/dist/_lib/types/ResourceData_Job.d.ts +97 -0
- package/dist/_lib/types/ResourceData_Job.js +1 -0
- package/dist/_lib/types/types.d.ts +659 -0
- package/dist/_lib/types/types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/schemas/Genesis.d.ts +2 -0
- package/dist/schemas/Genesis.js +2 -0
- package/dist/schemas/Genesis.json +1241 -0
- package/dist/schemas/Job.d.ts +2 -0
- package/dist/schemas/Job.js +2 -0
- package/dist/schemas/Job.json +161 -0
- package/dist/scripts/brandFactories.d.ts +25 -0
- package/dist/scripts/brandFactories.js +50 -0
- package/dist/scripts/extractSchemas.d.ts +1 -0
- package/dist/scripts/extractSchemas.js +173 -0
- package/dist/scripts/extractSubschemaWithDefs.d.ts +1 -0
- package/dist/scripts/extractSubschemaWithDefs.js +139 -0
- package/dist/scripts/generateResourceData.d.ts +1 -0
- package/dist/scripts/generateResourceData.js +132 -0
- package/dist/scripts/generateTypes.d.ts +1 -0
- package/dist/scripts/generateTypes.js +411 -0
- package/package.json +47 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { compileFromFile } from 'json-schema-to-typescript';
|
|
4
|
+
/**
|
|
5
|
+
* Generate a typed ResourceData variant where `extractedData` is typed to a specific schema
|
|
6
|
+
* extracted under `src/schemas/<name>.json`.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node ./dist/scripts/generateResourceData_js --name Job
|
|
9
|
+
*/
|
|
10
|
+
async function main() {
|
|
11
|
+
const { name } = parseArgs(process.argv.slice(2));
|
|
12
|
+
if (!name) {
|
|
13
|
+
console.error('Missing --name <SchemaBasename> argument');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
const schemasDir = path.join(projectRoot, 'src', 'schemas');
|
|
18
|
+
const inPath = path.join(schemasDir, `${name}.json`);
|
|
19
|
+
if (!fs.existsSync(inPath)) {
|
|
20
|
+
console.error(`Schema file not found: ${inPath}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// Basic validation against the expected shape of ExtractionSchema.
|
|
24
|
+
const raw = fs.readFileSync(inPath, 'utf8');
|
|
25
|
+
let parsed = null;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.error(`Failed to parse JSON schema ${inPath}:`, e);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Minimal checks that roughly match the ExtractionSchema constraints used elsewhere.
|
|
34
|
+
if (parsed.$schema && parsed.$schema !== 'https://json-schema.org/draft/2020-12/schema') {
|
|
35
|
+
console.warn(`Warning: schema $schema is '${parsed.$schema}', expected draft 2020-12. Proceeding anyway.`);
|
|
36
|
+
}
|
|
37
|
+
if (parsed.type && parsed.type !== 'object') {
|
|
38
|
+
console.warn(`Warning: ExtractionSchema usually has type: 'object' but this schema has type: '${parsed.type}'. Proceeding.`);
|
|
39
|
+
}
|
|
40
|
+
// Ensure output folder exists
|
|
41
|
+
const outDir = path.join(projectRoot, 'src', '_lib', 'types');
|
|
42
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
43
|
+
const outName = `ResourceData_${name}.d.ts`;
|
|
44
|
+
const outPath = path.join(outDir, outName);
|
|
45
|
+
const outJsName = `ResourceData_${name}.js`;
|
|
46
|
+
const outJsPath = path.join(outDir, outJsName);
|
|
47
|
+
// Build a composite schema in src/schemas so that local $ref paths are simple
|
|
48
|
+
const schemasOutDir = path.join(projectRoot, 'src', 'schemas');
|
|
49
|
+
const compositePath = path.join(schemasOutDir, `.composite.ResourceData_${name}.json`);
|
|
50
|
+
const compositeSchema = {
|
|
51
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
52
|
+
title: `ResourceData_${name}`,
|
|
53
|
+
type: 'object',
|
|
54
|
+
allOf: [
|
|
55
|
+
{ $ref: './Genesis.json#/$defs/ResourceDataMetaBase' },
|
|
56
|
+
{
|
|
57
|
+
type: 'object',
|
|
58
|
+
required: ['extractedData'],
|
|
59
|
+
properties: {
|
|
60
|
+
extractedData: { $ref: `./${name}.json` }
|
|
61
|
+
},
|
|
62
|
+
unevaluatedProperties: false
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
unevaluatedProperties: false
|
|
66
|
+
};
|
|
67
|
+
// Write composite schema
|
|
68
|
+
fs.writeFileSync(compositePath, JSON.stringify(compositeSchema, null, 2), 'utf8');
|
|
69
|
+
try {
|
|
70
|
+
// Compile to TypeScript declarations
|
|
71
|
+
let ts = await compileFromFile(compositePath, {
|
|
72
|
+
bannerComment: '',
|
|
73
|
+
declareExternallyReferenced: true,
|
|
74
|
+
unreachableDefinitions: true,
|
|
75
|
+
$refOptions: {
|
|
76
|
+
resolve: {
|
|
77
|
+
file: { order: 1 },
|
|
78
|
+
http: false,
|
|
79
|
+
https: false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// Remove noisy index signatures that make types too permissive
|
|
84
|
+
ts = ts.replace(/\n\s*\[k:\s*string\]:\s*unknown;\n/g, '\n');
|
|
85
|
+
// Ensure it is a module
|
|
86
|
+
if (!/\bexport\b/.test(ts)) {
|
|
87
|
+
ts += '\nexport {}\n';
|
|
88
|
+
}
|
|
89
|
+
const header = '// Auto-generated strict composite type. Do not edit.\n';
|
|
90
|
+
fs.writeFileSync(outPath, header + ts, 'utf8');
|
|
91
|
+
console.log(`Wrote ${outPath}`);
|
|
92
|
+
// Ensure a runtime-resolvable JS shim exists alongside the .d.ts for NodeNext resolution
|
|
93
|
+
if (!fs.existsSync(outJsPath)) {
|
|
94
|
+
fs.writeFileSync(outJsPath, 'export {}\n', 'utf8');
|
|
95
|
+
console.log(`Wrote ${outJsPath}`);
|
|
96
|
+
}
|
|
97
|
+
// Also copy both files into dist so consumers can resolve the module and its types
|
|
98
|
+
const distLibDir = path.join(projectRoot, 'dist', '_lib', 'types');
|
|
99
|
+
fs.mkdirSync(distLibDir, { recursive: true });
|
|
100
|
+
const distDtsPath = path.join(distLibDir, outName);
|
|
101
|
+
const distJsPath = path.join(distLibDir, outJsName);
|
|
102
|
+
fs.writeFileSync(distDtsPath, header + ts, 'utf8');
|
|
103
|
+
fs.writeFileSync(distJsPath, 'export {}\n', 'utf8');
|
|
104
|
+
console.log(`Wrote ${distDtsPath}`);
|
|
105
|
+
console.log(`Wrote ${distJsPath}`);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
// Cleanup composite schema file
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(compositePath);
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function parseArgs(args) {
|
|
116
|
+
let name;
|
|
117
|
+
for (let i = 0; i < args.length; i++) {
|
|
118
|
+
const a = args[i];
|
|
119
|
+
if (a === '--name') {
|
|
120
|
+
name = args[i + 1];
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
else if (a.startsWith('--name=')) {
|
|
124
|
+
name = a.split('=')[1];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { name };
|
|
128
|
+
}
|
|
129
|
+
main().catch((e) => {
|
|
130
|
+
console.error(e);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { compileFromFile } from 'json-schema-to-typescript';
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
const inputDir = path.join(projectRoot, 'src', 'schemas');
|
|
6
|
+
// We emit under src/_lib/types and dist/_lib/types
|
|
7
|
+
const srcLibTypesDir = path.join(projectRoot, 'src', '_lib', 'types');
|
|
8
|
+
const srcLibOutputPath = path.join(srcLibTypesDir, 'types.d.ts');
|
|
9
|
+
// Build an index of all schema files by their basename
|
|
10
|
+
// This supports location-independent $id values where folder segments were removed
|
|
11
|
+
function buildSchemaIndex(root) {
|
|
12
|
+
const index = new Map();
|
|
13
|
+
const stack = [root];
|
|
14
|
+
while (stack.length) {
|
|
15
|
+
const current = stack.pop();
|
|
16
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const full = path.join(current, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
// Skip dist or types output folders if present inside schemas (defensive)
|
|
21
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
22
|
+
continue;
|
|
23
|
+
stack.push(full);
|
|
24
|
+
}
|
|
25
|
+
else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
26
|
+
if (entry.name === '.combined-schema.json')
|
|
27
|
+
continue; // ignore temp file
|
|
28
|
+
const baseName = entry.name; // keep extension for direct mapping
|
|
29
|
+
if (index.has(baseName)) {
|
|
30
|
+
// Hard fail on collisions so they can be fixed explicitly
|
|
31
|
+
const existing = index.get(baseName);
|
|
32
|
+
throw new Error(`Schema basename collision detected for "${baseName}"\n` +
|
|
33
|
+
`First: ${existing}\n` +
|
|
34
|
+
`Second: ${full}\n` +
|
|
35
|
+
`Please rename one of the schemas to ensure unique basenames.`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
index.set(baseName, full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return index;
|
|
44
|
+
}
|
|
45
|
+
// List all schema files (relative to inputDir), excluding documentation and temp files
|
|
46
|
+
function listAllSchemaFiles(root) {
|
|
47
|
+
const files = [];
|
|
48
|
+
const stack = [root];
|
|
49
|
+
while (stack.length) {
|
|
50
|
+
const current = stack.pop();
|
|
51
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const full = path.join(current, entry.name);
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (entry.name === 'documentation' || entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
56
|
+
continue;
|
|
57
|
+
stack.push(full);
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
60
|
+
if (entry.name === '.combined-schema.json')
|
|
61
|
+
continue; // ignore temp
|
|
62
|
+
// buildersuce path relative to root with posix separators
|
|
63
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
64
|
+
files.push(rel);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
files.sort(); // deterministic order
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
async function main() {
|
|
72
|
+
// Ensure src/_lib/types exists (we no longer write to src/types to avoid duplication)
|
|
73
|
+
fs.mkdirSync(srcLibTypesDir, { recursive: true });
|
|
74
|
+
const parts = [];
|
|
75
|
+
parts.push('// Auto-generated from JSON schemas. Do not edit.\n');
|
|
76
|
+
// Precompute index for location-independent IDs (filename only after version segment)
|
|
77
|
+
const schemaIndex = buildSchemaIndex(inputDir);
|
|
78
|
+
const idToCanonical = {};
|
|
79
|
+
// Custom resolver to map our absolute schema IDs to local files, preventing HTTP fetches.
|
|
80
|
+
// Supports two patterns:
|
|
81
|
+
// 1. Legacy full-path IDs: https://schemas.toolproof.com/v0/genesis/Foo.json
|
|
82
|
+
// 2. Location-independent IDs: https://schemas.toolproof.com/v0/Foo.json
|
|
83
|
+
const toolproofResolver = {
|
|
84
|
+
order: 1,
|
|
85
|
+
canRead: (file) => {
|
|
86
|
+
const url = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
|
|
87
|
+
return (/^https?:\/\/schemas\.toolproof\.(documentation|com)\//i.test(url) ||
|
|
88
|
+
/^https?:\/\/toolproof\.com\/schemas\//i.test(url));
|
|
89
|
+
},
|
|
90
|
+
read: (file) => {
|
|
91
|
+
const url = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
|
|
92
|
+
// Strip hash part (anchors) for path resolution
|
|
93
|
+
const noHash = url.split('#')[0];
|
|
94
|
+
// Remove base domains
|
|
95
|
+
let rel = noHash
|
|
96
|
+
.replace(/^https?:\/\/schemas\.toolproof\.(documentation|com)\//i, '')
|
|
97
|
+
.replace(/^https?:\/\/toolproof\.com\/schemas\//i, '');
|
|
98
|
+
// Drop leading version segment (v0/, v1/, etc.) if present
|
|
99
|
+
rel = rel.replace(/^v\d+\//i, '');
|
|
100
|
+
// Resolve by basename only (location-independent IDs)
|
|
101
|
+
const fileName = path.basename(rel);
|
|
102
|
+
const indexed = schemaIndex.get(fileName);
|
|
103
|
+
if (indexed) {
|
|
104
|
+
return fs.readFileSync(indexed, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Toolproof resolver: could not locate schema for URL "${url}". ` +
|
|
107
|
+
`Tried basename "${fileName}" in schema index. Ensure unique basenames and correct $id.`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Files to include in the combined schema (auto-discovered, excludes documentation)
|
|
111
|
+
const toCompile = listAllSchemaFiles(inputDir);
|
|
112
|
+
// Build definitions for a combined schema that references each file.
|
|
113
|
+
const definitions = {};
|
|
114
|
+
const includedNames = [];
|
|
115
|
+
for (const fileName of toCompile) {
|
|
116
|
+
const p = path.join(inputDir, fileName);
|
|
117
|
+
if (!fs.existsSync(p)) {
|
|
118
|
+
console.warn(`Schema file missing, skipping: ${p}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Definition key: basename without extension, with path segments removed
|
|
122
|
+
const base = path.basename(fileName, '.json');
|
|
123
|
+
// Prefer the schema's declared $id so all references point to the same absolute URI
|
|
124
|
+
// This helps json-schema-to-typescript dedupe declarations instead of emitting FooJson and FooJson1
|
|
125
|
+
let refValue = `./${fileName}`;
|
|
126
|
+
try {
|
|
127
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (parsed && typeof parsed.$id === 'string' && parsed.$id.trim()) {
|
|
130
|
+
refValue = parsed.$id.trim();
|
|
131
|
+
idToCanonical[refValue] = base + 'Json';
|
|
132
|
+
}
|
|
133
|
+
// Promote this file's $defs to top-level combined $defs so each gets its own exported type
|
|
134
|
+
if (parsed && parsed.$defs && typeof parsed.$defs === 'object') {
|
|
135
|
+
const defNames = Object.keys(parsed.$defs).filter((k) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k));
|
|
136
|
+
for (const defName of defNames) {
|
|
137
|
+
// Prefer bare def name; if collision, namespace with file base
|
|
138
|
+
let entryName = defName;
|
|
139
|
+
if (definitions[entryName]) {
|
|
140
|
+
entryName = `${base}_${defName}`;
|
|
141
|
+
}
|
|
142
|
+
if (!definitions[entryName]) {
|
|
143
|
+
definitions[entryName] = { $ref: `${refValue}#/$defs/${defName}` };
|
|
144
|
+
includedNames.push(entryName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
// If parsing fails, fall back to relative ref; proceed gracefully
|
|
151
|
+
}
|
|
152
|
+
definitions[base] = { $ref: refValue };
|
|
153
|
+
includedNames.push(base);
|
|
154
|
+
}
|
|
155
|
+
if (includedNames.length === 0) {
|
|
156
|
+
console.warn('No schema files found to compile. Nothing to do.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const combinedSchema = {
|
|
160
|
+
$id: 'combined-entry',
|
|
161
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
162
|
+
$defs: definitions,
|
|
163
|
+
anyOf: includedNames.map((n) => ({ $ref: `#/$defs/${n}` }))
|
|
164
|
+
};
|
|
165
|
+
console.log('combinedSchema:', JSON.stringify(combinedSchema, null, 2));
|
|
166
|
+
// Write combined schema to a temp file inside inputDir so relative $ref resolves.
|
|
167
|
+
const combinedPath = path.join(inputDir, '.combined-schema.json');
|
|
168
|
+
try {
|
|
169
|
+
fs.writeFileSync(combinedPath, JSON.stringify(combinedSchema, null, 2), 'utf8');
|
|
170
|
+
// Compile the single combined schema; referenced schemas will be emitted once.
|
|
171
|
+
let ts = await compileFromFile(combinedPath, {
|
|
172
|
+
bannerComment: '',
|
|
173
|
+
//
|
|
174
|
+
declareExternallyReferenced: true,
|
|
175
|
+
unreachableDefinitions: true,
|
|
176
|
+
// Forward ref parser options so absolute $id/$ref URLs resolve from local files
|
|
177
|
+
$refOptions: {
|
|
178
|
+
// Don’t go to the network; we provide a local resolver for our domain
|
|
179
|
+
resolve: {
|
|
180
|
+
file: { order: 2 },
|
|
181
|
+
http: false,
|
|
182
|
+
https: false,
|
|
183
|
+
toolproof: toolproofResolver
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Remove permissive index signatures that make interfaces open-ended.
|
|
188
|
+
// Keep meaningful map-like signatures (e.g., `[k: string]: RoleLiteral`) intact.
|
|
189
|
+
// Robust single-pass: delete the entire line (with optional trailing newline) where the signature appears.
|
|
190
|
+
// This avoids introducing extra blank lines while handling CRLF/LF and varying indentation.
|
|
191
|
+
ts = ts.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*(?:\r?\n)?/gm, '');
|
|
192
|
+
// Prune verbose type/interface names buildersuced from absolute $id URLs.
|
|
193
|
+
// Deterministic pruning based on original $id -> baseName map
|
|
194
|
+
// This avoids heuristic truncation that dropped prefixes like Resource / Workflow.
|
|
195
|
+
function idToGeneratedIdentifier(id) {
|
|
196
|
+
// json-schema-to-typescript seems to create a PascalCase of the URL with protocol prefix
|
|
197
|
+
// Simplified reconstruction: 'https://' => 'Https', then capitalize path & host segments
|
|
198
|
+
const noProto = id.replace(/^https?:\/\//i, '');
|
|
199
|
+
const tokens = noProto
|
|
200
|
+
.split(/[\/#?.=&_-]+/)
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.map((t) => t.replace(/[^A-Za-z0-9]/g, ''))
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
|
|
205
|
+
return 'Https' + tokens.join('') + 'Json';
|
|
206
|
+
}
|
|
207
|
+
// Perform replacements for known IDs
|
|
208
|
+
for (const [id, canonical] of Object.entries(idToCanonical)) {
|
|
209
|
+
const longName = idToGeneratedIdentifier(id);
|
|
210
|
+
if (longName === canonical)
|
|
211
|
+
continue; // already minimal
|
|
212
|
+
const re = new RegExp(`\\b${longName}\\b`, 'g');
|
|
213
|
+
ts = ts.replace(re, canonical);
|
|
214
|
+
}
|
|
215
|
+
// Remove version prefixes inside any remaining long identifiers: Https...V0... -> remove V0 if followed by capital
|
|
216
|
+
ts = ts.replace(/(Https[A-Za-z0-9_]*?)V\d+([A-Z])/g, '$1$2');
|
|
217
|
+
// Final cleanup: aggressively strip the domain prefix `HttpsSchemasToolproofCom` from ALL identifiers.
|
|
218
|
+
// This is safe because those long names are only artifacts of json-schema-to-typescript; base names don't start with that sequence.
|
|
219
|
+
ts = ts.replace(/\bHttpsSchemasToolproofCom(?=[A-Z])/g, '');
|
|
220
|
+
// Remove accidental duplicate union entries in CombinedEntry after shortening.
|
|
221
|
+
ts = ts.replace(/export type CombinedEntry =([\s\S]*?);/, (m, body) => {
|
|
222
|
+
const lines = body.split(/\n/);
|
|
223
|
+
const seen2 = new Set();
|
|
224
|
+
const kept = [];
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
const match = /^\|\s*([A-Za-z0-9_]+)\b/.exec(trimmed);
|
|
228
|
+
if (match) {
|
|
229
|
+
const name = match[1];
|
|
230
|
+
if (!seen2.has(name)) {
|
|
231
|
+
seen2.add(name);
|
|
232
|
+
kept.push(' | ' + name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (trimmed.length) {
|
|
236
|
+
kept.push(line);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return 'export type CombinedEntry =\n' + kept.join('\n') + ';';
|
|
240
|
+
});
|
|
241
|
+
// If the compiler returned nothing (can happen if everything is externalized),
|
|
242
|
+
// try a direct compile of the primary catalog (Genesis.json) as a fallback.
|
|
243
|
+
if (!ts || !ts.trim()) {
|
|
244
|
+
const primary = path.join(inputDir, 'Genesis.json');
|
|
245
|
+
if (fs.existsSync(primary)) {
|
|
246
|
+
try {
|
|
247
|
+
ts = await compileFromFile(primary, {
|
|
248
|
+
bannerComment: '',
|
|
249
|
+
declareExternallyReferenced: true,
|
|
250
|
+
unreachableDefinitions: true,
|
|
251
|
+
$refOptions: {
|
|
252
|
+
resolve: {
|
|
253
|
+
file: { order: 2 },
|
|
254
|
+
http: false,
|
|
255
|
+
https: false,
|
|
256
|
+
toolproof: toolproofResolver
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
// ignore and fall through to stub
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Still nothing? ensure we emit a module so downstream imports don't fail.
|
|
267
|
+
if (!ts || !ts.trim()) {
|
|
268
|
+
ts = '// (No concrete types emitted by generator)\nexport {}\n';
|
|
269
|
+
}
|
|
270
|
+
// Overlay Id aliases with template literal types inferred from Genesis.json patterns.
|
|
271
|
+
// For each $defs entry ending with "Id" or specific key types that provide a `pattern`,
|
|
272
|
+
// we derive a TS template literal. If no recognizable pattern exists,
|
|
273
|
+
// we fall back to plain `string` (moving away from branded types).
|
|
274
|
+
function deriveTemplateFromPattern(pattern) {
|
|
275
|
+
// Common form: ^PREFIX-.+$ => PREFIX-${string}
|
|
276
|
+
const m1 = /^\^([^$]+)\.\+\$/.exec(pattern);
|
|
277
|
+
if (m1) {
|
|
278
|
+
const prefix = m1[1]; // e.g., 'WORKFLOW-'
|
|
279
|
+
// Basic safety: ensure backticks/interpolations aren't present
|
|
280
|
+
if (!/[`]/.test(prefix)) {
|
|
281
|
+
return '`' + prefix + '${string}`';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Slightly stricter forms: ^PREFIX-[A-Za-z0-9]+$ => PREFIX-${string}
|
|
285
|
+
const m2 = /^\^([A-Za-z0-9._:-]+-)\[?\^?[A-Za-z0-9]+\]?\+?\$/.exec(pattern);
|
|
286
|
+
if (m2) {
|
|
287
|
+
const prefix = m2[1];
|
|
288
|
+
if (!/[`]/.test(prefix)) {
|
|
289
|
+
return '`' + prefix + '${string}`';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function loadIdTemplates() {
|
|
295
|
+
const map = {};
|
|
296
|
+
try {
|
|
297
|
+
const genesisPath = path.join(inputDir, 'Genesis.json');
|
|
298
|
+
if (fs.existsSync(genesisPath)) {
|
|
299
|
+
const raw = fs.readFileSync(genesisPath, 'utf8');
|
|
300
|
+
const parsed = JSON.parse(raw);
|
|
301
|
+
const defs = parsed?.$defs && typeof parsed.$defs === 'object' ? parsed.$defs : {};
|
|
302
|
+
for (const [defName, defVal] of Object.entries(defs)) {
|
|
303
|
+
// Process Id types
|
|
304
|
+
const isIdType = /Id$/.test(defName);
|
|
305
|
+
if (!isIdType)
|
|
306
|
+
continue;
|
|
307
|
+
const v = defVal;
|
|
308
|
+
if (v && v.type === 'string' && typeof v.pattern === 'string') {
|
|
309
|
+
const tmpl = deriveTemplateFromPattern(v.pattern);
|
|
310
|
+
if (tmpl)
|
|
311
|
+
map[defName] = tmpl;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// ignore failures; we'll just fall back to string
|
|
318
|
+
}
|
|
319
|
+
return map;
|
|
320
|
+
}
|
|
321
|
+
const idTemplates = loadIdTemplates();
|
|
322
|
+
// Replace any exported Id aliases to use the inferred template literals where available.
|
|
323
|
+
// Handle both `= string;` and any pre-existing branded alias `= Branded<string, 'X'>;`.
|
|
324
|
+
ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*Id)(\s*=\s*)Branded<string,\s*'[^']+'>\s*;$/gm, (_m, p1, typeName, p3) => {
|
|
325
|
+
const tmpl = idTemplates[typeName];
|
|
326
|
+
return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
|
|
327
|
+
});
|
|
328
|
+
ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*Id)(\s*=\s*)string\s*;$/gm, (_m, p1, typeName, p3) => {
|
|
329
|
+
const tmpl = idTemplates[typeName];
|
|
330
|
+
return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
|
|
331
|
+
});
|
|
332
|
+
// Post-process map-like interfaces to enforce key constraints via template literal Id types.
|
|
333
|
+
// Replace index-signature interfaces with Record<IdType, ValueType> so object literal keys are checked.
|
|
334
|
+
ts = ts.replace(/export interface RoleMap\s*{[^}]*}/g, 'export type RoleMap = Record<RoleId, RoleLiteral>;');
|
|
335
|
+
ts = ts.replace(/export interface RoleBindingMap\s*{[^}]*}/g, 'export type RoleBindingMap = Record<RoleId, ResourceId>;');
|
|
336
|
+
ts = ts.replace(/export interface ResourceMap\s*\{[^}]*\{[^}]*\}[^}]*\}/gs, 'export type ResourceMap = Record<ExecutionId, Record<RoleId, ResourcePotential | ResourceData>>;');
|
|
337
|
+
parts.push(ts);
|
|
338
|
+
let output = parts.join('\n');
|
|
339
|
+
// Final guard: strip any lingering `[k: string]: unknown;` that might have been
|
|
340
|
+
// reintroduced by later transforms.
|
|
341
|
+
output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
|
|
342
|
+
// Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
|
|
343
|
+
// - Remove a single blank line before `};` and `}`
|
|
344
|
+
// - Collapse 3+ consecutive newlines into a maximum of 2
|
|
345
|
+
output = output
|
|
346
|
+
.replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
|
|
347
|
+
.replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
|
|
348
|
+
.replace(/(\r?\n){3,}/g, '\n\n');
|
|
349
|
+
// As an additional safeguard, make sure the final .d.ts is treated as a module.
|
|
350
|
+
// If no export/interface/module is present, append an empty export.
|
|
351
|
+
if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
|
|
352
|
+
output += '\nexport {}\n';
|
|
353
|
+
}
|
|
354
|
+
// Write only under src/_lib/types to avoid duplicate src/types folder
|
|
355
|
+
try {
|
|
356
|
+
fs.writeFileSync(srcLibOutputPath, output, 'utf8');
|
|
357
|
+
console.log('Wrote', srcLibOutputPath);
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
console.warn('Failed to write types to src/_lib:', e);
|
|
361
|
+
}
|
|
362
|
+
// Also write a copy into dist so consumers get the generated declarations
|
|
363
|
+
// Write only to dist/_lib/types to keep the same path structure under dist
|
|
364
|
+
const distLibTypesDir = path.join(projectRoot, 'dist', '_lib', 'types');
|
|
365
|
+
const distLibOutputPath = path.join(distLibTypesDir, 'types.d.ts');
|
|
366
|
+
try {
|
|
367
|
+
fs.mkdirSync(distLibTypesDir, { recursive: true });
|
|
368
|
+
fs.writeFileSync(distLibOutputPath, output, 'utf8');
|
|
369
|
+
console.log('Wrote', distLibOutputPath);
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
// If copying to dist fails, log but don't crash the generator.
|
|
373
|
+
console.warn('Failed to write types to dist:', e);
|
|
374
|
+
}
|
|
375
|
+
// Ensure there is a runtime-resolvable module for './_lib/types/types.js'
|
|
376
|
+
// Some consumers and TS NodeNext resolution expect a concrete .js next to .d.ts
|
|
377
|
+
// The file is intentionally empty as all exports are types-only.
|
|
378
|
+
try {
|
|
379
|
+
const srcLibTypesJsPath = path.join(srcLibTypesDir, 'types.js');
|
|
380
|
+
if (!fs.existsSync(srcLibTypesJsPath)) {
|
|
381
|
+
fs.writeFileSync(srcLibTypesJsPath, 'export {}\n', 'utf8');
|
|
382
|
+
console.log('Wrote', srcLibTypesJsPath);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
console.warn('Failed to write types.js to src/_lib:', e);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const distLibTypesJsPath = path.join(distLibTypesDir, 'types.js');
|
|
390
|
+
fs.writeFileSync(distLibTypesJsPath, 'export {}\n', 'utf8');
|
|
391
|
+
console.log('Wrote', distLibTypesJsPath);
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
console.warn('Failed to write types.js to dist/_lib:', e);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
// Best-effort cleanup of the temporary combined schema
|
|
399
|
+
try {
|
|
400
|
+
if (fs.existsSync(combinedPath))
|
|
401
|
+
fs.unlinkSync(combinedPath);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
// ignore cleanup errors
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
main().catch((err) => {
|
|
409
|
+
console.error(err);
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toolproof-npm/schema",
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "JSON schemas and TypeScript types for ToolProof",
|
|
5
|
+
"keywords": ["toolproof", "schemas", "json-schema", "typescript"],
|
|
6
|
+
"author": "ToolProof Team",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/ToolProof/core.git",
|
|
11
|
+
"directory": "packages/_schemas"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/ToolProof/core#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/ToolProof/core/issues"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -b",
|
|
28
|
+
"build:scripts": "tsc -p tsconfig.scripts.json",
|
|
29
|
+
"extractSchemas": "node ./dist/scripts/extractSchemas.js",
|
|
30
|
+
"extractSubschema": "node ./dist/scripts/extractSubschemaWithDefs.js",
|
|
31
|
+
"generateTypes": "node ./dist/scripts/generateTypes.js",
|
|
32
|
+
"generateResourceData": "node ./dist/scripts/generateResourceData.js",
|
|
33
|
+
"update": "rimraf /s /q dist && pnpm run build:scripts && pnpm run extractSchemas -- --in src/genesis/Genesis.json --out src/schemas/Genesis.json --id 'https://schemas.toolproof.com/v0/Genesis.json' && pnpm run extractSubschema -- --name Job && pnpm run generateTypes && pnpm run generateResourceData -- --name Job && pnpm run build"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@apidevtools/json-schema-ref-parser": "^14.2.1",
|
|
41
|
+
"@types/node": "^20.8.1",
|
|
42
|
+
"ajv-cli": "^5.0.0",
|
|
43
|
+
"ajv-formats": "^3.0.1",
|
|
44
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|