@intentius/chant 0.0.1
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 +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collision-free naming strategy for TypeScript class names.
|
|
3
|
+
*
|
|
4
|
+
* 5-phase algorithm:
|
|
5
|
+
* 1. Priority names (backward compatibility)
|
|
6
|
+
* 2. Priority aliases (additional short names)
|
|
7
|
+
* 3. Short names (last segment of type)
|
|
8
|
+
* 4. Collision resolution (service-prefixed)
|
|
9
|
+
* 5. Property type aliases (globally unique defs)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal input required by the naming strategy — avoids coupling
|
|
14
|
+
* to any specific schema parser output format.
|
|
15
|
+
*/
|
|
16
|
+
export interface NamingInput {
|
|
17
|
+
typeName: string;
|
|
18
|
+
propertyTypes: Array<{ name: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration that parameterizes the naming algorithm.
|
|
23
|
+
* The data tables and name-extraction helpers are provider-specific.
|
|
24
|
+
*/
|
|
25
|
+
export interface NamingConfig {
|
|
26
|
+
/** Fixed TypeScript class names for backward compatibility. */
|
|
27
|
+
priorityNames: Record<string, string>;
|
|
28
|
+
/** Additional TypeScript names beyond the primary priority name. */
|
|
29
|
+
priorityAliases: Record<string, string[]>;
|
|
30
|
+
/** Property type aliases that must always be emitted. */
|
|
31
|
+
priorityPropertyAliases: Record<string, Record<string, string>>;
|
|
32
|
+
/** Service name abbreviations for collision-resolved names. */
|
|
33
|
+
serviceAbbreviations: Record<string, string>;
|
|
34
|
+
/** Extract the short name from a type name (e.g. "Vendor::Service::Resource" → "Resource"). */
|
|
35
|
+
shortName: (typeName: string) => string;
|
|
36
|
+
/** Extract the service name from a type name (e.g. "Vendor::Service::Resource" → "Service"). */
|
|
37
|
+
serviceName: (typeName: string) => string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class NamingStrategy {
|
|
41
|
+
private assigned = new Map<string, string>(); // typeName → primary TS name
|
|
42
|
+
private _aliases = new Map<string, string[]>(); // typeName → additional TS names
|
|
43
|
+
private usedNames = new Set<string>();
|
|
44
|
+
private _propertyAliases = new Map<string, Map<string, string>>(); // typeName → (defName → aliasName)
|
|
45
|
+
|
|
46
|
+
constructor(results: NamingInput[], private config: NamingConfig) {
|
|
47
|
+
const typeNames = results.map((r) => r.typeName);
|
|
48
|
+
|
|
49
|
+
const abbreviateService = (service: string): string =>
|
|
50
|
+
config.serviceAbbreviations[service] ?? service;
|
|
51
|
+
|
|
52
|
+
// Phase 1: assign priority names
|
|
53
|
+
for (const t of typeNames) {
|
|
54
|
+
const name = config.priorityNames[t];
|
|
55
|
+
if (name) {
|
|
56
|
+
this.assigned.set(t, name);
|
|
57
|
+
this.usedNames.add(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Phase 1b: assign priority aliases
|
|
62
|
+
for (const t of typeNames) {
|
|
63
|
+
const extras = config.priorityAliases[t];
|
|
64
|
+
if (extras) {
|
|
65
|
+
for (const alias of extras) {
|
|
66
|
+
if (!this.usedNames.has(alias)) {
|
|
67
|
+
const existing = this._aliases.get(t) ?? [];
|
|
68
|
+
existing.push(alias);
|
|
69
|
+
this._aliases.set(t, existing);
|
|
70
|
+
this.usedNames.add(alias);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Phase 2: collect short names for non-priority types and detect collisions
|
|
77
|
+
const shortNameUsers = new Map<string, string[]>(); // shortName → typeNames
|
|
78
|
+
const nonPriority: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const t of typeNames) {
|
|
81
|
+
if (this.assigned.has(t)) continue;
|
|
82
|
+
nonPriority.push(t);
|
|
83
|
+
const short = config.shortName(t);
|
|
84
|
+
const users = shortNameUsers.get(short) ?? [];
|
|
85
|
+
users.push(t);
|
|
86
|
+
shortNameUsers.set(short, users);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Phase 3: assign non-colliding short names
|
|
90
|
+
for (const [short, users] of shortNameUsers) {
|
|
91
|
+
if (users.length === 1 && !this.usedNames.has(short)) {
|
|
92
|
+
this.assigned.set(users[0], short);
|
|
93
|
+
this.usedNames.add(short);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Phase 4: resolve collisions with service prefix
|
|
98
|
+
for (const t of nonPriority) {
|
|
99
|
+
if (this.assigned.has(t)) continue;
|
|
100
|
+
const short = config.shortName(t);
|
|
101
|
+
const service = abbreviateService(config.serviceName(t));
|
|
102
|
+
const prefixed = service + short;
|
|
103
|
+
this.assigned.set(t, prefixed);
|
|
104
|
+
this.usedNames.add(prefixed);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Phase 5: compute property type aliases
|
|
108
|
+
// First, force priority property aliases
|
|
109
|
+
for (const [typeName, aliases] of Object.entries(config.priorityPropertyAliases)) {
|
|
110
|
+
if (!this.assigned.has(typeName)) continue;
|
|
111
|
+
for (const [defName, aliasName] of Object.entries(aliases)) {
|
|
112
|
+
if (!this.usedNames.has(aliasName)) {
|
|
113
|
+
let ptAliases = this._propertyAliases.get(typeName);
|
|
114
|
+
if (!ptAliases) {
|
|
115
|
+
ptAliases = new Map();
|
|
116
|
+
this._propertyAliases.set(typeName, ptAliases);
|
|
117
|
+
}
|
|
118
|
+
ptAliases.set(defName, aliasName);
|
|
119
|
+
this.usedNames.add(aliasName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Count how many resources define each property type defName
|
|
125
|
+
const defNameCount = new Map<string, number>();
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
const shortName = config.shortName(r.typeName);
|
|
128
|
+
for (const pt of r.propertyTypes) {
|
|
129
|
+
const defName = extractDefName(pt.name, shortName);
|
|
130
|
+
defNameCount.set(defName, (defNameCount.get(defName) ?? 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// For globally unique defNames, create an alias
|
|
135
|
+
for (const r of results) {
|
|
136
|
+
const typeName = r.typeName;
|
|
137
|
+
const tsName = this.assigned.get(typeName);
|
|
138
|
+
if (!tsName) continue;
|
|
139
|
+
const shortName = config.shortName(typeName);
|
|
140
|
+
|
|
141
|
+
for (const pt of r.propertyTypes) {
|
|
142
|
+
const defName = extractDefName(pt.name, shortName);
|
|
143
|
+
if (defNameCount.get(defName) === 1 && !this.usedNames.has(defName)) {
|
|
144
|
+
let ptAliases = this._propertyAliases.get(typeName);
|
|
145
|
+
if (!ptAliases) {
|
|
146
|
+
ptAliases = new Map();
|
|
147
|
+
this._propertyAliases.set(typeName, ptAliases);
|
|
148
|
+
}
|
|
149
|
+
ptAliases.set(defName, defName);
|
|
150
|
+
this.usedNames.add(defName);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Primary TypeScript class name for a type. */
|
|
157
|
+
resolve(typeName: string): string | undefined {
|
|
158
|
+
return this.assigned.get(typeName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Additional TypeScript names for a type. */
|
|
162
|
+
aliases(typeName: string): string[] {
|
|
163
|
+
return this._aliases.get(typeName) ?? [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** All (typeName, tsName) pairs sorted by TS name. */
|
|
167
|
+
allAssignments(): [string, string][] {
|
|
168
|
+
const pairs: [string, string][] = [];
|
|
169
|
+
for (const [t, name] of this.assigned) {
|
|
170
|
+
pairs.push([t, name]);
|
|
171
|
+
}
|
|
172
|
+
for (const [t, extras] of this._aliases) {
|
|
173
|
+
for (const alias of extras) {
|
|
174
|
+
pairs.push([t, alias]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
pairs.sort((a, b) => a[1].localeCompare(b[1]));
|
|
178
|
+
return pairs;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Property type aliases for a given type. */
|
|
182
|
+
propertyTypeAliases(typeName: string): Map<string, string> | undefined {
|
|
183
|
+
return this._propertyAliases.get(typeName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Construct property type name: "Bucket_ServerSideEncryptionByDefault"
|
|
189
|
+
*/
|
|
190
|
+
export function propertyTypeName(parentTSName: string, defName: string): string {
|
|
191
|
+
return `${parentTSName}_${defName}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract the raw definition name from a parser-generated name.
|
|
196
|
+
* "Bucket_ServerSideEncryptionByDefault" → "ServerSideEncryptionByDefault"
|
|
197
|
+
*/
|
|
198
|
+
export function extractDefName(parserName: string, shortName: string): string {
|
|
199
|
+
const prefix = `${shortName}_`;
|
|
200
|
+
return parserName.startsWith(prefix) ? parserName.slice(prefix.length) : parserName;
|
|
201
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic lexicon packaging pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates: generate → manifest → collect rules → collect skills →
|
|
5
|
+
* assemble BundleSpec → compute integrity → attach metadata.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { BundleSpec, LexiconManifest } from "../lexicon";
|
|
11
|
+
import { computeIntegrity } from "../lexicon-integrity";
|
|
12
|
+
import type { GenerateResult } from "./generate";
|
|
13
|
+
|
|
14
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface PackageOptions {
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PackageResult {
|
|
22
|
+
spec: BundleSpec;
|
|
23
|
+
stats: {
|
|
24
|
+
resources: number;
|
|
25
|
+
properties: number;
|
|
26
|
+
enums: number;
|
|
27
|
+
ruleCount: number;
|
|
28
|
+
skillCount: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PackagePipelineConfig {
|
|
33
|
+
/** Run generation and return artifacts. */
|
|
34
|
+
generate: (opts: { verbose?: boolean; force?: boolean }) => Promise<GenerateResult>;
|
|
35
|
+
/** Build the lexicon manifest from the generate result. */
|
|
36
|
+
buildManifest: (genResult: GenerateResult) => LexiconManifest;
|
|
37
|
+
/** Source directory for collecting rules. */
|
|
38
|
+
srcDir: string;
|
|
39
|
+
/** Rule directories relative to srcDir (default: ["lint/rules", "lint/post-synth"]). */
|
|
40
|
+
ruleDirs?: string[];
|
|
41
|
+
/** Collect skill definitions. Returns map of filename → content. */
|
|
42
|
+
collectSkills: () => Map<string, string>;
|
|
43
|
+
/** Package version for metadata. */
|
|
44
|
+
version?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Pipeline ───────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run the packaging pipeline with the supplied config.
|
|
51
|
+
*/
|
|
52
|
+
export async function packagePipeline(
|
|
53
|
+
config: PackagePipelineConfig,
|
|
54
|
+
opts: PackageOptions = {},
|
|
55
|
+
): Promise<PackageResult> {
|
|
56
|
+
const log = opts.verbose
|
|
57
|
+
? (msg: string) => console.error(msg)
|
|
58
|
+
: (_msg: string) => {};
|
|
59
|
+
|
|
60
|
+
// Step 1: Run the generation pipeline
|
|
61
|
+
log("Running generation pipeline...");
|
|
62
|
+
const result = await config.generate({ verbose: opts.verbose, force: opts.force });
|
|
63
|
+
|
|
64
|
+
// Step 2: Build manifest
|
|
65
|
+
log("Building manifest...");
|
|
66
|
+
const manifest = config.buildManifest(result);
|
|
67
|
+
|
|
68
|
+
// Step 3: Collect rules
|
|
69
|
+
log("Collecting rules...");
|
|
70
|
+
const rules = collectRules(config.srcDir, config.ruleDirs);
|
|
71
|
+
|
|
72
|
+
// Step 4: Collect skills
|
|
73
|
+
log("Collecting skills...");
|
|
74
|
+
const skills = config.collectSkills();
|
|
75
|
+
|
|
76
|
+
// Step 5: Assemble BundleSpec
|
|
77
|
+
const spec: BundleSpec = {
|
|
78
|
+
manifest,
|
|
79
|
+
registry: result.lexiconJSON,
|
|
80
|
+
typesDTS: result.typesDTS,
|
|
81
|
+
rules,
|
|
82
|
+
skills,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Step 6: Compute integrity
|
|
86
|
+
log("Computing integrity...");
|
|
87
|
+
spec.integrity = computeIntegrity(spec);
|
|
88
|
+
|
|
89
|
+
// Step 7: Populate metadata
|
|
90
|
+
spec.metadata = {
|
|
91
|
+
generatedAt: new Date().toISOString(),
|
|
92
|
+
chantVersion: "0.1.0",
|
|
93
|
+
generatorVersion: config.version ?? "0.0.0",
|
|
94
|
+
sourceSchemaCount: result.resources,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
log(`Package assembled: ${rules.size} rules, ${skills.size} skills`);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
spec,
|
|
101
|
+
stats: {
|
|
102
|
+
resources: result.resources,
|
|
103
|
+
properties: result.properties,
|
|
104
|
+
enums: result.enums,
|
|
105
|
+
ruleCount: rules.size,
|
|
106
|
+
skillCount: skills.size,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Collect lint rule source files from a lexicon package.
|
|
115
|
+
* Auto-discovers .ts files in the specified directories,
|
|
116
|
+
* skipping test files, barrel files (index.ts), and non-.ts files.
|
|
117
|
+
*/
|
|
118
|
+
export function collectRules(
|
|
119
|
+
srcDir: string,
|
|
120
|
+
dirs: string[] = ["lint/rules", "lint/post-synth"],
|
|
121
|
+
): Map<string, string> {
|
|
122
|
+
const rules = new Map<string, string>();
|
|
123
|
+
|
|
124
|
+
for (const dir of dirs) {
|
|
125
|
+
const fullDir = join(srcDir, dir);
|
|
126
|
+
let entries: string[];
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(fullDir);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!entry.endsWith(".ts")) continue;
|
|
135
|
+
if (entry.endsWith(".test.ts")) continue;
|
|
136
|
+
if (entry === "index.ts") continue;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(join(fullDir, entry), "utf-8");
|
|
140
|
+
rules.set(entry, content);
|
|
141
|
+
} catch {
|
|
142
|
+
// Skip unreadable files
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return rules;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Collect skills from a plugin's skill definitions.
|
|
152
|
+
*/
|
|
153
|
+
export function collectSkills(
|
|
154
|
+
skillDefs: Array<{ name: string; content: string }>,
|
|
155
|
+
): Map<string, string> {
|
|
156
|
+
const skills = new Map<string, string>();
|
|
157
|
+
for (const s of skillDefs) {
|
|
158
|
+
skills.set(`${s.name}.md`, s.content);
|
|
159
|
+
}
|
|
160
|
+
return skills;
|
|
161
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { snapshotArtifacts, saveSnapshot, restoreSnapshot, listSnapshots } from "./rollback";
|
|
6
|
+
|
|
7
|
+
function makeTempDir(): string {
|
|
8
|
+
const dir = join(tmpdir(), `chant-rollback-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("rollback", () => {
|
|
14
|
+
test("snapshot captures generated files with default artifact names", () => {
|
|
15
|
+
const dir = makeTempDir();
|
|
16
|
+
const genDir = join(dir, "generated");
|
|
17
|
+
mkdirSync(genDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
writeFileSync(join(genDir, "lexicon.json"), '{"Bucket":{"kind":"resource"}}');
|
|
20
|
+
writeFileSync(join(genDir, "index.d.ts"), "declare class Bucket {}");
|
|
21
|
+
writeFileSync(join(genDir, "index.ts"), "export const Bucket = {};");
|
|
22
|
+
|
|
23
|
+
const snapshot = snapshotArtifacts(genDir);
|
|
24
|
+
expect(snapshot.files["lexicon.json"]).toBeDefined();
|
|
25
|
+
expect(snapshot.files["index.d.ts"]).toBeDefined();
|
|
26
|
+
expect(snapshot.files["index.ts"]).toBeDefined();
|
|
27
|
+
expect(snapshot.hashes["lexicon.json"]).toBeDefined();
|
|
28
|
+
expect(snapshot.resourceCount).toBe(1);
|
|
29
|
+
|
|
30
|
+
rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("snapshot uses custom artifact names", () => {
|
|
34
|
+
const dir = makeTempDir();
|
|
35
|
+
const genDir = join(dir, "generated");
|
|
36
|
+
mkdirSync(genDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
writeFileSync(join(genDir, "my-lexicon.json"), '{"Resource":{"kind":"resource"}}');
|
|
39
|
+
writeFileSync(join(genDir, "types.d.ts"), "declare class Resource {}");
|
|
40
|
+
|
|
41
|
+
const snapshot = snapshotArtifacts(genDir, ["my-lexicon.json", "types.d.ts"]);
|
|
42
|
+
expect(snapshot.files["my-lexicon.json"]).toBeDefined();
|
|
43
|
+
expect(snapshot.files["types.d.ts"]).toBeDefined();
|
|
44
|
+
expect(snapshot.resourceCount).toBe(1);
|
|
45
|
+
|
|
46
|
+
rmSync(dir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("save and list snapshots", () => {
|
|
50
|
+
const dir = makeTempDir();
|
|
51
|
+
const snapshotsDir = join(dir, ".snapshots");
|
|
52
|
+
|
|
53
|
+
const snapshot = {
|
|
54
|
+
timestamp: "2025-01-01T00:00:00.000Z",
|
|
55
|
+
files: { "test.json": "{}" },
|
|
56
|
+
hashes: { "test.json": "abc123" },
|
|
57
|
+
resourceCount: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
saveSnapshot(snapshot, snapshotsDir);
|
|
61
|
+
const list = listSnapshots(snapshotsDir);
|
|
62
|
+
expect(list.length).toBe(1);
|
|
63
|
+
expect(list[0].timestamp).toBe("2025-01-01T00:00:00.000Z");
|
|
64
|
+
|
|
65
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("restore snapshot overwrites generated files", () => {
|
|
69
|
+
const dir = makeTempDir();
|
|
70
|
+
const genDir = join(dir, "generated");
|
|
71
|
+
const snapshotsDir = join(dir, ".snapshots");
|
|
72
|
+
mkdirSync(genDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
writeFileSync(join(genDir, "lexicon.json"), '{"original":true}');
|
|
75
|
+
|
|
76
|
+
const snapshot = snapshotArtifacts(genDir);
|
|
77
|
+
const snapshotPath = saveSnapshot(snapshot, snapshotsDir);
|
|
78
|
+
|
|
79
|
+
writeFileSync(join(genDir, "lexicon.json"), '{"modified":true}');
|
|
80
|
+
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"modified":true}');
|
|
81
|
+
|
|
82
|
+
restoreSnapshot(snapshotPath, genDir);
|
|
83
|
+
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"original":true}');
|
|
84
|
+
|
|
85
|
+
rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("listSnapshots returns empty for nonexistent dir", () => {
|
|
89
|
+
const list = listSnapshots("/nonexistent/path");
|
|
90
|
+
expect(list).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact snapshot and restore for generation rollback.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { hashArtifact } from "../lexicon-integrity";
|
|
7
|
+
|
|
8
|
+
export interface ArtifactSnapshot {
|
|
9
|
+
timestamp: string;
|
|
10
|
+
files: Record<string, string>;
|
|
11
|
+
hashes: Record<string, string>;
|
|
12
|
+
resourceCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SnapshotInfo {
|
|
16
|
+
path: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
resourceCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_ARTIFACT_NAMES = ["lexicon.json", "index.d.ts", "index.ts"];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Snapshot current generated artifacts.
|
|
25
|
+
*
|
|
26
|
+
* @param generatedDir - Directory containing generated artifacts
|
|
27
|
+
* @param artifactNames - List of filenames to snapshot (defaults to generic names)
|
|
28
|
+
*/
|
|
29
|
+
export function snapshotArtifacts(
|
|
30
|
+
generatedDir: string,
|
|
31
|
+
artifactNames: string[] = DEFAULT_ARTIFACT_NAMES,
|
|
32
|
+
): ArtifactSnapshot {
|
|
33
|
+
const files: Record<string, string> = {};
|
|
34
|
+
const hashes: Record<string, string> = {};
|
|
35
|
+
let resourceCount = 0;
|
|
36
|
+
|
|
37
|
+
for (const entry of artifactNames) {
|
|
38
|
+
const path = join(generatedDir, entry);
|
|
39
|
+
if (existsSync(path)) {
|
|
40
|
+
const content = readFileSync(path, "utf-8");
|
|
41
|
+
files[entry] = content;
|
|
42
|
+
hashes[entry] = hashArtifact(content);
|
|
43
|
+
|
|
44
|
+
// Count resources in any .json artifact
|
|
45
|
+
if (entry.endsWith(".json")) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(content);
|
|
48
|
+
resourceCount = Object.values(parsed).filter(
|
|
49
|
+
(e: any) => e && typeof e === "object" && e.kind === "resource"
|
|
50
|
+
).length;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
files,
|
|
59
|
+
hashes,
|
|
60
|
+
resourceCount,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save a snapshot to the .snapshots directory.
|
|
66
|
+
*/
|
|
67
|
+
export function saveSnapshot(snapshot: ArtifactSnapshot, snapshotsDir: string): string {
|
|
68
|
+
mkdirSync(snapshotsDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const filename = `${snapshot.timestamp.replace(/[:.]/g, "-")}.json`;
|
|
71
|
+
const path = join(snapshotsDir, filename);
|
|
72
|
+
writeFileSync(path, JSON.stringify(snapshot, null, 2));
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Restore a snapshot to the generated directory.
|
|
78
|
+
*/
|
|
79
|
+
export function restoreSnapshot(snapshotPath: string, generatedDir: string): void {
|
|
80
|
+
const raw = readFileSync(snapshotPath, "utf-8");
|
|
81
|
+
const snapshot: ArtifactSnapshot = JSON.parse(raw);
|
|
82
|
+
|
|
83
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
84
|
+
for (const [filename, content] of Object.entries(snapshot.files)) {
|
|
85
|
+
writeFileSync(join(generatedDir, filename), content);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* List available snapshots.
|
|
91
|
+
*/
|
|
92
|
+
export function listSnapshots(snapshotsDir: string): SnapshotInfo[] {
|
|
93
|
+
if (!existsSync(snapshotsDir)) return [];
|
|
94
|
+
|
|
95
|
+
const entries = readdirSync(snapshotsDir)
|
|
96
|
+
.filter((f) => f.endsWith(".json"))
|
|
97
|
+
.sort()
|
|
98
|
+
.reverse();
|
|
99
|
+
|
|
100
|
+
const snapshots: SnapshotInfo[] = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
try {
|
|
103
|
+
const path = join(snapshotsDir, entry);
|
|
104
|
+
const raw = readFileSync(path, "utf-8");
|
|
105
|
+
const data = JSON.parse(raw);
|
|
106
|
+
snapshots.push({
|
|
107
|
+
path,
|
|
108
|
+
timestamp: data.timestamp,
|
|
109
|
+
resourceCount: data.resourceCount ?? 0,
|
|
110
|
+
});
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return snapshots;
|
|
115
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { topoSort } from "./topo-sort";
|
|
3
|
+
|
|
4
|
+
interface Node {
|
|
5
|
+
id: string;
|
|
6
|
+
deps: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("topoSort", () => {
|
|
10
|
+
test("returns empty array for empty input", () => {
|
|
11
|
+
const result = topoSort<Node>([], (n) => n.id, (n) => n.deps);
|
|
12
|
+
expect(result).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns single node", () => {
|
|
16
|
+
const nodes: Node[] = [{ id: "A", deps: [] }];
|
|
17
|
+
const result = topoSort(nodes, (n) => n.id, (n) => n.deps);
|
|
18
|
+
expect(result.map((n) => n.id)).toEqual(["A"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("sorts dependencies before dependents", () => {
|
|
22
|
+
const nodes: Node[] = [
|
|
23
|
+
{ id: "C", deps: ["B"] },
|
|
24
|
+
{ id: "A", deps: [] },
|
|
25
|
+
{ id: "B", deps: ["A"] },
|
|
26
|
+
];
|
|
27
|
+
const result = topoSort(nodes, (n) => n.id, (n) => n.deps);
|
|
28
|
+
const ids = result.map((n) => n.id);
|
|
29
|
+
|
|
30
|
+
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
|
|
31
|
+
expect(ids.indexOf("B")).toBeLessThan(ids.indexOf("C"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("handles diamond dependency", () => {
|
|
35
|
+
const nodes: Node[] = [
|
|
36
|
+
{ id: "D", deps: ["B", "C"] },
|
|
37
|
+
{ id: "B", deps: ["A"] },
|
|
38
|
+
{ id: "C", deps: ["A"] },
|
|
39
|
+
{ id: "A", deps: [] },
|
|
40
|
+
];
|
|
41
|
+
const result = topoSort(nodes, (n) => n.id, (n) => n.deps);
|
|
42
|
+
const ids = result.map((n) => n.id);
|
|
43
|
+
|
|
44
|
+
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
|
|
45
|
+
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("C"));
|
|
46
|
+
expect(ids.indexOf("B")).toBeLessThan(ids.indexOf("D"));
|
|
47
|
+
expect(ids.indexOf("C")).toBeLessThan(ids.indexOf("D"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("handles independent nodes", () => {
|
|
51
|
+
const nodes: Node[] = [
|
|
52
|
+
{ id: "A", deps: [] },
|
|
53
|
+
{ id: "B", deps: [] },
|
|
54
|
+
{ id: "C", deps: [] },
|
|
55
|
+
];
|
|
56
|
+
const result = topoSort(nodes, (n) => n.id, (n) => n.deps);
|
|
57
|
+
expect(result).toHaveLength(3);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("ignores deps pointing to nodes not in the input", () => {
|
|
61
|
+
const nodes: Node[] = [
|
|
62
|
+
{ id: "A", deps: ["X"] }, // X not in nodes
|
|
63
|
+
{ id: "B", deps: ["A"] },
|
|
64
|
+
];
|
|
65
|
+
const result = topoSort(nodes, (n) => n.id, (n) => n.deps);
|
|
66
|
+
const ids = result.map((n) => n.id);
|
|
67
|
+
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic DFS-based topological sort.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sort nodes in topological order (dependencies first).
|
|
7
|
+
*
|
|
8
|
+
* @param nodes - The items to sort.
|
|
9
|
+
* @param getId - Extract a unique identifier from a node.
|
|
10
|
+
* @param getEdges - Return the IDs of nodes that `node` depends on.
|
|
11
|
+
* @returns Sorted array where dependencies appear before dependents.
|
|
12
|
+
*/
|
|
13
|
+
export function topoSort<T>(
|
|
14
|
+
nodes: T[],
|
|
15
|
+
getId: (node: T) => string,
|
|
16
|
+
getEdges: (node: T) => string[],
|
|
17
|
+
): T[] {
|
|
18
|
+
const result: T[] = [];
|
|
19
|
+
const added = new Set<string>();
|
|
20
|
+
const nodeMap = new Map<string, T>();
|
|
21
|
+
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
nodeMap.set(getId(node), node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function visit(node: T): void {
|
|
27
|
+
const id = getId(node);
|
|
28
|
+
if (added.has(id)) return;
|
|
29
|
+
|
|
30
|
+
for (const dep of getEdges(node)) {
|
|
31
|
+
const depNode = nodeMap.get(dep);
|
|
32
|
+
if (depNode && !added.has(dep)) {
|
|
33
|
+
visit(depNode);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result.push(node);
|
|
38
|
+
added.add(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const node of nodes) {
|
|
42
|
+
visit(node);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|