@shortlink-org/portolan 0.1.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/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A React Flow viewport, saved as a picture.
|
|
2
|
+
//
|
|
3
|
+
// The viewport is captured at its own size rather than the box's, so the file
|
|
4
|
+
// holds the whole drawing however far it has been panned. Every canvas in the
|
|
5
|
+
// app - the flow, the ER schema, the dependency graph, the context map - goes
|
|
6
|
+
// out through here, so a picture of one looks like a picture of another.
|
|
7
|
+
|
|
8
|
+
import { toPng, toSvg } from "html-to-image";
|
|
9
|
+
import { artifactFilename } from "./export-file";
|
|
10
|
+
|
|
11
|
+
export type ImageKind = "png" | "svg";
|
|
12
|
+
|
|
13
|
+
/** The element html-to-image should capture: the pannable layer of a canvas. */
|
|
14
|
+
export function viewportOf(root: Element | null | undefined): HTMLElement | null {
|
|
15
|
+
return root?.querySelector<HTMLElement>(".react-flow__viewport") ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the viewport and hand it to the browser as a download. Resolves to
|
|
20
|
+
* the file name it went out under, so the caller can say so.
|
|
21
|
+
*/
|
|
22
|
+
export async function saveCanvasImage(
|
|
23
|
+
viewport: HTMLElement,
|
|
24
|
+
name: string,
|
|
25
|
+
kind: ImageKind,
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const render = kind === "png" ? toPng : toSvg;
|
|
28
|
+
const url = await render(viewport, {
|
|
29
|
+
backgroundColor: getComputedStyle(document.body).backgroundColor,
|
|
30
|
+
...(kind === "png" ? { pixelRatio: 2 } : {}),
|
|
31
|
+
});
|
|
32
|
+
const a = document.createElement("a");
|
|
33
|
+
a.href = url;
|
|
34
|
+
a.download = artifactFilename(name, kind);
|
|
35
|
+
a.click();
|
|
36
|
+
return a.download;
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { BuildInfo } from "./build-info";
|
|
3
|
+
import { artifactFilename } from "./export-file";
|
|
4
|
+
|
|
5
|
+
const info = (values: Partial<BuildInfo>): BuildInfo => ({
|
|
6
|
+
commit: "",
|
|
7
|
+
shortCommit: "",
|
|
8
|
+
branch: "",
|
|
9
|
+
builtAt: "",
|
|
10
|
+
commitUrl: "",
|
|
11
|
+
buildUrl: "",
|
|
12
|
+
buildNumber: "",
|
|
13
|
+
dirty: false,
|
|
14
|
+
repoUrl: "",
|
|
15
|
+
...values,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("artifactFilename", () => {
|
|
19
|
+
it("prefers the commit and sanitizes the artifact name", () => {
|
|
20
|
+
expect(artifactFilename("checkout / happy path", "svg", info({ shortCommit: "abc1234", branch: "main" }))).toBe(
|
|
21
|
+
"checkout-happy-path-abc1234.svg",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("uses a sanitized branch and safe fallbacks", () => {
|
|
26
|
+
expect(artifactFilename("///", ".png", info({ branch: "feature/export v2" }))).toBe(
|
|
27
|
+
"artifact-feature-export-v2.png",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildInfo } from "./build-info";
|
|
2
|
+
import type { BuildInfo } from "./build-info";
|
|
3
|
+
|
|
4
|
+
function safePart(value: string, fallback: string): string {
|
|
5
|
+
const safe = value
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
8
|
+
.replace(/^-+|-+$/g, "");
|
|
9
|
+
return safe || fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A reproducible export name that identifies both the artifact and its catalog revision. */
|
|
13
|
+
export function artifactFilename(
|
|
14
|
+
name: string,
|
|
15
|
+
extension: string,
|
|
16
|
+
info: BuildInfo = buildInfo,
|
|
17
|
+
): string {
|
|
18
|
+
const revision = info.shortCommit || info.branch || "catalog";
|
|
19
|
+
return `${safePart(name, "artifact")}-${safePart(revision, "catalog")}.${safePart(extension.replace(/^\./, ""), "bin")}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { catalog } from "../testing/estate";
|
|
3
|
+
import type { Flow } from "../catalog";
|
|
4
|
+
import {
|
|
5
|
+
GROUP_SHOW_HEAD,
|
|
6
|
+
flowEntry,
|
|
7
|
+
flowHealth,
|
|
8
|
+
flowOwner,
|
|
9
|
+
groupFlowsByOwner,
|
|
10
|
+
reachDots,
|
|
11
|
+
statusCounts,
|
|
12
|
+
visibleEntries,
|
|
13
|
+
} from "./flow-tree";
|
|
14
|
+
|
|
15
|
+
const bySlug = (slug: string): Flow => {
|
|
16
|
+
const flow = catalog.flows.find((f) => f.slug === slug);
|
|
17
|
+
if (!flow) throw new Error(`no flow ${slug}`);
|
|
18
|
+
return flow;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("flowOwner", () => {
|
|
22
|
+
it("reads the owner off the flow rather than working it out", () => {
|
|
23
|
+
// Every flow states the context it belongs to, because whatever derived it
|
|
24
|
+
// read that context's tree to find it. Nothing here infers.
|
|
25
|
+
expect(flowOwner(bySlug("checkout"))).toBe("shop");
|
|
26
|
+
expect(flowOwner(bySlug("gateway-webhook"))).toBe("payments");
|
|
27
|
+
expect(flowOwner(bySlug("shipment-tracking"))).toBe("delivery");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not fall back to the lanes when a flow names no owner", () => {
|
|
31
|
+
// The first service lane would happily supply one. Guessing here is what
|
|
32
|
+
// the validator exists to stop, so the flow comes back unowned and the
|
|
33
|
+
// tree files it as a defect.
|
|
34
|
+
const unowned = { ...bySlug("checkout"), owner: undefined } as unknown as Flow;
|
|
35
|
+
expect(flowOwner(unowned)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("flowHealth", () => {
|
|
40
|
+
it("is red the moment any step is unresolved", () => {
|
|
41
|
+
expect(flowHealth(bySlug("checkout"))).toBe("unresolved");
|
|
42
|
+
expect(flowHealth(bySlug("order-cancelled"))).toBe("unresolved");
|
|
43
|
+
expect(flowHealth(bySlug("shipment-tracking"))).toBe("unresolved");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is declared while some hop has not been seen running", () => {
|
|
47
|
+
// Written by hand from a design doc: every hop resolves, none observed.
|
|
48
|
+
expect(flowHealth(bySlug("refund-requested"))).toBe("declared");
|
|
49
|
+
// Pinned by a test on three of its four branches; the adopt branch is
|
|
50
|
+
// the one nobody covers, and one unseen message is enough.
|
|
51
|
+
expect(flowHealth(bySlug("gateway-webhook"))).toBe("declared");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("is verified once every call in and every message has been seen", () => {
|
|
55
|
+
// order-accepted is pinned by an integration test end to end; the auth
|
|
56
|
+
// flows by the recording in examples/auth/telemetry, which shows the
|
|
57
|
+
// request arriving and the events leaving. The store calls between stay
|
|
58
|
+
// declared, and do not hold the flow back: a query having run is not the
|
|
59
|
+
// claim the code makes about the repository method.
|
|
60
|
+
expect(flowHealth(bySlug("order-accepted"))).toBe("verified");
|
|
61
|
+
expect(flowHealth(bySlug("auth-register-user"))).toBe("verified");
|
|
62
|
+
expect(flowHealth(bySlug("auth-change-password"))).toBe("verified");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("is not verified by store calls alone", () => {
|
|
66
|
+
const only = { ...bySlug("auth-get-user"), steps: bySlug("auth-get-user").steps.slice(1) } as Flow;
|
|
67
|
+
// Only the ByID call is left, and a call is never a hop a trace vouches for.
|
|
68
|
+
expect(flowHealth(only)).toBe("declared");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("counts steps by status", () => {
|
|
72
|
+
expect(statusCounts(bySlug("auth-login"))).toEqual({ verified: 2, declared: 14, unresolved: 1 });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("flowEntry", () => {
|
|
77
|
+
it("excludes the owner from the reach, so the dots say where else it goes", () => {
|
|
78
|
+
const entry = flowEntry(bySlug("checkout"));
|
|
79
|
+
expect(entry.reach).not.toContain("shop");
|
|
80
|
+
expect(entry.reach).toEqual(["payments", "delivery"]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("groupFlowsByOwner", () => {
|
|
85
|
+
const groups = groupFlowsByOwner(catalog.flows);
|
|
86
|
+
|
|
87
|
+
it("files every flow under a context and leaves none unowned", () => {
|
|
88
|
+
expect(groups.map((g) => g.owner)).not.toContain(null);
|
|
89
|
+
const total = groups.reduce((n, g) => n + g.entries.length, 0);
|
|
90
|
+
expect(total).toBe(catalog.flows.length);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("orders groups by how many flows they hold", () => {
|
|
94
|
+
expect(
|
|
95
|
+
groups.map((g) => [g.owner, g.entries.length] as const),
|
|
96
|
+
).toEqual([
|
|
97
|
+
["shop", 11],
|
|
98
|
+
["auth", 7],
|
|
99
|
+
["delivery", 1],
|
|
100
|
+
["payments", 1],
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("puts the broken flows at the top of their group", () => {
|
|
105
|
+
const shop = groups.find((g) => g.owner === "shop");
|
|
106
|
+
if (!shop) throw new Error("no shop group");
|
|
107
|
+
// Two hand-written flows with a hop nobody resolves; then the ones every
|
|
108
|
+
// hop of which resolves but none has been seen running, cart's checkout
|
|
109
|
+
// among them because its call to pricing is answered by a stand-in; then
|
|
110
|
+
// the ones the recordings proved.
|
|
111
|
+
expect(shop.entries.map((e) => e.health)).toEqual([
|
|
112
|
+
"unresolved",
|
|
113
|
+
"unresolved",
|
|
114
|
+
"declared",
|
|
115
|
+
"declared",
|
|
116
|
+
"declared",
|
|
117
|
+
"verified",
|
|
118
|
+
"verified",
|
|
119
|
+
"verified",
|
|
120
|
+
"verified",
|
|
121
|
+
"verified",
|
|
122
|
+
"verified",
|
|
123
|
+
]);
|
|
124
|
+
// Red first, then name: the two unresolved flows are alphabetical.
|
|
125
|
+
expect(shop.entries.slice(0, 2).map((e) => e.flow.name)).toEqual([
|
|
126
|
+
"Checkout",
|
|
127
|
+
"Order cancelled",
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("sends the unowned group to the end, where it reads as a defect", () => {
|
|
132
|
+
const orphan = {
|
|
133
|
+
...bySlug("checkout"),
|
|
134
|
+
id: "flow.orphan",
|
|
135
|
+
slug: "orphan",
|
|
136
|
+
owner: undefined,
|
|
137
|
+
} as unknown as Flow;
|
|
138
|
+
const withOrphan = groupFlowsByOwner([orphan, ...catalog.flows]);
|
|
139
|
+
expect(withOrphan[withOrphan.length - 1]?.owner).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("visibleEntries", () => {
|
|
144
|
+
const entries = (n: number) =>
|
|
145
|
+
Array.from({ length: n }, (_, i) => ({
|
|
146
|
+
...flowEntry(bySlug("checkout")),
|
|
147
|
+
flow: { ...bySlug("checkout"), slug: `f${i}` },
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
it("lists a group whole while it still reads as a list", () => {
|
|
151
|
+
const eight = entries(8);
|
|
152
|
+
expect(visibleEntries(eight)).toEqual({ shown: eight, hidden: 0 });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("stops listing past the limit and says how many it stopped at", () => {
|
|
156
|
+
const nine = entries(9);
|
|
157
|
+
const { shown, hidden } = visibleEntries(nine);
|
|
158
|
+
expect(shown).toHaveLength(GROUP_SHOW_HEAD);
|
|
159
|
+
expect(hidden).toBe(4);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("reachDots", () => {
|
|
164
|
+
it("draws every context when three or fewer", () => {
|
|
165
|
+
expect(reachDots(["a", "b", "c"])).toEqual({
|
|
166
|
+
dots: ["a", "b", "c"],
|
|
167
|
+
more: 0,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("counts the rest rather than drawing an unreadable row of dots", () => {
|
|
172
|
+
expect(reachDots(["a", "b", "c", "d", "e"])).toEqual({
|
|
173
|
+
dots: ["a", "b", "c"],
|
|
174
|
+
more: 2,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Who a flow belongs to, and how far it can be trusted.
|
|
2
|
+
//
|
|
3
|
+
// The sidebar files flows under a context rather than listing them flat,
|
|
4
|
+
// because "which of our flows cross into payments" is a question about the
|
|
5
|
+
// estate and "how many flows does shop own" is a question about a team. The
|
|
6
|
+
// owner is never guessed: whatever derived the flow already knew whose tree it
|
|
7
|
+
// read, and this module only reads the answer back out.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here reads the DOM or the router, so every group, dot and count the
|
|
10
|
+
// tree draws can be asserted in a test.
|
|
11
|
+
|
|
12
|
+
import type { Flow, Status } from "../catalog";
|
|
13
|
+
import { flowContexts, walkSteps } from "../catalog";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How far a flow can be believed, in one word.
|
|
17
|
+
*
|
|
18
|
+
* Three states. A flow read out of source is in one of two: either every step
|
|
19
|
+
* resolves, or one points at something the catalog does not have. The third
|
|
20
|
+
* is reached only by a recording of the system running - a verifier that saw
|
|
21
|
+
* the hops happen - and it is held to the hops a trace can show: the call in,
|
|
22
|
+
* and every message between services. A `call` on a store is never raised by
|
|
23
|
+
* a trace, because a query having run is not the claim the code makes, so a
|
|
24
|
+
* flow whose every rpc and event is verified is as verified as a flow gets.
|
|
25
|
+
*/
|
|
26
|
+
export type FlowHealth = "verified" | "declared" | "unresolved";
|
|
27
|
+
|
|
28
|
+
/** Order of attention: a broken flow first, a proven one last. */
|
|
29
|
+
const HEALTH_RANK: Record<FlowHealth, number> = {
|
|
30
|
+
unresolved: 0,
|
|
31
|
+
declared: 1,
|
|
32
|
+
verified: 2,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const FLOW_HEALTH_NOTE: Record<FlowHealth, string> = {
|
|
36
|
+
unresolved: "a step in this flow points at something the catalog does not have",
|
|
37
|
+
declared: "every step resolves to something the catalog has; none has been seen running",
|
|
38
|
+
verified: "every call in and every message between services has been seen running",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function flowHealth(flow: Flow): FlowHealth {
|
|
42
|
+
const steps = walkSteps(flow.steps);
|
|
43
|
+
const statuses: Status[] = steps.map((s) => s.status);
|
|
44
|
+
if (statuses.some((s) => s === "unresolved")) return "unresolved";
|
|
45
|
+
const hops = steps.filter((s) => s.kind !== "call");
|
|
46
|
+
if (hops.length > 0 && hops.every((s) => s.status === "verified"))
|
|
47
|
+
return "verified";
|
|
48
|
+
return "declared";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** How many steps of each status, for a card or a row that counts rather than dots. */
|
|
52
|
+
export function statusCounts(flow: Flow): Record<Status, number> {
|
|
53
|
+
const counts: Record<Status, number> = { verified: 0, declared: 0, unresolved: 0 };
|
|
54
|
+
for (const step of walkSteps(flow.steps)) counts[step.status] += 1;
|
|
55
|
+
return counts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The context a flow belongs to, or null when the catalog does not say.
|
|
60
|
+
*
|
|
61
|
+
* Read off the flow, never worked out from it. A flow that names no owner is a
|
|
62
|
+
* broken extraction, not a flow that belongs wherever its first participant
|
|
63
|
+
* happens to sit - so it comes back null and the tree shows it rather than
|
|
64
|
+
* filing it somewhere plausible.
|
|
65
|
+
*/
|
|
66
|
+
export function flowOwner(flow: Flow): string | null {
|
|
67
|
+
return flow.owner ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface FlowEntry {
|
|
71
|
+
flow: Flow;
|
|
72
|
+
health: FlowHealth;
|
|
73
|
+
/** Contexts the flow crosses beyond the one that owns it, in lane order. */
|
|
74
|
+
reach: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface FlowGroup {
|
|
78
|
+
/** Context id, or null for the group of flows nothing claims. */
|
|
79
|
+
owner: string | null;
|
|
80
|
+
entries: FlowEntry[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function flowEntry(flow: Flow): FlowEntry {
|
|
84
|
+
const owner = flowOwner(flow);
|
|
85
|
+
return {
|
|
86
|
+
flow,
|
|
87
|
+
health: flowHealth(flow),
|
|
88
|
+
reach: flowContexts(flow).filter((c) => c !== owner),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Flows filed under their owner. Groups are ordered by how many flows they
|
|
94
|
+
* hold, so the context the estate does most of its work in is the one at the
|
|
95
|
+
* top; the unowned group is always last, because it is a defect report rather
|
|
96
|
+
* than a place.
|
|
97
|
+
*
|
|
98
|
+
* Inside a group the order is by health and then by name: a broken flow is the
|
|
99
|
+
* one worth opening, and everything below it is alphabetical so a reader can
|
|
100
|
+
* find a name they already know.
|
|
101
|
+
*/
|
|
102
|
+
export function groupFlowsByOwner(flows: Flow[]): FlowGroup[] {
|
|
103
|
+
const byOwner = new Map<string | null, FlowEntry[]>();
|
|
104
|
+
for (const flow of flows) {
|
|
105
|
+
const entry = flowEntry(flow);
|
|
106
|
+
const owner = flowOwner(flow);
|
|
107
|
+
const list = byOwner.get(owner);
|
|
108
|
+
if (list) list.push(entry);
|
|
109
|
+
else byOwner.set(owner, [entry]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const groups: FlowGroup[] = [...byOwner].map(([owner, entries]) => ({
|
|
113
|
+
owner,
|
|
114
|
+
entries: [...entries].sort(
|
|
115
|
+
(a, b) =>
|
|
116
|
+
HEALTH_RANK[a.health] - HEALTH_RANK[b.health] ||
|
|
117
|
+
a.flow.name.localeCompare(b.flow.name),
|
|
118
|
+
),
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
groups.sort((a, b) => {
|
|
122
|
+
if (a.owner === null) return 1;
|
|
123
|
+
if (b.owner === null) return -1;
|
|
124
|
+
return b.entries.length - a.entries.length || a.owner.localeCompare(b.owner);
|
|
125
|
+
});
|
|
126
|
+
return groups;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How many flows a group shows before it stops listing and starts counting.
|
|
131
|
+
* Past this the tree is no longer something a reader scans - the index page
|
|
132
|
+
* is, and the last row says so.
|
|
133
|
+
*/
|
|
134
|
+
const GROUP_SHOW_LIMIT = 8;
|
|
135
|
+
export const GROUP_SHOW_HEAD = 5;
|
|
136
|
+
|
|
137
|
+
/** The rows a group actually draws, and how many it is standing in for. */
|
|
138
|
+
export function visibleEntries(entries: FlowEntry[]): {
|
|
139
|
+
shown: FlowEntry[];
|
|
140
|
+
hidden: number;
|
|
141
|
+
} {
|
|
142
|
+
if (entries.length <= GROUP_SHOW_LIMIT) {
|
|
143
|
+
return { shown: entries, hidden: 0 };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
shown: entries.slice(0, GROUP_SHOW_HEAD),
|
|
147
|
+
hidden: entries.length - GROUP_SHOW_HEAD,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Up to three reach dots, and what the "+n" after them stands for. */
|
|
152
|
+
const REACH_DOTS = 3;
|
|
153
|
+
|
|
154
|
+
export function reachDots(reach: string[]): {
|
|
155
|
+
dots: string[];
|
|
156
|
+
more: number;
|
|
157
|
+
} {
|
|
158
|
+
if (reach.length <= REACH_DOTS) return { dots: reach, more: 0 };
|
|
159
|
+
return { dots: reach.slice(0, REACH_DOTS), more: reach.length - REACH_DOTS };
|
|
160
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ForgeRef } from "./github-catalog";
|
|
3
|
+
import { compareVersionsDesc, findRef, parseVersion, sortRefs } from "./forge-refs";
|
|
4
|
+
|
|
5
|
+
const ref = (name: string, kind: ForgeRef["kind"] = "branch"): ForgeRef => ({
|
|
6
|
+
name, kind, commit: "a".repeat(40), protected: false,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("parseVersion", () => {
|
|
10
|
+
it("reads a version out of the usual tag spellings", () => {
|
|
11
|
+
expect(parseVersion("v1.2.3")).toEqual({ parts: [1, 2, 3], pre: "" });
|
|
12
|
+
expect(parseVersion("release-2.0")).toEqual({ parts: [2, 0], pre: "" });
|
|
13
|
+
expect(parseVersion("1.0.0-rc.1")).toEqual({ parts: [1, 0, 0], pre: "rc.1" });
|
|
14
|
+
expect(parseVersion("nightly")).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("compareVersionsDesc", () => {
|
|
19
|
+
it("puts the newest first and a prerelease before its release", () => {
|
|
20
|
+
const sorted = ["v1.2.0", "v1.10.0", "v1.2.0-rc.1", "v1.9.3"].sort(compareVersionsDesc);
|
|
21
|
+
expect(sorted).toEqual(["v1.10.0", "v1.9.3", "v1.2.0", "v1.2.0-rc.1"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("keeps tags that are not versions after those that are", () => {
|
|
25
|
+
expect(["nightly", "v0.1.0", "archive"].sort(compareVersionsDesc)).toEqual(["v0.1.0", "nightly", "archive"]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("sortRefs", () => {
|
|
30
|
+
it("leads with the current branch and main, then names, then tags newest first", () => {
|
|
31
|
+
const sorted = sortRefs([
|
|
32
|
+
ref("v1.0.0", "tag"), ref("feature/b"), ref("main"), ref("v2.0.0", "tag"), ref("feature/a"), ref("release"),
|
|
33
|
+
], "release");
|
|
34
|
+
expect(sorted.map((r) => r.name)).toEqual(["release", "main", "feature/a", "feature/b", "v2.0.0", "v1.0.0"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not let a tag named like the current branch jump the queue", () => {
|
|
38
|
+
const sorted = sortRefs([ref("main", "tag"), ref("dev"), ref("main")], "main");
|
|
39
|
+
expect(sorted.map((r) => r.kind)).toEqual(["branch", "branch", "tag"]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("findRef", () => {
|
|
44
|
+
it("prefers the branch when a tag shares its name", () => {
|
|
45
|
+
const refs = [ref("v1", "tag"), ref("v1")];
|
|
46
|
+
expect(findRef(refs, "v1")?.kind).toBe("branch");
|
|
47
|
+
expect(findRef([ref("v1", "tag")], "v1")?.kind).toBe("tag");
|
|
48
|
+
expect(findRef(refs, "v2")).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The order a reader picks a comparison head in, and how a name is resolved.
|
|
2
|
+
//
|
|
3
|
+
// Branches are what is being worked on and tags are what was shipped, and
|
|
4
|
+
// they read differently: a branch list is scanned by name, a tag list from the
|
|
5
|
+
// newest release down. So branches sort by name after the two a reader
|
|
6
|
+
// reaches for first - the catalog's own branch, then main - and tags sort by
|
|
7
|
+
// version, highest first, with anything that is not a version after them.
|
|
8
|
+
|
|
9
|
+
import type { ForgeRef } from "./github-catalog";
|
|
10
|
+
|
|
11
|
+
type Version = { parts: number[]; pre: string };
|
|
12
|
+
|
|
13
|
+
/** `v1.2.3`, `1.2`, `release-2.0.1-rc.1` as numbers, or null when it is not one. */
|
|
14
|
+
export function parseVersion(name: string): Version | null {
|
|
15
|
+
const m = /(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?$/.exec(name);
|
|
16
|
+
if (!m) return null;
|
|
17
|
+
return { parts: m[1]!.split(".").map(Number), pre: m[2] ?? "" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Newest first. A prerelease comes before the release it precedes. */
|
|
21
|
+
export function compareVersionsDesc(a: string, b: string): number {
|
|
22
|
+
const va = parseVersion(a);
|
|
23
|
+
const vb = parseVersion(b);
|
|
24
|
+
if (!va || !vb) {
|
|
25
|
+
if (va) return -1;
|
|
26
|
+
if (vb) return 1;
|
|
27
|
+
return b.localeCompare(a);
|
|
28
|
+
}
|
|
29
|
+
const length = Math.max(va.parts.length, vb.parts.length);
|
|
30
|
+
for (let i = 0; i < length; i++) {
|
|
31
|
+
const d = (vb.parts[i] ?? 0) - (va.parts[i] ?? 0);
|
|
32
|
+
if (d !== 0) return d;
|
|
33
|
+
}
|
|
34
|
+
if (va.pre && !vb.pre) return 1;
|
|
35
|
+
if (!va.pre && vb.pre) return -1;
|
|
36
|
+
return vb.pre.localeCompare(va.pre) || b.localeCompare(a);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The picker's order: current, main, other branches by name, tags newest first. */
|
|
40
|
+
export function sortRefs(refs: ForgeRef[], current: string): ForgeRef[] {
|
|
41
|
+
const rank = (ref: ForgeRef) =>
|
|
42
|
+
ref.name === current && ref.kind === "branch" ? 0
|
|
43
|
+
: ref.name === "main" && ref.kind === "branch" ? 1
|
|
44
|
+
: ref.kind === "branch" ? 2
|
|
45
|
+
: 3;
|
|
46
|
+
return [...refs].sort((a, b) => {
|
|
47
|
+
const d = rank(a) - rank(b);
|
|
48
|
+
if (d !== 0) return d;
|
|
49
|
+
return a.kind === "tag" ? compareVersionsDesc(a.name, b.name) : a.name.localeCompare(b.name);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The ref a URL names. A tag and a branch may share a name, and the branch
|
|
55
|
+
* wins, because that is what git itself resolves a bare name to.
|
|
56
|
+
*/
|
|
57
|
+
export function findRef(refs: ForgeRef[], name: string): ForgeRef | undefined {
|
|
58
|
+
return refs.find((ref) => ref.name === name && ref.kind === "branch")
|
|
59
|
+
?? refs.find((ref) => ref.name === name);
|
|
60
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { absoluteTime, plural, relativeTime } from "./format";
|
|
3
|
+
|
|
4
|
+
const now = new Date("2026-08-31T12:00:00Z");
|
|
5
|
+
|
|
6
|
+
describe("relativeTime", () => {
|
|
7
|
+
it("collapses recent times", () => {
|
|
8
|
+
expect(relativeTime("2026-08-31T11:59:50Z", now)).toBe("just now");
|
|
9
|
+
});
|
|
10
|
+
it("counts minutes, hours, days", () => {
|
|
11
|
+
expect(relativeTime("2026-08-31T11:00:00Z", now)).toBe("1 hour ago");
|
|
12
|
+
expect(relativeTime("2026-08-29T12:00:00Z", now)).toBe("2 days ago");
|
|
13
|
+
});
|
|
14
|
+
it("passes through unparseable input", () => {
|
|
15
|
+
expect(relativeTime("not-a-date", now)).toBe("not-a-date");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("absoluteTime", () => {
|
|
20
|
+
it("renders a compact UTC stamp", () => {
|
|
21
|
+
expect(absoluteTime("2026-08-29T09:14:22Z")).toBe("2026-08-29 09:14:22Z");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("plural", () => {
|
|
26
|
+
it("keeps the singular at exactly one", () => {
|
|
27
|
+
expect(plural(1, "service")).toBe("service");
|
|
28
|
+
expect(plural(0, "service")).toBe("services");
|
|
29
|
+
expect(plural(2, "service")).toBe("services");
|
|
30
|
+
});
|
|
31
|
+
it("takes an irregular plural", () => {
|
|
32
|
+
expect(plural(1, "entity", "entities")).toBe("entity");
|
|
33
|
+
expect(plural(3, "entity", "entities")).toBe("entities");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** "3 days ago" style, coarse on purpose: precision here is noise. */
|
|
2
|
+
export function relativeTime(iso: string, now: Date = new Date()): string {
|
|
3
|
+
const then = new Date(iso).getTime();
|
|
4
|
+
if (Number.isNaN(then)) return iso;
|
|
5
|
+
const seconds = Math.round((now.getTime() - then) / 1000);
|
|
6
|
+
const future = seconds < 0;
|
|
7
|
+
const abs = Math.abs(seconds);
|
|
8
|
+
const units: [number, string][] = [
|
|
9
|
+
[60, "second"],
|
|
10
|
+
[3600, "minute"],
|
|
11
|
+
[86400, "hour"],
|
|
12
|
+
[604800, "day"],
|
|
13
|
+
[2629800, "week"],
|
|
14
|
+
[31557600, "month"],
|
|
15
|
+
[Number.POSITIVE_INFINITY, "year"],
|
|
16
|
+
];
|
|
17
|
+
let value = abs;
|
|
18
|
+
let label = "second";
|
|
19
|
+
let prev = 1;
|
|
20
|
+
for (const [limit, name] of units) {
|
|
21
|
+
if (abs < limit) {
|
|
22
|
+
value = Math.floor(abs / prev);
|
|
23
|
+
label = name;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
prev = limit;
|
|
27
|
+
label = name;
|
|
28
|
+
}
|
|
29
|
+
if (label === "second" && value < 45)
|
|
30
|
+
return future ? "in a moment" : "just now";
|
|
31
|
+
const plural = value === 1 ? label : `${label}s`;
|
|
32
|
+
return future ? `in ${value} ${plural}` : `${value} ${plural} ago`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function absoluteTime(iso: string): string {
|
|
36
|
+
const d = new Date(iso);
|
|
37
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
38
|
+
return d
|
|
39
|
+
.toISOString()
|
|
40
|
+
.replace("T", " ")
|
|
41
|
+
.replace(/\.\d+Z$/, "Z");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Middle truncation for file paths: the repo tells you where you are, the
|
|
46
|
+
* basename tells you what it is, and the middle is the part nobody reads.
|
|
47
|
+
* Returns the input untouched when it already fits.
|
|
48
|
+
*/
|
|
49
|
+
export function middleTruncate(value: string, max = 44): string {
|
|
50
|
+
if (value.length <= max) return value;
|
|
51
|
+
const keep = max - 1;
|
|
52
|
+
const head = Math.ceil(keep / 2);
|
|
53
|
+
const tail = Math.floor(keep / 2);
|
|
54
|
+
return `${value.slice(0, head)}…${value.slice(value.length - tail)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The noun that goes with a count. A first catalog is full of ones - one
|
|
59
|
+
* context, one service, one aggregate - and "1 services" is the first thing
|
|
60
|
+
* that makes the page look generated rather than written.
|
|
61
|
+
*/
|
|
62
|
+
export function plural(n: number, one: string, many = `${one}s`): string {
|
|
63
|
+
return n === 1 ? one : many;
|
|
64
|
+
}
|