Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c46ab4
feat(console): show line numbers next to console output
iahmedgamal Jul 8, 2026
34f97c4
fix: eslint
iahmedgamal Jul 10, 2026
ea237ed
feat(console): annotate output with source line numbers
iahmedgamal Jul 10, 2026
768d6cb
feat(console): implement toPositiveLineNumber utility and refactor co…
iahmedgamal Jul 10, 2026
f2bd38c
Merge branch 'develop' into feat/console-line-numbers
iahmedgamal Jul 10, 2026
40a4f61
Merge branch 'develop' into feat/console-line-numbers
iahmedgamal Jul 12, 2026
cac2bab
feat: fix all comments
iahmedgamal Jul 13, 2026
15b04da
feat(console): fix formatting
iahmedgamal Jul 13, 2026
740c893
remove scriptFilename for now
hatemhosny Jul 14, 2026
88cdc94
avoid unnecessarily marking inline scripts
hatemhosny Jul 14, 2026
5742cd7
fix aligning console line number in multiline logs
hatemhosny Jul 14, 2026
edacdd5
fix setting position in codemirror
hatemhosny Jul 14, 2026
d5bd218
minor cleanups
hatemhosny Jul 14, 2026
e8bd196
feat(console): fixed pullfrog comments
iahmedgamal Jul 14, 2026
26099a0
Merge branch 'feat/console-line-numbers' of https://github.com/iahmed…
hatemhosny Jul 14, 2026
70264ef
remove scriptFilename
hatemhosny Jul 14, 2026
c6f0788
feat(console): enhance error handling with precise line number mapping
iahmedgamal Jul 14, 2026
f93b011
feat(console): JS-compatible script types to avoid breaking Python an…
iahmedgamal Jul 14, 2026
8265cfe
fix(e2e): fix flaky tests with bounded range
iahmedgamal Jul 14, 2026
5ddb61f
Merge branch 'develop' into feat/console-line-numbers
hatemhosny Jul 15, 2026
28eaff3
fix(e2e): replace exact line number assertions with range-based chec…
iahmedgamal Jul 15, 2026
7ceae1b
fix setting editor position
hatemhosny Jul 18, 2026
0c677a3
preserve line number when converting cjs to esm
hatemhosny Jul 18, 2026
f2d5255
Merge branch 'develop' into feat/console-line-numbers
hatemhosny Jul 18, 2026
a9c1834
Merge branch 'feat/console-line-numbers' of https://github.com/iahmed…
hatemhosny Jul 18, 2026
f03f735
fix cjs2esm
hatemhosny Jul 18, 2026
cfa7c35
minor cleanups
hatemhosny Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
500 changes: 500 additions & 0 deletions e2e/specs/console-line-logs.spec.ts

Large diffs are not rendered by default.

247 changes: 247 additions & 0 deletions src/livecodes/compiler/__tests__/source-maps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { buildSourceLineMap, decodeVlq, toPositiveLineNumber } from '../source-maps';

describe('toPositiveLineNumber', () => {
test('returns the integer for a positive number', () => {
// Arrange
const input = 7;

// Act
const result = toPositiveLineNumber(input);

// Assert
expect(result).toBe(7);
});

test('truncates a positive float to its integer part', () => {
// Arrange
const input = 3.9;

// Act
const result = toPositiveLineNumber(input);

// Assert
expect(result).toBe(3);
});

test('returns undefined for zero', () => {
expect(toPositiveLineNumber(0)).toBeUndefined();
});

test('returns undefined for a negative number', () => {
expect(toPositiveLineNumber(-1)).toBeUndefined();
});

test('returns undefined for NaN', () => {
expect(toPositiveLineNumber(NaN)).toBeUndefined();
});

test('returns undefined for Infinity', () => {
expect(toPositiveLineNumber(Infinity)).toBeUndefined();
});

test('returns undefined for a string', () => {
expect(toPositiveLineNumber('7')).toBeUndefined();
});

test('returns undefined for undefined', () => {
expect(toPositiveLineNumber(undefined)).toBeUndefined();
});

test('returns undefined for null', () => {
expect(toPositiveLineNumber(null)).toBeUndefined();
});
});

describe('decodeVlq', () => {
test('decodes a single positive value', () => {

Check warning on line 56 in src/livecodes/compiler/__tests__/source-maps.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 3 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=live-codes_livecodes&issues=AZ9dorSTzjILkA76eHbx&open=AZ9dorSTzjILkA76eHbx&pullRequest=1008
// Arrange
// 'A' = 0 in base64, VLQ value 0
const input = 'A';
const startPos = 0;

// Act
const result = decodeVlq(input, startPos);

// Assert
expect(result).toEqual([0, 1]);
});

test('decodes a positive single-digit VLQ', () => {
// Arrange
// 'C' = 2 in base64. VLQ: low bit = sign (0 = positive), remaining = 1 β†’ value 1
const input = 'C';
const startPos = 0;

// Act
const result = decodeVlq(input, startPos);

// Assert
expect(result).toEqual([1, 1]);
});

test('decodes a negative single-digit VLQ', () => {
// Arrange
// 'D' = 3 in base64. VLQ: low bit = 1 (negative), remaining = 1 β†’ value -1
const input = 'D';
const startPos = 0;

// Act
const result = decodeVlq(input, startPos);

// Assert
expect(result).toEqual([-1, 1]);
});

test('decodes a multi-character VLQ (continuation bit set)', () => {
// Arrange
// 'yB' encodes value 25:
// 'y' = 50 = 0b110010 β†’ continuation bit set, low 5 bits = 0b10010 = 18
// 'B' = 1 = 0b000001 β†’ continuation bit clear, value = 1
// combined: (1 << 5) | 18 = 50, sign bit (low) = 0 β†’ positive β†’ 50 >> 1 = 25
const input = 'yB';
const startPos = 0;

// Act
const result = decodeVlq(input, startPos);

// Assert
expect(result).toEqual([25, 2]);
});

test('advances position correctly when starting mid-string', () => {
// Arrange
const input = 'AAA';
const startPos = 1;

// Act
const [value, nextPos] = decodeVlq(input, startPos);

// Assert
expect(value).toBe(0);
expect(nextPos).toBe(2);
});

test('stops at invalid character', () => {
// Arrange
// '!' is not a base64 VLQ character β†’ indexOf returns -1 β†’ loop breaks immediately
const input = '!';
const startPos = 0;

// Act
const [value, nextPos] = decodeVlq(input, startPos);

// Assert
expect(value).toBe(0);
expect(nextPos).toBe(1); // pos was incremented before the break
});
});

describe('buildSourceLineMap', () => {
test('returns empty map for invalid JSON', () => {
// Arrange
const input = 'not json';

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.size).toBe(0);
});

test('returns empty map for missing mappings field', () => {
// Arrange
const input = JSON.stringify({ version: 3 });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.size).toBe(0);
});

test('returns empty map for empty mappings string', () => {
// Arrange
const input = JSON.stringify({ mappings: '' });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.size).toBe(0);
});

test('maps compiled line 1 to original line 1 for identity mapping', () => {
// Arrange
// Minimal source map: one segment on compiled line 1
// Segment "AAAA": col=0, sources=0, line=0, col=0 β†’ originalLine delta=0 β†’ line 1
const input = JSON.stringify({ mappings: 'AAAA' });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.get(1)).toBe(1);
});

test('maps compiled lines to correct original lines across multiple compiled lines', () => {
// Arrange
// TypeScript-like: 2 preamble compiled lines (no mapping), then user code starts
// mappings: ";;AACA" β†’ compiled line 1 empty, line 2 empty, line 3 β†’ original line 2
// "AACA": col=0, sources=0, line delta=+1 (from 0) β†’ originalLine=1 β†’ line 2 (1-indexed)
const input = JSON.stringify({ mappings: ';;AACA' });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.get(1)).toBeUndefined();
expect(map.get(2)).toBeUndefined();
expect(map.get(3)).toBe(2);
});

test('only records the first segment per compiled line', () => {
// Arrange
// Two segments on compiled line 1: first maps to original line 1, second to line 3
// "AAAA,AAEA": first=line 1, second=line delta +2 β†’ original line 3
const input = JSON.stringify({ mappings: 'AAAA,AAEA' });

// Act
const map = buildSourceLineMap(input);

// Assert
// First segment wins
expect(map.get(1)).toBe(1);
});

test('handles a realistic TypeScript source map structure', () => {
// Arrange
// Simulate: TS source has 3 lines, compiled output has 3 lines starting at the same positions
// Each compiled line maps back to the corresponding original line
// mappings: "AAAA;AACA;AACA"
// Line 1: AAAA β†’ delta=0 β†’ original line 1
// Line 2: AACA β†’ delta=+1 β†’ original line 2
// Line 3: AACA β†’ delta=+1 β†’ original line 3
const input = JSON.stringify({ mappings: 'AAAA;AACA;AACA' });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.get(1)).toBe(1);
expect(map.get(2)).toBe(2);
expect(map.get(3)).toBe(3);
});

test('skips segments with only one field (no source info)', () => {
// Arrange
// A segment with only a generated column (no sources index, no line) should be skipped
// "A" = only generated column field, no further fields
const input = JSON.stringify({ mappings: 'A' });

// Act
const map = buildSourceLineMap(input);

// Assert
expect(map.get(1)).toBeUndefined();
});
});
10 changes: 5 additions & 5 deletions src/livecodes/compiler/import-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,26 +286,26 @@ export const cjs2esm = (code: string) => {
[
`import * as __requires_${i} from '${id}';`,
`const __requires_${i}_default = __requires_${i}.default;`,
].join('\n'),
].join(' '),
)
.join('\n');
.join(' ');
const lookup = `const __requires_lookup = { ${requires
.map((id, i) => `'${id}': __requires_${i}_default || __requires_${i}`)
.join(', ')} };`;

const require = `window.require = window.require || ((id) => {
if (id in __requires_lookup) return __requires_lookup[id];
throw new Error(\`Cannot require modules dynamically (\${id})\`);
});`;
});`.replaceAll('\n', '');

return [
imports,
lookup,
require,
`const exports = {}; const module = { exports };`,
code,
`export default module.exports;`,
].join('\n\n');
`\nexport default module.exports;`,
].join(' ');
};

export const createCSSModulesImportMap = (
Expand Down
Loading
Loading