@webpieces/rules-config 0.3.262 → 0.3.271
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 +1 -1
- package/src/WebpiecesRulesConfig.d.ts +3 -3
- package/src/WebpiecesRulesConfig.js +2 -2
- package/src/WebpiecesRulesConfig.js.map +1 -1
- package/src/commands-config.js +1 -1
- package/src/commands-config.js.map +1 -1
- package/src/constants.d.ts +0 -1
- package/src/constants.js +0 -1
- package/src/constants.js.map +1 -1
- package/src/default-rules.js +2 -2
- package/src/default-rules.js.map +1 -1
- package/src/index.d.ts +1 -2
- package/src/index.js +4 -10
- package/src/index.js.map +1 -1
- package/src/load-config.js +3 -1
- package/src/load-config.js.map +1 -1
- package/src/rule-configs.d.ts +6 -8
- package/src/rule-configs.js +12 -21
- package/src/rule-configs.js.map +1 -1
- package/src/sections.js +1 -1
- package/src/sections.js.map +1 -1
- package/src/validate-config.js +2 -2
- package/src/validate-config.js.map +1 -1
- package/templates/webpieces.git-workflow.md +19 -14
- package/src/controller-naming-config.d.ts +0 -22
- package/src/controller-naming-config.js +0 -189
- package/src/controller-naming-config.js.map +0 -1
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ControllerNamingViolation = void 0;
|
|
4
|
-
exports.isControllerNamingAllowedPath = isControllerNamingAllowedPath;
|
|
5
|
-
exports.toKebabCase = toKebabCase;
|
|
6
|
-
exports.findControllerNamingViolations = findControllerNamingViolations;
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// enforce-controller-naming — pure detector shared by BOTH engines (ai-hook-rules edit-time,
|
|
9
|
-
// code-rules build-time), mirroring findMatchRuleViolations in match-rules-config.ts.
|
|
10
|
-
//
|
|
11
|
-
// Every class whose heritage ends in `*Api` (`extends XxxApi` / `implements XxxApi`) must DECLARE
|
|
12
|
-
// ITS INTENT with one of two decorators:
|
|
13
|
-
// - `@Controller` → it IS a controller, and must then be named `{Something}Controller`
|
|
14
|
-
// AND live in a lower-case kebab file `{something}-controller.ts`.
|
|
15
|
-
// - `@NotController` → it is deliberately NOT a controller (a simulator/client/test double),
|
|
16
|
-
// and is exempt from the naming rules.
|
|
17
|
-
// A class implementing `*Api` with NEITHER decorator is a violation ("declare your intent").
|
|
18
|
-
// Any `@Controller`-decorated class is also naming-checked even without `*Api` heritage.
|
|
19
|
-
//
|
|
20
|
-
// The kebab file name is the real deliverable: a separate controller-discovery tool globs
|
|
21
|
-
// `**/*-controller.ts`, which only works if every controller file follows the convention.
|
|
22
|
-
//
|
|
23
|
-
// `allowedPaths` still exempts whole dirs (e.g. generated code); test files are always exempt.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
/** One naming violation. `message` differs per kind (class-name vs file-name) — callers surface it. */
|
|
26
|
-
class ControllerNamingViolation {
|
|
27
|
-
line;
|
|
28
|
-
context;
|
|
29
|
-
message;
|
|
30
|
-
constructor(line, context, message) {
|
|
31
|
-
this.line = line;
|
|
32
|
-
this.context = context;
|
|
33
|
-
this.message = message;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
exports.ControllerNamingViolation = ControllerNamingViolation;
|
|
37
|
-
const CONTROLLER_SUFFIX = 'Controller';
|
|
38
|
-
const TEST_PATHS = [/\.test\.ts$/, /\.spec\.ts$/, /__tests__\//];
|
|
39
|
-
// Glob → RegExp, matching the convention used by no-symbol-di-tokens / match-rules (`**` spans path
|
|
40
|
-
// separators, `*` stays within a segment). Kept local so rules-config owns no shared glob util.
|
|
41
|
-
function globToRegex(pattern) {
|
|
42
|
-
let re = '';
|
|
43
|
-
let i = 0;
|
|
44
|
-
while (i < pattern.length) {
|
|
45
|
-
const ch = pattern[i];
|
|
46
|
-
if (ch === '*') {
|
|
47
|
-
if (pattern[i + 1] === '*') {
|
|
48
|
-
re += '.*';
|
|
49
|
-
i += 2;
|
|
50
|
-
if (pattern[i] === '/')
|
|
51
|
-
i += 1;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
re += '[^/]*';
|
|
55
|
-
i += 1;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (ch === '?') {
|
|
59
|
-
re += '[^/]';
|
|
60
|
-
i += 1;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if ('.+^$(){}|[]\\'.includes(ch)) {
|
|
64
|
-
re += '\\' + ch;
|
|
65
|
-
i += 1;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
re += ch;
|
|
69
|
-
i += 1;
|
|
70
|
-
}
|
|
71
|
-
return new RegExp('^' + re + '$');
|
|
72
|
-
}
|
|
73
|
-
/** A file is exempt if it's a test file or matches one of the rule's allowedPaths globs. */
|
|
74
|
-
function isControllerNamingAllowedPath(relativePath, allowedPaths) {
|
|
75
|
-
if (TEST_PATHS.some((re) => re.test(relativePath)))
|
|
76
|
-
return true;
|
|
77
|
-
return allowedPaths.some((pattern) => globToRegex(pattern).test(relativePath));
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* PascalCase → kebab-case. Inserts a `-` before an uppercase letter that follows a lower-case
|
|
81
|
-
* letter or a digit, then lower-cases everything. `SaveController` → `save-controller`,
|
|
82
|
-
* `Server2Controller` → `server2-controller`, `UserAccountController` → `user-account-controller`.
|
|
83
|
-
*/
|
|
84
|
-
function toKebabCase(name) {
|
|
85
|
-
let out = '';
|
|
86
|
-
for (let i = 0; i < name.length; i += 1) {
|
|
87
|
-
const ch = name[i] ?? '';
|
|
88
|
-
const prev = name[i - 1];
|
|
89
|
-
if (i > 0 && /[A-Z]/.test(ch) && prev !== undefined && /[a-z0-9]/.test(prev)) {
|
|
90
|
-
out += '-';
|
|
91
|
-
}
|
|
92
|
-
out += ch.toLowerCase();
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
const CLASS_DECL = /(?:^|\s)(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/;
|
|
97
|
-
// The heritage segment (everything after the class name, up to the opening `{`) declares a
|
|
98
|
-
// controller contract when it `extends`/`implements` an identifier ending in `Api`.
|
|
99
|
-
function heritageMentionsApi(segment) {
|
|
100
|
-
return /\b(?:extends|implements)\b/.test(segment) && /\b[A-Za-z_$][\w$]*Api\b/.test(segment);
|
|
101
|
-
}
|
|
102
|
-
/** Which intent decorators sit directly above a class declaration. */
|
|
103
|
-
class ClassDecorators {
|
|
104
|
-
controller;
|
|
105
|
-
notController;
|
|
106
|
-
constructor(controller, notController) {
|
|
107
|
-
this.controller = controller;
|
|
108
|
-
this.notController = notController;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// Walk UP from the class line over the decorator/blank lines that precede it, collecting the
|
|
112
|
-
// `@Controller` / `@NotController` intent markers. Stops at the first real (non-decorator, non-blank)
|
|
113
|
-
// line. `@NotController` is matched first so `@Controller`'s \b test can't swallow it.
|
|
114
|
-
function scanClassDecorators(strippedLines, classLineIndex) {
|
|
115
|
-
let controller = false;
|
|
116
|
-
let notController = false;
|
|
117
|
-
for (let j = classLineIndex - 1; j >= 0; j -= 1) {
|
|
118
|
-
const s = (strippedLines[j] ?? '').trim();
|
|
119
|
-
if (s === '')
|
|
120
|
-
continue;
|
|
121
|
-
if (!s.startsWith('@'))
|
|
122
|
-
break;
|
|
123
|
-
if (/@NotController\b/.test(s))
|
|
124
|
-
notController = true;
|
|
125
|
-
else if (/@Controller\b/.test(s))
|
|
126
|
-
controller = true;
|
|
127
|
-
}
|
|
128
|
-
return new ClassDecorators(controller, notController);
|
|
129
|
-
}
|
|
130
|
-
// Collect the heritage text of the class declared on `strippedLines[classLineIndex]`. Heritage may
|
|
131
|
-
// wrap onto following lines, so we accumulate until the opening `{` (bounded to a few lines).
|
|
132
|
-
function collectHeritage(strippedLines, classLineIndex, afterName) {
|
|
133
|
-
let combined = afterName;
|
|
134
|
-
let k = classLineIndex;
|
|
135
|
-
while (!combined.includes('{') && k - classLineIndex < 6 && k + 1 < strippedLines.length) {
|
|
136
|
-
k += 1;
|
|
137
|
-
combined += ' ' + (strippedLines[k] ?? '');
|
|
138
|
-
}
|
|
139
|
-
return combined.split('{')[0] ?? combined;
|
|
140
|
-
}
|
|
141
|
-
function baseName(relativePath) {
|
|
142
|
-
const segments = relativePath.split('/');
|
|
143
|
-
return segments[segments.length - 1] ?? relativePath;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Pure engine. Scans the (comment/string-stripped) lines for controller classes and returns raw
|
|
147
|
-
* naming violations — NO disable filtering (ai-hook applies isLineDisabled; code-rules applies
|
|
148
|
-
* hasDisable). Returns [] for exempt paths (test files / allowedPaths).
|
|
149
|
-
*/
|
|
150
|
-
function findControllerNamingViolations(strippedLines, relativePath, config) {
|
|
151
|
-
if (isControllerNamingAllowedPath(relativePath, config.allowedPaths ?? []))
|
|
152
|
-
return [];
|
|
153
|
-
const violations = [];
|
|
154
|
-
const base = baseName(relativePath);
|
|
155
|
-
for (let i = 0; i < strippedLines.length; i += 1) {
|
|
156
|
-
const line = strippedLines[i] ?? '';
|
|
157
|
-
const match = CLASS_DECL.exec(line);
|
|
158
|
-
if (!match)
|
|
159
|
-
continue;
|
|
160
|
-
const className = match[1] ?? '';
|
|
161
|
-
const afterName = line.slice((match.index ?? 0) + match[0].length);
|
|
162
|
-
const heritage = collectHeritage(strippedLines, i, afterName);
|
|
163
|
-
const context = line.trim();
|
|
164
|
-
const decorators = scanClassDecorators(strippedLines, i);
|
|
165
|
-
const implementsApi = heritageMentionsApi(heritage);
|
|
166
|
-
// Not a controller and not an *Api implementer → nothing to enforce here.
|
|
167
|
-
if (!decorators.controller && !implementsApi)
|
|
168
|
-
continue;
|
|
169
|
-
// Implements *Api but explicitly opted out of being a controller → exempt.
|
|
170
|
-
if (!decorators.controller && decorators.notController)
|
|
171
|
-
continue;
|
|
172
|
-
if (!decorators.controller) {
|
|
173
|
-
// Implements *Api but declared no intent (no @Controller / @NotController).
|
|
174
|
-
violations.push(new ControllerNamingViolation(i + 1, context, `Class "${className}" implements/extends an *Api contract, so it must declare its intent: add @Controller (then name it "{Something}${CONTROLLER_SUFFIX}" in a "{something}-controller.ts" file) OR add @NotController if it is deliberately not a controller.`));
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
// @Controller-decorated → enforce the naming convention.
|
|
178
|
-
if (!className.endsWith(CONTROLLER_SUFFIX)) {
|
|
179
|
-
violations.push(new ControllerNamingViolation(i + 1, context, `Controller class "${className}" must be named "{Something}${CONTROLLER_SUFFIX}" (its name must end in "${CONTROLLER_SUFFIX}").`));
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const expected = `${toKebabCase(className)}.ts`;
|
|
183
|
-
if (base !== expected) {
|
|
184
|
-
violations.push(new ControllerNamingViolation(i + 1, context, `Controller file for class "${className}" must be named "${expected}" (lower-case kebab), but the file is "${base}". The controller-discovery tool finds controllers by globbing **/*-controller.ts.`));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return violations;
|
|
188
|
-
}
|
|
189
|
-
//# sourceMappingURL=controller-naming-config.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"controller-naming-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/controller-naming-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { EnforceControllerNamingConfig } from './rule-configs';\n\n// ---------------------------------------------------------------------------\n// enforce-controller-naming — pure detector shared by BOTH engines (ai-hook-rules edit-time,\n// code-rules build-time), mirroring findMatchRuleViolations in match-rules-config.ts.\n//\n// Every class whose heritage ends in `*Api` (`extends XxxApi` / `implements XxxApi`) must DECLARE\n// ITS INTENT with one of two decorators:\n// - `@Controller` → it IS a controller, and must then be named `{Something}Controller`\n// AND live in a lower-case kebab file `{something}-controller.ts`.\n// - `@NotController` → it is deliberately NOT a controller (a simulator/client/test double),\n// and is exempt from the naming rules.\n// A class implementing `*Api` with NEITHER decorator is a violation (\"declare your intent\").\n// Any `@Controller`-decorated class is also naming-checked even without `*Api` heritage.\n//\n// The kebab file name is the real deliverable: a separate controller-discovery tool globs\n// `**/*-controller.ts`, which only works if every controller file follows the convention.\n//\n// `allowedPaths` still exempts whole dirs (e.g. generated code); test files are always exempt.\n// ---------------------------------------------------------------------------\n\n/** One naming violation. `message` differs per kind (class-name vs file-name) — callers surface it. */\nexport class ControllerNamingViolation {\n readonly line: number;\n readonly context: string;\n readonly message: string;\n\n constructor(line: number, context: string, message: string) {\n this.line = line;\n this.context = context;\n this.message = message;\n }\n}\n\nconst CONTROLLER_SUFFIX = 'Controller';\n\nconst TEST_PATHS: readonly RegExp[] = [/\\.test\\.ts$/, /\\.spec\\.ts$/, /__tests__\\//];\n\n// Glob → RegExp, matching the convention used by no-symbol-di-tokens / match-rules (`**` spans path\n// separators, `*` stays within a segment). Kept local so rules-config owns no shared glob util.\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n\n/** A file is exempt if it's a test file or matches one of the rule's allowedPaths globs. */\nexport function isControllerNamingAllowedPath(relativePath: string, allowedPaths: readonly string[]): boolean {\n if (TEST_PATHS.some((re: RegExp) => re.test(relativePath))) return true;\n return allowedPaths.some((pattern: string) => globToRegex(pattern).test(relativePath));\n}\n\n/**\n * PascalCase → kebab-case. Inserts a `-` before an uppercase letter that follows a lower-case\n * letter or a digit, then lower-cases everything. `SaveController` → `save-controller`,\n * `Server2Controller` → `server2-controller`, `UserAccountController` → `user-account-controller`.\n */\nexport function toKebabCase(name: string): string {\n let out = '';\n for (let i = 0; i < name.length; i += 1) {\n const ch = name[i] ?? '';\n const prev = name[i - 1];\n if (i > 0 && /[A-Z]/.test(ch) && prev !== undefined && /[a-z0-9]/.test(prev)) {\n out += '-';\n }\n out += ch.toLowerCase();\n }\n return out;\n}\n\nconst CLASS_DECL = /(?:^|\\s)(?:export\\s+)?(?:default\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/;\n\n// The heritage segment (everything after the class name, up to the opening `{`) declares a\n// controller contract when it `extends`/`implements` an identifier ending in `Api`.\nfunction heritageMentionsApi(segment: string): boolean {\n return /\\b(?:extends|implements)\\b/.test(segment) && /\\b[A-Za-z_$][\\w$]*Api\\b/.test(segment);\n}\n\n/** Which intent decorators sit directly above a class declaration. */\nclass ClassDecorators {\n readonly controller: boolean;\n readonly notController: boolean;\n\n constructor(controller: boolean, notController: boolean) {\n this.controller = controller;\n this.notController = notController;\n }\n}\n\n// Walk UP from the class line over the decorator/blank lines that precede it, collecting the\n// `@Controller` / `@NotController` intent markers. Stops at the first real (non-decorator, non-blank)\n// line. `@NotController` is matched first so `@Controller`'s \\b test can't swallow it.\nfunction scanClassDecorators(strippedLines: readonly string[], classLineIndex: number): ClassDecorators {\n let controller = false;\n let notController = false;\n for (let j = classLineIndex - 1; j >= 0; j -= 1) {\n const s = (strippedLines[j] ?? '').trim();\n if (s === '') continue;\n if (!s.startsWith('@')) break;\n if (/@NotController\\b/.test(s)) notController = true;\n else if (/@Controller\\b/.test(s)) controller = true;\n }\n return new ClassDecorators(controller, notController);\n}\n\n// Collect the heritage text of the class declared on `strippedLines[classLineIndex]`. Heritage may\n// wrap onto following lines, so we accumulate until the opening `{` (bounded to a few lines).\nfunction collectHeritage(strippedLines: readonly string[], classLineIndex: number, afterName: string): string {\n let combined = afterName;\n let k = classLineIndex;\n while (!combined.includes('{') && k - classLineIndex < 6 && k + 1 < strippedLines.length) {\n k += 1;\n combined += ' ' + (strippedLines[k] ?? '');\n }\n return combined.split('{')[0] ?? combined;\n}\n\nfunction baseName(relativePath: string): string {\n const segments = relativePath.split('/');\n return segments[segments.length - 1] ?? relativePath;\n}\n\n/**\n * Pure engine. Scans the (comment/string-stripped) lines for controller classes and returns raw\n * naming violations — NO disable filtering (ai-hook applies isLineDisabled; code-rules applies\n * hasDisable). Returns [] for exempt paths (test files / allowedPaths).\n */\nexport function findControllerNamingViolations(\n strippedLines: readonly string[],\n relativePath: string,\n config: EnforceControllerNamingConfig,\n): ControllerNamingViolation[] {\n if (isControllerNamingAllowedPath(relativePath, config.allowedPaths ?? [])) return [];\n\n const violations: ControllerNamingViolation[] = [];\n const base = baseName(relativePath);\n\n for (let i = 0; i < strippedLines.length; i += 1) {\n const line = strippedLines[i] ?? '';\n const match = CLASS_DECL.exec(line);\n if (!match) continue;\n\n const className = match[1] ?? '';\n const afterName = line.slice((match.index ?? 0) + match[0].length);\n const heritage = collectHeritage(strippedLines, i, afterName);\n const context = line.trim();\n\n const decorators = scanClassDecorators(strippedLines, i);\n const implementsApi = heritageMentionsApi(heritage);\n\n // Not a controller and not an *Api implementer → nothing to enforce here.\n if (!decorators.controller && !implementsApi) continue;\n // Implements *Api but explicitly opted out of being a controller → exempt.\n if (!decorators.controller && decorators.notController) continue;\n\n if (!decorators.controller) {\n // Implements *Api but declared no intent (no @Controller / @NotController).\n violations.push(new ControllerNamingViolation(\n i + 1,\n context,\n `Class \"${className}\" implements/extends an *Api contract, so it must declare its intent: add @Controller (then name it \"{Something}${CONTROLLER_SUFFIX}\" in a \"{something}-controller.ts\" file) OR add @NotController if it is deliberately not a controller.`,\n ));\n continue;\n }\n\n // @Controller-decorated → enforce the naming convention.\n if (!className.endsWith(CONTROLLER_SUFFIX)) {\n violations.push(new ControllerNamingViolation(\n i + 1,\n context,\n `Controller class \"${className}\" must be named \"{Something}${CONTROLLER_SUFFIX}\" (its name must end in \"${CONTROLLER_SUFFIX}\").`,\n ));\n continue;\n }\n\n const expected = `${toKebabCase(className)}.ts`;\n if (base !== expected) {\n violations.push(new ControllerNamingViolation(\n i + 1,\n context,\n `Controller file for class \"${className}\" must be named \"${expected}\" (lower-case kebab), but the file is \"${base}\". The controller-discovery tool finds controllers by globbing **/*-controller.ts.`,\n ));\n }\n }\n\n return violations;\n}\n"]}
|