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 +16 -0
- package/bin/cli.mjs +14 -0
- package/lib/inline_sourcemaps.mjs +128 -0
- package/package.json +1 -1
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.
|
|
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": {
|