Skip to content

Commit 4d61f89

Browse files
authored
fix: build api CLI as ESM to support trpc-cli (#926)
## Summary - Build `api.ts` as ESM bundle (`.mjs`) using esbuild - trpc-cli requires ESM with top-level await - Use native `import()` via Function constructor to bypass TypeScript's conversion to `require()` - Add `make mux` target for convenient CLI invocation (e.g., `make mux server --port 3000`) ## Test plan - [x] `make mux -- api --help` works (was broken before) - [x] `make mux -- --help` works - [x] `make mux run -- --help` works - [x] `make mux server -- --help` works - [x] `make lint` passes _Generated with mux_
1 parent 1fc5493 commit 4d61f89

File tree

4 files changed

+45
-6
lines changed

4 files changed

+45
-6
lines changed

Makefile

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ include fmt.mk
5252
.PHONY: docs-server check-docs-links
5353
.PHONY: storybook storybook-build test-storybook chromatic
5454
.PHONY: benchmark-terminal
55-
.PHONY: ensure-deps rebuild-native
55+
.PHONY: ensure-deps rebuild-native mux
5656
.PHONY: check-eager-imports check-bundle-size check-startup
5757

5858
# Build tools
@@ -107,6 +107,17 @@ rebuild-native: node_modules/.installed ## Rebuild native modules (node-pty) for
107107
@npx @electron/rebuild -f -m node_modules/node-pty
108108
@echo "Native modules rebuilt successfully"
109109

110+
# Run compiled CLI with trailing arguments (builds only if missing)
111+
mux: ## Run the compiled mux CLI (e.g., make mux server --port 3000)
112+
@test -f dist/cli/index.js -a -f dist/cli/api.mjs || $(MAKE) build-main
113+
@node dist/cli/index.js $(filter-out $@,$(MAKECMDGOALS))
114+
115+
# Catch unknown targets passed to mux (prevents "No rule to make target" errors)
116+
ifneq ($(filter mux,$(MAKECMDGOALS)),)
117+
%:
118+
@:
119+
endif
120+
110121
## Help
111122
help: ## Show this help message
112123
@echo 'Usage: make [target]'
@@ -117,16 +128,18 @@ help: ## Show this help message
117128
## Development
118129
ifeq ($(OS),Windows_NT)
119130
dev: node_modules/.installed build-main ## Start development server (Vite + nodemon watcher for Windows compatibility)
120-
@echo "Starting dev mode (2 watchers: nodemon for main process, vite for renderer)..."
131+
@echo "Starting dev mode (3 watchers: nodemon for main process, esbuild for api, vite for renderer)..."
121132
# On Windows, use npm run because bunx doesn't correctly pass arguments to concurrently
122133
# https://github.com/oven-sh/bun/issues/18275
123134
@NODE_OPTIONS="--max-old-space-size=4096" npm x concurrently -k --raw \
124135
"bun x nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
136+
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
125137
"vite"
126138
else
127139
dev: node_modules/.installed build-main build-preload ## Start development server (Vite + tsgo watcher for 10x faster type checking)
128140
@bun x concurrently -k \
129141
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
142+
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
130143
"vite"
131144
endif
132145

@@ -140,6 +153,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
140153
@# On Windows, use npm run because bunx doesn't correctly pass arguments
141154
@npmx concurrently -k \
142155
"npmx nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
156+
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
143157
"npmx nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec \"node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
144158
"$(SHELL) -lc \"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite\""
145159
else
@@ -151,6 +165,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
151165
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
152166
@bun x concurrently -k \
153167
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
168+
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
154169
"bun x nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec 'NODE_ENV=development node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
155170
"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite"
156171
endif
@@ -163,13 +178,25 @@ start: node_modules/.installed build-main build-preload build-static ## Build an
163178
## Build targets (can run in parallel)
164179
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons build-static ## Build all targets
165180

166-
build-main: node_modules/.installed dist/cli/index.js ## Build main process
181+
build-main: node_modules/.installed dist/cli/index.js dist/cli/api.mjs ## Build main process
167182

168183
dist/cli/index.js: src/cli/index.ts src/desktop/main.ts src/cli/server.ts src/version.ts tsconfig.main.json tsconfig.json $(TS_SOURCES)
169184
@echo "Building main process..."
170185
@NODE_ENV=production $(TSGO) -p tsconfig.main.json
171186
@NODE_ENV=production bun x tsc-alias -p tsconfig.main.json
172187

188+
# Build API CLI as ESM bundle (trpc-cli requires ESM with top-level await)
189+
dist/cli/api.mjs: src/cli/api.ts src/cli/proxifyOrpc.ts $(TS_SOURCES)
190+
@echo "Building API CLI (ESM)..."
191+
@bun x esbuild src/cli/api.ts \
192+
--bundle \
193+
--format=esm \
194+
--platform=node \
195+
--outfile=dist/cli/api.mjs \
196+
--external:zod \
197+
--external:commander \
198+
--external:@trpc/server
199+
173200
build-preload: node_modules/.installed dist/preload.js ## Build preload script
174201

175202
dist/preload.js: src/desktop/preload.ts $(TS_SOURCES)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
},
197197
"files": [
198198
"dist/**/*.js",
199+
"dist/**/*.mjs",
199200
"dist/**/*.js.map",
200201
"dist/**/*.wasm",
201202
"dist/**/*.html",

scripts/smoke-test.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ fi
108108

109109
log_info "✅ mux binary found"
110110

111+
# Test that mux api subcommand works (requires ESM bundle api.mjs)
112+
log_info "Testing mux api subcommand (ESM bundle)..."
113+
if ! node_modules/.bin/mux api --help >/dev/null 2>&1; then
114+
log_error "mux api --help failed - ESM bundle (api.mjs) may be missing from package"
115+
exit 1
116+
fi
117+
118+
log_info "✅ mux api subcommand works"
119+
111120
# Start the server in background
112121
log_info "Starting mux server on $SERVER_HOST:$SERVER_PORT..."
113122
node_modules/.bin/mux server --host "$SERVER_HOST" --port "$SERVER_PORT" >server.log 2>&1 &

src/cli/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ if (subcommand === "run") {
4040
require("./server");
4141
} else if (subcommand === "api") {
4242
process.argv.splice(2, 1);
43-
// Dynamic import required: trpc-cli is ESM-only and can't be require()'d
44-
// eslint-disable-next-line no-restricted-syntax
45-
void import("./api");
43+
// Must use native import() to load ESM module - trpc-cli requires ESM with top-level await.
44+
// Using Function constructor prevents TypeScript from converting this to require().
45+
// The .mjs extension is critical for Node.js to treat it as ESM.
46+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call
47+
void new Function("return import('./api.mjs')")();
4648
} else if (
4749
subcommand === "desktop" ||
4850
(isElectron && (subcommand === undefined || isElectronLaunchArg))

0 commit comments

Comments
 (0)