@iviva/uxp-lint 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.
Files changed (85) hide show
  1. package/README.md +273 -0
  2. package/bin/uxp-lint.js +2 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +100 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/config.d.ts +4 -0
  7. package/dist/config.js +42 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/eslint/config.d.ts +2 -0
  10. package/dist/eslint/config.js +225 -0
  11. package/dist/eslint/config.js.map +1 -0
  12. package/dist/eslint/rules/event-name-format.d.ts +5 -0
  13. package/dist/eslint/rules/event-name-format.js +59 -0
  14. package/dist/eslint/rules/event-name-format.js.map +1 -0
  15. package/dist/eslint/rules/no-bad-hook-deps.d.ts +5 -0
  16. package/dist/eslint/rules/no-bad-hook-deps.js +57 -0
  17. package/dist/eslint/rules/no-bad-hook-deps.js.map +1 -0
  18. package/dist/eslint/rules/no-fa-prefix.d.ts +5 -0
  19. package/dist/eslint/rules/no-fa-prefix.js +59 -0
  20. package/dist/eslint/rules/no-fa-prefix.js.map +1 -0
  21. package/dist/eslint/rules/no-hardcoded-jsx-text.d.ts +5 -0
  22. package/dist/eslint/rules/no-hardcoded-jsx-text.js +55 -0
  23. package/dist/eslint/rules/no-hardcoded-jsx-text.js.map +1 -0
  24. package/dist/eslint/rules/no-inline-styles.d.ts +8 -0
  25. package/dist/eslint/rules/no-inline-styles.js +41 -0
  26. package/dist/eslint/rules/no-inline-styles.js.map +1 -0
  27. package/dist/eslint/rules/no-native-html-interactive.d.ts +5 -0
  28. package/dist/eslint/rules/no-native-html-interactive.js +81 -0
  29. package/dist/eslint/rules/no-native-html-interactive.js.map +1 -0
  30. package/dist/eslint/rules/require-memo.d.ts +9 -0
  31. package/dist/eslint/rules/require-memo.js +70 -0
  32. package/dist/eslint/rules/require-memo.js.map +1 -0
  33. package/dist/eslint/rules/service-config-shape.d.ts +5 -0
  34. package/dist/eslint/rules/service-config-shape.js +80 -0
  35. package/dist/eslint/rules/service-config-shape.js.map +1 -0
  36. package/dist/eslint/rules/url-params-form-state.d.ts +5 -0
  37. package/dist/eslint/rules/url-params-form-state.js +75 -0
  38. package/dist/eslint/rules/url-params-form-state.js.map +1 -0
  39. package/dist/reporter.d.ts +22 -0
  40. package/dist/reporter.js +260 -0
  41. package/dist/reporter.js.map +1 -0
  42. package/dist/rule-registry.d.ts +2 -0
  43. package/dist/rule-registry.js +46 -0
  44. package/dist/rule-registry.js.map +1 -0
  45. package/dist/rules-reader.d.ts +4 -0
  46. package/dist/rules-reader.js +16 -0
  47. package/dist/rules-reader.js.map +1 -0
  48. package/dist/runner.d.ts +7 -0
  49. package/dist/runner.js +62 -0
  50. package/dist/runner.js.map +1 -0
  51. package/dist/setup.d.ts +2 -0
  52. package/dist/setup.js +80 -0
  53. package/dist/setup.js.map +1 -0
  54. package/dist/types.d.ts +55 -0
  55. package/dist/types.js +39 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/validators/ai.d.ts +2 -0
  58. package/dist/validators/ai.js +115 -0
  59. package/dist/validators/ai.js.map +1 -0
  60. package/dist/validators/bulk-imports.d.ts +3 -0
  61. package/dist/validators/bulk-imports.js +94 -0
  62. package/dist/validators/bulk-imports.js.map +1 -0
  63. package/dist/validators/bundle-json.d.ts +3 -0
  64. package/dist/validators/bundle-json.js +130 -0
  65. package/dist/validators/bundle-json.js.map +1 -0
  66. package/dist/validators/bundle-size.d.ts +3 -0
  67. package/dist/validators/bundle-size.js +51 -0
  68. package/dist/validators/bundle-size.js.map +1 -0
  69. package/dist/validators/config-yml.d.ts +3 -0
  70. package/dist/validators/config-yml.js +148 -0
  71. package/dist/validators/config-yml.js.map +1 -0
  72. package/dist/validators/duplication.d.ts +3 -0
  73. package/dist/validators/duplication.js +65 -0
  74. package/dist/validators/duplication.js.map +1 -0
  75. package/dist/validators/folder-structure.d.ts +3 -0
  76. package/dist/validators/folder-structure.js +123 -0
  77. package/dist/validators/folder-structure.js.map +1 -0
  78. package/dist/validators/formatting.d.ts +3 -0
  79. package/dist/validators/formatting.js +97 -0
  80. package/dist/validators/formatting.js.map +1 -0
  81. package/dist/validators/scss-rules.d.ts +3 -0
  82. package/dist/validators/scss-rules.js +156 -0
  83. package/dist/validators/scss-rules.js.map +1 -0
  84. package/package.json +58 -0
  85. package/templates/prettier.config.js +11 -0
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # @iviva/uxp-lint
2
+
3
+ Linting and validation tool for iviva v5 UXP apps. Enforces the patterns established across the System, Location, and User reference apps.
4
+
5
+ ## What it checks
6
+
7
+ | Group | Rules |
8
+ |-------|-------|
9
+ | **Configuration.yml** | pageId format, nav group structure, duplicate links, otherRoutes redirects |
10
+ | **bundle.json** | label vs component key, registerUI/bundle.json ID sync, localization order |
11
+ | **Bulk Imports** | `name` attribute on root XML element |
12
+ | **Folder Structure** | one .tsx per views/ subfolder, forms in forms/, services.ts required |
13
+ | **Code Quality (ESLint)** | TypeScript recommended + React recommended + 9 custom UXP rules |
14
+ | **Formatting (Prettier)** | All .ts/.tsx/.scss files match project Prettier config |
15
+ | **SCSS Rules** | App-prefix class naming, no top-level .uxp-* overrides |
16
+ | **Bundle Size** | dist/main.js under configured limit (default 1MB) |
17
+ | **Duplicate Code** | Code clones ≥10 lines (via jscpd) |
18
+
19
+ Run `uxp-lint --list-rules` to see all 23 rules with severity. Run `uxp-lint --explain <rule>` for details on any rule.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ Install in the app you want to lint:
26
+
27
+ ```bash
28
+ npm install -D @iviva/uxp-lint
29
+ ```
30
+
31
+ Add a script to the app's `package.json`:
32
+
33
+ ```json
34
+ "scripts": {
35
+ "lint": "uxp-lint",
36
+ "lint:fix": "uxp-lint --fix",
37
+ "lint:ci": "uxp-lint --ci"
38
+ }
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Setup
44
+
45
+ On first run with no config file, an interactive setup wizard creates `.uxplintrc.json`:
46
+
47
+ ```bash
48
+ npx uxp-lint
49
+ ```
50
+
51
+ Or create `.uxplintrc.json` manually at the app root:
52
+
53
+ ```json
54
+ {
55
+ "v5": true,
56
+ "viewsSrc": "./Resources/views/src",
57
+ "configYml": "./Configuration.yml",
58
+ "bundleJson": "./Resources/views/bundle.json",
59
+ "bulkImportsDir": "./BulkImports",
60
+ "rules": {},
61
+ "bundleSize": { "maxKb": 1024, "severity": "warn" },
62
+ "duplication": { "minLines": 10, "severity": "warn" },
63
+ "ai": { "enabled": false, "model": "claude-sonnet-4-6" }
64
+ }
65
+ ```
66
+
67
+ ### App prefix (optional)
68
+
69
+ The SCSS validator auto-detects your app prefix from `global.scss`. To set it explicitly:
70
+
71
+ ```json
72
+ { "appPrefix": "ilocapp_" }
73
+ ```
74
+
75
+ ### Override rule severity
76
+
77
+ ```json
78
+ {
79
+ "rules": {
80
+ "no-hardcoded-jsx-text": "error",
81
+ "no-native-html-interactive": "off"
82
+ }
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Usage
89
+
90
+ ```bash
91
+ # Interactive run (default)
92
+ uxp-lint
93
+
94
+ # Auto-fix formatting and safe ESLint issues
95
+ uxp-lint --fix
96
+
97
+ # CI mode — exits with code 1 on any error
98
+ uxp-lint --ci
99
+
100
+ # Write full report to a file (default: uxp-lint-report.txt)
101
+ uxp-lint --report
102
+ uxp-lint --report my-report.txt
103
+
104
+ # List all rules
105
+ uxp-lint --list-rules
106
+
107
+ # Explain a specific rule
108
+ uxp-lint --explain no-direct-uxp-override
109
+
110
+ # Include AI code review (Claude API)
111
+ UXP_CLAUDE_API_KEY=sk-ant-... uxp-lint --ai
112
+ ```
113
+
114
+ ### Terminal output
115
+
116
+ Messages are grouped by file within each check group. Each file header shows the error and warning count at a glance:
117
+
118
+ ```
119
+ ── Code Quality (ESLint)
120
+
121
+ src/components/AuthManager.tsx 1 error 4 warnings
122
+ 14:14 error Component "AuthManager" should be wrapped with memo(). [uxp/require-memo]
123
+ 28:8 warn React Hook useEffect has a missing dependency: 'loadConfigs'. [react-hooks/exhaustive-deps]
124
+ ...
125
+ ```
126
+
127
+ A summary table is always printed at the end:
128
+
129
+ ```
130
+ ┌────────────────────────────────┬────────┬──────────┬────────────┐
131
+ │ Group │ Status │ Errors │ Warnings │
132
+ ├────────────────────────────────┼────────┼──────────┼────────────┤
133
+ │ Configuration.yml │ ✔ │ – │ – │
134
+ │ bundle.json │ ✔ │ – │ 1 │
135
+ │ Code Quality (ESLint) │ ✗ │ 418 │ 270 │
136
+ │ Formatting (Prettier) │ ✔ │ – │ 115 │
137
+ │ SCSS Rules │ ✗ │ 2 │ – │
138
+ ├────────────────────────────────┼────────┼──────────┼────────────┤
139
+ │ Total │ │ 420 │ 398 │
140
+ └────────────────────────────────┴────────┴──────────┴────────────┘
141
+ ```
142
+
143
+ With `--report`, per-group details are written to the file only; the terminal shows the summary table and the report path.
144
+
145
+ ---
146
+
147
+ ## Development
148
+
149
+ ### Prerequisites
150
+
151
+ - Node ≥ 18
152
+ - npm ≥ 9
153
+
154
+ ### Building
155
+
156
+ ```bash
157
+ cd /path/to/uxp-lint
158
+
159
+ # Install dependencies
160
+ npm install
161
+
162
+ # Build (compiles TypeScript to dist/)
163
+ npm run build
164
+
165
+ # Watch mode
166
+ npm run watch
167
+ ```
168
+
169
+ ### Testing against a real app
170
+
171
+ The fastest way to test is to point the CLI at a local v5 app without installing:
172
+
173
+ ```bash
174
+ # From inside the app directory (e.g. Location/5.0)
175
+ node /path/to/uxp-lint/bin/uxp-lint.js
176
+ ```
177
+
178
+ Or use a temporary config file to avoid touching the app's own `.uxplintrc.json`:
179
+
180
+ ```bash
181
+ # Write a temp config
182
+ cat > /tmp/lint-test.json << 'EOF'
183
+ {
184
+ "v5": true,
185
+ "viewsSrc": "./Resources/views/src",
186
+ "configYml": "./Configuration.yml",
187
+ "bulkImportsDir": "./BulkImports",
188
+ "rules": {},
189
+ "ai": { "enabled": false }
190
+ }
191
+ EOF
192
+
193
+ # Copy to app dir, run, then clean up
194
+ cp /tmp/lint-test.json /path/to/app/.uxplintrc.json
195
+ node /path/to/uxp-lint/bin/uxp-lint.js --ci
196
+ rm /path/to/app/.uxplintrc.json
197
+ ```
198
+
199
+ ### Testing specific features
200
+
201
+ ```bash
202
+ # Verify all 23 rules are registered
203
+ node bin/uxp-lint.js --list-rules
204
+
205
+ # Test --explain for a rule (picks any rule id from --list-rules)
206
+ node bin/uxp-lint.js --explain folder-structure
207
+ node bin/uxp-lint.js --explain no-direct-uxp-override
208
+ node bin/uxp-lint.js --explain require-memo
209
+
210
+ # Test --fix (run inside an app directory with .uxplintrc.json)
211
+ node /path/to/uxp-lint/bin/uxp-lint.js --fix
212
+ ```
213
+
214
+ ### Adding a new ESLint rule
215
+
216
+ 1. Create `src/eslint/rules/<rule-name>.ts`
217
+ 2. Export `doc: RuleDoc` and a default `Rule.RuleModule`
218
+ 3. Register in `src/eslint/config.ts` — add to `UXP_RULES` and `rules[...]`
219
+ 4. Import `doc` in `src/rule-registry.ts` and add to `ALL_RULES`
220
+ 5. Add the rule ID + default severity to `DEFAULT_CONFIG.rules` in `src/types.ts`
221
+ 6. `npm run build` — zero errors
222
+
223
+ ### Adding a new validator
224
+
225
+ 1. Create `src/validators/<name>.ts`
226
+ 2. Export `doc: RuleDoc` (or `docs: RuleDoc[]` for multiple rule IDs)
227
+ 3. Export an `async function validate<Name>(config, cwd): Promise<GroupResult>`
228
+ 4. Import and call it in `src/runner.ts`
229
+ 5. Import `doc`/`docs` in `src/rule-registry.ts` and add to `ALL_RULES`
230
+ 6. `npm run build` — zero errors
231
+
232
+ ### Project structure
233
+
234
+ ```
235
+ src/
236
+ cli.ts Entry point — parses CLI args, calls runner
237
+ runner.ts Orchestrates all validators in order
238
+ types.ts Shared interfaces (LintConfig, GroupResult, RuleDoc, …)
239
+ rule-registry.ts Collects all rule docs into ALL_RULES
240
+ rules-reader.ts Public API: loadAllRules(), findRule(), getRulesAsContext()
241
+ reporter.ts Console output formatting
242
+ setup.ts Interactive first-run config wizard
243
+ eslint/
244
+ config.ts Loads standard recommended configs + registers custom rules
245
+ rules/ 9 custom UXP/iviva ESLint rules (each exports doc + Rule.RuleModule)
246
+ validators/ File-system and static validators (each exports doc + validate fn)
247
+
248
+ bin/
249
+ uxp-lint.js Shell wrapper that calls dist/cli.js
250
+
251
+ templates/
252
+ prettier.config.js Prettier config template (copied to project on first run)
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Publishing
258
+
259
+ Publishing is automated via GitHub Actions (`.github/workflows/npm-publish.yml`). The workflow runs on every push to `master` and publishes to npm only when the version in `package.json` has changed.
260
+
261
+ **To release a new version:**
262
+
263
+ 1. Bump `version` in `package.json`
264
+ 2. Commit with a message starting with `Release`:
265
+ ```bash
266
+ git commit -m "Release 1.0.1"
267
+ git push
268
+ ```
269
+ 3. The workflow builds, publishes the package, and tags the commit `v1.0.1` automatically.
270
+
271
+ **Required GitHub secrets:**
272
+ - `NPM_AUTH_TOKEN` — npm access token with publish rights to `@iviva/uxp-lint`
273
+ - `ACTION_MONITORING_SLACK` *(optional)* — Slack webhook for build status notifications
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/cli').main();
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.main = main;
7
+ const path_1 = __importDefault(require("path"));
8
+ const commander_1 = require("commander");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const config_1 = require("./config");
11
+ const runner_1 = require("./runner");
12
+ const setup_1 = require("./setup");
13
+ const rules_reader_1 = require("./rules-reader");
14
+ const reporter_1 = require("./reporter");
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const { version } = require('../package.json');
17
+ async function main() {
18
+ var _a, _b, _c;
19
+ const program = new commander_1.Command();
20
+ program
21
+ .name('uxp-lint')
22
+ .description('Linting and validation tool for iviva v5 UXP apps')
23
+ .version(version)
24
+ .option('--fix', 'auto-fix formatting and safe ESLint issues')
25
+ .option('--ci', 'non-interactive mode — exit code 1 on any error')
26
+ .option('--ai', 'include AI-powered code review (requires Claude API key)')
27
+ .option('--report [file]', 'write full report to a file (default: uxp-lint-report.txt)')
28
+ .option('--explain <rule>', 'show documentation for a specific rule')
29
+ .option('--list-rules', 'list all rules with their severity');
30
+ program.parse(process.argv);
31
+ const opts = program.opts();
32
+ (0, reporter_1.printHeader)(version);
33
+ // --explain <rule>
34
+ if (opts.explain) {
35
+ const doc = (0, rules_reader_1.findRule)(opts.explain);
36
+ if (!doc) {
37
+ (0, reporter_1.printError)(`Rule "${opts.explain}" not found. Run --list-rules to see available rules.`);
38
+ process.exit(1);
39
+ }
40
+ console.log(chalk_1.default.bold(` Rule: ${doc.id}`));
41
+ console.log(chalk_1.default.gray(` Category: ${doc.category} | Severity: ${doc.severity}`));
42
+ console.log('');
43
+ console.log(` ${doc.description}`);
44
+ if (doc.rationale)
45
+ console.log(chalk_1.default.gray(`\n Why: ${doc.rationale}`));
46
+ if (doc.fix)
47
+ console.log(chalk_1.default.cyan(`\n Fix: ${doc.fix}`));
48
+ console.log('');
49
+ return;
50
+ }
51
+ // --list-rules
52
+ if (opts.listRules) {
53
+ const rules = (0, rules_reader_1.loadAllRules)();
54
+ if (rules.length === 0) {
55
+ (0, reporter_1.printInfo)('No rule documentation found.');
56
+ }
57
+ else {
58
+ console.log(chalk_1.default.bold(' Available rules:'));
59
+ console.log('');
60
+ for (const rule of rules) {
61
+ const sev = rule.severity === 'error' ? chalk_1.default.red(rule.severity) : chalk_1.default.yellow(rule.severity);
62
+ console.log(` ${sev.padEnd(14)} ${chalk_1.default.cyan(rule.id.padEnd(32))} ${chalk_1.default.gray(rule.description)}`);
63
+ }
64
+ }
65
+ console.log('');
66
+ return;
67
+ }
68
+ const cwd = process.cwd();
69
+ // Interactive setup if no config exists (and not in CI mode)
70
+ let config = (0, config_1.configExists)(cwd) ? (0, config_1.loadConfig)(cwd) : null;
71
+ if (!config) {
72
+ if (opts.ci) {
73
+ (0, reporter_1.printError)('No .uxplintrc.json found. Run uxp-lint (without --ci) to set up.');
74
+ process.exit(1);
75
+ }
76
+ config = await (0, setup_1.runSetup)();
77
+ (0, config_1.saveConfig)(config, cwd);
78
+ (0, reporter_1.printInfo)(`Saved .uxplintrc.json`);
79
+ console.log('');
80
+ }
81
+ // Resolve --report path: true (flag with no value) → default filename
82
+ const reportPath = opts.report
83
+ ? (typeof opts.report === 'string' ? opts.report : 'uxp-lint-report.txt')
84
+ : undefined;
85
+ try {
86
+ const { errors, reportPath: writtenPath } = await (0, runner_1.runAllValidators)(config, { fix: (_a = opts.fix) !== null && _a !== void 0 ? _a : false, ci: (_b = opts.ci) !== null && _b !== void 0 ? _b : false, ai: (_c = opts.ai) !== null && _c !== void 0 ? _c : false, report: reportPath }, cwd);
87
+ if (writtenPath) {
88
+ console.log(chalk_1.default.bold(' Report: ') + chalk_1.default.cyan(path_1.default.resolve(writtenPath)));
89
+ console.log('');
90
+ }
91
+ if (errors > 0) {
92
+ process.exit(1);
93
+ }
94
+ }
95
+ catch (e) {
96
+ (0, reporter_1.printError)(`Unexpected error: ${e}`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { LintConfig } from './types';
2
+ export declare function configExists(cwd?: string): boolean;
3
+ export declare function loadConfig(cwd?: string): LintConfig;
4
+ export declare function saveConfig(config: LintConfig, cwd?: string): void;
package/dist/config.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configExists = configExists;
7
+ exports.loadConfig = loadConfig;
8
+ exports.saveConfig = saveConfig;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const types_1 = require("./types");
12
+ const CONFIG_FILE = '.uxplintrc.json';
13
+ function configExists(cwd = process.cwd()) {
14
+ return fs_1.default.existsSync(path_1.default.join(cwd, CONFIG_FILE));
15
+ }
16
+ function loadConfig(cwd = process.cwd()) {
17
+ var _a, _b, _c, _d;
18
+ const configPath = path_1.default.join(cwd, CONFIG_FILE);
19
+ if (!fs_1.default.existsSync(configPath)) {
20
+ return { ...types_1.DEFAULT_CONFIG };
21
+ }
22
+ try {
23
+ const raw = fs_1.default.readFileSync(configPath, 'utf8');
24
+ const parsed = JSON.parse(raw);
25
+ return {
26
+ ...types_1.DEFAULT_CONFIG,
27
+ ...parsed,
28
+ rules: { ...types_1.DEFAULT_CONFIG.rules, ...((_a = parsed.rules) !== null && _a !== void 0 ? _a : {}) },
29
+ bundleSize: { ...types_1.DEFAULT_CONFIG.bundleSize, ...((_b = parsed.bundleSize) !== null && _b !== void 0 ? _b : {}) },
30
+ duplication: { ...types_1.DEFAULT_CONFIG.duplication, ...((_c = parsed.duplication) !== null && _c !== void 0 ? _c : {}) },
31
+ ai: { ...types_1.DEFAULT_CONFIG.ai, ...((_d = parsed.ai) !== null && _d !== void 0 ? _d : {}) },
32
+ };
33
+ }
34
+ catch (e) {
35
+ throw new Error(`Failed to parse ${CONFIG_FILE}: ${e}`);
36
+ }
37
+ }
38
+ function saveConfig(config, cwd = process.cwd()) {
39
+ const configPath = path_1.default.join(cwd, CONFIG_FILE);
40
+ fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
41
+ }
42
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;;;AAMA,oCAEC;AAED,gCAmBC;AAED,gCAGC;AAlCD,4CAAoB;AACpB,gDAAwB;AACxB,mCAAqD;AAErD,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,SAAgB,YAAY,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC9C,OAAO,YAAE,CAAC,UAAU,CAAC,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;;IAC5C,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC/C,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,GAAG,sBAAc,EAAE,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAwB,CAAC;QACtD,OAAO;YACL,GAAG,sBAAc;YACjB,GAAG,MAAM;YACT,KAAK,EAAE,EAAE,GAAG,sBAAc,CAAC,KAAK,EAAE,GAAG,CAAC,MAAA,MAAM,CAAC,KAAK,mCAAI,EAAE,CAAC,EAAE;YAC3D,UAAU,EAAE,EAAE,GAAG,sBAAc,CAAC,UAAU,EAAE,GAAG,CAAC,MAAA,MAAM,CAAC,UAAU,mCAAI,EAAE,CAAC,EAAE;YAC1E,WAAW,EAAE,EAAE,GAAG,sBAAc,CAAC,WAAW,EAAE,GAAG,CAAC,MAAA,MAAM,CAAC,WAAW,mCAAI,EAAE,CAAC,EAAE;YAC7E,EAAE,EAAE,EAAE,GAAG,sBAAc,CAAC,EAAE,EAAE,GAAG,CAAC,MAAA,MAAM,CAAC,EAAE,mCAAI,EAAE,CAAC,EAAE;SACnD,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,mBAAmB,WAAW,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAED,SAAgB,UAAU,CAAC,MAAkB,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAChE,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC/C,YAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { GroupResult, LintConfig } from '../types';
2
+ export declare function runEslint(config: LintConfig, cwd: string, fix: boolean): Promise<GroupResult>;
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runEslint = runEslint;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const eslint_1 = require("eslint");
10
+ // Suppress @typescript-eslint version mismatch warning — the app being linted
11
+ // may have a newer TypeScript than @typescript-eslint/parser v7 officially supports.
12
+ process.env['TYPESCRIPT_ESLINT_NO_VERSION_WARNING'] = '1';
13
+ const require_memo_1 = __importDefault(require("./rules/require-memo"));
14
+ const no_inline_styles_1 = __importDefault(require("./rules/no-inline-styles"));
15
+ const url_params_form_state_1 = __importDefault(require("./rules/url-params-form-state"));
16
+ const service_config_shape_1 = __importDefault(require("./rules/service-config-shape"));
17
+ const no_fa_prefix_1 = __importDefault(require("./rules/no-fa-prefix"));
18
+ const no_bad_hook_deps_1 = __importDefault(require("./rules/no-bad-hook-deps"));
19
+ const no_hardcoded_jsx_text_1 = __importDefault(require("./rules/no-hardcoded-jsx-text"));
20
+ const no_native_html_interactive_1 = __importDefault(require("./rules/no-native-html-interactive"));
21
+ const event_name_format_1 = __importDefault(require("./rules/event-name-format"));
22
+ // ── Custom uxp/ rules ──────────────────────────────────────────────────────
23
+ const UXP_RULES = {
24
+ 'require-memo': require_memo_1.default,
25
+ 'no-inline-styles': no_inline_styles_1.default,
26
+ 'url-params-form-state': url_params_form_state_1.default,
27
+ 'service-config-shape': service_config_shape_1.default,
28
+ 'no-fa-prefix': no_fa_prefix_1.default,
29
+ 'no-bad-hook-deps': no_bad_hook_deps_1.default,
30
+ 'no-hardcoded-jsx-text': no_hardcoded_jsx_text_1.default,
31
+ 'no-native-html-interactive': no_native_html_interactive_1.default,
32
+ 'event-name-format': event_name_format_1.default,
33
+ };
34
+ // Files excluded from linting — generated/vendor stubs
35
+ const EXCLUDED_FILES = new Set(['uxp.ts', 'uxp.d.ts', 'designer.d.ts']);
36
+ function collectSourceFiles(dir) {
37
+ if (!fs_1.default.existsSync(dir))
38
+ return [];
39
+ const results = [];
40
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
41
+ const full = path_1.default.join(dir, entry.name);
42
+ if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
43
+ results.push(...collectSourceFiles(full));
44
+ }
45
+ else if (entry.isFile() &&
46
+ (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) &&
47
+ !entry.name.endsWith('.d.ts') &&
48
+ !EXCLUDED_FILES.has(entry.name)) {
49
+ results.push(full);
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+ function sev(config, rule, fallback) {
55
+ var _a;
56
+ return (_a = config.rules[rule]) !== null && _a !== void 0 ? _a : fallback;
57
+ }
58
+ /**
59
+ * Load the recommended rule sets from each installed plugin and merge them.
60
+ * Using the Linter class (CJS-compatible) — no file-system plugin resolution needed.
61
+ */
62
+ function buildRulesConfig(config, linter) {
63
+ const rules = {};
64
+ // ── eslint:recommended ──────────────────────────────────────────────────
65
+ // The Linter class has built-in access to eslint:recommended rules via getRules().
66
+ // We apply the ones flagged as recommended.
67
+ try {
68
+ const builtinRules = linter.getRules();
69
+ builtinRules.forEach((rule, name) => {
70
+ var _a, _b;
71
+ if ((_b = (_a = rule.meta) === null || _a === void 0 ? void 0 : _a.docs) === null || _b === void 0 ? void 0 : _b.recommended) {
72
+ rules[name] = 'error';
73
+ }
74
+ });
75
+ }
76
+ catch {
77
+ // fallback: manual subset of eslint:recommended essentials
78
+ Object.assign(rules, {
79
+ 'no-debugger': 'error',
80
+ 'no-dupe-keys': 'error',
81
+ 'no-duplicate-case': 'error',
82
+ 'no-undef': 'off', // TypeScript handles this
83
+ 'no-unreachable': 'error',
84
+ 'no-unused-vars': 'off', // @typescript-eslint/no-unused-vars is used instead
85
+ });
86
+ }
87
+ // ── @typescript-eslint/recommended ─────────────────────────────────────
88
+ try {
89
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
90
+ const tsPlugin = require('@typescript-eslint/eslint-plugin');
91
+ // Register all plugin rules
92
+ for (const [name, rule] of Object.entries(tsPlugin.rules)) {
93
+ linter.defineRule(`@typescript-eslint/${name}`, rule);
94
+ }
95
+ // Apply recommended config rules
96
+ for (const [name, entry] of Object.entries(tsPlugin.configs.recommended.rules)) {
97
+ if (entry !== 'off' && entry !== 0) {
98
+ rules[name] = entry;
99
+ }
100
+ }
101
+ // @typescript-eslint/recommended disables base no-unused-vars — ensure our version is on
102
+ rules['no-unused-vars'] = 'off'; // disabled in favour of @typescript-eslint version
103
+ }
104
+ catch {
105
+ // plugin not installed
106
+ }
107
+ // ── eslint-plugin-react/recommended ────────────────────────────────────
108
+ try {
109
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
110
+ const reactPlugin = require('eslint-plugin-react');
111
+ for (const [name, rule] of Object.entries(reactPlugin.rules)) {
112
+ linter.defineRule(`react/${name}`, rule);
113
+ }
114
+ for (const [name, entry] of Object.entries(reactPlugin.configs.recommended.rules)) {
115
+ if (entry !== 'off' && entry !== 0) {
116
+ rules[name] = entry;
117
+ }
118
+ }
119
+ // We use React 18 with the new JSX transform — suppress prop-types and react-in-scope
120
+ rules['react/prop-types'] = 'off';
121
+ rules['react/react-in-jsx-scope'] = 'off';
122
+ rules['react/display-name'] = 'warn';
123
+ }
124
+ catch {
125
+ // plugin not installed
126
+ }
127
+ // ── eslint-plugin-react-hooks/recommended ──────────────────────────────
128
+ try {
129
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
130
+ const hooksPlugin = require('eslint-plugin-react-hooks');
131
+ for (const [name, rule] of Object.entries(hooksPlugin.rules)) {
132
+ linter.defineRule(`react-hooks/${name}`, rule);
133
+ }
134
+ for (const [name, entry] of Object.entries(hooksPlugin.configs.recommended.rules)) {
135
+ if (entry !== 'off' && entry !== 0) {
136
+ rules[name] = entry;
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // plugin not installed
142
+ }
143
+ // ── iviva / UXP custom rules (uxp/ prefix) ─────────────────────────────
144
+ rules['uxp/require-memo'] = sev(config, 'require-memo', 'error');
145
+ rules['uxp/no-inline-styles'] = sev(config, 'no-inline-styles', 'error');
146
+ rules['uxp/url-params-form-state'] = sev(config, 'url-params-form-state', 'error');
147
+ rules['uxp/service-config-shape'] = sev(config, 'service-config-shape', 'error');
148
+ rules['uxp/no-fa-prefix'] = sev(config, 'no-fa-prefix', 'error');
149
+ rules['uxp/no-bad-hook-deps'] = sev(config, 'no-bad-hook-deps', 'warn');
150
+ rules['uxp/no-hardcoded-jsx-text'] = sev(config, 'no-hardcoded-jsx-text', 'warn');
151
+ rules['uxp/no-native-html-interactive'] = sev(config, 'no-native-html-interactive', 'warn');
152
+ rules['uxp/event-name-format'] = sev(config, 'event-name-format', 'warn');
153
+ return rules;
154
+ }
155
+ async function runEslint(config, cwd, fix) {
156
+ var _a;
157
+ const errors = [];
158
+ const warnings = [];
159
+ const srcDir = path_1.default.resolve(cwd, config.viewsSrc);
160
+ const files = collectSourceFiles(srcDir);
161
+ if (files.length === 0) {
162
+ return {
163
+ group: 'Code Quality (ESLint)',
164
+ passed: true, errors: [], warnings: [],
165
+ skipped: true,
166
+ skipReason: `No source files found in ${config.viewsSrc}`,
167
+ };
168
+ }
169
+ const linter = new eslint_1.Linter();
170
+ // Register TypeScript parser
171
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
172
+ linter.defineParser('@typescript-eslint/parser', require('@typescript-eslint/parser'));
173
+ // Register custom rules
174
+ for (const [name, rule] of Object.entries(UXP_RULES)) {
175
+ linter.defineRule(`uxp/${name}`, rule);
176
+ }
177
+ const rules = buildRulesConfig(config, linter);
178
+ const lintConfig = {
179
+ parser: '@typescript-eslint/parser',
180
+ parserOptions: {
181
+ ecmaVersion: 2020,
182
+ sourceType: 'module',
183
+ ecmaFeatures: { jsx: true },
184
+ },
185
+ settings: {
186
+ react: { version: '18.3' },
187
+ },
188
+ env: { browser: true, es2020: true, node: true },
189
+ rules,
190
+ };
191
+ for (const filePath of files) {
192
+ const source = fs_1.default.readFileSync(filePath, 'utf8');
193
+ const relPath = path_1.default.relative(cwd, filePath);
194
+ let messages;
195
+ if (fix) {
196
+ const result = linter.verifyAndFix(source, lintConfig, { filename: filePath });
197
+ if (result.fixed)
198
+ fs_1.default.writeFileSync(filePath, result.output, 'utf8');
199
+ messages = linter.verify(result.output, lintConfig, { filename: filePath });
200
+ }
201
+ else {
202
+ messages = linter.verify(source, lintConfig, { filename: filePath });
203
+ }
204
+ for (const msg of messages) {
205
+ const item = {
206
+ file: relPath,
207
+ line: msg.line,
208
+ col: msg.column,
209
+ rule: (_a = msg.ruleId) !== null && _a !== void 0 ? _a : undefined,
210
+ message: msg.message,
211
+ };
212
+ if (msg.severity === 2)
213
+ errors.push(item);
214
+ else if (msg.severity === 1)
215
+ warnings.push(item);
216
+ }
217
+ }
218
+ return {
219
+ group: 'Code Quality (ESLint)',
220
+ passed: errors.length === 0,
221
+ errors,
222
+ warnings,
223
+ };
224
+ }
225
+ //# sourceMappingURL=config.js.map