@omlet/cli
Version:
Omlet (https://omlet.dev) is a component analytics tool that uses a CLI to scan your codebase to detect components and their usage. Get real usage insights from customizable charts to measure adoption across all projects and identify opportunities to impr
1,092 lines • 50.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzePartialToJson = exports.init = exports.analyze = exports.analyzeToJson = exports.analyzeRepo = exports.getWorkspace = exports.parse = exports.AnalysisLimitReachedError = exports.NoComponentFound = exports.NoModuleFound = exports.WorkspaceNotSetup = exports.PropValueType = exports.ObjectPropType = void 0;
const clr = __importStar(require("colorette"));
const fs_1 = require("fs");
const inquirer_1 = __importDefault(require("inquirer"));
const os_1 = __importDefault(require("os"));
const perf_hooks_1 = require("perf_hooks");
const stream_1 = require("stream");
const upath_1 = __importDefault(require("upath"));
const util_1 = require("util");
const apiClient_1 = require("./apiClient");
const binding_1 = require("./binding");
const ciUtils_1 = require("./ciUtils");
const config_1 = require("./config");
const userConfig_1 = require("./config/userConfig");
const error_1 = require("./error");
const fileUtils_1 = require("./fileUtils");
const hook_1 = require("./hook");
const jsonStream_1 = require("./jsonStream");
const logger_1 = require("./logger");
const npmUtils_1 = require("./npmUtils");
const projectUtils_1 = require("./projectUtils");
const repoUtils_1 = require("./repoUtils");
const sentry_1 = require("./sentry");
const spinner_1 = require("./spinner");
const stringUtils_1 = require("./stringUtils");
const tracking_1 = require("./tracking");
const DRY_RUN_OUTPUT_PATH = upath_1.default.join(process.cwd(), "omlet.out.json");
const INDENT = " ";
const pipeline = (0, util_1.promisify)(stream_1.pipeline);
var ObjectPropType;
(function (ObjectPropType) {
ObjectPropType["KeyValue"] = "KeyValue";
ObjectPropType["Shorthand"] = "Shorthand";
ObjectPropType["Spread"] = "Spread";
})(ObjectPropType = exports.ObjectPropType || (exports.ObjectPropType = {}));
var PropValueType;
(function (PropValueType) {
PropValueType["String"] = "String";
PropValueType["Number"] = "Number";
PropValueType["Identifier"] = "Identifier";
PropValueType["Bool"] = "Bool";
PropValueType["Regex"] = "Regex";
PropValueType["Null"] = "Null";
PropValueType["JSXElement"] = "JSXElement";
PropValueType["Function"] = "Function";
PropValueType["Getter"] = "Getter";
PropValueType["Setter"] = "Setter";
PropValueType["Array"] = "Array";
PropValueType["Object"] = "Object";
PropValueType["Spread"] = "Spread";
PropValueType["Member"] = "Member";
PropValueType["This"] = "This";
PropValueType["Super"] = "Super";
PropValueType["TemplateLiteral"] = "TemplateLiteral";
PropValueType["Expression"] = "Expression";
})(PropValueType = exports.PropValueType || (exports.PropValueType = {}));
function createWorkspaceProps(workspace) {
return {
workspaceId: workspace.id,
workspaceSlug: workspace.slug,
};
}
function transformDependency(nativeDep) {
function transformEdge(node) {
return {
...node.source,
name: node.name,
id: node.id,
};
}
return {
from: transformEdge(nativeDep.from),
to: transformEdge(nativeDep.to),
references: nativeDep.references.map(({ usages, trace }) => ({
usages,
trace: trace.map(t => ({ symbol: t.reference, source: t.source })),
})),
};
}
function transformComponent(nativeComp) {
var _a, _b, _c, _d;
const sws = nativeComp.source;
return ({
...nativeComp,
package_name: sws.source.package_name,
created_at: nativeComp.created_at ? new Date(nativeComp.created_at * 1000) : undefined,
updated_at: nativeComp.updated_at ? new Date(nativeComp.updated_at * 1000) : undefined,
dependencies: nativeComp.dependencies.map(transformDependency),
props: nativeComp.props.map(({ name, default_value, span }) => ({
name,
default_value: default_value === undefined ? undefined : JSON.parse(default_value),
span,
})),
...(nativeComp.span ? {
span: {
start: { line: (_a = nativeComp.span.start) === null || _a === void 0 ? void 0 : _a.line, column: (_b = nativeComp.span.start) === null || _b === void 0 ? void 0 : _b.column },
end: { line: (_c = nativeComp.span.end) === null || _c === void 0 ? void 0 : _c.line, column: (_d = nativeComp.span.end) === null || _d === void 0 ? void 0 : _d.column },
},
} : {}),
});
}
class TSParseError extends error_1.CliError {
constructor(reason, file) {
super("Parse error");
this.name = this.constructor.name;
this.reason = reason;
this.file = file;
}
}
class AliasParseError extends error_1.CliError {
constructor(reason, input) {
super("Alias config error", {
context: { reason, input },
});
this.name = this.constructor.name;
this.reason = reason;
this.input = input;
}
}
class GlobError extends error_1.CliError {
constructor(reason, root, pattern) {
super("Glob error", {
context: { reason, root, pattern },
});
this.name = this.constructor.name;
this.reason = reason;
this.pattern = pattern;
this.root = root;
}
}
class GitUtilError extends error_1.CliError {
constructor(reason, suggestion) {
super("Git Util error", {
context: { reason, suggestion },
});
this.name = this.constructor.name;
this.reason = reason;
this.suggestion = suggestion;
}
}
class WorkspaceAlreadySetupError extends error_1.CliError {
constructor() {
super("Workspace has already been set up");
this.name = this.constructor.name;
}
}
class AnalysisError extends error_1.CliError {
constructor(reason) {
super("Analysis error", {
context: { reason },
});
this.name = this.constructor.name;
this.reason = reason;
}
}
class UnexpectedAnalysisError extends error_1.CliError {
constructor(reason) {
super("Unexpected error", {
context: { reason },
});
this.name = this.constructor.name;
this.reason = reason;
}
}
class PathNotFound extends error_1.CliError {
constructor(path) {
super("No such file or directory", {
context: { path },
});
this.name = this.constructor.name;
this.path = path;
}
}
class NoGitRootFound extends error_1.CliError {
constructor(path) {
super("Could not find git root", { context: { path } });
this.path = path;
}
}
class NoCommitFound extends error_1.CliError {
constructor(path) {
super("Could not find commit in git root", { context: { path } });
this.path = path;
}
}
class WorkspaceNotSetup extends error_1.CliError {
constructor() {
super("Workspace has not been been set up yet");
this.name = this.constructor.name;
}
}
exports.WorkspaceNotSetup = WorkspaceNotSetup;
class NoModuleFound extends error_1.CliError {
constructor(data) {
super("Could not find any module", { context: data });
this.projectRootPath = data.projectRootPath;
this.config = data.config;
}
}
exports.NoModuleFound = NoModuleFound;
class NoComponentFound extends error_1.CliError {
constructor(data) {
super("Could not find any component", { context: data });
this.projectRootPath = data.projectRootPath;
this.config = data.config;
this.numberOfModules = data.numberOfModules;
}
}
exports.NoComponentFound = NoComponentFound;
class AnalysisLimitReachedError extends error_1.CliError {
constructor() {
super("Analysis limit reached");
this.name = this.constructor.name;
}
}
exports.AnalysisLimitReachedError = AnalysisLimitReachedError;
function transformTSParseError(ne) {
return new TSParseError(ne.TSParseError.reason, ne.TSParseError.file);
}
function transformNativeError(error) {
if (!("code" in error) || error.code === "Unknown") {
return new UnexpectedAnalysisError(error.message);
}
try {
const errorPayload = JSON.parse(error.message).error;
if ("GlobError" in errorPayload) {
return new GlobError(errorPayload.GlobError.reason, errorPayload.GlobError.root, errorPayload.GlobError.pattern);
}
else if ("AliasParseError" in errorPayload) {
return new AliasParseError(errorPayload.AliasParseError.reason, errorPayload.AliasParseError.input);
}
else if ("TSParseError" in errorPayload) {
return transformTSParseError(errorPayload);
}
else if ("AnalysisError" in errorPayload) {
return new AnalysisError(errorPayload.AnalysisError.reason);
}
else if ("GitUtilError" in errorPayload) {
return new GitUtilError(errorPayload.GitUtilError.reason, errorPayload.GitUtilError.suggestion);
}
else if ("MpscError" in errorPayload) {
return new UnexpectedAnalysisError(errorPayload.MpscError.reason);
}
else {
return new UnexpectedAnalysisError(`Unknown native error: ${error.message}`);
}
}
catch (err) {
return new UnexpectedAnalysisError(`Couldn't translate native error: ${error.message}`);
}
}
async function getProjectSetup(repoRoot, projectRoot, config, opts = {}) {
var _a;
const resolver = await projectUtils_1.ProjectSetupResolver.create(repoRoot, projectRoot, config);
const projectSetup = await resolver.getProjectSetup((_a = opts.failOnError) !== null && _a !== void 0 ? _a : false);
return projectSetup;
}
async function parse(projectRoot, config) {
const repoRoot = (0, repoUtils_1.getGitRoot)(projectRoot);
const extractedAliasMap = await getProjectSetup(repoRoot, projectRoot, config);
if (!repoRoot) {
const formattedProjectRoot = (0, fileUtils_1.normalizeTrimPath)(projectRoot);
console.log(clr.red(`No git root found on ${clr.bold(formattedProjectRoot)}.\n`));
console.log(clr.yellow("Make sure that you're running analysis on the right project path."));
return [];
}
const spinner = (0, spinner_1.createSpinner)("Parsing files");
spinner.start();
try {
const result = JSON.parse(await (0, binding_1.parse)(projectRoot, repoRoot, config.include, config.ignore, logger_1.logger.level, (0, logger_1.getLogFilePath)(), extractedAliasMap, config_1.GIT_HISTORY_LIMIT_DAYS));
spinner.succeed();
return result;
}
catch (e) {
spinner.fail();
throw e;
}
}
exports.parse = parse;
async function findInvalidDependencies(components, projectSetup) {
const invalidDependencies = {};
const validationResults = new Map();
const componentMap = new Map();
for (const component of components) {
componentMap.set(component.id, component);
}
for (const { dependencies, source: { source: { package_name: source_package_name } } } of components) {
for (const { to } of dependencies) {
const { source: { source: { mtype, package_name, path } } } = componentMap.get(to.id);
if (mtype === "local") {
continue;
}
const packageKey = `${source_package_name}::${package_name}`;
if (!validationResults.has(packageKey)) {
validationResults.set(packageKey, await projectSetup.isValidDependency(package_name, source_package_name));
}
if (!validationResults.get(packageKey)) {
const dependencyModuleKey = `${source_package_name}::${package_name}::${path}`;
invalidDependencies[dependencyModuleKey] = {
package_name,
path,
source_package_name: source_package_name,
};
}
}
}
return Object.values(invalidDependencies);
}
async function runAnalysis(repoRoot, repository, projectRoot, config, projectSetup, options) {
var _a;
const startTime = perf_hooks_1.performance.now();
let analysisResult;
const verboseOutput = (logger_1.logger.level === logger_1.LogLevel.Debug || logger_1.logger.level === logger_1.LogLevel.Trace) && options.dryRun;
const logLevel = options.quiet ? undefined : logger_1.logger.level;
const logPath = options.quiet ? undefined : (0, logger_1.getLogFilePath)();
try {
const nativeResult = await (0, binding_1.analyze)(projectRoot, repoRoot, config.include, config.ignore, logLevel, logPath, projectSetup, config_1.GIT_HISTORY_LIMIT_DAYS);
const components = nativeResult.components.map(c => ({
id: c.id,
name: c.name,
export_ids: c.exportIds,
source: JSON.parse(c.source),
created_at: c.createdAt,
updated_at: c.updatedAt,
dependencies: c.dependencies.map(d => JSON.parse(d)),
props: c.props.map(p => ({
name: p.name,
default_value: p.defaultValue,
span: {
start: p.start,
end: p.end,
},
})),
html_elements: c.htmlElements,
...(c.start && c.end ? {
span: { start: c.start, end: c.end },
} : {}),
}));
const exports = nativeResult.exports.map(e => ({
name: e.name,
module_id: JSON.parse(e.moduleId),
created_at: e.createdAt,
updated_at: e.updatedAt,
resolvedType: verboseOutput && e.resolvedType,
inferredType: verboseOutput && e.inferredType,
is_component: e.isComponent,
trace_to_declaration: e.traceToDeclaration.map(sws => JSON.parse(sws)),
}));
analysisResult = {
components,
exports,
errors: nativeResult.errors.map(e => JSON.parse(e)),
stats: JSON.parse(nativeResult.stats),
};
}
catch (error) {
throw transformNativeError(error);
}
const { components, exports, errors = [], stats } = analysisResult;
if (stats.num_of_modules === 0) {
throw new NoModuleFound({ config, projectRootPath: projectRoot });
}
if (stats.num_of_components === 0) {
throw new NoComponentFound({ config, projectRootPath: projectRoot, numberOfModules: stats.num_of_modules });
}
return {
components: components.map(c => transformComponent(c)),
exports: exports,
alias_map: {
root: projectSetup.root,
packages: projectSetup.packages,
},
meta: {
...stats,
duration_msec: Math.floor(perf_hooks_1.performance.now() - startTime),
cli_version: options.cliVersion,
node_version: process.version,
device_info: {
os: os_1.default.type(),
arch: os_1.default.arch(),
version: os_1.default.release(),
},
cli_params: {},
cli_config: config,
ci_vendor: ciUtils_1.ciVendor,
argv: process.argv.join(" "),
},
setup_issues: (_a = projectSetup.issues) !== null && _a !== void 0 ? _a : [],
invalid_dependencies: await findInvalidDependencies(components, projectSetup),
parser_errors: errors.map(e => transformTSParseError(e)),
repository: {
scope: repository.scope,
name: repository.name,
url: repository.url,
branch: repository.branch,
initialCommitHash: repository.initialCommitHash,
},
};
}
function generateAnalysisSummary(results) {
const countByPackage = {};
for (const component of results.components) {
let componentSet;
const packageName = component.package_name;
if (packageName in countByPackage) {
componentSet = countByPackage[packageName];
}
else {
componentSet = new Set();
countByPackage[packageName] = componentSet;
}
componentSet.add(component.id);
}
return {
numberOfModules: results.meta.num_of_modules,
numberOfComponents: results.meta.num_of_components,
packages: Object.keys(results.alias_map.packages).concat(results.alias_map.root.name),
componentCountByPackage: Object.entries(countByPackage).map(([packageName, componentSet]) => [packageName, componentSet.size]).reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {}),
};
}
function printInputParameters(projectRoot, config, indentationOffset = 0) {
const offset = INDENT.repeat(indentationOffset);
console.log("Parameter used in the scan:");
console.log(`${offset}${INDENT}${clr.cyan("Working directory:")}\n${INDENT}${INDENT}${process.cwd()}`);
console.log(`${offset}${INDENT}${clr.cyan("Project path:")}\n${INDENT}${INDENT}${projectRoot}`);
if ("include" in config) {
console.log(`${offset}${INDENT}${clr.cyan("Include patterns:")}\n${config.include.length ? config.include.map(p => `${offset}${INDENT}${INDENT}- ${p}`).join("\n") : "None"}`);
}
console.log(`${offset}${INDENT}${clr.cyan("Ignore patterns:")}\n${config.ignore.length ? config.ignore.map(p => `${offset}${INDENT}${INDENT}- ${p}`).join("\n") : "None"}`);
}
function getWorkspaceUrl(workspace) {
return new URL(`/${workspace.slug}`, config_1.BASE_URL).toString();
}
async function writeJSON(filePath, data) {
await pipeline((0, jsonStream_1.toJsonStringStream)(data, { spaces: 2 }), (0, fs_1.createWriteStream)(filePath));
}
function sleep(msec) {
return new Promise((resolve) => {
setTimeout(resolve, msec);
});
}
function printAnalyzeError(message, description, code) {
console.error(clr.red(message));
if (description) {
const codeStr = code && clr.dim(`(code: ${code})`);
console.error(clr.dim(description), codeStr !== null && codeStr !== void 0 ? codeStr : "");
}
}
function groupIssuesBy(issues, keyFn) {
const result = {};
for (const issue of issues) {
const key = keyFn(issue);
if (!result[key]) {
result[key] = [];
}
result[key].push(issue);
}
for (const key in result) {
result[key].sort((a, b) => a.packageName.localeCompare(b.packageName));
}
return result;
}
function printProjectSetupIssues(issues, reportLevel) {
const issueGroupToString = (issuesInGroup, entryTypeLabel, sourcePath) => {
const source = upath_1.default.relative(process.cwd(), sourcePath);
return [
`${INDENT}Source: ${source}`,
...issuesInGroup.map(({ level, entry, packageName }) => {
const entryName = level === projectUtils_1.ResolutionConfigIssueLevel.Error ? clr.bold(entry.name) : entry.name;
return `${INDENT} - Package: ${packageName}, ${entryTypeLabel}: "${entryName}", Patterns: ${JSON.stringify(entry.patterns)}`;
}),
].join("\n");
};
const keyEntrySourceFn = (issue) => issue.entry.sourcePath;
const keyEntryTypeFn = (issue) => issue.entry.type;
const errorsByEntryType = groupIssuesBy(issues.filter(i => i.type === projectUtils_1.ResolutionConfigIssueType.TargetNotExist), keyEntryTypeFn);
const warningsByEntryType = groupIssuesBy(issues.filter(i => i.type === projectUtils_1.ResolutionConfigIssueType.TargetNotIncluded), keyEntryTypeFn);
console.error("Issues detected in your project configuration!\n");
const { [projectUtils_1.PathResolutionEntryType.Alias]: aliasErrors, [projectUtils_1.PathResolutionEntryType.Export]: exportErrors } = errorsByEntryType;
if (aliasErrors || exportErrors) {
console.error(clr.bold(clr.red("Errors:")));
if (exportErrors) {
console.error(clr.bold("Cannot find export source in the project"));
for (const [source, issuesInGroup] of Object.entries(groupIssuesBy(exportErrors, keyEntrySourceFn))) {
console.error(`${issueGroupToString(issuesInGroup, "Export", source)}\n`);
}
}
if (aliasErrors) {
console.error(clr.bold("Cannot find alias target in the project"));
for (const [source, issuesInGroup] of Object.entries(groupIssuesBy(aliasErrors, keyEntrySourceFn))) {
console.error(`${issueGroupToString(issuesInGroup, "Alias", source)}\n`);
}
}
}
const { [projectUtils_1.PathResolutionEntryType.Alias]: aliasWarnings, [projectUtils_1.PathResolutionEntryType.Export]: exportWarnings } = warningsByEntryType;
if (aliasWarnings || exportWarnings) {
console.error(clr.bold(clr.yellow("Warnings:")));
if (exportWarnings) {
console.error(clr.bold("Export source not included in the scan"));
for (const [source, issuesInGroup] of Object.entries(groupIssuesBy(exportWarnings, keyEntrySourceFn))) {
console.error(`${issueGroupToString(issuesInGroup, "Export", source)}\n`);
}
}
if (aliasWarnings) {
console.error(clr.bold("Alias target not included in the scan"));
for (const [source, issuesInGroup] of Object.entries(groupIssuesBy(aliasWarnings, keyEntrySourceFn))) {
console.error(`${issueGroupToString(issuesInGroup, "Alias", source)}\n`);
}
}
}
const infoLineFirst = "Visit https://feta.omlet.dev/l/docs/cli-config for details on how to configure Omlet CLI to fix those issues.";
const infoLineSecond = `Ping us at ${config_1.OMLET_EMAIL} if you have any questions.`;
if (reportLevel === projectUtils_1.ResolutionConfigIssueLevel.Error) {
console.error(infoLineFirst);
console.error(infoLineSecond);
console.error(clr.dim("You can disable validation of project setup by passing --no-verify option."));
}
else {
console.error(clr.dim(infoLineFirst));
console.error(clr.dim(infoLineSecond));
console.error(clr.dim("You can silence these warnings by passing --quiet option."));
}
console.error();
}
function printProjectSetupError(error, quietOutput) {
if (error.issues.length === 0) {
console.error("Project configuration validation failed with an unexpected error", `Details:\n${error.message}\n\nReach out to us at ${config_1.OMLET_EMAIL} with the error logs`);
return;
}
const level = error.level;
if (quietOutput && level === projectUtils_1.ResolutionConfigIssueLevel.Warning) {
return;
}
printProjectSetupIssues(error.issues, level);
}
function trackProjectSetupError(error) {
const { issues } = error;
if (error.issues.length === 0) {
(0, tracking_1.trackProjectSetupValidationError)(error.message);
return;
}
const errorCount = issues.filter(i => i.level === projectUtils_1.ResolutionConfigIssueLevel.Error).length;
const warningCount = issues.filter(i => i.level === projectUtils_1.ResolutionConfigIssueLevel.Warning).length;
(0, tracking_1.trackProjectSetupValidationError)(error.message, {
numberOfIssues: issues.length,
numberOfErrors: errorCount,
numberOfWarnings: warningCount,
});
}
function handleConfigError(error) {
console.log(clr.red("Failed to load the configuration file."));
console.log(clr.yellow(`Error: ${error.message}`));
console.log(clr.dim("Please make sure that configuration file exists."));
console.log(clr.dim("See https://feta.omlet.dev/l/docs/cli-config for details."));
(0, logger_1.logError)(error);
(0, tracking_1.trackConfigLoadingError)(error.message);
}
function handleConfigValidationError(error) {
console.log(clr.red("Failed to load the configuration file."));
console.log(clr.yellow(`Error: ${error.message}`));
console.log(clr.dim("Please make sure that configuration file valid."));
console.log(clr.dim("See https://feta.omlet.dev/l/docs/cli-config for details."));
(0, logger_1.logError)(error);
(0, tracking_1.trackConfigLoadingError)(error.message);
}
function printUnexpectedError(error) {
printAnalyzeError("Analysis failed with an unexpected error", `Details: ${error.message}\nTry again and if the issue continues, ping us at ${config_1.OMLET_EMAIL} with the error logs:\n${(0, logger_1.getLogFilePath)()}`);
}
function handleAnalyzeError(error, quietOutput) {
var _a;
const logFilePath = (0, logger_1.getLogFilePath)();
console.log("");
if (error instanceof PathNotFound) {
printAnalyzeError(`No directory at ${clr.bold((0, fileUtils_1.normalizeTrimPath)(error.path))}`, "Double check the repository path and try again.");
}
else if (error instanceof NoGitRootFound) {
printAnalyzeError(`No git config at ${clr.bold((0, fileUtils_1.normalizeTrimPath)(error.path))}`, "Omlet uses git to collect historical usage data — make sure that you’re analyzing a repo using git.");
}
else if (error instanceof NoCommitFound) {
printAnalyzeError(`No git history at ${clr.bold((0, fileUtils_1.normalizeTrimPath)(error.path))}`, "Omlet uses git to collect historical usage data — make sure that you have commits in your repository.");
}
else if (error instanceof projectUtils_1.NoProjectFound) {
printAnalyzeError(`No package.json at ${clr.bold((0, fileUtils_1.normalizeTrimPath)(error.path))}`, "Double-check the repository path and try again.\n" +
"\n" +
"Omlet currently only supports React and React Native projects — let us know if you want Omlet to support more languages/frameworks: https://feta.omlet.dev/l/framework-request");
}
else if (error instanceof projectUtils_1.CannotLoadTSConfig) {
const extendErrorPath = (_a = error.detail.match(/File '(.*)' not found\./)) === null || _a === void 0 ? void 0 : _a[1];
if (extendErrorPath) {
printAnalyzeError(`No TSConfig found at ${(0, fileUtils_1.normalizeTrimPath)(extendErrorPath)}`, `${(0, fileUtils_1.normalizeTrimPath)(error.path)} extends ${(0, fileUtils_1.normalizeTrimPath)(extendErrorPath)}, but it is not found. Make sure to install dependencies, double-check the file path and try again.`);
}
else {
printAnalyzeError(`Invalid TSConfig at ${(0, fileUtils_1.normalizeTrimPath)(error.path)}`, error.detail);
}
}
else if (error instanceof GlobError) {
printAnalyzeError("Invalid glob pattern", `Failed parsing glob "${error.pattern}": ${error.reason}`);
}
else if (error instanceof GitUtilError) {
printAnalyzeError("Analyzing git history failed", "Omlet uses git to collect historical usage data — make sure that the repo is not a shallow clone.");
}
else if (error instanceof AliasParseError) {
printAnalyzeError("Parsing alias configuration failed", `Details: ${error.reason}\nReach out to us at ${config_1.OMLET_EMAIL} with the error logs:\n${logFilePath}`);
}
else if (error instanceof AnalysisError || error instanceof UnexpectedAnalysisError) {
printUnexpectedError(error);
}
else if (error instanceof NoModuleFound) {
printAnalyzeError("No JavaScript/TypeScript modules found on the project directory", "Double check the repository path, input parameters and try again.");
printInputParameters(error.projectRootPath, error.config);
}
else if (error instanceof NoComponentFound) {
printAnalyzeError(`No components found in ${(0, stringUtils_1.pluralize)("scanned module", error.numberOfModules)}`, "Omlet currently only supports React and React Native projects — let us know if you want Omlet to support more languages/frameworks: https://feta.omlet.dev/l/framework-request");
printInputParameters(error.projectRootPath, error.config);
}
else if (error instanceof projectUtils_1.InvalidProjectSetup) {
printProjectSetupError(error, quietOutput);
}
else if (error instanceof hook_1.CliHookError) {
const details = error.reason ? `${error.reason}\n` : error.message;
if (error.reason) {
// TODO: Update the error message when the docs are ready
printAnalyzeError("Hook script failed to run:", `${error.reason.stack}\n\nReach out to us at ${config_1.OMLET_EMAIL} with the error logs:\n${logFilePath}`);
}
else {
printAnalyzeError("Error while running CLI hook", `Details: ${details}\nReach out to us at ${config_1.OMLET_EMAIL} with the error logs:\n${logFilePath}`);
}
}
else {
printUnexpectedError(error);
}
console.error("");
(0, logger_1.logError)(error);
if (error instanceof projectUtils_1.InvalidProjectSetup) {
trackProjectSetupError(error);
}
else {
(0, tracking_1.trackAnalyzeError)(error.message);
}
}
function handleApiError(error) {
console.log("");
const { title, detail, code } = error.errorInfo;
printAnalyzeError(title, detail, code);
(0, tracking_1.trackAnalyzeError)(error.message, { code });
}
async function getWorkspace() {
if (config_1.WORKSPACE_SLUG) {
try {
return await (0, apiClient_1.getWorkspace)(config_1.WORKSPACE_SLUG);
}
catch (error) {
if (error instanceof apiClient_1.ApiError && error.errorInfo.code === apiClient_1.ErrorResponseCode.WORKSPACE_NOT_FOUND) {
console.error(`${clr.red("Workspace not found")}`);
console.error(clr.dim("Please remove `OMLET_WORKSPACE_SLUG` variable to use your default workspace."));
console.error(clr.dim(`Ping us at ${config_1.OMLET_EMAIL} if you have any questions.`));
}
throw error;
}
}
try {
return await (0, apiClient_1.getDefaultWorkspace)();
}
catch (error) {
if (error instanceof apiClient_1.ApiError && error.errorInfo.code === apiClient_1.ErrorResponseCode.USER_NOT_HAVE_WORKSPACE) {
console.error(`${clr.red("You don't have a workspace to set up.")}`);
console.error(clr.dim("Visit https://feta.omlet.dev/create-workspace to create your workspace and then run the init command first."));
console.error(clr.dim(`Ping us at ${config_1.OMLET_EMAIL} if you have any questions.`));
}
throw error;
}
}
exports.getWorkspace = getWorkspace;
async function analyzeRepo(projectRoot, options) {
var _a;
const resolvedProjectRoot = (0, fileUtils_1.resolvePath)(projectRoot);
logger_1.logger.debug(`Running analyze at ${resolvedProjectRoot} with params:${JSON.stringify(options, null, 2)}`);
logger_1.logger.info(config_1.HTTP_PROXY_URL ? `Using proxy ${config_1.HTTP_PROXY_URL}` : "No proxy used");
console.log(`Analyzing the project at ${resolvedProjectRoot}…${options.dryRun ? ` ${clr.dim("(dry-run)")}` : ""}\n`);
if (!await (0, fileUtils_1.pathExists)(resolvedProjectRoot)) {
const error = new PathNotFound(resolvedProjectRoot);
handleAnalyzeError(error, options.quiet);
throw error;
}
const repoRoot = (0, repoUtils_1.getGitRoot)(resolvedProjectRoot);
if (!repoRoot) {
const error = new NoGitRootFound(resolvedProjectRoot);
handleAnalyzeError(error, options.quiet);
throw error;
}
const repository = await (0, repoUtils_1.getRepoInfo)(repoRoot);
if (repository === undefined) {
const error = new NoCommitFound(resolvedProjectRoot);
handleAnalyzeError(error, options.quiet);
throw error;
}
let config;
try {
config = await (0, config_1.loadConfig)(repoRoot, resolvedProjectRoot, options.cliParams, options.cliParams.configPath);
logger_1.logger.debug(`Read user config from ${config.configPath} (input path: ${(_a = options.cliParams.configPath) !== null && _a !== void 0 ? _a : "none"}, repo root: ${repoRoot}, project root: ${resolvedProjectRoot}):`);
logger_1.logger.debug(JSON.stringify(config, null, 2));
}
catch (error) {
if (error instanceof config_1.ConfigError) {
handleConfigError(error);
}
else if (error instanceof userConfig_1.ConfigValidationError) {
handleConfigValidationError(error);
}
throw error;
}
let projectSetup;
const analysisSpinner = (0, spinner_1.createSpinner)("Detecting components and collecting component usages…");
try {
projectSetup = await getProjectSetup(repoRoot, resolvedProjectRoot, config, { failOnError: options.verifySetup });
logger_1.logger.debug("Project setup extracted successfully");
analysisSpinner.start();
const analysisResult = await runAnalysis(repoRoot, repository, resolvedProjectRoot, config, projectSetup, options);
if (config.hookScript) {
(0, tracking_1.trackHookScript)();
const hookContext = await (0, hook_1.initHooks)(config.hookScript, analysisResult);
await hookContext.afterScan();
for (const component of analysisResult.components) {
const metadata = hookContext.getComponentMetadata(component.id);
if (metadata) {
component.metadata = metadata;
}
}
}
analysisSpinner.succeed();
console.log("");
if (options.showSummary) {
const analysisSummary = generateAnalysisSummary(analysisResult);
console.log(`\n${clr.bold("Summary:")}`);
console.log(`${INDENT}${analysisSummary.numberOfModules} modules have been scanned successfully and ${clr.bold(clr.cyan(analysisSummary.numberOfComponents))} components detected.\n`);
console.log(`${INDENT}${clr.yellow("Number of components by package:")}`);
Object.entries(analysisSummary.componentCountByPackage).sort((e1, e2) => e2[1] - e1[1]).forEach(([packageName, count]) => {
console.log(`${INDENT}${INDENT}- ${packageName}: ${count ? count : "None"}`);
});
console.log("");
}
if (projectSetup.issues && projectSetup.issues.length > 0 && !options.quiet) {
printProjectSetupIssues(projectSetup.issues, projectUtils_1.ResolutionConfigIssueLevel.Warning);
}
return analysisResult;
}
catch (e) {
analysisSpinner.fail();
const error = e;
handleAnalyzeError(error, options.quiet);
(0, logger_1.logError)(error);
throw error;
}
}
exports.analyzeRepo = analyzeRepo;
async function analyzeToJson(options) {
var _a, _b;
const resolvedProjectRoot = (0, fileUtils_1.resolvePath)((_a = options.root) !== null && _a !== void 0 ? _a : process.cwd());
if (!await (0, fileUtils_1.pathExists)(resolvedProjectRoot)) {
throw new PathNotFound(resolvedProjectRoot);
}
const repoRoot = (0, repoUtils_1.getGitRoot)(resolvedProjectRoot);
if (!repoRoot) {
throw new NoGitRootFound(resolvedProjectRoot);
}
const repository = await (0, repoUtils_1.getRepoInfo)(repoRoot);
if (repository === undefined) {
throw new NoCommitFound(resolvedProjectRoot);
}
const config = await (0, config_1.loadConfig)(repoRoot, resolvedProjectRoot, {
include: options.include,
ignore: options.ignore,
tsconfigPath: options.tsconfigPath,
}, options.configPath);
const projectSetup = await getProjectSetup(repoRoot, resolvedProjectRoot, config, {
failOnError: (_b = options.verify) !== null && _b !== void 0 ? _b : true,
});
const analysisResult = await runAnalysis(repoRoot, repository, resolvedProjectRoot, config, projectSetup, {
dryRun: false,
cliVersion: (0, npmUtils_1.getCliVersion)(),
quiet: true,
});
return analysisResult;
}
exports.analyzeToJson = analyzeToJson;
async function analyze(projectRoot, options) {
let workspace;
if (!options.dryRun) {
workspace = await getWorkspace();
(0, tracking_1.setDefaultProps)(createWorkspaceProps(workspace));
(0, sentry_1.setErrorProps)(createWorkspaceProps(workspace));
if (workspace.hasReachedAnalysisLimit) {
(0, tracking_1.trackAnalysisLimitReachedError)();
const analysisLimitStr = (0, stringUtils_1.pluralize)("scan", workspace.subscription.features.analysisLimit);
printAnalyzeError("Scan limit reached", [
`Looks like this workspace has already reached the limit of ${analysisLimitStr} in the last 30 days.`,
"To continue analyzing your components, contact us to upgrade your plan: https://omlet.dev/contact",
].join("\n"));
throw new AnalysisLimitReachedError();
}
if (workspace.projects.length === 0) {
(0, tracking_1.trackAnalyzeBeforeInitError)();
console.error(`${clr.red("You have not completed setup for this workspace.")}`);
console.error(clr.dim("Run the init command to go through a guided process to initialize your workspace first."));
console.error(clr.dim("Visit https://feta.omlet.dev/l/docs/cli/init for more details."));
console.error(clr.dim(`Ping us at ${config_1.OMLET_EMAIL} if you have any questions.`));
throw new WorkspaceNotSetup();
}
}
console.log(`${clr.bgGreen(clr.black("Omlet"))} ${clr.green(`v${options.cliVersion}`)}\n`);
if (!options.dryRun) {
console.log(`${clr.bgYellow(clr.black("(^▽^)ノ"))} Good to see you again!\n`);
}
const analysisData = await analyzeRepo(projectRoot, { ...options, showSummary: true });
const logFilePath = (0, logger_1.getLogFilePath)();
if (options.dryRun) {
console.log(`Saving results to the file:\n${INDENT}${DRY_RUN_OUTPUT_PATH}\n`);
try {
await writeJSON(DRY_RUN_OUTPUT_PATH, analysisData);
}
catch (error) {
printAnalyzeError("Couldn't write dry-run output to the file", `Try again and if the issue continues, ping us at ${config_1.OMLET_EMAIL} with the error logs:\n${logFilePath}`);
(0, logger_1.logError)(error);
throw error;
}
}
else {
if (!workspace) {
workspace = await getWorkspace();
}
const submitSpinner = (0, spinner_1.createSpinner)("Uploading analysis to Omlet…");
submitSpinner.start();
try {
const { meta: { dataIssueCount } } = await (0, apiClient_1.postAnalysis)(workspace, analysisData);
submitSpinner.succeed();
console.log("");
console.log(`${clr.bgGreen(clr.black("(ノ◕ヮ◕)ノ*:・゚✧"))} ${clr.bold("All good - analysis in progress…")}`);
console.log(` In a minute, you can view the results in Omlet: ${clr.underline(clr.blue(getWorkspaceUrl(workspace)))}`);
if (dataIssueCount > 0) {
console.log("");
const issueUrl = `${new URL(getWorkspaceUrl(workspace))}/data-issues`;
console.log(clr.dim(`Omlet ran into ${(0, stringUtils_1.pluralize)("issue", dataIssueCount)} that might affect data accuracy. Please review them here: ${clr.underline(clr.blue(issueUrl))}`));
}
}
catch (e) {
submitSpinner.fail();
const error = e;
handleApiError(error);
throw error;
}
}
}
exports.analyze = analyze;
function isLocalComponent(c) {
return c.source.source.mtype === "local";
}
function getLocalPackages(analysisData) {
const localPackageNames = new Set();
analysisData.components.forEach(c => {
if (isLocalComponent(c)) {
localPackageNames.add(c.package_name);
}
});
return [...localPackageNames];
}
function extractPackageStats(analyses) {
const packages = {};
for (const analysis of analyses) {
analysis.components.forEach(c => {
if (isLocalComponent(c)) {
const packageName = c.package_name;
if (!packages[packageName]) {
packages[packageName] = { componentCount: 0 };
}
packages[packageName].componentCount += 1;
}
});
}
return packages;
}
const collator = new Intl.Collator("en", { caseFirst: "upper", numeric: true });
function comparePackageName(a, b) {
if (a.startsWith("@") && !b.startsWith("@")) {
return 1;
}
if (!a.startsWith("@") && b.startsWith("@")) {
return -1;
}
return collator.compare(a, b);
}
async function init(projectRoot, options) {
const analyzeOptions = { ...options, showSummary: false, cliParams: {} };
const { cliVersion } = options;
const logFilePath = (0, logger_1.getLogFilePath)();
logger_1.logger.info(config_1.HTTP_PROXY_URL ? `Using proxy ${config_1.HTTP_PROXY_URL}` : "No proxy used");
const workspace = await getWorkspace();
(0, tracking_1.setDefaultProps)(createWorkspaceProps(workspace));
(0, sentry_1.setErrorProps)(createWorkspaceProps(workspace));
console.log(`${clr.bgGreen(clr.black("Omlet"))} ${clr.green(`v${cliVersion}`)}\n`);
console.log(`${clr.bgYellow(clr.black("(^▽^)ノ"))} Welcome to Omlet!\n`);
await sleep(1000);
if (workspace.projects.length > 0) {
(0, tracking_1.trackInitAfterAnalyzeError)();
console.error(`${clr.red("Setup has already been completed for this workspace")}`);
console.error(clr.dim("Visit https://feta.omlet.dev/l/docs/cli/init for more details on how to reset your workspace and set it up again."));
console.error(clr.dim(`Ping us at ${config_1.OMLET_EMAIL} if you have any questions.`));
throw new WorkspaceAlreadySetupError();
}
const analyses = [];
const initialAnalysis = await analyzeRepo(projectRoot, analyzeOptions);
const packages = getLocalPackages(initialAnalysis);
analyses.push(initialAnalysis);
if (packages.length === 1) {
console.log(`Looks like you only scanned one project: ${clr.bold(packages[0])}\n`);
console.log(`${clr.dim("To get the most value out of Omlet, make sure to scan both projects that contain components and projects that use them.")}\n`);
const repoPrompt = (await inquirer_1.default.prompt([{
type: "confirm",
message: `Scan another repository? ${clr.reset(clr.dim("(Recommended)"))}`,
prefix: clr.bold(clr.yellow("?")),
name: "hasSeparateRepo",
default: true,
}]));
if (repoPrompt.hasSeparateRepo) {
let retry = false;
let numberOfRetries = 0;
do {
if (numberOfRetries > 3) {
console.error(`${clr.red("Setup could not be completed because of too many errors!")}`);
console.error(`${clr.dim(`Double check the repository path and try again.\nIf the issue persists, reach out to us at ${config_1.OMLET_EMAIL} with the error logs:\n${logFilePath}`)}`);
return;
}
const repoPathPrompt = (await inquirer_1.default.prompt([{
message: `Enter path to repository${retry ? clr.dim(" (Press return to cancel)") : ""}\n`,
prefix: clr.bold(clr.yellow("?")),
name: "appRepoPath",
}]));
if (retry && !repoPathPrompt.appRepoPath) {
break;
}
else if (!repoPathPrompt.appRepoPath) {
retry = true;
continue;
}
try {
console.log("");
const secondAnalysis = await analyzeRepo(repoPathPrompt.appRepoPath, analyzeOptions);
analyses.push(secondAnalysis);
(0, tracking_1.trackInitSecondScan)();
retry = false;
}
catch (e) {
const error = e;
(0, tracking_1.trackInitSecondScanError)(error.message);
numberOfRetries += 1;
retry = true;
}
} while (retry);
}
}
const sortedStats = Object.entries(extractPackageStats(analyses)).sort((p1, p2) => comparePackageName(p1[0], p2[0]));
const packageCount = Object.keys(sortedStats).length;
console.log(`Found ${packageCount > 1 ? `${packageCount} projects` : "1 project"}:`);
for (const [pkg, { componentCount }] of sortedStats) {
console.log(`${INDENT}• ${pkg} ${clr.dim(componentCount > 1 ? `${componentCount} components` : "1 component")}`);
}
console.log("");
await sleep(1000);
const submitSpinner = (0, spinner_1.createSpinner)("Uploading analysis to Omlet…");
submitSpinner.start();
try {
await (0, apiClient_1.initWorkspace)(workspace, analyses);
submitSpinner.succeed();
console.log("");
console.log(`${clr.bgGreen(clr.black("(ノ◕ヮ◕)ノ*:・゚✧"))} ${clr.bold("Analysis complete!")}`);
console.log(` View results in Omlet: ${clr.underline(clr.blue(getWorkspaceUrl(workspace)))}`);
}
catch (e) {
submitSpinner.fail();
const error = e;
handleApiError(error);
throw error;
}
}
exports.init = init;
async function analyzePartialToJson(options) {
var _a, _b, _c;
const startTime = perf_hooks_1.performance.now();
const resolvedProjectRoot = (0, fileUtils_1.resolvePath)((_a = options.root) !== null && _a !== void 0 ? _a : process.cwd());
if (!await (0, fileUtils_1.pathExists)(resolvedProjectRoot)) {
throw new PathNotFound(resolvedProjectRoot);
}
const repoRoot = (0, repoUtils_1.getGitRoot)(resolvedProjectRoot);
if (!repoRoot) {
throw new NoGitRootFound(resolvedProjectRoot);
}
const repository = await (0, repoUtils_1.getRepoInfo)(repoRoot);
if (repository === undefined) {
throw new NoCommitFound(resolvedProjectRoot);
}
const config = await (0, config_1.loadConfig)(repoRoot, resolvedProjectRoot, {
ignore: options.ignore,
tsconfigPath: options.tsconfigPath,
}, options.configPath);
const projectSetup = await getProjectSetup(repoRoot, resolvedProjectRoot, config, {
failOnError: (_b = options.verify) !== null && _b !== void 0 ? _b : true,
});
const analysisResult = await (0, binding_1.analyzePartial)(resolvedProjectRoot, options.modifiedFiles, options.relatedFiles, config.ignore || [], projectSetup);
const components = analysisResult.components.map(c => ({
id: c.id,
name: c.name,
export_ids: c.exportIds,
source: JSON.parse(c.source),
created_at: c.createdAt,
updated_at: c.updatedAt,
dependencies: c.dependencies.map(d => JSON.parse(d)),
props: c.props.map(p => ({
name: p.name,
default_value: p.defaultValue,
span: {
start: p.start,
end: p.end,
},
})),
html_elements: c.htmlElements,
...(c.start && c.end ? {
span: { start: c.start, end: c.end },
} : {}),
}));
const exports = analysisResult.exports.map(e => ({
name: e.name,
module_id: JSON.parse(e.moduleId),
created_at: e.createdAt,
updated_at: e.updatedAt,
resolvedType: e.resolvedType,
inferredType: e.inferredType,
is_component: e.isComponent,
trace_to_declaration: e.traceToDeclaration.map(sws => JSON.parse(sws)),
}));
const errors = analysisResult.errors.map(e => JSON.parse(e));
const parsedStats = JSON.parse(analysisResult.stats);
return {
components: components.map(c => transformComponent(c)),
exports,
alias_map: {
root: projectSetup.root,
packages: projectSetup.packages,
},
meta: {
...parsedStats,
duration_msec: Math.floor(perf_hooks_1.performance.now() - startTime),
cli_version: (0, npmUtils_1.getCliVersion)(),
node_version: process.version,
device_info: {
os: os_1.default.type(),
arch: os_1.default.arch(),
version: os_1.default.release(),
},
cli_params: {},
cli_config: config,
ci_vendor: ciUtils_1.ciVendor,
argv: process.argv.join(" "),
},
setup_issues: (_c = projectSetup.issues) !== null && _c !== void 0 ? _c : [],
invalid_dependencies: await findInvalidDependencies(components, projectSetup),
parser_errors: errors.map(e => transformTSParseError