@intentius/chant 0.0.3 → 0.0.5

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 (48) hide show
  1. package/README.md +10 -351
  2. package/package.json +1 -1
  3. package/src/bench.test.ts +3 -54
  4. package/src/build.ts +14 -1
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
  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 +12 -2
  8. package/src/cli/commands/import.test.ts +1 -1
  9. package/src/cli/commands/init-lexicon.ts +34 -20
  10. package/src/cli/commands/init.test.ts +10 -14
  11. package/src/cli/commands/init.ts +2 -7
  12. package/src/cli/commands/lint.ts +9 -33
  13. package/src/cli/main.ts +1 -1
  14. package/src/codegen/docs-interpolation.test.ts +77 -0
  15. package/src/codegen/docs.ts +80 -5
  16. package/src/codegen/generate-registry.test.ts +1 -1
  17. package/src/codegen/generate-registry.ts +3 -3
  18. package/src/codegen/package.ts +28 -1
  19. package/src/codegen/validate.ts +16 -0
  20. package/src/discovery/collect.ts +7 -0
  21. package/src/discovery/files.ts +6 -6
  22. package/src/discovery/import.ts +1 -1
  23. package/src/index.ts +0 -1
  24. package/src/lint/engine.ts +1 -5
  25. package/src/lint/rule.ts +0 -18
  26. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  27. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  28. package/src/lint/rules/index.ts +1 -22
  29. package/src/stack-output.ts +3 -3
  30. package/src/barrel.test.ts +0 -157
  31. package/src/barrel.ts +0 -101
  32. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  33. package/src/lint/rules/barrel-import-style.ts +0 -59
  34. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  35. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  36. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  37. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  38. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  39. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  40. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  41. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  42. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  43. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  44. package/src/lint/rules/stale-barrel-types.ts +0 -60
  45. package/src/project/scan.test.ts +0 -178
  46. package/src/project/scan.ts +0 -182
  47. package/src/project/sync.test.ts +0 -87
  48. package/src/project/sync.ts +0 -46
@@ -31,7 +31,7 @@ describe("EVL009: composite-no-constant", () => {
31
31
  expect(diags).toHaveLength(1);
32
32
  expect(diags[0].ruleId).toBe("EVL009");
33
33
  expect(diags[0].message).toContain("assumeRolePolicyDocument");
34
- expect(diags[0].message).toContain("_.$.name");
34
+ expect(diags[0].message).toContain("import directly");
35
35
  });
36
36
 
37
37
  test("flags inline array with objects that doesn't reference props", () => {
@@ -61,11 +61,25 @@ describe("EVL009: composite-no-constant", () => {
61
61
  expect(diags[0].message).toContain("policies");
62
62
  });
63
63
 
64
- test("allows barrel refs (_.$.name)", () => {
64
+ test("allows imported refs (direct import)", () => {
65
65
  const ctx = createContext(`
66
- const MyComp = _.Composite((props) => {
66
+ import { lambdaTrustPolicy } from "./defaults";
67
+ const MyComp = Composite((props) => {
68
+ const role = new Role({
69
+ assumeRolePolicyDocument: lambdaTrustPolicy,
70
+ });
71
+ return { role };
72
+ }, "MyComp");
73
+ `);
74
+ expect(evl009CompositeNoConstantRule.check(ctx)).toHaveLength(0);
75
+ });
76
+
77
+ test("allows namespace import refs", () => {
78
+ const ctx = createContext(`
79
+ import * as defaults from "./defaults";
80
+ const MyComp = Composite((props) => {
67
81
  const role = new Role({
68
- assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
82
+ assumeRolePolicyDocument: defaults.lambdaTrustPolicy,
69
83
  });
70
84
  return { role };
71
85
  }, "MyComp");
@@ -99,8 +113,9 @@ describe("EVL009: composite-no-constant", () => {
99
113
 
100
114
  test("allows sibling member reference", () => {
101
115
  const ctx = createContext(`
116
+ import { trustPolicy } from "./defaults";
102
117
  const MyComp = Composite((props) => {
103
- const role = new Role({ assumeRolePolicyDocument: _.$.trustPolicy });
118
+ const role = new Role({ assumeRolePolicyDocument: trustPolicy });
104
119
  const func = new Function({
105
120
  config: { roleArn: role.arn },
106
121
  });
@@ -146,16 +161,17 @@ describe("EVL009: composite-no-constant", () => {
146
161
  expect(diags).toHaveLength(2);
147
162
  });
148
163
 
149
- test("allows array wrapping barrel ref", () => {
164
+ test("allows array wrapping imported ref", () => {
150
165
  const ctx = createContext(`
166
+ import { lambdaBasicExecutionArn } from "./defaults";
151
167
  const MyComp = Composite((props) => {
152
168
  const role = new Role({
153
- managedPolicyArns: [_.$.lambdaBasicExecutionArn],
169
+ managedPolicyArns: [lambdaBasicExecutionArn],
154
170
  });
155
171
  return { role };
156
172
  }, "MyComp");
157
173
  `);
158
- // Array with barrel ref inside — not flagged (contains barrel ref)
174
+ // Array with imported ref inside — not flagged
159
175
  // Also it's an array of identifiers, no objects inside
160
176
  expect(evl009CompositeNoConstantRule.check(ctx)).toHaveLength(0);
161
177
  });
@@ -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,
@@ -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
- });
package/src/barrel.ts DELETED
@@ -1,101 +0,0 @@
1
- import { readdirSync, readFileSync } from "fs";
2
- import { join } from "path";
3
-
4
- export function barrel(dir: string): Record<string, unknown> {
5
- let allExports: Record<string, unknown> | null = null;
6
-
7
- function load(): Record<string, unknown> {
8
- if (allExports) return allExports;
9
- allExports = {};
10
-
11
- const files = readdirSync(dir)
12
- .filter(
13
- (f) =>
14
- f.endsWith(".ts") &&
15
- !f.startsWith("_") &&
16
- !f.endsWith(".test.ts") &&
17
- !f.endsWith(".spec.ts"),
18
- )
19
- .sort();
20
-
21
- // Identify files that reference the barrel (.$. or .$[) — these
22
- // may silently resolve cross-references to undefined if their
23
- // dependency files haven't loaded yet
24
- const barrelRefPattern = /\.\$[.\[]/;
25
- const usesBarrel = new Set<string>();
26
- for (const file of files) {
27
- const src = readFileSync(join(dir, file), "utf-8");
28
- if (barrelRefPattern.test(src)) {
29
- usesBarrel.add(file);
30
- }
31
- }
32
-
33
- function loadFile(file: string, overwrite = false): boolean {
34
- const fullPath = join(dir, file);
35
- try {
36
- const mod = require(fullPath);
37
- for (const [key, val] of Object.entries(mod)) {
38
- if (val !== undefined && (overwrite || !(key in allExports!))) {
39
- allExports![key] = val;
40
- }
41
- }
42
- return true;
43
- } catch {
44
- // Clear require cache so retry re-executes the file
45
- delete require.cache[require.resolve(fullPath)];
46
- return false;
47
- }
48
- }
49
-
50
- // First pass — load all files in alphabetical order
51
- const failed: string[] = [];
52
- for (const file of files) {
53
- if (!loadFile(file)) {
54
- failed.push(file);
55
- }
56
- }
57
-
58
- // Retry files that threw — their dependencies are now available
59
- for (const file of failed) {
60
- loadFile(file);
61
- }
62
-
63
- // Second pass — reload files that reference the barrel so
64
- // cross-references that silently resolved to undefined now
65
- // pick up the correct values. Files without barrel references
66
- // keep their original instances to preserve the reference graph.
67
- for (const file of files) {
68
- if (!usesBarrel.has(file)) continue;
69
- const fullPath = join(dir, file);
70
- try { delete require.cache[require.resolve(fullPath)]; } catch {}
71
- }
72
- for (const file of files) {
73
- if (!usesBarrel.has(file)) continue;
74
- loadFile(file, true);
75
- }
76
-
77
- return allExports;
78
- }
79
-
80
- return new Proxy<Record<string, unknown>>({}, {
81
- get(_, prop: string | symbol) {
82
- if (typeof prop === 'symbol') return undefined;
83
- return load()[prop];
84
- },
85
- has(_, prop: string | symbol) {
86
- if (typeof prop === 'symbol') return false;
87
- return prop in load();
88
- },
89
- ownKeys(_) {
90
- return Object.keys(load());
91
- },
92
- getOwnPropertyDescriptor(_, prop: string | symbol) {
93
- if (typeof prop === 'symbol') return undefined;
94
- const exports = load();
95
- if (prop in exports) {
96
- return { configurable: true, enumerable: true, value: exports[prop] };
97
- }
98
- return undefined;
99
- },
100
- });
101
- }
@@ -1,80 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import * as ts from "typescript";
3
- import { barrelImportStyleRule } from "./barrel-import-style";
4
- import type { LintContext } from "../rule";
5
-
6
- function createContext(code: string, filePath = "test.ts"): LintContext {
7
- const sourceFile = ts.createSourceFile(
8
- filePath,
9
- code,
10
- ts.ScriptTarget.Latest,
11
- true
12
- );
13
-
14
- return {
15
- sourceFile,
16
- entities: [],
17
- filePath,
18
- lexicon: undefined,
19
- };
20
- }
21
-
22
- describe("COR002: barrel-import-style", () => {
23
- test("rule metadata", () => {
24
- expect(barrelImportStyleRule.id).toBe("COR002");
25
- expect(barrelImportStyleRule.severity).toBe("error");
26
- expect(barrelImportStyleRule.category).toBe("style");
27
- });
28
-
29
- test("flags named import from ./_", () => {
30
- const code = `import { bucketEncryption } from "./_";`;
31
- const context = createContext(code);
32
- const diagnostics = barrelImportStyleRule.check(context);
33
-
34
- expect(diagnostics).toHaveLength(1);
35
- expect(diagnostics[0].ruleId).toBe("COR002");
36
- expect(diagnostics[0].severity).toBe("error");
37
- expect(diagnostics[0].message).toBe(
38
- `Use namespace import for local barrel — replace with: import * as _ from "./_"`
39
- );
40
- });
41
-
42
- test("allows namespace import from ./_", () => {
43
- const code = `import * as _ from "./_";`;
44
- const context = createContext(code);
45
- const diagnostics = barrelImportStyleRule.check(context);
46
- expect(diagnostics).toHaveLength(0);
47
- });
48
-
49
- test("allows type-only import from ./_", () => {
50
- const code = `import type { Config } from "./_";`;
51
- const context = createContext(code);
52
- const diagnostics = barrelImportStyleRule.check(context);
53
- expect(diagnostics).toHaveLength(0);
54
- });
55
-
56
- test("does not flag imports from other relative paths", () => {
57
- const code = `import { helper } from "./utils";`;
58
- const context = createContext(code);
59
- const diagnostics = barrelImportStyleRule.check(context);
60
- expect(diagnostics).toHaveLength(0);
61
- });
62
-
63
- test("does not flag imports from packages", () => {
64
- const code = `import { useState } from "react";`;
65
- const context = createContext(code);
66
- const diagnostics = barrelImportStyleRule.check(context);
67
- expect(diagnostics).toHaveLength(0);
68
- });
69
-
70
- test("reports correct line and column numbers", () => {
71
- const code = `import { foo } from "./_";`;
72
- const context = createContext(code);
73
- const diagnostics = barrelImportStyleRule.check(context);
74
-
75
- expect(diagnostics).toHaveLength(1);
76
- expect(diagnostics[0].line).toBe(1);
77
- expect(diagnostics[0].column).toBe(1);
78
- expect(diagnostics[0].file).toBe("test.ts");
79
- });
80
- });