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 { getRubyLanguage, initParser } from "../utilities/tree-sitter-wasm.js";
|
|
2
|
+
import is from "@sindresorhus/is";
|
|
3
|
+
//#region src/lib/parsers/gemspec-parser.ts
|
|
4
|
+
function emptySpec() {
|
|
5
|
+
return {
|
|
6
|
+
authors: [],
|
|
7
|
+
bindir: void 0,
|
|
8
|
+
cert_chain: [],
|
|
9
|
+
dependencies: [],
|
|
10
|
+
description: void 0,
|
|
11
|
+
email: void 0,
|
|
12
|
+
executables: [],
|
|
13
|
+
extensions: [],
|
|
14
|
+
extra: {},
|
|
15
|
+
extra_rdoc_files: [],
|
|
16
|
+
files: [],
|
|
17
|
+
homepage: void 0,
|
|
18
|
+
license: void 0,
|
|
19
|
+
licenses: [],
|
|
20
|
+
metadata: {},
|
|
21
|
+
name: void 0,
|
|
22
|
+
platform: void 0,
|
|
23
|
+
post_install_message: void 0,
|
|
24
|
+
rdoc_options: [],
|
|
25
|
+
require_paths: [],
|
|
26
|
+
required_ruby_version: void 0,
|
|
27
|
+
required_rubygems_version: void 0,
|
|
28
|
+
signing_key: void 0,
|
|
29
|
+
summary: void 0,
|
|
30
|
+
test_files: [],
|
|
31
|
+
version: void 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Filter nulls from web-tree-sitter's `namedChildren` array. */
|
|
35
|
+
function children(node) {
|
|
36
|
+
return node.namedChildren.filter((c) => c !== null);
|
|
37
|
+
}
|
|
38
|
+
/** Methods that return the receiver unchanged — safe to unwrap. */
|
|
39
|
+
const IDENTITY_METHODS = new Set([
|
|
40
|
+
"-@",
|
|
41
|
+
"dup",
|
|
42
|
+
"freeze"
|
|
43
|
+
]);
|
|
44
|
+
/** Extract the raw string value from a tree-sitter string/symbol node. */
|
|
45
|
+
function extractString(node) {
|
|
46
|
+
switch (node.type) {
|
|
47
|
+
case "call": {
|
|
48
|
+
const method = node.childForFieldName("method");
|
|
49
|
+
if (method && IDENTITY_METHODS.has(method.text)) {
|
|
50
|
+
const receiver = node.childForFieldName("receiver");
|
|
51
|
+
if (receiver) return extractString(receiver);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case "float":
|
|
56
|
+
case "integer": return node.text;
|
|
57
|
+
case "heredoc_body": return node.text.trim();
|
|
58
|
+
case "simple_symbol": return node.text.replace(/^:/, "");
|
|
59
|
+
case "string":
|
|
60
|
+
case "string_content": {
|
|
61
|
+
const parts = children(node).filter((c) => c.type === "string_content");
|
|
62
|
+
if (parts.length > 0) return parts.map((p) => p.text).join("");
|
|
63
|
+
return node.text.replaceAll(/^["']|["']$/g, "");
|
|
64
|
+
}
|
|
65
|
+
default: return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Extract a string array from an array node like `["a", "b"]`. */
|
|
69
|
+
function extractStringArray(node) {
|
|
70
|
+
if (node.type === "array") return children(node).map((element) => extractString(element)).filter((s) => s !== void 0);
|
|
71
|
+
if (node.type === "string_array") return children(node).map((c) => c.type === "bare_string" ? c.text : c.text);
|
|
72
|
+
const single = extractString(node);
|
|
73
|
+
return single === void 0 ? [] : [single];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Attempt to extract a usable value from an arbitrary RHS node.
|
|
77
|
+
* Returns string | string[] | null — we intentionally skip expressions
|
|
78
|
+
* we can't statically evaluate (method calls, constants, etc.).
|
|
79
|
+
*/
|
|
80
|
+
function extractValue(node) {
|
|
81
|
+
if (node.type === "array" || node.type === "string_array") return extractStringArray(node);
|
|
82
|
+
if (node.type === "call") {
|
|
83
|
+
const method = node.childForFieldName("method");
|
|
84
|
+
if (method && IDENTITY_METHODS.has(method.text)) {
|
|
85
|
+
const receiver = node.childForFieldName("receiver");
|
|
86
|
+
if (receiver) return extractValue(receiver);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (node.type === "true") return "true";
|
|
90
|
+
if (node.type === "false") return "false";
|
|
91
|
+
if (node.type === "nil") return void 0;
|
|
92
|
+
return extractString(node);
|
|
93
|
+
}
|
|
94
|
+
/** Resolve the attribute name from the LHS of `spec.foo = ...` */
|
|
95
|
+
function resolveAttribute(node) {
|
|
96
|
+
if (node.type === "call") return node.childForFieldName("method")?.text ?? void 0;
|
|
97
|
+
}
|
|
98
|
+
const DEP_METHODS = {
|
|
99
|
+
add_dependency: "runtime",
|
|
100
|
+
add_development_dependency: "development",
|
|
101
|
+
add_runtime_dependency: "runtime"
|
|
102
|
+
};
|
|
103
|
+
function tryParseDependency(node) {
|
|
104
|
+
if (node.type !== "call" && node.type !== "method_call") return void 0;
|
|
105
|
+
const methodNode = node.childForFieldName("method");
|
|
106
|
+
if (!methodNode) return void 0;
|
|
107
|
+
let methodName;
|
|
108
|
+
if (methodNode.type === "call") methodName = methodNode.childForFieldName("method")?.text ?? void 0;
|
|
109
|
+
else if (methodNode.type === "identifier") methodName = methodNode.text;
|
|
110
|
+
else methodName = methodNode.text;
|
|
111
|
+
if (!methodName) {
|
|
112
|
+
const topMethod = node.childForFieldName("method");
|
|
113
|
+
if (topMethod?.type === "identifier") methodName = topMethod.text;
|
|
114
|
+
}
|
|
115
|
+
if (!methodName || !DEP_METHODS[methodName]) {
|
|
116
|
+
for (const m of Object.keys(DEP_METHODS)) if (node.text.includes(`.${m}`)) {
|
|
117
|
+
methodName = m;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!methodName || !DEP_METHODS[methodName]) return void 0;
|
|
122
|
+
const depType = DEP_METHODS[methodName];
|
|
123
|
+
const arguments_ = node.childForFieldName("arguments");
|
|
124
|
+
if (!arguments_) return void 0;
|
|
125
|
+
const argumentNodes = children(arguments_);
|
|
126
|
+
if (argumentNodes.length === 0) return void 0;
|
|
127
|
+
const depName = extractString(argumentNodes[0]);
|
|
128
|
+
if (!depName) return void 0;
|
|
129
|
+
return {
|
|
130
|
+
name: depName,
|
|
131
|
+
requirements: argumentNodes.slice(1).map((element) => extractString(element)).filter((s) => s !== void 0),
|
|
132
|
+
type: depType
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function extractHash(node) {
|
|
136
|
+
const result = {};
|
|
137
|
+
if (node.type !== "hash") return result;
|
|
138
|
+
for (const pair of children(node)) {
|
|
139
|
+
if (pair.type !== "pair") continue;
|
|
140
|
+
const key = pair.childForFieldName("key");
|
|
141
|
+
const value = pair.childForFieldName("value");
|
|
142
|
+
if (!key || !value) continue;
|
|
143
|
+
const k = extractString(key);
|
|
144
|
+
const v = extractString(value);
|
|
145
|
+
if (k && v) result[k] = v;
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function setStringAttribute(spec, key, value) {
|
|
150
|
+
Object.assign(spec, { [key]: value });
|
|
151
|
+
}
|
|
152
|
+
function setArrayAttribute(spec, key, value) {
|
|
153
|
+
Object.assign(spec, { [key]: value });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Parse a `.gemspec` file's contents and return a plain object with the
|
|
157
|
+
* extracted fields.
|
|
158
|
+
*
|
|
159
|
+
* Uses tree-sitter with the Ruby grammar to walk the AST, so it can handle
|
|
160
|
+
* most real-world gemspec patterns without executing Ruby.
|
|
161
|
+
*
|
|
162
|
+
* Fields that reference Ruby constants (e.g. `Foo::VERSION`) or dynamic
|
|
163
|
+
* expressions (e.g. `Dir.glob(...)`) will be `null` / empty — the parser
|
|
164
|
+
* only extracts statically determinable values.
|
|
165
|
+
*/
|
|
166
|
+
async function parseGemspec(source) {
|
|
167
|
+
const parser = await initParser();
|
|
168
|
+
const ruby = await getRubyLanguage();
|
|
169
|
+
parser.setLanguage(ruby);
|
|
170
|
+
const tree = parser.parse(source);
|
|
171
|
+
if (!tree) throw new Error("Failed to parse gemspec source");
|
|
172
|
+
const spec = emptySpec();
|
|
173
|
+
/** Map of simple attribute names → setter logic */
|
|
174
|
+
const STRING_ATTRS = new Set([
|
|
175
|
+
"bindir",
|
|
176
|
+
"description",
|
|
177
|
+
"homepage",
|
|
178
|
+
"license",
|
|
179
|
+
"name",
|
|
180
|
+
"platform",
|
|
181
|
+
"post_install_message",
|
|
182
|
+
"required_ruby_version",
|
|
183
|
+
"required_rubygems_version",
|
|
184
|
+
"signing_key",
|
|
185
|
+
"summary",
|
|
186
|
+
"version"
|
|
187
|
+
]);
|
|
188
|
+
const ARRAY_ATTRS = new Set([
|
|
189
|
+
"authors",
|
|
190
|
+
"cert_chain",
|
|
191
|
+
"executables",
|
|
192
|
+
"extensions",
|
|
193
|
+
"extra_rdoc_files",
|
|
194
|
+
"files",
|
|
195
|
+
"licenses",
|
|
196
|
+
"rdoc_options",
|
|
197
|
+
"require_paths",
|
|
198
|
+
"test_files"
|
|
199
|
+
]);
|
|
200
|
+
function visit(node) {
|
|
201
|
+
if (node.type === "assignment") {
|
|
202
|
+
const lhs = node.childForFieldName("left");
|
|
203
|
+
const rhs = node.childForFieldName("right");
|
|
204
|
+
if (!lhs || !rhs) {
|
|
205
|
+
visitChildren(node);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const attribute = resolveAttribute(lhs);
|
|
209
|
+
if (!attribute) {
|
|
210
|
+
visitChildren(node);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (attribute === "email") {
|
|
214
|
+
const value = extractValue(rhs);
|
|
215
|
+
if (value !== void 0) spec.email = value;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (attribute === "metadata") {
|
|
219
|
+
if (rhs.type === "hash") spec.metadata = {
|
|
220
|
+
...is.plainObject(spec.metadata) ? spec.metadata : {},
|
|
221
|
+
...extractHash(rhs)
|
|
222
|
+
};
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (STRING_ATTRS.has(attribute)) {
|
|
226
|
+
const value = extractString(rhs);
|
|
227
|
+
if (value !== void 0) setStringAttribute(spec, attribute, value);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (ARRAY_ATTRS.has(attribute)) {
|
|
231
|
+
const array = extractStringArray(rhs);
|
|
232
|
+
if (array.length > 0) setArrayAttribute(spec, attribute, array);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const value = extractValue(rhs);
|
|
236
|
+
if (value !== void 0 && is.plainObject(spec.extra)) spec.extra[attribute] = value;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (node.type === "call" || node.type === "method_call") {
|
|
240
|
+
const dep = tryParseDependency(node);
|
|
241
|
+
if (dep) {
|
|
242
|
+
if (Array.isArray(spec.dependencies)) spec.dependencies.push(dep);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (node.type === "element_assignment" || node.type === "indexing") {}
|
|
247
|
+
visitChildren(node);
|
|
248
|
+
}
|
|
249
|
+
function visitChildren(node) {
|
|
250
|
+
for (const child of children(node)) visit(child);
|
|
251
|
+
}
|
|
252
|
+
visit(tree.rootNode);
|
|
253
|
+
return spec;
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
export { parseGemspec };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//#region src/lib/parsers/go-mod-parser.ts
|
|
2
|
+
/**
|
|
3
|
+
* Known source-repo hosts and the number of path segments that make a repo URL.
|
|
4
|
+
* e.g. github.com/owner/repo → 3 segments.
|
|
5
|
+
*/
|
|
6
|
+
const HOST_SEGMENTS = {
|
|
7
|
+
"bitbucket.com": 3,
|
|
8
|
+
"bitbucket.org": 3,
|
|
9
|
+
"codeberg.org": 3,
|
|
10
|
+
"git.sr.ht": 3,
|
|
11
|
+
"github.com": 3,
|
|
12
|
+
"gitlab.com": 3
|
|
13
|
+
};
|
|
14
|
+
/** Derive a repository URL from a Go module path, if on a known host. */
|
|
15
|
+
function moduleToRepoUrl(modulePath) {
|
|
16
|
+
const segments = modulePath.split("/");
|
|
17
|
+
const host = segments[0];
|
|
18
|
+
if (!host) return void 0;
|
|
19
|
+
const needed = HOST_SEGMENTS[host];
|
|
20
|
+
if (!needed || segments.length < needed) return void 0;
|
|
21
|
+
let repoPath = segments.slice(0, needed).join("/");
|
|
22
|
+
repoPath = repoPath.replace(/\/v\d+$/, "");
|
|
23
|
+
return `https://${repoPath}`;
|
|
24
|
+
}
|
|
25
|
+
/** Strip inline comments and trim whitespace. */
|
|
26
|
+
function stripComment(line) {
|
|
27
|
+
const index = line.indexOf("//");
|
|
28
|
+
return index === -1 ? line.trim() : line.slice(0, index).trim();
|
|
29
|
+
}
|
|
30
|
+
/** Check whether a line has an `// indirect` comment. */
|
|
31
|
+
function isIndirect(line) {
|
|
32
|
+
return /\/\/\s*indirect/.test(line);
|
|
33
|
+
}
|
|
34
|
+
/** Parse a require-style line: `module version [// indirect]` */
|
|
35
|
+
function parseRequireLine(line) {
|
|
36
|
+
const indirect = isIndirect(line);
|
|
37
|
+
const clean = stripComment(line);
|
|
38
|
+
const match = /^(\S+)\s+(\S+)/.exec(clean);
|
|
39
|
+
if (!match) return void 0;
|
|
40
|
+
const version = match[2].replace(/\+incompatible$/, "");
|
|
41
|
+
return {
|
|
42
|
+
indirect,
|
|
43
|
+
module: match[1],
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Parse a replace-style line: `old [version] => new version` or `old [version] => ./local` */
|
|
48
|
+
function parseReplaceLine(line) {
|
|
49
|
+
const parts = stripComment(line).split("=>");
|
|
50
|
+
if (parts.length !== 2) return void 0;
|
|
51
|
+
const left = parts[0].trim().split(/\s+/);
|
|
52
|
+
const right = parts[1].trim().split(/\s+/);
|
|
53
|
+
const from = left[0];
|
|
54
|
+
if (!from || right.length === 0) return void 0;
|
|
55
|
+
const target = right[0];
|
|
56
|
+
if (!target) return void 0;
|
|
57
|
+
if (target.startsWith("./") || target.startsWith("../") || target.startsWith("/")) return {
|
|
58
|
+
from,
|
|
59
|
+
to: "local"
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
from,
|
|
63
|
+
to: {
|
|
64
|
+
module: target,
|
|
65
|
+
version: (right[1] ?? "").replace(/\+incompatible$/, "")
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Parse a tool-style line: just a module path. */
|
|
70
|
+
function parseToolLine(line) {
|
|
71
|
+
const clean = stripComment(line).trim();
|
|
72
|
+
if (clean.length === 0) return void 0;
|
|
73
|
+
return clean.split(/\s+/)[0] || void 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse a go.mod file and return structured metadata.
|
|
77
|
+
*
|
|
78
|
+
* Extracts module identity, Go version, direct dependencies (skipping
|
|
79
|
+
* indirect ones), tool dependencies, and applies replace directives.
|
|
80
|
+
*/
|
|
81
|
+
function parseGoMod(source) {
|
|
82
|
+
const data = {
|
|
83
|
+
dependencies: [],
|
|
84
|
+
go_version: void 0,
|
|
85
|
+
module: void 0,
|
|
86
|
+
repository_url: void 0,
|
|
87
|
+
tool_dependencies: []
|
|
88
|
+
};
|
|
89
|
+
const directDeps = {};
|
|
90
|
+
const toolDeps = [];
|
|
91
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
92
|
+
let blockState = "none";
|
|
93
|
+
for (const rawLine of source.split("\n")) {
|
|
94
|
+
const line = rawLine.trim();
|
|
95
|
+
if (line === "" || line.startsWith("//") && blockState === "none") continue;
|
|
96
|
+
if (line === ")" || line.startsWith(")")) {
|
|
97
|
+
blockState = "none";
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (blockState !== "none") {
|
|
101
|
+
switch (blockState) {
|
|
102
|
+
case "replace": {
|
|
103
|
+
const rep = parseReplaceLine(line);
|
|
104
|
+
if (rep) replacements.set(rep.from, rep.to);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "require": {
|
|
108
|
+
const dep = parseRequireLine(line);
|
|
109
|
+
if (dep && !dep.indirect) directDeps[dep.module] = dep.version;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "skip": break;
|
|
113
|
+
case "tool": {
|
|
114
|
+
const tool = parseToolLine(line);
|
|
115
|
+
if (tool) toolDeps.push(tool);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith("module ")) data.module = line.slice(7).trim();
|
|
122
|
+
else if (line.startsWith("go ")) data.go_version = line.slice(3).trim();
|
|
123
|
+
else if (line.startsWith("require ")) if (line.includes("(")) blockState = "require";
|
|
124
|
+
else {
|
|
125
|
+
const dep = parseRequireLine(line.slice(8));
|
|
126
|
+
if (dep && !dep.indirect) directDeps[dep.module] = dep.version;
|
|
127
|
+
}
|
|
128
|
+
else if (line.startsWith("replace ")) if (line.includes("(")) blockState = "replace";
|
|
129
|
+
else {
|
|
130
|
+
const rep = parseReplaceLine(line.slice(8));
|
|
131
|
+
if (rep) replacements.set(rep.from, rep.to);
|
|
132
|
+
}
|
|
133
|
+
else if (line.startsWith("tool ")) if (line.includes("(")) blockState = "tool";
|
|
134
|
+
else {
|
|
135
|
+
const tool = parseToolLine(line.slice(5));
|
|
136
|
+
if (tool) toolDeps.push(tool);
|
|
137
|
+
}
|
|
138
|
+
else if ((line.startsWith("exclude ") || line.startsWith("retract ") || line.startsWith("godebug ") || line.startsWith("toolchain ")) && line.includes("(")) blockState = "skip";
|
|
139
|
+
}
|
|
140
|
+
for (const [from, to] of replacements) if (from in directDeps) {
|
|
141
|
+
delete directDeps[from];
|
|
142
|
+
if (to !== "local") directDeps[to.module] = to.version;
|
|
143
|
+
}
|
|
144
|
+
data.dependencies = Object.entries(directDeps).map(([module, version]) => ({
|
|
145
|
+
module,
|
|
146
|
+
version
|
|
147
|
+
}));
|
|
148
|
+
data.tool_dependencies = toolDeps;
|
|
149
|
+
if (data.module) data.repository_url = moduleToRepoUrl(data.module);
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { parseGoMod };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/lib/parsers/makefile-config-parser.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parser for Makefile-style configuration files.
|
|
4
|
+
*
|
|
5
|
+
* These files use a GNU Makefile-like syntax with section headers (lines
|
|
6
|
+
* ending in `:`) and variable assignments (`VAR = value` or `VAR += value`).
|
|
7
|
+
* This parser extracts metadata from the `meta:` section and structural
|
|
8
|
+
* information (dependencies, platform sections) from the rest of the file.
|
|
9
|
+
*
|
|
10
|
+
* The parser is intentionally simple — no Make variable expansion, no
|
|
11
|
+
* includes, no conditionals. It mirrors the line-by-line algorithm used
|
|
12
|
+
* by the openFrameworks Project Generator.
|
|
13
|
+
*/
|
|
14
|
+
/** Section header pattern: a word (with optional hyphens/slashes) followed by a colon. */
|
|
15
|
+
const SECTION_RE = /^[\w/][\w/-]*:$/;
|
|
16
|
+
/** Variable assignment pattern: VARNAME = value or VARNAME += value */
|
|
17
|
+
const ASSIGNMENT_RE = /^(\w+)\s*(\+?=)\s*(.*)/;
|
|
18
|
+
/**
|
|
19
|
+
* Sections that are not platform-specific and should be excluded
|
|
20
|
+
* from operatingSystem inference.
|
|
21
|
+
*/
|
|
22
|
+
const NON_PLATFORM_SECTIONS = new Set([
|
|
23
|
+
"all",
|
|
24
|
+
"common",
|
|
25
|
+
"meta"
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Parse a Makefile-style config file and return structured metadata.
|
|
29
|
+
*/
|
|
30
|
+
function parseMakefileConfig(content) {
|
|
31
|
+
const metaVariables = /* @__PURE__ */ new Map();
|
|
32
|
+
const commonDependencies = [];
|
|
33
|
+
const platformSectionsWithContent = /* @__PURE__ */ new Set();
|
|
34
|
+
let currentSection = "";
|
|
35
|
+
let currentSectionHasAssignment = false;
|
|
36
|
+
for (const rawLine of content.split("\n")) {
|
|
37
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
38
|
+
if (line.length === 0) continue;
|
|
39
|
+
if (SECTION_RE.test(line)) {
|
|
40
|
+
if (currentSectionHasAssignment && !NON_PLATFORM_SECTIONS.has(currentSection)) platformSectionsWithContent.add(currentSection);
|
|
41
|
+
currentSection = line.slice(0, -1);
|
|
42
|
+
currentSectionHasAssignment = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const match = ASSIGNMENT_RE.exec(line);
|
|
46
|
+
if (!match) continue;
|
|
47
|
+
const [, variableName, operator, rawValue] = match;
|
|
48
|
+
currentSectionHasAssignment = true;
|
|
49
|
+
if (currentSection === "meta") {
|
|
50
|
+
const values = tokenizeValues(rawValue);
|
|
51
|
+
if (operator === "+=") {
|
|
52
|
+
const existing = metaVariables.get(variableName) ?? [];
|
|
53
|
+
metaVariables.set(variableName, [...existing, ...values]);
|
|
54
|
+
} else metaVariables.set(variableName, values);
|
|
55
|
+
} else if (currentSection === "common" && variableName === "ADDON_DEPENDENCIES") {
|
|
56
|
+
const values = tokenizeValues(rawValue);
|
|
57
|
+
if (operator === "+=") commonDependencies.push(...values);
|
|
58
|
+
else {
|
|
59
|
+
commonDependencies.length = 0;
|
|
60
|
+
commonDependencies.push(...values);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (currentSectionHasAssignment && !NON_PLATFORM_SECTIONS.has(currentSection)) platformSectionsWithContent.add(currentSection);
|
|
65
|
+
return {
|
|
66
|
+
author: singleValue(metaVariables, "ADDON_AUTHOR"),
|
|
67
|
+
dependencies: commonDependencies,
|
|
68
|
+
description: singleValue(metaVariables, "ADDON_DESCRIPTION"),
|
|
69
|
+
name: singleValue(metaVariables, "ADDON_NAME"),
|
|
70
|
+
platformSections: [...platformSectionsWithContent],
|
|
71
|
+
tags: metaVariables.get("ADDON_TAGS") ?? [],
|
|
72
|
+
url: singleValue(metaVariables, "ADDON_URL")
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get a single string value from the meta variables map.
|
|
77
|
+
* Joins multiple tokens with spaces (for values split across `+=` lines).
|
|
78
|
+
* Returns undefined for empty/whitespace-only results.
|
|
79
|
+
*/
|
|
80
|
+
function singleValue(variables, key) {
|
|
81
|
+
const values = variables.get(key);
|
|
82
|
+
if (!values || values.length === 0) return void 0;
|
|
83
|
+
const joined = values.join(" ").trim();
|
|
84
|
+
return joined.length > 0 ? joined : void 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tokenize a value string, handling both bare tokens and "quoted multi-word"
|
|
88
|
+
* tokens. For example:
|
|
89
|
+
* `"computer vision" "opencv" bare` → `["computer vision", "opencv", "bare"]`
|
|
90
|
+
*/
|
|
91
|
+
function tokenizeValues(raw) {
|
|
92
|
+
const trimmed = raw.trim();
|
|
93
|
+
if (trimmed.length === 0) return [];
|
|
94
|
+
const values = [];
|
|
95
|
+
for (const [, quoted, bare] of trimmed.matchAll(/"([^"]+)"|(\S+)/g)) {
|
|
96
|
+
const value = quoted || bare;
|
|
97
|
+
if (value.length > 0) values.push(value);
|
|
98
|
+
}
|
|
99
|
+
return values;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { parseMakefileConfig };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/lib/parsers/properties-parser.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parser for Java `*.properties` files (key=value format).
|
|
4
|
+
*
|
|
5
|
+
* Used by both Arduino and Processing IDE library managers. These are
|
|
6
|
+
* flat UTF-8 properties files with no sections or nesting. Comment lines
|
|
7
|
+
* start with `#`. Only the first `=` on each line is the delimiter.
|
|
8
|
+
*
|
|
9
|
+
* Returns raw key-value pairs — all domain-specific validation and
|
|
10
|
+
* transformation is handled by the consuming source.
|
|
11
|
+
*
|
|
12
|
+
* See https://en.wikipedia.org/wiki/.properties
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Parse a `*.properties` content string into raw key-value pairs.
|
|
16
|
+
*/
|
|
17
|
+
function parseProperties(content) {
|
|
18
|
+
const raw = {};
|
|
19
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
20
|
+
const line = rawLine.trim();
|
|
21
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
22
|
+
const eqIndex = line.indexOf("=");
|
|
23
|
+
if (eqIndex === -1) continue;
|
|
24
|
+
const key = line.slice(0, eqIndex).trim();
|
|
25
|
+
const value = line.slice(eqIndex + 1).trim();
|
|
26
|
+
if (key.length > 0) raw[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { parseProperties };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/lib/parsers/rfc822-header-parser.ts
|
|
2
|
+
/** Multi-value headers that can appear multiple times. */
|
|
3
|
+
const MULTI_VALUE_HEADERS = new Set([
|
|
4
|
+
"Classifier",
|
|
5
|
+
"Platform",
|
|
6
|
+
"Project-URL",
|
|
7
|
+
"Requires-Dist",
|
|
8
|
+
"Requires-External",
|
|
9
|
+
"Supported-Platform"
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Parse RFC 822-style headers from PKG-INFO / METADATA content.
|
|
13
|
+
* Multi-value headers are collected into newline-separated strings.
|
|
14
|
+
* Stops at the first blank line (which separates headers from body).
|
|
15
|
+
*/
|
|
16
|
+
function parseRfc822Headers(content) {
|
|
17
|
+
const headers = {};
|
|
18
|
+
let lastKey = "";
|
|
19
|
+
for (const line of content.split("\n")) {
|
|
20
|
+
if (line.trim() === "") break;
|
|
21
|
+
if (/^\s/.test(line) && lastKey) {
|
|
22
|
+
const continuation = line.trim();
|
|
23
|
+
if (continuation) headers[lastKey] = `${headers[lastKey]}\n${continuation}`;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const colonIndex = line.indexOf(": ");
|
|
27
|
+
if (colonIndex > 0) {
|
|
28
|
+
const key = line.slice(0, colonIndex);
|
|
29
|
+
const value = line.slice(colonIndex + 2).trim();
|
|
30
|
+
headers[key] = MULTI_VALUE_HEADERS.has(key) && headers[key] ? `${headers[key]}\n${value}` : value;
|
|
31
|
+
lastKey = key;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
|
36
|
+
/** Extract body text after the first blank line. */
|
|
37
|
+
function extractRfc822Body(content) {
|
|
38
|
+
const blankIndex = content.indexOf("\n\n");
|
|
39
|
+
if (blankIndex === -1) return void 0;
|
|
40
|
+
return content.slice(blankIndex + 2).trim() || void 0;
|
|
41
|
+
}
|
|
42
|
+
/** Split newline-separated multi-value into array. */
|
|
43
|
+
function splitMultiValues(value) {
|
|
44
|
+
if (!value) return [];
|
|
45
|
+
return value.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { extractRfc822Body, parseRfc822Headers, splitMultiValues };
|