eslint-plugin-kerfjs 5.0.0-beta.53 → 5.0.0-beta.54
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/docs/rules/ui-composition.md +13 -1
- package/index.js +1 -1
- package/lib/ui-contract.js +93 -7
- package/package.json +1 -1
|
@@ -11,7 +11,19 @@ resolved `.kerf-ui-profile.json` declares under `catalogs`. Their components
|
|
|
11
11
|
resolve by package subpath (`import { GroupWrap } from '@acme/bits/group-wrap'`,
|
|
12
12
|
matched to the entry's `publicExports`) or, for an application's own relative
|
|
13
13
|
imports, by source file (matched to the entry's `source`, trying the TypeScript
|
|
14
|
-
ESM extensions a `.js` specifier stands for).
|
|
14
|
+
ESM extensions a `.js` specifier stands for). An export without a `subpath` (a
|
|
15
|
+
private application's, which is never imported by package name) resolves only
|
|
16
|
+
by source file.
|
|
17
|
+
|
|
18
|
+
A relative import may also reach the source file through relative re-exports,
|
|
19
|
+
the way apps import wrappers through a components barrel:
|
|
20
|
+
`import { DemandSegmentsControl } from './components/index.js'` resolves when
|
|
21
|
+
`index.ts` has `export { DemandSegmentsControl } from '../demand-segments-control.js'`,
|
|
22
|
+
a renamed `export { X as Y } from …` (import `Y`), or `export * from …`, and
|
|
23
|
+
through chains of them (a directory import tries its `index` file). Type-only
|
|
24
|
+
and namespace (`export * as ns`) re-exports, a component defined in the barrel
|
|
25
|
+
itself, and re-exports of anything the catalogs do not declare stay unresolved,
|
|
26
|
+
and cycles stop without resolving.
|
|
15
27
|
|
|
16
28
|
An entry that declares `rendersAs` (a wrapper that renders one of the listed
|
|
17
29
|
cataloged roots, or nothing) is checked as those roots:
|
package/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import uiPublicBoundaries from './lib/rules/ui-public-boundaries.js';
|
|
|
13
13
|
import uiWiring from './lib/rules/ui-wiring.js';
|
|
14
14
|
|
|
15
15
|
const plugin = {
|
|
16
|
-
meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.
|
|
16
|
+
meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.54' },
|
|
17
17
|
rules: {
|
|
18
18
|
'no-inline-jsx-event-handlers': noInlineJsxEventHandlers,
|
|
19
19
|
'no-raw-with-dynamic-arg': noRawWithDynamicArg,
|
package/lib/ui-contract.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, relative, resolve } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import process from 'node:process';
|
|
@@ -201,11 +201,15 @@ export function loadUiContract(context) {
|
|
|
201
201
|
const name = exportName(item);
|
|
202
202
|
if (!name || !isComponentExport(name)) continue;
|
|
203
203
|
const subpath = typeof item === 'string' ? '.' : item.subpath;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
// A private application's export has no subpath: it is never
|
|
205
|
+
// imported by package name, only through its source file below.
|
|
206
|
+
if (typeof subpath === 'string') {
|
|
207
|
+
const specifier =
|
|
208
|
+
subpath !== '.'
|
|
209
|
+
? `${entry.package ?? artifact.package}/${subpath.replace(/^\.\//, '')}`
|
|
210
|
+
: (entry.package ?? artifact.package);
|
|
211
|
+
packageExports.set(`${specifier}\0${name}`, key);
|
|
212
|
+
}
|
|
209
213
|
if (entry.source)
|
|
210
214
|
sourceExports.set(`${resolve(root, entry.source)}\0${name}`, key);
|
|
211
215
|
}
|
|
@@ -296,13 +300,95 @@ function declaredComponentKey(contract, source, imported, filename) {
|
|
|
296
300
|
if (byPackage) return byPackage;
|
|
297
301
|
if (!filename || !source.startsWith('.') || !contract.sourceExports?.size)
|
|
298
302
|
return undefined;
|
|
299
|
-
|
|
303
|
+
return sourceComponentKey(contract, filename, source, imported, new Set());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// A relative import names a cataloged component by its defining source file,
|
|
307
|
+
// or through relative re-exports (`export { X } from './x.js'`, `export * from
|
|
308
|
+
// './x.js'`, chains of either) that reach one: apps commonly import wrappers
|
|
309
|
+
// through a components barrel. Anything else — a bare re-export, a local
|
|
310
|
+
// definition, a file that cannot be read — stays unresolved.
|
|
311
|
+
const MAX_REEXPORT_VISITS = 64;
|
|
312
|
+
function sourceComponentKey(contract, fromFile, specifier, imported, seen) {
|
|
313
|
+
const candidates = relativeImportCandidates(fromFile, specifier);
|
|
314
|
+
for (const candidate of candidates) {
|
|
300
315
|
const key = contract.sourceExports.get(`${candidate}\0${imported}`);
|
|
301
316
|
if (key) return key;
|
|
302
317
|
}
|
|
318
|
+
// `seen` holds each (file, name) already followed: it breaks re-export
|
|
319
|
+
// cycles and bounds the work a pathological barrel graph can cost.
|
|
320
|
+
if (seen.size >= MAX_REEXPORT_VISITS) return undefined;
|
|
321
|
+
for (const candidate of candidates) {
|
|
322
|
+
const reExports = relativeReExports(candidate);
|
|
323
|
+
if (!reExports) continue;
|
|
324
|
+
const visit = `${candidate}\0${imported}`;
|
|
325
|
+
if (seen.has(visit)) return undefined;
|
|
326
|
+
seen.add(visit);
|
|
327
|
+
for (const { name, local, from } of reExports) {
|
|
328
|
+
if (name !== undefined && name !== imported) continue;
|
|
329
|
+
const key = sourceComponentKey(
|
|
330
|
+
contract,
|
|
331
|
+
candidate,
|
|
332
|
+
from,
|
|
333
|
+
name === undefined ? imported : local,
|
|
334
|
+
seen,
|
|
335
|
+
);
|
|
336
|
+
if (key) return key;
|
|
337
|
+
}
|
|
338
|
+
// The first file that exists is the one the import resolves to.
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
303
341
|
return undefined;
|
|
304
342
|
}
|
|
305
343
|
|
|
344
|
+
// The relative value re-exports of one module file, read from its source text:
|
|
345
|
+
// `{ name, local, from }` for `export { local as name } from`, and
|
|
346
|
+
// `{ from }` (every name) for `export * from`. Type-only re-exports and
|
|
347
|
+
// namespace re-exports (`export * as ns`) are skipped. Cached by modification
|
|
348
|
+
// time, so a long-lived editor session sees barrel edits. Undefined when the
|
|
349
|
+
// file does not exist.
|
|
350
|
+
const reExportCache = new Map();
|
|
351
|
+
function relativeReExports(file) {
|
|
352
|
+
let mtime;
|
|
353
|
+
try {
|
|
354
|
+
const stats = statSync(file);
|
|
355
|
+
if (!stats.isFile()) return undefined;
|
|
356
|
+
mtime = stats.mtimeMs;
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const cached = reExportCache.get(file);
|
|
361
|
+
if (cached?.mtime === mtime) return cached.reExports;
|
|
362
|
+
let text;
|
|
363
|
+
try {
|
|
364
|
+
text = readFileSync(file, 'utf8');
|
|
365
|
+
} catch {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const code = text
|
|
369
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
370
|
+
.replace(/(^|[^:\\])\/\/[^\n]*/g, '$1');
|
|
371
|
+
const reExports = [];
|
|
372
|
+
const named = /\bexport\s+(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\3/g;
|
|
373
|
+
for (const match of code.matchAll(named)) {
|
|
374
|
+
const [, typeOnly, list, , from] = match;
|
|
375
|
+
if (typeOnly || !from.startsWith('.')) continue;
|
|
376
|
+
for (const raw of list.split(',')) {
|
|
377
|
+
const part = raw.trim();
|
|
378
|
+
if (!part || /^type\s/.test(part)) continue;
|
|
379
|
+
const [local, name = local] = part.split(/\s+as\s+/).map((x) => x.trim());
|
|
380
|
+
reExports.push({ name, local, from });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const star = /\bexport\s+\*\s+from\s*(['"])([^'"]+)\1/g;
|
|
384
|
+
for (const match of code.matchAll(star)) {
|
|
385
|
+
const from = match[2];
|
|
386
|
+
if (from.startsWith('.')) reExports.push({ from });
|
|
387
|
+
}
|
|
388
|
+
reExportCache.set(file, { mtime, reExports });
|
|
389
|
+
return reExports;
|
|
390
|
+
}
|
|
391
|
+
|
|
306
392
|
export function jsxKey(name, registry, contract) {
|
|
307
393
|
if (name.type === 'JSXIdentifier') return registry.locals.get(name.name);
|
|
308
394
|
if (
|