Skip to content

Commit 2e2c6c3

Browse files
authored
feat: add await support (#2799)
For the new Svelte 5 "await at the top level / in template" feature
1 parent 2a0c8a0 commit 2e2c6c3

File tree

27 files changed

+464
-113
lines changed

27 files changed

+464
-113
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[
2+
{
3+
"range": {
4+
"start": { "line": 16, "character": 21 },
5+
"end": { "line": 16, "character": 22 }
6+
},
7+
"severity": 1,
8+
"source": "ts",
9+
"message": "Type 'string' is not assignable to type 'number'.",
10+
"code": 2322,
11+
"tags": []
12+
},
13+
{
14+
"range": {
15+
"start": { "line": 19, "character": 5 },
16+
"end": { "line": 19, "character": 17 }
17+
},
18+
"severity": 1,
19+
"source": "ts",
20+
"message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.",
21+
"code": 2367,
22+
"tags": []
23+
}
24+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts" generics="T">
2+
let { a, b }: { a: T[], b: T } = $props();
3+
4+
await Promise.resolve();
5+
</script>
6+
7+
{a} {b}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
import Generic from "./generic.svelte";
3+
4+
let a = Promise.resolve([1]);
5+
let b = Promise.resolve(2);
6+
let c = Promise.resolve('')
7+
</script>
8+
9+
<!-- valid -->
10+
<Generic a={await a} b={await b} />
11+
12+
{#each await a as item}
13+
{item === 1}
14+
{/each}
15+
16+
<!-- invalid -->
17+
<Generic a={await a} b={await c} />
18+
19+
{#each await a as item}
20+
{item === 'a'}
21+
{/each}

packages/language-server/test/plugins/typescript/test-utils.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,6 @@ export async function updateSnapshotIfFailedOrEmpty({
271271
}
272272

273273
export async function createJsonSnapshotFormatter(dir: string) {
274-
if (!process.argv.includes('--auto')) {
275-
return (_obj: any) => '';
276-
}
277-
278274
const prettierOptions = await resolveConfig(dir);
279275

280276
return (obj: any) =>

packages/svelte2tsx/src/htmlxtojsx_v2/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,15 @@ export function convertHtmlxToJsx(
182182
};
183183

184184
const eventHandler = new EventHandler();
185+
const path: BaseNode[] = [];
185186

186187
walk(ast as any, {
187188
enter: (estreeTypedNode, estreeTypedParent, prop: string) => {
188189
const node = estreeTypedNode as TemplateNode;
189190
const parent = estreeTypedParent as BaseNode;
190191

192+
path.push(node);
193+
191194
if (
192195
prop == 'params' &&
193196
(parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
@@ -416,6 +419,13 @@ export function convertHtmlxToJsx(
416419
);
417420
}
418421
break;
422+
case 'AwaitExpression':
423+
isRunes ||= path.every(
424+
({ type }) =>
425+
type !== 'ArrowFunctionExpression' &&
426+
type !== 'FunctionExpression' &&
427+
type !== 'FunctionDeclaration'
428+
);
419429
}
420430
} catch (e) {
421431
console.error('Error walking node ', node, e);
@@ -427,6 +437,8 @@ export function convertHtmlxToJsx(
427437
const node = estreeTypedNode as TemplateNode;
428438
const parent = estreeTypedParent as BaseNode;
429439

440+
path.pop();
441+
430442
if (
431443
prop == 'params' &&
432444
(parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')

packages/svelte2tsx/src/svelte2tsx/addComponentExport.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface AddComponentExportPara {
2525
generics: Generics;
2626
usesSlots: boolean;
2727
isSvelte5: boolean;
28+
hasTopLevelAwait: boolean;
2829
noSvelteComponentTyped?: boolean;
2930
}
3031

@@ -50,12 +51,12 @@ function addGenericsComponentExport({
5051
fileName,
5152
mode,
5253
usesAccessors,
53-
isTsFile,
5454
str,
5555
generics,
5656
usesSlots,
5757
isSvelte5,
58-
noSvelteComponentTyped
58+
noSvelteComponentTyped,
59+
hasTopLevelAwait
5960
}: AddComponentExportPara) {
6061
const genericsDef = generics.toDefinitionString();
6162
const genericsRef = generics.toReferencesString();
@@ -67,34 +68,41 @@ function addGenericsComponentExport({
6768
return `ReturnType<__sveltets_Render${genericsRef}['${forPart}']>`;
6869
}
6970

71+
const renderCall = hasTopLevelAwait
72+
? `(await ${internalHelpers.renderName}${genericsRef}())`
73+
: `${internalHelpers.renderName}${genericsRef}()`;
74+
7075
// TODO once Svelte 4 compatibility is dropped, we can simplify this, because since TS 4.7 it is possible to use generics
7176
// like this: `typeof render<T>` - which wasn't possibly before, hence the class + methods workaround.
7277
let statement = `
7378
class __sveltets_Render${genericsDef} {
7479
props() {
75-
return ${props(true, canHaveAnyProp, exportedNames, `${internalHelpers.renderName}${genericsRef}()`)}.props;
80+
return ${props(true, canHaveAnyProp, exportedNames, renderCall)}.props;
7681
}
7782
events() {
78-
return ${_events(events.hasStrictEvents() || exportedNames.usesRunes(), `${internalHelpers.renderName}${genericsRef}()`)}.events;
83+
return ${_events(events.hasStrictEvents() || exportedNames.isRunesMode(), renderCall)}.events;
7984
}
8085
slots() {
81-
return ${internalHelpers.renderName}${genericsRef}().slots;
86+
return ${renderCall}.slots;
8287
}
8388
`;
8489

8590
// For Svelte 5+ we assume TS > 4.7
86-
if (isSvelte5 && !isTsFile && exportedNames.usesRunes()) {
91+
if (isSvelte5 && exportedNames.isRunesMode()) {
92+
const renderType = hasTopLevelAwait
93+
? `Awaited<ReturnType<typeof ${internalHelpers.renderName}${genericsRef}>>`
94+
: `ReturnType<typeof ${internalHelpers.renderName}${genericsRef}>`;
8795
statement = `
8896
class __sveltets_Render${genericsDef} {
89-
props(): ReturnType<typeof ${internalHelpers.renderName}${genericsRef}>['props'] { return null as any; }
90-
events(): ReturnType<typeof ${internalHelpers.renderName}${genericsRef}>['events'] { return null as any; }
91-
slots(): ReturnType<typeof ${internalHelpers.renderName}${genericsRef}>['slots'] { return null as any; }
97+
props(): ${renderType}['props'] { return null as any; }
98+
events(): ${renderType}['events'] { return null as any; }
99+
slots(): ${renderType}['slots'] { return null as any; }
92100
`;
93101
}
94102

95103
statement += isSvelte5
96104
? ` bindings() { return ${exportedNames.createBindingsStr()}; }
97-
exports() { return ${exportedNames.hasExports() ? `${internalHelpers.renderName}${genericsRef}().exports` : '{}'}; }
105+
${hasTopLevelAwait ? 'async ' : ''}exports() { return ${exportedNames.hasExports() ? `${renderCall}.exports` : '{}'}; }
98106
}\n`
99107
: '}\n';
100108

@@ -109,7 +117,7 @@ class __sveltets_Render${genericsDef} {
109117
// Don't add props/events/slots type exports in dts mode for now, maybe someone asks for it to be back,
110118
// but it's safer to not do it for now to have more flexibility in the future.
111119
let eventsSlotsType = [];
112-
if (events.hasEvents() || !exportedNames.usesRunes()) {
120+
if (events.hasEvents() || !exportedNames.isRunesMode()) {
113121
eventsSlotsType.push(`$$events?: ${returnType('events')}`);
114122
}
115123
if (usesSlots) {
@@ -176,13 +184,24 @@ function addSimpleComponentExport({
176184
str,
177185
usesSlots,
178186
noSvelteComponentTyped,
179-
isSvelte5
187+
isSvelte5,
188+
hasTopLevelAwait
180189
}: AddComponentExportPara) {
190+
const renderCall = hasTopLevelAwait
191+
? `$${internalHelpers.renderName}`
192+
: `${internalHelpers.renderName}()`;
193+
const awaitDeclaration = hasTopLevelAwait
194+
? // tsconfig could disallow top-level await, so we need to wrap it in ignore
195+
surroundWithIgnoreComments(
196+
`const $${internalHelpers.renderName} = await ${internalHelpers.renderName}();`
197+
) + '\n'
198+
: '';
199+
181200
const propDef = props(
182201
isTsFile,
183202
canHaveAnyProp,
184203
exportedNames,
185-
_events(events.hasStrictEvents(), `${internalHelpers.renderName}()`)
204+
_events(events.hasStrictEvents(), renderCall)
186205
);
187206

188207
const doc = componentDocumentation.getFormatted();
@@ -191,9 +210,9 @@ function addSimpleComponentExport({
191210

192211
let statement: string;
193212
if (mode === 'dts') {
194-
if (isSvelte5 && exportedNames.usesRunes() && !usesSlots && !events.hasEvents()) {
213+
if (isSvelte5 && exportedNames.isRunesMode() && !usesSlots && !events.hasEvents()) {
195214
statement =
196-
`\n${doc}const ${componentName} = __sveltets_2_fn_component(${internalHelpers.renderName}());\n` +
215+
`\n${awaitDeclaration}${doc}const ${componentName} = __sveltets_2_fn_component(${renderCall});\n` +
197216
`type ${componentName} = ReturnType<typeof ${componentName}>;\n` +
198217
`export default ${componentName};`;
199218
} else if (isSvelte5) {
@@ -218,7 +237,7 @@ function addSimpleComponentExport({
218237
declare function $$__sveltets_2_isomorphic_component<
219238
Props extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>, Exports extends Record<string, any>, Bindings extends string
220239
>(klass: {props: Props, events: Events, slots: Slots, exports?: Exports, bindings?: Bindings }): $$__sveltets_2_IsomorphicComponent<Props, Events, Slots, Exports, Bindings>;\n`) +
221-
`${doc}const ${componentName} = $$__sveltets_2_isomorphic_component${usesSlots ? '_slots' : ''}(${propDef});\n` +
240+
`${awaitDeclaration}${doc}const ${componentName} = $$__sveltets_2_isomorphic_component${usesSlots ? '_slots' : ''}(${propDef});\n` +
222241
surroundWithIgnoreComments(
223242
`type ${componentName} = InstanceType<typeof ${componentName}>;\n`
224243
) +
@@ -257,9 +276,9 @@ declare function $$__sveltets_2_isomorphic_component<
257276
}
258277
} else {
259278
if (isSvelte5) {
260-
if (exportedNames.usesRunes() && !usesSlots && !events.hasEvents()) {
279+
if (exportedNames.isRunesMode() && !usesSlots && !events.hasEvents()) {
261280
statement =
262-
`\n${doc}const ${componentName} = __sveltets_2_fn_component(${internalHelpers.renderName}());\n` +
281+
`\n${awaitDeclaration}${doc}const ${componentName} = __sveltets_2_fn_component(${renderCall});\n` +
263282
// Surround the type with ignore comments so it is filtered out from go-to-definition etc,
264283
// which for some editors can cause duplicates
265284
surroundWithIgnoreComments(
@@ -268,7 +287,7 @@ declare function $$__sveltets_2_isomorphic_component<
268287
`export default ${componentName};`;
269288
} else {
270289
statement =
271-
`\n${doc}const ${componentName} = __sveltets_2_isomorphic_component${usesSlots ? '_slots' : ''}(${propDef});\n` +
290+
`\n${awaitDeclaration}${doc}const ${componentName} = __sveltets_2_isomorphic_component${usesSlots ? '_slots' : ''}(${propDef});\n` +
272291
surroundWithIgnoreComments(
273292
`type ${componentName} = InstanceType<typeof ${componentName}>;\n`
274293
) +
@@ -336,7 +355,7 @@ function props(
336355
exportedNames: ExportedNames,
337356
renderStr: string
338357
) {
339-
if (exportedNames.usesRunes()) {
358+
if (exportedNames.isRunesMode()) {
340359
return renderStr;
341360
} else if (isTsFile) {
342361
return canHaveAnyProp ? `__sveltets_2_with_any(${renderStr})` : renderStr;

packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function createRenderFunction({
3333
uses$$slots,
3434
uses$$SlotsInterface,
3535
generics,
36+
hasTopLevelAwait,
3637
isTsFile,
3738
mode
3839
}: CreateRenderFunctionPara) {
@@ -77,7 +78,11 @@ export function createRenderFunction({
7778
end--;
7879
}
7980

80-
str.overwrite(scriptTag.start + 1, start - 1, `function ${internalHelpers.renderName}`);
81+
str.overwrite(
82+
scriptTag.start + 1,
83+
start - 1,
84+
`${hasTopLevelAwait ? 'async ' : ''}function ${internalHelpers.renderName}`
85+
);
8186
str.overwrite(start - 1, start, isTsFile ? '<' : `<${IGNORE_START_COMMENT}`); // if the generics are unused, only this char is colored opaque
8287
str.overwrite(
8388
end,
@@ -88,7 +93,7 @@ export function createRenderFunction({
8893
str.overwrite(
8994
scriptTag.start + 1,
9095
scriptTagEnd,
91-
`function ${internalHelpers.renderName}${generics.toDefinitionString(true)}() {${propsDecl}\n`
96+
`${hasTopLevelAwait ? 'async ' : ''}function ${internalHelpers.renderName}${generics.toDefinitionString(true)}() {${propsDecl}\n`
9297
);
9398
}
9499

@@ -100,7 +105,7 @@ export function createRenderFunction({
100105
} else {
101106
str.prependRight(
102107
scriptDestination,
103-
`;function ${internalHelpers.renderName}() {` +
108+
`;${hasTopLevelAwait ? 'async ' : ''}function ${internalHelpers.renderName}() {` +
104109
`${propsDecl}${slotsDeclaration}\nasync () => {`
105110
);
106111
}

packages/svelte2tsx/src/svelte2tsx/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export function svelte2tsx(
104104
let exportedNames = new ExportedNames(str, 0, basename, isTsFile, svelte5Plus, isRunes);
105105
let generics = new Generics(str, 0, { attributes: [] } as any);
106106
let uses$$SlotsInterface = false;
107+
let hasTopLevelAwait = false;
107108
if (scriptTag) {
108109
//ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template)
109110
if (scriptTag.start != instanceScriptTarget) {
@@ -125,12 +126,15 @@ export function svelte2tsx(
125126
uses$$restProps = uses$$restProps || res.uses$$restProps;
126127
uses$$slots = uses$$slots || res.uses$$slots;
127128

128-
({ exportedNames, events, generics, uses$$SlotsInterface } = res);
129+
({ exportedNames, events, generics, uses$$SlotsInterface, hasTopLevelAwait } = res);
129130
}
130131

131132
exportedNames.usesAccessors = usesAccessors;
132133
if (svelte5Plus) {
133134
exportedNames.checkGlobalsForRunes(implicitStoreValues.getGlobals());
135+
if (hasTopLevelAwait) {
136+
exportedNames.enterRunesMode();
137+
}
134138
}
135139

136140
//wrap the script tag and template content in a function returning the slot and exports
@@ -146,6 +150,7 @@ export function svelte2tsx(
146150
uses$$slots,
147151
uses$$SlotsInterface,
148152
generics,
153+
hasTopLevelAwait,
149154
svelte5Plus,
150155
isTsFile,
151156
mode: options.mode
@@ -219,6 +224,7 @@ export function svelte2tsx(
219224
mode: options.mode,
220225
generics,
221226
isSvelte5: svelte5Plus,
227+
hasTopLevelAwait,
222228
noSvelteComponentTyped: options.noSvelteComponentTyped
223229
});
224230

0 commit comments

Comments
 (0)