package-registry-manager 0.21.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/CHANGELOG.md +60 -0
- package/LICENSE +24 -0
- package/README.md +21 -0
- package/package.json +53 -0
- package/src/approvals.mjs +72 -0
- package/src/auth-urls.mjs +149 -0
- package/src/automation.mjs +124 -0
- package/src/browser-options.mjs +175 -0
- package/src/browser.mjs +123 -0
- package/src/cli.mjs +431 -0
- package/src/containers.mjs +66 -0
- package/src/default-browser.mjs +149 -0
- package/src/discovery.mjs +461 -0
- package/src/flows.mjs +722 -0
- package/src/index.mjs +6 -0
- package/src/model.mjs +50 -0
- package/src/pages.mjs +91 -0
- package/src/plan.mjs +243 -0
- package/src/prerequisites.mjs +323 -0
- package/src/profile.mjs +104 -0
- package/src/publishers.mjs +495 -0
- package/src/registry-state.mjs +158 -0
- package/src/setup.mjs +820 -0
- package/src/skips.mjs +117 -0
- package/src/tokens.mjs +215 -0
- package/src/workflows.mjs +46 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { exec } from "command-stream";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Names of common browsers by macOS bundle id, Linux desktop entry, and
|
|
5
|
+
* Windows URL ProgId, so the tool can say which browser it opened.
|
|
6
|
+
*/
|
|
7
|
+
const KNOWN_BROWSERS = [
|
|
8
|
+
[/^com\.apple\.safari$|^SafariURL/i, "Safari"],
|
|
9
|
+
[/^com\.google\.chrome\.canary$/i, "Google Chrome Canary"],
|
|
10
|
+
[/^com\.google\.chrome$|google-chrome|^ChromeHTML$/i, "Google Chrome"],
|
|
11
|
+
[/^org\.chromium\.chromium$|chromium|^ChromiumHTM/i, "Chromium"],
|
|
12
|
+
[/^org\.mozilla\.firefox$|firefox|^FirefoxURL/i, "Firefox"],
|
|
13
|
+
[/^com\.microsoft\.edgemac$|microsoft-edge|^MSEdgeHTM$/i, "Microsoft Edge"],
|
|
14
|
+
[/^com\.brave\.browser$|brave-browser|^BraveHTML$/i, "Brave"],
|
|
15
|
+
[/^company\.thebrowser\.browser$/i, "Arc"],
|
|
16
|
+
[/^com\.operasoftware\.opera$|opera|^OperaStable$/i, "Opera"],
|
|
17
|
+
[/^com\.vivaldi\.vivaldi$|vivaldi|^VivaldiHTM/i, "Vivaldi"],
|
|
18
|
+
[/^IE\.HTTP/i, "Internet Explorer"],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** The command that reports the default browser, as an exact argument vector. */
|
|
22
|
+
export function defaultBrowserQuery(platform = process.platform) {
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
return {
|
|
25
|
+
program: "defaults",
|
|
26
|
+
args: [
|
|
27
|
+
"read",
|
|
28
|
+
"com.apple.LaunchServices/com.apple.launchservices.secure",
|
|
29
|
+
"LSHandlers",
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (platform === "win32") {
|
|
34
|
+
return {
|
|
35
|
+
program: "reg",
|
|
36
|
+
args: [
|
|
37
|
+
"query",
|
|
38
|
+
"HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice",
|
|
39
|
+
"/v",
|
|
40
|
+
"ProgId",
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { program: "xdg-settings", args: ["get", "default-web-browser"] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the browser id from the query's output: the https handler's bundle
|
|
49
|
+
* id on macOS (Safari when none is set), the desktop entry on Linux, and the
|
|
50
|
+
* ProgId on Windows.
|
|
51
|
+
*/
|
|
52
|
+
export function defaultBrowserId(output, platform = process.platform) {
|
|
53
|
+
const text = output ?? "";
|
|
54
|
+
if (platform === "darwin") {
|
|
55
|
+
for (const scheme of ["https", "http"]) {
|
|
56
|
+
for (const block of handlerEntries(text)) {
|
|
57
|
+
const handles = new RegExp(
|
|
58
|
+
`LSHandlerURLScheme\\s*=\\s*"?${scheme}"?\\s*;`,
|
|
59
|
+
).test(block);
|
|
60
|
+
const role = /LSHandlerRoleAll\s*=\s*"?([\w.-]+)"?\s*;/.exec(block);
|
|
61
|
+
if (handles && role) {
|
|
62
|
+
return role[1];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return "com.apple.safari";
|
|
67
|
+
}
|
|
68
|
+
if (platform === "win32") {
|
|
69
|
+
return /\bProgId\s+REG_SZ\s+(\S+)/i.exec(text)?.[1];
|
|
70
|
+
}
|
|
71
|
+
return text.trim() || undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The top-level dictionaries of macOS's LSHandlers array, without their nested
|
|
76
|
+
* dictionaries such as LSHandlerPreferredVersions.
|
|
77
|
+
*/
|
|
78
|
+
function handlerEntries(text) {
|
|
79
|
+
const entries = [];
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let entry = "";
|
|
82
|
+
for (const char of text) {
|
|
83
|
+
if (char === "{") {
|
|
84
|
+
depth += 1;
|
|
85
|
+
entry = depth === 1 ? "" : entry;
|
|
86
|
+
} else if (char === "}") {
|
|
87
|
+
depth -= 1;
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
entries.push(entry);
|
|
90
|
+
}
|
|
91
|
+
} else if (depth === 1) {
|
|
92
|
+
entry += char;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A readable browser name for an id, or undefined when it is unknown. */
|
|
99
|
+
export function browserName(id) {
|
|
100
|
+
if (!id) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const known = KNOWN_BROWSERS.find(([pattern]) => pattern.test(id));
|
|
104
|
+
return known?.[1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Names the user's default browser, or returns undefined when the platform
|
|
109
|
+
* does not say. `run(program, args)` resolves to `{ code, stdout }`.
|
|
110
|
+
*/
|
|
111
|
+
export async function detectDefaultBrowser({
|
|
112
|
+
platform = process.platform,
|
|
113
|
+
run = captureOutput,
|
|
114
|
+
verbose = false,
|
|
115
|
+
} = {}) {
|
|
116
|
+
const query = defaultBrowserQuery(platform);
|
|
117
|
+
try {
|
|
118
|
+
const result = await run(query.program, query.args);
|
|
119
|
+
if (result.code !== 0 && platform !== "darwin") {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
// Without any handler entry, macOS answers with an error and uses Safari.
|
|
123
|
+
// command-stream captures stdout as a string-like object, not a string.
|
|
124
|
+
const output = result.code === 0 ? String(result.stdout ?? "") : "";
|
|
125
|
+
return browserName(defaultBrowserId(output, platform));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (verbose) {
|
|
128
|
+
console.error(`could not detect the default browser: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function captureOutput(program, args) {
|
|
135
|
+
return exec(program, args, { capture: true, mirror: false, stdin: "ignore" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The exact argument vector that opens `url` in a chosen application:
|
|
140
|
+
* `open -a <app>` on macOS, and the application itself elsewhere.
|
|
141
|
+
*/
|
|
142
|
+
export function openWithCommand(url, app, platform = process.platform) {
|
|
143
|
+
if (!/^https?:\/\/\S+$/i.test(url)) {
|
|
144
|
+
throw new Error(`refusing to open a non-web URL: ${url}`);
|
|
145
|
+
}
|
|
146
|
+
return platform === "darwin"
|
|
147
|
+
? { program: "open", args: ["-a", app, url] }
|
|
148
|
+
: { program: app, args: [url] };
|
|
149
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { readFile, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { CONTAINER_FILES, containerPackages } from "./containers.mjs";
|
|
5
|
+
import { REGISTRIES } from "./model.mjs";
|
|
6
|
+
import {
|
|
7
|
+
TRUSTED_REGISTRIES,
|
|
8
|
+
detectPublisher,
|
|
9
|
+
tokenSecretWarning,
|
|
10
|
+
tokenSecrets,
|
|
11
|
+
} from "./publishers.mjs";
|
|
12
|
+
import {
|
|
13
|
+
ignoreReason,
|
|
14
|
+
ignoredBy,
|
|
15
|
+
readIgnoreList,
|
|
16
|
+
referencedByWorkflow,
|
|
17
|
+
testDirectory,
|
|
18
|
+
testDirectoryReason,
|
|
19
|
+
} from "./skips.mjs";
|
|
20
|
+
import { pagesWorkflow } from "./pages.mjs";
|
|
21
|
+
import { readWorkflows } from "./workflows.mjs";
|
|
22
|
+
|
|
23
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
24
|
+
".git",
|
|
25
|
+
".package-registry-manager",
|
|
26
|
+
".venv",
|
|
27
|
+
"node_modules",
|
|
28
|
+
"target",
|
|
29
|
+
"vendor",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const MANIFEST_NAMES = new Set([
|
|
33
|
+
"package.json",
|
|
34
|
+
"Cargo.toml",
|
|
35
|
+
"pyproject.toml",
|
|
36
|
+
"setup.py",
|
|
37
|
+
"go.mod",
|
|
38
|
+
"pom.xml",
|
|
39
|
+
"build.gradle",
|
|
40
|
+
"build.gradle.kts",
|
|
41
|
+
"composer.json",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Finds the packages a repository publishes. Manifests in test and example
|
|
46
|
+
* directories, or matched by the ignore list, are left out; `includeSkipped`
|
|
47
|
+
* lists them under `skipped`.
|
|
48
|
+
*/
|
|
49
|
+
export async function inspectRepository(repository, options = {}) {
|
|
50
|
+
const root = await realpath(repository);
|
|
51
|
+
const manifests = [];
|
|
52
|
+
const dockerfiles = [];
|
|
53
|
+
await collectManifests(root, manifests, dockerfiles);
|
|
54
|
+
manifests.sort();
|
|
55
|
+
const preferredManifests = manifests.filter(
|
|
56
|
+
(manifest) =>
|
|
57
|
+
path.basename(manifest) !== "setup.py" ||
|
|
58
|
+
!manifests.includes(path.join(path.dirname(manifest), "pyproject.toml")),
|
|
59
|
+
);
|
|
60
|
+
const workflows = await readWorkflows(root);
|
|
61
|
+
const ignore = await readIgnoreList(root);
|
|
62
|
+
const skipped = [];
|
|
63
|
+
const skip = (manifest, reason) => {
|
|
64
|
+
skipped.push({ manifest, reason });
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
const ignored = (manifest) => {
|
|
68
|
+
const pattern = ignoredBy(ignore, manifest);
|
|
69
|
+
return pattern !== null && !skip(manifest, ignoreReason(pattern));
|
|
70
|
+
};
|
|
71
|
+
// Test and example manifests stay only when a workflow publishes them.
|
|
72
|
+
const kept = (manifest, publishable) => {
|
|
73
|
+
const directory = testDirectory(manifest);
|
|
74
|
+
return (
|
|
75
|
+
directory === null ||
|
|
76
|
+
(publishable && referencedByWorkflow(workflows, manifest)) ||
|
|
77
|
+
skip(manifest, testDirectoryReason(directory))
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
const parsed = await Promise.all(
|
|
81
|
+
preferredManifests
|
|
82
|
+
.map((manifest) => [manifest, relativePath(root, manifest)])
|
|
83
|
+
.filter(([, manifest]) => !ignored(manifest))
|
|
84
|
+
.map(async ([manifestPath, manifest]) => {
|
|
85
|
+
try {
|
|
86
|
+
return await parseManifest(manifestPath, manifest);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (testDirectory(manifest) === null) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return { manifest, publishable: false, invalid: true };
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
const coordinates = await githubCoordinates(
|
|
96
|
+
root,
|
|
97
|
+
preferredManifests.filter((manifest) => {
|
|
98
|
+
const relative = relativePath(root, manifest);
|
|
99
|
+
return ignoredBy(ignore, relative) === null && !testDirectory(relative);
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const publishers = new Map();
|
|
103
|
+
for (const registry of TRUSTED_REGISTRIES) {
|
|
104
|
+
publishers.set(registry, await detectPublisher(root, workflows, registry));
|
|
105
|
+
}
|
|
106
|
+
const containerFiles = dockerfiles
|
|
107
|
+
.map((item) => relativePath(root, item))
|
|
108
|
+
.filter((item) => !ignored(item) && kept(item, true));
|
|
109
|
+
const packages = parsed
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.filter((item) => kept(item.manifest, item.publishable))
|
|
112
|
+
.map((item) => withPublisher(item, workflows, publishers))
|
|
113
|
+
.concat(containerPackages(containerFiles, workflows, coordinates))
|
|
114
|
+
.sort((left, right) => {
|
|
115
|
+
const registryOrder =
|
|
116
|
+
REGISTRIES.indexOf(left.registry) - REGISTRIES.indexOf(right.registry);
|
|
117
|
+
return (
|
|
118
|
+
registryOrder ||
|
|
119
|
+
[left.manifest, left.name]
|
|
120
|
+
.join("\0")
|
|
121
|
+
.localeCompare([right.manifest, right.name].join("\0"))
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const inspection = {
|
|
126
|
+
schema_version: 1,
|
|
127
|
+
repository: {
|
|
128
|
+
root,
|
|
129
|
+
...coordinates,
|
|
130
|
+
release_workflow: releaseWorkflow(publishers),
|
|
131
|
+
},
|
|
132
|
+
packages,
|
|
133
|
+
};
|
|
134
|
+
const pages = pagesWorkflow(workflows);
|
|
135
|
+
if (pages) {
|
|
136
|
+
inspection.repository.pages_workflow = pages;
|
|
137
|
+
}
|
|
138
|
+
if (options.includeSkipped && skipped.length > 0) {
|
|
139
|
+
inspection.skipped = skipped.sort((left, right) =>
|
|
140
|
+
left.manifest.localeCompare(right.manifest),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return inspection;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Names the workflow that npm publishes from, or else the first registry's,
|
|
148
|
+
* when exactly one workflow file publishes it with an OIDC token.
|
|
149
|
+
*/
|
|
150
|
+
function releaseWorkflow(publishers) {
|
|
151
|
+
for (const registry of TRUSTED_REGISTRIES) {
|
|
152
|
+
const workflow = publishers.get(registry)?.workflow;
|
|
153
|
+
if (workflow) {
|
|
154
|
+
return workflow;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function withPublisher(item, workflows, publishers) {
|
|
161
|
+
const publisher = item.publishable ? publishers.get(item.registry) : null;
|
|
162
|
+
if (!publisher) {
|
|
163
|
+
return item;
|
|
164
|
+
}
|
|
165
|
+
const result = { ...item };
|
|
166
|
+
if (publisher.workflow) {
|
|
167
|
+
result.workflow = publisher.workflow;
|
|
168
|
+
}
|
|
169
|
+
if (publisher.jobs.length > 0) {
|
|
170
|
+
result.workflow_jobs = publisher.jobs;
|
|
171
|
+
}
|
|
172
|
+
if (publisher.environment) {
|
|
173
|
+
result.environment = publisher.environment;
|
|
174
|
+
}
|
|
175
|
+
if (publisher.candidates.length > 1) {
|
|
176
|
+
result.workflow_candidates = publisher.candidates;
|
|
177
|
+
}
|
|
178
|
+
const secrets = tokenSecrets(workflows, item.registry);
|
|
179
|
+
const warnings = [
|
|
180
|
+
...publisher.warnings,
|
|
181
|
+
...secrets.map((secret) => tokenSecretWarning(item.registry, secret)),
|
|
182
|
+
];
|
|
183
|
+
if (warnings.length > 0) {
|
|
184
|
+
result.warnings = [...(item.warnings ?? []), ...warnings];
|
|
185
|
+
}
|
|
186
|
+
if (secrets.length > 0) {
|
|
187
|
+
result.token_secrets = [...new Set(secrets.map((item) => item.secret))];
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function collectManifests(directory, manifests, dockerfiles) {
|
|
193
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
194
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const item = path.join(directory, entry.name);
|
|
197
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
|
|
198
|
+
await collectManifests(item, manifests, dockerfiles);
|
|
199
|
+
} else if (entry.isFile() && CONTAINER_FILES.has(entry.name)) {
|
|
200
|
+
dockerfiles.push(item);
|
|
201
|
+
} else if (
|
|
202
|
+
entry.isFile() &&
|
|
203
|
+
(MANIFEST_NAMES.has(entry.name) ||
|
|
204
|
+
/\.(?:csproj|fsproj|vbproj)$/.test(entry.name))
|
|
205
|
+
) {
|
|
206
|
+
manifests.push(item);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function parseManifest(manifestPath, manifest) {
|
|
212
|
+
const contents = await readManifest(manifestPath);
|
|
213
|
+
const filename = path.basename(manifestPath);
|
|
214
|
+
switch (filename) {
|
|
215
|
+
case "package.json":
|
|
216
|
+
return parseNpm(contents, manifest);
|
|
217
|
+
case "Cargo.toml":
|
|
218
|
+
return parseCargo(contents, manifest);
|
|
219
|
+
case "pyproject.toml":
|
|
220
|
+
return parsePyproject(contents, manifest);
|
|
221
|
+
case "setup.py":
|
|
222
|
+
return packageInfo(
|
|
223
|
+
"pypi",
|
|
224
|
+
capture(contents, /\bname\s*=\s*['"]([^'"]+)/) ??
|
|
225
|
+
"unknown-python-package",
|
|
226
|
+
capture(contents, /\bversion\s*=\s*['"]([^'"]+)/),
|
|
227
|
+
manifest,
|
|
228
|
+
);
|
|
229
|
+
case "go.mod":
|
|
230
|
+
return packageInfo(
|
|
231
|
+
"go-modules",
|
|
232
|
+
capture(contents, /^\s*module\s+(\S+)/m) ?? "unknown-go-module",
|
|
233
|
+
null,
|
|
234
|
+
manifest,
|
|
235
|
+
);
|
|
236
|
+
case "pom.xml":
|
|
237
|
+
return parseMaven(contents, manifest);
|
|
238
|
+
case "build.gradle":
|
|
239
|
+
case "build.gradle.kts":
|
|
240
|
+
return parseGradle(contents, manifest);
|
|
241
|
+
case "composer.json":
|
|
242
|
+
return parseComposer(contents, manifest);
|
|
243
|
+
default:
|
|
244
|
+
if (/\.(?:csproj|fsproj|vbproj)$/.test(filename)) {
|
|
245
|
+
return parseDotnet(contents, manifestPath, manifest);
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseNpm(contents, manifest) {
|
|
252
|
+
const metadata = parseJson(contents, manifest);
|
|
253
|
+
if (!metadata.name) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const publishable = metadata.private !== true;
|
|
257
|
+
return packageInfo(
|
|
258
|
+
"npm",
|
|
259
|
+
metadata.name,
|
|
260
|
+
metadata.version ?? null,
|
|
261
|
+
manifest,
|
|
262
|
+
publishable,
|
|
263
|
+
publishable ? [] : ["package.json marks this package as private"],
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseCargo(contents, manifest) {
|
|
268
|
+
const section = tomlSection(contents, "package");
|
|
269
|
+
if (!section) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const name = tomlString(section, "name");
|
|
273
|
+
if (!name) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const publishable = !/^\s*publish\s*=\s*false\s*$/m.test(section);
|
|
277
|
+
return packageInfo(
|
|
278
|
+
"crates-io",
|
|
279
|
+
name,
|
|
280
|
+
tomlString(section, "version"),
|
|
281
|
+
manifest,
|
|
282
|
+
publishable,
|
|
283
|
+
publishable ? [] : ["Cargo.toml disables publishing"],
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parsePyproject(contents, manifest) {
|
|
288
|
+
const section =
|
|
289
|
+
tomlSection(contents, "project") ?? tomlSection(contents, "tool.poetry");
|
|
290
|
+
const name = section && tomlString(section, "name");
|
|
291
|
+
if (!name) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return packageInfo("pypi", name, tomlString(section, "version"), manifest);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseDotnet(contents, manifestPath, manifest) {
|
|
298
|
+
return packageInfo(
|
|
299
|
+
"nuget",
|
|
300
|
+
xmlTag(contents, "PackageId") ??
|
|
301
|
+
xmlTag(contents, "AssemblyName") ??
|
|
302
|
+
path.basename(manifestPath, path.extname(manifestPath)),
|
|
303
|
+
xmlTag(contents, "PackageVersion") ?? xmlTag(contents, "Version"),
|
|
304
|
+
manifest,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseMaven(contents, manifest) {
|
|
309
|
+
const artifact = xmlTag(contents, "artifactId") ?? "unknown-maven-artifact";
|
|
310
|
+
const group = xmlTag(contents, "groupId");
|
|
311
|
+
return packageInfo(
|
|
312
|
+
"maven-central",
|
|
313
|
+
group ? `${group}:${artifact}` : artifact,
|
|
314
|
+
xmlTag(contents, "version"),
|
|
315
|
+
manifest,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseGradle(contents, manifest) {
|
|
320
|
+
const group = capture(contents, /^\s*group\s*=\s*['"]([^'"]+)/m);
|
|
321
|
+
const artifact =
|
|
322
|
+
capture(
|
|
323
|
+
contents,
|
|
324
|
+
/^\s*(?:archivesBaseName|rootProject\.name)\s*=\s*['"]([^'"]+)/m,
|
|
325
|
+
) ?? "gradle-project";
|
|
326
|
+
return packageInfo(
|
|
327
|
+
"maven-central",
|
|
328
|
+
group ? `${group}:${artifact}` : artifact,
|
|
329
|
+
capture(contents, /^\s*version\s*=\s*['"]([^'"]+)/m),
|
|
330
|
+
manifest,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function parseComposer(contents, manifest) {
|
|
335
|
+
const metadata = parseJson(contents, manifest);
|
|
336
|
+
if (!metadata.name) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return packageInfo(
|
|
340
|
+
"packagist",
|
|
341
|
+
metadata.name,
|
|
342
|
+
metadata.version ?? null,
|
|
343
|
+
manifest,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function packageInfo(
|
|
348
|
+
registry,
|
|
349
|
+
name,
|
|
350
|
+
version,
|
|
351
|
+
manifest,
|
|
352
|
+
publishable = true,
|
|
353
|
+
problems = [],
|
|
354
|
+
) {
|
|
355
|
+
const result = {
|
|
356
|
+
registry,
|
|
357
|
+
name,
|
|
358
|
+
version: version ?? null,
|
|
359
|
+
manifest,
|
|
360
|
+
publishable,
|
|
361
|
+
};
|
|
362
|
+
if (problems.length > 0) {
|
|
363
|
+
result.problems = problems;
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Editors on Windows may save a byte order mark; npm and Cargo accept it.
|
|
369
|
+
async function readManifest(manifest) {
|
|
370
|
+
return (await readFile(manifest, "utf8")).replace(/^\uFEFF/, "");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function parseJson(contents, manifest) {
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(contents);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw new Error(`invalid JSON in ${manifest}: ${error.message}`, {
|
|
378
|
+
cause: error,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function tomlSection(contents, name) {
|
|
384
|
+
const escaped = name.replaceAll(".", "\\.");
|
|
385
|
+
return capture(
|
|
386
|
+
contents,
|
|
387
|
+
new RegExp(`^\\[${escaped}\\]\\s*$([\\s\\S]*?)(?=^\\[|(?![\\s\\S]))`, "m"),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function tomlString(section, key) {
|
|
392
|
+
return capture(section, new RegExp(`^\\s*${key}\\s*=\\s*['"]([^'"]+)`, "m"));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function xmlTag(contents, tag) {
|
|
396
|
+
return capture(
|
|
397
|
+
contents,
|
|
398
|
+
new RegExp(`<${tag}(?:\\s[^>]*)?>\\s*([^<]+?)\\s*</${tag}>`, "s"),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function capture(contents, expression) {
|
|
403
|
+
return expression.exec(contents)?.[1] ?? null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function relativePath(root, item) {
|
|
407
|
+
return path.relative(root, item).split(path.sep).join("/");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function githubCoordinates(root, manifests) {
|
|
411
|
+
let contents;
|
|
412
|
+
try {
|
|
413
|
+
contents = await readFile(path.join(root, ".git/config"), "utf8");
|
|
414
|
+
} catch {
|
|
415
|
+
contents = "";
|
|
416
|
+
}
|
|
417
|
+
const remote = capture(contents, /^\s*url\s*=\s*(\S+)/m);
|
|
418
|
+
const remoteCoordinates = parseGithubUrl(remote);
|
|
419
|
+
if (remoteCoordinates) {
|
|
420
|
+
return remoteCoordinates;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const manifest of manifests) {
|
|
424
|
+
const filename = path.basename(manifest);
|
|
425
|
+
contents = await readManifest(manifest);
|
|
426
|
+
let repository;
|
|
427
|
+
if (filename === "package.json") {
|
|
428
|
+
const metadata = parseJson(contents, relativePath(root, manifest));
|
|
429
|
+
repository =
|
|
430
|
+
typeof metadata.repository === "string"
|
|
431
|
+
? metadata.repository
|
|
432
|
+
: metadata.repository?.url;
|
|
433
|
+
} else if (filename === "Cargo.toml") {
|
|
434
|
+
const packageSection = tomlSection(contents, "package");
|
|
435
|
+
repository = packageSection && tomlString(packageSection, "repository");
|
|
436
|
+
}
|
|
437
|
+
const coordinates = parseGithubUrl(repository);
|
|
438
|
+
if (coordinates) {
|
|
439
|
+
return coordinates;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { github_owner: null, github_repository: null };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseGithubUrl(remote) {
|
|
446
|
+
const normalized = remote
|
|
447
|
+
?.replace(/\.git$/, "")
|
|
448
|
+
.replace(/^git\+/, "")
|
|
449
|
+
.replace("git@github.com:", "https://github.com/")
|
|
450
|
+
.replace("ssh://git@github.com/", "https://github.com/");
|
|
451
|
+
if (!normalized?.startsWith("https://github.com/")) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const [github_owner, github_repository] = normalized
|
|
455
|
+
.slice("https://github.com/".length)
|
|
456
|
+
.split("/");
|
|
457
|
+
return {
|
|
458
|
+
github_owner: github_owner ?? null,
|
|
459
|
+
github_repository: github_repository ?? null,
|
|
460
|
+
};
|
|
461
|
+
}
|