zero-swift 0.3.0 → 0.4.1
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 +23 -0
- package/dist/bin.js +279 -68
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,6 +55,29 @@ Rename a row struct whose name clashes with a type you use. A table named
|
|
|
55
55
|
`list` generates `struct List`, which hides SwiftUI's `List`; set
|
|
56
56
|
`tables.list.type` to `NoteList`, or set a `typePrefix`.
|
|
57
57
|
|
|
58
|
+
### Names
|
|
59
|
+
|
|
60
|
+
Tables, columns, relationships, JSON fields and enumeration values keep
|
|
61
|
+
their names where Swift allows them, and keywords are backticked. Otherwise:
|
|
62
|
+
|
|
63
|
+
- Characters Swift doesn't allow in identifiers become `_` (`a-b` is
|
|
64
|
+
`a_b`), and a leading digit gets a `_`. A name with none it allows is `_`
|
|
65
|
+
and its code points in hex: the enumeration value `+` is `_2B`.
|
|
66
|
+
- A member that is taken, that the generated code declares (such as
|
|
67
|
+
`columnValues`, `hashValue`, `zeroValue` or `rawValue`), or that Swift
|
|
68
|
+
doesn't allow even backticked (`self`, `Type`, `Protocol`) gets `_`
|
|
69
|
+
appended until it is free: `self_`. Names that need no change are claimed
|
|
70
|
+
first, so with columns `a-b` and `a_b`, `a_b` keeps its name and `a-b` is
|
|
71
|
+
`a_b_`.
|
|
72
|
+
- A row struct name that is a keyword, taken, or a type the generated code
|
|
73
|
+
uses (such as `Zero`, `ZeroRow` or `Codable`) gets `Row`, then a number
|
|
74
|
+
from 2: `zero` is `ZeroRow2`.
|
|
75
|
+
|
|
76
|
+
A renamed member still reads and writes the original name: the column
|
|
77
|
+
names, `CodingKeys`, and enumeration raw values use it.
|
|
78
|
+
|
|
79
|
+
A recursive `json<T>()` type is `JSONValue` where it recurs.
|
|
80
|
+
|
|
58
81
|
Each config key has an option, such as `--schema-file`, `--out`,
|
|
59
82
|
`--type-prefix`, and `--access`, that overrides it. `--config` picks the
|
|
60
83
|
config file. Run `npx zero-swift codegen --help` for the full list.
|
package/dist/bin.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Cli, z } from "incur";
|
|
|
9
9
|
// package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "zero-swift",
|
|
12
|
-
version: "0.
|
|
12
|
+
version: "0.4.1",
|
|
13
13
|
description: "Generates the Swift row types and schema of a Zero app for the Zero Swift client.",
|
|
14
14
|
license: "Apache-2.0",
|
|
15
15
|
repository: {
|
|
@@ -95,36 +95,221 @@ var reservedTypeNames = /* @__PURE__ */ new Set([
|
|
|
95
95
|
"Int",
|
|
96
96
|
"Optional",
|
|
97
97
|
"Set",
|
|
98
|
-
"String"
|
|
98
|
+
"String",
|
|
99
|
+
// The protocols the generated types conform to, and the types of their
|
|
100
|
+
// members.
|
|
101
|
+
"AllCases",
|
|
102
|
+
"CaseIterable",
|
|
103
|
+
"Codable",
|
|
104
|
+
"CodingKey",
|
|
105
|
+
"CodingKeys",
|
|
106
|
+
"Decodable",
|
|
107
|
+
"Encodable",
|
|
108
|
+
"Equatable",
|
|
109
|
+
"Hashable",
|
|
110
|
+
"Hasher",
|
|
111
|
+
"Identifiable",
|
|
112
|
+
"KeyPath",
|
|
113
|
+
"PartialKeyPath",
|
|
114
|
+
"RawRepresentable",
|
|
115
|
+
"RawValue",
|
|
116
|
+
"Sendable"
|
|
99
117
|
]);
|
|
100
118
|
var swiftKeywords = new Set(
|
|
101
|
-
"associatedtype borrowing class consuming deinit enum extension fileprivate func import init inout internal let nonisolated open operator package private precedencegroup protocol public rethrows static struct subscript typealias var break case catch continue default defer do else fallthrough for guard if in repeat return throw switch where while Any as await false is nil self Self super throws true try".split(" ")
|
|
119
|
+
"associatedtype borrowing class consuming deinit enum extension fileprivate func import init inout internal let nonisolated open operator package private precedencegroup protocol public rethrows static struct subscript typealias var break case catch continue default defer do else fallthrough for guard if in repeat return throw switch where while Any as await false is nil self Self super throws true try _".split(" ")
|
|
102
120
|
);
|
|
103
|
-
var
|
|
104
|
-
var
|
|
121
|
+
var unusableMembers = /* @__PURE__ */ new Set(["self", "Type", "Protocol"]);
|
|
122
|
+
var rowMembers = [
|
|
123
|
+
"tableName",
|
|
124
|
+
"columns",
|
|
125
|
+
"relationships",
|
|
126
|
+
"columnValues",
|
|
127
|
+
"hashValue",
|
|
128
|
+
"CodingKeys",
|
|
129
|
+
// The parameter of `init(entry:)`, which assigns the columns.
|
|
130
|
+
"entry"
|
|
131
|
+
];
|
|
132
|
+
var objectMembers = [
|
|
133
|
+
"zeroValue",
|
|
134
|
+
"valueForNull",
|
|
135
|
+
"hashValue",
|
|
136
|
+
"CodingKeys",
|
|
137
|
+
// The local of `init(zeroValue:)` and `zeroValue`, which use the fields.
|
|
138
|
+
"o"
|
|
139
|
+
];
|
|
140
|
+
var enumMembers = [
|
|
141
|
+
"rawValue",
|
|
142
|
+
"RawValue",
|
|
143
|
+
"zeroValue",
|
|
144
|
+
"valueForNull",
|
|
145
|
+
"allCases",
|
|
146
|
+
"AllCases"
|
|
147
|
+
];
|
|
148
|
+
var asciiIdentifierChar = /[A-Za-z0-9_]/;
|
|
149
|
+
var unicodeIdentifierChar = /[\p{L}\p{M}\p{N}]/u;
|
|
105
150
|
var wordBoundary = /([a-z0-9])([A-Z])/g;
|
|
106
|
-
var
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
151
|
+
var identifierRanges = [
|
|
152
|
+
[168, 168],
|
|
153
|
+
[170, 170],
|
|
154
|
+
[173, 173],
|
|
155
|
+
[175, 175],
|
|
156
|
+
[178, 181],
|
|
157
|
+
[183, 186],
|
|
158
|
+
[188, 190],
|
|
159
|
+
[192, 214],
|
|
160
|
+
[216, 246],
|
|
161
|
+
[248, 767],
|
|
162
|
+
[768, 879],
|
|
163
|
+
[880, 5759],
|
|
164
|
+
[5761, 6157],
|
|
165
|
+
[6159, 7615],
|
|
166
|
+
[7616, 7679],
|
|
167
|
+
[7680, 8191],
|
|
168
|
+
[8304, 8591],
|
|
169
|
+
[9312, 9471],
|
|
170
|
+
[10102, 10131],
|
|
171
|
+
[11264, 11775],
|
|
172
|
+
[11904, 12287],
|
|
173
|
+
[12292, 12295],
|
|
174
|
+
[12321, 12335],
|
|
175
|
+
[12337, 12351],
|
|
176
|
+
[12352, 55295],
|
|
177
|
+
[63744, 64829],
|
|
178
|
+
[64832, 64975],
|
|
179
|
+
[65008, 65055],
|
|
180
|
+
[65056, 65071],
|
|
181
|
+
[65072, 65092],
|
|
182
|
+
[65095, 65533],
|
|
183
|
+
[65536, 983037]
|
|
184
|
+
];
|
|
185
|
+
var nonHeadRanges = [
|
|
186
|
+
[48, 57],
|
|
187
|
+
[768, 879],
|
|
188
|
+
[7616, 7679],
|
|
189
|
+
[8400, 8447],
|
|
190
|
+
[65056, 65071]
|
|
191
|
+
];
|
|
192
|
+
var inRanges = (c, ranges) => ranges.some(([lo, hi]) => c >= lo && c <= hi);
|
|
193
|
+
function isIdentifierChar(ch) {
|
|
194
|
+
const c = ch.codePointAt(0);
|
|
195
|
+
return c < 128 ? asciiIdentifierChar.test(ch) : unicodeIdentifierChar.test(ch) && inRanges(c, identifierRanges);
|
|
196
|
+
}
|
|
197
|
+
function isIdentifierHead(ch) {
|
|
198
|
+
return isIdentifierChar(ch) && !inRanges(ch.codePointAt(0), nonHeadRanges);
|
|
199
|
+
}
|
|
200
|
+
function sanitize(name) {
|
|
201
|
+
const chars = [...name];
|
|
202
|
+
if (!chars.some(isIdentifierChar)) {
|
|
203
|
+
return chars.length ? "_" + chars.map((ch) => ch.codePointAt(0).toString(16).toUpperCase()).join("_") : "_";
|
|
112
204
|
}
|
|
205
|
+
const id = chars.map((ch) => isIdentifierChar(ch) ? ch : "_");
|
|
206
|
+
return (isIdentifierHead(id[0]) ? "" : "_") + id.join("");
|
|
207
|
+
}
|
|
208
|
+
function ref(id) {
|
|
113
209
|
return swiftKeywords.has(id) ? "`" + id + "`" : id;
|
|
114
210
|
}
|
|
211
|
+
function identifier(name) {
|
|
212
|
+
return ref(sanitize(name));
|
|
213
|
+
}
|
|
214
|
+
var Members = class {
|
|
215
|
+
#used;
|
|
216
|
+
constructor(reserved = []) {
|
|
217
|
+
this.#used = new Set(reserved);
|
|
218
|
+
}
|
|
219
|
+
#free(id) {
|
|
220
|
+
return !this.#used.has(id) && !unusableMembers.has(id);
|
|
221
|
+
}
|
|
222
|
+
#available(id, wrapped) {
|
|
223
|
+
return this.#free(id) && (!wrapped || this.#free("_" + id));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A free member for `id`, not backticked. `wrapped` also claims the
|
|
227
|
+
* `_id` storage of a property wrapper.
|
|
228
|
+
*/
|
|
229
|
+
claim(id, wrapped = false) {
|
|
230
|
+
while (!this.#available(id, wrapped)) {
|
|
231
|
+
id += "_";
|
|
232
|
+
}
|
|
233
|
+
this.#used.add(id);
|
|
234
|
+
if (wrapped) {
|
|
235
|
+
this.#used.add("_" + id);
|
|
236
|
+
}
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Claims a member for each name, from `id`, its identifier. The names
|
|
241
|
+
* that are their identifier and free claim first, so a name that works
|
|
242
|
+
* as it is keeps it.
|
|
243
|
+
*/
|
|
244
|
+
claimAll(names) {
|
|
245
|
+
const out = [];
|
|
246
|
+
const keep = names.map(
|
|
247
|
+
(n) => n.id === n.name && this.#available(n.id, n.wrapped ?? false)
|
|
248
|
+
);
|
|
249
|
+
for (const pass of [true, false]) {
|
|
250
|
+
for (const [i, n] of names.entries()) {
|
|
251
|
+
if (keep[i] === pass) {
|
|
252
|
+
out[i] = this.claim(n.id, n.wrapped);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
115
259
|
function words(name) {
|
|
116
|
-
|
|
260
|
+
const out = [];
|
|
261
|
+
let word = "";
|
|
262
|
+
for (const ch of name.replace(wordBoundary, "$1 $2")) {
|
|
263
|
+
if (ch !== "_" && isIdentifierChar(ch)) {
|
|
264
|
+
word += ch;
|
|
265
|
+
} else if (word) {
|
|
266
|
+
out.push(word);
|
|
267
|
+
word = "";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return word ? [...out, word] : out;
|
|
271
|
+
}
|
|
272
|
+
function capitalize(w) {
|
|
273
|
+
const [first, ...rest] = w;
|
|
274
|
+
return first.toUpperCase() + rest.join("");
|
|
117
275
|
}
|
|
118
276
|
function pascalCase(name) {
|
|
119
|
-
const out = words(name).map(
|
|
120
|
-
|
|
277
|
+
const out = words(name).map(capitalize).join("");
|
|
278
|
+
if (out === "") {
|
|
279
|
+
return sanitize(name);
|
|
280
|
+
}
|
|
281
|
+
return isIdentifierHead([...out][0]) ? out : "_" + out;
|
|
121
282
|
}
|
|
122
283
|
function camelCase(name) {
|
|
123
284
|
const p = pascalCase(name);
|
|
124
|
-
|
|
285
|
+
if (p[0] === "_") {
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
const [first, ...rest] = p;
|
|
289
|
+
return first.toLowerCase() + rest.join("");
|
|
125
290
|
}
|
|
126
291
|
function str(s) {
|
|
127
|
-
|
|
292
|
+
let out = '"';
|
|
293
|
+
for (const ch of s) {
|
|
294
|
+
const c = ch.codePointAt(0);
|
|
295
|
+
if (ch === '"' || ch === "\\") {
|
|
296
|
+
out += "\\" + ch;
|
|
297
|
+
} else if (ch === "\n") {
|
|
298
|
+
out += "\\n";
|
|
299
|
+
} else if (ch === "\r") {
|
|
300
|
+
out += "\\r";
|
|
301
|
+
} else if (ch === " ") {
|
|
302
|
+
out += "\\t";
|
|
303
|
+
} else if (c < 32 || c === 127 || c >= 55296 && c <= 57343) {
|
|
304
|
+
out += `\\u{${(c >= 55296 && c <= 57343 ? 65533 : c).toString(16)}}`;
|
|
305
|
+
} else {
|
|
306
|
+
out += ch;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out + '"';
|
|
310
|
+
}
|
|
311
|
+
function typeTaken(name, used) {
|
|
312
|
+
return used.has(name) || reservedTypeNames.has(name) || swiftKeywords.has(name);
|
|
128
313
|
}
|
|
129
314
|
function strings(a) {
|
|
130
315
|
return `[${a.map(str).join(", ")}]`;
|
|
@@ -152,9 +337,13 @@ var Emitter = class {
|
|
|
152
337
|
get named() {
|
|
153
338
|
return this.#named;
|
|
154
339
|
}
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
340
|
+
/**
|
|
341
|
+
* A free type name, without the prefix: the alias, else `path` as an
|
|
342
|
+
* identifier, else that with a number.
|
|
343
|
+
*/
|
|
344
|
+
#claimBare(preferred, path) {
|
|
345
|
+
const fallback = sanitize(path);
|
|
346
|
+
const free = (name) => sanitize(name) === name && !typeTaken(this.#prefix + name, this.#names);
|
|
158
347
|
for (const name of [preferred, fallback]) {
|
|
159
348
|
if (name && free(name)) {
|
|
160
349
|
this.#names.add(this.#prefix + name);
|
|
@@ -206,12 +395,16 @@ var Emitter = class {
|
|
|
206
395
|
function generate(schema, customTypes, config, source) {
|
|
207
396
|
const tableNames = Object.keys(schema.tables);
|
|
208
397
|
const prefix = config.typePrefix ?? "";
|
|
398
|
+
const schemaName = config.schemaName ?? "app";
|
|
399
|
+
const zqlName = config.zql ?? "zql";
|
|
400
|
+
const zqlType = zqlName === "zql" ? "ZQL" : pascalCase(zqlName);
|
|
209
401
|
const rowNames = /* @__PURE__ */ new Map();
|
|
210
|
-
const used = /* @__PURE__ */ new Set();
|
|
402
|
+
const used = /* @__PURE__ */ new Set([zqlType]);
|
|
211
403
|
for (const t of tableNames) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
404
|
+
const preferred = config.tables?.[t]?.type ?? prefix + pascalCase(t);
|
|
405
|
+
let name = preferred;
|
|
406
|
+
for (let i = 1; typeTaken(name, used); i++) {
|
|
407
|
+
name = `${preferred}Row${i === 1 ? "" : i}`;
|
|
215
408
|
}
|
|
216
409
|
used.add(name);
|
|
217
410
|
rowNames.set(t, name);
|
|
@@ -219,9 +412,6 @@ function generate(schema, customTypes, config, source) {
|
|
|
219
412
|
const e = new Emitter(config.access ?? "public", prefix, used);
|
|
220
413
|
const bare = (name) => prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
221
414
|
const pub = e.access;
|
|
222
|
-
const schemaName = config.schemaName ?? "app";
|
|
223
|
-
const zqlName = config.zql ?? "zql";
|
|
224
|
-
const zqlType = zqlName === "zql" ? "ZQL" : pascalCase(zqlName);
|
|
225
415
|
const out = [
|
|
226
416
|
`// Generated by \`zero-swift codegen\` from ${source}. Do not edit.`,
|
|
227
417
|
"// swift-format-ignore-file",
|
|
@@ -252,21 +442,27 @@ function generate(schema, customTypes, config, source) {
|
|
|
252
442
|
}
|
|
253
443
|
const optional = c.optional === true;
|
|
254
444
|
const swiftType = optional && !base.endsWith("?") ? base + "?" : base;
|
|
255
|
-
|
|
445
|
+
const id = sanitize(name);
|
|
446
|
+
return { name, id, property: ref(id), swiftType, optional };
|
|
256
447
|
});
|
|
257
448
|
const relationships = Object.entries(schema.relationships[t] ?? {}).map(
|
|
258
449
|
([name, hops]) => {
|
|
259
450
|
const last = hops.at(-1);
|
|
260
451
|
const dest = rowNames.get(last.destSchema);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
dest,
|
|
265
|
-
one: last.cardinality === "one"
|
|
266
|
-
};
|
|
452
|
+
const one = last.cardinality === "one";
|
|
453
|
+
const id = sanitize(name);
|
|
454
|
+
return { name, id, property: ref(id), dest, one };
|
|
267
455
|
}
|
|
268
456
|
);
|
|
269
|
-
const
|
|
457
|
+
const props = [...columns, ...relationships];
|
|
458
|
+
const ids = new Members(rowMembers).claimAll(
|
|
459
|
+
props.map((p) => ({ ...p, wrapped: "one" in p && p.one }))
|
|
460
|
+
);
|
|
461
|
+
for (const [i, p] of props.entries()) {
|
|
462
|
+
p.id = ids[i];
|
|
463
|
+
p.property = ref(ids[i]);
|
|
464
|
+
}
|
|
465
|
+
const identifiable = table.primaryKey.length === 1 && table.primaryKey[0] === "id" && columns.some((c) => c.name === "id" && c.id === "id" && !c.optional);
|
|
270
466
|
const conformances = ["ZeroRow", "Codable", "Hashable"];
|
|
271
467
|
if (identifiable) {
|
|
272
468
|
conformances.push("Identifiable");
|
|
@@ -338,6 +534,7 @@ function generate(schema, customTypes, config, source) {
|
|
|
338
534
|
}
|
|
339
535
|
lines.push(" ]");
|
|
340
536
|
lines.push(" }");
|
|
537
|
+
lines.push(...codingKeys([...columns, ...relationships]));
|
|
341
538
|
lines.push("}");
|
|
342
539
|
typeDecls.push(lines.join("\n"));
|
|
343
540
|
}
|
|
@@ -357,11 +554,14 @@ function generate(schema, customTypes, config, source) {
|
|
|
357
554
|
);
|
|
358
555
|
out.push(`${pub}struct ${zqlType}: Sendable {`);
|
|
359
556
|
out.push(` ${pub}init() {}`);
|
|
360
|
-
|
|
557
|
+
const tableIds = new Members().claimAll(
|
|
558
|
+
tableNames.map((t) => ({ name: t, id: sanitize(t) }))
|
|
559
|
+
);
|
|
560
|
+
for (const [i, t] of tableNames.entries()) {
|
|
361
561
|
const row = rowNames.get(t);
|
|
362
562
|
out.push("");
|
|
363
563
|
out.push(
|
|
364
|
-
` ${pub}var ${
|
|
564
|
+
` ${pub}var ${ref(tableIds[i])}: Query<${row}, [${row}]> { Query(AnyQuery(.${schemaName}, ${str(t)})) }`
|
|
365
565
|
);
|
|
366
566
|
}
|
|
367
567
|
out.push("}");
|
|
@@ -370,28 +570,43 @@ function generate(schema, customTypes, config, source) {
|
|
|
370
570
|
out.push("");
|
|
371
571
|
return out.join("\n");
|
|
372
572
|
}
|
|
573
|
+
function codingKeys(props) {
|
|
574
|
+
if (props.every((p) => p.id === p.name)) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
return [
|
|
578
|
+
"",
|
|
579
|
+
" private enum CodingKeys: String, CodingKey {",
|
|
580
|
+
...props.map(
|
|
581
|
+
(p) => p.id === p.name ? ` case ${p.property}` : ` case ${p.property} = ${str(p.name)}`
|
|
582
|
+
),
|
|
583
|
+
" }"
|
|
584
|
+
];
|
|
585
|
+
}
|
|
373
586
|
function enumDecl(pub, n) {
|
|
374
|
-
const
|
|
587
|
+
const ids = new Members(enumMembers).claimAll(
|
|
588
|
+
n.cases.map((value) => ({ name: value, id: camelCase(value) }))
|
|
589
|
+
);
|
|
375
590
|
const lines = [
|
|
376
591
|
`${pub}enum ${n.name}: String, ZeroEnum, Codable, CaseIterable {`
|
|
377
592
|
];
|
|
378
|
-
for (const value of n.cases) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
c
|
|
382
|
-
|
|
383
|
-
used.add(c);
|
|
384
|
-
const id = identifier(c);
|
|
385
|
-
lines.push(c === value ? ` case ${id}` : ` case ${id} = ${str(value)}`);
|
|
593
|
+
for (const [i, value] of n.cases.entries()) {
|
|
594
|
+
const c = ids[i];
|
|
595
|
+
lines.push(
|
|
596
|
+
c === value ? ` case ${ref(c)}` : ` case ${ref(c)} = ${str(value)}`
|
|
597
|
+
);
|
|
386
598
|
}
|
|
387
599
|
lines.push("}");
|
|
388
600
|
return lines.join("\n");
|
|
389
601
|
}
|
|
390
602
|
function objectDecl(e, pub, n) {
|
|
391
|
-
const
|
|
603
|
+
const ids = new Members(objectMembers).claimAll(
|
|
604
|
+
n.fields.map((f) => ({ name: f.name, id: sanitize(f.name) }))
|
|
605
|
+
);
|
|
606
|
+
const fields = n.fields.map((f, i) => {
|
|
392
607
|
const base = e.swiftType(f.type, e.bare(n.name) + pascalCase(f.name));
|
|
393
608
|
const type = f.optional && !base.endsWith("?") ? base + "?" : base;
|
|
394
|
-
return { ...f, property:
|
|
609
|
+
return { ...f, id: ids[i], property: ref(ids[i]), swiftType: type };
|
|
395
610
|
});
|
|
396
611
|
const lines = [`${pub}struct ${n.name}: ZeroValue, Codable, Hashable {`];
|
|
397
612
|
for (const f of fields) {
|
|
@@ -426,6 +641,7 @@ function objectDecl(e, pub, n) {
|
|
|
426
641
|
}
|
|
427
642
|
lines.push(" return o.value");
|
|
428
643
|
lines.push(" }");
|
|
644
|
+
lines.push(...codingKeys(fields));
|
|
429
645
|
lines.push("}");
|
|
430
646
|
return lines.join("\n");
|
|
431
647
|
}
|
|
@@ -545,13 +761,17 @@ function property(checker, type, name) {
|
|
|
545
761
|
const p = checker.getPropertyOfType(type, name);
|
|
546
762
|
return p && checker.getTypeOfSymbol(p);
|
|
547
763
|
}
|
|
764
|
+
var maxDepth = 32;
|
|
548
765
|
var nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
|
|
549
|
-
function model(checker, type) {
|
|
766
|
+
function model(checker, type, outer = /* @__PURE__ */ new Set()) {
|
|
767
|
+
if (outer.has(type) || outer.size >= maxDepth) {
|
|
768
|
+
return { kind: "json" };
|
|
769
|
+
}
|
|
550
770
|
const alias = aliasName(type);
|
|
551
771
|
if (type.isUnion()) {
|
|
552
772
|
const parts = type.types.filter((t) => !(t.flags & nullish));
|
|
553
773
|
const nullable = parts.length !== type.types.length;
|
|
554
|
-
const inner = unionModel(checker, parts, alias);
|
|
774
|
+
const inner = unionModel(checker, parts, alias, outer);
|
|
555
775
|
return nullable ? { kind: "nullable", wrapped: inner } : inner;
|
|
556
776
|
}
|
|
557
777
|
if (type.isIntersection()) {
|
|
@@ -571,23 +791,24 @@ function model(checker, type) {
|
|
|
571
791
|
const [element] = checker.getTypeArguments(type);
|
|
572
792
|
return {
|
|
573
793
|
kind: "array",
|
|
574
|
-
element: element ? model(checker, element) : { kind: "json" }
|
|
794
|
+
element: element ? model(checker, element, outer) : { kind: "json" }
|
|
575
795
|
};
|
|
576
796
|
}
|
|
577
797
|
if (type.flags & ts.TypeFlags.Object && isPlainObject(checker, type)) {
|
|
798
|
+
const inside = new Set(outer).add(type);
|
|
578
799
|
const fields = checker.getPropertiesOfType(type).map((p) => {
|
|
579
800
|
const optional = (p.flags & ts.SymbolFlags.Optional) !== 0;
|
|
580
801
|
let t = checker.getTypeOfSymbol(p);
|
|
581
802
|
if (optional) {
|
|
582
803
|
t = checker.getNonNullableType(t);
|
|
583
804
|
}
|
|
584
|
-
return { name: p.name, type: model(checker, t), optional };
|
|
805
|
+
return { name: p.name, type: model(checker, t, inside), optional };
|
|
585
806
|
});
|
|
586
807
|
return { kind: "object", ...alias ? { alias } : {}, fields };
|
|
587
808
|
}
|
|
588
809
|
return { kind: "json" };
|
|
589
810
|
}
|
|
590
|
-
function unionModel(checker, parts, alias) {
|
|
811
|
+
function unionModel(checker, parts, alias, outer) {
|
|
591
812
|
if (parts.length === 0) return { kind: "json" };
|
|
592
813
|
if (parts.every((t) => t.isStringLiteral())) {
|
|
593
814
|
return enumModel(
|
|
@@ -601,7 +822,7 @@ function unionModel(checker, parts, alias) {
|
|
|
601
822
|
if (parts.every((t) => t.flags & ts.TypeFlags.NumberLike)) {
|
|
602
823
|
return { kind: "number" };
|
|
603
824
|
}
|
|
604
|
-
if (parts.length === 1) return model(checker, parts[0]);
|
|
825
|
+
if (parts.length === 1) return model(checker, parts[0], outer);
|
|
605
826
|
return { kind: "json" };
|
|
606
827
|
}
|
|
607
828
|
function enumModel(cases, alias) {
|
|
@@ -707,7 +928,7 @@ var cli = Cli.create("zero-swift", {
|
|
|
707
928
|
version: package_default.version,
|
|
708
929
|
description: "Generates what the Zero Swift client needs from a Zero app's TypeScript schema, queries, and mutators."
|
|
709
930
|
}).command("codegen", {
|
|
710
|
-
description: "Generate the Swift row types, schema, and query builder from the app's TypeScript schema. Options override zero-swift.json, which is read when present.",
|
|
931
|
+
description: "Generate the Swift row types, schema, and query builder from the app's TypeScript schema. Options override zero-swift.json, which is read when present. See swift/Sources/Zero/Zero.docc/GettingStarted.md in github.com/wevm/zero.",
|
|
711
932
|
options: z.object({
|
|
712
933
|
config: z.string().optional().describe("Config file (default zero-swift.json, when present)"),
|
|
713
934
|
schemaFile: z.string().optional().describe("TS schema module (default schema.ts)"),
|
|
@@ -719,20 +940,10 @@ var cli = Cli.create("zero-swift", {
|
|
|
719
940
|
access: z.enum(["public", "internal"]).optional().describe("Access level of the generated code (default public)"),
|
|
720
941
|
check
|
|
721
942
|
}),
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
schemaFile: "shared/schema.ts",
|
|
727
|
-
out: "Sources/App/Schema.swift"
|
|
728
|
-
},
|
|
729
|
-
description: "Generate without a config file"
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
options: { check: true },
|
|
733
|
-
description: "Fail in CI when the output is stale"
|
|
734
|
-
}
|
|
735
|
-
],
|
|
943
|
+
// incur 0.7.1 prints example options as `--camelKey value`, so the hint
|
|
944
|
+
// shows `--schema-file` and a bare `--check` instead.
|
|
945
|
+
examples: [{ description: "Generate from zero-swift.json" }],
|
|
946
|
+
hint: "Without a config file: zero-swift codegen --schema-file shared/schema.ts --out Sources/App/Schema.swift. In CI, zero-swift codegen --check fails when the output is stale.",
|
|
736
947
|
run: (c) => reporting(c.error, async () => {
|
|
737
948
|
const cwd = process.cwd();
|
|
738
949
|
const {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-swift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Generates the Swift row types and schema of a Zero app for the Zero Swift client.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"oxfmt": "^0.74.0",
|
|
26
26
|
"vitest": "^5.0.0",
|
|
27
27
|
"@rocicorp/zero": "1.12.0",
|
|
28
|
+
"zero-schema": "0.0.0",
|
|
28
29
|
"zero-types": "0.0.0",
|
|
29
|
-
"zql": "0.0.0"
|
|
30
|
-
"zero-schema": "0.0.0"
|
|
30
|
+
"zql": "0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@rocicorp/zero": ">=1.11.0"
|