@zgeoff/oxlint-config 0.2.10 → 0.2.12
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/README.md +25 -10
- package/import-zones-schema.js +2 -0
- package/import-zones.js +24 -1
- package/load-workspace-manifests.js +64 -0
- package/load-zone-options.js +2 -0
- package/no-abbreviated-names.js +133 -21
- package/package.json +2 -1
- package/resolve-import-target.js +68 -18
package/README.md
CHANGED
|
@@ -109,11 +109,19 @@ the paths that may name the words:
|
|
|
109
109
|
|
|
110
110
|
## `zgeoff/no-abbreviated-names`
|
|
111
111
|
|
|
112
|
-
The `names` option maps each abbreviation to the text that replaces it, and the rule reports
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
The `names` option maps each abbreviation to the text that replaces it, and the rule reports every
|
|
113
|
+
value name that has one of those names. A value name is any name that a declaration, an import, an
|
|
114
|
+
export alias, a key or a member write binds, so function, class and method names count beside
|
|
115
|
+
variables and properties. A key counts in each of its spellings, so `src`, `'src'`, `['src']` and
|
|
116
|
+
`#src` all report. A name matches whole, so `ms` reports `ms` but not `timeoutMs`.
|
|
117
|
+
|
|
118
|
+
The rule leaves alone the names that hold no value of their own. A destructured key that an alias
|
|
119
|
+
renames belongs to the object being read, so the rule reports only the alias. A computed key that
|
|
120
|
+
reads a variable, such as `[src]`, names no property, and the rule checks the variable at its
|
|
121
|
+
declaration. Type names, type-only imports and exports, labels and quoted module names hold no
|
|
122
|
+
value, so the rule skips them.
|
|
123
|
+
|
|
124
|
+
The shipped config lists the abbreviations that the code-style skill bans and turns on `id-length`
|
|
117
125
|
beside the rule, so a one-letter name fails too, other than `x`, `y`, `z`, `$` and `_`.
|
|
118
126
|
|
|
119
127
|
## `zgeoff/no-file-snapshot`
|
|
@@ -134,19 +142,26 @@ Each entry has a `name`, which starts its report, an optional `reason`, which en
|
|
|
134
142
|
|
|
135
143
|
- `edges` judge an import that resolves to a file: relative, with any bundler query such as `?url`
|
|
136
144
|
dropped, or the package's own name resolved through its `exports` under `conditions` plus
|
|
137
|
-
`default`.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
`default`. They also judge the name of each package whose directory a `workspaces` glob matches,
|
|
146
|
+
such as `packages/*`, with one directory level per segment. That name maps to the package folder
|
|
147
|
+
joined with the subpath, never to an export's target: `@demo/store/testing` is judged as
|
|
148
|
+
`packages/store/testing`, so a zone names the folder path, such as `packages/store{,/**}`. A
|
|
149
|
+
target in a `to` zone fails unless it is also in an `allow` zone; with `allow` alone, a target in
|
|
150
|
+
none of the `allow` zones fails, a target in no zone included. `allowTypeOnly` exempts type-only
|
|
151
|
+
imports (`true`, or globs the target must match), and `allowWithin: "src/*"` exempts an import
|
|
152
|
+
inside the importer's own `src/<name>/`.
|
|
141
153
|
- `modules` restrict a package or builtin (`node:` prefix optional, subpaths included) to the
|
|
142
154
|
`allow` zones; `allowTypeOnly: true` exempts type-only imports. A name that ends in `*` restricts
|
|
143
155
|
every specifier that starts with the text before it, such as `@scope/*`, and `node:*` restricts
|
|
144
156
|
every builtin.
|
|
145
157
|
- `globals` restrict a call to a global, bare or through `globalThis`, `self` or `window`.
|
|
146
158
|
- `unzoned` reports a file in a `from` zone that belongs to none of `zones`.
|
|
147
|
-
- `unresolved` reports a relative or self-referencing import that resolves to no file,
|
|
159
|
+
- `unresolved` reports a relative or self-referencing import that resolves to no file, a workspace
|
|
160
|
+
import whose subpath no `exports` key lists, or one that a `null` key blocks, and a dynamic
|
|
148
161
|
`import()`, `require()` or `import.meta.require()` whose specifier is computed. Bun's
|
|
149
162
|
`import.meta.require` used as a value counts as computed too.
|
|
163
|
+
- `selfImports` reports an import of the importer's own package by name, which a relative path
|
|
164
|
+
reaches without the `exports` map.
|
|
150
165
|
|
|
151
166
|
```jsonc
|
|
152
167
|
{
|
package/import-zones-schema.js
CHANGED
|
@@ -26,6 +26,7 @@ const importZonesSchema = {
|
|
|
26
26
|
zones: { type: 'object', additionalProperties: stringList },
|
|
27
27
|
extensions: stringList,
|
|
28
28
|
conditions: stringList,
|
|
29
|
+
workspaces: stringList,
|
|
29
30
|
edges: buildEntrySchema(
|
|
30
31
|
{
|
|
31
32
|
to: stringList,
|
|
@@ -43,6 +44,7 @@ const importZonesSchema = {
|
|
|
43
44
|
globals: buildEntrySchema({ globals: stringList, allow: stringList }, ['globals', 'allow']),
|
|
44
45
|
unzoned: buildEntrySchema({ zones: stringList }, ['from', 'zones']),
|
|
45
46
|
unresolved: buildEntrySchema({}, []),
|
|
47
|
+
selfImports: buildEntrySchema({}, []),
|
|
46
48
|
},
|
|
47
49
|
additionalProperties: false,
|
|
48
50
|
};
|
package/import-zones.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isBuiltin } from 'node:module';
|
|
|
2
2
|
import { relative, sep } from 'node:path';
|
|
3
3
|
import findRepoRoot from './find-repo-root.js';
|
|
4
4
|
import importZonesSchema from './import-zones-schema.js';
|
|
5
|
+
import loadWorkspaceManifests from './load-workspace-manifests.js';
|
|
5
6
|
import loadZoneOptions from './load-zone-options.js';
|
|
6
7
|
import resolveImportTarget from './resolve-import-target.js';
|
|
7
8
|
|
|
@@ -14,7 +15,17 @@ function buildFileState(context) {
|
|
|
14
15
|
const root = findRepoRoot(filename) ?? context.cwd;
|
|
15
16
|
const path = toRootPath(root, filename);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
const workspaces = loadWorkspaceManifests(root, options.workspaces);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
context,
|
|
22
|
+
options,
|
|
23
|
+
filename,
|
|
24
|
+
root,
|
|
25
|
+
path,
|
|
26
|
+
workspaces,
|
|
27
|
+
zones: collectZones(options.zones, path),
|
|
28
|
+
};
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
function toRootPath(root, path) {
|
|
@@ -66,8 +77,13 @@ function checkImport(file, imported) {
|
|
|
66
77
|
root: file.root,
|
|
67
78
|
extensions: file.options.extensions,
|
|
68
79
|
conditions: file.options.conditions,
|
|
80
|
+
workspaces: file.workspaces,
|
|
69
81
|
});
|
|
70
82
|
|
|
83
|
+
if (target.self === true) {
|
|
84
|
+
emitSelfImport(file, imported.node, specifier);
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
if (target.kind === 'module') {
|
|
72
88
|
checkModule(file, imported, target.specifier);
|
|
73
89
|
} else if (target.kind === 'missing') {
|
|
@@ -96,6 +112,12 @@ function emitUnresolved(file, node, report) {
|
|
|
96
112
|
}
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
function emitSelfImport(file, node, specifier) {
|
|
116
|
+
for (const entry of collectEntries(file, file.options.selfImports)) {
|
|
117
|
+
emitReport(file, entry, { at: { node }, messageId: 'self', data: { specifier } });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
function checkModule(file, imported, specifier) {
|
|
100
122
|
for (const entry of collectEntries(file, file.options.modules)) {
|
|
101
123
|
const isAllowed =
|
|
@@ -270,6 +292,7 @@ const importZones = {
|
|
|
270
292
|
global: '{{rule}}: {{global}}() is allowed only in {{allow}}{{reason}}',
|
|
271
293
|
unzoned: '{{rule}}: this file belongs to none of {{zones}}; add its path to one{{reason}}',
|
|
272
294
|
unresolved: '{{rule}}: cannot resolve {{specifier}}{{reason}}',
|
|
295
|
+
self: '{{rule}}: {{specifier}} names the package this file belongs to; import it by a relative path{{reason}}',
|
|
273
296
|
computed: '{{rule}}: cannot check a computed import specifier{{reason}}',
|
|
274
297
|
},
|
|
275
298
|
schema: [importZonesSchema],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import makeGlobMatcher from './make-glob-matcher.js';
|
|
4
|
+
|
|
5
|
+
// The manifest of every package directory the globs match, relative to the
|
|
6
|
+
// root. Each glob segment matches one directory level, as a workspaces
|
|
7
|
+
// field's globs do.
|
|
8
|
+
export default function loadWorkspaceManifests(root, globs) {
|
|
9
|
+
return withManifestCache(JSON.stringify([root, globs]), () =>
|
|
10
|
+
globs
|
|
11
|
+
.flatMap((glob) => readGlobDirectories(root, splitGlob(glob), ''))
|
|
12
|
+
.map((directory) => loadManifest(join(root, directory)))
|
|
13
|
+
.filter((manifest) => typeof manifest?.name === 'string'),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const loadedManifests = new Map();
|
|
18
|
+
|
|
19
|
+
// each linted file asks again, so the manifests load once per root and globs
|
|
20
|
+
function withManifestCache(key, load) {
|
|
21
|
+
if (!loadedManifests.has(key)) {
|
|
22
|
+
loadedManifests.set(key, load());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return loadedManifests.get(key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitGlob(glob) {
|
|
29
|
+
return glob.split('/').filter((segment) => segment !== '' && segment !== '.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readGlobDirectories(root, segments, prefix) {
|
|
33
|
+
const [segment, ...rest] = segments;
|
|
34
|
+
|
|
35
|
+
if (segment === undefined) {
|
|
36
|
+
return [prefix];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isMatch = makeGlobMatcher([segment]);
|
|
40
|
+
const parent = join(root, prefix);
|
|
41
|
+
const names = existsSync(parent)
|
|
42
|
+
? readdirSync(parent, { withFileTypes: true })
|
|
43
|
+
.filter((entry) => entry.isDirectory() && entry.name !== 'node_modules')
|
|
44
|
+
.map((entry) => entry.name)
|
|
45
|
+
.filter((name) => isMatch(name))
|
|
46
|
+
: [];
|
|
47
|
+
|
|
48
|
+
return names.flatMap((name) => readGlobDirectories(root, rest, join(prefix, name)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `dir` is the field the resolver reads from every manifest it loads
|
|
52
|
+
function loadManifest(directory) {
|
|
53
|
+
const path = join(directory, 'package.json');
|
|
54
|
+
|
|
55
|
+
if (!existsSync(path)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
60
|
+
|
|
61
|
+
manifest.dir = directory;
|
|
62
|
+
|
|
63
|
+
return manifest;
|
|
64
|
+
}
|
package/load-zone-options.js
CHANGED
|
@@ -30,11 +30,13 @@ function buildZoneOptions(options) {
|
|
|
30
30
|
zones,
|
|
31
31
|
extensions: options.extensions ?? defaultExtensions,
|
|
32
32
|
conditions: new Set([...(options.conditions ?? []), 'default']),
|
|
33
|
+
workspaces: options.workspaces ?? [],
|
|
33
34
|
edges: buildEntries(options.edges),
|
|
34
35
|
modules: buildEntries(options.modules),
|
|
35
36
|
globals: buildEntries(options.globals),
|
|
36
37
|
unzoned: buildEntries(options.unzoned),
|
|
37
38
|
unresolved: buildEntries(options.unresolved),
|
|
39
|
+
selfImports: buildEntries(options.selfImports),
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
|
package/no-abbreviated-names.js
CHANGED
|
@@ -25,9 +25,9 @@ const noAbbreviatedNames = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const checkName = (node) => {
|
|
28
|
-
const
|
|
28
|
+
const name = findStaticName(node);
|
|
29
29
|
|
|
30
|
-
if (
|
|
30
|
+
if (name === null || !names.has(name)) {
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ const noAbbreviatedNames = {
|
|
|
36
36
|
context.report({
|
|
37
37
|
node,
|
|
38
38
|
messageId: 'abbreviated',
|
|
39
|
-
data: { name: `${prefix}${
|
|
39
|
+
data: { name: `${prefix}${name}`, replacement: names.get(name) },
|
|
40
40
|
});
|
|
41
41
|
};
|
|
42
42
|
|
|
@@ -46,32 +46,63 @@ const noAbbreviatedNames = {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// a computed key names its property as a plain key does when it is a
|
|
50
|
+
// string written in place, as in `{ ['src']: 1 }`; a computed identifier
|
|
51
|
+
// reads a variable, whose declaration is checked where it is made
|
|
52
|
+
const checkMemberName = (node, name) => {
|
|
53
|
+
if (!node.computed || name.type !== 'Identifier') {
|
|
54
|
+
checkName(name);
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
|
|
58
|
+
const checkKey = (node) => {
|
|
59
|
+
checkMemberName(node, node.key);
|
|
60
|
+
};
|
|
61
|
+
|
|
55
62
|
const checkParams = (node) => {
|
|
56
63
|
for (const param of node.params) {
|
|
57
64
|
checkPattern(param);
|
|
58
65
|
}
|
|
59
66
|
};
|
|
60
67
|
|
|
68
|
+
// a write to a member names that property, whether the member stands
|
|
69
|
+
// alone or sits inside a destructuring target
|
|
70
|
+
const checkAssignmentTarget = (node) => {
|
|
71
|
+
for (const member of collectTargetMembers(node)) {
|
|
72
|
+
checkMemberName(member, member.property);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const checkLoopTarget = (node) => {
|
|
77
|
+
if (node.left.type !== 'VariableDeclaration') {
|
|
78
|
+
checkAssignmentTarget(node.left);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// function-verb checks only a plain function or method name, so a quoted
|
|
83
|
+
// method, a signature or a declared function would pass both rules
|
|
84
|
+
const checkFunction = (node) => {
|
|
85
|
+
checkName(node.id);
|
|
86
|
+
checkParams(node);
|
|
87
|
+
};
|
|
88
|
+
|
|
61
89
|
return {
|
|
62
90
|
VariableDeclarator(node) {
|
|
63
91
|
checkPattern(node.id);
|
|
64
92
|
},
|
|
65
|
-
FunctionDeclaration:
|
|
66
|
-
FunctionExpression:
|
|
93
|
+
FunctionDeclaration: checkFunction,
|
|
94
|
+
FunctionExpression: checkFunction,
|
|
67
95
|
ArrowFunctionExpression: checkParams,
|
|
68
96
|
|
|
69
97
|
// a signature without a body names its parameters as an implementation does
|
|
70
|
-
TSDeclareFunction:
|
|
98
|
+
TSDeclareFunction: checkFunction,
|
|
71
99
|
TSEmptyBodyFunctionExpression: checkParams,
|
|
72
100
|
TSFunctionType: checkParams,
|
|
73
101
|
TSConstructorType: checkParams,
|
|
74
|
-
TSMethodSignature
|
|
102
|
+
TSMethodSignature(node) {
|
|
103
|
+
checkKey(node);
|
|
104
|
+
checkParams(node);
|
|
105
|
+
},
|
|
75
106
|
TSCallSignatureDeclaration: checkParams,
|
|
76
107
|
TSConstructSignatureDeclaration: checkParams,
|
|
77
108
|
CatchClause(node) {
|
|
@@ -83,15 +114,19 @@ const noAbbreviatedNames = {
|
|
|
83
114
|
TSAbstractPropertyDefinition: checkKey,
|
|
84
115
|
TSAbstractAccessorProperty: checkKey,
|
|
85
116
|
TSPropertySignature: checkKey,
|
|
117
|
+
MethodDefinition: checkKey,
|
|
118
|
+
TSAbstractMethodDefinition: checkKey,
|
|
86
119
|
|
|
87
|
-
// a
|
|
88
|
-
// value name
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
// a class, an enum and a namespace are values at run time; a quoted
|
|
121
|
+
// module name is a path, not a value name
|
|
122
|
+
'ClassDeclaration, ClassExpression, TSEnumDeclaration'(node) {
|
|
123
|
+
checkName(node.id);
|
|
124
|
+
},
|
|
125
|
+
TSModuleDeclaration(node) {
|
|
126
|
+
if (node.id.type === 'Identifier') {
|
|
127
|
+
checkName(node.id);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
95
130
|
|
|
96
131
|
// an import's local binding is a value name in this module, whatever the
|
|
97
132
|
// exporting module calls it; a type-only import names a type, not a value
|
|
@@ -105,19 +140,96 @@ const noAbbreviatedNames = {
|
|
|
105
140
|
checkName(node.id);
|
|
106
141
|
}
|
|
107
142
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
checkName(
|
|
143
|
+
TSIndexSignature(node) {
|
|
144
|
+
for (const parameter of node.parameters) {
|
|
145
|
+
checkName(parameter);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
TSEnumMember(node) {
|
|
149
|
+
checkName(node.id);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// an export alias is the name every importing module reads the value by
|
|
153
|
+
ExportSpecifier(node) {
|
|
154
|
+
if (!isTypeOnlyExport(node)) {
|
|
155
|
+
checkName(node.exported);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
ExportAllDeclaration(node) {
|
|
159
|
+
if (node.exportKind !== 'type') {
|
|
160
|
+
checkName(node.exported);
|
|
111
161
|
}
|
|
112
162
|
},
|
|
163
|
+
AssignmentExpression(node) {
|
|
164
|
+
checkAssignmentTarget(node.left);
|
|
165
|
+
},
|
|
166
|
+
ForInStatement: checkLoopTarget,
|
|
167
|
+
ForOfStatement: checkLoopTarget,
|
|
113
168
|
};
|
|
114
169
|
},
|
|
115
170
|
};
|
|
116
171
|
|
|
172
|
+
// the name a key or binding spells out in the source: an identifier, a
|
|
173
|
+
// string, or a template with no substitutions; a key built at run time has
|
|
174
|
+
// no static name, so the result is null
|
|
175
|
+
function findStaticName(node) {
|
|
176
|
+
if (node?.type === 'Identifier' || node?.type === 'PrivateIdentifier') {
|
|
177
|
+
return node.name;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (node?.type === 'Literal' && typeof node.value === 'string') {
|
|
181
|
+
return node.value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
185
|
+
return node.quasis[0].value.cooked ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
117
191
|
function isTypeOnlyImport(specifier) {
|
|
118
192
|
return specifier.importKind === 'type' || specifier.parent.importKind === 'type';
|
|
119
193
|
}
|
|
120
194
|
|
|
195
|
+
function isTypeOnlyExport(specifier) {
|
|
196
|
+
return specifier.exportKind === 'type' || specifier.parent.exportKind === 'type';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// the members an assignment target writes; a bare identifier in the target
|
|
200
|
+
// writes a variable declared elsewhere, and its declaration is checked there
|
|
201
|
+
function collectTargetMembers(node) {
|
|
202
|
+
if (node === null || node === undefined) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (node.type === 'MemberExpression') {
|
|
207
|
+
return [node];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (node.type === 'ObjectPattern') {
|
|
211
|
+
return node.properties.flatMap((property) => {
|
|
212
|
+
const target = property.type === 'RestElement' ? property : property.value;
|
|
213
|
+
|
|
214
|
+
return collectTargetMembers(target);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (node.type === 'ArrayPattern') {
|
|
219
|
+
return node.elements.flatMap((element) => collectTargetMembers(element));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (node.type === 'AssignmentPattern') {
|
|
223
|
+
return collectTargetMembers(node.left);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (node.type === 'RestElement') {
|
|
227
|
+
return collectTargetMembers(node.argument);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
121
233
|
// the names a binding pattern declares: a destructured key's alias, not the
|
|
122
234
|
// key, since the key belongs to the object being read
|
|
123
235
|
function collectPatternIdentifiers(node) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/oxlint-config",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Shareable oxlint config: strict categories, house-style rules, and a custom JS plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"import-zones.js",
|
|
23
23
|
"import-zones-schema.js",
|
|
24
24
|
"find-repo-root.js",
|
|
25
|
+
"load-workspace-manifests.js",
|
|
25
26
|
"load-zone-options.js",
|
|
26
27
|
"make-glob-matcher.js",
|
|
27
28
|
"resolve-import-target.js",
|
package/resolve-import-target.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { isBuiltin } from 'node:module';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
|
|
4
5
|
// a `.js` specifier in TypeScript source names the `.ts` file beside it
|
|
5
6
|
const sourceExtensions = { '.js': ['.ts', '.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };
|
|
6
7
|
|
|
7
|
-
// `module`
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// `module` for a package or builtin, `file` with the absolute path, or
|
|
9
|
+
// `missing`. The own name, which `self` marks, resolves through `exports`, `#`
|
|
10
|
+
// names through `imports`, and a workspace name to a path in its folder.
|
|
10
11
|
export default function resolveImportTarget(filename, specifier, settings) {
|
|
11
12
|
if (isPathSpecifier(specifier)) {
|
|
12
13
|
return toTarget(findImportedFile(filename, toFileSpecifier(specifier), settings.extensions));
|
|
@@ -14,30 +15,73 @@ export default function resolveImportTarget(filename, specifier, settings) {
|
|
|
14
15
|
|
|
15
16
|
const manifest = findPackageManifest(dirname(filename), settings.root);
|
|
16
17
|
|
|
17
|
-
return resolveManifestTarget(manifest, specifier, settings
|
|
18
|
+
return resolveManifestTarget(manifest, specifier, settings);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function resolveManifestTarget(manifest, specifier,
|
|
21
|
-
|
|
21
|
+
function resolveManifestTarget(manifest, specifier, settings) {
|
|
22
|
+
if (!specifier.startsWith('#')) {
|
|
23
|
+
return resolveBareTarget(manifest, specifier, settings);
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const mapped = findMappedTarget(manifest?.imports, specifier, settings.conditions);
|
|
27
|
+
|
|
28
|
+
return mapped !== null && !isPathSpecifier(mapped)
|
|
29
|
+
? resolveBareTarget(manifest, mapped, settings)
|
|
30
|
+
: toMappedTarget(manifest, mapped);
|
|
31
|
+
}
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
// a builtin keeps its name even when a package shares it
|
|
34
|
+
function resolveBareTarget(manifest, specifier, settings) {
|
|
35
|
+
const name = manifest?.name;
|
|
36
|
+
|
|
37
|
+
if (isBuiltin(specifier)) {
|
|
38
|
+
return { kind: 'module', specifier };
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
if (
|
|
41
|
+
if (isPackageSpecifier(name, specifier)) {
|
|
32
42
|
const subpath = `.${specifier.slice(name.length)}`;
|
|
43
|
+
const mapped = findMappedTarget(toSubpathMap(manifest.exports), subpath, settings.conditions);
|
|
44
|
+
|
|
45
|
+
return { ...toMappedTarget(manifest, mapped), self: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return resolveWorkspaceTarget(settings.workspaces, specifier);
|
|
49
|
+
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
function isPackageSpecifier(name, specifier) {
|
|
52
|
+
return typeof name === 'string' && (specifier === name || specifier.startsWith(`${name}/`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// the edges judge the package folder joined with the subpath, such as
|
|
56
|
+
// `packages/store/testing`, so the exports map's keys matter and not its targets
|
|
57
|
+
function resolveWorkspaceTarget(workspaces, specifier) {
|
|
58
|
+
const workspace = workspaces.find((candidate) => isPackageSpecifier(candidate.name, specifier));
|
|
59
|
+
|
|
60
|
+
if (workspace === undefined) {
|
|
61
|
+
return { kind: 'module', specifier };
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
const subpath = `.${specifier.slice(workspace.name.length)}`;
|
|
65
|
+
|
|
66
|
+
return isExportedSubpath(workspace.exports, subpath)
|
|
67
|
+
? { kind: 'file', path: join(workspace.dir, subpath) }
|
|
68
|
+
: { kind: 'missing' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// with no exports map, every subpath is public, and an empty one exports none
|
|
72
|
+
function isExportedSubpath(exports, subpath) {
|
|
73
|
+
if (exports === undefined) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof exports === 'object' && exports !== null && Object.keys(exports).length === 0) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const map = toSubpathMap(exports);
|
|
82
|
+
const key = Object.hasOwn(map, subpath) ? subpath : findPatternKey(map, subpath);
|
|
83
|
+
|
|
84
|
+
return key !== undefined && map[key] !== null;
|
|
41
85
|
}
|
|
42
86
|
|
|
43
87
|
// a bundler query such as Vite's `./styles.css?url` names the file before it
|
|
@@ -105,6 +149,12 @@ function findMappedTarget(map, key, conditions) {
|
|
|
105
149
|
return findConditionTarget(map[key], conditions);
|
|
106
150
|
}
|
|
107
151
|
|
|
152
|
+
const pattern = findPatternKey(map, key);
|
|
153
|
+
|
|
154
|
+
return pattern === undefined ? null : findPatternTarget(map, pattern, { key, conditions });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findPatternKey(map, key) {
|
|
108
158
|
const [pattern] = Object.keys(map)
|
|
109
159
|
.filter((candidate) => isPatternMatch(candidate, key))
|
|
110
160
|
.toSorted((left, right) => {
|
|
@@ -113,7 +163,7 @@ function findMappedTarget(map, key, conditions) {
|
|
|
113
163
|
return byPrefix === 0 ? right.length - left.length : byPrefix;
|
|
114
164
|
});
|
|
115
165
|
|
|
116
|
-
return pattern
|
|
166
|
+
return pattern;
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
function findPatternTarget(map, pattern, lookup) {
|