Skip to content

Commit

Permalink
Add detect-project command to CLI (thirdweb-dev#926)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonas Daniels <[email protected]>
  • Loading branch information
Marfuen and jnsdls authored May 16, 2023
1 parent 49ec2d1 commit 84fc3b6
Show file tree
Hide file tree
Showing 55 changed files with 1,693 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-keys-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Added detect-project command, reorganized folder structure for detecting, added many more detections.
5 changes: 5 additions & 0 deletions .changeset/red-panthers-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Added log to see detected app type to the "detect-project" command
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public/dist
yalc.lock
build/
playwright-report/
.vscode/
.vscode/

# Artifacts
packages/cli/artifacts/
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@types/cross-spawn": "^6.0.2",
"@types/inquirer": "^8.2.1",
"@types/jest": "^29.2.1",
"@types/js-yaml": "^4.0.5",
"@types/node": "^18.0.0",
"@types/prompts": "^2.0.14",
"@types/rimraf": "^3.0.0",
Expand All @@ -34,6 +35,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@thirdweb-dev/chains": "workspace:*",
"@thirdweb-dev/sdk": "workspace:*",
"@thirdweb-dev/storage": "workspace:*",
Expand All @@ -44,6 +46,7 @@
"ethers": "^5.7.2",
"got": "11.8.5",
"inquirer": "^8.2.3",
"js-yaml": "^4.1.0",
"open": "^8.4.0",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { detectExtensions } from "../common/feature-detector";
import { detectProjectV2 } from "../common/project-detector-v2";
import { processProject } from "../common/processor";
import { cliVersion, pkg } from "../constants/urls";
import { info, logger, spinner } from "../core/helpers/logger";
Expand Down Expand Up @@ -406,6 +407,17 @@ const main = async () => {
await detectExtensions(options);
});

program
.command("detect-project")
.description(
"Detect the type of project your are running and let you know what it is.",
)
.option("-p, --path <project-path>", "path to project", ".")
.option("-d, --debug", "show debug logs")
.action(async (options) => {
await detectProjectV2(options);
});

program
.command("generate")
.description(
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/src/common/project-detector-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import detectPackageManager from "../core/detection/detectPackageManager";
import detectFramework from "../core/detection/detectFramework";
import detectLibrary from "../core/detection/detectLibrary";
import detectLanguage from "../core/detection/detectLanguage";
import { logger } from "../core/helpers/logger";
import path from "path";
import { ContractLibrariesType, contractLibraries } from "../core/types/ProjectType";

export async function detectProjectV2(options: any) {
logger.setSettings({
minLevel: options.debug ? "debug" : "info",
});

let projectPath = process.cwd();
if (options.path) {
logger.debug("Overriding project path to " + options.path);

const resolvedPath = (options.path as string).startsWith("/")
? options.path
: path.resolve(`${projectPath}/${options.path}`);
projectPath = resolvedPath;
}

logger.debug("Processing project at path " + projectPath);

const detectedPackageManager = await detectPackageManager(
projectPath,
options,
);
const detectedLanguage = await detectLanguage(projectPath, options);
const detectedLibrary = await detectLibrary(
projectPath,
options,
detectedPackageManager,
);
const detectedFramework = await detectFramework(
projectPath,
options,
detectedPackageManager,
);
const detectedAppType = contractLibraries.includes(detectedFramework as ContractLibrariesType) ? "contract" : "app";

logger.debug("Detected package manager: " + detectedPackageManager);
logger.debug("Detected library: " + detectedLibrary);
logger.debug("Detected language: " + detectedLanguage);
logger.debug("Detected framework: " + detectedFramework);
logger.debug("Detected app type: " + detectedAppType);
}
182 changes: 182 additions & 0 deletions packages/cli/src/common/project-detector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { ALWAYS_SUGGESTED } from "../constants/features";
import build from "../core/builder/build";
import detect from "../core/detection/detect";
import detectPackageManager from "../core/detection/detectPackageManager";
import { logger, spinner } from "../core/helpers/logger";
import { createContractsPrompt } from "../core/helpers/selector";
import { ContractFeatures, Feature } from "../core/interfaces/ContractFeatures";
import { ContractPayload } from "../core/interfaces/ContractPayload";
import { getPkgManager } from "../create/helpers/get-pkg-manager";
import { detectFeatures, FeatureWithEnabled } from "@thirdweb-dev/sdk";
import chalk from "chalk";
import { existsSync, readFileSync } from "fs";
import ora from "ora";
import path from "path";

export async function detectProject(options: any) {
logger.setSettings({
minLevel: options.debug ? "debug" : "info",
});

let projectPath = process.cwd();
if (options.path) {
logger.debug("Overriding project path to " + options.path);

const resolvedPath = (options.path as string).startsWith("/")
? options.path
: path.resolve(`${projectPath}/${options.path}`);
projectPath = resolvedPath;
}

logger.debug("Processing project at path " + projectPath);

const packageManagerType = await detectPackageManager(projectPath, options);
console.log(packageManagerType);
const projectType = await detect(projectPath, options);

let compiledResult;
const compileLoader = spinner("Compiling project...");
try {
compiledResult = await build(projectPath, projectType, options);
} catch (e) {
compileLoader.fail("Compilation failed");
logger.error(e);
process.exit(1);
}
compileLoader.succeed("Compilation successful");

let selectedContracts: ContractPayload[] = [];
if (compiledResult.contracts.length === 1) {
selectedContracts = [compiledResult.contracts[0]];
} else {
if (options.all) {
selectedContracts = compiledResult.contracts;
} else {
const choices = compiledResult.contracts.map((c) => ({
name: c.name,
value: c,
}));
const prompt = createContractsPrompt(
choices,
"Choose which contracts to run detection on",
);
const selection: Record<string, ContractPayload> = await prompt.run();
selectedContracts = Object.keys(selection).map((key) => selection[key]);
}
}

let contractsWithFeatures: ContractFeatures[] = selectedContracts.map(
(contract) => {
const abi: Parameters<typeof detectFeatures>[0] = JSON.parse(
contract.metadata,
)["output"]["abi"];
const features = extractFeatures(detectFeatures(abi));

const enabledFeatures: Feature[] = features.enabledFeatures.map(
(feature) => ({
name: feature.name,
reference: `https://portal.thirdweb.com/interfaces/${feature.name.toLowerCase()}`,
}),
);
const suggestedFeatures: Feature[] = features.suggestedFeatures.map(
(feature) => ({
name: feature.name,
reference: `https://portal.thirdweb.com/interfaces/${feature.name.toLowerCase()}`,
}),
);

return {
name: contract.name,
enabledFeatures,
suggestedFeatures,
};
},
);

contractsWithFeatures.map((contractWithFeatures) => {
logger.info(`\n`);
if (contractWithFeatures.enabledFeatures.length === 0) {
ora(
`No extensions detected on ${chalk.blueBright(
contractWithFeatures.name,
)}`,
).stopAndPersist({ symbol: "🔎" });
} else {
ora(
`Detected extension on ${chalk.blueBright(contractWithFeatures.name)}`,
).stopAndPersist({ symbol: "🔎" });
contractWithFeatures.enabledFeatures.map((feature) => {
logger.info(`✔️ ${chalk.green(feature.name)}`);
});
}
logger.info(``);
ora(`Suggested extensions`).info();
contractWithFeatures.suggestedFeatures.map((feature) => {
logger.info(
`${chalk.dim(chalk.gray(`-`))} ${chalk.gray(
feature.name,
)} - ${chalk.dim(chalk.gray(feature.reference))}`,
);
});

let deployCmd = `npx thirdweb@latest deploy`;
if (existsSync(projectPath + "/package.json")) {
const packageManager = getPkgManager();
const useYarn = packageManager === "yarn";
const pkgJson = JSON.parse(
readFileSync(projectPath + "/package.json", "utf-8"),
);
if (pkgJson?.scripts?.deploy === deployCmd) {
deployCmd = `${packageManager}${useYarn ? "" : " run"} deploy`;
}
}

logger.info(``);
ora(
`Once you're done writing your contracts, you can run the following command to deploy them:`,
).info();
logger.info(``);
logger.info(` ${chalk.cyan(deployCmd)}`);
logger.info(``);
});
}

function extractFeatures(
input: ReturnType<typeof detectFeatures>,
enabledFeatures: FeatureWithEnabled[] = [],
suggestedFeatures: FeatureWithEnabled[] = [],
parent = "__ROOT__",
) {
if (!input) {
return {
enabledFeatures,
suggestedFeatures,
};
}
for (const featureKey in input) {
const feature = input[featureKey];
// if feature is enabled, then add it to enabledFeatures
if (feature.enabled) {
enabledFeatures.push(feature);
}
// otherwise if it is disabled, but it's parent is enabled or suggested, then add it to suggestedFeatures
else if (
enabledFeatures.findIndex((f) => f.name === parent) > -1 ||
ALWAYS_SUGGESTED.includes(feature.name)
) {
suggestedFeatures.push(feature);
}
// recurse
extractFeatures(
feature.features,
enabledFeatures,
suggestedFeatures,
feature.name,
);
}

return {
enabledFeatures,
suggestedFeatures,
};
}
88 changes: 88 additions & 0 deletions packages/cli/src/core/detection/detectFramework.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { info } from "../helpers/logger";
import { FrameworkType, PackageManagerType } from "../types/ProjectType";
import inquirer from "inquirer";
import NextDetector from "./frameworks/next";
import CRADetector from "./frameworks/cra";
import RemixDetector from "./frameworks/remix";
import GatsbyDetector from "./frameworks/gatsby";
import VueDetector from "./frameworks/vue";
import ReactNativeCLIDetector from "./frameworks/reactNativeCli";
import DjangoDetector from "./frameworks/django";
import ExpoDetector from "./frameworks/expo";
import FoundryDetector from "./frameworks/foundry";
import HardhatDetector from "./frameworks/hardhat";
import { FrameworkDetector } from "./detector";
import TruffleDetector from "./frameworks/truffle";
import BrownieDetector from "./frameworks/brownie";
import FastAPIDetector from "./frameworks/fastAPI";
import FlaskDetector from "./frameworks/flask";
import PopulusDetector from "./frameworks/populus";
import FastifyDetector from "./frameworks/fastify";
import EchoDetector from "./frameworks/echo";
import FiberDetector from "./frameworks/fiber";
import GinDetector from "./frameworks/gin";
import RevelDetector from "./frameworks/revel";
import ZenjectDetector from "./frameworks/zenject";

export default async function detect(
path: string,
options: any,
detectedPackageManager: PackageManagerType,
): Promise<FrameworkType> {
// We could optimize further if we want, by only running the detectors that match the package manager.
const frameworkDetectors: FrameworkDetector[] = [
new BrownieDetector(),
new CRADetector(),
new DjangoDetector(),
new EchoDetector(),
new ExpoDetector(),
new FastAPIDetector(),
new FastifyDetector(),
new FiberDetector(),
new FlaskDetector(),
new FoundryDetector(),
new GatsbyDetector(),
new GinDetector(),
new HardhatDetector(),
new NextDetector(),
new PopulusDetector(),
new ReactNativeCLIDetector(),
new RemixDetector(),
new RevelDetector(),
new TruffleDetector(),
new VueDetector(),
new ZenjectDetector(),
];

const possibleFrameworks = frameworkDetectors
.filter((detector) => detector.matches(path, detectedPackageManager))
.map((detector) => detector.frameworkType);

if (!possibleFrameworks.length) {
return "none";
}

if (possibleFrameworks.length === 1) {
return possibleFrameworks[0];
}

info(
`Detected multiple possible frameworks: ${possibleFrameworks
.map((s) => `"${s}"`)
.join(", ")}`,
);

const question =
"We detected multiple possible frameworks which one do you want to use?";

if (options.ci) {
return possibleFrameworks[0];
} else {
const answer = await inquirer.prompt({
type: "list",
choices: possibleFrameworks,
name: question,
});
return answer[question];
}
}
Loading

0 comments on commit 84fc3b6

Please sign in to comment.