typespun-codegen 0.0.8 → 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,5 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Diagnostic, SourceLocation } from '../contracts.js';
|
|
3
|
+
export declare function locationOf(node: ts.Node): SourceLocation;
|
|
4
|
+
export declare function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[];
|
|
5
|
+
//# sourceMappingURL=diagnostic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic.d.ts","sourceRoot":"","sources":["../../src/analyzer/diagnostic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,wBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,cAAc,CAUxD;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAQvE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export function locationOf(node) {
|
|
3
|
+
const source = node.getSourceFile();
|
|
4
|
+
const position = source.getLineAndCharacterOfPosition(ts.isSourceFile(node) ? 0 : node.getStart(source));
|
|
5
|
+
return {
|
|
6
|
+
file: source.fileName,
|
|
7
|
+
line: position.line + 1,
|
|
8
|
+
column: position.character + 1,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function sortDiagnostics(diagnostics) {
|
|
12
|
+
return diagnostics.sort((a, b) => a.location.file.localeCompare(b.location.file) ||
|
|
13
|
+
a.location.line - b.location.line ||
|
|
14
|
+
a.location.column - b.location.column ||
|
|
15
|
+
a.code.localeCompare(b.code));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../../src/analyzer/ir.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,UAAU,EACV,OAAO,EACP,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenerateDiagnostic } from '../generate.js';
|
|
2
|
+
export interface DiagnosticFormatOptions {
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
readonly color: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function formatDiagnostic(diagnostic: GenerateDiagnostic, options: DiagnosticFormatOptions): string;
|
|
7
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/cli/diagnostics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CAoBR"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isAbsolute, relative } from 'node:path';
|
|
2
|
+
export function formatDiagnostic(diagnostic, options) {
|
|
3
|
+
const file = diagnosticFile(diagnostic);
|
|
4
|
+
const location = 'location' in diagnostic ? diagnostic.location : undefined;
|
|
5
|
+
const displayedFile = file === undefined
|
|
6
|
+
? '<project>'
|
|
7
|
+
: isAbsolute(file)
|
|
8
|
+
? relative(options.cwd, file) || file
|
|
9
|
+
: file;
|
|
10
|
+
const prefix = `${displayedFile}:${location?.line ?? 1}:${location?.column ?? 1}`;
|
|
11
|
+
const code = options.color
|
|
12
|
+
? `\u001b[36m[${diagnostic.code}]\u001b[0m`
|
|
13
|
+
: `[${diagnostic.code}]`;
|
|
14
|
+
const suggestion = 'suggestion' in diagnostic && diagnostic.suggestion !== undefined
|
|
15
|
+
? `\n suggestion: ${diagnostic.suggestion}`
|
|
16
|
+
: '';
|
|
17
|
+
const affectedPath = 'path' in diagnostic && diagnostic.path ? `path ${diagnostic.path}: ` : '';
|
|
18
|
+
return `${prefix} ${code} ${affectedPath}${diagnostic.message}${suggestion}`;
|
|
19
|
+
}
|
|
20
|
+
function diagnosticFile(diagnostic) {
|
|
21
|
+
if ('location' in diagnostic)
|
|
22
|
+
return diagnostic.location.file;
|
|
23
|
+
if ('file' in diagnostic)
|
|
24
|
+
return diagnostic.file;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { GenerateDiagnostic } from '../generate.js';
|
|
2
|
+
export interface InitOptions {
|
|
3
|
+
readonly style?: 'interface' | 'class';
|
|
4
|
+
readonly input?: string;
|
|
5
|
+
readonly output?: string;
|
|
6
|
+
readonly envPrefix?: string;
|
|
7
|
+
readonly interactive?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface InitResult {
|
|
10
|
+
readonly exitCode: 0 | 1 | 2;
|
|
11
|
+
readonly messages: readonly string[];
|
|
12
|
+
readonly warnings: readonly string[];
|
|
13
|
+
readonly diagnostics: readonly GenerateDiagnostic[];
|
|
14
|
+
}
|
|
15
|
+
export declare class InitProjectError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export declare function initializeProject(cwd: string, options: InitOptions): Promise<InitResult>;
|
|
19
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAQzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACrD;AAkBD,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAuJrB"}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync, } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { generateProject } from '../generate.js';
|
|
7
|
+
import { CONVENTIONAL_INPUTS, discoverDefaultsPath, findNearestTsconfig, } from '../project/discovery.js';
|
|
8
|
+
const SCHEMA_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);
|
|
9
|
+
export class InitProjectError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'InitProjectError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function initializeProject(cwd, options) {
|
|
16
|
+
const projectDirectory = resolve(cwd);
|
|
17
|
+
const packagePath = join(projectDirectory, 'package.json');
|
|
18
|
+
rejectMutableSymlink(packagePath, 'package.json');
|
|
19
|
+
const packageDocument = readPackageDocument(packagePath);
|
|
20
|
+
const configPath = join(projectDirectory, 'typespun.json');
|
|
21
|
+
rejectMutableSymlink(configPath, 'typespun.json');
|
|
22
|
+
const existingConfig = existsSync(configPath)
|
|
23
|
+
? readInitConfig(configPath)
|
|
24
|
+
: undefined;
|
|
25
|
+
const shouldPrompt = options.interactive !== undefined
|
|
26
|
+
? options.interactive
|
|
27
|
+
: process.stdin.isTTY === true && process.env.CI !== 'true';
|
|
28
|
+
const input = selectInput(projectDirectory, options, existingConfig);
|
|
29
|
+
const output = options.output ??
|
|
30
|
+
existingConfig?.output ??
|
|
31
|
+
`src/generated/typespun${extname(input)}`;
|
|
32
|
+
validateSchemaPath(input, '--input');
|
|
33
|
+
validateSchemaPath(output, '--output');
|
|
34
|
+
rejectConflictingOptions(options, existingConfig, input, output);
|
|
35
|
+
validateExistingSchemaStyle(projectDirectory, input, options.style);
|
|
36
|
+
const inputPath = resolve(projectDirectory, input);
|
|
37
|
+
const outputPath = resolve(projectDirectory, output);
|
|
38
|
+
const inputNeedsCreation = !existsSync(inputPath);
|
|
39
|
+
const style = inputNeedsCreation
|
|
40
|
+
? await resolveStyle(options.style, shouldPrompt)
|
|
41
|
+
: (options.style ?? 'interface');
|
|
42
|
+
const envPrefix = options.envPrefix === undefined
|
|
43
|
+
? await resolveEnvPrefix(options.envPrefix, existingConfig?.envPrefix, shouldPrompt)
|
|
44
|
+
: options.envPrefix;
|
|
45
|
+
if (pathsReferToSameFile(inputPath, outputPath)) {
|
|
46
|
+
throw new InitProjectError('Schema input and generated output must differ');
|
|
47
|
+
}
|
|
48
|
+
if (pathEntryExists(outputPath) && !isTypespunGeneratedOutput(outputPath)) {
|
|
49
|
+
throw new InitProjectError(`Initialization refuses to overwrite the existing output at ${output}`);
|
|
50
|
+
}
|
|
51
|
+
const discoveredDefaults = existingConfig?.defaults === undefined
|
|
52
|
+
? discoverDefaultsPath(projectDirectory)
|
|
53
|
+
: undefined;
|
|
54
|
+
const createDefaults = existingConfig?.defaults === undefined && discoveredDefaults === undefined;
|
|
55
|
+
const defaultDefaultsPath = join(projectDirectory, 'config.yaml');
|
|
56
|
+
if (createDefaults && pathEntryExists(defaultDefaultsPath)) {
|
|
57
|
+
throw new InitProjectError('Initialization refuses to overwrite the existing entry at config.yaml');
|
|
58
|
+
}
|
|
59
|
+
const config = {
|
|
60
|
+
...existingConfig,
|
|
61
|
+
input,
|
|
62
|
+
output,
|
|
63
|
+
...(envPrefix === undefined ? {} : { envPrefix }),
|
|
64
|
+
};
|
|
65
|
+
preflightResolvedConfig(projectDirectory, config, inputPath);
|
|
66
|
+
const configNeedsMissingKeys = existingConfig !== undefined &&
|
|
67
|
+
(existingConfig.input === undefined ||
|
|
68
|
+
existingConfig.output === undefined ||
|
|
69
|
+
(options.envPrefix !== undefined &&
|
|
70
|
+
existingConfig.envPrefix === undefined));
|
|
71
|
+
const messages = [];
|
|
72
|
+
if (inputNeedsCreation) {
|
|
73
|
+
await writeNewFile(inputPath, schemaTemplate(style));
|
|
74
|
+
messages.push(`Created ${input}.`);
|
|
75
|
+
}
|
|
76
|
+
if (createDefaults) {
|
|
77
|
+
await writeNewFile(defaultDefaultsPath, inputNeedsCreation ? 'port: 3000\n' : '{}\n');
|
|
78
|
+
messages.push('Created config.yaml.');
|
|
79
|
+
}
|
|
80
|
+
const serializedConfig = existingConfig === undefined
|
|
81
|
+
? serializeNewConfig(config, discoveredDefaults === undefined
|
|
82
|
+
? 'config.yaml'
|
|
83
|
+
: displayPath(projectDirectory, discoveredDefaults))
|
|
84
|
+
: `${JSON.stringify(config, null, 2)}\n`;
|
|
85
|
+
if (existingConfig === undefined) {
|
|
86
|
+
await writeNewFile(configPath, serializedConfig);
|
|
87
|
+
messages.push('Created typespun.json.');
|
|
88
|
+
}
|
|
89
|
+
else if (configNeedsMissingKeys) {
|
|
90
|
+
await writeFile(configPath, serializedConfig);
|
|
91
|
+
messages.push('Added missing paths to typespun.json.');
|
|
92
|
+
}
|
|
93
|
+
if (addMissingScripts(packageDocument)) {
|
|
94
|
+
await writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`);
|
|
95
|
+
messages.push('Added config:generate and config:check scripts.');
|
|
96
|
+
}
|
|
97
|
+
const missing = missingDependencies(projectDirectory);
|
|
98
|
+
if (missing.length > 0) {
|
|
99
|
+
return {
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
messages,
|
|
102
|
+
warnings: installationGuidance(projectDirectory, packageDocument, missing),
|
|
103
|
+
diagnostics: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const generated = await generateProject({
|
|
107
|
+
projectDirectory,
|
|
108
|
+
configPath,
|
|
109
|
+
mode: 'write',
|
|
110
|
+
});
|
|
111
|
+
if (generated.diagnostics.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
exitCode: generated.diagnostics.some((diagnostic) => diagnostic.code === 'typescript_config')
|
|
114
|
+
? 2
|
|
115
|
+
: 1,
|
|
116
|
+
messages,
|
|
117
|
+
warnings: [],
|
|
118
|
+
diagnostics: [...generated.warnings, ...generated.diagnostics],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
messages.push(`${generated.status === 'written' ? 'Generated' : 'Unchanged'} ${displayPath(projectDirectory, generated.outputPath)}.`);
|
|
122
|
+
return {
|
|
123
|
+
exitCode: 0,
|
|
124
|
+
messages,
|
|
125
|
+
warnings: [],
|
|
126
|
+
diagnostics: generated.warnings,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function readPackageDocument(path) {
|
|
130
|
+
if (!isFile(path))
|
|
131
|
+
throw new InitProjectError('package.json is required');
|
|
132
|
+
let value;
|
|
133
|
+
try {
|
|
134
|
+
value = JSON.parse(readFileSync(path, 'utf8'));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new InitProjectError('package.json must contain valid JSON');
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
throw new InitProjectError('package.json must contain an object');
|
|
141
|
+
}
|
|
142
|
+
if (value.scripts !== undefined && !isRecord(value.scripts)) {
|
|
143
|
+
throw new InitProjectError('package.json scripts must contain an object');
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
async function resolveStyle(style, shouldPrompt) {
|
|
148
|
+
if (style !== undefined)
|
|
149
|
+
return style;
|
|
150
|
+
if (!shouldPrompt)
|
|
151
|
+
return 'interface';
|
|
152
|
+
while (true) {
|
|
153
|
+
const answer = await promptLine('Which schema style would you like for config? [interface/class] (default: interface): ');
|
|
154
|
+
if (answer.length === 0)
|
|
155
|
+
return 'interface';
|
|
156
|
+
if (answer === 'interface' || answer === 'class')
|
|
157
|
+
return answer;
|
|
158
|
+
process.stderr.write('Please answer "interface" or "class"\n');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function resolveEnvPrefix(provided, existing, shouldPrompt) {
|
|
162
|
+
if (provided !== undefined || existing !== undefined) {
|
|
163
|
+
return provided ?? existing;
|
|
164
|
+
}
|
|
165
|
+
if (!shouldPrompt)
|
|
166
|
+
return undefined;
|
|
167
|
+
const answer = await promptLine('Optional environment variable prefix (leave blank to skip): ');
|
|
168
|
+
return answer.length === 0 ? undefined : answer;
|
|
169
|
+
}
|
|
170
|
+
async function promptLine(question) {
|
|
171
|
+
const input = createInterface({
|
|
172
|
+
input: process.stdin,
|
|
173
|
+
output: process.stdout,
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
const value = await input.question(question);
|
|
177
|
+
return value.trim();
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
input.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function validateTsconfig(path) {
|
|
184
|
+
if (!isFile(path))
|
|
185
|
+
throw new InitProjectError('tsconfig.json is required');
|
|
186
|
+
const loaded = ts.readConfigFile(path, ts.sys.readFile);
|
|
187
|
+
if (loaded.error !== undefined) {
|
|
188
|
+
throw new InitProjectError('tsconfig.json must contain usable TypeScript configuration');
|
|
189
|
+
}
|
|
190
|
+
const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, dirname(path), undefined, path);
|
|
191
|
+
if (parsed.errors.some((diagnostic) => diagnostic.code !== 18003)) {
|
|
192
|
+
throw new InitProjectError('tsconfig.json must contain usable TypeScript configuration');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function preflightResolvedConfig(projectDirectory, config, inputPath) {
|
|
196
|
+
const tsconfigPath = config.tsconfig === undefined
|
|
197
|
+
? findNearestTsconfig(inputPath)
|
|
198
|
+
: resolve(projectDirectory, config.tsconfig);
|
|
199
|
+
if (tsconfigPath === undefined) {
|
|
200
|
+
throw new InitProjectError(`Could not find tsconfig.json for ${displayPath(projectDirectory, inputPath)}`);
|
|
201
|
+
}
|
|
202
|
+
validateTsconfig(tsconfigPath);
|
|
203
|
+
const defaults = config.defaults;
|
|
204
|
+
const hasExplicitDefaultsPath = typeof defaults === 'string' ||
|
|
205
|
+
(isRecord(defaults) && typeof defaults.path === 'string');
|
|
206
|
+
if (!hasExplicitDefaultsPath) {
|
|
207
|
+
try {
|
|
208
|
+
discoverDefaultsPath(projectDirectory);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw new InitProjectError(error instanceof Error
|
|
212
|
+
? error.message
|
|
213
|
+
: 'Defaults-file discovery failed');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function readInitConfig(path) {
|
|
218
|
+
let value;
|
|
219
|
+
try {
|
|
220
|
+
const parsed = ts.parseConfigFileTextToJson(path, readFileSync(path, 'utf8'));
|
|
221
|
+
if (parsed.error !== undefined)
|
|
222
|
+
throw new Error('invalid JSON');
|
|
223
|
+
value = parsed.config;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new InitProjectError('typespun.json must contain valid JSON with comments');
|
|
227
|
+
}
|
|
228
|
+
if (!isRecord(value)) {
|
|
229
|
+
throw new InitProjectError('typespun.json must contain an object');
|
|
230
|
+
}
|
|
231
|
+
const allowed = new Set([
|
|
232
|
+
'input',
|
|
233
|
+
'output',
|
|
234
|
+
'tsconfig',
|
|
235
|
+
'envPrefix',
|
|
236
|
+
'defaults',
|
|
237
|
+
'secretDefaults',
|
|
238
|
+
]);
|
|
239
|
+
for (const key of Object.keys(value)) {
|
|
240
|
+
if (!allowed.has(key)) {
|
|
241
|
+
throw new InitProjectError(`Unknown typespun.json key: ${key}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const key of ['input', 'output', 'tsconfig', 'envPrefix']) {
|
|
245
|
+
if (value[key] !== undefined && typeof value[key] !== 'string') {
|
|
246
|
+
throw new InitProjectError(`typespun.json ${key} must be a string`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
validateDefaultsConfig(value.defaults);
|
|
250
|
+
if (value.secretDefaults !== undefined &&
|
|
251
|
+
value.secretDefaults !== 'warn' &&
|
|
252
|
+
value.secretDefaults !== 'allow' &&
|
|
253
|
+
value.secretDefaults !== 'error') {
|
|
254
|
+
throw new InitProjectError('typespun.json secretDefaults must be warn, allow, or error');
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function serializeNewConfig(config, defaultsPath) {
|
|
259
|
+
const prefixLines = config.envPrefix === undefined
|
|
260
|
+
? [
|
|
261
|
+
'',
|
|
262
|
+
' // Optional environment prefix. APP produces keys such as APP_PORT.',
|
|
263
|
+
' // "envPrefix": "APP",',
|
|
264
|
+
]
|
|
265
|
+
: [
|
|
266
|
+
` "envPrefix": ${JSON.stringify(config.envPrefix)},`,
|
|
267
|
+
'',
|
|
268
|
+
' // Environment keys use the configured prefix, such as APP_PORT.',
|
|
269
|
+
];
|
|
270
|
+
return `${[
|
|
271
|
+
'{',
|
|
272
|
+
` "input": ${JSON.stringify(config.input)},`,
|
|
273
|
+
` "output": ${JSON.stringify(config.output)},`,
|
|
274
|
+
...prefixLines,
|
|
275
|
+
'',
|
|
276
|
+
' // Optional tsconfig override. Default: nearest tsconfig.json to the input.',
|
|
277
|
+
' // "tsconfig": "tsconfig.json",',
|
|
278
|
+
'',
|
|
279
|
+
` // Optional defaults override. ${defaultsPath} is discovered automatically.`,
|
|
280
|
+
' // "defaults": {',
|
|
281
|
+
` // "path": ${JSON.stringify(defaultsPath)},`,
|
|
282
|
+
' // "unknownKeys": "error" // Allowed: "error", "warn", or "ignore".',
|
|
283
|
+
' // },',
|
|
284
|
+
'',
|
|
285
|
+
' // Policy for defaults on secret fields. Allowed: "warn", "allow", or "error".',
|
|
286
|
+
' // "secretDefaults": "warn"',
|
|
287
|
+
'}',
|
|
288
|
+
].join('\n')}\n`;
|
|
289
|
+
}
|
|
290
|
+
function validateDefaultsConfig(value) {
|
|
291
|
+
if (value === undefined || typeof value === 'string')
|
|
292
|
+
return;
|
|
293
|
+
if (!isRecord(value)) {
|
|
294
|
+
throw new InitProjectError('typespun.json defaults must be a string or object');
|
|
295
|
+
}
|
|
296
|
+
for (const key of Object.keys(value)) {
|
|
297
|
+
if (key !== 'path' && key !== 'unknownKeys') {
|
|
298
|
+
throw new InitProjectError(`Unknown typespun.json defaults key: ${key}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (value.path !== undefined && typeof value.path !== 'string') {
|
|
302
|
+
throw new InitProjectError('typespun.json defaults.path must be a string');
|
|
303
|
+
}
|
|
304
|
+
if (value.unknownKeys !== undefined &&
|
|
305
|
+
value.unknownKeys !== 'error' &&
|
|
306
|
+
value.unknownKeys !== 'warn' &&
|
|
307
|
+
value.unknownKeys !== 'ignore') {
|
|
308
|
+
throw new InitProjectError('typespun.json defaults.unknownKeys must be error, warn, or ignore');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function selectInput(projectDirectory, options, existingConfig) {
|
|
312
|
+
if (options.input !== undefined)
|
|
313
|
+
return options.input;
|
|
314
|
+
if (existingConfig?.input !== undefined)
|
|
315
|
+
return existingConfig.input;
|
|
316
|
+
const candidates = CONVENTIONAL_INPUTS.filter((path) => isFile(resolve(projectDirectory, path)));
|
|
317
|
+
if (candidates.length > 1) {
|
|
318
|
+
throw new InitProjectError(`Multiple conventional schemas exist: ${candidates.join(', ')}; use --input.`);
|
|
319
|
+
}
|
|
320
|
+
return candidates[0] ?? 'src/config.ts';
|
|
321
|
+
}
|
|
322
|
+
function rejectConflictingOptions(options, existing, input, output) {
|
|
323
|
+
if (existing === undefined)
|
|
324
|
+
return;
|
|
325
|
+
const comparisons = [
|
|
326
|
+
['--input', options.input, existing.input ?? input],
|
|
327
|
+
['--output', options.output, existing.output ?? output],
|
|
328
|
+
[
|
|
329
|
+
'--env-prefix',
|
|
330
|
+
options.envPrefix,
|
|
331
|
+
existing.envPrefix ?? options.envPrefix,
|
|
332
|
+
],
|
|
333
|
+
];
|
|
334
|
+
for (const [flag, requested, configured] of comparisons) {
|
|
335
|
+
if (requested !== undefined && requested !== configured) {
|
|
336
|
+
throw new InitProjectError(`${flag} conflicts with the existing typespun.json`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function validateExistingSchemaStyle(projectDirectory, input, requestedStyle) {
|
|
341
|
+
const path = resolve(projectDirectory, input);
|
|
342
|
+
if (requestedStyle === undefined || !isFile(path))
|
|
343
|
+
return;
|
|
344
|
+
const source = readFileSync(path, 'utf8');
|
|
345
|
+
const matches = requestedStyle === 'class'
|
|
346
|
+
? /\b(?:export\s+)?class\s+[A-Za-z_$]/.test(source)
|
|
347
|
+
: /\b(?:export\s+)?interface\s+[A-Za-z_$]/.test(source);
|
|
348
|
+
if (!matches) {
|
|
349
|
+
throw new InitProjectError(`--style ${requestedStyle} conflicts with the existing schema at ${input}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function validateSchemaPath(path, flag) {
|
|
353
|
+
if (!SCHEMA_EXTENSIONS.has(extname(path))) {
|
|
354
|
+
throw new InitProjectError(`${flag} must use a .ts, .mts, or .cts extension`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function schemaTemplate(style) {
|
|
358
|
+
return style === 'interface'
|
|
359
|
+
? `/** @typespun */\nexport interface AppConfig {\n port: number;\n}\n`
|
|
360
|
+
: `import { Config } from 'typespun';\n\n@Config()\nexport class AppConfig {\n port!: number;\n}\n`;
|
|
361
|
+
}
|
|
362
|
+
function addMissingScripts(document) {
|
|
363
|
+
const scripts = document.scripts ?? {};
|
|
364
|
+
let changed = false;
|
|
365
|
+
if (scripts['config:generate'] === undefined) {
|
|
366
|
+
scripts['config:generate'] = 'typespun generate';
|
|
367
|
+
changed = true;
|
|
368
|
+
}
|
|
369
|
+
if (scripts['config:check'] === undefined) {
|
|
370
|
+
scripts['config:check'] = 'typespun check';
|
|
371
|
+
changed = true;
|
|
372
|
+
}
|
|
373
|
+
if (document.scripts === undefined)
|
|
374
|
+
document.scripts = scripts;
|
|
375
|
+
return changed;
|
|
376
|
+
}
|
|
377
|
+
function missingDependencies(projectDirectory) {
|
|
378
|
+
return ['typespun', 'typespun-codegen'].filter((name) => !findInstalledPackage(projectDirectory, name));
|
|
379
|
+
}
|
|
380
|
+
function findInstalledPackage(projectDirectory, packageName) {
|
|
381
|
+
let directory = resolve(projectDirectory);
|
|
382
|
+
while (true) {
|
|
383
|
+
if (isFile(join(directory, 'node_modules', packageName, 'package.json'))) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const parent = dirname(directory);
|
|
387
|
+
if (parent === directory)
|
|
388
|
+
return false;
|
|
389
|
+
directory = parent;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function installationGuidance(projectDirectory, packageDocument, missing) {
|
|
393
|
+
const manager = detectPackageManager(projectDirectory, packageDocument);
|
|
394
|
+
const commands = [];
|
|
395
|
+
if (missing.includes('typespun')) {
|
|
396
|
+
commands.push(installCommand(manager, 'typespun', false));
|
|
397
|
+
}
|
|
398
|
+
if (missing.includes('typespun-codegen')) {
|
|
399
|
+
commands.push(installCommand(manager, 'typespun-codegen', true));
|
|
400
|
+
}
|
|
401
|
+
return [
|
|
402
|
+
'Dependencies are missing; generated output was not created. Install them, then run typespun generate:',
|
|
403
|
+
...commands,
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
function detectPackageManager(projectDirectory, document) {
|
|
407
|
+
const configured = document.packageManager?.split('@')[0];
|
|
408
|
+
if (configured === 'bun' ||
|
|
409
|
+
configured === 'npm' ||
|
|
410
|
+
configured === 'pnpm' ||
|
|
411
|
+
configured === 'yarn') {
|
|
412
|
+
return configured;
|
|
413
|
+
}
|
|
414
|
+
if (isFile(join(projectDirectory, 'bun.lock')) ||
|
|
415
|
+
isFile(join(projectDirectory, 'bun.lockb'))) {
|
|
416
|
+
return 'bun';
|
|
417
|
+
}
|
|
418
|
+
if (isFile(join(projectDirectory, 'pnpm-lock.yaml')))
|
|
419
|
+
return 'pnpm';
|
|
420
|
+
if (isFile(join(projectDirectory, 'yarn.lock')))
|
|
421
|
+
return 'yarn';
|
|
422
|
+
return 'npm';
|
|
423
|
+
}
|
|
424
|
+
function installCommand(manager, packageName, development) {
|
|
425
|
+
if (manager === 'npm') {
|
|
426
|
+
return `npm install${development ? ' --save-dev' : ''} ${packageName}`;
|
|
427
|
+
}
|
|
428
|
+
if (manager === 'yarn') {
|
|
429
|
+
return `yarn add${development ? ' --dev' : ''} ${packageName}`;
|
|
430
|
+
}
|
|
431
|
+
const developmentFlag = development
|
|
432
|
+
? manager === 'pnpm'
|
|
433
|
+
? ' --save-dev'
|
|
434
|
+
: ' --dev'
|
|
435
|
+
: '';
|
|
436
|
+
return `${manager} add${developmentFlag} ${packageName}`;
|
|
437
|
+
}
|
|
438
|
+
async function writeNewFile(path, contents) {
|
|
439
|
+
await mkdir(dirname(path), { recursive: true });
|
|
440
|
+
await writeFile(path, contents, { encoding: 'utf8', flag: 'wx' });
|
|
441
|
+
}
|
|
442
|
+
function displayPath(projectDirectory, path) {
|
|
443
|
+
return path.startsWith(`${projectDirectory}/`)
|
|
444
|
+
? path.slice(projectDirectory.length + 1)
|
|
445
|
+
: path;
|
|
446
|
+
}
|
|
447
|
+
function pathsReferToSameFile(first, second) {
|
|
448
|
+
const firstIdentity = fileIdentity(first);
|
|
449
|
+
const secondIdentity = fileIdentity(second);
|
|
450
|
+
if (firstIdentity.stat !== undefined &&
|
|
451
|
+
secondIdentity.stat !== undefined &&
|
|
452
|
+
firstIdentity.stat.dev === secondIdentity.stat.dev &&
|
|
453
|
+
firstIdentity.stat.ino === secondIdentity.stat.ino) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
return firstIdentity.canonicalPath === secondIdentity.canonicalPath;
|
|
457
|
+
}
|
|
458
|
+
function fileIdentity(path) {
|
|
459
|
+
try {
|
|
460
|
+
return { canonicalPath: realpathSync(path), stat: statSync(path) };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
let ancestor = dirname(path);
|
|
464
|
+
const remainder = [basename(path)];
|
|
465
|
+
while (true) {
|
|
466
|
+
try {
|
|
467
|
+
return {
|
|
468
|
+
canonicalPath: resolve(realpathSync(ancestor), ...remainder),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
const parent = dirname(ancestor);
|
|
473
|
+
if (parent === ancestor)
|
|
474
|
+
return { canonicalPath: resolve(path) };
|
|
475
|
+
remainder.unshift(basename(ancestor));
|
|
476
|
+
ancestor = parent;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function isTypespunGeneratedOutput(path) {
|
|
482
|
+
let contents;
|
|
483
|
+
try {
|
|
484
|
+
contents = readFileSync(path, 'utf8');
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return (/^\/\/ Generated by typespun-codegen\. Do not edit\.\n\/\/ Schema fingerprint: [a-f0-9]{64}\n\n/u.test(contents) &&
|
|
490
|
+
contents.includes("from 'typespun/generated';") &&
|
|
491
|
+
contents.includes('export type Config = TypespunConfig;') &&
|
|
492
|
+
contents.includes('export const loadConfig = createLoader<Config>(schema);'));
|
|
493
|
+
}
|
|
494
|
+
function isFile(path) {
|
|
495
|
+
try {
|
|
496
|
+
return existsSync(path) && statSync(path).isFile();
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function pathEntryExists(path) {
|
|
503
|
+
try {
|
|
504
|
+
lstatSync(path);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function rejectMutableSymlink(path, displayName) {
|
|
512
|
+
try {
|
|
513
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
514
|
+
throw new InitProjectError(`Initialization refuses to update the symbolic link at ${displayName}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error instanceof InitProjectError)
|
|
519
|
+
throw error;
|
|
520
|
+
if (typeof error === 'object' &&
|
|
521
|
+
error !== null &&
|
|
522
|
+
'code' in error &&
|
|
523
|
+
error.code === 'ENOENT') {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
throw new InitProjectError(`Could not inspect ${displayName}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function isRecord(value) {
|
|
530
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
531
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface CliStreams {
|
|
2
|
+
readonly stdout: {
|
|
3
|
+
write(value: string): unknown;
|
|
4
|
+
readonly isTTY?: boolean;
|
|
5
|
+
};
|
|
6
|
+
readonly stderr: {
|
|
7
|
+
write(value: string): unknown;
|
|
8
|
+
readonly isTTY?: boolean;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function runCli(args: readonly string[], streams?: CliStreams, cwd?: string): Promise<number>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAyBA,UAAU,UAAU;IAClB,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAID,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,UAAoB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CA+EjB"}
|