Skip to content

Commit 5649061

Browse files
Hugos68antfubot
andauthored
feat(twoslash-svelte): markup cutting (#91)
Co-authored-by: Anthony Fu (via agent) <reg-github-bot@antfu.me>
1 parent f86ad13 commit 5649061

37 files changed

Lines changed: 516 additions & 76 deletions

packages/twoslash-svelte/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@
4646
"start": "esno src/index.ts"
4747
},
4848
"peerDependencies": {
49+
"svelte": ">=5",
4950
"typescript": "*"
5051
},
5152
"dependencies": {
5253
"@jridgewell/sourcemap-codec": "catalog:",
5354
"@volar/language-core": "catalog:",
55+
"estree-walker": "catalog:",
5456
"svelte2tsx": "catalog:",
5557
"twoslash": "workspace:*",
5658
"twoslash-protocol": "workspace:*"

packages/twoslash-svelte/src/index.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { createRequire } from 'node:module'
44
import { decode } from '@jridgewell/sourcemap-codec'
55
import { SourceMap } from '@volar/language-core'
66
import { svelte2tsx } from 'svelte2tsx'
7+
import { parse } from 'svelte/compiler'
78
import { createTwoslasher as createTwoSlasherBase, defaultCompilerOptions, defaultHandbookOptions, findFlagNotations, findQueryMarkers } from 'twoslash'
89
import { createPositionConverter, removeCodeRanges, resolveNodePositions } from 'twoslash-protocol'
10+
import { findCutNotations } from 'twoslash/core'
911
import ts from 'typescript'
1012

1113
export interface CreateTwoslashSvelteOptions extends CreateTwoslashOptions {
@@ -78,6 +80,7 @@ export function createTwoslasher(createOptions: CreateTwoslashSvelteOptions = {}
7880
}
7981

8082
const compiled = svelte2tsx(strippedCode)
83+
8184
const map = generateSourceMap(strippedCode, compiled.code, compiled.map.mappings)
8285

8386
function getLastGeneratedOffset(pos: number) {
@@ -146,16 +149,20 @@ export function createTwoslasher(createOptions: CreateTwoslashSvelteOptions = {}
146149
})
147150
.filter(value => value != null)
148151

152+
const ast = parse(strippedCode, { modern: true })
153+
154+
findCutNotations(code, sourceMeta, {
155+
reCutBefore: /^<!--\s*---cut(-before)?---\s*-->$/,
156+
reCutAfter: /^<!--\s*---cut-after---\s*-->$/,
157+
reCutStart: /^<!--\s*---cut-start---\s*-->$/,
158+
reCutEnd: /^<!--\s*---cut-end---\s*-->$/,
159+
})
160+
149161
const mappedRemovals = [
150162
...sourceMeta.removals,
151-
...result.meta.removals.map((r) => {
152-
const start = get(map.toSourceLocation(r[0]), 0)?.[0] ?? code.match(/(?<=<script[\s\S]*>\s)/)?.index
153-
const end = get(map.toSourceLocation(r[1]), 0)?.[0]
154-
if (start == null || end == null || start < 0 || end < 0 || start >= end) {
155-
return undefined
156-
}
157-
return [start, end] as Range
158-
}).filter(value => value != null),
163+
...result.meta.removals
164+
.map(r => mapRemovalToSource(r, map, ast))
165+
.filter(value => value != null),
159166
]
160167

161168
if (!options.handbookOptions?.keepNotations) {
@@ -275,3 +282,49 @@ function generateSourceMap(
275282
}
276283
return new SourceMap(mappings)
277284
}
285+
286+
function mapRemovalToSource(
287+
r: Range,
288+
map: SourceMap,
289+
ast: ReturnType<typeof parse>,
290+
): Range | undefined {
291+
const instanceContent = hasRange(ast.instance?.content) ? ast.instance.content : undefined
292+
const moduleContent = hasRange(ast.module?.content) ? ast.module.content : undefined
293+
294+
let start = get(map.toSourceLocation(r[0]), 0)?.[0]
295+
let end = get(map.toSourceLocation(r[1]), 0)?.[0]
296+
297+
// Determine which script block this removal belongs to
298+
const scriptContent = start != null
299+
? (instanceContent && start >= instanceContent.start && start <= instanceContent.end ? instanceContent : undefined)
300+
?? (moduleContent && start >= moduleContent.start && start <= moduleContent.end ? moduleContent : undefined)
301+
: instanceContent ?? moduleContent
302+
303+
// Fall back to script boundaries for unmappable positions
304+
start ??= scriptContent?.start
305+
if (end == null && scriptContent != null && r[1] > scriptContent.end)
306+
end = scriptContent.end
307+
308+
if (start == null || end == null || start < 0 || end < 0 || start >= end)
309+
return undefined
310+
311+
// Clamp to script content boundaries to protect <script> tags
312+
if (scriptContent) {
313+
start = Math.max(start, scriptContent.start)
314+
end = Math.min(end, scriptContent.end)
315+
}
316+
317+
if (start >= end)
318+
return undefined
319+
320+
return [start, end]
321+
}
322+
323+
function hasRange(range: unknown): range is { start: number, end: number } {
324+
return typeof range === 'object'
325+
&& range != null
326+
&& 'start' in range
327+
&& 'end' in range
328+
&& typeof range.start === 'number'
329+
&& typeof range.end === 'number'
330+
}

packages/twoslash-svelte/test/fixtures/completion.svelte renamed to packages/twoslash-svelte/test/fixtures/script/completion.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
<script>
22
let count = $state(0);
33
// ^|
4-
54
function increment() {
65
count++;
76
}
87
</script>
9-
108
<button onclick={increment}>
119
Count is: {count}
1210
</button>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let count = $state(0);
3+
// ---cut-after---
4+
function increment() {
5+
count++;
6+
}
7+
</script>
8+
<button onclick={increment}>
9+
Count is: {count}
10+
</button>

packages/twoslash-svelte/test/fixtures/cut-around.svelte renamed to packages/twoslash-svelte/test/fixtures/script/cut-before.svelte

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
<script>
22
let count = $state(0);
3-
43
// ---cut-before---
54
function increment() {
65
count++;
76
}
8-
// ---cut-after---
97
</script>
10-
118
<button onclick={increment}>
129
Count is: {count}
1310
</button>

packages/twoslash-svelte/test/fixtures/cut-in.svelte renamed to packages/twoslash-svelte/test/fixtures/script/cut-region.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
<script>
22
let count = $state(0);
3-
43
// ---cut-start---
54
function increment() {
65
count++;
76
}
87
// ---cut-end---
98
</script>
10-
119
<button onclick={increment}>
1210
Count is: {count}
1311
</button>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let count = $state(0);
3+
// ---cut---
4+
function increment() {
5+
count++;
6+
}
7+
</script>
8+
<button onclick={increment}>
9+
Count is: {count}
10+
</button>

packages/twoslash-svelte/test/fixtures/hover.svelte renamed to packages/twoslash-svelte/test/fixtures/script/hover.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script>
22
let count = $state(0);
3-
43
function increment() {
54
count++;
65
}
76
</script>
8-
97
<button onclick={increment}>
108
Count is: {count}
119
</button>

packages/twoslash-svelte/test/fixtures/query.svelte renamed to packages/twoslash-svelte/test/fixtures/script/query.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
<script>
22
let count = $state(0);
33
// ^?
4-
54
function increment() {
65
count++;
76
}
87
</script>
9-
108
<button onclick={increment}>
119
Count is: {count}
1210
</button>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let count = $state(0);
3+
// ---cut-start---
4+
function increment() {
5+
count++;
6+
}
7+
</script>
8+
<button onclick={increment}>
9+
Count is: {count}
10+
</button>

0 commit comments

Comments
 (0)