From 02e2313aa9e5f3e50dfea68ed9a39f5bdc12515e Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sat, 18 Jul 2026 10:33:55 -0700 Subject: [PATCH] Generate typed React component bindings --- flake.lock | 10 +- flake.nix | 2 +- src/camino/schema_pb.ts | 17 ++- src/codegen-ts-runtime.ts | 305 +++++++++++++++++++++++++++++++++++--- src/schema-compiler.ts | 24 ++- src/schema-ir.ts | 3 + 6 files changed, 328 insertions(+), 33 deletions(-) diff --git a/flake.lock b/flake.lock index 3c88b60..44e5eb3 100644 --- a/flake.lock +++ b/flake.lock @@ -74,17 +74,17 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1784127127, - "narHash": "sha256-M36R+j10krhGRrhCozKkV7PsRM9BklQNqWox2bW7t/0=", + "lastModified": 1784395768, + "narHash": "sha256-g03xOnzJHiL/edNV6nqURtaPqiapq2uCm7wQTZh4b5s=", "ref": "refs/heads/exported", - "rev": "a9134843bc947546c78b28902d081f451733ddba", - "revCount": 22, + "rev": "e6b34535afa6e94a118e1f9d082054855e7df632", + "revCount": 24, "type": "git", "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" }, "original": { "ref": "refs/heads/exported", - "rev": "a9134843bc947546c78b28902d081f451733ddba", + "rev": "e6b34535afa6e94a118e1f9d082054855e7df632", "type": "git", "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" } diff --git a/flake.nix b/flake.nix index cfa23c9..2e3bdf2 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; - quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/heads/exported&rev=a9134843bc947546c78b28902d081f451733ddba"; + quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/heads/exported&rev=e6b34535afa6e94a118e1f9d082054855e7df632"; quixosNixHelpers = { url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/heads/exported&rev=1f0c39b01501d646fe97dfc6e5777ccab0bde2f1"; flake = false; diff --git a/src/camino/schema_pb.ts b/src/camino/schema_pb.ts index cd451ff..f34388a 100644 --- a/src/camino/schema_pb.ts +++ b/src/camino/schema_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file camino/schema.proto. */ export const file_camino_schema: GenFile = /*@__PURE__*/ - fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iSwoJU3ltYm9sUmVmEhEKCW5hbWVzcGFjZRgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkSDAoEaGFzaBgEIAEoCSIYCgNSZWYSEQoJb2JqZWN0X2lkGAEgASgJIg0KC0N1c3RvbUZpZWxkIiQKEFdhdGNoU3RhcnRSZXN1bHQSEAoId2F0Y2hfaWQYASABKAkiJAoQV2F0Y2hTdG9wUmVxdWVzdBIQCgh3YXRjaF9pZBgBIAEoCSIRCg9XYXRjaFN0b3BSZXN1bHQiGwoIQ3JkdFRleHQSDwoHcGF5bG9hZBgBIAEoDCIfCgxDcmR0UmljaFRleHQSDwoHcGF5bG9hZBgBIAEoDCIbCghDcmR0SnNvbhIPCgdwYXlsb2FkGAEgASgMIqgDCgtDbGFzc1NjaGVtYRIdCgJpZBgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSDwoHdmVyc2lvbhgCIAEoDRIjCgZmaWVsZHMYAyADKAsyEy5jYW1pbm8uRmllbGRTY2hlbWESIQoFZWRnZXMYBCADKAsyEi5jYW1pbm8uRWRnZVNjaGVtYRIlCgdtZXRob2RzGAUgAygLMhQuY2FtaW5vLk1ldGhvZFNjaGVtYRIpCgptaWdyYXRpb25zGAYgAygLMhUuY2FtaW5vLk1pZ3JhdGlvblNwZWMSEwoLc291cmNlX2ZpbGUYByABKAkSFQoNcHJvdG9fbWVzc2FnZRgIIAEoCRI6ChJvcGVyYXRpb25fc2VydmljZXMYCSADKAsyHi5jYW1pbm8uT3BlcmF0aW9uU2VydmljZVNjaGVtYRI5CgppbXBsZW1lbnRzGAogAygLMiUuY2FtaW5vLkludGVyZmFjZUltcGxlbWVudGF0aW9uU2NoZW1hEiwKC2NvbnN0cnVjdG9yGAsgASgLMhcuY2FtaW5vLkNvbnN0cnVjdG9yU3BlYyKzAgoLRmllbGRTY2hlbWESDAoEbmFtZRgBIAEoCRILCgN0YWcYAiABKA0SHQoEdHlwZRgDIAEoCzIPLmNhbWluby5UeXBlUmVmEioKCGNvbmZsaWN0GAQgASgOMhguY2FtaW5vLkNvbmZsaWN0U3RyYXRlZ3kSEAoIcmVwZWF0ZWQYBSABKAgSEAoIb3B0aW9uYWwYBiABKAgSJQoHc3RvcmFnZRgHIAEoCzIULmNhbWluby5GaWVsZFN0b3JhZ2USHQoDb3BzGAggASgLMhAuY2FtaW5vLkZpZWxkT3BzEjoKEmludGVyZmFjZV9jb250cmFjdBgJIAEoCzIeLmNhbWluby5JbnRlcmZhY2VGaWVsZENvbnRyYWN0EhgKEGlzX2Rpc3BsYXlfbGFiZWwYCiABKAgibAoMRmllbGRTdG9yYWdlEiYKBGtpbmQYASABKA4yGC5jYW1pbm8uRmllbGRTdG9yYWdlS2luZBIlCghyZXNvbHZlchgCIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZhINCgVjYWNoZRgDIAEoCCJACgdUeXBlUmVmEhIKCnByb3RvX3R5cGUYASABKAkSIQoGc3ltYm9sGAIgASgLMhEuY2FtaW5vLlN5bWJvbFJlZiI6CgtUeXBlQmluZGluZxIMCgRuYW1lGAEgASgJEh0KBHR5cGUYAiABKAsyDy5jYW1pbm8uVHlwZVJlZiKUAQoWSW50ZXJmYWNlRmllbGRDb250cmFjdBIQCghyZXF1aXJlZBgBIAEoCBIQCghyZWFkYWJsZRgCIAEoCBIQCgh3cml0YWJsZRgDIAEoCBIRCgl3YXRjaGFibGUYBCABKAgSEgoKdHlwZV9wYXJhbRgFIAEoCRIdCgR0eXBlGAYgASgLMg8uY2FtaW5vLlR5cGVSZWYicQodSW50ZXJmYWNlSW1wbGVtZW50YXRpb25TY2hlbWESJAoJaW50ZXJmYWNlGAEgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIqCg10eXBlX2JpbmRpbmdzGAIgAygLMhMuY2FtaW5vLlR5cGVCaW5kaW5nIpIBCg9JbnRlcmZhY2VTY2hlbWESHQoCaWQYASABKAsyES5jYW1pbm8uU3ltYm9sUmVmEg8KB3ZlcnNpb24YAiABKA0SIwoGZmllbGRzGAMgAygLMhMuY2FtaW5vLkZpZWxkU2NoZW1hEhMKC3NvdXJjZV9maWxlGAQgASgJEhUKDXByb3RvX21lc3NhZ2UYBSABKAkiUAoIRmllbGRPcHMSMwoOaW1wbGVtZW50YXRpb24YASABKA4yGy5jYW1pbm8uRmllbGRJbXBsZW1lbnRhdGlvbhIPCgdzZXJ2aWNlGAIgASgJItkDCgpFZGdlU2NoZW1hEh0KAmlkGAEgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIUCgxzb3VyY2VfZmllbGQYAiABKAkSJQoKZnJvbV9jbGFzcxgDIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSIwoIdG9fY2xhc3MYBCABKAsyES5jYW1pbm8uU3ltYm9sUmVmEigKC2NhcmRpbmFsaXR5GAUgASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5Eg8KB2ludmVyc2UYBiABKAkSIwoGZmllbGRzGAcgAygLMhMuY2FtaW5vLkZpZWxkU2NoZW1hEjEKDWZyb21fZW5kcG9pbnQYCCABKAsyGi5jYW1pbm8uRWRnZUVuZHBvaW50U2NoZW1hEi8KC3RvX2VuZHBvaW50GAkgASgLMhouY2FtaW5vLkVkZ2VFbmRwb2ludFNjaGVtYRIqCg9kZWNsYXJpbmdfY2xhc3MYCiABKAsyES5jYW1pbm8uU3ltYm9sUmVmEh8KBHRhZ3MYCyADKAsyES5jYW1pbm8uU3ltYm9sUmVmEiUKCmltcGxlbWVudHMYDCADKAsyES5jYW1pbm8uU3ltYm9sUmVmEhIKCnVuZGlyZWN0ZWQYDSABKAgi3QEKEkVkZ2VFbmRwb2ludFNjaGVtYRIgCgVjbGFzcxgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSJAoJaW50ZXJmYWNlGAIgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhISCgpwcm9qZWN0aW9uGAMgASgJEigKC2NhcmRpbmFsaXR5GAQgASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5Eg8KB2luZGV4ZWQYBSABKAgSMAoLbWF0ZXJpYWxpemUYBiABKAsyGy5jYW1pbm8uRWRnZU1hdGVyaWFsaXphdGlvbiJTChNFZGdlTWF0ZXJpYWxpemF0aW9uEiAKBWNsYXNzGAEgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIaChJjb25uZWN0X3Byb2plY3Rpb24YAiABKAkiQwoMTWV0aG9kU2NoZW1hEgwKBG5hbWUYASABKAkSJQoIZnVuY3Rpb24YAiABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWYiYAoNTWlncmF0aW9uU3BlYxIUCgxmcm9tX3ZlcnNpb24YASABKA0SEgoKdG9fdmVyc2lvbhgCIAEoDRIlCghmdW5jdGlvbhgDIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZiI4Cg9Db25zdHJ1Y3RvclNwZWMSJQoIZnVuY3Rpb24YASABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWYikQEKD09wZXJhdGlvblNjaGVtYRIMCgRuYW1lGAEgASgJEiMKCmlucHV0X3R5cGUYAiABKAsyDy5jYW1pbm8uVHlwZVJlZhIkCgtvdXRwdXRfdHlwZRgDIAEoCzIPLmNhbWluby5UeXBlUmVmEiUKCGZ1bmN0aW9uGAQgASgLMhMucXVpeG9zLkZ1bmN0aW9uUmVmImYKFk9wZXJhdGlvblNlcnZpY2VTY2hlbWESDAoEbmFtZRgBIAEoCRIRCglmdWxsX25hbWUYAiABKAkSKwoKb3BlcmF0aW9ucxgDIAMoCzIXLmNhbWluby5PcGVyYXRpb25TY2hlbWEimwEKEURvY1JlZkRlY2xhcmF0aW9uEhIKCmxvY2FsX25hbWUYASABKAkSHwoEdHlwZRgCIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSJwoMdGFyZ2V0X2NsYXNzGAMgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIoCghzdHJlbmd0aBgEIAEoDjIWLmNhbWluby5Eb2NSZWZTdHJlbmd0aCpkChBDb25mbGljdFN0cmF0ZWd5EiEKHUNPTkZMSUNUX1NUUkFURUdZX1VOU1BFQ0lGSUVEEAASCwoHUkVQTEFDRRABEhYKElBSRVNFUlZFX0NPTkZMSUNUUxACEggKBENSRFQQAyqTAQoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEEhAKDE1BTllfT1JERVJFRBAFEhcKE01BTllfVU5JUVVFX09SREVSRUQQBipUCg5Eb2NSZWZTdHJlbmd0aBIgChxET0NfUkVGX1NUUkVOR1RIX1VOU1BFQ0lGSUVEEAASDAoIU09GVF9SRUYQARISCg5QUk9KRUNURURfRURHRRACKnkKEEZpZWxkU3RvcmFnZUtpbmQSIgoeRklFTERfU1RPUkFHRV9LSU5EX1VOU1BFQ0lGSUVEEAASCgoGU1RPUkVEEAESCwoHREVSSVZFRBACEggKBExBWlkQAxIMCghFWFRFUk5BTBAEEhAKDFNUQVRJQ19GSU5BTBAFKkgKE0ZpZWxkSW1wbGVtZW50YXRpb24SJAogRklFTERfSU1QTEVNRU5UQVRJT05fVU5TUEVDSUZJRUQQABILCgdTRVJWSUNFEAFiBnByb3RvMw", [file_quixos_refs]); + fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iSwoJU3ltYm9sUmVmEhEKCW5hbWVzcGFjZRgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkSDAoEaGFzaBgEIAEoCSIYCgNSZWYSEQoJb2JqZWN0X2lkGAEgASgJIg0KC0N1c3RvbUZpZWxkIiQKEFdhdGNoU3RhcnRSZXN1bHQSEAoId2F0Y2hfaWQYASABKAkiJAoQV2F0Y2hTdG9wUmVxdWVzdBIQCgh3YXRjaF9pZBgBIAEoCSIRCg9XYXRjaFN0b3BSZXN1bHQiGwoIQ3JkdFRleHQSDwoHcGF5bG9hZBgBIAEoDCIfCgxDcmR0UmljaFRleHQSDwoHcGF5bG9hZBgBIAEoDCIbCghDcmR0SnNvbhIPCgdwYXlsb2FkGAEgASgMIqgDCgtDbGFzc1NjaGVtYRIdCgJpZBgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSDwoHdmVyc2lvbhgCIAEoDRIjCgZmaWVsZHMYAyADKAsyEy5jYW1pbm8uRmllbGRTY2hlbWESIQoFZWRnZXMYBCADKAsyEi5jYW1pbm8uRWRnZVNjaGVtYRIlCgdtZXRob2RzGAUgAygLMhQuY2FtaW5vLk1ldGhvZFNjaGVtYRIpCgptaWdyYXRpb25zGAYgAygLMhUuY2FtaW5vLk1pZ3JhdGlvblNwZWMSEwoLc291cmNlX2ZpbGUYByABKAkSFQoNcHJvdG9fbWVzc2FnZRgIIAEoCRI6ChJvcGVyYXRpb25fc2VydmljZXMYCSADKAsyHi5jYW1pbm8uT3BlcmF0aW9uU2VydmljZVNjaGVtYRI5CgppbXBsZW1lbnRzGAogAygLMiUuY2FtaW5vLkludGVyZmFjZUltcGxlbWVudGF0aW9uU2NoZW1hEiwKC2NvbnN0cnVjdG9yGAsgASgLMhcuY2FtaW5vLkNvbnN0cnVjdG9yU3BlYyKzAgoLRmllbGRTY2hlbWESDAoEbmFtZRgBIAEoCRILCgN0YWcYAiABKA0SHQoEdHlwZRgDIAEoCzIPLmNhbWluby5UeXBlUmVmEioKCGNvbmZsaWN0GAQgASgOMhguY2FtaW5vLkNvbmZsaWN0U3RyYXRlZ3kSEAoIcmVwZWF0ZWQYBSABKAgSEAoIb3B0aW9uYWwYBiABKAgSJQoHc3RvcmFnZRgHIAEoCzIULmNhbWluby5GaWVsZFN0b3JhZ2USHQoDb3BzGAggASgLMhAuY2FtaW5vLkZpZWxkT3BzEjoKEmludGVyZmFjZV9jb250cmFjdBgJIAEoCzIeLmNhbWluby5JbnRlcmZhY2VGaWVsZENvbnRyYWN0EhgKEGlzX2Rpc3BsYXlfbGFiZWwYCiABKAgibAoMRmllbGRTdG9yYWdlEiYKBGtpbmQYASABKA4yGC5jYW1pbm8uRmllbGRTdG9yYWdlS2luZBIlCghyZXNvbHZlchgCIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZhINCgVjYWNoZRgDIAEoCCJACgdUeXBlUmVmEhIKCnByb3RvX3R5cGUYASABKAkSIQoGc3ltYm9sGAIgASgLMhEuY2FtaW5vLlN5bWJvbFJlZiI6CgtUeXBlQmluZGluZxIMCgRuYW1lGAEgASgJEh0KBHR5cGUYAiABKAsyDy5jYW1pbm8uVHlwZVJlZiLiAQoWSW50ZXJmYWNlRmllbGRDb250cmFjdBIQCghyZXF1aXJlZBgBIAEoCBIQCghyZWFkYWJsZRgCIAEoCBIQCgh3cml0YWJsZRgDIAEoCBIRCgl3YXRjaGFibGUYBCABKAgSEgoKdHlwZV9wYXJhbRgFIAEoCRIdCgR0eXBlGAYgASgLMg8uY2FtaW5vLlR5cGVSZWYSHQoVcmVmX3RhcmdldF90eXBlX3BhcmFtGAcgASgJEi0KEGVkZ2VfY2FyZGluYWxpdHkYCCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkicQodSW50ZXJmYWNlSW1wbGVtZW50YXRpb25TY2hlbWESJAoJaW50ZXJmYWNlGAEgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIqCg10eXBlX2JpbmRpbmdzGAIgAygLMhMuY2FtaW5vLlR5cGVCaW5kaW5nIqsBCg9JbnRlcmZhY2VTY2hlbWESHQoCaWQYASABKAsyES5jYW1pbm8uU3ltYm9sUmVmEg8KB3ZlcnNpb24YAiABKA0SIwoGZmllbGRzGAMgAygLMhMuY2FtaW5vLkZpZWxkU2NoZW1hEhMKC3NvdXJjZV9maWxlGAQgASgJEhUKDXByb3RvX21lc3NhZ2UYBSABKAkSFwoPdHlwZV9wYXJhbWV0ZXJzGAYgAygJIlAKCEZpZWxkT3BzEjMKDmltcGxlbWVudGF0aW9uGAEgASgOMhsuY2FtaW5vLkZpZWxkSW1wbGVtZW50YXRpb24SDwoHc2VydmljZRgCIAEoCSLZAwoKRWRnZVNjaGVtYRIdCgJpZBgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSFAoMc291cmNlX2ZpZWxkGAIgASgJEiUKCmZyb21fY2xhc3MYAyABKAsyES5jYW1pbm8uU3ltYm9sUmVmEiMKCHRvX2NsYXNzGAQgASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIoCgtjYXJkaW5hbGl0eRgFIAEoDjITLmNhbWluby5DYXJkaW5hbGl0eRIPCgdpbnZlcnNlGAYgASgJEiMKBmZpZWxkcxgHIAMoCzITLmNhbWluby5GaWVsZFNjaGVtYRIxCg1mcm9tX2VuZHBvaW50GAggASgLMhouY2FtaW5vLkVkZ2VFbmRwb2ludFNjaGVtYRIvCgt0b19lbmRwb2ludBgJIAEoCzIaLmNhbWluby5FZGdlRW5kcG9pbnRTY2hlbWESKgoPZGVjbGFyaW5nX2NsYXNzGAogASgLMhEuY2FtaW5vLlN5bWJvbFJlZhIfCgR0YWdzGAsgAygLMhEuY2FtaW5vLlN5bWJvbFJlZhIlCgppbXBsZW1lbnRzGAwgAygLMhEuY2FtaW5vLlN5bWJvbFJlZhISCgp1bmRpcmVjdGVkGA0gASgIIt0BChJFZGdlRW5kcG9pbnRTY2hlbWESIAoFY2xhc3MYASABKAsyES5jYW1pbm8uU3ltYm9sUmVmEiQKCWludGVyZmFjZRgCIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSEgoKcHJvamVjdGlvbhgDIAEoCRIoCgtjYXJkaW5hbGl0eRgEIAEoDjITLmNhbWluby5DYXJkaW5hbGl0eRIPCgdpbmRleGVkGAUgASgIEjAKC21hdGVyaWFsaXplGAYgASgLMhsuY2FtaW5vLkVkZ2VNYXRlcmlhbGl6YXRpb24iUwoTRWRnZU1hdGVyaWFsaXphdGlvbhIgCgVjbGFzcxgBIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSGgoSY29ubmVjdF9wcm9qZWN0aW9uGAIgASgJIkMKDE1ldGhvZFNjaGVtYRIMCgRuYW1lGAEgASgJEiUKCGZ1bmN0aW9uGAIgASgLMhMucXVpeG9zLkZ1bmN0aW9uUmVmImAKDU1pZ3JhdGlvblNwZWMSFAoMZnJvbV92ZXJzaW9uGAEgASgNEhIKCnRvX3ZlcnNpb24YAiABKA0SJQoIZnVuY3Rpb24YAyABKAsyEy5xdWl4b3MuRnVuY3Rpb25SZWYiOAoPQ29uc3RydWN0b3JTcGVjEiUKCGZ1bmN0aW9uGAEgASgLMhMucXVpeG9zLkZ1bmN0aW9uUmVmIpEBCg9PcGVyYXRpb25TY2hlbWESDAoEbmFtZRgBIAEoCRIjCgppbnB1dF90eXBlGAIgASgLMg8uY2FtaW5vLlR5cGVSZWYSJAoLb3V0cHV0X3R5cGUYAyABKAsyDy5jYW1pbm8uVHlwZVJlZhIlCghmdW5jdGlvbhgEIAEoCzITLnF1aXhvcy5GdW5jdGlvblJlZiJmChZPcGVyYXRpb25TZXJ2aWNlU2NoZW1hEgwKBG5hbWUYASABKAkSEQoJZnVsbF9uYW1lGAIgASgJEisKCm9wZXJhdGlvbnMYAyADKAsyFy5jYW1pbm8uT3BlcmF0aW9uU2NoZW1hIpsBChFEb2NSZWZEZWNsYXJhdGlvbhISCgpsb2NhbF9uYW1lGAEgASgJEh8KBHR5cGUYAiABKAsyES5jYW1pbm8uU3ltYm9sUmVmEicKDHRhcmdldF9jbGFzcxgDIAEoCzIRLmNhbWluby5TeW1ib2xSZWYSKAoIc3RyZW5ndGgYBCABKA4yFi5jYW1pbm8uRG9jUmVmU3RyZW5ndGgqZAoQQ29uZmxpY3RTdHJhdGVneRIhCh1DT05GTElDVF9TVFJBVEVHWV9VTlNQRUNJRklFRBAAEgsKB1JFUExBQ0UQARIWChJQUkVTRVJWRV9DT05GTElDVFMQAhIICgRDUkRUEAMqkwEKC0NhcmRpbmFsaXR5EhsKF0NBUkRJTkFMSVRZX1VOU1BFQ0lGSUVEEAASEAoMT1BUSU9OQUxfT05FEAESDwoLRVhBQ1RMWV9PTkUQAhIICgRNQU5ZEAMSDwoLTUFOWV9VTklRVUUQBBIQCgxNQU5ZX09SREVSRUQQBRIXChNNQU5ZX1VOSVFVRV9PUkRFUkVEEAYqVAoORG9jUmVmU3RyZW5ndGgSIAocRE9DX1JFRl9TVFJFTkdUSF9VTlNQRUNJRklFRBAAEgwKCFNPRlRfUkVGEAESEgoOUFJPSkVDVEVEX0VER0UQAip5ChBGaWVsZFN0b3JhZ2VLaW5kEiIKHkZJRUxEX1NUT1JBR0VfS0lORF9VTlNQRUNJRklFRBAAEgoKBlNUT1JFRBABEgsKB0RFUklWRUQQAhIICgRMQVpZEAMSDAoIRVhURVJOQUwQBBIQCgxTVEFUSUNfRklOQUwQBSpIChNGaWVsZEltcGxlbWVudGF0aW9uEiQKIEZJRUxEX0lNUExFTUVOVEFUSU9OX1VOU1BFQ0lGSUVEEAASCwoHU0VSVklDRRABYgZwcm90bzM", [file_quixos_refs]); /** * @generated from message camino.SymbolRef @@ -407,6 +407,16 @@ export type InterfaceFieldContract = Message<"camino.InterfaceFieldContract"> & * @generated from field: camino.TypeRef type = 6; */ type?: TypeRef | undefined; + + /** + * @generated from field: string ref_target_type_param = 7; + */ + refTargetTypeParam: string; + + /** + * @generated from field: camino.Cardinality edge_cardinality = 8; + */ + edgeCardinality: Cardinality; }; /** @@ -466,6 +476,11 @@ export type InterfaceSchema = Message<"camino.InterfaceSchema"> & { * @generated from field: string proto_message = 5; */ protoMessage: string; + + /** + * @generated from field: repeated string type_parameters = 6; + */ + typeParameters: string[]; }; /** diff --git a/src/codegen-ts-runtime.ts b/src/codegen-ts-runtime.ts index d14508f..cf09c8a 100644 --- a/src/codegen-ts-runtime.ts +++ b/src/codegen-ts-runtime.ts @@ -1,7 +1,11 @@ #!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; -import { compileCaminoSchema } from "./schema-compiler.js"; +import protobuf from "protobufjs"; +import { + compileCaminoSchema, + loadCaminoSchemaRoot, +} from "./schema-compiler.js"; import type { ClassSchema, FieldSchema, @@ -12,7 +16,7 @@ import type { const usage = (): never => { console.error( - "Usage: camino-codegen-ts-runtime [implementation-import]", + "Usage: camino-codegen-ts-runtime [--react-only] [implementation-import]", ); process.exit(1); }; @@ -46,11 +50,22 @@ const classId = (schema: ClassSchema) => const symbolId = (value: { namespace: string; name: string; version: string; hash?: string }) => `${value.namespace}:${value.name}:${value.version}:${value.hash ?? ""}`; -const tsTypeForTypeRef = (type: TypeRef): string => { +const normalizedProtoType = (value: string) => value.replace(/^\./, ""); + +const declarationName = (value: protobuf.Type | protobuf.Enum) => value.name; + +const tsTypeForTypeRef = ( + type: TypeRef, + root?: protobuf.Root, +): string => { + if (type.symbol) { + return `ObjectRef<${stringLiteral(symbolId(type.symbol))}>`; + } if (type.enumValues && type.enumValues.length > 0) { return type.enumValues.map(stringLiteral).join(" | "); } - switch (type.protoType.replace(/^\./, "")) { + const protoType = normalizedProtoType(type.protoType); + switch (protoType) { case "string": case "google.protobuf.StringValue": return "string"; @@ -80,19 +95,31 @@ const tsTypeForTypeRef = (type: TypeRef): string => { case "google.protobuf.BytesValue": return "Uint8Array"; default: + if (protoType === "google.protobuf.Empty") { + return "Record"; + } + try { + const reflected = root?.lookup(protoType); + if (reflected instanceof protobuf.Type || reflected instanceof protobuf.Enum) { + return declarationName(reflected); + } + } catch { + // Unknown imported message types remain opaque at this codegen boundary. + } return "unknown"; } }; -const tsTypeForField = (field: FieldSchema): string => tsTypeForTypeRef(field.type); +const tsTypeForField = (field: FieldSchema, root?: protobuf.Root): string => + tsTypeForTypeRef(field.type, root); -const tsValueTypeForField = (field: FieldSchema) => - field.repeated ? `${tsTypeForField(field)}[]` : tsTypeForField(field); +const tsValueTypeForField = (field: FieldSchema, root?: protobuf.Root) => + field.repeated ? `${tsTypeForField(field, root)}[]` : tsTypeForField(field, root); -const tsFieldApiType = (field: FieldSchema) => +const tsFieldApiType = (field: FieldSchema, root?: protobuf.Root) => field.conflict === "crdt" - ? `CrdtField<${tsValueTypeForField(field)}>` - : `Field<${tsValueTypeForField(field)}>`; + ? `CrdtField<${tsValueTypeForField(field, root)}>` + : `Field<${tsValueTypeForField(field, root)}>`; const functionExportName = (fn: FunctionRef, fallback: string) => { if (isIdentifier(fn.symbol)) { @@ -107,6 +134,74 @@ const functionExportName = (fn: FunctionRef, fallback: string) => { const generatedWarning = `// @generated by camino-codegen-ts-runtime. Do not edit.\n`; +const scalarTsType = (field: protobuf.Field, root: protobuf.Root): string => { + const primitive = tsTypeForTypeRef({ protoType: field.type }, root); + if (primitive !== "unknown") { + return primitive; + } + const resolved = field.resolvedType; + if (resolved instanceof protobuf.Type || resolved instanceof protobuf.Enum) { + return declarationName(resolved); + } + return "unknown"; +}; + +const collectProtoDeclarations = ( + root: protobuf.Root, + types: TypeRef[], +) => { + const declarations = new Map(); + const visit = (value: protobuf.Type | protobuf.Enum) => { + const key = value.fullName; + if (declarations.has(key)) { + return; + } + declarations.set(key, value); + if (value instanceof protobuf.Type) { + for (const field of value.fieldsArray) { + if ( + field.resolvedType instanceof protobuf.Type || + field.resolvedType instanceof protobuf.Enum + ) { + visit(field.resolvedType); + } + } + } + }; + for (const type of types) { + if (type.symbol || !type.protoType) { + continue; + } + try { + const reflected = root.lookup(normalizedProtoType(type.protoType)); + if (reflected instanceof protobuf.Type || reflected instanceof protobuf.Enum) { + visit(reflected); + } + } catch { + // Opaque imported types are represented as unknown by the generated API. + } + } + return [...declarations.values()] + .map((value) => { + if (value instanceof protobuf.Enum) { + return `export type ${declarationName(value)} = ${Object.keys(value.values) + .map(stringLiteral) + .join(" | ")};`; + } + const fields = value.fieldsArray.map((field) => { + const fieldType = scalarTsType(field, root); + const valueType = field.map + ? `Record` + : field.repeated + ? `${fieldType}[]` + : fieldType; + return ` ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${valueType};`; + }); + return `export type ${declarationName(value)} = {\n${fields.join("\n")}\n};`; + }) + .join("\n\n"); +}; + type FunctionSpec = { exportName: string; symbol: string; @@ -158,10 +253,10 @@ ${bindings} }) .join("\n\n"); -const interfaceMixins = (schema: ClassSchema) => +const interfaceMixins = (schema: ClassSchema, root: protobuf.Root) => schema.implements.map((implementation) => { const typeArgs = implementation.typeBindings.map((binding) => - tsTypeForTypeRef(binding.type), + tsTypeForTypeRef(binding.type, root), ); return `${interfaceTypeName(implementation)}${ typeArgs.length > 0 ? `<${typeArgs.join(", ")}>` : "" @@ -202,7 +297,7 @@ const operationFunctionSpecs = (schema: ClassSchema): FunctionSpec[] => })); }); -const renderRuntime = (schema: ClassSchema) => { +const renderRuntime = (schema: ClassSchema, root: protobuf.Root) => { const className = schema.id.name; const objectTypeName = `${className}Object`; const refTypeName = `${className}Ref`; @@ -212,7 +307,7 @@ const renderRuntime = (schema: ClassSchema) => { const createObjectName = `create${className}Object`; const interfaceRefTypes = renderInterfaceRefTypes(schema); const refType = - [ `ObjectRef<${stringLiteral(classId(schema))}>`, ...interfaceMixins(schema)] + [ `ObjectRef<${stringLiteral(classId(schema))}>`, ...interfaceMixins(schema, root)] .join(" & "); const edgeFields = new Set(schema.edges.map((edge) => edge.sourceField)); const writableFields = schema.fields.filter( @@ -291,6 +386,8 @@ const renderRuntime = (schema: ClassSchema) => { serveQuixosPackageRuntime, } from "@quixos/camino-package-runtime"; +${collectProtoDeclarations(root, schema.implements.flatMap((entry) => entry.typeBindings.map((binding) => binding.type)))} + ${interfaceRefTypes ? `${interfaceRefTypes}\n\n` : ""}export type ${refTypeName} = ${refType}; export type ${objectTypeName} = { @@ -298,7 +395,7 @@ export type ${objectTypeName} = { ${writableFields .map( (field) => - ` ${camel(field.name)}: ${tsFieldApiType(field)};`, + ` ${camel(field.name)}: ${tsFieldApiType(field, root)};`, ) .join("\n")} }; @@ -379,6 +476,150 @@ export const ${serveName} = (implementation: ${implementationTypeName}) => { `; }; +const implementationNamed = (schema: ClassSchema, name: string) => + schema.implements.find( + (entry) => + entry.interface.namespace === "quixos.react" && + entry.interface.name === name, + ); + +const requiredBinding = ( + implementation: InterfaceImplementation, + name: string, +) => { + const binding = implementation.typeBindings.find((entry) => entry.name === name); + if (!binding) { + throw new Error( + `${implementation.interface.name} implementation is missing ${name}`, + ); + } + return binding.type; +}; + +const renderReactClient = (schema: ClassSchema, root: protobuf.Root) => { + const react = implementationNamed(schema, "ReactComponent"); + const componentFor = implementationNamed(schema, "ReactComponentFor"); + if (!react || !componentFor) { + return undefined; + } + const renderProps = requiredBinding(react, "RenderProps"); + const action = requiredBinding(react, "Action"); + const object = requiredBinding(componentFor, "Object"); + if (!object.symbol) { + throw new Error(`${schema.id.name}.ReactComponentFor.Object must bind a class symbol`); + } + const edge = schema.edges.find((candidate) => candidate.sourceField === "for_object"); + if (!edge) { + throw new Error(`${schema.id.name} must declare its ReactComponentFor edge`); + } + const componentEndpoint = + edge.fromEndpoint.projection === "for_object" + ? edge.fromEndpoint + : edge.toEndpoint.projection === "for_object" + ? edge.toEndpoint + : undefined; + const objectEndpoint = + componentEndpoint === edge.fromEndpoint ? edge.toEndpoint : edge.fromEndpoint; + if (!componentEndpoint || !objectEndpoint.projection) { + throw new Error(`${schema.id.name}.for_object edge has no inverse projection`); + } + if (componentEndpoint.cardinality !== "exactly_one") { + throw new Error(`${schema.id.name}.for_object must have exactly_one cardinality`); + } + if ( + !objectEndpoint.materialization || + symbolId(objectEndpoint.materialization.class) !== symbolId(schema.id) + ) { + throw new Error( + `${schema.id.name}.${objectEndpoint.projection} must materialize ${classId(schema)}`, + ); + } + + const className = schema.id.name; + const componentName = className.endsWith("ReactComponent") + ? `${className.slice(0, -"ReactComponent".length)}Component` + : className; + const propsTypeName = `${componentName}Props`; + const renderPropsType = tsTypeForTypeRef(renderProps, root); + const actionType = tsTypeForTypeRef(action, root); + const objectType = tsTypeForTypeRef(object, root); + const declarations = collectProtoDeclarations(root, [renderProps, action]); + + return `${generatedWarning}import { + createReactComponentFor, + type ReactComponentHostProps, + type ObjectRef, +} from "@quixos/camino-react-runtime"; + +${declarations} + +export type ${propsTypeName} = { + forObject: ${objectType}; +} & ${renderPropsType} & ReactComponentHostProps<${actionType}>; + +export const ${componentName} = createReactComponentFor< + ${objectType}, + ${renderPropsType}, + ${actionType} +>({ + componentClassId: ${stringLiteral(classId(schema))}, + projection: ${stringLiteral(objectEndpoint.projection)}, +}); +`; +}; + +const reactRuntimeDeclaration = `${generatedWarning}declare module "@quixos/camino-react-runtime" { + import type * as AutomergeNamespace from "@automerge/automerge"; + + export type ObjectRef = string & { + readonly $caminoClass: ClassId; + }; + + export type LiveFieldProp = { + value: T; + source: { + objectId: string; + fieldName: string; + fieldType?: string; + fieldStorage?: string; + conflictStrategy?: string; + revision?: string | number | bigint; + }; + }; + + export type ReactComponentHostProps = { + onAction?: (action: Action) => void; + fallback?: unknown; + className?: string; + style?: Record; + onError?: (error: Error) => void; + }; + + export type ReactComponentImplementationProps = { + camino: CaminoProps; + render: RenderProps; + dispatch: (action: Action) => void; + }; + + export const createReactComponentFor: < + ForObject extends ObjectRef, + RenderProps extends object, + Action, + >(config: { + componentClassId: string; + projection: string; + }) => (props: { + forObject: ForObject; + } & RenderProps & ReactComponentHostProps) => any; + + export const h: (...args: any[]) => any; + export const useLiveField: ( + field: LiveFieldProp, + ) => [T, (value: T) => void | Promise]; + export const Automerge: typeof AutomergeNamespace; +} +`; + const renderServer = (schema: ClassSchema, implementationImport: string) => { const className = schema.id.name; return `${generatedWarning}import * as implementation from ${stringLiteral(implementationImport)}; @@ -389,10 +630,13 @@ serve${className}Runtime(implementation); }; const main = async () => { - const sourceFile = required(process.argv[2]); - const outDir = required(process.argv[3]); - const implementationImport = process.argv[4] ?? "../task.impl.js"; + const reactOnly = process.argv[2] === "--react-only"; + const offset = reactOnly ? 1 : 0; + const sourceFile = required(process.argv[2 + offset]); + const outDir = required(process.argv[3 + offset]); + const implementationImport = process.argv[4 + offset] ?? "../task.impl.js"; const compiled = await compileCaminoSchema(sourceFile); + const { root } = await loadCaminoSchemaRoot(sourceFile); if (compiled.classes.length !== 1) { throw new Error( `Expected exactly one Camino class in ${sourceFile}, found ${compiled.classes.length}`, @@ -404,11 +648,26 @@ const main = async () => { } fs.mkdirSync(outDir, { recursive: true }); const baseName = `${schema.id.namespace}.${schema.id.name}`; - fs.writeFileSync(path.join(outDir, `${baseName}.runtime.ts`), renderRuntime(schema)); - fs.writeFileSync( - path.join(outDir, `${baseName}.server.ts`), - renderServer(schema, implementationImport), - ); + const reactClient = renderReactClient(schema, root); + if (!reactOnly) { + fs.writeFileSync( + path.join(outDir, `${baseName}.runtime.ts`), + renderRuntime(schema, root), + ); + fs.writeFileSync( + path.join(outDir, `${baseName}.server.ts`), + renderServer(schema, implementationImport), + ); + } + if (reactClient) { + fs.writeFileSync(path.join(outDir, `${baseName}.react.ts`), reactClient); + fs.writeFileSync( + path.join(outDir, "camino-react-runtime.d.ts"), + reactRuntimeDeclaration, + ); + } else if (reactOnly) { + throw new Error(`${schema.id.name} does not implement ReactComponentFor`); + } }; await main(); diff --git a/src/schema-compiler.ts b/src/schema-compiler.ts index 4efa9f7..8eb1038 100644 --- a/src/schema-compiler.ts +++ b/src/schema-compiler.ts @@ -187,6 +187,11 @@ const interfaceFieldContractFromOption = ( return undefined; } const type = typeRefFromOption(object.type, schemaVersion); + const refTargetTypeParam = readString(object, "ref_target_type_param"); + const edgeCardinality = + object.edge_cardinality === undefined + ? undefined + : cardinalityFromOption(object.edge_cardinality); return { required: readBoolean(object, "required") ?? false, readable: readBoolean(object, "readable") ?? false, @@ -196,6 +201,8 @@ const interfaceFieldContractFromOption = ( ? { typeParam: readString(object, "type_param") } : {}), ...(type ? { type } : {}), + ...(refTargetTypeParam ? { refTargetTypeParam } : {}), + ...(edgeCardinality ? { edgeCardinality } : {}), }; }; @@ -854,6 +861,9 @@ const interfaceSchemaFromType = ( ), sourceFile, protoMessage: type.fullName, + typeParameters: asArray(interfaceOption.type_parameter) + .map((entry) => String(entry).trim()) + .filter(Boolean), }; }; @@ -884,11 +894,10 @@ const walkServices = ( } }; -export const compileCaminoSchema = async ( +export const loadCaminoSchemaRoot = async ( sourceFile: string, -): Promise => { +): Promise<{ absoluteSourceFile: string; root: protobuf.Root }> => { const absoluteSourceFile = path.resolve(sourceFile); - const defaults = extractFileDefaults(absoluteSourceFile); const root = new protobuf.Root(); const envIncludeDirs = [ process.env.QUIXOS_PROTO_PATH, @@ -923,6 +932,15 @@ export const compileCaminoSchema = async ( await root.load(absoluteSourceFile, { keepCase: true }); root.resolveAll(); + return { absoluteSourceFile, root }; +}; + +export const compileCaminoSchema = async ( + sourceFile: string, +): Promise => { + const { absoluteSourceFile, root } = await loadCaminoSchemaRoot(sourceFile); + const defaults = extractFileDefaults(absoluteSourceFile); + const operationServices: OperationServiceSchema[] = []; walkServices(root, (service) => { operationServices.push( diff --git a/src/schema-ir.ts b/src/schema-ir.ts index 2d30bcf..5c40a24 100644 --- a/src/schema-ir.ts +++ b/src/schema-ir.ts @@ -69,6 +69,8 @@ export type InterfaceFieldContract = { watchable: boolean; typeParam?: string; type?: TypeRef; + refTargetTypeParam?: string; + edgeCardinality?: Cardinality; }; export type InterfaceImplementation = { @@ -162,6 +164,7 @@ export type InterfaceSchema = { fields: FieldSchema[]; sourceFile: string; protoMessage: string; + typeParameters: string[]; }; export type SchemaCompileResult = {