ci: Update to support ESM projects #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - master | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| build-and-release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' # Node 22+ 对 ESM + ts-node 支持更好 | |
| - name: Detect project module format (CJS/ESM) | |
| id: detect_format | |
| run: | | |
| echo "🔎 检测项目模块格式..." | |
| # 方法1: 检查 package.json 中的 "type" 字段 | |
| PKG_TYPE=$(jq -r '.type // empty' package.json 2>/dev/null) | |
| echo "package.json type: '${PKG_TYPE}'" | |
| # 方法2: 检查 tsconfig.json 中的 module 设置 | |
| TSC_MODULE="" | |
| if [ -f "tsconfig.json" ]; then | |
| TSC_MODULE=$(jq -r '.compilerOptions.module // empty' tsconfig.json 2>/dev/null | tr '[:upper:]' '[:lower:]') | |
| echo "tsconfig.json module: '${TSC_MODULE}'" | |
| fi | |
| # 方法3: 检查 build 脚本是否包含 esbuild/rollup/vite 等 ESM 工具 | |
| BUILD_SCRIPT=$(jq -r '.scripts.build // empty' package.json 2>/dev/null) | |
| echo "build script: '${BUILD_SCRIPT}'" | |
| # 方法4: 检查源文件是否使用 import/export 语法 | |
| HAS_IMPORTS=false | |
| if ls src/*.ts src/*.js src/**/*.ts src/**/*.js 2>/dev/null | head -5 > /dev/null 2>&1; then | |
| if grep -rl "^import\|^export" src/ --include="*.ts" --include="*.js" 2>/dev/null | head -1 > /dev/null 2>&1; then | |
| HAS_IMPORTS=true | |
| fi | |
| fi | |
| echo "Has ES module imports: ${HAS_IMPORTS}" | |
| # 综合判断 | |
| IS_ESM=false | |
| # 明确指定为 ESM | |
| if [ "$PKG_TYPE" = "module" ]; then | |
| IS_ESM=true | |
| echo "✅ 检测到 ESM: package.json type=module" | |
| # tsconfig 使用 ESM 相关模块设置 | |
| elif [[ "$TSC_MODULE" == "esnext" || "$TSC_MODULE" == "es2020" || "$TSC_MODULE" == "es2022" || "$TSC_MODULE" == "nodenext" || "$TSC_MODULE" == "node16" ]]; then | |
| IS_ESM=true | |
| echo "✅ 检测到 ESM: tsconfig module=${TSC_MODULE}" | |
| # 使用现代打包工具 | |
| elif echo "$BUILD_SCRIPT" | grep -qE "esbuild|rollup|vite|tsup|unbuild"; then | |
| IS_ESM=true | |
| echo "✅ 检测到 ESM: 使用现代打包工具" | |
| # 明确指定为 CJS | |
| elif [ "$PKG_TYPE" = "commonjs" ]; then | |
| IS_ESM=false | |
| echo "✅ 检测到 CJS: package.json type=commonjs" | |
| # 默认使用 ESModule 语法的源文件视为 ESM | |
| elif [ "$HAS_IMPORTS" = "true" ]; then | |
| # 进一步检查是否有 tsconfig 中明确设置为 CommonJS | |
| if [[ "$TSC_MODULE" == "commonjs" || "$TSC_MODULE" == "umd" || "$TSC_MODULE" == "amd" ]]; then | |
| IS_ESM=false | |
| echo "✅ 检测到 CJS: tsconfig module=${TSC_MODULE}" | |
| else | |
| IS_ESM=true | |
| echo "✅ 检测到 ESM: 源文件使用 import/export 语法" | |
| fi | |
| else | |
| IS_ESM=false | |
| echo "✅ 默认使用 CJS 模式" | |
| fi | |
| echo "IS_ESM=${IS_ESM}" >> $GITHUB_ENV | |
| echo "is_esm=${IS_ESM}" >> $GITHUB_OUTPUT | |
| - name: Install dependencies | |
| run: | | |
| if [ "$IS_ESM" = "true" ]; then | |
| echo "📦 ESM 项目: 安装依赖并添加 tsx" | |
| npm install | |
| # ESM 项目需要 tsx 来处理 .ts 文件(ts-node 在 ESM 模式下有兼容问题) | |
| npm install -D tsx 2>/dev/null || echo "tsx 安装失败或已存在" | |
| else | |
| echo "📦 CJS 项目: 使用 npm install" | |
| npm install | |
| fi | |
| - name: Build project | |
| run: | | |
| if [ "$IS_ESM" = "true" ]; then | |
| echo "🔨 ESM 项目: 执行构建" | |
| # 检查是否有专门的 esm 构建脚本 | |
| ESM_BUILD_SCRIPT=$(jq -r '.scripts["build:esm"] // empty' package.json 2>/dev/null) | |
| if [ -n "$ESM_BUILD_SCRIPT" ]; then | |
| echo "使用 build:esm 脚本" | |
| npm run build:esm | |
| else | |
| echo "使用默认 build 脚本" | |
| # Node 22+ 对 ESM + ts-node 支持较好,先尝试直接运行 | |
| if npm run build; then | |
| echo "✅ 构建成功" | |
| else | |
| echo "⚠️ 直接构建失败,尝试使用 tsx 替代 ts-node" | |
| # 安装 tsx | |
| npm install -D tsx 2>/dev/null || true | |
| # 备份并临时替换 package.json | |
| cp package.json package.json.bak | |
| jq '(.scripts.compile // "") |= gsub("ts-node"; "tsx") | (.scripts.build // "") |= gsub("ts-node"; "tsx")' package.json > package.json.tmp && mv package.json.tmp package.json | |
| echo "修改后的构建脚本:" | |
| jq '.scripts | {build, compile}' package.json | |
| # 使用 tsx 执行构建 | |
| npm run build | |
| # 恢复原始 package.json | |
| mv package.json.bak package.json | |
| echo "✅ 使用 tsx 构建完成" | |
| fi | |
| fi | |
| else | |
| echo "🔨 CJS 项目: 执行构建" | |
| npm run build | |
| fi | |
| - name: Verify build output format | |
| id: verify_build | |
| run: | | |
| echo "🔍 验证构建输出格式..." | |
| # 检查构建产物目录 | |
| DIST_DIR="build/dist" | |
| if [ ! -d "$DIST_DIR" ]; then | |
| echo "⚠️ 构建产物目录不存在: $DIST_DIR" | |
| # 尝试其他可能的目录 | |
| for dir in dist build output; do | |
| if [ -d "$dir" ]; then | |
| DIST_DIR="$dir" | |
| echo "✅ 找到构建目录: $dir" | |
| break | |
| fi | |
| done | |
| fi | |
| # 检查是否有 .mjs 或 .cjs 文件来确认格式 | |
| if ls "$DIST_DIR"/**/*.mjs 2>/dev/null | head -1 > /dev/null 2>&1; then | |
| echo "✅ 确认 ESM 格式输出 (.mjs 文件)" | |
| echo "BUILD_FORMAT=esm" >> $GITHUB_OUTPUT | |
| elif ls "$DIST_DIR"/**/*.cjs 2>/dev/null | head -1 > /dev/null 2>&1; then | |
| echo "✅ 确认 CJS 格式输出 (.cjs 文件)" | |
| echo "BUILD_FORMAT=cjs" >> $GITHUB_OUTPUT | |
| else | |
| echo "ℹ️ 无法从文件扩展名判断格式,使用检测结果" | |
| echo "BUILD_FORMAT=$IS_ESM" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Get version & extension name from extension.json | |
| id: meta | |
| run: | | |
| echo "VERSION=$(jq -r .version extension.json)" >> $GITHUB_ENV | |
| echo "EEXTNAME=$(jq -r .name extension.json)" >> $GITHUB_ENV | |
| - name: Check and create tag | |
| id: check_tag | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| TAG="v${VERSION}" | |
| echo "🔎 检查 tag $TAG 是否存在..." | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "⚠️ Tag $TAG 已存在,跳过发布。" | |
| echo "SKIP_RELEASE=true" >> $GITHUB_ENV | |
| else | |
| echo "✅ Tag $TAG 不存在,创建并推送。" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| echo "SKIP_RELEASE=false" >> $GITHUB_ENV | |
| fi | |
| - name: Extract changelog for version | |
| if: env.SKIP_RELEASE == 'false' | |
| id: changelog | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| EEXTNAME=${{ env.EEXTNAME }} | |
| echo "🔎 Extracting changelog for version $VERSION from CHANGELOG.md" | |
| ESCAPED_VERSION=$(echo "$VERSION" | sed 's/\./\\./g') | |
| if [ -f "CHANGELOG.md" ]; then | |
| # 支持多种 CHANGELOG 格式(兼容大小写 V): | |
| # - # v1.5.3 或 # V1.5.3 或 # 1.5.3 | |
| # - ## v1.5.3 或 ## V1.5.3 或 ## 1.5.3 | |
| # - ## [1.5.3] - 2024-01-01 或 ## [v1.5.3] | |
| NOTES=$(awk ' | |
| BEGIN { found=0 } | |
| # 匹配版本行(兼容大小写 v/V) | |
| /^(#|##) \[?[vV]?'"$ESCAPED_VERSION"'\]?([[:space:]-]|$)/ { | |
| if (found) exit | |
| found=1 | |
| next | |
| } | |
| # 遇到下一个版本标题时停止 | |
| /^(#|##) \[?[vV]?[0-9]+\.[0-9]+\.[0-9]+\]?([[:space:]-]|$)/ { | |
| if (found) exit | |
| } | |
| # 遇到一级标题(如"更新日志"、"Changelog"、"修改记录")时停止 | |
| /^# [^[\-0-9]/ { | |
| if (found) exit | |
| } | |
| found { print } | |
| ' CHANGELOG.md) | |
| # 清理空行 | |
| NOTES=$(echo "$NOTES" | sed '/^[[:space:]]*$/d' | sed '1{/^$/d}') | |
| if [ -z "$NOTES" ]; then | |
| echo "⚠️ CHANGELOG.md 中未找到版本 $VERSION 的记录,使用默认说明" | |
| NOTES="Release v${VERSION}" | |
| else | |
| echo "✅ 已从 CHANGELOG.md 中提取版本 $VERSION 的更新日志" | |
| fi | |
| else | |
| echo "⚠️ 未找到 CHANGELOG.md 文件,使用默认说明" | |
| NOTES="Release v${VERSION}" | |
| fi | |
| # 添加页脚 | |
| NOTES="${NOTES} | |
| --- | |
| 由GitHub Actions自动构建并发布 | |
| Built and released automatically by GitHub Actions | |
| 也可前往[嘉立创EDA扩展广场](https://jlc-ext.com/item/oshwhub/${EEXTNAME})下载 | |
| Also available at [JLCEDA Extension Plaza](https://jlcext.com/item/oshwlab/${EEXTNAME})" | |
| # 使用 EOF 分隔符设置多行环境变量 | |
| echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV | |
| echo "$NOTES" >> $GITHUB_ENV | |
| echo "EOF" >> $GITHUB_ENV | |
| echo "------ Release Notes ------" | |
| echo "$NOTES" | |
| echo "---------------------------" | |
| - name: Build zh-cn package | |
| if: env.SKIP_RELEASE == 'false' | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| EEXTNAME=${{ env.EEXTNAME }} | |
| DIST_DIR="build/dist" | |
| BASE_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}.eext" | |
| ZH_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}_zh-cn.eext" | |
| echo "Building zh-cn package..." | |
| # Detect language by counting Chinese characters in README.md | |
| CN_COUNT=$(grep -oP '[\x{4e00}-\x{9fff}]' README.md 2>/dev/null | wc -l) | |
| if [ "$CN_COUNT" -gt 10 ]; then | |
| IS_CHINESE=true | |
| else | |
| IS_CHINESE=false | |
| fi | |
| echo "README.md Chinese chars: $CN_COUNT, is_chinese: $IS_CHINESE" | |
| if [ "$IS_CHINESE" = "true" ]; then | |
| # README.md is Chinese, just copy | |
| echo "README.md is Chinese, just copy" | |
| cp "$BASE_FILE" "$ZH_FILE" | |
| else | |
| # README.md is English, find zh and swap | |
| ZH_README="" | |
| if [ -f "README_zh.md" ]; then | |
| ZH_README="README_zh.md" | |
| elif [ -f "README.zh.md" ]; then | |
| ZH_README="README.zh.md" | |
| else | |
| ZH_README=$(ls README_zh-*.md README.zh-*.md 2>/dev/null | head -1) | |
| fi | |
| if [ -z "$ZH_README" ]; then | |
| echo "README.md is English, no zh found, just copy" | |
| cp "$BASE_FILE" "$ZH_FILE" | |
| else | |
| echo "README.md is English, swap zh: $ZH_README" | |
| mv README.md README.en.md | |
| cp "$ZH_README" README.md | |
| find . -name "*.md" -not -path "./build/*" -not -path "./node_modules/*" -not -path "./.git/*" -exec sed -i 's/README\.md/README.en.md/g' {} + | |
| # Swap extension.json fields with zh-Hans translations | |
| ZH_I18N="locales/extensionJson/zh-Hans.json" | |
| if [ -f "$ZH_I18N" ]; then | |
| echo "Swapping extension.json with zh-Hans translations..." | |
| TMP_EXT=$(mktemp) | |
| jq --slurpfile i18n "$ZH_I18N" ' | |
| (if .displayName then .displayName = ($i18n[0].displayName // $i18n[0][.displayName] // .displayName) else . end) | | |
| (if .description then .description = ($i18n[0].description // $i18n[0][.description] // .description) else . end) | | |
| (if .keywords then .keywords = [.keywords[] | ($i18n[0][.] // .)] else . end) | |
| ' extension.json > "$TMP_EXT" && mv "$TMP_EXT" extension.json | |
| echo "extension.json swapped to zh-Hans" | |
| fi | |
| # 根据项目格式选择构建命令 | |
| if [ "$IS_ESM" = "true" ]; then | |
| ESM_BUILD_SCRIPT=$(jq -r '.scripts["build:esm"] // empty' package.json 2>/dev/null) | |
| if [ -n "$ESM_BUILD_SCRIPT" ]; then | |
| npm run build:esm | |
| else | |
| # 先尝试直接运行(Node 22+ 支持较好) | |
| if ! npm run build; then | |
| echo "⚠️ 直接构建失败,使用 tsx 替代" | |
| npm install -D tsx 2>/dev/null || true | |
| cp package.json package.json.bak | |
| jq '(.scripts.compile // "") |= gsub("ts-node"; "tsx") | (.scripts.build // "") |= gsub("ts-node"; "tsx")' package.json > package.json.tmp && mv package.json.tmp package.json | |
| npm run build | |
| mv package.json.bak package.json | |
| fi | |
| fi | |
| else | |
| npm run build | |
| fi | |
| mv "$BASE_FILE" "$ZH_FILE" | |
| git checkout -- "*.md" 2>/dev/null || true | |
| git checkout -- extension.json 2>/dev/null || true | |
| rm -f README.en.md 2>/dev/null || true | |
| fi | |
| fi | |
| echo "Done: $ZH_FILE" | |
| - name: Build global package | |
| if: env.SKIP_RELEASE == 'false' | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| EEXTNAME=${{ env.EEXTNAME }} | |
| DIST_DIR="build/dist" | |
| BASE_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}.eext" | |
| GLOBAL_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}_global.eext" | |
| echo "Building global package..." | |
| # Detect language (reuse result from zh-cn or re-detect) | |
| CN_COUNT=$(grep -oP '[\x{4e00}-\x{9fff}]' README.md 2>/dev/null | wc -l) | |
| if [ "$CN_COUNT" -gt 10 ]; then | |
| IS_CHINESE=true | |
| else | |
| IS_CHINESE=false | |
| fi | |
| echo "README.md Chinese chars: $CN_COUNT, is_chinese: $IS_CHINESE" | |
| if [ "$IS_CHINESE" = "false" ]; then | |
| # README.md is English, just copy | |
| echo "README.md is English, just copy" | |
| cp "$BASE_FILE" "$GLOBAL_FILE" | |
| else | |
| # README.md is Chinese, find en and swap | |
| EN_README="" | |
| if [ -f "README_EN.md" ]; then | |
| EN_README="README_EN.md" | |
| elif [ -f "README_en.md" ]; then | |
| EN_README="README_en.md" | |
| elif [ -f "README.en.md" ]; then | |
| EN_README="README.en.md" | |
| else | |
| EN_README=$(ls README_EN-*.md README_en-*.md README.en-*.md README.En-*.md 2>/dev/null | head -1) | |
| fi | |
| if [ -z "$EN_README" ]; then | |
| echo "README.md is Chinese, no en found, just copy" | |
| cp "$BASE_FILE" "$GLOBAL_FILE" | |
| else | |
| echo "README.md is Chinese, swap en: $EN_README" | |
| mv README.md README.zh.md | |
| cp "$EN_README" README.md | |
| find . -name "*.md" -not -path "./build/*" -not -path "./node_modules/*" -not -path "./.git/*" -exec sed -i 's/README\.md/README.zh.md/g' {} + | |
| # Swap extension.json fields with en translations | |
| EN_I18N="locales/extensionJson/en.json" | |
| if [ -f "$EN_I18N" ]; then | |
| echo "Swapping extension.json with en translations..." | |
| TMP_EXT=$(mktemp) | |
| jq --slurpfile i18n "$EN_I18N" ' | |
| (if .displayName then .displayName = ($i18n[0].displayName // $i18n[0][.displayName] // .displayName) else . end) | | |
| (if .description then .description = ($i18n[0].description // $i18n[0][.description] // .description) else . end) | | |
| (if .keywords then .keywords = [.keywords[] | ($i18n[0][.] // .)] else . end) | |
| ' extension.json > "$TMP_EXT" && mv "$TMP_EXT" extension.json | |
| echo "extension.json swapped to en" | |
| fi | |
| # 根据项目格式选择构建命令 | |
| if [ "$IS_ESM" = "true" ]; then | |
| ESM_BUILD_SCRIPT=$(jq -r '.scripts["build:esm"] // empty' package.json 2>/dev/null) | |
| if [ -n "$ESM_BUILD_SCRIPT" ]; then | |
| npm run build:esm | |
| else | |
| # 先尝试直接运行(Node 22+ 支持较好) | |
| if ! npm run build; then | |
| echo "⚠️ 直接构建失败,使用 tsx 替代" | |
| npm install -D tsx 2>/dev/null || true | |
| cp package.json package.json.bak | |
| jq '(.scripts.compile // "") |= gsub("ts-node"; "tsx") | (.scripts.build // "") |= gsub("ts-node"; "tsx")' package.json > package.json.tmp && mv package.json.tmp package.json | |
| npm run build | |
| mv package.json.bak package.json | |
| fi | |
| fi | |
| else | |
| npm run build | |
| fi | |
| mv "$BASE_FILE" "$GLOBAL_FILE" | |
| git checkout -- "*.md" 2>/dev/null || true | |
| git checkout -- extension.json 2>/dev/null || true | |
| rm -f README.zh.md 2>/dev/null || true | |
| fi | |
| fi | |
| echo "Done: $GLOBAL_FILE" | |
| - name: Cleanup base eext | |
| if: env.SKIP_RELEASE == 'false' | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| EEXTNAME=${{ env.EEXTNAME }} | |
| rm -f "build/dist/${EEXTNAME}_v${VERSION}.eext" | |
| echo "Removed base eext" | |
| - name: Debug check artifacts | |
| if: env.SKIP_RELEASE == 'false' | |
| run: | | |
| VERSION=${{ env.VERSION }} | |
| EEXTNAME=${{ env.EEXTNAME }} | |
| DIST_DIR="build/dist" | |
| ZH_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}_zh-cn.eext" | |
| GLOBAL_FILE="${DIST_DIR}/${EEXTNAME}_v${VERSION}_global.eext" | |
| echo "🔍 检查构建产物..." | |
| if [ -f "$ZH_FILE" ]; then | |
| echo "✅ 中文版产物存在: $ZH_FILE" | |
| else | |
| echo "❌ 中文版产物不存在: $ZH_FILE" | |
| ls -R build || true | |
| exit 1 | |
| fi | |
| if [ -f "$GLOBAL_FILE" ]; then | |
| echo "✅ 国际版产物存在: $GLOBAL_FILE" | |
| else | |
| echo "❌ 国际版产物不存在: $GLOBAL_FILE" | |
| ls -R build || true | |
| exit 1 | |
| fi | |
| - name: Upload Release | |
| if: env.SKIP_RELEASE == 'false' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ env.VERSION }} | |
| name: Release v${{ env.VERSION }} | |
| body: ${{ env.RELEASE_NOTES }} | |
| files: | | |
| build/dist/${{ env.EEXTNAME }}_v${{ env.VERSION }}_zh-cn.eext | |
| build/dist/${{ env.EEXTNAME }}_v${{ env.VERSION }}_global.eext | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |