@supernovaio/cli
Version:
Supernova.io Command Line Interface
447 lines (445 loc) • 17.4 kB
JavaScript
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d7fb5558-a601-5ef4-a9cb-750e2573bb35")}catch(e){}}();
import path from "node:path";
import * as ts from "typescript";
import { analyzeComponents } from "../components/analyze.js";
import { componentKeyFrom } from "./helpers.js";
import { resolveProjectAnalyzeTarget } from "../../utils/project-setup-resolver.js";
const ignoredReferenceTypeNames = new Set([
"AbortSignal",
"Array",
"ArrayBuffer",
"Blob",
"Booleanish",
"ChangeEvent",
"ClassName",
"CSSProperties",
"Date",
"ElementType",
"Error",
"Event",
"Exclude",
"Extract",
"FocusEvent",
"FormEvent",
"HTMLElement",
"HTMLInputElement",
"HTMLLabelElement",
"IntersectionObserver",
"JSX",
"KeyboardEvent",
"LegacyRef",
"Map",
"MouseEvent",
"NonNullable",
"Omit",
"Partial",
"Pick",
"PointerEvent",
"Promise",
"PropsWithChildren",
"Readonly",
"Record",
"Ref",
"RefObject",
"Required",
"ReturnType",
"Set",
"String",
"SubmitEvent",
"SyntheticEvent",
"Uint8Array",
"URL",
]);
function serializePropType(type) {
if (type === null || type === undefined) {
return "unknown";
}
if (typeof type === "string") {
return type;
}
if (typeof type === "object") {
const maybeRaw = type.raw;
if (typeof maybeRaw === "string" && maybeRaw.trim().length > 0) {
return maybeRaw.replaceAll(/\s+/g, " ").trim();
}
}
return JSON.stringify(type).replaceAll(/\s+/g, " ").trim();
}
function nameTokens(value) {
return value
.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2")
.split(/[^a-zA-Z0-9]+/)
.map(token => token.toLowerCase())
.filter(token => token.length > 1);
}
function isExternalDeclaration(fileName) {
const normalized = fileName.replaceAll("\\", "/");
return normalized.startsWith("../") || normalized.includes("node_modules/") || normalized.includes("@types/");
}
function isJavaScriptComponent(componentPath) {
return /\.(?:cjs|js|jsx|mjs)$/.test(componentPath);
}
function isSameComponentDirectory(fileName, componentPath) {
const normalizedFileName = fileName.replaceAll("\\", "/");
const normalizedComponentPath = componentPath.replaceAll("\\", "/");
return path.posix.dirname(normalizedFileName) === path.posix.dirname(normalizedComponentPath);
}
function resolveComponentSourceFile(component, sourceRootDir) {
const fileCandidates = [
path.resolve(sourceRootDir, component.componentPath),
path.resolve(sourceRootDir, "src", component.componentPath),
];
return fileCandidates.find(candidate => ts.sys.fileExists(candidate)) ?? null;
}
function collectComponentSourceReferences(component, sourceRootDir) {
const componentFilePath = resolveComponentSourceFile(component, sourceRootDir);
const sourceText = componentFilePath ? ts.sys.readFile(componentFilePath) : undefined;
if (!componentFilePath || !sourceText) {
return new Set();
}
const scriptKind = componentFilePath.endsWith(".tsx")
? ts.ScriptKind.TSX
: componentFilePath.endsWith(".jsx")
? ts.ScriptKind.JSX
: ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(componentFilePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind);
const references = new Set();
const collectNode = (node) => {
if (ts.isBindingElement(node) && ts.isIdentifier(node.name)) {
references.add(node.name.text);
}
else if (ts.isPropertyAccessExpression(node)) {
references.add(node.name.text);
}
else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
references.add(node.argumentExpression.text);
}
ts.forEachChild(node, collectNode);
};
collectNode(sourceFile);
return references;
}
function hasComponentApiDeclaration(property, component) {
const declarations = property.declarations ?? [];
if (declarations.length === 0) {
return false;
}
const componentFileName = component.componentPath.replaceAll("\\", "/");
const componentTokens = new Set(nameTokens(component.exportName));
return declarations.some(declaration => {
const fileName = declaration.fileName.replaceAll("\\", "/");
if (fileName === componentFileName) {
return true;
}
if (isExternalDeclaration(fileName)) {
return false;
}
if (isSameComponentDirectory(fileName, componentFileName)) {
return true;
}
const declarationTokens = nameTokens(declaration.name);
return declarationTokens.some(token => componentTokens.has(token));
});
}
function shouldIncludeProp(property, component, options, sourceReferences) {
if (options.includeAllProps) {
return true;
}
if ((property.declarations?.length ?? 0) === 0) {
return isJavaScriptComponent(component.componentPath) || sourceReferences.has(property.name);
}
return hasComponentApiDeclaration(property, component);
}
function extractTypeReferences(typeText) {
const matches = typeText.match(/\b[A-Z][A-Za-z0-9_]*\b/g) ?? [];
return [...new Set(matches.filter(token => !ignoredReferenceTypeNames.has(token)))];
}
function resolveRelativeImportPath(fromFilePath, moduleSpecifier) {
if (!moduleSpecifier.startsWith(".")) {
return null;
}
const basePath = path.resolve(path.dirname(fromFilePath), moduleSpecifier);
const candidates = [
basePath,
`${basePath}.ts`,
`${basePath}.tsx`,
`${basePath}.d.ts`,
path.join(basePath, "index.ts"),
path.join(basePath, "index.tsx"),
path.join(basePath, "index.d.ts"),
];
return candidates.find(candidate => ts.sys.fileExists(candidate)) ?? null;
}
function collectImportedTypeBindings(filePath) {
const sourceText = ts.sys.readFile(filePath);
if (!sourceText) {
return new Map();
}
const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind);
const imports = new Map();
for (const statement of sourceFile.statements) {
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
continue;
}
const resolvedImportPath = resolveRelativeImportPath(filePath, statement.moduleSpecifier.text);
if (!resolvedImportPath) {
continue;
}
const namedBindings = statement.importClause?.namedBindings;
if (!namedBindings || !ts.isNamedImports(namedBindings)) {
continue;
}
for (const element of namedBindings.elements) {
const localName = element.name.getText(sourceFile);
const importedName = element.propertyName ? element.propertyName.getText(sourceFile) : localName;
imports.set(localName, { importedName, sourceFilePath: resolvedImportPath });
}
}
return imports;
}
function collectRelativeImportSourceFiles(filePath) {
const sourceText = ts.sys.readFile(filePath);
if (!sourceText) {
return [];
}
const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind);
const importSourceFiles = new Set();
for (const statement of sourceFile.statements) {
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
continue;
}
const resolvedImportPath = resolveRelativeImportPath(filePath, statement.moduleSpecifier.text);
if (resolvedImportPath) {
importSourceFiles.add(resolvedImportPath);
}
}
return [...importSourceFiles];
}
function collectLocalTypeDeclarations(filePath) {
const sourceText = ts.sys.readFile(filePath);
if (!sourceText) {
return new Map();
}
const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind);
const declarations = new Map();
const collectNode = (node) => {
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
const declaration = {
definition: node.getText(sourceFile).trim(),
lineEnd: end.line + 1,
lineStart: start.line + 1,
name: node.name.getText(sourceFile),
};
declarations.set(declaration.name, declaration);
}
};
for (const statement of sourceFile.statements) {
collectNode(statement);
}
return declarations;
}
function resolveLinkedTypes(component, sourceRootDir) {
const fileCandidates = [
path.resolve(sourceRootDir, component.componentPath),
path.resolve(sourceRootDir, "src", component.componentPath),
];
const componentFilePath = fileCandidates.find(candidate => ts.sys.fileExists(candidate));
if (!componentFilePath) {
return [];
}
const declarationsByFile = new Map();
const importsByFile = new Map();
const importSourceFilesByFile = new Map();
const ensureFileData = (filePath) => {
if (!declarationsByFile.has(filePath)) {
declarationsByFile.set(filePath, collectLocalTypeDeclarations(filePath));
}
if (!importsByFile.has(filePath)) {
importsByFile.set(filePath, collectImportedTypeBindings(filePath));
}
if (!importSourceFilesByFile.has(filePath)) {
importSourceFilesByFile.set(filePath, collectRelativeImportSourceFiles(filePath));
}
};
ensureFileData(componentFilePath);
const queue = [];
const seen = new Set();
for (const property of Object.values(component.properties)) {
const serialized = serializePropType(property.type);
for (const reference of extractTypeReferences(serialized)) {
queue.push({ contextFilePath: componentFilePath, typeName: reference });
}
}
const resolved = [];
const added = new Set();
const toDisplaySourcePath = (filePath) => {
const sourceRelative = path.relative(sourceRootDir, filePath).replaceAll(path.sep, "/");
return sourceRelative.startsWith("src/") ? sourceRelative.slice(4) : sourceRelative;
};
while (queue.length > 0) {
const current = queue.shift();
if (!current) {
continue;
}
const stateKey = `${current.contextFilePath}::${current.typeName}`;
if (seen.has(stateKey)) {
continue;
}
seen.add(stateKey);
ensureFileData(current.contextFilePath);
const fileDeclarations = declarationsByFile.get(current.contextFilePath) ?? new Map();
const declaration = fileDeclarations.get(current.typeName);
if (declaration) {
const addedKey = `${current.contextFilePath}::${declaration.name}`;
if (!added.has(addedKey)) {
resolved.push({
definition: declaration.definition,
lineEnd: declaration.lineEnd,
lineStart: declaration.lineStart,
name: declaration.name,
sourcePath: toDisplaySourcePath(current.contextFilePath),
});
added.add(addedKey);
}
for (const nestedReference of extractTypeReferences(declaration.definition)) {
if (fileDeclarations.has(nestedReference)) {
queue.push({ contextFilePath: current.contextFilePath, typeName: nestedReference });
}
}
continue;
}
const fileImports = importsByFile.get(current.contextFilePath) ?? new Map();
const importedBinding = fileImports.get(current.typeName);
if (importedBinding) {
queue.push({ contextFilePath: importedBinding.sourceFilePath, typeName: importedBinding.importedName });
continue;
}
const candidateFiles = importSourceFilesByFile.get(current.contextFilePath) ?? [];
const matchingFiles = [];
for (const candidateFilePath of candidateFiles) {
ensureFileData(candidateFilePath);
const declarations = declarationsByFile.get(candidateFilePath);
if (declarations?.has(current.typeName)) {
matchingFiles.push(candidateFilePath);
}
}
if (matchingFiles.length === 1) {
queue.push({ contextFilePath: matchingFiles[0], typeName: current.typeName });
}
}
return resolved.sort((a, b) => {
if (a.name === b.name) {
return a.sourcePath.localeCompare(b.sourcePath);
}
return a.name.localeCompare(b.name);
});
}
function mapProps(component, sourceRootDir, options) {
const sourceReferences = options.includeAllProps
? new Set()
: collectComponentSourceReferences(component, sourceRootDir);
return Object.values(component.properties)
.filter(property => shouldIncludeProp(property, component, options, sourceReferences))
.map(property => ({
name: property.name,
required: property.required,
type: serializePropType(property.type),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
function resolveEnrichmentStatus(input) {
if (input.hasDescription && input.hasProps) {
return "full";
}
if (input.hasDescription || input.hasProps) {
return "partial";
}
return "none";
}
async function analyzeComponentsFromBestTarget(projectRoot, importFrom, options) {
const candidates = [];
const resolvedTarget = resolveProjectAnalyzeTarget({
importFrom,
rootDir: projectRoot,
scannerType: "components",
});
if (resolvedTarget && typeof resolvedTarget.importFrom === "string") {
candidates.push({ importFrom: resolvedTarget.importFrom, rootDir: resolvedTarget.rootDir });
}
candidates.push({ importFrom, rootDir: projectRoot });
let lastError;
for (const candidate of candidates) {
try {
const components = await analyzeComponents({
importFrom: candidate.importFrom,
parserOptions: {
skipChildrenPropWithoutDoc: options.includeAllProps ? false : undefined,
},
rootDir: candidate.rootDir,
});
if (components.length > 0) {
return {
components,
sourceRootDir: candidate.rootDir,
};
}
}
catch (error) {
lastError = error;
continue;
}
}
if (lastError) {
throw lastError;
}
return {
components: [],
sourceRootDir: projectRoot,
};
}
export async function analyzeStaticComponents(projectRoot, importFrom, options = {}) {
const { components, sourceRootDir } = await analyzeComponentsFromBestTarget(projectRoot, importFrom, options);
const descriptors = [];
const jsdocByComponentKey = {};
const typescriptApiByComponentKey = {};
const existingKeys = new Set();
for (const component of components.sort((a, b) => a.exportName.localeCompare(b.exportName))) {
const componentKey = componentKeyFrom(component.exportName, component.componentPath, existingKeys);
const hasDescription = component.description.trim().length > 0;
const props = mapProps(component, sourceRootDir, options);
const hasProps = props.length > 0;
descriptors.push({
componentKey,
componentPath: component.componentPath,
enrichmentStatus: resolveEnrichmentStatus({ hasDescription, hasProps }),
exportName: component.exportName,
exportType: component.exportType,
});
if (hasDescription) {
jsdocByComponentKey[componentKey] = {
description: component.description,
sourcePath: component.componentPath,
};
}
if (hasProps) {
const linkedTypes = resolveLinkedTypes(component, sourceRootDir);
typescriptApiByComponentKey[componentKey] = {
linkedTypes,
props,
sourcePath: component.componentPath,
};
}
}
return {
components: descriptors.sort((a, b) => a.componentKey.localeCompare(b.componentKey)),
jsdocByComponentKey,
typescriptApiByComponentKey,
};
}
//# sourceMappingURL=static-components.js.map
//# debugId=d7fb5558-a601-5ef4-a9cb-750e2573bb35