eslint-plugin-kerfjs 5.0.0-beta.52 → 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 +43 -1
- package/index.js +1 -1
- package/lib/rules/ui-composition.js +47 -12
- package/lib/rules/ui-css-values.js +1 -1
- package/lib/rules/ui-preferences.js +1 -1
- package/lib/rules/ui-wiring.js +1 -1
- package/lib/ui-contract.js +181 -4
- package/package.json +1 -1
|
@@ -2,4 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
Enforces composition facts published by the versioned UI catalog. `KUI-L201` reports a catalog component under a known, disallowed direct parent. `KUI-L202` reports a statically visible zone child—including an intrinsic or unknown JSX element—that is not one of the zone's cataloged `accepts` entries. `KUI-L203` reports a statically provable zone cardinality violation. A zone participates only when it declares an explicit `jsx.prop` binding, including `children`; the rule never guesses that a zone id is a JSX prop. Toolbar, Pane, TabBar, AppTab, feedback, search, and layout validation is therefore catalog-driven rather than duplicated in rule-local component cases.
|
|
4
4
|
|
|
5
|
-
The parent check is intentionally conservative: it
|
|
5
|
+
The parent check is intentionally conservative about the child: it runs only for a child that resolves to a cataloged entry (or a declared wrapper's roots) whose parents are `listed`. Its direct JSX parent may be any element the rule can see: a cataloged component, a declared wrapper (which counts as each of its roots), an intrinsic element such as `<div>`, or an unresolved component, and any parent that is not a listed parent reports `KUI-L201`. Bound zone checks understand aliases, namespace imports, fragments, arrays, and conditional/logical branches, but expressions whose content cannot be resolved statically remain runtime and accessibility work. Accepted concepts that do not identify a catalog entry remain machine-readable guidance while the rule enforces their statically provable cardinality only. No autofix is offered because wrapping or moving UI changes structure and behavior.
|
|
6
|
+
|
|
7
|
+
## Application and third-party wrappers
|
|
8
|
+
|
|
9
|
+
Beyond `@kerfjs/ui`'s own catalog, the rule loads every composition catalog the
|
|
10
|
+
resolved `.kerf-ui-profile.json` declares under `catalogs`. Their components
|
|
11
|
+
resolve by package subpath (`import { GroupWrap } from '@acme/bits/group-wrap'`,
|
|
12
|
+
matched to the entry's `publicExports`) or, for an application's own relative
|
|
13
|
+
imports, by source file (matched to the entry's `source`, trying the TypeScript
|
|
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.
|
|
27
|
+
|
|
28
|
+
An entry that declares `rendersAs` (a wrapper that renders one of the listed
|
|
29
|
+
cataloged roots, or nothing) is checked as those roots:
|
|
30
|
+
|
|
31
|
+
- **Zones (`KUI-L202`):** the zone must accept every declared root.
|
|
32
|
+
- **Parents (`KUI-L201`):** each root's listed parents apply where the wrapper
|
|
33
|
+
is placed, and a wrapper used as a parent counts as each of its roots.
|
|
34
|
+
- **Cardinality (`KUI-L203`):** the wrapper counts as zero or one child, since
|
|
35
|
+
it may render nothing.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Toolbar } from '@kerfjs/ui';
|
|
39
|
+
import { DemandSegmentsControl } from './demand-segments-control.js';
|
|
40
|
+
|
|
41
|
+
// Valid when the app's catalog declares
|
|
42
|
+
// rendersAs: ["@kerfjs/ui:toolbar-control-group"].
|
|
43
|
+
<Toolbar trailing={<DemandSegmentsControl />} />;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A wrapper without `rendersAs`, or one the profile's catalogs do not declare,
|
|
47
|
+
is still an unknown element and reports `KUI-L202` in a cataloged zone.
|
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,
|
|
@@ -35,6 +35,14 @@ function zoneShape(node, registry, contract) {
|
|
|
35
35
|
if (!node || node.type === 'JSXEmptyExpression') return emptyShape();
|
|
36
36
|
if (node.type === 'JSXElement') {
|
|
37
37
|
const key = jsxKey(node.openingElement.name, registry, contract);
|
|
38
|
+
const roots = contract.entries.get(key)?.rendersAs;
|
|
39
|
+
// A declared wrapper renders one of its roots, or nothing.
|
|
40
|
+
if (roots?.length)
|
|
41
|
+
return {
|
|
42
|
+
min: 0,
|
|
43
|
+
max: 1,
|
|
44
|
+
items: [{ key, roots, label: `${key} (renders ${roots.join(' | ')})` }],
|
|
45
|
+
};
|
|
38
46
|
return {
|
|
39
47
|
min: 1,
|
|
40
48
|
max: 1,
|
|
@@ -94,6 +102,11 @@ function directParentKey(node, registry, contract) {
|
|
|
94
102
|
: undefined;
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
// The cataloged roots an element stands for: a declared wrapper's `rendersAs`,
|
|
106
|
+
// otherwise the element's own entry.
|
|
107
|
+
const rootsOf = (key, contract) =>
|
|
108
|
+
contract.entries.get(key)?.rendersAs ?? [key];
|
|
109
|
+
|
|
97
110
|
function boundZone(openingElement, zone, registry, contract) {
|
|
98
111
|
const prop = zone.jsx?.prop;
|
|
99
112
|
if (!prop) return undefined;
|
|
@@ -146,24 +159,42 @@ export default {
|
|
|
146
159
|
messageId: 'config',
|
|
147
160
|
data: { error: contract.error },
|
|
148
161
|
});
|
|
149
|
-
else registry = importRegistry(node, contract);
|
|
162
|
+
else registry = importRegistry(node, contract, filename);
|
|
150
163
|
},
|
|
151
164
|
JSXOpeningElement(node) {
|
|
152
165
|
if (!registry) return;
|
|
153
166
|
const key = jsxKey(node.name, registry, contract);
|
|
154
167
|
const entry = contract.entries.get(key);
|
|
155
168
|
if (!entry) return;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
// A wrapper answers to its own parent contract and to each root's.
|
|
170
|
+
const placed = [
|
|
171
|
+
{ entry, child: key },
|
|
172
|
+
...(entry.rendersAs ?? []).flatMap((root) => {
|
|
173
|
+
const rootEntry = contract.entries.get(root);
|
|
174
|
+
return rootEntry
|
|
175
|
+
? [{ entry: rootEntry, child: `${key} (renders ${root})` }]
|
|
176
|
+
: [];
|
|
177
|
+
}),
|
|
178
|
+
].filter(
|
|
179
|
+
({ entry: placedEntry }) => placedEntry.parents?.mode === 'listed',
|
|
180
|
+
);
|
|
181
|
+
if (placed.length && !isExcepted(contract, PARENT_CODE, filename)) {
|
|
160
182
|
const parent = directParentKey(node, registry, contract);
|
|
161
|
-
if (parent
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
183
|
+
if (parent)
|
|
184
|
+
for (const { entry: placedEntry, child } of placed)
|
|
185
|
+
if (
|
|
186
|
+
!rootsOf(parent.key, contract).every((root) =>
|
|
187
|
+
placedEntry.parents.entries.includes(root),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
context.report({
|
|
191
|
+
node: node.name,
|
|
192
|
+
messageId: 'parent',
|
|
193
|
+
data: {
|
|
194
|
+
child,
|
|
195
|
+
parents: placedEntry.parents.entries.join(', '),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
167
198
|
}
|
|
168
199
|
for (const zone of entry.zones ?? []) {
|
|
169
200
|
const bound = boundZone(node, zone, registry, contract);
|
|
@@ -180,7 +211,11 @@ export default {
|
|
|
180
211
|
);
|
|
181
212
|
if (allowed.size && !isExcepted(contract, ZONE_CODE, filename))
|
|
182
213
|
for (const child of shape.items)
|
|
183
|
-
if (
|
|
214
|
+
if (
|
|
215
|
+
child.roots
|
|
216
|
+
? !child.roots.every((root) => allowed.has(root))
|
|
217
|
+
: !child.key || !allowed.has(child.key)
|
|
218
|
+
)
|
|
184
219
|
context.report({
|
|
185
220
|
node: reportNode,
|
|
186
221
|
messageId: 'zone',
|
|
@@ -206,7 +206,7 @@ export default {
|
|
|
206
206
|
messageId: 'config',
|
|
207
207
|
data: { error: loaded.error },
|
|
208
208
|
});
|
|
209
|
-
else registry = importRegistry(node, loaded);
|
|
209
|
+
else registry = importRegistry(node, loaded, filename);
|
|
210
210
|
},
|
|
211
211
|
JSXOpeningElement(node) {
|
|
212
212
|
if (!registry) return;
|
|
@@ -35,7 +35,7 @@ export default {
|
|
|
35
35
|
messageId: 'config',
|
|
36
36
|
data: { error: contract.error },
|
|
37
37
|
});
|
|
38
|
-
else registry = importRegistry(node, contract);
|
|
38
|
+
else registry = importRegistry(node, contract, filename);
|
|
39
39
|
},
|
|
40
40
|
JSXOpeningElement(node) {
|
|
41
41
|
if (!registry || isExcepted(contract, CODE, filename)) return;
|
package/lib/rules/ui-wiring.js
CHANGED
package/lib/ui-contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, relative, resolve } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, extname, relative, resolve } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
|
|
@@ -25,6 +25,57 @@ export function isComponentExport(name) {
|
|
|
25
25
|
|
|
26
26
|
const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'));
|
|
27
27
|
|
|
28
|
+
// The directory whose package.json names `packageName`, searched upward from a
|
|
29
|
+
// catalog file; a catalog's `source` paths are relative to that package root.
|
|
30
|
+
function packageRootOf(start, packageName) {
|
|
31
|
+
for (let directory = dirname(start); ; directory = dirname(directory)) {
|
|
32
|
+
const manifest = resolve(directory, 'package.json');
|
|
33
|
+
if (existsSync(manifest)) {
|
|
34
|
+
try {
|
|
35
|
+
if (readJson(manifest).name === packageName) return directory;
|
|
36
|
+
} catch {
|
|
37
|
+
// An unreadable manifest is not the owner; keep searching.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (dirname(directory) === directory) return dirname(start);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const exportName = (item) => (typeof item === 'string' ? item : item?.name);
|
|
45
|
+
|
|
46
|
+
// Application and third-party composition catalogs a profile declares, beyond
|
|
47
|
+
// the @kerfjs/ui catalog the rules load directly. Their entries join the
|
|
48
|
+
// contract, and their components resolve by package subpath (a bare import)
|
|
49
|
+
// or by source file (an app's relative import).
|
|
50
|
+
function profileCatalogs(loadedProfile, basePackage) {
|
|
51
|
+
const catalogs = [];
|
|
52
|
+
for (const catalog of loadedProfile.profile?.catalogs ?? []) {
|
|
53
|
+
if (catalog.package === basePackage) continue;
|
|
54
|
+
const owner = loadedProfile.provenance?.[`$catalogs.${catalog.package}`];
|
|
55
|
+
if (!owner || !catalog.composition?.path || owner.startsWith('<')) continue;
|
|
56
|
+
const path = resolve(dirname(owner), catalog.composition.path);
|
|
57
|
+
const artifact = readJson(path);
|
|
58
|
+
catalogs.push({ artifact, root: packageRootOf(path, catalog.package) });
|
|
59
|
+
}
|
|
60
|
+
return catalogs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The files a relative import specifier may name, in the order TypeScript's
|
|
64
|
+
// ESM resolution tries them (a `.js` specifier also names its `.ts`/`.tsx`).
|
|
65
|
+
const SCRIPT_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.mjs'];
|
|
66
|
+
export function relativeImportCandidates(fromFile, specifier) {
|
|
67
|
+
const base = resolve(dirname(fromFile), specifier);
|
|
68
|
+
const extension = extname(base);
|
|
69
|
+
const stem = SCRIPT_EXTENSIONS.includes(extension)
|
|
70
|
+
? base.slice(0, -extension.length)
|
|
71
|
+
: base;
|
|
72
|
+
return [
|
|
73
|
+
base,
|
|
74
|
+
...SCRIPT_EXTENSIONS.map((candidate) => `${stem}${candidate}`),
|
|
75
|
+
...SCRIPT_EXTENSIONS.map((candidate) => resolve(base, `index${candidate}`)),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
function packageAsset(cwd, name) {
|
|
29
80
|
try {
|
|
30
81
|
return createRequire(resolve(cwd, 'package.json')).resolve(
|
|
@@ -134,6 +185,35 @@ export function loadUiContract(context) {
|
|
|
134
185
|
}
|
|
135
186
|
const profile = loadedProfile.profile;
|
|
136
187
|
const entries = new Map(catalog.entries.map((entry) => [entry.key, entry]));
|
|
188
|
+
// Declared application/third-party components: `packageExports` maps
|
|
189
|
+
// `<package or package/subpath>\0<Export>` and `sourceExports` maps
|
|
190
|
+
// `<absolute source file>\0<Export>` to the entry key.
|
|
191
|
+
const packageExports = new Map();
|
|
192
|
+
const sourceExports = new Map();
|
|
193
|
+
for (const { artifact, root } of profileCatalogs(
|
|
194
|
+
loadedProfile,
|
|
195
|
+
selection.package,
|
|
196
|
+
))
|
|
197
|
+
for (const entry of artifact.entries ?? []) {
|
|
198
|
+
const key = entry.key ?? `${artifact.package}:${entry.id}`;
|
|
199
|
+
entries.set(key, entry);
|
|
200
|
+
for (const item of entry.publicExports ?? []) {
|
|
201
|
+
const name = exportName(item);
|
|
202
|
+
if (!name || !isComponentExport(name)) continue;
|
|
203
|
+
const subpath = typeof item === 'string' ? '.' : item.subpath;
|
|
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
|
+
}
|
|
213
|
+
if (entry.source)
|
|
214
|
+
sourceExports.set(`${resolve(root, entry.source)}\0${name}`, key);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
137
217
|
const imports = new Map();
|
|
138
218
|
const exports = new Map();
|
|
139
219
|
const helperSources = new Map();
|
|
@@ -160,6 +240,8 @@ export function loadUiContract(context) {
|
|
|
160
240
|
package: selection.package,
|
|
161
241
|
imports,
|
|
162
242
|
exports,
|
|
243
|
+
packageExports,
|
|
244
|
+
sourceExports,
|
|
163
245
|
helperSources,
|
|
164
246
|
profile,
|
|
165
247
|
cwd,
|
|
@@ -180,7 +262,7 @@ export function loadUiContract(context) {
|
|
|
180
262
|
}
|
|
181
263
|
}
|
|
182
264
|
|
|
183
|
-
export function importRegistry(program, contract) {
|
|
265
|
+
export function importRegistry(program, contract, filename) {
|
|
184
266
|
const locals = new Map();
|
|
185
267
|
const namespaces = new Map();
|
|
186
268
|
const helpers = new Map();
|
|
@@ -204,7 +286,7 @@ export function importRegistry(program, contract) {
|
|
|
204
286
|
: (directKey ??
|
|
205
287
|
(source === contract.package
|
|
206
288
|
? contract.exports?.get(imported)
|
|
207
|
-
:
|
|
289
|
+
: declaredComponentKey(contract, source, imported, filename)));
|
|
208
290
|
if (key) locals.set(specifier.local.name, key);
|
|
209
291
|
helpers.set(specifier.local.name, { imported, source });
|
|
210
292
|
}
|
|
@@ -212,6 +294,101 @@ export function importRegistry(program, contract) {
|
|
|
212
294
|
return { locals, namespaces, helpers, sources };
|
|
213
295
|
}
|
|
214
296
|
|
|
297
|
+
// An application or third-party component declared in a profile catalog.
|
|
298
|
+
function declaredComponentKey(contract, source, imported, filename) {
|
|
299
|
+
const byPackage = contract.packageExports?.get(`${source}\0${imported}`);
|
|
300
|
+
if (byPackage) return byPackage;
|
|
301
|
+
if (!filename || !source.startsWith('.') || !contract.sourceExports?.size)
|
|
302
|
+
return undefined;
|
|
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) {
|
|
315
|
+
const key = contract.sourceExports.get(`${candidate}\0${imported}`);
|
|
316
|
+
if (key) return key;
|
|
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
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
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
|
+
|
|
215
392
|
export function jsxKey(name, registry, contract) {
|
|
216
393
|
if (name.type === 'JSXIdentifier') return registry.locals.get(name.name);
|
|
217
394
|
if (
|