@shortlink-org/portolan 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +22 -4
- package/scripts/local-api.test.mjs +84 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +6 -2
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +12 -8
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { InitError, defaultAnswers, init } from "./init.mjs";
|
|
8
|
+
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const created = [];
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
process.env.PORTOLAN_SCHEMA = resolve(root, "schema/portolan.schema.json");
|
|
14
|
+
process.env.PORTOLAN_INSTALL_ROOT = root;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const path of created.splice(0)) rmSync(path, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function workspace(files) {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), "portolan-init-"));
|
|
23
|
+
created.push(dir);
|
|
24
|
+
for (const [name, content] of Object.entries(files)) {
|
|
25
|
+
mkdirSync(dirname(join(dir, name)), { recursive: true });
|
|
26
|
+
writeFileSync(join(dir, name), content);
|
|
27
|
+
}
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const quiet = { version: "0.0.0-test", log: () => {}, ask: { ...defaultAnswers, warn: () => {} } };
|
|
32
|
+
const manifestOf = (dir) => JSON.parse(readFileSync(join(dir, "portolan.json"), "utf8"));
|
|
33
|
+
const goService = {
|
|
34
|
+
"go.mod": "module example.com/orders\n\ngo 1.24\n",
|
|
35
|
+
"internal/domain/order/order.go": "package order\n\ntype Order struct{ ID string }\n",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("init with defaults", () => {
|
|
39
|
+
it("reads what the repository contains into one project at the root", async () => {
|
|
40
|
+
const dir = workspace({
|
|
41
|
+
"package.json": '{"name":"@acme/order-service","scripts":{"test":"vitest"}}\n',
|
|
42
|
+
...goService,
|
|
43
|
+
"api/openapi.yaml": "openapi: 3.0.0\ninfo:\n title: Orders\n version: '1'\npaths: {}\n",
|
|
44
|
+
"docs/adr/0001-free-form.md": "# Not an ADR Portolan understands\n",
|
|
45
|
+
});
|
|
46
|
+
const result = await init(dir, quiet);
|
|
47
|
+
const manifest = manifestOf(dir);
|
|
48
|
+
|
|
49
|
+
expect(manifest.projects).toEqual([{ id: "order-service", name: "Order Service", root: ".", group: "order-service", component: "order-service" }]);
|
|
50
|
+
expect(manifest.sources).toEqual(["portolan/*.json"]);
|
|
51
|
+
expect(manifest.extract.map((step) => step.plugin)).toEqual(["project", "go-domain", "openapi"]);
|
|
52
|
+
expect(manifest.extract.every((step) => step.in === "." && step.out === "portolan")).toBe(true);
|
|
53
|
+
expect(manifest.extract[0].options).toMatchObject({ groupKind: "bounded-context", componentKind: "service" });
|
|
54
|
+
expect(manifest.extract[2].options.spec).toBe("api/openapi.yaml");
|
|
55
|
+
expect(manifest.generate.map((step) => step.plugin)).toEqual(["markdown", "mermaid"]);
|
|
56
|
+
expect(result.generate).toBe(false);
|
|
57
|
+
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
59
|
+
expect(pkg.scripts).toEqual({ test: "vitest", "architecture": "portolan dev", "architecture:gen": "portolan generate", "architecture:check": "portolan check", "architecture:build": "portolan build" });
|
|
60
|
+
expect(readFileSync(join(dir, ".gitignore"), "utf8")).toContain(".portolan/\n");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("turns nested build files into one project each when the root has none", async () => {
|
|
64
|
+
const dir = workspace({
|
|
65
|
+
"README.md": "# Estate\n",
|
|
66
|
+
"services/orders/go.mod": goService["go.mod"],
|
|
67
|
+
"services/orders/internal/domain/order/order.go": goService["internal/domain/order/order.go"],
|
|
68
|
+
"services/web/package.json": '{"name":"web"}\n',
|
|
69
|
+
});
|
|
70
|
+
await init(dir, quiet);
|
|
71
|
+
const manifest = manifestOf(dir);
|
|
72
|
+
const repository = manifest.projects[0].group;
|
|
73
|
+
|
|
74
|
+
expect(manifest.projects.map((project) => [project.id, project.root, project.component])).toEqual([
|
|
75
|
+
["orders", "services/orders", "orders"],
|
|
76
|
+
["web", "services/web", "web"],
|
|
77
|
+
]);
|
|
78
|
+
expect(manifest.projects.every((project) => project.group === repository)).toBe(true);
|
|
79
|
+
expect(manifest.sources).toEqual(["services/orders/portolan/*.json", "services/web/portolan/*.json"]);
|
|
80
|
+
expect(manifest.extract.map((step) => [step.plugin, step.in])).toEqual([
|
|
81
|
+
["project", "services/orders"],
|
|
82
|
+
["go-domain", "services/orders"],
|
|
83
|
+
["project", "services/web"],
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps the root as the project when it has a build file of its own", async () => {
|
|
88
|
+
const dir = workspace({
|
|
89
|
+
"package.json": '{"name":"tool"}\n',
|
|
90
|
+
"examples/demo/go.mod": goService["go.mod"],
|
|
91
|
+
});
|
|
92
|
+
await init(dir, quiet);
|
|
93
|
+
expect(manifestOf(dir).projects.map((project) => project.root)).toEqual(["."]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("still writes a manifest for an empty directory", async () => {
|
|
97
|
+
const dir = workspace({});
|
|
98
|
+
await init(dir, quiet);
|
|
99
|
+
const manifest = manifestOf(dir);
|
|
100
|
+
expect(manifest.extract.map((step) => step.plugin)).toEqual(["project"]);
|
|
101
|
+
expect(manifest.projects[0].root).toBe(".");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("refuses to touch an existing manifest", async () => {
|
|
105
|
+
const dir = workspace({ "portolan.json": "{}\n" });
|
|
106
|
+
await expect(init(dir, quiet)).rejects.toThrow(InitError);
|
|
107
|
+
expect(readFileSync(join(dir, "portolan.json"), "utf8")).toBe("{}\n");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("init with answers", () => {
|
|
112
|
+
it("writes only the roots, plugins and identity that were chosen", async () => {
|
|
113
|
+
const dir = workspace({
|
|
114
|
+
"README.md": "# Estate\n",
|
|
115
|
+
"services/orders/go.mod": goService["go.mod"],
|
|
116
|
+
"services/orders/internal/domain/order/order.go": goService["internal/domain/order/order.go"],
|
|
117
|
+
"services/web/package.json": '{"name":"web"}\n',
|
|
118
|
+
});
|
|
119
|
+
const asked = [];
|
|
120
|
+
const result = await init(dir, {
|
|
121
|
+
...quiet,
|
|
122
|
+
ask: {
|
|
123
|
+
...quiet.ask,
|
|
124
|
+
roots: (choices) => { asked.push(["roots", choices.map((choice) => [choice.path, choice.selected])]); return ["services/orders"]; },
|
|
125
|
+
plugins: (root, detections) => { asked.push(["plugins", root, detections.map((detection) => detection.plugin)]); return ["project"]; },
|
|
126
|
+
scripts: () => false,
|
|
127
|
+
generate: () => true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const manifest = manifestOf(dir);
|
|
131
|
+
|
|
132
|
+
expect(asked).toEqual([
|
|
133
|
+
["roots", [[".", false], ["services/orders", true], ["services/web", true]]],
|
|
134
|
+
["plugins", "services/orders", ["project", "go-domain"]],
|
|
135
|
+
]);
|
|
136
|
+
expect(manifest.projects.map((project) => project.id)).toEqual(["orders"]);
|
|
137
|
+
expect(manifest.extract.map((step) => step.plugin)).toEqual(["project"]);
|
|
138
|
+
expect(result.generate).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("uses the id the user typed for a single project", async () => {
|
|
142
|
+
const dir = workspace({ "package.json": '{"name":"@acme/whatever"}\n' });
|
|
143
|
+
await init(dir, {
|
|
144
|
+
...quiet,
|
|
145
|
+
ask: { ...quiet.ask, identity: (defaults) => ({ ...defaults, id: "orders", name: "Orders", group: "orders", component: "orders" }) },
|
|
146
|
+
});
|
|
147
|
+
const manifest = manifestOf(dir);
|
|
148
|
+
expect(manifest.projects[0]).toMatchObject({ id: "orders", name: "Orders" });
|
|
149
|
+
expect(manifest.extract[0].options).toMatchObject({ group: "orders", component: "orders", componentName: "Orders" });
|
|
150
|
+
expect(manifest.generate[0].options.title).toBe("Orders");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("writes nothing when the user declines", async () => {
|
|
154
|
+
const dir = workspace({ "package.json": '{"name":"x"}\n' });
|
|
155
|
+
await expect(init(dir, { ...quiet, ask: { ...quiet.ask, write: () => false } })).rejects.toThrow(/cancelled/);
|
|
156
|
+
expect(() => readFileSync(join(dir, "portolan.json"))).toThrow();
|
|
157
|
+
});
|
|
158
|
+
});
|
package/cli/portolan.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import { glob } from "node:fs/promises";
|
|
|
18
18
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
|
+
import { InitError, commandWorks, init as runInit, isInteractive, promptAnswers, toolchainFor } from "./init.mjs";
|
|
22
|
+
|
|
21
23
|
const installRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
24
|
const packageJson = JSON.parse(readFileSync(resolve(installRoot, "package.json"), "utf8"));
|
|
23
25
|
|
|
@@ -84,7 +86,7 @@ function help() {
|
|
|
84
86
|
Usage: portolan <command> [options]
|
|
85
87
|
|
|
86
88
|
Commands:
|
|
87
|
-
init
|
|
89
|
+
init inspect this repository and create portolan.json
|
|
88
90
|
dev open the local architecture site
|
|
89
91
|
generate update catalog fragments, docs, and exports
|
|
90
92
|
check fail when committed generated files are out of date
|
|
@@ -99,73 +101,48 @@ Options:
|
|
|
99
101
|
--base PATH deployed URL base (default: /)
|
|
100
102
|
--host HOST dev server host (default: 127.0.0.1)
|
|
101
103
|
--port PORT dev server port
|
|
102
|
-
--yes, -y
|
|
104
|
+
--yes, -y init without questions: take every detected default`);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
function init(workspace) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const manifest = {
|
|
114
|
-
$schema: `https://raw.githubusercontent.com/shortlink-org/portolan/${VERSION}/schema/portolan.schema.json`,
|
|
115
|
-
sources: ["portolan/*.json"],
|
|
116
|
-
projects: [{ id, name: title, root: ".", group: id, component: id }],
|
|
117
|
-
extract: [{
|
|
118
|
-
plugin: "project",
|
|
119
|
-
in: ".",
|
|
120
|
-
out: "portolan",
|
|
121
|
-
options: { group: id, groupName: title, component: id, componentName: title },
|
|
122
|
-
}],
|
|
123
|
-
generate: [
|
|
124
|
-
{ plugin: "markdown", out: "docs", options: { title } },
|
|
125
|
-
{ plugin: "mermaid", out: "exports/mermaid", options: { title: `${title} flows` } },
|
|
126
|
-
],
|
|
127
|
-
};
|
|
128
|
-
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
|
|
129
|
-
|
|
130
|
-
if (projectPackage) {
|
|
131
|
-
const scripts = { ...(projectPackage.scripts ?? {}) };
|
|
132
|
-
const defaults = {
|
|
133
|
-
"architecture": "portolan dev",
|
|
134
|
-
"architecture:gen": "portolan generate",
|
|
135
|
-
"architecture:check": "portolan check",
|
|
136
|
-
"architecture:build": "portolan build",
|
|
137
|
-
};
|
|
138
|
-
for (const [name, command] of Object.entries(defaults)) {
|
|
139
|
-
if (!(name in scripts)) scripts[name] = command;
|
|
140
|
-
}
|
|
141
|
-
writeFileSync(resolve(workspace, "package.json"), `${JSON.stringify({ ...projectPackage, scripts }, null, 2)}\n`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const ignorePath = resolve(workspace, ".gitignore");
|
|
145
|
-
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
146
|
-
if (!ignore.split(/\r?\n/).includes(".portolan/")) {
|
|
147
|
-
writeFileSync(ignorePath, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}\n# Portolan local build state\n.portolan/\n`);
|
|
107
|
+
async function init(workspace, options) {
|
|
108
|
+
const interactive = !options.yes && isInteractive();
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = await runInit(workspace, { version: VERSION, ask: interactive ? promptAnswers(VERSION) : undefined });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof InitError) fail(error.message);
|
|
114
|
+
throw error;
|
|
148
115
|
}
|
|
149
|
-
|
|
150
|
-
console.log("created portolan.json");
|
|
151
|
-
if (projectPackage) console.log("added architecture scripts to package.json");
|
|
152
|
-
console.log("next: portolan generate && portolan dev");
|
|
116
|
+
if (result.generate) runScript("scripts/gen.mjs", [], workspace);
|
|
153
117
|
}
|
|
154
118
|
|
|
155
119
|
function doctor(workspace) {
|
|
120
|
+
const manifestPath = resolve(workspace, "portolan.json");
|
|
121
|
+
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : null;
|
|
156
122
|
const checks = [
|
|
157
123
|
["Node.js >= 24", Number(process.versions.node.split(".")[0]) >= 24, process.version],
|
|
158
|
-
["portolan.json",
|
|
124
|
+
["portolan.json", Boolean(manifest), "required"],
|
|
159
125
|
["Git", commandWorks("git", ["--version"]), "used for deterministic source stamps"],
|
|
160
|
-
["Go", commandWorks("go", ["version"]), "required by built-in Go extractors"],
|
|
161
|
-
["Python 3", commandWorks("python3", ["--version"]), "required by Python extractors"],
|
|
162
|
-
["Java", commandWorks("java", ["-version"]), "required by the Java extractor"],
|
|
163
|
-
["Cargo", commandWorks("cargo", ["--version"]), "required by the Rust extractor"],
|
|
164
126
|
];
|
|
127
|
+
|
|
128
|
+
// A toolchain is required only when a step in this manifest names a plugin
|
|
129
|
+
// that runs in it; the built-in Go plugins run as wasm and ask for nothing.
|
|
130
|
+
const needed = new Map();
|
|
131
|
+
const steps = manifest ? ["extract", "verify", "generate"].flatMap((phase) => manifest[phase] ?? []) : [];
|
|
132
|
+
for (const name of new Set(steps.map((step) => step.plugin))) {
|
|
133
|
+
const need = toolchainFor(name, (manifest.plugins ?? []).find((plugin) => plugin.name === name));
|
|
134
|
+
if (need) needed.set(need.label, [...(needed.get(need.label) ?? []), name]);
|
|
135
|
+
}
|
|
136
|
+
for (const [label, command, args] of [["Go", "go", ["version"]], ["Python 3", "python3", ["--version"]], ["Java", "java", ["-version"]], ["Cargo", "cargo", ["--version"]]]) {
|
|
137
|
+
const plugins = needed.get(label);
|
|
138
|
+
const present = commandWorks(command, args);
|
|
139
|
+
checks.push([label, present || !plugins, plugins ? `needed by ${plugins.join(", ")}${present ? "" : "; not on PATH"}` : `${present ? "present" : "absent"}; nothing in portolan.json needs it`]);
|
|
140
|
+
}
|
|
141
|
+
|
|
165
142
|
for (const [label, ok, note] of checks) {
|
|
166
143
|
console.log(`${ok ? "ok" : "--"} ${label}${note ? ` — ${note}` : ""}`);
|
|
167
144
|
}
|
|
168
|
-
if (
|
|
145
|
+
if (checks.some(([, ok]) => !ok)) process.exitCode = 1;
|
|
169
146
|
}
|
|
170
147
|
|
|
171
148
|
async function build(workspace, options) {
|
|
@@ -194,6 +171,7 @@ async function dev(workspace, options) {
|
|
|
194
171
|
runNode(packageBin("vite", "bin/vite.js"), args, workspace, {
|
|
195
172
|
...process.env,
|
|
196
173
|
PORTOLAN_WORKSPACE: workspace,
|
|
174
|
+
BASE_PATH: options.base ?? process.env.BASE_PATH ?? "/",
|
|
197
175
|
});
|
|
198
176
|
}
|
|
199
177
|
|
|
@@ -248,7 +226,10 @@ async function prepareSite(workspace) {
|
|
|
248
226
|
|
|
249
227
|
function generateLikeC4(stage) {
|
|
250
228
|
runNode(resolve(stage, "scripts/gen-likec4.mjs"), [], stage);
|
|
251
|
-
|
|
229
|
+
// Inside a container likec4 switches to a graphviz binary by default and,
|
|
230
|
+
// finding none, reports "no views found". The wasm engine it uses
|
|
231
|
+
// everywhere else is the one wanted, so it is asked for by name.
|
|
232
|
+
runNode(packageBin("likec4", "bin/likec4.mjs"), ["gen", "react", "likec4", "-o", "src/likec4/generated.jsx", "--no-use-dot"], stage);
|
|
252
233
|
}
|
|
253
234
|
|
|
254
235
|
async function matchedFiles(workspace, patterns) {
|
|
@@ -341,23 +322,6 @@ function inside(root, target) {
|
|
|
341
322
|
return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
|
|
342
323
|
}
|
|
343
324
|
|
|
344
|
-
function commandWorks(command, args) {
|
|
345
|
-
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
346
|
-
return !result.error && result.status === 0;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function readJson(path) {
|
|
350
|
-
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function slug(value) {
|
|
354
|
-
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function titleOf(value) {
|
|
358
|
-
return value.split("-").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
325
|
function fail(message) {
|
|
362
326
|
console.error(`portolan: ${message}`);
|
|
363
327
|
process.exit(1);
|
package/index.html
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="Portolan turns code, specifications, schemas, traces and ADRs into a static, navigable architecture catalog."
|
|
10
|
+
/>
|
|
11
|
+
<meta property="og:title" content="Portolan — architecture from code" />
|
|
12
|
+
<meta
|
|
13
|
+
property="og:description"
|
|
14
|
+
content="A navigable map of your software estate, generated from code and evidence."
|
|
15
|
+
/>
|
|
16
|
+
<meta property="og:type" content="website" />
|
|
17
|
+
<meta
|
|
18
|
+
property="og:image"
|
|
19
|
+
content="https://shortlink-org.github.io/portolan/og.png"
|
|
20
|
+
/>
|
|
21
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
22
|
+
<meta
|
|
23
|
+
name="twitter:image"
|
|
24
|
+
content="https://shortlink-org.github.io/portolan/og.png"
|
|
25
|
+
/>
|
|
7
26
|
<title>portolan</title>
|
|
8
27
|
</head>
|
|
9
28
|
<body>
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@shortlink-org/portolan",
|
|
3
3
|
"description": "Generate a navigable architecture catalog from code and specifications.",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"homepage": "https://github.com/shortlink-org/portolan#readme",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"public",
|
|
27
27
|
"schema",
|
|
28
28
|
"plugins/*.wasm",
|
|
29
|
-
"plugins/*/*.go",
|
|
30
29
|
"plugins/*/*.py",
|
|
31
30
|
"plugins/*/*.ts",
|
|
32
31
|
"plugins/*/*.json",
|
|
@@ -35,15 +34,10 @@
|
|
|
35
34
|
"plugins/*/*.lock",
|
|
36
35
|
"plugins/*/src",
|
|
37
36
|
"plugins/extract-java/build",
|
|
38
|
-
"plugin",
|
|
39
|
-
"catalog",
|
|
40
|
-
"internal",
|
|
41
37
|
"index.html",
|
|
42
38
|
"vite.config.ts",
|
|
43
39
|
"tsconfig.json",
|
|
44
40
|
"portolan.json",
|
|
45
|
-
"go.mod",
|
|
46
|
-
"go.sum",
|
|
47
41
|
"README.md",
|
|
48
42
|
"LICENSE"
|
|
49
43
|
],
|
|
@@ -61,10 +55,10 @@
|
|
|
61
55
|
"test:package": "node scripts/package-smoke.mjs",
|
|
62
56
|
"test:watch": "vitest",
|
|
63
57
|
"typecheck": "tsc --noEmit && tsc -p plugins/extract-ts",
|
|
64
|
-
"likec4:gen": "node scripts/gen-likec4.mjs && likec4 gen react likec4 -o src/likec4/generated.jsx",
|
|
58
|
+
"likec4:gen": "node scripts/gen-likec4.mjs && likec4 gen react likec4 -o src/likec4/generated.jsx --no-use-dot",
|
|
65
59
|
"likec4:validate": "likec4 validate likec4",
|
|
66
60
|
"predev": "npm run likec4:gen",
|
|
67
|
-
"plugins:build": "GOOS=wasip1 GOARCH=wasm go build -
|
|
61
|
+
"plugins:build": "GOOS=wasip1 GOARCH=wasm go build -trimpath -ldflags=\"-s -w\" -o plugins/portolan-go.wasm ./plugins/cmd/portolan-go && javac --release 21 -d plugins/extract-java/build plugins/extract-java/src/org/portolan/extract/*.java",
|
|
68
62
|
"schema": "npm run plugins:build && node scripts/schema.mjs",
|
|
69
63
|
"schema:check": "npm run plugins:build && node scripts/schema.mjs --check",
|
|
70
64
|
"gen": "npm run plugins:build && node scripts/gen.mjs",
|
|
@@ -78,6 +72,7 @@
|
|
|
78
72
|
"@ai-sdk/openai-compatible": "3.0.44",
|
|
79
73
|
"@ai-sdk/react": "4.0.96",
|
|
80
74
|
"@asyncapi/react-component": "^3.1.8",
|
|
75
|
+
"@clack/prompts": "^1.8.0",
|
|
81
76
|
"@headlessui/react": "^2.2.10",
|
|
82
77
|
"@scalar/api-reference-react": "^0.9.66",
|
|
83
78
|
"@tanstack/react-query": "^5.102.8",
|
|
@@ -108,7 +103,7 @@
|
|
|
108
103
|
"zustand": "^5.0.15"
|
|
109
104
|
},
|
|
110
105
|
"devDependencies": {
|
|
111
|
-
"@types/node": "^26.
|
|
106
|
+
"@types/node": "^26.5.0",
|
|
112
107
|
"@types/react": "^19.0.0",
|
|
113
108
|
"@types/react-dom": "^19.0.0",
|
|
114
109
|
"typescript": "^7.0.2",
|
package/plugins/README.md
CHANGED
|
@@ -31,6 +31,13 @@ is what makes that worth having: `encoding/json` drops a field it does not
|
|
|
31
31
|
recognise, so before this a misspelled option was no option at all and nothing
|
|
32
32
|
said so.
|
|
33
33
|
|
|
34
|
+
A descriptor may also carry `needs`: what the host must put in the request
|
|
35
|
+
beyond the tree, because a sandboxed module cannot reach it. The one need so
|
|
36
|
+
far is `history` (portolan.0007) - when each file under the root was first
|
|
37
|
+
committed and last changed, read by the host from one `git log` and handed
|
|
38
|
+
over as `input.history`, keyed by the path the plugin would open. `extract-adr`
|
|
39
|
+
asks for it; a plugin that does not ask is handed nothing.
|
|
40
|
+
|
|
34
41
|
A generator **names** files; it never writes them. `scripts/gen.mjs` writes what
|
|
35
42
|
comes back, refuses a name that points outside the output directory, and deletes
|
|
36
43
|
pages that stopped being generated. That is what lets a generator run as a wasm
|
|
@@ -56,7 +63,7 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
|
|
|
56
63
|
|
|
57
64
|
## Adding one
|
|
58
65
|
|
|
59
|
-
1. Write it. In Go, a new
|
|
66
|
+
1. Write it. In Go, a new package here whose `run` hands its options
|
|
60
67
|
type to `plugin.Serve`, which reads the request, answers a describe and calls
|
|
61
68
|
the work; `catalog.Catalog` from `github.com/shortlink-org/portolan/catalog`
|
|
62
69
|
is the mirror of the schema, and `internal/goscan` is the tree as syntax -
|
|
@@ -71,8 +78,12 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
|
|
|
71
78
|
`go:embed` and returned in the descriptor. `schematest.Check` in a test keeps
|
|
72
79
|
it from drifting from the options struct: a field renamed on one side and not
|
|
73
80
|
the other fails, and so does an option with no description.
|
|
74
|
-
3. Build it.
|
|
75
|
-
|
|
81
|
+
3. Build it. A built-in Go plugin is a library package with
|
|
82
|
+
`Serve(io.Reader, io.Writer) error`; add it to the map in
|
|
83
|
+
`plugins/cmd/portolan-go/main.go`, and `plugins:build` in `package.json`
|
|
84
|
+
puts it in `plugins/portolan-go.wasm` with the rest (a test keeps the map
|
|
85
|
+
and `portolan.json` in step). A plugin of your own is its own module:
|
|
86
|
+
`GOOS=wasip1 GOARCH=wasm go build`.
|
|
76
87
|
4. Declare it in `portolan.json`, under `plugins` (how to run it) and
|
|
77
88
|
`generate` (what to run it on), then run `npm run schema` so the manifest
|
|
78
89
|
schema learns its options.
|
|
@@ -80,7 +91,7 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
|
|
|
80
91
|
```json
|
|
81
92
|
{
|
|
82
93
|
"plugins": [
|
|
83
|
-
{ "name": "markdown", "wasm": { "url": "file://plugins/
|
|
94
|
+
{ "name": "markdown", "wasm": { "url": "file://plugins/portolan-go.wasm" } }
|
|
84
95
|
],
|
|
85
96
|
"generate": [
|
|
86
97
|
{ "plugin": "markdown", "out": "docs", "options": { "title": "Example estate" } }
|
|
@@ -449,7 +460,7 @@ only left out of its own stamp when the output is *inside* the input root, and
|
|
|
449
460
|
|
|
450
461
|
```json
|
|
451
462
|
{
|
|
452
|
-
"plugins": [{ "name": "adr", "
|
|
463
|
+
"plugins": [{ "name": "adr", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
|
|
453
464
|
"extract": [
|
|
454
465
|
{
|
|
455
466
|
"plugin": "adr",
|
|
@@ -514,7 +525,7 @@ is the point of the id; the same word twice in one context is an error.
|
|
|
514
525
|
|
|
515
526
|
```json
|
|
516
527
|
{
|
|
517
|
-
"plugins": [{ "name": "glossary", "
|
|
528
|
+
"plugins": [{ "name": "glossary", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
|
|
518
529
|
"extract": [
|
|
519
530
|
{
|
|
520
531
|
"plugin": "glossary",
|
|
@@ -588,7 +599,7 @@ runner files sit beside the service, so the step is told which one it is.
|
|
|
588
599
|
|
|
589
600
|
```json
|
|
590
601
|
{
|
|
591
|
-
"plugins": [{ "name": "commands", "
|
|
602
|
+
"plugins": [{ "name": "commands", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
|
|
592
603
|
"extract": [
|
|
593
604
|
{
|
|
594
605
|
"plugin": "commands",
|
|
@@ -808,15 +819,31 @@ the forge looks.
|
|
|
808
819
|
|
|
809
820
|
## wasm or process
|
|
810
821
|
|
|
811
|
-
`wasm` is the default and should stay that way. The module gets no
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
822
|
+
`wasm` is the default and should stay that way. The module gets no network, no
|
|
823
|
+
environment and no way to start a process. A generator gets no filesystem
|
|
824
|
+
either. An extract or verify step gets the workspace preopened as `/`
|
|
825
|
+
(portolan.0006), which is how the built-in Go extractors read a tree without a
|
|
826
|
+
Go toolchain on the machine: every one of them, and the three generators, is
|
|
827
|
+
the single module `plugins/portolan-go.wasm`, which answers to the plugin name
|
|
828
|
+
the host passes as `argv[0]`. WASI preopens read-write, so an extractor is
|
|
829
|
+
trusted not to write the tree it reads, the same trust a process plugin has
|
|
830
|
+
today; a `sha256` pins that trust to a build.
|
|
831
|
+
|
|
832
|
+
`process` is the escape hatch for a plugin that needs a toolchain: the Rust,
|
|
833
|
+
Java, Python and TypeScript extractors run in their own runtimes, and
|
|
834
|
+
`fetch-bsr` still talks to its registry from Go. It gets the same protocol
|
|
835
|
+
and none of the sandbox, which is the trade being made and the reason it is
|
|
836
|
+
not the default. It declares `command` and an `args` array; the host never
|
|
837
|
+
feeds a command string through a shell. A built-in Go plugin that still runs
|
|
838
|
+
as a process is the same code reached as
|
|
839
|
+
`go run ./plugins/cmd/portolan-go <name>`.
|
|
840
|
+
|
|
841
|
+
`host` is for Portolan's own code that needs what only the host has - a git
|
|
842
|
+
binary, a socket - and so runs inside the host process (portolan.0008):
|
|
843
|
+
`{ "name": "git", "host": "fetch-git" }`. The name is resolved against the
|
|
844
|
+
modules shipped in `scripts/host-plugins/` and nothing else, so a manifest
|
|
845
|
+
cannot point the host at arbitrary code; the contract is the same as any
|
|
846
|
+
plugin's, files named and never written.
|
|
820
847
|
|
|
821
848
|
A plugin fetched over `https://` must declare its `sha256`; the host verifies it
|
|
822
849
|
and caches by digest. A `file://` plugin may declare one, but a checksum
|
|
@@ -838,10 +865,15 @@ paths inside the copy are the repository's own, which is the point: the
|
|
|
838
865
|
extract step that follows points its `in` at the vendored service and reads
|
|
839
866
|
it exactly as it would read that service's checkout.
|
|
840
867
|
|
|
868
|
+
It runs inside the host (`scripts/host-plugins/fetch-git.mjs`,
|
|
869
|
+
portolan.0008) rather than as a module, because it needs a git binary and a
|
|
870
|
+
socket, and a manifest names it with `host` rather than `wasm` or `process`.
|
|
871
|
+
The contract is the same: it names files, the host writes them.
|
|
872
|
+
|
|
841
873
|
```json
|
|
842
874
|
{
|
|
843
875
|
"sources": ["data/*.json", "vendor/repos/*/*/git.repo.json"],
|
|
844
|
-
"plugins": [{ "name": "git", "
|
|
876
|
+
"plugins": [{ "name": "git", "host": "fetch-git" }],
|
|
845
877
|
"extract": [
|
|
846
878
|
{
|
|
847
879
|
"plugin": "git",
|
|
@@ -928,8 +960,8 @@ its protos and locks are on disk by the time the parser reads them.
|
|
|
928
960
|
```json
|
|
929
961
|
{
|
|
930
962
|
"plugins": [
|
|
931
|
-
{ "name": "bsr", "
|
|
932
|
-
{ "name": "proto", "
|
|
963
|
+
{ "name": "bsr", "host": "fetch-bsr" },
|
|
964
|
+
{ "name": "proto", "wasm": { "url": "file://plugins/portolan-go.wasm" } }
|
|
933
965
|
],
|
|
934
966
|
"extract": [
|
|
935
967
|
{
|
|
@@ -966,12 +998,14 @@ decides what to do with them — and not an oversight to work around.
|
|
|
966
998
|
|
|
967
999
|
### Why fetch-bsr can never be wasm
|
|
968
1000
|
|
|
969
|
-
It needs a socket and a credential
|
|
970
|
-
`
|
|
971
|
-
protocol's "no ambient state" rule is about *facts*: nothing about
|
|
972
|
-
come from anywhere but the request. A credential is not a fact
|
|
973
|
-
it decides whether the fetch succeeds, never what the fetch
|
|
974
|
-
asserts the output is byte-identical with and without a
|
|
1001
|
+
It needs a socket and a credential, which is why it runs inside the host
|
|
1002
|
+
(`scripts/host-plugins/fetch-bsr.mjs`, portolan.0008) and is declared with
|
|
1003
|
+
`host`. The protocol's "no ambient state" rule is about *facts*: nothing about
|
|
1004
|
+
the estate may come from anywhere but the request. A credential is not a fact
|
|
1005
|
+
about the estate — it decides whether the fetch succeeds, never what the fetch
|
|
1006
|
+
says — and a test asserts the output is byte-identical with and without a
|
|
1007
|
+
token. The token comes from `BUF_TOKEN` or the netrc `buf registry login`
|
|
1008
|
+
wrote, never from the manifest.
|
|
975
1009
|
|
|
976
1010
|
### Pinning, and the offline rule
|
|
977
1011
|
|
|
@@ -1037,6 +1071,11 @@ the reading over a tree it can verify without a registry existing at all.
|
|
|
1037
1071
|
| output | one schema and a `csr.lock.json` per subject | one catalog fragment |
|
|
1038
1072
|
| deterministic | only because it is pinned and cached | absolutely |
|
|
1039
1073
|
|
|
1074
|
+
Declared as `{ "name": "csr", "host": "fetch-csr" }` and
|
|
1075
|
+
`{ "name": "csr-schemas", "wasm": { "url": "file://plugins/portolan-go.wasm" } }`:
|
|
1076
|
+
the fetcher runs inside the host (`scripts/host-plugins/fetch-csr.mjs`,
|
|
1077
|
+
portolan.0008), the reader in the shared module.
|
|
1078
|
+
|
|
1040
1079
|
A registered version is immutable: subject `orders-value` at version 3 is the
|
|
1041
1080
|
same bytes today and next year, and re-registering a changed schema makes
|
|
1042
1081
|
version 4. That is the promise a BSR commit makes, so the same four rules govern
|
|
@@ -1050,8 +1089,8 @@ Each lands in its own directory, and the referring subject's lock is what an
|
|
|
1050
1089
|
offline run follows to find them.
|
|
1051
1090
|
|
|
1052
1091
|
Avro and JSON schemas arrive minified onto one line. They are written out
|
|
1053
|
-
indented —
|
|
1054
|
-
|
|
1092
|
+
indented — re-spaced token by token, never re-parsed, so the file still says
|
|
1093
|
+
what the registry said in the order it said it — because a version
|
|
1055
1094
|
bump that is one unreadable line is a review nobody can do. The digest is over
|
|
1056
1095
|
the bytes as written, so verifying needs no reformatting of anything.
|
|
1057
1096
|
|
|
Binary file
|