Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ jobs:
run: |
node --check bin/cli.mjs
node --check lib/debug_server.mjs
node --check lib/inline_sourcemaps.mjs
node --check lib/resolve_launch.mjs

- name: Unit tests
run: npm test

- name: Verify tag matches package.json version
run: |
Expand Down
52 changes: 39 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ container URL** — so you open one link in your own browser and debug. No wstun
no rebuild of your source, and **no browser in the Actor**.

```dockerfile
# Get the package
RUN npm install actor-debugger
# Get the package (globally, so the Actor's own node_modules and lockfile are untouched)
RUN npm install --global actor-debugger

# pause on the first line until a debugger attaches (for short-lived Actors):
CMD ["npx", "actor-debugger", "--brk"]
# Recommended: wrap the image's existing CMD. Docker appends it to the ENTRYPOINT, so your real
# start command is used as-is, and an ENTRYPOINT in your own Dockerfile can't swallow it.
# `--brk` pauses on the first line until a debugger attaches (for short-lived Actors).
ENTRYPOINT ["actor-debugger", "--brk"]

# or point at a specific entry:
# Or replace the CMD instead - the entrypoint is then auto-detected, or given explicitly:
CMD ["npx", "actor-debugger", "--brk"]
CMD ["npx", "actor-debugger", "dist/main.js"]
```

Expand All @@ -36,10 +39,10 @@ chii's build is the same frontend, ready to serve.

## Activation

Running the Actor through `npx actor-debugger` **is** the switch — debugging is on whenever the
`CMD` line above is in place. To turn it off, revert the `CMD` to the Actor's normal entrypoint
(e.g. `CMD ["npm", "start"]`) and rebuild. Pass `--brk` to pause on the first line until a
debugger attaches — useful for Actors that would otherwise finish before you connect.
Running the Actor through `actor-debugger` **is** the switch — debugging is on whenever the
`ENTRYPOINT` (or `CMD`) line above is in place. To turn it off, drop that line and rebuild. Pass
`--brk` to pause on the first line until a debugger attaches — useful for Actors that would
otherwise finish before you connect.

## Connect

Expand Down Expand Up @@ -76,10 +79,33 @@ your `src/` into the final stage to fix it.

## Entrypoint detection order

1. An explicit path argument, if given.
2. The file in `package.json` `scripts.start` (e.g. `node dist/main.js` → `dist/main.js`).
3. `package.json` `main`.
4. Conventional paths: `dist/main.js`, `dist/index.js`, `build/main.js`, `src/main.js`, `main.js`, `index.js`.
Anything after `--brk` is treated as the Actor's start command. As `ENTRYPOINT`, that is the image's
own `CMD`, appended by Docker — so the Actor starts the way it normally does, with its Node flags and
arguments intact, instead of being guessed at.

Understood start commands:

| Passed-through command | Resolves to |
| --- | --- |
| `node [flags] <script> [args]` | that script, keeping flags (incl. `-r x`/`--import x`) and args |
| `npm start`, `npm run <s>`, `pnpm …`, `yarn …` | the matching `package.json` script, parsed again |
| `sh -c "<command>"` | the inner command, parsed again |
| `dist/main.js` | that file directly (the explicit-path form) |

Chained scripts are handled by trying the segments from the last back, so `npm run build && node
dist/main.js` debugs `dist/main.js` rather than the build step.

With **no** command passed (the plain `CMD ["npx", "actor-debugger"]` form), the entrypoint is
auto-detected instead:

1. The file in `package.json` `scripts.start` (e.g. `node dist/main.js` → `dist/main.js`).
2. `package.json` `main`.
3. Conventional paths: `dist/main.js`, `dist/index.js`, `build/main.js`, `src/main.js`, `main.js`, `index.js`.

If a command is passed but no Node.js entrypoint can be derived from it (e.g. a `tsx`/`ts-node`
launcher), the debugger **fails fast** with the reason rather than starting the Actor without a
debugger attached — a run you asked to debug should not silently run undebuggable. Pass the
entrypoint explicitly in that case: `ENTRYPOINT ["actor-debugger", "--brk", "dist/main.js"]`.

## Security

Expand Down
54 changes: 27 additions & 27 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
* actor-debugger - launch an Apify Node/TS Actor under the Node inspector, reachable over the
* run's container URL, with a one-line Dockerfile change:
*
* ENTRYPOINT ["actor-debugger", "--brk"] # recommended: keeps the image's own CMD
* CMD ["npx", "actor-debugger"] # auto-detect the Actor's entrypoint
* CMD ["npx", "actor-debugger", "dist/x.js"] # explicit entrypoint
* CMD ["npx", "actor-debugger", "--brk"] # pause on the first line until attached
*
* Running through this command is what enables debugging - revert the CMD to the normal
* entrypoint to turn it off.
* As ENTRYPOINT, Docker appends the image's original CMD to our arguments, so the Actor's real
* start command (`npm start`, `node dist/main.js`, ...) is honoured instead of guessed, and an
* ENTRYPOINT cannot be silently swallowed by one already defined in the Actor's Dockerfile.
*
* Running through this command is what enables debugging - revert the ENTRYPOINT/CMD to the
* normal entrypoint to turn it off.
*/
import { spawn } from 'node:child_process';
import fs from 'node:fs';
Expand All @@ -18,9 +23,12 @@ import path from 'node:path';

import { startDebugServer } from '../lib/debug_server.mjs';
import { inlineSourceMaps } from '../lib/inline_sourcemaps.mjs';
import { resolveLaunch } from '../lib/resolve_launch.mjs';

const INSPECTOR_PORT = 9229;
const TAG = '[actor-debugger]';
/** Our own flag; everything else on the command line belongs to the Actor. */
const BRK_FLAG = '--brk';

/** Locate chii's prebuilt Chrome DevTools frontend (chrome-devtools-frontend npm ships unbuilt source). */
function findFrontendDir() {
Expand All @@ -39,24 +47,6 @@ function findFrontendDir() {
return null;
}

function resolveEntry(argEntry) {
if (argEntry) return path.resolve(argEntry);
const candidates = [];
try {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const startMatch = pkg.scripts?.start && /node\s+(?:--\S+\s+)*(\S+)/.exec(pkg.scripts.start);
if (startMatch) candidates.push(startMatch[1]);
if (pkg.main) candidates.push(pkg.main);
} catch {
// no/invalid package.json - fall through to conventional paths
}
candidates.push('dist/main.js', 'dist/index.js', 'build/main.js', 'src/main.js', 'main.js', 'index.js');
for (const candidate of candidates) {
if (candidate && fs.existsSync(candidate)) return path.resolve(candidate);
}
return null;
}

function fetchInspectorUuid(retries = 40) {
return new Promise((resolve) => {
const attempt = (left) => {
Expand Down Expand Up @@ -108,13 +98,20 @@ async function announce(webServerUrl, hasFrontend) {
}

const args = process.argv.slice(2);
const brk = args.includes('--brk');
const entry = resolveEntry(args.find((a) => !a.startsWith('--')));
if (!entry) {
console.error(`${TAG} could not find an Actor entrypoint. Pass one explicitly:`);
console.error(`${TAG} CMD ["npx", "actor-debugger", "dist/main.js"]`);
const brk = args.includes(BRK_FLAG);
// Everything except our own flag is either an explicit entrypoint or - when we run as the image
// ENTRYPOINT - the Actor's original CMD, appended by Docker.
const command = args.filter((arg) => arg !== BRK_FLAG);

const launch = resolveLaunch({ command });
if (!launch.entry) {
console.error(`${TAG} ${launch.reason}.`);
console.error(`${TAG} pass the Actor's entrypoint explicitly, e.g.:`);
console.error(`${TAG} ENTRYPOINT ["actor-debugger", "--brk", "dist/main.js"]`);
process.exit(1);
}
const { entry, nodeFlags, scriptArgs, source } = launch;
console.error(`${TAG} starting the Actor from ${source}.`);

// A remote DevTools frontend cannot fetch file:// URLs, so external .map files (and the .ts
// sources they reference) are unreachable to it. Inline them into the compiled files up front.
Expand All @@ -130,9 +127,12 @@ if (maps.inlined > 0) {
}

const inspectFlag = brk ? '--inspect-brk' : '--inspect';
const nodeArgs = ['--enable-source-maps', `${inspectFlag}=127.0.0.1:${INSPECTOR_PORT}`];
const nodeArgs = ['--enable-source-maps', `${inspectFlag}=127.0.0.1:${INSPECTOR_PORT}`, ...nodeFlags];

const child = spawn(process.execPath, [...nodeArgs, entry], { stdio: 'inherit', env: process.env });
const child = spawn(process.execPath, [...nodeArgs, entry, ...scriptArgs], {
stdio: 'inherit',
env: process.env,
});

let server;
const { ACTOR_WEB_SERVER_PORT: port, ACTOR_WEB_SERVER_URL: url } = process.env;
Expand Down
Loading
Loading