@intentius/chant 0.0.4 → 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 (46) 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/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
  6. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +12 -2
  7. package/src/cli/commands/import.test.ts +1 -1
  8. package/src/cli/commands/init-lexicon.ts +34 -20
  9. package/src/cli/commands/init.test.ts +10 -14
  10. package/src/cli/commands/init.ts +2 -7
  11. package/src/cli/commands/lint.ts +9 -33
  12. package/src/cli/main.ts +1 -1
  13. package/src/codegen/docs-interpolation.test.ts +2 -2
  14. package/src/codegen/docs.ts +5 -4
  15. package/src/codegen/generate-registry.test.ts +1 -1
  16. package/src/codegen/generate-registry.ts +3 -3
  17. package/src/codegen/package.ts +28 -1
  18. package/src/codegen/validate.ts +16 -0
  19. package/src/discovery/files.ts +6 -6
  20. package/src/discovery/import.ts +1 -1
  21. package/src/index.ts +0 -1
  22. package/src/lint/engine.ts +1 -5
  23. package/src/lint/rule.ts +0 -18
  24. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  25. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  26. package/src/lint/rules/index.ts +1 -22
  27. package/src/stack-output.ts +3 -3
  28. package/src/barrel.test.ts +0 -157
  29. package/src/barrel.ts +0 -101
  30. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  31. package/src/lint/rules/barrel-import-style.ts +0 -59
  32. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  33. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  34. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  35. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  36. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  37. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  38. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  39. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  40. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  41. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  42. package/src/lint/rules/stale-barrel-types.ts +0 -60
  43. package/src/project/scan.test.ts +0 -178
  44. package/src/project/scan.ts +0 -182
  45. package/src/project/sync.test.ts +0 -87
  46. package/src/project/sync.ts +0 -46
@@ -22,14 +22,13 @@ describe("initCommand", () => {
22
22
  expect(result.createdFiles).toContain("tsconfig.json");
23
23
  expect(result.createdFiles).toContain("chant.config.ts");
24
24
  expect(result.createdFiles).toContain(".gitignore");
25
- expect(result.createdFiles).toContain("src/_.ts");
26
25
  expect(result.createdFiles).toContain("src/config.ts");
27
26
  expect(result.createdFiles).toContain("src/data-bucket.ts");
28
27
  expect(result.createdFiles).toContain("src/logs-bucket.ts");
29
28
  });
30
29
  });
31
30
 
32
- test("aws source files use namespace imports", async () => {
31
+ test("aws source files use direct imports", async () => {
33
32
  await withTestDir(async (testDir) => {
34
33
  const options: InitOptions = {
35
34
  path: testDir,
@@ -41,15 +40,15 @@ describe("initCommand", () => {
41
40
  await initCommand(options);
42
41
 
43
42
  const configContent = readFileSync(join(testDir, "src", "config.ts"), "utf-8");
44
- expect(configContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
43
+ expect(configContent).toContain('from "@intentius/chant-lexicon-aws"');
45
44
 
46
45
  const dataBucketContent = readFileSync(join(testDir, "src", "data-bucket.ts"), "utf-8");
47
- expect(dataBucketContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
48
- expect(dataBucketContent).toContain('import * as _ from "./_"');
46
+ expect(dataBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
47
+ expect(dataBucketContent).toContain('from "./config"');
49
48
 
50
49
  const logsBucketContent = readFileSync(join(testDir, "src", "logs-bucket.ts"), "utf-8");
51
- expect(logsBucketContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
52
- expect(logsBucketContent).toContain('import * as _ from "./_"');
50
+ expect(logsBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
51
+ expect(logsBucketContent).toContain('from "./config"');
53
52
  });
54
53
  });
55
54
 
@@ -177,7 +176,7 @@ describe("initCommand", () => {
177
176
  });
178
177
  });
179
178
 
180
- test("generates barrel file", async () => {
179
+ test("does not generate barrel file", async () => {
181
180
  await withTestDir(async (testDir) => {
182
181
  const options: InitOptions = {
183
182
  path: testDir,
@@ -188,13 +187,11 @@ describe("initCommand", () => {
188
187
 
189
188
  await initCommand(options);
190
189
 
190
+ // No _.ts barrel — direct imports are used instead
191
191
  const barrelPath = join(testDir, "src", "_.ts");
192
- expect(existsSync(barrelPath)).toBe(true);
192
+ expect(existsSync(barrelPath)).toBe(false);
193
193
 
194
- const barrelContent = readFileSync(barrelPath, "utf-8");
195
- expect(barrelContent).toContain('export * from "./config"');
196
-
197
- // No index.ts — barrel re-exports cause duplicate entity errors during build
194
+ // No index.ts either
198
195
  const indexPath = join(testDir, "src", "index.ts");
199
196
  expect(existsSync(indexPath)).toBe(false);
200
197
  });
@@ -218,7 +215,6 @@ describe("initCommand", () => {
218
215
  expect(coreContent).toContain("Value<T>");
219
216
  expect(coreContent).toContain("Serializer");
220
217
  expect(coreContent).toContain("ChantConfig");
221
- expect(coreContent).toContain("barrel");
222
218
 
223
219
  const corePkg = join(testDir, ".chant", "types", "core", "package.json");
224
220
  expect(existsSync(corePkg)).toBe(true);
@@ -214,8 +214,6 @@ export interface ChantConfig {
214
214
  };
215
215
  }
216
216
 
217
- /** Barrel proxy — lazy-loads all sibling exports */
218
- export declare function barrel(dir: string): Record<string, unknown>;
219
217
  `;
220
218
  }
221
219
 
@@ -345,7 +343,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
345
343
  warnings,
346
344
  );
347
345
 
348
- // Generate source files from plugin (or fallback to a minimal barrel)
346
+ // Generate source files from plugin (if available)
349
347
  let sourceFiles: Record<string, string> = {};
350
348
  try {
351
349
  const plugin = await loadPlugin(options.lexicon);
@@ -353,10 +351,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
353
351
  sourceFiles = plugin.initTemplates();
354
352
  }
355
353
  } catch {
356
- // Plugin not yet installed — write a minimal barrel stub
357
- sourceFiles = {
358
- "_.ts": "// Barrel — re-export shared config here\n",
359
- };
354
+ // Plugin not yet installed — no source files to scaffold
360
355
  }
361
356
  for (const [filename, content] of Object.entries(sourceFiles)) {
362
357
  writeIfNotExists(
@@ -1,7 +1,7 @@
1
1
  import { resolve, join } from "path";
2
2
  import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
3
3
  import { runLint } from "../../lint/engine";
4
- import type { LintRule, LintDiagnostic, LintFix, LintRunOptions } from "../../lint/rule";
4
+ import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
5
5
  import { loadPlugins, resolveProjectLexicons } from "../plugins";
6
6
  import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
7
7
  import { loadLocalRules } from "../../lint/rule-loader";
@@ -241,54 +241,30 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
241
241
  // Get all TypeScript files
242
242
  const files = getTypeScriptFiles(infraPath);
243
243
 
244
- // Build barrel exports context for EVL008
245
- let runOptions: LintRunOptions | undefined;
246
- try {
247
- const { scanProject } = require("../../project/scan");
248
- const scan = scanProject(infraPath);
249
- const barrelExports = new Set<string>(scan.exports.map((e: { name: string }) => e.name));
250
- const projectExports = new Map<string, { file: string; className: string }>();
251
- for (const exp of scan.exports) {
252
- projectExports.set(exp.name, { file: exp.file, className: exp.className });
253
- }
254
- runOptions = { barrelExports, projectExports, projectScan: scan };
255
- } catch {
256
- // No barrel file found — EVL008/COR016 will be no-ops
257
- }
258
-
259
244
  // Run lint — use per-file rules when overrides are present
260
245
  let diagnostics: LintDiagnostic[];
261
246
  if (options.rules) {
262
- diagnostics = await runLint(files, options.rules, undefined, runOptions);
247
+ diagnostics = await runLint(files, options.rules, undefined);
263
248
  } else if (hasOverrides) {
264
249
  diagnostics = [];
265
250
  for (const file of files) {
266
251
  const relativePath = file.slice(infraPath.length + 1);
267
252
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
268
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
253
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
269
254
  diagnostics.push(...fileDiagnostics);
270
255
  }
271
256
  } else {
272
257
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
273
- diagnostics = await runLint(files, rules, ruleOptions, runOptions);
258
+ diagnostics = await runLint(files, rules, ruleOptions);
274
259
  }
275
260
 
276
261
  // Apply fixes if requested
277
262
  if (options.fix) {
278
- // Handle cross-file write fixes (COR016) separately
279
- for (const diag of diagnostics) {
280
- if (diag.fix?.kind === "write-file" && diag.fix.params) {
281
- const path = diag.fix.params.path as string;
282
- const content = diag.fix.params.content as string;
283
- writeFileSync(path, content, "utf-8");
284
- }
285
- }
286
-
287
- // Group remaining fixes by file (exclude write-file fixes)
263
+ // Group fixes by file
288
264
  const fixesByFile = new Map<string, LintFix[]>();
289
265
 
290
266
  for (const diag of diagnostics) {
291
- if (diag.fix && diag.fix.kind !== "write-file") {
267
+ if (diag.fix) {
292
268
  const existing = fixesByFile.get(diag.file) ?? [];
293
269
  existing.push(diag.fix);
294
270
  fixesByFile.set(diag.file, existing);
@@ -303,18 +279,18 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
303
279
  // Re-lint after fixes to get updated diagnostics
304
280
  let postFixDiagnostics: LintDiagnostic[];
305
281
  if (options.rules) {
306
- postFixDiagnostics = await runLint(files, options.rules, undefined, runOptions);
282
+ postFixDiagnostics = await runLint(files, options.rules, undefined);
307
283
  } else if (hasOverrides) {
308
284
  postFixDiagnostics = [];
309
285
  for (const file of files) {
310
286
  const relativePath = file.slice(infraPath.length + 1);
311
287
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
312
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
288
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
313
289
  postFixDiagnostics.push(...fileDiagnostics);
314
290
  }
315
291
  } else {
316
292
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
317
- postFixDiagnostics = await runLint(files, rules, ruleOptions, runOptions);
293
+ postFixDiagnostics = await runLint(files, rules, ruleOptions);
318
294
  }
319
295
  diagnostics.length = 0;
320
296
  diagnostics.push(...postFixDiagnostics);
package/src/cli/main.ts CHANGED
@@ -161,7 +161,7 @@ const registry: CommandDef[] = [
161
161
  { name: "import", handler: runImport },
162
162
  { name: "init", handler: runInit },
163
163
  { name: "init lexicon", handler: runInitLexicon },
164
- { name: "update", handler: runUpdate },
164
+ { name: "update", handler: runUpdate },
165
165
  { name: "doctor", handler: runDoctor },
166
166
 
167
167
  // Dev subcommands
@@ -12,7 +12,7 @@ describe("expandFileMarkers", () => {
12
12
  mkdirSync(join(dir, "sub"), { recursive: true });
13
13
  writeFileSync(
14
14
  join(dir, "example.ts"),
15
- 'import * as _ from "./_";\n\nexport const bucket = new _.Bucket({\n bucketName: "test",\n});\n',
15
+ 'import { Bucket } from "@intentius/chant-lexicon-aws";\n\nexport const bucket = new Bucket({\n bucketName: "test",\n});\n',
16
16
  );
17
17
  writeFileSync(
18
18
  join(dir, "sub", "nested.ts"),
@@ -27,7 +27,7 @@ describe("expandFileMarkers", () => {
27
27
  test("expands full file marker", () => {
28
28
  const result = expandFileMarkers("Before\n\n{{file:example.ts}}\n\nAfter", dir);
29
29
  expect(result).toContain('```typescript title="example.ts"');
30
- expect(result).toContain('import * as _ from "./_";');
30
+ expect(result).toContain('import { Bucket } from "@intentius/chant-lexicon-aws";');
31
31
  expect(result).toContain("```");
32
32
  expect(result).toStartWith("Before\n\n");
33
33
  expect(result).toEndWith("\n\nAfter");
@@ -190,10 +190,11 @@ export function docsPipeline(config: DocsConfig): DocsResult {
190
190
  // Generate pages
191
191
  const pages = new Map<string, string>();
192
192
 
193
- pages.set(
194
- "index.mdx",
195
- generateOverview(config, manifest, resources, properties, serviceGroups, rules),
196
- );
193
+ let overviewContent = generateOverview(config, manifest, resources, properties, serviceGroups, rules);
194
+ if (config.examplesDir) {
195
+ overviewContent = expandFileMarkers(overviewContent, config.examplesDir);
196
+ }
197
+ pages.set("index.mdx", overviewContent);
197
198
  const suppress = new Set(config.suppressPages ?? []);
198
199
 
199
200
  // Extra pages from lexicon config
@@ -69,7 +69,7 @@ describe("buildRegistry", () => {
69
69
  typeName: "Test::S3::Bucket",
70
70
  attributes: [],
71
71
  properties: [],
72
- propertyTypes: [{ name: "Bucket_Versioning", cfnType: "Versioning" }],
72
+ propertyTypes: [{ name: "Bucket_Versioning", specType: "Versioning" }],
73
73
  },
74
74
  ];
75
75
  const naming = makeNaming(resources);
@@ -15,11 +15,11 @@ export interface RegistryResource {
15
15
  typeName: string;
16
16
  attributes: { name: string }[];
17
17
  properties: { name: string; constraints: PropertyConstraints }[];
18
- propertyTypes: { name: string; cfnType: string }[];
18
+ propertyTypes: { name: string; specType: string }[];
19
19
  }
20
20
 
21
21
  export interface RegistryConfig<E> {
22
- /** Short name extractor (e.g. cfnShortName). */
22
+ /** Short name extractor (e.g. shortName from the spec type). */
23
23
  shortName: (typeName: string) => string;
24
24
  /** Build a resource entry from parsed data. */
25
25
  buildEntry: (
@@ -80,7 +80,7 @@ export function buildRegistry<E>(
80
80
  for (const pt of r.propertyTypes) {
81
81
  const defName = extractDefName(pt.name, shortName);
82
82
  const ptName = propertyTypeName(tsName, defName);
83
- const ptEntry = config.buildPropertyEntry(typeName, pt.cfnType);
83
+ const ptEntry = config.buildPropertyEntry(typeName, pt.specType);
84
84
  entries[ptName] = ptEntry;
85
85
 
86
86
  if (ptAliases) {
@@ -5,7 +5,7 @@
5
5
  * assemble BundleSpec → compute integrity → attach metadata.
6
6
  */
7
7
 
8
- import { readFileSync, readdirSync } from "fs";
8
+ import { readFileSync, readdirSync, writeFileSync, mkdirSync } from "fs";
9
9
  import { join } from "path";
10
10
  import type { BundleSpec, LexiconManifest } from "../lexicon";
11
11
  import { computeIntegrity } from "../lexicon-integrity";
@@ -147,6 +147,33 @@ export function collectRules(
147
147
  return rules;
148
148
  }
149
149
 
150
+ /**
151
+ * Write a BundleSpec to the given dist directory.
152
+ *
153
+ * Creates the directory structure and writes all artifacts:
154
+ * manifest.json, meta.json, types/index.d.ts, rules/*, skills/*, integrity.json.
155
+ */
156
+ export function writeBundleSpec(spec: BundleSpec, distDir: string): void {
157
+ mkdirSync(join(distDir, "types"), { recursive: true });
158
+ mkdirSync(join(distDir, "rules"), { recursive: true });
159
+ mkdirSync(join(distDir, "skills"), { recursive: true });
160
+
161
+ writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
162
+ writeFileSync(join(distDir, "meta.json"), spec.registry);
163
+ writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
164
+
165
+ for (const [name, content] of spec.rules) {
166
+ writeFileSync(join(distDir, "rules", name), content);
167
+ }
168
+ for (const [name, content] of spec.skills) {
169
+ writeFileSync(join(distDir, "skills", name), content);
170
+ }
171
+
172
+ if (spec.integrity) {
173
+ writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
174
+ }
175
+ }
176
+
150
177
  /**
151
178
  * Collect skills from a plugin's skill definitions.
152
179
  */
@@ -141,3 +141,19 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
141
141
  checks,
142
142
  };
143
143
  }
144
+
145
+ /**
146
+ * Print validation results to stderr and throw on failure.
147
+ */
148
+ export function printValidationResult(result: ValidateResult): void {
149
+ for (const check of result.checks) {
150
+ const status = check.ok ? "OK" : "FAIL";
151
+ const msg = check.error ? ` — ${check.error}` : "";
152
+ console.error(` [${status}] ${check.name}${msg}`);
153
+ }
154
+
155
+ if (!result.success) {
156
+ throw new Error("Validation failed");
157
+ }
158
+ console.error("All validation checks passed.");
159
+ }
@@ -30,12 +30,12 @@ export async function findInfraFiles(path: string): Promise<string[]> {
30
30
  }
31
31
 
32
32
  if (entry.isDirectory()) {
33
- // Child project boundary — a directory with its own barrel file is a
34
- // separate scope, but only if we've already found the project's own
35
- // source root (the first barrel directory). The project's own src/
36
- // with _.ts is the source root, not a child project.
37
- const barrelPath = join(fullPath, "_.ts");
38
- if (existsSync(barrelPath)) {
33
+ // Child project boundary — a directory with its own chant.config.ts
34
+ // is a separate scope, but only if we've already found the project's
35
+ // own source root. The first config directory is the source root, not
36
+ // a child project.
37
+ const configPath = join(fullPath, "chant.config.ts");
38
+ if (existsSync(configPath)) {
39
39
  if (sourceRoot === null) {
40
40
  sourceRoot = fullPath;
41
41
  } else {
@@ -11,7 +11,7 @@ export async function importModule(
11
11
  path: string
12
12
  ): Promise<Record<string, unknown>> {
13
13
  try {
14
- return require(path);
14
+ return await import(path);
15
15
  } catch (error) {
16
16
  const message =
17
17
  error instanceof Error ? error.message : "Unknown import error";
package/src/index.ts CHANGED
@@ -42,7 +42,6 @@ export * from "./lexicon-schema";
42
42
  export * from "./config";
43
43
  export * from "./validation";
44
44
  export * from "./project-validation";
45
- export { barrel } from "./barrel";
46
45
  export * from "./codegen/naming";
47
46
  export * from "./codegen/fetch";
48
47
  export * from "./codegen/generate";
@@ -1,4 +1,4 @@
1
- import type { LintRule, LintDiagnostic, LintContext, LintRunOptions } from "./rule";
1
+ import type { LintRule, LintDiagnostic, LintContext } from "./rule";
2
2
  import { parseFile } from "./parser";
3
3
  import { readFileSync } from "fs";
4
4
 
@@ -120,7 +120,6 @@ export async function runLint(
120
120
  files: string[],
121
121
  rules: LintRule[],
122
122
  ruleOptions?: Map<string, Record<string, unknown>>,
123
- runOptions?: LintRunOptions,
124
123
  ): Promise<LintDiagnostic[]> {
125
124
  const allDiagnostics: LintDiagnostic[] = [];
126
125
  const allRuleIds = new Set(rules.map((r) => r.id));
@@ -140,9 +139,6 @@ export async function runLint(
140
139
  entities: [],
141
140
  filePath,
142
141
  lexicon: undefined,
143
- barrelExports: runOptions?.barrelExports,
144
- projectExports: runOptions?.projectExports,
145
- projectScan: runOptions?.projectScan,
146
142
  };
147
143
 
148
144
  // Execute each rule
package/src/lint/rule.ts CHANGED
@@ -56,24 +56,6 @@ export interface LintContext {
56
56
  filePath: string;
57
57
  /** Optional lexicon context (undefined for core rules) */
58
58
  lexicon?: string;
59
- /** Export names from the barrel file (for EVL008) */
60
- barrelExports?: Set<string>;
61
- /** All project exports keyed by name (for EVL008) */
62
- projectExports?: Map<string, { file: string; className: string }>;
63
- /** Project scan result (for COR016) */
64
- projectScan?: import("../project/scan").ProjectScan;
65
- }
66
-
67
- /**
68
- * Options for extending the lint context with project-level information
69
- */
70
- export interface LintRunOptions {
71
- /** Export names from the barrel file */
72
- barrelExports?: Set<string>;
73
- /** All project exports keyed by name */
74
- projectExports?: Map<string, { file: string; className: string }>;
75
- /** Project scan result (for COR016) */
76
- projectScan?: import("../project/scan").ProjectScan;
77
59
  }
78
60
 
79
61
  /**
@@ -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
  });