eslint-plugin-kerfjs 4.4.0 → 4.5.0-beta.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,251 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, relative, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import process from 'node:process';
5
+
6
+ const PROFILE = '.kerf-ui-profile.json';
7
+ const cache = new Map();
8
+ const loadCommonJs = createRequire(import.meta.url);
9
+
10
+ export const UI_CONTRACT_LOAD_CODE = 'KUI-L090';
11
+
12
+ const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'));
13
+
14
+ function packageAsset(cwd, name) {
15
+ try {
16
+ return createRequire(resolve(cwd, 'package.json')).resolve(
17
+ `@kerfjs/ui/ai/${name}`,
18
+ );
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+
24
+ function discoveredProfiles(cwd, filename, settings) {
25
+ if (settings.profile)
26
+ return [
27
+ {
28
+ source: '<eslint settings kerfjs.ui.profile>',
29
+ profile: settings.profile,
30
+ },
31
+ ];
32
+ const layers = [];
33
+ const add = (path, expectedScope) => {
34
+ const profile = readJson(path);
35
+ if (profile.scope !== expectedScope)
36
+ throw new Error(
37
+ `${path} must have profile scope ${expectedScope}, received ${profile.scope ?? 'missing'}`,
38
+ );
39
+ layers.push({ source: path, profile });
40
+ };
41
+ const defaultsPath =
42
+ settings.profileDefaultsPath ??
43
+ packageAsset(cwd, 'application-ui-profile.defaults.json');
44
+ if (defaultsPath && existsSync(defaultsPath)) add(defaultsPath, 'package');
45
+ const workspace = resolve(settings.workspaceRoot ?? cwd);
46
+ const target = resolve(dirname(filename));
47
+ if (relative(workspace, target).startsWith('..')) return layers;
48
+ const directories = [];
49
+ let directory = target;
50
+ while (true) {
51
+ directories.push(directory);
52
+ if (directory === workspace) break;
53
+ directory = dirname(directory);
54
+ }
55
+ for (const candidate of directories.reverse()) {
56
+ const path = resolve(candidate, PROFILE);
57
+ if (existsSync(path))
58
+ add(path, candidate === workspace ? 'workspace' : 'directory');
59
+ }
60
+ return layers;
61
+ }
62
+
63
+ export function loadUiContract(context) {
64
+ const settings = context.settings?.kerfjs?.ui ?? {};
65
+ const cwd = settings.workspaceRoot ?? context.cwd ?? process.cwd();
66
+ const filename = context.filename ?? context.getFilename();
67
+ const key = JSON.stringify([cwd, filename, settings]);
68
+ if (cache.has(key)) return cache.get(key);
69
+ try {
70
+ const catalog =
71
+ settings.catalog ??
72
+ readJson(
73
+ settings.catalogPath ?? packageAsset(cwd, 'component-catalog-v2.json'),
74
+ );
75
+ const selection =
76
+ settings.selectionCatalog ??
77
+ readJson(
78
+ settings.selectionCatalogPath ??
79
+ packageAsset(cwd, 'component-catalog.json'),
80
+ );
81
+ const profiles = settings.profilePath
82
+ ? [
83
+ {
84
+ source: settings.profilePath,
85
+ profile: readJson(settings.profilePath),
86
+ },
87
+ ]
88
+ : discoveredProfiles(cwd, filename, settings);
89
+ const profileContractPath =
90
+ settings.profileContractPath ??
91
+ packageAsset(cwd, 'application-ui-profile-sync.cjs');
92
+ if (!profileContractPath)
93
+ throw new Error(
94
+ 'Cannot resolve @kerfjs/ui/ai/application-ui-profile-sync.cjs; install @kerfjs/ui or set kerfjs.ui.profileContractPath.',
95
+ );
96
+ const { loadApplicationUiProfileSync } = loadCommonJs(profileContractPath);
97
+ const diagnosticRegistryPath =
98
+ settings.diagnosticRegistryPath ??
99
+ resolve(
100
+ dirname(profileContractPath),
101
+ 'application-ui-diagnostic-ids-v1.json',
102
+ );
103
+ const diagnosticRegistry = readJson(diagnosticRegistryPath);
104
+ if (
105
+ diagnosticRegistry.schemaVersion !== 1 ||
106
+ !Array.isArray(diagnosticRegistry.ids)
107
+ )
108
+ throw new Error(
109
+ `${diagnosticRegistryPath} must be an application UI diagnostic registry with schemaVersion 1.`,
110
+ );
111
+ const loadedProfile = loadApplicationUiProfileSync(profiles, {
112
+ fallbackCatalogs: [catalog],
113
+ knownRules: diagnosticRegistry.ids,
114
+ });
115
+ if (loadedProfile.diagnostics.length) {
116
+ const diagnostic = loadedProfile.diagnostics[0];
117
+ throw new Error(
118
+ `${diagnostic.source} ${diagnostic.path} ${diagnostic.code}: ${diagnostic.message}`,
119
+ );
120
+ }
121
+ const profile = loadedProfile.profile;
122
+ const entries = new Map(catalog.entries.map((entry) => [entry.key, entry]));
123
+ const imports = new Map();
124
+ const exports = new Map();
125
+ const helperSources = new Map();
126
+ const addHelperSource = (name, source) => {
127
+ const sources = helperSources.get(name) ?? new Set();
128
+ sources.add(source);
129
+ helperSources.set(name, sources);
130
+ };
131
+ for (const entry of selection.entries) {
132
+ const key = `${selection.package}:${entry.id}`;
133
+ if (entry.delivery?.browserImport)
134
+ imports.set(entry.delivery.browserImport, key);
135
+ if (entry.delivery?.moduleImport)
136
+ imports.set(entry.delivery.moduleImport, key);
137
+ for (const name of entry.publicExports ?? []) {
138
+ exports.set(name, key);
139
+ addHelperSource(name, selection.package);
140
+ }
141
+ for (const wiring of entry.wiring ?? [])
142
+ addHelperSource(wiring.export, wiring.import);
143
+ }
144
+ const value = {
145
+ entries,
146
+ package: selection.package,
147
+ imports,
148
+ exports,
149
+ helperSources,
150
+ profile,
151
+ cwd,
152
+ publicClasses: new Set(
153
+ catalog.entries.flatMap((entry) => entry.boundaries.publicClasses),
154
+ ),
155
+ publicTokens: new Set(
156
+ catalog.entries.flatMap((entry) => entry.boundaries.publicTokens),
157
+ ),
158
+ error: undefined,
159
+ };
160
+ cache.set(key, value);
161
+ return value;
162
+ } catch (error) {
163
+ const value = { error: error.message };
164
+ cache.set(key, value);
165
+ return value;
166
+ }
167
+ }
168
+
169
+ export function importRegistry(program, contract) {
170
+ const locals = new Map();
171
+ const namespaces = new Map();
172
+ const helpers = new Map();
173
+ const sources = new Set();
174
+ for (const node of program.body) {
175
+ if (node.type !== 'ImportDeclaration') continue;
176
+ const source = node.source.value;
177
+ sources.add(source);
178
+ const directKey = contract.imports?.get(source);
179
+ for (const specifier of node.specifiers) {
180
+ if (specifier.type === 'ImportNamespaceSpecifier') {
181
+ namespaces.set(specifier.local.name, source);
182
+ continue;
183
+ }
184
+ const imported =
185
+ specifier.type === 'ImportDefaultSpecifier'
186
+ ? 'default'
187
+ : (specifier.imported.name ?? specifier.imported.value);
188
+ const key =
189
+ directKey ??
190
+ (source === contract.package
191
+ ? contract.exports?.get(imported)
192
+ : undefined);
193
+ if (key) locals.set(specifier.local.name, key);
194
+ helpers.set(specifier.local.name, { imported, source });
195
+ }
196
+ }
197
+ return { locals, namespaces, helpers, sources };
198
+ }
199
+
200
+ export function jsxKey(name, registry, contract) {
201
+ if (name.type === 'JSXIdentifier') return registry.locals.get(name.name);
202
+ if (
203
+ name.type === 'JSXMemberExpression' &&
204
+ name.object.type === 'JSXIdentifier' &&
205
+ name.property.type === 'JSXIdentifier' &&
206
+ registry.namespaces.has(name.object.name)
207
+ ) {
208
+ const source = registry.namespaces.get(name.object.name);
209
+ const exportedKey = contract.exports.get(name.property.name);
210
+ if (source === contract.package) return exportedKey;
211
+ if (contract.imports.get(source) === exportedKey) return exportedKey;
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ export function helperCall(callee, registry, contract) {
217
+ let imported;
218
+ let source;
219
+ if (callee.type === 'Identifier') {
220
+ ({ imported, source } = registry.helpers.get(callee.name) ?? {});
221
+ } else if (
222
+ callee.type === 'MemberExpression' &&
223
+ !callee.computed &&
224
+ callee.object.type === 'Identifier' &&
225
+ callee.property.type === 'Identifier'
226
+ ) {
227
+ imported = callee.property.name;
228
+ source = registry.namespaces.get(callee.object.name);
229
+ }
230
+ if (!imported || !source) return undefined;
231
+ return contract.helperSources.get(imported)?.has(source)
232
+ ? { imported, source }
233
+ : undefined;
234
+ }
235
+
236
+ export function isExcepted(contract, code, filename) {
237
+ const projectPath = relative(contract.cwd, filename).replaceAll('\\', '/');
238
+ return (contract.profile?.exceptions ?? []).some((exception) => {
239
+ if (!exception.rules.includes(code)) return false;
240
+ const target = exception.target.replace(/^\.\//, '').replace(/\/$/, '');
241
+ return projectPath === target || projectPath.startsWith(`${target}/`);
242
+ });
243
+ }
244
+
245
+ export const UI_RULE_SCHEMA = [
246
+ {
247
+ type: 'object',
248
+ properties: {},
249
+ additionalProperties: false,
250
+ },
251
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-kerfjs",
3
- "version": "4.4.0",
3
+ "version": "4.5.0-beta.1",
4
4
  "description": "ESLint rules that enforce kerf's hard rules — catches AI-shaped bugs at edit time.",
5
5
  "type": "module",
6
6
  "license": "MIT",