UNPKG

@sdk-usage/core

Version:
244 lines (236 loc) 6.88 kB
import { existsSync, readFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { spawn } from "node:child_process"; import { createRequire } from "node:module"; import { visit } from "@open-vanilla/visitor"; import { parse } from "@swc/core"; import { fdir } from "fdir"; //#region src/entities/item/createLocation.ts const createLocation = ({ code, file, link, module, offset, path }) => { const linesTillOffset = code.slice(0, Math.max(0, offset)).split(/\n/); const line = linesTillOffset.length; return { column: linesTillOffset[line - 1].length, file: `./${relative(path, file)}`, line, link, module }; }; //#endregion //#region src/entities/item/createItem.ts /** * Aggregate factory that creates an item. * @param input - Factory variables. * @param input.name - Name. * @param input.type - Component type. * @param input.module - Source module. * @param input.version - Module version. * @param input.location - Location. * @param input.input - Parameter list and metadata describing the way it's passed. * @returns Created item. * @example * createItem({ ... }); */ const createItem = ({ input, location, module, name, type, version }) => { return { createdAt: (/* @__PURE__ */ new Date()).toISOString(), input: { data: input?.data ?? {}, metadata: { withSpreading: input?.metadata.withSpreading ?? false } }, location: createLocation(location), module, name, type, version }; }; //#endregion //#region src/helpers.ts const require = createRequire(import.meta.url); const resolvePackageJson = (fromPath) => { const filepath = join(fromPath, "./package.json"); if (existsSync(filepath)) return filepath; return resolvePackageJson(join(fromPath, "../")); }; /** * Execute an external command. * @param command - The command to execute. * @param options - Options including current working directory configuration. * @param options.cwd - Configure the current working directory. * @returns The output (either the command output or error). * @example * exec("ls"); */ const exec = async (command, options = {}) => { return new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; const childProcess = spawn(command, { cwd: options.cwd, shell: true, stdio: "pipe" }); childProcess.stdout.on("data", (chunk) => { stdout += chunk; }); childProcess.stderr.on("data", (chunk) => { stderr += chunk; }); childProcess.on("close", (exitCode) => { if (exitCode === 0) resolve(stdout.trim()); else { const output = `${stderr}${stdout}`; reject(new Error(output.trim())); } }); }); }; //#endregion //#region src/modules/parser/parse.ts const parse$1 = async (code, { onAdd, plugins }) => { const context = { imports: /* @__PURE__ */ new Map() }; let ast; try { ast = await parse(code, { syntax: "typescript", tsx: true }); } catch { ast = void 0; } if (ast === void 0) return; const visitor = { ImportDeclaration(node) { const module = node.source.value; node.specifiers.forEach((specifier) => { const specifierValue = specifier.local.value; context.imports.set(specifierValue, { alias: specifierValue, module, name: specifier.imported?.value ?? specifierValue }); }); } }; for (const plugin of plugins) { const pluginOutput = plugin(context, { getJSXAttributeValue }); const nodeKeys = Object.keys(pluginOutput); for (const nodeKey of nodeKeys) { const currentVisitorFunction = visitor[nodeKey]; visitor[nodeKey] = (node) => { if (typeof currentVisitorFunction === "function") currentVisitorFunction(node); const output = pluginOutput[nodeKey]?.(node); if (output) onAdd(output); }; } } visit(ast, visitor); }; const getJSXAttributeValue = (node) => { if (!node) return true; switch (node.type) { case "BigIntLiteral": case "BooleanLiteral": case "JSXText": case "NumericLiteral": case "StringLiteral": return node.value; case "JSXExpressionContainer": return getJSXAttributeValue(node.expression); case "NullLiteral": return null; default: return createUnknownToken(node.type); } }; /** * Helper to unify the way unknown AST token are managed. * @param token - AST token value. * @returns Formatted AST token. * @example * createUnknownToken("VariableDeclaration"); */ const createUnknownToken = (token) => `#${token}`; //#endregion //#region src/modules/scanner/scan.ts const scan = async (path, options = {}) => { const excludedFolders = options.excludeFolders ?? DEFAULT_EXCLUDED_FOLDERS; const includedFiles = options.includeFiles ?? DEFAULT_INCLUDED_FILES; const projectPaths = new fdir().withBasePath().glob("**/package.json").exclude((directoryName) => excludedFolders.includes(directoryName)).crawl(path).sync(); const projects = []; for (const projectPath of projectPaths) { const metadata = require(projectPath); const folder = dirname(projectPath); let link; try { link = await exec("git config --get remote.origin.url", { cwd: folder }); } catch { link = ""; } projects.push({ folder, link, metadata }); } return projects.map((project) => { const files = new fdir().withBasePath().glob(...includedFiles).exclude((directoryName) => excludedFolders.includes(directoryName)).crawl(project.folder).sync(); return { ...project, files }; }); }; const DEFAULT_EXCLUDED_FOLDERS = [ ".git", "node_modules", "dist", "out" ]; const DEFAULT_INCLUDED_FILES = ["**/*/!(test|*.test|stories|*.stories).?(m){j,t}s?(x)"]; //#endregion //#region src/createInstance.ts const createInstance = (path, options) => { return { async getItems() { const projects = await scan(path); const items = []; for (const project of projects) { const module = project.metadata.name; const dependencies = { ...project.metadata.devDependencies, ...project.metadata.optionalDependencies, ...project.metadata.dependencies }; const link = project.link; for (const file of project.files) { const code = readFileSync(file, "utf8"); await parse$1(code, { onAdd(item) { if (options.includeModules && options.includeModules.length > 0 && !options.includeModules.includes(item.module)) return; let version = dependencies[item.module] ?? ""; if (options.resolveInstalledVersions) try { version = require(resolvePackageJson(require.resolve(item.module, { paths: [file] }))).version; } catch {} items.push(createItem({ ...item, location: { code, file, link, module, offset: item.offset, path }, version })); }, plugins: options.plugins ?? [] }); } } return items; } }; }; //#endregion //#region src/modules/plugin/createPlugin.ts const createPlugin = (input) => { return input; }; //#endregion export { createInstance, createPlugin };