@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.
Files changed (75) hide show
  1. package/README.md +10 -351
  2. package/bin/chant +20 -0
  3. package/package.json +18 -17
  4. package/src/bench.test.ts +3 -54
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
  6. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
  7. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +8 -23
  8. package/src/cli/commands/build.ts +1 -2
  9. package/src/cli/commands/import.test.ts +1 -1
  10. package/src/cli/commands/import.ts +2 -2
  11. package/src/cli/commands/init-lexicon.test.ts +0 -3
  12. package/src/cli/commands/init-lexicon.ts +31 -95
  13. package/src/cli/commands/init.test.ts +10 -14
  14. package/src/cli/commands/init.ts +16 -10
  15. package/src/cli/commands/lint.ts +9 -33
  16. package/src/cli/commands/list.ts +2 -2
  17. package/src/cli/commands/update.ts +5 -3
  18. package/src/cli/conflict-check.test.ts +0 -1
  19. package/src/cli/handlers/dev.ts +1 -9
  20. package/src/cli/main.ts +14 -4
  21. package/src/cli/mcp/server.test.ts +207 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +107 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/codegen/docs-interpolation.test.ts +2 -2
  27. package/src/codegen/docs.ts +5 -4
  28. package/src/codegen/generate-registry.test.ts +2 -2
  29. package/src/codegen/generate-registry.ts +5 -6
  30. package/src/codegen/generate-typescript.test.ts +6 -6
  31. package/src/codegen/generate-typescript.ts +2 -6
  32. package/src/codegen/generate.ts +1 -12
  33. package/src/codegen/package.ts +28 -1
  34. package/src/codegen/typecheck.ts +6 -11
  35. package/src/codegen/validate.ts +16 -0
  36. package/src/config.ts +4 -0
  37. package/src/discovery/files.ts +6 -6
  38. package/src/discovery/import.ts +1 -1
  39. package/src/index.ts +1 -2
  40. package/src/lexicon-integrity.ts +5 -4
  41. package/src/lexicon.ts +2 -6
  42. package/src/lint/config.ts +8 -6
  43. package/src/lint/engine.ts +1 -5
  44. package/src/lint/rule.ts +0 -18
  45. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  46. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  47. package/src/lint/rules/index.ts +1 -22
  48. package/src/runtime-adapter.ts +158 -0
  49. package/src/serializer-walker.test.ts +0 -9
  50. package/src/serializer-walker.ts +1 -3
  51. package/src/stack-output.ts +3 -3
  52. package/src/barrel.test.ts +0 -157
  53. package/src/barrel.ts +0 -101
  54. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  55. package/src/codegen/case.test.ts +0 -30
  56. package/src/codegen/case.ts +0 -11
  57. package/src/codegen/rollback.test.ts +0 -92
  58. package/src/codegen/rollback.ts +0 -115
  59. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  60. package/src/lint/rules/barrel-import-style.ts +0 -59
  61. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  62. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  63. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  64. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  65. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  66. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  67. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  68. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  69. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  70. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  71. package/src/lint/rules/stale-barrel-types.ts +0 -60
  72. package/src/project/scan.test.ts +0 -178
  73. package/src/project/scan.ts +0 -182
  74. package/src/project/sync.test.ts +0 -87
  75. 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 barrel refs.
9
- * Object literals and arrays-of-objects that don't reference any of these are
10
- * extractable constants that belong in a separate file (e.g. defaults.ts).
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: _.$.lambdaTrustPolicy
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
- * - A barrel ref (_.$ or $)
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 reference via _.$.name`,
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
- checkNode(context.sourceFile, context, diagnostics);
188
+ const importedNames = collectImportedNames(context.sourceFile);
189
+ checkNode(context.sourceFile, context, diagnostics, importedNames);
169
190
  return diagnostics;
170
191
  },
171
192
  };
@@ -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 28 core lint rules (COR + EVL).
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");
@@ -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 = visitor.transformKey ? visitor.transformKey(key) : key;
74
+ const outKey = key;
77
75
  result[outKey] = walkValue(val, entityNames, visitor);
78
76
  }
79
77
  return result;
@@ -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 * as _ from "./_";
53
+ * import { vpc, subnet } from "./vpc";
54
54
  *
55
- * export const vpcId = stackOutput(_.$.vpc.vpcId);
56
- * export const subnetId = stackOutput(_.$.subnet.subnetId, {
55
+ * export const vpcId = stackOutput(vpc.vpcId);
56
+ * export const subnetId = stackOutput(subnet.subnetId, {
57
57
  * description: "Primary subnet ID",
58
58
  * });
59
59
  * ```
@@ -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
- });