import dedent from 'dedent'
import { SourceMapGenerator, type RawSourceMap } from 'source-map-js'
import { expect, test } from 'vitest'
import { visualizeSourceMap, visualizeSourceMapRanges } from './visualize-source-map'
const css = dedent
function sourceMap({
sources,
mappings,
}: {
sources: Record<string, string>
mappings: {
source: string
original: [line: number, column: number]
generated: [line: number, column: number]
}[]
}) {
let generator = new SourceMapGenerator()
for (let mapping of mappings) {
generator.addMapping({
source: mapping.source,
original: {
line: mapping.original[0],
column: mapping.original[1],
},
generated: {
line: mapping.generated[0],
column: mapping.generated[1],
},
})
}
for (let [source, content] of Object.entries(sources)) {
generator.setSourceContent(source, content)
}
return JSON.parse(generator.toString()) as RawSourceMap
}
test('visualizes single-line ranges', () => {
expect(
visualizeSourceMapRanges(
{
'input.css': css`.foo { color: red; }`,
},
css`
.foo {
color: red;
}
`,
[
{
original: {
source: 'input.css',
start: [1, 7],
end: [1, 17],
},
generated: {
start: [2, 2],
end: [2, 12],
},
},
],
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
1 .foo { |
2 color: red; | 1 .foo { color: red; }
^^^^^^^^^^ A @ 2:2-12 | ^^^^^^^^^^ A @ 1:7-17
3 } |
"
`)
})
test('visualizes multi-line ranges', () => {
expect(
visualizeSourceMapRanges(
{
'input.css': css`
`,
},
css`
`,
[
{
original: {
source: 'input.css',
start: [1, 0],
end: [2, 7],
},
generated: {
start: [1, 0],
end: [2, 7],
},
},
],
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
1 /*! foo | 1 /*! foo
^^^^^^^ A @ 1:0-2:7 | ^^^^^^^ A @ 1:0-2:7
2 bar */ | 2 bar */
^^^^^^^ A | ^^^^^^^ A
"
`)
})
test('visualizes multiple sources', () => {
expect(
visualizeSourceMapRanges(
{
'utilities.css': css`
@tailwind utilities;
`,
'input.css': css`
@utility custom {
color: red;
}
`,
},
css`
.flex {
display: flex;
}
.custom {
color: red;
}
`,
[
{
original: {
source: 'utilities.css',
start: [1, 0],
end: [1, 19],
},
generated: {
start: [1, 0],
end: [1, 6],
},
},
{
original: {
source: 'utilities.css',
start: [1, 0],
end: [1, 19],
},
generated: {
start: [2, 2],
end: [2, 15],
},
},
{
original: {
source: 'input.css',
start: [2, 2],
end: [2, 12],
},
generated: {
start: [5, 2],
end: [5, 12],
},
},
],
),
).toMatchInlineSnapshot(`
"
output.css | original
|
| --- utilities.css ---
1 .flex { | 1 @tailwind utilities;
^^^^^^ A @ 1:0-6 | ^^^^^^^^^^^^^^^^^^^ A @ 1:0-19
2 display: flex; |
^^^^^^^^^^^^^ A @ 2:2-15 |
3 } |
4 .custom { |
| --- input.css ---
5 color: red; | 2 color: red;
^^^^^^^^^^ B @ 5:2-12 | ^^^^^^^^^^ B @ 2:2-12
| 3 }
6 } |
"
`)
})
test('visualizes unmapped source ranges', () => {
expect(
visualizeSourceMapRanges(
{
'input.css': css`.foo { color: red; }`,
},
css`
.foo {
color: red;
}
`,
[
{
original: {
source: 'input.css',
start: [1, 7],
end: [1, 17],
},
generated: null,
},
],
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
unmapped A | 1 .foo { color: red; }
| ^^^^^^^^^^ A @ 1:7-17
1 .foo { |
2 color... |
3 } |
"
`)
})
test('visualizes source-map points as generated segments', () => {
let generated = css`
`
expect(
visualizeSourceMap(
sourceMap({
sources: {
'input.css': css`
`,
},
mappings: [
{ source: 'input.css', original: [1, 0], generated: [1, 0] },
{ source: 'input.css', original: [2, 0], generated: [2, 0] },
{ source: 'input.css', original: [2, 7], generated: [2, 7] },
],
}),
generated,
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
1 /*! foo | 1 /*! foo
^^^^^^^ A @ 1:0-2:0 | ^^^^^^^ A @ 1:0-2:0
2 bar */ | 2 bar */
^^^^^^^ B @ 2:0-7 | ^^^^^^^ B @ 2:0-7
"
`)
})
test('visualizes final source-map point through end of line', () => {
expect(
visualizeSourceMap(
sourceMap({
sources: {
'input.css': css`aaaa`,
},
mappings: [{ source: 'input.css', original: [1, 0], generated: [1, 0] }],
}),
css`aaaa`,
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
1 aaaa | 1 aaaa
^^^^ A @ 1:0-2:0 | ^ A @ 1:0
"
`)
})
test('visualizes duplicate source-map points without hiding them', () => {
expect(
visualizeSourceMap(
sourceMap({
sources: {
'input.css': css`
aaaa
bbbb
`,
},
mappings: [
{ source: 'input.css', original: [1, 0], generated: [1, 0] },
{ source: 'input.css', original: [2, 0], generated: [1, 0] },
{ source: 'input.css', original: [2, 4], generated: [1, 4] },
],
}),
css`
bbbb
`,
),
).toMatchInlineSnapshot(`
"
output.css | input.css
|
1 bbbb | 1 aaaa
^ A @ 1:0 | ^ A @ 1:0
1 bbbb | 2 bbbb
^^^^ B @ 1:0-4 | ^^^^ B @ 2:0-4
"
`)
})