typespun-codegen 0.0.4

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/dist/analyzer/analyze.d.ts +4 -0
  4. package/dist/analyzer/analyze.d.ts.map +1 -0
  5. package/dist/analyzer/analyze.js +414 -0
  6. package/dist/analyzer/annotations.d.ts +14 -0
  7. package/dist/analyzer/annotations.d.ts.map +1 -0
  8. package/dist/analyzer/annotations.js +102 -0
  9. package/dist/analyzer/default-expression.d.ts +10 -0
  10. package/dist/analyzer/default-expression.d.ts.map +1 -0
  11. package/dist/analyzer/default-expression.js +67 -0
  12. package/dist/analyzer/diagnostic.d.ts +5 -0
  13. package/dist/analyzer/diagnostic.d.ts.map +1 -0
  14. package/dist/analyzer/diagnostic.js +16 -0
  15. package/dist/analyzer/ir.d.ts +2 -0
  16. package/dist/analyzer/ir.d.ts.map +1 -0
  17. package/dist/analyzer/ir.js +1 -0
  18. package/dist/bin.d.ts +3 -0
  19. package/dist/bin.d.ts.map +1 -0
  20. package/dist/bin.js +3 -0
  21. package/dist/cli/diagnostics.d.ts +7 -0
  22. package/dist/cli/diagnostics.d.ts.map +1 -0
  23. package/dist/cli/diagnostics.js +26 -0
  24. package/dist/cli/init.d.ts +18 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +417 -0
  27. package/dist/cli/main.d.ts +13 -0
  28. package/dist/cli/main.d.ts.map +1 -0
  29. package/dist/cli/main.js +157 -0
  30. package/dist/contracts.d.ts +33 -0
  31. package/dist/contracts.d.ts.map +1 -0
  32. package/dist/contracts.js +1 -0
  33. package/dist/emitter/emit.d.ts +13 -0
  34. package/dist/emitter/emit.d.ts.map +1 -0
  35. package/dist/emitter/emit.js +65 -0
  36. package/dist/emitter/fingerprint.d.ts +10 -0
  37. package/dist/emitter/fingerprint.d.ts.map +1 -0
  38. package/dist/emitter/fingerprint.js +28 -0
  39. package/dist/generate.d.ts +29 -0
  40. package/dist/generate.d.ts.map +1 -0
  41. package/dist/generate.js +267 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +2 -0
  45. package/dist/project/config.d.ts +20 -0
  46. package/dist/project/config.d.ts.map +1 -0
  47. package/dist/project/config.js +140 -0
  48. package/dist/project/defaults.d.ts +22 -0
  49. package/dist/project/defaults.d.ts.map +1 -0
  50. package/dist/project/defaults.js +216 -0
  51. package/dist/project/discovery.d.ts +9 -0
  52. package/dist/project/discovery.d.ts.map +1 -0
  53. package/dist/project/discovery.js +66 -0
  54. package/package.json +54 -0
@@ -0,0 +1,67 @@
1
+ import ts from 'typescript';
2
+ const invalid = { ok: false };
3
+ /** Reads syntax only. No schema expressions or user modules are executed. */
4
+ export function evaluateDefault(expression, checker) {
5
+ if (ts.isParenthesizedExpression(expression) ||
6
+ ts.isAsExpression(expression) ||
7
+ ts.isSatisfiesExpression(expression) ||
8
+ ts.isTypeAssertionExpression(expression)) {
9
+ return evaluateDefault(expression.expression, checker);
10
+ }
11
+ if (ts.isStringLiteral(expression))
12
+ return { ok: true, value: expression.text };
13
+ if (ts.isNumericLiteral(expression)) {
14
+ const value = Number(expression.text);
15
+ return Number.isFinite(value) ? { ok: true, value } : invalid;
16
+ }
17
+ if (expression.kind === ts.SyntaxKind.TrueKeyword)
18
+ return { ok: true, value: true };
19
+ if (expression.kind === ts.SyntaxKind.FalseKeyword)
20
+ return { ok: true, value: false };
21
+ if (ts.isPrefixUnaryExpression(expression) &&
22
+ expression.operator === ts.SyntaxKind.MinusToken &&
23
+ ts.isNumericLiteral(expression.operand)) {
24
+ const value = -Number(expression.operand.text);
25
+ return Number.isFinite(value) ? { ok: true, value } : invalid;
26
+ }
27
+ if (ts.isArrayLiteralExpression(expression)) {
28
+ const values = [];
29
+ for (const element of expression.elements) {
30
+ const result = evaluateDefault(element, checker);
31
+ if (!result.ok)
32
+ return invalid;
33
+ values.push(result.value);
34
+ }
35
+ return { ok: true, value: values };
36
+ }
37
+ if (ts.isObjectLiteralExpression(expression)) {
38
+ const value = {};
39
+ for (const property of expression.properties) {
40
+ if (!ts.isPropertyAssignment(property) ||
41
+ !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)))
42
+ return invalid;
43
+ const key = property.name.text;
44
+ if (['__proto__', 'constructor', 'prototype'].includes(key) ||
45
+ Object.hasOwn(value, key))
46
+ return invalid;
47
+ const result = evaluateDefault(property.initializer, checker);
48
+ if (!result.ok)
49
+ return invalid;
50
+ value[key] = result.value;
51
+ }
52
+ return { ok: true, value };
53
+ }
54
+ if (ts.isPropertyAccessExpression(expression) ||
55
+ ts.isElementAccessExpression(expression)) {
56
+ const symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(expression) ? expression.name : expression);
57
+ if (symbol?.flags && symbol.flags & ts.SymbolFlags.EnumMember) {
58
+ const declaration = symbol.valueDeclaration;
59
+ const value = declaration && ts.isEnumMember(declaration)
60
+ ? checker.getConstantValue(declaration)
61
+ : undefined;
62
+ if (typeof value === 'string')
63
+ return { ok: true, value };
64
+ }
65
+ }
66
+ return invalid;
67
+ }
@@ -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,2 @@
1
+ export type { AnalyzeResult, Diagnostic, FieldIR, RootExport, SourceLocation, } from '../contracts.js';
2
+ //# sourceMappingURL=ir.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './cli/main.js';
3
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -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,18 @@
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
+ }
8
+ export interface InitResult {
9
+ readonly exitCode: 0 | 1 | 2;
10
+ readonly messages: readonly string[];
11
+ readonly warnings: readonly string[];
12
+ readonly diagnostics: readonly GenerateDiagnostic[];
13
+ }
14
+ export declare class InitProjectError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export declare function initializeProject(cwd: string, options: InitOptions): Promise<InitResult>;
18
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAUA,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;CAC7B;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,CA6GrB"}
@@ -0,0 +1,417 @@
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 ts from 'typescript';
5
+ import { generateProject } from '../generate.js';
6
+ import { CONVENTIONAL_INPUTS, discoverDefaultsPath, findNearestTsconfig, } from '../project/discovery.js';
7
+ const SCHEMA_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);
8
+ export class InitProjectError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'InitProjectError';
12
+ }
13
+ }
14
+ export async function initializeProject(cwd, options) {
15
+ const projectDirectory = resolve(cwd);
16
+ const packagePath = join(projectDirectory, 'package.json');
17
+ const packageDocument = readPackageDocument(packagePath);
18
+ const configPath = join(projectDirectory, 'typespun.json');
19
+ const existingConfig = existsSync(configPath)
20
+ ? readInitConfig(configPath)
21
+ : undefined;
22
+ const input = selectInput(projectDirectory, options, existingConfig);
23
+ const output = options.output ??
24
+ existingConfig?.output ??
25
+ `src/generated/typespun${extname(input)}`;
26
+ validateSchemaPath(input, '--input');
27
+ validateSchemaPath(output, '--output');
28
+ rejectConflictingOptions(options, existingConfig, input, output);
29
+ validateExistingSchemaStyle(projectDirectory, input, options.style);
30
+ const inputPath = resolve(projectDirectory, input);
31
+ const outputPath = resolve(projectDirectory, output);
32
+ if (pathsReferToSameFile(inputPath, outputPath)) {
33
+ throw new InitProjectError('Schema input and generated output must differ');
34
+ }
35
+ if (pathEntryExists(outputPath) && !isTypespunGeneratedOutput(outputPath)) {
36
+ throw new InitProjectError(`Initialization refuses to overwrite the existing output at ${output}`);
37
+ }
38
+ const config = {
39
+ ...existingConfig,
40
+ input,
41
+ output,
42
+ ...(options.envPrefix === undefined
43
+ ? existingConfig?.envPrefix === undefined
44
+ ? {}
45
+ : { envPrefix: existingConfig.envPrefix }
46
+ : { envPrefix: options.envPrefix }),
47
+ };
48
+ preflightResolvedConfig(projectDirectory, config, inputPath);
49
+ const configNeedsMissingKeys = existingConfig !== undefined &&
50
+ (existingConfig.input === undefined ||
51
+ existingConfig.output === undefined ||
52
+ (options.envPrefix !== undefined &&
53
+ existingConfig.envPrefix === undefined));
54
+ const messages = [];
55
+ if (!existsSync(inputPath)) {
56
+ await writeNewFile(inputPath, schemaTemplate(options.style ?? 'interface'));
57
+ messages.push(`Created ${input}.`);
58
+ }
59
+ const serializedConfig = `${JSON.stringify(config, null, 2)}\n`;
60
+ if (existingConfig === undefined) {
61
+ await writeNewFile(configPath, serializedConfig);
62
+ messages.push('Created typespun.json.');
63
+ }
64
+ else if (configNeedsMissingKeys) {
65
+ await writeFile(configPath, serializedConfig);
66
+ messages.push('Added missing paths to typespun.json.');
67
+ }
68
+ if (addMissingScripts(packageDocument)) {
69
+ await writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`);
70
+ messages.push('Added config:generate and config:check scripts.');
71
+ }
72
+ const missing = missingDependencies(projectDirectory);
73
+ if (missing.length > 0) {
74
+ return {
75
+ exitCode: 0,
76
+ messages,
77
+ warnings: installationGuidance(projectDirectory, packageDocument, missing),
78
+ diagnostics: [],
79
+ };
80
+ }
81
+ const generated = await generateProject({
82
+ projectDirectory,
83
+ configPath,
84
+ mode: 'write',
85
+ });
86
+ if (generated.diagnostics.length > 0) {
87
+ return {
88
+ exitCode: generated.diagnostics.some((diagnostic) => diagnostic.code === 'typescript_config')
89
+ ? 2
90
+ : 1,
91
+ messages,
92
+ warnings: [],
93
+ diagnostics: [...generated.warnings, ...generated.diagnostics],
94
+ };
95
+ }
96
+ messages.push(`${generated.status === 'written' ? 'Generated' : 'Unchanged'} ${displayPath(projectDirectory, generated.outputPath)}.`);
97
+ return {
98
+ exitCode: 0,
99
+ messages,
100
+ warnings: [],
101
+ diagnostics: generated.warnings,
102
+ };
103
+ }
104
+ function readPackageDocument(path) {
105
+ if (!isFile(path))
106
+ throw new InitProjectError('package.json is required');
107
+ let value;
108
+ try {
109
+ value = JSON.parse(readFileSync(path, 'utf8'));
110
+ }
111
+ catch {
112
+ throw new InitProjectError('package.json must contain valid JSON');
113
+ }
114
+ if (!isRecord(value)) {
115
+ throw new InitProjectError('package.json must contain an object');
116
+ }
117
+ if (value.scripts !== undefined && !isRecord(value.scripts)) {
118
+ throw new InitProjectError('package.json scripts must contain an object');
119
+ }
120
+ return value;
121
+ }
122
+ function validateTsconfig(path) {
123
+ if (!isFile(path))
124
+ throw new InitProjectError('tsconfig.json is required');
125
+ const loaded = ts.readConfigFile(path, ts.sys.readFile);
126
+ if (loaded.error !== undefined) {
127
+ throw new InitProjectError('tsconfig.json must contain usable TypeScript configuration');
128
+ }
129
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, dirname(path), undefined, path);
130
+ if (parsed.errors.some((diagnostic) => diagnostic.code !== 18003)) {
131
+ throw new InitProjectError('tsconfig.json must contain usable TypeScript configuration');
132
+ }
133
+ }
134
+ function preflightResolvedConfig(projectDirectory, config, inputPath) {
135
+ const tsconfigPath = config.tsconfig === undefined
136
+ ? findNearestTsconfig(inputPath)
137
+ : resolve(projectDirectory, config.tsconfig);
138
+ if (tsconfigPath === undefined) {
139
+ throw new InitProjectError(`Could not find tsconfig.json for ${displayPath(projectDirectory, inputPath)}`);
140
+ }
141
+ validateTsconfig(tsconfigPath);
142
+ const defaults = config.defaults;
143
+ const hasExplicitDefaultsPath = typeof defaults === 'string' ||
144
+ (isRecord(defaults) && typeof defaults.path === 'string');
145
+ if (!hasExplicitDefaultsPath) {
146
+ try {
147
+ discoverDefaultsPath(projectDirectory);
148
+ }
149
+ catch (error) {
150
+ throw new InitProjectError(error instanceof Error
151
+ ? error.message
152
+ : 'Defaults-file discovery failed');
153
+ }
154
+ }
155
+ }
156
+ function readInitConfig(path) {
157
+ let value;
158
+ try {
159
+ value = JSON.parse(readFileSync(path, 'utf8'));
160
+ }
161
+ catch {
162
+ throw new InitProjectError('typespun.json must contain valid JSON');
163
+ }
164
+ if (!isRecord(value)) {
165
+ throw new InitProjectError('typespun.json must contain an object');
166
+ }
167
+ const allowed = new Set([
168
+ 'input',
169
+ 'output',
170
+ 'tsconfig',
171
+ 'envPrefix',
172
+ 'defaults',
173
+ 'secretDefaults',
174
+ ]);
175
+ for (const key of Object.keys(value)) {
176
+ if (!allowed.has(key)) {
177
+ throw new InitProjectError(`Unknown typespun.json key: ${key}`);
178
+ }
179
+ }
180
+ for (const key of ['input', 'output', 'tsconfig', 'envPrefix']) {
181
+ if (value[key] !== undefined && typeof value[key] !== 'string') {
182
+ throw new InitProjectError(`typespun.json ${key} must be a string`);
183
+ }
184
+ }
185
+ validateDefaultsConfig(value.defaults);
186
+ if (value.secretDefaults !== undefined &&
187
+ value.secretDefaults !== 'warn' &&
188
+ value.secretDefaults !== 'allow' &&
189
+ value.secretDefaults !== 'error') {
190
+ throw new InitProjectError('typespun.json secretDefaults must be warn, allow, or error');
191
+ }
192
+ return value;
193
+ }
194
+ function validateDefaultsConfig(value) {
195
+ if (value === undefined || typeof value === 'string')
196
+ return;
197
+ if (!isRecord(value)) {
198
+ throw new InitProjectError('typespun.json defaults must be a string or object');
199
+ }
200
+ for (const key of Object.keys(value)) {
201
+ if (key !== 'path' && key !== 'unknownKeys') {
202
+ throw new InitProjectError(`Unknown typespun.json defaults key: ${key}`);
203
+ }
204
+ }
205
+ if (value.path !== undefined && typeof value.path !== 'string') {
206
+ throw new InitProjectError('typespun.json defaults.path must be a string');
207
+ }
208
+ if (value.unknownKeys !== undefined &&
209
+ value.unknownKeys !== 'error' &&
210
+ value.unknownKeys !== 'warn' &&
211
+ value.unknownKeys !== 'ignore') {
212
+ throw new InitProjectError('typespun.json defaults.unknownKeys must be error, warn, or ignore');
213
+ }
214
+ }
215
+ function selectInput(projectDirectory, options, existingConfig) {
216
+ if (options.input !== undefined)
217
+ return options.input;
218
+ if (existingConfig?.input !== undefined)
219
+ return existingConfig.input;
220
+ const candidates = CONVENTIONAL_INPUTS.filter((path) => isFile(resolve(projectDirectory, path)));
221
+ if (candidates.length > 1) {
222
+ throw new InitProjectError(`Multiple conventional schemas exist: ${candidates.join(', ')}; use --input.`);
223
+ }
224
+ return candidates[0] ?? 'src/config.ts';
225
+ }
226
+ function rejectConflictingOptions(options, existing, input, output) {
227
+ if (existing === undefined)
228
+ return;
229
+ const comparisons = [
230
+ ['--input', options.input, existing.input ?? input],
231
+ ['--output', options.output, existing.output ?? output],
232
+ [
233
+ '--env-prefix',
234
+ options.envPrefix,
235
+ existing.envPrefix ?? options.envPrefix,
236
+ ],
237
+ ];
238
+ for (const [flag, requested, configured] of comparisons) {
239
+ if (requested !== undefined && requested !== configured) {
240
+ throw new InitProjectError(`${flag} conflicts with the existing typespun.json`);
241
+ }
242
+ }
243
+ }
244
+ function validateExistingSchemaStyle(projectDirectory, input, requestedStyle) {
245
+ const path = resolve(projectDirectory, input);
246
+ if (requestedStyle === undefined || !isFile(path))
247
+ return;
248
+ const source = readFileSync(path, 'utf8');
249
+ const matches = requestedStyle === 'class'
250
+ ? /\b(?:export\s+)?class\s+[A-Za-z_$]/.test(source)
251
+ : /\b(?:export\s+)?interface\s+[A-Za-z_$]/.test(source);
252
+ if (!matches) {
253
+ throw new InitProjectError(`--style ${requestedStyle} conflicts with the existing schema at ${input}`);
254
+ }
255
+ }
256
+ function validateSchemaPath(path, flag) {
257
+ if (!SCHEMA_EXTENSIONS.has(extname(path))) {
258
+ throw new InitProjectError(`${flag} must use a .ts, .mts, or .cts extension`);
259
+ }
260
+ }
261
+ function schemaTemplate(style) {
262
+ return style === 'interface'
263
+ ? `/** @typespun */\nexport interface AppConfig {\n port: number;\n}\n`
264
+ : `import { Config } from 'typespun';\n\n@Config()\nexport class AppConfig {\n port!: number;\n}\n`;
265
+ }
266
+ function addMissingScripts(document) {
267
+ const scripts = document.scripts ?? {};
268
+ let changed = false;
269
+ if (scripts['config:generate'] === undefined) {
270
+ scripts['config:generate'] = 'typespun generate';
271
+ changed = true;
272
+ }
273
+ if (scripts['config:check'] === undefined) {
274
+ scripts['config:check'] = 'typespun check';
275
+ changed = true;
276
+ }
277
+ if (document.scripts === undefined)
278
+ document.scripts = scripts;
279
+ return changed;
280
+ }
281
+ function missingDependencies(projectDirectory) {
282
+ return ['typespun', 'typespun-codegen'].filter((name) => !findInstalledPackage(projectDirectory, name));
283
+ }
284
+ function findInstalledPackage(projectDirectory, packageName) {
285
+ let directory = resolve(projectDirectory);
286
+ while (true) {
287
+ if (isFile(join(directory, 'node_modules', packageName, 'package.json'))) {
288
+ return true;
289
+ }
290
+ const parent = dirname(directory);
291
+ if (parent === directory)
292
+ return false;
293
+ directory = parent;
294
+ }
295
+ }
296
+ function installationGuidance(projectDirectory, packageDocument, missing) {
297
+ const manager = detectPackageManager(projectDirectory, packageDocument);
298
+ const commands = [];
299
+ if (missing.includes('typespun')) {
300
+ commands.push(installCommand(manager, 'typespun', false));
301
+ }
302
+ if (missing.includes('typespun-codegen')) {
303
+ commands.push(installCommand(manager, 'typespun-codegen', true));
304
+ }
305
+ return [
306
+ 'Dependencies are missing; generated output was not created. Install them, then run typespun generate:',
307
+ ...commands,
308
+ ];
309
+ }
310
+ function detectPackageManager(projectDirectory, document) {
311
+ const configured = document.packageManager?.split('@')[0];
312
+ if (configured === 'bun' ||
313
+ configured === 'npm' ||
314
+ configured === 'pnpm' ||
315
+ configured === 'yarn') {
316
+ return configured;
317
+ }
318
+ if (isFile(join(projectDirectory, 'bun.lock')) ||
319
+ isFile(join(projectDirectory, 'bun.lockb'))) {
320
+ return 'bun';
321
+ }
322
+ if (isFile(join(projectDirectory, 'pnpm-lock.yaml')))
323
+ return 'pnpm';
324
+ if (isFile(join(projectDirectory, 'yarn.lock')))
325
+ return 'yarn';
326
+ return 'npm';
327
+ }
328
+ function installCommand(manager, packageName, development) {
329
+ if (manager === 'npm') {
330
+ return `npm install${development ? ' --save-dev' : ''} ${packageName}`;
331
+ }
332
+ if (manager === 'yarn') {
333
+ return `yarn add${development ? ' --dev' : ''} ${packageName}`;
334
+ }
335
+ const developmentFlag = development
336
+ ? manager === 'pnpm'
337
+ ? ' --save-dev'
338
+ : ' --dev'
339
+ : '';
340
+ return `${manager} add${developmentFlag} ${packageName}`;
341
+ }
342
+ async function writeNewFile(path, contents) {
343
+ await mkdir(dirname(path), { recursive: true });
344
+ await writeFile(path, contents, { encoding: 'utf8', flag: 'wx' });
345
+ }
346
+ function displayPath(projectDirectory, path) {
347
+ return path.startsWith(`${projectDirectory}/`)
348
+ ? path.slice(projectDirectory.length + 1)
349
+ : path;
350
+ }
351
+ function pathsReferToSameFile(first, second) {
352
+ const firstIdentity = fileIdentity(first);
353
+ const secondIdentity = fileIdentity(second);
354
+ if (firstIdentity.stat !== undefined &&
355
+ secondIdentity.stat !== undefined &&
356
+ firstIdentity.stat.dev === secondIdentity.stat.dev &&
357
+ firstIdentity.stat.ino === secondIdentity.stat.ino) {
358
+ return true;
359
+ }
360
+ return firstIdentity.canonicalPath === secondIdentity.canonicalPath;
361
+ }
362
+ function fileIdentity(path) {
363
+ try {
364
+ return { canonicalPath: realpathSync(path), stat: statSync(path) };
365
+ }
366
+ catch {
367
+ let ancestor = dirname(path);
368
+ const remainder = [basename(path)];
369
+ while (true) {
370
+ try {
371
+ return {
372
+ canonicalPath: resolve(realpathSync(ancestor), ...remainder),
373
+ };
374
+ }
375
+ catch {
376
+ const parent = dirname(ancestor);
377
+ if (parent === ancestor)
378
+ return { canonicalPath: resolve(path) };
379
+ remainder.unshift(basename(ancestor));
380
+ ancestor = parent;
381
+ }
382
+ }
383
+ }
384
+ }
385
+ function isTypespunGeneratedOutput(path) {
386
+ let contents;
387
+ try {
388
+ contents = readFileSync(path, 'utf8');
389
+ }
390
+ catch {
391
+ return false;
392
+ }
393
+ return (/^\/\/ Generated by typespun-codegen\. Do not edit\.\n\/\/ Schema fingerprint: [a-f0-9]{64}\n\n/u.test(contents) &&
394
+ contents.includes("from 'typespun/generated';") &&
395
+ contents.includes('export type Config = TypespunConfig;') &&
396
+ contents.includes('export const loadConfig = createLoader<Config>(schema);'));
397
+ }
398
+ function isFile(path) {
399
+ try {
400
+ return existsSync(path) && statSync(path).isFile();
401
+ }
402
+ catch {
403
+ return false;
404
+ }
405
+ }
406
+ function pathEntryExists(path) {
407
+ try {
408
+ lstatSync(path);
409
+ return true;
410
+ }
411
+ catch {
412
+ return false;
413
+ }
414
+ }
415
+ function isRecord(value) {
416
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
417
+ }
@@ -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":"AAwBA,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"}