@cplieger/deadset-ts 8.0.0-dev.14 → 8.0.0-dev.16
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/dist/src/alias-chain.js +2 -2
- package/dist/src/analysis.js +43 -38
- package/dist/src/commands/read.js +6 -7
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +4 -2
- package/dist/src/contract/config.generated.js +1 -1
- package/dist/src/contract/contract.generated.js +120 -2
- package/dist/src/dependencies.js +81 -49
- package/dist/src/entries/build-scripts.js +222 -0
- package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
- package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
- package/dist/src/entries/conventions.js +739 -0
- package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
- package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
- package/dist/src/entries/run-directories.js +77 -0
- package/dist/src/entries/sources.js +144 -0
- package/dist/src/entries/static-module.js +669 -0
- package/dist/src/entries/test-runners.js +684 -0
- package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
- package/dist/src/extensions.js +37 -0
- package/dist/src/facts.js +38 -0
- package/dist/src/file-facts.js +1 -1
- package/dist/src/findings/emitters.js +5 -8
- package/dist/src/findings/self-check.js +36 -20
- package/dist/src/findings-pass.js +5 -6
- package/dist/src/framework-lifecycle.js +1 -1
- package/dist/src/fs/manifest.js +15 -20
- package/dist/src/generated-file.js +172 -29
- package/dist/src/graph.js +2 -5
- package/dist/src/index.js +1 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/inventory/commonjs.js +507 -0
- package/dist/src/inventory/enumerate.js +167 -0
- package/dist/src/inventory/syntax.js +236 -0
- package/dist/src/inventory.js +305 -705
- package/dist/src/javascript-file.js +26 -11
- package/dist/src/load/classify.js +28 -9
- package/dist/src/load/diagnostics.js +8 -1
- package/dist/src/load/discover.js +16 -8
- package/dist/src/load/package-scope.js +56 -0
- package/dist/src/load/plan.js +2 -2
- package/dist/src/load/synthesize.js +5 -5
- package/dist/src/load/workspace-manifests.js +1 -1
- package/dist/src/matrix.js +6 -13
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/paths.js +16 -0
- package/dist/src/platform-calls.js +78 -0
- package/dist/src/platform-symbols.js +140 -0
- package/dist/src/position.js +41 -0
- package/dist/src/ref.js +11 -20
- package/dist/src/references.js +281 -50
- package/dist/src/resolve.js +1 -1
- package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
- package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
- package/dist/src/{manifest.js → roots/manifest.js} +4 -4
- package/dist/src/roots/published-reach.js +412 -0
- package/dist/src/roots/root-set.js +489 -0
- package/dist/src/roots/unseen-loads.js +208 -0
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/session.js +3 -0
- package/dist/src/source-files.js +5 -9
- package/dist/src/suppress-inline.js +33 -81
- package/dist/src/suppress.js +30 -32
- package/dist/src/test-support.js +4 -2
- package/dist/src/verbs/analyze.js +13 -1
- package/dist/src/verbs/describe.js +5 -3
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +2 -2
- package/src/analysis.ts +77 -53
- package/src/automatic-types.ts +1 -1
- package/src/commands/read.ts +6 -8
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +5 -1
- package/src/contract/config.generated.ts +1 -1
- package/src/contract/contract.generated.ts +139 -2
- package/src/dependencies.ts +98 -50
- package/src/entries/build-scripts.ts +292 -0
- package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
- package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
- package/src/{conventions.ts → entries/conventions.ts} +347 -522
- package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
- package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
- package/src/entries/run-directories.ts +108 -0
- package/src/entries/sources.ts +266 -0
- package/src/entries/static-module.ts +857 -0
- package/src/entries/test-runners.ts +868 -0
- package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
- package/src/exempt.ts +3 -2
- package/src/extensions.ts +45 -0
- package/src/facts.ts +44 -0
- package/src/file-facts.ts +2 -2
- package/src/findings/emitters.ts +14 -10
- package/src/findings/self-check.ts +41 -22
- package/src/findings/visibility-narrowing.ts +1 -1
- package/src/findings-pass.ts +5 -12
- package/src/framework-lifecycle.ts +1 -1
- package/src/fs/manifest.ts +25 -21
- package/src/generated-file.ts +240 -36
- package/src/graph.ts +3 -6
- package/src/index.ts +1 -1
- package/src/injection-container.ts +1 -1
- package/src/inventory/commonjs.ts +696 -0
- package/src/inventory/enumerate.ts +203 -0
- package/src/inventory/syntax.ts +311 -0
- package/src/inventory.ts +424 -878
- package/src/javascript-file.ts +29 -12
- package/src/load/classify.ts +49 -13
- package/src/load/diagnostics.ts +11 -1
- package/src/load/discover.ts +28 -12
- package/src/load/package-scope.ts +74 -0
- package/src/load/plan.ts +11 -2
- package/src/load/synthesize.ts +6 -6
- package/src/load/test-globals.ts +1 -1
- package/src/load/workspace-manifests.ts +1 -1
- package/src/load/workspace-run.ts +1 -1
- package/src/matrix.ts +7 -14
- package/src/outside-tests.ts +1 -1
- package/src/paths.ts +18 -0
- package/src/platform-calls.ts +119 -0
- package/src/platform-symbols.ts +205 -0
- package/src/position.ts +42 -0
- package/src/ref.ts +11 -20
- package/src/references.ts +383 -73
- package/src/report.ts +1 -1
- package/src/resolve.ts +1 -1
- package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
- package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
- package/src/{manifest.ts → roots/manifest.ts} +5 -5
- package/src/roots/published-reach.ts +481 -0
- package/src/{roots.ts → roots/root-set.ts} +203 -630
- package/src/roots/unseen-loads.ts +307 -0
- package/src/serialization-contract.ts +1 -1
- package/src/session.ts +17 -1
- package/src/source-files.ts +5 -10
- package/src/suppress-inline.ts +53 -89
- package/src/suppress.ts +48 -33
- package/src/test-support.ts +8 -3
- package/src/verbs/analyze.ts +17 -4
- package/src/verbs/describe.ts +5 -2
- package/src/verbs/explain.ts +1 -1
- package/dist/src/build-scripts.js +0 -418
- package/dist/src/conventions.js +0 -907
- package/dist/src/entry-points.js +0 -513
- package/dist/src/roots.js +0 -865
- package/dist/src/unseen-loads.js +0 -100
- package/src/build-scripts.ts +0 -536
- package/src/entry-points.ts +0 -661
- package/src/unseen-loads.ts +0 -134
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root set of one project: every declaration kept live without a reference, and why.
|
|
3
|
+
* Every root seeds reachability and none counts as a reference; a root of a kind that
|
|
4
|
+
* names a caller is live under reference counting too, which the sweep decides, so a
|
|
5
|
+
* published export, which only supposes a consumer, stays a reference-counting candidate.
|
|
6
|
+
*/
|
|
7
|
+
import { aliasChains } from "../alias-chain.js";
|
|
8
|
+
import { filesNamedBy, outsideChain } from "../entries/configuration-files.js";
|
|
9
|
+
import { relativeSpecifiers } from "../entries/static-module.js";
|
|
10
|
+
import { nodeKey } from "../inventory.js";
|
|
11
|
+
import { ancestors, dirnamePath, relativePath, resolvePath } from "../paths.js";
|
|
12
|
+
import { byPosition } from "../position.js";
|
|
13
|
+
import { UNANSWERED } from "../query.js";
|
|
14
|
+
import { sourceFilesOf } from "../source-files.js";
|
|
15
|
+
import { typeQueryAliases } from "../type-query-alias.js";
|
|
16
|
+
import { bytewise } from "../text/compare.js";
|
|
17
|
+
import { holdsManifest } from "../fs/manifest.js";
|
|
18
|
+
import { globExpression } from "../text/glob.js";
|
|
19
|
+
import { spelledFiles } from "../text/named-files.js";
|
|
20
|
+
import { extensionOf, OUTPUT_EXTENSIONS } from "../extensions.js";
|
|
21
|
+
import { publishedAPI, releaseTagged } from "./published-reach.js";
|
|
22
|
+
import { skippedRoots } from "../type-errors.js";
|
|
23
|
+
import { mergedGlobalInterfaces } from "../augmentations.js";
|
|
24
|
+
/** Whether one declaration is written below the directory of a published workspace member. */
|
|
25
|
+
function inPublishedMember(held, id, root, members) {
|
|
26
|
+
const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
|
|
27
|
+
if (path === undefined || members === undefined) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const file = resolvePath(root, path);
|
|
31
|
+
return [...members].some((dir) => file.startsWith(`${dir}/`));
|
|
32
|
+
}
|
|
33
|
+
/** The kind of root each role of a manifest entry makes. */
|
|
34
|
+
const ENTRY_KINDS = {
|
|
35
|
+
import: "manifest-entry",
|
|
36
|
+
run: "manifest-binary",
|
|
37
|
+
script: "script",
|
|
38
|
+
};
|
|
39
|
+
/** The rules whose rooted files a manifest target is read back to by name. */
|
|
40
|
+
const READ_BACK_RULES = new Set([
|
|
41
|
+
"configuration-file",
|
|
42
|
+
"configuration-string",
|
|
43
|
+
"html-entry",
|
|
44
|
+
]);
|
|
45
|
+
/** The extension a source file's name drops. */
|
|
46
|
+
const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
|
|
47
|
+
/** Whether no `package.json` sits between one file and the manifest's directory, which holds it. */
|
|
48
|
+
function ofPackage(host, dir, file) {
|
|
49
|
+
const between = ancestors(dirnamePath(file), dir);
|
|
50
|
+
return between.length > 0 && !between.slice(0, -1).some((at) => holdsManifest(host, at));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The rooted files one manifest target reads back to by name: its path below the
|
|
54
|
+
* manifest's directory, past its first directory where it names one, without its output
|
|
55
|
+
* extension, is the name, and a rooted file of the manifest's own package is one whose
|
|
56
|
+
* path below the same directory, without its source extension, ends with that name at a
|
|
57
|
+
* directory boundary.
|
|
58
|
+
*/
|
|
59
|
+
function readBackByName(host, entry, rooted) {
|
|
60
|
+
const dir = resolvePath(host.workingDirectory(), entry.dir);
|
|
61
|
+
const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
|
|
62
|
+
if (below === undefined || below === "") {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const cut = below.indexOf("/");
|
|
66
|
+
const written = cut === -1 ? below : below.slice(cut + 1);
|
|
67
|
+
const name = written.slice(0, written.length - (extensionOf(written, OUTPUT_EXTENSIONS)?.length ?? 0));
|
|
68
|
+
if (name === "") {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const found = new Map();
|
|
72
|
+
for (const file of rooted) {
|
|
73
|
+
const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
|
|
74
|
+
if (path !== undefined &&
|
|
75
|
+
(path === name || path.endsWith(`/${name}`)) &&
|
|
76
|
+
ofPackage(host, dir, file.fileName)) {
|
|
77
|
+
found.set(file.fileName, file);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...found.values()];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The program files a manifest target no program holds reaches: the target is a file
|
|
84
|
+
* read as a configuration file outside the program is, and each file of the chain it
|
|
85
|
+
* starts loads the program files its relative specifiers name, read through the emit
|
|
86
|
+
* mapping, as a command's shim loads the compiled entry it runs.
|
|
87
|
+
*/
|
|
88
|
+
function throughOutside(options, targetRoot, files, entry) {
|
|
89
|
+
const cwd = options.host.workingDirectory();
|
|
90
|
+
const path = resolvePath(cwd, entry.path);
|
|
91
|
+
if (options.parse === undefined ||
|
|
92
|
+
options.runFiles?.has(path) === true ||
|
|
93
|
+
options.host.kindOf(path) !== "file") {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const chain = outsideChain(options.host, targetRoot, files, options.parse, [
|
|
97
|
+
{ path, manifestDir: resolvePath(cwd, entry.dir) },
|
|
98
|
+
]);
|
|
99
|
+
const found = new Map();
|
|
100
|
+
for (const { file } of chain) {
|
|
101
|
+
for (const specifier of relativeSpecifiers(file)) {
|
|
102
|
+
for (const named of filesNamedBy(files, dirnamePath(file.fileName), specifier)) {
|
|
103
|
+
found.set(named.fileName, named);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [...found.values()];
|
|
108
|
+
}
|
|
109
|
+
/** Whether a configured string carries either special character. */
|
|
110
|
+
function isPattern(text) {
|
|
111
|
+
return text.includes("*") || text.includes("?");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether ref matches a pattern whose only special characters are `*`, which stands
|
|
115
|
+
* for any run of characters including the solidus, and `?`, which stands for exactly
|
|
116
|
+
* one character counted as a code point. A star keeps the position it last stood at,
|
|
117
|
+
* so the shortest run it can stand for is tried first.
|
|
118
|
+
*/
|
|
119
|
+
function matchGlob(pattern, ref) {
|
|
120
|
+
let next = 0;
|
|
121
|
+
let star = -1;
|
|
122
|
+
let resume = 0;
|
|
123
|
+
let at = 0;
|
|
124
|
+
while (at < ref.length) {
|
|
125
|
+
if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
|
|
126
|
+
next += 1;
|
|
127
|
+
at += 1;
|
|
128
|
+
}
|
|
129
|
+
else if (next < pattern.length && pattern[next] === "*") {
|
|
130
|
+
star = next;
|
|
131
|
+
resume = at;
|
|
132
|
+
next += 1;
|
|
133
|
+
}
|
|
134
|
+
else if (star >= 0) {
|
|
135
|
+
resume += 1;
|
|
136
|
+
next = star + 1;
|
|
137
|
+
at = resume;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
while (next < pattern.length && pattern[next] === "*") {
|
|
144
|
+
next += 1;
|
|
145
|
+
}
|
|
146
|
+
return next === pattern.length;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Whether one configured string names the symbol reference ref. A string carrying
|
|
150
|
+
* neither special character names only the reference it spells.
|
|
151
|
+
*
|
|
152
|
+
* Both sides are read as code points: a character outside the basic plane is two
|
|
153
|
+
* string units, and `?` stands for one character.
|
|
154
|
+
*/
|
|
155
|
+
export function matchRef(pattern, ref) {
|
|
156
|
+
if (!isPattern(pattern)) {
|
|
157
|
+
return pattern === ref;
|
|
158
|
+
}
|
|
159
|
+
return matchGlob(Array.from(pattern), Array.from(ref));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Per package, the own files its held texts name, as `spelledFiles` reads them, but those a
|
|
163
|
+
* root enters with all they export: a command can use no more of such a file than the root
|
|
164
|
+
* keeps live. A manifest's are its entries' held texts, and the target root's package has the
|
|
165
|
+
* workflow steps, git hooks and makefile recipes beside its own, with the whole text of each
|
|
166
|
+
* file they are read from.
|
|
167
|
+
*/
|
|
168
|
+
function scriptHolds(entries, files, entered, host, targetRoot, workflows) {
|
|
169
|
+
const rooted = new Set(entered.filter((one) => one.exports && one.names === undefined).map((one) => one.file.fileName));
|
|
170
|
+
const spelled = [...files.byName.values()]
|
|
171
|
+
.filter((file) => !rooted.has(file.fileName))
|
|
172
|
+
.map((file) => ({ file, paths: [file.fileName, ...files.writtenAs(file)] }));
|
|
173
|
+
const cwd = host.workingDirectory();
|
|
174
|
+
const commands = new Map([
|
|
175
|
+
[
|
|
176
|
+
resolvePath(cwd, targetRoot),
|
|
177
|
+
[...workflows.readings.map((reading) => reading.held), ...workflows.texts],
|
|
178
|
+
],
|
|
179
|
+
]);
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (entry.held !== undefined) {
|
|
182
|
+
const dir = resolvePath(cwd, entry.dir);
|
|
183
|
+
commands.set(dir, [...(commands.get(dir) ?? []), entry.held]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const held = [];
|
|
187
|
+
for (const [dir, texts] of commands) {
|
|
188
|
+
const named = spelledFiles(texts.join("\n"), dir, spelled);
|
|
189
|
+
if (named.length > 0) {
|
|
190
|
+
const paths = named.map((file) => relativePath(targetRoot, file.fileName) ?? file.fileName);
|
|
191
|
+
held.push({ dir, files: paths });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return held;
|
|
195
|
+
}
|
|
196
|
+
/** The target's manifest entries, then those of its other packages. */
|
|
197
|
+
function packageEntries(options) {
|
|
198
|
+
return [
|
|
199
|
+
...options.manifest.entries.map((entry) => ({
|
|
200
|
+
entry,
|
|
201
|
+
publishes: options.publishedAPI &&
|
|
202
|
+
entry.published &&
|
|
203
|
+
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
204
|
+
})),
|
|
205
|
+
...(options.otherPackages ?? []).map((entry) => ({
|
|
206
|
+
entry,
|
|
207
|
+
publishes: entry.published &&
|
|
208
|
+
(options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
|
|
209
|
+
})),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The program files one manifest entry names, read once per entry: its path, else its
|
|
214
|
+
* output's source, else a rooted file of `readBackTo` by name, else the chain it starts
|
|
215
|
+
* outside every program.
|
|
216
|
+
*/
|
|
217
|
+
function entryFileReader(options, targetRoot, files, readBackTo) {
|
|
218
|
+
const read = new Map();
|
|
219
|
+
const readEntry = (entry) => {
|
|
220
|
+
const named = files.named(entry.path);
|
|
221
|
+
if (named.length > 0 || entry.role === "script") {
|
|
222
|
+
return named;
|
|
223
|
+
}
|
|
224
|
+
const cwd = options.host.workingDirectory();
|
|
225
|
+
const shipped = files.namedAsOutput(resolvePath(cwd, entry.dir), resolvePath(cwd, entry.path));
|
|
226
|
+
if (shipped.length > 0) {
|
|
227
|
+
return shipped;
|
|
228
|
+
}
|
|
229
|
+
const byName = readBackByName(options.host, entry, readBackTo);
|
|
230
|
+
return byName.length > 0 ? byName : throughOutside(options, targetRoot, files, entry);
|
|
231
|
+
};
|
|
232
|
+
return (entry) => {
|
|
233
|
+
const known = read.get(entry) ?? readEntry(entry);
|
|
234
|
+
read.set(entry, known);
|
|
235
|
+
return known;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** The files `ts.entry_files` names, each with the pattern that names it. */
|
|
239
|
+
function patternedFiles(files, patterns) {
|
|
240
|
+
const patterned = [];
|
|
241
|
+
for (const pattern of patterns) {
|
|
242
|
+
const expression = globExpression(pattern);
|
|
243
|
+
for (const [path, file] of files.byPath) {
|
|
244
|
+
if (expression.test(path)) {
|
|
245
|
+
patterned.push({ file, pattern });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return patterned;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* The files every root source enters, in the order manifest entries, entry-file patterns,
|
|
253
|
+
* entry sources; the files whose exports a consumer outside the target reaches; and the
|
|
254
|
+
* entries that name no file even read back by name to another entered file.
|
|
255
|
+
*/
|
|
256
|
+
function enteredFiles(options, targetRoot, files, entries) {
|
|
257
|
+
const points = options.sources.entered;
|
|
258
|
+
const scripted = entries.flatMap(({ entry }) => entry.role === "script" && entry.held === undefined ? files.named(entry.path) : []);
|
|
259
|
+
const entryFiles = entryFileReader(options, targetRoot, files, [
|
|
260
|
+
...scripted,
|
|
261
|
+
...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
|
|
262
|
+
]);
|
|
263
|
+
const published = entries.flatMap(({ entry, publishes }) => (publishes ? entryFiles(entry) : []));
|
|
264
|
+
if (options.publishedAPI) {
|
|
265
|
+
// A library's bundle hands what its entry exports to every program that loads it.
|
|
266
|
+
published.push(...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file));
|
|
267
|
+
}
|
|
268
|
+
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
269
|
+
const patterned = patternedFiles(files, options.entryFiles);
|
|
270
|
+
const entered = [];
|
|
271
|
+
const unmapped = [];
|
|
272
|
+
const mapped = patterned.map(({ file }) => file);
|
|
273
|
+
for (const { entry } of entries) {
|
|
274
|
+
if (entry.held !== undefined) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const named = entryFiles(entry);
|
|
278
|
+
if (named.length === 0 && entry.role !== "script") {
|
|
279
|
+
unmapped.push(entry);
|
|
280
|
+
}
|
|
281
|
+
mapped.push(...named);
|
|
282
|
+
for (const file of named) {
|
|
283
|
+
const kind = ENTRY_KINDS[entry.role];
|
|
284
|
+
entered.push({
|
|
285
|
+
file,
|
|
286
|
+
kind,
|
|
287
|
+
source: entry.member,
|
|
288
|
+
exports: !publishedNames.has(file.fileName),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const { file, pattern } of patterned) {
|
|
293
|
+
entered.push({ file, kind: "entry-file", source: pattern, exports: true });
|
|
294
|
+
}
|
|
295
|
+
for (const point of points) {
|
|
296
|
+
entered.push({
|
|
297
|
+
file: point.file,
|
|
298
|
+
kind: point.rule,
|
|
299
|
+
source: point.source,
|
|
300
|
+
exports: point.exports,
|
|
301
|
+
...(point.names === undefined ? {} : { names: point.names }),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
entered,
|
|
306
|
+
published,
|
|
307
|
+
unmappedEntries: unmapped.filter((entry) => readBackByName(options.host, entry, mapped).length === 0),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** Every declaration and container of a `typeof import(...)` global, with the type's text. */
|
|
311
|
+
function typeQueryRoots(files, declared, add) {
|
|
312
|
+
for (const file of files.byPath.values()) {
|
|
313
|
+
for (const alias of typeQueryAliases(file)) {
|
|
314
|
+
const source = alias.type.getText().replace(/\s+/gu, " ");
|
|
315
|
+
for (const node of [alias.declaration, ...alias.containers]) {
|
|
316
|
+
const id = declared.get(nodeKey(file, node));
|
|
317
|
+
if (id !== undefined) {
|
|
318
|
+
add(id, "type-query-alias", source);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** The roots in the order {@link Roots.liveUnderReachability} states. */
|
|
325
|
+
function orderedRoots(found, held) {
|
|
326
|
+
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
327
|
+
return [...found].sort((a, b) => {
|
|
328
|
+
const left = sites.get(a.id);
|
|
329
|
+
const right = sites.get(b.id);
|
|
330
|
+
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
331
|
+
return site || bytewise(a.kind, b.kind) || bytewise(a.source, b.source);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The roots one project holds, and every configured string that named nothing in it. A
|
|
336
|
+
* file a manifest member, an entry-file pattern or a tool's configuration names is a root
|
|
337
|
+
* with each name of its export table and every re-export link to what it carries; in a
|
|
338
|
+
* library, the published API alone roots what a published file exports. A test file is a
|
|
339
|
+
* root by itself, and so is each declaration a type error's skip keeps and each global
|
|
340
|
+
* interface merging into one declared outside the project.
|
|
341
|
+
*/
|
|
342
|
+
export function rootSet(project, held, targetRoot, options) {
|
|
343
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
344
|
+
const fileIds = new Map();
|
|
345
|
+
for (const file of files.byName.values()) {
|
|
346
|
+
const id = held.declarations.get(nodeKey(file, file));
|
|
347
|
+
if (id !== undefined) {
|
|
348
|
+
fileIds.set(file.fileName, id);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const found = new Map();
|
|
352
|
+
const add = (id, kind, source) => {
|
|
353
|
+
found.set([id, kind, source].join("\u0000"), { id, kind, source });
|
|
354
|
+
};
|
|
355
|
+
const entries = packageEntries(options);
|
|
356
|
+
const { entered, published, unmappedEntries } = enteredFiles(options, targetRoot, files, entries);
|
|
357
|
+
const heldScripts = scriptHolds(entries.map(({ entry }) => entry), files, entered, options.host, targetRoot, options.sources.workflows);
|
|
358
|
+
const heldFiles = [...new Set(heldScripts.flatMap((one) => one.files))].flatMap((path) => files.byPath.get(path) ?? []);
|
|
359
|
+
typeQueryRoots(files, held.declarations, add);
|
|
360
|
+
const { tables: exportsOf, named, answered, } = exportTables(project, held, [
|
|
361
|
+
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
362
|
+
...published,
|
|
363
|
+
...heldFiles,
|
|
364
|
+
]);
|
|
365
|
+
for (const entry of entered) {
|
|
366
|
+
const id = fileIds.get(entry.file.fileName);
|
|
367
|
+
if (id === undefined) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
add(id, entry.kind, entry.source);
|
|
371
|
+
if (entry.exports) {
|
|
372
|
+
const byName = named.get(entry.file.fileName);
|
|
373
|
+
const exported = entry.names === undefined || byName === undefined
|
|
374
|
+
? (exportsOf.get(entry.file.fileName) ?? [])
|
|
375
|
+
: entry.names.flatMap((name) => byName.get(name) ?? []);
|
|
376
|
+
for (const one of exported) {
|
|
377
|
+
add(one, entry.kind, entry.source);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const tagged = releaseTagged(held, files).filter((id) => options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers));
|
|
382
|
+
const api = published.length > 0 || tagged.length > 0
|
|
383
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
|
|
384
|
+
: { ids: [], guessed: [] };
|
|
385
|
+
for (const id of api.ids) {
|
|
386
|
+
add(id, "published-api", "");
|
|
387
|
+
}
|
|
388
|
+
const unmatched = configured(held, options.patterns, add);
|
|
389
|
+
for (const id of skippedRoots(project, held, options.skips)) {
|
|
390
|
+
add(id, "type-error", "");
|
|
391
|
+
}
|
|
392
|
+
for (const id of mergedGlobalInterfaces(project, held, files)) {
|
|
393
|
+
add(id, "global-augmentation", "");
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
liveUnderReachability: orderedRoots(found.values(), held),
|
|
397
|
+
unmatched,
|
|
398
|
+
unanswered: [
|
|
399
|
+
...(answered ? api.guessed : held.symbols.map((symbol) => symbol.id)),
|
|
400
|
+
// A command runs a held file and may read any export of it, so the file, which reaches its
|
|
401
|
+
// top level, and its exports are held; a declaration neither reaches is still dead.
|
|
402
|
+
...heldFiles.flatMap((file) => {
|
|
403
|
+
const id = fileIds.get(file.fileName);
|
|
404
|
+
return [...(id === undefined ? [] : [id]), ...(exportsOf.get(file.fileName) ?? [])];
|
|
405
|
+
}),
|
|
406
|
+
],
|
|
407
|
+
unmappedEntries,
|
|
408
|
+
heldScripts,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Every declaration each file's module exports, by the file's name: the declaration
|
|
413
|
+
* each name of the export table is, and every link of a re-export's chain. A name
|
|
414
|
+
* whose declaration is outside the project's own files names nothing here. One batch
|
|
415
|
+
* resolves every file to its module and one request per module reads its export
|
|
416
|
+
* table, star re-exports resolved into it. A file that is no module exports nothing.
|
|
417
|
+
* `answered` is false where the checker left a table or a link of a chain unanswered.
|
|
418
|
+
*/
|
|
419
|
+
function exportTables(project, held, wanted) {
|
|
420
|
+
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
421
|
+
const tables = new Map();
|
|
422
|
+
const named = new Map();
|
|
423
|
+
let answered = true;
|
|
424
|
+
if (unique.length === 0) {
|
|
425
|
+
return { tables, named, answered };
|
|
426
|
+
}
|
|
427
|
+
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
428
|
+
const chains = aliasChains(project, held);
|
|
429
|
+
const topLevel = (file) => {
|
|
430
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
431
|
+
return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
|
|
432
|
+
};
|
|
433
|
+
unique.forEach((file, index) => {
|
|
434
|
+
const module = modules[index];
|
|
435
|
+
if (module === undefined) {
|
|
436
|
+
tables.set(file.fileName, []);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
|
|
440
|
+
if (table === UNANSWERED) {
|
|
441
|
+
answered = false;
|
|
442
|
+
tables.set(file.fileName, topLevel(file));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const ids = new Set();
|
|
446
|
+
const byName = new Map();
|
|
447
|
+
for (const exported of table) {
|
|
448
|
+
for (const target of chains.chainOf(exported)) {
|
|
449
|
+
answered &&= !target.guessed;
|
|
450
|
+
ids.add(target.id);
|
|
451
|
+
byName.set(exported.name, [...(byName.get(exported.name) ?? []), target.id]);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
tables.set(file.fileName, [...ids]);
|
|
455
|
+
named.set(file.fileName, byName);
|
|
456
|
+
});
|
|
457
|
+
return { tables, named, answered };
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Keeps every declaration the configuration names and returns every string that
|
|
461
|
+
* named none, in configuration order. A string the configuration lists twice is one
|
|
462
|
+
* configured root.
|
|
463
|
+
*/
|
|
464
|
+
function configured(held, patterns, add) {
|
|
465
|
+
const unmatched = [];
|
|
466
|
+
for (const pattern of new Set(patterns)) {
|
|
467
|
+
const kind = isPattern(pattern) ? "pattern" : "configured";
|
|
468
|
+
let matched = false;
|
|
469
|
+
for (const symbol of held.symbols) {
|
|
470
|
+
if (matchRef(pattern, symbol.ref)) {
|
|
471
|
+
add(symbol.id, kind, pattern);
|
|
472
|
+
matched = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!matched) {
|
|
476
|
+
unmatched.push(pattern);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return unmatched;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Every configured string that named no declaration in any project of the run, in
|
|
483
|
+
* configuration order and each once. The finding is about the configuration, so a string
|
|
484
|
+
* naming a declaration of one project has named something, and a run with no project
|
|
485
|
+
* names nothing with any of them.
|
|
486
|
+
*/
|
|
487
|
+
export function unmatchedEverywhere(patterns, perProject) {
|
|
488
|
+
return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
|
|
489
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loads by a computed path: those whose files the analysis resolves, and those it
|
|
3
|
+
* cannot, whose files are roots the analysis does not see, so the configuration's roots
|
|
4
|
+
* are never complete.
|
|
5
|
+
*/
|
|
6
|
+
import { isBinaryExpression, isCallExpression, isIdentifier, isMetaProperty, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
7
|
+
import { membersNamed, textsOf, writtenValue } from "../entries/static-module.js";
|
|
8
|
+
import { writtenName } from "../platform-calls.js";
|
|
9
|
+
import { dirnamePath, relativePath, resolvePath } from "../paths.js";
|
|
10
|
+
import { unwrapped } from "../unwrap.js";
|
|
11
|
+
import { globExpression } from "../text/glob.js";
|
|
12
|
+
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
13
|
+
function leadingLiteral(node) {
|
|
14
|
+
const at = unwrapped(node);
|
|
15
|
+
if (isTemplateExpression(at)) {
|
|
16
|
+
return at.head.text;
|
|
17
|
+
}
|
|
18
|
+
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
19
|
+
const left = unwrapped(at.left);
|
|
20
|
+
return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
|
|
21
|
+
? left.text
|
|
22
|
+
: leadingLiteral(at.left);
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The directory below `dir` a computed load's leading literal names, every file of which
|
|
28
|
+
* the load reaches, or none where it names a package, `dir` itself or a directory holding
|
|
29
|
+
* it, each of which would name every sibling.
|
|
30
|
+
*/
|
|
31
|
+
function computedLoadDirectory(dir, prefix) {
|
|
32
|
+
if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const target = resolvePath(dir, prefix);
|
|
36
|
+
return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
|
|
37
|
+
}
|
|
38
|
+
/** Whether one node is a call that may load a module by a computed path. */
|
|
39
|
+
export function isLoadCall(node) {
|
|
40
|
+
if (!isCallExpression(node)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return (node.expression.kind === SyntaxKind.ImportKeyword ||
|
|
44
|
+
writtenName(node.expression) !== undefined ||
|
|
45
|
+
isMetaGlob(node));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The files one `import.meta.glob` call loads, as Vite's `toAbsoluteGlob` reads its globs
|
|
49
|
+
* (https://vite.dev/guide/features.html#glob-import): `/` against the project root, `./`
|
|
50
|
+
* and `../` against a literal `base` (read against the root where it starts with `/`) or the
|
|
51
|
+
* calling file's directory, `**` as written, and `!` removing what it matches. Undefined
|
|
52
|
+
* where Vite reads a glob through an alias, the root is unknown, or a glob or base is computed.
|
|
53
|
+
*/
|
|
54
|
+
function globbed(file, first, options, others, rootOf) {
|
|
55
|
+
const patterns = textsOf(writtenValue(first), true);
|
|
56
|
+
const bases = globOption(options, "base");
|
|
57
|
+
const [written, ...more] = bases ?? [];
|
|
58
|
+
const base = written === undefined ? undefined : textsOf(written, false);
|
|
59
|
+
if (patterns === undefined ||
|
|
60
|
+
bases === undefined ||
|
|
61
|
+
more.length > 0 ||
|
|
62
|
+
(written !== undefined && base === undefined)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const dir = dirnamePath(file.fileName);
|
|
66
|
+
const root = () => rootOf(file.fileName);
|
|
67
|
+
const against = (at) => {
|
|
68
|
+
if (!at.startsWith("/")) {
|
|
69
|
+
return resolvePath(dir, at);
|
|
70
|
+
}
|
|
71
|
+
const top = root();
|
|
72
|
+
return top === undefined ? undefined : resolvePath(top, `.${at}`);
|
|
73
|
+
};
|
|
74
|
+
const from = base?.[0] === undefined ? dir : against(base[0]);
|
|
75
|
+
const keep = [];
|
|
76
|
+
const drop = [];
|
|
77
|
+
for (const written of patterns) {
|
|
78
|
+
const negated = written.startsWith("!");
|
|
79
|
+
const pattern = negated ? written.slice(1) : written;
|
|
80
|
+
let path;
|
|
81
|
+
if (pattern.startsWith("/")) {
|
|
82
|
+
path = against(pattern);
|
|
83
|
+
}
|
|
84
|
+
else if (pattern.startsWith("./") || pattern.startsWith("../")) {
|
|
85
|
+
path = from === undefined ? undefined : resolvePath(from, pattern);
|
|
86
|
+
}
|
|
87
|
+
else if (pattern.startsWith("**")) {
|
|
88
|
+
path = pattern;
|
|
89
|
+
}
|
|
90
|
+
if (path === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
(negated ? drop : keep).push(globExpression(path));
|
|
94
|
+
}
|
|
95
|
+
return others.filter((candidate) => keep.some((glob) => glob.test(candidate.file.fileName)) &&
|
|
96
|
+
!drop.some((glob) => glob.test(candidate.file.fileName)));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The computed loads among one file's load calls, each loading the `candidates` but the
|
|
100
|
+
* file itself: an `import()` or `require()` whose path starts with a relative literal naming
|
|
101
|
+
* a directory below the file's loads every candidate below it, as a bundler's context module
|
|
102
|
+
* reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
|
|
103
|
+
* call of a computed path that names no such directory and no package is an unresolved load.
|
|
104
|
+
*/
|
|
105
|
+
export function computedLoadsOf(file, calls, candidates, rootOf) {
|
|
106
|
+
const dir = dirnamePath(file.fileName);
|
|
107
|
+
const others = candidates.filter((candidate) => candidate.file !== file);
|
|
108
|
+
return calls.flatMap(({ call, from }) => {
|
|
109
|
+
const [first, options] = call.arguments;
|
|
110
|
+
if (first === undefined) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
if (isMetaGlob(call)) {
|
|
114
|
+
const files = globbed(file, first, options, others, rootOf);
|
|
115
|
+
const asset = readsAsAsset(options);
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
call,
|
|
119
|
+
from,
|
|
120
|
+
require: undefined,
|
|
121
|
+
files: files ?? [],
|
|
122
|
+
resolved: files !== undefined,
|
|
123
|
+
asset,
|
|
124
|
+
eager: isEager(options),
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
const argument = unwrapped(first);
|
|
129
|
+
const require = call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
|
|
130
|
+
if (!mayNamePath(argument)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const prefix = leadingLiteral(argument);
|
|
134
|
+
if (namesPackage(prefix)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const target = prefix === undefined ? undefined : computedLoadDirectory(dir, prefix);
|
|
138
|
+
const files = target === undefined
|
|
139
|
+
? []
|
|
140
|
+
: others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
|
|
141
|
+
return [
|
|
142
|
+
{ call, from, require, files, resolved: target !== undefined, asset: false, eager: false },
|
|
143
|
+
];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether a computed argument may be a module path: anything but a string, which names its
|
|
148
|
+
* module, and a value no path is, a number, a boolean, `null`, an object, an array, a
|
|
149
|
+
* function or a class.
|
|
150
|
+
*/
|
|
151
|
+
function mayNamePath(argument) {
|
|
152
|
+
return !NO_PATH.has(argument.kind);
|
|
153
|
+
}
|
|
154
|
+
const NO_PATH = new Set([
|
|
155
|
+
SyntaxKind.StringLiteral,
|
|
156
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
157
|
+
SyntaxKind.NumericLiteral,
|
|
158
|
+
SyntaxKind.BigIntLiteral,
|
|
159
|
+
SyntaxKind.TrueKeyword,
|
|
160
|
+
SyntaxKind.FalseKeyword,
|
|
161
|
+
SyntaxKind.NullKeyword,
|
|
162
|
+
SyntaxKind.ObjectLiteralExpression,
|
|
163
|
+
SyntaxKind.ArrayLiteralExpression,
|
|
164
|
+
SyntaxKind.ArrowFunction,
|
|
165
|
+
SyntaxKind.FunctionExpression,
|
|
166
|
+
SyntaxKind.ClassExpression,
|
|
167
|
+
SyntaxKind.RegularExpressionLiteral,
|
|
168
|
+
]);
|
|
169
|
+
/** Whether one call is `import.meta.glob(...)`. */
|
|
170
|
+
function isMetaGlob(node) {
|
|
171
|
+
if (!isPropertyAccessExpression(node.expression)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const { expression, name } = node.expression;
|
|
175
|
+
return (isIdentifier(name) &&
|
|
176
|
+
name.text === "glob" &&
|
|
177
|
+
isMetaProperty(expression) &&
|
|
178
|
+
expression.keywordToken === SyntaxKind.ImportKeyword);
|
|
179
|
+
}
|
|
180
|
+
/** The values the options of an `import.meta.glob` write for one key, undefined where a spread may hide one. */
|
|
181
|
+
function globOption(options, key) {
|
|
182
|
+
return options === undefined ? [] : membersNamed(writtenValue(options), key);
|
|
183
|
+
}
|
|
184
|
+
/** Whether the options of an `import.meta.glob` set `eager` to `true`. */
|
|
185
|
+
function isEager(options) {
|
|
186
|
+
return (globOption(options, "eager")?.some((value) => value.kind === "boolean" && value.value) === true);
|
|
187
|
+
}
|
|
188
|
+
/** The query parameters that make Vite load a file as its text or its URL, each written bare. */
|
|
189
|
+
const ASSET_QUERY = /(?:^\?|&)(?:raw|url|inline)(?:&|$)/u;
|
|
190
|
+
/**
|
|
191
|
+
* Whether the options of an `import.meta.glob` read each file as text or as a URL: a
|
|
192
|
+
* `query` string holding a bare `raw`, `url` or `inline`, or an `as` of `raw` or `url`,
|
|
193
|
+
* which Vite reads as that query (https://vite.dev/guide/features.html#glob-import). Any
|
|
194
|
+
* other query is read as loading the module, which keeps more than it may.
|
|
195
|
+
*/
|
|
196
|
+
function readsAsAsset(options) {
|
|
197
|
+
const texts = (key) => (globOption(options, key) ?? []).flatMap((value) => value.kind === "text" ? [value.text] : []);
|
|
198
|
+
return (texts("as").some((text) => text === "raw" || text === "url") ||
|
|
199
|
+
texts("query").some((text) => ASSET_QUERY.test(text.startsWith("?") ? text : `?${text}`)));
|
|
200
|
+
}
|
|
201
|
+
/** Whether a computed path's leading literal names a package, whose files are none of the target's. */
|
|
202
|
+
function namesPackage(prefix) {
|
|
203
|
+
return (prefix !== undefined &&
|
|
204
|
+
prefix !== "" &&
|
|
205
|
+
!prefix.startsWith(".") &&
|
|
206
|
+
!prefix.startsWith("/") &&
|
|
207
|
+
!/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix));
|
|
208
|
+
}
|