@tsrx/oxc 0.0.0-trusted-publishing-bootstrap → 0.8.0
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/LICENSE +21 -0
- package/README.md +141 -0
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/bin/oxc-tsrx +2 -0
- package/bin/oxc-tsrx-fmt +2 -0
- package/bin/oxc-tsrx-lint +2 -0
- package/bin/oxc-tsrx-lsp +2 -0
- package/bin/oxfmt +2 -0
- package/bin/oxlint +2 -0
- package/dist/bin/oxc-tsrx-fmt.js +13 -0
- package/dist/bin/oxc-tsrx-lint.js +13 -0
- package/dist/bin/oxc-tsrx-lsp.js +13 -0
- package/dist/bin/oxc-tsrx.js +115 -0
- package/dist/bin/oxfmt.js +24 -0
- package/dist/bin/oxlint.js +33 -0
- package/dist/canonical-command.d.ts +50 -0
- package/dist/canonical-command.js +196 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +1615 -0
- package/dist/editor-resolution.js +508 -0
- package/dist/format-cli.js +276 -0
- package/dist/format-invocation.js +97 -0
- package/dist/format.d.ts +1 -0
- package/dist/format.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -0
- package/dist/lint-cli.js +487 -0
- package/dist/lint-invocation.js +192 -0
- package/dist/lint-js-plugins.js +819 -0
- package/dist/lint-plugins-dev.d.ts +1 -0
- package/dist/lint-plugins-dev.js +2 -0
- package/dist/lint-prestart.js +16 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +2 -0
- package/dist/native-targets.js +76 -0
- package/dist/oxlint-lsp-multiplexer.js +622 -0
- package/dist/package-binary.js +29 -0
- package/dist/parser.d.ts +216 -0
- package/dist/parser.js +557 -0
- package/dist/process.js +88 -0
- package/dist/provider-resolve.d.ts +160 -0
- package/dist/provider-resolve.js +471 -0
- package/dist/providers-report.js +49 -0
- package/dist/runtime.js +323 -0
- package/dist/spawn-command.d.ts +20 -0
- package/dist/spawn-command.js +87 -0
- package/dist/tsrx-core-compat/facade.js +1184 -0
- package/dist/tsrx-core-compat/index.d.ts +6 -0
- package/dist/tsrx-core-compat/index.js +9 -0
- package/dist/tsrx-core-compat/style.js +525 -0
- package/dist/tsrx-core-compat/types/estree.d.ts +20 -0
- package/dist/tsrx-core-compat/types/index.d.ts +50 -0
- package/dist/tsrx-transfer.js +352 -0
- package/package.json +144 -5
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export type OxcProviderCapabilityName = "parse" | "lint" | "format" | "lsp";
|
|
2
|
+
|
|
3
|
+
export type OxcProviderDiagnosticCode =
|
|
4
|
+
| "ancestor-provider"
|
|
5
|
+
| "duplicate-id"
|
|
6
|
+
| "extension-conflict"
|
|
7
|
+
| "invalid-capability"
|
|
8
|
+
| "invalid-provider"
|
|
9
|
+
| "reserved-extension"
|
|
10
|
+
| "unreadable-manifest"
|
|
11
|
+
| "unresolved-capability"
|
|
12
|
+
| "unsupported-protocol";
|
|
13
|
+
|
|
14
|
+
export interface OxcProviderDiagnostic {
|
|
15
|
+
readonly severity: "error" | "warning";
|
|
16
|
+
readonly code: OxcProviderDiagnosticCode;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly packages?: readonly string[];
|
|
19
|
+
readonly extension?: string;
|
|
20
|
+
readonly capability?: OxcProviderCapabilityName;
|
|
21
|
+
readonly protocol?: number;
|
|
22
|
+
readonly id?: string;
|
|
23
|
+
readonly manifest?: string;
|
|
24
|
+
readonly root?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OxcProviderBinCapability {
|
|
28
|
+
readonly kind: "bin";
|
|
29
|
+
readonly bin: string;
|
|
30
|
+
readonly path: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OxcProviderModuleCapability {
|
|
34
|
+
readonly kind: "module";
|
|
35
|
+
readonly subpath: string;
|
|
36
|
+
readonly specifier: string;
|
|
37
|
+
readonly path: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type OxcProviderCapability = OxcProviderBinCapability | OxcProviderModuleCapability;
|
|
41
|
+
|
|
42
|
+
export type OxcProviderCapabilities = {
|
|
43
|
+
readonly [key in OxcProviderCapabilityName]?: OxcProviderCapability;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface OxcProviderLanguage {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly extensions: readonly string[];
|
|
49
|
+
readonly capabilities: OxcProviderCapabilities;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface OxcProvider {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly version: string | null;
|
|
55
|
+
readonly root: string;
|
|
56
|
+
readonly manifest: string;
|
|
57
|
+
readonly protocol: number;
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly languages: readonly OxcProviderLanguage[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface OxcProviderExtension {
|
|
63
|
+
readonly extension: string;
|
|
64
|
+
readonly package: string;
|
|
65
|
+
readonly providerId: string;
|
|
66
|
+
readonly providerRoot: string;
|
|
67
|
+
readonly language: string;
|
|
68
|
+
readonly capabilities: OxcProviderCapabilities;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface OxcProviderIndex {
|
|
72
|
+
readonly root: string;
|
|
73
|
+
readonly providers: readonly OxcProvider[];
|
|
74
|
+
readonly extensions: { readonly [extension: string]: OxcProviderExtension };
|
|
75
|
+
readonly diagnostics: readonly OxcProviderDiagnostic[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Issuer-aware module resolution. A Plug'n'Play host passes its PnP API's
|
|
80
|
+
* `resolveRequest`, which already has this exact `(request, issuer)` shape.
|
|
81
|
+
*/
|
|
82
|
+
export type OxcProviderResolve = (request: string, issuer: string) => string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Manifest reader. It must read through the same layer `resolve` answers from.
|
|
86
|
+
*
|
|
87
|
+
* Under Plug'n'Play there is no `node_modules`: packages stay zipped in
|
|
88
|
+
* `.yarn/cache` and `.pnp.cjs` answers with a path *into* the archive, which an
|
|
89
|
+
* ordinary `fs.readFile` cannot open (it fails with `ENOTDIR`). A host that
|
|
90
|
+
* injects the PnP resolver but keeps reading with an ordinary `fs` resolves
|
|
91
|
+
* every manifest and reads none of them.
|
|
92
|
+
*/
|
|
93
|
+
export type OxcProviderReadFile = (
|
|
94
|
+
path: string,
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
) => string | Promise<string | Uint8Array>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Host obligation: `resolve` and `readFile` travel together. Injecting one
|
|
100
|
+
* without the other is a host fault, not a protocol option.
|
|
101
|
+
*
|
|
102
|
+
* A Plug'n'Play host must supply both the PnP `resolveRequest` **and** a reader
|
|
103
|
+
* backed by the PnP filesystem layer, which in practice means running under the
|
|
104
|
+
* PnP runtime (`--require .pnp.cjs`, or a `yarn node` launcher) so `fs` is
|
|
105
|
+
* patched to see inside the zip. An injected resolver alone is not enough.
|
|
106
|
+
*
|
|
107
|
+
* A host that gets this wrong is told so. A dependency manifest that resolves
|
|
108
|
+
* and then cannot be read or parsed produces an `unreadable-manifest` warning
|
|
109
|
+
* naming the package and the manifest path, so a Plug'n'Play host with an
|
|
110
|
+
* unpatched reader sees one warning per direct dependency instead of an empty
|
|
111
|
+
* index and silence. A dependency that does not resolve at all stays quiet,
|
|
112
|
+
* because that only means it is not installed.
|
|
113
|
+
*/
|
|
114
|
+
export interface OxcProviderDiscoveryOptions {
|
|
115
|
+
readonly root?: string;
|
|
116
|
+
readonly resolve?: OxcProviderResolve;
|
|
117
|
+
readonly readFile?: OxcProviderReadFile;
|
|
118
|
+
readonly protocols?: readonly number[];
|
|
119
|
+
readonly throwOnError?: boolean;
|
|
120
|
+
readonly inspectAncestors?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type OxcResolvedCapability = OxcProviderCapability & {
|
|
124
|
+
readonly package: string;
|
|
125
|
+
readonly providerId: string;
|
|
126
|
+
readonly providerRoot: string;
|
|
127
|
+
readonly language: string;
|
|
128
|
+
readonly extension: string;
|
|
129
|
+
readonly capability: OxcProviderCapabilityName;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export declare const PROTOCOL_VERSION: 1;
|
|
133
|
+
export declare const SUPPORTED_PROTOCOLS: readonly number[];
|
|
134
|
+
export declare const CAPABILITY_NAMES: readonly OxcProviderCapabilityName[];
|
|
135
|
+
export declare const DEPENDENCY_FIELDS: readonly string[];
|
|
136
|
+
export declare const RESERVED_EXTENSIONS: readonly string[];
|
|
137
|
+
|
|
138
|
+
export declare class ProviderProtocolError extends Error {
|
|
139
|
+
readonly diagnostics: readonly OxcProviderDiagnostic[];
|
|
140
|
+
constructor(diagnostics: readonly OxcProviderDiagnostic[]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export declare function dependencyNames(manifest: unknown): string[];
|
|
144
|
+
export declare function providerDeclaration(manifest: unknown): Record<string, unknown> | null;
|
|
145
|
+
export declare function extensionOf(filePath: string): string | null;
|
|
146
|
+
export declare function isReservedExtension(extension: string): boolean;
|
|
147
|
+
export declare function findProjectRoot(
|
|
148
|
+
start?: string,
|
|
149
|
+
options?: { readonly readFile?: OxcProviderReadFile },
|
|
150
|
+
): Promise<string>;
|
|
151
|
+
export declare function discoverProviders(
|
|
152
|
+
options?: OxcProviderDiscoveryOptions,
|
|
153
|
+
): Promise<OxcProviderIndex>;
|
|
154
|
+
export declare function providerExtensions(index: OxcProviderIndex): string[];
|
|
155
|
+
export declare function hasProviderErrors(index: OxcProviderIndex): boolean;
|
|
156
|
+
export declare function resolveCapability(
|
|
157
|
+
index: OxcProviderIndex,
|
|
158
|
+
filePath: string,
|
|
159
|
+
capability: OxcProviderCapabilityName,
|
|
160
|
+
): OxcResolvedCapability | null;
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, parse, relative, resolve, sep } from "node:path";
|
|
4
|
+
//#region src/provider-resolve.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reference implementation of the static `oxc.provider` discovery protocol.
|
|
7
|
+
*
|
|
8
|
+
* A provider declares one static top-level `oxc.provider` block in its own
|
|
9
|
+
* package.json. A host reads the project root manifest, takes the union of the
|
|
10
|
+
* direct dependency fields, resolves `<name>/package.json` for each, parses the
|
|
11
|
+
* JSON, and builds an extension index. Nothing in this file imports, requires,
|
|
12
|
+
* or spawns a dependency: module resolution and `JSON.parse` are the only
|
|
13
|
+
* operations performed against a candidate package.
|
|
14
|
+
*
|
|
15
|
+
* The module deliberately contains no knowledge of any individual provider, so
|
|
16
|
+
* it can be vendored by an unrelated host unchanged.
|
|
17
|
+
*/
|
|
18
|
+
const PROTOCOL_VERSION = 1;
|
|
19
|
+
const SUPPORTED_PROTOCOLS = Object.freeze([1]);
|
|
20
|
+
const CAPABILITY_NAMES = Object.freeze([
|
|
21
|
+
"parse",
|
|
22
|
+
"lint",
|
|
23
|
+
"format",
|
|
24
|
+
"lsp"
|
|
25
|
+
]);
|
|
26
|
+
const DEPENDENCY_FIELDS = Object.freeze([
|
|
27
|
+
"dependencies",
|
|
28
|
+
"devDependencies",
|
|
29
|
+
"optionalDependencies"
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Extensions the core toolchain owns. A provider claiming one is a hard error,
|
|
33
|
+
* which is what structurally keeps ordinary source files off provider paths.
|
|
34
|
+
*/
|
|
35
|
+
const RESERVED_EXTENSIONS = Object.freeze([
|
|
36
|
+
".astro",
|
|
37
|
+
".cjs",
|
|
38
|
+
".cts",
|
|
39
|
+
".js",
|
|
40
|
+
".json",
|
|
41
|
+
".json5",
|
|
42
|
+
".jsonc",
|
|
43
|
+
".jsx",
|
|
44
|
+
".mjs",
|
|
45
|
+
".mts",
|
|
46
|
+
".svelte",
|
|
47
|
+
".ts",
|
|
48
|
+
".tsx",
|
|
49
|
+
".vue"
|
|
50
|
+
]);
|
|
51
|
+
const RESERVED = new Set(RESERVED_EXTENSIONS);
|
|
52
|
+
const IDENTIFIER = /^[a-z][a-z0-9-]*$/u;
|
|
53
|
+
var ProviderProtocolError = class extends Error {
|
|
54
|
+
diagnostics;
|
|
55
|
+
constructor(diagnostics) {
|
|
56
|
+
const failures = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
57
|
+
super(`the declared language providers cannot be indexed:\n${failures.map((diagnostic) => `- ${diagnostic.message}`).join("\n")}`);
|
|
58
|
+
this.name = "ProviderProtocolError";
|
|
59
|
+
this.diagnostics = diagnostics;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function defaultReadFile(path) {
|
|
63
|
+
return readFile(path, "utf8");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Issuer-aware Node resolution. A host with a different module map (Yarn PnP)
|
|
67
|
+
* injects `pnp.resolveRequest`, which has the same `(request, issuer)` shape.
|
|
68
|
+
*/
|
|
69
|
+
function createDefaultResolver() {
|
|
70
|
+
const requires = /* @__PURE__ */ new Map();
|
|
71
|
+
return (request, issuer) => {
|
|
72
|
+
let resolver = requires.get(issuer);
|
|
73
|
+
if (resolver === void 0) {
|
|
74
|
+
resolver = createRequire(issuer);
|
|
75
|
+
requires.set(issuer, resolver);
|
|
76
|
+
}
|
|
77
|
+
return resolver.resolve(request);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function isPlainObject(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
function parseJson(text, path) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(String(text));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function diagnostic(severity, code, message, details = {}) {
|
|
91
|
+
return {
|
|
92
|
+
severity,
|
|
93
|
+
code,
|
|
94
|
+
message,
|
|
95
|
+
...details
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function isInside(directory, path) {
|
|
99
|
+
const offset = relative(directory, path);
|
|
100
|
+
return offset.length > 0 && !offset.startsWith("..") && !offset.startsWith(`${sep}`);
|
|
101
|
+
}
|
|
102
|
+
/** Union of the direct dependency fields, sorted for deterministic output. */
|
|
103
|
+
function dependencyNames(manifest) {
|
|
104
|
+
const names = /* @__PURE__ */ new Set();
|
|
105
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
106
|
+
const declared = manifest?.[field];
|
|
107
|
+
if (!isPlainObject(declared)) continue;
|
|
108
|
+
for (const name of Object.keys(declared)) names.add(name);
|
|
109
|
+
}
|
|
110
|
+
return [...names].sort();
|
|
111
|
+
}
|
|
112
|
+
function providerDeclaration(manifest) {
|
|
113
|
+
const declaration = manifest?.oxc?.provider;
|
|
114
|
+
return isPlainObject(declaration) ? declaration : null;
|
|
115
|
+
}
|
|
116
|
+
function extensionOf(filePath) {
|
|
117
|
+
if (typeof filePath !== "string") return null;
|
|
118
|
+
const name = filePath.split(/[/\\]/u).at(-1) ?? "";
|
|
119
|
+
const dot = name.lastIndexOf(".");
|
|
120
|
+
if (dot <= 0) return null;
|
|
121
|
+
return name.slice(dot).toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
function isReservedExtension(extension) {
|
|
124
|
+
return typeof extension === "string" && RESERVED.has(extension.toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
/** The nearest ancestor directory of `start` that contains a package.json. */
|
|
127
|
+
async function findProjectRoot(start = process.cwd(), options = {}) {
|
|
128
|
+
const readFile = options.readFile ?? defaultReadFile;
|
|
129
|
+
const from = resolve(start);
|
|
130
|
+
const filesystemRoot = parse(from).root;
|
|
131
|
+
let directory = from;
|
|
132
|
+
for (;;) try {
|
|
133
|
+
await readFile(join(directory, "package.json"), "utf8");
|
|
134
|
+
return directory;
|
|
135
|
+
} catch {
|
|
136
|
+
if (directory === filesystemRoot) throw new Error(`no package.json was found at or above ${from}`);
|
|
137
|
+
directory = dirname(directory);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function normalizeBinMap(manifest) {
|
|
141
|
+
if (typeof manifest.bin === "string") return typeof manifest.name === "string" ? { [manifest.name]: manifest.bin } : {};
|
|
142
|
+
return isPlainObject(manifest.bin) ? manifest.bin : {};
|
|
143
|
+
}
|
|
144
|
+
function moduleSpecifier(name, subpath) {
|
|
145
|
+
if (subpath === ".") return name;
|
|
146
|
+
return `${name}/${subpath.replace(/^\.\/?/u, "")}`;
|
|
147
|
+
}
|
|
148
|
+
function readCapabilities(declared, context) {
|
|
149
|
+
const capabilities = {};
|
|
150
|
+
if (!isPlainObject(declared)) return capabilities;
|
|
151
|
+
for (const capability of CAPABILITY_NAMES) {
|
|
152
|
+
const value = declared[capability];
|
|
153
|
+
if (value === void 0) continue;
|
|
154
|
+
const target = readCapabilityTarget(capability, value, context);
|
|
155
|
+
if (target !== null) capabilities[capability] = target;
|
|
156
|
+
}
|
|
157
|
+
return capabilities;
|
|
158
|
+
}
|
|
159
|
+
function readCapabilityTarget(capability, value, context) {
|
|
160
|
+
const { name, providerRoot, manifest, diagnostics, resolve: resolve$1, issuer } = context;
|
|
161
|
+
if (!isPlainObject(value)) {
|
|
162
|
+
diagnostics.push(diagnostic("warning", "invalid-capability", `package ${name} declares a non-object ${capability} capability`, {
|
|
163
|
+
packages: [name],
|
|
164
|
+
capability
|
|
165
|
+
}));
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (typeof value.bin === "string" && value.bin.length > 0) {
|
|
169
|
+
const declared = normalizeBinMap(manifest)[value.bin];
|
|
170
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
171
|
+
diagnostics.push(diagnostic("warning", "invalid-capability", `package ${name} declares the ${capability} capability as bin "${value.bin}", which is not a key of its own bin map`, {
|
|
172
|
+
packages: [name],
|
|
173
|
+
capability
|
|
174
|
+
}));
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const path = resolve(providerRoot, declared);
|
|
178
|
+
if (!isInside(providerRoot, path)) {
|
|
179
|
+
diagnostics.push(diagnostic("warning", "invalid-capability", `package ${name} declares the ${capability} capability outside its own package directory`, {
|
|
180
|
+
packages: [name],
|
|
181
|
+
capability
|
|
182
|
+
}));
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
kind: "bin",
|
|
187
|
+
bin: value.bin,
|
|
188
|
+
path
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (typeof value.module === "string" && value.module.startsWith(".")) {
|
|
192
|
+
const specifier = moduleSpecifier(name, value.module);
|
|
193
|
+
let path = null;
|
|
194
|
+
try {
|
|
195
|
+
path = resolve$1(specifier, issuer);
|
|
196
|
+
} catch {
|
|
197
|
+
diagnostics.push(diagnostic("warning", "unresolved-capability", `package ${name} declares the ${capability} capability at ${specifier}, which its exports map does not resolve for this host`, {
|
|
198
|
+
packages: [name],
|
|
199
|
+
capability
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
kind: "module",
|
|
204
|
+
subpath: value.module,
|
|
205
|
+
specifier,
|
|
206
|
+
path
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
diagnostics.push(diagnostic("warning", "invalid-capability", `package ${name} declares a ${capability} capability that is neither an export subpath nor an own bin key`, {
|
|
210
|
+
packages: [name],
|
|
211
|
+
capability
|
|
212
|
+
}));
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function readLanguages(declared, context) {
|
|
216
|
+
const { name, diagnostics } = context;
|
|
217
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
218
|
+
diagnostics.push(diagnostic("warning", "invalid-provider", `package ${name} declares no provider languages`, { packages: [name] }));
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const languages = [];
|
|
222
|
+
for (const entry of declared) {
|
|
223
|
+
if (!isPlainObject(entry) || typeof entry.id !== "string" || !IDENTIFIER.test(entry.id)) {
|
|
224
|
+
diagnostics.push(diagnostic("warning", "invalid-provider", `package ${name} declares a language without a valid id`, { packages: [name] }));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const extensions = [];
|
|
228
|
+
for (const extension of Array.isArray(entry.extensions) ? entry.extensions : []) {
|
|
229
|
+
if (typeof extension !== "string" || extension.length < 2 || !extension.startsWith(".") || extension !== extension.toLowerCase() || /[\s/\\]/u.test(extension)) {
|
|
230
|
+
diagnostics.push(diagnostic("warning", "invalid-provider", `package ${name} declares the malformed extension ${JSON.stringify(extension)} for language ${entry.id}`, { packages: [name] }));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (RESERVED.has(extension)) {
|
|
234
|
+
diagnostics.push(diagnostic("error", "reserved-extension", `package ${name} claims the reserved ${extension} extension, which protocol 1 keeps on the core toolchain`, {
|
|
235
|
+
packages: [name],
|
|
236
|
+
extension
|
|
237
|
+
}));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!extensions.includes(extension)) extensions.push(extension);
|
|
241
|
+
}
|
|
242
|
+
if (extensions.length === 0) continue;
|
|
243
|
+
languages.push({
|
|
244
|
+
id: entry.id,
|
|
245
|
+
extensions,
|
|
246
|
+
capabilities: readCapabilities(entry.capabilities, context)
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return languages;
|
|
250
|
+
}
|
|
251
|
+
function readProvider(name, record, declaration, context) {
|
|
252
|
+
const { diagnostics, protocols } = context;
|
|
253
|
+
const { protocol, id } = declaration;
|
|
254
|
+
if (!Number.isInteger(protocol)) {
|
|
255
|
+
diagnostics.push(diagnostic("warning", "invalid-provider", `package ${name} declares a non-integer provider protocol`, { packages: [name] }));
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
if (!protocols.has(protocol)) {
|
|
259
|
+
diagnostics.push(diagnostic("warning", "unsupported-protocol", `package ${name} declares provider protocol ${protocol}, which this host does not support; it is ignored`, {
|
|
260
|
+
packages: [name],
|
|
261
|
+
protocol
|
|
262
|
+
}));
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (typeof id !== "string" || !IDENTIFIER.test(id)) {
|
|
266
|
+
diagnostics.push(diagnostic("warning", "invalid-provider", `package ${name} declares an invalid provider id ${JSON.stringify(id ?? null)}`, { packages: [name] }));
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const languages = readLanguages(declaration.languages, {
|
|
270
|
+
...context,
|
|
271
|
+
name,
|
|
272
|
+
manifest: record.manifest,
|
|
273
|
+
providerRoot: record.root
|
|
274
|
+
});
|
|
275
|
+
if (languages.length === 0) return null;
|
|
276
|
+
return {
|
|
277
|
+
name,
|
|
278
|
+
version: typeof record.manifest.version === "string" ? record.manifest.version : null,
|
|
279
|
+
root: record.root,
|
|
280
|
+
manifest: record.manifestPath,
|
|
281
|
+
protocol,
|
|
282
|
+
id,
|
|
283
|
+
languages
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Read one candidate dependency's manifest.
|
|
288
|
+
*
|
|
289
|
+
* The two failure modes below look alike and are not alike. Resolution failing
|
|
290
|
+
* means the package is simply not installed, which is ordinary and stays quiet;
|
|
291
|
+
* an uninstalled optional dependency must never warn. A manifest that resolves
|
|
292
|
+
* and then cannot be read or parsed is different: resolution already proved the
|
|
293
|
+
* package is there, so failing to read it is a host or environment fault, and it
|
|
294
|
+
* must never be mistaken for "this package is not a provider".
|
|
295
|
+
*/
|
|
296
|
+
async function readDependencyManifest(name, issuer, resolve, readFile, diagnostics) {
|
|
297
|
+
let manifestPath;
|
|
298
|
+
try {
|
|
299
|
+
manifestPath = resolve(`${name}/package.json`, issuer);
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
let manifest;
|
|
304
|
+
try {
|
|
305
|
+
manifest = JSON.parse(String(await readFile(manifestPath, "utf8")));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
diagnostics?.push(diagnostic("warning", "unreadable-manifest", `package ${name} resolved to ${manifestPath}, which this host could not read or parse: ${error instanceof Error ? error.message : String(error)}; a host with its own module map must supply a readFile that reads through the same layer`, {
|
|
308
|
+
packages: [name],
|
|
309
|
+
manifest: manifestPath
|
|
310
|
+
}));
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (!isPlainObject(manifest)) {
|
|
314
|
+
diagnostics?.push(diagnostic("warning", "unreadable-manifest", `package ${name} resolved to ${manifestPath}, which does not contain a JSON object`, {
|
|
315
|
+
packages: [name],
|
|
316
|
+
manifest: manifestPath
|
|
317
|
+
}));
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
manifestPath,
|
|
322
|
+
root: dirname(manifestPath),
|
|
323
|
+
manifest
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function inspectAncestors(root, directNames, context) {
|
|
327
|
+
const { readFile, resolve, diagnostics } = context;
|
|
328
|
+
const filesystemRoot = parse(root).root;
|
|
329
|
+
let directory = root;
|
|
330
|
+
while (directory !== filesystemRoot) {
|
|
331
|
+
directory = dirname(directory);
|
|
332
|
+
const manifestPath = join(directory, "package.json");
|
|
333
|
+
let manifest;
|
|
334
|
+
try {
|
|
335
|
+
manifest = JSON.parse(String(await readFile(manifestPath, "utf8")));
|
|
336
|
+
} catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
for (const name of dependencyNames(manifest)) {
|
|
340
|
+
if (directNames.has(name)) continue;
|
|
341
|
+
const record = await readDependencyManifest(name, manifestPath, resolve, readFile, diagnostics);
|
|
342
|
+
if (record === null || providerDeclaration(record.manifest) === null) continue;
|
|
343
|
+
diagnostics.push(diagnostic("warning", "ancestor-provider", `package ${name} declares a language provider and is a dependency of ${manifestPath}, but it is not a direct dependency of ${join(root, "package.json")}; it is not activated`, {
|
|
344
|
+
packages: [name],
|
|
345
|
+
manifest: manifestPath,
|
|
346
|
+
root: join(root, "package.json")
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Build the provider index for one project root.
|
|
353
|
+
*
|
|
354
|
+
* `resolve(request, issuer)` and `readFile(path, encoding)` are injectable so a
|
|
355
|
+
* host with a non-filesystem module map can supply its own. Fatal protocol
|
|
356
|
+
* violations (a reserved extension, two providers claiming one extension, two
|
|
357
|
+
* providers claiming one id) throw unless `throwOnError` is `false`; a report
|
|
358
|
+
* command uses `false` to show every violation at once.
|
|
359
|
+
*/
|
|
360
|
+
async function discoverProviders(options = {}) {
|
|
361
|
+
const root = resolve(options.root ?? process.cwd());
|
|
362
|
+
const readFile = options.readFile ?? defaultReadFile;
|
|
363
|
+
const resolve$2 = options.resolve ?? createDefaultResolver();
|
|
364
|
+
const protocols = new Set(options.protocols ?? SUPPORTED_PROTOCOLS);
|
|
365
|
+
const throwOnError = options.throwOnError !== false;
|
|
366
|
+
const manifestPath = join(root, "package.json");
|
|
367
|
+
const diagnostics = [];
|
|
368
|
+
const names = dependencyNames(parseJson(await readFile(manifestPath, "utf8"), manifestPath));
|
|
369
|
+
const context = {
|
|
370
|
+
diagnostics,
|
|
371
|
+
protocols,
|
|
372
|
+
resolve: resolve$2,
|
|
373
|
+
readFile,
|
|
374
|
+
issuer: manifestPath
|
|
375
|
+
};
|
|
376
|
+
const declared = [];
|
|
377
|
+
for (const name of names) {
|
|
378
|
+
const record = await readDependencyManifest(name, manifestPath, resolve$2, readFile, diagnostics);
|
|
379
|
+
if (record === null) continue;
|
|
380
|
+
const declaration = providerDeclaration(record.manifest);
|
|
381
|
+
if (declaration === null) continue;
|
|
382
|
+
const provider = readProvider(name, record, declaration, context);
|
|
383
|
+
if (provider !== null) declared.push(provider);
|
|
384
|
+
}
|
|
385
|
+
const byIdentifier = /* @__PURE__ */ new Map();
|
|
386
|
+
for (const provider of declared) {
|
|
387
|
+
const group = byIdentifier.get(provider.id);
|
|
388
|
+
if (group === void 0) byIdentifier.set(provider.id, [provider]);
|
|
389
|
+
else group.push(provider);
|
|
390
|
+
}
|
|
391
|
+
const rejectedIdentifiers = /* @__PURE__ */ new Set();
|
|
392
|
+
for (const [id, group] of byIdentifier) {
|
|
393
|
+
if (group.length < 2) continue;
|
|
394
|
+
rejectedIdentifiers.add(id);
|
|
395
|
+
const packages = group.map((provider) => provider.name);
|
|
396
|
+
diagnostics.push(diagnostic("error", "duplicate-id", `packages ${packages.join(" and ")} both declare the provider id "${id}"`, {
|
|
397
|
+
packages,
|
|
398
|
+
id
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
const providers = declared.filter((provider) => !rejectedIdentifiers.has(provider.id));
|
|
402
|
+
const claims = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const provider of providers) for (const language of provider.languages) for (const extension of language.extensions) {
|
|
404
|
+
const claim = {
|
|
405
|
+
provider,
|
|
406
|
+
language
|
|
407
|
+
};
|
|
408
|
+
const group = claims.get(extension);
|
|
409
|
+
if (group === void 0) claims.set(extension, [claim]);
|
|
410
|
+
else group.push(claim);
|
|
411
|
+
}
|
|
412
|
+
const extensions = {};
|
|
413
|
+
for (const extension of [...claims.keys()].sort()) {
|
|
414
|
+
const group = claims.get(extension);
|
|
415
|
+
if (group.length > 1) {
|
|
416
|
+
const packages = group.map((claim) => claim.provider.name);
|
|
417
|
+
diagnostics.push(diagnostic("error", "extension-conflict", `packages ${packages.join(" and ")} both claim the ${extension} extension; protocol 1 never picks a winner`, {
|
|
418
|
+
packages,
|
|
419
|
+
extension
|
|
420
|
+
}));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const [{ provider, language }] = group;
|
|
424
|
+
extensions[extension] = {
|
|
425
|
+
extension,
|
|
426
|
+
package: provider.name,
|
|
427
|
+
providerId: provider.id,
|
|
428
|
+
providerRoot: provider.root,
|
|
429
|
+
language: language.id,
|
|
430
|
+
capabilities: language.capabilities
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (options.inspectAncestors !== false) await inspectAncestors(root, new Set(names), context);
|
|
434
|
+
const index = {
|
|
435
|
+
root,
|
|
436
|
+
providers,
|
|
437
|
+
extensions,
|
|
438
|
+
diagnostics
|
|
439
|
+
};
|
|
440
|
+
if (throwOnError && diagnostics.some((entry) => entry.severity === "error")) throw new ProviderProtocolError(diagnostics);
|
|
441
|
+
return index;
|
|
442
|
+
}
|
|
443
|
+
function providerExtensions(index) {
|
|
444
|
+
return Object.keys(index?.extensions ?? {}).sort();
|
|
445
|
+
}
|
|
446
|
+
function hasProviderErrors(index) {
|
|
447
|
+
return (index?.diagnostics ?? []).some((entry) => entry.severity === "error");
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Look up one capability for one file. Returns `null` for every extension the
|
|
451
|
+
* index does not own, which is the fast path for ordinary source files.
|
|
452
|
+
*/
|
|
453
|
+
function resolveCapability(index, filePath, capability) {
|
|
454
|
+
const extension = extensionOf(filePath);
|
|
455
|
+
if (extension === null) return null;
|
|
456
|
+
const entry = index?.extensions?.[extension];
|
|
457
|
+
if (entry === void 0) return null;
|
|
458
|
+
const target = entry.capabilities?.[capability];
|
|
459
|
+
if (target === void 0) return null;
|
|
460
|
+
return {
|
|
461
|
+
package: entry.package,
|
|
462
|
+
providerId: entry.providerId,
|
|
463
|
+
providerRoot: entry.providerRoot,
|
|
464
|
+
language: entry.language,
|
|
465
|
+
extension,
|
|
466
|
+
capability,
|
|
467
|
+
...target
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
export { CAPABILITY_NAMES, DEPENDENCY_FIELDS, PROTOCOL_VERSION, ProviderProtocolError, RESERVED_EXTENSIONS, SUPPORTED_PROTOCOLS, dependencyNames, discoverProviders, extensionOf, findProjectRoot, hasProviderErrors, isReservedExtension, providerDeclaration, providerExtensions, resolveCapability };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CAPABILITY_NAMES, discoverProviders, findProjectRoot, hasProviderErrors } from "./provider-resolve.js";
|
|
2
|
+
//#region src/providers-report.ts
|
|
3
|
+
/**
|
|
4
|
+
* Read-only audit of the language providers a project root activates.
|
|
5
|
+
*
|
|
6
|
+
* The report never writes a file, never mutates node_modules, and never runs a
|
|
7
|
+
* dependency: it is the auditable counterpart to a host quietly executing a
|
|
8
|
+
* provider it discovered.
|
|
9
|
+
*/
|
|
10
|
+
async function collectProviderReport(options = {}) {
|
|
11
|
+
const root = await findProjectRoot(options.projectRoot ?? process.cwd());
|
|
12
|
+
const index = await discoverProviders({
|
|
13
|
+
root,
|
|
14
|
+
throwOnError: false
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
ok: !hasProviderErrors(index),
|
|
19
|
+
root: index.root,
|
|
20
|
+
providers: index.providers,
|
|
21
|
+
extensions: index.extensions,
|
|
22
|
+
diagnostics: index.diagnostics
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function capabilityLabel(capability) {
|
|
26
|
+
if (capability.kind === "bin") return `bin ${capability.bin} -> ${capability.path}`;
|
|
27
|
+
return `module ${capability.specifier}${capability.path === null ? " (unresolved)" : ""}`;
|
|
28
|
+
}
|
|
29
|
+
function formatProviderReport(report) {
|
|
30
|
+
const lines = [`project root: ${report.root}`];
|
|
31
|
+
if (report.providers.length === 0) lines.push("no language providers are declared by the direct dependencies");
|
|
32
|
+
for (const provider of report.providers) {
|
|
33
|
+
lines.push(`${provider.name}${provider.version === null ? "" : `@${provider.version}`} (provider ${provider.id}, protocol ${provider.protocol})`);
|
|
34
|
+
for (const language of provider.languages) {
|
|
35
|
+
lines.push(` language ${language.id}: ${language.extensions.join(" ")}`);
|
|
36
|
+
for (const capability of CAPABILITY_NAMES) {
|
|
37
|
+
const target = language.capabilities[capability];
|
|
38
|
+
if (target === void 0) continue;
|
|
39
|
+
lines.push(` ${capability}: ${capabilityLabel(target)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const routed = Object.keys(report.extensions).sort();
|
|
44
|
+
if (routed.length > 0) lines.push(`routed extensions: ${routed.map((extension) => `${extension} -> ${report.extensions[extension].package}`).join(", ")}`);
|
|
45
|
+
for (const diagnostic of report.diagnostics) lines.push(`${diagnostic.severity}: ${diagnostic.message}`);
|
|
46
|
+
return `${lines.join("\n")}\n`;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { collectProviderReport, formatProviderReport };
|