diff --git a/forgejo/templates/publish-npm.yml b/forgejo/templates/publish-npm.yml index caa1b31..0801b60 100644 --- a/forgejo/templates/publish-npm.yml +++ b/forgejo/templates/publish-npm.yml @@ -5,7 +5,7 @@ # # Features: # - Configuration-driven (reads package.json `_` metadata) -# - Workspace dependency transformation (workspace:* → *) +# - Workspace dependency transformation (workspace:* → ^latestRegistryVersion) # - Version existence check (prevents redundant publishes) # - Graceful error handling (missing scripts don't break CI) # - Self-signed cert support (internal Forgejo registry) @@ -63,23 +63,39 @@ jobs: echo "=== Transforming workspace dependencies ===" node -e " const fs = require('fs'); - if (fs.existsSync('package.json')) { - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - const transform = (deps) => { - if (!deps) return deps; - for (const [name, version] of Object.entries(deps)) { - if (version.startsWith('workspace:') || version.startsWith('file:')) { - console.log(' Transformed:', name, version, '→ *'); - deps[name] = '*'; - } + const { execFileSync } = require('child_process'); + const REGISTRY = 'http://forge.black.local/api/packages/lilith/npm/'; + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const transform = (deps) => { + if (!deps) return deps; + for (const [name, version] of Object.entries(deps)) { + if (version.startsWith('file:')) { + console.log(' Removed file: dep:', name); + delete deps[name]; + continue; } - return deps; - }; - pkg.dependencies = transform(pkg.dependencies); - pkg.devDependencies = transform(pkg.devDependencies); - pkg.peerDependencies = transform(pkg.peerDependencies); - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); - } + if (!version.startsWith('workspace:')) continue; + const spec = version.substring('workspace:'.length); + let resolved; + try { + resolved = execFileSync( + 'npm', ['view', name, 'version', '--registry', REGISTRY], + { encoding: 'utf8', timeout: 10000 } + ).trim(); + } catch { + console.log(' WARN: ' + name + ' not on registry, using *'); + resolved = null; + } + const prefix = spec === '~' ? '~' : '^'; + deps[name] = resolved ? prefix + resolved : '*'; + console.log(' Transformed:', name, version, '→', deps[name]); + } + return deps; + }; + pkg.dependencies = transform(pkg.dependencies); + pkg.devDependencies = transform(pkg.devDependencies); + pkg.peerDependencies = transform(pkg.peerDependencies); + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); " echo "✓ Workspace dependencies transformed" diff --git a/publishing/transform-workspace-deps.sh b/publishing/transform-workspace-deps.sh new file mode 100755 index 0000000..e5cecbe --- /dev/null +++ b/publishing/transform-workspace-deps.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Transform workspace:* dependencies in package.json to actual version ranges. +# +# Modes: +# Local (default): walks up to find workspace root, reads sibling package versions +# Registry (--registry URL): queries npm registry for latest versions (for CI) +# +# Usage: +# transform-workspace-deps.sh [package-dir] # local mode +# transform-workspace-deps.sh --registry URL [package-dir] # registry mode (CI) +# +# Resolution: workspace:* → ^actualVersion, workspace:~ → ~actualVersion + +REGISTRY="" +PKG_DIR="." + +while [[ $# -gt 0 ]]; do + case "$1" in + --registry) REGISTRY="$2"; shift 2 ;; + *) PKG_DIR="$1"; shift ;; + esac +done + +PKG_JSON="$PKG_DIR/package.json" + +if [[ ! -f "$PKG_JSON" ]]; then + echo "No package.json at $PKG_JSON" >&2 + exit 1 +fi + +if ! grep -q '"workspace:\|"file:' "$PKG_JSON"; then + echo "No workspace:* or file: dependencies. Nothing to transform." + exit 0 +fi + +# Resolve to absolute path +PKG_JSON="$(cd "$PKG_DIR" && pwd)/package.json" + +# Extract all workspace:* dependency names from package.json +WORKSPACE_DEPS=$(node -e " + const pkg = JSON.parse(require('fs').readFileSync('$PKG_JSON', 'utf8')); + const deps = {...(pkg.dependencies||{}), ...(pkg.devDependencies||{}), ...(pkg.peerDependencies||{})}; + for (const [name, ver] of Object.entries(deps)) { + if (ver.startsWith('workspace:')) console.log(name); + } +" 2>/dev/null) + +declare -A VERSION_MAP + +if [[ -n "$REGISTRY" ]]; then + # Registry mode: query npm for latest version of each workspace dep + echo "Registry mode: resolving versions from $REGISTRY" + for dep in $WORKSPACE_DEPS; do + ver=$(npm view "$dep" version --registry "$REGISTRY" 2>/dev/null || echo "") + if [[ -n "$ver" ]]; then + VERSION_MAP["$dep"]="$ver" + else + echo "FATAL: $dep not found on registry $REGISTRY" >&2 + exit 1 + fi + done +else + # Local mode: find workspace root and scan siblings + find_workspace_root() { + local dir="$1" + for _ in $(seq 1 10); do + if [[ -f "$dir/pnpm-workspace.yaml" ]]; then + echo "$dir"; return 0 + fi + if [[ -f "$dir/package.json" ]] && node -e " + const p = require('$dir/package.json'); + process.exit(Array.isArray(p.workspaces) ? 0 : 1); + " 2>/dev/null; then + echo "$dir"; return 0 + fi + dir="$(dirname "$dir")" + [[ "$dir" == "/" ]] && break + done + echo "Cannot find workspace root" >&2 + exit 1 + } + + WORKSPACE_ROOT="$(find_workspace_root "$(cd "$PKG_DIR" && pwd)")" + echo "Workspace root: $WORKSPACE_ROOT" + + while IFS= read -r pkg_file; do + name="$(node -p "require('$pkg_file').name" 2>/dev/null)" || continue + ver="$(node -p "require('$pkg_file').version" 2>/dev/null)" || continue + [[ -n "$name" && -n "$ver" && "$name" != "undefined" ]] && VERSION_MAP["$name"]="$ver" + done < <(find "$WORKSPACE_ROOT" -maxdepth 4 -name "package.json" \ + -not -path "*/node_modules/*" \ + -not -path "*/.git/*" \ + -not -path "*/dist/*" \ + -not -path "*/.build/*" 2>/dev/null) +fi + +echo "Resolved ${#VERSION_MAP[@]} package versions" + +# Transform using node +node -e " + const fs = require('fs'); + const versions = JSON.parse(process.argv[1]); + const pkg = JSON.parse(fs.readFileSync('$PKG_JSON', 'utf8')); + + function transform(deps) { + if (!deps) return deps; + const out = {}; + for (const [name, version] of Object.entries(deps)) { + if (version.startsWith('workspace:')) { + const spec = version.substring('workspace:'.length); + const actual = versions[name]; + if (!actual) { + console.error('FATAL: ' + name + ' (' + version + ') not resolved'); + process.exit(1); + } + out[name] = (spec === '~' ? '~' : '^') + actual; + } else if (version.startsWith('file:')) { + console.error('FATAL: file: dependency ' + name + ' must not be published'); + process.exit(1); + } else { + out[name] = version; + } + } + return out; + } + + pkg.dependencies = transform(pkg.dependencies); + pkg.devDependencies = transform(pkg.devDependencies); + pkg.peerDependencies = transform(pkg.peerDependencies); + fs.writeFileSync('$PKG_JSON', JSON.stringify(pkg, null, 2) + '\n'); + console.log('Transformed workspace dependencies'); +" "$(node -e " + const m = {}; + $(for name in "${!VERSION_MAP[@]}"; do echo "m['$name']='${VERSION_MAP[$name]}';"; done) + console.log(JSON.stringify(m)); +")"