@webpieces/eslint-rules 0.0.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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ESLint rule: no-mat-cell-def
3
+ *
4
+ * Bans *matCellDef and *matHeaderCellDef in Angular HTML templates.
5
+ * New files should use the div-grid table pattern instead of mat-table.
6
+ *
7
+ * Works with @angular-eslint/template-parser AST where structural directives
8
+ * (*matCellDef) are desugared into Template nodes with templateAttrs[].
9
+ *
10
+ * NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
11
+ * It is intended for Angular HTML template files (**.html).
12
+ */
13
+
14
+ import type { Rule } from 'eslint';
15
+
16
+ // webpieces-disable no-any-unknown -- Angular template AST node interfaces
17
+ // These interfaces represent the Template node shape from @angular-eslint/template-parser.
18
+ // We define them inline since the parser is not a dependency of this plugin.
19
+ interface AngularTemplateNode {
20
+ templateAttrs?: Array<{ name: string }>;
21
+ // webpieces-disable no-any-unknown -- ESTree AST index signature
22
+ [key: string]: any;
23
+ }
24
+
25
+ const BANNED_DIRECTIVES = ['matCellDef', 'matHeaderCellDef'];
26
+
27
+ const rule: Rule.RuleModule = {
28
+ meta: {
29
+ type: 'problem',
30
+ docs: {
31
+ description: 'Ban *matCellDef and *matHeaderCellDef — use div-grid tables instead',
32
+ },
33
+ messages: {
34
+ noMatCellDef:
35
+ '*{{ directive }} is banned in new files. Use the div-grid table pattern instead. ' +
36
+ 'Div-grid tables are inherently type-safe with @for loops + strictTemplates.',
37
+ },
38
+ schema: [],
39
+ },
40
+ create(context: Rule.RuleContext): Rule.RuleListener {
41
+ return {
42
+ Template(node: AngularTemplateNode): void {
43
+ // Structural directives (*matCellDef) are desugared into Template nodes.
44
+ // The directive name appears in node.templateAttrs as either a
45
+ // BoundAttribute or TextAttribute.
46
+ const attrs = node.templateAttrs || [];
47
+ for (const attr of attrs) {
48
+ if (BANNED_DIRECTIVES.includes(attr.name)) {
49
+ context.report({
50
+ // webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
51
+ node: node as unknown as Rule.Node,
52
+ messageId: 'noMatCellDef',
53
+ data: { directive: attr.name },
54
+ });
55
+ }
56
+ }
57
+ },
58
+ };
59
+ },
60
+ };
61
+
62
+ export = rule;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * ESLint rule to discourage try-catch blocks outside test files
3
+ *
4
+ * Works alongside catch-error-pattern rule:
5
+ * - catch-error-pattern: Enforces HOW to handle exceptions (with toError())
6
+ * - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)
7
+ *
8
+ * Philosophy: Exceptions should bubble to global error handlers where they are logged
9
+ * with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.
10
+ * Local try-catch blocks break this architecture and create blind spots in production.
11
+ *
12
+ * Auto-allowed in:
13
+ * - Test files (.test.ts, .spec.ts, __tests__/)
14
+ *
15
+ * Requires eslint-disable comment in:
16
+ * - Retry loops with exponential backoff
17
+ * - Batch processing where partial failure is expected
18
+ * - Resource cleanup (with approval)
19
+ */
20
+
21
+ import type { Rule } from 'eslint';
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import { toError } from '../toError';
25
+
26
+ // webpieces-disable no-any-unknown -- ESTree AST node interface
27
+ interface TryStatementNode {
28
+ handler?: unknown;
29
+ }
30
+
31
+ /**
32
+ * Determines if a file is a test file based on naming conventions
33
+ * Test files are auto-allowed to use try-catch blocks
34
+ */
35
+ function isTestFile(filename: string): boolean {
36
+ const normalizedPath = filename.toLowerCase();
37
+
38
+ // Check file extensions
39
+ if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {
40
+ return true;
41
+ }
42
+
43
+ // Check directory names (cross-platform)
44
+ if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\__tests__\\')) {
45
+ return true;
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Finds the workspace root by walking up the directory tree
53
+ * Looks for package.json with workspaces or name === 'webpieces-ts'
54
+ */
55
+ function getWorkspaceRoot(context: Rule.RuleContext): string {
56
+ const filename = context.filename || context.getFilename();
57
+ let dir = path.dirname(filename);
58
+
59
+ // Walk up directory tree
60
+ for (let i = 0; i < 10; i++) {
61
+ const pkgPath = path.join(dir, 'package.json');
62
+ if (fs.existsSync(pkgPath)) {
63
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
64
+ try {
65
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
66
+ // Check if this is the root workspace
67
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
68
+ return dir;
69
+ }
70
+ } catch (err: unknown) {
71
+ //const error = toError(err);
72
+ void err; // Invalid JSON, keep searching
73
+ }
74
+ }
75
+
76
+ const parentDir = path.dirname(dir);
77
+ if (parentDir === dir) break; // Reached filesystem root
78
+ dir = parentDir;
79
+ }
80
+
81
+ // Fallback: return current directory
82
+ return process.cwd();
83
+ }
84
+
85
+ /**
86
+ * Ensures a documentation file exists at the given path
87
+ * Creates parent directories if needed
88
+ */
89
+ function ensureDocFile(docPath: string, content: string): boolean {
90
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
91
+ try {
92
+ const dir = path.dirname(docPath);
93
+ if (!fs.existsSync(dir)) {
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ }
96
+
97
+ // Only write if file doesn't exist or is empty
98
+ if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {
99
+ fs.writeFileSync(docPath, content, 'utf-8');
100
+ }
101
+
102
+ return true;
103
+ } catch (err: unknown) {
104
+ //const error = toError(err);
105
+ void err; // Silently fail - don't break linting if file creation fails
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Ensures the exception documentation markdown file exists
112
+ * Only creates file once per lint run using module-level flag
113
+ *
114
+ * Reads from the template file packaged with @webpieces/webpieces-rules
115
+ * and copies it to .webpieces/instruct-ai/ for AI agents to read.
116
+ */
117
+ function ensureExceptionDoc(context: Rule.RuleContext): void {
118
+ if (exceptionDocCreated) return;
119
+
120
+ const workspaceRoot = getWorkspaceRoot(context);
121
+ const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');
122
+
123
+ // Read from the template file packaged with the npm module
124
+ // Path: from eslint-plugin/rules/ -> ../../templates/
125
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');
126
+
127
+ let content: string;
128
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
129
+ try {
130
+ content = fs.readFileSync(templatePath, 'utf-8');
131
+ } catch (err: unknown) {
132
+ //const error = toError(err);
133
+ void err;
134
+ // Fallback message if template not found (shouldn't happen in published package)
135
+ content = `# Exception Documentation Not Found\n\nTemplate file not found at: ${templatePath}\n\nPlease ensure @webpieces/webpieces-rules is properly installed.`;
136
+ }
137
+
138
+ if (ensureDocFile(docPath, content)) {
139
+ exceptionDocCreated = true;
140
+ }
141
+ }
142
+
143
+ // Module-level flag to prevent redundant markdown file creation
144
+ let exceptionDocCreated = false;
145
+
146
+ // NOTE: Documentation content moved to templates/webpieces.exceptions.md
147
+ // The ensureExceptionDoc function reads from that file at runtime.
148
+
149
+ const rule: Rule.RuleModule = {
150
+ meta: {
151
+ type: 'problem',
152
+ docs: {
153
+ description: 'Discourage try-catch blocks outside test files - use global error handlers',
154
+ category: 'Best Practices',
155
+ recommended: true,
156
+ url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',
157
+ },
158
+ messages: {
159
+ noUnmanagedExceptions:
160
+ 'AI Agent: READ .webpieces/instruct-ai/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',
161
+ },
162
+ fixable: undefined,
163
+ schema: [],
164
+ },
165
+
166
+ create(context: Rule.RuleContext): Rule.RuleListener {
167
+ return {
168
+ // webpieces-disable no-any-unknown -- ESLint visitor callback parameter type
169
+ TryStatement(node: unknown): void {
170
+ // Skip try..finally blocks (no catch handler, no exception handling)
171
+ // webpieces-disable no-any-unknown -- ESTree AST node type assertion
172
+ if (!(node as TryStatementNode).handler) {
173
+ return;
174
+ }
175
+
176
+ // Auto-allow in test files
177
+ const filename = context.filename || context.getFilename();
178
+ if (isTestFile(filename)) {
179
+ return;
180
+ }
181
+
182
+ // Has catch block outside test file - report violation
183
+ ensureExceptionDoc(context);
184
+ context.report({
185
+ node: node as Rule.Node,
186
+ messageId: 'noUnmanagedExceptions',
187
+ });
188
+ },
189
+ };
190
+ },
191
+ };
192
+
193
+ export = rule;
194
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ESLint rule: require-typed-template
3
+ *
4
+ * Enforces that every <ng-template> with let- variables also has [templateClassType]
5
+ * to preserve type safety via TypedTemplateOutletDirective.
6
+ *
7
+ * Works with @angular-eslint/template-parser AST where:
8
+ * - ng-template variables (let-xxx) appear in node.variables[]
9
+ * - bound inputs ([templateClassType]) appear in node.inputs[]
10
+ * - static attributes appear in node.attributes[]
11
+ *
12
+ * NOTE: This rule only works when files are parsed with @angular-eslint/template-parser.
13
+ * It is intended for Angular HTML template files (**.html).
14
+ */
15
+
16
+ import type { Rule } from 'eslint';
17
+
18
+ // webpieces-disable no-any-unknown -- Angular template AST node interfaces
19
+ // These interfaces represent the Template node shape from @angular-eslint/template-parser.
20
+ // We define them inline since the parser is not a dependency of this plugin.
21
+ interface AngularTemplateNode {
22
+ tagName?: string;
23
+ variables?: Array<{ name: string }>;
24
+ inputs?: Array<{ name: string }>;
25
+ attributes?: Array<{ name: string }>;
26
+ // webpieces-disable no-any-unknown -- ESTree AST index signature
27
+ [key: string]: any;
28
+ }
29
+
30
+ const rule: Rule.RuleModule = {
31
+ meta: {
32
+ type: 'problem',
33
+ docs: {
34
+ description:
35
+ 'Require [templateClassType] on ng-template elements that use let- variables',
36
+ },
37
+ messages: {
38
+ missingTypedTemplate:
39
+ 'ng-template with let- variables must include ' +
40
+ '[templateClassType]="YourDtoClass" to preserve type safety. ' +
41
+ 'Fix: (1) Add [templateClassType]="YourDtoClass" to this ng-template, ' +
42
+ '(2) Add TypedTemplateOutletDirective to component imports array, ' +
43
+ '(3) Expose the DTO class: protected readonly YourDtoClass = YourDtoClass. ' +
44
+ 'See @fuse/directives/typed-template-outlet/.',
45
+ },
46
+ schema: [],
47
+ },
48
+ create(context: Rule.RuleContext): Rule.RuleListener {
49
+ return {
50
+ Template(node: AngularTemplateNode): void {
51
+ // Only match explicit <ng-template>, not desugared structural directives
52
+ // (*ngFor, *ngIf, etc.) which also produce Template AST nodes
53
+ if (node.tagName !== 'ng-template') {
54
+ return;
55
+ }
56
+
57
+ const hasLetVariables = node.variables && node.variables.length > 0;
58
+ if (!hasLetVariables) {
59
+ return;
60
+ }
61
+
62
+ const hasTemplateClassType =
63
+ (node.inputs &&
64
+ node.inputs.some((input) => input.name === 'templateClassType')) ||
65
+ (node.attributes &&
66
+ node.attributes.some((attr) => attr.name === 'templateClassType'));
67
+
68
+ if (!hasTemplateClassType) {
69
+ context.report({
70
+ // webpieces-disable no-any-unknown -- ESTree AST cast for ESLint report
71
+ node: node as unknown as Rule.Node,
72
+ messageId: 'missingTypedTemplate',
73
+ });
74
+ }
75
+ },
76
+ };
77
+ },
78
+ };
79
+
80
+ export = rule;
package/src/toError.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lightweight duplicate of @webpieces/core-util toError for use in eslint-plugin.
3
+ * eslint-plugin is Level 0 and cannot depend on core-util (also Level 0).
4
+ */
5
+ // webpieces-disable no-any-unknown -- toError intentionally accepts unknown to safely convert any thrown value to Error
6
+ export function toError(err: unknown): Error {
7
+ if (err instanceof Error) {
8
+ return err;
9
+ }
10
+
11
+ if (err && typeof err === 'object') {
12
+ if ('message' in err) {
13
+ const error = new Error(String(err.message));
14
+
15
+ if ('stack' in err && typeof err.stack === 'string') {
16
+ error.stack = err.stack;
17
+ }
18
+ if ('name' in err && typeof err.name === 'string') {
19
+ error.name = err.name;
20
+ }
21
+ return error;
22
+ }
23
+
24
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- must handle circular references without recursion
25
+ try {
26
+ return new Error(`Non-Error object thrown: ${JSON.stringify(err)}`);
27
+ } catch (err: unknown) {
28
+ //const error = toError(err);
29
+ void err;
30
+ return new Error('Non-Error object thrown (unable to stringify)');
31
+ }
32
+ }
33
+
34
+ const message = err == null ? 'Null or undefined thrown' : String(err);
35
+ return new Error(message);
36
+ }
@@ -0,0 +1,5 @@
1
+ # Exception Documentation Not Found
2
+
3
+ Template file not found at: /Users/deanhiller/workspace/personal/webpieces-ts30/packages/tooling/eslint-plugin/templates/webpieces.exceptions.md
4
+
5
+ Please ensure @webpieces/dev-config is properly installed.
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "forceConsistentCasingInFileNames": true,
6
+ "strict": true,
7
+ "noImplicitOverride": true,
8
+ "noPropertyAccessFromIndexSignature": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true
11
+ },
12
+ "files": [],
13
+ "include": [],
14
+ "references": [
15
+ {
16
+ "path": "./tsconfig.lib.json"
17
+ },
18
+ {
19
+ "path": "./tsconfig.spec.json"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/__tests__/**/*.ts"]
10
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["jest", "node"]
7
+ },
8
+ "include": [
9
+ "jest.config.ts",
10
+ "src/**/*.test.ts",
11
+ "src/**/*.spec.ts",
12
+ "src/__tests__/**/*.ts"
13
+ ]
14
+ }