@abreen/tada 1.13.9 → 1.14.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 +46 -0
- package/build/generate-content-assets.ts +2 -10
- package/build/types.d.ts +18 -6
- package/build/utils/html-comments.ts +3 -0
- package/build/utils/jdi-runner/TraceRunner.java +227 -38
- package/build/utils/markdown-partials.ts +235 -0
- package/build/utils/markdown.ts +12 -1
- package/build/utils/python-runner/trace_runner.py +58 -13
- package/build/utils/render.ts +14 -12
- package/build/utils/trace-core.ts +148 -16
- package/build/utils/trace-heap.ts +34 -0
- package/build/utils/trace-java.ts +60 -7
- package/build/utils/trace-layout.ts +58 -13
- package/build/utils/trace-python.ts +6 -2
- package/build/utils/trace-svg.ts +68 -66
- package/build/utils/trace.ts +193 -78
- package/build/watch/compiler-types.d.ts +2 -2
- package/build/watch/compiler.ts +5 -1
- package/init/content/lectures/02/_pr1.md +1 -1
- package/init/content/lectures/02/index.md +2 -2
- package/init/content/lectures/02/subdir/_foobar.md +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +3 -3
- package/package.json +1 -1
- package/src/question/index.ts +13 -1
- package/src/slides/index.ts +7 -3
- package/src/trace/index.ts +49 -12
- package/src/trace/style.scss +10 -0
- package/src/trace/types.d.ts +16 -4
- package/build/utils/include.ts +0 -57
- package/init/content/lectures/02/subdir/_foobar.html +0 -3
package/build/utils/trace-svg.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TraceObjectLayout,
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { ARRAY_MAX_CELLS, DEFAULT_FONT_SIZE } from './trace-layout';
|
|
10
|
+
import { formatHeapObjectValue, isInlineHeapObject } from './trace-heap';
|
|
10
11
|
|
|
11
12
|
/** Gap between stack area and heap area. */
|
|
12
13
|
const SECTION_GAP = 60;
|
|
@@ -17,6 +18,9 @@ const PADDING = 8;
|
|
|
17
18
|
/** Horizontal margin when routing stack-to-heap arrows. */
|
|
18
19
|
const ARROW_MARGIN = 20;
|
|
19
20
|
|
|
21
|
+
/** Approximate monospace character width for arrow targeting. */
|
|
22
|
+
const CHAR_WIDTH_RATIO = 0.6;
|
|
23
|
+
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
21
25
|
// Escape & format helpers
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
@@ -58,13 +62,9 @@ export function formatValue(val: TraceValue): string {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// filterStep -- remove args from main(), hide
|
|
65
|
+
// filterStep -- remove args from main(), hide unreachable heap objects
|
|
62
66
|
// ---------------------------------------------------------------------------
|
|
63
67
|
|
|
64
|
-
function isStringObject(obj: TraceHeapObject): boolean {
|
|
65
|
-
return 'value' in obj && obj.type === 'String';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
68
|
export function filterStep(step: TraceStep): {
|
|
69
69
|
stack: TraceStackFrame[];
|
|
70
70
|
heap: Record<string, TraceHeapObject>;
|
|
@@ -84,38 +84,6 @@ export function filterStep(step: TraceStep): {
|
|
|
84
84
|
return { ...frame, locals };
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
// Find objects reachable only through the hidden 'args' variable
|
|
88
|
-
const argsOnly = new Set<string>();
|
|
89
|
-
for (const frame of step.stack) {
|
|
90
|
-
if (frame.method === 'main') {
|
|
91
|
-
const argsVal = frame.locals.args;
|
|
92
|
-
if (argsVal?.type === 'ref') {
|
|
93
|
-
const q = [argsVal.id];
|
|
94
|
-
while (q.length > 0) {
|
|
95
|
-
const id = q.shift()!;
|
|
96
|
-
if (argsOnly.has(id) || !step.heap[id]) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
argsOnly.add(id);
|
|
100
|
-
const obj = step.heap[id];
|
|
101
|
-
if ('elements' in obj) {
|
|
102
|
-
for (const el of obj.elements) {
|
|
103
|
-
if (el.type === 'ref') {
|
|
104
|
-
q.push(el.id);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} else if ('fields' in obj) {
|
|
108
|
-
for (const v of Object.values(obj.fields)) {
|
|
109
|
-
if (v.type === 'ref') {
|
|
110
|
-
q.push(v.id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
87
|
// Find objects reachable from visible (non-args) variables
|
|
120
88
|
const reachable = new Set<string>();
|
|
121
89
|
const queue: string[] = [];
|
|
@@ -126,6 +94,9 @@ export function filterStep(step: TraceStep): {
|
|
|
126
94
|
}
|
|
127
95
|
}
|
|
128
96
|
}
|
|
97
|
+
for (const id of step.transientHeapRoots ?? []) {
|
|
98
|
+
queue.push(id);
|
|
99
|
+
}
|
|
129
100
|
while (queue.length > 0) {
|
|
130
101
|
const id = queue.shift()!;
|
|
131
102
|
if (reachable.has(id) || !step.heap[id]) {
|
|
@@ -148,11 +119,10 @@ export function filterStep(step: TraceStep): {
|
|
|
148
119
|
}
|
|
149
120
|
}
|
|
150
121
|
|
|
151
|
-
// Keep heap objects
|
|
152
|
-
// aren't exclusively reachable from the hidden 'args'
|
|
122
|
+
// Keep only heap objects reachable from visible variables.
|
|
153
123
|
const heap: Record<string, TraceHeapObject> = {};
|
|
154
124
|
const sortedIds = Object.keys(step.heap)
|
|
155
|
-
.filter(id => reachable.has(id)
|
|
125
|
+
.filter(id => reachable.has(id))
|
|
156
126
|
.sort((a, b) => {
|
|
157
127
|
const numA = parseInt(a.split('_').pop()!, 10);
|
|
158
128
|
const numB = parseInt(b.split('_').pop()!, 10);
|
|
@@ -294,11 +264,12 @@ function svgHeapObject(
|
|
|
294
264
|
`<g class="trace-obj" data-id="${escapeXml(id)}" transform="translate(${absX},${absY})">`,
|
|
295
265
|
);
|
|
296
266
|
|
|
297
|
-
if ('value' in obj &&
|
|
298
|
-
// Strings
|
|
267
|
+
if ('value' in obj && isInlineHeapObject(obj)) {
|
|
268
|
+
// Strings and Java boxed primitives are heap values, but render inline.
|
|
299
269
|
parts.push(
|
|
300
|
-
`<text class="trace-val" x="
|
|
301
|
-
`dominant-baseline="central"
|
|
270
|
+
`<text class="trace-val" x="${width / 2}" y="${height / 2}" ` +
|
|
271
|
+
`text-anchor="middle" dominant-baseline="central">` +
|
|
272
|
+
`${escapeXml(formatHeapObjectValue(obj.type, obj.value))}</text>`,
|
|
302
273
|
);
|
|
303
274
|
parts.push('</g>');
|
|
304
275
|
return parts.join('');
|
|
@@ -505,6 +476,43 @@ function rectBoundaryPoint(
|
|
|
505
476
|
return { x: Math.round(cx + dx * scale), y: Math.round(cy + dy * scale) };
|
|
506
477
|
}
|
|
507
478
|
|
|
479
|
+
function heapTargetBoundaryPoint(
|
|
480
|
+
sx: number,
|
|
481
|
+
sy: number,
|
|
482
|
+
target: HeapObjInfo,
|
|
483
|
+
obj: TraceHeapObject | undefined,
|
|
484
|
+
dims: { fontSize: number },
|
|
485
|
+
): { x: number; y: number } {
|
|
486
|
+
if (!obj || !('value' in obj) || !isInlineHeapObject(obj)) {
|
|
487
|
+
return rectBoundaryPoint(
|
|
488
|
+
sx,
|
|
489
|
+
sy,
|
|
490
|
+
target.absX,
|
|
491
|
+
target.absY,
|
|
492
|
+
target.width,
|
|
493
|
+
target.height,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const text = formatHeapObjectValue(obj.type, obj.value);
|
|
498
|
+
const textWidth = Math.max(
|
|
499
|
+
dims.fontSize,
|
|
500
|
+
text.length * dims.fontSize * CHAR_WIDTH_RATIO,
|
|
501
|
+
);
|
|
502
|
+
const textHeight = dims.fontSize * 1.2;
|
|
503
|
+
const textPadding = 4;
|
|
504
|
+
const rx = target.absX + (target.width - textWidth) / 2 - textPadding;
|
|
505
|
+
const ry = target.absY + (target.height - textHeight) / 2 - textPadding;
|
|
506
|
+
return rectBoundaryPoint(
|
|
507
|
+
sx,
|
|
508
|
+
sy,
|
|
509
|
+
rx,
|
|
510
|
+
ry,
|
|
511
|
+
textWidth + textPadding * 2,
|
|
512
|
+
textHeight + textPadding * 2,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
508
516
|
/**
|
|
509
517
|
* Get the center of the child-slot box for a given field.
|
|
510
518
|
* The slot row has a small label at the top and a box below it.
|
|
@@ -537,6 +545,7 @@ function generateArrows(
|
|
|
537
545
|
stackLayouts: FrameLayout[],
|
|
538
546
|
heapObjMap: Map<string, HeapObjInfo>,
|
|
539
547
|
dims: { rowH: number; fontSize: number },
|
|
548
|
+
markerIds: { arrowhead: string; arrowheadDim: string },
|
|
540
549
|
): string[] {
|
|
541
550
|
const parts: string[] = [];
|
|
542
551
|
|
|
@@ -547,17 +556,11 @@ function generateArrows(
|
|
|
547
556
|
target: HeapObjInfo,
|
|
548
557
|
style: 'fg' | 'dim' = 'fg',
|
|
549
558
|
): string {
|
|
550
|
-
const ep =
|
|
551
|
-
sx,
|
|
552
|
-
sy,
|
|
553
|
-
target.absX,
|
|
554
|
-
target.absY,
|
|
555
|
-
target.width,
|
|
556
|
-
target.height,
|
|
557
|
-
);
|
|
559
|
+
const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
|
|
558
560
|
const cpx = Math.round((sx + ep.x) / 2);
|
|
559
561
|
const cls = style === 'fg' ? 'trace-arrow' : 'trace-arrow-dim';
|
|
560
|
-
const marker =
|
|
562
|
+
const marker =
|
|
563
|
+
style === 'fg' ? markerIds.arrowhead : markerIds.arrowheadDim;
|
|
561
564
|
return (
|
|
562
565
|
`<path class="${cls}" d="` +
|
|
563
566
|
`M ${sx} ${sy} Q ${cpx} ${sy}, ${ep.x} ${ep.y}" ` +
|
|
@@ -576,7 +579,7 @@ function generateArrows(
|
|
|
576
579
|
if (Math.abs(tx - sx) < 4) {
|
|
577
580
|
return (
|
|
578
581
|
`<path class="trace-arrow-heap" d="M ${sx} ${sy} L ${tx} ${ty}" ` +
|
|
579
|
-
`marker-end="url(
|
|
582
|
+
`marker-end="url(#${markerIds.arrowheadDim})"/>`
|
|
580
583
|
);
|
|
581
584
|
}
|
|
582
585
|
|
|
@@ -599,21 +602,14 @@ function generateArrows(
|
|
|
599
602
|
|
|
600
603
|
return (
|
|
601
604
|
`<path class="trace-arrow-heap" d="${path}" ` +
|
|
602
|
-
`marker-end="url(
|
|
605
|
+
`marker-end="url(#${markerIds.arrowheadDim})"/>`
|
|
603
606
|
);
|
|
604
607
|
}
|
|
605
608
|
|
|
606
609
|
// Non-structural heap arrow: cubic Bezier with auto-detected direction
|
|
607
610
|
// (typically horizontal toward ref chains).
|
|
608
611
|
function heapArrow(sx: number, sy: number, target: HeapObjInfo): string {
|
|
609
|
-
const ep =
|
|
610
|
-
sx,
|
|
611
|
-
sy,
|
|
612
|
-
target.absX,
|
|
613
|
-
target.absY,
|
|
614
|
-
target.width,
|
|
615
|
-
target.height,
|
|
616
|
-
);
|
|
612
|
+
const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
|
|
617
613
|
const dx = ep.x - sx;
|
|
618
614
|
const dy = ep.y - sy;
|
|
619
615
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -650,7 +646,7 @@ function generateArrows(
|
|
|
650
646
|
`<path class="trace-arrow-heap" d="` +
|
|
651
647
|
`M ${sx} ${sy} C ${Math.round(cp1x)} ${Math.round(cp1y)}, ` +
|
|
652
648
|
`${Math.round(cp2x)} ${Math.round(cp2y)}, ${ep.x} ${ep.y}" ` +
|
|
653
|
-
`marker-end="url(
|
|
649
|
+
`marker-end="url(#${markerIds.arrowheadDim})"/>`
|
|
654
650
|
);
|
|
655
651
|
}
|
|
656
652
|
|
|
@@ -778,6 +774,7 @@ export function generateStepSvg(
|
|
|
778
774
|
step: TraceStep,
|
|
779
775
|
layout: TraceLayout,
|
|
780
776
|
fontSize: number = DEFAULT_FONT_SIZE,
|
|
777
|
+
markerIdPrefix = 'trace',
|
|
781
778
|
): string {
|
|
782
779
|
const { stack, heap } = filterStep(step);
|
|
783
780
|
|
|
@@ -842,6 +839,10 @@ export function generateStepSvg(
|
|
|
842
839
|
|
|
843
840
|
// Build SVG
|
|
844
841
|
const parts: string[] = [];
|
|
842
|
+
const markerIds = {
|
|
843
|
+
arrowhead: `${markerIdPrefix}-arrowhead`,
|
|
844
|
+
arrowheadDim: `${markerIdPrefix}-arrowhead-dim`,
|
|
845
|
+
};
|
|
845
846
|
parts.push(
|
|
846
847
|
`<svg class="trace-memory" viewBox="0 0 ${totalWidth} ${totalHeight}" ` +
|
|
847
848
|
`width="${totalWidth}" height="${totalHeight}" ` +
|
|
@@ -851,13 +852,13 @@ export function generateStepSvg(
|
|
|
851
852
|
// Defs: arrowhead markers
|
|
852
853
|
parts.push('<defs>');
|
|
853
854
|
parts.push(
|
|
854
|
-
|
|
855
|
+
`<marker id="${markerIds.arrowhead}" markerWidth="8" markerHeight="6" ` +
|
|
855
856
|
'refX="8" refY="3" orient="auto" markerUnits="strokeWidth">',
|
|
856
857
|
);
|
|
857
858
|
parts.push('<polygon class="trace-arrowhead" points="0 0, 8 3, 0 6"/>');
|
|
858
859
|
parts.push('</marker>');
|
|
859
860
|
parts.push(
|
|
860
|
-
|
|
861
|
+
`<marker id="${markerIds.arrowheadDim}" markerWidth="8" markerHeight="6" ` +
|
|
861
862
|
'refX="8" refY="3" orient="auto" markerUnits="strokeWidth">',
|
|
862
863
|
);
|
|
863
864
|
parts.push('<polygon class="trace-arrowhead-dim" points="0 0, 8 3, 0 6"/>');
|
|
@@ -890,6 +891,7 @@ export function generateStepSvg(
|
|
|
890
891
|
stackLayouts,
|
|
891
892
|
heapObjMap,
|
|
892
893
|
dims,
|
|
894
|
+
markerIds,
|
|
893
895
|
);
|
|
894
896
|
for (const arrow of arrowParts) {
|
|
895
897
|
parts.push(arrow);
|
package/build/utils/trace.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { makeLogger } from '../log';
|
|
4
5
|
import { B } from '../colors';
|
|
@@ -43,9 +44,9 @@ export interface TraceContext {
|
|
|
43
44
|
export interface TraceResult {
|
|
44
45
|
manifestUrl: string;
|
|
45
46
|
artifactId: string;
|
|
46
|
-
highlightedSource: string;
|
|
47
|
+
highlightedSources: { file: string; highlightedSource: string }[];
|
|
47
48
|
totalSteps: number;
|
|
48
|
-
|
|
49
|
+
sourceMtims: Record<string, number>;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function isTraceSourceFile(filePath: string): boolean {
|
|
@@ -89,31 +90,44 @@ function disabledTraceMessage(fileName: string): string {
|
|
|
89
90
|
return `Python was not found; trace for ${B`${fileName}`} will be disabled`;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
interface TraceSourceFile {
|
|
94
|
+
requestedPath: string;
|
|
95
|
+
absolutePath: string;
|
|
96
|
+
file: string;
|
|
97
|
+
source: string;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
function runTraceForFile(
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
primaryFilePath: string,
|
|
102
|
+
tracedFilePaths: string[],
|
|
103
|
+
sources: { file: string; source: string }[],
|
|
100
104
|
): { output: string; ignoreFields?: Record<string, string[]> } {
|
|
101
|
-
const ext = path.extname(
|
|
105
|
+
const ext = path.extname(primaryFilePath).toLowerCase();
|
|
102
106
|
if (ext === '.java') {
|
|
103
|
-
validateJavaTraceTarget(
|
|
107
|
+
validateJavaTraceTarget(primaryFilePath, sources[0].source);
|
|
108
|
+
const ignoreFields: Record<string, string[]> = {};
|
|
109
|
+
for (const source of sources) {
|
|
110
|
+
const sourceFields = parseIgnoreFields(source.source);
|
|
111
|
+
for (const [className, fieldNames] of Object.entries(sourceFields)) {
|
|
112
|
+
ignoreFields[className] = [
|
|
113
|
+
...(ignoreFields[className] ?? []),
|
|
114
|
+
...fieldNames,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
104
118
|
return {
|
|
105
|
-
output: runJavaTrace(
|
|
106
|
-
ignoreFields
|
|
119
|
+
output: runJavaTrace(tracedFilePaths, path.parse(primaryFilePath).name),
|
|
120
|
+
ignoreFields,
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
123
|
if (ext === '.py') {
|
|
110
|
-
return { output: runPythonTrace(
|
|
124
|
+
return { output: runPythonTrace(primaryFilePath, tracedFilePaths) };
|
|
111
125
|
}
|
|
112
|
-
throw new Error(`Unsupported trace source: ${
|
|
126
|
+
throw new Error(`Unsupported trace source: ${primaryFilePath}`);
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
export function createTraceHelpers(context: TraceContext): {
|
|
116
|
-
renderTrace: (sourceFile: string) => string;
|
|
130
|
+
renderTrace: (sourceFile: string, companionFiles?: string[]) => string;
|
|
117
131
|
} {
|
|
118
132
|
const {
|
|
119
133
|
filePath,
|
|
@@ -127,60 +141,158 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
127
141
|
} = context;
|
|
128
142
|
const pageDir = path.dirname(filePath);
|
|
129
143
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
function resolveTraceSources(
|
|
145
|
+
sourceFile: string,
|
|
146
|
+
companionFiles?: string[],
|
|
147
|
+
): TraceSourceFile[] {
|
|
148
|
+
if (companionFiles !== undefined && !Array.isArray(companionFiles)) {
|
|
149
|
+
throw new Error('renderTrace companionFiles must be an array');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const requestedPaths = [sourceFile, ...(companionFiles ?? [])];
|
|
153
|
+
const sources = requestedPaths.map(requestedPath => {
|
|
154
|
+
const absolutePath = path.resolve(pageDir, requestedPath);
|
|
155
|
+
if (!fs.existsSync(absolutePath)) {
|
|
156
|
+
throw new Error(`Trace target not found: ${absolutePath}`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
requestedPath,
|
|
160
|
+
absolutePath,
|
|
161
|
+
file: path.basename(requestedPath),
|
|
162
|
+
source: fs.readFileSync(absolutePath, 'utf-8'),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const primaryExt = path.extname(sources[0].absolutePath).toLowerCase();
|
|
167
|
+
for (const source of sources) {
|
|
168
|
+
const ext = path.extname(source.absolutePath).toLowerCase();
|
|
169
|
+
if (ext !== primaryExt) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Trace companion must use the same extension as ${sourceFile}: ${source.requestedPath}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
traceLanguageForFile(source.absolutePath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const seenBasenames = new Set<string>();
|
|
178
|
+
for (const source of sources) {
|
|
179
|
+
if (seenBasenames.has(source.file)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Trace files must have unique basenames in a flat workspace: ${source.file}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
seenBasenames.add(source.file);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return sources;
|
|
188
|
+
}
|
|
135
189
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
190
|
+
function cacheKeyForSources(sources: TraceSourceFile[]): string {
|
|
191
|
+
return JSON.stringify(sources.map(source => source.absolutePath));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getSourceMtims(sources: TraceSourceFile[]): Record<string, number> {
|
|
195
|
+
return Object.fromEntries(
|
|
196
|
+
sources.map(source => [
|
|
197
|
+
source.absolutePath,
|
|
198
|
+
fs.statSync(source.absolutePath).mtimeMs,
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cacheIsFresh(
|
|
204
|
+
sources: TraceSourceFile[],
|
|
205
|
+
cached: TraceResult,
|
|
206
|
+
): boolean {
|
|
207
|
+
return sources.every(source => {
|
|
208
|
+
const mtime = fs.statSync(source.absolutePath, {
|
|
139
209
|
throwIfNoEntry: false,
|
|
140
210
|
})?.mtimeMs;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
sourceDistDir: cachedTraceSourceDir || distDir,
|
|
153
|
-
})
|
|
154
|
-
) {
|
|
155
|
-
for (const outputPath of outputPaths) {
|
|
156
|
-
dependencyCollector?.generatedOutputPaths?.add(outputPath);
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
...cached,
|
|
160
|
-
manifestUrl: buildManifestUrl({
|
|
161
|
-
relDir,
|
|
162
|
-
traceName,
|
|
163
|
-
artifactId: cached.artifactId,
|
|
164
|
-
applyBasePath,
|
|
165
|
-
}),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
211
|
+
return (
|
|
212
|
+
mtime !== undefined && cached.sourceMtims[source.absolutePath] === mtime
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createTraceWorkspace(sources: TraceSourceFile[]): string {
|
|
218
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-work-'));
|
|
219
|
+
try {
|
|
220
|
+
for (const source of sources) {
|
|
221
|
+
fs.copyFileSync(source.absolutePath, path.join(tempDir, source.file));
|
|
168
222
|
}
|
|
223
|
+
return tempDir;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
226
|
+
throw err;
|
|
169
227
|
}
|
|
228
|
+
}
|
|
170
229
|
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
function getOrRunTrace(
|
|
231
|
+
sourceFile: string,
|
|
232
|
+
companionFiles?: string[],
|
|
233
|
+
): TraceResult {
|
|
234
|
+
const sources = resolveTraceSources(sourceFile, companionFiles);
|
|
235
|
+
for (const source of sources) {
|
|
236
|
+
dependencyCollector?.traceFiles?.add(source.absolutePath);
|
|
173
237
|
}
|
|
238
|
+
const sourceFilePath = sources[0].absolutePath;
|
|
239
|
+
const traceName = path.parse(sourceFile).name;
|
|
240
|
+
const relDir = toPosix(path.relative(contentDir, pageDir));
|
|
241
|
+
const cacheKey = cacheKeyForSources(sources);
|
|
174
242
|
|
|
175
|
-
const
|
|
243
|
+
const cached = cache.get(cacheKey);
|
|
244
|
+
if (cached && cacheIsFresh(sources, cached)) {
|
|
245
|
+
const outputPaths = getTraceOutputPaths(
|
|
246
|
+
relDir,
|
|
247
|
+
traceName,
|
|
248
|
+
cached.artifactId,
|
|
249
|
+
cached.totalSteps,
|
|
250
|
+
);
|
|
251
|
+
if (
|
|
252
|
+
materializeTraceOutputs({
|
|
253
|
+
outputPaths,
|
|
254
|
+
targetDistDir: distDir,
|
|
255
|
+
sourceDistDir: cachedTraceSourceDir || distDir,
|
|
256
|
+
})
|
|
257
|
+
) {
|
|
258
|
+
for (const outputPath of outputPaths) {
|
|
259
|
+
dependencyCollector?.generatedOutputPaths?.add(outputPath);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
...cached,
|
|
263
|
+
manifestUrl: buildManifestUrl({
|
|
264
|
+
relDir,
|
|
265
|
+
traceName,
|
|
266
|
+
artifactId: cached.artifactId,
|
|
267
|
+
applyBasePath,
|
|
268
|
+
}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
176
272
|
|
|
177
273
|
log.info`Tracing ${B`${sourceFile}`}`;
|
|
178
274
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
const workspaceDir = createTraceWorkspace(sources);
|
|
276
|
+
let output: string;
|
|
277
|
+
let ignoreFields: Record<string, string[]> | undefined;
|
|
278
|
+
try {
|
|
279
|
+
const workspacePaths = sources.map(source =>
|
|
280
|
+
path.join(workspaceDir, source.file),
|
|
281
|
+
);
|
|
282
|
+
({ output, ignoreFields } = runTraceForFile(
|
|
283
|
+
workspacePaths[0],
|
|
284
|
+
workspacePaths,
|
|
285
|
+
sources.map(source => ({ file: source.file, source: source.source })),
|
|
286
|
+
));
|
|
287
|
+
} finally {
|
|
288
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const language = traceLanguageForFile(sourceFilePath);
|
|
292
|
+
const highlightedSources = sources.map(source => ({
|
|
293
|
+
file: source.file,
|
|
294
|
+
highlightedSource: highlightTraceSource(source.source, language),
|
|
295
|
+
}));
|
|
184
296
|
const traceOutputDir = path.join(distDir, relDir, '_traces', traceName);
|
|
185
297
|
|
|
186
298
|
const traceOutput = chunkTraceOutput(
|
|
@@ -188,8 +300,8 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
188
300
|
traceOutputDir,
|
|
189
301
|
relDir,
|
|
190
302
|
traceName,
|
|
191
|
-
|
|
192
|
-
source,
|
|
303
|
+
sources[0].file,
|
|
304
|
+
sources.map(source => ({ file: source.file, source: source.source })),
|
|
193
305
|
{ chunkSize: DEFAULT_CHUNK_SIZE, ignoreFields },
|
|
194
306
|
);
|
|
195
307
|
for (const outputPath of traceOutput.outputPaths) {
|
|
@@ -204,45 +316,48 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
204
316
|
});
|
|
205
317
|
|
|
206
318
|
const totalSteps = traceOutput.manifest.totalSteps;
|
|
207
|
-
const
|
|
208
|
-
cache.set(
|
|
319
|
+
const sourceMtims = getSourceMtims(sources);
|
|
320
|
+
cache.set(cacheKey, {
|
|
209
321
|
manifestUrl,
|
|
210
322
|
artifactId: traceOutput.artifactId,
|
|
211
|
-
|
|
323
|
+
highlightedSources,
|
|
212
324
|
totalSteps,
|
|
213
|
-
|
|
325
|
+
sourceMtims,
|
|
214
326
|
});
|
|
215
327
|
return {
|
|
216
328
|
manifestUrl,
|
|
217
329
|
artifactId: traceOutput.artifactId,
|
|
218
|
-
|
|
330
|
+
highlightedSources,
|
|
219
331
|
totalSteps,
|
|
220
|
-
|
|
332
|
+
sourceMtims,
|
|
221
333
|
};
|
|
222
334
|
}
|
|
223
335
|
|
|
224
336
|
return {
|
|
225
|
-
renderTrace: (sourceFile: string): string => {
|
|
226
|
-
const
|
|
337
|
+
renderTrace: (sourceFile: string, companionFiles?: string[]): string => {
|
|
338
|
+
const sources = resolveTraceSources(sourceFile, companionFiles);
|
|
339
|
+
for (const source of sources) {
|
|
340
|
+
dependencyCollector?.traceFiles?.add(source.absolutePath);
|
|
341
|
+
}
|
|
342
|
+
const sourceFilePath = sources[0].absolutePath;
|
|
227
343
|
const available = availabilityForFile(sourceFilePath, toolAvailability);
|
|
228
344
|
if (available === false) {
|
|
229
345
|
log.warn`${disabledTraceMessage(sourceFile)}`;
|
|
230
|
-
|
|
231
|
-
throw new Error(`Trace target not found: ${sourceFilePath}`);
|
|
232
|
-
}
|
|
233
|
-
const source = fs.readFileSync(sourceFilePath, 'utf-8');
|
|
346
|
+
const language = traceLanguageForFile(sourceFilePath);
|
|
234
347
|
return renderTraceWidgetHtml({
|
|
235
|
-
|
|
236
|
-
source,
|
|
237
|
-
|
|
238
|
-
),
|
|
348
|
+
highlightedSources: sources.map(source => ({
|
|
349
|
+
file: source.file,
|
|
350
|
+
highlightedSource: highlightTraceSource(source.source, language),
|
|
351
|
+
})),
|
|
239
352
|
});
|
|
240
353
|
}
|
|
241
354
|
|
|
242
|
-
const { manifestUrl,
|
|
243
|
-
|
|
355
|
+
const { manifestUrl, highlightedSources, totalSteps } = getOrRunTrace(
|
|
356
|
+
sourceFile,
|
|
357
|
+
companionFiles,
|
|
358
|
+
);
|
|
244
359
|
return renderTraceWidgetHtml({
|
|
245
|
-
|
|
360
|
+
highlightedSources,
|
|
246
361
|
manifestUrl,
|
|
247
362
|
totalSteps,
|
|
248
363
|
});
|
|
@@ -7,9 +7,9 @@ import type {
|
|
|
7
7
|
export interface TraceCacheEntry {
|
|
8
8
|
manifestUrl: string;
|
|
9
9
|
artifactId: string;
|
|
10
|
-
highlightedSource: string;
|
|
10
|
+
highlightedSources: { file: string; highlightedSource: string }[];
|
|
11
11
|
totalSteps: number;
|
|
12
|
-
|
|
12
|
+
sourceMtims: Record<string, number>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type TraceCache = Map<string, TraceCacheEntry>;
|
package/build/watch/compiler.ts
CHANGED
|
@@ -23,7 +23,11 @@ export function invalidateTraceCacheForBatch(
|
|
|
23
23
|
for (const change of batch.changes) {
|
|
24
24
|
const sourcePath = path.resolve(change.path);
|
|
25
25
|
if (isTraceSourceFile(sourcePath)) {
|
|
26
|
-
traceCache
|
|
26
|
+
for (const [cacheKey, entry] of traceCache) {
|
|
27
|
+
if (sourcePath in entry.sourceMtims) {
|
|
28
|
+
traceCache.delete(cacheKey);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Problem 2 # The second example problem
|
|
2
2
|
|
|
3
|
-
Here's the `_pr2.md` partial. If we
|
|
4
|
-
relative to this file, **not** the page or partial that included us.
|
|
3
|
+
Here's the `_pr2.md` partial. If we include another partial here, the path is
|
|
4
|
+
resolved relative to this file, **not** the page or partial that included us.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
{{{ _foobar.md }}}
|