metascope 0.1.0 → 0.2.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/dist/.DS_Store +0 -0
- package/dist/bin/cli.js +14 -14
- package/dist/lib/{chunk-DrSxFLj_.js → _virtual/_rolldown/runtime.js} +1 -1
- package/dist/lib/file-matching.js +152 -0
- package/dist/lib/index.d.ts +11 -1496
- package/dist/lib/index.js +6 -6215
- package/dist/lib/log.d.ts +11 -0
- package/dist/lib/log.js +20 -0
- package/dist/lib/metadata-types.d.ts +151 -0
- package/dist/lib/metadata-types.js +30 -0
- package/dist/lib/metadata.d.ts +16 -0
- package/dist/lib/metadata.js +235 -0
- package/dist/lib/package.js +5 -0
- package/dist/lib/parsers/configparser-parser.js +43 -0
- package/dist/lib/parsers/gemspec-parser.js +256 -0
- package/dist/lib/parsers/go-mod-parser.js +153 -0
- package/dist/lib/parsers/makefile-config-parser.js +102 -0
- package/dist/lib/parsers/properties-parser.js +31 -0
- package/dist/lib/parsers/rfc822-header-parser.js +48 -0
- package/dist/lib/parsers/setup-py-parser.js +173 -0
- package/dist/lib/source.d.ts +17 -0
- package/dist/lib/source.js +34 -0
- package/dist/lib/sources/arduino-library-properties.d.ts +45 -0
- package/dist/lib/sources/arduino-library-properties.js +208 -0
- package/dist/lib/sources/cinder-cinderblock-xml.d.ts +21 -0
- package/dist/lib/sources/cinder-cinderblock-xml.js +134 -0
- package/dist/lib/sources/code-stats.d.ts +14 -0
- package/dist/lib/sources/code-stats.js +40 -0
- package/dist/lib/sources/codemeta-json.d.ts +117 -0
- package/dist/lib/sources/codemeta-json.js +226 -0
- package/dist/lib/sources/dependency-updates.d.ts +22 -0
- package/dist/lib/sources/dependency-updates.js +132 -0
- package/dist/lib/sources/file-stats.d.ts +12 -0
- package/dist/lib/sources/file-stats.js +48 -0
- package/dist/lib/sources/git-config.d.ts +8 -0
- package/dist/lib/sources/git-config.js +21 -0
- package/dist/lib/sources/git-stats.d.ts +35 -0
- package/dist/lib/sources/git-stats.js +130 -0
- package/dist/lib/sources/github.d.ts +94 -0
- package/dist/lib/sources/github.js +399 -0
- package/dist/lib/sources/go-go-mod.d.ts +19 -0
- package/dist/lib/sources/go-go-mod.js +38 -0
- package/dist/lib/sources/go-goreleaser-yaml.d.ts +19 -0
- package/dist/lib/sources/go-goreleaser-yaml.js +152 -0
- package/dist/lib/sources/java-pom-xml.d.ts +52 -0
- package/dist/lib/sources/java-pom-xml.js +248 -0
- package/dist/lib/sources/license-file.d.ts +10 -0
- package/dist/lib/sources/license-file.js +26 -0
- package/dist/lib/sources/metadata-file.d.ts +14 -0
- package/dist/lib/sources/metadata-file.js +109 -0
- package/dist/lib/sources/metascope.d.ts +14 -0
- package/dist/lib/sources/metascope.js +35 -0
- package/dist/lib/sources/node-npm-registry.d.ts +19 -0
- package/dist/lib/sources/node-npm-registry.js +74 -0
- package/dist/lib/sources/node-package-json.d.ts +7 -0
- package/dist/lib/sources/node-package-json.js +27 -0
- package/dist/lib/sources/obsidian-plugin-manifest-json.d.ts +17 -0
- package/dist/lib/sources/obsidian-plugin-manifest-json.js +34 -0
- package/dist/lib/sources/obsidian-plugin-registry.d.ts +10 -0
- package/dist/lib/sources/obsidian-plugin-registry.js +44 -0
- package/dist/lib/sources/openframeworks-addon-config-mk.d.ts +17 -0
- package/dist/lib/sources/openframeworks-addon-config-mk.js +39 -0
- package/dist/lib/sources/openframeworks-install-xml.d.ts +20 -0
- package/dist/lib/sources/openframeworks-install-xml.js +153 -0
- package/dist/lib/sources/processing-library-properties.d.ts +44 -0
- package/dist/lib/sources/processing-library-properties.js +219 -0
- package/dist/lib/sources/processing-sketch-properties.d.ts +38 -0
- package/dist/lib/sources/processing-sketch-properties.js +185 -0
- package/dist/lib/sources/publiccode-yaml.d.ts +73 -0
- package/dist/lib/sources/publiccode-yaml.js +256 -0
- package/dist/lib/sources/python-pkg-info.d.ts +31 -0
- package/dist/lib/sources/python-pkg-info.js +115 -0
- package/dist/lib/sources/python-pypi-registry.d.ts +19 -0
- package/dist/lib/sources/python-pypi-registry.js +101 -0
- package/dist/lib/sources/python-pyproject-toml.d.ts +7 -0
- package/dist/lib/sources/python-pyproject-toml.js +30 -0
- package/dist/lib/sources/python-setup-cfg.d.ts +28 -0
- package/dist/lib/sources/python-setup-cfg.js +106 -0
- package/dist/lib/sources/python-setup-py.d.ts +28 -0
- package/dist/lib/sources/python-setup-py.js +48 -0
- package/dist/lib/sources/readme-file.d.ts +11 -0
- package/dist/lib/sources/readme-file.js +55 -0
- package/dist/lib/sources/ruby-gemspec.d.ts +44 -0
- package/dist/lib/sources/ruby-gemspec.js +62 -0
- package/dist/lib/sources/rust-cargo-toml.d.ts +40 -0
- package/dist/lib/sources/rust-cargo-toml.js +159 -0
- package/dist/lib/sources/xcode-info-plist.d.ts +22 -0
- package/dist/lib/sources/xcode-info-plist.js +199 -0
- package/dist/lib/sources/xcode-project-pbxproj.d.ts +21 -0
- package/dist/lib/sources/xcode-project-pbxproj.js +222 -0
- package/dist/lib/templates/codemeta.d.ts +47 -0
- package/dist/lib/templates/codemeta.js +494 -0
- package/dist/lib/templates/frontmatter.d.ts +87 -0
- package/dist/lib/templates/frontmatter.js +111 -0
- package/dist/lib/templates/index.d.ts +181 -0
- package/dist/lib/templates/index.js +22 -0
- package/dist/lib/templates/metadata.d.ts +17 -0
- package/dist/lib/templates/metadata.js +35 -0
- package/dist/lib/templates/project.d.ts +39 -0
- package/dist/lib/templates/project.js +51 -0
- package/dist/lib/utilities/codemeta-helpers.d.ts +39 -0
- package/dist/lib/utilities/codemeta-helpers.js +83 -0
- package/dist/lib/utilities/fetch.js +43 -0
- package/dist/lib/utilities/formatting.js +28 -0
- package/dist/lib/utilities/license-identification.js +141 -0
- package/dist/lib/utilities/schema-primitives.js +47 -0
- package/dist/lib/utilities/template-helpers.d.ts +135 -0
- package/dist/lib/utilities/template-helpers.js +310 -0
- package/dist/lib/utilities/tree-sitter-wasm.js +30 -0
- package/package.json +6 -6
- package/readme.md +62 -15
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { getMatches } from "../file-matching.js";
|
|
2
|
+
import { defineSource } from "../source.js";
|
|
3
|
+
import { nonEmptyString, optionalUrl, stringArray } from "../utilities/schema-primitives.js";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
//#region src/lib/sources/publiccode-yaml.ts
|
|
9
|
+
/**
|
|
10
|
+
* Source and parser for `publiccode.yml` / `publiccode.yaml` files.
|
|
11
|
+
*
|
|
12
|
+
* Publiccode.yml is a metadata standard for public software repositories,
|
|
13
|
+
* primarily used in Europe (Italy, Netherlands, etc.).
|
|
14
|
+
* See: https://yml.publiccode.tools/
|
|
15
|
+
*
|
|
16
|
+
* Extracts project metadata including name, version, license, contacts,
|
|
17
|
+
* multi-language descriptions, dependencies, and categorization.
|
|
18
|
+
*/
|
|
19
|
+
const publiccodeContactEntrySchema = z.object({
|
|
20
|
+
affiliation: z.string().optional(),
|
|
21
|
+
email: z.string().optional(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
phone: z.string().optional()
|
|
24
|
+
});
|
|
25
|
+
const publiccodeContractorEntrySchema = z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
until: z.string().optional(),
|
|
28
|
+
website: z.string().optional()
|
|
29
|
+
});
|
|
30
|
+
const publiccodeDependencyEntrySchema = z.object({
|
|
31
|
+
category: z.enum([
|
|
32
|
+
"hardware",
|
|
33
|
+
"open",
|
|
34
|
+
"proprietary"
|
|
35
|
+
]),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
optional: z.boolean().optional(),
|
|
38
|
+
version: z.string().optional(),
|
|
39
|
+
versionMax: z.string().optional(),
|
|
40
|
+
versionMin: z.string().optional()
|
|
41
|
+
});
|
|
42
|
+
const publiccodeDescriptionSchema = z.object({
|
|
43
|
+
documentation: z.string().optional(),
|
|
44
|
+
features: z.array(z.string()),
|
|
45
|
+
genericName: z.string().optional(),
|
|
46
|
+
localisedName: z.string().optional(),
|
|
47
|
+
longDescription: z.string().optional(),
|
|
48
|
+
shortDescription: z.string().optional()
|
|
49
|
+
});
|
|
50
|
+
const publiccodeSchema = z.object({
|
|
51
|
+
applicationSuite: nonEmptyString,
|
|
52
|
+
availableLanguages: stringArray,
|
|
53
|
+
categories: stringArray,
|
|
54
|
+
contacts: z.array(publiccodeContactEntrySchema),
|
|
55
|
+
contractors: z.array(publiccodeContractorEntrySchema),
|
|
56
|
+
dependencies: z.array(publiccodeDependencyEntrySchema),
|
|
57
|
+
description: publiccodeDescriptionSchema.optional(),
|
|
58
|
+
descriptions: z.record(z.string(), publiccodeDescriptionSchema),
|
|
59
|
+
developmentStatus: nonEmptyString,
|
|
60
|
+
inputTypes: stringArray,
|
|
61
|
+
isBasedOn: optionalUrl,
|
|
62
|
+
landingUrl: optionalUrl,
|
|
63
|
+
license: nonEmptyString,
|
|
64
|
+
localisationReady: z.boolean().optional(),
|
|
65
|
+
logo: optionalUrl,
|
|
66
|
+
mainCopyrightOwner: nonEmptyString,
|
|
67
|
+
maintenanceType: nonEmptyString,
|
|
68
|
+
monochromeLogo: optionalUrl,
|
|
69
|
+
name: nonEmptyString,
|
|
70
|
+
outputTypes: stringArray,
|
|
71
|
+
platforms: stringArray,
|
|
72
|
+
publiccodeYmlVersion: nonEmptyString,
|
|
73
|
+
releaseDate: nonEmptyString,
|
|
74
|
+
repoOwner: nonEmptyString,
|
|
75
|
+
roadmap: optionalUrl,
|
|
76
|
+
softwareType: nonEmptyString,
|
|
77
|
+
softwareVersion: nonEmptyString,
|
|
78
|
+
url: optionalUrl,
|
|
79
|
+
usedBy: stringArray
|
|
80
|
+
});
|
|
81
|
+
/** Coerce YAML values that may be parsed as non-strings back to strings. */
|
|
82
|
+
function toString(value) {
|
|
83
|
+
if (typeof value === "string") return value;
|
|
84
|
+
if (typeof value === "number") return String(value);
|
|
85
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
86
|
+
}
|
|
87
|
+
/** Check if a value is a non-empty string. */
|
|
88
|
+
function isNonEmptyString(value) {
|
|
89
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
90
|
+
}
|
|
91
|
+
/** Check if a value is a plain object (not null, not array). */
|
|
92
|
+
function isPlainObject(value) {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
94
|
+
}
|
|
95
|
+
/** Extract string array from a YAML value, filtering non-strings. */
|
|
96
|
+
function toStringArray(value) {
|
|
97
|
+
if (!Array.isArray(value)) return [];
|
|
98
|
+
return value.filter((item) => typeof item === "string");
|
|
99
|
+
}
|
|
100
|
+
/** Parse a description block from a publiccode description object. */
|
|
101
|
+
function parseDescription(data) {
|
|
102
|
+
const features = [];
|
|
103
|
+
if (Array.isArray(data.features)) {
|
|
104
|
+
for (const feature of data.features) if (typeof feature === "string") features.push(feature);
|
|
105
|
+
else if (isPlainObject(feature)) for (const [key, value] of Object.entries(feature)) features.push(typeof value === "string" ? `${key}: ${value}` : key);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
...isNonEmptyString(data.documentation) ? { documentation: data.documentation } : {},
|
|
109
|
+
features,
|
|
110
|
+
...isNonEmptyString(data.genericName) ? { genericName: data.genericName } : {},
|
|
111
|
+
...isNonEmptyString(data.localisedName) ? { localisedName: data.localisedName } : {},
|
|
112
|
+
...isNonEmptyString(data.longDescription) ? { longDescription: data.longDescription.trim() } : {},
|
|
113
|
+
...isNonEmptyString(data.shortDescription) ? { shortDescription: data.shortDescription.trim() } : {}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse a publiccode.yml / publiccode.yaml file content.
|
|
118
|
+
* @param content - Raw YAML file content.
|
|
119
|
+
* @returns Parsed publiccode metadata, or `undefined` if the content is invalid.
|
|
120
|
+
*/
|
|
121
|
+
function parse$1(content) {
|
|
122
|
+
let data;
|
|
123
|
+
try {
|
|
124
|
+
data = parse(content);
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!isPlainObject(data)) return void 0;
|
|
129
|
+
if (!isNonEmptyString(data.name) && !isNonEmptyString(data.url)) return void 0;
|
|
130
|
+
const descriptions = {};
|
|
131
|
+
let description;
|
|
132
|
+
if (isPlainObject(data.description)) {
|
|
133
|
+
const descObject = data.description;
|
|
134
|
+
for (const [lang, langData] of Object.entries(descObject)) if (isPlainObject(langData)) descriptions[lang] = parseDescription(langData);
|
|
135
|
+
const preferredLang = "en" in descriptions ? "en" : Object.keys(descriptions)[0];
|
|
136
|
+
if (preferredLang) description = descriptions[preferredLang];
|
|
137
|
+
}
|
|
138
|
+
const contacts = [];
|
|
139
|
+
if (isPlainObject(data.maintenance)) {
|
|
140
|
+
const { maintenance } = data;
|
|
141
|
+
if (Array.isArray(maintenance.contacts)) {
|
|
142
|
+
for (const contact of maintenance.contacts) if (isPlainObject(contact) && isNonEmptyString(contact.name)) {
|
|
143
|
+
const entry = { name: contact.name };
|
|
144
|
+
if (isNonEmptyString(contact.email)) entry.email = contact.email;
|
|
145
|
+
if (isNonEmptyString(contact.phone)) entry.phone = contact.phone;
|
|
146
|
+
if (isNonEmptyString(contact.affiliation)) entry.affiliation = contact.affiliation;
|
|
147
|
+
contacts.push(entry);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const contractors = [];
|
|
152
|
+
if (isPlainObject(data.maintenance)) {
|
|
153
|
+
const { maintenance } = data;
|
|
154
|
+
if (Array.isArray(maintenance.contractors)) {
|
|
155
|
+
for (const contractor of maintenance.contractors) if (isPlainObject(contractor) && isNonEmptyString(contractor.name)) {
|
|
156
|
+
const entry = { name: contractor.name };
|
|
157
|
+
const until = toString(contractor.until);
|
|
158
|
+
if (until) entry.until = until;
|
|
159
|
+
if (isNonEmptyString(contractor.website)) entry.website = contractor.website;
|
|
160
|
+
contractors.push(entry);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const dependencies = [];
|
|
165
|
+
if (isPlainObject(data.dependsOn)) {
|
|
166
|
+
const { dependsOn } = data;
|
|
167
|
+
for (const category of [
|
|
168
|
+
"open",
|
|
169
|
+
"proprietary",
|
|
170
|
+
"hardware"
|
|
171
|
+
]) {
|
|
172
|
+
const categoryDeps = dependsOn[category];
|
|
173
|
+
if (Array.isArray(categoryDeps)) {
|
|
174
|
+
for (const dep of categoryDeps) if (isPlainObject(dep) && isNonEmptyString(dep.name)) {
|
|
175
|
+
const entry = {
|
|
176
|
+
category,
|
|
177
|
+
name: dep.name
|
|
178
|
+
};
|
|
179
|
+
const version = toString(dep.version);
|
|
180
|
+
if (version) entry.version = version;
|
|
181
|
+
const versionMin = toString(dep.versionMin);
|
|
182
|
+
if (versionMin) entry.versionMin = versionMin;
|
|
183
|
+
const versionMax = toString(dep.versionMax);
|
|
184
|
+
if (versionMax) entry.versionMax = versionMax;
|
|
185
|
+
if (typeof dep.optional === "boolean") entry.optional = dep.optional;
|
|
186
|
+
dependencies.push(entry);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let availableLanguages = [];
|
|
192
|
+
let localisationReady;
|
|
193
|
+
if (isPlainObject(data.localisation)) {
|
|
194
|
+
const loc = data.localisation;
|
|
195
|
+
availableLanguages = toStringArray(loc.availableLanguages);
|
|
196
|
+
if (typeof loc.localisationReady === "boolean") localisationReady = loc.localisationReady;
|
|
197
|
+
}
|
|
198
|
+
let license;
|
|
199
|
+
let mainCopyrightOwner;
|
|
200
|
+
let repoOwner;
|
|
201
|
+
if (isPlainObject(data.legal)) {
|
|
202
|
+
const { legal } = data;
|
|
203
|
+
if (isNonEmptyString(legal.license)) license = legal.license;
|
|
204
|
+
if (isNonEmptyString(legal.mainCopyrightOwner)) mainCopyrightOwner = legal.mainCopyrightOwner;
|
|
205
|
+
if (isNonEmptyString(legal.repoOwner)) repoOwner = legal.repoOwner;
|
|
206
|
+
}
|
|
207
|
+
const version = toString(data.softwareVersion);
|
|
208
|
+
const releaseDate = toString(data.releaseDate);
|
|
209
|
+
return publiccodeSchema.parse({
|
|
210
|
+
...isNonEmptyString(data.applicationSuite) ? { applicationSuite: data.applicationSuite } : {},
|
|
211
|
+
availableLanguages,
|
|
212
|
+
categories: toStringArray(data.categories),
|
|
213
|
+
contacts,
|
|
214
|
+
contractors,
|
|
215
|
+
dependencies,
|
|
216
|
+
...description ? { description } : {},
|
|
217
|
+
descriptions,
|
|
218
|
+
...isNonEmptyString(data.developmentStatus) ? { developmentStatus: data.developmentStatus } : {},
|
|
219
|
+
inputTypes: toStringArray(data.inputTypes),
|
|
220
|
+
...isNonEmptyString(data.isBasedOn) ? { isBasedOn: data.isBasedOn } : {},
|
|
221
|
+
...isNonEmptyString(data.landingURL) ? { landingUrl: data.landingURL } : {},
|
|
222
|
+
...license ? { license } : {},
|
|
223
|
+
...localisationReady === void 0 ? {} : { localisationReady },
|
|
224
|
+
...isNonEmptyString(data.logo) ? { logo: data.logo } : {},
|
|
225
|
+
...mainCopyrightOwner ? { mainCopyrightOwner } : {},
|
|
226
|
+
...isPlainObject(data.maintenance) && isNonEmptyString(data.maintenance.type) ? { maintenanceType: data.maintenance.type } : {},
|
|
227
|
+
...isNonEmptyString(data.monochromeLogo) ? { monochromeLogo: data.monochromeLogo } : {},
|
|
228
|
+
...isNonEmptyString(data.name) ? { name: data.name } : {},
|
|
229
|
+
outputTypes: toStringArray(data.outputTypes),
|
|
230
|
+
platforms: toStringArray(data.platforms),
|
|
231
|
+
...isNonEmptyString(data.publiccodeYmlVersion) ? { publiccodeYmlVersion: data.publiccodeYmlVersion } : toString(data.publiccodeYmlVersion) ? { publiccodeYmlVersion: toString(data.publiccodeYmlVersion) } : {},
|
|
232
|
+
...releaseDate ? { releaseDate } : {},
|
|
233
|
+
...repoOwner ? { repoOwner } : {},
|
|
234
|
+
...isNonEmptyString(data.roadmap) ? { roadmap: data.roadmap } : {},
|
|
235
|
+
...isNonEmptyString(data.softwareType) ? { softwareType: data.softwareType } : {},
|
|
236
|
+
...version ? { softwareVersion: version } : {},
|
|
237
|
+
...isNonEmptyString(data.url) ? { url: data.url } : {},
|
|
238
|
+
usedBy: toStringArray(data.usedBy)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const publiccodeYamlSource = defineSource({
|
|
242
|
+
async discover(context) {
|
|
243
|
+
return getMatches(context.options, ["publiccode.yml", "publiccode.yaml"]);
|
|
244
|
+
},
|
|
245
|
+
key: "publiccodeYaml",
|
|
246
|
+
async parse(input, context) {
|
|
247
|
+
const data = parse$1(await readFile(resolve(context.options.path, input), "utf8"));
|
|
248
|
+
if (data !== void 0) return {
|
|
249
|
+
data,
|
|
250
|
+
source: input
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
phase: 1
|
|
254
|
+
});
|
|
255
|
+
//#endregion
|
|
256
|
+
export { publiccodeYamlSource };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { OneOrMany, SourceRecord } from "../source.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/sources/python-pkg-info.d.ts
|
|
5
|
+
/** Parsed PKG-INFO / METADATA metadata */
|
|
6
|
+
declare const pkgInfoDataSchema: z.ZodObject<{
|
|
7
|
+
author: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
8
|
+
author_email: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
9
|
+
classifiers: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
|
|
10
|
+
description: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
11
|
+
description_content_type: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
12
|
+
download_url: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
13
|
+
home_page: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
14
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
license: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
16
|
+
long_description: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
17
|
+
maintainer: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
18
|
+
maintainer_email: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
19
|
+
metadata_version: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
20
|
+
name: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
21
|
+
platforms: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
|
|
22
|
+
project_urls: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
23
|
+
requires_dist: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
|
|
24
|
+
requires_python: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
25
|
+
summary: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
26
|
+
version: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
type PkgInfo = z.infer<typeof pkgInfoDataSchema>;
|
|
29
|
+
type PythonPkgInfoData = OneOrMany<SourceRecord<PkgInfo>> | undefined;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { PythonPkgInfoData };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getMatches } from "../file-matching.js";
|
|
2
|
+
import { defineSource } from "../source.js";
|
|
3
|
+
import { nonEmptyString, optionalUrl, stringArray } from "../utilities/schema-primitives.js";
|
|
4
|
+
import { splitCommaSeparated } from "../utilities/template-helpers.js";
|
|
5
|
+
import { extractRfc822Body, parseRfc822Headers, splitMultiValues } from "../parsers/rfc822-header-parser.js";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
//#region src/lib/sources/python-pkg-info.ts
|
|
10
|
+
/** Parsed PKG-INFO / METADATA metadata */
|
|
11
|
+
const pkgInfoDataSchema = z.object({
|
|
12
|
+
author: nonEmptyString,
|
|
13
|
+
author_email: nonEmptyString,
|
|
14
|
+
classifiers: stringArray,
|
|
15
|
+
description: nonEmptyString,
|
|
16
|
+
description_content_type: nonEmptyString,
|
|
17
|
+
download_url: optionalUrl,
|
|
18
|
+
home_page: optionalUrl,
|
|
19
|
+
keywords: z.array(z.string()).optional(),
|
|
20
|
+
license: nonEmptyString,
|
|
21
|
+
long_description: nonEmptyString,
|
|
22
|
+
maintainer: nonEmptyString,
|
|
23
|
+
maintainer_email: nonEmptyString,
|
|
24
|
+
metadata_version: nonEmptyString,
|
|
25
|
+
name: nonEmptyString,
|
|
26
|
+
platforms: stringArray,
|
|
27
|
+
project_urls: z.record(z.string(), z.string()),
|
|
28
|
+
requires_dist: stringArray,
|
|
29
|
+
requires_python: nonEmptyString,
|
|
30
|
+
summary: nonEmptyString,
|
|
31
|
+
version: nonEmptyString
|
|
32
|
+
});
|
|
33
|
+
/** Simple header-to-field mappings. */
|
|
34
|
+
const HEADER_MAP = {
|
|
35
|
+
Author: "author",
|
|
36
|
+
"Author-email": "author_email",
|
|
37
|
+
"Description-Content-Type": "description_content_type",
|
|
38
|
+
"Download-URL": "download_url",
|
|
39
|
+
"Home-Page": "home_page",
|
|
40
|
+
"Home-page": "home_page",
|
|
41
|
+
License: "license",
|
|
42
|
+
Maintainer: "maintainer",
|
|
43
|
+
"Maintainer-email": "maintainer_email",
|
|
44
|
+
"Metadata-Version": "metadata_version",
|
|
45
|
+
Name: "name",
|
|
46
|
+
"Requires-Python": "requires_python",
|
|
47
|
+
Summary: "summary",
|
|
48
|
+
Version: "version"
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Parse a PKG-INFO or METADATA file and return structured metadata.
|
|
52
|
+
*
|
|
53
|
+
* Handles RFC 822-style headers with multi-value fields (Classifier,
|
|
54
|
+
* Requires-Dist, Project-URL, Platform), continuation lines, and
|
|
55
|
+
* the body section as long_description.
|
|
56
|
+
*/
|
|
57
|
+
function parse(source) {
|
|
58
|
+
const headers = parseRfc822Headers(source);
|
|
59
|
+
const data = {
|
|
60
|
+
author: void 0,
|
|
61
|
+
author_email: void 0,
|
|
62
|
+
classifiers: [],
|
|
63
|
+
description: void 0,
|
|
64
|
+
description_content_type: void 0,
|
|
65
|
+
download_url: void 0,
|
|
66
|
+
home_page: void 0,
|
|
67
|
+
keywords: void 0,
|
|
68
|
+
license: void 0,
|
|
69
|
+
long_description: void 0,
|
|
70
|
+
maintainer: void 0,
|
|
71
|
+
maintainer_email: void 0,
|
|
72
|
+
metadata_version: void 0,
|
|
73
|
+
name: void 0,
|
|
74
|
+
platforms: [],
|
|
75
|
+
project_urls: {},
|
|
76
|
+
requires_dist: [],
|
|
77
|
+
requires_python: void 0,
|
|
78
|
+
summary: void 0,
|
|
79
|
+
version: void 0
|
|
80
|
+
};
|
|
81
|
+
for (const [header, field] of Object.entries(HEADER_MAP)) {
|
|
82
|
+
const value = headers[header];
|
|
83
|
+
if (value && value !== "UNKNOWN") Object.assign(data, { [field]: value });
|
|
84
|
+
}
|
|
85
|
+
if (headers.Summary && headers.Summary !== "UNKNOWN") data.description = headers.Summary;
|
|
86
|
+
if (headers.Keywords && headers.Keywords !== "UNKNOWN") data.keywords = splitCommaSeparated(headers.Keywords);
|
|
87
|
+
data.classifiers = splitMultiValues(headers.Classifier);
|
|
88
|
+
data.platforms = splitMultiValues(headers.Platform);
|
|
89
|
+
data.requires_dist = splitMultiValues(headers["Requires-Dist"]);
|
|
90
|
+
if (headers["Project-URL"]) for (const line of splitMultiValues(headers["Project-URL"])) {
|
|
91
|
+
const commaIndex = line.indexOf(", ");
|
|
92
|
+
if (commaIndex > 0) {
|
|
93
|
+
const label = line.slice(0, commaIndex).trim();
|
|
94
|
+
const url = line.slice(commaIndex + 2).trim();
|
|
95
|
+
if (url) data.project_urls[label] = url;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
data.long_description = extractRfc822Body(source);
|
|
99
|
+
return pkgInfoDataSchema.parse(data);
|
|
100
|
+
}
|
|
101
|
+
const pythonPkgInfoSource = defineSource({
|
|
102
|
+
async discover(context) {
|
|
103
|
+
return getMatches(context.options, ["PKG-INFO"]);
|
|
104
|
+
},
|
|
105
|
+
key: "pythonPkgInfo",
|
|
106
|
+
async parse(input, context) {
|
|
107
|
+
return {
|
|
108
|
+
data: parse(await readFile(resolve(context.options.path, input), "utf8")),
|
|
109
|
+
source: input
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
phase: 1
|
|
113
|
+
});
|
|
114
|
+
//#endregion
|
|
115
|
+
export { pythonPkgInfoSource };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OneOrMany, SourceRecord } from "../source.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/sources/python-pypi-registry.d.ts
|
|
4
|
+
type PythonPypiRegistryInfo = {
|
|
5
|
+
/** Total downloads over the last 180 days. */downloads180Days?: number; /** Downloads in the last day. */
|
|
6
|
+
downloadsDaily?: number; /** Downloads in the last month. */
|
|
7
|
+
downloadsMonthly?: number; /** Downloads in the last week. */
|
|
8
|
+
downloadsWeekly?: number; /** ISO 8601 date the package was last published. */
|
|
9
|
+
publishDateLatest?: string; /** Total number of releases on PyPI. */
|
|
10
|
+
releaseCount?: number; /** Size in bytes of the latest release artifact. */
|
|
11
|
+
sizeBytes?: number; /** PyPI project URL. */
|
|
12
|
+
url?: string; /** Latest published version string. */
|
|
13
|
+
versionLatest?: string; /** Whether the latest version has been yanked. */
|
|
14
|
+
yanked?: boolean; /** Reason the version was yanked, if provided. */
|
|
15
|
+
yankedReason?: string;
|
|
16
|
+
};
|
|
17
|
+
type PythonPypiRegistryData = OneOrMany<SourceRecord<PythonPypiRegistryInfo>> | undefined;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { PythonPypiRegistryData };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { log } from "../log.js";
|
|
2
|
+
import { defineSource } from "../source.js";
|
|
3
|
+
import { ensureArray } from "../utilities/template-helpers.js";
|
|
4
|
+
import { fetchWithRetry } from "../utilities/fetch.js";
|
|
5
|
+
import { pythonPkgInfoSource } from "./python-pkg-info.js";
|
|
6
|
+
import { pythonPyprojectTomlSource } from "./python-pyproject-toml.js";
|
|
7
|
+
import { pythonSetupCfgSource } from "./python-setup-cfg.js";
|
|
8
|
+
import { pythonSetupPySource } from "./python-setup-py.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
//#region src/lib/sources/python-pypi-registry.ts
|
|
11
|
+
const pypiResponseSchema = z.object({
|
|
12
|
+
info: z.object({
|
|
13
|
+
version: z.string(),
|
|
14
|
+
yanked: z.boolean().optional(),
|
|
15
|
+
yanked_reason: z.string().nullable().optional()
|
|
16
|
+
}),
|
|
17
|
+
releases: z.record(z.string(), z.array(z.unknown())),
|
|
18
|
+
urls: z.array(z.object({
|
|
19
|
+
size: z.number().optional(),
|
|
20
|
+
upload_time_iso_8601: z.string().optional()
|
|
21
|
+
}))
|
|
22
|
+
});
|
|
23
|
+
const pypistatsRecentSchema = z.object({ data: z.object({
|
|
24
|
+
last_day: z.number(),
|
|
25
|
+
last_month: z.number(),
|
|
26
|
+
last_week: z.number()
|
|
27
|
+
}) });
|
|
28
|
+
const pypistatsOverallSchema = z.object({ data: z.array(z.object({
|
|
29
|
+
category: z.string(),
|
|
30
|
+
downloads: z.number()
|
|
31
|
+
})) });
|
|
32
|
+
const pythonPypiRegistrySource = defineSource({
|
|
33
|
+
async discover(context) {
|
|
34
|
+
let packageNames = [];
|
|
35
|
+
packageNames = ensureArray(context.metadata?.pythonPyprojectToml).map((value) => value.data.project?.name).filter((value) => value !== void 0);
|
|
36
|
+
if (packageNames.length === 0) packageNames = ensureArray(context.metadata?.pythonSetupCfg).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
37
|
+
if (packageNames.length === 0) packageNames = ensureArray(context.metadata?.pythonSetupPy).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
38
|
+
if (packageNames.length === 0) packageNames = ensureArray(context.metadata?.pythonPkgInfo).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
39
|
+
if (packageNames.length === 0) {
|
|
40
|
+
if (![
|
|
41
|
+
"pythonPyprojectToml",
|
|
42
|
+
"pythonSetupCfg",
|
|
43
|
+
"pythonSetupPy",
|
|
44
|
+
"pythonPkgInfo"
|
|
45
|
+
].some((key) => context.completedSources?.has(key))) {
|
|
46
|
+
log.warn(`Missing python package names in source context metadata for ${context.options.path}, extracting them now...`);
|
|
47
|
+
packageNames = ensureArray(await pythonPyprojectTomlSource.extract(context)).map((value) => value.data.project?.name).filter((value) => value !== void 0);
|
|
48
|
+
if (packageNames.length === 0) packageNames = ensureArray(await pythonSetupCfgSource.extract(context)).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
49
|
+
if (packageNames.length === 0) packageNames = ensureArray(await pythonSetupPySource.extract(context)).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
50
|
+
if (packageNames.length === 0) packageNames = ensureArray(await pythonPkgInfoSource.extract(context)).map((value) => value.data.name).filter((value) => value !== void 0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return packageNames;
|
|
54
|
+
},
|
|
55
|
+
key: "pythonPypiRegistry",
|
|
56
|
+
async parse(input) {
|
|
57
|
+
log.debug("Extracting PyPI metadata...");
|
|
58
|
+
const name = input;
|
|
59
|
+
const [pypiResult, pypistatsRecentResult, pypistatsOverallResult] = await Promise.all([
|
|
60
|
+
fetchWithRetry(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`).then(async (response) => {
|
|
61
|
+
if (!response.ok) return;
|
|
62
|
+
return pypiResponseSchema.parse(await response.json());
|
|
63
|
+
}).catch(() => void 0),
|
|
64
|
+
fetchWithRetry(`https://pypistats.org/api/packages/${encodeURIComponent(name)}/recent`).then(async (response) => {
|
|
65
|
+
if (!response.ok) return;
|
|
66
|
+
return pypistatsRecentSchema.parse(await response.json());
|
|
67
|
+
}).catch(() => void 0),
|
|
68
|
+
fetchWithRetry(`https://pypistats.org/api/packages/${encodeURIComponent(name)}/overall?mirrors=false`).then(async (response) => {
|
|
69
|
+
if (!response.ok) return;
|
|
70
|
+
return pypistatsOverallSchema.parse(await response.json());
|
|
71
|
+
}).catch(() => void 0)
|
|
72
|
+
]);
|
|
73
|
+
if (!pypiResult) return;
|
|
74
|
+
const latestUploadTime = pypiResult.urls[0]?.upload_time_iso_8601;
|
|
75
|
+
const sizeBytes = pypiResult.urls[0]?.size;
|
|
76
|
+
const info = {
|
|
77
|
+
publishDateLatest: latestUploadTime,
|
|
78
|
+
releaseCount: Object.keys(pypiResult.releases).length,
|
|
79
|
+
sizeBytes,
|
|
80
|
+
url: `https://pypi.org/project/${encodeURIComponent(name)}/`,
|
|
81
|
+
versionLatest: pypiResult.info.version
|
|
82
|
+
};
|
|
83
|
+
if (pypiResult.info.yanked) {
|
|
84
|
+
info.yanked = true;
|
|
85
|
+
if (pypiResult.info.yanked_reason) info.yankedReason = pypiResult.info.yanked_reason;
|
|
86
|
+
}
|
|
87
|
+
if (pypistatsRecentResult) {
|
|
88
|
+
info.downloadsDaily = pypistatsRecentResult.data.last_day;
|
|
89
|
+
info.downloadsMonthly = pypistatsRecentResult.data.last_month;
|
|
90
|
+
info.downloadsWeekly = pypistatsRecentResult.data.last_week;
|
|
91
|
+
}
|
|
92
|
+
if (pypistatsOverallResult) info.downloads180Days = pypistatsOverallResult.data.reduce((sum, entry) => sum + entry.downloads, 0) || void 0;
|
|
93
|
+
return {
|
|
94
|
+
data: info,
|
|
95
|
+
source: `https://pypi.org/project/${encodeURIComponent(name)}/`
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
phase: 2
|
|
99
|
+
});
|
|
100
|
+
//#endregion
|
|
101
|
+
export { pythonPypiRegistrySource };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OneOrMany, SourceRecord } from "../source.js";
|
|
2
|
+
import { PyprojectData } from "read-pyproject";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/sources/python-pyproject-toml.d.ts
|
|
5
|
+
type PythonPyprojectTomlData = OneOrMany<SourceRecord<PyprojectData>> | undefined;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { PythonPyprojectTomlData };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getMatches } from "../file-matching.js";
|
|
2
|
+
import { defineSource } from "../source.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { parsePyproject } from "read-pyproject";
|
|
6
|
+
//#region src/lib/sources/python-pyproject-toml.ts
|
|
7
|
+
/**
|
|
8
|
+
* Parse pyproject.toml content and return structured metadata.
|
|
9
|
+
*/
|
|
10
|
+
function parse(content) {
|
|
11
|
+
return parsePyproject(content, {
|
|
12
|
+
camelCase: true,
|
|
13
|
+
unknownKeyPolicy: "strip"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const pythonPyprojectTomlSource = defineSource({
|
|
17
|
+
async discover(context) {
|
|
18
|
+
return getMatches(context.options, ["pyproject.toml"]);
|
|
19
|
+
},
|
|
20
|
+
key: "pythonPyprojectToml",
|
|
21
|
+
async parse(input, context) {
|
|
22
|
+
return {
|
|
23
|
+
data: parse(await readFile(resolve(context.options.path, input), "utf8")),
|
|
24
|
+
source: input
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
phase: 1
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
export { pythonPyprojectTomlSource };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { OneOrMany, SourceRecord } from "../source.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/sources/python-setup-cfg.d.ts
|
|
5
|
+
declare const setupCfgDataSchema: z.ZodObject<{
|
|
6
|
+
author: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
7
|
+
author_email: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
8
|
+
classifiers: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
|
|
9
|
+
description: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
10
|
+
download_url: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
11
|
+
extras_require: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
12
|
+
install_requires: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
|
|
13
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
|
+
license: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
15
|
+
long_description: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
16
|
+
maintainer: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
17
|
+
maintainer_email: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
18
|
+
name: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
19
|
+
project_urls: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
20
|
+
python_requires: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
21
|
+
url: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
22
|
+
version: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
/** Parsed setup.cfg metadata */
|
|
25
|
+
type SetupCfg = z.infer<typeof setupCfgDataSchema>;
|
|
26
|
+
type PythonSetupCfgData = OneOrMany<SourceRecord<SetupCfg>> | undefined;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { PythonSetupCfgData };
|