@shortlink-org/portolan 0.2.1 → 0.2.3
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 +7 -2
- package/cli/init.mjs +14 -6
- package/cli/init.test.mjs +8 -1
- package/cli/portolan.mjs +92 -13
- package/cli/portolan.test.mjs +47 -0
- package/package.json +7 -6
- package/plugins/extract-django/README.md +1 -1
- package/plugins/extract-django/extract_test.py +1 -1
- package/plugins/extract-django/openapi.py +4 -14
- package/plugins/extract-django/openapi_test.py +4 -4
- package/plugins/extract-go/options.schema.json +5 -0
- package/plugins/extract-project/options.schema.json +21 -0
- package/plugins/extract-ts/README.md +1 -1
- package/plugins/extract-ts/clients.ts +1 -1
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/graphql.test.ts +2 -2
- package/plugins/extract-ts/openapi.test.ts +6 -7
- package/plugins/extract-ts/openapi.ts +3 -10
- package/plugins/portolan-go.wasm +0 -0
- package/public/404-cat-v1.webp +0 -0
- package/public/cat-about-v1.webp +0 -0
- package/public/cat-clear-skies-v1.webp +0 -0
- package/public/cat-no-changes-v1.webp +0 -0
- package/public/cat-onboarding-v1.webp +0 -0
- package/public/cat-trial-success-v1.webp +0 -0
- package/schema/portolan.schema.json +61 -0
- package/scripts/builtin-plugins.mjs +3 -2
- package/scripts/catalog-sources.mjs +14 -2
- package/scripts/catalog-sources.test.mjs +45 -0
- package/scripts/delivery-presets.mjs +207 -24
- package/scripts/diff.mjs +5 -1
- package/scripts/gen-likec4.mjs +242 -85
- package/scripts/gen-likec4.test.mjs +168 -14
- package/scripts/gen.mjs +5 -3
- package/scripts/host-plugins/fetch-git.mjs +22 -12
- package/scripts/host-plugins/fetch-git.test.mjs +25 -5
- package/scripts/local-api.mjs +330 -346
- package/scripts/local-api.test.mjs +245 -1
- package/scripts/local-discovery.mjs +378 -0
- package/scripts/manifest.mjs +42 -1
- package/scripts/manifest.test.mjs +24 -1
- package/scripts/plugin-host.mjs +14 -3
- package/scripts/plugin-host.test.mjs +8 -0
- package/scripts/run-builtin.mjs +4 -2
- package/scripts/schema.mjs +11 -0
- package/scripts/site-docs.mjs +2 -2
- package/src/app/CatalogApp.tsx +23 -8
- package/src/app/CommandPalette.tsx +7 -2
- package/src/app/Sidebar.tsx +13 -730
- package/src/app/SidebarFlowSections.tsx +251 -0
- package/src/app/SidebarFooter.tsx +160 -0
- package/src/app/SidebarTree.tsx +322 -0
- package/src/app/toast.tsx +11 -7
- package/src/catalog-index.ts +485 -0
- package/src/catalog-model.ts +1339 -0
- package/src/catalog-validation.ts +1570 -0
- package/src/catalog.test.ts +49 -0
- package/src/catalog.ts +6 -3124
- package/src/components/CatIllustration.tsx +30 -0
- package/src/components/CommitLink.tsx +154 -0
- package/src/components/EditorLink.tsx +15 -8
- package/src/components/GrpcMethodReference.tsx +93 -0
- package/src/components/Markdown.tsx +17 -0
- package/src/components/MethodRows.tsx +8 -1
- package/src/components/PageHeader.test.tsx +23 -0
- package/src/components/PageHeader.tsx +34 -0
- package/src/components/ProblemRow.tsx +3 -0
- package/src/components/ShapeRows.tsx +12 -0
- package/src/components/SourceDoc.tsx +27 -12
- package/src/components/SourcePreview.tsx +125 -37
- package/src/enrich.test.ts +209 -2
- package/src/enrich.ts +284 -15
- package/src/er/RedisSchema.tsx +86 -5
- package/src/flow/FlowTable.tsx +19 -4
- package/src/flow/StepDetail.tsx +461 -101
- package/src/flow/StepRail.tsx +36 -13
- package/src/flow/answers-response.test.ts +100 -0
- package/src/flow/answers.ts +11 -8
- package/src/flow/chapters.ts +2 -1
- package/src/flow/labels.ts +10 -0
- package/src/flow/mermaid.test.ts +87 -3
- package/src/flow/mermaid.ts +26 -14
- package/src/index.css +132 -17
- package/src/landing/LandingPage.tsx +16 -12
- package/src/lib/all-problems.ts +1 -1
- package/src/lib/api.test.ts +4 -6
- package/src/lib/build-info.test.ts +29 -0
- package/src/lib/build-info.ts +21 -0
- package/src/lib/data-model.ts +14 -0
- package/src/lib/derive.ts +1 -0
- package/src/lib/flow-tree.ts +1 -1
- package/src/lib/github-catalog.test.ts +51 -0
- package/src/lib/github-catalog.ts +69 -0
- package/src/lib/grpc-reference.test.ts +72 -0
- package/src/lib/grpc-reference.ts +117 -0
- package/src/lib/local-api.ts +36 -11
- package/src/lib/motion.test.ts +4 -2
- package/src/lib/motion.tsx +5 -4
- package/src/lib/product.ts +7 -0
- package/src/lib/proto-problems.test.ts +170 -3
- package/src/lib/proto-problems.ts +176 -4
- package/src/lib/queries.ts +16 -1
- package/src/lib/readme-assets.test.ts +19 -0
- package/src/lib/readme-assets.ts +34 -0
- package/src/lib/setup-info.test.ts +7 -0
- package/src/lib/setup-info.ts +5 -0
- package/src/lib/source-link.test.ts +106 -18
- package/src/lib/source-link.ts +22 -6
- package/src/merge.test.ts +101 -0
- package/src/merge.ts +65 -1
- package/src/pages/AdrDetail.tsx +3 -2
- package/src/pages/AdrIndex.tsx +10 -7
- package/src/pages/AggregatePage.tsx +34 -4
- package/src/pages/BlockPage.tsx +39 -32
- package/src/pages/Changes.tsx +7 -9
- package/src/pages/ContextMap.tsx +11 -2
- package/src/pages/ContextPage.tsx +11 -4
- package/src/pages/FlowIndex.tsx +16 -7
- package/src/pages/GraphPage.tsx +21 -7
- package/src/pages/Language.tsx +9 -9
- package/src/pages/NotFound.tsx +58 -15
- package/src/pages/Overview.tsx +6 -0
- package/src/pages/Problems.tsx +10 -8
- package/src/pages/RegistryIndex.tsx +10 -8
- package/src/pages/ServicePage.tsx +1 -1
- package/src/pages/Settings.tsx +257 -44
- package/src/pages/settings/AboutSettings.tsx +129 -0
- package/src/pages/settings/DeliverySettings.tsx +71 -15
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +213 -113
- package/src/selection/pages.test.ts +14 -1
- package/src/selection/pages.ts +9 -3
- package/src/testing/setup.ts +14 -0
- package/vite.config.ts +3 -1
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { rmSync } from "node:fs";
|
|
7
7
|
|
|
8
|
-
import { classifyRepositoryFailure, diffGeneratedFiles, discoverProject, forgetRepositoryCredential, inspectionRoot, localApiPath, planProject, readLocalSource, resolveRepositoryCommit, storeRepositoryCredential, summarizeProjectTrial, writeProject } from "./local-api.mjs";
|
|
8
|
+
import { classifyRepositoryFailure, diffGeneratedFiles, discoverProject, externalProjectDefaults, forgetRepositoryCredential, inspectionRoot, localApiPath, manifestWithoutProject, manifestWithProject, planProject, readLocalSource, removeProject, resolveRepositoryCommit, starterManifestProject, storeRepositoryCredential, summarizeProjectTrial, undoProjectRemoval, writeProject } from "./local-api.mjs";
|
|
9
9
|
import { installDeliveryPreset, planDeliveryPreset, providerFromRemote, publicDeliveryPreset } from "./delivery-presets.mjs";
|
|
10
10
|
|
|
11
11
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
@@ -72,6 +72,44 @@ describe("local project setup", () => {
|
|
|
72
72
|
expect(installDeliveryPreset(root, { provider: "github", revision: installed.revision }).written).toEqual([]);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it("generates only the selected GitHub jobs and keeps elevated permissions isolated", () => {
|
|
76
|
+
const root = gitWorkspace("github");
|
|
77
|
+
const preview = planDeliveryPreset(root, { provider: "github", features: ["check", "diff", "sarif"] });
|
|
78
|
+
expect(preview.features.filter((feature) => feature.selected).map((feature) => feature.id)).toEqual(["check", "diff", "sarif"]);
|
|
79
|
+
expect(preview.files.map((file) => file.path)).toEqual([
|
|
80
|
+
".github/workflows/portolan-check.yml",
|
|
81
|
+
".github/workflows/portolan-review.yml",
|
|
82
|
+
]);
|
|
83
|
+
const installed = installDeliveryPreset(root, {
|
|
84
|
+
provider: "github",
|
|
85
|
+
features: ["check", "diff", "sarif"],
|
|
86
|
+
revision: preview.revision,
|
|
87
|
+
});
|
|
88
|
+
expect(installed.status).toBe("installed");
|
|
89
|
+
const check = readFileSync(join(root, ".github/workflows/portolan-check.yml"), "utf8");
|
|
90
|
+
const review = readFileSync(join(root, ".github/workflows/portolan-review.yml"), "utf8");
|
|
91
|
+
expect(check).not.toContain("pull-requests: write");
|
|
92
|
+
expect(review).toContain("command: diff");
|
|
93
|
+
expect(review).toContain("pull-requests: write");
|
|
94
|
+
expect(review).toContain("security-events: write");
|
|
95
|
+
expect(review).toContain("github/codeql-action/upload-sarif@v4");
|
|
96
|
+
expect(existsSync(join(root, ".github/workflows/portolan-pages.yml"))).toBe(false);
|
|
97
|
+
expect(planDeliveryPreset(root).features.filter((feature) => feature.selected).map((feature) => feature.id)).toEqual(["check", "diff", "sarif"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("removes a disabled GitHub job only when Portolan owns the file", () => {
|
|
101
|
+
const root = gitWorkspace("github");
|
|
102
|
+
const initial = planDeliveryPreset(root, { provider: "github", features: ["check", "diff"] });
|
|
103
|
+
installDeliveryPreset(root, { provider: "github", features: ["check", "diff"], revision: initial.revision });
|
|
104
|
+
const withoutDiff = planDeliveryPreset(root, { provider: "github", features: ["check"] });
|
|
105
|
+
expect(withoutDiff.files.find((file) => file.path.endsWith("portolan-review.yml"))?.status).toBe("removed");
|
|
106
|
+
installDeliveryPreset(root, { provider: "github", features: ["check"], revision: withoutDiff.revision });
|
|
107
|
+
expect(existsSync(join(root, ".github/workflows/portolan-review.yml"))).toBe(false);
|
|
108
|
+
|
|
109
|
+
writeFileSync(join(root, ".github/workflows/portolan-review.yml"), "name: Mine\n");
|
|
110
|
+
expect(planDeliveryPreset(root, { provider: "github", features: ["check"] }).files.some((file) => file.path.endsWith("portolan-review.yml"))).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
75
113
|
it("refuses to overwrite an unmanaged GitHub workflow or apply a stale preview", () => {
|
|
76
114
|
const root = gitWorkspace("github");
|
|
77
115
|
mkdirSync(join(root, ".github/workflows"), { recursive: true });
|
|
@@ -112,6 +150,18 @@ describe("local project setup", () => {
|
|
|
112
150
|
expect(existsSync(join(root, ".github"))).toBe(false);
|
|
113
151
|
});
|
|
114
152
|
|
|
153
|
+
it("builds the GitLab managed region from selected jobs", () => {
|
|
154
|
+
const root = gitWorkspace("gitlab");
|
|
155
|
+
const preview = planDeliveryPreset(root, { provider: "gitlab", features: ["diff"] });
|
|
156
|
+
expect(preview.files[0].diff).toContain('+"portolan:review":');
|
|
157
|
+
expect(preview.files[0].diff).not.toContain('+"portolan:check":');
|
|
158
|
+
expect(preview.files[0].diff).not.toContain('+"portolan:pages":');
|
|
159
|
+
const installed = installDeliveryPreset(root, { provider: "gitlab", features: ["diff"], revision: preview.revision });
|
|
160
|
+
expect(planDeliveryPreset(root).features.filter((feature) => feature.selected).map((feature) => feature.id)).toEqual(["diff"]);
|
|
161
|
+
expect(installed.status).toBe("installed");
|
|
162
|
+
expect(() => planDeliveryPreset(root, { provider: "gitlab", features: ["sarif"] })).toThrow(/only for GitHub/);
|
|
163
|
+
});
|
|
164
|
+
|
|
115
165
|
it("classifies repository authentication, authorization, and timeout failures", () => {
|
|
116
166
|
expect(classifyRepositoryFailure(new Error("fatal: Authentication failed"), "GitHub")).toMatchObject({
|
|
117
167
|
code: "repository_auth_required", status: 401, retryable: true, provider: "GitHub",
|
|
@@ -189,6 +239,18 @@ describe("local project setup", () => {
|
|
|
189
239
|
expect(discovery.detections.find((item) => item.plugin === "adr")?.selected).toBe(false);
|
|
190
240
|
});
|
|
191
241
|
|
|
242
|
+
it("names external projects from the repository or selected component instead of the inspection cache", () => {
|
|
243
|
+
expect(externalProjectDefaults("https://github.com/batazor/microservice-template-ddd")).toEqual({
|
|
244
|
+
id: "microservice-template-ddd",
|
|
245
|
+
name: "Microservice Template Ddd",
|
|
246
|
+
group: "microservice-template-ddd",
|
|
247
|
+
component: "microservice-template-ddd",
|
|
248
|
+
context: "microservice-template-ddd",
|
|
249
|
+
service: "microservice-template-ddd",
|
|
250
|
+
});
|
|
251
|
+
expect(externalProjectDefaults("git@github.com:acme/platform.git", "services/payments").id).toBe("payments");
|
|
252
|
+
});
|
|
253
|
+
|
|
192
254
|
it("finds component roots in a monorepo without reading dependency directories", () => {
|
|
193
255
|
const root = workspace();
|
|
194
256
|
mkdirSync(join(root, "services/orders"), { recursive: true });
|
|
@@ -209,6 +271,55 @@ describe("local project setup", () => {
|
|
|
209
271
|
]);
|
|
210
272
|
});
|
|
211
273
|
|
|
274
|
+
it("splits corroborated Go cmd entrypoints into deployable services", () => {
|
|
275
|
+
const root = workspace();
|
|
276
|
+
const platform = join(root, "services/platform");
|
|
277
|
+
mkdirSync(platform, { recursive: true });
|
|
278
|
+
writeFileSync(join(platform, "go.mod"), "module example.com/platform\nrequire github.com/go-redis/redis v6.15.9+incompatible\n");
|
|
279
|
+
for (const service of ["api", "billing", "book", "user"]) {
|
|
280
|
+
mkdirSync(join(platform, "cmd", service), { recursive: true });
|
|
281
|
+
mkdirSync(join(platform, "ops/dockerfile"), { recursive: true });
|
|
282
|
+
writeFileSync(join(platform, "cmd", service, `${service}.go`), "package main\nfunc main() {}\n");
|
|
283
|
+
writeFileSync(join(platform, "ops/dockerfile", `${service}.Dockerfile`), `FROM golang\nRUN go build -o app ./cmd/${service}\n`);
|
|
284
|
+
}
|
|
285
|
+
for (const service of ["billing", "book", "user"]) {
|
|
286
|
+
mkdirSync(join(platform, "internal", service, "infrastructure/rpc"), { recursive: true });
|
|
287
|
+
writeFileSync(join(platform, "internal", service, "infrastructure/rpc", `${service}.proto`), `syntax = "proto3"; package ${service}; service ${service}RPC {}\n`);
|
|
288
|
+
mkdirSync(join(platform, "internal", service, "domain"), { recursive: true });
|
|
289
|
+
writeFileSync(join(platform, "internal", service, "domain", `${service}.go`), `package domain\ntype ${service[0].toUpperCase() + service.slice(1)} struct{}\n`);
|
|
290
|
+
}
|
|
291
|
+
mkdirSync(join(platform, "internal/db/redis"), { recursive: true });
|
|
292
|
+
mkdirSync(join(platform, "internal/di"), { recursive: true });
|
|
293
|
+
writeFileSync(join(platform, "internal/db/redis/redis.go"), "package redisdb\nimport redis \"github.com/go-redis/redis\"\nfunc Open() { _ = redis.NewClient(nil) }\n");
|
|
294
|
+
writeFileSync(join(platform, "internal/di/book.go"), "package di\nimport _ \"example.com/platform/internal/db\"\n");
|
|
295
|
+
|
|
296
|
+
const discovery = discoverProject(root, "services/platform");
|
|
297
|
+
expect(discovery.deployables.map(({ slug, confidence }) => [slug, confidence])).toEqual([
|
|
298
|
+
["api", "high"], ["billing", "high"], ["book", "high"], ["user", "high"],
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const manifest = JSON.parse(readFileSync(join(root, "portolan.json"), "utf8"));
|
|
302
|
+
const plan = planProject(root, manifest, {
|
|
303
|
+
root: "services/platform", id: "platform", name: "Platform", group: "platform", component: "platform", componentKind: "application", plugins: ["project", "go-domain", "proto", "redis"],
|
|
304
|
+
});
|
|
305
|
+
expect(plan.project).toMatchObject({ group: "platform", components: ["api", "billing", "book", "user"] });
|
|
306
|
+
expect(plan.project).not.toHaveProperty("component");
|
|
307
|
+
expect(plan.steps[0].options.components.map((component) => component.slug)).toEqual(["api", "billing", "book", "user"]);
|
|
308
|
+
expect(plan.steps.filter((step) => step.plugin === "proto").map((step) => [step.options.service, step.options.out])).toEqual([
|
|
309
|
+
["billing", "proto-billing.json"], ["book", "proto-book.json"], ["user", "proto-user.json"],
|
|
310
|
+
]);
|
|
311
|
+
expect(plan.steps.filter((step) => step.plugin === "redis").map((step) => [step.options.service, step.options.out])).toEqual([
|
|
312
|
+
["book", "redis-book.json"],
|
|
313
|
+
]);
|
|
314
|
+
expect(plan.steps.filter((step) => step.plugin === "go-domain").map((step) => [step.options.service, step.options.scope, step.options.store, step.options.peers, step.options.out])).toEqual([
|
|
315
|
+
["api", "api", undefined, { billing: "platform.billing", book: "platform.book", user: "platform.user" }, "go-domain-api.json"],
|
|
316
|
+
["billing", "billing", undefined, { billing: "platform.billing", book: "platform.book", user: "platform.user" }, "go-domain-billing.json"],
|
|
317
|
+
["book", "book", "redis", { billing: "platform.billing", book: "platform.book", user: "platform.user" }, "go-domain-book.json"],
|
|
318
|
+
["user", "user", undefined, { billing: "platform.billing", book: "platform.book", user: "platform.user" }, "go-domain-user.json"],
|
|
319
|
+
]);
|
|
320
|
+
expect(plan.steps[0].options.groupKind).toBe("system");
|
|
321
|
+
});
|
|
322
|
+
|
|
212
323
|
it("only offers the Go domain extractor when the layout contains an aggregate root", () => {
|
|
213
324
|
const root = workspace();
|
|
214
325
|
mkdirSync(join(root, "services/billing/internal/domain/invoice"), { recursive: true });
|
|
@@ -233,6 +344,14 @@ describe("local project setup", () => {
|
|
|
233
344
|
expect(discovery.detections.find((item) => item.plugin === "sql")?.evidence).toBe("2 repository packages");
|
|
234
345
|
});
|
|
235
346
|
|
|
347
|
+
it("accepts a feature domain package named domain", () => {
|
|
348
|
+
const root = workspace();
|
|
349
|
+
mkdirSync(join(root, "services/billing/internal/invoice/domain"), { recursive: true });
|
|
350
|
+
writeFileSync(join(root, "services/billing/internal/invoice/domain/invoice.go"), "package domain\n\ntype Invoice struct{}\n");
|
|
351
|
+
const discovery = discoverProject(root, "services/billing");
|
|
352
|
+
expect(discovery.detections.find((item) => item.plugin === "go-domain")?.evidence).toBe("internal/invoice/domain/invoice.go");
|
|
353
|
+
});
|
|
354
|
+
|
|
236
355
|
it("offers the River extractor when the Go module uses River", () => {
|
|
237
356
|
const root = workspace();
|
|
238
357
|
writeFileSync(join(root, "services/billing/go.mod"), "module example.com/billing\nrequire github.com/riverqueue/river v0.26.0\n");
|
|
@@ -383,6 +502,131 @@ describe("local project setup", () => {
|
|
|
383
502
|
expect(() => writeProject(root, { ...request, id: "another" })).toThrow(/already exists/);
|
|
384
503
|
});
|
|
385
504
|
|
|
505
|
+
it("removes a project's complete catalog slice without touching its neighbour", () => {
|
|
506
|
+
const manifest = {
|
|
507
|
+
sources: ["portolan/*.json", "services/billing/portolan/*.json"],
|
|
508
|
+
defaultCatalog: "portolan",
|
|
509
|
+
catalogs: [
|
|
510
|
+
{ id: "portolan", title: "Portolan", sources: ["portolan/*.json"], contexts: ["portolan"], projects: ["portolan"] },
|
|
511
|
+
{ id: "billing", title: "Billing", sources: ["services/billing/portolan/*.json"], contexts: ["finance"], projects: ["billing"] },
|
|
512
|
+
],
|
|
513
|
+
projects: [
|
|
514
|
+
{ id: "portolan", name: "Portolan", root: ".", group: "portolan", component: "portolan" },
|
|
515
|
+
{ id: "billing", name: "Billing", root: "services/billing", group: "finance", component: "billing" },
|
|
516
|
+
],
|
|
517
|
+
extract: [
|
|
518
|
+
{ plugin: "project", in: ".", out: "portolan", options: {} },
|
|
519
|
+
{ plugin: "commands", in: "scripts", out: "portolan", options: {} },
|
|
520
|
+
{ plugin: "project", in: "services/billing", out: "services/billing/portolan", options: {} },
|
|
521
|
+
],
|
|
522
|
+
verify: [
|
|
523
|
+
{ plugin: "otel", in: ".", out: "portolan", options: {} },
|
|
524
|
+
{ plugin: "otel", in: "services/billing", out: "services/billing/portolan", options: {} },
|
|
525
|
+
],
|
|
526
|
+
generate: [
|
|
527
|
+
{ plugin: "markdown", catalog: "portolan", out: "docs", options: {} },
|
|
528
|
+
{ plugin: "markdown", catalog: "billing", out: "docs/billing", options: {} },
|
|
529
|
+
],
|
|
530
|
+
};
|
|
531
|
+
const result = manifestWithoutProject(manifest, "portolan");
|
|
532
|
+
expect(result.manifest.projects.map((project) => project.id)).toEqual(["billing"]);
|
|
533
|
+
expect(result.manifest.sources).toEqual(["services/billing/portolan/*.json"]);
|
|
534
|
+
expect(result.manifest.extract.map((step) => step.out)).toEqual(["services/billing/portolan"]);
|
|
535
|
+
expect(result.manifest.verify.map((step) => step.out)).toEqual(["services/billing/portolan"]);
|
|
536
|
+
expect(result.manifest.catalogs.map((catalog) => catalog.id)).toEqual(["billing"]);
|
|
537
|
+
expect(result.manifest.defaultCatalog).toBe("billing");
|
|
538
|
+
expect(result.manifest.generate.map((step) => step.catalog)).toEqual(["billing"]);
|
|
539
|
+
expect(result.removedOutputs).toEqual(["docs"]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("keeps a valid empty-workspace source and replaces it when a project is added", () => {
|
|
543
|
+
const removed = manifestWithoutProject({
|
|
544
|
+
sources: ["portolan/*.json"],
|
|
545
|
+
projects: [{ id: "portolan", name: "Portolan", root: ".", group: "portolan", component: "portolan" }],
|
|
546
|
+
extract: [{ plugin: "project", in: ".", out: "portolan", options: {} }],
|
|
547
|
+
generate: [{ plugin: "markdown", out: "docs", options: {} }],
|
|
548
|
+
}, "portolan");
|
|
549
|
+
expect(removed.manifest).toMatchObject({ sources: ["portolan/*.json"], projects: [], extract: [] });
|
|
550
|
+
expect(removed.removedOutputs).toEqual(["docs"]);
|
|
551
|
+
|
|
552
|
+
const added = planProject(workspace(), { sources: ["portolan/*.json"], projects: [], extract: [] }, {
|
|
553
|
+
root: "services/billing", id: "aviaadmin", name: "Avia Admin", group: "avia", groupKind: "bounded-context", component: "aviaadmin", plugins: ["project"],
|
|
554
|
+
});
|
|
555
|
+
expect(added.project).toMatchObject({ id: "aviaadmin", group: "avia", groupKind: "bounded-context" });
|
|
556
|
+
expect(added.steps[0].options).toMatchObject({ group: "avia", groupKind: "bounded-context" });
|
|
557
|
+
const manifest = { sources: ["portolan/*.json"], projects: [], extract: [] };
|
|
558
|
+
expect(manifestWithProject(manifest, added).sources).toEqual(["services/billing/portolan/*.json"]);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("does not duplicate an existing external fetch when a project is re-added", () => {
|
|
562
|
+
const fetch = { repo: "https://github.com/acme/orders", commit: "a".repeat(40), paths: [] };
|
|
563
|
+
const manifest = {
|
|
564
|
+
sources: ["vendor/repos/*/*/git.repo.json"], projects: [],
|
|
565
|
+
extract: [{ plugin: "git", in: "vendor", out: "vendor/repos", options: { cache: "vendor/repos", repos: [fetch] } }],
|
|
566
|
+
};
|
|
567
|
+
const plan = {
|
|
568
|
+
project: { id: "orders", root: "vendor/repos/acme/orders", group: "orders", component: "orders" },
|
|
569
|
+
steps: [{ plugin: "project", in: "vendor/repos/acme/orders", out: "vendor/repos/acme/orders/portolan", options: {} }],
|
|
570
|
+
source: "vendor/repos/**/portolan/*.json", fetch,
|
|
571
|
+
};
|
|
572
|
+
expect(manifestWithProject(manifest, plan).extract[0].options.repos).toEqual([fetch]);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("adds the project slice to the default catalog profile", () => {
|
|
576
|
+
const root = workspace();
|
|
577
|
+
const manifest = {
|
|
578
|
+
sources: ["portolan/*.json"],
|
|
579
|
+
defaultCatalog: "company",
|
|
580
|
+
catalogs: [{ id: "company", title: "Company", sources: ["portolan/*.json"], contexts: ["platform"], projects: ["portolan"] }],
|
|
581
|
+
projects: [{ id: "portolan", root: ".", group: "platform", component: "portolan" }],
|
|
582
|
+
extract: [],
|
|
583
|
+
};
|
|
584
|
+
const added = planProject(root, manifest, {
|
|
585
|
+
root: "services/billing", id: "billing", name: "Billing", group: "finance", component: "billing", plugins: ["project"],
|
|
586
|
+
});
|
|
587
|
+
expect(manifestWithProject(manifest, added).catalogs[0]).toMatchObject({
|
|
588
|
+
sources: ["portolan/*.json", "services/billing/portolan/*.json"],
|
|
589
|
+
contexts: ["platform", "finance"],
|
|
590
|
+
projects: ["portolan", "billing"],
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("replaces a starter atomically after project discovery succeeds", () => {
|
|
595
|
+
const root = workspace();
|
|
596
|
+
const manifest = JSON.parse(readFileSync(join(root, "portolan.json"), "utf8"));
|
|
597
|
+
Object.assign(manifest, {
|
|
598
|
+
sources: ["portolan/*.json"],
|
|
599
|
+
projects: [{ id: "starter", name: "Starter", root: ".", group: "starter", component: "starter" }],
|
|
600
|
+
extract: [{ plugin: "project", in: ".", out: "portolan", options: { group: "starter", component: "starter" } }],
|
|
601
|
+
});
|
|
602
|
+
writeFileSync(join(root, "portolan.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
603
|
+
expect(starterManifestProject(manifest)?.id).toBe("starter");
|
|
604
|
+
const result = writeProject(root, {
|
|
605
|
+
root: "services/billing", id: "aviaadmin", name: "Avia Admin", group: "avia", contextName: "Aviation", contextSummary: "Flight administration.", classification: "core", groupKind: "bounded-context", component: "aviaadmin", componentKind: "service", replaceStarter: true, plugins: ["project"],
|
|
606
|
+
});
|
|
607
|
+
const written = JSON.parse(readFileSync(join(root, "portolan.json"), "utf8"));
|
|
608
|
+
expect(result.replacedProject?.id).toBe("starter");
|
|
609
|
+
expect(written.projects.map((project) => project.id)).toEqual(["aviaadmin"]);
|
|
610
|
+
expect(starterManifestProject(written)).toBeNull();
|
|
611
|
+
expect(written.sources).toEqual(["services/billing/portolan/*.json"]);
|
|
612
|
+
expect(written.extract).toHaveLength(1);
|
|
613
|
+
expect(written.extract[0].options).toMatchObject({ group: "avia", groupName: "Aviation", groupSummary: "Flight administration.", classification: "core", groupKind: "bounded-context", componentKind: "service" });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("undoes a project removal only while the manifest is unchanged", () => {
|
|
617
|
+
const root = workspace();
|
|
618
|
+
writeProject(root, { root: "services/billing", id: "billing", name: "Billing", group: "finance", component: "billing", plugins: ["project"] });
|
|
619
|
+
mkdirSync(join(root, "services/billing/portolan"), { recursive: true });
|
|
620
|
+
writeFileSync(join(root, "services/billing/portolan/project.json"), "generated slice\n");
|
|
621
|
+
const removed = removeProject(root, "billing");
|
|
622
|
+
expect(JSON.parse(readFileSync(join(root, "portolan.json"), "utf8")).projects).toEqual([]);
|
|
623
|
+
expect(existsSync(join(root, "services/billing/portolan/project.json"))).toBe(false);
|
|
624
|
+
expect(undoProjectRemoval(root, removed.undoToken)).toEqual({ restored: true });
|
|
625
|
+
expect(JSON.parse(readFileSync(join(root, "portolan.json"), "utf8")).projects[0].id).toBe("billing");
|
|
626
|
+
expect(readFileSync(join(root, "services/billing/portolan/project.json"), "utf8")).toBe("generated slice\n");
|
|
627
|
+
expect(() => undoProjectRemoval(root, removed.undoToken)).toThrow(/can no longer be undone/);
|
|
628
|
+
});
|
|
629
|
+
|
|
386
630
|
it("plans a pinned external repository through the built-in git fetcher", () => {
|
|
387
631
|
const root = workspace();
|
|
388
632
|
const repository = "https://github.com/acme/platform.git";
|