@fitzzero/quickdraw-core 3.6.0 → 3.8.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/README.md +218 -64
- package/dist/client/index.d.ts +34 -4
- package/dist/client/index.js +57 -65
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/client/testing.js.map +1 -1
- package/dist/server/express/index.d.ts +37 -0
- package/dist/server/express/index.js +60 -0
- package/dist/server/express/index.js.map +1 -0
- package/dist/server/index.d.ts +330 -5
- package/dist/server/index.js +593 -144
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing-prisma.d.ts +101 -0
- package/dist/server/testing-prisma.js +172 -0
- package/dist/server/testing-prisma.js.map +1 -0
- package/dist/server/testing.d.ts +1 -1
- package/dist/server/testing.js +87 -22
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.d.ts +77 -1
- package/dist/shared/index.js +5 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/{types-DniMxkQs.d.ts → types-ClgSr90l.d.ts} +5 -1
- package/dist/{types-B1z5aXdR.d.ts → types-DUHPIy_5.d.ts} +20 -1
- package/eslint-config/index.mjs +1 -2
- package/eslint-plugin-quickdraw/index.mjs +15 -7
- package/eslint-plugin-quickdraw/rules/no-cross-service-mutations.mjs +31 -32
- package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +17 -18
- package/eslint-plugin-quickdraw/rules/no-raw-button-strings.mjs +52 -0
- package/eslint-plugin-quickdraw/rules/no-raw-service-room-string.mjs +140 -0
- package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +20 -22
- package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +17 -17
- package/eslint-plugin-quickdraw/rules/no-raw-tooltip-strings.mjs +59 -0
- package/eslint-plugin-quickdraw/rules/no-raw-typography-strings.mjs +52 -0
- package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +3 -3
- package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +8 -8
- package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +15 -20
- package/package.json +74 -50
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const MUTATION_METHODS = new Set(["create", "update", "delete"])
|
|
1
|
+
const MUTATION_METHODS = new Set(["create", "update", "delete"]);
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extract the service directory name from a file path.
|
|
@@ -6,8 +6,8 @@ const MUTATION_METHODS = new Set(["create", "update", "delete"])
|
|
|
6
6
|
* "/app/services/task/helpers.ts" → "task"
|
|
7
7
|
*/
|
|
8
8
|
function getServiceDirName(filename) {
|
|
9
|
-
const match = filename.match(/services\/([^/]+)\//)
|
|
10
|
-
return match ? match[1] : null
|
|
9
|
+
const match = filename.match(/services\/([^/]+)\//);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -16,7 +16,7 @@ function getServiceDirName(filename) {
|
|
|
16
16
|
* e.g. "cloud-build" → "cloudBuild", "task" → "task"
|
|
17
17
|
*/
|
|
18
18
|
function dirNameToModelName(dirName) {
|
|
19
|
-
return dirName.replace(/[-_](\w)/g, (_, c) => c.toUpperCase())
|
|
19
|
+
return dirName.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
@@ -56,38 +56,37 @@ export default {
|
|
|
56
56
|
],
|
|
57
57
|
},
|
|
58
58
|
create(context) {
|
|
59
|
-
const filename = context.filename || context.getFilename()
|
|
60
|
-
const serviceDirName = getServiceDirName(filename)
|
|
59
|
+
const filename = context.filename || context.getFilename();
|
|
60
|
+
const serviceDirName = getServiceDirName(filename);
|
|
61
61
|
|
|
62
|
-
if (!serviceDirName) return {}
|
|
62
|
+
if (!serviceDirName) return {};
|
|
63
63
|
|
|
64
|
-
const ownedModel = dirNameToModelName(serviceDirName)
|
|
65
|
-
const isIndexFile = filename.endsWith("/index.ts") || filename.endsWith("/index.js")
|
|
66
|
-
const options = context.options[0] || {}
|
|
67
|
-
const allowedModels = options.allowedModels || {}
|
|
68
|
-
const allowedForThisService = new Set(allowedModels[serviceDirName] || [])
|
|
64
|
+
const ownedModel = dirNameToModelName(serviceDirName);
|
|
65
|
+
const isIndexFile = filename.endsWith("/index.ts") || filename.endsWith("/index.js");
|
|
66
|
+
const options = context.options[0] || {};
|
|
67
|
+
const allowedModels = options.allowedModels || {};
|
|
68
|
+
const allowedForThisService = new Set(allowedModels[serviceDirName] || []);
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
71
|
CallExpression(node) {
|
|
72
|
-
const callee = node.callee
|
|
73
|
-
if (callee.type !== "MemberExpression") return
|
|
74
|
-
if (callee.property.type !== "Identifier") return
|
|
72
|
+
const callee = node.callee;
|
|
73
|
+
if (callee.type !== "MemberExpression") return;
|
|
74
|
+
if (callee.property.type !== "Identifier") return;
|
|
75
75
|
|
|
76
|
-
const methodName = callee.property.name
|
|
77
|
-
if (!MUTATION_METHODS.has(methodName)) return
|
|
76
|
+
const methodName = callee.property.name;
|
|
77
|
+
if (!MUTATION_METHODS.has(methodName)) return;
|
|
78
78
|
|
|
79
|
-
const obj = callee.object
|
|
80
|
-
if (obj.type !== "MemberExpression") return
|
|
79
|
+
const obj = callee.object;
|
|
80
|
+
if (obj.type !== "MemberExpression") return;
|
|
81
81
|
|
|
82
|
-
const parentObj = obj.object
|
|
83
|
-
if (parentObj.type !== "MemberExpression") return
|
|
84
|
-
if (parentObj.property.type !== "Identifier") return
|
|
85
|
-
if (parentObj.property.name !== "prisma") return
|
|
82
|
+
const parentObj = obj.object;
|
|
83
|
+
if (parentObj.type !== "MemberExpression") return;
|
|
84
|
+
if (parentObj.property.type !== "Identifier") return;
|
|
85
|
+
if (parentObj.property.name !== "prisma") return;
|
|
86
86
|
|
|
87
|
-
if (parentObj.object.type !== "ThisExpression") return
|
|
87
|
+
if (parentObj.object.type !== "ThisExpression") return;
|
|
88
88
|
|
|
89
|
-
const modelName =
|
|
90
|
-
obj.property.type === "Identifier" ? obj.property.name : "model"
|
|
89
|
+
const modelName = obj.property.type === "Identifier" ? obj.property.name : "model";
|
|
91
90
|
|
|
92
91
|
if (modelName === ownedModel) {
|
|
93
92
|
if (!isIndexFile) {
|
|
@@ -95,19 +94,19 @@ export default {
|
|
|
95
94
|
node,
|
|
96
95
|
messageId: "useBaseService",
|
|
97
96
|
data: { method: methodName, model: modelName },
|
|
98
|
-
})
|
|
97
|
+
});
|
|
99
98
|
}
|
|
100
|
-
return
|
|
99
|
+
return;
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
if (allowedForThisService.has(modelName)) return
|
|
102
|
+
if (allowedForThisService.has(modelName)) return;
|
|
104
103
|
|
|
105
104
|
context.report({
|
|
106
105
|
node,
|
|
107
106
|
messageId: "crossServiceMutation",
|
|
108
107
|
data: { method: methodName, model: modelName },
|
|
109
|
-
})
|
|
108
|
+
});
|
|
110
109
|
},
|
|
111
|
-
}
|
|
110
|
+
};
|
|
112
111
|
},
|
|
113
|
-
}
|
|
112
|
+
};
|
|
@@ -14,36 +14,35 @@ export default {
|
|
|
14
14
|
schema: [],
|
|
15
15
|
},
|
|
16
16
|
create(context) {
|
|
17
|
-
const MUTATION_METHODS = new Set(["create", "update", "delete"])
|
|
17
|
+
const MUTATION_METHODS = new Set(["create", "update", "delete"]);
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
20
|
CallExpression(node) {
|
|
21
|
-
const callee = node.callee
|
|
22
|
-
if (callee.type !== "MemberExpression") return
|
|
23
|
-
if (callee.property.type !== "Identifier") return
|
|
21
|
+
const callee = node.callee;
|
|
22
|
+
if (callee.type !== "MemberExpression") return;
|
|
23
|
+
if (callee.property.type !== "Identifier") return;
|
|
24
24
|
|
|
25
|
-
const methodName = callee.property.name
|
|
26
|
-
if (!MUTATION_METHODS.has(methodName)) return
|
|
25
|
+
const methodName = callee.property.name;
|
|
26
|
+
if (!MUTATION_METHODS.has(methodName)) return;
|
|
27
27
|
|
|
28
|
-
const obj = callee.object
|
|
29
|
-
if (obj.type !== "MemberExpression") return
|
|
28
|
+
const obj = callee.object;
|
|
29
|
+
if (obj.type !== "MemberExpression") return;
|
|
30
30
|
|
|
31
|
-
const parentObj = obj.object
|
|
32
|
-
if (parentObj.type !== "MemberExpression") return
|
|
33
|
-
if (parentObj.property.type !== "Identifier") return
|
|
34
|
-
if (parentObj.property.name !== "prisma") return
|
|
31
|
+
const parentObj = obj.object;
|
|
32
|
+
if (parentObj.type !== "MemberExpression") return;
|
|
33
|
+
if (parentObj.property.type !== "Identifier") return;
|
|
34
|
+
if (parentObj.property.name !== "prisma") return;
|
|
35
35
|
|
|
36
|
-
if (parentObj.object.type !== "ThisExpression") return
|
|
36
|
+
if (parentObj.object.type !== "ThisExpression") return;
|
|
37
37
|
|
|
38
|
-
const modelName =
|
|
39
|
-
obj.property.type === "Identifier" ? obj.property.name : "model"
|
|
38
|
+
const modelName = obj.property.type === "Identifier" ? obj.property.name : "model";
|
|
40
39
|
|
|
41
40
|
context.report({
|
|
42
41
|
node,
|
|
43
42
|
messageId: "noDirectMutation",
|
|
44
43
|
data: { method: methodName, model: modelName },
|
|
45
|
-
})
|
|
44
|
+
});
|
|
46
45
|
},
|
|
47
|
-
}
|
|
46
|
+
};
|
|
48
47
|
},
|
|
49
|
-
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Disallow raw string literals in Button components. Use useTranslations() or the <T> component for localization.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noRawButtonStrings:
|
|
11
|
+
"Raw string '{{content}}' in Button component should be localized. Use t('key') or <T>key</T> instead.",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
JSXElement(node) {
|
|
18
|
+
// Check if this is a Button element
|
|
19
|
+
if (
|
|
20
|
+
node.openingElement.name.type !== "JSXIdentifier" ||
|
|
21
|
+
node.openingElement.name.name !== "Button"
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check all children for raw strings
|
|
27
|
+
for (const child of node.children) {
|
|
28
|
+
if (child.type === "JSXText") {
|
|
29
|
+
// Flag JSXText that contains non-whitespace characters
|
|
30
|
+
const trimmedText = child.value.trim();
|
|
31
|
+
if (trimmedText.length > 0) {
|
|
32
|
+
context.report({
|
|
33
|
+
node: child,
|
|
34
|
+
messageId: "noRawButtonStrings",
|
|
35
|
+
data: { content: trimmedText },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
39
|
+
// Flag JSXExpressionContainer where the expression is a string literal
|
|
40
|
+
if (child.expression.type === "Literal" && typeof child.expression.value === "string") {
|
|
41
|
+
context.report({
|
|
42
|
+
node: child,
|
|
43
|
+
messageId: "noRawButtonStrings",
|
|
44
|
+
data: { content: child.expression.value },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bans raw template literals used as room name strings in emit/to/join calls.
|
|
3
|
+
* Use serviceRoom(), userRoom(), etc. helpers from your shared package instead.
|
|
4
|
+
*
|
|
5
|
+
* Options: { additionalPatterns: string[] } — app-specific room patterns
|
|
6
|
+
* (regex sources) checked in addition to the defaults.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ROOM_PATTERNS = [/Service:\$\{/, /^user:\$\{/];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a template literal node looks like a room string.
|
|
13
|
+
*/
|
|
14
|
+
function isRoomTemplateLiteral(node, patterns) {
|
|
15
|
+
if (node.type !== "TemplateLiteral" || node.expressions.length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Reconstruct the raw template pattern by replacing expressions with ${...}
|
|
20
|
+
let raw = "";
|
|
21
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
22
|
+
raw += node.quasis[i].value.raw;
|
|
23
|
+
if (i < node.expressions.length) {
|
|
24
|
+
raw += "${...}";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return patterns.some((pattern) => pattern.test(raw));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a node is a call to emitToRoom, .to(), .join(), or
|
|
33
|
+
* emitConnectedUsers and return the argument index that represents the room name.
|
|
34
|
+
*/
|
|
35
|
+
function getRoomArgIndex(node) {
|
|
36
|
+
if (node.type !== "CallExpression") return -1;
|
|
37
|
+
|
|
38
|
+
const callee = node.callee;
|
|
39
|
+
|
|
40
|
+
// this.emitToRoom(ROOM, ...) or service.emitToRoom(ROOM, ...)
|
|
41
|
+
if (
|
|
42
|
+
callee.type === "MemberExpression" &&
|
|
43
|
+
callee.property.type === "Identifier" &&
|
|
44
|
+
callee.property.name === "emitToRoom"
|
|
45
|
+
) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// .to(ROOM) — Socket.IO chaining
|
|
50
|
+
if (
|
|
51
|
+
callee.type === "MemberExpression" &&
|
|
52
|
+
callee.property.type === "Identifier" &&
|
|
53
|
+
callee.property.name === "to"
|
|
54
|
+
) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// .join(ROOM) — Socket.IO room joining
|
|
59
|
+
if (
|
|
60
|
+
callee.type === "MemberExpression" &&
|
|
61
|
+
callee.property.type === "Identifier" &&
|
|
62
|
+
callee.property.name === "join"
|
|
63
|
+
) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// this.emitConnectedUsers(entityId, ROOM)
|
|
68
|
+
if (
|
|
69
|
+
callee.type === "MemberExpression" &&
|
|
70
|
+
callee.property.type === "Identifier" &&
|
|
71
|
+
callee.property.name === "emitConnectedUsers"
|
|
72
|
+
) {
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
80
|
+
export default {
|
|
81
|
+
meta: {
|
|
82
|
+
type: "problem",
|
|
83
|
+
docs: {
|
|
84
|
+
description:
|
|
85
|
+
"Disallow raw template literal room strings in emit/to/join calls. Use serviceRoom() or other room helpers from your shared package.",
|
|
86
|
+
},
|
|
87
|
+
messages: {
|
|
88
|
+
noRawRoomString:
|
|
89
|
+
"Use serviceRoom() or other room helpers from your shared package instead of raw template literals for room names.",
|
|
90
|
+
},
|
|
91
|
+
schema: [
|
|
92
|
+
{
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
additionalPatterns: {
|
|
96
|
+
type: "array",
|
|
97
|
+
items: { type: "string" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
create(context) {
|
|
105
|
+
const options = context.options[0] ?? {};
|
|
106
|
+
const patterns = [
|
|
107
|
+
...DEFAULT_ROOM_PATTERNS,
|
|
108
|
+
...(options.additionalPatterns ?? []).map((source) => new RegExp(source)),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
CallExpression(node) {
|
|
113
|
+
const argIndex = getRoomArgIndex(node);
|
|
114
|
+
if (argIndex < 0 || !node.arguments[argIndex]) return;
|
|
115
|
+
|
|
116
|
+
const arg = node.arguments[argIndex];
|
|
117
|
+
if (isRoomTemplateLiteral(arg, patterns)) {
|
|
118
|
+
context.report({
|
|
119
|
+
node: arg,
|
|
120
|
+
messageId: "noRawRoomString",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
// Also catch: const room = `chatService:${id}`;
|
|
125
|
+
VariableDeclarator(node) {
|
|
126
|
+
if (
|
|
127
|
+
node.id.type === "Identifier" &&
|
|
128
|
+
/room/i.test(node.id.name) &&
|
|
129
|
+
node.init &&
|
|
130
|
+
isRoomTemplateLiteral(node.init, patterns)
|
|
131
|
+
) {
|
|
132
|
+
context.report({
|
|
133
|
+
node: node.init,
|
|
134
|
+
messageId: "noRawRoomString",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Allowed event name prefixes that are exempt from this rule.
|
|
3
3
|
* These are documented exceptions for protocols that don't map to BaseService.
|
|
4
4
|
*/
|
|
5
|
-
const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"]
|
|
5
|
+
const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"];
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Allowed exact event names that are exempt from this rule.
|
|
9
9
|
*/
|
|
10
|
-
const DEFAULT_ALLOWED_EVENTS = new Set(["subscribe", "unsubscribe"])
|
|
10
|
+
const DEFAULT_ALLOWED_EVENTS = new Set(["subscribe", "unsubscribe"]);
|
|
11
11
|
|
|
12
12
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
13
13
|
export default {
|
|
@@ -51,45 +51,43 @@ export default {
|
|
|
51
51
|
],
|
|
52
52
|
},
|
|
53
53
|
create(context) {
|
|
54
|
-
const options = context.options[0] || {}
|
|
55
|
-
const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES
|
|
56
|
-
const allowedEvents = new Set(
|
|
57
|
-
options.allowedEvents || DEFAULT_ALLOWED_EVENTS
|
|
58
|
-
)
|
|
54
|
+
const options = context.options[0] || {};
|
|
55
|
+
const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES;
|
|
56
|
+
const allowedEvents = new Set(options.allowedEvents || DEFAULT_ALLOWED_EVENTS);
|
|
59
57
|
|
|
60
58
|
return {
|
|
61
59
|
CallExpression(node) {
|
|
62
|
-
const callee = node.callee
|
|
63
|
-
if (callee.type !== "MemberExpression") return
|
|
64
|
-
if (callee.property.type !== "Identifier") return
|
|
65
|
-
if (callee.property.name !== "emit") return
|
|
60
|
+
const callee = node.callee;
|
|
61
|
+
if (callee.type !== "MemberExpression") return;
|
|
62
|
+
if (callee.property.type !== "Identifier") return;
|
|
63
|
+
if (callee.property.name !== "emit") return;
|
|
66
64
|
|
|
67
|
-
const obj = callee.object
|
|
68
|
-
if (obj.type !== "Identifier" || obj.name !== "socket") return
|
|
65
|
+
const obj = callee.object;
|
|
66
|
+
if (obj.type !== "Identifier" || obj.name !== "socket") return;
|
|
69
67
|
|
|
70
|
-
const eventArg = node.arguments[0]
|
|
71
|
-
if (!eventArg) return
|
|
68
|
+
const eventArg = node.arguments[0];
|
|
69
|
+
if (!eventArg) return;
|
|
72
70
|
|
|
73
71
|
if (eventArg.type === "Literal" && typeof eventArg.value === "string") {
|
|
74
|
-
const eventName = eventArg.value
|
|
72
|
+
const eventName = eventArg.value;
|
|
75
73
|
|
|
76
|
-
if (allowedEvents.has(eventName)) return
|
|
74
|
+
if (allowedEvents.has(eventName)) return;
|
|
77
75
|
if (allowedPrefixes.some((prefix) => eventName.startsWith(prefix))) {
|
|
78
|
-
return
|
|
76
|
+
return;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
context.report({
|
|
82
80
|
node,
|
|
83
81
|
messageId: "noRawSocketEmit",
|
|
84
82
|
data: { eventName },
|
|
85
|
-
})
|
|
83
|
+
});
|
|
86
84
|
} else {
|
|
87
85
|
context.report({
|
|
88
86
|
node,
|
|
89
87
|
messageId: "noRawSocketEmitDynamic",
|
|
90
|
-
})
|
|
88
|
+
});
|
|
91
89
|
}
|
|
92
90
|
},
|
|
93
|
-
}
|
|
91
|
+
};
|
|
94
92
|
},
|
|
95
|
-
}
|
|
93
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Allowed event name prefixes that are exempt from this rule.
|
|
3
3
|
* These are documented exceptions for protocols that don't map to BaseService.
|
|
4
4
|
*/
|
|
5
|
-
const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"]
|
|
5
|
+
const DEFAULT_ALLOWED_PREFIXES = ["agentRunner:", "projectRunner:"];
|
|
6
6
|
|
|
7
7
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
8
|
export default {
|
|
@@ -39,41 +39,41 @@ export default {
|
|
|
39
39
|
],
|
|
40
40
|
},
|
|
41
41
|
create(context) {
|
|
42
|
-
const options = context.options[0] || {}
|
|
43
|
-
const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES
|
|
42
|
+
const options = context.options[0] || {};
|
|
43
|
+
const allowedPrefixes = options.allowedPrefixes || DEFAULT_ALLOWED_PREFIXES;
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
46
|
CallExpression(node) {
|
|
47
|
-
const callee = node.callee
|
|
48
|
-
if (callee.type !== "MemberExpression") return
|
|
49
|
-
if (callee.property.type !== "Identifier") return
|
|
50
|
-
if (callee.property.name !== "on") return
|
|
47
|
+
const callee = node.callee;
|
|
48
|
+
if (callee.type !== "MemberExpression") return;
|
|
49
|
+
if (callee.property.type !== "Identifier") return;
|
|
50
|
+
if (callee.property.name !== "on") return;
|
|
51
51
|
|
|
52
|
-
const obj = callee.object
|
|
53
|
-
if (obj.type !== "Identifier" || obj.name !== "socket") return
|
|
52
|
+
const obj = callee.object;
|
|
53
|
+
if (obj.type !== "Identifier" || obj.name !== "socket") return;
|
|
54
54
|
|
|
55
|
-
const eventArg = node.arguments[0]
|
|
56
|
-
if (!eventArg) return
|
|
55
|
+
const eventArg = node.arguments[0];
|
|
56
|
+
if (!eventArg) return;
|
|
57
57
|
|
|
58
58
|
if (eventArg.type === "Literal" && typeof eventArg.value === "string") {
|
|
59
|
-
const eventName = eventArg.value
|
|
59
|
+
const eventName = eventArg.value;
|
|
60
60
|
|
|
61
61
|
if (allowedPrefixes.some((prefix) => eventName.startsWith(prefix))) {
|
|
62
|
-
return
|
|
62
|
+
return;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
context.report({
|
|
66
66
|
node,
|
|
67
67
|
messageId: "noRawSocketOn",
|
|
68
68
|
data: { eventName },
|
|
69
|
-
})
|
|
69
|
+
});
|
|
70
70
|
} else {
|
|
71
71
|
context.report({
|
|
72
72
|
node,
|
|
73
73
|
messageId: "noRawSocketOnDynamic",
|
|
74
|
-
})
|
|
74
|
+
});
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
-
}
|
|
77
|
+
};
|
|
78
78
|
},
|
|
79
|
-
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Disallow raw string literals in Tooltip title props. Use useTranslations() for localization.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noRawTooltipStrings:
|
|
11
|
+
"Raw string '{{content}}' in Tooltip title should be localized. Use t('key') instead.",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
JSXOpeningElement(node) {
|
|
18
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "Tooltip") {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const titleAttr = node.attributes.find(
|
|
23
|
+
(a) => a.type === "JSXAttribute" && a.name.name === "title",
|
|
24
|
+
);
|
|
25
|
+
if (!titleAttr) return;
|
|
26
|
+
|
|
27
|
+
// Check for title="raw string"
|
|
28
|
+
if (
|
|
29
|
+
titleAttr.value &&
|
|
30
|
+
(titleAttr.value.type === "StringLiteral" || titleAttr.value.type === "Literal") &&
|
|
31
|
+
typeof titleAttr.value.value === "string" &&
|
|
32
|
+
titleAttr.value.value.trim().length > 0
|
|
33
|
+
) {
|
|
34
|
+
context.report({
|
|
35
|
+
node: titleAttr,
|
|
36
|
+
messageId: "noRawTooltipStrings",
|
|
37
|
+
data: { content: titleAttr.value.value },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for title={"raw string"}
|
|
42
|
+
if (
|
|
43
|
+
titleAttr.value &&
|
|
44
|
+
titleAttr.value.type === "JSXExpressionContainer" &&
|
|
45
|
+
titleAttr.value.expression &&
|
|
46
|
+
titleAttr.value.expression.type === "Literal" &&
|
|
47
|
+
typeof titleAttr.value.expression.value === "string" &&
|
|
48
|
+
titleAttr.value.expression.value.trim().length > 0
|
|
49
|
+
) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: titleAttr,
|
|
52
|
+
messageId: "noRawTooltipStrings",
|
|
53
|
+
data: { content: titleAttr.value.expression.value },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Disallow raw string literals in Typography components. Use useTranslations() or the <T> component for localization.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noRawTypographyStrings:
|
|
11
|
+
"Raw string '{{content}}' in Typography component should be localized. Use t('key') or <T>key</T> instead.",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
JSXElement(node) {
|
|
18
|
+
// Check if this is a Typography element
|
|
19
|
+
if (
|
|
20
|
+
node.openingElement.name.type !== "JSXIdentifier" ||
|
|
21
|
+
node.openingElement.name.name !== "Typography"
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check all children for raw strings
|
|
27
|
+
for (const child of node.children) {
|
|
28
|
+
if (child.type === "JSXText") {
|
|
29
|
+
// Flag JSXText that contains non-whitespace characters
|
|
30
|
+
const trimmedText = child.value.trim();
|
|
31
|
+
if (trimmedText.length > 0) {
|
|
32
|
+
context.report({
|
|
33
|
+
node: child,
|
|
34
|
+
messageId: "noRawTypographyStrings",
|
|
35
|
+
data: { content: trimmedText },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
39
|
+
// Flag JSXExpressionContainer where the expression is a string literal
|
|
40
|
+
if (child.expression.type === "Literal" && typeof child.expression.value === "string") {
|
|
41
|
+
context.report({
|
|
42
|
+
node: child,
|
|
43
|
+
messageId: "noRawTypographyStrings",
|
|
44
|
+
data: { content: child.expression.value },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -17,12 +17,12 @@ export default {
|
|
|
17
17
|
create(context) {
|
|
18
18
|
return {
|
|
19
19
|
TSAsExpression(node) {
|
|
20
|
-
const typeAnnotation = node.typeAnnotation
|
|
21
|
-
if (typeAnnotation.type !== "TSTypeReference") return
|
|
22
|
-
if (typeAnnotation.typeName?.type !== "TSQualifiedName") return
|
|
20
|
+
const typeAnnotation = node.typeAnnotation;
|
|
21
|
+
if (typeAnnotation.type !== "TSTypeReference") return;
|
|
22
|
+
if (typeAnnotation.typeName?.type !== "TSQualifiedName") return;
|
|
23
23
|
|
|
24
|
-
const left = typeAnnotation.typeName.left
|
|
25
|
-
const right = typeAnnotation.typeName.right
|
|
24
|
+
const left = typeAnnotation.typeName.left;
|
|
25
|
+
const right = typeAnnotation.typeName.right;
|
|
26
26
|
|
|
27
27
|
if (
|
|
28
28
|
left.type === "Identifier" &&
|
|
@@ -33,9 +33,9 @@ export default {
|
|
|
33
33
|
context.report({
|
|
34
34
|
node,
|
|
35
35
|
messageId: "noUnsafePayloadCast",
|
|
36
|
-
})
|
|
36
|
+
});
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
}
|
|
39
|
+
};
|
|
40
40
|
},
|
|
41
|
-
}
|
|
41
|
+
};
|