From 80d42eb0ba40ae1dda4fe5357685f8f511aecaf0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 6 Feb 2026 17:18:10 -0800 Subject: [PATCH] fix(docker): support .mjs entrypoints in images and e2e --- Dockerfile | 4 +- openclaw.mjs | 21 +++++- scripts/e2e/Dockerfile | 2 +- scripts/e2e/doctor-install-switch-docker.sh | 18 +++-- scripts/e2e/gateway-network-docker.sh | 6 +- scripts/e2e/onboard-docker.sh | 77 ++++++++++++--------- scripts/e2e/plugins-docker.sh | 32 ++++++--- scripts/release-check.ts | 15 ++-- 8 files changed, 116 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8572616fb..237a6a238a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,5 +44,5 @@ USER node # # For container platforms requiring external health checks: # 1. Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD env var -# 2. Override CMD: ["node","dist/index.js","gateway","--allow-unconfigured","--bind","lan"] -CMD ["node", "dist/index.js", "gateway", "--allow-unconfigured"] +# 2. Override CMD: ["node","openclaw.mjs","gateway","--allow-unconfigured","--bind","lan"] +CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"] diff --git a/openclaw.mjs b/openclaw.mjs index 78992f94ab..686593fb22 100755 --- a/openclaw.mjs +++ b/openclaw.mjs @@ -11,4 +11,23 @@ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) { } } -await import("./dist/entry.js"); +const tryImport = async (specifier) => { + try { + await import(specifier); + return true; + } catch (err) { + // Only swallow missing-module errors; rethrow real runtime errors. + if (err && typeof err === "object" && "code" in err && err.code === "ERR_MODULE_NOT_FOUND") { + return false; + } + throw err; + } +}; + +if (await tryImport("./dist/entry.js")) { + // OK +} else if (await tryImport("./dist/entry.mjs")) { + // OK +} else { + throw new Error("openclaw: missing dist/entry.(m)js (build output)."); +} diff --git a/scripts/e2e/Dockerfile b/scripts/e2e/Dockerfile index 225cb8e2af..40c658dfe3 100644 --- a/scripts/e2e/Dockerfile +++ b/scripts/e2e/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning" -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json vitest.config.ts vitest.e2e.config.ts openclaw.mjs ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts openclaw.mjs ./ COPY src ./src COPY test ./test COPY scripts ./scripts diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index a918cb0d92..d5a48c909a 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -80,10 +80,20 @@ LOGINCTL fi npm install -g --prefix /tmp/npm-prefix "/app/$pkg_tgz" - npm_bin="/tmp/npm-prefix/bin/openclaw" - npm_entry="/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.js" - git_entry="/app/dist/index.js" - git_cli="/app/openclaw.mjs" + npm_bin="/tmp/npm-prefix/bin/openclaw" + npm_root="/tmp/npm-prefix/lib/node_modules/openclaw" + if [ -f "$npm_root/dist/index.mjs" ]; then + npm_entry="$npm_root/dist/index.mjs" + else + npm_entry="$npm_root/dist/index.js" + fi + + if [ -f "/app/dist/index.mjs" ]; then + git_entry="/app/dist/index.mjs" + else + git_entry="/app/dist/index.js" + fi + git_cli="/app/openclaw.mjs" assert_entrypoint() { local unit_path="$1" diff --git a/scripts/e2e/gateway-network-docker.sh b/scripts/e2e/gateway-network-docker.sh index a3990e77c5..2757adc153 100644 --- a/scripts/e2e/gateway-network-docker.sh +++ b/scripts/e2e/gateway-network-docker.sh @@ -31,7 +31,7 @@ echo "Starting gateway container..." -e "OPENCLAW_SKIP_CRON=1" \ -e "OPENCLAW_SKIP_CANVAS_HOST=1" \ "$IMAGE_NAME" \ - bash -lc "node dist/index.js gateway --port $PORT --bind lan --allow-unconfigured > /tmp/gateway-net-e2e.log 2>&1" + bash -lc "entry=dist/index.mjs; [ -f \"\$entry\" ] || entry=dist/index.js; node \"\$entry\" gateway --port $PORT --bind lan --allow-unconfigured > /tmp/gateway-net-e2e.log 2>&1" echo "Waiting for gateway to come up..." ready=0 @@ -77,9 +77,9 @@ docker run --rm \ -e "GW_URL=ws://$GW_NAME:$PORT" \ -e "GW_TOKEN=$TOKEN" \ "$IMAGE_NAME" \ - bash -lc "node - <<'NODE' + bash -lc "node --import tsx - <<'NODE' import { WebSocket } from \"ws\"; -import { PROTOCOL_VERSION } from \"./dist/gateway/protocol/index.js\"; +import { PROTOCOL_VERSION } from \"./src/gateway/protocol/index.ts\"; const url = process.env.GW_URL; const token = process.env.GW_TOKEN; diff --git a/scripts/e2e/onboard-docker.sh b/scripts/e2e/onboard-docker.sh index f0f49b736f..5539dfd52c 100755 --- a/scripts/e2e/onboard-docker.sh +++ b/scripts/e2e/onboard-docker.sh @@ -10,9 +10,20 @@ docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" echo "Running onboarding E2E..." docker run --rm -t "$IMAGE_NAME" bash -lc ' set -euo pipefail - trap "" PIPE - export TERM=xterm-256color - ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-daemon --skip-ui" + trap "" PIPE + export TERM=xterm-256color + ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-daemon --skip-ui" + # tsdown may emit dist/index.js or dist/index.mjs depending on runtime/bundler. + if [ -f dist/index.mjs ]; then + OPENCLAW_ENTRY="dist/index.mjs" + elif [ -f dist/index.js ]; then + OPENCLAW_ENTRY="dist/index.js" + else + echo "Missing dist/index.(m)js (build output):" + ls -la dist || true + exit 1 + fi + export OPENCLAW_ENTRY # Provide a minimal trash shim to avoid noisy "missing trash" logs in containers. export PATH="/tmp/openclaw-bin:$PATH" @@ -82,10 +93,10 @@ TRASH done } - start_gateway() { - node dist/index.js gateway --port 18789 --bind loopback --allow-unconfigured > /tmp/gateway-e2e.log 2>&1 & - GATEWAY_PID="$!" - } + start_gateway() { + node "$OPENCLAW_ENTRY" gateway --port 18789 --bind loopback --allow-unconfigured > /tmp/gateway-e2e.log 2>&1 & + GATEWAY_PID="$!" + } wait_for_gateway() { for _ in $(seq 1 20); do @@ -184,9 +195,9 @@ TRASH local send_fn="$3" local validate_fn="${4:-}" - # Default onboarding command wrapper. - run_wizard_cmd "$case_name" "$home_dir" "node dist/index.js onboard $ONBOARD_FLAGS" "$send_fn" true "$validate_fn" - } + # Default onboarding command wrapper. + run_wizard_cmd "$case_name" "$home_dir" "node \"$OPENCLAW_ENTRY\" onboard $ONBOARD_FLAGS" "$send_fn" true "$validate_fn" + } make_home() { mktemp -d "/tmp/openclaw-e2e-$1.XXXXXX" @@ -263,14 +274,14 @@ TRASH send "" 1.0 } - run_case_local_basic() { - local home_dir - home_dir="$(make_home local-basic)" - export HOME="$home_dir" - mkdir -p "$HOME" - node dist/index.js onboard \ - --non-interactive \ - --accept-risk \ + run_case_local_basic() { + local home_dir + home_dir="$(make_home local-basic)" + export HOME="$home_dir" + mkdir -p "$HOME" + node "$OPENCLAW_ENTRY" onboard \ + --non-interactive \ + --accept-risk \ --flow quickstart \ --mode local \ --skip-channels \ @@ -343,11 +354,11 @@ NODE local home_dir home_dir="$(make_home remote-non-interactive)" export HOME="$home_dir" - mkdir -p "$HOME" - # Smoke test non-interactive remote config write. - node dist/index.js onboard --non-interactive --accept-risk \ - --mode remote \ - --remote-url ws://gateway.local:18789 \ + mkdir -p "$HOME" + # Smoke test non-interactive remote config write. + node "$OPENCLAW_ENTRY" onboard --non-interactive --accept-risk \ + --mode remote \ + --remote-url ws://gateway.local:18789 \ --remote-token remote-token \ --skip-skills \ --skip-health @@ -388,7 +399,7 @@ NODE export HOME="$home_dir" mkdir -p "$HOME/.openclaw" # Seed a remote config to exercise reset path. - cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"' + cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"' { "agents": { "defaults": { "workspace": "/root/old" } }, "gateway": { @@ -398,9 +409,9 @@ NODE } JSON - node dist/index.js onboard \ - --non-interactive \ - --accept-risk \ + node "$OPENCLAW_ENTRY" onboard \ + --non-interactive \ + --accept-risk \ --flow quickstart \ --mode local \ --reset \ @@ -438,10 +449,10 @@ NODE } run_case_channels() { - local home_dir - home_dir="$(make_home channels)" - # Channels-only configure flow. - run_wizard_cmd channels "$home_dir" "node dist/index.js configure --section channels" send_channels_flow + local home_dir + home_dir="$(make_home channels)" + # Channels-only configure flow. + run_wizard_cmd channels "$home_dir" "node \"$OPENCLAW_ENTRY\" configure --section channels" send_channels_flow config_path="$HOME/.openclaw/openclaw.json" assert_file "$config_path" @@ -483,7 +494,7 @@ NODE export HOME="$home_dir" mkdir -p "$HOME/.openclaw" # Seed skills config to ensure it survives the wizard. - cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"' + cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"' { "skills": { "allowBundled": ["__none__"], @@ -492,7 +503,7 @@ NODE } JSON - run_wizard_cmd skills "$home_dir" "node dist/index.js configure --section skills" send_skills_flow + run_wizard_cmd skills "$home_dir" "node \"$OPENCLAW_ENTRY\" configure --section skills" send_skills_flow config_path="$HOME/.openclaw/openclaw.json" assert_file "$config_path" diff --git a/scripts/e2e/plugins-docker.sh b/scripts/e2e/plugins-docker.sh index 0cea4c5f9a..f4797b931e 100755 --- a/scripts/e2e/plugins-docker.sh +++ b/scripts/e2e/plugins-docker.sh @@ -8,11 +8,21 @@ echo "Building Docker image..." docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" echo "Running plugins Docker E2E..." -docker run --rm -t "$IMAGE_NAME" bash -lc ' - set -euo pipefail + docker run --rm -t "$IMAGE_NAME" bash -lc ' + set -euo pipefail + if [ -f dist/index.mjs ]; then + OPENCLAW_ENTRY="dist/index.mjs" + elif [ -f dist/index.js ]; then + OPENCLAW_ENTRY="dist/index.js" + else + echo "Missing dist/index.(m)js (build output):" + ls -la dist || true + exit 1 + fi + export OPENCLAW_ENTRY - home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX") - export HOME="$home_dir" + home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX") + export HOME="$home_dir" mkdir -p "$HOME/.openclaw/extensions/demo-plugin" cat > "$HOME/.openclaw/extensions/demo-plugin/index.js" <<'"'"'JS'"'"' @@ -38,7 +48,7 @@ JS } JSON - node dist/index.js plugins list --json > /tmp/plugins.json + node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins.json node - <<'"'"'NODE'"'"' const fs = require("node:fs"); @@ -99,8 +109,8 @@ JS JSON tar -czf /tmp/demo-plugin-tgz.tgz -C "$pack_dir" package - node dist/index.js plugins install /tmp/demo-plugin-tgz.tgz - node dist/index.js plugins list --json > /tmp/plugins2.json + node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz + node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins2.json node - <<'"'"'NODE'"'"' const fs = require("node:fs"); @@ -145,8 +155,8 @@ JS } JSON - node dist/index.js plugins install "$dir_plugin" - node dist/index.js plugins list --json > /tmp/plugins3.json + node "$OPENCLAW_ENTRY" plugins install "$dir_plugin" + node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins3.json node - <<'"'"'NODE'"'"' const fs = require("node:fs"); @@ -192,8 +202,8 @@ JS } JSON - node dist/index.js plugins install "file:$file_pack_dir/package" - node dist/index.js plugins list --json > /tmp/plugins4.json + node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package" + node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins4.json node - <<'"'"'NODE'"'"' const fs = require("node:fs"); diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 531fbbfc75..0555cd66f0 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -7,9 +7,9 @@ import { join, resolve } from "node:path"; type PackFile = { path: string }; type PackResult = { files?: PackFile[] }; -const requiredPaths = [ - "dist/index.js", - "dist/entry.js", +const requiredPathGroups = [ + ["dist/index.js", "dist/index.mjs"], + ["dist/entry.js", "dist/entry.mjs"], "dist/plugin-sdk/index.js", "dist/plugin-sdk/index.d.ts", "dist/build-info.json", @@ -82,7 +82,14 @@ function main() { const files = results.flatMap((entry) => entry.files ?? []); const paths = new Set(files.map((file) => file.path)); - const missing = requiredPaths.filter((path) => !paths.has(path)); + const missing = requiredPathGroups + .flatMap((group) => { + if (Array.isArray(group)) { + return group.some((path) => paths.has(path)) ? [] : [group.join(" or ")]; + } + return paths.has(group) ? [] : [group]; + }) + .toSorted(); const forbidden = [...paths].filter((path) => forbiddenPrefixes.some((prefix) => path.startsWith(prefix)), );