evergreen-trace-runtime 0.1.0
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/cli.js +42 -0
- package/evergreen-trace-runtime-0.1.0.tgz +0 -0
- package/package.json +19 -0
- package/register.js +9 -0
- package/src/instrument-cache.js +113 -0
- package/src/register.js +324 -0
- package/src/transform.js +84 -0
package/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawn } = require("node:child_process");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
// eslint-disable-next-line no-console
|
|
9
|
+
console.log(
|
|
10
|
+
"Usage: evergreen-trace run <entry> [-- <args>...]\n" +
|
|
11
|
+
"Example: evergreen-trace run src/server.js -- --port 4321",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function run() {
|
|
16
|
+
const argv = process.argv.slice(2); // drop node + script
|
|
17
|
+
|
|
18
|
+
const [cmd, entry, ...rest] = argv;
|
|
19
|
+
|
|
20
|
+
if (cmd !== "run" || !entry) {
|
|
21
|
+
printUsage();
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entryPath = path.resolve(process.cwd(), entry);
|
|
26
|
+
|
|
27
|
+
const child = spawn(
|
|
28
|
+
process.execPath,
|
|
29
|
+
["--require", "evergreen-trace-runtime/register", entryPath, ...rest],
|
|
30
|
+
{ stdio: "inherit" },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
child.on("exit", (code, signal) => {
|
|
34
|
+
if (signal) {
|
|
35
|
+
process.kill(process.pid, signal);
|
|
36
|
+
} else {
|
|
37
|
+
process.exit(code ?? 0);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
run();
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "evergreen-trace-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Evergreen Trace runtime loader and CLI",
|
|
5
|
+
"main": "register.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"evergreen-trace": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./register": "./register.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@babel/core": "^7.23.0",
|
|
14
|
+
"micromatch": "^4.0.0",
|
|
15
|
+
"evergreen-babel-plugin": "file:../evergreen-babel-plugin",
|
|
16
|
+
"evergreen-sdk": "file:../evergreen-sdk"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
package/register.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Runtime entrypoint for Evergreen Trace loader.
|
|
5
|
+
// In this monorepo, we reuse the root loader implementation.
|
|
6
|
+
// When publishing, this file can be replaced with a compiled copy.
|
|
7
|
+
|
|
8
|
+
module.exports = require("./src/register.js");
|
|
9
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
|
|
7
|
+
function hashContent(content) {
|
|
8
|
+
return crypto.createHash("sha1").update(content, "utf8").digest("hex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hashConfig(config) {
|
|
12
|
+
const minimal = {
|
|
13
|
+
ignore: Array.isArray(config.ignore) ? config.ignore : [],
|
|
14
|
+
include: Array.isArray(config.include) ? config.include : [],
|
|
15
|
+
};
|
|
16
|
+
return crypto
|
|
17
|
+
.createHash("sha1")
|
|
18
|
+
.update(JSON.stringify(minimal), "utf8")
|
|
19
|
+
.digest("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hashKey(logicalKey) {
|
|
23
|
+
// Stable serialization
|
|
24
|
+
const json = JSON.stringify({
|
|
25
|
+
filename: logicalKey.filename,
|
|
26
|
+
contentHash: logicalKey.contentHash,
|
|
27
|
+
configHash: logicalKey.configHash,
|
|
28
|
+
toolVersion: logicalKey.toolVersion,
|
|
29
|
+
});
|
|
30
|
+
return crypto.createHash("sha1").update(json, "utf8").digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDir(dirPath, onWarn) {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(dirPath)) {
|
|
36
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (onWarn) onWarn("[Evergreen Trace] Failed to ensure directory:", err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJsonFile(filePath, onWarn) {
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(filePath)) return null;
|
|
46
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (onWarn) onWarn("[Evergreen Trace] Failed to read cache entry:", err);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeJsonFile(filePath, payload, onWarn) {
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(filePath, JSON.stringify(payload), "utf8");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (onWarn) onWarn("[Evergreen Trace] Failed to write cache entry:", err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {object} args
|
|
64
|
+
* @param {string} args.projectRoot
|
|
65
|
+
* @param {(msg: string, ...args: any[]) => void} [args.onWarn]
|
|
66
|
+
* @returns {{ getOrTransform: (content: string, filename: string, config: any, instrumentSource: Function) => { code: string, map: any, cacheKey: string, hit: boolean, meta: any } }}
|
|
67
|
+
*/
|
|
68
|
+
function createInstrumentCache({ projectRoot, onWarn }) {
|
|
69
|
+
const cacheEntriesDir = path.join(projectRoot, ".evergreen-cache", "entries");
|
|
70
|
+
ensureDir(cacheEntriesDir, onWarn);
|
|
71
|
+
|
|
72
|
+
function getOrTransform(content, filename, config, instrumentSource) {
|
|
73
|
+
const contentHash = hashContent(content);
|
|
74
|
+
const configHash = hashConfig(config);
|
|
75
|
+
const toolVersion = "v1";
|
|
76
|
+
|
|
77
|
+
const meta = { filename, contentHash, configHash, toolVersion };
|
|
78
|
+
const cacheKey = hashKey(meta);
|
|
79
|
+
const entryPath = path.join(cacheEntriesDir, `${cacheKey}.json`);
|
|
80
|
+
|
|
81
|
+
const cached = readJsonFile(entryPath, onWarn);
|
|
82
|
+
if (cached && typeof cached.code === "string") {
|
|
83
|
+
return {
|
|
84
|
+
code: cached.code,
|
|
85
|
+
map: cached.map || null,
|
|
86
|
+
cacheKey,
|
|
87
|
+
hit: true,
|
|
88
|
+
meta: cached.meta || meta,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { code, map } = instrumentSource(content, filename, config);
|
|
93
|
+
|
|
94
|
+
writeJsonFile(
|
|
95
|
+
entryPath,
|
|
96
|
+
{
|
|
97
|
+
code,
|
|
98
|
+
map: map || null,
|
|
99
|
+
meta,
|
|
100
|
+
},
|
|
101
|
+
onWarn,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return { code, map: map || null, cacheKey, hit: false, meta };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { getOrTransform };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
createInstrumentCache,
|
|
112
|
+
};
|
|
113
|
+
|
package/src/register.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// register.js
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const Module = require("module");
|
|
7
|
+
const micromatch = require("micromatch");
|
|
8
|
+
const { instrumentSource } = require("./transform");
|
|
9
|
+
const { createInstrumentCache } = require("./instrument-cache");
|
|
10
|
+
|
|
11
|
+
// -----------------------------------------------------------------------------
|
|
12
|
+
// 1. Project root detection
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// For MVP we treat the CWD as the project root. Later you can walk up to find package.json.
|
|
16
|
+
const PROJECT_ROOT = process.cwd();
|
|
17
|
+
// Cache is managed by src/instrument-cache.js
|
|
18
|
+
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
// 2. Config loading
|
|
21
|
+
// -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shape of the in-memory Evergreen config.
|
|
25
|
+
* ignore/include are arrays of glob strings (not yet compiled).
|
|
26
|
+
*/
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
const defaultConfig = {
|
|
29
|
+
ignore: ["node_modules/**", "dist/**", "coverage/**"],
|
|
30
|
+
include: [], // empty means "everything else"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let fileConfig = {};
|
|
34
|
+
const configPath = path.join(PROJECT_ROOT, ".evergreen-trace.json");
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(configPath)) {
|
|
38
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (parsed && typeof parsed === "object") {
|
|
41
|
+
fileConfig = {
|
|
42
|
+
ignore: Array.isArray(parsed.ignore) ? parsed.ignore : [],
|
|
43
|
+
include: Array.isArray(parsed.include) ? parsed.include : [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Fail-soft: just log and fall back to defaults.
|
|
49
|
+
safeLogWarn("[Evergreen Trace] Failed to load .evergreen-trace.json:", err);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Env overrides (comma-separated lists)
|
|
53
|
+
const envIgnore =
|
|
54
|
+
process.env.EVERGREEN_TRACE_IGNORE &&
|
|
55
|
+
process.env.EVERGREEN_TRACE_IGNORE.split(",")
|
|
56
|
+
.map((s) => s.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
|
|
59
|
+
const envInclude =
|
|
60
|
+
process.env.EVERGREEN_TRACE_INCLUDE &&
|
|
61
|
+
process.env.EVERGREEN_TRACE_INCLUDE.split(",")
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
ignore:
|
|
67
|
+
envIgnore && envIgnore.length
|
|
68
|
+
? envIgnore
|
|
69
|
+
: fileConfig.ignore || defaultConfig.ignore,
|
|
70
|
+
include:
|
|
71
|
+
envInclude && envInclude.length
|
|
72
|
+
? envInclude
|
|
73
|
+
: fileConfig.include || defaultConfig.include,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const CONFIG = loadConfig();
|
|
78
|
+
const DEBUG =
|
|
79
|
+
process.env.EVERGREEN_TRACE_DEBUG === "1" ||
|
|
80
|
+
process.env.EVERGREEN_TRACE_DEBUG === "true";
|
|
81
|
+
|
|
82
|
+
const instrumentCache = createInstrumentCache({
|
|
83
|
+
projectRoot: PROJECT_ROOT,
|
|
84
|
+
onWarn: safeLogWarn,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Pre-compile glob patterns for selective instrumentation (Phase 4).
|
|
88
|
+
const ignorePatterns = CONFIG.ignore || [];
|
|
89
|
+
const includePatterns = CONFIG.include || [];
|
|
90
|
+
const hasInclude = includePatterns.length > 0;
|
|
91
|
+
|
|
92
|
+
function matchesAny(patterns, relPath) {
|
|
93
|
+
return patterns.length > 0 && micromatch.isMatch(relPath, patterns);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -----------------------------------------------------------------------------
|
|
97
|
+
// 3. Logging helpers (fail-soft)
|
|
98
|
+
// -----------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function safeLog(...args) {
|
|
101
|
+
try {
|
|
102
|
+
// eslint-disable-next-line no-console
|
|
103
|
+
console.log(...args);
|
|
104
|
+
} catch (_) {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function safeLogWarn(...args) {
|
|
110
|
+
try {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.warn(...args);
|
|
113
|
+
} catch (_) {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function debugLog(...args) {
|
|
119
|
+
if (!DEBUG) return;
|
|
120
|
+
safeLog("[Evergreen Trace DEBUG]", ...args);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------------
|
|
124
|
+
// 4. Instrumentation decision stub (Phase 1)
|
|
125
|
+
// -----------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Decide whether this absolute filename should be instrumented.
|
|
129
|
+
* Phase 1: only apply basic rules; glob-based include/ignore can be added later.
|
|
130
|
+
*/
|
|
131
|
+
function shouldInstrument(filename) {
|
|
132
|
+
// Skip internal Node modules and non-absolute paths
|
|
133
|
+
if (!path.isAbsolute(filename)) {
|
|
134
|
+
debugLog("Skipping (non-absolute or internal):", filename);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const normalized = filename.split(path.sep).join(path.posix.sep);
|
|
139
|
+
|
|
140
|
+
// Skip anything under node_modules
|
|
141
|
+
if (normalized.includes("/node_modules/")) {
|
|
142
|
+
debugLog("Skipping (node_modules):", filename);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Skip this repo's own extension/runtime code (e.g. src/register.js)
|
|
147
|
+
const srcRoot = path
|
|
148
|
+
.join(PROJECT_ROOT, "packages", "evergreen-trace", "src")
|
|
149
|
+
.split(path.sep)
|
|
150
|
+
.join(path.posix.sep);
|
|
151
|
+
|
|
152
|
+
if (normalized.startsWith(srcRoot)) {
|
|
153
|
+
debugLog("Skipping (extension/runtime src):", filename);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip SDK build output to avoid require cycles
|
|
158
|
+
if (normalized.includes("/packages/evergreen-sdk/dist/")) {
|
|
159
|
+
debugLog("Skipping (SDK build output):", filename);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Skip all tests/fixtures. Node's test runner may execute these under ESM
|
|
164
|
+
// semantics, which can conflict with CJS fixtures (module.exports) after
|
|
165
|
+
// transformation.
|
|
166
|
+
if (normalized.includes("/__tests__/")) {
|
|
167
|
+
debugLog("Skipping (tests/fixtures):", filename);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Only instrument .js and .cjs
|
|
172
|
+
if (!filename.endsWith(".js") && !filename.endsWith(".cjs")) {
|
|
173
|
+
debugLog("Skipping (extension):", filename);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Phase 4: config-based ignore/include using project-root-relative POSIX paths
|
|
178
|
+
const rel = path.relative(PROJECT_ROOT, filename);
|
|
179
|
+
const relPosix = rel.split(path.sep).join("/");
|
|
180
|
+
|
|
181
|
+
// Ignore takes precedence
|
|
182
|
+
if (matchesAny(ignorePatterns, relPosix)) {
|
|
183
|
+
debugLog("Skipping (config ignore):", filename);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If include list exists, only instrument included paths
|
|
188
|
+
if (hasInclude && !matchesAny(includePatterns, relPosix)) {
|
|
189
|
+
debugLog("Skipping (not in include set):", filename);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// -----------------------------------------------------------------------------
|
|
197
|
+
// 5. Transform stub (Phase 1)
|
|
198
|
+
// -----------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
// -----------------------------------------------------------------------------
|
|
201
|
+
// 5. Cache-backed instrumentation (Phase 2)
|
|
202
|
+
// -----------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
function getTransformedFromCacheOrBabel(content, filename, config) {
|
|
205
|
+
const result = instrumentCache.getOrTransform(
|
|
206
|
+
content,
|
|
207
|
+
filename,
|
|
208
|
+
config,
|
|
209
|
+
instrumentSource,
|
|
210
|
+
);
|
|
211
|
+
if (result.hit) debugLog("Cache hit for", filename);
|
|
212
|
+
else debugLog("Cache miss for", filename, "- running Babel transform");
|
|
213
|
+
return { code: result.code, map: result.map || null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stripExistingSourceMappingURL(code) {
|
|
217
|
+
const lines = code.split("\n");
|
|
218
|
+
while (
|
|
219
|
+
lines.length > 0 &&
|
|
220
|
+
/sourceMappingURL=/.test(lines[lines.length - 1].trim())
|
|
221
|
+
) {
|
|
222
|
+
lines.pop();
|
|
223
|
+
}
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Phase 3: attach inline source map if available.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} code
|
|
231
|
+
* @param {string} filename
|
|
232
|
+
* @param {any} maybeMap
|
|
233
|
+
*/
|
|
234
|
+
function attachSourceMapIfPresent(code, filename, maybeMap) {
|
|
235
|
+
if (!maybeMap) {
|
|
236
|
+
return code;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
if (!maybeMap.file) {
|
|
241
|
+
maybeMap.file = filename;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const json = JSON.stringify(maybeMap);
|
|
245
|
+
const base64 = Buffer.from(json, "utf8").toString("base64");
|
|
246
|
+
const comment =
|
|
247
|
+
"//# sourceMappingURL=data:application/json;base64," + base64;
|
|
248
|
+
|
|
249
|
+
const stripped = stripExistingSourceMappingURL(code);
|
|
250
|
+
|
|
251
|
+
return stripped + "\n" + comment + "\n";
|
|
252
|
+
} catch (err) {
|
|
253
|
+
safeLogWarn(
|
|
254
|
+
"[Evergreen Trace] Failed to attach source map for",
|
|
255
|
+
filename,
|
|
256
|
+
err,
|
|
257
|
+
);
|
|
258
|
+
return code;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// -----------------------------------------------------------------------------
|
|
263
|
+
// 6. Capture and wrap Module.prototype._compile
|
|
264
|
+
// -----------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
const originalCompile = Module.prototype._compile;
|
|
267
|
+
|
|
268
|
+
function evergreenCompileHook(content, filename) {
|
|
269
|
+
// Early exit: if anything goes wrong, fall back to original behavior.
|
|
270
|
+
try {
|
|
271
|
+
if (!shouldInstrument(filename)) {
|
|
272
|
+
return originalCompile.call(this, content, filename);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
debugLog("Instrumenting file:", filename);
|
|
276
|
+
|
|
277
|
+
// Phase 2: transform with Babel + Evergreen plugin, with persistent cache.
|
|
278
|
+
const { code, map } = getTransformedFromCacheOrBabel(
|
|
279
|
+
content,
|
|
280
|
+
filename,
|
|
281
|
+
CONFIG,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Phase 2: ignore map until Phase 3 wires in source maps.
|
|
285
|
+
const finalCode = attachSourceMapIfPresent(code, filename, map);
|
|
286
|
+
|
|
287
|
+
return originalCompile.call(this, finalCode, filename);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
safeLogWarn(
|
|
290
|
+
"[Evergreen Trace] Failed during compile hook for",
|
|
291
|
+
filename,
|
|
292
|
+
err,
|
|
293
|
+
);
|
|
294
|
+
// Fail-safe: execute original code so the app still runs.
|
|
295
|
+
return originalCompile.call(this, content, filename);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Install the hook
|
|
300
|
+
Module.prototype._compile = function evergreenCompileWrapper(
|
|
301
|
+
content,
|
|
302
|
+
filename,
|
|
303
|
+
) {
|
|
304
|
+
return evergreenCompileHook.call(this, content, filename);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// -----------------------------------------------------------------------------
|
|
308
|
+
// 7. Startup banner
|
|
309
|
+
// -----------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
safeLog("🌲 Evergreen Trace: Loader active (Phase 1).");
|
|
312
|
+
|
|
313
|
+
if (DEBUG) {
|
|
314
|
+
safeLog("[Evergreen Trace] DEBUG mode enabled.");
|
|
315
|
+
safeLog("[Evergreen Trace] Project root:", PROJECT_ROOT);
|
|
316
|
+
safeLog("[Evergreen Trace] Config:", CONFIG);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Test hooks (used only in automated tests to verify config behavior)
|
|
320
|
+
if (process.env.NODE_ENV === "test") {
|
|
321
|
+
module.exports.__test__ = {
|
|
322
|
+
shouldInstrument,
|
|
323
|
+
};
|
|
324
|
+
}
|
package/src/transform.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { transformSync } = require("@babel/core");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// Try to resolve the Evergreen Babel plugin. In this monorepo it should be
|
|
7
|
+
// available as a local package; in published form it will be a normal
|
|
8
|
+
// dependency.
|
|
9
|
+
let evergreenBabelPlugin;
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
|
+
evergreenBabelPlugin = require("evergreen-babel-plugin");
|
|
13
|
+
// Support both default and named export styles
|
|
14
|
+
if (evergreenBabelPlugin && evergreenBabelPlugin.default) {
|
|
15
|
+
evergreenBabelPlugin = evergreenBabelPlugin.default;
|
|
16
|
+
}
|
|
17
|
+
} catch (err) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"[Evergreen Trace] Failed to load evergreen-babel-plugin. " +
|
|
20
|
+
"Ensure it is installed and built before using the loader.\n" +
|
|
21
|
+
String(err),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Perform on-the-fly instrumentation of a single source file.
|
|
27
|
+
* Returns transformed code and a source map.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} content - Original source code.
|
|
30
|
+
* @param {string} filename - Absolute path to the file on disk.
|
|
31
|
+
* @param {object} config - Evergreen config (include/ignore etc).
|
|
32
|
+
* @returns {{ code: string; map: any }}
|
|
33
|
+
*/
|
|
34
|
+
function instrumentSource(content, filename, config) {
|
|
35
|
+
const result = transformSync(content, {
|
|
36
|
+
filename,
|
|
37
|
+
// Pass config through in case the plugin wants to use it.
|
|
38
|
+
plugins: [[evergreenBabelPlugin, { evergreenConfig: config }]],
|
|
39
|
+
sourceMaps: true,
|
|
40
|
+
ast: false,
|
|
41
|
+
// Allow parsing modern syntax; tweak as needed.
|
|
42
|
+
parserOpts: {
|
|
43
|
+
sourceType: "unambiguous",
|
|
44
|
+
},
|
|
45
|
+
generatorOpts: {
|
|
46
|
+
// Keep output readable; not strictly required.
|
|
47
|
+
compact: false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!result || typeof result.code !== "string") {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[Evergreen Trace] Babel transform returned no code for ${filename}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let map = result.map || null;
|
|
58
|
+
|
|
59
|
+
if (map) {
|
|
60
|
+
// Ensure the map clearly references the transformed file and original source.
|
|
61
|
+
map.file = filename;
|
|
62
|
+
if (!Array.isArray(map.sources) || map.sources.length === 0) {
|
|
63
|
+
map.sources = [filename];
|
|
64
|
+
} else {
|
|
65
|
+
map.sources[0] = filename;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize sources to absolute paths so debuggers and Jump to Code work
|
|
69
|
+
// regardless of how Babel chose to emit them.
|
|
70
|
+
map.sources = map.sources.map((s) =>
|
|
71
|
+
path.isAbsolute(s) ? s : path.resolve(path.dirname(filename), s),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
code: result.code,
|
|
77
|
+
map,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
instrumentSource,
|
|
83
|
+
};
|
|
84
|
+
|