ushman-characterize 0.4.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/AGENTS.md +110 -0
- package/CHANGELOG.md +41 -0
- package/LICENSE.md +21 -0
- package/README.md +193 -0
- package/bin/ushman-characterize +19 -0
- package/dist/babel-config.d.ts +7 -0
- package/dist/babel-config.d.ts.map +1 -0
- package/dist/babel-config.js +17 -0
- package/dist/capture-server.d.ts +31 -0
- package/dist/capture-server.d.ts.map +1 -0
- package/dist/capture-server.js +199 -0
- package/dist/capture.d.ts +97 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +620 -0
- package/dist/cli/logger.d.ts +7 -0
- package/dist/cli/logger.d.ts.map +1 -0
- package/dist/cli/logger.js +14 -0
- package/dist/cli/parse-flags.d.ts +8 -0
- package/dist/cli/parse-flags.d.ts.map +1 -0
- package/dist/cli/parse-flags.js +60 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +439 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/dedupe-contract.d.ts +26 -0
- package/dist/dedupe-contract.d.ts.map +1 -0
- package/dist/dedupe-contract.js +12 -0
- package/dist/default-export.d.ts +6 -0
- package/dist/default-export.d.ts.map +1 -0
- package/dist/default-export.js +52 -0
- package/dist/format-contract.d.ts +25 -0
- package/dist/format-contract.d.ts.map +1 -0
- package/dist/format-contract.js +96 -0
- package/dist/function-utils.d.ts +6 -0
- package/dist/function-utils.d.ts.map +1 -0
- package/dist/function-utils.js +22 -0
- package/dist/generate-replay.d.ts +18 -0
- package/dist/generate-replay.d.ts.map +1 -0
- package/dist/generate-replay.js +158 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/instrument.d.ts +39 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +605 -0
- package/dist/ledger.d.ts +19 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +50 -0
- package/dist/puppeteer-harness.d.ts +74 -0
- package/dist/puppeteer-harness.d.ts.map +1 -0
- package/dist/puppeteer-harness.js +248 -0
- package/dist/purity-classifier.d.ts +28 -0
- package/dist/purity-classifier.d.ts.map +1 -0
- package/dist/purity-classifier.js +363 -0
- package/dist/rebind.d.ts +26 -0
- package/dist/rebind.d.ts.map +1 -0
- package/dist/rebind.js +356 -0
- package/dist/replay-report.d.ts +18 -0
- package/dist/replay-report.d.ts.map +1 -0
- package/dist/replay-report.js +12 -0
- package/dist/scene.d.ts +24 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +235 -0
- package/dist/schema-types.d.ts +40 -0
- package/dist/schema-types.d.ts.map +1 -0
- package/dist/schema-types.js +32 -0
- package/dist/seed-scaffolds.d.ts +31 -0
- package/dist/seed-scaffolds.d.ts.map +1 -0
- package/dist/seed-scaffolds.js +96 -0
- package/dist/shared.d.ts +36 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +390 -0
- package/dist/state-dag.d.ts +5 -0
- package/dist/state-dag.d.ts.map +1 -0
- package/dist/state-dag.js +27 -0
- package/dist/stub-pure.d.ts +57 -0
- package/dist/stub-pure.d.ts.map +1 -0
- package/dist/stub-pure.js +987 -0
- package/dist/time.d.ts +3 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +10 -0
- package/dist/trace-format.d.ts +24 -0
- package/dist/trace-format.d.ts.map +1 -0
- package/dist/trace-format.js +213 -0
- package/dist/trace-serializer.d.ts +94 -0
- package/dist/trace-serializer.d.ts.map +1 -0
- package/dist/trace-serializer.js +607 -0
- package/dist/tracer-runtime.d.ts +25 -0
- package/dist/tracer-runtime.d.ts.map +1 -0
- package/dist/tracer-runtime.js +291 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/workspace-paths.d.ts +64 -0
- package/dist/workspace-paths.d.ts.map +1 -0
- package/dist/workspace-paths.js +288 -0
- package/package.json +86 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import generate from '@babel/generator';
|
|
3
|
+
import traverse, {} from '@babel/traverse';
|
|
4
|
+
import * as t from '@babel/types';
|
|
5
|
+
import { parseModuleAst } from "./babel-config.js";
|
|
6
|
+
import { TRACE_RUNTIME_VERSION } from "./constants.js";
|
|
7
|
+
import { getAnonymousDefaultExportBindingName } from "./default-export.js";
|
|
8
|
+
import { isTrivialGetterLike } from "./function-utils.js";
|
|
9
|
+
const MARKER = 'ushman-characterize:instrumented';
|
|
10
|
+
const GENERATED_MARKER = '__ushmanCharacterizeGenerated';
|
|
11
|
+
const SOURCE_MAP_COMMENT_PREFIXES = ['# sourceMappingURL=', '@ sourceMappingURL='];
|
|
12
|
+
const TRACE_AWAIT_HELPER_NAME = '__ushmanTraceAwait';
|
|
13
|
+
const assertSourceMapMode = (mode) => {
|
|
14
|
+
if (mode === 'external' || mode === 'inline' || mode === 'off') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Unsupported sourceMapMode "${mode}". Use "external", "inline", or "off".`);
|
|
18
|
+
};
|
|
19
|
+
const helperTemplates = parseModuleAst({
|
|
20
|
+
source: `
|
|
21
|
+
async function ${TRACE_AWAIT_HELPER_NAME}(callId, value) {
|
|
22
|
+
// Native await suspends outside our wrapper body, so we temporarily leave the
|
|
23
|
+
// sync stack and restore the same call id when the awaited work resumes.
|
|
24
|
+
const tracer = globalThis.__USHMAN_TRACER__;
|
|
25
|
+
tracer?.leaveSyncFrame?.(callId);
|
|
26
|
+
try {
|
|
27
|
+
return await value;
|
|
28
|
+
} finally {
|
|
29
|
+
tracer?.enterAsyncFrame?.(callId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function __ushmanTraceInvoke(meta, args, thisArg, fn, passCallId) {
|
|
34
|
+
const tracer = globalThis.__USHMAN_TRACER__;
|
|
35
|
+
if (!tracer) {
|
|
36
|
+
return fn.apply(thisArg, args);
|
|
37
|
+
}
|
|
38
|
+
const callId = tracer.enter(meta, args, thisArg);
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
if (passCallId) {
|
|
42
|
+
tracer.pushInvocationCallId?.(callId);
|
|
43
|
+
}
|
|
44
|
+
result = fn.apply(thisArg, args);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
tracer.exit(callId, undefined, error);
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
if (passCallId) {
|
|
50
|
+
tracer.popInvocationCallId?.(callId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (result && typeof result.then === 'function') {
|
|
54
|
+
// Async wrappers hand control to ${TRACE_AWAIT_HELPER_NAME} at each await,
|
|
55
|
+
// but promise-returning functions with no explicit await still need one
|
|
56
|
+
// sync-to-async handoff here so later resolution exits the right call.
|
|
57
|
+
tracer.leaveSyncFrame?.(callId);
|
|
58
|
+
return result.then(
|
|
59
|
+
(value) => {
|
|
60
|
+
tracer.exit(callId, value, undefined);
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
tracer.exit(callId, undefined, error);
|
|
65
|
+
throw error;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
tracer.exit(callId, result, undefined);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
sourcePath: `inline://${TRACE_RUNTIME_VERSION}/instrument-helper.ts`,
|
|
74
|
+
}).program.body;
|
|
75
|
+
const createMetaLiteral = ({ bindingName, className, functionName, memberKind, methodName, }) => t.objectExpression([
|
|
76
|
+
t.objectProperty(t.identifier('bindingName'), t.stringLiteral(bindingName)),
|
|
77
|
+
t.objectProperty(t.identifier('className'), className ? t.stringLiteral(className) : t.nullLiteral()),
|
|
78
|
+
t.objectProperty(t.identifier('functionName'), t.stringLiteral(functionName)),
|
|
79
|
+
t.objectProperty(t.identifier('memberKind'), t.stringLiteral(memberKind)),
|
|
80
|
+
t.objectProperty(t.identifier('methodName'), methodName ? t.stringLiteral(methodName) : t.nullLiteral()),
|
|
81
|
+
]);
|
|
82
|
+
const isGeneratedFunction = (path) => Boolean(path.node[GENERATED_MARKER]);
|
|
83
|
+
const isGeneratedVariableDeclaration = (path) => Boolean(path.node[GENERATED_MARKER]);
|
|
84
|
+
const isSourceMapComment = (comment) => SOURCE_MAP_COMMENT_PREFIXES.some((prefix) => comment.value.trimStart().startsWith(prefix));
|
|
85
|
+
const stripSourceMapComments = (ast) => {
|
|
86
|
+
ast.comments = ast.comments?.filter((comment) => !isSourceMapComment(comment)) ?? [];
|
|
87
|
+
traverse(ast, {
|
|
88
|
+
enter(path) {
|
|
89
|
+
path.node.leadingComments = path.node.leadingComments?.filter((comment) => !isSourceMapComment(comment));
|
|
90
|
+
path.node.innerComments = path.node.innerComments?.filter((comment) => !isSourceMapComment(comment));
|
|
91
|
+
path.node.trailingComments = path.node.trailingComments?.filter((comment) => !isSourceMapComment(comment));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const isSkippableFunctionLike = ({ body, generator, params, }) => generator || isTrivialGetterLike({ body, params });
|
|
96
|
+
const toFunctionBody = (body) => t.isBlockStatement(body) ? t.cloneNode(body, true) : t.blockStatement([t.returnStatement(t.cloneNode(body, true))]);
|
|
97
|
+
const createHelperParams = ({ params, }) => params.map((parameter) => t.cloneNode(parameter, true));
|
|
98
|
+
const createTraceCallIdDeclaration = (callIdId) => {
|
|
99
|
+
const statement = parseModuleAst({
|
|
100
|
+
source: `const ${callIdId.name} = globalThis.__USHMAN_TRACER__?.peekInvocationCallId?.();`,
|
|
101
|
+
sourcePath: `inline://${TRACE_RUNTIME_VERSION}/instrument-call-id.ts`,
|
|
102
|
+
}).program.body[0];
|
|
103
|
+
if (!statement || !t.isVariableDeclaration(statement)) {
|
|
104
|
+
throw new Error('Failed to build the async call id declaration template.');
|
|
105
|
+
}
|
|
106
|
+
return statement;
|
|
107
|
+
};
|
|
108
|
+
const wrapAwaitArgument = ({ argument, callIdId, }) => t.callExpression(t.identifier(TRACE_AWAIT_HELPER_NAME), [t.cloneNode(callIdId), argument]);
|
|
109
|
+
const createHelperBody = ({ async, body, callIdId, }) => {
|
|
110
|
+
if (!async) {
|
|
111
|
+
return t.cloneNode(body, true);
|
|
112
|
+
}
|
|
113
|
+
if (!callIdId) {
|
|
114
|
+
throw new Error('Async helper bodies require a generated call id identifier.');
|
|
115
|
+
}
|
|
116
|
+
const rootFunction = t.functionExpression(null, [], t.cloneNode(body, true), false, true);
|
|
117
|
+
rootFunction.body.body.unshift(createTraceCallIdDeclaration(callIdId));
|
|
118
|
+
const file = t.file(t.program([t.expressionStatement(rootFunction)]));
|
|
119
|
+
traverse(file, {
|
|
120
|
+
AwaitExpression(path) {
|
|
121
|
+
path.replaceWith(t.awaitExpression(wrapAwaitArgument({
|
|
122
|
+
argument: t.cloneNode(path.node.argument, true),
|
|
123
|
+
callIdId,
|
|
124
|
+
})));
|
|
125
|
+
path.skip();
|
|
126
|
+
},
|
|
127
|
+
Function(path) {
|
|
128
|
+
if (path.node !== rootFunction) {
|
|
129
|
+
path.skip();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return file.program.body[0].expression.body;
|
|
134
|
+
};
|
|
135
|
+
const createOriginalVariableInitializer = ({ callIdId, init, }) => {
|
|
136
|
+
const helperParams = createHelperParams({
|
|
137
|
+
params: init.params,
|
|
138
|
+
});
|
|
139
|
+
const helperBody = createHelperBody({
|
|
140
|
+
async: init.async,
|
|
141
|
+
body: toFunctionBody(init.body),
|
|
142
|
+
callIdId,
|
|
143
|
+
});
|
|
144
|
+
if (t.isArrowFunctionExpression(init)) {
|
|
145
|
+
return t.arrowFunctionExpression(helperParams, helperBody, init.async);
|
|
146
|
+
}
|
|
147
|
+
return t.functionExpression(init.id ? t.cloneNode(init.id, true) : null, helperParams, helperBody, init.generator, init.async);
|
|
148
|
+
};
|
|
149
|
+
const buildVariableWrapperInitializer = ({ helperId, init, symbolName, }) => {
|
|
150
|
+
const argsId = t.identifier('__ushmanArgs');
|
|
151
|
+
const traceInvoke = t.callExpression(t.identifier('__ushmanTraceInvoke'), [
|
|
152
|
+
createMetaLiteral({
|
|
153
|
+
bindingName: symbolName,
|
|
154
|
+
className: null,
|
|
155
|
+
functionName: symbolName,
|
|
156
|
+
memberKind: 'function',
|
|
157
|
+
methodName: null,
|
|
158
|
+
}),
|
|
159
|
+
argsId,
|
|
160
|
+
t.isArrowFunctionExpression(init) ? t.identifier('undefined') : t.thisExpression(),
|
|
161
|
+
helperId,
|
|
162
|
+
t.booleanLiteral(init.async),
|
|
163
|
+
]);
|
|
164
|
+
if (t.isArrowFunctionExpression(init)) {
|
|
165
|
+
return t.arrowFunctionExpression([t.restElement(argsId)], traceInvoke, false);
|
|
166
|
+
}
|
|
167
|
+
return t.functionExpression(null, [t.restElement(argsId)], t.blockStatement([t.returnStatement(traceInvoke)]), false, false);
|
|
168
|
+
};
|
|
169
|
+
const flushVariableStatement = ({ declarators, exportNamed, kind, statements, }) => {
|
|
170
|
+
if (declarators.length === 0) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const declaration = t.variableDeclaration(kind, declarators);
|
|
174
|
+
statements.push(exportNamed ? t.exportNamedDeclaration(declaration) : declaration);
|
|
175
|
+
};
|
|
176
|
+
const instrumentTopLevelClassExpression = ({ className, instrumentedSymbols, skippedSymbols, variableDeclaratorPath, }) => {
|
|
177
|
+
const initPath = variableDeclaratorPath.get('init');
|
|
178
|
+
if (!initPath.isClassExpression()) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const memberPath of initPath.get('body.body')) {
|
|
182
|
+
if (!memberPath.isClassMethod()) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
instrumentClassMethod({
|
|
186
|
+
className,
|
|
187
|
+
instrumentedSymbols,
|
|
188
|
+
memberPath,
|
|
189
|
+
skippedSymbols,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const getInstrumentableVariableInit = (declarator) => {
|
|
194
|
+
if (!t.isIdentifier(declarator.id) || !declarator.init) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (!t.isArrowFunctionExpression(declarator.init) && !t.isFunctionExpression(declarator.init)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return declarator.init;
|
|
201
|
+
};
|
|
202
|
+
const shouldSkipVariableDeclarator = ({ declarator: _declarator, init, }) => isSkippableFunctionLike({
|
|
203
|
+
body: toFunctionBody(init.body),
|
|
204
|
+
generator: t.isFunctionExpression(init) ? init.generator : false,
|
|
205
|
+
params: init.params,
|
|
206
|
+
});
|
|
207
|
+
const createInstrumentedVariableStatements = ({ declarationPath, exportNamed, init, symbolName, }) => {
|
|
208
|
+
const helperId = declarationPath.scope.generateUidIdentifier(`ushmanOriginal_${symbolName}`);
|
|
209
|
+
const callIdId = init.async ? declarationPath.scope.generateUidIdentifier('ushmanTraceCallId') : undefined;
|
|
210
|
+
const helperDeclaration = t.variableDeclaration('const', [
|
|
211
|
+
t.variableDeclarator(helperId, createOriginalVariableInitializer({
|
|
212
|
+
callIdId,
|
|
213
|
+
init,
|
|
214
|
+
})),
|
|
215
|
+
]);
|
|
216
|
+
helperDeclaration[GENERATED_MARKER] = true;
|
|
217
|
+
const wrapperDeclaration = t.variableDeclaration(declarationPath.node.kind, [
|
|
218
|
+
t.variableDeclarator(t.identifier(symbolName), buildVariableWrapperInitializer({
|
|
219
|
+
helperId,
|
|
220
|
+
init,
|
|
221
|
+
symbolName,
|
|
222
|
+
})),
|
|
223
|
+
]);
|
|
224
|
+
wrapperDeclaration[GENERATED_MARKER] = true;
|
|
225
|
+
return [helperDeclaration, exportNamed ? t.exportNamedDeclaration(wrapperDeclaration) : wrapperDeclaration];
|
|
226
|
+
};
|
|
227
|
+
const createInstrumentedDefaultExportStatements = ({ exportPath, init, symbolName, }) => {
|
|
228
|
+
const helperId = exportPath.scope.generateUidIdentifier(`ushmanOriginal_${symbolName}`);
|
|
229
|
+
const callIdId = init.async ? exportPath.scope.generateUidIdentifier('ushmanTraceCallId') : undefined;
|
|
230
|
+
const helperDeclaration = t.variableDeclaration('const', [
|
|
231
|
+
t.variableDeclarator(helperId, createOriginalVariableInitializer({
|
|
232
|
+
callIdId,
|
|
233
|
+
init,
|
|
234
|
+
})),
|
|
235
|
+
]);
|
|
236
|
+
helperDeclaration[GENERATED_MARKER] = true;
|
|
237
|
+
const wrapperDeclaration = t.variableDeclaration('const', [
|
|
238
|
+
t.variableDeclarator(t.identifier(symbolName), buildVariableWrapperInitializer({
|
|
239
|
+
helperId,
|
|
240
|
+
init,
|
|
241
|
+
symbolName,
|
|
242
|
+
})),
|
|
243
|
+
]);
|
|
244
|
+
wrapperDeclaration[GENERATED_MARKER] = true;
|
|
245
|
+
return [helperDeclaration, wrapperDeclaration, t.exportDefaultDeclaration(t.identifier(symbolName))];
|
|
246
|
+
};
|
|
247
|
+
const instrumentTopLevelVariableDeclaration = ({ declarationPath, instrumentedSymbols, skippedSymbols, }) => {
|
|
248
|
+
if (isGeneratedVariableDeclaration(declarationPath)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const parent = declarationPath.parentPath;
|
|
252
|
+
const exportNamed = parent.isExportNamedDeclaration() && parent.parentPath?.isProgram();
|
|
253
|
+
const isTopLevel = parent.isProgram() || exportNamed;
|
|
254
|
+
if (!isTopLevel) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const replacementStatements = [];
|
|
258
|
+
const untouchedDeclarators = [];
|
|
259
|
+
let changed = false;
|
|
260
|
+
for (const declarator of declarationPath.node.declarations) {
|
|
261
|
+
const declaratorPath = declarationPath
|
|
262
|
+
.get('declarations')
|
|
263
|
+
.find((candidatePath) => candidatePath.node === declarator);
|
|
264
|
+
if (declaratorPath && t.isIdentifier(declarator.id) && t.isClassExpression(declarator.init)) {
|
|
265
|
+
instrumentTopLevelClassExpression({
|
|
266
|
+
className: declarator.id.name,
|
|
267
|
+
instrumentedSymbols,
|
|
268
|
+
skippedSymbols,
|
|
269
|
+
variableDeclaratorPath: declaratorPath,
|
|
270
|
+
});
|
|
271
|
+
untouchedDeclarators.push(t.cloneNode(declarator, true));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const init = getInstrumentableVariableInit(declarator);
|
|
275
|
+
if (!init || !t.isIdentifier(declarator.id)) {
|
|
276
|
+
untouchedDeclarators.push(t.cloneNode(declarator, true));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (shouldSkipVariableDeclarator({
|
|
280
|
+
declarator: declarator,
|
|
281
|
+
init,
|
|
282
|
+
})) {
|
|
283
|
+
skippedSymbols.push(declarator.id.name);
|
|
284
|
+
untouchedDeclarators.push(t.cloneNode(declarator, true));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
changed = true;
|
|
288
|
+
flushVariableStatement({
|
|
289
|
+
declarators: untouchedDeclarators.splice(0, untouchedDeclarators.length),
|
|
290
|
+
exportNamed: Boolean(exportNamed),
|
|
291
|
+
kind: declarationPath.node.kind,
|
|
292
|
+
statements: replacementStatements,
|
|
293
|
+
});
|
|
294
|
+
replacementStatements.push(...createInstrumentedVariableStatements({
|
|
295
|
+
declarationPath,
|
|
296
|
+
exportNamed: Boolean(exportNamed),
|
|
297
|
+
init,
|
|
298
|
+
symbolName: declarator.id.name,
|
|
299
|
+
}));
|
|
300
|
+
instrumentedSymbols.push(declarator.id.name);
|
|
301
|
+
}
|
|
302
|
+
if (!changed) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
flushVariableStatement({
|
|
306
|
+
declarators: untouchedDeclarators,
|
|
307
|
+
exportNamed: Boolean(exportNamed),
|
|
308
|
+
kind: declarationPath.node.kind,
|
|
309
|
+
statements: replacementStatements,
|
|
310
|
+
});
|
|
311
|
+
if (exportNamed) {
|
|
312
|
+
parent.replaceWithMultiple(replacementStatements);
|
|
313
|
+
parent.skip();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
declarationPath.replaceWithMultiple(replacementStatements);
|
|
317
|
+
declarationPath.skip();
|
|
318
|
+
};
|
|
319
|
+
const hasUnsafeMethodFeatures = (path) => {
|
|
320
|
+
let unsafe = false;
|
|
321
|
+
path.traverse({
|
|
322
|
+
PrivateName(inner) {
|
|
323
|
+
unsafe = true;
|
|
324
|
+
inner.stop();
|
|
325
|
+
},
|
|
326
|
+
Super(inner) {
|
|
327
|
+
unsafe = true;
|
|
328
|
+
inner.stop();
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return unsafe;
|
|
332
|
+
};
|
|
333
|
+
const createOriginalFunctionExpression = ({ async, body, callIdId, generator, params, }) => t.functionExpression(null, createHelperParams({
|
|
334
|
+
params,
|
|
335
|
+
}), createHelperBody({
|
|
336
|
+
async,
|
|
337
|
+
body,
|
|
338
|
+
callIdId,
|
|
339
|
+
}), generator, async);
|
|
340
|
+
const instrumentClassMethod = ({ className, instrumentedSymbols, memberPath, skippedSymbols, }) => {
|
|
341
|
+
const methodName = t.isIdentifier(memberPath.node.key) && !memberPath.node.computed ? memberPath.node.key.name : null;
|
|
342
|
+
if (!methodName) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const displayName = `${className}.${methodName}`;
|
|
346
|
+
if (memberPath.node.kind !== 'method' || hasUnsafeMethodFeatures(memberPath)) {
|
|
347
|
+
skippedSymbols.push(displayName);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (isSkippableFunctionLike(memberPath.node)) {
|
|
351
|
+
skippedSymbols.push(displayName);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const isAsync = memberPath.node.async;
|
|
355
|
+
const helperId = memberPath.scope.generateUidIdentifier(`ushmanOriginal_${className}_${methodName}`);
|
|
356
|
+
const callIdId = isAsync ? memberPath.scope.generateUidIdentifier('ushmanTraceCallId') : undefined;
|
|
357
|
+
const helper = t.variableDeclaration('const', [
|
|
358
|
+
t.variableDeclarator(helperId, createOriginalFunctionExpression({
|
|
359
|
+
async: isAsync,
|
|
360
|
+
body: memberPath.node.body,
|
|
361
|
+
callIdId,
|
|
362
|
+
generator: memberPath.node.generator,
|
|
363
|
+
params: memberPath.node.params,
|
|
364
|
+
})),
|
|
365
|
+
]);
|
|
366
|
+
memberPath.node.async = false;
|
|
367
|
+
memberPath.node.generator = false;
|
|
368
|
+
memberPath.node.params = [t.restElement(t.identifier('__ushmanArgs'))];
|
|
369
|
+
memberPath.node.body = t.blockStatement([
|
|
370
|
+
helper,
|
|
371
|
+
t.returnStatement(t.callExpression(t.identifier('__ushmanTraceInvoke'), [
|
|
372
|
+
createMetaLiteral({
|
|
373
|
+
bindingName: className,
|
|
374
|
+
className,
|
|
375
|
+
functionName: displayName,
|
|
376
|
+
memberKind: 'method',
|
|
377
|
+
methodName,
|
|
378
|
+
}),
|
|
379
|
+
t.identifier('__ushmanArgs'),
|
|
380
|
+
t.thisExpression(),
|
|
381
|
+
helperId,
|
|
382
|
+
t.booleanLiteral(isAsync),
|
|
383
|
+
])),
|
|
384
|
+
]);
|
|
385
|
+
instrumentedSymbols.push(displayName);
|
|
386
|
+
};
|
|
387
|
+
const createInstrumentedFunctionReplacement = ({ callIdId, className, functionName, originalFunction, }) => {
|
|
388
|
+
const helperId = t.identifier(`__ushmanOriginal_${functionName}`);
|
|
389
|
+
const helper = t.functionDeclaration(helperId, createHelperParams({
|
|
390
|
+
params: originalFunction.params,
|
|
391
|
+
}), createHelperBody({
|
|
392
|
+
async: originalFunction.async,
|
|
393
|
+
body: originalFunction.body,
|
|
394
|
+
callIdId,
|
|
395
|
+
}), originalFunction.generator, originalFunction.async);
|
|
396
|
+
helper.leadingComments = [{ type: 'CommentBlock', value: ` ${MARKER} ` }];
|
|
397
|
+
helper[GENERATED_MARKER] = true;
|
|
398
|
+
const wrapper = t.functionDeclaration(t.identifier(functionName), [t.restElement(t.identifier('__ushmanArgs'))], t.blockStatement([
|
|
399
|
+
t.returnStatement(t.callExpression(t.identifier('__ushmanTraceInvoke'), [
|
|
400
|
+
createMetaLiteral({
|
|
401
|
+
bindingName: functionName,
|
|
402
|
+
className,
|
|
403
|
+
functionName,
|
|
404
|
+
memberKind: 'function',
|
|
405
|
+
methodName: null,
|
|
406
|
+
}),
|
|
407
|
+
t.identifier('__ushmanArgs'),
|
|
408
|
+
t.thisExpression(),
|
|
409
|
+
helperId,
|
|
410
|
+
t.booleanLiteral(originalFunction.async),
|
|
411
|
+
])),
|
|
412
|
+
]), false, false);
|
|
413
|
+
wrapper[GENERATED_MARKER] = true;
|
|
414
|
+
return {
|
|
415
|
+
helper,
|
|
416
|
+
wrapper,
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
const instrumentTopLevelFunction = ({ anonymousDefaultExportBindingName, instrumentedSymbols, path, skippedSymbols, }) => {
|
|
420
|
+
if (isGeneratedFunction(path)) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const parent = path.parentPath;
|
|
424
|
+
const isTopLevel = parent.isProgram() ||
|
|
425
|
+
((parent.isExportNamedDeclaration() || parent.isExportDefaultDeclaration()) && parent.parentPath?.isProgram());
|
|
426
|
+
const functionName = path.node.id?.name ??
|
|
427
|
+
(parent.isExportDefaultDeclaration() && parent.parentPath?.isProgram()
|
|
428
|
+
? anonymousDefaultExportBindingName
|
|
429
|
+
: undefined);
|
|
430
|
+
if (!functionName || !isTopLevel) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (isSkippableFunctionLike(path.node)) {
|
|
434
|
+
skippedSymbols.push(functionName);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const replacement = createInstrumentedFunctionReplacement({
|
|
438
|
+
callIdId: path.node.async ? path.scope.generateUidIdentifier('ushmanTraceCallId') : undefined,
|
|
439
|
+
className: null,
|
|
440
|
+
functionName,
|
|
441
|
+
originalFunction: path.node,
|
|
442
|
+
});
|
|
443
|
+
if (parent.isExportNamedDeclaration()) {
|
|
444
|
+
parent.replaceWithMultiple([replacement.helper, t.exportNamedDeclaration(replacement.wrapper)]);
|
|
445
|
+
parent.skip();
|
|
446
|
+
}
|
|
447
|
+
else if (parent.isExportDefaultDeclaration()) {
|
|
448
|
+
parent.replaceWithMultiple([replacement.helper, t.exportDefaultDeclaration(replacement.wrapper)]);
|
|
449
|
+
parent.skip();
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
path.replaceWithMultiple([replacement.helper, replacement.wrapper]);
|
|
453
|
+
path.skip();
|
|
454
|
+
}
|
|
455
|
+
instrumentedSymbols.push(functionName);
|
|
456
|
+
};
|
|
457
|
+
const instrumentAnonymousDefaultExportExpression = ({ anonymousDefaultExportBindingName, exportPath, instrumentedSymbols, skippedSymbols, }) => {
|
|
458
|
+
const declarationPath = exportPath.get('declaration');
|
|
459
|
+
if (!anonymousDefaultExportBindingName) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!declarationPath.isArrowFunctionExpression() && !declarationPath.isFunctionExpression()) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (isSkippableFunctionLike({
|
|
466
|
+
body: toFunctionBody(declarationPath.node.body),
|
|
467
|
+
generator: declarationPath.isFunctionExpression() ? declarationPath.node.generator : false,
|
|
468
|
+
params: declarationPath.node.params,
|
|
469
|
+
})) {
|
|
470
|
+
skippedSymbols.push(anonymousDefaultExportBindingName);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
exportPath.replaceWithMultiple(createInstrumentedDefaultExportStatements({
|
|
474
|
+
exportPath,
|
|
475
|
+
init: declarationPath.node,
|
|
476
|
+
symbolName: anonymousDefaultExportBindingName,
|
|
477
|
+
}));
|
|
478
|
+
exportPath.skip();
|
|
479
|
+
instrumentedSymbols.push(anonymousDefaultExportBindingName);
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Instrument a bundle source string with the characterization tracer wrappers.
|
|
483
|
+
*/
|
|
484
|
+
export const instrumentBundleSource = ({ source, sourceMapFile, sourceMapMode = 'off', sourcePath, }) => {
|
|
485
|
+
assertSourceMapMode(sourceMapMode);
|
|
486
|
+
if (source.includes(MARKER)) {
|
|
487
|
+
return {
|
|
488
|
+
code: source,
|
|
489
|
+
instrumentedSymbols: [],
|
|
490
|
+
map: null,
|
|
491
|
+
skippedSymbols: [],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (helperTemplates.length === 0 || helperTemplates.some((statement) => !t.isFunctionDeclaration(statement))) {
|
|
495
|
+
throw new Error('Failed to build the characterization helper template.');
|
|
496
|
+
}
|
|
497
|
+
const ast = parseModuleAst({
|
|
498
|
+
source,
|
|
499
|
+
sourcePath,
|
|
500
|
+
});
|
|
501
|
+
stripSourceMapComments(ast);
|
|
502
|
+
const anonymousDefaultExportBindingName = getAnonymousDefaultExportBindingName(ast.program.body) ?? undefined;
|
|
503
|
+
const instrumentedSymbols = [];
|
|
504
|
+
const skippedSymbols = [];
|
|
505
|
+
traverse(ast, {
|
|
506
|
+
ClassDeclaration(path) {
|
|
507
|
+
const className = path.node.id?.name;
|
|
508
|
+
if (!className) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (const memberPath of path.get('body.body')) {
|
|
512
|
+
if (!memberPath.isClassMethod()) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
instrumentClassMethod({
|
|
516
|
+
className,
|
|
517
|
+
instrumentedSymbols,
|
|
518
|
+
memberPath,
|
|
519
|
+
skippedSymbols,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
ExportDefaultDeclaration(path) {
|
|
524
|
+
instrumentAnonymousDefaultExportExpression({
|
|
525
|
+
anonymousDefaultExportBindingName,
|
|
526
|
+
exportPath: path,
|
|
527
|
+
instrumentedSymbols,
|
|
528
|
+
skippedSymbols,
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
FunctionDeclaration(path) {
|
|
532
|
+
instrumentTopLevelFunction({
|
|
533
|
+
anonymousDefaultExportBindingName,
|
|
534
|
+
instrumentedSymbols,
|
|
535
|
+
path,
|
|
536
|
+
skippedSymbols,
|
|
537
|
+
});
|
|
538
|
+
},
|
|
539
|
+
VariableDeclaration(path) {
|
|
540
|
+
instrumentTopLevelVariableDeclaration({
|
|
541
|
+
declarationPath: path,
|
|
542
|
+
instrumentedSymbols,
|
|
543
|
+
skippedSymbols,
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
const helperNodes = helperTemplates.map((statement, index) => {
|
|
548
|
+
const helperNode = t.cloneNode(statement, true);
|
|
549
|
+
helperNode.leadingComments = index === 0 ? [{ type: 'CommentBlock', value: ` ${MARKER} ` }] : [];
|
|
550
|
+
helperNode[GENERATED_MARKER] = true;
|
|
551
|
+
return helperNode;
|
|
552
|
+
});
|
|
553
|
+
ast.program.body.unshift(...helperNodes);
|
|
554
|
+
const generated = generate(ast, {
|
|
555
|
+
comments: true,
|
|
556
|
+
compact: false,
|
|
557
|
+
sourceFileName: sourcePath,
|
|
558
|
+
sourceMaps: sourceMapMode !== 'off',
|
|
559
|
+
}, source);
|
|
560
|
+
const map = generated.map ?? null;
|
|
561
|
+
if (sourceMapMode === 'inline' && map) {
|
|
562
|
+
const inlinePayload = Buffer.from(JSON.stringify(map), 'utf8').toString('base64');
|
|
563
|
+
return {
|
|
564
|
+
code: `${generated.code}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${inlinePayload}`,
|
|
565
|
+
instrumentedSymbols,
|
|
566
|
+
map,
|
|
567
|
+
skippedSymbols,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (sourceMapMode === 'external' && map) {
|
|
571
|
+
const mappingUrl = sourceMapFile ? path.basename(sourceMapFile) : `${path.basename(sourcePath)}.map`;
|
|
572
|
+
return {
|
|
573
|
+
code: `${generated.code}\n//# sourceMappingURL=${mappingUrl}`,
|
|
574
|
+
instrumentedSymbols,
|
|
575
|
+
map,
|
|
576
|
+
skippedSymbols,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
code: generated.code,
|
|
581
|
+
instrumentedSymbols,
|
|
582
|
+
map,
|
|
583
|
+
skippedSymbols,
|
|
584
|
+
};
|
|
585
|
+
};
|
|
586
|
+
export const instrumentBundle = async ({ bundlePath, outputPath, sourceMapMode = 'external', }) => {
|
|
587
|
+
assertSourceMapMode(sourceMapMode);
|
|
588
|
+
const source = await Bun.file(bundlePath).text();
|
|
589
|
+
const mapPath = sourceMapMode === 'external' ? `${outputPath}.map` : null;
|
|
590
|
+
const result = instrumentBundleSource({
|
|
591
|
+
source,
|
|
592
|
+
sourceMapFile: mapPath ?? undefined,
|
|
593
|
+
sourceMapMode,
|
|
594
|
+
sourcePath: bundlePath,
|
|
595
|
+
});
|
|
596
|
+
await Bun.write(outputPath, `${result.code.trimEnd()}\n`);
|
|
597
|
+
if (mapPath && result.map) {
|
|
598
|
+
await Bun.write(mapPath, `${JSON.stringify(result.map, null, 2)}\n`);
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
...result,
|
|
602
|
+
mapPath,
|
|
603
|
+
outputPath,
|
|
604
|
+
};
|
|
605
|
+
};
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const writeCharacterizeReport: ({ payload, prefix, workspaceRoot, }: {
|
|
2
|
+
readonly payload: unknown;
|
|
3
|
+
readonly prefix: string;
|
|
4
|
+
readonly workspaceRoot: string;
|
|
5
|
+
}) => Promise<string>;
|
|
6
|
+
export declare const recordCharacterizeValidatorResult: ({ affectedFiles, metrics, resultPath, summary, verdict, workspaceRoot, }: {
|
|
7
|
+
readonly affectedFiles?: readonly string[];
|
|
8
|
+
readonly metrics?: Record<string, unknown>;
|
|
9
|
+
readonly resultPath?: string;
|
|
10
|
+
readonly summary: string;
|
|
11
|
+
readonly verdict: "green" | "red" | "yellow";
|
|
12
|
+
readonly workspaceRoot: string;
|
|
13
|
+
}) => Promise<void>;
|
|
14
|
+
export declare const recordCharacterizeToolingGap: ({ body, summary, workspaceRoot, }: {
|
|
15
|
+
readonly body: string;
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
readonly workspaceRoot: string;
|
|
18
|
+
}) => Promise<void>;
|
|
19
|
+
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,uBAAuB,GAAU,qCAI3C;IACC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,oBAMA,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAAU,0EAOrD;IACC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,kBAiBA,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAU,mCAIhD;IACC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,kBAOA,CAAC"}
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildValidatorResultRecord, openLedger } from '@ushman/ledger';
|
|
4
|
+
import { nowIso } from "./time.js";
|
|
5
|
+
import { workspaceHarnessPaths } from "./trace-format.js";
|
|
6
|
+
const sanitizeTimestamp = (value) => value.replace(/[:.]/gu, '-');
|
|
7
|
+
let cachedPackageVersion = null;
|
|
8
|
+
const readPackageVersion = async () => {
|
|
9
|
+
if (!cachedPackageVersion) {
|
|
10
|
+
cachedPackageVersion = (async () => {
|
|
11
|
+
const packageJsonPath = new URL('../package.json', import.meta.url);
|
|
12
|
+
const packageJson = (await Bun.file(packageJsonPath).json());
|
|
13
|
+
return typeof packageJson.version === 'string' && packageJson.version.length > 0
|
|
14
|
+
? packageJson.version
|
|
15
|
+
: '0.0.0';
|
|
16
|
+
})();
|
|
17
|
+
}
|
|
18
|
+
return cachedPackageVersion;
|
|
19
|
+
};
|
|
20
|
+
export const writeCharacterizeReport = async ({ payload, prefix, workspaceRoot, }) => {
|
|
21
|
+
const harnessPaths = workspaceHarnessPaths(workspaceRoot);
|
|
22
|
+
await mkdir(harnessPaths.reportsDir, { recursive: true });
|
|
23
|
+
const reportPath = path.join(harnessPaths.reportsDir, `${prefix}-${sanitizeTimestamp(nowIso())}.json`);
|
|
24
|
+
await Bun.write(reportPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
25
|
+
return reportPath;
|
|
26
|
+
};
|
|
27
|
+
export const recordCharacterizeValidatorResult = async ({ affectedFiles, metrics, resultPath, summary, verdict, workspaceRoot, }) => {
|
|
28
|
+
const ledger = await openLedger(workspaceRoot);
|
|
29
|
+
await ledger.record(buildValidatorResultRecord({
|
|
30
|
+
emitter: {
|
|
31
|
+
tool: '@ushman/characterize',
|
|
32
|
+
version: await readPackageVersion(),
|
|
33
|
+
},
|
|
34
|
+
links: affectedFiles && affectedFiles.length > 0 ? { affectedFiles: [...affectedFiles] } : undefined,
|
|
35
|
+
metrics,
|
|
36
|
+
phase: 'characterize',
|
|
37
|
+
resultPath,
|
|
38
|
+
summary,
|
|
39
|
+
validator: 'characterize',
|
|
40
|
+
verdict,
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
export const recordCharacterizeToolingGap = async ({ body, summary, workspaceRoot, }) => {
|
|
44
|
+
const ledger = await openLedger(workspaceRoot);
|
|
45
|
+
await ledger.note('tooling-gap', {
|
|
46
|
+
body,
|
|
47
|
+
phase: 'characterize',
|
|
48
|
+
summary,
|
|
49
|
+
});
|
|
50
|
+
};
|