@intentius/chant 0.108.0 → 0.109.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/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +6 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +30 -3
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +10 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +35 -2
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/provenance.d.ts +38 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.d.ts +1 -0
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.d.ts +25 -0
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +6 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/lifecycle.test.ts +106 -1
- package/src/cli/handlers/lifecycle.ts +26 -5
- package/src/cli/main.ts +15 -1
- package/src/cli/registry.ts +6 -0
- package/src/composite.ts +37 -0
- package/src/discovery/collect.ts +13 -3
- package/src/discovery/fold-import.ts +251 -9
- package/src/discovery/fold-package-composite.test.ts +14 -1
- package/src/discovery/fold-project-provenance.test.ts +532 -0
- package/src/discovery/param-deps.test.ts +120 -1
- package/src/discovery/param-deps.ts +73 -3
- package/src/fold/subset-public-export.test.ts +38 -0
- package/src/fold/subset.ts +10 -2
- package/src/fold-provenance.test.ts +179 -16
- package/src/fold-provenance.ts +136 -11
- package/src/provenance.ts +46 -1
- package/src/workspace/ci-green-cli.ts +20 -3
- package/src/workspace/ci-green-workflow.test.ts +90 -0
- package/src/workspace/ci-green-workflow.ts +37 -0
- package/src/workspace/ci-green.test.ts +27 -1
- package/src/workspace/ci-green.ts +17 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { sep } from "node:path";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
3
|
import { createMockPlugin, staticDescribeResources, staticObservation, staticDeepObservation, staticListArtifacts } from "@intentius/chant-test-utils";
|
|
4
4
|
import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
|
|
5
5
|
import { deepObservation } from "../../deep-observation";
|
|
6
6
|
import { heldElsewhere } from "../../held-elsewhere";
|
|
7
|
+
import { setProvenance } from "../../provenance";
|
|
7
8
|
import type { BuildResult } from "../../build";
|
|
8
9
|
import type { ParsedArgs } from "../registry";
|
|
9
10
|
|
|
@@ -739,6 +740,110 @@ describe("runLifecycleDiff --live", () => {
|
|
|
739
740
|
});
|
|
740
741
|
});
|
|
741
742
|
|
|
743
|
+
// #3597 — a drifted field names where it came from: the composite argument
|
|
744
|
+
// and its line, the composite that fixes it, or an unknown origin and why.
|
|
745
|
+
describe("drift names its origin in source (#3597)", () => {
|
|
746
|
+
const app = join(process.cwd(), "src", "app.ts");
|
|
747
|
+
const call = { file: app, line: 10, column: 20 };
|
|
748
|
+
const names = ["webDeployment", "webService", "worker", "legacy"];
|
|
749
|
+
|
|
750
|
+
const runOriginDiff = async (args: Partial<ParsedArgs> = {}) => {
|
|
751
|
+
const build = makeBuildResult({ k8s: names });
|
|
752
|
+
const declare = (name: string, provenance?: Parameters<typeof setProvenance>[1]) => {
|
|
753
|
+
const entity = { lexicon: "k8s", entityType: "Deployment", props: { spec: { replicas: 3 } } };
|
|
754
|
+
if (provenance) setProvenance(entity, provenance);
|
|
755
|
+
build.entities.set(name, entity as never);
|
|
756
|
+
};
|
|
757
|
+
declare("webDeployment", {
|
|
758
|
+
sourceFile: app,
|
|
759
|
+
composite: "WebApp",
|
|
760
|
+
compositeInstance: "web",
|
|
761
|
+
compositeCall: call,
|
|
762
|
+
paths: {
|
|
763
|
+
"spec.replicas": {
|
|
764
|
+
kind: "composite-parameter",
|
|
765
|
+
composite: "WebApp",
|
|
766
|
+
parameters: ["replicas"],
|
|
767
|
+
arguments: [{ parameter: "replicas", file: app, line: 12, column: 3, text: "replicas: 3" }],
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
declare("webService", {
|
|
772
|
+
sourceFile: app,
|
|
773
|
+
composite: "WebApp",
|
|
774
|
+
compositeInstance: "web",
|
|
775
|
+
compositeCall: call,
|
|
776
|
+
paths: { "spec.replicas": { kind: "composite-literal", composite: "WebApp" } },
|
|
777
|
+
});
|
|
778
|
+
declare("worker", { sourceFile: app });
|
|
779
|
+
declare("legacy", { sourceFile: app, composite: "LegacyApp", compositeInstance: "legacy" });
|
|
780
|
+
buildMock.mockResolvedValue(build);
|
|
781
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
782
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
783
|
+
const live = Object.fromEntries(names.map((n) => [n, { type: "Deployment", properties: { spec: { replicas: 5 } } }]));
|
|
784
|
+
const plugins: LexiconPlugin[] = [
|
|
785
|
+
createMockPlugin({
|
|
786
|
+
name: "k8s",
|
|
787
|
+
describeResources: staticObservation(Object.fromEntries(names.map((n) => [n, meta({ type: "Deployment" })]))),
|
|
788
|
+
observeResourcesDeep: staticDeepObservation(live),
|
|
789
|
+
}),
|
|
790
|
+
];
|
|
791
|
+
return runLifecycleDiff({
|
|
792
|
+
args: makeArgs({ command: "state", path: "diff", extraPositional: "prod", live: true, ...args }),
|
|
793
|
+
plugins,
|
|
794
|
+
serializers: plugins.map((p) => p.serializer),
|
|
795
|
+
} as never);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
test("the human report names the argument and line, the fixing composite, and an unknown origin", async () => {
|
|
799
|
+
await runOriginDiff();
|
|
800
|
+
const output = stdoutBuf.join("\n");
|
|
801
|
+
expect(output).toContain(
|
|
802
|
+
"spec.replicas on Deployment webDeployment comes from WebApp({ replicas: 3 }) at src/app.ts:12",
|
|
803
|
+
);
|
|
804
|
+
expect(output).toContain("change parameter `replicas` of the `web` call of composite WebApp at src/app.ts:12");
|
|
805
|
+
expect(output).toContain(
|
|
806
|
+
"spec.replicas on Deployment webService is fixed inside composite WebApp; no argument to the call at src/app.ts:10 moves it",
|
|
807
|
+
);
|
|
808
|
+
expect(output).toContain(
|
|
809
|
+
"spec.replicas on Deployment legacy has an unknown origin: expanded by a composite whose factory this build did not interpret",
|
|
810
|
+
);
|
|
811
|
+
// Direct keeps today's output: the row, and nothing under it.
|
|
812
|
+
expect(output).not.toContain("on Deployment worker");
|
|
813
|
+
const workerRow = output.split("\n").findIndex((l) => l.includes("- worker (Deployment)"));
|
|
814
|
+
expect(output.split("\n")[workerRow + 1]).toMatch(/^ {6}spec\.replicas: 3 → 5/);
|
|
815
|
+
expect(output.split("\n")[workerRow + 2] ?? "").not.toMatch(/^ {8}\S/);
|
|
816
|
+
// An unknown origin is never shown as a direct declaration.
|
|
817
|
+
expect(output).not.toMatch(/legacy.*declared directly/);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("--json carries the origin object for every drifted field", async () => {
|
|
821
|
+
await runOriginDiff({ json: true });
|
|
822
|
+
const payload = JSON.parse(stdoutBuf.join("\n")) as {
|
|
823
|
+
lexicons: { k8s: { reconcile: Array<{ entity: string; origin: Record<string, unknown>; summary: string }> } };
|
|
824
|
+
};
|
|
825
|
+
const byEntity = Object.fromEntries(payload.lexicons.k8s.reconcile.map((r) => [r.entity, r]));
|
|
826
|
+
expect(byEntity.webDeployment.origin).toEqual({
|
|
827
|
+
kind: "composite-parameter",
|
|
828
|
+
composite: "WebApp",
|
|
829
|
+
instance: "web",
|
|
830
|
+
parameters: ["replicas"],
|
|
831
|
+
call,
|
|
832
|
+
arguments: [{ parameter: "replicas", file: app, line: 12, column: 3, text: "replicas: 3" }],
|
|
833
|
+
});
|
|
834
|
+
expect(byEntity.webService.origin).toEqual({ kind: "composite-literal", composite: "WebApp", instance: "web", call });
|
|
835
|
+
expect(byEntity.worker.origin).toEqual({ kind: "direct" });
|
|
836
|
+
expect(byEntity.legacy.origin).toEqual({ kind: "unknown", reason: "composite-not-interpreted" });
|
|
837
|
+
expect(byEntity.webDeployment.summary).toContain("at src/app.ts:12");
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("the diff builds the way `chant build` does, folding by default", async () => {
|
|
841
|
+
await runOriginDiff();
|
|
842
|
+
const options = buildMock.mock.calls[0][3] as { fold?: boolean };
|
|
843
|
+
expect(options.fold).toBe(true);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
742
847
|
// #2162 — a `heldElsewhere()` marker reports as held, not drift, and a
|
|
743
848
|
// held property nothing ever writes is flagged suspicious on stderr.
|
|
744
849
|
describe("heldElsewhere() markers (#2162)", () => {
|
|
@@ -41,6 +41,7 @@ import { loadChantConfig, environmentNames, matchesDeclaredEnvironment, resolveO
|
|
|
41
41
|
import { unknownEnvError, isProdLikeEnvironment } from "../../env";
|
|
42
42
|
import { planTeardown, executeTeardown, type TeardownPlan, type TeardownReport } from "../../lifecycle/teardown";
|
|
43
43
|
import { collectBuildRootContributors } from "../plugins";
|
|
44
|
+
import { resolveBuildModes, resolveProjectBuildOptions } from "../build-options";
|
|
44
45
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
45
46
|
import { isObservableDeclarable } from "../../declarable";
|
|
46
47
|
import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
|
|
@@ -378,11 +379,22 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
378
379
|
const endpointResult = applyLiveEndpoint(config.environments, environment, liveLexicons);
|
|
379
380
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
380
381
|
|
|
381
|
-
|
|
382
|
+
// The build `chant build` would produce for this project (#2002's one
|
|
383
|
+
// place), so the diff folds by default as `chant build` does. A run build
|
|
384
|
+
// cannot say which composite argument produced a field, so without this
|
|
385
|
+
// every composite field in the live diff read as unknown (#3597).
|
|
386
|
+
const projectRoot = resolve(".");
|
|
387
|
+
const diffBuildOptions = resolveProjectBuildOptions({
|
|
388
|
+
config,
|
|
389
|
+
configDir: projectRoot,
|
|
390
|
+
plugins,
|
|
391
|
+
modes: resolveBuildModes(config, { fold: args.fold, sandbox: args.sandbox }),
|
|
392
|
+
buildParams: declaredParams,
|
|
393
|
+
});
|
|
382
394
|
|
|
383
395
|
try {
|
|
384
396
|
for (const target of targets) {
|
|
385
|
-
const buildResult = await build(target.root, targetSerializers, undefined,
|
|
397
|
+
const buildResult = await build(target.root, targetSerializers, undefined, diffBuildOptions);
|
|
386
398
|
if (buildResult.errors.length > 0) {
|
|
387
399
|
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
388
400
|
console.error(formatError({ message: `Build failed for ${label} — fix errors before diffing` }));
|
|
@@ -434,6 +446,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
434
446
|
baseline,
|
|
435
447
|
updateBaseline: args.updateBaseline,
|
|
436
448
|
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
449
|
+
projectRoot,
|
|
437
450
|
});
|
|
438
451
|
totalDrift += r.totalDrift;
|
|
439
452
|
totalUnobserved += r.totalUnobserved;
|
|
@@ -687,6 +700,8 @@ interface LiveDiffArgs {
|
|
|
687
700
|
/** `--namespace <ns>` (#1629): where to read entities that declare no
|
|
688
701
|
* namespace of their own. */
|
|
689
702
|
namespace?: string;
|
|
703
|
+
/** Directory source locations in the report are printed relative to (#3597). */
|
|
704
|
+
projectRoot?: string;
|
|
690
705
|
}
|
|
691
706
|
|
|
692
707
|
interface LiveDiffOutcome {
|
|
@@ -902,8 +917,8 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
902
917
|
if (args.json) {
|
|
903
918
|
const entry = (byLexicon[lexiconName] ??= {});
|
|
904
919
|
entry.deep = deep;
|
|
905
|
-
entry.reconcile = resolveDeepDrift(deep.drifted, provenanceOf);
|
|
906
|
-
} else renderDeepDiff(lexiconName, deep, provenanceOf);
|
|
920
|
+
entry.reconcile = resolveDeepDrift(deep.drifted, provenanceOf, { root: args.projectRoot });
|
|
921
|
+
} else renderDeepDiff(lexiconName, deep, provenanceOf, args.projectRoot);
|
|
907
922
|
// Printed on stderr regardless of `--json` (#2162), same reason the
|
|
908
923
|
// unobserved/disruption notices are: a shape with no column for
|
|
909
924
|
// "suspicious" must not read as clean.
|
|
@@ -971,6 +986,7 @@ function renderDeepDiff(
|
|
|
971
986
|
lexiconName: string,
|
|
972
987
|
deep: DeepDiffResult,
|
|
973
988
|
provenanceOf: (entity: string) => EntityProvenance | undefined = () => undefined,
|
|
989
|
+
projectRoot?: string,
|
|
974
990
|
): void {
|
|
975
991
|
const drift = countPropertyDrift(deep);
|
|
976
992
|
const unclaimedCount = countUnclaimed(deep);
|
|
@@ -1018,15 +1034,20 @@ function renderDeepDiff(
|
|
|
1018
1034
|
// something today's behaviour would get wrong. A field the author
|
|
1019
1035
|
// declared directly is left unremarked: editing it IS today's
|
|
1020
1036
|
// behaviour, and saying so on every row would bury the two that matter.
|
|
1021
|
-
|
|
1037
|
+
// #3597 — the line before the proposal names the composite call and
|
|
1038
|
+
// the argument behind the field, with the file and line it was written on.
|
|
1039
|
+
const { resolution, summary } = resolveDriftedField({
|
|
1022
1040
|
entity: entity.name,
|
|
1041
|
+
entityType: entity.type,
|
|
1023
1042
|
path: change.path,
|
|
1024
1043
|
...("declared" in change ? { declared: change.declared } : {}),
|
|
1025
1044
|
...("live" in change ? { live: change.live } : {}),
|
|
1026
1045
|
...(change.origin ? { origin: change.origin } : {}),
|
|
1027
1046
|
...(provenance ? { provenance } : {}),
|
|
1047
|
+
...(projectRoot ? { root: projectRoot } : {}),
|
|
1028
1048
|
});
|
|
1029
1049
|
if (resolution.kind !== "edit-declaration") {
|
|
1050
|
+
console.log(` ${summary}`);
|
|
1030
1051
|
console.log(` ${resolution.description}`);
|
|
1031
1052
|
}
|
|
1032
1053
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -486,6 +486,17 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
486
486
|
// `chant ci workflow --install <command>` (#3573): a step before the tick that installs what --chant runs.
|
|
487
487
|
result.ciInstall = args[++i];
|
|
488
488
|
if (!result.ciInstall || result.ciInstall.startsWith("-")) throw new Error("--install needs the install command, such as \"npm install\": --install <command>");
|
|
489
|
+
} else if (arg === "--token-secret") {
|
|
490
|
+
// `chant ci workflow --token-secret <NAME>` (#3573): the secret the tick's tag push authenticates with.
|
|
491
|
+
result.ciTokenSecret = args[++i];
|
|
492
|
+
if (!result.ciTokenSecret || result.ciTokenSecret.startsWith("-")) throw new Error("--token-secret needs a repository secret's name, such as CI_GREEN_TOKEN: --token-secret <NAME>");
|
|
493
|
+
} else if (arg === "--app-id-var") {
|
|
494
|
+
// `chant ci workflow --app-id-var <VAR> --app-key-secret <NAME>` (#3573): a GitHub App token pushes the tags.
|
|
495
|
+
result.ciAppIdVar = args[++i];
|
|
496
|
+
if (!result.ciAppIdVar || result.ciAppIdVar.startsWith("-")) throw new Error("--app-id-var needs the Actions variable holding the App's id: --app-id-var <VAR>");
|
|
497
|
+
} else if (arg === "--app-key-secret") {
|
|
498
|
+
result.ciAppKeySecret = args[++i];
|
|
499
|
+
if (!result.ciAppKeySecret || result.ciAppKeySecret.startsWith("-")) throw new Error("--app-key-secret needs the secret holding the App's private key: --app-key-secret <NAME>");
|
|
489
500
|
} else if (arg === "--verdict") {
|
|
490
501
|
// `chant workspace records review <id> --verdict agree|dissent|abstain` (#2670)
|
|
491
502
|
result.verdict = args[++i];
|
|
@@ -873,10 +884,13 @@ CI:
|
|
|
873
884
|
Reads check runs from the forge, pushes the tags.
|
|
874
885
|
ci workflow [--workflow <name>]... [--output <file>] Write
|
|
875
886
|
[--chant <command>] [--install <command>]
|
|
887
|
+
[--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
|
|
876
888
|
.github/workflows/chant-ci-green.yml, which runs the
|
|
877
889
|
tick when a required workflow completes and every 15m.
|
|
878
890
|
--chant replaces the npx @intentius/chant pin the tick
|
|
879
|
-
runs; --install adds a step before it.
|
|
891
|
+
runs; --install adds a step before it. --token-secret,
|
|
892
|
+
or a GitHub App's id and key, pushes the tags with a
|
|
893
|
+
token that may tag commits changing workflow files.
|
|
880
894
|
|
|
881
895
|
Change sets:
|
|
882
896
|
change-set summary <file> The grouped plan summary of a change-set
|
package/src/cli/registry.ts
CHANGED
|
@@ -400,6 +400,12 @@ export interface ParsedArgs {
|
|
|
400
400
|
ciChant?: string;
|
|
401
401
|
/** `chant ci workflow --install <command>` (#3573): a step before the tick that installs what `--chant` runs. */
|
|
402
402
|
ciInstall?: string;
|
|
403
|
+
/** `chant ci workflow --token-secret <NAME>` (#3573): the repository secret the tick's tag push authenticates with. */
|
|
404
|
+
ciTokenSecret?: string;
|
|
405
|
+
/** `chant ci workflow --app-id-var <VAR>` (#3573): the Actions variable holding the GitHub App id whose token pushes the tags. */
|
|
406
|
+
ciAppIdVar?: string;
|
|
407
|
+
/** `chant ci workflow --app-key-secret <NAME>` (#3573): the secret holding that App's private key. */
|
|
408
|
+
ciAppKeySecret?: string;
|
|
403
409
|
/**
|
|
404
410
|
* `chant workspace records review <id> --by <principal>` (#2670): the
|
|
405
411
|
* reviewer, as the caller names them. Also `chant workspace records new
|
package/src/composite.ts
CHANGED
|
@@ -134,6 +134,16 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// chant#3608 — every member records the composite that built it, here,
|
|
138
|
+
// when the instance is made. A member destructured from the call
|
|
139
|
+
// (`export const { vpc } = VpcDefault({})`) is never expanded, so this is
|
|
140
|
+
// the only trace that a composite wrote it; `collectEntities` reads it
|
|
141
|
+
// through {@link compositeOfMember}. A nested instance recorded its own
|
|
142
|
+
// members first and the first record is kept, so a member keeps its
|
|
143
|
+
// innermost composite. Kept off the provenance record so the composite
|
|
144
|
+
// `expandComposite` records for an instance exported whole is unchanged.
|
|
145
|
+
markCompositeMembers(members, compositeName);
|
|
146
|
+
|
|
137
147
|
// Define `members` and `_definition` as non-enumerable so spreading a
|
|
138
148
|
// composite instance (`...someComposite`) only exposes the actual member
|
|
139
149
|
// resources, not the framework's bookkeeping properties. Without this, a
|
|
@@ -156,6 +166,33 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
|
|
|
156
166
|
return definition;
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
/** Non-enumerable, symbol-keyed: invisible to serializers, spreads and comparisons, like provenance. */
|
|
170
|
+
const COMPOSITE_MEMBER_OF = Symbol.for("chant.composite.memberOf");
|
|
171
|
+
|
|
172
|
+
/** Record `compositeName` on each declarable member, descending into nested instances. First record wins. */
|
|
173
|
+
function markCompositeMembers(members: object, compositeName: string): void {
|
|
174
|
+
for (const member of Object.values(members)) {
|
|
175
|
+
if (isCompositeInstance(member)) {
|
|
176
|
+
markCompositeMembers(member.members, compositeName);
|
|
177
|
+
} else if (
|
|
178
|
+
isDeclarable(member) &&
|
|
179
|
+
Object.isExtensible(member) &&
|
|
180
|
+
!Object.prototype.hasOwnProperty.call(member, COMPOSITE_MEMBER_OF)
|
|
181
|
+
) {
|
|
182
|
+
Object.defineProperty(member, COMPOSITE_MEMBER_OF, { value: compositeName, enumerable: false, configurable: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* chant#3608 — the composite whose call built `entity`, or `undefined` for an
|
|
189
|
+
* entity no chant composite built.
|
|
190
|
+
*/
|
|
191
|
+
export function compositeOfMember(entity: object): string | undefined {
|
|
192
|
+
const name = (entity as Record<symbol, unknown>)[COMPOSITE_MEMBER_OF];
|
|
193
|
+
return typeof name === "string" ? name : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
159
196
|
/**
|
|
160
197
|
* Expands a CompositeInstance into a flat Map of prefixed entity names to Declarables.
|
|
161
198
|
* Handles nested composites recursively.
|
package/src/discovery/collect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
2
2
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
3
|
-
import { isCompositeInstance, expandComposite } from "../composite";
|
|
3
|
+
import { isCompositeInstance, expandComposite, compositeOfMember } from "../composite";
|
|
4
4
|
import { isLexiconOutput } from "../lexicon-output";
|
|
5
5
|
import { DiscoveryError } from "../errors";
|
|
6
6
|
import { setProvenance, type EntityProvenance } from "../provenance";
|
|
@@ -43,6 +43,16 @@ function stackPrefix(file: string, buildRoot: string | undefined): string {
|
|
|
43
43
|
*/
|
|
44
44
|
const AUTHORED_ROOT: EntityProvenance["paths"] = { "": { kind: "authored" } };
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The record for an entity exported on its own. chant#3608: one destructured
|
|
48
|
+
* from a composite call also names that composite, so fold provenance reads
|
|
49
|
+
* its unattributed paths as `unknown` rather than as the file's own `direct`.
|
|
50
|
+
*/
|
|
51
|
+
function authoredProvenance(entity: Declarable, file: string): EntityProvenance {
|
|
52
|
+
const composite = compositeOfMember(entity);
|
|
53
|
+
return { sourceFile: file, ...(composite ? { composite } : {}), paths: AUTHORED_ROOT };
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
/** One entity to be placed into the map, produced by {@link enumerateEntries}. */
|
|
47
57
|
interface PendingEntry {
|
|
48
58
|
/** The un-disambiguated map key this entity would take (export name, indexed
|
|
@@ -82,12 +92,12 @@ function enumerateEntries(
|
|
|
82
92
|
for (const [rawName, value] of sortedExports) {
|
|
83
93
|
const name = exportKey(rawName, file);
|
|
84
94
|
if (isDeclarable(value)) {
|
|
85
|
-
entries.push({ bareKey: name, value, file, provenance:
|
|
95
|
+
entries.push({ bareKey: name, value, file, provenance: authoredProvenance(value, file) });
|
|
86
96
|
} else if (Array.isArray(value)) {
|
|
87
97
|
for (let i = 0; i < value.length; i++) {
|
|
88
98
|
const item = value[i];
|
|
89
99
|
if (isDeclarable(item)) {
|
|
90
|
-
entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance:
|
|
100
|
+
entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance: authoredProvenance(item, file) });
|
|
91
101
|
} else if (isCompositeInstance(item)) {
|
|
92
102
|
const indexedName = `${name}_${i}`;
|
|
93
103
|
for (const [expandedName, entity] of expandComposite(indexedName, item)) {
|