@abreen/tada 1.13.9 → 1.14.1

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.
@@ -1,4 +1,4 @@
1
- import type { TraceManifest, TraceChunkEntry } from './types';
1
+ import type { TraceManifest, TraceChunkEntry, TraceOutputEvent } from './types';
2
2
  import { globals } from '../globals';
3
3
 
4
4
  interface WidgetState {
@@ -9,7 +9,7 @@ interface WidgetState {
9
9
 
10
10
  interface WidgetElements {
11
11
  root: HTMLElement;
12
- source: HTMLElement;
12
+ sourceWrapper: HTMLElement;
13
13
  controls: HTMLElement;
14
14
  content: HTMLElement;
15
15
  diagram: HTMLElement;
@@ -77,6 +77,22 @@ function updateSourceHighlight(panel: HTMLElement, currentLine: number): void {
77
77
  }
78
78
  }
79
79
 
80
+ function updateVisibleSourcePanel(
81
+ sourceWrapper: HTMLElement,
82
+ currentFile: string,
83
+ ): HTMLElement | null {
84
+ let activePanel: HTMLElement | null = null;
85
+ sourceWrapper.querySelectorAll('.trace-source').forEach(panel => {
86
+ const sourcePanel = panel as HTMLElement;
87
+ const matches = sourcePanel.dataset.traceSourceFile === currentFile;
88
+ sourcePanel.hidden = !matches;
89
+ if (matches) {
90
+ activePanel = sourcePanel;
91
+ }
92
+ });
93
+ return activePanel;
94
+ }
95
+
80
96
  function updateStepControls(
81
97
  container: HTMLElement,
82
98
  currentStep: number,
@@ -202,13 +218,13 @@ function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
202
218
 
203
219
  function updateOutput(
204
220
  content: HTMLElement,
205
- output: string,
221
+ output: TraceOutputEvent[],
206
222
  doc: Document,
207
223
  ): void {
208
224
  const existing = content.querySelector(
209
225
  ':scope > .trace-output',
210
226
  ) as HTMLPreElement | null;
211
- if (!output) {
227
+ if (output.length === 0) {
212
228
  existing?.remove();
213
229
  return;
214
230
  }
@@ -218,7 +234,16 @@ function updateOutput(
218
234
  outputElement.className = 'trace-output';
219
235
  content.append(outputElement);
220
236
  }
221
- outputElement.textContent = output;
237
+ outputElement.replaceChildren(
238
+ ...output.map(event => {
239
+ const span = doc.createElement('span');
240
+ if (event.stream === 'stderr') {
241
+ span.className = 'trace-output-stderr';
242
+ }
243
+ span.textContent = event.text;
244
+ return span;
245
+ }),
246
+ );
222
247
  }
223
248
 
224
249
  function renderWidgetState(
@@ -227,21 +252,25 @@ function renderWidgetState(
227
252
  doc: Document,
228
253
  ): void {
229
254
  const entry = getStep(state);
230
- updateSourceHighlight(elements.source, entry.line);
255
+ const sourcePanel = updateVisibleSourcePanel(
256
+ elements.sourceWrapper,
257
+ entry.file,
258
+ );
259
+ if (sourcePanel) {
260
+ updateSourceHighlight(sourcePanel, entry.line);
261
+ }
231
262
  updateStepControls(
232
263
  elements.controls,
233
264
  state.currentStep,
234
265
  state.manifest.totalSteps,
235
266
  doc,
236
267
  );
237
- let output = '';
268
+ const output: TraceOutputEvent[] = [];
238
269
  for (let i = 0; i <= state.currentStep; i++) {
239
270
  const ci = Math.floor(i / state.manifest.chunkSize);
240
271
  const off = i % state.manifest.chunkSize;
241
272
  const s = state.chunks.get(ci)![off];
242
- if (s.stdout) {
243
- output += s.stdout;
244
- }
273
+ output.push(...(s.output ?? []));
245
274
  }
246
275
  updateOutput(elements.content, output, doc);
247
276
 
@@ -277,7 +306,9 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
277
306
 
278
307
  await loadChunk(state, manifestUrl, 0);
279
308
 
280
- const source = root.querySelector('.trace-source') as HTMLElement;
309
+ const sourceWrapper = root.querySelector(
310
+ '.trace-source-wrapper',
311
+ ) as HTMLElement;
281
312
  const content = root.querySelector('.trace-content') as HTMLElement;
282
313
  const diagram = root.querySelector('.trace-diagram') as HTMLElement;
283
314
 
@@ -321,7 +352,13 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
321
352
  next.focus();
322
353
  });
323
354
 
324
- const elements: WidgetElements = { root, source, controls, content, diagram };
355
+ const elements: WidgetElements = {
356
+ root,
357
+ sourceWrapper,
358
+ controls,
359
+ content,
360
+ diagram,
361
+ };
325
362
 
326
363
  renderWidgetState(state, elements, doc);
327
364
  }
@@ -141,6 +141,16 @@
141
141
  content: 'Output';
142
142
  }
143
143
 
144
+ .trace-output-stderr {
145
+ color: hsl(0deg 70% 45%);
146
+ }
147
+
148
+ @media (prefers-color-scheme: dark) {
149
+ .trace-output-stderr {
150
+ color: hsl(0deg 85% 72%);
151
+ }
152
+ }
153
+
144
154
  .trace-memory {
145
155
  max-width: 100%;
146
156
  height: auto;
@@ -1,7 +1,12 @@
1
1
  export interface TraceManifest {
2
2
  totalSteps: number;
3
3
  chunkSize: number;
4
- sourceFile: string;
4
+ primaryFile: string;
5
+ sources: TraceSource[];
6
+ }
7
+
8
+ export interface TraceSource {
9
+ file: string;
5
10
  source: string;
6
11
  lineToSteps: Record<number, number[]>;
7
12
  }
@@ -11,7 +16,13 @@ export interface TraceStep {
11
16
  file: string;
12
17
  stack: TraceStackFrame[];
13
18
  heap: Record<string, TraceHeapObject>;
14
- stdout: string;
19
+ output?: TraceOutputEvent[];
20
+ transientHeapRoots?: string[];
21
+ }
22
+
23
+ export interface TraceOutputEvent {
24
+ stream: 'stdout' | 'stderr';
25
+ text: string;
15
26
  }
16
27
 
17
28
  export interface TraceStackFrame {
@@ -37,11 +48,12 @@ export type TraceValue =
37
48
  export type TraceHeapObject =
38
49
  | { type: string; elements: TraceValue[] }
39
50
  | { type: string; fields: Record<string, TraceValue> }
40
- | { type: string; value: string };
51
+ | { type: string; value: string | number | boolean };
41
52
 
42
53
  /** A single entry in a chunk file (new format with precomputed SVG). */
43
54
  export interface TraceChunkEntry {
55
+ file: string;
44
56
  line: number;
45
- stdout: string;
57
+ output?: TraceOutputEvent[];
46
58
  svg: string;
47
59
  }
@@ -1,57 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import _ from 'lodash';
4
- import { isPartial } from './file-types';
5
- import { stripHtmlComments } from './render';
6
- import type { RenderDependencyCollector } from '../types';
7
-
8
- const MAX_INCLUDE_DEPTH = 10;
9
-
10
- export function createIncludeFunction(
11
- callerFilePath: string,
12
- templateParams: Record<string, unknown>,
13
- collector?: RenderDependencyCollector,
14
- depth: number = 0,
15
- ): (relativePath: string) => string {
16
- return (relativePath: string): string => {
17
- const callerDir = path.dirname(callerFilePath);
18
- const resolved = path.resolve(callerDir, relativePath);
19
-
20
- if (!fs.existsSync(resolved)) {
21
- throw new Error(
22
- `${callerFilePath}: partial not found: ${relativePath} (resolved to ${resolved})`,
23
- );
24
- }
25
- if (!isPartial(resolved)) {
26
- throw new Error(
27
- `${callerFilePath}: include target must start with "_": ${relativePath}`,
28
- );
29
- }
30
- collector?.partials?.add(resolved);
31
- if (depth >= MAX_INCLUDE_DEPTH) {
32
- throw new Error(
33
- `${callerFilePath}: maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded`,
34
- );
35
- }
36
-
37
- const raw = fs.readFileSync(resolved, 'utf-8');
38
- const content = stripHtmlComments(raw);
39
-
40
- const nestedInclude = createIncludeFunction(
41
- resolved,
42
- templateParams,
43
- collector,
44
- depth + 1,
45
- );
46
- const nestedParams = { ...templateParams, include: nestedInclude };
47
-
48
- try {
49
- return _.template(content)(nestedParams);
50
- } catch (err) {
51
- throw new Error(
52
- `${resolved}: Lodash template error in partial: ${(err as Error).message}`,
53
- { cause: err },
54
- );
55
- }
56
- };
57
- }
@@ -1,3 +0,0 @@
1
- <p>Here's an HTML partial in a subdirectory.</p>
2
-
3
- <p>Here's the title of the page including this partial: <%= page.title %></p>