From 30db216d2961bfa9e7e71943bb60f4d4ad719d20 Mon Sep 17 00:00:00 2001 From: nonergodic Date: Thu, 5 Jun 2025 11:24:48 -0700 Subject: [PATCH] impl scripts for publishing SDK on NPM for Hardhat --- foundry.toml | 13 ---- npm_publish/clean_remappings.js | 58 +++++++++++++++ npm_publish/package.json | 22 ++++++ npm_publish/publish.sh | 124 ++++++++++++++++++++++++++++++++ remappings.txt | 4 ++ 5 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 npm_publish/clean_remappings.js create mode 100644 npm_publish/package.json create mode 100755 npm_publish/publish.sh create mode 100644 remappings.txt diff --git a/foundry.toml b/foundry.toml index f553a940..eae1d291 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,17 +6,4 @@ out = "out" libs = ["lib"] via_ir = true -#currently, forge does not allow remapping of individual files -# (see here: https://github.com/foundry-rs/foundry/issues/7527#issuecomment-2269444829) -#so for now we are using the IERC20 prefix as a workaround that allows users -# to override the IERC20 interface with whatever they use in their project -# (well, as long as their file is also called IERC20.sol and is interface compatible) -remappings = [ - "ds-test/=lib/forge-std/lib/ds-test/src/", - "forge-std/=lib/forge-std/src/", - "wormhole-sdk/=src/", - "IERC20/=src/interfaces/token/", - "SafeERC20/=src/libraries/", -] - verbosity = 3 diff --git a/npm_publish/clean_remappings.js b/npm_publish/clean_remappings.js new file mode 100644 index 00000000..d07a4722 --- /dev/null +++ b/npm_publish/clean_remappings.js @@ -0,0 +1,58 @@ +if (process.argv.length !== 4) { + console.error("Usage: node clean_remappings.js "); + process.exit(1); +} +const remappingsFilePath = process.argv[2] + "/remappings.txt"; +const contractsDir = process.argv[3]; + +const fs = require("fs"); +const path = require("path"); + +const remappings = fs.readFileSync(remappingsFilePath, "utf8") + .split("\n") + .reduce( + (acc, line) => { + if (line && !line.startsWith("#")) { + const [label, target] = line.split("/="); + if (target.startsWith("src/")) + acc = { ...acc, [label]: target.slice(4) }; + } + return acc; + }, + {} + ); + +const findSolFiles = (dir) => fs.readdirSync(dir) + .reduce( + (acc, item) => { + const fullPath = path.join(dir, item); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) + acc.push(...findSolFiles(fullPath)); + else if (stat.isFile() && path.extname(item) === ".sol") + acc.push(fullPath); + return acc; + }, + [] + ); + +//match plain import statements or starting at from to avoid hassle of parsing multi-line imports +const importRe = /((?:import|from)\s+")([^/]+)\/((?:[^/"]+)\/)*([^.]+\.sol";)/g; + +for (const filePath of findSolFiles(contractsDir)) { + let modified = false; + const fileContent = fs.readFileSync(filePath, "utf8").replace(importRe, (match, head, label, subdir, tail) => { + const target = remappings[label]; + if (target !== undefined) { + modified = true; + const longPath = path.join(contractsDir, target, subdir ?? ""); + const shortPath = path.relative(path.dirname(filePath), longPath) || "."; + const finalPath = (shortPath.startsWith(".") ? "" : "./") + shortPath + "/"; + return `${head}${finalPath}${tail}`; + } + return match; + }); + + if (modified) + fs.writeFileSync(filePath, fileContent, "utf8"); +} diff --git a/npm_publish/package.json b/npm_publish/package.json new file mode 100644 index 00000000..74c0f913 --- /dev/null +++ b/npm_publish/package.json @@ -0,0 +1,22 @@ +{ + "name": "@wormhole-foundation/solidity-sdk", + "version": "1.0.0", + "description": "Wormhole Solidity SDK", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/wormhole-foundation/wormhole-solidity-sdk.git" + }, + "files": [ + "contracts/**/*.sol" + ], + "keywords": [ + "solidity", + "wormhole", + "cross-chain", + "evm", + "foundry", + "hardhat", + "sdk" + ] +} \ No newline at end of file diff --git a/npm_publish/publish.sh b/npm_publish/publish.sh new file mode 100755 index 00000000..cbc2d70b --- /dev/null +++ b/npm_publish/publish.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +# NPM Publishing Script for Wormhole Solidity SDK +# +# Usage: +# ./publish.sh [tag] [dry-run] +# +# Examples: +# ./publish.sh # Publish with 'latest' tag +# ./publish.sh beta # Publish with 'beta' tag +# ./publish.sh latest true # Dry run with 'latest' tag +# ./publish.sh beta true # Dry run with 'beta' tag + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +TEMP_PUBLISH_DIR="${PROJECT_ROOT_DIR}/dist_npm" + +NPM_PACKAGE_NAME="$(node -p "require('${SCRIPT_DIR}/package.json').name")" +NPM_PACKAGE_VERSION="$(node -p "require('${SCRIPT_DIR}/package.json').version")" + +NPM_TAG="${1:-latest}" +DRY_RUN="${2:-false}" + +# read Solidity version from foundry.toml for Hardhat compilation check +SOLC_VERSION=$(grep -oP '(?<=solc_version = ")[^"]*' "${PROJECT_ROOT_DIR}/foundry.toml") + +fail() { + echo "ERROR: $1" >&2 + exit 1 +} + +get_npm_version() { + local result=$(npm view "$1" "$2" --json --silent 2>/dev/null || echo "null") + if echo "${result}" | grep -q '"error":'; then + echo "${3:-}" + elif echo "${result}" | grep -q '"'; then + echo "${result}" | tr -d '"' + else + fail "Failed to get version for package '${1}' with tag '${2}' - error: ${result}" + fi +} + +check_version_increment() { + LATEST_VERSION=$(get_npm_version "${NPM_PACKAGE_NAME}" "version" "0.0.0") + if [ "${LATEST_VERSION}" = "0.0.0" ]; then + echo "Package not found on npm registry. Assuming first publish." + else + echo "Latest version on npm: ${LATEST_VERSION}" + fi + + TAG_VERSION=$(get_npm_version "${NPM_PACKAGE_NAME}" "dist-tags.${NPM_TAG}" "") + echo "Current '${NPM_TAG}' tag points to: ${TAG_VERSION:-none}" + + if [ "${TAG_VERSION}" = "${NPM_PACKAGE_VERSION}" ]; then + fail "Version '${NPM_PACKAGE_VERSION}' is already published to tag '${NPM_TAG}'." + fi + + if [ "${LATEST_VERSION}" != "0.0.0" ] && + [ "$(printf '%s\n' "${NPM_PACKAGE_VERSION}" "${LATEST_VERSION}" | sort -V | head -n1)" = "${NPM_PACKAGE_VERSION}" ] && + [ "${NPM_PACKAGE_VERSION}" != "${LATEST_VERSION}" ]; then + fail "Version '${NPM_PACKAGE_VERSION}' is older than latest version '${LATEST_VERSION}'." + fi +} + +# --- Main Script --- + +echo "Starting NPM publish process..." +echo "Version: ${NPM_PACKAGE_VERSION}" +echo "Tag: ${NPM_TAG}" +echo "Dry Run: ${DRY_RUN}" + +echo "1. Check Version Against NPM Registry" +check_version_increment + +echo "2. Git Status Checks" +if [ "${NPM_TAG}" = "latest" ] && [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then + fail "Publishing to 'latest' tag but not on the 'main' branch." +fi +if ! git diff-index --quiet HEAD --; then + fail "There are uncommitted changes. Please commit or stash them before publishing." +fi + +echo "3. Cleaning up and setting up temporary publish directory" +rm -rf "${TEMP_PUBLISH_DIR}" +mkdir -p "${TEMP_PUBLISH_DIR}" +( + cd "${TEMP_PUBLISH_DIR}" + cp -a "${PROJECT_ROOT_DIR}/src" . + mv "src" "contracts" + rm -rf "contracts/testing" "contracts/legacy" # remove stuff that depends on forge-std + cp "${SCRIPT_DIR}/package.json" "${PROJECT_ROOT_DIR}/README.md" "${PROJECT_ROOT_DIR}/LICENSE" . +) + +echo "4. Transforming Foundry remappings to relative paths for Hardhat..." +node "${SCRIPT_DIR}/clean_remappings.js" "${PROJECT_ROOT_DIR}" "${TEMP_PUBLISH_DIR}/contracts" + +echo "5. Hardhat compilation check" +( + cd "${TEMP_PUBLISH_DIR}" + echo "module.exports = { solidity: { version: '${SOLC_VERSION}', settings: { viaIR: true, optimizer: { enabled: true } } } };" > hardhat.config.js + npm install --no-save --silent hardhat + if ! npx hardhat compile > /dev/null 2>&1; then + fail "Hardhat compilation failed. Aborting." + fi +) + +echo "6. Cleaning up temporary files before publishing" +( + cd "${TEMP_PUBLISH_DIR}" + rm -rf hardhat.config.js node_modules cache artifacts +) + +echo "7. Publishing version ${NPM_PACKAGE_VERSION} with tag '${NPM_TAG}'" +( + cd "${TEMP_PUBLISH_DIR}" + npm publish --silent --tag "${NPM_TAG}" $([ "${DRY_RUN}" = "true" ] && echo "--dry-run") +) + +echo "8. Cleaning up temporary publish directory: ${TEMP_PUBLISH_DIR}" +rm -rf "${TEMP_PUBLISH_DIR}" + +echo "Successfully $([ "${DRY_RUN}" = "true" ] && echo "completed dry run for" || echo "published") version ${NPM_PACKAGE_VERSION} with tag '${NPM_TAG}'" diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 00000000..70209382 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,4 @@ +forge-std/=lib/forge-std/src/ +wormhole-sdk/=src/ +IERC20/=src/interfaces/token/ +SafeERC20/=src/libraries/