zero-swift 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/bin.js +216 -52
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ truth, and this tool generates what the Swift client needs from them.
|
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install --save-dev zero-swift
|
|
9
|
+
npx zero-swift --help
|
|
9
10
|
```
|
|
10
11
|
|
|
11
12
|
It needs Node 22.18 or later, which loads TypeScript files directly. It also
|
|
@@ -36,7 +37,28 @@ npx zero-swift codegen --check # fails when the output is out of date
|
|
|
36
37
|
`Int`; it must then hold integers. For a `json<T>()` column whose `T` is a
|
|
37
38
|
plain object or a literal union, the tool generates a `Codable` type.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
Every key is optional:
|
|
41
|
+
|
|
42
|
+
| Key | Default | Meaning |
|
|
43
|
+
| ----------------------------- | ------------------- | ----------------------------------------- |
|
|
44
|
+
| `schema` | `schema.ts` | The TypeScript schema module |
|
|
45
|
+
| `export` | `schema` | The module export that holds the schema |
|
|
46
|
+
| `output` | `Schema.swift` | The Swift file to write |
|
|
47
|
+
| `schemaName` | `app` | The `ZeroSchema` static, `ZeroSchema.app` |
|
|
48
|
+
| `zql` | `zql` | The global query builder, of type `ZQL` |
|
|
49
|
+
| `typePrefix` | none | Prepended to every generated type name |
|
|
50
|
+
| `access` | `public` | `public` or `internal` |
|
|
51
|
+
| `tables.<t>.type` | `<t>` in PascalCase | The row struct's name |
|
|
52
|
+
| `tables.<t>.columns.<c>.type` | from the schema | `int` or `double` |
|
|
53
|
+
|
|
54
|
+
Rename a row struct whose name clashes with a type you use. A table named
|
|
55
|
+
`list` generates `struct List`, which hides SwiftUI's `List`; set
|
|
56
|
+
`tables.list.type` to `NoteList`, or set a `typePrefix`.
|
|
57
|
+
|
|
58
|
+
Each config key has an option, such as `--schema-file`, `--out`,
|
|
59
|
+
`--type-prefix`, and `--access`, that overrides it. `--config` picks the
|
|
60
|
+
config file. Run `npx zero-swift codegen --help` for the full list.
|
|
61
|
+
`--schema <path>` from 0.1.0 still works, as `--schema-file`.
|
|
40
62
|
|
|
41
63
|
## `zero-swift drift record`
|
|
42
64
|
|
|
@@ -59,3 +81,11 @@ The samples module's default export holds:
|
|
|
59
81
|
|
|
60
82
|
Record again whenever either side changes. `--check` fails when the recorded
|
|
61
83
|
file is out of date.
|
|
84
|
+
|
|
85
|
+
## Output and agents
|
|
86
|
+
|
|
87
|
+
The CLI is built on [incur](https://github.com/wevm/incur). Errors print a
|
|
88
|
+
code and a message and exit 1, with no stack trace. `--json` (or `--format`)
|
|
89
|
+
prints the result as JSON, for scripts and CI. `--llms` prints a manifest of
|
|
90
|
+
the commands for a coding agent, and `npx zero-swift skills add` installs
|
|
91
|
+
them as an agent skill.
|
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,62 @@
|
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { relative, resolve as resolve2 } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { Cli, z } from "incur";
|
|
8
|
+
|
|
9
|
+
// package.json
|
|
10
|
+
var package_default = {
|
|
11
|
+
name: "zero-swift",
|
|
12
|
+
version: "0.2.0",
|
|
13
|
+
description: "Generates the Swift row types and schema of a Zero app for the Zero Swift client.",
|
|
14
|
+
license: "Apache-2.0",
|
|
15
|
+
repository: {
|
|
16
|
+
type: "git",
|
|
17
|
+
url: "git+https://github.com/wevm/zero.git",
|
|
18
|
+
directory: "packages/zero-swift-cli"
|
|
19
|
+
},
|
|
20
|
+
bin: {
|
|
21
|
+
"zero-swift": "./src/bin.ts"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
type: "module",
|
|
27
|
+
publishConfig: {
|
|
28
|
+
bin: {
|
|
29
|
+
"zero-swift": "./dist/bin.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
scripts: {
|
|
33
|
+
build: "esbuild src/bin.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:@rocicorp/zero --external:typescript --external:incur",
|
|
34
|
+
test: "vitest run",
|
|
35
|
+
generate: "UPDATE_GENERATED=1 vitest run",
|
|
36
|
+
"check-types": "tsc",
|
|
37
|
+
lint: "oxlint --quiet --config ../../oxlint.config.ts src/",
|
|
38
|
+
format: "oxfmt .",
|
|
39
|
+
"check-format": "oxfmt --check .",
|
|
40
|
+
"version-packages": "cd ../.. && pnpm dlx @changesets/cli@3.0.3 version && jq -r .version packages/zero-swift-cli/package.json > swift/VERSION"
|
|
41
|
+
},
|
|
42
|
+
dependencies: {
|
|
43
|
+
incur: "0.7.1",
|
|
44
|
+
typescript: "6.0.3"
|
|
45
|
+
},
|
|
46
|
+
devDependencies: {
|
|
47
|
+
"@rocicorp/zero": "workspace:*",
|
|
48
|
+
"@types/node": "^22.19.19",
|
|
49
|
+
esbuild: "0.28.2",
|
|
50
|
+
oxfmt: "^0.74.0",
|
|
51
|
+
vitest: "^5.0.0",
|
|
52
|
+
"zero-schema": "workspace:*",
|
|
53
|
+
"zero-types": "workspace:*",
|
|
54
|
+
zql: "workspace:*"
|
|
55
|
+
},
|
|
56
|
+
peerDependencies: {
|
|
57
|
+
"@rocicorp/zero": ">=1.11.0"
|
|
58
|
+
},
|
|
59
|
+
engines: {
|
|
60
|
+
node: ">=22.18"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
8
63
|
|
|
9
64
|
// src/generate.ts
|
|
10
65
|
var reservedTypeNames = /* @__PURE__ */ new Set([
|
|
@@ -567,14 +622,32 @@ function aliasName(type) {
|
|
|
567
622
|
}
|
|
568
623
|
|
|
569
624
|
// src/cli.ts
|
|
625
|
+
var UsageError = class extends Error {
|
|
626
|
+
code;
|
|
627
|
+
constructor(code, message) {
|
|
628
|
+
super(message);
|
|
629
|
+
this.code = code;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
function mustExist(path, code, message) {
|
|
633
|
+
if (!existsSync(path)) {
|
|
634
|
+
throw new UsageError(code, message);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
570
637
|
async function codegenFile(config, cwd) {
|
|
571
638
|
const schemaPath = resolve2(cwd, config.schema ?? "schema.ts");
|
|
639
|
+
mustExist(
|
|
640
|
+
schemaPath,
|
|
641
|
+
"SCHEMA_NOT_FOUND",
|
|
642
|
+
`Schema ${relative(cwd, schemaPath)} doesn't exist. Pass --schema-file, or set "schema" in zero-swift.json.`
|
|
643
|
+
);
|
|
572
644
|
const exportName = config.export ?? "schema";
|
|
573
645
|
const module = await import(pathToFileURL(schemaPath).href);
|
|
574
646
|
const schema = module[exportName];
|
|
575
647
|
if (!schema || typeof schema !== "object" || !("tables" in schema)) {
|
|
576
|
-
throw new
|
|
577
|
-
|
|
648
|
+
throw new UsageError(
|
|
649
|
+
"SCHEMA_NOT_EXPORTED",
|
|
650
|
+
`${relative(cwd, schemaPath)} does not export a schema as "${exportName}". Pass --export, or set "export" in zero-swift.json.`
|
|
578
651
|
);
|
|
579
652
|
}
|
|
580
653
|
const customTypes = readCustomTypes(schemaPath, exportName);
|
|
@@ -585,65 +658,156 @@ async function codegenFile(config, cwd) {
|
|
|
585
658
|
relative(cwd, schemaPath).replaceAll("\\", "/")
|
|
586
659
|
);
|
|
587
660
|
}
|
|
588
|
-
async function
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
out: { type: "string" },
|
|
595
|
-
config: { type: "string" },
|
|
596
|
-
samples: { type: "string" },
|
|
597
|
-
check: { type: "boolean" }
|
|
661
|
+
async function reporting(fail, body) {
|
|
662
|
+
try {
|
|
663
|
+
return await body();
|
|
664
|
+
} catch (e) {
|
|
665
|
+
if (e instanceof UsageError) {
|
|
666
|
+
return fail({ code: e.code, message: e.message });
|
|
598
667
|
}
|
|
599
|
-
|
|
600
|
-
const [command = "codegen", subcommand] = positionals;
|
|
601
|
-
const cwd = process.cwd();
|
|
602
|
-
if (command === "drift" && subcommand === "record") {
|
|
603
|
-
const { recordDriftFile } = await import("./drift-S756Q2ZA.js");
|
|
604
|
-
const vectors = await recordDriftFile(
|
|
605
|
-
resolve2(cwd, values.samples ?? "drift-samples.ts")
|
|
606
|
-
);
|
|
607
|
-
return writeOutput(
|
|
608
|
-
resolve2(cwd, values.out ?? "drift-vectors.json"),
|
|
609
|
-
vectors,
|
|
610
|
-
values.check ?? false,
|
|
611
|
-
cwd
|
|
612
|
-
);
|
|
668
|
+
throw e;
|
|
613
669
|
}
|
|
614
|
-
if (command !== "codegen") {
|
|
615
|
-
process.stderr.write(`Unknown command "${positionals.join(" ")}"
|
|
616
|
-
`);
|
|
617
|
-
return 2;
|
|
618
|
-
}
|
|
619
|
-
const configPath = values.config ?? (existsSync(resolve2(cwd, "zero-swift.json")) ? "zero-swift.json" : void 0);
|
|
620
|
-
const fileConfig = configPath ? JSON.parse(readFileSync(resolve2(cwd, configPath), "utf8")) : {};
|
|
621
|
-
const config = {
|
|
622
|
-
...fileConfig,
|
|
623
|
-
...values.schema ? { schema: values.schema } : {},
|
|
624
|
-
...values.out ? { output: values.out } : {}
|
|
625
|
-
};
|
|
626
|
-
const output = resolve2(cwd, config.output ?? "Schema.swift");
|
|
627
|
-
const swift = await codegenFile(config, cwd);
|
|
628
|
-
return writeOutput(output, swift, values.check ?? false, cwd);
|
|
629
670
|
}
|
|
630
|
-
function writeOutput(output, contents,
|
|
671
|
+
function writeOutput(fail, output, contents, check2, cwd) {
|
|
672
|
+
const path = relative(cwd, output);
|
|
631
673
|
const current = existsSync(output) ? readFileSync(output, "utf8") : void 0;
|
|
632
|
-
if (
|
|
674
|
+
if (check2) {
|
|
633
675
|
if (current !== contents) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
676
|
+
return fail({
|
|
677
|
+
code: "OUT_OF_DATE",
|
|
678
|
+
message: `${path} is out of date. Run the command again without --check to update it.`
|
|
679
|
+
});
|
|
637
680
|
}
|
|
638
|
-
return
|
|
681
|
+
return { path, upToDate: true };
|
|
639
682
|
}
|
|
640
683
|
if (current !== contents) {
|
|
641
684
|
writeFileSync(output, contents);
|
|
642
685
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
686
|
+
return { path, written: current !== contents };
|
|
687
|
+
}
|
|
688
|
+
function legacyArgv(argv) {
|
|
689
|
+
if (argv[0] !== "codegen") return [...argv];
|
|
690
|
+
const out = [];
|
|
691
|
+
for (let i = 0; i < argv.length; i++) {
|
|
692
|
+
const token = argv[i];
|
|
693
|
+
const next = argv[i + 1];
|
|
694
|
+
if (token === "--schema" && next !== void 0 && !next.startsWith("-")) {
|
|
695
|
+
out.push("--schema-file", next);
|
|
696
|
+
i++;
|
|
697
|
+
} else if (token.startsWith("--schema=")) {
|
|
698
|
+
out.push("--schema-file", token.slice("--schema=".length));
|
|
699
|
+
} else {
|
|
700
|
+
out.push(token);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
646
704
|
}
|
|
705
|
+
var check = z.boolean().optional().describe("Fail when the output is out of date instead of writing it");
|
|
706
|
+
var cli = Cli.create("zero-swift", {
|
|
707
|
+
version: package_default.version,
|
|
708
|
+
description: "Generates what the Zero Swift client needs from a Zero app's TypeScript schema, queries, and mutators."
|
|
709
|
+
}).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.",
|
|
711
|
+
options: z.object({
|
|
712
|
+
config: z.string().optional().describe("Config file (default zero-swift.json, when present)"),
|
|
713
|
+
schemaFile: z.string().optional().describe("TS schema module (default schema.ts)"),
|
|
714
|
+
export: z.string().optional().describe("Export of the module that holds the schema (default schema)"),
|
|
715
|
+
out: z.string().optional().describe("Swift file to write (default Schema.swift)"),
|
|
716
|
+
schemaName: z.string().optional().describe("Name of the generated ZeroSchema static (default app)"),
|
|
717
|
+
zql: z.string().optional().describe("Name of the generated query builder global (default zql)"),
|
|
718
|
+
typePrefix: z.string().optional().describe("Prefix for every generated type name (default none)"),
|
|
719
|
+
access: z.enum(["public", "internal"]).optional().describe("Access level of the generated code (default public)"),
|
|
720
|
+
check
|
|
721
|
+
}),
|
|
722
|
+
examples: [
|
|
723
|
+
{ description: "Generate from zero-swift.json" },
|
|
724
|
+
{
|
|
725
|
+
options: {
|
|
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
|
+
],
|
|
736
|
+
run: (c) => reporting(c.error, async () => {
|
|
737
|
+
const cwd = process.cwd();
|
|
738
|
+
const {
|
|
739
|
+
config: configOption,
|
|
740
|
+
schemaFile,
|
|
741
|
+
out,
|
|
742
|
+
check: check2,
|
|
743
|
+
...overrides
|
|
744
|
+
} = c.options;
|
|
745
|
+
const configPath = configOption ?? (existsSync(resolve2(cwd, "zero-swift.json")) ? "zero-swift.json" : void 0);
|
|
746
|
+
if (configPath) {
|
|
747
|
+
mustExist(
|
|
748
|
+
resolve2(cwd, configPath),
|
|
749
|
+
"CONFIG_NOT_FOUND",
|
|
750
|
+
`Config ${configPath} doesn't exist.`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const fileConfig = configPath ? JSON.parse(readFileSync(resolve2(cwd, configPath), "utf8")) : {};
|
|
754
|
+
const config = {
|
|
755
|
+
...fileConfig,
|
|
756
|
+
...Object.fromEntries(
|
|
757
|
+
Object.entries(overrides).filter(([, v]) => v !== void 0)
|
|
758
|
+
),
|
|
759
|
+
...schemaFile ? { schema: schemaFile } : {},
|
|
760
|
+
...out ? { output: out } : {}
|
|
761
|
+
};
|
|
762
|
+
const swift = await codegenFile(config, cwd);
|
|
763
|
+
return writeOutput(
|
|
764
|
+
c.error,
|
|
765
|
+
resolve2(cwd, config.output ?? "Schema.swift"),
|
|
766
|
+
swift,
|
|
767
|
+
check2 ?? false,
|
|
768
|
+
cwd
|
|
769
|
+
);
|
|
770
|
+
})
|
|
771
|
+
});
|
|
772
|
+
cli.command(
|
|
773
|
+
Cli.create("drift", {
|
|
774
|
+
description: "Record what the TS queries and mutators do, for the Swift drift test (Drift.failures in ZeroTesting)."
|
|
775
|
+
}).command("record", {
|
|
776
|
+
description: "Run the app's TS queries and mutators on the samples a module exports, and write what they did.",
|
|
777
|
+
options: z.object({
|
|
778
|
+
samples: z.string().optional().describe("Samples module (default drift-samples.ts)"),
|
|
779
|
+
out: z.string().optional().describe("Vectors file to write (default drift-vectors.json)"),
|
|
780
|
+
check
|
|
781
|
+
}),
|
|
782
|
+
examples: [
|
|
783
|
+
{
|
|
784
|
+
options: {
|
|
785
|
+
samples: "drift-samples.ts",
|
|
786
|
+
out: "Tests/AppTests/drift-vectors.json"
|
|
787
|
+
},
|
|
788
|
+
description: "Record the drift vectors"
|
|
789
|
+
}
|
|
790
|
+
],
|
|
791
|
+
run: (c) => reporting(c.error, async () => {
|
|
792
|
+
const cwd = process.cwd();
|
|
793
|
+
const samples = resolve2(cwd, c.options.samples ?? "drift-samples.ts");
|
|
794
|
+
mustExist(
|
|
795
|
+
samples,
|
|
796
|
+
"SAMPLES_NOT_FOUND",
|
|
797
|
+
`Samples file ${relative(cwd, samples)} doesn't exist. Pass --samples.`
|
|
798
|
+
);
|
|
799
|
+
const { recordDriftFile } = await import("./drift-S756Q2ZA.js");
|
|
800
|
+
const vectors = await recordDriftFile(samples);
|
|
801
|
+
return writeOutput(
|
|
802
|
+
c.error,
|
|
803
|
+
resolve2(cwd, c.options.out ?? "drift-vectors.json"),
|
|
804
|
+
vectors,
|
|
805
|
+
c.options.check ?? false,
|
|
806
|
+
cwd
|
|
807
|
+
);
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
);
|
|
647
811
|
|
|
648
812
|
// src/bin.ts
|
|
649
|
-
|
|
813
|
+
await cli.serve(legacyArgv(process.argv.slice(2)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-swift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
@@ -16,15 +16,16 @@
|
|
|
16
16
|
],
|
|
17
17
|
"type": "module",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"incur": "0.7.1",
|
|
19
20
|
"typescript": "6.0.3"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^22.
|
|
23
|
+
"@types/node": "^22.19.19",
|
|
23
24
|
"esbuild": "0.28.2",
|
|
24
|
-
"oxfmt": "^0.
|
|
25
|
+
"oxfmt": "^0.74.0",
|
|
25
26
|
"vitest": "^5.0.0",
|
|
26
|
-
"zero-schema": "0.0.0",
|
|
27
27
|
"@rocicorp/zero": "1.12.0",
|
|
28
|
+
"zero-schema": "0.0.0",
|
|
28
29
|
"zql": "0.0.0",
|
|
29
30
|
"zero-types": "0.0.0"
|
|
30
31
|
},
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"node": ">=22.18"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build": "esbuild src/bin.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:@rocicorp/zero --external:typescript",
|
|
39
|
+
"build": "esbuild src/bin.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:@rocicorp/zero --external:typescript --external:incur",
|
|
39
40
|
"test": "vitest run",
|
|
40
41
|
"generate": "UPDATE_GENERATED=1 vitest run",
|
|
41
42
|
"check-types": "tsc",
|