ormbridge 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +408 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/cliRunner.d.ts +20 -0
- package/dist/cliRunner.js +343 -0
- package/dist/cliRunner.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +204 -0
- package/dist/config.js.map +1 -0
- package/dist/convert.d.ts +20 -0
- package/dist/convert.js +61 -0
- package/dist/convert.js.map +1 -0
- package/dist/emitters/django.d.ts +4 -0
- package/dist/emitters/django.js +501 -0
- package/dist/emitters/django.js.map +1 -0
- package/dist/emitters/doctrine.d.ts +20 -0
- package/dist/emitters/doctrine.js +1134 -0
- package/dist/emitters/doctrine.js.map +1 -0
- package/dist/emitters/graphene.d.ts +4 -0
- package/dist/emitters/graphene.js +431 -0
- package/dist/emitters/graphene.js.map +1 -0
- package/dist/emitters/laravel.d.ts +33 -0
- package/dist/emitters/laravel.js +1695 -0
- package/dist/emitters/laravel.js.map +1 -0
- package/dist/emitters/prisma.d.ts +21 -0
- package/dist/emitters/prisma.js +1058 -0
- package/dist/emitters/prisma.js.map +1 -0
- package/dist/emitters/typeorm.d.ts +9 -0
- package/dist/emitters/typeorm.js +833 -0
- package/dist/emitters/typeorm.js.map +1 -0
- package/dist/emitters/typescriptInterfaces.d.ts +15 -0
- package/dist/emitters/typescriptInterfaces.js +350 -0
- package/dist/emitters/typescriptInterfaces.js.map +1 -0
- package/dist/formats.d.ts +80 -0
- package/dist/formats.js +212 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +48 -0
- package/dist/io.js +328 -0
- package/dist/io.js.map +1 -0
- package/dist/ir.d.ts +197 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +88 -0
- package/dist/naming.js.map +1 -0
- package/dist/parsers/django.d.ts +14 -0
- package/dist/parsers/django.js +1161 -0
- package/dist/parsers/django.js.map +1 -0
- package/dist/parsers/doctrine.d.ts +12 -0
- package/dist/parsers/doctrine.js +1782 -0
- package/dist/parsers/doctrine.js.map +1 -0
- package/dist/parsers/laravel.d.ts +17 -0
- package/dist/parsers/laravel.js +2824 -0
- package/dist/parsers/laravel.js.map +1 -0
- package/dist/parsers/phpSyntax.d.ts +124 -0
- package/dist/parsers/phpSyntax.js +429 -0
- package/dist/parsers/phpSyntax.js.map +1 -0
- package/dist/parsers/prisma.d.ts +12 -0
- package/dist/parsers/prisma.js +964 -0
- package/dist/parsers/prisma.js.map +1 -0
- package/dist/parsers/pythonSyntax.d.ts +38 -0
- package/dist/parsers/pythonSyntax.js +120 -0
- package/dist/parsers/pythonSyntax.js.map +1 -0
- package/dist/parsers/typeorm.d.ts +12 -0
- package/dist/parsers/typeorm.js +1614 -0
- package/dist/parsers/typeorm.js.map +1 -0
- package/dist/parsers/typescriptSyntax.d.ts +57 -0
- package/dist/parsers/typescriptSyntax.js +200 -0
- package/dist/parsers/typescriptSyntax.js.map +1 -0
- package/dist/prismaOnlyConstructs.d.ts +7 -0
- package/dist/prismaOnlyConstructs.js +76 -0
- package/dist/prismaOnlyConstructs.js.map +1 -0
- package/dist/result.d.ts +20 -0
- package/dist/result.js +18 -0
- package/dist/result.js.map +1 -0
- package/dist/transforms.d.ts +13 -0
- package/dist/transforms.js +142 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +83 -4
package/dist/formats.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { emitDjango } from './emitters/django.js';
|
|
2
|
+
import { emitDoctrine, isValidPhpNamespace } from './emitters/doctrine.js';
|
|
3
|
+
import { emitGraphene } from './emitters/graphene.js';
|
|
4
|
+
import { emitLaravel } from './emitters/laravel.js';
|
|
5
|
+
import { emitPrisma, } from './emitters/prisma.js';
|
|
6
|
+
import { emitTypeorm } from './emitters/typeorm.js';
|
|
7
|
+
import { emitTypescriptInterfaces } from './emitters/typescriptInterfaces.js';
|
|
8
|
+
import { parseDoctrine } from './parsers/doctrine.js';
|
|
9
|
+
import { parseDjango } from './parsers/django.js';
|
|
10
|
+
import { parseLaravel } from './parsers/laravel.js';
|
|
11
|
+
import { parsePrisma } from './parsers/prisma.js';
|
|
12
|
+
import { parseTypeorm } from './parsers/typeorm.js';
|
|
13
|
+
import { err, ok } from './result.js';
|
|
14
|
+
import { expandManyToMany, normalizeSchema, } from './transforms.js';
|
|
15
|
+
export const DEFAULT_APP_LABEL = 'app';
|
|
16
|
+
const adapters = new Map();
|
|
17
|
+
/** Adds an adapter to the registry. Fails when the name or an extension is already taken. */
|
|
18
|
+
export function registerFormat(adapter) {
|
|
19
|
+
if (adapter.name.trim() === '') {
|
|
20
|
+
return err('INVALID_OPTION', 'A format adapter must have a non-empty name.');
|
|
21
|
+
}
|
|
22
|
+
if (adapters.has(adapter.name)) {
|
|
23
|
+
return err('INVALID_OPTION', `The format "${adapter.name}" is already registered.`);
|
|
24
|
+
}
|
|
25
|
+
for (const extension of adapter.extensions) {
|
|
26
|
+
const owner = getFormatByExtension(extension);
|
|
27
|
+
if (owner !== undefined) {
|
|
28
|
+
return err('INVALID_OPTION', `The extension "${extension}" cannot be used by the format "${adapter.name}" because it is already used by "${owner.name}".`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
adapters.set(adapter.name, adapter);
|
|
32
|
+
return ok(adapter);
|
|
33
|
+
}
|
|
34
|
+
/** Removes an adapter from the registry. Returns false when the name was not registered. */
|
|
35
|
+
export function unregisterFormat(name) {
|
|
36
|
+
return adapters.delete(name);
|
|
37
|
+
}
|
|
38
|
+
/** Looks up a format by name. Returns a descriptive error that lists the valid names. */
|
|
39
|
+
export function getFormat(name) {
|
|
40
|
+
const adapter = adapters.get(name);
|
|
41
|
+
if (adapter === undefined) {
|
|
42
|
+
return err('UNSUPPORTED_CONVERSION', `Unknown format "${name}". Expected one of: ${listFormatNames().join(', ')}.`);
|
|
43
|
+
}
|
|
44
|
+
return ok(adapter);
|
|
45
|
+
}
|
|
46
|
+
/** Returns the adapter that owns a file extension (for example ".py"), or undefined. */
|
|
47
|
+
export function getFormatByExtension(extension) {
|
|
48
|
+
for (const adapter of adapters.values()) {
|
|
49
|
+
if (adapter.extensions.includes(extension)) {
|
|
50
|
+
return adapter;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Returns every registered adapter in registration order. */
|
|
56
|
+
export function listFormats() {
|
|
57
|
+
return [...adapters.values()];
|
|
58
|
+
}
|
|
59
|
+
/** Returns every registered format name in registration order. */
|
|
60
|
+
export function listFormatNames() {
|
|
61
|
+
return [...adapters.keys()];
|
|
62
|
+
}
|
|
63
|
+
/** Builds the text printed by the `formats` command: one line per registered format. */
|
|
64
|
+
export function describeFormats(adapters) {
|
|
65
|
+
const nameWidth = Math.max(...adapters.map((adapter) => adapter.name.length));
|
|
66
|
+
const lines = adapters.map((adapter) => {
|
|
67
|
+
const abilities = [];
|
|
68
|
+
if (adapter.parse !== undefined) {
|
|
69
|
+
abilities.push('read');
|
|
70
|
+
}
|
|
71
|
+
if (adapter.emit !== undefined) {
|
|
72
|
+
abilities.push('write');
|
|
73
|
+
}
|
|
74
|
+
return (`${adapter.name.padEnd(nameWidth)} ` +
|
|
75
|
+
`extensions: ${adapter.extensions.join(', ') || '(none)'} ` +
|
|
76
|
+
`${abilities.join(' + ') || 'no read/write support'} ` +
|
|
77
|
+
adapter.description);
|
|
78
|
+
});
|
|
79
|
+
return `${lines.join('\n')}\n`;
|
|
80
|
+
}
|
|
81
|
+
const djangoAdapter = {
|
|
82
|
+
name: 'django',
|
|
83
|
+
extensions: ['.py'],
|
|
84
|
+
description: 'Django models (models.py or a models/ package)',
|
|
85
|
+
parse: (sources, options) => {
|
|
86
|
+
const djangoSources = sources.map((source) => ({
|
|
87
|
+
path: source.path,
|
|
88
|
+
text: source.text,
|
|
89
|
+
appLabel: options.appLabel ?? source.appLabel ?? DEFAULT_APP_LABEL,
|
|
90
|
+
}));
|
|
91
|
+
return parseDjango(djangoSources, { autoField: options.autoField });
|
|
92
|
+
},
|
|
93
|
+
emit: (schema) => ok(emitDjango(schema)),
|
|
94
|
+
};
|
|
95
|
+
const prismaAdapter = {
|
|
96
|
+
name: 'prisma',
|
|
97
|
+
extensions: ['.prisma'],
|
|
98
|
+
description: 'Prisma schema (schema.prisma)',
|
|
99
|
+
parse: (sources, options) => {
|
|
100
|
+
const prismaSources = sources.map((source) => ({
|
|
101
|
+
path: source.path,
|
|
102
|
+
text: source.text,
|
|
103
|
+
}));
|
|
104
|
+
return Promise.resolve(parsePrisma(prismaSources, {
|
|
105
|
+
appLabel: options.appLabel ?? DEFAULT_APP_LABEL,
|
|
106
|
+
}));
|
|
107
|
+
},
|
|
108
|
+
emit: (schema, options) => {
|
|
109
|
+
const prepared = options.naming === 'normalize'
|
|
110
|
+
? normalizeSchema(schema)
|
|
111
|
+
: expandManyToMany(schema);
|
|
112
|
+
return ok(emitPrisma(prepared, {
|
|
113
|
+
provider: options.provider,
|
|
114
|
+
header: options.header,
|
|
115
|
+
camelFields: options.naming === 'normalize',
|
|
116
|
+
...(options.prismaVersion === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { prismaVersion: options.prismaVersion }),
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const typeormAdapter = {
|
|
123
|
+
name: 'typeorm',
|
|
124
|
+
// No extension is claimed: ".ts" is too generic to infer, so pass --from/--to typeorm.
|
|
125
|
+
extensions: [],
|
|
126
|
+
description: 'TypeORM entity classes (TypeScript)',
|
|
127
|
+
parse: (sources, options) => parseTypeorm(sources.map((source) => ({
|
|
128
|
+
path: source.path,
|
|
129
|
+
text: source.text,
|
|
130
|
+
})), { appLabel: options.appLabel ?? DEFAULT_APP_LABEL }),
|
|
131
|
+
emit: (schema, options) => {
|
|
132
|
+
const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
|
|
133
|
+
return ok(emitTypeorm(prepared, {
|
|
134
|
+
provider: options.provider,
|
|
135
|
+
camelFields: options.naming === 'normalize',
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const grapheneAdapter = {
|
|
140
|
+
name: 'graphene',
|
|
141
|
+
// No extension is claimed: ".py" belongs to Django, so pass --to graphene.
|
|
142
|
+
extensions: [],
|
|
143
|
+
description: 'Graphene (graphene-django) GraphQL schema',
|
|
144
|
+
emit: (schema) => ok(emitGraphene(schema)),
|
|
145
|
+
};
|
|
146
|
+
const typescriptAdapter = {
|
|
147
|
+
name: 'typescript',
|
|
148
|
+
// Output only, and no extension is claimed: TypeORM already reads TypeScript, so pass --to typescript.
|
|
149
|
+
extensions: [],
|
|
150
|
+
description: 'Plain TypeScript interfaces and enums',
|
|
151
|
+
emit: (schema, options) => {
|
|
152
|
+
const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
|
|
153
|
+
return ok(emitTypescriptInterfaces(prepared, {
|
|
154
|
+
camelFields: options.naming === 'normalize',
|
|
155
|
+
}));
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const doctrineAdapter = {
|
|
159
|
+
name: 'doctrine',
|
|
160
|
+
// No extension is claimed: ".php" is too generic to infer, so pass --from/--to doctrine.
|
|
161
|
+
extensions: [],
|
|
162
|
+
description: 'Doctrine ORM entities (PHP 8 attributes)',
|
|
163
|
+
parse: (sources, options) => parseDoctrine(sources.map((source) => ({
|
|
164
|
+
path: source.path,
|
|
165
|
+
text: source.text,
|
|
166
|
+
})), { appLabel: options.appLabel ?? DEFAULT_APP_LABEL }),
|
|
167
|
+
emit: (schema, options) => {
|
|
168
|
+
const namespace = options.namespace?.replace(/^\\+|\\+$/g, '');
|
|
169
|
+
if (namespace !== undefined && !isValidPhpNamespace(namespace)) {
|
|
170
|
+
return err('INVALID_OPTION', `Invalid namespace "${options.namespace ?? ''}". Expected a PHP namespace such as App\\Entity.`);
|
|
171
|
+
}
|
|
172
|
+
const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
|
|
173
|
+
return ok(emitDoctrine(prepared, {
|
|
174
|
+
provider: options.provider,
|
|
175
|
+
camelFields: options.naming === 'normalize',
|
|
176
|
+
...(namespace === undefined ? {} : { namespace }),
|
|
177
|
+
}));
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const laravelAdapter = {
|
|
181
|
+
name: 'laravel',
|
|
182
|
+
// No extension is claimed: ".php" is too generic to infer, so pass --from/--to laravel.
|
|
183
|
+
extensions: [],
|
|
184
|
+
description: 'Laravel migrations and Eloquent models',
|
|
185
|
+
parse: (sources, options) => parseLaravel(sources.map((source) => ({
|
|
186
|
+
path: source.path,
|
|
187
|
+
text: source.text,
|
|
188
|
+
})), { appLabel: options.appLabel ?? DEFAULT_APP_LABEL }),
|
|
189
|
+
emit: (schema, options) => {
|
|
190
|
+
const namespace = options.namespace?.replace(/^\\+|\\+$/g, '');
|
|
191
|
+
if (namespace !== undefined && !isValidPhpNamespace(namespace)) {
|
|
192
|
+
return err('INVALID_OPTION', `Invalid namespace "${options.namespace ?? ''}". Expected a PHP namespace such as App\\Models.`);
|
|
193
|
+
}
|
|
194
|
+
return ok(emitLaravel(schema, {
|
|
195
|
+
provider: options.provider,
|
|
196
|
+
naming: options.naming,
|
|
197
|
+
...(namespace === undefined ? {} : { namespace }),
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
// The built-in names and extensions are distinct, so these registrations cannot fail.
|
|
202
|
+
const builtIns = [
|
|
203
|
+
registerFormat(djangoAdapter),
|
|
204
|
+
registerFormat(prismaAdapter),
|
|
205
|
+
registerFormat(typeormAdapter),
|
|
206
|
+
registerFormat(doctrineAdapter),
|
|
207
|
+
registerFormat(laravelAdapter),
|
|
208
|
+
registerFormat(grapheneAdapter),
|
|
209
|
+
registerFormat(typescriptAdapter),
|
|
210
|
+
];
|
|
211
|
+
export const BUILT_IN_FORMAT_NAMES = builtIns.flatMap((registered) => registered.ok ? [registered.value.name] : []);
|
|
212
|
+
//# sourceMappingURL=formats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formats.js","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export { convertText, DEFAULT_APP_LABEL, FORMAT_NAMES } from './convert.js';
|
|
2
|
+
export type { ConvertOptions, ConvertResult, FormatName, SourceText, } from './convert.js';
|
|
3
|
+
export { describeFormats, getFormat, getFormatByExtension, listFormatNames, listFormats, registerFormat, unregisterFormat, } from './formats.js';
|
|
4
|
+
export type { FormatAdapter, FormatEmitOutput, FormatOptions, MultiFileEmitOutput, } from './formats.js';
|
|
5
|
+
export { runConversion, deriveAppLabel } from './io.js';
|
|
6
|
+
export type { RunOptions, RunSummary } from './io.js';
|
|
7
|
+
export { parseDjango } from './parsers/django.js';
|
|
8
|
+
export type { DjangoSourceFile, DjangoParseOptions } from './parsers/django.js';
|
|
9
|
+
export { parsePrisma } from './parsers/prisma.js';
|
|
10
|
+
export type { PrismaSourceFile, PrismaParseOptions } from './parsers/prisma.js';
|
|
11
|
+
export { parseDoctrine } from './parsers/doctrine.js';
|
|
12
|
+
export type { DoctrineSourceFile, DoctrineParseOptions, } from './parsers/doctrine.js';
|
|
13
|
+
export { parseLaravel } from './parsers/laravel.js';
|
|
14
|
+
export type { LaravelSourceFile, LaravelParseOptions, } from './parsers/laravel.js';
|
|
15
|
+
export { parseTypeorm } from './parsers/typeorm.js';
|
|
16
|
+
export type { TypeormSourceFile, TypeormParseOptions, } from './parsers/typeorm.js';
|
|
17
|
+
export { emitPrisma, PRISMA_PROVIDERS } from './emitters/prisma.js';
|
|
18
|
+
export type { EmitOutput, PrismaEmitOptions, PrismaProvider, } from './emitters/prisma.js';
|
|
19
|
+
export { emitDjango } from './emitters/django.js';
|
|
20
|
+
export { emitDoctrine } from './emitters/doctrine.js';
|
|
21
|
+
export type { DoctrineEmitOptions } from './emitters/doctrine.js';
|
|
22
|
+
export { emitLaravel } from './emitters/laravel.js';
|
|
23
|
+
export type { LaravelEmitOptions } from './emitters/laravel.js';
|
|
24
|
+
export { emitGraphene } from './emitters/graphene.js';
|
|
25
|
+
export { emitTypeorm } from './emitters/typeorm.js';
|
|
26
|
+
export type { TypeormEmitOptions } from './emitters/typeorm.js';
|
|
27
|
+
export { emitTypescriptInterfaces } from './emitters/typescriptInterfaces.js';
|
|
28
|
+
export type { TypescriptDateMode, TypescriptInterfacesOptions, } from './emitters/typescriptInterfaces.js';
|
|
29
|
+
export { expandManyToMany, normalizeSchema } from './transforms.js';
|
|
30
|
+
export type { NamingMode } from './transforms.js';
|
|
31
|
+
export type * from './ir.js';
|
|
32
|
+
export { err, ok } from './result.js';
|
|
33
|
+
export type { ConversionError, ErrorCode, Result } from './result.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { convertText, DEFAULT_APP_LABEL, FORMAT_NAMES } from './convert.js';
|
|
2
|
+
export { describeFormats, getFormat, getFormatByExtension, listFormatNames, listFormats, registerFormat, unregisterFormat, } from './formats.js';
|
|
3
|
+
export { runConversion, deriveAppLabel } from './io.js';
|
|
4
|
+
export { parseDjango } from './parsers/django.js';
|
|
5
|
+
export { parsePrisma } from './parsers/prisma.js';
|
|
6
|
+
export { parseDoctrine } from './parsers/doctrine.js';
|
|
7
|
+
export { parseLaravel } from './parsers/laravel.js';
|
|
8
|
+
export { parseTypeorm } from './parsers/typeorm.js';
|
|
9
|
+
export { emitPrisma, PRISMA_PROVIDERS } from './emitters/prisma.js';
|
|
10
|
+
export { emitDjango } from './emitters/django.js';
|
|
11
|
+
export { emitDoctrine } from './emitters/doctrine.js';
|
|
12
|
+
export { emitLaravel } from './emitters/laravel.js';
|
|
13
|
+
export { emitGraphene } from './emitters/graphene.js';
|
|
14
|
+
export { emitTypeorm } from './emitters/typeorm.js';
|
|
15
|
+
export { emitTypescriptInterfaces } from './emitters/typescriptInterfaces.js';
|
|
16
|
+
export { expandManyToMany, normalizeSchema } from './transforms.js';
|
|
17
|
+
export { err, ok } from './result.js';
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAO5E,OAAO,EACL,eAAe,EACf,SAAS,EACT,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAOtB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAKtD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAKpD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAKpD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMpE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAK9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ConvertOptions, type ConvertResult } from './convert.js';
|
|
2
|
+
import { type Result } from './result.js';
|
|
3
|
+
export interface RunOptions extends ConvertOptions {
|
|
4
|
+
/** Files or directories to read. */
|
|
5
|
+
inputs: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Destination. A file path for single-text formats; a directory for formats
|
|
8
|
+
* that yield several files. When omitted the converted text is only returned.
|
|
9
|
+
*/
|
|
10
|
+
output?: string;
|
|
11
|
+
/** Run the whole conversion and report what would be written, without touching the filesystem. */
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
/** Compare the result with the existing output and report differences, without writing. */
|
|
14
|
+
check?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** How a planned output file relates to what is on disk. */
|
|
17
|
+
export type OutputState = 'created' | 'changed' | 'unchanged';
|
|
18
|
+
/** One file the conversion produces, with its relation to the existing file. */
|
|
19
|
+
export interface PlannedFile {
|
|
20
|
+
/** Absolute destination path. */
|
|
21
|
+
path: string;
|
|
22
|
+
text: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
lines: number;
|
|
25
|
+
state: OutputState;
|
|
26
|
+
/** Short description of the differences for a changed file, such as "+3 -1 lines (first difference at line 12)". */
|
|
27
|
+
diff?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RunSummary extends ConvertResult {
|
|
30
|
+
inputFiles: string[];
|
|
31
|
+
/** The output path as written: the file for single-text formats, the directory for multi-file formats. */
|
|
32
|
+
outputPath?: string;
|
|
33
|
+
/** Every file planned for the output path. Empty when no output path was given. */
|
|
34
|
+
plannedFiles: PlannedFile[];
|
|
35
|
+
/** True when files were written (never in dry-run or check mode). */
|
|
36
|
+
written: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Reads the inputs, converts them, and writes the result to the output path when one is given. */
|
|
39
|
+
export declare function runConversion(options: RunOptions): Promise<Result<RunSummary>>;
|
|
40
|
+
/** Counts lines the way an editor does: a trailing newline does not add a line. */
|
|
41
|
+
export declare function countLines(text: string): number;
|
|
42
|
+
/**
|
|
43
|
+
* Describes how two texts differ in a short line, for example
|
|
44
|
+
* "+3 -1 lines (first difference at line 12)".
|
|
45
|
+
*/
|
|
46
|
+
export declare function summarizeDiff(before: string, after: string): string;
|
|
47
|
+
/** Derives a Django app label from the location of a models.py file or models/ package. */
|
|
48
|
+
export declare function deriveAppLabel(filePath: string): string;
|
package/dist/io.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, resolve, } from 'node:path';
|
|
3
|
+
import { convertText, } from './convert.js';
|
|
4
|
+
import { describeThrown, err, ok } from './result.js';
|
|
5
|
+
/** Directories skipped only when reading PHP projects: Composer packages and Symfony's cache/log directory. */
|
|
6
|
+
const PHP_SKIPPED_DIRECTORIES = new Set(['vendor', 'var']);
|
|
7
|
+
/**
|
|
8
|
+
* Directories skipped when reading a Laravel project. The Django list does not
|
|
9
|
+
* apply: `migrations` is where Laravel keeps its schema, and `build` / `env` /
|
|
10
|
+
* `dist` are ordinary names there.
|
|
11
|
+
*/
|
|
12
|
+
const LARAVEL_SKIPPED_DIRECTORIES = new Set([
|
|
13
|
+
'vendor',
|
|
14
|
+
'storage',
|
|
15
|
+
'node_modules',
|
|
16
|
+
'tests',
|
|
17
|
+
'.git',
|
|
18
|
+
]);
|
|
19
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
20
|
+
'node_modules',
|
|
21
|
+
'venv',
|
|
22
|
+
'.venv',
|
|
23
|
+
'env',
|
|
24
|
+
'.git',
|
|
25
|
+
'__pycache__',
|
|
26
|
+
'migrations',
|
|
27
|
+
'site-packages',
|
|
28
|
+
'dist',
|
|
29
|
+
'build',
|
|
30
|
+
]);
|
|
31
|
+
/** Reads the inputs, converts them, and writes the result to the output path when one is given. */
|
|
32
|
+
export async function runConversion(options) {
|
|
33
|
+
const discovered = await discoverInputFiles(options.inputs, options.from);
|
|
34
|
+
if (!discovered.ok) {
|
|
35
|
+
return discovered;
|
|
36
|
+
}
|
|
37
|
+
const sources = [];
|
|
38
|
+
for (const filePath of discovered.value) {
|
|
39
|
+
const read = await readTextFile(filePath);
|
|
40
|
+
if (!read.ok) {
|
|
41
|
+
return read;
|
|
42
|
+
}
|
|
43
|
+
sources.push({
|
|
44
|
+
path: filePath,
|
|
45
|
+
text: read.value,
|
|
46
|
+
appLabel: deriveAppLabel(filePath),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const converted = await convertText(sources, options);
|
|
50
|
+
if (!converted.ok) {
|
|
51
|
+
return converted;
|
|
52
|
+
}
|
|
53
|
+
const summary = {
|
|
54
|
+
...converted.value,
|
|
55
|
+
inputFiles: discovered.value,
|
|
56
|
+
plannedFiles: [],
|
|
57
|
+
written: false,
|
|
58
|
+
};
|
|
59
|
+
if (options.output === undefined) {
|
|
60
|
+
if (converted.value.files !== undefined) {
|
|
61
|
+
return err('INVALID_OPTION', `The "${options.to}" format produces ${Object.keys(converted.value.files).length} files, so it needs an output directory. Pass -o <directory>.`);
|
|
62
|
+
}
|
|
63
|
+
return ok(summary);
|
|
64
|
+
}
|
|
65
|
+
const planned = await planOutput(resolve(options.output), converted.value, options.to);
|
|
66
|
+
if (!planned.ok) {
|
|
67
|
+
return planned;
|
|
68
|
+
}
|
|
69
|
+
const outputPath = resolve(options.output);
|
|
70
|
+
const withPlan = {
|
|
71
|
+
...summary,
|
|
72
|
+
outputPath,
|
|
73
|
+
plannedFiles: planned.value,
|
|
74
|
+
};
|
|
75
|
+
if (options.dryRun === true || options.check === true) {
|
|
76
|
+
return ok(withPlan);
|
|
77
|
+
}
|
|
78
|
+
for (const file of planned.value) {
|
|
79
|
+
if (file.state === 'unchanged') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const written = await writeTextFile(file.path, file.text);
|
|
83
|
+
if (!written.ok) {
|
|
84
|
+
return written;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return ok({ ...withPlan, written: true });
|
|
88
|
+
}
|
|
89
|
+
/** Works out the destination files and compares each with what is already on disk. */
|
|
90
|
+
async function planOutput(outputPath, converted, format) {
|
|
91
|
+
const existing = await classifyPath(outputPath);
|
|
92
|
+
const entries = [];
|
|
93
|
+
if (converted.files === undefined) {
|
|
94
|
+
if (existing === 'directory') {
|
|
95
|
+
return err('INVALID_OPTION', `The output path "${outputPath}" is a directory, but the "${format}" format produces a single file. Pass a file path to -o.`);
|
|
96
|
+
}
|
|
97
|
+
entries.push([outputPath, converted.output]);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const names = Object.keys(converted.files).sort();
|
|
101
|
+
if (existing === 'file' ||
|
|
102
|
+
(existing === 'missing' && extname(outputPath) !== '')) {
|
|
103
|
+
return err('INVALID_OPTION', `The "${format}" format produces ${names.length} files (${names.join(', ')}), so -o must be a directory, but "${outputPath}" is a file path. Pass a directory such as "${join(dirname(outputPath), basename(outputPath, extname(outputPath)))}".`);
|
|
104
|
+
}
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
const target = resolveInside(outputPath, name);
|
|
107
|
+
if (!target.ok) {
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
entries.push([target.value, converted.files[name] ?? '']);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const planned = [];
|
|
114
|
+
for (const [path, text] of entries) {
|
|
115
|
+
const current = await readIfExists(path);
|
|
116
|
+
const file = {
|
|
117
|
+
path,
|
|
118
|
+
text,
|
|
119
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
120
|
+
lines: countLines(text),
|
|
121
|
+
state: current === undefined
|
|
122
|
+
? 'created'
|
|
123
|
+
: current === text
|
|
124
|
+
? 'unchanged'
|
|
125
|
+
: 'changed',
|
|
126
|
+
};
|
|
127
|
+
if (current !== undefined && current !== text) {
|
|
128
|
+
file.diff = summarizeDiff(current, text);
|
|
129
|
+
}
|
|
130
|
+
planned.push(file);
|
|
131
|
+
}
|
|
132
|
+
return ok(planned);
|
|
133
|
+
}
|
|
134
|
+
async function classifyPath(path) {
|
|
135
|
+
try {
|
|
136
|
+
const info = await stat(path);
|
|
137
|
+
if (info.isDirectory()) {
|
|
138
|
+
return 'directory';
|
|
139
|
+
}
|
|
140
|
+
return info.isFile() ? 'file' : 'other';
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return 'missing';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function readIfExists(path) {
|
|
147
|
+
try {
|
|
148
|
+
return await readFile(path, 'utf8');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Resolves an emitter-provided relative path under the output directory, rejecting escapes. */
|
|
155
|
+
function resolveInside(directory, relativePath) {
|
|
156
|
+
if (relativePath === '' ||
|
|
157
|
+
isAbsolute(relativePath) ||
|
|
158
|
+
relativePath.split(/[\\/]/).includes('..')) {
|
|
159
|
+
return err('EMIT_FAILED', `The emitter returned the file path "${relativePath}", which is not a relative path inside the output directory.`);
|
|
160
|
+
}
|
|
161
|
+
return ok(resolve(directory, relativePath));
|
|
162
|
+
}
|
|
163
|
+
/** Counts lines the way an editor does: a trailing newline does not add a line. */
|
|
164
|
+
export function countLines(text) {
|
|
165
|
+
if (text === '') {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
const newlines = text.split('\n').length - 1;
|
|
169
|
+
return text.endsWith('\n') ? newlines : newlines + 1;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Describes how two texts differ in a short line, for example
|
|
173
|
+
* "+3 -1 lines (first difference at line 12)".
|
|
174
|
+
*/
|
|
175
|
+
export function summarizeDiff(before, after) {
|
|
176
|
+
const oldLines = before.split('\n');
|
|
177
|
+
const newLines = after.split('\n');
|
|
178
|
+
const counts = new Map();
|
|
179
|
+
for (const line of oldLines) {
|
|
180
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
181
|
+
}
|
|
182
|
+
let added = 0;
|
|
183
|
+
for (const line of newLines) {
|
|
184
|
+
const remaining = counts.get(line) ?? 0;
|
|
185
|
+
if (remaining > 0) {
|
|
186
|
+
counts.set(line, remaining - 1);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
added += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let removed = 0;
|
|
193
|
+
for (const remaining of counts.values()) {
|
|
194
|
+
removed += remaining;
|
|
195
|
+
}
|
|
196
|
+
let first = 0;
|
|
197
|
+
while (first < oldLines.length &&
|
|
198
|
+
first < newLines.length &&
|
|
199
|
+
oldLines[first] === newLines[first]) {
|
|
200
|
+
first += 1;
|
|
201
|
+
}
|
|
202
|
+
return `+${added} -${removed} lines (first difference at line ${first + 1})`;
|
|
203
|
+
}
|
|
204
|
+
async function readTextFile(filePath) {
|
|
205
|
+
try {
|
|
206
|
+
return ok(await readFile(filePath, 'utf8'));
|
|
207
|
+
}
|
|
208
|
+
catch (thrown) {
|
|
209
|
+
return err('INPUT_READ_FAILED', `Could not read "${filePath}": ${describeThrown(thrown)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function writeTextFile(outputPath, text) {
|
|
213
|
+
const absolutePath = resolve(outputPath);
|
|
214
|
+
try {
|
|
215
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
216
|
+
await writeFile(absolutePath, text, 'utf8');
|
|
217
|
+
return ok(absolutePath);
|
|
218
|
+
}
|
|
219
|
+
catch (thrown) {
|
|
220
|
+
return err('OUTPUT_WRITE_FAILED', `Could not write the output file "${absolutePath}": ${describeThrown(thrown)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Derives a Django app label from the location of a models.py file or models/ package. */
|
|
224
|
+
export function deriveAppLabel(filePath) {
|
|
225
|
+
const absolutePath = resolve(filePath);
|
|
226
|
+
const directory = dirname(absolutePath);
|
|
227
|
+
if (basename(directory) === 'models') {
|
|
228
|
+
return basename(dirname(directory));
|
|
229
|
+
}
|
|
230
|
+
return basename(directory);
|
|
231
|
+
}
|
|
232
|
+
async function discoverInputFiles(inputs, format) {
|
|
233
|
+
const files = [];
|
|
234
|
+
for (const input of inputs) {
|
|
235
|
+
let inputStat;
|
|
236
|
+
try {
|
|
237
|
+
inputStat = await stat(input);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return err('INPUT_NOT_FOUND', `The input path "${input}" does not exist or is not readable.`);
|
|
241
|
+
}
|
|
242
|
+
if (inputStat.isDirectory()) {
|
|
243
|
+
const roots = format === 'laravel' ? await laravelRoots(input) : [input];
|
|
244
|
+
for (const root of roots) {
|
|
245
|
+
files.push(...(await walkDirectory(root, format)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
files.push(input);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (files.length === 0) {
|
|
253
|
+
const expected = format === 'django'
|
|
254
|
+
? 'models.py files (or a models/ package)'
|
|
255
|
+
: format === 'typeorm'
|
|
256
|
+
? 'TypeScript (.ts) entity files'
|
|
257
|
+
: format === 'doctrine'
|
|
258
|
+
? 'PHP (.php) entity files'
|
|
259
|
+
: format === 'laravel'
|
|
260
|
+
? 'PHP (.php) migration and model files (database/migrations and app/)'
|
|
261
|
+
: '.prisma files';
|
|
262
|
+
return err('NO_INPUT_FILES', `No ${expected} were found in: ${inputs.join(', ')}.`);
|
|
263
|
+
}
|
|
264
|
+
return ok(files);
|
|
265
|
+
}
|
|
266
|
+
function isRelevantFile(filePath, format) {
|
|
267
|
+
if (format === 'prisma') {
|
|
268
|
+
return extname(filePath) === '.prisma';
|
|
269
|
+
}
|
|
270
|
+
if (format === 'typeorm') {
|
|
271
|
+
return (extname(filePath) === '.ts' &&
|
|
272
|
+
!/\.(d|test|spec)\.ts$/.test(basename(filePath)));
|
|
273
|
+
}
|
|
274
|
+
if (format === 'doctrine' || format === 'laravel') {
|
|
275
|
+
return (extname(filePath) === '.php' && !/Test\.php$/.test(basename(filePath)));
|
|
276
|
+
}
|
|
277
|
+
const parentName = basename(dirname(filePath));
|
|
278
|
+
const fileName = basename(filePath);
|
|
279
|
+
if (fileName === 'models.py') {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return (parentName === 'models' &&
|
|
283
|
+
extname(fileName) === '.py' &&
|
|
284
|
+
fileName !== '__init__.py');
|
|
285
|
+
}
|
|
286
|
+
function isSkippedDirectory(name, parent, format) {
|
|
287
|
+
if (format === 'laravel') {
|
|
288
|
+
// bootstrap/cache holds generated files; the rest of bootstrap/ is ordinary code.
|
|
289
|
+
return (LARAVEL_SKIPPED_DIRECTORIES.has(name) ||
|
|
290
|
+
(name === 'cache' && basename(parent) === 'bootstrap'));
|
|
291
|
+
}
|
|
292
|
+
return (SKIPPED_DIRECTORIES.has(name) ||
|
|
293
|
+
(format === 'doctrine' && PHP_SKIPPED_DIRECTORIES.has(name)));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* For a Laravel project root (it has `database/migrations` or `app`), only those
|
|
297
|
+
* two directories are read. Any other directory, such as `app/Models` or
|
|
298
|
+
* `database/migrations` passed on its own, is read as given.
|
|
299
|
+
*/
|
|
300
|
+
async function laravelRoots(directory) {
|
|
301
|
+
const roots = [];
|
|
302
|
+
for (const candidate of [
|
|
303
|
+
join(directory, 'database', 'migrations'),
|
|
304
|
+
join(directory, 'app'),
|
|
305
|
+
]) {
|
|
306
|
+
if ((await classifyPath(candidate)) === 'directory') {
|
|
307
|
+
roots.push(candidate);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return roots.length > 0 ? roots : [directory];
|
|
311
|
+
}
|
|
312
|
+
async function walkDirectory(directory, format) {
|
|
313
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
314
|
+
const found = [];
|
|
315
|
+
for (const entry of entries.sort((first, second) => first.name.localeCompare(second.name))) {
|
|
316
|
+
const entryPath = join(directory, entry.name);
|
|
317
|
+
if (entry.isDirectory()) {
|
|
318
|
+
if (!isSkippedDirectory(entry.name, directory, format)) {
|
|
319
|
+
found.push(...(await walkDirectory(entryPath, format)));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (isRelevantFile(entryPath, format)) {
|
|
323
|
+
found.push(entryPath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return found;
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=io.js.map
|