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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy