UNPKG

@supernovaio/cli

Version:

Supernova.io Command Line Interface

447 lines (445 loc) 17.4 kB
!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