@craft-ng/dev-tools 0.5.0-beta.0 → 0.5.0-beta.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-ng/dev-tools",
3
- "version": "0.5.0-beta.0",
3
+ "version": "0.5.0-beta.2",
4
4
  "description": "Development tools for ng-craft: ESLint configs, ESLint rules, and codemods",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -85,9 +85,20 @@ module.exports = {
85
85
  path.dirname(filePath),
86
86
  getCwd(context),
87
87
  );
88
+ if (process.env.DEBUG_CRAFT_RULE) {
89
+ console.log('[DEBUG] file text snippet:', text.substring(0, 300));
90
+ console.log('[DEBUG] sf text snippet:', sourceFile.getFullText().substring(0, 300));
91
+ }
88
92
  const analysis = analyzeSourceFileDependencies(sourceFile, {
89
93
  configFilePath,
90
94
  });
95
+ if (process.env.DEBUG_CRAFT_RULE) {
96
+ console.log('[DEBUG] analysis deps:', analysis.dependencyGroups);
97
+ console.log('[DEBUG] generated deps:', analysis.generatedDependencyGroups);
98
+ console.log('[DEBUG] className:', analysis.className, 'kind:', analysis.angularKind);
99
+ console.log('[DEBUG] sf has class?', !!sourceFile.getClass('DemoComponent'));
100
+ console.log('[DEBUG] full sf text:', sourceFile.getFullText());
101
+ }
91
102
  if (
92
103
  !analysis.classDeclaration ||
93
104
  analysis.skipped ||
@@ -0,0 +1,125 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description:
6
+ "Ensure craftComputed(name, ...) is called with a string literal first argument that matches the declared variable or class property name.",
7
+ },
8
+ fixable: 'code',
9
+ schema: [],
10
+ messages: {
11
+ missingName:
12
+ "craftComputed must be called with a string literal name matching '{{declaredName}}' as the first argument.",
13
+ mismatchedName:
14
+ "craftComputed first argument '{{actual}}' must match the declared name '{{declaredName}}'.",
15
+ },
16
+ },
17
+ create(context) {
18
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
19
+
20
+ return {
21
+ CallExpression(node) {
22
+ if (
23
+ node.callee.type !== 'Identifier' ||
24
+ node.callee.name !== 'craftComputed'
25
+ ) {
26
+ return;
27
+ }
28
+
29
+ const declaredName = getDeclaredName(node);
30
+ if (!declaredName) {
31
+ return;
32
+ }
33
+
34
+ const firstArg = node.arguments[0];
35
+
36
+ if (!firstArg) {
37
+ context.report({
38
+ node,
39
+ messageId: 'missingName',
40
+ data: { declaredName },
41
+ fix(fixer) {
42
+ const openParen = sourceCode.getTokenAfter(
43
+ node.callee,
44
+ (token) => token.type === 'Punctuator' && token.value === '(',
45
+ );
46
+ if (!openParen) return null;
47
+ return fixer.insertTextAfter(openParen, `'${declaredName}'`);
48
+ },
49
+ });
50
+ return;
51
+ }
52
+
53
+ if (isStringLiteral(firstArg)) {
54
+ const actual = getStringLiteralValue(firstArg);
55
+ if (actual === declaredName) {
56
+ return;
57
+ }
58
+ context.report({
59
+ node: firstArg,
60
+ messageId: 'mismatchedName',
61
+ data: { declaredName, actual },
62
+ fix(fixer) {
63
+ return fixer.replaceText(firstArg, `'${declaredName}'`);
64
+ },
65
+ });
66
+ return;
67
+ }
68
+
69
+ context.report({
70
+ node: firstArg,
71
+ messageId: 'missingName',
72
+ data: { declaredName },
73
+ fix(fixer) {
74
+ return fixer.insertTextBefore(firstArg, `'${declaredName}', `);
75
+ },
76
+ });
77
+ },
78
+ };
79
+ },
80
+ };
81
+
82
+ function getDeclaredName(callNode) {
83
+ const parent = callNode.parent;
84
+ if (!parent) return undefined;
85
+
86
+ if (
87
+ parent.type === 'VariableDeclarator' &&
88
+ parent.init === callNode &&
89
+ parent.id.type === 'Identifier'
90
+ ) {
91
+ return parent.id.name;
92
+ }
93
+
94
+ if (
95
+ parent.type === 'PropertyDefinition' &&
96
+ parent.value === callNode &&
97
+ !parent.computed &&
98
+ parent.key.type === 'Identifier'
99
+ ) {
100
+ return parent.key.name;
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ function isStringLiteral(node) {
107
+ if (node.type === 'Literal' && typeof node.value === 'string') {
108
+ return true;
109
+ }
110
+ if (
111
+ node.type === 'TemplateLiteral' &&
112
+ node.expressions.length === 0 &&
113
+ node.quasis.length === 1
114
+ ) {
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ function getStringLiteralValue(node) {
121
+ if (node.type === 'Literal') {
122
+ return node.value;
123
+ }
124
+ return node.quasis[0].value.cooked;
125
+ }
@@ -0,0 +1,166 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description:
6
+ "Ensure craftMethod(name, ...) is called with a string literal first argument (or { name } object) that matches the declared variable or class property name.",
7
+ },
8
+ fixable: 'code',
9
+ schema: [],
10
+ messages: {
11
+ missingName:
12
+ "craftMethod must be called with a string literal name matching '{{declaredName}}' as the first argument.",
13
+ mismatchedName:
14
+ "craftMethod first argument '{{actual}}' must match the declared name '{{declaredName}}'.",
15
+ },
16
+ },
17
+ create(context) {
18
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
19
+
20
+ return {
21
+ CallExpression(node) {
22
+ if (
23
+ node.callee.type !== 'Identifier' ||
24
+ node.callee.name !== 'craftMethod'
25
+ ) {
26
+ return;
27
+ }
28
+
29
+ const declaredName = getDeclaredName(node);
30
+ if (!declaredName) {
31
+ return;
32
+ }
33
+
34
+ const firstArg = node.arguments[0];
35
+
36
+ if (!firstArg) {
37
+ context.report({
38
+ node,
39
+ messageId: 'missingName',
40
+ data: { declaredName },
41
+ fix(fixer) {
42
+ const openParen = sourceCode.getTokenAfter(
43
+ node.callee,
44
+ (token) => token.type === 'Punctuator' && token.value === '(',
45
+ );
46
+ if (!openParen) return null;
47
+ return fixer.insertTextAfter(openParen, `'${declaredName}'`);
48
+ },
49
+ });
50
+ return;
51
+ }
52
+
53
+ // Handle object config form: { name: 'methodName', providers: [...] }
54
+ if (firstArg.type === 'ObjectExpression') {
55
+ const nameProp = firstArg.properties.find(
56
+ (p) =>
57
+ p.type === 'Property' &&
58
+ !p.computed &&
59
+ p.key.type === 'Identifier' &&
60
+ p.key.name === 'name',
61
+ );
62
+ if (!nameProp) {
63
+ context.report({
64
+ node: firstArg,
65
+ messageId: 'missingName',
66
+ data: { declaredName },
67
+ });
68
+ return;
69
+ }
70
+ const nameValue = nameProp.value;
71
+ if (isStringLiteral(nameValue)) {
72
+ const actual = getStringLiteralValue(nameValue);
73
+ if (actual === declaredName) {
74
+ return;
75
+ }
76
+ context.report({
77
+ node: nameValue,
78
+ messageId: 'mismatchedName',
79
+ data: { declaredName, actual },
80
+ fix(fixer) {
81
+ return fixer.replaceText(nameValue, `'${declaredName}'`);
82
+ },
83
+ });
84
+ } else {
85
+ context.report({
86
+ node: nameValue,
87
+ messageId: 'mismatchedName',
88
+ data: { declaredName, actual: sourceCode.getText(nameValue) },
89
+ });
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (isStringLiteral(firstArg)) {
95
+ const actual = getStringLiteralValue(firstArg);
96
+ if (actual === declaredName) {
97
+ return;
98
+ }
99
+ context.report({
100
+ node: firstArg,
101
+ messageId: 'mismatchedName',
102
+ data: { declaredName, actual },
103
+ fix(fixer) {
104
+ return fixer.replaceText(firstArg, `'${declaredName}'`);
105
+ },
106
+ });
107
+ return;
108
+ }
109
+
110
+ context.report({
111
+ node: firstArg,
112
+ messageId: 'missingName',
113
+ data: { declaredName },
114
+ fix(fixer) {
115
+ return fixer.insertTextBefore(firstArg, `'${declaredName}', `);
116
+ },
117
+ });
118
+ },
119
+ };
120
+ },
121
+ };
122
+
123
+ function getDeclaredName(callNode) {
124
+ const parent = callNode.parent;
125
+ if (!parent) return undefined;
126
+
127
+ if (
128
+ parent.type === 'VariableDeclarator' &&
129
+ parent.init === callNode &&
130
+ parent.id.type === 'Identifier'
131
+ ) {
132
+ return parent.id.name;
133
+ }
134
+
135
+ if (
136
+ parent.type === 'PropertyDefinition' &&
137
+ parent.value === callNode &&
138
+ !parent.computed &&
139
+ parent.key.type === 'Identifier'
140
+ ) {
141
+ return parent.key.name;
142
+ }
143
+
144
+ return undefined;
145
+ }
146
+
147
+ function isStringLiteral(node) {
148
+ if (node.type === 'Literal' && typeof node.value === 'string') {
149
+ return true;
150
+ }
151
+ if (
152
+ node.type === 'TemplateLiteral' &&
153
+ node.expressions.length === 0 &&
154
+ node.quasis.length === 1
155
+ ) {
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ function getStringLiteralValue(node) {
162
+ if (node.type === 'Literal') {
163
+ return node.value;
164
+ }
165
+ return node.quasis[0].value.cooked;
166
+ }
@@ -2,11 +2,16 @@ const brandAngularGenDepsRequired = require('./brand-angular-gen-deps-required.c
2
2
  const brandAngularDepsMatch = require('./brand-angular-deps-match.cjs');
3
3
  const appStartRegistryMatch = require('./app-start-registry-match.cjs');
4
4
  const componentTestGenDepsMatch = require('./component-test-gen-deps-match.cjs');
5
+ const craftMethodNameMatch = require('./craft-method-name-match.cjs');
6
+ const craftComputedNameMatch = require('./craft-computed-name-match.cjs');
7
+ const preferCraftComputed = require('./prefer-craft-computed.cjs');
5
8
  const noAngularInject = require('./no-angular-inject.cjs');
6
9
  const noAngularSignalForms = require('./no-angular-signal-forms.cjs');
10
+ const provideHostNameMatchComponent = require('./provide-host-name-match-component.cjs');
7
11
  const preferCraftHttpClient = require('./prefer-craft-http-client.cjs');
8
12
  const preferCraftService = require('./prefer-craft-service.cjs');
9
13
  const preferBrowserBoundaries = require('./prefer-browser-boundaries.cjs');
14
+ const requireComponentMonitoring = require('./require-component-monitoring.cjs');
10
15
 
11
16
  module.exports = {
12
17
  rules: {
@@ -14,10 +19,15 @@ module.exports = {
14
19
  'brand-angular-gen-deps-required': brandAngularGenDepsRequired,
15
20
  'brand-angular-deps-match': brandAngularDepsMatch,
16
21
  'component-test-gen-deps-match': componentTestGenDepsMatch,
22
+ 'craft-method-name-match': craftMethodNameMatch,
23
+ 'craft-computed-name-match': craftComputedNameMatch,
24
+ 'prefer-craft-computed': preferCraftComputed,
17
25
  'no-angular-inject': noAngularInject,
18
26
  'no-angular-signal-forms': noAngularSignalForms,
27
+ 'provide-host-name-match-component': provideHostNameMatchComponent,
19
28
  'prefer-craft-http-client': preferCraftHttpClient,
20
29
  'prefer-craft-service': preferCraftService,
21
30
  'prefer-browser-boundaries': preferBrowserBoundaries,
31
+ 'require-component-monitoring': requireComponentMonitoring,
22
32
  },
23
33
  };
@@ -0,0 +1,105 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'suggestion',
4
+ docs: {
5
+ description:
6
+ "Prefer craftComputed() over computed() from @angular/core for better observability and host name tracking.",
7
+ },
8
+ hasSuggestions: true,
9
+ schema: [],
10
+ messages: {
11
+ preferCraftComputed:
12
+ "Use craftComputed('{{name}}', ...) instead of computed() for better observability. craftComputed adds HostName tracking to the computed signal.",
13
+ preferCraftComputedUnnamed:
14
+ "Use craftComputed('name', ...) instead of computed() for better observability. craftComputed adds HostName tracking to the computed signal.",
15
+ },
16
+ },
17
+ create(context) {
18
+ return {
19
+ CallExpression(node) {
20
+ if (
21
+ node.callee.type !== 'Identifier' ||
22
+ node.callee.name !== 'computed'
23
+ ) {
24
+ return;
25
+ }
26
+
27
+ if (!isAngularComputedImport(node, context)) {
28
+ return;
29
+ }
30
+
31
+ const declaredName = getDeclaredName(node);
32
+
33
+ if (declaredName) {
34
+ context.report({
35
+ node,
36
+ messageId: 'preferCraftComputed',
37
+ data: { name: declaredName },
38
+ suggest: [
39
+ {
40
+ desc: `Replace with craftComputed('${declaredName}', ...)`,
41
+ fix(fixer) {
42
+ return fixer.replaceText(node.callee, 'craftComputed');
43
+ },
44
+ },
45
+ ],
46
+ });
47
+ } else {
48
+ context.report({
49
+ node,
50
+ messageId: 'preferCraftComputedUnnamed',
51
+ });
52
+ }
53
+ },
54
+ };
55
+ },
56
+ };
57
+
58
+ function getDeclaredName(callNode) {
59
+ const parent = callNode.parent;
60
+ if (!parent) return undefined;
61
+
62
+ if (
63
+ parent.type === 'VariableDeclarator' &&
64
+ parent.init === callNode &&
65
+ parent.id.type === 'Identifier'
66
+ ) {
67
+ return parent.id.name;
68
+ }
69
+
70
+ if (
71
+ parent.type === 'PropertyDefinition' &&
72
+ parent.value === callNode &&
73
+ !parent.computed &&
74
+ parent.key.type === 'Identifier'
75
+ ) {
76
+ return parent.key.name;
77
+ }
78
+
79
+ return undefined;
80
+ }
81
+
82
+ function isAngularComputedImport(callNode, context) {
83
+ const scope = context.getScope ? context.getScope() : context.sourceCode.getScope(callNode);
84
+
85
+ let currentScope = scope;
86
+ while (currentScope) {
87
+ for (const variable of currentScope.variables) {
88
+ if (variable.name === 'computed') {
89
+ for (const def of variable.defs) {
90
+ if (
91
+ def.type === 'ImportBinding' &&
92
+ def.parent &&
93
+ def.parent.source &&
94
+ def.parent.source.value === '@angular/core'
95
+ ) {
96
+ return true;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ currentScope = currentScope.upper;
102
+ }
103
+
104
+ return false;
105
+ }