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.
Files changed (3) hide show
  1. package/README.md +31 -1
  2. package/dist/bin.js +216 -52
  3. 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
- `--schema`, `--out`, and `--config` override the config file.
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 { parseArgs } from "node:util";
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 Error(
577
- `${schemaPath} does not export a schema as "${exportName}"`
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 main(argv) {
589
- const { values, positionals } = parseArgs({
590
- args: argv,
591
- allowPositionals: true,
592
- options: {
593
- schema: { type: "string" },
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, check, cwd) {
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 (check) {
674
+ if (check2) {
633
675
  if (current !== contents) {
634
- process.stderr.write(`${relative(cwd, output)} is out of date
635
- `);
636
- return 1;
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 0;
681
+ return { path, upToDate: true };
639
682
  }
640
683
  if (current !== contents) {
641
684
  writeFileSync(output, contents);
642
685
  }
643
- process.stdout.write(`Wrote ${relative(cwd, output)}
644
- `);
645
- return 0;
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
- process.exitCode = await main(process.argv.slice(2));
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.1.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.10.5",
23
+ "@types/node": "^22.19.19",
23
24
  "esbuild": "0.28.2",
24
- "oxfmt": "^0.65.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",