@hustle-together/api-dev-tools 3.12.3 → 4.5.1
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/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Plugin for Zod Schema Linting
|
|
3
|
+
*
|
|
4
|
+
* Enforces best practices for Zod schemas:
|
|
5
|
+
* - require-description: All schemas should have .describe()
|
|
6
|
+
* - consistent-naming: Keys should follow naming convention
|
|
7
|
+
* - require-error-message: String validations should have error messages
|
|
8
|
+
* - no-unsafe-defaults: Avoid empty/zero defaults
|
|
9
|
+
* - prefer-strict: Objects should use .strict()
|
|
10
|
+
*
|
|
11
|
+
* Installation:
|
|
12
|
+
* 1. Copy this to your project's eslint-plugin-zod-schema/index.js
|
|
13
|
+
* 2. Add to eslint.config.js (see bottom of file)
|
|
14
|
+
*
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
* @see docs/SCHEMA-LINT.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Helper: Check if node is a Zod method call
|
|
20
|
+
function isZodCall(node) {
|
|
21
|
+
if (node.type !== 'CallExpression') return false;
|
|
22
|
+
|
|
23
|
+
// Check for z.string(), z.object(), etc.
|
|
24
|
+
if (
|
|
25
|
+
node.callee.type === 'MemberExpression' &&
|
|
26
|
+
node.callee.object.name === 'z'
|
|
27
|
+
) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for chained calls like z.string().email()
|
|
32
|
+
if (
|
|
33
|
+
node.callee.type === 'MemberExpression' &&
|
|
34
|
+
node.callee.object.type === 'CallExpression'
|
|
35
|
+
) {
|
|
36
|
+
return isZodCall(node.callee.object);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Helper: Check if chain has .describe()
|
|
43
|
+
function hasDescribe(node) {
|
|
44
|
+
let current = node;
|
|
45
|
+
while (current) {
|
|
46
|
+
if (
|
|
47
|
+
current.type === 'CallExpression' &&
|
|
48
|
+
current.callee.type === 'MemberExpression' &&
|
|
49
|
+
current.callee.property.name === 'describe'
|
|
50
|
+
) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Move up the chain
|
|
54
|
+
if (current.callee && current.callee.object) {
|
|
55
|
+
current = current.callee.object;
|
|
56
|
+
} else {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Helper: Check naming convention
|
|
64
|
+
function matchesCase(name, caseType) {
|
|
65
|
+
if (caseType === 'camelCase') {
|
|
66
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(name);
|
|
67
|
+
}
|
|
68
|
+
if (caseType === 'snake_case') {
|
|
69
|
+
return /^[a-z][a-z0-9_]*$/.test(name);
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Helper: Check if node is inside z.object()
|
|
75
|
+
function isInsideZodObject(node, context) {
|
|
76
|
+
const ancestors = context.getAncestors();
|
|
77
|
+
return ancestors.some(
|
|
78
|
+
(ancestor) =>
|
|
79
|
+
ancestor.type === 'CallExpression' &&
|
|
80
|
+
ancestor.callee.type === 'MemberExpression' &&
|
|
81
|
+
ancestor.callee.object.name === 'z' &&
|
|
82
|
+
ancestor.callee.property.name === 'object'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Helper: Check if string validation has error message
|
|
87
|
+
function hasErrorMessage(node) {
|
|
88
|
+
// Check for { message: '...' } in validation call
|
|
89
|
+
if (node.arguments && node.arguments.length > 0) {
|
|
90
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
91
|
+
if (lastArg.type === 'ObjectExpression') {
|
|
92
|
+
return lastArg.properties.some(
|
|
93
|
+
(prop) => prop.key && prop.key.name === 'message'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
// Some validations accept message as string directly
|
|
97
|
+
if (lastArg.type === 'Literal' && typeof lastArg.value === 'string') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Helper: Check for unsafe defaults
|
|
105
|
+
function isUnsafeDefault(node) {
|
|
106
|
+
if (
|
|
107
|
+
node.callee.type === 'MemberExpression' &&
|
|
108
|
+
node.callee.property.name === 'default'
|
|
109
|
+
) {
|
|
110
|
+
const defaultArg = node.arguments[0];
|
|
111
|
+
if (!defaultArg) return true; // No argument is unsafe
|
|
112
|
+
|
|
113
|
+
// Empty string
|
|
114
|
+
if (defaultArg.type === 'Literal' && defaultArg.value === '') {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// Zero for numbers (might be intentional, but warn)
|
|
118
|
+
if (defaultArg.type === 'Literal' && defaultArg.value === 0) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// null
|
|
122
|
+
if (defaultArg.type === 'Literal' && defaultArg.value === null) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
// Empty array
|
|
126
|
+
if (
|
|
127
|
+
defaultArg.type === 'ArrayExpression' &&
|
|
128
|
+
defaultArg.elements.length === 0
|
|
129
|
+
) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Empty object
|
|
133
|
+
if (
|
|
134
|
+
defaultArg.type === 'ObjectExpression' &&
|
|
135
|
+
defaultArg.properties.length === 0
|
|
136
|
+
) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// String validation methods that should have error messages
|
|
144
|
+
const STRING_VALIDATIONS = [
|
|
145
|
+
'email',
|
|
146
|
+
'url',
|
|
147
|
+
'uuid',
|
|
148
|
+
'cuid',
|
|
149
|
+
'regex',
|
|
150
|
+
'min',
|
|
151
|
+
'max',
|
|
152
|
+
'length',
|
|
153
|
+
'startsWith',
|
|
154
|
+
'endsWith',
|
|
155
|
+
'includes',
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
meta: {
|
|
160
|
+
name: 'eslint-plugin-zod-schema',
|
|
161
|
+
version: '1.0.0',
|
|
162
|
+
},
|
|
163
|
+
rules: {
|
|
164
|
+
/**
|
|
165
|
+
* Require .describe() on all Zod schemas
|
|
166
|
+
*/
|
|
167
|
+
'require-description': {
|
|
168
|
+
meta: {
|
|
169
|
+
type: 'suggestion',
|
|
170
|
+
docs: {
|
|
171
|
+
description: 'Require .describe() on Zod schemas for documentation',
|
|
172
|
+
category: 'Best Practices',
|
|
173
|
+
recommended: true,
|
|
174
|
+
},
|
|
175
|
+
messages: {
|
|
176
|
+
missingDescribe:
|
|
177
|
+
'Zod schema should have .describe() for documentation and OpenAPI generation',
|
|
178
|
+
},
|
|
179
|
+
schema: [],
|
|
180
|
+
},
|
|
181
|
+
create(context) {
|
|
182
|
+
return {
|
|
183
|
+
VariableDeclarator(node) {
|
|
184
|
+
// Only check schemas (variables ending with Schema or containing schema)
|
|
185
|
+
const varName = node.id.name || '';
|
|
186
|
+
if (
|
|
187
|
+
!varName.toLowerCase().includes('schema') &&
|
|
188
|
+
!varName.endsWith('Schema')
|
|
189
|
+
) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (node.init && isZodCall(node.init) && !hasDescribe(node.init)) {
|
|
194
|
+
context.report({
|
|
195
|
+
node: node.init,
|
|
196
|
+
messageId: 'missingDescribe',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Enforce consistent naming in z.object() keys
|
|
206
|
+
*/
|
|
207
|
+
'consistent-naming': {
|
|
208
|
+
meta: {
|
|
209
|
+
type: 'problem',
|
|
210
|
+
docs: {
|
|
211
|
+
description: 'Enforce consistent naming convention in schema keys',
|
|
212
|
+
category: 'Stylistic Issues',
|
|
213
|
+
recommended: true,
|
|
214
|
+
},
|
|
215
|
+
messages: {
|
|
216
|
+
inconsistentCase: "Schema key '{{name}}' should be {{case}}",
|
|
217
|
+
},
|
|
218
|
+
schema: [
|
|
219
|
+
{
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
case: {
|
|
223
|
+
enum: ['camelCase', 'snake_case'],
|
|
224
|
+
default: 'camelCase',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
create(context) {
|
|
232
|
+
const options = context.options[0] || {};
|
|
233
|
+
const caseType = options.case || 'camelCase';
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
Property(node) {
|
|
237
|
+
// Only check inside z.object()
|
|
238
|
+
if (!isInsideZodObject(node, context)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const keyName = node.key.name || node.key.value;
|
|
243
|
+
if (keyName && !matchesCase(keyName, caseType)) {
|
|
244
|
+
context.report({
|
|
245
|
+
node: node.key,
|
|
246
|
+
messageId: 'inconsistentCase',
|
|
247
|
+
data: {
|
|
248
|
+
name: keyName,
|
|
249
|
+
case: caseType,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Require custom error messages on string validations
|
|
260
|
+
*/
|
|
261
|
+
'require-error-message': {
|
|
262
|
+
meta: {
|
|
263
|
+
type: 'suggestion',
|
|
264
|
+
docs: {
|
|
265
|
+
description:
|
|
266
|
+
'Require custom error messages on string validation methods',
|
|
267
|
+
category: 'Best Practices',
|
|
268
|
+
recommended: false,
|
|
269
|
+
},
|
|
270
|
+
messages: {
|
|
271
|
+
missingErrorMessage:
|
|
272
|
+
"String validation '{{method}}' should have a custom error message",
|
|
273
|
+
},
|
|
274
|
+
schema: [],
|
|
275
|
+
},
|
|
276
|
+
create(context) {
|
|
277
|
+
return {
|
|
278
|
+
CallExpression(node) {
|
|
279
|
+
if (
|
|
280
|
+
node.callee.type === 'MemberExpression' &&
|
|
281
|
+
STRING_VALIDATIONS.includes(node.callee.property.name) &&
|
|
282
|
+
isZodCall(node)
|
|
283
|
+
) {
|
|
284
|
+
if (!hasErrorMessage(node)) {
|
|
285
|
+
context.report({
|
|
286
|
+
node,
|
|
287
|
+
messageId: 'missingErrorMessage',
|
|
288
|
+
data: {
|
|
289
|
+
method: node.callee.property.name,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Warn about potentially unsafe default values
|
|
301
|
+
*/
|
|
302
|
+
'no-unsafe-defaults': {
|
|
303
|
+
meta: {
|
|
304
|
+
type: 'suggestion',
|
|
305
|
+
docs: {
|
|
306
|
+
description:
|
|
307
|
+
'Warn about empty/zero default values that might cause issues',
|
|
308
|
+
category: 'Best Practices',
|
|
309
|
+
recommended: true,
|
|
310
|
+
},
|
|
311
|
+
messages: {
|
|
312
|
+
unsafeDefault:
|
|
313
|
+
'Default value might be unsafe. Empty strings, zeros, nulls, and empty arrays/objects can cause issues. Use .optional() instead or provide a meaningful default.',
|
|
314
|
+
},
|
|
315
|
+
schema: [],
|
|
316
|
+
},
|
|
317
|
+
create(context) {
|
|
318
|
+
return {
|
|
319
|
+
CallExpression(node) {
|
|
320
|
+
if (isZodCall(node) && isUnsafeDefault(node)) {
|
|
321
|
+
context.report({
|
|
322
|
+
node,
|
|
323
|
+
messageId: 'unsafeDefault',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Prefer .strict() on z.object() to prevent extra properties
|
|
333
|
+
*/
|
|
334
|
+
'prefer-strict': {
|
|
335
|
+
meta: {
|
|
336
|
+
type: 'suggestion',
|
|
337
|
+
docs: {
|
|
338
|
+
description:
|
|
339
|
+
'Prefer .strict() on z.object() to reject extra properties',
|
|
340
|
+
category: 'Best Practices',
|
|
341
|
+
recommended: false,
|
|
342
|
+
},
|
|
343
|
+
messages: {
|
|
344
|
+
preferStrict:
|
|
345
|
+
'Consider using .strict() on z.object() to reject extra properties and prevent data leaks',
|
|
346
|
+
},
|
|
347
|
+
schema: [],
|
|
348
|
+
},
|
|
349
|
+
create(context) {
|
|
350
|
+
return {
|
|
351
|
+
CallExpression(node) {
|
|
352
|
+
// Check for z.object() without .strict()
|
|
353
|
+
if (
|
|
354
|
+
node.callee.type === 'MemberExpression' &&
|
|
355
|
+
node.callee.object.name === 'z' &&
|
|
356
|
+
node.callee.property.name === 'object'
|
|
357
|
+
) {
|
|
358
|
+
// Check if parent chain includes .strict()
|
|
359
|
+
const parent = node.parent;
|
|
360
|
+
if (
|
|
361
|
+
parent &&
|
|
362
|
+
parent.type === 'MemberExpression' &&
|
|
363
|
+
parent.property.name === 'strict'
|
|
364
|
+
) {
|
|
365
|
+
return; // Has .strict()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check full chain
|
|
369
|
+
let current = node.parent;
|
|
370
|
+
while (current) {
|
|
371
|
+
if (
|
|
372
|
+
current.type === 'CallExpression' &&
|
|
373
|
+
current.callee.type === 'MemberExpression' &&
|
|
374
|
+
current.callee.property.name === 'strict'
|
|
375
|
+
) {
|
|
376
|
+
return; // Has .strict() somewhere in chain
|
|
377
|
+
}
|
|
378
|
+
current = current.parent;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
context.report({
|
|
382
|
+
node,
|
|
383
|
+
messageId: 'preferStrict',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
// Recommended config
|
|
393
|
+
configs: {
|
|
394
|
+
recommended: {
|
|
395
|
+
plugins: ['zod-schema'],
|
|
396
|
+
rules: {
|
|
397
|
+
'zod-schema/require-description': 'warn',
|
|
398
|
+
'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
|
|
399
|
+
'zod-schema/no-unsafe-defaults': 'warn',
|
|
400
|
+
'zod-schema/require-error-message': 'off',
|
|
401
|
+
'zod-schema/prefer-strict': 'off',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
strict: {
|
|
405
|
+
plugins: ['zod-schema'],
|
|
406
|
+
rules: {
|
|
407
|
+
'zod-schema/require-description': 'error',
|
|
408
|
+
'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
|
|
409
|
+
'zod-schema/no-unsafe-defaults': 'error',
|
|
410
|
+
'zod-schema/require-error-message': 'warn',
|
|
411
|
+
'zod-schema/prefer-strict': 'warn',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/*
|
|
418
|
+
* USAGE IN eslint.config.js:
|
|
419
|
+
*
|
|
420
|
+
* import zodSchemaPlugin from './eslint-plugin-zod-schema';
|
|
421
|
+
*
|
|
422
|
+
* export default [
|
|
423
|
+
* {
|
|
424
|
+
* files: ['**\/*.schema.ts', '**\/*.schemas.ts'],
|
|
425
|
+
* plugins: {
|
|
426
|
+
* 'zod-schema': zodSchemaPlugin,
|
|
427
|
+
* },
|
|
428
|
+
* rules: {
|
|
429
|
+
* 'zod-schema/require-description': 'warn',
|
|
430
|
+
* 'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
|
|
431
|
+
* 'zod-schema/no-unsafe-defaults': 'warn',
|
|
432
|
+
* },
|
|
433
|
+
* },
|
|
434
|
+
* ];
|
|
435
|
+
*
|
|
436
|
+
* Or use the recommended config:
|
|
437
|
+
*
|
|
438
|
+
* import zodSchemaPlugin from './eslint-plugin-zod-schema';
|
|
439
|
+
*
|
|
440
|
+
* export default [
|
|
441
|
+
* {
|
|
442
|
+
* files: ['**\/*.schema.ts'],
|
|
443
|
+
* ...zodSchemaPlugin.configs.recommended,
|
|
444
|
+
* },
|
|
445
|
+
* ];
|
|
446
|
+
*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-zod-schema",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint plugin for Zod schema best practices",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"eslint",
|
|
8
|
+
"eslintplugin",
|
|
9
|
+
"eslint-plugin",
|
|
10
|
+
"zod",
|
|
11
|
+
"schema",
|
|
12
|
+
"validation"
|
|
13
|
+
],
|
|
14
|
+
"author": "Hustle Together",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"eslint": ">=8.0.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/hustle-together/api-dev-tools"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|