Skip to content

Commit 9a5a6af

Browse files
authored
feat: hoist snippets to module context if possible (#2601)
sveltejs/svelte#10350
1 parent 050ecc1 commit 9a5a6af

File tree

10 files changed

+199
-10
lines changed

10 files changed

+199
-10
lines changed

packages/svelte2tsx/src/htmlxtojsx_v2/index.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from '../svelte2tsx/nodes/handleScopeAndResolveForSlot';
4141
import { EventHandler } from '../svelte2tsx/nodes/event-handler';
4242
import { ComponentEvents } from '../svelte2tsx/nodes/ComponentEvents';
43+
import { analyze } from 'periscopic';
4344

4445
export interface TemplateProcessResult {
4546
/**
@@ -53,7 +54,7 @@ export interface TemplateProcessResult {
5354
scriptTag: BaseNode;
5455
moduleScriptTag: BaseNode;
5556
/** Start/end positions of snippets that should be moved to the instance script or possibly even module script */
56-
rootSnippets: Array<[number, number]>;
57+
rootSnippets: Array<[start: number, end: number, globals: Map<string, any>]>;
5758
/** To be added later as a comment on the default class export */
5859
componentDocumentation: ComponentDocumentation;
5960
events: ComponentEvents;
@@ -92,7 +93,7 @@ export function convertHtmlxToJsx(
9293

9394
stripDoctype(str);
9495

95-
const rootSnippets: Array<[number, number]> = [];
96+
const rootSnippets: Array<[number, number, Map<string, any>]> = [];
9697
let element: Element | InlineComponent | undefined;
9798

9899
const pendingSnippetHoistCheck = new Set<BaseNode>();
@@ -249,7 +250,21 @@ export function convertHtmlxToJsx(
249250
);
250251
if (parent === ast) {
251252
// root snippet -> move to instance script or possibly even module script
252-
rootSnippets.push([node.start, node.end]);
253+
const result = analyze({
254+
type: 'FunctionDeclaration',
255+
start: -1,
256+
end: -1,
257+
id: node.expression,
258+
params: node.parameters ?? [],
259+
body: {
260+
type: 'BlockStatement',
261+
start: -1,
262+
end: -1,
263+
body: node.children as any[] // wrong AST, but periscopic doesn't care
264+
}
265+
});
266+
267+
rootSnippets.push([node.start, node.end, result.globals]);
253268
} else {
254269
pendingSnippetHoistCheck.add(parent);
255270
}

packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts

-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
88
str: MagicString;
99
scriptTag: Node;
1010
scriptDestination: number;
11-
rootSnippets: Array<[number, number]>;
1211
slots: Map<string, Map<string, string>>;
1312
events: ComponentEvents;
1413
uses$$SlotsInterface: boolean;
@@ -20,7 +19,6 @@ export function createRenderFunction({
2019
str,
2120
scriptTag,
2221
scriptDestination,
23-
rootSnippets,
2422
slots,
2523
events,
2624
exportedNames,
@@ -82,10 +80,6 @@ export function createRenderFunction({
8280
);
8381
}
8482

85-
for (const rootSnippet of rootSnippets) {
86-
str.move(rootSnippet[0], rootSnippet[1], scriptTagEnd);
87-
}
88-
8983
const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1);
9084
// wrap template with callback
9185
str.overwrite(scriptEndTagStart, scriptTag.end, `${slotsDeclaration};\nasync () => {`, {

packages/svelte2tsx/src/svelte2tsx/index.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ export function svelte2tsx(
139139
str,
140140
scriptTag,
141141
scriptDestination: instanceScriptTarget,
142-
rootSnippets,
143142
slots,
144143
events,
145144
exportedNames,
@@ -164,6 +163,32 @@ export function svelte2tsx(
164163
),
165164
moduleAst
166165
);
166+
if (!scriptTag) {
167+
moduleAst.tsAst.forEachChild((node) =>
168+
exportedNames.hoistableInterfaces.analyzeModuleScriptNode(node)
169+
);
170+
}
171+
}
172+
173+
if (moduleScriptTag || scriptTag) {
174+
const allowed = exportedNames.hoistableInterfaces.getAllowedValues();
175+
for (const [start, end, globals] of rootSnippets) {
176+
const hoist_to_module =
177+
moduleScriptTag &&
178+
(globals.size === 0 || [...globals.keys()].every((id) => allowed.has(id)));
179+
180+
if (hoist_to_module) {
181+
str.move(
182+
start,
183+
end,
184+
scriptTag
185+
? scriptTag.start + 1 // +1 because imports are also moved at that position, and we want to move interfaces after imports
186+
: moduleScriptTag.end
187+
);
188+
} else if (scriptTag) {
189+
str.move(start, end, renderFunctionStart);
190+
}
191+
}
167192
}
168193

169194
addComponentExport({

packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts

+4
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ export class HoistableInterfaces {
358358
}
359359
}
360360

361+
getAllowedValues() {
362+
return this.import_value_set;
363+
}
364+
361365
/**
362366
* Collects type and value dependencies from a given TypeNode.
363367
* @param type_node The TypeNode to analyze.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
///<reference types="svelte" />
2+
;
3+
let module = true;
4+
;;
5+
6+
import { imported } from './x';
7+
const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
8+
{ svelteHTML.createElement("div", {}); }
9+
};return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = (bar)/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
10+
{ svelteHTML.createElement("div", {});bar; }
11+
};return __sveltets_2_any(0)}; const hoistable3/*Ωignore_positionΩ*/ = (bar: string)/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
12+
{ svelteHTML.createElement("div", {});bar; }
13+
};return __sveltets_2_any(0)}; const hoistable4/*Ωignore_positionΩ*/ = (foo)/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
14+
{ svelteHTML.createElement("div", {});foo; }
15+
};return __sveltets_2_any(0)}; const hoistable5/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
16+
{ svelteHTML.createElement("button", { "onclick":e => e,}); }
17+
};return __sveltets_2_any(0)}; const hoistable6/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
18+
{ svelteHTML.createElement("div", {});module; }
19+
};return __sveltets_2_any(0)}; const hoistable7/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
20+
{ svelteHTML.createElement("div", {});imported; }
21+
};return __sveltets_2_any(0)};function render() {
22+
const not_hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
23+
{ svelteHTML.createElement("div", {});foo; }
24+
};return __sveltets_2_any(0)};
25+
26+
let foo = true;
27+
;
28+
async () => {
29+
30+
31+
32+
33+
34+
35+
36+
37+
38+
39+
40+
41+
42+
43+
44+
45+
46+
};
47+
return { props: /** @type {Record<string, never>} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }}
48+
const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render())));
49+
/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType<typeof Input__SvelteComponent_>;
50+
/*Ωignore_endΩ*/export default Input__SvelteComponent_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script module>
2+
let module = true;
3+
</script>
4+
5+
<script lang="ts">
6+
import { imported } from './x';
7+
let foo = true;
8+
</script>
9+
10+
{#snippet hoistable1()}
11+
<div>hello</div>
12+
{/snippet}
13+
14+
{#snippet hoistable2(bar)}
15+
<div>{bar}</div>
16+
{/snippet}
17+
18+
{#snippet hoistable3(bar: string)}
19+
<div>{bar}</div>
20+
{/snippet}
21+
22+
{#snippet hoistable4(foo)}
23+
<div>{foo}</div>
24+
{/snippet}
25+
26+
{#snippet hoistable5()}
27+
<button onclick={e => e}>click</button>
28+
{/snippet}
29+
30+
{#snippet hoistable6()}
31+
<div>{module}</div>
32+
{/snippet}
33+
34+
{#snippet hoistable7()}
35+
<div>{imported}</div>
36+
{/snippet}
37+
38+
{#snippet not_hoistable()}
39+
<div>{foo}</div>
40+
{/snippet}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
///<reference types="svelte" />
2+
;
3+
import { imported } from './x';
4+
function render() {
5+
const hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
6+
{ svelteHTML.createElement("div", {}); }
7+
};return __sveltets_2_any(0)}; const not_hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
8+
{ svelteHTML.createElement("div", {});foo; }
9+
};return __sveltets_2_any(0)};
10+
11+
let foo = true;
12+
;
13+
async () => {
14+
15+
16+
17+
};
18+
return { props: /** @type {Record<string, never>} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }}
19+
const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render())));
20+
/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType<typeof Input__SvelteComponent_>;
21+
/*Ωignore_endΩ*/export default Input__SvelteComponent_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script lang="ts">
2+
import { imported } from './x';
3+
let foo = true;
4+
</script>
5+
6+
{#snippet hoistable()}
7+
<div>hello</div>
8+
{/snippet}
9+
10+
{#snippet not_hoistable()}
11+
<div>{foo}</div>
12+
{/snippet}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
///<reference types="svelte" />
2+
;
3+
let foo = true;
4+
; const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
5+
{ svelteHTML.createElement("div", {}); }
6+
};return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType<import('svelte').Snippet>/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
7+
{ svelteHTML.createElement("div", {});foo; }
8+
};return __sveltets_2_any(0)};;function render() {
9+
async () => {
10+
11+
12+
13+
};
14+
return { props: /** @type {Record<string, never>} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }}
15+
const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render())));
16+
/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType<typeof Input__SvelteComponent_>;
17+
/*Ωignore_endΩ*/export default Input__SvelteComponent_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script module lang="ts">
2+
let foo = true;
3+
</script>
4+
5+
{#snippet hoistable1()}
6+
<div>hello</div>
7+
{/snippet}
8+
9+
{#snippet hoistable2()}
10+
<div>{foo}</div>
11+
{/snippet}

0 commit comments

Comments
 (0)