typespun-codegen 0.0.7 → 0.0.9
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 +15 -7
- package/dist/analyzer/analyze.d.ts +4 -0
- package/dist/analyzer/analyze.d.ts.map +1 -0
- package/dist/analyzer/analyze.js +414 -0
- package/dist/analyzer/annotations.d.ts +14 -0
- package/dist/analyzer/annotations.d.ts.map +1 -0
- package/dist/analyzer/annotations.js +102 -0
- package/dist/analyzer/default-expression.d.ts +10 -0
- package/dist/analyzer/default-expression.d.ts.map +1 -0
- package/dist/analyzer/default-expression.js +67 -0
- package/dist/analyzer/diagnostic.d.ts +5 -0
- package/dist/analyzer/diagnostic.d.ts.map +1 -0
- package/dist/analyzer/diagnostic.js +16 -0
- package/dist/analyzer/ir.d.ts +2 -0
- package/dist/analyzer/ir.d.ts.map +1 -0
- package/dist/analyzer/ir.js +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli/diagnostics.d.ts +7 -0
- package/dist/cli/diagnostics.d.ts.map +1 -0
- package/dist/cli/diagnostics.js +26 -0
- package/dist/cli/init.d.ts +19 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +531 -0
- package/dist/cli/main.d.ts +13 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +164 -0
- package/dist/contracts.d.ts +33 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +1 -0
- package/dist/emitter/emit.d.ts +13 -0
- package/dist/emitter/emit.d.ts.map +1 -0
- package/dist/emitter/emit.js +65 -0
- package/dist/emitter/fingerprint.d.ts +10 -0
- package/dist/emitter/fingerprint.d.ts.map +1 -0
- package/dist/emitter/fingerprint.js +28 -0
- package/dist/generate.d.ts +29 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +267 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/project/config.d.ts +20 -0
- package/dist/project/config.d.ts.map +1 -0
- package/dist/project/config.js +144 -0
- package/dist/project/defaults.d.ts +22 -0
- package/dist/project/defaults.d.ts.map +1 -0
- package/dist/project/defaults.js +216 -0
- package/dist/project/discovery.d.ts +9 -0
- package/dist/project/discovery.d.ts.map +1 -0
- package/dist/project/discovery.js +66 -0
- package/package.json +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { discoverDefaultsPath, discoverInputPath, findNearestTsconfig, } from './discovery.js';
|
|
5
|
+
const OUTPUT_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);
|
|
6
|
+
const UNKNOWN_KEYS_POLICIES = new Set([
|
|
7
|
+
'error',
|
|
8
|
+
'warn',
|
|
9
|
+
'ignore',
|
|
10
|
+
]);
|
|
11
|
+
const SECRET_DEFAULTS_POLICIES = new Set([
|
|
12
|
+
'warn',
|
|
13
|
+
'allow',
|
|
14
|
+
'error',
|
|
15
|
+
]);
|
|
16
|
+
export class ProjectConfigError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'ProjectConfigError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function loadProjectConfig(options) {
|
|
23
|
+
const projectDirectory = resolve(options.projectDirectory);
|
|
24
|
+
const configPath = options.configPath
|
|
25
|
+
? resolve(projectDirectory, options.configPath)
|
|
26
|
+
: resolve(projectDirectory, 'typespun.json');
|
|
27
|
+
const hasConfig = options.configPath !== undefined || existsSync(configPath);
|
|
28
|
+
const config = hasConfig ? readProjectConfig(configPath) : {};
|
|
29
|
+
const configDirectory = hasConfig ? dirname(configPath) : projectDirectory;
|
|
30
|
+
const inputPath = config.input
|
|
31
|
+
? resolve(configDirectory, config.input)
|
|
32
|
+
: discoverInputPath(configDirectory);
|
|
33
|
+
validateInputExtension(inputPath);
|
|
34
|
+
const outputPath = config.output
|
|
35
|
+
? resolve(configDirectory, config.output)
|
|
36
|
+
: resolve(configDirectory, `src/generated/typespun${extname(inputPath)}`);
|
|
37
|
+
validateOutputExtension(outputPath);
|
|
38
|
+
const tsconfigPath = config.tsconfig
|
|
39
|
+
? resolve(configDirectory, config.tsconfig)
|
|
40
|
+
: findNearestTsconfig(inputPath);
|
|
41
|
+
if (tsconfigPath === undefined) {
|
|
42
|
+
throw new Error(`Could not find tsconfig.json for ${inputPath}`);
|
|
43
|
+
}
|
|
44
|
+
const defaults = resolveDefaults(config.defaults, configDirectory);
|
|
45
|
+
const envPrefix = config.envPrefix?.replace(/_+$/, '');
|
|
46
|
+
return {
|
|
47
|
+
configDirectory,
|
|
48
|
+
inputPath,
|
|
49
|
+
outputPath,
|
|
50
|
+
tsconfigPath,
|
|
51
|
+
...(defaults.path === undefined ? {} : { defaultsPath: defaults.path }),
|
|
52
|
+
...(envPrefix === undefined ? {} : { envPrefix }),
|
|
53
|
+
unknownKeys: defaults.unknownKeys,
|
|
54
|
+
secretDefaults: config.secretDefaults ?? 'warn',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function readProjectConfig(path) {
|
|
58
|
+
let value;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = ts.parseConfigFileTextToJson(path, readFileSync(path, 'utf8'));
|
|
61
|
+
if (parsed.error !== undefined)
|
|
62
|
+
throw new Error('invalid JSON');
|
|
63
|
+
value = parsed.config;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new ProjectConfigError('Could not parse typespun.json as JSON with comments');
|
|
67
|
+
}
|
|
68
|
+
if (!isRecord(value)) {
|
|
69
|
+
throw new ProjectConfigError('typespun.json must contain an object');
|
|
70
|
+
}
|
|
71
|
+
rejectUnknownKeys(value, ['input', 'output', 'tsconfig', 'envPrefix', 'defaults', 'secretDefaults'], 'typespun.json');
|
|
72
|
+
assertOptionalString(value, 'input', 'typespun.json.input');
|
|
73
|
+
assertOptionalString(value, 'output', 'typespun.json.output');
|
|
74
|
+
assertOptionalString(value, 'tsconfig', 'typespun.json.tsconfig');
|
|
75
|
+
assertOptionalString(value, 'envPrefix', 'typespun.json.envPrefix');
|
|
76
|
+
assertSecretDefaultsPolicy(value.secretDefaults);
|
|
77
|
+
if (value.defaults !== undefined) {
|
|
78
|
+
validateDefaultsConfig(value.defaults);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function validateDefaultsConfig(value) {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(value)) {
|
|
87
|
+
throw new ProjectConfigError('typespun.json.defaults must be a string or object');
|
|
88
|
+
}
|
|
89
|
+
rejectUnknownKeys(value, ['path', 'unknownKeys'], 'defaults');
|
|
90
|
+
assertOptionalString(value, 'path', 'typespun.json.defaults.path');
|
|
91
|
+
assertUnknownKeysPolicy(value.unknownKeys);
|
|
92
|
+
}
|
|
93
|
+
function resolveDefaults(defaults, configDirectory) {
|
|
94
|
+
if (typeof defaults === 'string') {
|
|
95
|
+
return { path: resolve(configDirectory, defaults), unknownKeys: 'error' };
|
|
96
|
+
}
|
|
97
|
+
if (defaults?.path !== undefined) {
|
|
98
|
+
return {
|
|
99
|
+
path: resolve(configDirectory, defaults.path),
|
|
100
|
+
unknownKeys: defaults.unknownKeys ?? 'error',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const path = discoverDefaultsPath(configDirectory);
|
|
104
|
+
return path === undefined
|
|
105
|
+
? { unknownKeys: defaults?.unknownKeys ?? 'error' }
|
|
106
|
+
: { path, unknownKeys: defaults?.unknownKeys ?? 'error' };
|
|
107
|
+
}
|
|
108
|
+
function rejectUnknownKeys(value, allowedKeys, context) {
|
|
109
|
+
for (const key of Object.keys(value)) {
|
|
110
|
+
if (!allowedKeys.includes(key)) {
|
|
111
|
+
throw new ProjectConfigError(`Unknown ${context} key: ${key}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function assertOptionalString(value, key, context) {
|
|
116
|
+
if (value[key] !== undefined && typeof value[key] !== 'string') {
|
|
117
|
+
throw new ProjectConfigError(`${context} must be a string`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function assertUnknownKeysPolicy(value) {
|
|
121
|
+
if (value !== undefined &&
|
|
122
|
+
!UNKNOWN_KEYS_POLICIES.has(value)) {
|
|
123
|
+
throw new ProjectConfigError('typespun.json.defaults.unknownKeys must be error, warn, or ignore');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function assertSecretDefaultsPolicy(value) {
|
|
127
|
+
if (value !== undefined &&
|
|
128
|
+
!SECRET_DEFAULTS_POLICIES.has(value)) {
|
|
129
|
+
throw new ProjectConfigError('typespun.json.secretDefaults must be warn, allow, or error');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function validateInputExtension(path) {
|
|
133
|
+
if (!OUTPUT_EXTENSIONS.has(extname(path))) {
|
|
134
|
+
throw new ProjectConfigError('input must use a .ts, .mts, or .cts extension');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function validateOutputExtension(path) {
|
|
138
|
+
if (!OUTPUT_EXTENSIONS.has(extname(path))) {
|
|
139
|
+
throw new ProjectConfigError('output must use a .ts, .mts, or .cts extension');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isRecord(value) {
|
|
143
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
144
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FieldIR, SecretDefaultsPolicy, UnknownKeysPolicy } from '../contracts.js';
|
|
2
|
+
export interface DefaultsDocument {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CompileDefaultsPolicies {
|
|
7
|
+
readonly unknownKeys: UnknownKeysPolicy;
|
|
8
|
+
readonly secretDefaults: SecretDefaultsPolicy;
|
|
9
|
+
}
|
|
10
|
+
export interface DefaultsDiagnostic {
|
|
11
|
+
readonly code: 'invalid_defaults_document' | 'invalid_default_value' | 'secret_default' | 'unknown_defaults_key' | 'unsafe_defaults_key';
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CompiledDefaultsResult {
|
|
17
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly warnings: readonly DefaultsDiagnostic[];
|
|
19
|
+
readonly errors: readonly DefaultsDiagnostic[];
|
|
20
|
+
}
|
|
21
|
+
export declare function compileDefaults(document: DefaultsDocument, fields: readonly FieldIR[], policies: CompileDefaultsPolicies): CompiledDefaultsResult;
|
|
22
|
+
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/project/defaults.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,OAAO,EACP,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;CAC/C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EACT,2BAA2B,GAC3B,uBAAuB,GACvB,gBAAgB,GAChB,sBAAsB,GACtB,qBAAqB,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAChD;AAKD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,QAAQ,EAAE,uBAAuB,GAChC,sBAAsB,CAwDxB"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { parseDocument } from 'yaml';
|
|
3
|
+
import { validateTypedValue } from 'typespun/generated';
|
|
4
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
5
|
+
const ALIAS_LIMIT = 20;
|
|
6
|
+
export function compileDefaults(document, fields, policies) {
|
|
7
|
+
const warnings = [];
|
|
8
|
+
const errors = [];
|
|
9
|
+
const values = {};
|
|
10
|
+
const compiledPaths = new Set();
|
|
11
|
+
const fieldByDefaultsPath = new Map(fields.map((field) => [toPathKey(field.defaultsPath), field]));
|
|
12
|
+
const parsed = parseDefaultsDocument(document, errors);
|
|
13
|
+
if (!isRecord(parsed)) {
|
|
14
|
+
if (parsed !== undefined) {
|
|
15
|
+
errors.push(diagnostic('invalid_defaults_document', [], document.path, 'Defaults document must contain an object at its root'));
|
|
16
|
+
}
|
|
17
|
+
applyInlineDefaults(fields, compiledPaths, policies, values, warnings, errors);
|
|
18
|
+
return { values, warnings, errors };
|
|
19
|
+
}
|
|
20
|
+
collectUnsafeKeys(parsed, [], document.path, errors, new Set());
|
|
21
|
+
walkDefaults(parsed, [], fieldByDefaultsPath, fields, policies, document.path, values, warnings, errors, compiledPaths);
|
|
22
|
+
applyInlineDefaults(fields, compiledPaths, policies, values, warnings, errors);
|
|
23
|
+
return { values, warnings, errors };
|
|
24
|
+
}
|
|
25
|
+
function parseDefaultsDocument(document, errors) {
|
|
26
|
+
if (extname(document.path) === '.json') {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(document.content);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
errors.push(diagnostic('invalid_defaults_document', [], document.path, 'Defaults document is not valid JSON'));
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = parseDocument(document.content, {
|
|
37
|
+
customTags: [],
|
|
38
|
+
merge: false,
|
|
39
|
+
prettyErrors: false,
|
|
40
|
+
resolveKnownTags: false,
|
|
41
|
+
schema: 'core',
|
|
42
|
+
stringKeys: true,
|
|
43
|
+
uniqueKeys: true,
|
|
44
|
+
});
|
|
45
|
+
if (parsed.errors.length > 0 || parsed.warnings.length > 0) {
|
|
46
|
+
errors.push(diagnostic('invalid_defaults_document', [], document.path, 'Defaults document is not valid YAML'));
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return parsed.toJS({ maxAliasCount: ALIAS_LIMIT });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
errors.push(diagnostic('invalid_defaults_document', [], document.path, 'Defaults document is not valid YAML'));
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function walkDefaults(value, path, fieldByDefaultsPath, fields, policies, file, values, warnings, errors, compiledPaths) {
|
|
57
|
+
for (const [key, child] of Object.entries(value)) {
|
|
58
|
+
const childPath = [...path, key];
|
|
59
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const field = fieldByDefaultsPath.get(toPathKey(childPath));
|
|
63
|
+
if (field !== undefined) {
|
|
64
|
+
compileField(field, child, childPath, policies, file, values, warnings, errors, compiledPaths);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (hasDefaultsDescendant(fields, childPath)) {
|
|
68
|
+
if (!isRecord(child)) {
|
|
69
|
+
errors.push(diagnostic('invalid_default_value', childPath, file, 'Expected an object for nested defaults'));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
walkDefaults(child, childPath, fieldByDefaultsPath, fields, policies, file, values, warnings, errors, compiledPaths);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
reportUnknownPath(childPath, policies.unknownKeys, file, warnings, errors);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function collectUnsafeKeys(value, path, file, errors, ancestors) {
|
|
79
|
+
if (!Array.isArray(value) && !isRecord(value)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (ancestors.has(value)) {
|
|
83
|
+
errors.push(diagnostic('invalid_defaults_document', path, file, 'Defaults document contains a recursive alias'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
ancestors.add(value);
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const [index, child] of value.entries()) {
|
|
89
|
+
collectUnsafeKeys(child, [...path, String(index)], file, errors, ancestors);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
for (const [key, child] of Object.entries(value)) {
|
|
94
|
+
const childPath = [...path, key];
|
|
95
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
96
|
+
errors.push(diagnostic('unsafe_defaults_key', childPath, file, 'Defaults path contains an unsafe key'));
|
|
97
|
+
}
|
|
98
|
+
collectUnsafeKeys(child, childPath, file, errors, ancestors);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
ancestors.delete(value);
|
|
102
|
+
}
|
|
103
|
+
function compileField(field, value, defaultsPath, policies, file, values, warnings, errors, compiledPaths) {
|
|
104
|
+
compiledPaths.add(toPathKey(field.propertyPath));
|
|
105
|
+
const validationMessage = validateTypedValue(field.kind, value);
|
|
106
|
+
if (validationMessage !== undefined) {
|
|
107
|
+
errors.push(diagnostic('invalid_default_value', defaultsPath, file, field.secret
|
|
108
|
+
? 'Secret default does not match its declared field type'
|
|
109
|
+
: validationMessage));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (field.secret) {
|
|
113
|
+
if (policies.secretDefaults === 'error') {
|
|
114
|
+
errors.push(diagnostic('secret_default', defaultsPath, file, 'Secret fields cannot have compiled defaults'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (policies.secretDefaults === 'warn') {
|
|
118
|
+
warnings.push(diagnostic('secret_default', defaultsPath, file, 'Secret field has a compiled default'));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!setValue(values, field.propertyPath, cloneValue(value))) {
|
|
122
|
+
errors.push(diagnostic('unsafe_defaults_key', defaultsPath, file, 'Defaults path contains an unsafe key'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function applyInlineDefaults(fields, compiledPaths, policies, values, warnings, errors) {
|
|
126
|
+
for (const field of fields) {
|
|
127
|
+
if (!field.hasDefault ||
|
|
128
|
+
field.defaultValue === undefined ||
|
|
129
|
+
compiledPaths.has(toPathKey(field.propertyPath))) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (field.secret) {
|
|
133
|
+
if (policies.secretDefaults === 'error') {
|
|
134
|
+
errors.push(diagnostic('secret_default', field.defaultsPath, field.location.file, 'Secret fields cannot have inline defaults'));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (policies.secretDefaults === 'warn') {
|
|
138
|
+
warnings.push(diagnostic('secret_default', field.defaultsPath, field.location.file, 'Secret field has an inline default'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!setValue(values, field.propertyPath, cloneValue(field.defaultValue))) {
|
|
142
|
+
errors.push(diagnostic('unsafe_defaults_key', field.defaultsPath, field.location.file, 'Defaults path contains an unsafe key'));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function reportUnknownPath(path, policy, file, warnings, errors) {
|
|
147
|
+
if (policy === 'ignore') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const target = policy === 'warn' ? warnings : errors;
|
|
151
|
+
target.push(diagnostic('unknown_defaults_key', path, file, 'Defaults path does not match a declared field'));
|
|
152
|
+
}
|
|
153
|
+
function hasDefaultsDescendant(fields, path) {
|
|
154
|
+
return fields.some((field) => field.defaultsPath.length > path.length &&
|
|
155
|
+
path.every((part, index) => field.defaultsPath[index] === part));
|
|
156
|
+
}
|
|
157
|
+
function setValue(target, path, value) {
|
|
158
|
+
if (path.length === 0 || path.some((part) => DANGEROUS_KEYS.has(part))) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
let current = target;
|
|
162
|
+
for (const [index, part] of path.entries()) {
|
|
163
|
+
if (index === path.length - 1) {
|
|
164
|
+
Object.defineProperty(current, part, {
|
|
165
|
+
configurable: true,
|
|
166
|
+
enumerable: true,
|
|
167
|
+
value,
|
|
168
|
+
writable: true,
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
const existing = current[part];
|
|
173
|
+
if (isRecord(existing)) {
|
|
174
|
+
current = existing;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const next = {};
|
|
178
|
+
Object.defineProperty(current, part, {
|
|
179
|
+
configurable: true,
|
|
180
|
+
enumerable: true,
|
|
181
|
+
value: next,
|
|
182
|
+
writable: true,
|
|
183
|
+
});
|
|
184
|
+
current = next;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function cloneValue(value) {
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
return value.map((item) => cloneValue(item));
|
|
191
|
+
}
|
|
192
|
+
if (isRecord(value)) {
|
|
193
|
+
const cloned = {};
|
|
194
|
+
for (const [key, child] of Object.entries(value)) {
|
|
195
|
+
if (!DANGEROUS_KEYS.has(key)) {
|
|
196
|
+
Object.defineProperty(cloned, key, {
|
|
197
|
+
configurable: true,
|
|
198
|
+
enumerable: true,
|
|
199
|
+
value: cloneValue(child),
|
|
200
|
+
writable: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return cloned;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function diagnostic(code, path, file, message) {
|
|
209
|
+
return { code, path: path.join('.'), file, message };
|
|
210
|
+
}
|
|
211
|
+
function toPathKey(path) {
|
|
212
|
+
return JSON.stringify(path);
|
|
213
|
+
}
|
|
214
|
+
function isRecord(value) {
|
|
215
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
216
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const CONVENTIONAL_INPUTS: readonly ["src/config.ts", "src/config.mts", "src/config.cts"];
|
|
2
|
+
export declare const CONVENTIONAL_DEFAULTS: readonly ["config.yaml", "config.yml", "config.json", "config/config.yaml", "config/config.yml", "config/config.json", "src/config.yaml", "src/config.yml", "src/config.json"];
|
|
3
|
+
export declare class ProjectDiscoveryError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function discoverInputPath(projectDirectory: string): string;
|
|
7
|
+
export declare function discoverDefaultsPath(projectDirectory: string): string | undefined;
|
|
8
|
+
export declare function findNearestTsconfig(inputPath: string): string | undefined;
|
|
9
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/project/discovery.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,gEAItB,CAAC;AAEX,eAAO,MAAM,qBAAqB,gLAUxB,CAAC;AAEX,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAalE;AAED,wBAAgB,oBAAoB,CAClC,gBAAgB,EAAE,MAAM,GACvB,MAAM,GAAG,SAAS,CAapB;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAezE"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
export const CONVENTIONAL_INPUTS = [
|
|
4
|
+
'src/config.ts',
|
|
5
|
+
'src/config.mts',
|
|
6
|
+
'src/config.cts',
|
|
7
|
+
];
|
|
8
|
+
export const CONVENTIONAL_DEFAULTS = [
|
|
9
|
+
'config.yaml',
|
|
10
|
+
'config.yml',
|
|
11
|
+
'config.json',
|
|
12
|
+
'config/config.yaml',
|
|
13
|
+
'config/config.yml',
|
|
14
|
+
'config/config.json',
|
|
15
|
+
'src/config.yaml',
|
|
16
|
+
'src/config.yml',
|
|
17
|
+
'src/config.json',
|
|
18
|
+
];
|
|
19
|
+
export class ProjectDiscoveryError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ProjectDiscoveryError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function discoverInputPath(projectDirectory) {
|
|
26
|
+
const candidates = findExistingCandidates(projectDirectory, CONVENTIONAL_INPUTS);
|
|
27
|
+
if (candidates.length !== 1) {
|
|
28
|
+
throw new ProjectDiscoveryError(`Expected exactly one conventional input, found ${candidates.length}.`);
|
|
29
|
+
}
|
|
30
|
+
return candidates[0];
|
|
31
|
+
}
|
|
32
|
+
export function discoverDefaultsPath(projectDirectory) {
|
|
33
|
+
const candidates = findExistingCandidates(projectDirectory, CONVENTIONAL_DEFAULTS);
|
|
34
|
+
if (candidates.length > 1) {
|
|
35
|
+
throw new ProjectDiscoveryError(`Multiple conventional defaults files found: ${candidates.join(', ')}. Configure defaults.path explicitly.`);
|
|
36
|
+
}
|
|
37
|
+
return candidates[0];
|
|
38
|
+
}
|
|
39
|
+
export function findNearestTsconfig(inputPath) {
|
|
40
|
+
let directory = dirname(resolve(inputPath));
|
|
41
|
+
while (true) {
|
|
42
|
+
const candidate = join(directory, 'tsconfig.json');
|
|
43
|
+
if (isFile(candidate)) {
|
|
44
|
+
return candidate;
|
|
45
|
+
}
|
|
46
|
+
const parent = dirname(directory);
|
|
47
|
+
if (parent === directory) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
directory = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function findExistingCandidates(projectDirectory, candidates) {
|
|
54
|
+
const root = resolve(projectDirectory);
|
|
55
|
+
return candidates
|
|
56
|
+
.map((candidate) => join(root, candidate))
|
|
57
|
+
.filter((candidate) => isFile(candidate));
|
|
58
|
+
}
|
|
59
|
+
function isFile(path) {
|
|
60
|
+
try {
|
|
61
|
+
return existsSync(path) && statSync(path).isFile();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespun-codegen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "TypeScript schema analyzer, generator, and CLI for Typespun",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"!dist/.tsbuildinfo"
|
|
30
30
|
],
|
|
31
31
|
"bin": {
|
|
32
|
-
"typespun": "dist/bin.js"
|
|
32
|
+
"typespun-codegen": "dist/bin.js"
|
|
33
33
|
},
|
|
34
34
|
"main": "./dist/index.js",
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"typescript": "6.0.3",
|
|
51
|
-
"typespun": "^0.0.
|
|
51
|
+
"typespun": "^0.0.9",
|
|
52
52
|
"yaml": "2.9.1"
|
|
53
53
|
}
|
|
54
54
|
}
|