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 all 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
6 changes: 3 additions & 3 deletions contributors/DEVELOPER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ 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.
It's highly recommended that you go through [TEMPLATING.md](TEMPLATING.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.

While you might be tempted to change files straight in the source, we've created a better way to do it with the dev mode. We feel this is worth a separate section in this document.

Expand Down
44 changes: 1 addition & 43 deletions contributors/core-extensions.md → contributors/TEMPLATING.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,3 @@
# Main concepts

I propose we treat anything other than "base" as an extension. That way we don't need to make distinctions between solidity frameworks or any other kind of extension.

This change should make it easier to grow the options we provide our users without having to classify extensions by category.

This change requires a new file, `src/config.ts`, where a few things about the extensions are defined, e.g. the sequence of questions about extensions.

# Config files

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

### `src/config.ts`

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 @@ -191,8 +152,6 @@ I've thought about how the strings should be joined, but an option is to use [ta

# Extension folder anatomy

When creating a new extension, simply create a new folder under `templates/extensions` with the name you want the extension to have.

Inside the folder you will have a mix of normal, templated, and special files and folders.

## Normal files and folders
Expand All @@ -211,8 +170,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
27 changes: 0 additions & 27 deletions src/config.ts

This file was deleted.

14 changes: 14 additions & 0 deletions src/curated-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ExternalExtension } from "./types";

const CURATED_EXTENSIONS: { [key: string]: ExternalExtension } = {
subgraph: {
repository: "https://github.com/scaffold-eth/create-eth-extensions",
branch: "subgraph",
},
"eip-712": {
repository: "https://github.com/scaffold-eth/create-eth-extensions",
branch: "eip-712",
},
};

export { CURATED_EXTENSIONS };
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
97 changes: 50 additions & 47 deletions src/tasks/copy-template-files.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
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 { 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";
import link from "../utils/link";
import { getArgumentFromExternalExtensionOption } from "../utils/external-extensions";

const EXTERNAL_EXTENSION_TMP_FOLDER = "tmp-external-extension";
const BASE_DIR = "base";
const EXTERNAL_EXTENSION_TMP_DIR = "tmp-external-extension";
const SOLIDITY_FRAMEWORKS_DIR = "solidity-frameworks";

const copy = promisify(ncp);
let copyOrLink = copy;

Expand All @@ -21,10 +22,13 @@ 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, templatesDirectory: string) =>
path.resolve(templatesDirectory, SOLIDITY_FRAMEWORKS_DIR, solidityFramework);

const copyBaseFiles = async (basePath: string, targetDir: string, { dev: isDev }: Options) => {
await copyOrLink(basePath, targetDir, {
clobber: false,
Expand Down Expand Up @@ -69,12 +73,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,40 +116,39 @@ const copyExtensionsFiles = async ({ dev: isDev }: Options, extensionPath: strin
};

const processTemplatedFiles = async (
{ extensions, externalExtension, dev: isDev }: Options,
{ solidityFramework, externalExtension, dev: isDev }: Options,
basePath: string,
solidityFrameworkPath: string | null,
targetDir: string,
) => {
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 => ({
path: extensionTemplatePath,
fileUrl: url.pathToFileURL(extensionTemplatePath).href,
relativePath: extensionTemplatePath.split(extensionDict[ext].path)[1],
source: `extension ${extensionDict[ext].name}`,
}),
),
)
.flat();
const solidityFrameworkTemplatedFileDescriptors: TemplateDescriptor[] = solidityFrameworkPath
? findFilesRecursiveSync(solidityFrameworkPath, filePath => isTemplateRegex.test(filePath))
.map(solidityFrameworkTemplatePath => ({
path: solidityFrameworkTemplatePath,
fileUrl: pathToFileURL(solidityFrameworkTemplatePath).href,
relativePath: solidityFrameworkTemplatePath.split(solidityFrameworkPath)[1],
source: `extension ${solidityFramework}`,
}))
.flat()
: [];

const externalExtensionFolder = isDev
? path.join(basePath, "../../externalExtensions", externalExtension as string, "extension")
: path.join(targetDir, EXTERNAL_EXTENSION_TMP_FOLDER, "extension");
: path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR, "extension");
const externalExtensionTemplatedFileDescriptors: TemplateDescriptor[] = externalExtension
? 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,32 +158,31 @@ 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
? path.join(basePath, "../../externalExtensions", externalExtension as string, "extension", argsPath)
: path.join(targetDir, EXTERNAL_EXTENSION_TMP_FOLDER, "extension", argsPath);
: path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR, "extension", argsPath);

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

Expand Down Expand Up @@ -276,19 +279,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 tmpDir = path.join(targetDir, EXTERNAL_EXTENSION_TMP_FOLDER);
const basePath = path.join(templateDir, BASE_DIR);
const tmpDir = path.join(targetDir, EXTERNAL_EXTENSION_TMP_DIR);

// 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);
}),
);
// 2. Copy solidity framework folder
const solidityFrameworkPath =
options.solidityFramework && getSolidityFrameworkPath(options.solidityFramework, templateDir);

if (solidityFrameworkPath) {
await copyExtensionsFiles(options, solidityFrameworkPath, targetDir);
}

// 3. Set up external extension if needed
if (options.externalExtension) {
Expand All @@ -308,7 +311,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, solidityFrameworkPath, targetDir);

// 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 @@ -25,7 +25,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 @@ -39,7 +39,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
Loading
Loading