@sdk-usage/core 0.2.0-next.1794009490550.b76492b → 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/index.cjs +415 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.mjs +413 -0
- package/package.json +1 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
let node_module = require("node:module");
|
|
6
|
+
let _open_vanilla_visitor = require("@open-vanilla/visitor");
|
|
7
|
+
let _swc_core = require("@swc/core");
|
|
8
|
+
let node_child_process = require("node:child_process");
|
|
9
|
+
let node_stream = require("node:stream");
|
|
10
|
+
let node_util = require("node:util");
|
|
11
|
+
|
|
12
|
+
// src/entities/item/create-item.ts
|
|
13
|
+
/**
|
|
14
|
+
* Creates an SDK usage item.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* createItem({ ... });
|
|
18
|
+
*
|
|
19
|
+
* @param input - Factory variables.
|
|
20
|
+
* @param input.name - Name.
|
|
21
|
+
* @param input.type - Component type.
|
|
22
|
+
* @param input.module - Source module.
|
|
23
|
+
* @param input.version - Module version.
|
|
24
|
+
* @param input.location - Location.
|
|
25
|
+
* @param input.input - Parameter list and metadata describing the way it's passed.
|
|
26
|
+
* @returns An SDK usage item with its source location and module version.
|
|
27
|
+
*/
|
|
28
|
+
const createItem = ({ name, input, location, module, type, version }) => {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
input: {
|
|
33
|
+
metadata: { withSpreading: input?.metadata.withSpreading ?? false },
|
|
34
|
+
data: input?.data ?? {}
|
|
35
|
+
},
|
|
36
|
+
location,
|
|
37
|
+
module,
|
|
38
|
+
type,
|
|
39
|
+
version
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/entities/item/create-locator.ts
|
|
44
|
+
/**
|
|
45
|
+
* Creates a locator for SDK usages in a source file.
|
|
46
|
+
*
|
|
47
|
+
* @param input - Source file and its package metadata.
|
|
48
|
+
* @param input.code - Source text.
|
|
49
|
+
* @param input.file - Absolute source file path.
|
|
50
|
+
* @param input.link - Repository link.
|
|
51
|
+
* @param input.module - Package name.
|
|
52
|
+
* @param input.path - Root path that the reported file is relative to.
|
|
53
|
+
* @returns A function that reports a usage's source location and package metadata.
|
|
54
|
+
*/
|
|
55
|
+
const createLocator = ({ code, file, link, module, path }) => {
|
|
56
|
+
const source = Buffer.from(code);
|
|
57
|
+
const lineStarts = [0];
|
|
58
|
+
const relativeFile = `./${(0, node_path.relative)(path, file)}`;
|
|
59
|
+
for (let index = source.indexOf("\n"); index !== -1; index = source.indexOf("\n", index + 1)) lineStarts.push(index + 1);
|
|
60
|
+
return (offset) => {
|
|
61
|
+
const line = Math.max(1, lineStarts.findLastIndex((lineStart) => {
|
|
62
|
+
return lineStart <= offset;
|
|
63
|
+
}) + 1);
|
|
64
|
+
return {
|
|
65
|
+
column: source.toString("utf8", lineStarts[line - 1], offset).length,
|
|
66
|
+
file: relativeFile,
|
|
67
|
+
line,
|
|
68
|
+
link,
|
|
69
|
+
module
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/helpers.ts
|
|
75
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
76
|
+
const resolvePackageJson = (fromPath) => {
|
|
77
|
+
const filepath = (0, node_path.join)(fromPath, "./package.json");
|
|
78
|
+
if ((0, node_fs.existsSync)(filepath)) return filepath;
|
|
79
|
+
return resolvePackageJson((0, node_path.join)(fromPath, "../"));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/modules/parser/parse.ts
|
|
83
|
+
/**
|
|
84
|
+
* Collects SDK usages from a source file using the enabled plugins.
|
|
85
|
+
*
|
|
86
|
+
* @param code - Source text to parse.
|
|
87
|
+
* @param options - Parse options.
|
|
88
|
+
* @param options.file - Source file path, used in parse errors.
|
|
89
|
+
* @param options.plugins - Enabled plugins.
|
|
90
|
+
* @returns An iterator of collected SDK usages with their source positions.
|
|
91
|
+
*/
|
|
92
|
+
const parse = async (code, { file, plugins }) => {
|
|
93
|
+
let ast;
|
|
94
|
+
try {
|
|
95
|
+
ast = await (0, _swc_core.parse)(code, {
|
|
96
|
+
syntax: "typescript",
|
|
97
|
+
tsx: true
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`Failed to parse source file: ${file}`, { cause: error });
|
|
101
|
+
}
|
|
102
|
+
const context = { imports: /* @__PURE__ */ new Map() };
|
|
103
|
+
(0, _open_vanilla_visitor.visit)(ast, { ImportDeclaration(node) {
|
|
104
|
+
for (const specifier of node.specifiers) {
|
|
105
|
+
const alias = specifier.local.value;
|
|
106
|
+
const name = "imported" in specifier && Boolean(specifier.imported) ? specifier.imported.value : alias;
|
|
107
|
+
const typeOnly = node.typeOnly || "isTypeOnly" in specifier && specifier.isTypeOnly;
|
|
108
|
+
context.imports.set(alias, {
|
|
109
|
+
name,
|
|
110
|
+
alias,
|
|
111
|
+
bindingContext: getBindingContext(specifier.local),
|
|
112
|
+
module: node.source.value,
|
|
113
|
+
typeOnly
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} });
|
|
117
|
+
(0, _open_vanilla_visitor.visit)(ast, { VariableDeclarator(node) {
|
|
118
|
+
const imported = getDeclaratorImport(node, context.imports);
|
|
119
|
+
if (imported) addImportedBindings(node.id, imported, context.imports);
|
|
120
|
+
} });
|
|
121
|
+
const parentNodes = getParentNodes(ast);
|
|
122
|
+
const getParentNode = (node) => {
|
|
123
|
+
return parentNodes.get(node);
|
|
124
|
+
};
|
|
125
|
+
const visitorsByType = Map.groupBy(plugins.flatMap((plugin) => {
|
|
126
|
+
return Object.entries(plugin({
|
|
127
|
+
...context,
|
|
128
|
+
getJSXAttributeValue,
|
|
129
|
+
getParentNode
|
|
130
|
+
}));
|
|
131
|
+
}), ([type]) => {
|
|
132
|
+
return type;
|
|
133
|
+
});
|
|
134
|
+
return getNodes(ast).flatMap((node) => {
|
|
135
|
+
return (visitorsByType.get(String(node.type)) ?? []).map(([, visitor]) => {
|
|
136
|
+
const item = visitor(node);
|
|
137
|
+
if (item) item.offset -= 1;
|
|
138
|
+
return item;
|
|
139
|
+
});
|
|
140
|
+
}).filter((item) => {
|
|
141
|
+
return item !== void 0;
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const getNodes = (ast) => {
|
|
145
|
+
const pending = [ast];
|
|
146
|
+
return Iterator.from({ next() {
|
|
147
|
+
while (pending.length > 0) {
|
|
148
|
+
const value = pending.pop();
|
|
149
|
+
if (!isRecord(value)) continue;
|
|
150
|
+
const children = Object.values(value);
|
|
151
|
+
for (let index = children.length - 1; index >= 0; index -= 1) pending.push(children[index]);
|
|
152
|
+
if (!Array.isArray(value)) return {
|
|
153
|
+
done: false,
|
|
154
|
+
value
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
done: true,
|
|
159
|
+
value: void 0
|
|
160
|
+
};
|
|
161
|
+
} });
|
|
162
|
+
};
|
|
163
|
+
const getParentNodes = (ast) => {
|
|
164
|
+
const parentNodes = /* @__PURE__ */ new WeakMap();
|
|
165
|
+
const pending = [{
|
|
166
|
+
parent: void 0,
|
|
167
|
+
value: ast
|
|
168
|
+
}];
|
|
169
|
+
while (pending.length > 0) {
|
|
170
|
+
const current = pending.pop();
|
|
171
|
+
if (!current) continue;
|
|
172
|
+
const { parent, value } = current;
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
for (const child of value) pending.push({
|
|
175
|
+
parent,
|
|
176
|
+
value: child
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!isRecord(value)) continue;
|
|
181
|
+
if (parent) parentNodes.set(value, parent);
|
|
182
|
+
for (const child of Object.values(value)) pending.push({
|
|
183
|
+
parent: value,
|
|
184
|
+
value: child
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return parentNodes;
|
|
188
|
+
};
|
|
189
|
+
const isRecord = (value) => {
|
|
190
|
+
return typeof value === "object" && value !== null;
|
|
191
|
+
};
|
|
192
|
+
const getBindingContext = (identifier) => {
|
|
193
|
+
return "ctxt" in identifier && typeof identifier.ctxt === "number" ? identifier.ctxt : void 0;
|
|
194
|
+
};
|
|
195
|
+
const getDeclaratorImport = (node, imports) => {
|
|
196
|
+
const initializer = node.init;
|
|
197
|
+
if (initializer?.type !== "Identifier") return;
|
|
198
|
+
const imported = imports.get(initializer.value);
|
|
199
|
+
return imported?.bindingContext !== void 0 && imported.bindingContext === getBindingContext(initializer) ? imported : void 0;
|
|
200
|
+
};
|
|
201
|
+
const addImportedBindings = (pattern, imported, imports) => {
|
|
202
|
+
if (pattern.type === "Identifier") {
|
|
203
|
+
const { value: alias } = pattern;
|
|
204
|
+
const bindingContext = getBindingContext(pattern);
|
|
205
|
+
const existing = imports.get(alias);
|
|
206
|
+
if (bindingContext === void 0 || existing && existing.bindingContext !== bindingContext) return;
|
|
207
|
+
imports.set(alias, {
|
|
208
|
+
...imported,
|
|
209
|
+
alias,
|
|
210
|
+
bindingContext
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (pattern.type !== "ObjectPattern") return;
|
|
215
|
+
for (const property of pattern.properties) {
|
|
216
|
+
if (property.type === "KeyValuePatternProperty") {
|
|
217
|
+
if (property.key.type === "Identifier" || property.key.type === "StringLiteral") addImportedBindings(property.value, {
|
|
218
|
+
...imported,
|
|
219
|
+
name: property.key.value
|
|
220
|
+
}, imports);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (property.type === "AssignmentPatternProperty") addImportedBindings(property.key, {
|
|
224
|
+
...imported,
|
|
225
|
+
name: property.key.value
|
|
226
|
+
}, imports);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const getJSXAttributeValue = (node) => {
|
|
230
|
+
if (!node) return true;
|
|
231
|
+
if (node.type === "BigIntLiteral" || node.type === "BooleanLiteral" || node.type === "JSXText" || node.type === "NumericLiteral" || node.type === "StringLiteral") return node.value;
|
|
232
|
+
if (node.type === "JSXExpressionContainer") return getJSXAttributeValue(node.expression);
|
|
233
|
+
if (node.type === "NullLiteral") return;
|
|
234
|
+
return createUnknownToken(node.type);
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Creates a placeholder for an unsupported source expression.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* createUnknownToken("VariableDeclaration");
|
|
241
|
+
*
|
|
242
|
+
* @param syntax - SWC syntax node name.
|
|
243
|
+
* @returns A placeholder identifying the unsupported expression.
|
|
244
|
+
*/
|
|
245
|
+
const createUnknownToken = (syntax) => {
|
|
246
|
+
return {
|
|
247
|
+
syntax,
|
|
248
|
+
type: "unsupported-syntax"
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/modules/scanner/scan.ts
|
|
253
|
+
const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
254
|
+
const DEFAULT_EXCLUDED_FOLDERS = [
|
|
255
|
+
".git",
|
|
256
|
+
"node_modules",
|
|
257
|
+
"dist",
|
|
258
|
+
"out"
|
|
259
|
+
];
|
|
260
|
+
const DEFAULT_INCLUDED_FILES = ["**/!(test|*.test|stories|*.stories).?(m){j,t}s?(x)"];
|
|
261
|
+
/**
|
|
262
|
+
* Finds matching source files in the package tree at `path`.
|
|
263
|
+
*
|
|
264
|
+
* @param path - Root directory to scan.
|
|
265
|
+
* @param signal - Cancels the scan.
|
|
266
|
+
* @param options - File and folder filters.
|
|
267
|
+
* @returns A backpressure-aware stream of matching source files and their package metadata.
|
|
268
|
+
*/
|
|
269
|
+
const scan = (path, signal, options) => {
|
|
270
|
+
const excludedFolders = new Set(options.excludeFolders ?? DEFAULT_EXCLUDED_FOLDERS);
|
|
271
|
+
const includedFiles = options.includeFiles ?? DEFAULT_INCLUDED_FILES;
|
|
272
|
+
const isIncluded = (file, project) => {
|
|
273
|
+
const relativeFile = (0, node_path.relative)(project.folder, file).replaceAll(node_path.sep, "/");
|
|
274
|
+
return includedFiles.some((pattern) => {
|
|
275
|
+
return (0, node_path.matchesGlob)(relativeFile, pattern);
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
const scanDirectory = (folder, parentProject) => {
|
|
279
|
+
return node_stream.Readable.from([folder]).flatMap(async () => {
|
|
280
|
+
const project = await getProject(folder) ?? parentProject;
|
|
281
|
+
return node_stream.Readable.from(await (0, node_fs_promises.opendir)(folder)).flatMap((entry) => {
|
|
282
|
+
const file = (0, node_path.join)(folder, entry.name);
|
|
283
|
+
if (entry.isDirectory() && excludedFolders.has(entry.name)) return [];
|
|
284
|
+
if (entry.isDirectory()) return scanDirectory(file, project);
|
|
285
|
+
const isSourceFile = entry.isFile() || entry.isSymbolicLink();
|
|
286
|
+
if (!project || !isSourceFile || !isIncluded(file, project)) return [];
|
|
287
|
+
return [{
|
|
288
|
+
...project,
|
|
289
|
+
file
|
|
290
|
+
}];
|
|
291
|
+
}, { signal });
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
return scanDirectory(path);
|
|
295
|
+
};
|
|
296
|
+
const getProject = async (folder) => {
|
|
297
|
+
try {
|
|
298
|
+
const content = await (0, node_fs_promises.readFile)((0, node_path.join)(folder, "package.json"), "utf8");
|
|
299
|
+
return {
|
|
300
|
+
metadata: JSON.parse(content),
|
|
301
|
+
folder,
|
|
302
|
+
link: await getRepositoryUrl(folder)
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const getRepositoryUrl = async (folder) => {
|
|
310
|
+
try {
|
|
311
|
+
const { stdout } = await execFileAsync("git", [
|
|
312
|
+
"config",
|
|
313
|
+
"--get",
|
|
314
|
+
"remote.origin.url"
|
|
315
|
+
], { cwd: folder });
|
|
316
|
+
return stdout.trim();
|
|
317
|
+
} catch {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/create-instance.ts
|
|
323
|
+
/**
|
|
324
|
+
* Creates an analyzer for the package tree at `path`.
|
|
325
|
+
*
|
|
326
|
+
* @param path - Root directory to scan.
|
|
327
|
+
* @param options - Optional file filters, module filters, and parser plugins.
|
|
328
|
+
* @returns An analyzer that collects matching SDK usages.
|
|
329
|
+
*/
|
|
330
|
+
const createInstance = (path, options) => {
|
|
331
|
+
const { includeModules = [], plugins = [], resolveInstalledVersions = false } = options;
|
|
332
|
+
const getFileItems = async ({ metadata, file, link }, signal) => {
|
|
333
|
+
const code = await (0, node_fs_promises.readFile)(file, {
|
|
334
|
+
encoding: "utf8",
|
|
335
|
+
signal
|
|
336
|
+
});
|
|
337
|
+
const items = await parse(code, {
|
|
338
|
+
file,
|
|
339
|
+
plugins
|
|
340
|
+
});
|
|
341
|
+
const locate = createLocator({
|
|
342
|
+
code,
|
|
343
|
+
file,
|
|
344
|
+
link,
|
|
345
|
+
module: metadata.name,
|
|
346
|
+
path
|
|
347
|
+
});
|
|
348
|
+
const dependencies = {
|
|
349
|
+
...metadata.devDependencies,
|
|
350
|
+
...metadata.optionalDependencies,
|
|
351
|
+
...metadata.dependencies
|
|
352
|
+
};
|
|
353
|
+
return items.filter(({ module }) => {
|
|
354
|
+
return includeModules.length === 0 || includeModules.includes(module);
|
|
355
|
+
}).map((item) => {
|
|
356
|
+
const version = (resolveInstalledVersions ? getInstalledVersion(item.module, file) : void 0) ?? dependencies[item.module] ?? "";
|
|
357
|
+
return createItem({
|
|
358
|
+
...item,
|
|
359
|
+
location: locate(item.offset),
|
|
360
|
+
version
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
/**
|
|
366
|
+
* Collects matching SDK usages from the package tree. Stopping iteration cancels pending
|
|
367
|
+
* scans; scans already in progress may finish.
|
|
368
|
+
*
|
|
369
|
+
* @returns An async iterable of matching SDK usages with their source locations and
|
|
370
|
+
* versions.
|
|
371
|
+
*/
|
|
372
|
+
getItems() {
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const { signal } = controller;
|
|
375
|
+
const iterator = scan(path, signal, options).flatMap(async (file) => {
|
|
376
|
+
return getFileItems(file, signal);
|
|
377
|
+
}).iterator();
|
|
378
|
+
return {
|
|
379
|
+
next: iterator.next.bind(iterator),
|
|
380
|
+
async return() {
|
|
381
|
+
controller.abort();
|
|
382
|
+
await iterator.return?.();
|
|
383
|
+
return {
|
|
384
|
+
done: true,
|
|
385
|
+
value: void 0
|
|
386
|
+
};
|
|
387
|
+
},
|
|
388
|
+
[Symbol.asyncIterator]() {
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
} };
|
|
393
|
+
};
|
|
394
|
+
const getInstalledVersion = (module, file) => {
|
|
395
|
+
try {
|
|
396
|
+
const { version } = require$1(resolvePackageJson(require$1.resolve(module, { paths: [file] })));
|
|
397
|
+
return version;
|
|
398
|
+
} catch {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/modules/plugin/create-plugin.ts
|
|
404
|
+
/**
|
|
405
|
+
* Returns a plugin definition unchanged for convenient type inference.
|
|
406
|
+
*
|
|
407
|
+
* @param input - Plugin implementation.
|
|
408
|
+
* @returns The supplied plugin implementation.
|
|
409
|
+
*/
|
|
410
|
+
const createPlugin = (input) => {
|
|
411
|
+
return input;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
exports.createInstance = createInstance;
|
|
415
|
+
exports.createPlugin = createPlugin;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CallExpression, Identifier, ImportDeclaration, JSXAttrValue as AstJSXAttrValue, JSXOpeningElement, NewExpression, OptionalChainingExpression, TaggedTemplateExpression, TsExpressionWithTypeArguments, TsType, VariableDeclarator } from "@swc/core";
|
|
2
|
+
type AstNode = Record<string, unknown>;
|
|
3
|
+
type AstNodes = {
|
|
4
|
+
CallExpression: CallExpression;
|
|
5
|
+
Identifier: Identifier;
|
|
6
|
+
ImportDeclaration: ImportDeclaration;
|
|
7
|
+
JSXOpeningElement: JSXOpeningElement;
|
|
8
|
+
NewExpression: NewExpression;
|
|
9
|
+
OptionalChainingExpression: OptionalChainingExpression;
|
|
10
|
+
TaggedTemplateExpression: TaggedTemplateExpression;
|
|
11
|
+
TsExpressionWithTypeArguments: TsExpressionWithTypeArguments;
|
|
12
|
+
TsImportType: Extract<TsType, {
|
|
13
|
+
type: "TsImportType";
|
|
14
|
+
}>;
|
|
15
|
+
TsTypeQuery: Extract<TsType, {
|
|
16
|
+
type: "TsTypeQuery";
|
|
17
|
+
}>;
|
|
18
|
+
TsTypeReference: Extract<TsType, {
|
|
19
|
+
type: "TsTypeReference";
|
|
20
|
+
}>;
|
|
21
|
+
VariableDeclarator: VariableDeclarator;
|
|
22
|
+
};
|
|
23
|
+
/** Import entity to model an import statement. */
|
|
24
|
+
type Import = {
|
|
25
|
+
name: string;
|
|
26
|
+
alias: string;
|
|
27
|
+
/** SWC context used to distinguish this import from shadowing local bindings. */
|
|
28
|
+
bindingContext: number | undefined;
|
|
29
|
+
module: string;
|
|
30
|
+
typeOnly: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Package entity to model `package.json` metadata. */
|
|
33
|
+
type Package = {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
dependencies?: Record<string, string>;
|
|
37
|
+
devDependencies?: Record<string, string>;
|
|
38
|
+
optionalDependencies?: Record<string, string>;
|
|
39
|
+
peerDependencies?: Record<string, string>;
|
|
40
|
+
version: string;
|
|
41
|
+
};
|
|
42
|
+
type Primitive = bigint | boolean | null | number | string | undefined;
|
|
43
|
+
/** Placeholder for a JSX expression whose source value is not statically known. */
|
|
44
|
+
type UnsupportedSyntaxToken = {
|
|
45
|
+
syntax: string;
|
|
46
|
+
type: "unsupported-syntax";
|
|
47
|
+
};
|
|
48
|
+
type Location = {
|
|
49
|
+
column: number;
|
|
50
|
+
file: string;
|
|
51
|
+
line: number;
|
|
52
|
+
link: string;
|
|
53
|
+
module: string;
|
|
54
|
+
};
|
|
55
|
+
type Item = {
|
|
56
|
+
name: string;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
input: {
|
|
59
|
+
metadata: {
|
|
60
|
+
withSpreading: boolean;
|
|
61
|
+
};
|
|
62
|
+
data: Record<string, unknown>;
|
|
63
|
+
};
|
|
64
|
+
location: Location;
|
|
65
|
+
module: Package["name"];
|
|
66
|
+
type: string;
|
|
67
|
+
version: Package["version"];
|
|
68
|
+
};
|
|
69
|
+
type ItemDTO = Partial<Pick<Item, "input">> & Pick<Item, "module" | "name" | "type"> & {
|
|
70
|
+
offset: number;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Returns a plugin definition unchanged for convenient type inference.
|
|
74
|
+
*
|
|
75
|
+
* @param input - Plugin implementation.
|
|
76
|
+
* @returns The supplied plugin implementation.
|
|
77
|
+
*/
|
|
78
|
+
export declare const createPlugin: (input: Plugin) => Plugin;
|
|
79
|
+
type Plugin = (input: {
|
|
80
|
+
getJSXAttributeValue: (node: AstJSXAttrValue | undefined) => Primitive | UnsupportedSyntaxToken;
|
|
81
|
+
getParentNode: (node: object) => AstNode | undefined;
|
|
82
|
+
imports: Map<Import["alias"], Import>;
|
|
83
|
+
}) => { [Key in keyof AstNodes]?: (node: AstNodes[Key]) => ItemDTO | undefined; };
|
|
84
|
+
type ScanOptions = {
|
|
85
|
+
/** A list of folders to ignore. */
|
|
86
|
+
excludeFolders?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Include patterns using `node:path.matchesGlob` semantics. Dot-prefixed path segments must be
|
|
89
|
+
* matched explicitly; leading `!` is not negation.
|
|
90
|
+
*/
|
|
91
|
+
includeFiles?: string[];
|
|
92
|
+
};
|
|
93
|
+
type Options = Partial<ScanOptions & {
|
|
94
|
+
/** Only analyze components imported from the specified module list. */
|
|
95
|
+
includeModules: string[];
|
|
96
|
+
/** A list of plugins to enable. */
|
|
97
|
+
plugins: Plugin[];
|
|
98
|
+
/**
|
|
99
|
+
* Attempt to resolve installed versions of modules. If false or not possible, the specified
|
|
100
|
+
* version from the package.json will be used.
|
|
101
|
+
*
|
|
102
|
+
* @default false
|
|
103
|
+
*/
|
|
104
|
+
resolveInstalledVersions: boolean;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Creates an analyzer for the package tree at `path`.
|
|
108
|
+
*
|
|
109
|
+
* @param path - Root directory to scan.
|
|
110
|
+
* @param options - Optional file filters, module filters, and parser plugins.
|
|
111
|
+
* @returns An analyzer that collects matching SDK usages.
|
|
112
|
+
*/
|
|
113
|
+
export declare const createInstance: (path: string, options: Options) => {
|
|
114
|
+
/**
|
|
115
|
+
* Collects matching SDK usages from the package tree. Stopping iteration cancels pending
|
|
116
|
+
* scans; scans already in progress may finish.
|
|
117
|
+
*
|
|
118
|
+
* @returns An async iterable of matching SDK usages with their source locations and
|
|
119
|
+
* versions.
|
|
120
|
+
*/
|
|
121
|
+
getItems(): AsyncIterable<Item>;
|
|
122
|
+
};
|
|
123
|
+
export type { Plugin };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { opendir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, matchesGlob, relative, sep } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { visit } from "@open-vanilla/visitor";
|
|
6
|
+
import { parse } from "@swc/core";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
|
|
11
|
+
// src/entities/item/create-item.ts
|
|
12
|
+
/**
|
|
13
|
+
* Creates an SDK usage item.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* createItem({ ... });
|
|
17
|
+
*
|
|
18
|
+
* @param input - Factory variables.
|
|
19
|
+
* @param input.name - Name.
|
|
20
|
+
* @param input.type - Component type.
|
|
21
|
+
* @param input.module - Source module.
|
|
22
|
+
* @param input.version - Module version.
|
|
23
|
+
* @param input.location - Location.
|
|
24
|
+
* @param input.input - Parameter list and metadata describing the way it's passed.
|
|
25
|
+
* @returns An SDK usage item with its source location and module version.
|
|
26
|
+
*/
|
|
27
|
+
const createItem = ({ name, input, location, module, type, version }) => {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
input: {
|
|
32
|
+
metadata: { withSpreading: input?.metadata.withSpreading ?? false },
|
|
33
|
+
data: input?.data ?? {}
|
|
34
|
+
},
|
|
35
|
+
location,
|
|
36
|
+
module,
|
|
37
|
+
type,
|
|
38
|
+
version
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/entities/item/create-locator.ts
|
|
43
|
+
/**
|
|
44
|
+
* Creates a locator for SDK usages in a source file.
|
|
45
|
+
*
|
|
46
|
+
* @param input - Source file and its package metadata.
|
|
47
|
+
* @param input.code - Source text.
|
|
48
|
+
* @param input.file - Absolute source file path.
|
|
49
|
+
* @param input.link - Repository link.
|
|
50
|
+
* @param input.module - Package name.
|
|
51
|
+
* @param input.path - Root path that the reported file is relative to.
|
|
52
|
+
* @returns A function that reports a usage's source location and package metadata.
|
|
53
|
+
*/
|
|
54
|
+
const createLocator = ({ code, file, link, module, path }) => {
|
|
55
|
+
const source = Buffer.from(code);
|
|
56
|
+
const lineStarts = [0];
|
|
57
|
+
const relativeFile = `./${relative(path, file)}`;
|
|
58
|
+
for (let index = source.indexOf("\n"); index !== -1; index = source.indexOf("\n", index + 1)) lineStarts.push(index + 1);
|
|
59
|
+
return (offset) => {
|
|
60
|
+
const line = Math.max(1, lineStarts.findLastIndex((lineStart) => {
|
|
61
|
+
return lineStart <= offset;
|
|
62
|
+
}) + 1);
|
|
63
|
+
return {
|
|
64
|
+
column: source.toString("utf8", lineStarts[line - 1], offset).length,
|
|
65
|
+
file: relativeFile,
|
|
66
|
+
line,
|
|
67
|
+
link,
|
|
68
|
+
module
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/helpers.ts
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
const resolvePackageJson = (fromPath) => {
|
|
76
|
+
const filepath = join(fromPath, "./package.json");
|
|
77
|
+
if (existsSync(filepath)) return filepath;
|
|
78
|
+
return resolvePackageJson(join(fromPath, "../"));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/modules/parser/parse.ts
|
|
82
|
+
/**
|
|
83
|
+
* Collects SDK usages from a source file using the enabled plugins.
|
|
84
|
+
*
|
|
85
|
+
* @param code - Source text to parse.
|
|
86
|
+
* @param options - Parse options.
|
|
87
|
+
* @param options.file - Source file path, used in parse errors.
|
|
88
|
+
* @param options.plugins - Enabled plugins.
|
|
89
|
+
* @returns An iterator of collected SDK usages with their source positions.
|
|
90
|
+
*/
|
|
91
|
+
const parse$1 = async (code, { file, plugins }) => {
|
|
92
|
+
let ast;
|
|
93
|
+
try {
|
|
94
|
+
ast = await parse(code, {
|
|
95
|
+
syntax: "typescript",
|
|
96
|
+
tsx: true
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Failed to parse source file: ${file}`, { cause: error });
|
|
100
|
+
}
|
|
101
|
+
const context = { imports: /* @__PURE__ */ new Map() };
|
|
102
|
+
visit(ast, { ImportDeclaration(node) {
|
|
103
|
+
for (const specifier of node.specifiers) {
|
|
104
|
+
const alias = specifier.local.value;
|
|
105
|
+
const name = "imported" in specifier && Boolean(specifier.imported) ? specifier.imported.value : alias;
|
|
106
|
+
const typeOnly = node.typeOnly || "isTypeOnly" in specifier && specifier.isTypeOnly;
|
|
107
|
+
context.imports.set(alias, {
|
|
108
|
+
name,
|
|
109
|
+
alias,
|
|
110
|
+
bindingContext: getBindingContext(specifier.local),
|
|
111
|
+
module: node.source.value,
|
|
112
|
+
typeOnly
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} });
|
|
116
|
+
visit(ast, { VariableDeclarator(node) {
|
|
117
|
+
const imported = getDeclaratorImport(node, context.imports);
|
|
118
|
+
if (imported) addImportedBindings(node.id, imported, context.imports);
|
|
119
|
+
} });
|
|
120
|
+
const parentNodes = getParentNodes(ast);
|
|
121
|
+
const getParentNode = (node) => {
|
|
122
|
+
return parentNodes.get(node);
|
|
123
|
+
};
|
|
124
|
+
const visitorsByType = Map.groupBy(plugins.flatMap((plugin) => {
|
|
125
|
+
return Object.entries(plugin({
|
|
126
|
+
...context,
|
|
127
|
+
getJSXAttributeValue,
|
|
128
|
+
getParentNode
|
|
129
|
+
}));
|
|
130
|
+
}), ([type]) => {
|
|
131
|
+
return type;
|
|
132
|
+
});
|
|
133
|
+
return getNodes(ast).flatMap((node) => {
|
|
134
|
+
return (visitorsByType.get(String(node.type)) ?? []).map(([, visitor]) => {
|
|
135
|
+
const item = visitor(node);
|
|
136
|
+
if (item) item.offset -= 1;
|
|
137
|
+
return item;
|
|
138
|
+
});
|
|
139
|
+
}).filter((item) => {
|
|
140
|
+
return item !== void 0;
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const getNodes = (ast) => {
|
|
144
|
+
const pending = [ast];
|
|
145
|
+
return Iterator.from({ next() {
|
|
146
|
+
while (pending.length > 0) {
|
|
147
|
+
const value = pending.pop();
|
|
148
|
+
if (!isRecord(value)) continue;
|
|
149
|
+
const children = Object.values(value);
|
|
150
|
+
for (let index = children.length - 1; index >= 0; index -= 1) pending.push(children[index]);
|
|
151
|
+
if (!Array.isArray(value)) return {
|
|
152
|
+
done: false,
|
|
153
|
+
value
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
done: true,
|
|
158
|
+
value: void 0
|
|
159
|
+
};
|
|
160
|
+
} });
|
|
161
|
+
};
|
|
162
|
+
const getParentNodes = (ast) => {
|
|
163
|
+
const parentNodes = /* @__PURE__ */ new WeakMap();
|
|
164
|
+
const pending = [{
|
|
165
|
+
parent: void 0,
|
|
166
|
+
value: ast
|
|
167
|
+
}];
|
|
168
|
+
while (pending.length > 0) {
|
|
169
|
+
const current = pending.pop();
|
|
170
|
+
if (!current) continue;
|
|
171
|
+
const { parent, value } = current;
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
for (const child of value) pending.push({
|
|
174
|
+
parent,
|
|
175
|
+
value: child
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!isRecord(value)) continue;
|
|
180
|
+
if (parent) parentNodes.set(value, parent);
|
|
181
|
+
for (const child of Object.values(value)) pending.push({
|
|
182
|
+
parent: value,
|
|
183
|
+
value: child
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return parentNodes;
|
|
187
|
+
};
|
|
188
|
+
const isRecord = (value) => {
|
|
189
|
+
return typeof value === "object" && value !== null;
|
|
190
|
+
};
|
|
191
|
+
const getBindingContext = (identifier) => {
|
|
192
|
+
return "ctxt" in identifier && typeof identifier.ctxt === "number" ? identifier.ctxt : void 0;
|
|
193
|
+
};
|
|
194
|
+
const getDeclaratorImport = (node, imports) => {
|
|
195
|
+
const initializer = node.init;
|
|
196
|
+
if (initializer?.type !== "Identifier") return;
|
|
197
|
+
const imported = imports.get(initializer.value);
|
|
198
|
+
return imported?.bindingContext !== void 0 && imported.bindingContext === getBindingContext(initializer) ? imported : void 0;
|
|
199
|
+
};
|
|
200
|
+
const addImportedBindings = (pattern, imported, imports) => {
|
|
201
|
+
if (pattern.type === "Identifier") {
|
|
202
|
+
const { value: alias } = pattern;
|
|
203
|
+
const bindingContext = getBindingContext(pattern);
|
|
204
|
+
const existing = imports.get(alias);
|
|
205
|
+
if (bindingContext === void 0 || existing && existing.bindingContext !== bindingContext) return;
|
|
206
|
+
imports.set(alias, {
|
|
207
|
+
...imported,
|
|
208
|
+
alias,
|
|
209
|
+
bindingContext
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (pattern.type !== "ObjectPattern") return;
|
|
214
|
+
for (const property of pattern.properties) {
|
|
215
|
+
if (property.type === "KeyValuePatternProperty") {
|
|
216
|
+
if (property.key.type === "Identifier" || property.key.type === "StringLiteral") addImportedBindings(property.value, {
|
|
217
|
+
...imported,
|
|
218
|
+
name: property.key.value
|
|
219
|
+
}, imports);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (property.type === "AssignmentPatternProperty") addImportedBindings(property.key, {
|
|
223
|
+
...imported,
|
|
224
|
+
name: property.key.value
|
|
225
|
+
}, imports);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const getJSXAttributeValue = (node) => {
|
|
229
|
+
if (!node) return true;
|
|
230
|
+
if (node.type === "BigIntLiteral" || node.type === "BooleanLiteral" || node.type === "JSXText" || node.type === "NumericLiteral" || node.type === "StringLiteral") return node.value;
|
|
231
|
+
if (node.type === "JSXExpressionContainer") return getJSXAttributeValue(node.expression);
|
|
232
|
+
if (node.type === "NullLiteral") return;
|
|
233
|
+
return createUnknownToken(node.type);
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Creates a placeholder for an unsupported source expression.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* createUnknownToken("VariableDeclaration");
|
|
240
|
+
*
|
|
241
|
+
* @param syntax - SWC syntax node name.
|
|
242
|
+
* @returns A placeholder identifying the unsupported expression.
|
|
243
|
+
*/
|
|
244
|
+
const createUnknownToken = (syntax) => {
|
|
245
|
+
return {
|
|
246
|
+
syntax,
|
|
247
|
+
type: "unsupported-syntax"
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/modules/scanner/scan.ts
|
|
252
|
+
const execFileAsync = promisify(execFile);
|
|
253
|
+
const DEFAULT_EXCLUDED_FOLDERS = [
|
|
254
|
+
".git",
|
|
255
|
+
"node_modules",
|
|
256
|
+
"dist",
|
|
257
|
+
"out"
|
|
258
|
+
];
|
|
259
|
+
const DEFAULT_INCLUDED_FILES = ["**/!(test|*.test|stories|*.stories).?(m){j,t}s?(x)"];
|
|
260
|
+
/**
|
|
261
|
+
* Finds matching source files in the package tree at `path`.
|
|
262
|
+
*
|
|
263
|
+
* @param path - Root directory to scan.
|
|
264
|
+
* @param signal - Cancels the scan.
|
|
265
|
+
* @param options - File and folder filters.
|
|
266
|
+
* @returns A backpressure-aware stream of matching source files and their package metadata.
|
|
267
|
+
*/
|
|
268
|
+
const scan = (path, signal, options) => {
|
|
269
|
+
const excludedFolders = new Set(options.excludeFolders ?? DEFAULT_EXCLUDED_FOLDERS);
|
|
270
|
+
const includedFiles = options.includeFiles ?? DEFAULT_INCLUDED_FILES;
|
|
271
|
+
const isIncluded = (file, project) => {
|
|
272
|
+
const relativeFile = relative(project.folder, file).replaceAll(sep, "/");
|
|
273
|
+
return includedFiles.some((pattern) => {
|
|
274
|
+
return matchesGlob(relativeFile, pattern);
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const scanDirectory = (folder, parentProject) => {
|
|
278
|
+
return Readable.from([folder]).flatMap(async () => {
|
|
279
|
+
const project = await getProject(folder) ?? parentProject;
|
|
280
|
+
return Readable.from(await opendir(folder)).flatMap((entry) => {
|
|
281
|
+
const file = join(folder, entry.name);
|
|
282
|
+
if (entry.isDirectory() && excludedFolders.has(entry.name)) return [];
|
|
283
|
+
if (entry.isDirectory()) return scanDirectory(file, project);
|
|
284
|
+
const isSourceFile = entry.isFile() || entry.isSymbolicLink();
|
|
285
|
+
if (!project || !isSourceFile || !isIncluded(file, project)) return [];
|
|
286
|
+
return [{
|
|
287
|
+
...project,
|
|
288
|
+
file
|
|
289
|
+
}];
|
|
290
|
+
}, { signal });
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
return scanDirectory(path);
|
|
294
|
+
};
|
|
295
|
+
const getProject = async (folder) => {
|
|
296
|
+
try {
|
|
297
|
+
const content = await readFile(join(folder, "package.json"), "utf8");
|
|
298
|
+
return {
|
|
299
|
+
metadata: JSON.parse(content),
|
|
300
|
+
folder,
|
|
301
|
+
link: await getRepositoryUrl(folder)
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const getRepositoryUrl = async (folder) => {
|
|
309
|
+
try {
|
|
310
|
+
const { stdout } = await execFileAsync("git", [
|
|
311
|
+
"config",
|
|
312
|
+
"--get",
|
|
313
|
+
"remote.origin.url"
|
|
314
|
+
], { cwd: folder });
|
|
315
|
+
return stdout.trim();
|
|
316
|
+
} catch {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/create-instance.ts
|
|
322
|
+
/**
|
|
323
|
+
* Creates an analyzer for the package tree at `path`.
|
|
324
|
+
*
|
|
325
|
+
* @param path - Root directory to scan.
|
|
326
|
+
* @param options - Optional file filters, module filters, and parser plugins.
|
|
327
|
+
* @returns An analyzer that collects matching SDK usages.
|
|
328
|
+
*/
|
|
329
|
+
const createInstance = (path, options) => {
|
|
330
|
+
const { includeModules = [], plugins = [], resolveInstalledVersions = false } = options;
|
|
331
|
+
const getFileItems = async ({ metadata, file, link }, signal) => {
|
|
332
|
+
const code = await readFile(file, {
|
|
333
|
+
encoding: "utf8",
|
|
334
|
+
signal
|
|
335
|
+
});
|
|
336
|
+
const items = await parse$1(code, {
|
|
337
|
+
file,
|
|
338
|
+
plugins
|
|
339
|
+
});
|
|
340
|
+
const locate = createLocator({
|
|
341
|
+
code,
|
|
342
|
+
file,
|
|
343
|
+
link,
|
|
344
|
+
module: metadata.name,
|
|
345
|
+
path
|
|
346
|
+
});
|
|
347
|
+
const dependencies = {
|
|
348
|
+
...metadata.devDependencies,
|
|
349
|
+
...metadata.optionalDependencies,
|
|
350
|
+
...metadata.dependencies
|
|
351
|
+
};
|
|
352
|
+
return items.filter(({ module }) => {
|
|
353
|
+
return includeModules.length === 0 || includeModules.includes(module);
|
|
354
|
+
}).map((item) => {
|
|
355
|
+
const version = (resolveInstalledVersions ? getInstalledVersion(item.module, file) : void 0) ?? dependencies[item.module] ?? "";
|
|
356
|
+
return createItem({
|
|
357
|
+
...item,
|
|
358
|
+
location: locate(item.offset),
|
|
359
|
+
version
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
/**
|
|
365
|
+
* Collects matching SDK usages from the package tree. Stopping iteration cancels pending
|
|
366
|
+
* scans; scans already in progress may finish.
|
|
367
|
+
*
|
|
368
|
+
* @returns An async iterable of matching SDK usages with their source locations and
|
|
369
|
+
* versions.
|
|
370
|
+
*/
|
|
371
|
+
getItems() {
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const { signal } = controller;
|
|
374
|
+
const iterator = scan(path, signal, options).flatMap(async (file) => {
|
|
375
|
+
return getFileItems(file, signal);
|
|
376
|
+
}).iterator();
|
|
377
|
+
return {
|
|
378
|
+
next: iterator.next.bind(iterator),
|
|
379
|
+
async return() {
|
|
380
|
+
controller.abort();
|
|
381
|
+
await iterator.return?.();
|
|
382
|
+
return {
|
|
383
|
+
done: true,
|
|
384
|
+
value: void 0
|
|
385
|
+
};
|
|
386
|
+
},
|
|
387
|
+
[Symbol.asyncIterator]() {
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
} };
|
|
392
|
+
};
|
|
393
|
+
const getInstalledVersion = (module, file) => {
|
|
394
|
+
try {
|
|
395
|
+
const { version } = require(resolvePackageJson(require.resolve(module, { paths: [file] })));
|
|
396
|
+
return version;
|
|
397
|
+
} catch {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/modules/plugin/create-plugin.ts
|
|
403
|
+
/**
|
|
404
|
+
* Returns a plugin definition unchanged for convenient type inference.
|
|
405
|
+
*
|
|
406
|
+
* @param input - Plugin implementation.
|
|
407
|
+
* @returns The supplied plugin implementation.
|
|
408
|
+
*/
|
|
409
|
+
const createPlugin = (input) => {
|
|
410
|
+
return input;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
export { createInstance, createPlugin };
|