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

cleanup #45

Merged
merged 12 commits into from
Jun 14, 2024
Merged
62 changes: 2 additions & 60 deletions contributors/RFC-extensions.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we change this filename to core-extensions or something like that? So then we can create external-extensions for extensions that will live in other repos. Or are we planning to put all of them together here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

voting for first option

Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,6 @@ This change should make it easier to grow the options we provide our users witho

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.

## Nested extensions

We can add extensions of extensions, just by adding an `/extensions` folder inside an existing extension. For example, adding [next-auth][1] as an extension for next would look something like the following:

```text
create-dapp-example/
├─ src/
│ ├─ ...
├─ templates/
│ ├─ base/
│ ├─ extensions/
│ │ ├─nextjs
│ │ ├─ ...
│ │ ├─ extensions/
│ │ │ ├─ next-auth/
│ │ │ ├─ ...
```

## Extending extensions

> sorry for the confusing naming

Extensions can also inherit (or extend) another extension. The goal of this feature is that two extensions can share files or nested extensions without duplication. An example of this could be hardhat and foundry, which are two different extensions, but both of them could have a shared UI to debug smart contracts.

The file structure could be like this:

```text
create-dapp-example/
├─ src/
│ ├─ ...
├─ templates/
│ ├─ base/
│ ├─ extensions/
│ │ ├─ foundry/
│ │ │ ├─ config.json <- important to declare the `extends` field here
│ │ │ ├─ ...
│ │ ├─ hardhat/
│ │ │ ├─ config.json <- important to declare the `extends` field here
│ │ │ ├─ ...
│ │ ├─ common/
│ │ │ ├─ extensions/
│ │ │ │ ├─ possible-shared-nested-extension/
│ │ │ │ ├─ ...
│ │ │ ├─ shared-file.md
```

For `foundry` and `hardhat` extensions to inherit from `common`, they need to add the `extends` field to the config.json file.

```json
{
"extends": "common"
}
```

# Config files

There's one main `src/config.ts` file to configure the questions shown to the user.
Expand All @@ -86,14 +30,12 @@ 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.

- `extends`: the name of a different extension used as "parent extension". Read more at the [Extending extensions](#extending-extensions) section

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-tree.ts file so the new field from the config is actually added into the extension descriptor
| - Update the src/utils/extensions-dictionary.ts file so the new field from the config is actually added into the extension descriptor

# Template files

Expand Down Expand Up @@ -270,7 +212,7 @@ The special files and folders are:

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

# Things worth mentioning

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"arg": "5.0.2",
"chalk": "5.2.0",
"execa": "7.1.1",
"handlebars": "^4.7.7",
"inquirer": "9.2.0",
"listr": "0.14.3",
"merge-packages": "^0.1.6",
Expand Down
74 changes: 14 additions & 60 deletions src/tasks/copy-template-files.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execa } from "execa";
import { Extension, isDefined, Options, TemplateDescriptor } from "../types";
import { Options, TemplateDescriptor } from "../types";
import { baseDir } from "../utils/consts";
import { extensionDict } from "../utils/extensions-tree";
import { extensionDict } from "../utils/extensions-dictionary";
import { findFilesRecursiveSync } from "../utils/find-files-recursively";
import { mergePackageJson } from "../utils/merge-package-json";
import fs from "fs";
Expand All @@ -16,64 +16,21 @@ const EXTERNAL_EXTENSION_TMP_FOLDER = "tmp-external-extension";
const copy = promisify(ncp);
let copyOrLink = copy;

const expandExtensions = (options: Options): Extension[] => {
const expandedExtensions = options.extensions
.map(extension => extensionDict[extension])
.map(extDescriptor => [extDescriptor.extends, extDescriptor.value].filter(isDefined))
.flat()
// this reduce just removes duplications
.reduce((exts, ext) => (exts.includes(ext) ? exts : [...exts, ext]), [] as Extension[]);

return expandedExtensions;
};
technophile-04 marked this conversation as resolved.
Show resolved Hide resolved

const isTemplateRegex = /([^/\\]*?)\.template\./;
const isPackageJsonRegex = /package\.json/;
const isYarnLockRegex = /yarn\.lock/;
const isNextGeneratedRegex = /packages\/nextjs\/generated/;
const isConfigRegex = /([^/\\]*?)\\config\.json/;
const isArgsRegex = /([^/\\]*?)\.args\./;
const isExtensionFolderRegex = /extensions$/;
const isPackagesFolderRegex = /packages$/;

const copyBaseFiles = async ({ dev: isDev }: Options, basePath: string, targetDir: string) => {
const copyBaseFiles = async (basePath: string, targetDir: string) => {
await copyOrLink(basePath, targetDir, {
clobber: false,
filter: fileName => {
// NOTE: filter IN
const isTemplate = isTemplateRegex.test(fileName);
const isPackageJson = isPackageJsonRegex.test(fileName);
const isYarnLock = isYarnLockRegex.test(fileName);
const isNextGenerated = isNextGeneratedRegex.test(fileName);

const skipAlways = isTemplate || isPackageJson;
const skipDevOnly = isYarnLock || isNextGenerated;
const shouldSkip = skipAlways || (isDev && skipDevOnly);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to understand this.

  • why were we not copying package.json before?
  • dev is not important anymore here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh my bad actually I planned to write a comment on why did I remove all this and forgot about it but here is description:

So basically this function copies "base" dir into user mentioned path.

why were we not copying package.json before?

We were ignore package.json here because just below this copyOrLink func we were using mergePackageJson to merge package.json's :

const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path));
basePackageJsonPaths.forEach(packageJsonPath => {
const partialPath = packageJsonPath.split(basePath)[1];
mergePackageJson(path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev);
});

I have removed this part in this PR since:

  1. "base" dir is the first dir copied to user mentioned path.
  2. which means the user mentioned path is empty.
  3. So we don't need merging of package.json at all. we could just copy the whole "base" dir to user mentioned path directly except template files. (hence we are not skipping package.json in copyOrLink func in this PR)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dev is not important anymore here?

I initially thought it was not needed since we have removed "nextjs/generated" but just updated the logic and added it back so that we don't symlink yarn.lock and packages/nextjs/contracts/deployedContracts.ts since both are reproducible and we don't want to commit them while creating extension.

return !shouldSkip;
return !isTemplate;
},
});

const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path));

basePackageJsonPaths.forEach(packageJsonPath => {
const partialPath = packageJsonPath.split(basePath)[1];
mergePackageJson(path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev);
});

if (isDev) {
const baseYarnLockPaths = findFilesRecursiveSync(basePath, path => isYarnLockRegex.test(path));
baseYarnLockPaths.forEach(yarnLockPath => {
const partialPath = yarnLockPath.split(basePath)[1];
void copy(path.join(basePath, partialPath), path.join(targetDir, partialPath));
});

const nextGeneratedPaths = findFilesRecursiveSync(basePath, path => isNextGeneratedRegex.test(path));
nextGeneratedPaths.forEach(nextGeneratedPath => {
const partialPath = nextGeneratedPath.split(basePath)[1];
void copy(path.join(basePath, partialPath), path.join(targetDir, partialPath));
});
}
};

const copyExtensionsFiles = async ({ dev: isDev }: Options, extensionPath: string, targetDir: string) => {
Expand Down Expand Up @@ -211,22 +168,22 @@ const processTemplatedFiles = async (
);
}

// ToDo. Bug, if arg not present in arg[0], but present in arg[1], it will not be added.
const allKeys = [...new Set(args.flatMap(Object.keys))];

const freshArgs: { [key: string]: string[] } = Object.fromEntries(
Object.keys(args[0] ?? {}).map(key => [
allKeys.map(key => [
key, // INFO: key for the freshArgs object
[], // INFO: initial value for the freshArgs object
]),
);

const combinedArgs = args.reduce<typeof freshArgs>((accumulated, arg) => {
Object.entries(arg).map(([key, value]) => {
accumulated[key]?.push(value);
});
return accumulated;
}, freshArgs);

// TODO test: if first arg file found only uses 1 name, I think the rest are not used?

const output = fileTemplate(combinedArgs);

const targetPath = path.join(
Expand Down Expand Up @@ -290,34 +247,31 @@ export async function copyTemplateFiles(options: Options, templateDir: string, t
const tmpDir = path.join(targetDir, EXTERNAL_EXTENSION_TMP_FOLDER);

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

// 2. Add "parent" extensions (set via config.json#extend field)
options.extensions = expandExtensions(options);
Comment on lines -295 to -296
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no more need this since we are not allowing extending extension anymore

await copyBaseFiles(basePath, targetDir);

// 3. Copy extensions folders
// 2. Copy extensions folders
await Promise.all(
options.extensions.map(async extension => {
const extensionPath = extensionDict[extension].path;
await copyExtensionsFiles(options, extensionPath, targetDir);
}),
);

// 4. Set up external extension if needed
// 3. Set up external extension if needed
if (options.externalExtension) {
await setUpExternalExtensionFiles(options, tmpDir);
await copyExtensionsFiles(options, path.join(tmpDir, "extension"), targetDir);
}

// 5. Process templated files and generate output
// 4. Process templated files and generate output
await processTemplatedFiles(options, basePath, targetDir);

// 6. Delete tmp directory
// 5. Delete tmp directory
if (options.externalExtension) {
await fs.promises.rm(tmpDir, { recursive: true });
}

// 7. Initialize git repo to avoid husky error
// 6. Initialize git repo to avoid husky error
await execa("git", ["init"], { cwd: targetDir });
await execa("git", ["checkout", "-b", "main"], { cwd: targetDir });
}
11 changes: 0 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,12 @@ export type ExtensionDescriptor = {
name: string;
value: Extension;
path: string;
extensions?: Extension[];
extends?: Extension;
};

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

export const extensionWithSubextensions = (
extension: ExtensionDescriptor | undefined,
): extension is ExtensionBranch => {
return Object.prototype.hasOwnProperty.call(extension, "extensions");
};

export type TemplateDescriptor = {
path: string;
fileUrl: string;
Expand Down
57 changes: 57 additions & 0 deletions src/utils/extensions-dictionary.ts
technophile-04 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import { Extension, ExtensionDescriptor, ExtensionDict } from "../types";

// global object to store mapping of extension name and its descriptor
export const extensionDict: ExtensionDict = {} as ExtensionDict;

const currentFileUrl = import.meta.url;

const templatesDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates");

/**
* This function has side effects. It initializes the extensionDict.
*/
const initializeExtensionsDict = (basePath: string) => {
const extensionsPath = path.resolve(basePath, "extensions");
let extensions: Extension[];
try {
extensions = fs.readdirSync(extensionsPath) as Extension[];
} catch (error) {
console.error(`Couldn't read the extensions directory: ${extensionsPath}`);
return;
}

extensions.forEach(ext => {
const extPath = path.resolve(extensionsPath, ext);
const configPath = path.resolve(extPath, "config.json");

let config: Record<string, string> = {};
try {
config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, string>;
} catch (error) {
if (fs.existsSync(configPath)) {
throw new Error(
`Couldn't parse existing config.json file.
Extension: ${ext};
Config file path: ${configPath}`,
);
}
}
const name = config.name ?? ext;
const value = ext;

const extDescriptor: ExtensionDescriptor = {
name,
value,
path: extPath,
};

extensionDict[ext] = extDescriptor;
});
};

// This function call will run only once due to Node.js module caching in first import of file
// it won't run again even if imported in multiple files.
initializeExtensionsDict(templatesDirectory);
68 changes: 0 additions & 68 deletions src/utils/extensions-tree.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't removed this file it's just rename checkout #45 (comment)

This file was deleted.

Loading
Loading