actor-debugger 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,6 +54,22 @@ in your sources (via source maps), step, inspect — no `devtools://` URL, no lo
54
54
  Prefer the raw channel? `npx wscat -c "wss://<run>.runs.apify.net/<uuid>"`, or point
55
55
  `Playwright/Puppeteer connectOverCDP` at that wss URL.
56
56
 
57
+ ## TypeScript sources (automatic)
58
+
59
+ A remote DevTools frontend can never fetch `file://` URLs from the container, so external
60
+ `.js.map` files — the standard `"sourceMap": true` tsc output — are unreachable to it, and
61
+ DevTools would fall back to the generated JS ("Source map failed to load"). The debugger fixes
62
+ this itself at startup: it scans the compiled output, reads each external `.map` from the
63
+ container's disk, embeds the original TS text into it (`sourcesContent`, read from the `.ts`
64
+ files in the image), and rewrites the reference into an inline `data:` URL. Any tsc setup that
65
+ emits source maps at all (`sourceMap` or `inlineSourceMap`) therefore just works — no tsconfig
66
+ changes needed.
67
+
68
+ The one unrecoverable case is a build with no source maps: then the run log prints a hint to
69
+ compile with `"sourceMap": true`. If the `.ts` files aren't in the image (a multi-stage build
70
+ copying only `dist/`), mappings still inline but sources can't be shown — the log says so; `COPY`
71
+ your `src/` into the final stage to fix it.
72
+
57
73
  ## Entrypoint detection order
58
74
 
59
75
  1. An explicit path argument, if given.
package/bin/cli.mjs CHANGED
@@ -17,6 +17,7 @@ import { createRequire } from 'node:module';
17
17
  import path from 'node:path';
18
18
 
19
19
  import { startDebugServer } from '../lib/debug_server.mjs';
20
+ import { inlineSourceMaps } from '../lib/inline_sourcemaps.mjs';
20
21
 
21
22
  const INSPECTOR_PORT = 9229;
22
23
  const TAG = '[actor-debugger]';
@@ -110,6 +111,19 @@ if (!entry) {
110
111
  process.exit(1);
111
112
  }
112
113
 
114
+ // A remote DevTools frontend cannot fetch file:// URLs, so external .map files (and the .ts
115
+ // sources they reference) are unreachable to it. Inline them into the compiled files up front.
116
+ const mapRoot = path.resolve(path.dirname(entry)).startsWith(process.cwd()) ? process.cwd() : path.dirname(entry);
117
+ const maps = inlineSourceMaps(mapRoot, (msg) => console.error(`${TAG} ${msg}`));
118
+ if (maps.inlined > 0) {
119
+ console.error(`${TAG} inlined ${maps.inlined} source map(s) so DevTools can show original sources.`);
120
+ if (maps.missingSources > 0) {
121
+ console.error(`${TAG} ${maps.missingSources} original source file(s) not in the image - those stay JS-only. COPY your src/ into the image to fix.`);
122
+ }
123
+ } else if (maps.alreadyInline === 0) {
124
+ console.error(`${TAG} no source maps found - TS Actors: compile with "sourceMap": true to debug original sources.`);
125
+ }
126
+
113
127
  const inspectFlag = brk ? '--inspect-brk' : '--inspect';
114
128
  const nodeArgs = ['--enable-source-maps', `${inspectFlag}=127.0.0.1:${INSPECTOR_PORT}`];
115
129
 
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.actor', 'apify_storage', 'storage']);
5
+ const JS_EXT = new Set(['.js', '.mjs', '.cjs']);
6
+ const MAP_RE = /(\/\/[#@][ \t]*sourceMappingURL=)([^\s]+)/g;
7
+ const MAX_FILES = 5000;
8
+
9
+ function lastMapRef(source) {
10
+ let match = null;
11
+ for (const m of source.matchAll(MAP_RE)) match = m;
12
+ return match;
13
+ }
14
+
15
+ function decodeDataUrl(url) {
16
+ const m = /^data:application\/json[^,]*;base64,(.*)$/.exec(url);
17
+ if (!m) return null;
18
+ try {
19
+ return JSON.parse(Buffer.from(m[1], 'base64').toString('utf8'));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /** Fill in map.sourcesContent from the .ts/.js sources on disk where missing. */
26
+ function embedSources(map, mapDir) {
27
+ const sources = map.sources ?? [];
28
+ const content = map.sourcesContent ?? new Array(sources.length).fill(null);
29
+ let missing = 0;
30
+ for (let i = 0; i < sources.length; i++) {
31
+ if (content[i] != null) continue;
32
+ const src = sources[i];
33
+ if (!src || /^[a-z+]+:/i.test(src)) {
34
+ missing++;
35
+ continue;
36
+ }
37
+ const file = path.resolve(mapDir, map.sourceRoot ?? '', src);
38
+ try {
39
+ content[i] = fs.readFileSync(file, 'utf8');
40
+ } catch {
41
+ missing++;
42
+ }
43
+ }
44
+ map.sourcesContent = content;
45
+ return missing;
46
+ }
47
+
48
+ /**
49
+ * Make every compiled file's source map usable by a REMOTE DevTools frontend. The browser serving
50
+ * our DevTools UI can never fetch `file://` URLs from the container, so external `.map` files (and
51
+ * the original .ts sources they point at) are unreachable to it - only what is embedded in the
52
+ * script itself gets through the inspector. This walks `rootDir`, and for each .js/.mjs/.cjs file:
53
+ * - resolves an external sourceMappingURL against the file, reading the .map from disk;
54
+ * - embeds the original sources into the map (`sourcesContent`) from disk where missing;
55
+ * - rewrites the sourceMappingURL comment to an inline base64 `data:` URL.
56
+ * Already-inline maps just get missing `sourcesContent` filled in. Files without a map reference
57
+ * are left untouched.
58
+ *
59
+ * @returns {{ inlined: number, alreadyInline: number, scanned: number, missingSources: number }}
60
+ */
61
+ export function inlineSourceMaps(rootDir, log = () => {}) {
62
+ const stats = { inlined: 0, alreadyInline: 0, scanned: 0, missingSources: 0 };
63
+ const walk = (dir) => {
64
+ let entries;
65
+ try {
66
+ entries = fs.readdirSync(dir, { withFileTypes: true });
67
+ } catch {
68
+ return;
69
+ }
70
+ for (const entry of entries) {
71
+ if (stats.scanned >= MAX_FILES) return;
72
+ const full = path.join(dir, entry.name);
73
+ if (entry.isDirectory()) {
74
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) walk(full);
75
+ continue;
76
+ }
77
+ if (!entry.isFile() || !JS_EXT.has(path.extname(entry.name))) continue;
78
+ stats.scanned++;
79
+ processFile(full, stats, log);
80
+ }
81
+ };
82
+ walk(rootDir);
83
+ return stats;
84
+ }
85
+
86
+ function processFile(file, stats, log) {
87
+ let source;
88
+ try {
89
+ source = fs.readFileSync(file, 'utf8');
90
+ } catch {
91
+ return;
92
+ }
93
+ const ref = lastMapRef(source);
94
+ if (!ref) return;
95
+ const url = ref[2];
96
+
97
+ let map;
98
+ let alreadyInline = false;
99
+ if (url.startsWith('data:')) {
100
+ map = decodeDataUrl(url);
101
+ alreadyInline = true;
102
+ // Inline map already carrying all sources - nothing for us to do.
103
+ if (map && (map.sourcesContent ?? []).length >= (map.sources ?? []).length
104
+ && (map.sourcesContent ?? []).every((c) => c != null)) {
105
+ stats.alreadyInline++;
106
+ return;
107
+ }
108
+ } else if (!/^[a-z+]+:/i.test(url)) {
109
+ try {
110
+ map = JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), url), 'utf8'));
111
+ } catch {
112
+ log(`map file for ${file} not readable (${url}) - skipped`);
113
+ return;
114
+ }
115
+ }
116
+ if (!map) return;
117
+
118
+ stats.missingSources += embedSources(map, path.dirname(file));
119
+ const dataUrl = `data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString('base64')}`;
120
+ const start = ref.index + ref[1].length;
121
+ const patched = source.slice(0, start) + dataUrl + source.slice(start + url.length);
122
+ try {
123
+ fs.writeFileSync(file, patched);
124
+ stats.inlined++;
125
+ } catch {
126
+ log(`could not rewrite ${file} - skipped${alreadyInline ? '' : ' (map stays external)'}`);
127
+ }
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actor-debugger",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Drop-in remote debugger for Apify Node/TS Actors: one Dockerfile CMD line launches your Actor under the Node inspector, reachable over the run's container URL. No local setup, no rebuild of your own code.",
5
5
  "type": "module",
6
6
  "bin": {