zero-swift 0.1.1 → 0.3.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 +30 -1
  2. package/dist/bin.js +206 -99
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -37,7 +37,28 @@ npx zero-swift codegen --check # fails when the output is out of date
37
37
  `Int`; it must then hold integers. For a `json<T>()` column whose `T` is a
38
38
  plain object or a literal union, the tool generates a `Codable` type.
39
39
 
40
- `--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`.
41
62
 
42
63
  ## `zero-swift drift record`
43
64
 
@@ -60,3 +81,11 @@ The samples module's default export holds:
60
81
 
61
82
  Record again whenever either side changes. `--check` fails when the recorded
62
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.3.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,39 +622,32 @@ function aliasName(type) {
567
622
  }
568
623
 
569
624
  // src/cli.ts
570
- var usage = `Usage:
571
- zero-swift codegen [--schema schema.ts] [--out Schema.swift]
572
- [--config zero-swift.json] [--check]
573
- zero-swift drift record [--samples drift-samples.ts]
574
- [--out drift-vectors.json] [--check]
575
-
576
- codegen generates Swift from the app's TypeScript schema. Options override
577
- zero-swift.json, which is read when present.
578
- drift record runs the app's TS queries and mutators on samples and records
579
- what they did, for the Swift drift test.
580
- --check fails when the output is out of date instead of writing it.
581
-
582
- Docs: https://github.com/wevm/zero/tree/main/packages/zero-swift-cli
583
- `;
584
625
  var UsageError = class extends Error {
626
+ code;
627
+ constructor(code, message) {
628
+ super(message);
629
+ this.code = code;
630
+ }
585
631
  };
586
- function mustExist(path, message) {
632
+ function mustExist(path, code, message) {
587
633
  if (!existsSync(path)) {
588
- throw new UsageError(message);
634
+ throw new UsageError(code, message);
589
635
  }
590
636
  }
591
637
  async function codegenFile(config, cwd) {
592
638
  const schemaPath = resolve2(cwd, config.schema ?? "schema.ts");
593
639
  mustExist(
594
640
  schemaPath,
595
- `Schema ${relative(cwd, schemaPath)} doesn't exist. Pass --schema, or set "schema" in zero-swift.json.`
641
+ "SCHEMA_NOT_FOUND",
642
+ `Schema ${relative(cwd, schemaPath)} doesn't exist. Pass --schema-file, or set "schema" in zero-swift.json.`
596
643
  );
597
644
  const exportName = config.export ?? "schema";
598
645
  const module = await import(pathToFileURL(schemaPath).href);
599
646
  const schema = module[exportName];
600
647
  if (!schema || typeof schema !== "object" || !("tables" in schema)) {
601
648
  throw new UsageError(
602
- `${schemaPath} does not export a schema as "${exportName}"`
649
+ "SCHEMA_NOT_EXPORTED",
650
+ `${relative(cwd, schemaPath)} does not export a schema as "${exportName}". Pass --export, or set "export" in zero-swift.json.`
603
651
  );
604
652
  }
605
653
  const customTypes = readCustomTypes(schemaPath, exportName);
@@ -610,97 +658,156 @@ async function codegenFile(config, cwd) {
610
658
  relative(cwd, schemaPath).replaceAll("\\", "/")
611
659
  );
612
660
  }
613
- async function main(argv) {
614
- let parsed;
661
+ async function reporting(fail, body) {
615
662
  try {
616
- parsed = parseArgs({
617
- args: argv,
618
- allowPositionals: true,
619
- options: {
620
- schema: { type: "string" },
621
- out: { type: "string" },
622
- config: { type: "string" },
623
- samples: { type: "string" },
624
- check: { type: "boolean" },
625
- help: { type: "boolean", short: "h" }
626
- }
627
- });
663
+ return await body();
628
664
  } catch (e) {
629
- process.stderr.write(`${e.message}
630
-
631
- ${usage}`);
632
- return 2;
633
- }
634
- const { values, positionals } = parsed;
635
- const [command = "codegen", subcommand] = positionals;
636
- if (values.help || command === "help") {
637
- process.stdout.write(usage);
638
- return 0;
639
- }
640
- const cwd = process.cwd();
641
- if (command === "drift" && subcommand === "record") {
642
- const samples = resolve2(cwd, values.samples ?? "drift-samples.ts");
643
- mustExist(
644
- samples,
645
- `Samples file ${relative(cwd, samples)} doesn't exist. Pass --samples.`
646
- );
647
- const { recordDriftFile } = await import("./drift-S756Q2ZA.js");
648
- const vectors = await recordDriftFile(samples);
649
- return writeOutput(
650
- resolve2(cwd, values.out ?? "drift-vectors.json"),
651
- vectors,
652
- values.check ?? false,
653
- cwd
654
- );
665
+ if (e instanceof UsageError) {
666
+ return fail({ code: e.code, message: e.message });
667
+ }
668
+ throw e;
655
669
  }
656
- if (command !== "codegen") {
657
- process.stderr.write(
658
- `Unknown command "${positionals.join(" ")}"
659
-
660
- ${usage}`
661
- );
662
- return 2;
663
- }
664
- const configPath = values.config ?? (existsSync(resolve2(cwd, "zero-swift.json")) ? "zero-swift.json" : void 0);
665
- if (configPath) {
666
- mustExist(resolve2(cwd, configPath), `Config ${configPath} doesn't exist.`);
667
- }
668
- const fileConfig = configPath ? JSON.parse(readFileSync(resolve2(cwd, configPath), "utf8")) : {};
669
- const config = {
670
- ...fileConfig,
671
- ...values.schema ? { schema: values.schema } : {},
672
- ...values.out ? { output: values.out } : {}
673
- };
674
- const output = resolve2(cwd, config.output ?? "Schema.swift");
675
- const swift = await codegenFile(config, cwd);
676
- return writeOutput(output, swift, values.check ?? false, cwd);
677
670
  }
678
- function writeOutput(output, contents, check, cwd) {
671
+ function writeOutput(fail, output, contents, check2, cwd) {
672
+ const path = relative(cwd, output);
679
673
  const current = existsSync(output) ? readFileSync(output, "utf8") : void 0;
680
- if (check) {
674
+ if (check2) {
681
675
  if (current !== contents) {
682
- process.stderr.write(`${relative(cwd, output)} is out of date
683
- `);
684
- 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
+ });
685
680
  }
686
- return 0;
681
+ return { path, upToDate: true };
687
682
  }
688
683
  if (current !== contents) {
689
684
  writeFileSync(output, contents);
690
685
  }
691
- process.stdout.write(`Wrote ${relative(cwd, output)}
692
- `);
693
- return 0;
686
+ return { path, written: current !== contents };
694
687
  }
695
-
696
- // src/bin.ts
697
- try {
698
- process.exitCode = await main(process.argv.slice(2));
699
- } catch (e) {
700
- if (!(e instanceof UsageError)) {
701
- throw e;
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
702
  }
703
- process.stderr.write(`zero-swift: ${e.message}
704
- `);
705
- process.exitCode = 1;
703
+ return out;
706
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
+ );
811
+
812
+ // src/bin.ts
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.1",
3
+ "version": "0.3.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,6 +16,7 @@
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": {
@@ -24,9 +25,9 @@
24
25
  "oxfmt": "^0.74.0",
25
26
  "vitest": "^5.0.0",
26
27
  "@rocicorp/zero": "1.12.0",
27
- "zero-schema": "0.0.0",
28
28
  "zero-types": "0.0.0",
29
- "zql": "0.0.0"
29
+ "zql": "0.0.0",
30
+ "zero-schema": "0.0.0"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@rocicorp/zero": ">=1.11.0"
@@ -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",