Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension to solidity framework #68

Merged
merged 14 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contributors/DEVELOPER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ yarn cli

This command will run the compiled script inside `bin/`.

You can send any option or flag to the CLI command. For example, a handy command is `yarn cli -s` to tell the CLI to skip installing dependencies or `yarn cli [project_path]` example `yarn cli ../test-cli` this will skip the "Project Name" prompt and use the provided path where project instance will be created in.
You can send any option or flag to the CLI command. For example, a handy command is `yarn cli --skip` to tell the CLI to skip installing dependencies or `yarn cli [project_path]` example `yarn cli ../test-cli` this will skip the "Project Name" prompt and use the provided path where project instance will be created in.

## Changes to the Resulting Project

The source files for the instance projects can be found under `templates/`. You'll see there are two folders there: `base/` and `extensions/`. The `base/` folder has the source files that will be present in all instances, whereas `extensions/` hold the source files that will be added or not to the instances based on the user choices within the CLI tool.
The source files for the instance projects can be found under `templates/`. You'll see there are two folders there: `base/` and `solidity-frameworks/`. The `base/` folder has the source files that will be present in all instances, whereas `solidity-frameworks/` hold the source files that will be added or not to the instances based on the user choices within the CLI tool.

It's highly recommended that you go through [core-extensions.md](core-extensions.md) to understand the template API to create extensions. We use a custom template API to allow extensions to modify any file inside the `templates/` folder. While flexible and powerful, it requires developers to understand how it works. It's JS based, so there's no new technology needed to understand and use it.

Expand Down
20 changes: 1 addition & 19 deletions contributors/core-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ This change requires a new file, `src/config.ts`, where a few things about the e

There's one main `src/config.ts` file to configure the questions shown to the user.

For each extension there is an optional `templates/extensions/{extensionName}/config.json` file providing information about the specific extension.

| ⚠️ Note how the extension config file is a JSON file

## Config files API
Expand All @@ -22,21 +20,6 @@ Have a look at `src/types.ts#Config` or the file itself.

Just a quick note to mention that adding `null` as an item in the `extensions` property will show the "None" option to the user. That option doesn't add any extension for the given question.

### `{extension}/config.json`

Since these files can't be .ts, the API is not typed. However, there are certain properties that are used in the code.

Those properties are:

- `name`: the string to be used when showing the package name to the user via the cli, as well as for error reporting.

Note that all values are optional, as well as the file itself.

| ⚠️ TODO list when new properties are added to config.json:
| - Update this document
| - Update the ExtensionDescriptor type at /src/types.ts
| - Update the src/utils/extensions-dictionary.ts file so the new field from the config is actually added into the extension descriptor

# Template files

A Template file is a file to which extensions can add content. Removing content is out of scope for this experiment.
Expand Down Expand Up @@ -211,8 +194,7 @@ Templated files are both [Template files](#template-files), and [Args files](#ar
The special files and folders are:

- [`package.json` file](#merging-packagejson-files)
- [`config.json` file](#extensionconfigjson)
- `extensions/` folder
- `solidity-frameworks/` folder

# Things worth mentioning

Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function cli(args: Args) {
}

const options = await promptForMissingOptions(rawOptions);
if (options.extensions.includes(SOLIDITY_FRAMEWORKS.FOUNDRY)) {
if (options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) {
await validateFoundryUp();
}

Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function createProject(options: Options) {
},
},
{
title: `📡 Initializing Git repository${options.extensions.includes(SOLIDITY_FRAMEWORKS.FOUNDRY) ? " and submodules" : ""}`,
title: `📡 Initializing Git repository${options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY ? " and submodules" : ""}`,
task: () => createFirstGitCommit(targetDirectory, options),
},
],
Expand Down
89 changes: 47 additions & 42 deletions src/tasks/copy-template-files.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { execa } from "execa";
import { ExternalExtension, Options, TemplateDescriptor } from "../types";
import { baseDir } from "../utils/consts";
import { extensionDict } from "../utils/extensions-dictionary";
import { ExternalExtension, Options, SolidityFramework, TemplateDescriptor } from "../types";
import { BASE_DIR } from "../utils/consts";
import { findFilesRecursiveSync } from "../utils/find-files-recursively";
import { mergePackageJson } from "../utils/merge-package-json";
import fs from "fs";
import url from "url";
import { pathToFileURL } from "url";
import ncp from "ncp";
import path from "path";
import { promisify } from "util";
Expand All @@ -21,10 +20,16 @@ const isPackageJsonRegex = /package\.json/;
const isYarnLockRegex = /yarn\.lock/;
const isConfigRegex = /([^/\\]*?)\\config\.json/;
const isArgsRegex = /([^/\\]*?)\.args\./;
const isExtensionFolderRegex = /extensions$/;
const isSolidityFrameworkFolderRegex = /solidity-frameworks$/;
const isPackagesFolderRegex = /packages$/;
const isDeployedContractsRegex = /packages\/nextjs\/contracts\/deployedContracts\.ts/;

const getSolidityFrameworkPath = (solidityFramework: SolidityFramework | null, templatesDirectory: string) => {
if (!solidityFramework) return;

return path.resolve(templatesDirectory, "solidity-frameworks", solidityFramework);
};

const copyBaseFiles = async (basePath: string, targetDir: string, { dev: isDev }: Options) => {
await copyOrLink(basePath, targetDir, {
clobber: false,
Expand Down Expand Up @@ -69,12 +74,13 @@ const copyExtensionsFiles = async ({ dev: isDev }: Options, extensionPath: strin
filter: path => {
const isConfig = isConfigRegex.test(path);
const isArgs = isArgsRegex.test(path);
const isExtensionFolder = isExtensionFolderRegex.test(path) && fs.lstatSync(path).isDirectory();
const isSolidityFrameworkFolder = isSolidityFrameworkFolderRegex.test(path) && fs.lstatSync(path).isDirectory();
const isPackagesFolder = isPackagesFolderRegex.test(path) && fs.lstatSync(path).isDirectory();
const isTemplate = isTemplateRegex.test(path);
// PR NOTE: this wasn't needed before because ncp had the clobber: false
const isPackageJson = isPackageJsonRegex.test(path);
const shouldSkip = isConfig || isArgs || isTemplate || isPackageJson || isExtensionFolder || isPackagesFolder;
const shouldSkip =
isConfig || isArgs || isTemplate || isPackageJson || isSolidityFrameworkFolder || isPackagesFolder;
return !shouldSkip;
},
});
Expand Down Expand Up @@ -111,31 +117,31 @@ const copyExtensionsFiles = async ({ dev: isDev }: Options, extensionPath: strin
};

const processTemplatedFiles = async (
{ extensions, externalExtension, dev: isDev }: Options,
{ solidityFramework, externalExtension, dev: isDev }: Options,
basePath: string,
templateDir: string,
targetDir: string,
) => {
const solidityFrameworkPath = getSolidityFrameworkPath(solidityFramework, templateDir);
rin-st marked this conversation as resolved.
Show resolved Hide resolved
const baseTemplatedFileDescriptors: TemplateDescriptor[] = findFilesRecursiveSync(basePath, path =>
isTemplateRegex.test(path),
).map(baseTemplatePath => ({
path: baseTemplatePath,
fileUrl: url.pathToFileURL(baseTemplatePath).href,
fileUrl: pathToFileURL(baseTemplatePath).href,
relativePath: baseTemplatePath.split(basePath)[1],
source: "base",
}));

const extensionsTemplatedFileDescriptors: TemplateDescriptor[] = extensions
.map(ext =>
findFilesRecursiveSync(extensionDict[ext].path, filePath => isTemplateRegex.test(filePath)).map(
extensionTemplatePath => ({
const solidityFrameworkTemplatedFileDescriptors: TemplateDescriptor[] = solidityFrameworkPath
? findFilesRecursiveSync(solidityFrameworkPath, filePath => isTemplateRegex.test(filePath))
.map(extensionTemplatePath => ({
rin-st marked this conversation as resolved.
Show resolved Hide resolved
path: extensionTemplatePath,
fileUrl: url.pathToFileURL(extensionTemplatePath).href,
relativePath: extensionTemplatePath.split(extensionDict[ext].path)[1],
source: `extension ${extensionDict[ext].name}`,
}),
),
)
.flat();
fileUrl: pathToFileURL(extensionTemplatePath).href,
relativePath: extensionTemplatePath.split(solidityFrameworkPath)[1],
source: `extension ${solidityFramework}`,
}))
.flat()
: [];

const externalExtensionFolder = isDev
? path.join(basePath, "../../externalExtensions", externalExtension as string, "extension")
Expand All @@ -144,7 +150,7 @@ const processTemplatedFiles = async (
? findFilesRecursiveSync(externalExtensionFolder, filePath => isTemplateRegex.test(filePath)).map(
extensionTemplatePath => ({
path: extensionTemplatePath,
fileUrl: url.pathToFileURL(extensionTemplatePath).href,
fileUrl: pathToFileURL(extensionTemplatePath).href,
relativePath: extensionTemplatePath.split(externalExtensionFolder)[1],
source: `external extension ${isDev ? (externalExtension as string) : getArgumentFromExternalExtensionOption(externalExtension)}`,
}),
Expand All @@ -154,23 +160,22 @@ const processTemplatedFiles = async (
await Promise.all(
[
...baseTemplatedFileDescriptors,
...extensionsTemplatedFileDescriptors,
...solidityFrameworkTemplatedFileDescriptors,
...externalExtensionTemplatedFileDescriptors,
].map(async templateFileDescriptor => {
const templateTargetName = templateFileDescriptor.path.match(isTemplateRegex)?.[1] as string;

const argsPath = templateFileDescriptor.relativePath.replace(isTemplateRegex, `${templateTargetName}.args.`);

const argsFileUrls = extensions
.map(extension => {
const argsFilePath = path.join(extensionDict[extension].path, argsPath);
const fileExists = fs.existsSync(argsFilePath);
if (!fileExists) {
return [];
}
return url.pathToFileURL(argsFilePath).href;
})
.flat();
const argsFileUrls = [];

if (solidityFrameworkPath) {
const argsFilePath = path.join(solidityFrameworkPath, argsPath);
const fileExists = fs.existsSync(argsFilePath);
if (fileExists) {
argsFileUrls.push(pathToFileURL(argsFilePath).href);
}
}

if (externalExtension) {
const argsFilePath = isDev
Expand All @@ -179,7 +184,7 @@ const processTemplatedFiles = async (

const fileExists = fs.existsSync(argsFilePath);
if (fileExists) {
argsFileUrls?.push(url.pathToFileURL(argsFilePath).href);
argsFileUrls?.push(pathToFileURL(argsFilePath).href);
}
}

Expand Down Expand Up @@ -276,19 +281,19 @@ const setUpExternalExtensionFiles = async (options: Options, tmpDir: string) =>

export async function copyTemplateFiles(options: Options, templateDir: string, targetDir: string) {
copyOrLink = options.dev ? link : copy;
const basePath = path.join(templateDir, baseDir);
const basePath = path.join(templateDir, BASE_DIR);
const tmpDir = path.join(targetDir, EXTERNAL_EXTENSION_TMP_FOLDER);

// 1. Copy base template to target directory
await copyBaseFiles(basePath, targetDir, options);

// 2. Copy extensions folders
await Promise.all(
options.extensions.map(async extension => {
const extensionPath = extensionDict[extension].path;
await copyExtensionsFiles(options, extensionPath, targetDir);
}),
);
let solidityFrameworkPath;
rin-st marked this conversation as resolved.
Show resolved Hide resolved

// 2. Copy solidity framework folder
if (options.solidityFramework) {
solidityFrameworkPath = getSolidityFrameworkPath(options.solidityFramework, templateDir) as string;
await copyExtensionsFiles(options, solidityFrameworkPath, targetDir);
}

// 3. Set up external extension if needed
if (options.externalExtension) {
Expand All @@ -308,7 +313,7 @@ export async function copyTemplateFiles(options: Options, templateDir: string, t
}

// 4. Process templated files and generate output
await processTemplatedFiles(options, basePath, targetDir);
await processTemplatedFiles(options, basePath, templateDir, targetDir);
rin-st marked this conversation as resolved.
Show resolved Hide resolved

// 5. Delete tmp directory
if (options.externalExtension && !options.dev) {
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/create-first-git-commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function createFirstGitCommit(targetDir: string, options: Options)
await execa("git", ["add", "-A"], { cwd: targetDir });
await execa("git", ["commit", "-m", "Initial commit with 🏗️ Scaffold-ETH 2", "--no-verify"], { cwd: targetDir });

if (options.extensions.includes(SOLIDITY_FRAMEWORKS.FOUNDRY)) {
if (options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) {
const foundryWorkSpacePath = path.resolve(targetDir, "packages", SOLIDITY_FRAMEWORKS.FOUNDRY);
// forge install foundry libraries
await execa("forge", ["install", ...foundryLibraries], { cwd: foundryWorkSpacePath });
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/prettier-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function prettierFormat(targetDir: string, options: Options) {

await runPrettier([nextJsPath], nextPrettierConfig, ["--plugin=@trivago/prettier-plugin-sort-imports"]);

if (options.extensions.includes(SOLIDITY_FRAMEWORKS.HARDHAT)) {
if (options.solidityFramework === SOLIDITY_FRAMEWORKS.HARDHAT) {
const hardhatPackagePath = path.join(targetDir, "packages", SOLIDITY_FRAMEWORKS.HARDHAT);
const hardhatPrettierConfig = path.join(hardhatPackagePath, ".prettierrc.json");
const hardhatPaths = [
Expand All @@ -40,7 +40,7 @@ export async function prettierFormat(targetDir: string, options: Options) {
await runPrettier(hardhatPaths, hardhatPrettierConfig, ["--plugin=prettier-plugin-solidity"]);
}

if (options.extensions.includes(SOLIDITY_FRAMEWORKS.FOUNDRY)) {
if (options.solidityFramework === SOLIDITY_FRAMEWORKS.FOUNDRY) {
const foundryPackagePath = path.resolve(targetDir, "packages", SOLIDITY_FRAMEWORKS.FOUNDRY);
const foundryResult = await execa("forge", ["fmt"], { cwd: foundryPackagePath });
if (foundryResult.failed) {
Expand Down
40 changes: 12 additions & 28 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import type { Question } from "inquirer";

export type Args = string[];

export type SolidityFramework = "hardhat" | "foundry";

type BaseOptions = {
project: string | null;
install: boolean | null;
dev: boolean;
externalExtension: ExternalExtension | ExternalExtensionNameDev | null;
solidityFramework: SolidityFramework | "none" | null;
};

export type ExternalExtension = {
Expand All @@ -17,42 +20,33 @@ export type ExternalExtension = {
export type ExternalExtensionNameDev = string;

export type RawOptions = BaseOptions & {
solidityFramework: SolidityFramework | "none" | null;
help: boolean;
};

type MergedOptions = BaseOptions & {
extensions: Extension[];
};

type NonNullableMergedOptions = {
[Prop in keyof Omit<MergedOptions, "externalExtension">]: NonNullable<MergedOptions[Prop]>;
type MergedOptions = {
[Prop in keyof Omit<BaseOptions, "externalExtension" | "solidityFramework">]: NonNullable<BaseOptions[Prop]>;
} & {
externalExtension: RawOptions["externalExtension"];
solidityFramework: SolidityFramework | null;
};

export type Options = NonNullableMergedOptions;

export type SolidityFramework = "hardhat" | "foundry";

export type Extension = SolidityFramework;
export type Options = MergedOptions;

type NullExtension = null;
export type ExtensionOrNull = Extension | NullExtension;

export type SolidityFrameworkOrNull = SolidityFramework | NullExtension;
// corresponds to inquirer question types:
// - multi-select -> checkbox
// - single-select -> list
type QuestionType = "multi-select" | "single-select";
interface ExtensionQuestion<T extends ExtensionOrNull[] = ExtensionOrNull[]> {
interface SolidityFrameworkQuestion<T extends SolidityFrameworkOrNull[] = SolidityFrameworkOrNull[]> {
type: QuestionType;
extensions: T;
name: string;
message: Question["message"];
default?: T[number];
}

export const isExtension = (item: ExtensionOrNull): item is Extension => item !== null;

/**
* This function makes sure that the `T` generic type is narrowed down to
* whatever `extensions` are passed in the question prop. That way we can type
Expand All @@ -62,23 +56,13 @@ export const isExtension = (item: ExtensionOrNull): item is Extension => item !=
* Questions can be created without this function, just using a normal object,
* but `default` type will be any valid Extension.
*/
export const typedQuestion = <T extends ExtensionOrNull[]>(question: ExtensionQuestion<T>) => question;
export const typedQuestion = <T extends SolidityFrameworkOrNull[]>(question: SolidityFrameworkQuestion<T>) => question;
export type Config = {
questions: ExtensionQuestion[];
questions: SolidityFrameworkQuestion[];
};

export const isDefined = <T>(item: T | undefined | null): item is T => item !== undefined && item !== null;

export type ExtensionDescriptor = {
name: string;
value: Extension;
path: string;
};

export type ExtensionDict = {
[extension in Extension]: ExtensionDescriptor;
};

export type TemplateDescriptor = {
path: string;
fileUrl: string;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/consts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const baseDir = "base";
export const BASE_DIR = "base";

rin-st marked this conversation as resolved.
Show resolved Hide resolved
export const SOLIDITY_FRAMEWORKS = {
HARDHAT: "hardhat",
Expand Down
Loading
Loading