Skip to content

Commit

Permalink
Object merge with special characters (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
rin-st authored Feb 14, 2025
1 parent 47ccc6f commit d14462d
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-hounds-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-eth": major
---

adjust level of customization and improved ways to pass args to tmeplates
19 changes: 10 additions & 9 deletions contributors/TEMPLATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,20 @@ The special files and folders are:

Most of the time you will use string arguments for templating, but sometimes you will need to add arrays, objects, bigints, etc. You can handle them however you want, but we're recommending to use the table below as a helper.

Note: The `stringify` function used in the examples below should be imported from the `templates/utils.js` file:
The `stringify` and `deepMerge` functions used in the examples below should be imported from the `templates/utils.js` file:

```javascript
import { stringify } from "../path/to/templates/utils.js";
import { stringify, deepMerge } from "../path/to/templates/utils.js";
```

| Pattern | Template | Args | Result |
| ----------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Replace an object | `const replacedObj = ${stringify(replacedObj[0])}` | `const replacedObj = { key1: "Replaced", key2: "Object" }` | `const replacedObj = { key1: "Replaced", key2: "Object" }` |
| Replace an array | `const replacedArr = ${stringify(replacedArr[0])}` | `const replacedArr = ["Replaced", "Array"]` | `const replacedArr = ["Replaced", "Array"]` |
| Object, add new entries | `const mergedObj = ${stringify({ key1: "value1", key2: "value2", ...objToMerge[0] })};` | `const objToMerge = { key3: "Merged", key4: "Object" }` | `const mergedObj = { key1: "value1", key2: "value2", key3: "Merged", key4: "Object" };` |
| Array, add new items | `const arrWithAdditionalItems = ${stringify(['a', 'b', ...arrayToSpread[0]])}` | `const arrayToSpread = ["Spread", "This"]` | `const arrWithAdditionalItems = ["a", "b", "Spread", "This"]` |
| BigInt | `const bigInt = ${stringify(someBigInt[0])};` | `const someBigInt = 123n` | `const bigInt = 123n;` |
| Pattern | Template | Args | Result |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Replace an object | `const replacedObj = ${stringify(replacedObj[0])}` | `const replacedObj = { key1: "Replaced", key2: "Object" }` | `const replacedObj = { key1: "Replaced", key2: "Object" }` |
| Replace an array | `const replacedArr = ${stringify(replacedArr[0])}` | `const replacedArr = ["Replaced", "Array"]` | `const replacedArr = ["Replaced", "Array"]` |
| Object, add new entries | `const mergedObj = ${stringify(deepMerge({ key1: "value1", key2: "value2"}, objToMerge[0] })};` | `const objToMerge = { key3: "Merged", key4: "Object" }` | `const mergedObj = { key1: "value1", key2: "value2", key3: "Merged", key4: "Object" };` |
| Array, add new items | `const arrWithAdditionalItems = ${stringify(['a', 'b', ...arrayToSpread[0]])}` | `const arrayToSpread = ["Spread", "This"]` | `const arrWithAdditionalItems = ["a", "b", "Spread", "This"]` |
| Object, deep merge (Arrays are replaced) | `const deepMergedObj = ${deepMerge({ key1: "value1", key2: "value2", key3: ["value3_0", "value3_1"]}, objToDeepMerge[0] ))};` | `const objToDeepMerge = { key1: "replacedValue_1", key3: ["replacedValue3_0"], key4: "value4" }` | `const deepMergedObj = { key1: "replacedValue_1", key2: "value2", key3: ["replacedValue3_0"], key4: "value4" }` |
| BigInt | `const bigInt = ${stringify(someBigInt[0])};` | `const someBigInt = 123n` | `const bigInt = 123n;` |

> NOTE: If the object contains function as a value `stringify` utility truncates it to `[Function keyName]`. In this case we have to use object spread operator while merging the objects and `JSON.stringify` while replacing the objects. Checkout [`next.config.js.template.mjs`](https://github.com/scaffold-eth/create-eth/blob/main/templates/base/packages/nextjs/next.config.js.template.mjs) for an example.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.15.0",
"@fastify/deepmerge": "^2.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-typescript": "11.1.0",
"@types/inquirer": "9.0.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,190 +1,183 @@
import { withDefaults } from "../../../../utils.js";
import { withDefaults, stringify, deepMerge } from "../../../../utils.js";

const contents = ({ imports, solidityVersion, networks, compilers }) => {

const massagedCompilers = compilers[0]?.[0] ? JSON.stringify(compilers[0]) : '';

const defaultCompilers = `[
{
version: "${solidityVersion[0]}",
settings: {
optimizer: {
enabled: true,
// https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options
runs: 200,
},
},
},
]`;

return `import * as dotenv from "dotenv";
dotenv.config();
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-chai-matchers";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@nomicfoundation/hardhat-verify";
import "hardhat-deploy";
import "hardhat-deploy-ethers";
import { task } from "hardhat/config";
import generateTsAbis from "./scripts/generateTsAbis";
${imports.filter(Boolean).join("\n")}
// If not set, it uses ours Alchemy's default API key.
// You can get your own at https://dashboard.alchemyapi.io
const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
// If not set, it uses the hardhat account 0 private key.
// You can generate a random account with \`yarn generate\` or \`yarn account:import\` to import your existing PK
const deployerPrivateKey =
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
// If not set, it uses our block explorers default API keys.
const etherscanApiKey = process.env.ETHERSCAN_MAINNET_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
const etherscanOptimisticApiKey = process.env.ETHERSCAN_OPTIMISTIC_API_KEY || "RM62RDISS1RH448ZY379NX625ASG1N633R";
const basescanApiKey = process.env.BASESCAN_API_KEY || "ZZZEIPMT1MNJ8526VV2Y744CA7TNZR64G6";
const config: HardhatUserConfig = {
const defaultConfig = {
solidity: {
compilers: ${massagedCompilers || defaultCompilers}
compilers: [
{
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
]
},
defaultNetwork: "localhost",
namedAccounts: {
deployer: {
// By default, it will take the first Hardhat account as the deployer
default: 0,
},
},
networks: {
${networks[0] && `${networks[0]},`}
// View the networks that are pre-configured.
// If the network you are looking for is not here you can add new network settings
hardhat: {
forking: {
url: \`https://eth-mainnet.alchemyapi.io/v2/\${providerApiKey}\`,
enabled: process.env.MAINNET_FORKING_ENABLED === "true",
url: `https://eth-mainnet.alchemyapi.io/v2/\${providerApiKey}`,
enabled: '$$process.env.MAINNET_FORKING_ENABLED === "true"$$',
},
},
mainnet: {
url: \`https://eth-mainnet.alchemyapi.io/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://eth-mainnet.alchemyapi.io/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
sepolia: {
url: \`https://eth-sepolia.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://eth-sepolia.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
arbitrum: {
url: \`https://arb-mainnet.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://arb-mainnet.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
arbitrumSepolia: {
url: \`https://arb-sepolia.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://arb-sepolia.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
optimism: {
url: \`https://opt-mainnet.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://opt-mainnet.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
verify: {
etherscan: {
apiUrl: "https://api-optimistic.etherscan.io",
apiKey: etherscanOptimisticApiKey,
apiKey: "$$etherscanOptimisticApiKey$$",
},
},
},
optimismSepolia: {
url: \`https://opt-sepolia.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://opt-sepolia.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
verify: {
etherscan: {
apiUrl: "https://api-sepolia-optimistic.etherscan.io",
apiKey: etherscanOptimisticApiKey,
apiKey: "$$etherscanOptimisticApiKey$$",
},
},
},
polygon: {
url: \`https://polygon-mainnet.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://polygon-mainnet.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
polygonMumbai: {
url: \`https://polygon-mumbai.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://polygon-mumbai.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
polygonZkEvm: {
url: \`https://polygonzkevm-mainnet.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://polygonzkevm-mainnet.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
polygonZkEvmTestnet: {
url: \`https://polygonzkevm-testnet.g.alchemy.com/v2/\${providerApiKey}\`,
accounts: [deployerPrivateKey],
url: `https://polygonzkevm-testnet.g.alchemy.com/v2/\${providerApiKey}`,
accounts: ["$$deployerPrivateKey$$"],
},
gnosis: {
url: "https://rpc.gnosischain.com",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
chiado: {
url: "https://rpc.chiadochain.net",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
base: {
url: "https://mainnet.base.org",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
verify: {
etherscan: {
apiUrl: "https://api.basescan.org",
apiKey: basescanApiKey,
apiKey: "$$basescanApiKey$$",
},
},
},
baseSepolia: {
url: "https://sepolia.base.org",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
verify: {
etherscan: {
apiUrl: "https://api-sepolia.basescan.org",
apiKey: basescanApiKey,
apiKey: "$$basescanApiKey$$",
},
},
},
scrollSepolia: {
url: "https://sepolia-rpc.scroll.io",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
scroll: {
url: "https://rpc.scroll.io",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
pgn: {
url: "https://rpc.publicgoods.network",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
pgnTestnet: {
url: "https://sepolia.publicgoods.network",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
celo: {
url: "https://forno.celo.org",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
celoAlfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [deployerPrivateKey],
accounts: ["$$deployerPrivateKey$$"],
},
},
// configuration for harhdat-verify plugin
etherscan: {
apiKey: \`\${etherscanApiKey}\`,
apiKey: "$$etherscanApiKey$$",
},
// configuration for etherscan-verify from hardhat-deploy plugin
verify: {
etherscan: {
apiKey: \`\${etherscanApiKey}\`,
apiKey: "$$etherscanApiKey$$",
},
},
sourcify: {
enabled: false,
},
};

const contents = ({ preConfigContent, configOverrides }) => {
// Merge the default config with any overrides
const finalConfig = deepMerge(defaultConfig, configOverrides[0] || {});

return `import * as dotenv from "dotenv";
dotenv.config();
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-chai-matchers";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@nomicfoundation/hardhat-verify";
import "hardhat-deploy";
import "hardhat-deploy-ethers";
import { task } from "hardhat/config";
import generateTsAbis from "./scripts/generateTsAbis";
${preConfigContent[0] || ''}
// If not set, it uses ours Alchemy's default API key.
// You can get your own at https://dashboard.alchemyapi.io
const providerApiKey = process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF";
// If not set, it uses the hardhat account 0 private key.
// You can generate a random account with \`yarn generate\` or \`yarn account:import\` to import your existing PK
const deployerPrivateKey =
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
// If not set, it uses our block explorers default API keys.
const etherscanApiKey = process.env.ETHERSCAN_MAINNET_API_KEY || "DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW";
const etherscanOptimisticApiKey = process.env.ETHERSCAN_OPTIMISTIC_API_KEY || "RM62RDISS1RH448ZY379NX625ASG1N633R";
const basescanApiKey = process.env.BASESCAN_API_KEY || "ZZZEIPMT1MNJ8526VV2Y744CA7TNZR64G6";
const config: HardhatUserConfig = ${stringify(finalConfig)};
// Extend the deploy task
task("deploy").setAction(async (args, hre, runSuper) => {
// Run the original deploy task
Expand All @@ -193,14 +186,10 @@ task("deploy").setAction(async (args, hre, runSuper) => {
await generateTsAbis(hre);
});
export default config;`
export default config;`;
};

export default withDefaults(contents, {
imports: "",
solidityVersion: "0.8.20",
networks: "",
// set solidity compilers
// https://hardhat.org/hardhat-runner/docs/advanced/multiple-solidity-versions#multiple-solidity-versions
compilers: [],
preConfigContent: "",
configOverrides: {},
});
19 changes: 18 additions & 1 deletion templates/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { inspect } from "util";
import createDeepMerge from "@fastify/deepmerge";

// https://github.com/fastify/deepmerge?tab=readme-ov-file#mergearray Example 1
const replaceByClonedSource = (options) => {
const clone = options.clone
return (_target, source) => {
return clone(source)
}
}

export const deepMerge = createDeepMerge({ mergeArray: replaceByClonedSource });

export const withDefaults =
(template, expectedArgsDefaults, debug = false) =>
Expand Down Expand Up @@ -26,4 +37,10 @@ export const withDefaults =
return template(argsWithDefault);
};

export const stringify = val => inspect(val, { depth: null, compact: true, maxArrayLength: null, maxStringLength: null })
export const stringify = val => {
const str = inspect(val, { depth: null, compact: true, maxArrayLength: null, maxStringLength: null });
return str
.replace(/"\$\$([^"]+)\$\$"/g, '$1')
.replace(/'\$\$([^']+)\$\$'/g, '$1')
.replace(/(['"])(.*?\$\{.*?\}.*?)\1/g, '`$2`');
};
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,13 @@ __metadata:
languageName: node
linkType: hard

"@fastify/deepmerge@npm:^2.0.1":
version: 2.0.1
resolution: "@fastify/deepmerge@npm:2.0.1"
checksum: be504f17bcd5e26fd39ec09fcf03013ab65d3109716d536cad452f846e45bd2a533358897af9d4efd73fc3789d32bfc59a6a06b3706aa6fc00f013d3a11de03b
languageName: node
linkType: hard

"@gar/promisify@npm:^1.1.3":
version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3"
Expand Down Expand Up @@ -1391,6 +1398,7 @@ __metadata:
dependencies:
"@changesets/cli": ^2.26.2
"@eslint/js": ^9.15.0
"@fastify/deepmerge": ^2.0.1
"@rollup/plugin-json": ^6.1.0
"@rollup/plugin-typescript": 11.1.0
"@types/inquirer": 9.0.3
Expand Down

0 comments on commit d14462d

Please sign in to comment.