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,157 @@
1
+ import { generateProject } from '../generate.js';
2
+ import { formatDiagnostic } from './diagnostics.js';
3
+ import { initializeProject } from './init.js';
4
+ const HELP = `Usage: typespun <command> [options]
5
+
6
+ Commands:
7
+ init Initialize a Typespun project
8
+ generate Generate the typed configuration loader
9
+ check Check that generated output is current
10
+
11
+ Options:
12
+ -h, --help Show help
13
+
14
+ Init options:
15
+ --style interface|class
16
+ --input <path>
17
+ --output <path>
18
+ --env-prefix <prefix>
19
+
20
+ Generate/check options:
21
+ --config <path>
22
+ `;
23
+ class UsageError extends Error {
24
+ }
25
+ export async function runCli(args, streams = process, cwd = process.cwd()) {
26
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
27
+ streams.stdout.write(HELP);
28
+ return 0;
29
+ }
30
+ try {
31
+ const [command, ...commandArgs] = args;
32
+ if (command === 'generate' || command === 'check') {
33
+ if (isHelpOnly(commandArgs)) {
34
+ streams.stdout.write(HELP);
35
+ return 0;
36
+ }
37
+ const configPath = parseConfigArguments(commandArgs);
38
+ const result = await generateProject({
39
+ projectDirectory: cwd,
40
+ mode: command === 'generate' ? 'write' : 'check',
41
+ ...(configPath === undefined ? {} : { configPath }),
42
+ });
43
+ const color = streams.stderr.isTTY === true && process.env.NO_COLOR === undefined;
44
+ for (const warning of result.warnings) {
45
+ streams.stderr.write(`${formatDiagnostic(warning, { cwd, color })}\n`);
46
+ }
47
+ for (const diagnostic of result.diagnostics) {
48
+ streams.stderr.write(`${formatDiagnostic(diagnostic, { cwd, color })}\n`);
49
+ }
50
+ if (result.diagnostics.length > 0) {
51
+ return result.diagnostics.some((diagnostic) => diagnostic.code === 'typescript_config')
52
+ ? 2
53
+ : 1;
54
+ }
55
+ if (result.status === 'stale') {
56
+ streams.stderr.write(`${relativeOutput(result.outputPath, cwd)} is stale; run typespun generate.\n`);
57
+ return 1;
58
+ }
59
+ streams.stdout.write(command === 'generate'
60
+ ? `${result.status === 'written' ? 'Generated' : 'Unchanged'} ${relativeOutput(result.outputPath, cwd)}.\n`
61
+ : `${relativeOutput(result.outputPath, cwd)} is up to date.\n`);
62
+ return 0;
63
+ }
64
+ if (command === 'init') {
65
+ if (isHelpOnly(commandArgs)) {
66
+ streams.stdout.write(HELP);
67
+ return 0;
68
+ }
69
+ const result = await initializeProject(cwd, parseInitArguments(commandArgs));
70
+ for (const line of result.messages)
71
+ streams.stdout.write(`${line}\n`);
72
+ for (const line of result.warnings)
73
+ streams.stderr.write(`${line}\n`);
74
+ const color = streams.stderr.isTTY === true && process.env.NO_COLOR === undefined;
75
+ for (const diagnostic of result.diagnostics) {
76
+ streams.stderr.write(`${formatDiagnostic(diagnostic, { cwd, color })}\n`);
77
+ }
78
+ return result.exitCode;
79
+ }
80
+ throw new UsageError(`Unknown command: ${command ?? ''}`);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ streams.stderr.write(`typespun: ${message}\n`);
85
+ if (error instanceof UsageError || isProjectConfigurationError(error)) {
86
+ streams.stderr.write('Run typespun --help for usage.\n');
87
+ return 2;
88
+ }
89
+ return 1;
90
+ }
91
+ }
92
+ function parseConfigArguments(args) {
93
+ let configPath;
94
+ for (let index = 0; index < args.length; index++) {
95
+ const argument = args[index];
96
+ if (argument !== '--config')
97
+ throw new UsageError(`Unknown option: ${argument}`);
98
+ if (configPath !== undefined)
99
+ throw new UsageError('--config may be specified only once');
100
+ configPath = requiredValue(args, ++index, '--config');
101
+ }
102
+ return configPath;
103
+ }
104
+ function parseInitArguments(args) {
105
+ const options = {};
106
+ for (let index = 0; index < args.length; index++) {
107
+ const argument = args[index];
108
+ if (argument === '--style') {
109
+ const style = requiredValue(args, ++index, argument);
110
+ if (style !== 'interface' && style !== 'class') {
111
+ throw new UsageError('--style must be interface or class');
112
+ }
113
+ if (options.style !== undefined)
114
+ throw new UsageError('--style may be specified only once');
115
+ options.style = style;
116
+ }
117
+ else if (argument === '--input') {
118
+ options.input = uniqueValue(options.input, requiredValue(args, ++index, argument), argument);
119
+ }
120
+ else if (argument === '--output') {
121
+ options.output = uniqueValue(options.output, requiredValue(args, ++index, argument), argument);
122
+ }
123
+ else if (argument === '--env-prefix') {
124
+ options.envPrefix = uniqueValue(options.envPrefix, requiredValue(args, ++index, argument), argument);
125
+ }
126
+ else {
127
+ throw new UsageError(`Unknown option: ${argument}`);
128
+ }
129
+ }
130
+ return options;
131
+ }
132
+ function requiredValue(args, index, flag) {
133
+ const value = args[index];
134
+ if (value === undefined || value.length === 0 || value.startsWith('--')) {
135
+ throw new UsageError(`${flag} requires a value`);
136
+ }
137
+ return value;
138
+ }
139
+ function uniqueValue(previous, value, flag) {
140
+ if (previous !== undefined)
141
+ throw new UsageError(`${flag} may be specified only once`);
142
+ return value;
143
+ }
144
+ function isHelpOnly(args) {
145
+ return args.length === 1 && (args[0] === '--help' || args[0] === '-h');
146
+ }
147
+ function isProjectConfigurationError(error) {
148
+ if (!(error instanceof Error))
149
+ return false;
150
+ return (error.name === 'ProjectConfigError' ||
151
+ error.name === 'ProjectDiscoveryError' ||
152
+ error.name === 'InitProjectError' ||
153
+ error.message.startsWith('Could not find tsconfig.json'));
154
+ }
155
+ function relativeOutput(path, cwd) {
156
+ return (path.startsWith(cwd) ? path.slice(cwd.length + 1) : path) || path;
157
+ }
@@ -0,0 +1,33 @@
1
+ import type { FieldKind, FieldSchema } from 'typespun/generated';
2
+ export interface SourceLocation {
3
+ readonly file: string;
4
+ readonly line: number;
5
+ readonly column: number;
6
+ }
7
+ export interface Diagnostic {
8
+ readonly code: string;
9
+ readonly message: string;
10
+ readonly location: SourceLocation;
11
+ readonly suggestion?: string;
12
+ }
13
+ export interface FieldIR extends FieldSchema {
14
+ readonly location: SourceLocation;
15
+ readonly kind: FieldKind;
16
+ }
17
+ export type RootExport = {
18
+ readonly kind: 'named';
19
+ readonly name: string;
20
+ } | {
21
+ readonly kind: 'default';
22
+ };
23
+ export interface AnalyzeResult {
24
+ readonly inputPath: string;
25
+ readonly rootName?: string;
26
+ /** The import binding for the selected root, present on successful analysis. */
27
+ readonly rootExport?: RootExport;
28
+ readonly fields: readonly FieldIR[];
29
+ readonly diagnostics: readonly Diagnostic[];
30
+ }
31
+ export type UnknownKeysPolicy = 'error' | 'warn' | 'ignore';
32
+ export type SecretDefaultsPolicy = 'warn' | 'allow' | 'error';
33
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,OAAQ,SAAQ,WAAW;IAC1C,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;CAC7C;AAED,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE5D,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import ts from 'typescript';
2
+ import type { FieldIR, RootExport } from '../contracts.js';
3
+ export interface EmitGeneratedModuleOptions {
4
+ readonly rootExport: RootExport;
5
+ readonly rootName: string;
6
+ readonly typeImport: string;
7
+ readonly fields: readonly FieldIR[];
8
+ readonly compiledDefaults: Readonly<Record<string, unknown>>;
9
+ readonly fingerprint: string;
10
+ }
11
+ export declare function emitGeneratedModule(options: EmitGeneratedModuleOptions): string;
12
+ export declare function relativeTypeImportSpecifier(inputPath: string, outputPath: string, compilerOptions: ts.CompilerOptions): string;
13
+ //# sourceMappingURL=emit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/emitter/emit.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG3D,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,MAAM,CAwBR;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,EAAE,CAAC,eAAe,GAClC,MAAM,CAUR"}
@@ -0,0 +1,65 @@
1
+ import { dirname, extname, relative, sep } from 'node:path';
2
+ import ts from 'typescript';
3
+ import { stableJson } from './fingerprint.js';
4
+ export function emitGeneratedModule(options) {
5
+ const fields = options.fields.map(stripLocation);
6
+ const schema = {
7
+ compiledDefaults: options.compiledDefaults,
8
+ fields,
9
+ protocolVersion: 1,
10
+ };
11
+ const typeImport = options.rootExport.kind === 'default'
12
+ ? `import type TypespunConfig from ${quote(options.typeImport)};`
13
+ : `import type { ${formatExportName(options.rootExport.name)} as TypespunConfig } from ${quote(options.typeImport)};`;
14
+ return `// Generated by typespun-codegen. Do not edit.
15
+ // Schema fingerprint: ${options.fingerprint}
16
+
17
+ import { createLoader, type GeneratedSchema } from 'typespun/generated';
18
+ ${typeImport}
19
+
20
+ export type Config = TypespunConfig;
21
+
22
+ const schema = ${stableJson(schema, 2)} as const satisfies GeneratedSchema;
23
+
24
+ export const loadConfig = createLoader<Config>(schema);
25
+ `;
26
+ }
27
+ export function relativeTypeImportSpecifier(inputPath, outputPath, compilerOptions) {
28
+ const relativePath = relative(dirname(outputPath), inputPath)
29
+ .split(sep)
30
+ .join('/');
31
+ const withoutExtension = relativePath.slice(0, -extname(relativePath).length);
32
+ const prefix = withoutExtension.startsWith('.')
33
+ ? withoutExtension
34
+ : `./${withoutExtension}`;
35
+ return `${prefix}${usesNodeEmittedSpecifiers(compilerOptions) ? emittedExtension(inputPath) : ''}`;
36
+ }
37
+ function stripLocation(field) {
38
+ const { location: _location, ...schemaField } = field;
39
+ return schemaField;
40
+ }
41
+ function usesNodeEmittedSpecifiers(options) {
42
+ return (options.moduleResolution === ts.ModuleResolutionKind.Node16 ||
43
+ options.moduleResolution === ts.ModuleResolutionKind.NodeNext ||
44
+ options.moduleResolution === ts.ModuleResolutionKind.Bundler ||
45
+ (options.moduleResolution === undefined &&
46
+ (options.module === ts.ModuleKind.Node16 ||
47
+ options.module === ts.ModuleKind.NodeNext)));
48
+ }
49
+ function emittedExtension(path) {
50
+ switch (extname(path)) {
51
+ case '.mts':
52
+ return '.mjs';
53
+ case '.cts':
54
+ return '.cjs';
55
+ default:
56
+ return '.js';
57
+ }
58
+ }
59
+ function formatExportName(name) {
60
+ return /^[$A-Z_a-z][$\w]*$/u.test(name) ? name : quote(name);
61
+ }
62
+ function quote(value) {
63
+ const json = stableJson(value);
64
+ return `'${json.slice(1, -1).replace(/'/g, "\\'").replace(/\\"/g, '"')}'`;
65
+ }
@@ -0,0 +1,10 @@
1
+ export interface FingerprintInput {
2
+ readonly protocolVersion: number;
3
+ readonly generatorVersion: string;
4
+ readonly configuration: unknown;
5
+ readonly analysis: unknown;
6
+ readonly compiledDefaults: unknown;
7
+ }
8
+ export declare function createFingerprint(input: FingerprintInput): string;
9
+ export declare function stableJson(value: unknown, space?: number): string;
10
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/emitter/fingerprint.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAEjE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE"}
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function createFingerprint(input) {
3
+ return createHash('sha256').update(stableJson(input)).digest('hex');
4
+ }
5
+ export function stableJson(value, space) {
6
+ return escapeLineSeparators(JSON.stringify(canonicalize(value), null, space));
7
+ }
8
+ function canonicalize(value) {
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => canonicalize(item));
11
+ }
12
+ if (isRecord(value)) {
13
+ const result = {};
14
+ for (const key of Object.keys(value).sort()) {
15
+ if (value[key] !== undefined) {
16
+ result[key] = canonicalize(value[key]);
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ return value;
22
+ }
23
+ function escapeLineSeparators(value) {
24
+ return value.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
25
+ }
26
+ function isRecord(value) {
27
+ return typeof value === 'object' && value !== null;
28
+ }
@@ -0,0 +1,29 @@
1
+ import type { Diagnostic } from './contracts.js';
2
+ import { type FingerprintInput } from './emitter/fingerprint.js';
3
+ import { type DefaultsDiagnostic } from './project/defaults.js';
4
+ export interface GenerateProjectOptions {
5
+ readonly configPath?: string;
6
+ readonly projectDirectory?: string;
7
+ readonly mode: 'write' | 'check';
8
+ }
9
+ export interface GenerationIoDiagnostic {
10
+ readonly code: 'defaults_read_failed';
11
+ readonly file: string;
12
+ readonly path: string;
13
+ readonly message: string;
14
+ }
15
+ export interface GenerationSafetyDiagnostic {
16
+ readonly code: 'overlapping_paths';
17
+ readonly message: string;
18
+ }
19
+ export type GenerateDiagnostic = Diagnostic | DefaultsDiagnostic | GenerationIoDiagnostic | GenerationSafetyDiagnostic;
20
+ export interface GenerateResult {
21
+ readonly status: 'unchanged' | 'written' | 'stale';
22
+ readonly outputPath: string;
23
+ readonly warnings: readonly GenerateDiagnostic[];
24
+ readonly diagnostics: readonly GenerateDiagnostic[];
25
+ }
26
+ export declare function generateProject(options: GenerateProjectOptions): Promise<GenerateResult>;
27
+ export declare function createGenerationFingerprint(input: Omit<FingerprintInput, 'generatorVersion'>, packageJsonUrl?: URL): string;
28
+ export declare function atomicWrite(path: string, contents: string, temporaryPathForAttempt?: (attempt: number) => string): Promise<void>;
29
+ //# sourceMappingURL=generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,gBAAgB,CAAC;AAK1D,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAM/B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GACV,kBAAkB,GAClB,sBAAsB,GACtB,0BAA0B,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACrD;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,cAAc,CAAC,CA4GzB;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,EACjD,cAAc,GAAE,GAAsB,GACrC,MAAM,CAYR;AA0ED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,uBAAuB,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MACI,GACjD,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
@@ -0,0 +1,267 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { mkdir, open, readFile, realpath, rename, stat, unlink, } from 'node:fs/promises';
4
+ import { basename, dirname, relative, resolve, sep } from 'node:path';
5
+ import ts from 'typescript';
6
+ import { analyzeProgram } from './analyzer/analyze.js';
7
+ import { emitGeneratedModule, relativeTypeImportSpecifier, } from './emitter/emit.js';
8
+ import { createFingerprint, } from './emitter/fingerprint.js';
9
+ import { loadProjectConfig, } from './project/config.js';
10
+ import { compileDefaults, } from './project/defaults.js';
11
+ const PROTOCOL_VERSION = 1;
12
+ const TEMPORARY_ATTEMPTS = 8;
13
+ const PACKAGE_JSON_URL = new URL('../package.json', import.meta.url);
14
+ export async function generateProject(options) {
15
+ const projectDirectory = resolve(options.projectDirectory ?? process.cwd());
16
+ const config = loadProjectConfig({
17
+ projectDirectory,
18
+ ...(options.configPath === undefined
19
+ ? {}
20
+ : { configPath: options.configPath }),
21
+ });
22
+ if (await referToSameFile(config.inputPath, config.outputPath)) {
23
+ return failedResult(config.outputPath, [
24
+ {
25
+ code: 'overlapping_paths',
26
+ message: 'The schema input and generated output must be different files.',
27
+ },
28
+ ]);
29
+ }
30
+ const programResult = createProjectProgram(config);
31
+ if (programResult.diagnostics.length > 0) {
32
+ return failedResult(config.outputPath, programResult.diagnostics);
33
+ }
34
+ const analysis = analyzeProgram(programResult.program, config.inputPath, config.envPrefix);
35
+ if (analysis.diagnostics.length > 0 ||
36
+ analysis.rootName === undefined ||
37
+ analysis.rootExport === undefined) {
38
+ return failedResult(config.outputPath, analysis.diagnostics);
39
+ }
40
+ const defaultsDocument = config.defaultsPath === undefined
41
+ ? { path: config.inputPath, content: '{}' }
42
+ : await readDefaults(config.defaultsPath);
43
+ if ('diagnostic' in defaultsDocument) {
44
+ return failedResult(config.outputPath, [defaultsDocument.diagnostic]);
45
+ }
46
+ const defaults = compileDefaults({
47
+ path: defaultsDocument.path,
48
+ content: defaultsDocument.content,
49
+ }, analysis.fields, {
50
+ unknownKeys: config.unknownKeys,
51
+ secretDefaults: config.secretDefaults,
52
+ });
53
+ if (defaults.errors.length > 0) {
54
+ return failedResult(config.outputPath, defaults.errors, defaults.warnings);
55
+ }
56
+ const portableConfig = normalizeConfiguration(config);
57
+ const typeImport = relativeTypeImportSpecifier(config.inputPath, config.outputPath, programResult.program.getCompilerOptions());
58
+ const portableAnalysis = {
59
+ rootExport: analysis.rootExport,
60
+ fields: analysis.fields.map(withoutLocation),
61
+ typeImport,
62
+ };
63
+ const fingerprint = createGenerationFingerprint({
64
+ protocolVersion: PROTOCOL_VERSION,
65
+ configuration: portableConfig,
66
+ analysis: portableAnalysis,
67
+ compiledDefaults: defaults.values,
68
+ });
69
+ const canonical = emitGeneratedModule({
70
+ rootExport: analysis.rootExport,
71
+ rootName: analysis.rootName,
72
+ typeImport,
73
+ fields: analysis.fields,
74
+ compiledDefaults: defaults.values,
75
+ fingerprint,
76
+ });
77
+ const existing = await readExisting(config.outputPath);
78
+ if (existing === canonical) {
79
+ return {
80
+ status: 'unchanged',
81
+ outputPath: config.outputPath,
82
+ warnings: defaults.warnings,
83
+ diagnostics: [],
84
+ };
85
+ }
86
+ if (options.mode === 'check') {
87
+ return {
88
+ status: 'stale',
89
+ outputPath: config.outputPath,
90
+ warnings: defaults.warnings,
91
+ diagnostics: [],
92
+ };
93
+ }
94
+ await atomicWrite(config.outputPath, canonical);
95
+ return {
96
+ status: 'written',
97
+ outputPath: config.outputPath,
98
+ warnings: defaults.warnings,
99
+ diagnostics: [],
100
+ };
101
+ }
102
+ export function createGenerationFingerprint(input, packageJsonUrl = PACKAGE_JSON_URL) {
103
+ const manifest = JSON.parse(readFileSync(packageJsonUrl, 'utf8'));
104
+ if (typeof manifest !== 'object' ||
105
+ manifest === null ||
106
+ !('version' in manifest) ||
107
+ typeof manifest.version !== 'string' ||
108
+ manifest.version.length === 0) {
109
+ throw new Error('typespun-codegen package version is missing');
110
+ }
111
+ return createFingerprint({ ...input, generatorVersion: manifest.version });
112
+ }
113
+ function createProjectProgram(config) {
114
+ const loaded = ts.readConfigFile(config.tsconfigPath, ts.sys.readFile);
115
+ if (loaded.error !== undefined) {
116
+ return {
117
+ program: ts.createProgram([], {}),
118
+ diagnostics: [typescriptConfigDiagnostic(config.tsconfigPath)],
119
+ };
120
+ }
121
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, dirname(config.tsconfigPath), undefined, config.tsconfigPath);
122
+ if (parsed.errors.length > 0) {
123
+ return {
124
+ program: ts.createProgram([], parsed.options),
125
+ diagnostics: [typescriptConfigDiagnostic(config.tsconfigPath)],
126
+ };
127
+ }
128
+ const rootNames = Array.from(new Set([
129
+ ...parsed.fileNames.map((path) => resolve(path)),
130
+ resolve(config.inputPath),
131
+ ]));
132
+ return {
133
+ program: ts.createProgram(rootNames, parsed.options),
134
+ diagnostics: [],
135
+ };
136
+ }
137
+ function typescriptConfigDiagnostic(path) {
138
+ return {
139
+ code: 'typescript_config',
140
+ message: 'The TypeScript project configuration could not be loaded.',
141
+ location: { file: path, line: 1, column: 1 },
142
+ };
143
+ }
144
+ async function readDefaults(path) {
145
+ try {
146
+ return { path, content: await readFile(path, 'utf8') };
147
+ }
148
+ catch {
149
+ return {
150
+ diagnostic: {
151
+ code: 'defaults_read_failed',
152
+ file: path,
153
+ path: '',
154
+ message: 'The configured defaults file could not be read.',
155
+ },
156
+ };
157
+ }
158
+ }
159
+ async function readExisting(path) {
160
+ try {
161
+ return await readFile(path, 'utf8');
162
+ }
163
+ catch (error) {
164
+ if (isNodeError(error) && error.code === 'ENOENT')
165
+ return undefined;
166
+ throw error;
167
+ }
168
+ }
169
+ export async function atomicWrite(path, contents, temporaryPathForAttempt = (_attempt) => `${dirname(path)}/.typespun.${randomUUID()}.tmp`) {
170
+ await mkdir(dirname(path), { recursive: true });
171
+ let temporaryPath;
172
+ let handle;
173
+ for (let attempt = 0; attempt < TEMPORARY_ATTEMPTS; attempt++) {
174
+ const candidate = temporaryPathForAttempt(attempt);
175
+ try {
176
+ handle = await open(candidate, 'wx');
177
+ temporaryPath = candidate;
178
+ break;
179
+ }
180
+ catch (error) {
181
+ if (isNodeError(error) && error.code === 'EEXIST')
182
+ continue;
183
+ throw error;
184
+ }
185
+ }
186
+ if (handle === undefined || temporaryPath === undefined) {
187
+ throw new Error('Could not reserve a temporary generated-output file.');
188
+ }
189
+ let renamed = false;
190
+ try {
191
+ await handle.writeFile(contents, 'utf8');
192
+ await handle.sync();
193
+ await handle.close();
194
+ handle = undefined;
195
+ await rename(temporaryPath, path);
196
+ renamed = true;
197
+ }
198
+ finally {
199
+ await handle?.close().catch(() => undefined);
200
+ if (!renamed)
201
+ await unlink(temporaryPath).catch(() => undefined);
202
+ }
203
+ }
204
+ function failedResult(outputPath, diagnostics, warnings = []) {
205
+ return { status: 'unchanged', outputPath, warnings, diagnostics };
206
+ }
207
+ function normalizeConfiguration(config) {
208
+ return {
209
+ input: portableRelative(config.configDirectory, config.inputPath),
210
+ output: portableRelative(config.configDirectory, config.outputPath),
211
+ tsconfig: portableRelative(config.configDirectory, config.tsconfigPath),
212
+ ...(config.defaultsPath === undefined
213
+ ? {}
214
+ : {
215
+ defaults: portableRelative(config.configDirectory, config.defaultsPath),
216
+ }),
217
+ ...(config.envPrefix === undefined ? {} : { envPrefix: config.envPrefix }),
218
+ unknownKeys: config.unknownKeys,
219
+ secretDefaults: config.secretDefaults,
220
+ };
221
+ }
222
+ async function referToSameFile(first, second) {
223
+ const [firstIdentity, secondIdentity] = await Promise.all([
224
+ fileIdentity(first),
225
+ fileIdentity(second),
226
+ ]);
227
+ if (firstIdentity.stat !== undefined &&
228
+ secondIdentity.stat !== undefined &&
229
+ firstIdentity.stat.dev === secondIdentity.stat.dev &&
230
+ firstIdentity.stat.ino === secondIdentity.stat.ino) {
231
+ return true;
232
+ }
233
+ return firstIdentity.canonicalPath === secondIdentity.canonicalPath;
234
+ }
235
+ async function fileIdentity(path) {
236
+ try {
237
+ return { canonicalPath: await realpath(path), stat: await stat(path) };
238
+ }
239
+ catch {
240
+ let ancestor = dirname(path);
241
+ const remainder = [basename(path)];
242
+ while (true) {
243
+ try {
244
+ return {
245
+ canonicalPath: resolve(await realpath(ancestor), ...remainder),
246
+ };
247
+ }
248
+ catch {
249
+ const parent = dirname(ancestor);
250
+ if (parent === ancestor)
251
+ return { canonicalPath: resolve(path) };
252
+ remainder.unshift(basename(ancestor));
253
+ ancestor = parent;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ function portableRelative(from, to) {
259
+ return relative(from, to).split(sep).join('/');
260
+ }
261
+ function withoutLocation(field) {
262
+ const { location: _location, ...portable } = field;
263
+ return portable;
264
+ }
265
+ function isNodeError(error) {
266
+ return error instanceof Error && 'code' in error;
267
+ }
@@ -0,0 +1,3 @@
1
+ /** Public code-generation entry point. APIs will be added in the first implementation slice. */
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,OAAO,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Public code-generation entry point. APIs will be added in the first implementation slice. */
2
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { SecretDefaultsPolicy, UnknownKeysPolicy } from '../contracts.js';
2
+ export interface LoadProjectConfigOptions {
3
+ readonly projectDirectory: string;
4
+ readonly configPath?: string;
5
+ }
6
+ export interface ProjectConfigResult {
7
+ readonly configDirectory: string;
8
+ readonly inputPath: string;
9
+ readonly outputPath: string;
10
+ readonly tsconfigPath: string;
11
+ readonly defaultsPath?: string;
12
+ readonly envPrefix?: string;
13
+ readonly unknownKeys: UnknownKeysPolicy;
14
+ readonly secretDefaults: SecretDefaultsPolicy;
15
+ }
16
+ export declare class ProjectConfigError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function loadProjectConfig(options: LoadProjectConfigOptions): ProjectConfigResult;
20
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/project/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAO/E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;CAC/C;AA4BD,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,wBAAwB,GAChC,mBAAmB,CAuCrB"}