Skip to content

Commit c659a40

Browse files
authored
Merge pull request #146 from ony3000/fix/issue-145-svelte-module-script
Fix Svelte module script formatting
2 parents a91d21c + 7d9ff9e commit c659a40

6 files changed

Lines changed: 203 additions & 43 deletions

File tree

src/core-parts/parser.ts

Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,42 @@ function base64Decode(input: string): string {
88
}
99

1010
function refineSvelteAst(preprocessedText: string, ast: AST) {
11-
if (!ast.instance) {
12-
return ast;
13-
}
14-
15-
const scriptTag = preprocessedText.slice(ast.instance.start, ast.instance.end);
16-
const matchResult = scriptTag.match(/ prettier:content="([^"]*)"/);
17-
18-
if (matchResult === null) {
11+
const svelteAst = ast as AST & {
12+
module?: unknown;
13+
instance?: unknown;
14+
};
15+
const scriptInfos = [
16+
{ key: 'module' as const, node: svelteAst.module },
17+
{ key: 'instance' as const, node: svelteAst.instance },
18+
]
19+
.filter(
20+
(
21+
info,
22+
): info is {
23+
key: 'module' | 'instance';
24+
node: { start: number; end: number; content?: { body?: unknown[] } };
25+
} =>
26+
isTypeof(
27+
info.node,
28+
z.object({
29+
start: z.number(),
30+
end: z.number(),
31+
}),
32+
),
33+
)
34+
.map(({ key, node }) => ({
35+
key,
36+
node,
37+
originalStart: node.start,
38+
originalEnd: node.end,
39+
}))
40+
.sort((a, b) => a.originalStart - b.originalStart);
41+
42+
if (scriptInfos.length === 0) {
1943
return ast;
2044
}
2145

22-
const [temporaryAttributeWithLeadingSpace, encodedContent] = matchResult;
23-
const plainContent = base64Decode(encodedContent);
24-
25-
const restoreTextOffset =
26-
plainContent.length - (temporaryAttributeWithLeadingSpace.length + '{}'.length);
27-
const restoreLineOffset = plainContent.split(EOL).length - 1;
28-
29-
function recursion(node: unknown): void {
46+
function shiftNode(node: unknown, thresholdEnd: number, textOffset: number, lineOffset: number): void {
3047
if (!isTypeof(node, z.object({ type: z.string() }))) {
3148
return;
3249
}
@@ -38,12 +55,12 @@ function refineSvelteAst(preprocessedText: string, ast: AST) {
3855

3956
if (Array.isArray(value)) {
4057
value.forEach((childNode: unknown) => {
41-
recursion(childNode);
58+
shiftNode(childNode, thresholdEnd, textOffset, lineOffset);
4259
});
4360
return;
4461
}
4562

46-
recursion(value);
63+
shiftNode(value, thresholdEnd, textOffset, lineOffset);
4764
});
4865

4966
if (
@@ -58,8 +75,8 @@ function refineSvelteAst(preprocessedText: string, ast: AST) {
5875
return;
5976
}
6077

61-
if (ast.instance.end <= node.start) {
62-
node.start += restoreTextOffset;
78+
if (thresholdEnd <= node.start) {
79+
node.start += textOffset;
6380

6481
if (
6582
isTypeof(
@@ -75,12 +92,12 @@ function refineSvelteAst(preprocessedText: string, ast: AST) {
7592
) {
7693
node.loc.start = {
7794
...node.loc.start,
78-
line: node.loc.start.line + restoreLineOffset,
95+
line: node.loc.start.line + lineOffset,
7996
};
8097
}
8198
}
82-
if (ast.instance.end <= node.end) {
83-
node.end += restoreTextOffset;
99+
if (thresholdEnd <= node.end) {
100+
node.end += textOffset;
84101

85102
if (
86103
isTypeof(
@@ -96,36 +113,79 @@ function refineSvelteAst(preprocessedText: string, ast: AST) {
96113
) {
97114
node.loc.end = {
98115
...node.loc.end,
99-
line: node.loc.end.line + restoreLineOffset,
116+
line: node.loc.end.line + lineOffset,
100117
};
101118
}
102119
}
103120
}
104121

105-
recursion(ast.html);
106-
recursion(ast.fragment);
122+
let cumulativeTextOffset = 0;
123+
let cumulativeLineOffset = 0;
107124

108-
ast.instance = {
109-
type: 'RefinedScript',
110-
start: ast.instance.start,
111-
end: ast.instance.end + restoreTextOffset,
112-
loc: {
113-
start: {
114-
line: preprocessedText.slice(0, ast.instance.start).split(EOL).length,
115-
},
116-
},
117-
content: {
118-
type: 'RefinedScriptSource',
119-
start: ast.instance.end + restoreTextOffset - ('</script>'.length + plainContent.length),
120-
end: ast.instance.end + restoreTextOffset - '</script>'.length,
125+
scriptInfos.forEach(({ key, node, originalStart, originalEnd }, scriptIndex) => {
126+
const scriptTag = preprocessedText.slice(originalStart, originalEnd);
127+
const matchResult = scriptTag.match(/ prettier:content="([^"]*)"/);
128+
129+
if (matchResult === null) {
130+
return;
131+
}
132+
133+
const [temporaryAttributeWithLeadingSpace, encodedContent] = matchResult;
134+
const plainContent = base64Decode(encodedContent);
135+
136+
const restoreTextOffset =
137+
plainContent.length - (temporaryAttributeWithLeadingSpace.length + '{}'.length);
138+
const restoreLineOffset = plainContent.split(EOL).length - 1;
139+
const currentStart = originalStart + cumulativeTextOffset;
140+
const currentEnd = originalEnd + cumulativeTextOffset;
141+
const currentStartLine =
142+
preprocessedText.slice(0, originalStart).split(EOL).length + cumulativeLineOffset;
143+
const firstBodyNode = node.content?.body?.at(0);
144+
const contentStartLine = isTypeof(
145+
firstBodyNode,
146+
z.object({
147+
loc: z.object({
148+
start: z.object({
149+
line: z.number(),
150+
}),
151+
}),
152+
}),
153+
)
154+
? firstBodyNode.loc.start.line + cumulativeLineOffset
155+
: currentStartLine + 1;
156+
157+
shiftNode(svelteAst.html, currentEnd, restoreTextOffset, restoreLineOffset);
158+
shiftNode(svelteAst.fragment, currentEnd, restoreTextOffset, restoreLineOffset);
159+
160+
scriptInfos.slice(scriptIndex + 1).forEach(({ key: nextKey }) => {
161+
shiftNode(svelteAst[nextKey], currentEnd, restoreTextOffset, restoreLineOffset);
162+
});
163+
164+
svelteAst[key] = {
165+
type: 'RefinedScript',
166+
start: currentStart,
167+
end: currentEnd + restoreTextOffset,
121168
loc: {
122169
start: {
123-
line: ast.instance.content.body[0].loc.start.line,
170+
line: currentStartLine,
124171
},
125172
},
126-
value: plainContent,
127-
},
128-
};
173+
content: {
174+
type: 'RefinedScriptSource',
175+
start: currentEnd + restoreTextOffset - ('</script>'.length + plainContent.length),
176+
end: currentEnd + restoreTextOffset - '</script>'.length,
177+
loc: {
178+
start: {
179+
line: contentStartLine,
180+
},
181+
},
182+
value: plainContent,
183+
},
184+
};
185+
186+
cumulativeTextOffset += restoreTextOffset;
187+
cumulativeLineOffset += restoreLineOffset;
188+
});
129189

130190
return ast;
131191
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`'(1) module script containing type dec…' > expectation 1`] = `
4+
"<script lang="ts" module>
5+
export type TabProps = {
6+
/** Use to lazily load elements in tab. */
7+
tabLoaded: boolean;
8+
};
9+
</script>
10+
11+
<script lang="ts">
12+
import type { Snippet } from "svelte";
13+
14+
interface Props {
15+
children?: Snippet;
16+
class?: string;
17+
}
18+
19+
let { children, class: _class }: Props = $props();
20+
</script>
21+
22+
<div class={["p-2 bg-primary-300 gap-2", _class]}>{@render children?.()}</div>
23+
"
24+
`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`'(1) module script containing type dec…' > expectation 1`] = `
4+
"<script lang="ts" module>
5+
export type TabProps = {
6+
/** Use to lazily load elements in tab. */
7+
tabLoaded: boolean;
8+
};
9+
</script>
10+
11+
<script lang="ts">
12+
import type { Snippet } from "svelte";
13+
14+
interface Props {
15+
children?: Snippet;
16+
class?: string;
17+
}
18+
19+
let { children, class: _class }: Props = $props();
20+
</script>
21+
22+
<div class={["p-2 bg-primary-300 gap-2", _class]}>{@render children?.()}</div>
23+
"
24+
`;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { thisPlugin, testSnapshotEach } from '../../adaptor';
2+
import { baseOptions } from '../../settings';
3+
import { fixtures } from './fixtures';
4+
5+
const options = {
6+
...baseOptions,
7+
plugins: ['prettier-plugin-svelte', thisPlugin],
8+
parser: 'svelte',
9+
endingPosition: 'absolute',
10+
};
11+
12+
testSnapshotEach(fixtures, options);

tests/svelte/issue-145/fixtures.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Fixture } from '../../settings';
2+
3+
export const fixtures: Omit<Fixture, 'output'>[] = [
4+
{
5+
name: '(1) module script containing type declaration',
6+
input: `
7+
<script lang="ts" module>
8+
export type TabProps = {
9+
/** Use to lazily load elements in tab. */
10+
tabLoaded: boolean;
11+
};
12+
</script>
13+
14+
<script lang="ts">
15+
import type { Snippet } from "svelte";
16+
17+
interface Props {
18+
children?: Snippet;
19+
class?: string;
20+
}
21+
22+
let { children, class: _class }: Props = $props();
23+
</script>
24+
25+
<div class={["p-2 bg-primary-300 gap-2", _class]}>{@render children?.()}</div>
26+
`,
27+
},
28+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { thisPlugin, testSnapshotEach } from '../../adaptor';
2+
import { baseOptions } from '../../settings';
3+
import { fixtures } from './fixtures';
4+
5+
const options = {
6+
...baseOptions,
7+
plugins: ['prettier-plugin-svelte', thisPlugin],
8+
parser: 'svelte',
9+
endingPosition: 'relative',
10+
};
11+
12+
testSnapshotEach(fixtures, options);

0 commit comments

Comments
 (0)