@supalive/codegen 1.2.1 → 1.3.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/dist/cli.js +10 -3
- package/dist/index.d.ts +61 -6
- package/dist/index.js +2 -2
- package/dist/{src-DIj2CWMc.js → src-O2U-Keyx.js} +339 -56
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as generateToDisk, r as readEntryConfig } from "./src-
|
|
2
|
+
import { n as generateToDisk, r as readEntryConfig } from "./src-O2U-Keyx.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
function parseArgs(argv) {
|
|
@@ -35,6 +35,9 @@ function parseArgs(argv) {
|
|
|
35
35
|
case "--include-internal":
|
|
36
36
|
args.includeInternal = true;
|
|
37
37
|
break;
|
|
38
|
+
case "--schema-module":
|
|
39
|
+
args.schemaModule = argv[++i];
|
|
40
|
+
break;
|
|
38
41
|
default:
|
|
39
42
|
if (a.startsWith("-")) throw new Error(`Unknown option: ${a}`);
|
|
40
43
|
if (args.entry) throw new Error(`Unexpected extra argument: ${a}`);
|
|
@@ -55,6 +58,8 @@ Options:
|
|
|
55
58
|
--config <name> Config object export name (default: dartCodegen)
|
|
56
59
|
--tsconfig <path> tsconfig.json used to resolve types
|
|
57
60
|
--include-internal Also generate internal (server-only) procedures
|
|
61
|
+
--schema-module <path> Module that registers defineSchema tables, for
|
|
62
|
+
schema-aware enum names (relative to the entry)
|
|
58
63
|
-h, --help Show this help
|
|
59
64
|
|
|
60
65
|
The entry file may export a \`dartCodegen\` object with any of the above
|
|
@@ -68,14 +73,16 @@ async function main() {
|
|
|
68
73
|
const fromEntry = readEntryConfig(args.entry, args.configExport, args.tsconfig);
|
|
69
74
|
const entryDir = path.dirname(path.resolve(args.entry));
|
|
70
75
|
const output = args.output ?? (fromEntry.output ? path.resolve(entryDir, fromEntry.output) : void 0);
|
|
71
|
-
const
|
|
76
|
+
const schemaModule = args.schemaModule ? path.resolve(args.schemaModule) : fromEntry.schemaModule ? path.resolve(entryDir, fromEntry.schemaModule) : void 0;
|
|
77
|
+
const { files, config } = await generateToDisk({
|
|
72
78
|
entry: args.entry,
|
|
73
79
|
output,
|
|
74
80
|
router: args.router ?? fromEntry.router,
|
|
75
81
|
clientClassName: args.clientClass ?? fromEntry.clientClassName,
|
|
76
82
|
includeInternal: args.includeInternal ?? fromEntry.includeInternal,
|
|
77
83
|
tsconfig: args.tsconfig ?? fromEntry.tsconfig,
|
|
78
|
-
numericOverrides: fromEntry.numericOverrides
|
|
84
|
+
numericOverrides: fromEntry.numericOverrides,
|
|
85
|
+
schemaModule
|
|
79
86
|
});
|
|
80
87
|
console.log(`Generated ${files.length} file(s) into ${config.output}:\n` + files.map((f) => ` - ${f.path}`).join("\n"));
|
|
81
88
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SchemaDefinition } from "@supalive/core/client";
|
|
2
|
+
|
|
1
3
|
//#region src/config.d.ts
|
|
2
4
|
interface SupaliveDartConfig {
|
|
3
5
|
/** Output directory for generated Dart files. */
|
|
@@ -15,11 +17,21 @@ interface SupaliveDartConfig {
|
|
|
15
17
|
* distinction (outputs). Keyed by `ModelName.fieldName`.
|
|
16
18
|
*/
|
|
17
19
|
numericOverrides?: Record<string, "int" | "double" | "bigint">;
|
|
20
|
+
/**
|
|
21
|
+
* Path to a module that, when imported, registers every `defineSchema` table
|
|
22
|
+
* (typically the schema barrel, e.g. `"../schema/index.ts"`). Resolved
|
|
23
|
+
* relative to this entry file's directory. When set, the generator
|
|
24
|
+
* runtime-imports it (with schema tracking on) and names string-literal enums
|
|
25
|
+
* after their originating table+column (`StaffStatus`, `PosDeviceStatus`)
|
|
26
|
+
* instead of structurally. Best-effort: import failures degrade gracefully.
|
|
27
|
+
*/
|
|
28
|
+
schemaModule?: string;
|
|
18
29
|
}
|
|
19
|
-
interface ResolvedConfig extends Required<Omit<SupaliveDartConfig, "tsconfig" | "numericOverrides">> {
|
|
30
|
+
interface ResolvedConfig extends Required<Omit<SupaliveDartConfig, "tsconfig" | "numericOverrides" | "schemaModule">> {
|
|
20
31
|
entry: string;
|
|
21
32
|
tsconfig?: string;
|
|
22
33
|
numericOverrides: Record<string, "int" | "double" | "bigint">;
|
|
34
|
+
schemaModule?: string;
|
|
23
35
|
}
|
|
24
36
|
//#endregion
|
|
25
37
|
//#region src/ir.d.ts
|
|
@@ -107,9 +119,48 @@ interface EmittedFile {
|
|
|
107
119
|
}
|
|
108
120
|
declare function emit(ir: ClientIR): EmittedFile[];
|
|
109
121
|
//#endregion
|
|
122
|
+
//#region src/schema-enums.d.ts
|
|
123
|
+
/** One enum-typed column discovered in a tracked schema. */
|
|
124
|
+
interface SchemaEnumEntry {
|
|
125
|
+
/** SQL table name, e.g. "pos_devices". */
|
|
126
|
+
table: string;
|
|
127
|
+
/** PascalCase singular table stem, e.g. "PosDevice". */
|
|
128
|
+
tableStem: string;
|
|
129
|
+
/** camelCase column/field key, e.g. "status". */
|
|
130
|
+
field: string;
|
|
131
|
+
/** Canonical Dart enum name, e.g. "PosDeviceStatus". */
|
|
132
|
+
enumName: string;
|
|
133
|
+
/** Values in schema (canonical) order. */
|
|
134
|
+
values: string[];
|
|
135
|
+
/** Order-independent value-set signature, for matching. */
|
|
136
|
+
setSig: string;
|
|
137
|
+
}
|
|
138
|
+
declare class SchemaEnumRegistry {
|
|
139
|
+
private readonly byField;
|
|
140
|
+
private constructor();
|
|
141
|
+
static empty(): SchemaEnumRegistry;
|
|
142
|
+
get size(): number;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a string-literal union to a schema enum, or `undefined` when there
|
|
145
|
+
* is no confident match. Requires the field name and the exact value set to
|
|
146
|
+
* agree; when several tables share both (e.g. `status: active|disabled` on
|
|
147
|
+
* staff/tenants/stores/pos_devices) the result model's `stem` disambiguates.
|
|
148
|
+
*/
|
|
149
|
+
match(field: string, values: string[], stem: string): SchemaEnumEntry | undefined;
|
|
150
|
+
static fromRegistry(registry: Map<string, SchemaDefinition<string, any, any>>): SchemaEnumRegistry;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Runtime-import the configured `schemaModule` with schema tracking enabled and
|
|
154
|
+
* build a {@link SchemaEnumRegistry}. Best-effort: any failure (no module
|
|
155
|
+
* configured, no `@supalive/core`, no TypeScript loader for a `.ts` module)
|
|
156
|
+
* downgrades to an empty registry so generation still succeeds with structural
|
|
157
|
+
* names.
|
|
158
|
+
*/
|
|
159
|
+
declare function loadSchemaEnums(config: ResolvedConfig): Promise<SchemaEnumRegistry>;
|
|
160
|
+
//#endregion
|
|
110
161
|
//#region src/extract.d.ts
|
|
111
162
|
/** Load the router from the entry file and extract a ClientIR. */
|
|
112
|
-
declare function extractClient(config: ResolvedConfig): ClientIR;
|
|
163
|
+
declare function extractClient(config: ResolvedConfig, schemaEnums?: SchemaEnumRegistry): ClientIR;
|
|
113
164
|
//#endregion
|
|
114
165
|
//#region src/index.d.ts
|
|
115
166
|
interface GenerateOptions extends SupaliveDartConfig {
|
|
@@ -122,10 +173,14 @@ interface GenerateResult {
|
|
|
122
173
|
}
|
|
123
174
|
/** Resolve a partial config against defaults, anchoring relative paths. */
|
|
124
175
|
declare function resolveConfig(opts: GenerateOptions): ResolvedConfig;
|
|
125
|
-
/**
|
|
126
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Extract + emit. Does not touch the filesystem, and stays synchronous. Pass a
|
|
178
|
+
* pre-loaded {@link SchemaEnumRegistry} to get schema-aware enum names; omit it
|
|
179
|
+
* (the default) for purely structural naming.
|
|
180
|
+
*/
|
|
181
|
+
declare function generate(opts: GenerateOptions, schemaEnums?: SchemaEnumRegistry): GenerateResult;
|
|
127
182
|
/** Extract + emit + write the files to `config.output`. */
|
|
128
|
-
declare function generateToDisk(opts: GenerateOptions): GenerateResult
|
|
183
|
+
declare function generateToDisk(opts: GenerateOptions): Promise<GenerateResult>;
|
|
129
184
|
/**
|
|
130
185
|
* Best-effort static read of a config object exported from the entry file,
|
|
131
186
|
* so users can keep `router` + codegen options next to the router itself:
|
|
@@ -137,4 +192,4 @@ declare function generateToDisk(opts: GenerateOptions): GenerateResult;
|
|
|
137
192
|
*/
|
|
138
193
|
declare function readEntryConfig(entry: string, exportName?: string, tsconfig?: string): SupaliveDartConfig;
|
|
139
194
|
//#endregion
|
|
140
|
-
export { ClientIR, DartType, type EmittedFile, EnumIR, EnumValueIR, FieldIR, GenerateOptions, GenerateResult, ModelIR, ProcedureIR, ProcedureKind, type ResolvedConfig, type SupaliveDartConfig, emit, extractClient, generate, generateToDisk, readEntryConfig, resolveConfig };
|
|
195
|
+
export { ClientIR, DartType, type EmittedFile, EnumIR, EnumValueIR, FieldIR, GenerateOptions, GenerateResult, ModelIR, ProcedureIR, ProcedureKind, type ResolvedConfig, SchemaEnumRegistry, type SupaliveDartConfig, emit, extractClient, generate, generateToDisk, loadSchemaEnums, readEntryConfig, resolveConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as emit, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, t as generate } from "./src-
|
|
2
|
-
export { emit, extractClient, generate, generateToDisk, readEntryConfig, resolveConfig };
|
|
1
|
+
import { a as emit, c as loadSchemaEnums, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, s as SchemaEnumRegistry, t as generate } from "./src-O2U-Keyx.js";
|
|
2
|
+
export { SchemaEnumRegistry, emit, extractClient, generate, generateToDisk, loadSchemaEnums, readEntryConfig, resolveConfig };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { Node, Project, ts } from "ts-morph";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { schemaRegistry, trackSchema } from "@supalive/core/procedure";
|
|
4
7
|
//#region src/zod-schema.ts
|
|
5
8
|
/**
|
|
6
9
|
* Map each procedure to the doc comment on its declaration, so the generated
|
|
@@ -185,9 +188,177 @@ function findZodObjectLiteral(node) {
|
|
|
185
188
|
return cur && Node.isObjectLiteralExpression(cur) ? cur : void 0;
|
|
186
189
|
}
|
|
187
190
|
//#endregion
|
|
191
|
+
//#region src/names.ts
|
|
192
|
+
/** PascalCase an arbitrary string (splitting on any non-alphanumeric run). */
|
|
193
|
+
function pascal(s) {
|
|
194
|
+
return s.replace(/[^A-Za-z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
195
|
+
}
|
|
196
|
+
/** camelCase an arbitrary string. */
|
|
197
|
+
function camel(s) {
|
|
198
|
+
const p = pascal(s);
|
|
199
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Best-effort English singularization of a single trailing noun, used to turn a
|
|
203
|
+
* (usually plural) table name into a clean type stem: `pos_devices → PosDevice`,
|
|
204
|
+
* `settlement_entries → SettlementEntry`, `card_batches → CardBatch`. Leaves
|
|
205
|
+
* words that don't look plural untouched (`staff`, `status`).
|
|
206
|
+
*/
|
|
207
|
+
function singularizeWord(w) {
|
|
208
|
+
if (w.length > 4 && /(?:ch|sh|ss|x|s)es$/i.test(w)) return w.slice(0, -2);
|
|
209
|
+
if (w.length > 3 && /ies$/i.test(w)) return w.slice(0, -3) + "y";
|
|
210
|
+
if (/ss$/i.test(w)) return w;
|
|
211
|
+
if (w.length > 1 && /s$/i.test(w)) return w.slice(0, -1);
|
|
212
|
+
return w;
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/schema-enums.ts
|
|
216
|
+
function setSignature(values) {
|
|
217
|
+
return [...values].sort().join("");
|
|
218
|
+
}
|
|
219
|
+
var SchemaEnumRegistry = class SchemaEnumRegistry {
|
|
220
|
+
byField = /* @__PURE__ */ new Map();
|
|
221
|
+
constructor(entries) {
|
|
222
|
+
for (const e of entries) {
|
|
223
|
+
const arr = this.byField.get(e.field);
|
|
224
|
+
if (arr) arr.push(e);
|
|
225
|
+
else this.byField.set(e.field, [e]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
static empty() {
|
|
229
|
+
return new SchemaEnumRegistry([]);
|
|
230
|
+
}
|
|
231
|
+
get size() {
|
|
232
|
+
let n = 0;
|
|
233
|
+
for (const arr of this.byField.values()) n += arr.length;
|
|
234
|
+
return n;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolve a string-literal union to a schema enum, or `undefined` when there
|
|
238
|
+
* is no confident match. Requires the field name and the exact value set to
|
|
239
|
+
* agree; when several tables share both (e.g. `status: active|disabled` on
|
|
240
|
+
* staff/tenants/stores/pos_devices) the result model's `stem` disambiguates.
|
|
241
|
+
*/
|
|
242
|
+
match(field, values, stem) {
|
|
243
|
+
const candidates = this.byField.get(camel(field));
|
|
244
|
+
if (!candidates) return void 0;
|
|
245
|
+
const sig = setSignature(values);
|
|
246
|
+
const matches = candidates.filter((c) => c.setSig === sig);
|
|
247
|
+
if (matches.length === 0) return void 0;
|
|
248
|
+
if (matches.length === 1) return matches[0];
|
|
249
|
+
const pick = (test) => {
|
|
250
|
+
let best;
|
|
251
|
+
for (const m of matches) if (test(m.tableStem) && (!best || m.tableStem.length > best.tableStem.length)) best = m;
|
|
252
|
+
return best;
|
|
253
|
+
};
|
|
254
|
+
return pick((t) => stem.endsWith(t)) ?? pick((t) => stem.includes(t));
|
|
255
|
+
}
|
|
256
|
+
static fromRegistry(registry) {
|
|
257
|
+
const entries = [];
|
|
258
|
+
for (const def of registry.values()) {
|
|
259
|
+
const table = def.table;
|
|
260
|
+
const shape = def.schema.shape;
|
|
261
|
+
if (typeof table !== "string" || !shape || typeof shape !== "object") continue;
|
|
262
|
+
const tableStem = tableStemName(table);
|
|
263
|
+
for (const [fieldKey, zodType] of Object.entries(shape)) {
|
|
264
|
+
const values = readEnumValues(zodType);
|
|
265
|
+
if (!values || values.length === 0) continue;
|
|
266
|
+
entries.push({
|
|
267
|
+
table,
|
|
268
|
+
tableStem,
|
|
269
|
+
field: camel(fieldKey),
|
|
270
|
+
enumName: tableStem + pascal(fieldKey),
|
|
271
|
+
values,
|
|
272
|
+
setSig: setSignature(values)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return new SchemaEnumRegistry(entries);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
/** "pos_devices" → "PosDevice"; "settlement_entries" → "SettlementEntry". */
|
|
280
|
+
function tableStemName(table) {
|
|
281
|
+
const parts = table.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
282
|
+
if (parts.length === 0) return pascal(table);
|
|
283
|
+
const last = parts.length - 1;
|
|
284
|
+
return parts.map((p, i) => i === last ? pascal(singularizeWord(p)) : pascal(p)).join("");
|
|
285
|
+
}
|
|
286
|
+
/** Read a Zod enum's values, unwrapping optional/nullable/default wrappers.
|
|
287
|
+
* Realm-independent: never uses `instanceof` (the schema's Zod may be a
|
|
288
|
+
* different module instance than any the codegen could import). */
|
|
289
|
+
function readEnumValues(field) {
|
|
290
|
+
const e = unwrapToEnum(field);
|
|
291
|
+
if (!e) return void 0;
|
|
292
|
+
try {
|
|
293
|
+
const opts = e.options;
|
|
294
|
+
if (Array.isArray(opts)) return opts.map((v) => String(v));
|
|
295
|
+
} catch {}
|
|
296
|
+
const def = zodDef(e);
|
|
297
|
+
const entries = def?.entries ?? def?.values;
|
|
298
|
+
if (entries && typeof entries === "object") return Object.values(entries).map((v) => String(v));
|
|
299
|
+
}
|
|
300
|
+
function zodDef(node) {
|
|
301
|
+
const n = node;
|
|
302
|
+
return n?._zod?.def ?? n?.def;
|
|
303
|
+
}
|
|
304
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
305
|
+
"optional",
|
|
306
|
+
"nullable",
|
|
307
|
+
"default",
|
|
308
|
+
"catch",
|
|
309
|
+
"readonly",
|
|
310
|
+
"nonoptional",
|
|
311
|
+
"prefault"
|
|
312
|
+
]);
|
|
313
|
+
function unwrapToEnum(field) {
|
|
314
|
+
let cur = field;
|
|
315
|
+
for (let guard = 0; cur && guard < 12; guard++) {
|
|
316
|
+
const def = zodDef(cur);
|
|
317
|
+
if (!def) return void 0;
|
|
318
|
+
if (def.type === "enum") return cur;
|
|
319
|
+
if (def.type && WRAPPERS.has(def.type) && def.innerType) {
|
|
320
|
+
cur = def.innerType;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Runtime-import the configured `schemaModule` with schema tracking enabled and
|
|
328
|
+
* build a {@link SchemaEnumRegistry}. Best-effort: any failure (no module
|
|
329
|
+
* configured, no `@supalive/core`, no TypeScript loader for a `.ts` module)
|
|
330
|
+
* downgrades to an empty registry so generation still succeeds with structural
|
|
331
|
+
* names.
|
|
332
|
+
*/
|
|
333
|
+
async function loadSchemaEnums(config) {
|
|
334
|
+
if (!config.schemaModule) return SchemaEnumRegistry.empty();
|
|
335
|
+
try {
|
|
336
|
+
const require_ = createRequire(config.entry);
|
|
337
|
+
pathToFileURL(require_.resolve("@supalive/core/schema-sql")).href;
|
|
338
|
+
let unregister;
|
|
339
|
+
try {
|
|
340
|
+
unregister = (await import(pathToFileURL(require_.resolve("tsx/esm/api")).href)).register();
|
|
341
|
+
} catch {}
|
|
342
|
+
try {
|
|
343
|
+
trackSchema();
|
|
344
|
+
await import(pathToFileURL(config.schemaModule).href);
|
|
345
|
+
const registry = schemaRegistry;
|
|
346
|
+
if (!registry || registry.size === 0) return SchemaEnumRegistry.empty();
|
|
347
|
+
return SchemaEnumRegistry.fromRegistry(registry);
|
|
348
|
+
} finally {
|
|
349
|
+
if (unregister) try {
|
|
350
|
+
await unregister();
|
|
351
|
+
} catch {}
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.warn(`[supalive-codegen] Could not load schemaModule (${config.schemaModule}) for schema-aware enum names; falling back to structural names. ${err instanceof Error ? err.message : String(err)}`);
|
|
355
|
+
return SchemaEnumRegistry.empty();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
188
359
|
//#region src/extract.ts
|
|
189
360
|
/** Load the router from the entry file and extract a ClientIR. */
|
|
190
|
-
function extractClient(config) {
|
|
361
|
+
function extractClient(config, schemaEnums = SchemaEnumRegistry.empty()) {
|
|
191
362
|
const project = new Project({
|
|
192
363
|
tsConfigFilePath: config.tsconfig,
|
|
193
364
|
skipAddingFilesFromTsConfig: config.tsconfig ? false : true,
|
|
@@ -207,7 +378,7 @@ function extractClient(config) {
|
|
|
207
378
|
const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
|
|
208
379
|
const argMeta = scanZodArgMeta(routerDecl);
|
|
209
380
|
const procDocs = scanProcedureDocs(routerDecl);
|
|
210
|
-
const extractor = new Extractor(routerDecl, config, argMeta, project.getTypeChecker());
|
|
381
|
+
const extractor = new Extractor(routerDecl, config, argMeta, project.getTypeChecker(), schemaEnums);
|
|
211
382
|
const procedures = [];
|
|
212
383
|
for (const procSym of proceduresType.getProperties()) {
|
|
213
384
|
const name = procSym.getName();
|
|
@@ -232,17 +403,20 @@ var Extractor = class {
|
|
|
232
403
|
config;
|
|
233
404
|
argMeta;
|
|
234
405
|
tc;
|
|
406
|
+
schemaEnums;
|
|
235
407
|
modelRegistry = /* @__PURE__ */ new Map();
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
408
|
+
/** Discovered enums, keyed by a dedup key (schema name, or stem∙field∙set). */
|
|
409
|
+
enumGroups = /* @__PURE__ */ new Map();
|
|
410
|
+
finalEnums = [];
|
|
411
|
+
enumsFinalized = false;
|
|
239
412
|
nameCounts = /* @__PURE__ */ new Map();
|
|
240
413
|
visiting = /* @__PURE__ */ new Set();
|
|
241
|
-
constructor(locationNode, config, argMeta, tc) {
|
|
414
|
+
constructor(locationNode, config, argMeta, tc, schemaEnums) {
|
|
242
415
|
this.locationNode = locationNode;
|
|
243
416
|
this.config = config;
|
|
244
417
|
this.argMeta = argMeta;
|
|
245
418
|
this.tc = tc;
|
|
419
|
+
this.schemaEnums = schemaEnums;
|
|
246
420
|
}
|
|
247
421
|
/** Doc comment attached to a symbol's declaration (resolves TS aliases). */
|
|
248
422
|
symbolDoc(sym) {
|
|
@@ -255,10 +429,12 @@ var Extractor = class {
|
|
|
255
429
|
}
|
|
256
430
|
}
|
|
257
431
|
models() {
|
|
432
|
+
this.finalizeEnums();
|
|
258
433
|
return [...this.modelRegistry.values()];
|
|
259
434
|
}
|
|
260
435
|
enums() {
|
|
261
|
-
|
|
436
|
+
this.finalizeEnums();
|
|
437
|
+
return [...this.finalEnums].sort((a, b) => a.name.localeCompare(b.name));
|
|
262
438
|
}
|
|
263
439
|
procedure(name, procType) {
|
|
264
440
|
const kind = this.literalString(procType, "procedureType");
|
|
@@ -298,7 +474,7 @@ var Extractor = class {
|
|
|
298
474
|
walk(type, hint) {
|
|
299
475
|
const { core } = splitNullish(type);
|
|
300
476
|
if (core.length === 0) return { kind: "dynamic" };
|
|
301
|
-
if (core.length > 1) return this.walkUnion(core, hint);
|
|
477
|
+
if (core.length > 1) return this.walkUnion(core, { hint });
|
|
302
478
|
return this.walkSingle(core[0], hint);
|
|
303
479
|
}
|
|
304
480
|
walkSingle(type, hint) {
|
|
@@ -326,7 +502,7 @@ var Extractor = class {
|
|
|
326
502
|
if (type.isUnion()) {
|
|
327
503
|
const { core } = splitNullish(type);
|
|
328
504
|
if (core.length === 1) return this.walkSingle(core[0], hint);
|
|
329
|
-
return this.walkUnion(core, hint);
|
|
505
|
+
return this.walkUnion(core, { hint });
|
|
330
506
|
}
|
|
331
507
|
if (type.isObject()) {
|
|
332
508
|
const props = type.getProperties();
|
|
@@ -342,10 +518,10 @@ var Extractor = class {
|
|
|
342
518
|
}
|
|
343
519
|
return { kind: "dynamic" };
|
|
344
520
|
}
|
|
345
|
-
walkUnion(members,
|
|
521
|
+
walkUnion(members, ctx) {
|
|
346
522
|
if (members.every((m) => m.getFlags() & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Boolean))) return { kind: "bool" };
|
|
347
|
-
if (members.every((m) => m.getFlags() & ts.TypeFlags.StringLiteral)) return this.registerEnum(
|
|
348
|
-
if (members.every((m) => m.isObject())) return this.registerSealed(hint, members);
|
|
523
|
+
if (members.every((m) => m.getFlags() & ts.TypeFlags.StringLiteral)) return this.registerEnum(members.map((m) => String(m.getLiteralValue())), ctx);
|
|
524
|
+
if (members.every((m) => m.isObject())) return this.registerSealed(ctx.hint, members);
|
|
349
525
|
return { kind: "dynamic" };
|
|
350
526
|
}
|
|
351
527
|
walkObject(type, hint) {
|
|
@@ -371,7 +547,11 @@ var Extractor = class {
|
|
|
371
547
|
let dartType;
|
|
372
548
|
if (core.length === 0) dartType = { kind: "dynamic" };
|
|
373
549
|
else if (core.length === 1) dartType = this.walkSingle(core[0], name + pascal(jsonKey));
|
|
374
|
-
else dartType = this.walkUnion(core,
|
|
550
|
+
else dartType = this.walkUnion(core, {
|
|
551
|
+
hint: name + pascal(jsonKey),
|
|
552
|
+
stem: cleanStem(name),
|
|
553
|
+
field: jsonKey
|
|
554
|
+
});
|
|
375
555
|
const override = this.config.numericOverrides[`${name}.${jsonKey}`];
|
|
376
556
|
if (override && (dartType.kind === "double" || dartType.kind === "int")) dartType = { kind: override };
|
|
377
557
|
fields.push({
|
|
@@ -416,26 +596,106 @@ var Extractor = class {
|
|
|
416
596
|
name
|
|
417
597
|
};
|
|
418
598
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Register a string-literal union as an enum. Two tiers:
|
|
601
|
+
* 1. Schema-backed — the field name + value set match a tracked `defineSchema`
|
|
602
|
+
* column, so we use its canonical `<Table><Column>` name and reuse it
|
|
603
|
+
* everywhere that column appears. Values follow the schema's order.
|
|
604
|
+
* 2. Structural fallback — no schema match; named later in `finalizeEnums`
|
|
605
|
+
* from the model stem + field (bare when the field name is globally
|
|
606
|
+
* unique). Deduped per `(stem, field, value-set)`.
|
|
607
|
+
* Returns a shared DartType instance whose `name` is filled in at finalize.
|
|
608
|
+
*/
|
|
609
|
+
registerEnum(values, ctx) {
|
|
610
|
+
if (ctx.field) {
|
|
611
|
+
const hit = this.schemaEnums.match(ctx.field, values, ctx.stem ?? "");
|
|
612
|
+
if (hit) {
|
|
613
|
+
const key = `schema${hit.enumName}`;
|
|
614
|
+
const existing = this.enumGroups.get(key);
|
|
615
|
+
if (existing) return existing.type;
|
|
616
|
+
const type = {
|
|
617
|
+
kind: "enum",
|
|
618
|
+
name: hit.enumName
|
|
619
|
+
};
|
|
620
|
+
this.enumGroups.set(key, {
|
|
621
|
+
type,
|
|
622
|
+
values: hit.values,
|
|
623
|
+
schemaName: hit.enumName,
|
|
624
|
+
stem: ctx.stem,
|
|
625
|
+
field: ctx.field,
|
|
626
|
+
hint: ctx.hint
|
|
627
|
+
});
|
|
628
|
+
return type;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const key = `fallback${ctx.stem ?? ""}${ctx.field ?? ctx.hint}${values.join(" ")}`;
|
|
632
|
+
const existing = this.enumGroups.get(key);
|
|
633
|
+
if (existing) return existing.type;
|
|
634
|
+
const type = {
|
|
423
635
|
kind: "enum",
|
|
424
|
-
name:
|
|
636
|
+
name: key
|
|
425
637
|
};
|
|
426
|
-
|
|
427
|
-
|
|
638
|
+
this.enumGroups.set(key, {
|
|
639
|
+
type,
|
|
640
|
+
values,
|
|
641
|
+
stem: ctx.stem,
|
|
642
|
+
field: ctx.field,
|
|
643
|
+
hint: ctx.hint
|
|
644
|
+
});
|
|
645
|
+
return type;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Assign every enum its final Dart name and materialize the `EnumIR` list.
|
|
649
|
+
* Idempotent — mutates the shared DartType instances in place, so every
|
|
650
|
+
* referencing model field / procedure result sees the resolved name.
|
|
651
|
+
*/
|
|
652
|
+
finalizeEnums() {
|
|
653
|
+
if (this.enumsFinalized) return;
|
|
654
|
+
this.enumsFinalized = true;
|
|
655
|
+
const used = new Set(this.modelRegistry.keys());
|
|
656
|
+
const groups = [...this.enumGroups.values()];
|
|
657
|
+
const schemaGroups = groups.filter((g) => g.schemaName).sort((a, b) => a.schemaName.localeCompare(b.schemaName));
|
|
658
|
+
for (const g of schemaGroups) {
|
|
659
|
+
const name = this.ensureUnique(g.schemaName, used);
|
|
660
|
+
g.type.name = name;
|
|
661
|
+
this.pushEnumIR(name, g.values);
|
|
662
|
+
}
|
|
663
|
+
const fallbackGroups = groups.filter((g) => !g.schemaName).sort((a, b) => fallbackBase(a).localeCompare(fallbackBase(b)));
|
|
664
|
+
const baseCount = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const g of fallbackGroups) {
|
|
666
|
+
const base = fallbackBase(g);
|
|
667
|
+
baseCount.set(base, (baseCount.get(base) ?? 0) + 1);
|
|
668
|
+
}
|
|
669
|
+
for (const g of fallbackGroups) {
|
|
670
|
+
const base = fallbackBase(g);
|
|
671
|
+
let name;
|
|
672
|
+
if (baseCount.get(base) === 1 && !used.has(base)) name = base;
|
|
673
|
+
else if (g.stem && g.field) name = pascal(g.stem) + pascal(g.field);
|
|
674
|
+
else name = pascal(g.hint);
|
|
675
|
+
name = this.ensureUnique(name, used);
|
|
676
|
+
g.type.name = name;
|
|
677
|
+
this.pushEnumIR(name, g.values);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
ensureUnique(name, used) {
|
|
681
|
+
if (!used.has(name)) {
|
|
682
|
+
used.add(name);
|
|
683
|
+
return name;
|
|
684
|
+
}
|
|
685
|
+
let i = 2;
|
|
686
|
+
while (used.has(`${name}${i}`)) i++;
|
|
687
|
+
const unique = `${name}${i}`;
|
|
688
|
+
used.add(unique);
|
|
689
|
+
return unique;
|
|
690
|
+
}
|
|
691
|
+
pushEnumIR(name, values) {
|
|
692
|
+
this.finalEnums.push({
|
|
428
693
|
name,
|
|
429
694
|
values: values.map((wire) => ({
|
|
430
695
|
dartName: enumValueName(wire),
|
|
431
696
|
wire
|
|
432
697
|
}))
|
|
433
698
|
});
|
|
434
|
-
this.enumBySignature.set(signature, name);
|
|
435
|
-
return {
|
|
436
|
-
kind: "enum",
|
|
437
|
-
name
|
|
438
|
-
};
|
|
439
699
|
}
|
|
440
700
|
registerSealed(hint, members) {
|
|
441
701
|
const name = this.uniqueHint(hint);
|
|
@@ -454,7 +714,11 @@ var Extractor = class {
|
|
|
454
714
|
const propType = prop.getTypeAtLocation(this.locationNode);
|
|
455
715
|
const declaredOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
456
716
|
const { core, hasNull, hasUndefined } = splitNullish(propType);
|
|
457
|
-
const dt = core.length === 0 ? { kind: "dynamic" } : core.length === 1 ? this.walkSingle(core[0], name + pascal(jsonKey)) : this.walkUnion(core,
|
|
717
|
+
const dt = core.length === 0 ? { kind: "dynamic" } : core.length === 1 ? this.walkSingle(core[0], name + pascal(jsonKey)) : this.walkUnion(core, {
|
|
718
|
+
hint: name + pascal(jsonKey),
|
|
719
|
+
stem: cleanStem(name),
|
|
720
|
+
field: jsonKey
|
|
721
|
+
});
|
|
458
722
|
const acc = fieldMap.get(jsonKey) ?? {
|
|
459
723
|
types: [],
|
|
460
724
|
present: 0,
|
|
@@ -543,19 +807,34 @@ function splitNullish(type) {
|
|
|
543
807
|
function uniqueName(symName, registry, _type) {
|
|
544
808
|
return pascal(symName);
|
|
545
809
|
}
|
|
546
|
-
function pascal(s) {
|
|
547
|
-
return s.replace(/[^A-Za-z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
548
|
-
}
|
|
549
|
-
function camel(s) {
|
|
550
|
-
const p = pascal(s);
|
|
551
|
-
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
552
|
-
}
|
|
553
810
|
function singular(hint) {
|
|
554
811
|
if (hint.endsWith("ies")) return hint.slice(0, -3) + "y";
|
|
555
812
|
if (hint.endsWith("s") && !hint.endsWith("ss")) return hint.slice(0, -1);
|
|
556
813
|
return hint + "Item";
|
|
557
814
|
}
|
|
558
815
|
/**
|
|
816
|
+
* A clean type stem for a generated model name, used both to name structural
|
|
817
|
+
* enums and to disambiguate schema matches: strip the trailing generated
|
|
818
|
+
* suffix (`ResultItem`/`Result`/`Args`) and de-pluralize, so
|
|
819
|
+
* `PosDevicesResultItem → PosDevice`, `CurrentCashSessionResult → CurrentCashSession`.
|
|
820
|
+
*/
|
|
821
|
+
function cleanStem(modelName) {
|
|
822
|
+
let s = modelName;
|
|
823
|
+
for (const suffix of [
|
|
824
|
+
"ResultItem",
|
|
825
|
+
"Result",
|
|
826
|
+
"Args"
|
|
827
|
+
]) if (s.length > suffix.length && s.endsWith(suffix)) {
|
|
828
|
+
s = s.slice(0, -suffix.length);
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
return singularizeWord(s);
|
|
832
|
+
}
|
|
833
|
+
/** The bare candidate name for a structural (non-schema) enum group. */
|
|
834
|
+
function fallbackBase(g) {
|
|
835
|
+
return pascal(g.field ?? g.hint);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
559
838
|
* Dart reserved words — none may be used as a bare identifier (e.g. an enum
|
|
560
839
|
* constant), so a wire value like `void` must be escaped.
|
|
561
840
|
*/
|
|
@@ -700,7 +979,6 @@ function emitModels(ir) {
|
|
|
700
979
|
const out = [HEADER];
|
|
701
980
|
if (modelsUseBytes(ir)) out.push("import 'dart:typed_data';\n");
|
|
702
981
|
out.push("import 'package:supalive_client/supalive_client.dart';\n");
|
|
703
|
-
out.push("const Object _undefined = Object();\n");
|
|
704
982
|
for (const e of ir.enums) out.push(emitEnum(e));
|
|
705
983
|
for (const m of ir.models) out.push(emitModel(m));
|
|
706
984
|
return out.join("\n");
|
|
@@ -736,7 +1014,7 @@ ${values};
|
|
|
736
1014
|
}
|
|
737
1015
|
/** Dart declared type for a field, applying nullability. */
|
|
738
1016
|
function fieldType(f) {
|
|
739
|
-
if (f.optional && f.nullable) return `
|
|
1017
|
+
if (f.optional && f.nullable) return `Option<${typeName(f.type)}>`;
|
|
740
1018
|
const base = typeName(f.type);
|
|
741
1019
|
const nullable = f.optional || f.nullable;
|
|
742
1020
|
if (base === "Object?") return "Object?";
|
|
@@ -744,17 +1022,17 @@ function fieldType(f) {
|
|
|
744
1022
|
}
|
|
745
1023
|
function emitModel(m) {
|
|
746
1024
|
const ctorParams = m.fields.map((f) => {
|
|
747
|
-
if (f.optional && f.nullable) return ` this.${f.name} = const
|
|
1025
|
+
if (f.optional && f.nullable) return ` this.${f.name} = const .undefined()`;
|
|
748
1026
|
return !(f.optional || f.nullable) ? ` required this.${f.name}` : ` this.${f.name}`;
|
|
749
1027
|
}).join(",\n");
|
|
750
1028
|
const fields = m.fields.map((f) => `${docComment(f.doc, " ")} final ${fieldType(f)} ${f.name};`).join("\n");
|
|
751
1029
|
const fromJson = m.fields.map((f) => emitFromJsonField(f)).join("\n");
|
|
752
1030
|
const toJson = m.fields.map((f) => emitToJsonField(f)).join("\n");
|
|
753
|
-
const
|
|
1031
|
+
const isNullableType = (f) => fieldType(f).endsWith("?");
|
|
1032
|
+
const copyParams = m.fields.map((f) => isNullableType(f) ? ` Option<${typeName(f.type)}> ${f.name} = const .undefined()` : ` ${fieldType(f)}? ${f.name}`).join(",\n");
|
|
754
1033
|
const copyArgs = m.fields.map((f) => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
return ` ${f.name}: identical(${f.name}, _undefined) ? this.${f.name} : ${value}`;
|
|
1034
|
+
if (isNullableType(f)) return ` ${f.name}: ${f.name}.isUndefined ? this.${f.name} : ${f.name}.casted()`;
|
|
1035
|
+
return ` ${f.name}: ${f.name} ?? this.${f.name}`;
|
|
758
1036
|
}).join(",\n");
|
|
759
1037
|
const isDeep = (f) => {
|
|
760
1038
|
if (f.optional && f.nullable) return false;
|
|
@@ -797,23 +1075,23 @@ ${toJson}
|
|
|
797
1075
|
}
|
|
798
1076
|
function emitFromJsonField(f) {
|
|
799
1077
|
const key = `json['${f.jsonKey}']`;
|
|
800
|
-
if (f.optional && f.nullable) {
|
|
801
|
-
const inner = typeName(f.type);
|
|
802
|
-
return ` ${f.name}: json.containsKey('${f.jsonKey}')
|
|
1078
|
+
if (f.optional && f.nullable) return ` ${f.name}: json.containsKey('${f.jsonKey}')
|
|
803
1079
|
? (${key} == null
|
|
804
|
-
? const
|
|
805
|
-
:
|
|
806
|
-
: const
|
|
807
|
-
}
|
|
1080
|
+
? const Option.nil()
|
|
1081
|
+
: Option.value(${decodeNonNull(f.type, key)}))
|
|
1082
|
+
: const Option.undefined(),`;
|
|
808
1083
|
if (f.optional || f.nullable) return ` ${f.name}: ${decodeNullable(f.type, key)},`;
|
|
809
1084
|
return ` ${f.name}: ${decodeNonNull(f.type, key)},`;
|
|
810
1085
|
}
|
|
811
1086
|
function emitToJsonField(f) {
|
|
812
1087
|
const key = `'${f.jsonKey}'`;
|
|
813
|
-
if (f.optional && f.nullable)
|
|
814
|
-
|
|
1088
|
+
if (f.optional && f.nullable) {
|
|
1089
|
+
const inner = typeName(f.type);
|
|
1090
|
+
return ` if (!${f.name}.isUndefined) {
|
|
1091
|
+
final v = ${f.name}.casted<${inner}?>();
|
|
815
1092
|
json[${key}] = v == null ? null : ${encodeNonNull(f.type, "v")};
|
|
816
1093
|
}`;
|
|
1094
|
+
}
|
|
817
1095
|
if (f.optional) return ` if (${f.name} != null) json[${key}] = ${encodeNonNull(f.type, `${f.name}!`)};`;
|
|
818
1096
|
if (f.nullable) return ` json[${key}] = ${encodeNullable(f.type, f.name)};`;
|
|
819
1097
|
return ` json[${key}] = ${encodeNonNull(f.type, f.name)};`;
|
|
@@ -895,20 +1173,25 @@ function resolveConfig(opts) {
|
|
|
895
1173
|
includeInternal: opts.includeInternal ?? DEFAULTS.includeInternal,
|
|
896
1174
|
clientClassName: opts.clientClassName ?? DEFAULTS.clientClassName,
|
|
897
1175
|
tsconfig: opts.tsconfig ? path.resolve(opts.tsconfig) : void 0,
|
|
898
|
-
numericOverrides: opts.numericOverrides ?? {}
|
|
1176
|
+
numericOverrides: opts.numericOverrides ?? {},
|
|
1177
|
+
schemaModule: opts.schemaModule ? path.resolve(opts.schemaModule) : void 0
|
|
899
1178
|
};
|
|
900
1179
|
}
|
|
901
|
-
/**
|
|
902
|
-
|
|
1180
|
+
/**
|
|
1181
|
+
* Extract + emit. Does not touch the filesystem, and stays synchronous. Pass a
|
|
1182
|
+
* pre-loaded {@link SchemaEnumRegistry} to get schema-aware enum names; omit it
|
|
1183
|
+
* (the default) for purely structural naming.
|
|
1184
|
+
*/
|
|
1185
|
+
function generate(opts, schemaEnums) {
|
|
903
1186
|
const config = resolveConfig(opts);
|
|
904
1187
|
return {
|
|
905
|
-
files: emit(extractClient(config)),
|
|
1188
|
+
files: emit(extractClient(config, schemaEnums)),
|
|
906
1189
|
config
|
|
907
1190
|
};
|
|
908
1191
|
}
|
|
909
1192
|
/** Extract + emit + write the files to `config.output`. */
|
|
910
|
-
function generateToDisk(opts) {
|
|
911
|
-
const result = generate(opts);
|
|
1193
|
+
async function generateToDisk(opts) {
|
|
1194
|
+
const result = generate(opts, await loadSchemaEnums(resolveConfig(opts)));
|
|
912
1195
|
fs.mkdirSync(result.config.output, { recursive: true });
|
|
913
1196
|
for (const file of result.files) fs.writeFileSync(path.join(result.config.output, file.path), file.contents, "utf8");
|
|
914
1197
|
return result;
|
|
@@ -955,4 +1238,4 @@ function evalLiteral(node) {
|
|
|
955
1238
|
if (text === "false") return false;
|
|
956
1239
|
}
|
|
957
1240
|
//#endregion
|
|
958
|
-
export { emit as a, resolveConfig as i, generateToDisk as n, extractClient as o, readEntryConfig as r, generate as t };
|
|
1241
|
+
export { emit as a, loadSchemaEnums as c, resolveConfig as i, generateToDisk as n, extractClient as o, readEntryConfig as r, SchemaEnumRegistry as s, generate as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supalive/codegen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Generate type-safe client code (Dart, and more) from a Supalive TypeScript router.",
|
|
5
5
|
"author": "Rebaz Raouf",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"type-safe"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"supalive-
|
|
24
|
+
"supalive-codegen": "./dist/cli.js"
|
|
25
25
|
},
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"test": "vitest --run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"ts-morph": "^24.0.0"
|
|
48
|
+
"ts-morph": "^24.0.0",
|
|
49
|
+
"@supalive/core": "*"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"tsdown": "^0.22.3",
|