@@ -172,7 +172,7 @@ import "@openzeppelin/{{__item_context.file.absolutePath}}";
-
{{{name}}}({{typed-params params}})
+
{{{name}}}({{typedParams params}})
error
#
@@ -180,7 +180,7 @@ import "@openzeppelin/{{__item_context.file.absolutePath}}";
-{{{process-natspec natspec.dev}}}
+{{{processNatspec natspec.dev}}}
diff --git a/docgen/templates-md/helpers.js b/docgen/templates-md/helpers.js
index 1c679f5f..bc27ee87 100644
--- a/docgen/templates-md/helpers.js
+++ b/docgen/templates-md/helpers.js
@@ -6,9 +6,9 @@ const os = require('os');
const API_DOCS_PATH = 'contracts/5.x/api';
-module.exports['oz-version'] = () => version;
+module.exports.ozVersion = () => version;
-module.exports['readme-path'] = opts => {
+module.exports.readmePath = opts => {
const pageId = opts.data.root.id;
const basePath = pageId.replace(/\.(adoc|mdx)$/, '');
return 'contracts/' + basePath + '/README.adoc';
@@ -31,7 +31,7 @@ module.exports.names = params => params?.map(p => p.name).join(', ');
// Simple function counter for unique IDs
const functionNameCounts = {};
-module.exports['simple-id'] = function (name) {
+module.exports.simpleId = function (name) {
if (!functionNameCounts[name]) {
functionNameCounts[name] = 1;
return name;
@@ -41,7 +41,7 @@ module.exports['simple-id'] = function (name) {
}
};
-module.exports['reset-function-counts'] = function () {
+module.exports.resetFunctionCounts = function () {
Object.keys(functionNameCounts).forEach(key => delete functionNameCounts[key]);
return '';
};
@@ -52,13 +52,13 @@ module.exports.eq = (a, b) => a === b;
// import specifier for non-`contracts` packages, where the file is published
// at the package root (e.g. @openzeppelin/community-contracts/account/X.sol)
// but the source lives at contracts/account/X.sol in the repo.
-module.exports['strip-contracts-prefix'] = function (p) {
+module.exports.stripContractsPrefix = function (p) {
return typeof p === 'string' ? p.replace(/^contracts\//, '') : p;
};
-module.exports['starts-with'] = (str, prefix) => str && str.startsWith(prefix);
+module.exports.startsWith = (str, prefix) => str && str.startsWith(prefix);
// Process natspec content with {REF} and link replacement
-module.exports['process-natspec'] = function (natspec, opts) {
+module.exports.processNatspec = function (natspec, opts) {
if (!natspec) return '';
const currentPage = opts.data.root.__item_context?.page || opts.data.root.id;
@@ -68,7 +68,7 @@ module.exports['process-natspec'] = function (natspec, opts) {
return processCallouts(processed); // Add callout processing at the end
};
-module.exports['typed-params'] = params => {
+module.exports.typedParams = params => {
return params?.map(p => `${p.type}${p.indexed ? ' indexed' : ''}${p.name ? ' ' + p.name : ''}`).join(', ');
};
@@ -497,7 +497,7 @@ module.exports.description = opts => {
return `Smart contract ${dirName.replace('-', ' ')} utilities and implementations`;
};
-module.exports['with-prelude'] = opts => {
+module.exports.withPrelude = opts => {
const currentPage = opts.data.root.id;
const links = getAllLinks(opts.data.site.items, currentPage);
const contents = opts.fn();
diff --git a/docgen/templates-md/page.hbs b/docgen/templates-md/page.hbs
index 11c79513..70f1155b 100644
--- a/docgen/templates-md/page.hbs
+++ b/docgen/templates-md/page.hbs
@@ -3,9 +3,9 @@ title: "{{title}}"
description: "{{description}}"
---
-{{#with-prelude}}
-{{readme (readme-path)}}
-{{/with-prelude}}
+{{#withPrelude}}
+{{readme (readmePath)}}
+{{/withPrelude}}
{{#each items}}
{{>contract}}
diff --git a/docgen/templates-md/properties.js b/docgen/templates-md/properties.js
index f2453b63..1b6e4491 100644
--- a/docgen/templates-md/properties.js
+++ b/docgen/templates-md/properties.js
@@ -44,24 +44,24 @@ module.exports.inheritance = function ({ item, build }) {
.filter((c, i) => c.name !== 'Context' || i === 0);
};
-module.exports['has-functions'] = function ({ item }) {
+module.exports.hasFunctions = function ({ item }) {
return item.inheritance && item.inheritance.some(c => c.functions.length > 0);
};
-module.exports['has-events'] = function ({ item }) {
+module.exports.hasEvents = function ({ item }) {
return item.inheritance && item.inheritance.some(c => c.events.length > 0);
};
-module.exports['has-errors'] = function ({ item }) {
+module.exports.hasErrors = function ({ item }) {
return item.inheritance && item.inheritance.some(c => c.errors.length > 0);
};
-module.exports['internal-variables'] = function ({ item }) {
+module.exports.internalVariables = function ({ item }) {
return item.variables ? item.variables.filter(({ visibility }) => visibility === 'internal') : [];
};
-module.exports['has-internal-variables'] = function ({ item }) {
- return module.exports['internal-variables']({ item }).length > 0;
+module.exports.hasInternalVariables = function ({ item }) {
+ return module.exports.internalVariables({ item }).length > 0;
};
module.exports.functions = function ({ item }) {
@@ -79,7 +79,7 @@ module.exports.returns2 = function ({ item }) {
}
};
-module.exports['inherited-functions'] = function ({ item }) {
+module.exports.inheritedFunctions = function ({ item }) {
const { inheritance } = item;
if (!inheritance) return [];
diff --git a/scripts/generate-api-docs.js b/scripts/generate-api-docs.js
index b4bfff0b..7b716871 100755
--- a/scripts/generate-api-docs.js
+++ b/scripts/generate-api-docs.js
@@ -126,6 +126,32 @@ function derivePackageName(repoName) {
return withoutPrefix;
}
+// Helpers and properties exported by templates-md/{helpers,properties}.js,
+// used to generate ESM shim files when injecting into the new layout. The
+// hardhat3-style docgen plugin only loads helpers.ts/properties.ts via
+// `await import()`, so it needs real named exports — kebab-case CJS keys
+// don't survive ESM interop.
+const HELPER_EXPORTS = [
+ "ozVersion", "readmePath", "readme", "names", "simpleId",
+ "resetFunctionCounts", "eq", "stripContractsPrefix", "startsWith",
+ "processNatspec", "typedParams", "slug", "title", "description",
+ "withPrelude",
+];
+const PROPERTY_EXPORTS = [
+ "anchor", "fullname", "inheritance", "hasFunctions", "hasEvents",
+ "hasErrors", "internalVariables", "hasInternalVariables", "functions",
+ "returns2", "inheritedFunctions",
+];
+
+function tsShim(varName, sourcePath, exports) {
+ return [
+ `import ${varName} from '${sourcePath}';`,
+ "",
+ ...exports.map(e => `export const ${e} = ${varName}.${e};`),
+ "",
+ ].join("\n");
+}
+
async function injectTemplates(tempDir, options) {
const { contractsRepo, contractsBranch, apiOutputDir } = options;
const repoInfo = extractRepoInfo(contractsRepo);
@@ -133,24 +159,84 @@ async function injectTemplates(tempDir, options) {
console.log("📋 Injecting canonical MDX templates...");
- const templatesTarget = path.join(tempDir, "docs", "templates-md");
- // Overwrite the cloned repo's docs/config.js with our canonical config.
- // The cloned repo's hardhat config and prepare-docs.sh scripts both load
- // `./docs/config`, so this single overwrite covers both paths without
- // needing a separate config-md.js write or a hardhat-config regex patch.
- const configTarget = path.join(tempDir, "docs", "config.js");
+ // Detect new layout (hardhat3-style): docs/config.mjs + docs/templates/.
+ // Legacy layout uses docs/config.js + docs/templates-md/.
+ let newLayout = false;
+ try {
+ await fs.access(path.join(tempDir, "docs", "config.mjs"));
+ newLayout = true;
+ } catch {}
+
+ const templatesTarget = path.join(
+ tempDir,
+ "docs",
+ newLayout ? "templates" : "templates-md",
+ );
+ const configTarget = path.join(
+ tempDir,
+ "docs",
+ newLayout ? "config.mjs" : "config.js",
+ );
+ const configSource = path.join(
+ DOCGEN_DIR,
+ newLayout ? "config-md.mjs" : "config-md.js",
+ );
+ if (newLayout) {
+ console.log(" ✓ Detected new layout (config.mjs + docs/templates/)");
+ }
+
+ // Wipe target templates dir to avoid stale files from the cloned repo
+ // (e.g. the hardhat3 branch's helpers.ts/properties.ts/contract.hbs).
+ await fs.rm(templatesTarget, { recursive: true, force: true });
await fs.mkdir(templatesTarget, { recursive: true });
await copyDirRecursive(
path.join(DOCGEN_DIR, "templates-md"),
templatesTarget,
);
- await fs.copyFile(path.join(DOCGEN_DIR, "config-md.js"), configTarget);
+ await fs.copyFile(configSource, configTarget);
+
+ // The new plugin loads helpers via `require.resolve(dir + '/helpers.ts')`
+ // and `await import(...)` — only `.ts` is searched and only ESM named
+ // function exports are picked up. Rename our CJS .js files to .cjs and
+ // emit thin .ts shims that re-export each function.
+ const helpersFileName = newLayout ? "helpers.cjs" : "helpers.js";
+ const propertiesFileName = newLayout ? "properties.cjs" : "properties.js";
+ if (newLayout) {
+ await fs.rename(
+ path.join(templatesTarget, "helpers.js"),
+ path.join(templatesTarget, helpersFileName),
+ );
+ await fs.rename(
+ path.join(templatesTarget, "properties.js"),
+ path.join(templatesTarget, propertiesFileName),
+ );
+ // `require('./helpers')` in properties.cjs no longer resolves once
+ // the file is renamed (Node's CJS resolver does not search for
+ // `.cjs` by default). Patch the import to use the explicit name.
+ const propertiesCjsPath = path.join(templatesTarget, propertiesFileName);
+ let propertiesSource = await fs.readFile(propertiesCjsPath, "utf8");
+ propertiesSource = propertiesSource.replace(
+ /require\(['"]\.\/helpers['"]\)/g,
+ "require('./helpers.cjs')",
+ );
+ await fs.writeFile(propertiesCjsPath, propertiesSource, "utf8");
+ await fs.writeFile(
+ path.join(templatesTarget, "helpers.ts"),
+ tsShim("h", "./helpers.cjs", HELPER_EXPORTS),
+ "utf8",
+ );
+ await fs.writeFile(
+ path.join(templatesTarget, "properties.ts"),
+ tsShim("p", "./properties.cjs", PROPERTY_EXPORTS),
+ "utf8",
+ );
+ }
- // Customize API_DOCS_PATH in helpers.js (URL path = file path minus content/)
+ // Customize API_DOCS_PATH in helpers (URL path = file path minus content/)
const apiDocsPath = apiOutputDir.replace(/^content\//, "");
- const helpersPath = path.join(templatesTarget, "helpers.js");
+ const helpersPath = path.join(templatesTarget, helpersFileName);
let helpers = await fs.readFile(helpersPath, "utf8");
helpers = helpers.replace(
/const API_DOCS_PATH = ['"][^'"]+['"]/,
@@ -173,7 +259,7 @@ async function injectTemplates(tempDir, options) {
// /blob/v0.0.1/... when community-contracts package.json has a placeholder
// version that doesn't correspond to an actual tag.
contract = contract.replace(
- /blob\/v\{\{oz-version\}\}/g,
+ /blob\/v\{\{ozVersion\}\}/g,
`blob/${contractsBranch}`,
);
@@ -182,11 +268,11 @@ async function injectTemplates(tempDir, options) {
// (npm package is `@openzeppelin/contracts`, file path keeps the prefix).
// For other packages (community-contracts, confidential-contracts), the
// npm package is published with `contracts/` as the package root, so the
- // import skips that prefix — strip it via the strip-contracts-prefix helper.
+ // import skips that prefix via the stripContractsPrefix helper.
if (packageName !== "contracts") {
contract = contract.replace(
/import "@openzeppelin\/\{\{__item_context\.file\.absolutePath\}\}";/g,
- `import "@openzeppelin/${packageName}/{{strip-contracts-prefix __item_context.file.absolutePath}}";`,
+ `import "@openzeppelin/${packageName}/{{stripContractsPrefix __item_context.file.absolutePath}}";`,
);
}
await fs.writeFile(contractPath, contract, "utf8");