@intentius/chant 0.0.4 → 0.0.8
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 +10 -351
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +3 -54
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +8 -23
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +31 -95
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +16 -10
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/main.ts +14 -4
- package/src/cli/mcp/server.test.ts +207 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +107 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/codegen/docs-interpolation.test.ts +2 -2
- package/src/codegen/docs.ts +5 -4
- package/src/codegen/generate-registry.test.ts +2 -2
- package/src/codegen/generate-registry.ts +5 -6
- package/src/codegen/generate-typescript.test.ts +6 -6
- package/src/codegen/generate-typescript.ts +2 -6
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +28 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/codegen/validate.ts +16 -0
- package/src/config.ts +4 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +1 -2
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon.ts +2 -6
- package/src/lint/config.ts +8 -6
- package/src/lint/engine.ts +1 -5
- package/src/lint/rule.ts +0 -18
- package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
- package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
- package/src/lint/rules/index.ts +1 -22
- package/src/runtime-adapter.ts +158 -0
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
- package/src/lint/rules/barrel-import-style.test.ts +0 -80
- package/src/lint/rules/barrel-import-style.ts +0 -59
- package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
- package/src/lint/rules/enforce-barrel-import.ts +0 -81
- package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
- package/src/lint/rules/enforce-barrel-ref.ts +0 -75
- package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
- package/src/lint/rules/evl006-barrel-usage.ts +0 -95
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
- package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
- package/src/lint/rules/prefer-namespace-import.ts +0 -63
- package/src/lint/rules/stale-barrel-types.ts +0 -60
- package/src/project/scan.test.ts +0 -178
- package/src/project/scan.ts +0 -182
- package/src/project/sync.test.ts +0 -87
- package/src/project/sync.ts +0 -46
|
@@ -5,60 +5,78 @@ import { getCompositeFactory } from "./composite-scope";
|
|
|
5
5
|
/**
|
|
6
6
|
* EVL009: No extractable constants inside Composite factory
|
|
7
7
|
*
|
|
8
|
-
* Composite factories should only reference props, sibling members, and
|
|
9
|
-
* Object literals and arrays-of-objects that don't
|
|
10
|
-
* extractable constants that belong in a
|
|
8
|
+
* Composite factories should only reference props, sibling members, and
|
|
9
|
+
* imported identifiers. Object literals and arrays-of-objects that don't
|
|
10
|
+
* reference any of these are extractable constants that belong in a
|
|
11
|
+
* separate file (e.g. defaults.ts).
|
|
11
12
|
*
|
|
12
13
|
* Triggers on: assumeRolePolicyDocument: { Version: "2012-10-17", Statement: [...] }
|
|
13
14
|
* Triggers on: managedPolicyArns: ["arn:aws:iam::aws:policy/..."]
|
|
14
|
-
* OK: assumeRolePolicyDocument:
|
|
15
|
+
* OK: assumeRolePolicyDocument: lambdaTrustPolicy (imported)
|
|
15
16
|
* OK: policies: props.policies
|
|
16
17
|
* OK: role: role.arn
|
|
17
18
|
* OK: action: "lambda:InvokeFunction" (simple string literal — not an object)
|
|
18
19
|
*/
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Collect all imported identifiers from import declarations in the file.
|
|
23
|
+
*/
|
|
24
|
+
function collectImportedNames(sourceFile: ts.SourceFile): Set<string> {
|
|
25
|
+
const names = new Set<string>();
|
|
26
|
+
for (const stmt of sourceFile.statements) {
|
|
27
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue;
|
|
28
|
+
|
|
29
|
+
// import foo from "..."
|
|
30
|
+
if (stmt.importClause.name) {
|
|
31
|
+
names.add(stmt.importClause.name.text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bindings = stmt.importClause.namedBindings;
|
|
35
|
+
if (!bindings) continue;
|
|
36
|
+
|
|
37
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
38
|
+
// import * as ns from "..."
|
|
39
|
+
names.add(bindings.name.text);
|
|
40
|
+
} else if (ts.isNamedImports(bindings)) {
|
|
41
|
+
// import { a, b } from "..."
|
|
42
|
+
for (const spec of bindings.elements) {
|
|
43
|
+
names.add(spec.name.text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
/**
|
|
21
51
|
* Check whether any leaf node in the subtree references:
|
|
22
52
|
* - The props parameter identifier
|
|
23
|
-
* -
|
|
53
|
+
* - An imported identifier (direct import or namespace import)
|
|
24
54
|
* - A local variable declared in the composite factory body
|
|
25
55
|
*/
|
|
26
56
|
function referencesPropsOrMembers(
|
|
27
57
|
node: ts.Node,
|
|
28
58
|
propsName: string,
|
|
29
59
|
localNames: Set<string>,
|
|
60
|
+
importedNames: Set<string>,
|
|
30
61
|
): boolean {
|
|
31
62
|
if (ts.isIdentifier(node)) {
|
|
32
63
|
// Direct props reference
|
|
33
64
|
if (node.text === propsName) return true;
|
|
34
65
|
// Local variable reference (sibling member)
|
|
35
66
|
if (localNames.has(node.text)) return true;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Barrel ref: _.$.something or $.something
|
|
39
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
40
|
-
if (isBarrelRef(node)) return true;
|
|
67
|
+
// Imported identifier (direct or namespace)
|
|
68
|
+
if (importedNames.has(node.text)) return true;
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
let found = false;
|
|
44
72
|
ts.forEachChild(node, (child) => {
|
|
45
|
-
if (!found && referencesPropsOrMembers(child, propsName, localNames)) {
|
|
73
|
+
if (!found && referencesPropsOrMembers(child, propsName, localNames, importedNames)) {
|
|
46
74
|
found = true;
|
|
47
75
|
}
|
|
48
76
|
});
|
|
49
77
|
return found;
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
function isBarrelRef(node: ts.PropertyAccessExpression): boolean {
|
|
53
|
-
// _.$.x or $.x
|
|
54
|
-
const expr = node.expression;
|
|
55
|
-
if (ts.isIdentifier(expr) && expr.text === "$") return true;
|
|
56
|
-
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name) && expr.name.text === "$") {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
80
|
/**
|
|
63
81
|
* Check if a value is an extractable constant:
|
|
64
82
|
* - Object literal (with any nesting)
|
|
@@ -79,6 +97,7 @@ function checkComposite(
|
|
|
79
97
|
call: ts.CallExpression,
|
|
80
98
|
context: LintContext,
|
|
81
99
|
diagnostics: LintDiagnostic[],
|
|
100
|
+
importedNames: Set<string>,
|
|
82
101
|
): void {
|
|
83
102
|
const factory = call.arguments[0];
|
|
84
103
|
if (!factory || (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))) return;
|
|
@@ -103,7 +122,7 @@ function checkComposite(
|
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
// Walk all NewExpression nodes inside the factory body
|
|
106
|
-
checkForConstants(body, context, diagnostics, propsName, localNames);
|
|
125
|
+
checkForConstants(body, context, diagnostics, propsName, localNames, importedNames);
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
function checkForConstants(
|
|
@@ -112,6 +131,7 @@ function checkForConstants(
|
|
|
112
131
|
diagnostics: LintDiagnostic[],
|
|
113
132
|
propsName: string,
|
|
114
133
|
localNames: Set<string>,
|
|
134
|
+
importedNames: Set<string>,
|
|
115
135
|
): void {
|
|
116
136
|
if (ts.isNewExpression(node) && node.arguments && node.arguments.length > 0) {
|
|
117
137
|
const firstArg = node.arguments[0];
|
|
@@ -120,7 +140,7 @@ function checkForConstants(
|
|
|
120
140
|
if (!ts.isPropertyAssignment(prop)) continue;
|
|
121
141
|
const value = prop.initializer;
|
|
122
142
|
|
|
123
|
-
if (isExtractableShape(value) && !referencesPropsOrMembers(value, propsName, localNames)) {
|
|
143
|
+
if (isExtractableShape(value) && !referencesPropsOrMembers(value, propsName, localNames, importedNames)) {
|
|
124
144
|
const propName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
125
145
|
? (ts.isIdentifier(prop.name) ? prop.name.text : prop.name.text)
|
|
126
146
|
: "<computed>";
|
|
@@ -135,7 +155,7 @@ function checkForConstants(
|
|
|
135
155
|
column: character + 1,
|
|
136
156
|
ruleId: "EVL009",
|
|
137
157
|
severity: "warning",
|
|
138
|
-
message: `Extractable constant in Composite factory property "${propName}" — move to a separate file and
|
|
158
|
+
message: `Extractable constant in Composite factory property "${propName}" — move to a separate file and import directly`,
|
|
139
159
|
});
|
|
140
160
|
}
|
|
141
161
|
}
|
|
@@ -143,20 +163,20 @@ function checkForConstants(
|
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
ts.forEachChild(node, (child) =>
|
|
146
|
-
checkForConstants(child, context, diagnostics, propsName, localNames),
|
|
166
|
+
checkForConstants(child, context, diagnostics, propsName, localNames, importedNames),
|
|
147
167
|
);
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
170
|
+
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[], importedNames: Set<string>): void {
|
|
151
171
|
if (ts.isCallExpression(node)) {
|
|
152
172
|
const factory = getCompositeFactory(node);
|
|
153
173
|
if (factory) {
|
|
154
|
-
checkComposite(node, context, diagnostics);
|
|
174
|
+
checkComposite(node, context, diagnostics, importedNames);
|
|
155
175
|
return; // Don't recurse further — checkComposite handles it
|
|
156
176
|
}
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
|
|
179
|
+
ts.forEachChild(node, (child) => checkNode(child, context, diagnostics, importedNames));
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
export const evl009CompositeNoConstantRule: LintRule = {
|
|
@@ -165,7 +185,8 @@ export const evl009CompositeNoConstantRule: LintRule = {
|
|
|
165
185
|
category: "style",
|
|
166
186
|
check(context: LintContext): LintDiagnostic[] {
|
|
167
187
|
const diagnostics: LintDiagnostic[] = [];
|
|
168
|
-
|
|
188
|
+
const importedNames = collectImportedNames(context.sourceFile);
|
|
189
|
+
checkNode(context.sourceFile, context, diagnostics, importedNames);
|
|
169
190
|
return diagnostics;
|
|
170
191
|
},
|
|
171
192
|
};
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -8,8 +8,6 @@ export { flatDeclarationsRule } from "./flat-declarations";
|
|
|
8
8
|
export { exportRequiredRule } from "./export-required";
|
|
9
9
|
export { fileDeclarableLimitRule } from "./file-declarable-limit";
|
|
10
10
|
export { singleConcernFileRule } from "./single-concern-file";
|
|
11
|
-
export { preferNamespaceImportRule } from "./prefer-namespace-import";
|
|
12
|
-
export { barrelImportStyleRule } from "./barrel-import-style";
|
|
13
11
|
export { declarableNamingConventionRule } from "./declarable-naming-convention";
|
|
14
12
|
export { noUnusedDeclarableImportRule } from "./no-unused-declarable-import";
|
|
15
13
|
export { noRedundantValueCastRule } from "./no-redundant-value-cast";
|
|
@@ -17,17 +15,12 @@ export { noUnusedDeclarableRule } from "./no-unused-declarable";
|
|
|
17
15
|
export { noCyclicDeclarableRefRule } from "./no-cyclic-declarable-ref";
|
|
18
16
|
export { noRedundantTypeImportRule } from "./no-redundant-type-import";
|
|
19
17
|
export { noStringRefRule } from "./no-string-ref";
|
|
20
|
-
export { enforceBarrelImportRule } from "./enforce-barrel-import";
|
|
21
|
-
export { enforceBarrelRefRule } from "./enforce-barrel-ref";
|
|
22
18
|
export { evl001NonLiteralExpressionRule } from "./evl001-non-literal-expression";
|
|
23
19
|
export { evl002ControlFlowResourceRule } from "./evl002-control-flow-resource";
|
|
24
20
|
export { evl003DynamicPropertyAccessRule } from "./evl003-dynamic-property-access";
|
|
25
21
|
export { evl004SpreadNonConstRule } from "./evl004-spread-non-const";
|
|
26
22
|
export { evl005ResourceBlockBodyRule } from "./evl005-resource-block-body";
|
|
27
|
-
export { evl006BarrelUsageRule } from "./evl006-barrel-usage";
|
|
28
23
|
export { evl007InvalidSiblingsRule } from "./evl007-invalid-siblings";
|
|
29
|
-
export { evl008UnresolvableBarrelRefRule } from "./evl008-unresolvable-barrel-ref";
|
|
30
|
-
export { staleBarrelTypesRule } from "./stale-barrel-types";
|
|
31
24
|
export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
32
25
|
export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
33
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
@@ -37,8 +30,6 @@ import { flatDeclarationsRule } from "./flat-declarations";
|
|
|
37
30
|
import { exportRequiredRule } from "./export-required";
|
|
38
31
|
import { fileDeclarableLimitRule } from "./file-declarable-limit";
|
|
39
32
|
import { singleConcernFileRule } from "./single-concern-file";
|
|
40
|
-
import { preferNamespaceImportRule } from "./prefer-namespace-import";
|
|
41
|
-
import { barrelImportStyleRule } from "./barrel-import-style";
|
|
42
33
|
import { declarableNamingConventionRule } from "./declarable-naming-convention";
|
|
43
34
|
import { noUnusedDeclarableImportRule } from "./no-unused-declarable-import";
|
|
44
35
|
import { noRedundantValueCastRule } from "./no-redundant-value-cast";
|
|
@@ -46,24 +37,19 @@ import { noUnusedDeclarableRule } from "./no-unused-declarable";
|
|
|
46
37
|
import { noCyclicDeclarableRefRule } from "./no-cyclic-declarable-ref";
|
|
47
38
|
import { noRedundantTypeImportRule } from "./no-redundant-type-import";
|
|
48
39
|
import { noStringRefRule } from "./no-string-ref";
|
|
49
|
-
import { enforceBarrelImportRule } from "./enforce-barrel-import";
|
|
50
|
-
import { enforceBarrelRefRule } from "./enforce-barrel-ref";
|
|
51
40
|
import { evl001NonLiteralExpressionRule } from "./evl001-non-literal-expression";
|
|
52
41
|
import { evl002ControlFlowResourceRule } from "./evl002-control-flow-resource";
|
|
53
42
|
import { evl003DynamicPropertyAccessRule } from "./evl003-dynamic-property-access";
|
|
54
43
|
import { evl004SpreadNonConstRule } from "./evl004-spread-non-const";
|
|
55
44
|
import { evl005ResourceBlockBodyRule } from "./evl005-resource-block-body";
|
|
56
|
-
import { evl006BarrelUsageRule } from "./evl006-barrel-usage";
|
|
57
45
|
import { evl007InvalidSiblingsRule } from "./evl007-invalid-siblings";
|
|
58
|
-
import { evl008UnresolvableBarrelRefRule } from "./evl008-unresolvable-barrel-ref";
|
|
59
|
-
import { staleBarrelTypesRule } from "./stale-barrel-types";
|
|
60
46
|
import { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
61
47
|
import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
62
48
|
import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
63
49
|
import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
64
50
|
|
|
65
51
|
/**
|
|
66
|
-
* Load all
|
|
52
|
+
* Load all 21 core lint rules (COR + EVL).
|
|
67
53
|
*/
|
|
68
54
|
export function loadCoreRules(): LintRule[] {
|
|
69
55
|
return [
|
|
@@ -71,8 +57,6 @@ export function loadCoreRules(): LintRule[] {
|
|
|
71
57
|
exportRequiredRule,
|
|
72
58
|
fileDeclarableLimitRule,
|
|
73
59
|
singleConcernFileRule,
|
|
74
|
-
preferNamespaceImportRule,
|
|
75
|
-
barrelImportStyleRule,
|
|
76
60
|
declarableNamingConventionRule,
|
|
77
61
|
noUnusedDeclarableImportRule,
|
|
78
62
|
noRedundantValueCastRule,
|
|
@@ -80,17 +64,12 @@ export function loadCoreRules(): LintRule[] {
|
|
|
80
64
|
noCyclicDeclarableRefRule,
|
|
81
65
|
noRedundantTypeImportRule,
|
|
82
66
|
noStringRefRule,
|
|
83
|
-
enforceBarrelImportRule,
|
|
84
|
-
enforceBarrelRefRule,
|
|
85
67
|
evl001NonLiteralExpressionRule,
|
|
86
68
|
evl002ControlFlowResourceRule,
|
|
87
69
|
evl003DynamicPropertyAccessRule,
|
|
88
70
|
evl004SpreadNonConstRule,
|
|
89
71
|
evl005ResourceBlockBodyRule,
|
|
90
|
-
evl006BarrelUsageRule,
|
|
91
72
|
evl007InvalidSiblingsRule,
|
|
92
|
-
evl008UnresolvableBarrelRefRule,
|
|
93
|
-
staleBarrelTypesRule,
|
|
94
73
|
evl009CompositeNoConstantRule,
|
|
95
74
|
evl010CompositeNoTransformRule,
|
|
96
75
|
cor017CompositeNameMatchRule,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime adapter — abstracts Bun-specific APIs so chant can run on Bun or Node.js.
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects the host runtime and delegates to the appropriate implementation.
|
|
5
|
+
* The `target` parameter (from config) controls what gets spawned (bun vs node/npx/npm),
|
|
6
|
+
* not which adapter class is used.
|
|
7
|
+
*/
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { execFile } from "child_process";
|
|
12
|
+
// @ts-ignore — picomatch has no types declaration
|
|
13
|
+
import picomatch from "picomatch";
|
|
14
|
+
|
|
15
|
+
export interface SpawnResult {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RuntimeCommands {
|
|
22
|
+
/** Runtime binary: "bun" | "node" */
|
|
23
|
+
runner: string;
|
|
24
|
+
/** Package executor: "bunx" | "npx" */
|
|
25
|
+
exec: string;
|
|
26
|
+
/** Pack command: ["bun", "pm", "pack"] | ["npm", "pack"] */
|
|
27
|
+
packCmd: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RuntimeAdapter {
|
|
31
|
+
readonly name: "bun" | "node";
|
|
32
|
+
/** Hash content and return hex string */
|
|
33
|
+
hash(content: string): string;
|
|
34
|
+
/** Algorithm name recorded in integrity.json */
|
|
35
|
+
readonly hashAlgorithm: string;
|
|
36
|
+
/** Test whether filePath matches a glob pattern */
|
|
37
|
+
globMatch(pattern: string, filePath: string): boolean;
|
|
38
|
+
/** Spawn a child process and collect output */
|
|
39
|
+
spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult>;
|
|
40
|
+
/** Commands to use when spawning package manager / executor */
|
|
41
|
+
readonly commands: RuntimeCommands;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Bun adapter ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
class BunRuntimeAdapter implements RuntimeAdapter {
|
|
47
|
+
readonly name = "bun" as const;
|
|
48
|
+
readonly hashAlgorithm = "xxhash64";
|
|
49
|
+
readonly commands: RuntimeCommands;
|
|
50
|
+
|
|
51
|
+
constructor(target: "bun" | "node") {
|
|
52
|
+
this.commands =
|
|
53
|
+
target === "node"
|
|
54
|
+
? { runner: "node", exec: "npx", packCmd: ["npm", "pack"] }
|
|
55
|
+
: { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hash(content: string): string {
|
|
59
|
+
return Bun.hash(content).toString(16);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
globMatch(pattern: string, filePath: string): boolean {
|
|
63
|
+
const glob = new Bun.Glob(pattern);
|
|
64
|
+
return glob.match(filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
68
|
+
const proc = Bun.spawn(cmd, {
|
|
69
|
+
cwd: opts?.cwd,
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
});
|
|
73
|
+
const [stdout, stderr] = await Promise.all([
|
|
74
|
+
new Response(proc.stdout).text(),
|
|
75
|
+
new Response(proc.stderr).text(),
|
|
76
|
+
]);
|
|
77
|
+
const exitCode = await proc.exited;
|
|
78
|
+
return { stdout, stderr, exitCode };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Node adapter ─────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
85
|
+
readonly name = "node" as const;
|
|
86
|
+
readonly hashAlgorithm = "sha256";
|
|
87
|
+
readonly commands: RuntimeCommands;
|
|
88
|
+
|
|
89
|
+
constructor(target: "bun" | "node") {
|
|
90
|
+
this.commands =
|
|
91
|
+
target === "bun"
|
|
92
|
+
? { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] }
|
|
93
|
+
: { runner: "node", exec: "npx", packCmd: ["npm", "pack"] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hash(content: string): string {
|
|
97
|
+
return createHash("sha256").update(content).digest("hex");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
globMatch(pattern: string, filePath: string): boolean {
|
|
101
|
+
return picomatch(pattern)(filePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
execFile(cmd[0], cmd.slice(1), { cwd: opts?.cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
107
|
+
resolve({
|
|
108
|
+
stdout: stdout ?? "",
|
|
109
|
+
stderr: stderr ?? "",
|
|
110
|
+
exitCode: err ? (err as any).code ?? 1 : 0,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Singleton ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
let _runtime: RuntimeAdapter | undefined;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Detect whether we're running under Bun.
|
|
123
|
+
*/
|
|
124
|
+
function isBun(): boolean {
|
|
125
|
+
return typeof globalThis.Bun !== "undefined";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Initialize the runtime adapter singleton.
|
|
130
|
+
*
|
|
131
|
+
* @param target - Which commands to spawn ("bun" or "node"). Defaults to auto-detect.
|
|
132
|
+
* Controls the `commands` property, not which adapter class is used.
|
|
133
|
+
*/
|
|
134
|
+
export function initRuntime(target?: "bun" | "node"): RuntimeAdapter {
|
|
135
|
+
const resolvedTarget = target ?? (isBun() ? "bun" : "node");
|
|
136
|
+
_runtime = isBun()
|
|
137
|
+
? new BunRuntimeAdapter(resolvedTarget)
|
|
138
|
+
: new NodeRuntimeAdapter(resolvedTarget);
|
|
139
|
+
return _runtime;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the runtime adapter. Lazily initializes with auto-detection if not yet set.
|
|
144
|
+
*/
|
|
145
|
+
export function getRuntime(): RuntimeAdapter {
|
|
146
|
+
if (!_runtime) {
|
|
147
|
+
return initRuntime();
|
|
148
|
+
}
|
|
149
|
+
return _runtime;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert `import.meta.url` to a directory path.
|
|
154
|
+
* Works on both Bun and Node (replaces Bun-only `import.meta.dir`).
|
|
155
|
+
*/
|
|
156
|
+
export function moduleDir(importMetaUrl: string): string {
|
|
157
|
+
return dirname(fileURLToPath(importMetaUrl));
|
|
158
|
+
}
|
|
@@ -93,15 +93,6 @@ describe("walkValue", () => {
|
|
|
93
93
|
expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test("applies transformKey when provided", () => {
|
|
97
|
-
const visitor: SerializerVisitor = {
|
|
98
|
-
...mockVisitor,
|
|
99
|
-
transformKey: (k) => k.toUpperCase(),
|
|
100
|
-
};
|
|
101
|
-
const names = new Map<Declarable, string>();
|
|
102
|
-
expect(walkValue({ foo: 1, bar: 2 }, names, visitor)).toEqual({ FOO: 1, BAR: 2 });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
96
|
test("complex nested structure", () => {
|
|
106
97
|
const resource = makeDeclarable("Test::Role");
|
|
107
98
|
const ref = new AttrRef(resource, "arn");
|
package/src/serializer-walker.ts
CHANGED
|
@@ -17,8 +17,6 @@ export interface SerializerVisitor {
|
|
|
17
17
|
resourceRef(logicalName: string): unknown;
|
|
18
18
|
/** Format a property-level Declarable by walking its props. */
|
|
19
19
|
propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
|
|
20
|
-
/** Optional key transformation (e.g. camelCase → PascalCase). */
|
|
21
|
-
transformKey?(key: string): string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
@@ -73,7 +71,7 @@ export function walkValue(
|
|
|
73
71
|
if (typeof value === "object") {
|
|
74
72
|
const result: Record<string, unknown> = {};
|
|
75
73
|
for (const [key, val] of Object.entries(value)) {
|
|
76
|
-
const outKey =
|
|
74
|
+
const outKey = key;
|
|
77
75
|
result[outKey] = walkValue(val, entityNames, visitor);
|
|
78
76
|
}
|
|
79
77
|
return result;
|
package/src/stack-output.ts
CHANGED
|
@@ -50,10 +50,10 @@ export function isStackOutput(value: unknown): value is StackOutput {
|
|
|
50
50
|
* @example
|
|
51
51
|
* ```ts
|
|
52
52
|
* import { stackOutput } from "@intentius/chant";
|
|
53
|
-
* import
|
|
53
|
+
* import { vpc, subnet } from "./vpc";
|
|
54
54
|
*
|
|
55
|
-
* export const vpcId = stackOutput(
|
|
56
|
-
* export const subnetId = stackOutput(
|
|
55
|
+
* export const vpcId = stackOutput(vpc.vpcId);
|
|
56
|
+
* export const subnetId = stackOutput(subnet.subnetId, {
|
|
57
57
|
* description: "Primary subnet ID",
|
|
58
58
|
* });
|
|
59
59
|
* ```
|
package/src/barrel.test.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import { barrel } from "./barrel";
|
|
6
|
-
|
|
7
|
-
let tempDir: string;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
tempDir = mkdtempSync(join(tmpdir(), "barrel-test-"));
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("barrel", () => {
|
|
18
|
-
test("returns exports from sibling .ts files", () => {
|
|
19
|
-
writeFileSync(
|
|
20
|
-
join(tempDir, "alpha.ts"),
|
|
21
|
-
"export const alphaValue = 42;",
|
|
22
|
-
);
|
|
23
|
-
writeFileSync(
|
|
24
|
-
join(tempDir, "beta.ts"),
|
|
25
|
-
"export const betaValue = 'hello';",
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const $ = barrel(tempDir);
|
|
29
|
-
|
|
30
|
-
expect($.alphaValue).toBe(42);
|
|
31
|
-
expect($.betaValue).toBe("hello");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("excludes files starting with underscore", () => {
|
|
35
|
-
writeFileSync(
|
|
36
|
-
join(tempDir, "_config.ts"),
|
|
37
|
-
"export const secret = 'hidden';",
|
|
38
|
-
);
|
|
39
|
-
writeFileSync(
|
|
40
|
-
join(tempDir, "visible.ts"),
|
|
41
|
-
"export const visible = true;",
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const $ = barrel(tempDir);
|
|
45
|
-
|
|
46
|
-
expect($.secret).toBeUndefined();
|
|
47
|
-
expect($.visible).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("excludes test files", () => {
|
|
51
|
-
writeFileSync(
|
|
52
|
-
join(tempDir, "foo.test.ts"),
|
|
53
|
-
"export const testVal = 1;",
|
|
54
|
-
);
|
|
55
|
-
writeFileSync(
|
|
56
|
-
join(tempDir, "bar.spec.ts"),
|
|
57
|
-
"export const specVal = 2;",
|
|
58
|
-
);
|
|
59
|
-
writeFileSync(
|
|
60
|
-
join(tempDir, "real.ts"),
|
|
61
|
-
"export const realVal = 3;",
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const $ = barrel(tempDir);
|
|
65
|
-
|
|
66
|
-
expect($.testVal).toBeUndefined();
|
|
67
|
-
expect($.specVal).toBeUndefined();
|
|
68
|
-
expect($.realVal).toBe(3);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("caches results after first access", () => {
|
|
72
|
-
writeFileSync(
|
|
73
|
-
join(tempDir, "mod.ts"),
|
|
74
|
-
"export const x = 1; export const y = 2;",
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const $ = barrel(tempDir);
|
|
78
|
-
|
|
79
|
-
// First access triggers load
|
|
80
|
-
expect($.x).toBe(1);
|
|
81
|
-
|
|
82
|
-
// Write a new file after first load
|
|
83
|
-
writeFileSync(
|
|
84
|
-
join(tempDir, "late.ts"),
|
|
85
|
-
"export const lateVal = 99;",
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// Should NOT pick up new file since cache is already populated
|
|
89
|
-
expect($.lateVal).toBeUndefined();
|
|
90
|
-
// Existing values still work
|
|
91
|
-
expect($.y).toBe(2);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("returns undefined for missing keys", () => {
|
|
95
|
-
writeFileSync(
|
|
96
|
-
join(tempDir, "mod.ts"),
|
|
97
|
-
"export const exists = true;",
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const $ = barrel(tempDir);
|
|
101
|
-
|
|
102
|
-
expect($.nonExistent).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("has and ownKeys work correctly", () => {
|
|
106
|
-
writeFileSync(
|
|
107
|
-
join(tempDir, "mod.ts"),
|
|
108
|
-
"export const dataBucket = 's3://bucket';",
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const $ = barrel(tempDir);
|
|
112
|
-
|
|
113
|
-
expect("dataBucket" in $).toBe(true);
|
|
114
|
-
expect("missing" in $).toBe(false);
|
|
115
|
-
expect(Object.keys($)).toEqual(["dataBucket"]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("handles empty directory", () => {
|
|
119
|
-
const $ = barrel(tempDir);
|
|
120
|
-
|
|
121
|
-
expect(Object.keys($)).toEqual([]);
|
|
122
|
-
expect($.anything).toBeUndefined();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("handles load errors gracefully", () => {
|
|
126
|
-
writeFileSync(
|
|
127
|
-
join(tempDir, "broken.ts"),
|
|
128
|
-
"export const x = ;; SYNTAX ERROR @@#$",
|
|
129
|
-
);
|
|
130
|
-
writeFileSync(
|
|
131
|
-
join(tempDir, "good.ts"),
|
|
132
|
-
"export const goodVal = 'works';",
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const $ = barrel(tempDir);
|
|
136
|
-
|
|
137
|
-
expect($.goodVal).toBe("works");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("first export wins on name collision", () => {
|
|
141
|
-
// Files are sorted by readdirSync (OS-dependent), but we can verify
|
|
142
|
-
// that the proxy doesn't crash on collisions
|
|
143
|
-
writeFileSync(
|
|
144
|
-
join(tempDir, "aaa.ts"),
|
|
145
|
-
"export const shared = 'first';",
|
|
146
|
-
);
|
|
147
|
-
writeFileSync(
|
|
148
|
-
join(tempDir, "zzz.ts"),
|
|
149
|
-
"export const shared = 'second';",
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const $ = barrel(tempDir);
|
|
153
|
-
|
|
154
|
-
// One of them wins (first by readdir order)
|
|
155
|
-
expect(typeof $.shared).toBe("string");
|
|
156
|
-
});
|
|
157
|
-
});
|