@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.
@@ -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 objects reachable only via args
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 that are reachable from visible variables, or that
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) || !argsOnly.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 && isStringObject(obj)) {
298
- // Strings: just quoted text, no box
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="0" y="${height / 2}" ` +
301
- `dominant-baseline="central">${escapeXml(`"${obj.value}"`)}</text>`,
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 = rectBoundaryPoint(
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 = style === 'fg' ? 'trace-arrowhead' : 'trace-arrowhead-dim';
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(#trace-arrowhead-dim)"/>`
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(#trace-arrowhead-dim)"/>`
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 = rectBoundaryPoint(
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(#trace-arrowhead-dim)"/>`
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
- '<marker id="trace-arrowhead" markerWidth="8" markerHeight="6" ' +
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
- '<marker id="trace-arrowhead-dim" markerWidth="8" markerHeight="6" ' +
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);
@@ -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
- mtime: number;
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
- function sourceFileNameForTrace(fileName: string): string {
93
- const { name, ext } = path.parse(fileName);
94
- return `${name}${ext}`;
93
+ interface TraceSourceFile {
94
+ requestedPath: string;
95
+ absolutePath: string;
96
+ file: string;
97
+ source: string;
95
98
  }
96
99
 
97
100
  function runTraceForFile(
98
- filePath: string,
99
- source: string,
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(filePath).toLowerCase();
105
+ const ext = path.extname(primaryFilePath).toLowerCase();
102
106
  if (ext === '.java') {
103
- validateJavaTraceTarget(filePath, source);
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(filePath, path.parse(filePath).name),
106
- ignoreFields: parseIgnoreFields(source),
119
+ output: runJavaTrace(tracedFilePaths, path.parse(primaryFilePath).name),
120
+ ignoreFields,
107
121
  };
108
122
  }
109
123
  if (ext === '.py') {
110
- return { output: runPythonTrace(filePath) };
124
+ return { output: runPythonTrace(primaryFilePath, tracedFilePaths) };
111
125
  }
112
- throw new Error(`Unsupported trace source: ${filePath}`);
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 getOrRunTrace(sourceFile: string): TraceResult {
131
- const sourceFilePath = path.resolve(pageDir, sourceFile);
132
- dependencyCollector?.traceFiles?.add(sourceFilePath);
133
- const traceName = path.parse(sourceFile).name;
134
- const relDir = toPosix(path.relative(contentDir, pageDir));
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
- const cached = cache.get(sourceFilePath);
137
- if (cached) {
138
- const mtime = fs.statSync(sourceFilePath, {
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
- if (mtime !== undefined && mtime === cached.mtime) {
142
- const outputPaths = getTraceOutputPaths(
143
- relDir,
144
- traceName,
145
- cached.artifactId,
146
- cached.totalSteps,
147
- );
148
- if (
149
- materializeTraceOutputs({
150
- outputPaths,
151
- targetDistDir: distDir,
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
- if (!fs.existsSync(sourceFilePath)) {
172
- throw new Error(`Trace target not found: ${sourceFilePath}`);
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 source = fs.readFileSync(sourceFilePath, 'utf-8');
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 { output, ignoreFields } = runTraceForFile(sourceFilePath, source);
180
- const highlightedSource = highlightTraceSource(
181
- source,
182
- traceLanguageForFile(sourceFilePath),
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
- sourceFileNameForTrace(sourceFile),
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 mtime = fs.statSync(sourceFilePath).mtimeMs;
208
- cache.set(sourceFilePath, {
319
+ const sourceMtims = getSourceMtims(sources);
320
+ cache.set(cacheKey, {
209
321
  manifestUrl,
210
322
  artifactId: traceOutput.artifactId,
211
- highlightedSource,
323
+ highlightedSources,
212
324
  totalSteps,
213
- mtime,
325
+ sourceMtims,
214
326
  });
215
327
  return {
216
328
  manifestUrl,
217
329
  artifactId: traceOutput.artifactId,
218
- highlightedSource,
330
+ highlightedSources,
219
331
  totalSteps,
220
- mtime,
332
+ sourceMtims,
221
333
  };
222
334
  }
223
335
 
224
336
  return {
225
- renderTrace: (sourceFile: string): string => {
226
- const sourceFilePath = path.resolve(pageDir, sourceFile);
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
- if (!fs.existsSync(sourceFilePath)) {
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
- highlightedSource: highlightTraceSource(
236
- source,
237
- traceLanguageForFile(sourceFilePath),
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, highlightedSource, totalSteps } =
243
- getOrRunTrace(sourceFile);
355
+ const { manifestUrl, highlightedSources, totalSteps } = getOrRunTrace(
356
+ sourceFile,
357
+ companionFiles,
358
+ );
244
359
  return renderTraceWidgetHtml({
245
- highlightedSource,
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
- mtime: number;
12
+ sourceMtims: Record<string, number>;
13
13
  }
14
14
 
15
15
  export type TraceCache = Map<string, TraceCacheEntry>;
@@ -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.delete(sourcePath);
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
  }
@@ -2,4 +2,4 @@
2
2
 
3
3
  Here's the `_pr1.md` partial. From this partial we can use the page-level
4
4
  front matter fields from the page that includes us. The page that called
5
- `include()` has a title of: <%= page.title %>
5
+ this partial has a title of: <%= page.title %>
@@ -8,6 +8,6 @@ author: alex
8
8
 
9
9
  Here is a page constructed from multiple partials.
10
10
 
11
- <%= include('_pr1.md') %>
11
+ {{{ _pr1.md }}}
12
12
 
13
- <%= include('subdir/_pr2.md') %>
13
+ {{{ subdir/_pr2.md }}}
@@ -0,0 +1,3 @@
1
+ Here's a nested Markdown partial in a subdirectory.
2
+
3
+ Here's the title of the page including this partial: <%= page.title %>
@@ -1,6 +1,6 @@
1
1
  ## Problem 2 # The second example problem
2
2
 
3
- Here's the `_pr2.md` partial. If we use `include()` here, the path is resolved
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
- <%= include('_foobar.html') %>
6
+ {{{ _foobar.md }}}