@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.
- package/README.md +10 -351
- package/package.json +1 -1
- package/src/bench.test.ts +3 -54
- package/src/build.ts +14 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +12 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/init-lexicon.ts +34 -20
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +2 -7
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/main.ts +1 -1
- package/src/codegen/docs-interpolation.test.ts +77 -0
- package/src/codegen/docs.ts +80 -5
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +3 -3
- package/src/codegen/package.ts +28 -1
- package/src/codegen/validate.ts +16 -0
- package/src/discovery/collect.ts +7 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +0 -1
- 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/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- 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
|
@@ -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("
|
|
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
|
|
64
|
+
test("allows imported refs (direct import)", () => {
|
|
65
65
|
const ctx = createContext(`
|
|
66
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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: [
|
|
169
|
+
managedPolicyArns: [lambdaBasicExecutionArn],
|
|
154
170
|
});
|
|
155
171
|
return { role };
|
|
156
172
|
}, "MyComp");
|
|
157
173
|
`);
|
|
158
|
-
// Array with
|
|
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
|
|
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,
|
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
|
-
});
|
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
|
-
});
|