@composurecdk/eslint-plugin 0.10.0
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/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/commonjs/configs/presets.d.ts +42 -0
- package/dist/commonjs/configs/presets.d.ts.map +1 -0
- package/dist/commonjs/configs/presets.js +58 -0
- package/dist/commonjs/configs/presets.js.map +1 -0
- package/dist/commonjs/index.d.ts +84 -0
- package/dist/commonjs/index.d.ts.map +1 -0
- package/dist/commonjs/index.js +63 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/rules/builder-must-be-tagged.d.ts +15 -0
- package/dist/commonjs/rules/builder-must-be-tagged.d.ts.map +1 -0
- package/dist/commonjs/rules/builder-must-be-tagged.js +70 -0
- package/dist/commonjs/rules/builder-must-be-tagged.js.map +1 -0
- package/dist/commonjs/rules/builder-must-implement-copy-state.d.ts +13 -0
- package/dist/commonjs/rules/builder-must-implement-copy-state.d.ts.map +1 -0
- package/dist/commonjs/rules/builder-must-implement-copy-state.js +123 -0
- package/dist/commonjs/rules/builder-must-implement-copy-state.js.map +1 -0
- package/dist/commonjs/rules/constraint-metadata-required.d.ts +16 -0
- package/dist/commonjs/rules/constraint-metadata-required.d.ts.map +1 -0
- package/dist/commonjs/rules/constraint-metadata-required.js +79 -0
- package/dist/commonjs/rules/constraint-metadata-required.js.map +1 -0
- package/dist/commonjs/rules/index.d.ts +14 -0
- package/dist/commonjs/rules/index.d.ts.map +1 -0
- package/dist/commonjs/rules/index.js +52 -0
- package/dist/commonjs/rules/index.js.map +1 -0
- package/dist/commonjs/rules/lib/imports.d.ts +24 -0
- package/dist/commonjs/rules/lib/imports.d.ts.map +1 -0
- package/dist/commonjs/rules/lib/imports.js +76 -0
- package/dist/commonjs/rules/lib/imports.js.map +1 -0
- package/dist/commonjs/rules/lifecycle-build-context-required.d.ts +14 -0
- package/dist/commonjs/rules/lifecycle-build-context-required.d.ts.map +1 -0
- package/dist/commonjs/rules/lifecycle-build-context-required.js +46 -0
- package/dist/commonjs/rules/lifecycle-build-context-required.js.map +1 -0
- package/dist/commonjs/rules/lifecycle-build-must-forward-context.d.ts +18 -0
- package/dist/commonjs/rules/lifecycle-build-must-forward-context.d.ts.map +1 -0
- package/dist/commonjs/rules/lifecycle-build-must-forward-context.js +54 -0
- package/dist/commonjs/rules/lifecycle-build-must-forward-context.js.map +1 -0
- package/dist/commonjs/rules/no-cdk-api-above-floor.d.ts +15 -0
- package/dist/commonjs/rules/no-cdk-api-above-floor.d.ts.map +1 -0
- package/dist/commonjs/rules/no-cdk-api-above-floor.js +80 -0
- package/dist/commonjs/rules/no-cdk-api-above-floor.js.map +1 -0
- package/dist/commonjs/rules/no-cjs-incompatible-syntax.d.ts +14 -0
- package/dist/commonjs/rules/no-cjs-incompatible-syntax.d.ts.map +1 -0
- package/dist/commonjs/rules/no-cjs-incompatible-syntax.js +55 -0
- package/dist/commonjs/rules/no-cjs-incompatible-syntax.js.map +1 -0
- package/dist/commonjs/rules/no-realm-bound-instanceof.d.ts +25 -0
- package/dist/commonjs/rules/no-realm-bound-instanceof.d.ts.map +1 -0
- package/dist/commonjs/rules/no-realm-bound-instanceof.js +98 -0
- package/dist/commonjs/rules/no-realm-bound-instanceof.js.map +1 -0
- package/dist/commonjs/rules/no-typescript-private-modifier.d.ts +17 -0
- package/dist/commonjs/rules/no-typescript-private-modifier.d.ts.map +1 -0
- package/dist/commonjs/rules/no-typescript-private-modifier.js +91 -0
- package/dist/commonjs/rules/no-typescript-private-modifier.js.map +1 -0
- package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.d.ts +17 -0
- package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.d.ts.map +1 -0
- package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.js +152 -0
- package/dist/commonjs/rules/redeclared-prop-must-track-cdk-type.js.map +1 -0
- package/dist/esm/configs/presets.d.ts +42 -0
- package/dist/esm/configs/presets.d.ts.map +1 -0
- package/dist/esm/configs/presets.js +55 -0
- package/dist/esm/configs/presets.js.map +1 -0
- package/dist/esm/index.d.ts +84 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +60 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/rules/builder-must-be-tagged.d.ts +15 -0
- package/dist/esm/rules/builder-must-be-tagged.d.ts.map +1 -0
- package/dist/esm/rules/builder-must-be-tagged.js +67 -0
- package/dist/esm/rules/builder-must-be-tagged.js.map +1 -0
- package/dist/esm/rules/builder-must-implement-copy-state.d.ts +13 -0
- package/dist/esm/rules/builder-must-implement-copy-state.d.ts.map +1 -0
- package/dist/esm/rules/builder-must-implement-copy-state.js +120 -0
- package/dist/esm/rules/builder-must-implement-copy-state.js.map +1 -0
- package/dist/esm/rules/constraint-metadata-required.d.ts +16 -0
- package/dist/esm/rules/constraint-metadata-required.d.ts.map +1 -0
- package/dist/esm/rules/constraint-metadata-required.js +76 -0
- package/dist/esm/rules/constraint-metadata-required.js.map +1 -0
- package/dist/esm/rules/index.d.ts +14 -0
- package/dist/esm/rules/index.d.ts.map +1 -0
- package/dist/esm/rules/index.js +49 -0
- package/dist/esm/rules/index.js.map +1 -0
- package/dist/esm/rules/lib/imports.d.ts +24 -0
- package/dist/esm/rules/lib/imports.d.ts.map +1 -0
- package/dist/esm/rules/lib/imports.js +70 -0
- package/dist/esm/rules/lib/imports.js.map +1 -0
- package/dist/esm/rules/lifecycle-build-context-required.d.ts +14 -0
- package/dist/esm/rules/lifecycle-build-context-required.d.ts.map +1 -0
- package/dist/esm/rules/lifecycle-build-context-required.js +43 -0
- package/dist/esm/rules/lifecycle-build-context-required.js.map +1 -0
- package/dist/esm/rules/lifecycle-build-must-forward-context.d.ts +18 -0
- package/dist/esm/rules/lifecycle-build-must-forward-context.d.ts.map +1 -0
- package/dist/esm/rules/lifecycle-build-must-forward-context.js +51 -0
- package/dist/esm/rules/lifecycle-build-must-forward-context.js.map +1 -0
- package/dist/esm/rules/no-cdk-api-above-floor.d.ts +15 -0
- package/dist/esm/rules/no-cdk-api-above-floor.d.ts.map +1 -0
- package/dist/esm/rules/no-cdk-api-above-floor.js +77 -0
- package/dist/esm/rules/no-cdk-api-above-floor.js.map +1 -0
- package/dist/esm/rules/no-cjs-incompatible-syntax.d.ts +14 -0
- package/dist/esm/rules/no-cjs-incompatible-syntax.d.ts.map +1 -0
- package/dist/esm/rules/no-cjs-incompatible-syntax.js +52 -0
- package/dist/esm/rules/no-cjs-incompatible-syntax.js.map +1 -0
- package/dist/esm/rules/no-realm-bound-instanceof.d.ts +25 -0
- package/dist/esm/rules/no-realm-bound-instanceof.d.ts.map +1 -0
- package/dist/esm/rules/no-realm-bound-instanceof.js +95 -0
- package/dist/esm/rules/no-realm-bound-instanceof.js.map +1 -0
- package/dist/esm/rules/no-typescript-private-modifier.d.ts +17 -0
- package/dist/esm/rules/no-typescript-private-modifier.d.ts.map +1 -0
- package/dist/esm/rules/no-typescript-private-modifier.js +88 -0
- package/dist/esm/rules/no-typescript-private-modifier.js.map +1 -0
- package/dist/esm/rules/redeclared-prop-must-track-cdk-type.d.ts +17 -0
- package/dist/esm/rules/redeclared-prop-must-track-cdk-type.d.ts.map +1 -0
- package/dist/esm/rules/redeclared-prop-must-track-cdk-type.js +149 -0
- package/dist/esm/rules/redeclared-prop-must-track-cdk-type.js.map +1 -0
- package/docs/rules/builder-must-be-tagged.md +46 -0
- package/docs/rules/builder-must-implement-copy-state.md +50 -0
- package/docs/rules/constraint-metadata-required.md +44 -0
- package/docs/rules/lifecycle-build-context-required.md +39 -0
- package/docs/rules/lifecycle-build-must-forward-context.md +50 -0
- package/docs/rules/no-cdk-api-above-floor.md +56 -0
- package/docs/rules/no-cjs-incompatible-syntax.md +48 -0
- package/docs/rules/no-realm-bound-instanceof.md +87 -0
- package/docs/rules/no-typescript-private-modifier.md +72 -0
- package/docs/rules/redeclared-prop-must-track-cdk-type.md +60 -0
- package/package.json +81 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The member's own name, for the message. A computed key has no name to give —
|
|
3
|
+
* `#[expr]` is not a thing — so the message falls back to a placeholder rather
|
|
4
|
+
* than naming whatever expression produced the key.
|
|
5
|
+
*/
|
|
6
|
+
function memberName(key, computed) {
|
|
7
|
+
if (computed)
|
|
8
|
+
return "field";
|
|
9
|
+
if (key.type === "Identifier")
|
|
10
|
+
return key.name;
|
|
11
|
+
if (key.type === "Literal" && typeof key.value === "string")
|
|
12
|
+
return key.value;
|
|
13
|
+
return "field";
|
|
14
|
+
}
|
|
15
|
+
/** The bound name of a parameter, seeing through a default value. */
|
|
16
|
+
function parameterName(parameter) {
|
|
17
|
+
const bound = parameter.type === "AssignmentPattern" ? parameter.left : parameter;
|
|
18
|
+
return bound.type === "Identifier" ? bound.name : "field";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Bans the TypeScript `private` modifier in favour of ECMAScript private fields.
|
|
22
|
+
*
|
|
23
|
+
* The two are not interchangeable once a class is generic over its own members.
|
|
24
|
+
* A TypeScript `private` member is still part of the type, so it appears in
|
|
25
|
+
* `keyof T`, survives into a mapped type, and reaches the emitted `.d.ts` — where
|
|
26
|
+
* a consumer's compiler cannot name it and reports TS4094. A `#field` is not
|
|
27
|
+
* part of the type at all, so it never gets that far.
|
|
28
|
+
*
|
|
29
|
+
* A private constructor stays allowed: `#constructor` is not valid syntax, so
|
|
30
|
+
* there is nothing to prefer over it.
|
|
31
|
+
*
|
|
32
|
+
* See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/no-typescript-private-modifier.md | the rule documentation}.
|
|
33
|
+
*/
|
|
34
|
+
export const rule = {
|
|
35
|
+
meta: {
|
|
36
|
+
type: "problem",
|
|
37
|
+
docs: {
|
|
38
|
+
description: "Ban the TypeScript `private` modifier in favour of ECMAScript `#` fields",
|
|
39
|
+
},
|
|
40
|
+
schema: [],
|
|
41
|
+
messages: {
|
|
42
|
+
property: "Use an ECMAScript private field (`#{{name}}`) instead of the TypeScript `private` " +
|
|
43
|
+
"modifier. A `private` member still appears in `keyof T` and is carried through mapped " +
|
|
44
|
+
"types into the emitted .d.ts, where it produces TS4094 for consumers.",
|
|
45
|
+
method: "Use an ECMAScript private method (`#{{name}}()`) instead of the TypeScript `private` " +
|
|
46
|
+
"modifier. A private constructor is the only permitted use, since `#constructor` is not " +
|
|
47
|
+
"valid syntax.",
|
|
48
|
+
parameterProperty: "A parameter property cannot be ECMAScript private. Declare the field as " +
|
|
49
|
+
"`readonly #{{name}}` and assign it in the constructor body.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
create(ctx) {
|
|
53
|
+
// Each handler reports on the member's own name rather than the whole
|
|
54
|
+
// declaration: the fix replaces the modifier and renames the key, so a
|
|
55
|
+
// narrow squiggle points at both.
|
|
56
|
+
return {
|
|
57
|
+
"PropertyDefinition[accessibility='private']"(node) {
|
|
58
|
+
ctx.report({
|
|
59
|
+
node: node.key,
|
|
60
|
+
messageId: "property",
|
|
61
|
+
data: { name: memberName(node.key, node.computed) },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
"MethodDefinition[accessibility='private'][kind!='constructor']"(node) {
|
|
65
|
+
ctx.report({
|
|
66
|
+
node: node.key,
|
|
67
|
+
messageId: "method",
|
|
68
|
+
data: { name: memberName(node.key, node.computed) },
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
// `accessor x` is a distinct node from a plain field, and its `private`
|
|
72
|
+
// reaches `keyof T` the same way.
|
|
73
|
+
"AccessorProperty[accessibility='private']"(node) {
|
|
74
|
+
const { key, computed } = node;
|
|
75
|
+
ctx.report({ node: key, messageId: "property", data: { name: memberName(key, computed) } });
|
|
76
|
+
},
|
|
77
|
+
"TSParameterProperty[accessibility='private']"(node) {
|
|
78
|
+
const { parameter } = node;
|
|
79
|
+
ctx.report({
|
|
80
|
+
node: parameter,
|
|
81
|
+
messageId: "parameterProperty",
|
|
82
|
+
data: { name: parameterName(parameter) },
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=no-typescript-private-modifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-typescript-private-modifier.js","sourceRoot":"","sources":["../../../src/rules/no-typescript-private-modifier.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AACH,SAAS,UAAU,CAAC,GAAS,EAAE,QAAiB;IAC9C,IAAI,QAAQ;QAAE,OAAO,OAAO,CAAC;IAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAC/C,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC,KAAK,CAAC;IAC9E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,qEAAqE;AACrE,SAAS,aAAa,CAAC,SAAe;IACpC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,OAAO,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,IAAI,GAAoB;IACnC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,0EAA0E;SACxF;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,QAAQ,EACN,oFAAoF;gBACpF,wFAAwF;gBACxF,uEAAuE;YACzE,MAAM,EACJ,uFAAuF;gBACvF,yFAAyF;gBACzF,eAAe;YACjB,iBAAiB,EACf,0EAA0E;gBAC1E,6DAA6D;SAChE;KACF;IACD,MAAM,CAAC,GAAG;QACR,sEAAsE;QACtE,uEAAuE;QACvE,kCAAkC;QAClC,OAAO;YACL,6CAA6C,CAAC,IAAwB;gBACpE,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,IAAI,CAAC,GAAG;oBACd,SAAS,EAAE,UAAU;oBACrB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;iBACpD,CAAC,CAAC;YACL,CAAC;YACD,gEAAgE,CAAC,IAAsB;gBACrF,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,IAAI,CAAC,GAAG;oBACd,SAAS,EAAE,QAAQ;oBACnB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;iBACpD,CAAC,CAAC;YACL,CAAC;YACD,wEAAwE;YACxE,kCAAkC;YAClC,2CAA2C,CAAC,IAAU;gBACpD,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAuC,CAAC;gBAClE,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9F,CAAC;YACD,8CAA8C,CAAC,IAAU;gBACvD,MAAM,EAAE,SAAS,EAAE,GAAG,IAAwC,CAAC;gBAC/D,GAAG,CAAC,MAAM,CAAC;oBACT,IAAI,EAAE,SAAS;oBACf,SAAS,EAAE,mBAAmB;oBAC9B,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE;iBACzC,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Flags a re-declared CDK prop that pins a named CDK interface inside
|
|
4
|
+
* `Resolvable<…>` rather than reading the type from CDK's own prop.
|
|
5
|
+
*
|
|
6
|
+
* A builder's props type is CDK's props type with a few keys lifted out and
|
|
7
|
+
* re-declared. Everything left inside the `Omit` keeps tracking whichever
|
|
8
|
+
* `aws-cdk-lib` the consumer installed; a key re-spelled with a named interface
|
|
9
|
+
* stops tracking it, freezing at whatever that interface meant when it was
|
|
10
|
+
* written. When CDK later widens that prop, the builder goes on rejecting values
|
|
11
|
+
* the construct it wraps now accepts — and it surfaces in the consumer's own
|
|
12
|
+
* compile, not in this package's.
|
|
13
|
+
*
|
|
14
|
+
* See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/redeclared-prop-must-track-cdk-type.md | the rule documentation}.
|
|
15
|
+
*/
|
|
16
|
+
export declare const rule: Rule.RuleModule;
|
|
17
|
+
//# sourceMappingURL=redeclared-prop-must-track-cdk-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redeclared-prop-must-track-cdk-type.d.ts","sourceRoot":"","sources":["../../../src/rules/redeclared-prop-must-track-cdk-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAqFnC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,IAAI,CAAC,UAiEvB,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isCdkSource } from "./lib/imports.js";
|
|
2
|
+
/** True for `@aws-cdk/aws-*-alpha` and friends, alongside `aws-cdk-lib`. */
|
|
3
|
+
function isCdkModule(source) {
|
|
4
|
+
return isCdkSource(source) || source.startsWith("@aws-cdk/");
|
|
5
|
+
}
|
|
6
|
+
/** The first type argument of a type reference or interface heritage, either spelling. */
|
|
7
|
+
function typeArgAt(node, index) {
|
|
8
|
+
const args = (node.typeArguments ?? node.typeParameters);
|
|
9
|
+
const params = args?.params ?? [];
|
|
10
|
+
return params.at(index);
|
|
11
|
+
}
|
|
12
|
+
/** The leftmost identifier of `Foo` or `ns.Foo`, or `undefined` for anything else. */
|
|
13
|
+
function rootName(typeName) {
|
|
14
|
+
let current = typeName;
|
|
15
|
+
while (current.type === "TSQualifiedName")
|
|
16
|
+
current = current.left;
|
|
17
|
+
return current.type === "Identifier" ? current.name : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** The string keys named by `Omit`'s second argument, ignoring non-literal ones. */
|
|
20
|
+
function literalKeys(node) {
|
|
21
|
+
if (node === undefined)
|
|
22
|
+
return [];
|
|
23
|
+
if (node.type === "TSUnionType") {
|
|
24
|
+
return node.types.flatMap((t) => literalKeys(t));
|
|
25
|
+
}
|
|
26
|
+
if (node.type !== "TSLiteralType")
|
|
27
|
+
return [];
|
|
28
|
+
const literal = node.literal;
|
|
29
|
+
return typeof literal.value === "string" ? [literal.value] : [];
|
|
30
|
+
}
|
|
31
|
+
/** The keys an interface's `extends` clause omits from a base CDK props type. */
|
|
32
|
+
function omittedKeys(heritage) {
|
|
33
|
+
const keys = new Set();
|
|
34
|
+
for (const entry of heritage) {
|
|
35
|
+
const expression = entry.expression;
|
|
36
|
+
if (expression?.type !== "Identifier" || expression.name !== "Omit")
|
|
37
|
+
continue;
|
|
38
|
+
for (const key of literalKeys(typeArgAt(entry, 1)))
|
|
39
|
+
keys.add(key);
|
|
40
|
+
}
|
|
41
|
+
return keys;
|
|
42
|
+
}
|
|
43
|
+
/** Every `Resolvable<…>` reference nested anywhere inside a type annotation. */
|
|
44
|
+
function resolvableRefs(node, found = []) {
|
|
45
|
+
if (node === null || typeof node !== "object" || !("type" in node))
|
|
46
|
+
return found;
|
|
47
|
+
const typed = node;
|
|
48
|
+
if (typed.type === "TSTypeReference" && rootName(typed.typeName) === "Resolvable") {
|
|
49
|
+
found.push(typed);
|
|
50
|
+
}
|
|
51
|
+
for (const [key, value] of Object.entries(typed)) {
|
|
52
|
+
// `parent` points back up the tree — following it never terminates.
|
|
53
|
+
if (key === "parent")
|
|
54
|
+
continue;
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
for (const item of value)
|
|
57
|
+
resolvableRefs(item, found);
|
|
58
|
+
else
|
|
59
|
+
resolvableRefs(value, found);
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
/** The property name of a `key: T` signature, or `undefined` for a computed one. */
|
|
64
|
+
function propertyName(member) {
|
|
65
|
+
if (member.type !== "TSPropertySignature" || member.computed === true)
|
|
66
|
+
return undefined;
|
|
67
|
+
const key = member.key;
|
|
68
|
+
if (key.type === "Identifier")
|
|
69
|
+
return key.name;
|
|
70
|
+
if (key.type === "Literal" && typeof key.value === "string")
|
|
71
|
+
return key.value;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Flags a re-declared CDK prop that pins a named CDK interface inside
|
|
76
|
+
* `Resolvable<…>` rather than reading the type from CDK's own prop.
|
|
77
|
+
*
|
|
78
|
+
* A builder's props type is CDK's props type with a few keys lifted out and
|
|
79
|
+
* re-declared. Everything left inside the `Omit` keeps tracking whichever
|
|
80
|
+
* `aws-cdk-lib` the consumer installed; a key re-spelled with a named interface
|
|
81
|
+
* stops tracking it, freezing at whatever that interface meant when it was
|
|
82
|
+
* written. When CDK later widens that prop, the builder goes on rejecting values
|
|
83
|
+
* the construct it wraps now accepts — and it surfaces in the consumer's own
|
|
84
|
+
* compile, not in this package's.
|
|
85
|
+
*
|
|
86
|
+
* See {@link https://github.com/laazyj/composureCDK/blob/main/packages/eslint-plugin/docs/rules/redeclared-prop-must-track-cdk-type.md | the rule documentation}.
|
|
87
|
+
*/
|
|
88
|
+
export const rule = {
|
|
89
|
+
meta: {
|
|
90
|
+
type: "problem",
|
|
91
|
+
docs: {
|
|
92
|
+
description: "A re-declared CDK prop must read its type from CDK's own prop, not pin an interface",
|
|
93
|
+
},
|
|
94
|
+
schema: [],
|
|
95
|
+
messages: {
|
|
96
|
+
pinnedType: "`{{prop}}` re-declares a prop omitted from `{{base}}`, but pins `{{pinned}}` — the " +
|
|
97
|
+
"builder stops accepting what CDK accepts as soon as CDK widens the prop. Read the type " +
|
|
98
|
+
'from CDK\'s own prop instead: `Resolvable<NonNullable<{{base}}["{{prop}}"]>>`. ' +
|
|
99
|
+
"Keep the indexed access inline — extracting it to a named alias puts an unnameable " +
|
|
100
|
+
"type into the emitted declaration.",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
create(ctx) {
|
|
104
|
+
const cdkTypeNames = new Set();
|
|
105
|
+
const listener = {
|
|
106
|
+
ImportDeclaration(node) {
|
|
107
|
+
if (typeof node.source.value !== "string" || !isCdkModule(node.source.value))
|
|
108
|
+
return;
|
|
109
|
+
for (const spec of node.specifiers) {
|
|
110
|
+
cdkTypeNames.add(spec.local.name);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
listener.TSInterfaceDeclaration = (node) => {
|
|
115
|
+
const decl = node;
|
|
116
|
+
const heritage = decl.extends ?? [];
|
|
117
|
+
const omitted = omittedKeys(heritage);
|
|
118
|
+
if (omitted.size === 0)
|
|
119
|
+
return;
|
|
120
|
+
// Name the base the indexed access should read from — the first `Omit`'s
|
|
121
|
+
// own base, which is the CDK props type in every builder that has one.
|
|
122
|
+
const omit = heritage.find((entry) => entry.expression?.name === "Omit");
|
|
123
|
+
const omitBase = omit === undefined ? undefined : typeArgAt(omit, 0);
|
|
124
|
+
const base = (omitBase === undefined ? undefined : rootName(omitBase.typeName)) ??
|
|
125
|
+
"CdkProps";
|
|
126
|
+
const body = decl.body?.body ?? [];
|
|
127
|
+
for (const member of body) {
|
|
128
|
+
const prop = propertyName(member);
|
|
129
|
+
if (prop === undefined || !omitted.has(prop))
|
|
130
|
+
continue;
|
|
131
|
+
for (const resolvable of resolvableRefs(member.typeAnnotation)) {
|
|
132
|
+
const inner = typeArgAt(resolvable, 0);
|
|
133
|
+
if (inner?.type !== "TSTypeReference")
|
|
134
|
+
continue;
|
|
135
|
+
const pinned = rootName(inner.typeName);
|
|
136
|
+
if (pinned === undefined || !cdkTypeNames.has(pinned))
|
|
137
|
+
continue;
|
|
138
|
+
ctx.report({
|
|
139
|
+
node: inner,
|
|
140
|
+
messageId: "pinnedType",
|
|
141
|
+
data: { prop, base, pinned },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
return listener;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=redeclared-prop-must-track-cdk-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redeclared-prop-must-track-cdk-type.js","sourceRoot":"","sources":["../../../src/rules/redeclared-prop-must-track-cdk-type.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# composurecdk/builder-must-be-tagged
|
|
2
|
+
|
|
3
|
+
Flags `Builder()` or `IBuilder<…>` imported from `@composurecdk/core` in a library builder, where the tagged equivalents belong.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `internal` (`error`)
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tagging is a cross-cutting concern every deployable resource needs, and it is not something each builder should re-invent. `taggedBuilder` / `ITaggedBuilder` from `@composurecdk/cloudformation` wrap the core builder with the shared tagging surface, so a consumer gets the same `.tags(…)` API on every builder in the library rather than on whichever ones remembered.
|
|
10
|
+
|
|
11
|
+
`@composurecdk/core` stays CDK-agnostic and cannot provide this itself — tagging is a CloudFormation concern, so the tagged wrapper lives one layer up.
|
|
12
|
+
|
|
13
|
+
## ❌ Incorrect
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Builder, type IBuilder } from "@composurecdk/core";
|
|
17
|
+
|
|
18
|
+
export function createBucketBuilder(): IBuilder<BucketBuilderProps, Bucket> {
|
|
19
|
+
return Builder(BucketBuilderImpl);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ✅ Correct
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { taggedBuilder, type ITaggedBuilder } from "@composurecdk/cloudformation";
|
|
27
|
+
|
|
28
|
+
export function createBucketBuilder(): ITaggedBuilder<BucketBuilderProps, Bucket> {
|
|
29
|
+
return taggedBuilder(BucketBuilderImpl);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Escape hatch
|
|
34
|
+
|
|
35
|
+
Some CloudFormation resources have no `Tags` property at all — Route53 records, IAM `ManagedPolicy`, SNS `Subscription`, AWS Budgets. A builder wrapping one of those cannot be tagged, so disable the rule on the line and name the resource, keeping the exception visible in review:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// eslint-disable-next-line composurecdk/builder-must-be-tagged -- AWS::Route53::RecordSet has no Tags property
|
|
39
|
+
return Builder(RecordSetBuilderImpl);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The implementation of the tagged wrapper itself is exempt in this repo's root config — `packages/cloudformation/src/tagged-builder.ts` _is_ the tagged surface, so by definition it reaches for the core builder.
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
Syntactic. The rule looks for `Builder` / `IBuilder` bindings imported from `@composurecdk/core` and reports their use.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# composurecdk/builder-must-implement-copy-state
|
|
2
|
+
|
|
3
|
+
Flags a builder class holding private state with no `[COPY_STATE]` hook.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `recommended` (`error`)
|
|
6
|
+
- **Decision:** [ADR-0005](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0005-builder-copy.md)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
`.copy()` shallow-clones `props`. Anything a builder keeps _outside_ `props` — private fields — is not carried across unless `[COPY_STATE]` says so. Without the hook, `.copy()` silently drops that state, breaking both variant authoring and strategy hand-off, and it fails quietly: the copy looks fine and behaves differently.
|
|
11
|
+
|
|
12
|
+
[ADR-0005](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0005-builder-copy.md) has the design.
|
|
13
|
+
|
|
14
|
+
## ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
class BucketBuilderImpl {
|
|
18
|
+
#alarms: AlarmDefinition[] = [];
|
|
19
|
+
// no [COPY_STATE] — `.copy()` returns a builder with no alarms
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ✅ Correct
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
class BucketBuilderImpl {
|
|
27
|
+
#alarms: AlarmDefinition[] = [];
|
|
28
|
+
|
|
29
|
+
[COPY_STATE](target: this): void {
|
|
30
|
+
target.#alarms = [...this.#alarms];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Existence, not correctness
|
|
36
|
+
|
|
37
|
+
The rule checks only that the hook is **present**. A hook copying three of five fields passes. The companion test helper `assertCopyPreservesState` from `@composurecdk/core/testing` closes that gap on the test side — the two are meant to be used together.
|
|
38
|
+
|
|
39
|
+
## Per-field opt-out
|
|
40
|
+
|
|
41
|
+
Annotate a field to exempt it, e.g. for cache-shaped state regenerated on each build. The justification after `--` is required:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// @copy-state: ignore -- memoised per build, regenerated on the clone
|
|
45
|
+
#resolvedArn?: string;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
Syntactic. The rule identifies builder classes as those passed as the first argument to `Builder()` or `taggedBuilder()`, then checks for private fields against the presence of a `[COPY_STATE]` member.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# composurecdk/constraint-metadata-required
|
|
2
|
+
|
|
3
|
+
Requires every `stringConstraint({ … })` call to set a non-empty `name`, `allowed`, and `source`.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `internal` (`error`)
|
|
6
|
+
- **Decision:** [ADR-0010](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0010-aws-property-constraints.md)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Those three fields are the whole value of a synth-time validation error. They name the property, list the allowed character set, and link the AWS documentation — turning "deployment failed" into something the author can act on at the call site.
|
|
11
|
+
|
|
12
|
+
The factory's type already makes the keys required, so the rule's real job is what the type checker cannot see: a present-but-empty literal such as `allowed: ""` compiles fine while silently degrading every message the constraint produces.
|
|
13
|
+
|
|
14
|
+
## ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const GROUP_DESCRIPTION = stringConstraint({
|
|
18
|
+
name: "EC2 SecurityGroup GroupDescription",
|
|
19
|
+
charClass: "a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*",
|
|
20
|
+
allowed: "",
|
|
21
|
+
source: "",
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## ✅ Correct
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const GROUP_DESCRIPTION = stringConstraint({
|
|
29
|
+
name: "EC2 SecurityGroup GroupDescription",
|
|
30
|
+
charClass: "a-zA-Z0-9 ._\\-:/()#,@\\[\\]+=&;{}!$*",
|
|
31
|
+
allowed: "letters, digits, spaces and ._-:/()#,@[]+=&;{}!$*",
|
|
32
|
+
source:
|
|
33
|
+
"https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html",
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Not flagged
|
|
38
|
+
|
|
39
|
+
- **Non-literal values** — `allowed: SG_ALLOWED`. The contents are not statically knowable, so the rule leaves them alone.
|
|
40
|
+
- **Calls that spread another object** — skipped, to avoid false positives where the required field arrives via the spread.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
Syntactic. The rule keys on the `stringConstraint` callee name, which is unique to the constraint-catalogue mechanism.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# composurecdk/lifecycle-build-context-required
|
|
2
|
+
|
|
3
|
+
Flags a `Lifecycle` class that uses `Resolvable<…>` but whose `build` method takes no `context` parameter.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `recommended` (`error`)
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A builder holding a `Resolvable<…>` accepts refs at configuration time. Resolving one at build time needs the context — `resolve(value, context)`. With no `context` parameter the call receives `undefined`, and the ref throws "cannot be resolved" at synth.
|
|
10
|
+
|
|
11
|
+
This rule checks the **declaration**. Its call-site counterpart, [`lifecycle-build-must-forward-context`](lifecycle-build-must-forward-context.md), checks that a builder which delegates actually passes its context on — having the parameter is not enough.
|
|
12
|
+
|
|
13
|
+
## ❌ Incorrect
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
class TopicBuilderImpl implements Lifecycle<Topic> {
|
|
17
|
+
#props: { masterKey?: Resolvable<IKey> } = {};
|
|
18
|
+
|
|
19
|
+
build(scope: IConstruct, id: string): Topic {
|
|
20
|
+
// no `context` — resolving `masterKey` here cannot work
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## ✅ Correct
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
class TopicBuilderImpl implements Lifecycle<Topic> {
|
|
29
|
+
#props: { masterKey?: Resolvable<IKey> } = {};
|
|
30
|
+
|
|
31
|
+
build(scope: IConstruct, id: string, context?: BuildContext): Topic {
|
|
32
|
+
const masterKey = resolve(this.#props.masterKey, context);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
Syntactic. The rule keys on the identifier `Resolvable` appearing in the class body — a name unique to this codebase. Keying on `resolve(` instead would produce false positives on `Promise.resolve` and friends.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# composurecdk/lifecycle-build-must-forward-context
|
|
2
|
+
|
|
3
|
+
Flags a two-argument `builder.build(scope, id)` call in library source, which gives the sub-builder no context to resolve refs against.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `libraryAuthor` (`error`)
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
This is the call-site counterpart to [`lifecycle-build-context-required`](lifecycle-build-context-required.md), which checks the declaration. Accepting a `context` parameter is not enough: a builder that delegates to a sub-builder must pass its context on. When it does not, the sub-builder resolves refs against `{}`, and any `ref()` the caller supplied through a `configure` callback dies with "component not found in context".
|
|
10
|
+
|
|
11
|
+
The declaration rule cannot see this. It keys on `Resolvable<` in the builder's own class body, but in every real occurrence the resolvable lived one delegation away — in the _sub-builder's_ props — while the offending call sat in a free helper function (`resolveAccessLogs`, `resolveFlowLogs`, …), often in another file.
|
|
12
|
+
|
|
13
|
+
Checking the call site catches both failure modes at once: to satisfy the rule at the leaf you must thread `context` into the helper, which in turn forces the parameter onto the parent's `build`.
|
|
14
|
+
|
|
15
|
+
## ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
function resolveAccessLogs(scope: IConstruct, props: Props): Bucket {
|
|
19
|
+
return props.logBucket.build(scope, "LogBucket");
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ✅ Correct
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
function resolveAccessLogs(scope: IConstruct, props: Props, context?: BuildContext): Bucket {
|
|
27
|
+
return props.logBucket.build(scope, "LogBucket", context);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why exactly two arguments
|
|
32
|
+
|
|
33
|
+
`Lifecycle.build(scope, id, context?)` is always called with at least `scope` and `id`, so a two-argument call is the precise signature of the bug. Matching exactly two — rather than "fewer than three" — keeps the rule off unrelated `build()` methods that share the name, notably `StatementBuilder.build()` in `@composurecdk/iam`, which takes none.
|
|
34
|
+
|
|
35
|
+
A spread call such as `target.build(...args)` is one argument and is likewise skipped: that is the generic forwarding wrapper in `tagged-builder.ts`, which passes context through by construction.
|
|
36
|
+
|
|
37
|
+
## Escape hatch
|
|
38
|
+
|
|
39
|
+
A root-level build genuinely has no context to forward — nothing composed it. Silence those explicitly, so the exception stays visible in review:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// eslint-disable-next-line composurecdk/lifecycle-build-must-forward-context -- root build: the strategy callback receives only (scope, id)
|
|
43
|
+
stackBuilder.build(scope, id);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Application entry points build at the root as a matter of course, so this repo's root config switches the rule off for `packages/examples/src`.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
Deliberately syntactic, like every rule in this plugin. Type information would let it assert the receiver really is a `Lifecycle`, but the residual false-positive set is small and specific — root-level standalone builds, above — so the added cost is not yet worth paying.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# composurecdk/no-cdk-api-above-floor
|
|
2
|
+
|
|
3
|
+
Flags use of an `aws-cdk-lib` API newer than the package's supported peer-dependency floor.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `internal` (`error`)
|
|
6
|
+
- **Decision:** [ADR-0008](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0008-aws-cdk-lib-version-floors.md)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Such calls compile fine — devDependencies track the latest CDK — but **throw at runtime** for a consumer on an older, still-supported version inside the declared `^2` peer range. Nothing else catches it: the type checker sees the newest CDK, and the unit suites run against it too.
|
|
11
|
+
|
|
12
|
+
[ADR-0008](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0008-aws-cdk-lib-version-floors.md) covers how the floors are chosen and enforced.
|
|
13
|
+
|
|
14
|
+
## ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { CfnAlarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
18
|
+
|
|
19
|
+
if (CfnAlarm.isCfnAlarm(node)) {
|
|
20
|
+
// TypeError on aws-cdk-lib < 2.231.0
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ✅ Correct
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { CfnResource } from "aws-cdk-lib";
|
|
28
|
+
import { CfnAlarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
29
|
+
|
|
30
|
+
if (CfnResource.isCfnResource(node) && node.cfnResourceType === CfnAlarm.CFN_RESOURCE_TYPE_NAME) {
|
|
31
|
+
// what the static does internally, valid across the whole peer range
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`@composurecdk/cloudwatch` exposes this as an `isCfnAlarm` helper; see [ADR-0011](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0011-cross-component-relationship-guards.md) for the pattern.
|
|
36
|
+
|
|
37
|
+
## The ban list
|
|
38
|
+
|
|
39
|
+
Currently one entry: the per-resource `Cfn<Resource>.isCfn<Resource>` L1 static type guards, which `aws-cdk-lib` first shipped on every generated L1 in **2.231.0** (verified by installing real versions — 2.230.0 lacks them). See issue #146.
|
|
40
|
+
|
|
41
|
+
`isCfnResource` and `isCfnElement` are explicitly allowed: they are core, predate the floor, and `isCfnResource` is the portable replacement.
|
|
42
|
+
|
|
43
|
+
The list is expected to grow as floors are pinned and lowered ahead of 1.0.0. Add an entry whenever an API turns out to postdate the floor.
|
|
44
|
+
|
|
45
|
+
## Known gaps
|
|
46
|
+
|
|
47
|
+
Both uncommon in this ESM source:
|
|
48
|
+
|
|
49
|
+
- Computed bracket access — `CfnAlarm["isCfnAlarm"](x)`.
|
|
50
|
+
- `import = require()` bindings.
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
Matched by the accessed **member name** rather than the owning class identifier, so it holds regardless of how the class is imported or aliased — `CfnAlarm.isCfnAlarm`, `cw.CfnAlarm.isCfnAlarm`, `cdk.aws_cloudwatch.CfnAlarm.isCfnAlarm` all match.
|
|
55
|
+
|
|
56
|
+
Resolution runs through ESLint's scope manager, so a local shadowing the import name (a parameter called `CfnAlarm`, say) is correctly treated as a non-CDK binding. TS-only wrappers (`as`, `satisfies`, `!`, angle-bracket assertion) are unwrapped so they cannot smuggle a call past the rule. A call in the chain — `Stack.of(x).isCfnY()` — breaks the root link, since that reads a runtime value rather than the import.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# composurecdk/no-cjs-incompatible-syntax
|
|
2
|
+
|
|
3
|
+
Flags syntax in library `src/` that has no CommonJS emit: `import.meta`, top-level `await`, and top-level `for await…of`.
|
|
4
|
+
|
|
5
|
+
- **Preset:** `dualPublishing` (`error`)
|
|
6
|
+
- **Decision:** [ADR-0007](https://github.com/laazyj/composureCDK/blob/main/docs/adr/0007-dual-esm-cjs-publishing.md)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Every `@composurecdk/*` package is dual-published, ESM and CommonJS. All three constructs are valid ESM with no CJS equivalent, so `tsc` — and `tshy`'s CommonJS dialect — errors on them.
|
|
11
|
+
|
|
12
|
+
Catching them at lint time gives an in-editor error immediately, rather than waiting for the per-dialect compile to fail.
|
|
13
|
+
|
|
14
|
+
## ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const here = import.meta.url;
|
|
18
|
+
|
|
19
|
+
const config = await loadConfig();
|
|
20
|
+
|
|
21
|
+
for await (const chunk of stream) {
|
|
22
|
+
// …
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## ✅ Correct
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
async function init(): Promise<Config> {
|
|
32
|
+
const config = await loadConfig();
|
|
33
|
+
for await (const chunk of stream) {
|
|
34
|
+
// …
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Move the `await` inside an async function. Where a module genuinely needs its own location, take it from a parameter or a caller-supplied value rather than `import.meta`.
|
|
41
|
+
|
|
42
|
+
## Scope
|
|
43
|
+
|
|
44
|
+
Library `src/` only. Test files are never published, so they may use all three freely — this repo's root config applies the preset to `packages/*/src/**/*.ts`.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
Syntactic. `import.meta` is matched as a `MetaProperty`; `await` and `for await…of` are reported only when no enclosing function declaration, function expression or arrow function is found in the ancestor chain — that is, only at the top level.
|