@syncular/typegen 0.1.3 → 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.
Files changed (99) hide show
  1. package/README.md +436 -94
  2. package/dist/cli.d.ts +1 -2
  3. package/dist/cli.js +159 -73
  4. package/dist/emit-dart.d.ts +17 -0
  5. package/dist/emit-dart.js +201 -0
  6. package/dist/emit-kotlin.d.ts +16 -0
  7. package/dist/emit-kotlin.js +201 -0
  8. package/dist/emit-queries-dart.d.ts +2 -0
  9. package/dist/emit-queries-dart.js +160 -0
  10. package/dist/emit-queries-kotlin.d.ts +2 -0
  11. package/dist/emit-queries-kotlin.js +162 -0
  12. package/dist/emit-queries-swift.d.ts +2 -0
  13. package/dist/emit-queries-swift.js +146 -0
  14. package/dist/emit-queries.d.ts +2 -0
  15. package/dist/emit-queries.js +129 -0
  16. package/dist/emit-swift.d.ts +18 -0
  17. package/dist/emit-swift.js +235 -0
  18. package/dist/emit.d.ts +16 -0
  19. package/dist/emit.js +191 -0
  20. package/dist/errors.d.ts +6 -0
  21. package/dist/errors.js +10 -0
  22. package/dist/generate.d.ts +53 -18
  23. package/dist/generate.js +391 -77
  24. package/dist/index.d.ts +17 -14
  25. package/dist/index.js +16 -13
  26. package/dist/init.d.ts +12 -0
  27. package/dist/init.js +85 -0
  28. package/dist/ir.d.ts +90 -0
  29. package/dist/ir.js +94 -0
  30. package/dist/manifest.d.ts +76 -0
  31. package/dist/manifest.js +303 -0
  32. package/dist/query.d.ts +96 -0
  33. package/dist/query.js +719 -0
  34. package/dist/sql.d.ts +13 -0
  35. package/dist/sql.js +440 -0
  36. package/package.json +14 -34
  37. package/src/cli.ts +161 -82
  38. package/src/emit-dart.ts +245 -0
  39. package/src/emit-kotlin.ts +257 -0
  40. package/src/emit-queries-dart.ts +203 -0
  41. package/src/emit-queries-kotlin.ts +209 -0
  42. package/src/emit-queries-swift.ts +198 -0
  43. package/src/emit-queries.ts +183 -0
  44. package/src/emit-swift.ts +295 -0
  45. package/src/emit.ts +239 -0
  46. package/src/errors.ts +11 -0
  47. package/src/generate.ts +574 -105
  48. package/src/index.ts +16 -13
  49. package/src/init.ts +100 -0
  50. package/src/ir.ts +197 -0
  51. package/src/manifest.ts +445 -0
  52. package/src/query.ts +918 -0
  53. package/src/sql.ts +533 -0
  54. package/dist/app-contract.d.ts +0 -154
  55. package/dist/app-contract.d.ts.map +0 -1
  56. package/dist/app-contract.js +0 -250
  57. package/dist/app-contract.js.map +0 -1
  58. package/dist/checksums.d.ts +0 -6
  59. package/dist/checksums.d.ts.map +0 -1
  60. package/dist/checksums.js +0 -173
  61. package/dist/checksums.js.map +0 -1
  62. package/dist/cli.d.ts.map +0 -1
  63. package/dist/cli.js.map +0 -1
  64. package/dist/generate.d.ts.map +0 -1
  65. package/dist/generate.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/introspect-postgres.d.ts +0 -8
  69. package/dist/introspect-postgres.d.ts.map +0 -1
  70. package/dist/introspect-postgres.js +0 -94
  71. package/dist/introspect-postgres.js.map +0 -1
  72. package/dist/introspect-sqlite.d.ts +0 -10
  73. package/dist/introspect-sqlite.d.ts.map +0 -1
  74. package/dist/introspect-sqlite.js +0 -97
  75. package/dist/introspect-sqlite.js.map +0 -1
  76. package/dist/introspect.d.ts +0 -8
  77. package/dist/introspect.d.ts.map +0 -1
  78. package/dist/introspect.js +0 -18
  79. package/dist/introspect.js.map +0 -1
  80. package/dist/map-types.d.ts +0 -20
  81. package/dist/map-types.d.ts.map +0 -1
  82. package/dist/map-types.js +0 -151
  83. package/dist/map-types.js.map +0 -1
  84. package/dist/render.d.ts +0 -23
  85. package/dist/render.d.ts.map +0 -1
  86. package/dist/render.js +0 -140
  87. package/dist/render.js.map +0 -1
  88. package/dist/types.d.ts +0 -122
  89. package/dist/types.d.ts.map +0 -1
  90. package/dist/types.js +0 -5
  91. package/dist/types.js.map +0 -1
  92. package/src/app-contract.ts +0 -531
  93. package/src/checksums.ts +0 -257
  94. package/src/introspect-postgres.ts +0 -149
  95. package/src/introspect-sqlite.ts +0 -156
  96. package/src/introspect.ts +0 -36
  97. package/src/map-types.ts +0 -189
  98. package/src/render.ts +0 -196
  99. package/src/types.ts +0 -137
package/dist/cli.js CHANGED
@@ -1,88 +1,174 @@
1
1
  #!/usr/bin/env bun
2
- import { readFile } from 'node:fs/promises';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { loadSyncularClientContract, toSyncularCodegenJson, writeSyncularCodegenJsonFromModule, } from './app-contract.js';
6
- async function main(argv) {
7
- const command = argv[0];
8
- if (command !== 'codegen-config') {
9
- printUsageAndExit(command ? `Unknown command ${command}` : undefined);
10
- }
11
- const options = parseCodegenConfigArgs(argv.slice(1));
12
- if (options.check) {
13
- const contract = await loadSyncularClientContract({
14
- modulePath: options.app,
15
- exportName: options.exportName,
16
- });
17
- const expected = toSyncularCodegenJson(contract);
18
- const actual = await readFile(options.out, 'utf8').catch((error) => {
19
- throw new Error(`Cannot read ${options.out}; run without --check to generate it first: ${error.message}`);
20
- });
21
- if (actual !== expected) {
22
- throw new Error(`${options.out} does not match ${options.app}; run syncular generate --app ${options.app}`);
23
- }
24
- return;
25
- }
26
- await writeSyncularCodegenJsonFromModule({
27
- modulePath: options.app,
28
- exportName: options.exportName,
29
- outputPath: options.out,
30
- });
2
+ /**
3
+ * The `syncular` CLI. Subcommands:
4
+ *
5
+ * generate [--manifest-dir <dir>] [--check] [--watch]
6
+ * Reads `syncular.json` + `migrations/` under the manifest dir and writes
7
+ * the neutral IR JSON and generated TS module (paths from the manifest's
8
+ * `output`). `--check` regenerates in memory and fails (exit 1) unless the
9
+ * files on disk match byte-exactly. `--watch` regenerates on change.
10
+ *
11
+ * init [--manifest-dir <dir>]
12
+ * Drops a starter `syncular.json` + `migrations/0001_initial` into an
13
+ * existing project (the "add syncular to my app" path).
14
+ *
15
+ * The generate CONTRACT (inputs, outputs, byte-exact `--check`) is unchanged;
16
+ * this file only adds `init`, `--watch`, and friendlier errors.
17
+ */
18
+ import { watch } from 'node:fs';
19
+ import { resolve } from 'node:path';
20
+ import { checkOutputs, generate, writeOutputs } from './generate.js';
21
+ import { InitError, initProject } from './init.js';
22
+ import { MANIFEST_FILENAME } from './manifest.js';
23
+ const DOCS_HINT = 'See the schema guide: https://github.com/bkniffler/syncular (guide-schema), ' +
24
+ 'or run `syncular init` to scaffold a starter manifest + migration.';
25
+ const USAGE = `usage: syncular <command> [options]
26
+
27
+ commands:
28
+ generate build the IR + typed module from syncular.json + migrations/
29
+ init scaffold a starter syncular.json + migrations/0001_initial
30
+
31
+ generate options:
32
+ --manifest-dir <dir> directory holding syncular.json (default: .)
33
+ --check fail unless generated files are byte-exactly fresh
34
+ --watch regenerate on file change (Ctrl-C to stop)
35
+
36
+ init options:
37
+ --manifest-dir <dir> directory to scaffold into (default: .)`;
38
+ function fail(message) {
39
+ console.error(message);
40
+ process.exit(1);
31
41
  }
32
- function parseCodegenConfigArgs(args) {
33
- let app;
34
- let out;
35
- let exportName;
36
- let check = false;
37
- for (let index = 0; index < args.length; index += 1) {
38
- const arg = args[index];
39
- if (arg === '--app') {
40
- app = requireValue(args, (index += 1), '--app');
42
+ function parseGenerateArgs(argv) {
43
+ const args = { manifestDir: '.', check: false, watch: false };
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const arg = argv[i];
46
+ if (arg === '--check') {
47
+ args.check = true;
41
48
  }
42
- else if (arg === '--out') {
43
- out = requireValue(args, (index += 1), '--out');
49
+ else if (arg === '--watch') {
50
+ args.watch = true;
44
51
  }
45
- else if (arg === '--export') {
46
- exportName = requireValue(args, (index += 1), '--export');
52
+ else if (arg === '--manifest-dir') {
53
+ const value = argv[i + 1];
54
+ if (value === undefined)
55
+ fail('--manifest-dir requires a value');
56
+ args.manifestDir = value;
57
+ i += 1;
47
58
  }
48
- else if (arg === '--check') {
49
- check = true;
59
+ else {
60
+ fail(`unknown argument ${JSON.stringify(arg)}\n${USAGE}`);
50
61
  }
51
- else if (arg === '--help' || arg === '-h') {
52
- printUsageAndExit();
62
+ }
63
+ if (args.check && args.watch) {
64
+ fail('--check and --watch cannot be combined');
65
+ }
66
+ return args;
67
+ }
68
+ function parseManifestDir(argv) {
69
+ let manifestDir = '.';
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const arg = argv[i];
72
+ if (arg === '--manifest-dir') {
73
+ const value = argv[i + 1];
74
+ if (value === undefined)
75
+ fail('--manifest-dir requires a value');
76
+ manifestDir = value;
77
+ i += 1;
53
78
  }
54
79
  else {
55
- printUsageAndExit(`Unknown option ${arg}`);
80
+ fail(`unknown argument ${JSON.stringify(arg)}\n${USAGE}`);
56
81
  }
57
82
  }
58
- if (!app)
59
- printUsageAndExit('Missing --app');
60
- return {
61
- app,
62
- out: out ?? defaultCodegenConfigPath(app),
63
- exportName,
64
- check,
65
- };
83
+ return manifestDir;
66
84
  }
67
- function defaultCodegenConfigPath(app) {
68
- const appPath = app.startsWith('file:') ? fileURLToPath(app) : app;
69
- return join(dirname(appPath), 'generated', 'syncular.codegen.json');
85
+ /** Add a docs pointer to the fail-loud missing-input errors. */
86
+ function friendlyGenerateError(error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ if (message.includes('manifest not found') ||
89
+ message.includes('migrations directory does not exist') ||
90
+ message.includes('no migrations found')) {
91
+ return `${message}\n${DOCS_HINT}`;
92
+ }
93
+ return message;
70
94
  }
71
- function requireValue(args, index, option) {
72
- const value = args[index];
73
- if (!value || value.startsWith('--')) {
74
- printUsageAndExit(`${option} requires a value`);
95
+ function runGenerateOnce(args) {
96
+ let result;
97
+ try {
98
+ result = generate(args.manifestDir);
99
+ }
100
+ catch (error) {
101
+ fail(friendlyGenerateError(error));
102
+ }
103
+ if (args.check) {
104
+ const drift = checkOutputs(result);
105
+ if (drift.length > 0) {
106
+ fail(`generated output is out of date:\n${drift.join('\n')}`);
107
+ }
108
+ console.log('generated output is up to date');
109
+ return;
110
+ }
111
+ writeOutputs(result);
112
+ for (const output of result.outputs) {
113
+ console.log(`wrote ${output.path}`);
114
+ }
115
+ }
116
+ /** `--watch`: regenerate on any change under the manifest dir. Uses Bun's
117
+ * `fs.watch` recursively; debounced so a burst of writes triggers one run. */
118
+ function runGenerateWatch(args) {
119
+ const dir = resolve(args.manifestDir);
120
+ const safeRun = () => {
121
+ try {
122
+ const result = generate(args.manifestDir);
123
+ // Skip the write when the outputs are already fresh — otherwise writing
124
+ // them (they live under the watched dir) would re-trigger the watcher in
125
+ // a loop.
126
+ if (checkOutputs(result).length === 0)
127
+ return;
128
+ writeOutputs(result);
129
+ console.log(`regenerated ${result.modulePath}`);
130
+ }
131
+ catch (error) {
132
+ // In watch mode a bad intermediate state is expected — report, don't die.
133
+ console.error(friendlyGenerateError(error));
134
+ }
135
+ };
136
+ safeRun();
137
+ console.log(`watching ${dir} for changes (Ctrl-C to stop)…`);
138
+ let timer;
139
+ watch(dir, { recursive: true }, () => {
140
+ if (timer !== undefined)
141
+ clearTimeout(timer);
142
+ timer = setTimeout(safeRun, 50);
143
+ });
144
+ }
145
+ export function runCli(argv) {
146
+ const command = argv[0];
147
+ if (command === 'generate') {
148
+ const args = parseGenerateArgs(argv.slice(1));
149
+ if (args.watch) {
150
+ runGenerateWatch(args);
151
+ return;
152
+ }
153
+ runGenerateOnce(args);
154
+ return;
155
+ }
156
+ if (command === 'init') {
157
+ const dir = parseManifestDir(argv.slice(1));
158
+ try {
159
+ const result = initProject(dir);
160
+ for (const path of result.written)
161
+ console.log(`wrote ${path}`);
162
+ console.log('\nNext: `syncular generate` to build the typed schema, then wire ' +
163
+ `${MANIFEST_FILENAME}'s "output.module" into your server + client.`);
164
+ }
165
+ catch (error) {
166
+ fail(error instanceof InitError ? error.message : String(error));
167
+ }
168
+ return;
75
169
  }
76
- return value;
170
+ fail(USAGE);
77
171
  }
78
- function printUsageAndExit(message) {
79
- if (message)
80
- console.error(message);
81
- console.error([
82
- 'Usage:',
83
- ' syncular-typegen codegen-config --app ./syncular.app.ts [--out ./generated/syncular.codegen.json] [--export app] [--check]',
84
- ].join('\n'));
85
- process.exit(message ? 1 : 0);
172
+ if (import.meta.main) {
173
+ runCli(process.argv.slice(2));
86
174
  }
87
- await main(process.argv.slice(2));
88
- //# sourceMappingURL=cli.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Dart emitter: neutral IR → one standalone `.dart` file exporting
3
+ *
4
+ * - `const syncularSchema` — the ServerSchema-compatible `Map<String,
5
+ * Object?>` built from the IR (fed straight into `SyncularClient.create(
6
+ * schema: …)`),
7
+ * - one class per table with typed fields (per the six §2.4 column types +
8
+ * blob_ref/crdt) and a `fromRow(Map<String, Object?>)` factory,
9
+ * - a `syncular<Name>Subscription` helper per subscription with a typed
10
+ * `scopes(...)` builder.
11
+ *
12
+ * The row shape is `Map<String, Object?>` — the exact type the Dart wrapper's
13
+ * `query`/`readRows` return. The header carries the IR hash so `--check`
14
+ * verifies freshness byte-exactly.
15
+ */
16
+ import type { IrDocument } from './ir.js';
17
+ export declare function emitDartModule(ir: IrDocument, hash: string): string;
@@ -0,0 +1,201 @@
1
+ /** §2.4 column type → honest Dart type. `json`/`blob_ref` are the raw
2
+ * canonical JSON string; `bytes`/`crdt` are opaque `List<int>`. */
3
+ const DART_TYPE = {
4
+ string: 'String',
5
+ integer: 'int',
6
+ float: 'double',
7
+ boolean: 'bool',
8
+ json: 'String',
9
+ bytes: 'List<int>',
10
+ blob_ref: 'String',
11
+ crdt: 'List<int>',
12
+ };
13
+ /** §5.11: app-side Dart type — declaredType for an encrypted column. */
14
+ function appDartType(column) {
15
+ const type = column.encrypted === true && column.declaredType !== undefined
16
+ ? column.declaredType
17
+ : column.type;
18
+ return DART_TYPE[type];
19
+ }
20
+ function pascalCase(name) {
21
+ return name
22
+ .split(/[_-]+/)
23
+ .filter((part) => part.length > 0)
24
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
25
+ .join('');
26
+ }
27
+ function camelCase(name) {
28
+ const pascal = pascalCase(name);
29
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
30
+ }
31
+ function quote(value) {
32
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\$/g, '\\$')}'`;
33
+ }
34
+ function emitSchemaValue(ir, indent) {
35
+ const lines = [];
36
+ const i = indent;
37
+ lines.push(`${i}<String, Object?>{`);
38
+ lines.push(`${i} 'version': ${ir.schemaVersion},`);
39
+ lines.push(`${i} 'tables': [`);
40
+ for (const table of ir.tables) {
41
+ lines.push(`${i} {`);
42
+ lines.push(`${i} 'name': ${quote(table.name)},`);
43
+ lines.push(`${i} 'primaryKey': ${quote(table.primaryKey)},`);
44
+ lines.push(`${i} 'columns': [`);
45
+ for (const column of table.columns) {
46
+ const parts = [
47
+ `'name': ${quote(column.name)}`,
48
+ `'type': ${quote(column.type)}`,
49
+ `'nullable': ${column.nullable}`,
50
+ ];
51
+ if (column.crdtType !== undefined) {
52
+ parts.push(`'crdtType': ${quote(column.crdtType)}`);
53
+ }
54
+ if (column.encrypted === true) {
55
+ parts.push("'encrypted': true");
56
+ parts.push(`'declaredType': ${quote(column.declaredType ?? column.type)}`);
57
+ }
58
+ lines.push(`${i} {${parts.join(', ')}},`);
59
+ }
60
+ lines.push(`${i} ],`);
61
+ lines.push(`${i} 'scopes': [`);
62
+ for (const scope of table.scopes) {
63
+ lines.push(`${i} {'pattern': ${quote(scope.pattern)}, 'column': ${quote(scope.column)}},`);
64
+ }
65
+ lines.push(`${i} ],`);
66
+ lines.push(`${i} },`);
67
+ }
68
+ lines.push(`${i} ],`);
69
+ lines.push(`${i}}`);
70
+ return lines;
71
+ }
72
+ /** The accessor lifting a §2.4 value out of a `Map<String, Object?>` row. */
73
+ function rowAccessor(column) {
74
+ const key = `row[${quote(column.name)}]`;
75
+ switch (column.type) {
76
+ case 'string':
77
+ case 'json':
78
+ case 'blob_ref':
79
+ return `${key} as String?`;
80
+ case 'integer':
81
+ return `(${key} as num?)?.toInt()`;
82
+ case 'float':
83
+ return `(${key} as num?)?.toDouble()`;
84
+ case 'boolean':
85
+ return `_rowBool(${key})`;
86
+ case 'bytes':
87
+ case 'crdt':
88
+ return `_rowBytes(${key})`;
89
+ }
90
+ }
91
+ function emitClass(table) {
92
+ const type = pascalCase(table.name);
93
+ const lines = [];
94
+ lines.push(`/// One ${table.name} row (§2.4 column order).`);
95
+ lines.push(`class ${type} {`);
96
+ for (const column of table.columns) {
97
+ const dart = appDartType(column);
98
+ const opt = column.nullable ? '?' : '';
99
+ lines.push(` final ${dart}${opt} ${camelCase(column.name)};`);
100
+ }
101
+ lines.push('');
102
+ const ctorParams = table.columns
103
+ .map((c) => `${c.nullable ? '' : 'required '}this.${camelCase(c.name)}`)
104
+ .join(', ');
105
+ lines.push(` const ${type}({${ctorParams}});`);
106
+ lines.push('');
107
+ lines.push(` /// Build from a \`query\`/\`readRows\` row. Returns null when a`);
108
+ lines.push(` /// non-nullable column is missing or mistyped.`);
109
+ lines.push(` static ${type}? fromRow(Map<String, Object?> row) {`);
110
+ for (const column of table.columns) {
111
+ if (!column.nullable) {
112
+ const name = camelCase(column.name);
113
+ lines.push(` final ${name} = ${rowAccessor(column)};`);
114
+ lines.push(` if (${name} == null) return null;`);
115
+ }
116
+ }
117
+ const args = table.columns
118
+ .map((c) => {
119
+ const name = camelCase(c.name);
120
+ return c.nullable ? `${name}: ${rowAccessor(c)}` : `${name}: ${name}`;
121
+ })
122
+ .join(', ');
123
+ lines.push(` return ${type}(${args});`);
124
+ lines.push(' }');
125
+ lines.push('}');
126
+ return lines;
127
+ }
128
+ function emitSubscription(sub) {
129
+ const params = [];
130
+ for (const scope of sub.scopes) {
131
+ for (const value of scope.values) {
132
+ if (value.kind === 'parameter' && !params.includes(value.name)) {
133
+ params.push(value.name);
134
+ }
135
+ }
136
+ }
137
+ const lines = [];
138
+ lines.push(`/// Requested-scope template for the ${quote(sub.name)} subscription.`);
139
+ lines.push(`class Syncular${pascalCase(sub.name)}Subscription {`);
140
+ lines.push(` static const String name = ${quote(sub.name)};`);
141
+ lines.push(` static const String table = ${quote(sub.table)};`);
142
+ const args = params.map((p) => `required String ${camelCase(p)}`).join(', ');
143
+ const argsClause = args.length > 0 ? `{${args}}` : '';
144
+ lines.push(` static Map<String, List<String>> scopes(${argsClause}) => {`);
145
+ for (const scope of sub.scopes) {
146
+ const values = scope.values
147
+ .map((value) => value.kind === 'literal' ? quote(value.value) : camelCase(value.name))
148
+ .join(', ');
149
+ lines.push(` ${quote(scope.variable)}: [${values}],`);
150
+ }
151
+ lines.push(' };');
152
+ lines.push('}');
153
+ return lines;
154
+ }
155
+ export function emitDartModule(ir, hash) {
156
+ const parts = [];
157
+ parts.push([
158
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
159
+ `// irVersion: ${ir.irVersion}`,
160
+ `// irHash: ${hash}`,
161
+ '// ignore_for_file: type=lint',
162
+ ].join('\n'));
163
+ // The schema constant.
164
+ const schemaLines = [];
165
+ schemaLines.push('/// ServerSchema-compatible schema (SPEC §2.4, §3.1) —');
166
+ schemaLines.push('/// pass to `SyncularClient.create(schema: …)`.');
167
+ const schemaValue = emitSchemaValue(ir, '');
168
+ schemaValue[0] = `const Map<String, Object?> syncularSchema = ${schemaValue[0]}`;
169
+ schemaValue[schemaValue.length - 1] =
170
+ `${schemaValue[schemaValue.length - 1]};`;
171
+ for (const line of schemaValue)
172
+ schemaLines.push(line);
173
+ parts.push(schemaLines.join('\n'));
174
+ // Shared row-decode helpers.
175
+ parts.push([
176
+ '/// Lift a SQLite boolean: a real bool, or 0/1 as a number.',
177
+ 'bool? _rowBool(Object? value) {',
178
+ ' if (value is bool) return value;',
179
+ ' if (value is num) return value != 0;',
180
+ ' return null;',
181
+ '}',
182
+ '',
183
+ "/// Decode the core's {'\\$bytes': '<hex>'} marshaling (bytes as hex).",
184
+ 'List<int>? _rowBytes(Object? value) {',
185
+ ' if (value is! Map) return null;',
186
+ " final hex = value[r'$bytes'];",
187
+ ' if (hex is! String || hex.length % 2 != 0) return null;',
188
+ ' return [',
189
+ ' for (var i = 0; i < hex.length; i += 2)',
190
+ ' int.parse(hex.substring(i, i + 2), radix: 16),',
191
+ ' ];',
192
+ '}',
193
+ ].join('\n'));
194
+ for (const table of ir.tables) {
195
+ parts.push(emitClass(table).join('\n'));
196
+ }
197
+ for (const sub of ir.subscriptions) {
198
+ parts.push(emitSubscription(sub).join('\n'));
199
+ }
200
+ return `${parts.join('\n\n')}\n`;
201
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Kotlin emitter: neutral IR → one standalone `.kt` file exporting
3
+ *
4
+ * - `object <ObjectName> { val schema: JsonValue }` — the ServerSchema
5
+ * object built from the IR (fed straight into `SyncularClient.create(
6
+ * schema = …)`),
7
+ * - one `data class` per table with typed properties (per the six §2.4 column
8
+ * types + blob_ref/crdt) and a `fromRow(row: JsonValue)` companion factory,
9
+ * - a `Subscriptions` object of typed requested-scope builders.
10
+ *
11
+ * The row shape is `JsonValue` — the exact type the Kotlin wrapper's
12
+ * `query`/`readRows` return (each row a `JsonValue.Obj`). The header carries
13
+ * the IR hash so `--check` verifies freshness byte-exactly.
14
+ */
15
+ import type { IrDocument } from './ir.js';
16
+ export declare function emitKotlinModule(ir: IrDocument, hash: string, packageName: string, objectName: string): string;