@supalive/codegen 1.1.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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @supalive/codegen
2
+
3
+ Generate a type-safe client from a [Supalive](https://github.com/rebaz94/supalive)
4
+ TypeScript router. The TypeScript router is the single source of truth: this tool
5
+ reads its procedures, inputs, and output schemas and emits a strongly-typed
6
+ client for another language.
7
+
8
+ Dart/Flutter is the first target (the `supalive-dart` binary). The package is
9
+ named generically so additional targets (e.g. Swift, Kotlin) can ship as
10
+ sibling binaries in future releases.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install --save-dev @supalive/codegen
16
+ ```
17
+
18
+ The generated Dart code depends on the `supalive_client` package — add it to
19
+ your Flutter/Dart app's `pubspec.yaml`.
20
+
21
+ ## CLI
22
+
23
+ ```sh
24
+ npx supalive-dart <entry.ts> [options]
25
+ ```
26
+
27
+ `<entry.ts>` is a TypeScript file that exports your router.
28
+
29
+ | Option | Description | Default |
30
+ | --- | --- | --- |
31
+ | `-o, --output <dir>` | Output directory for generated Dart files | `.` |
32
+ | `-r, --router <name>` | Router export name | `appRouter` |
33
+ | `-c, --client-class <name>` | Generated client class name | `AppClient` |
34
+ | `--config <name>` | Config object export name | `dartCodegen` |
35
+ | `--tsconfig <path>` | `tsconfig.json` used to resolve types | — |
36
+ | `--include-internal` | Also generate `internal: true` procedures | off |
37
+ | `-h, --help` | Show help | — |
38
+
39
+ Example:
40
+
41
+ ```sh
42
+ npx supalive-dart src/server/procedures.ts \
43
+ --router appRouter \
44
+ --client-class BasicClient \
45
+ --output lib/api \
46
+ --tsconfig tsconfig.json
47
+ ```
48
+
49
+ This writes `models.dart` and `client.dart` into `lib/api/`. Treat that
50
+ directory as generated — never edit it by hand; re-run the command after
51
+ changing your procedures.
52
+
53
+ ## Config in the entry file
54
+
55
+ Instead of passing flags, export a `dartCodegen` object next to your router.
56
+ CLI flags take precedence over these values.
57
+
58
+ ```ts
59
+ export const dartCodegen = {
60
+ output: "lib/api",
61
+ clientClassName: "BasicClient",
62
+ // router, includeInternal, tsconfig, numericOverrides also supported
63
+ };
64
+ ```
65
+
66
+ Only JSON-literal values are read statically; dynamic expressions are ignored.
67
+
68
+ ## Programmatic API
69
+
70
+ ```ts
71
+ import { generateToDisk, generate } from "@supalive/codegen";
72
+
73
+ // Extract + emit + write files to `output`.
74
+ generateToDisk({
75
+ entry: "src/server/procedures.ts",
76
+ output: "lib/api",
77
+ clientClassName: "BasicClient",
78
+ tsconfig: "tsconfig.json",
79
+ });
80
+
81
+ // Or get the emitted files in memory without touching disk.
82
+ const { files } = generate({ entry: "src/server/procedures.ts" });
83
+ ```
84
+
85
+ `extractClient` (build the intermediate representation) and `emit` (turn the IR
86
+ into files) are also exported for advanced use.
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { n as generateToDisk, r as readEntryConfig } from "./src-BD-RIuCu.js";
3
+ import path from "node:path";
4
+ //#region src/cli.ts
5
+ function parseArgs(argv) {
6
+ const args = {
7
+ configExport: "dartCodegen",
8
+ help: false
9
+ };
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const a = argv[i];
12
+ switch (a) {
13
+ case "-h":
14
+ case "--help":
15
+ args.help = true;
16
+ break;
17
+ case "-o":
18
+ case "--output":
19
+ args.output = argv[++i];
20
+ break;
21
+ case "-r":
22
+ case "--router":
23
+ args.router = argv[++i];
24
+ break;
25
+ case "-c":
26
+ case "--client-class":
27
+ args.clientClass = argv[++i];
28
+ break;
29
+ case "--config":
30
+ args.configExport = argv[++i] ?? args.configExport;
31
+ break;
32
+ case "--tsconfig":
33
+ args.tsconfig = argv[++i];
34
+ break;
35
+ case "--include-internal":
36
+ args.includeInternal = true;
37
+ break;
38
+ default:
39
+ if (a.startsWith("-")) throw new Error(`Unknown option: ${a}`);
40
+ if (args.entry) throw new Error(`Unexpected extra argument: ${a}`);
41
+ args.entry = a;
42
+ }
43
+ }
44
+ return args;
45
+ }
46
+ const HELP = `supalive-dart — generate a type-safe Dart client from a Supalive router
47
+
48
+ Usage:
49
+ supalive-dart <entry.ts> [options]
50
+
51
+ Options:
52
+ -o, --output <dir> Output directory for generated Dart files
53
+ -r, --router <name> Router export name (default: appRouter)
54
+ -c, --client-class <name> Generated client class name (default: AppClient)
55
+ --config <name> Config object export name (default: dartCodegen)
56
+ --tsconfig <path> tsconfig.json used to resolve types
57
+ --include-internal Also generate internal (server-only) procedures
58
+ -h, --help Show this help
59
+
60
+ The entry file may export a \`dartCodegen\` object with any of the above
61
+ options; CLI flags take precedence.`;
62
+ async function main() {
63
+ const args = parseArgs(process.argv.slice(2));
64
+ if (args.help || !args.entry) {
65
+ console.log(HELP);
66
+ process.exit(args.entry ? 0 : args.help ? 0 : 1);
67
+ }
68
+ const fromEntry = readEntryConfig(args.entry, args.configExport, args.tsconfig);
69
+ const entryDir = path.dirname(path.resolve(args.entry));
70
+ const output = args.output ?? (fromEntry.output ? path.resolve(entryDir, fromEntry.output) : void 0);
71
+ const { files, config } = generateToDisk({
72
+ entry: args.entry,
73
+ output,
74
+ router: args.router ?? fromEntry.router,
75
+ clientClassName: args.clientClass ?? fromEntry.clientClassName,
76
+ includeInternal: args.includeInternal ?? fromEntry.includeInternal,
77
+ tsconfig: args.tsconfig ?? fromEntry.tsconfig,
78
+ numericOverrides: fromEntry.numericOverrides
79
+ });
80
+ console.log(`Generated ${files.length} file(s) into ${config.output}:\n` + files.map((f) => ` - ${f.path}`).join("\n"));
81
+ }
82
+ main().catch((err) => {
83
+ console.error(`supalive-dart: ${err instanceof Error ? err.message : err}`);
84
+ process.exit(1);
85
+ });
86
+ //#endregion
87
+ export {};
@@ -0,0 +1,134 @@
1
+ //#region src/config.d.ts
2
+ interface SupaliveDartConfig {
3
+ /** Output directory for generated Dart files. */
4
+ output?: string;
5
+ /** Export name of the router value in the entry file. Default "appRouter". */
6
+ router?: string;
7
+ /** Generate internal (server-only) procedures too. Default false. */
8
+ includeInternal?: boolean;
9
+ /** Name of the generated client class. Default "AppClient". */
10
+ clientClassName?: string;
11
+ /** Path to tsconfig.json used to resolve types. */
12
+ tsconfig?: string;
13
+ /**
14
+ * Force specific numeric fields to Dart `int` where TS erased the
15
+ * distinction (outputs). Keyed by `ModelName.fieldName`.
16
+ */
17
+ numericOverrides?: Record<string, "int" | "double" | "bigint">;
18
+ }
19
+ interface ResolvedConfig extends Required<Omit<SupaliveDartConfig, "tsconfig" | "numericOverrides">> {
20
+ entry: string;
21
+ tsconfig?: string;
22
+ numericOverrides: Record<string, "int" | "double" | "bigint">;
23
+ }
24
+ //#endregion
25
+ //#region src/ir.d.ts
26
+ type DartType = {
27
+ kind: "string";
28
+ } | {
29
+ kind: "bool";
30
+ } | {
31
+ kind: "double";
32
+ } | {
33
+ kind: "int";
34
+ } | {
35
+ kind: "bigint";
36
+ } | {
37
+ kind: "bytes";
38
+ } | {
39
+ kind: "datetime";
40
+ } | {
41
+ kind: "dynamic";
42
+ } | {
43
+ kind: "list";
44
+ element: DartType;
45
+ } | {
46
+ kind: "map";
47
+ value: DartType;
48
+ } | {
49
+ kind: "enum";
50
+ name: string;
51
+ } | {
52
+ kind: "model";
53
+ name: string;
54
+ };
55
+ interface FieldIR {
56
+ /** Dart field name (camelCase). */
57
+ name: string;
58
+ /** Wire JSON key. */
59
+ jsonKey: string;
60
+ type: DartType;
61
+ /** May be omitted from the payload (Zod `.optional()` / TS `?`). */
62
+ optional: boolean;
63
+ /** May be explicitly null (Zod `.nullable()` / TS `| null`). */
64
+ nullable: boolean;
65
+ }
66
+ interface ModelIR {
67
+ name: string;
68
+ fields: FieldIR[];
69
+ }
70
+ interface EnumValueIR {
71
+ dartName: string;
72
+ wire: string;
73
+ }
74
+ interface EnumIR {
75
+ name: string;
76
+ values: EnumValueIR[];
77
+ }
78
+ type ProcedureKind = "query" | "mutation" | "action";
79
+ interface ProcedureIR {
80
+ name: string;
81
+ kind: ProcedureKind;
82
+ internal: boolean;
83
+ args: DartType;
84
+ /** Whether args (the whole input) is a plain object model. */
85
+ argsIsModel: boolean;
86
+ result: DartType;
87
+ /** Whether the result may be null (TS `T | null`). */
88
+ resultNullable: boolean;
89
+ }
90
+ interface ClientIR {
91
+ clientClassName: string;
92
+ procedures: ProcedureIR[];
93
+ models: ModelIR[];
94
+ enums: EnumIR[];
95
+ }
96
+ //#endregion
97
+ //#region src/emit.d.ts
98
+ interface EmittedFile {
99
+ path: string;
100
+ contents: string;
101
+ }
102
+ declare function emit(ir: ClientIR): EmittedFile[];
103
+ //#endregion
104
+ //#region src/extract.d.ts
105
+ /** Load the router from the entry file and extract a ClientIR. */
106
+ declare function extractClient(config: ResolvedConfig): ClientIR;
107
+ //#endregion
108
+ //#region src/index.d.ts
109
+ interface GenerateOptions extends SupaliveDartConfig {
110
+ /** Path to the TypeScript entry file that exports the router. */
111
+ entry: string;
112
+ }
113
+ interface GenerateResult {
114
+ files: EmittedFile[];
115
+ config: ResolvedConfig;
116
+ }
117
+ /** Resolve a partial config against defaults, anchoring relative paths. */
118
+ declare function resolveConfig(opts: GenerateOptions): ResolvedConfig;
119
+ /** Extract + emit. Does not touch the filesystem. */
120
+ declare function generate(opts: GenerateOptions): GenerateResult;
121
+ /** Extract + emit + write the files to `config.output`. */
122
+ declare function generateToDisk(opts: GenerateOptions): GenerateResult;
123
+ /**
124
+ * Best-effort static read of a config object exported from the entry file,
125
+ * so users can keep `router` + codegen options next to the router itself:
126
+ *
127
+ * export const dartCodegen = { output: "lib/api", clientClassName: "Api" };
128
+ *
129
+ * Only JSON-literal values are read (string / boolean / number / nested
130
+ * object). Anything dynamic is ignored. Returns `{}` if not found.
131
+ */
132
+ declare function readEntryConfig(entry: string, exportName?: string, tsconfig?: string): SupaliveDartConfig;
133
+ //#endregion
134
+ export { ClientIR, DartType, type EmittedFile, EnumIR, EnumValueIR, FieldIR, GenerateOptions, GenerateResult, ModelIR, ProcedureIR, ProcedureKind, type ResolvedConfig, type SupaliveDartConfig, emit, extractClient, generate, generateToDisk, readEntryConfig, resolveConfig };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as emit, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, t as generate } from "./src-BD-RIuCu.js";
2
+ export { emit, extractClient, generate, generateToDisk, readEntryConfig, resolveConfig };
@@ -0,0 +1,812 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Node, Project, ts } from "ts-morph";
4
+ //#region src/zod-schema.ts
5
+ function scanZodArgMeta(routerDecl) {
6
+ const result = /* @__PURE__ */ new Map();
7
+ try {
8
+ const proceduresObj = getProceduresObjectLiteral(routerDecl);
9
+ if (!proceduresObj) return result;
10
+ for (const prop of proceduresObj.getProperties()) {
11
+ const key = getPropertyKey(prop);
12
+ if (!key) continue;
13
+ const call = resolveProcedureCall(prop);
14
+ if (!call) continue;
15
+ const argsSchema = getArgsSchema(call);
16
+ if (!argsSchema) continue;
17
+ const fieldMap = scanObjectSchema(argsSchema);
18
+ if (fieldMap.size > 0) result.set(key, fieldMap);
19
+ }
20
+ } catch {}
21
+ return result;
22
+ }
23
+ function getProceduresObjectLiteral(routerDecl) {
24
+ let init;
25
+ if (Node.isVariableDeclaration(routerDecl)) init = routerDecl.getInitializer();
26
+ else if (Node.isExportAssignment(routerDecl)) init = routerDecl.getExpression();
27
+ if (!init || !Node.isCallExpression(init)) return void 0;
28
+ const arg = init.getArguments()[0];
29
+ if (!arg || !Node.isObjectLiteralExpression(arg)) return void 0;
30
+ const proceduresProp = arg.getProperty("procedures");
31
+ if (!proceduresProp || !Node.isPropertyAssignment(proceduresProp)) return;
32
+ const value = proceduresProp.getInitializer();
33
+ return value && Node.isObjectLiteralExpression(value) ? value : void 0;
34
+ }
35
+ function getPropertyKey(prop) {
36
+ if (Node.isPropertyAssignment(prop) || Node.isShorthandPropertyAssignment(prop)) return prop.getName();
37
+ }
38
+ /** Follow a procedures-map entry to its `query/mutation/action({...})` call. */
39
+ function resolveProcedureCall(prop) {
40
+ let expr;
41
+ if (Node.isShorthandPropertyAssignment(prop)) expr = prop.getNameNode();
42
+ else if (Node.isPropertyAssignment(prop)) expr = prop.getInitializer();
43
+ return followToCall(expr, 0);
44
+ }
45
+ function followToCall(node, depth) {
46
+ if (!node || depth > 8) return void 0;
47
+ if (Node.isCallExpression(node)) return node;
48
+ if (Node.isIdentifier(node)) {
49
+ for (const def of node.getDefinitionNodes()) if (Node.isVariableDeclaration(def)) {
50
+ const found = followToCall(def.getInitializer(), depth + 1);
51
+ if (found) return found;
52
+ }
53
+ }
54
+ }
55
+ /** From a `query({ args: z.object({...}), handler })` call, get the schema. */
56
+ function getArgsSchema(call) {
57
+ if (!Node.isCallExpression(call)) return void 0;
58
+ const arg = call.getArguments()[0];
59
+ if (!arg || !Node.isObjectLiteralExpression(arg)) return void 0;
60
+ const argsProp = arg.getProperty("args");
61
+ if (!argsProp || !Node.isPropertyAssignment(argsProp)) return void 0;
62
+ return argsProp.getInitializer();
63
+ }
64
+ /** Scan `z.object({ field: <expr>, ... })` for per-field metadata. */
65
+ function scanObjectSchema(schema) {
66
+ const map = /* @__PURE__ */ new Map();
67
+ const objectLiteral = findZodObjectLiteral(schema);
68
+ if (!objectLiteral || !Node.isObjectLiteralExpression(objectLiteral)) return map;
69
+ for (const prop of objectLiteral.getProperties()) {
70
+ if (!Node.isPropertyAssignment(prop)) continue;
71
+ const key = prop.getName();
72
+ const init = prop.getInitializer();
73
+ if (!init) continue;
74
+ const methods = topLevelZodMethods(init);
75
+ const meta = {};
76
+ if (methods.has("bigint")) meta.numeric = "bigint";
77
+ else if (methods.has("int")) meta.numeric = "int";
78
+ if (methods.has("default") || methods.has("catch") || methods.has("prefault")) meta.optional = true;
79
+ if (meta.numeric || meta.optional) map.set(key, meta);
80
+ }
81
+ return map;
82
+ }
83
+ /**
84
+ * Names of the methods called at the top level of a Zod field expression.
85
+ * For `z.number().int().default(0)` → `{ number, int, default }`. Nested
86
+ * schemas (inside an object argument) are not descended into.
87
+ */
88
+ function topLevelZodMethods(node) {
89
+ const methods = /* @__PURE__ */ new Set();
90
+ let cur = node;
91
+ while (cur && Node.isCallExpression(cur)) {
92
+ const expr = cur.getExpression();
93
+ if (Node.isPropertyAccessExpression(expr)) {
94
+ methods.add(expr.getName());
95
+ cur = expr.getExpression();
96
+ } else break;
97
+ }
98
+ return methods;
99
+ }
100
+ function findZodObjectLiteral(node) {
101
+ let cur = node;
102
+ while (cur && Node.isCallExpression(cur)) {
103
+ const expr = cur.getExpression();
104
+ const exprText = expr.getText();
105
+ if (/(^|\.)(strict|loose)?[oO]bject$/.test(exprText)) {
106
+ const arg = cur.getArguments()[0];
107
+ return arg && Node.isObjectLiteralExpression(arg) ? arg : void 0;
108
+ }
109
+ if (Node.isPropertyAccessExpression(expr)) cur = expr.getExpression();
110
+ else break;
111
+ }
112
+ return cur && Node.isObjectLiteralExpression(cur) ? cur : void 0;
113
+ }
114
+ //#endregion
115
+ //#region src/extract.ts
116
+ /** Load the router from the entry file and extract a ClientIR. */
117
+ function extractClient(config) {
118
+ const exported = new Project({
119
+ tsConfigFilePath: config.tsconfig,
120
+ skipAddingFilesFromTsConfig: config.tsconfig ? false : true,
121
+ compilerOptions: config.tsconfig ? void 0 : {
122
+ strict: true,
123
+ target: ts.ScriptTarget.ES2022
124
+ }
125
+ }).addSourceFileAtPath(path.resolve(config.entry)).getExportedDeclarations().get(config.router);
126
+ if (!exported || exported.length === 0) throw new Error(`Could not find an export named "${config.router}" in ${config.entry}. Set { router: "<name>" } in your config or pass --router.`);
127
+ const routerDecl = exported[0];
128
+ const proceduresSym = routerDecl.getType().getProperty("procedures");
129
+ if (!proceduresSym) throw new Error(`Export "${config.router}" does not look like a Supalive router (no "procedures" property).`);
130
+ const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
131
+ const extractor = new Extractor(routerDecl, config, scanZodArgMeta(routerDecl));
132
+ const procedures = [];
133
+ for (const procSym of proceduresType.getProperties()) {
134
+ const name = procSym.getName();
135
+ const procType = procSym.getTypeAtLocation(routerDecl);
136
+ const ir = extractor.procedure(name, procType);
137
+ if (!ir) continue;
138
+ if (ir.internal && !config.includeInternal) continue;
139
+ procedures.push(ir);
140
+ }
141
+ procedures.sort((a, b) => a.name.localeCompare(b.name));
142
+ return {
143
+ clientClassName: config.clientClassName,
144
+ procedures,
145
+ models: extractor.models(),
146
+ enums: extractor.enums()
147
+ };
148
+ }
149
+ var Extractor = class {
150
+ locationNode;
151
+ config;
152
+ argMeta;
153
+ modelRegistry = /* @__PURE__ */ new Map();
154
+ enumRegistry = /* @__PURE__ */ new Map();
155
+ /** Value-set signature → already-registered enum name, for structural dedup. */
156
+ enumBySignature = /* @__PURE__ */ new Map();
157
+ nameCounts = /* @__PURE__ */ new Map();
158
+ visiting = /* @__PURE__ */ new Set();
159
+ constructor(locationNode, config, argMeta) {
160
+ this.locationNode = locationNode;
161
+ this.config = config;
162
+ this.argMeta = argMeta;
163
+ }
164
+ models() {
165
+ return [...this.modelRegistry.values()];
166
+ }
167
+ enums() {
168
+ return [...this.enumRegistry.values()];
169
+ }
170
+ procedure(name, procType) {
171
+ const kind = this.literalString(procType, "procedureType");
172
+ if (!kind) return null;
173
+ const internal = this.literalBoolean(procType, "internal") ?? false;
174
+ if (internal && !this.config.includeInternal) return null;
175
+ const fnSym = procType.getProperty("fn");
176
+ if (!fnSym) return null;
177
+ const sig = fnSym.getTypeAtLocation(this.locationNode).getCallSignatures()[0];
178
+ if (!sig) return null;
179
+ const inputParam = sig.getParameters()[1];
180
+ const argsType = inputParam ? this.walk(inputParam.getTypeAtLocation(this.locationNode), pascal(name) + "Args") : { kind: "dynamic" };
181
+ const metas = this.argMeta.get(name);
182
+ if (metas && argsType.kind === "model") {
183
+ const model = this.modelRegistry.get(argsType.name);
184
+ if (model) for (const f of model.fields) {
185
+ const meta = metas.get(f.jsonKey);
186
+ if (!meta) continue;
187
+ if (meta.numeric && (f.type.kind === "double" || f.type.kind === "int")) f.type = { kind: meta.numeric };
188
+ if (meta.optional) f.optional = true;
189
+ }
190
+ }
191
+ const ret = unwrapPromise(sig.getReturnType());
192
+ const { hasNull, hasUndefined } = splitNullish(ret);
193
+ const resultType = this.walk(ret, pascal(name) + "Result");
194
+ return {
195
+ name,
196
+ kind,
197
+ internal,
198
+ args: argsType,
199
+ argsIsModel: argsType.kind === "model",
200
+ result: resultType,
201
+ resultNullable: hasNull || hasUndefined
202
+ };
203
+ }
204
+ walk(type, hint) {
205
+ const { core } = splitNullish(type);
206
+ if (core.length === 0) return { kind: "dynamic" };
207
+ if (core.length > 1) return this.walkUnion(core, hint);
208
+ return this.walkSingle(core[0], hint);
209
+ }
210
+ walkSingle(type, hint) {
211
+ const flags = type.getFlags();
212
+ if (flags & ts.TypeFlags.String) return { kind: "string" };
213
+ if (flags & ts.TypeFlags.Number) return this.numberType(hint);
214
+ if (flags & ts.TypeFlags.Boolean) return { kind: "bool" };
215
+ if (flags & ts.TypeFlags.BooleanLiteral) return { kind: "bool" };
216
+ if (flags & ts.TypeFlags.BigInt) return { kind: "bigint" };
217
+ if (flags & ts.TypeFlags.BigIntLiteral) return { kind: "bigint" };
218
+ if (flags & ts.TypeFlags.StringLiteral) return { kind: "string" };
219
+ if (flags & ts.TypeFlags.NumberLiteral) return this.numberType(hint);
220
+ if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return { kind: "dynamic" };
221
+ if (type.isArray()) {
222
+ const el = type.getArrayElementTypeOrThrow();
223
+ return {
224
+ kind: "list",
225
+ element: this.walk(el, singular(hint))
226
+ };
227
+ }
228
+ const symName = type.getSymbol()?.getName();
229
+ if (symName === "Uint8Array") return { kind: "bytes" };
230
+ if (symName === "Date") return { kind: "datetime" };
231
+ if (isFunctionLike(type)) return { kind: "dynamic" };
232
+ if (type.isUnion()) {
233
+ const { core } = splitNullish(type);
234
+ if (core.length === 1) return this.walkSingle(core[0], hint);
235
+ return this.walkUnion(core, hint);
236
+ }
237
+ if (type.isObject()) {
238
+ const props = type.getProperties();
239
+ const indexType = type.getStringIndexType();
240
+ if (props.length === 0 && indexType) {
241
+ if (indexType.isNever()) return this.registerEmptyModel(hint);
242
+ return {
243
+ kind: "map",
244
+ value: this.walk(indexType, hint + "Value")
245
+ };
246
+ }
247
+ return this.walkObject(type, hint);
248
+ }
249
+ return { kind: "dynamic" };
250
+ }
251
+ walkUnion(members, hint, enumHint) {
252
+ if (members.every((m) => m.getFlags() & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.Boolean))) return { kind: "bool" };
253
+ if (members.every((m) => m.getFlags() & ts.TypeFlags.StringLiteral)) return this.registerEnum(enumHint ?? hint, members.map((m) => String(m.getLiteralValue())));
254
+ if (members.every((m) => m.isObject())) return this.registerSealed(hint, members);
255
+ return { kind: "dynamic" };
256
+ }
257
+ walkObject(type, hint) {
258
+ const name = this.modelName(type, hint);
259
+ if (this.modelRegistry.has(name)) return {
260
+ kind: "model",
261
+ name
262
+ };
263
+ if (this.visiting.has(name)) return {
264
+ kind: "model",
265
+ name
266
+ };
267
+ this.visiting.add(name);
268
+ const fields = [];
269
+ for (const prop of type.getProperties()) {
270
+ const jsonKey = prop.getName();
271
+ const propType = prop.getTypeAtLocation(this.locationNode);
272
+ if (prop.getFlags() & ts.SymbolFlags.Method || isFunctionLike(propType)) continue;
273
+ const declaredOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
274
+ const { core, hasNull, hasUndefined } = splitNullish(propType);
275
+ const optional = declaredOptional || hasUndefined;
276
+ const nullable = hasNull;
277
+ let dartType;
278
+ if (core.length === 0) dartType = { kind: "dynamic" };
279
+ else if (core.length === 1) dartType = this.walkSingle(core[0], name + pascal(jsonKey));
280
+ else dartType = this.walkUnion(core, name + pascal(jsonKey), pascal(jsonKey));
281
+ const override = this.config.numericOverrides[`${name}.${jsonKey}`];
282
+ if (override && (dartType.kind === "double" || dartType.kind === "int")) dartType = { kind: override };
283
+ fields.push({
284
+ name: camel(jsonKey),
285
+ jsonKey,
286
+ type: dartType,
287
+ optional,
288
+ nullable
289
+ });
290
+ }
291
+ this.visiting.delete(name);
292
+ this.modelRegistry.set(name, {
293
+ name,
294
+ fields
295
+ });
296
+ return {
297
+ kind: "model",
298
+ name
299
+ };
300
+ }
301
+ numberType(hint) {
302
+ return { kind: "double" };
303
+ }
304
+ modelName(type, hint) {
305
+ const symName = (type.getAliasSymbol() ?? type.getSymbol())?.getName();
306
+ if (symName && symName !== "__type" && symName !== "__object") return uniqueName(symName, this.modelRegistry, type);
307
+ return this.uniqueHint(hint);
308
+ }
309
+ /** Register (or reuse) a fieldless model named from the hint. */
310
+ registerEmptyModel(hint) {
311
+ const name = this.uniqueHint(hint);
312
+ if (!this.modelRegistry.has(name)) this.modelRegistry.set(name, {
313
+ name,
314
+ fields: []
315
+ });
316
+ return {
317
+ kind: "model",
318
+ name
319
+ };
320
+ }
321
+ registerEnum(hint, values) {
322
+ const signature = values.join("\0");
323
+ const existing = this.enumBySignature.get(signature);
324
+ if (existing) return {
325
+ kind: "enum",
326
+ name: existing
327
+ };
328
+ const name = this.uniqueHint(hint);
329
+ this.enumRegistry.set(name, {
330
+ name,
331
+ values: values.map((wire) => ({
332
+ dartName: enumValueName(wire),
333
+ wire
334
+ }))
335
+ });
336
+ this.enumBySignature.set(signature, name);
337
+ return {
338
+ kind: "enum",
339
+ name
340
+ };
341
+ }
342
+ registerSealed(hint, members) {
343
+ const name = this.uniqueHint(hint);
344
+ if (this.modelRegistry.has(name)) return {
345
+ kind: "model",
346
+ name
347
+ };
348
+ if (this.visiting.has(name)) return {
349
+ kind: "model",
350
+ name
351
+ };
352
+ this.visiting.add(name);
353
+ const fieldMap = /* @__PURE__ */ new Map();
354
+ for (const m of members) for (const prop of m.getProperties()) {
355
+ const jsonKey = prop.getName();
356
+ const propType = prop.getTypeAtLocation(this.locationNode);
357
+ const declaredOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
358
+ const { core, hasNull, hasUndefined } = splitNullish(propType);
359
+ const dt = core.length === 0 ? { kind: "dynamic" } : core.length === 1 ? this.walkSingle(core[0], name + pascal(jsonKey)) : this.walkUnion(core, name + pascal(jsonKey), pascal(jsonKey));
360
+ const acc = fieldMap.get(jsonKey) ?? {
361
+ types: [],
362
+ present: 0,
363
+ nullable: false,
364
+ optional: false
365
+ };
366
+ acc.types.push(dt);
367
+ acc.present++;
368
+ if (hasNull) acc.nullable = true;
369
+ if (declaredOptional || hasUndefined) acc.optional = true;
370
+ fieldMap.set(jsonKey, acc);
371
+ }
372
+ const fields = [];
373
+ for (const [jsonKey, acc] of fieldMap) {
374
+ const consistent = acc.types.every((t) => JSON.stringify(t) === JSON.stringify(acc.types[0]));
375
+ fields.push({
376
+ name: camel(jsonKey),
377
+ jsonKey,
378
+ type: consistent ? acc.types[0] : { kind: "dynamic" },
379
+ optional: acc.optional || acc.present < members.length,
380
+ nullable: acc.nullable
381
+ });
382
+ }
383
+ this.visiting.delete(name);
384
+ this.modelRegistry.set(name, {
385
+ name,
386
+ fields
387
+ });
388
+ return {
389
+ kind: "model",
390
+ name
391
+ };
392
+ }
393
+ uniqueHint(hint) {
394
+ const base = pascal(hint);
395
+ const count = this.nameCounts.get(base) ?? 0;
396
+ if (count === 0) {
397
+ this.nameCounts.set(base, 1);
398
+ return base;
399
+ }
400
+ this.nameCounts.set(base, count + 1);
401
+ return `${base}${count + 1}`;
402
+ }
403
+ literalString(type, prop) {
404
+ const p = type.getProperty(prop);
405
+ if (!p) return void 0;
406
+ const t = p.getTypeAtLocation(this.locationNode);
407
+ if (t.getFlags() & ts.TypeFlags.StringLiteral) return String(t.getLiteralValue());
408
+ }
409
+ literalBoolean(type, prop) {
410
+ const p = type.getProperty(prop);
411
+ if (!p) return void 0;
412
+ const txt = p.getTypeAtLocation(this.locationNode).getText();
413
+ if (txt === "true") return true;
414
+ if (txt === "false") return false;
415
+ }
416
+ };
417
+ /** A type whose primary shape is a call/construct signature (a function). */
418
+ function isFunctionLike(type) {
419
+ return type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0;
420
+ }
421
+ function unwrapPromise(type) {
422
+ if (type.getSymbol()?.getName() === "Promise") {
423
+ const args = type.getTypeArguments();
424
+ if (args.length === 1) return args[0];
425
+ }
426
+ return type;
427
+ }
428
+ function splitNullish(type) {
429
+ const parts = type.isUnion() ? type.getUnionTypes() : [type];
430
+ let hasNull = false;
431
+ let hasUndefined = false;
432
+ const core = [];
433
+ for (const p of parts) {
434
+ const f = p.getFlags();
435
+ if (f & ts.TypeFlags.Null) hasNull = true;
436
+ else if (f & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) hasUndefined = true;
437
+ else core.push(p);
438
+ }
439
+ return {
440
+ core,
441
+ hasNull,
442
+ hasUndefined
443
+ };
444
+ }
445
+ function uniqueName(symName, registry, _type) {
446
+ return pascal(symName);
447
+ }
448
+ function pascal(s) {
449
+ return s.replace(/[^A-Za-z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
450
+ }
451
+ function camel(s) {
452
+ const p = pascal(s);
453
+ return p.charAt(0).toLowerCase() + p.slice(1);
454
+ }
455
+ function singular(hint) {
456
+ if (hint.endsWith("ies")) return hint.slice(0, -3) + "y";
457
+ if (hint.endsWith("s") && !hint.endsWith("ss")) return hint.slice(0, -1);
458
+ return hint + "Item";
459
+ }
460
+ function enumValueName(wire) {
461
+ const c = camel(wire);
462
+ if (!c || /^[0-9]/.test(c)) return "v" + pascal(wire);
463
+ return c;
464
+ }
465
+ //#endregion
466
+ //#region src/emit.ts
467
+ const HEADER = "// GENERATED by @supalive/codegen. Do not edit by hand.\n// ignore_for_file: type=lint\n";
468
+ function emit(ir) {
469
+ return [{
470
+ path: "models.dart",
471
+ contents: emitModels(ir)
472
+ }, {
473
+ path: "client.dart",
474
+ contents: emitClient(ir)
475
+ }];
476
+ }
477
+ /** Dart type name for a non-null [DartType]. */
478
+ function typeName(t) {
479
+ switch (t.kind) {
480
+ case "string": return "String";
481
+ case "bool": return "bool";
482
+ case "double": return "double";
483
+ case "int": return "int";
484
+ case "bigint": return "BigInt";
485
+ case "bytes": return "Uint8List";
486
+ case "datetime": return "DateTime";
487
+ case "dynamic": return "Object?";
488
+ case "list": return `List<${typeName(t.element)}>`;
489
+ case "map": return `Map<String, ${typeName(t.value)}>`;
490
+ case "enum": return t.name;
491
+ case "model": return t.name;
492
+ }
493
+ }
494
+ /** Decode `expr` (an `Object?` value known to be non-null) into [t]. */
495
+ function decodeNonNull(t, expr) {
496
+ switch (t.kind) {
497
+ case "string": return `${expr} as String`;
498
+ case "bool": return `${expr} as bool`;
499
+ case "double": return `doubleFromJson(${expr})`;
500
+ case "int": return `intFromJson(${expr})`;
501
+ case "bigint": return `bigIntFromJson(${expr})`;
502
+ case "bytes": return `bytesFromJson(${expr})`;
503
+ case "datetime": return `dateTimeFromJson(${expr})`;
504
+ case "dynamic": return expr;
505
+ case "enum": return `${t.name}.fromJson(${expr})`;
506
+ case "model": return `${t.name}.fromJson(${expr} as Map<String, Object?>)`;
507
+ case "list": return `(${expr} as List).map((e) => ${decodeNonNull(t.element, "e")}).toList()`;
508
+ case "map": return `(${expr} as Map<String, Object?>).map((k, v) => MapEntry(k, ${decodeNonNull(t.value, "v")}))`;
509
+ }
510
+ }
511
+ /** Decode `expr` allowing null. */
512
+ function decodeNullable(t, expr) {
513
+ switch (t.kind) {
514
+ case "string": return `${expr} as String?`;
515
+ case "bool": return `${expr} as bool?`;
516
+ case "dynamic": return expr;
517
+ default: return `${expr} == null ? null : ${decodeNonNull(t, expr)}`;
518
+ }
519
+ }
520
+ /** Encode a non-null Dart value [expr] of type [t] to a JSON-native value. */
521
+ function encodeNonNull(t, expr) {
522
+ switch (t.kind) {
523
+ case "string":
524
+ case "bool":
525
+ case "double":
526
+ case "int":
527
+ case "bigint":
528
+ case "bytes":
529
+ case "dynamic": return expr;
530
+ case "datetime": return `dateTimeToJson(${expr})`;
531
+ case "enum": return `${expr}.toJson()`;
532
+ case "model": return `${expr}.toJson()`;
533
+ case "list": return `${expr}.map((e) => ${encodeNonNull(t.element, "e")}).toList()`;
534
+ case "map": return `${expr}.map((k, v) => MapEntry(k, ${encodeNonNull(t.value, "v")}))`;
535
+ }
536
+ }
537
+ /** Encode a possibly-null value. */
538
+ function encodeNullable(t, expr) {
539
+ switch (t.kind) {
540
+ case "string":
541
+ case "bool":
542
+ case "double":
543
+ case "int":
544
+ case "bigint":
545
+ case "bytes":
546
+ case "dynamic": return expr;
547
+ default: return `${expr} == null ? null : ${encodeNonNull(t, stripNull(expr))}`;
548
+ }
549
+ }
550
+ function stripNull(expr) {
551
+ return `${expr}!`;
552
+ }
553
+ function emitModels(ir) {
554
+ const out = [HEADER];
555
+ if (modelsUseBytes(ir)) out.push("import 'dart:typed_data';\n");
556
+ out.push("import 'package:supalive_client/supalive_client.dart';\n");
557
+ out.push("const Object _undefined = Object();\n");
558
+ for (const e of ir.enums) out.push(emitEnum(e));
559
+ for (const m of ir.models) out.push(emitModel(m));
560
+ return out.join("\n");
561
+ }
562
+ /** Whether any generated model field decodes to `Uint8List` (needs typed_data). */
563
+ function modelsUseBytes(ir) {
564
+ const hasBytes = (t) => {
565
+ switch (t.kind) {
566
+ case "bytes": return true;
567
+ case "list": return hasBytes(t.element);
568
+ case "map": return hasBytes(t.value);
569
+ default: return false;
570
+ }
571
+ };
572
+ return ir.models.some((m) => m.fields.some((f) => hasBytes(f.type)));
573
+ }
574
+ function emitEnum(e) {
575
+ const values = e.values.map((v) => ` ${v.dartName}('${v.wire}')`).join(",\n");
576
+ return `enum ${e.name} {
577
+ ${values};
578
+
579
+ const ${e.name}(this.wire);
580
+ final String wire;
581
+
582
+ static ${e.name} fromJson(Object? value) => values.firstWhere(
583
+ (e) => e.wire == value,
584
+ orElse: () => throw ArgumentError('Unknown ${e.name}: \$value'),
585
+ );
586
+
587
+ String toJson() => wire;
588
+ }
589
+ `;
590
+ }
591
+ /** Dart declared type for a field, applying nullability. */
592
+ function fieldType(f) {
593
+ if (f.optional && f.nullable) return `Optional<${typeName(f.type)}>`;
594
+ const base = typeName(f.type);
595
+ const nullable = f.optional || f.nullable;
596
+ if (base === "Object?") return "Object?";
597
+ return nullable ? `${base}?` : base;
598
+ }
599
+ function emitModel(m) {
600
+ const ctorParams = m.fields.map((f) => {
601
+ if (f.optional && f.nullable) return ` this.${f.name} = const Optional.absent()`;
602
+ return !(f.optional || f.nullable) ? ` required this.${f.name}` : ` this.${f.name}`;
603
+ }).join(",\n");
604
+ const fields = m.fields.map((f) => ` final ${fieldType(f)} ${f.name};`).join("\n");
605
+ const fromJson = m.fields.map((f) => emitFromJsonField(f)).join("\n");
606
+ const toJson = m.fields.map((f) => emitToJsonField(f)).join("\n");
607
+ const copyParams = m.fields.map((f) => ` Object? ${f.name} = _undefined`).join(",\n");
608
+ const copyArgs = m.fields.map((f) => {
609
+ const ft = fieldType(f);
610
+ const value = ft === "Object?" ? f.name : `${f.name} as ${ft}`;
611
+ return ` ${f.name}: identical(${f.name}, _undefined) ? this.${f.name} : ${value}`;
612
+ }).join(",\n");
613
+ const isDeep = (f) => {
614
+ if (f.optional && f.nullable) return false;
615
+ const k = f.type.kind;
616
+ return k === "list" || k === "map" || k === "bytes" || k === "dynamic";
617
+ };
618
+ const eqFields = m.fields.map((f) => {
619
+ const cmp = isDeep(f) ? `deepEquals(other.${f.name}, ${f.name})` : `other.${f.name} == ${f.name}`;
620
+ return `(identical(other.${f.name}, ${f.name}) || ${cmp})`;
621
+ }).join(" &&\n ");
622
+ const hashParts = m.fields.map((f) => isDeep(f) ? `deepHashCode(${f.name})` : f.name).join(", ");
623
+ const hashBody = m.fields.length === 0 ? "runtimeType.hashCode" : m.fields.length <= 19 ? `Object.hash(runtimeType, ${hashParts})` : `Object.hashAll([runtimeType, ${hashParts}])`;
624
+ return `class ${m.name} implements SupaliveModel {
625
+ const ${m.name}(${m.fields.length ? "{\n" + ctorParams + ",\n }" : ""});
626
+
627
+ ${fields}
628
+
629
+ factory ${m.name}.fromJson(Map<String, Object?> json) => ${m.name}(${m.fields.length ? "\n" + fromJson + "\n " : ""});
630
+
631
+ @override
632
+ Map<String, Object?> toJson() {
633
+ final json = <String, Object?>{};
634
+ ${toJson}
635
+ return json;
636
+ }
637
+
638
+ ${m.name} copyWith(${m.fields.length ? "{\n" + copyParams + ",\n }" : ""}) =>
639
+ ${m.name}(${m.fields.length ? "\n" + copyArgs + ",\n " : ""});
640
+
641
+ @override
642
+ bool operator ==(Object other) =>
643
+ identical(this, other) ||
644
+ (other is ${m.name} &&
645
+ other.runtimeType == runtimeType${m.fields.length ? " &&\n " + eqFields : ""});
646
+
647
+ @override
648
+ int get hashCode => ${hashBody};
649
+ }
650
+ `;
651
+ }
652
+ function emitFromJsonField(f) {
653
+ const key = `json['${f.jsonKey}']`;
654
+ if (f.optional && f.nullable) {
655
+ const inner = typeName(f.type);
656
+ return ` ${f.name}: json.containsKey('${f.jsonKey}')
657
+ ? (${key} == null
658
+ ? const Optional<${inner}>.ofNull()
659
+ : Optional<${inner}>.of(${decodeNonNull(f.type, key)}))
660
+ : const Optional<${inner}>.absent(),`;
661
+ }
662
+ if (f.optional || f.nullable) return ` ${f.name}: ${decodeNullable(f.type, key)},`;
663
+ return ` ${f.name}: ${decodeNonNull(f.type, key)},`;
664
+ }
665
+ function emitToJsonField(f) {
666
+ const key = `'${f.jsonKey}'`;
667
+ if (f.optional && f.nullable) return ` if (${f.name}.isPresent) {
668
+ final v = ${f.name}.value;
669
+ json[${key}] = v == null ? null : ${encodeNonNull(f.type, "v")};
670
+ }`;
671
+ if (f.optional) return ` if (${f.name} != null) json[${key}] = ${encodeNonNull(f.type, `${f.name}!`)};`;
672
+ if (f.nullable) return ` json[${key}] = ${encodeNullable(f.type, f.name)};`;
673
+ return ` json[${key}] = ${encodeNonNull(f.type, f.name)};`;
674
+ }
675
+ function emitClient(ir) {
676
+ const out = [HEADER];
677
+ out.push("import 'package:supalive_client/supalive_client.dart';");
678
+ out.push("import 'models.dart';");
679
+ out.push("\nexport 'models.dart';");
680
+ out.push("export 'package:supalive_client/supalive_client.dart';\n");
681
+ const endpoints = ir.procedures.map((p) => emitEndpoint(p)).join("\n\n");
682
+ out.push(`class ${ir.clientClassName} {
683
+ ${ir.clientClassName}(SupaliveConfig config) : _client = SupaliveClient(config);
684
+
685
+ /// Construct around an existing transport (e.g. shared across clients).
686
+ ${ir.clientClassName}.fromClient(this._client);
687
+
688
+ final SupaliveClient _client;
689
+
690
+ /// The underlying transport, for connection-state APIs.
691
+ SupaliveClient get raw => _client;
692
+
693
+ Future<void> connect() => _client.connect();
694
+ void disconnect() => _client.disconnect();
695
+ bool get isReady => _client.isReady;
696
+ bool get isAuthenticated => _client.isAuthenticated;
697
+ bool get isAnonymous => _client.isAnonymous;
698
+ String? get authUserId => _client.authUserId;
699
+ ClientPublicState get publicState => _client.publicState;
700
+ void Function() onClientState(void Function(ClientPublicState) listener) =>
701
+ _client.onClientState(listener);
702
+
703
+ ${endpoints}
704
+ }
705
+ `);
706
+ return out.join("\n");
707
+ }
708
+ function argsTypeName(p) {
709
+ return p.argsIsModel ? typeName(p.args) : typeName(p.args);
710
+ }
711
+ function encodeArgs(p) {
712
+ if (p.argsIsModel) return "(a) => a.toJson()";
713
+ return `(a) => ${encodeNonNull(p.args, "a")}`;
714
+ }
715
+ function resultTypeName(p) {
716
+ const base = typeName(p.result);
717
+ if (base === "Object?") return "Object?";
718
+ return p.resultNullable ? `${base}?` : base;
719
+ }
720
+ function decodeResult(p) {
721
+ if (p.resultNullable) return `(raw) => ${decodeNullable(p.result, "raw")}`;
722
+ return `(raw) => ${decodeNonNull(p.result, "raw")}`;
723
+ }
724
+ function emitEndpoint(p) {
725
+ const endpointClass = p.kind === "query" ? "QueryEndpoint" : p.kind === "mutation" ? "MutationEndpoint" : "ActionEndpoint";
726
+ const args = argsTypeName(p);
727
+ const result = resultTypeName(p);
728
+ return ` late final ${p.name} = ${endpointClass}<${args}, ${result}>(
729
+ _client,
730
+ '${p.name}',
731
+ ${encodeArgs(p)},
732
+ ${decodeResult(p)},
733
+ );`;
734
+ }
735
+ //#endregion
736
+ //#region src/index.ts
737
+ const DEFAULTS = {
738
+ output: "lib/generated",
739
+ router: "appRouter",
740
+ includeInternal: false,
741
+ clientClassName: "AppClient"
742
+ };
743
+ /** Resolve a partial config against defaults, anchoring relative paths. */
744
+ function resolveConfig(opts) {
745
+ return {
746
+ entry: path.resolve(opts.entry),
747
+ output: path.resolve(opts.output ?? DEFAULTS.output),
748
+ router: opts.router ?? DEFAULTS.router,
749
+ includeInternal: opts.includeInternal ?? DEFAULTS.includeInternal,
750
+ clientClassName: opts.clientClassName ?? DEFAULTS.clientClassName,
751
+ tsconfig: opts.tsconfig ? path.resolve(opts.tsconfig) : void 0,
752
+ numericOverrides: opts.numericOverrides ?? {}
753
+ };
754
+ }
755
+ /** Extract + emit. Does not touch the filesystem. */
756
+ function generate(opts) {
757
+ const config = resolveConfig(opts);
758
+ return {
759
+ files: emit(extractClient(config)),
760
+ config
761
+ };
762
+ }
763
+ /** Extract + emit + write the files to `config.output`. */
764
+ function generateToDisk(opts) {
765
+ const result = generate(opts);
766
+ fs.mkdirSync(result.config.output, { recursive: true });
767
+ for (const file of result.files) fs.writeFileSync(path.join(result.config.output, file.path), file.contents, "utf8");
768
+ return result;
769
+ }
770
+ /**
771
+ * Best-effort static read of a config object exported from the entry file,
772
+ * so users can keep `router` + codegen options next to the router itself:
773
+ *
774
+ * export const dartCodegen = { output: "lib/api", clientClassName: "Api" };
775
+ *
776
+ * Only JSON-literal values are read (string / boolean / number / nested
777
+ * object). Anything dynamic is ignored. Returns `{}` if not found.
778
+ */
779
+ function readEntryConfig(entry, exportName = "dartCodegen", tsconfig) {
780
+ const decls = new Project({
781
+ tsConfigFilePath: tsconfig,
782
+ skipAddingFilesFromTsConfig: true,
783
+ compilerOptions: tsconfig ? void 0 : { allowJs: true }
784
+ }).addSourceFileAtPath(path.resolve(entry)).getExportedDeclarations().get(exportName);
785
+ if (!decls || decls.length === 0) return {};
786
+ const decl = decls[0];
787
+ const init = Node.isVariableDeclaration(decl) ? decl.getInitializer() : Node.isExpression(decl) ? decl : void 0;
788
+ if (!init || !Node.isObjectLiteralExpression(init)) return {};
789
+ return evalObjectLiteral(init);
790
+ }
791
+ function evalObjectLiteral(obj) {
792
+ const out = {};
793
+ if (!Node.isObjectLiteralExpression(obj)) return out;
794
+ for (const prop of obj.getProperties()) {
795
+ if (!Node.isPropertyAssignment(prop)) continue;
796
+ const nameNode = prop.getNameNode();
797
+ const key = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();
798
+ const value = evalLiteral(prop.getInitializerOrThrow());
799
+ if (value !== void 0) out[key] = value;
800
+ }
801
+ return out;
802
+ }
803
+ function evalLiteral(node) {
804
+ if (Node.isStringLiteral(node)) return node.getLiteralValue();
805
+ if (Node.isNumericLiteral(node)) return node.getLiteralValue();
806
+ if (Node.isObjectLiteralExpression(node)) return evalObjectLiteral(node);
807
+ const text = node.getText();
808
+ if (text === "true") return true;
809
+ if (text === "false") return false;
810
+ }
811
+ //#endregion
812
+ export { emit as a, resolveConfig as i, generateToDisk as n, extractClient as o, readEntryConfig as r, generate as t };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@supalive/codegen",
3
+ "version": "1.1.0",
4
+ "description": "Generate type-safe client code (Dart, and more) from a Supalive TypeScript router.",
5
+ "author": "Rebaz Raouf",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/rebaz94/supalive",
11
+ "directory": "packages/codegen"
12
+ },
13
+ "homepage": "https://github.com/rebaz94/supalive#readme",
14
+ "bugs": "https://github.com/rebaz94/supalive/issues",
15
+ "keywords": [
16
+ "supalive",
17
+ "codegen",
18
+ "dart",
19
+ "flutter",
20
+ "client",
21
+ "type-safe"
22
+ ],
23
+ "bin": {
24
+ "supalive-dart": "./dist/cli.js"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "dev": "node --import tsx ./src/cli.ts",
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
+ "dependencies": {
47
+ "ts-morph": "^24.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "tsdown": "^0.22.3",
51
+ "tsx": "^4.19.0",
52
+ "typescript": "^6.0.0",
53
+ "@types/node": "25.6.0"
54
+ }
55
+ }