Skip to content

Commit edd815e

Browse files
authored
[fix] better type generation for load functions with different return values (#7425)
* [fix] better type generation for load functions with different return values Fixes #7408 Also reworking write_types tests to type-check the outcome, not compare the generated code * explanation comment * fix excludes
1 parent 934db68 commit edd815e

File tree

39 files changed

+223
-688
lines changed

39 files changed

+223
-688
lines changed

.changeset/friendly-pears-wash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[fix] better type generation for load functions with different return values

packages/kit/src/core/sync/write_types/index.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ function update_types(config, routes, route, to_delete = new Set()) {
215215
);
216216
// null & {} == null, we need to prevent that in some situations
217217
declarations.push(`type EnsureDefined<T> = T extends null | undefined ? {} : T;`);
218+
// Takes a union type and returns a union type where each type also has all properties
219+
// of all possible types (typed as undefined), making accessing them more ergonomic
220+
declarations.push(
221+
`type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;`
222+
);
218223
}
219224

220225
if (route.leaf) {
@@ -402,7 +407,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true
402407
proxy
403408
);
404409

405-
data = `Expand<Omit<${parent_type}, keyof ${type}> & EnsureDefined<${type}>>`;
410+
data = `Expand<Omit<${parent_type}, keyof ${type}> & OptionalUnion<EnsureDefined<${type}>>>`;
406411

407412
const output_data_shape =
408413
!is_page && all_pages_have_load
@@ -438,7 +443,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true
438443
? `./proxy${replace_ext_with_js(path.basename(file_path))}`
439444
: path_to_original(outdir, file_path);
440445
const type = `Kit.AwaitedProperties<Awaited<ReturnType<typeof import('${from}').load>>>`;
441-
return expand ? `Expand<${type}>` : type;
446+
return expand ? `Expand<OptionalUnion<EnsureDefined<${type}>>>` : type;
442447
} else {
443448
return fallback;
444449
}

packages/kit/src/core/sync/write_types/index.spec.js

+14-71
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,19 @@
1-
// @ts-nocheck
21
import path from 'path';
3-
import fs from 'fs';
4-
import { format } from 'prettier';
52
import { fileURLToPath } from 'url';
63
import { test } from 'uvu';
74
import * as assert from 'uvu/assert';
8-
import { rimraf, walk } from '../../../utils/filesystem.js';
5+
import { rimraf } from '../../../utils/filesystem.js';
96
import options from '../../config/options.js';
107
import create_manifest_data from '../create_manifest_data/index.js';
118
import { tweak_types, write_all_types } from './index.js';
9+
import { execSync } from 'child_process';
1210

1311
const cwd = fileURLToPath(new URL('./test', import.meta.url));
1412

15-
function format_dts(file) {
16-
// format with the same settings we use in this monorepo so
17-
// the output is the same as visible when opening the $types.d.ts
18-
// files in the editor
19-
return format(fs.readFileSync(file, 'utf-8'), {
20-
parser: 'typescript',
21-
useTabs: true,
22-
singleQuote: true,
23-
trailingComma: 'none',
24-
printWidth: 100
25-
});
26-
}
27-
2813
/**
2914
* @param {string} dir
30-
* @param {(code: string) => string} [prepare_expected]
3115
*/
32-
async function run_test(dir, prepare_expected = (code) => code) {
16+
async function run_test(dir) {
3317
rimraf(path.join(cwd, dir, '.svelte-kit'));
3418

3519
const initial = options({}, 'config');
@@ -43,66 +27,25 @@ async function run_test(dir, prepare_expected = (code) => code) {
4327
config: /** @type {import('types').ValidatedConfig} */ (initial)
4428
});
4529
await write_all_types(initial, manifest);
46-
47-
const expected_dir = path.join(cwd, dir, '_expected');
48-
const expected_files = walk(expected_dir, true);
49-
const actual_dir = path.join(
50-
path.join(cwd, dir, '.svelte-kit', 'types'),
51-
path.relative(process.cwd(), path.join(cwd, dir))
52-
);
53-
const actual_files = walk(actual_dir, true);
54-
55-
assert.equal(actual_files, expected_files);
56-
57-
for (const file of actual_files) {
58-
const expected_file = path.join(expected_dir, file);
59-
const actual_file = path.join(actual_dir, file);
60-
if (fs.statSync(path.join(actual_dir, file)).isDirectory()) {
61-
assert.ok(fs.statSync(actual_file).isDirectory(), 'Expected a directory');
62-
continue;
63-
}
64-
65-
const expected = format_dts(expected_file);
66-
const actual = format_dts(actual_file);
67-
const err_msg = `Expected equal file contents for ${file} in ${dir}`;
68-
assert.fixture(actual, prepare_expected(expected), err_msg);
69-
}
7030
}
7131

72-
test('Create $types for +page.js', async () => {
32+
test('Creates correct $types', async () => {
33+
// To safe us from creating a real SvelteKit project for each of the tests,
34+
// we first run the type generation directly for each test case, and then
35+
// call `tsc` to check that the generated types are valid.
7336
await run_test('simple-page-shared-only');
74-
});
75-
76-
test('Create $types for page.server.js', async () => {
7737
await run_test('simple-page-server-only');
78-
});
79-
80-
test('Create $types for page(.server).js', async () => {
8138
await run_test('simple-page-server-and-shared');
82-
});
83-
84-
test('Create $types for layout and page', async () => {
8539
await run_test('layout');
86-
});
87-
88-
test('Create $types for grouped layout and page', async () => {
8940
await run_test('layout-advanced');
90-
});
91-
92-
test('Create $types with params', async () => {
93-
await run_test('slugs', (code) =>
94-
// For some reason, the order of the params differentiates between windows and mac/linux
95-
process.platform === 'win32'
96-
? code.replace(
97-
'rest?: string; slug?: string; optional?: string',
98-
'optional?: string; rest?: string; slug?: string'
99-
)
100-
: code
101-
);
102-
});
103-
104-
test('Create $types with params and required return types for layout', async () => {
41+
await run_test('slugs');
10542
await run_test('slugs-layout-not-all-pages-have-load');
43+
try {
44+
execSync('pnpm testtypes', { cwd });
45+
} catch (e) {
46+
console.error(/** @type {any} */ (e).stdout.toString());
47+
throw new Error('Type tests failed');
48+
}
10649
});
10750

10851
test('Rewrites types for a TypeScript module', () => {
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
// test to see if layout adjusts correctly if +page.js exists, but no load function
2+
3+
/** @type {import('../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/$types').PageData} */
4+
const data = {
5+
root: ''
6+
};
7+
data.root;
8+
// @ts-expect-error
9+
data.main;
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1-
export function load() {
1+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/sub/$types').PageLoad} */
2+
export async function load({ parent }) {
3+
const p = await parent();
4+
p.main;
5+
p.root;
6+
// @ts-expect-error
7+
p.sub;
28
return {
39
sub: 'sub'
410
};
511
}
12+
13+
/** @type {import('../../.svelte-kit/types/src/core/sync/write_types/test/layout-advanced/(main)/sub/$types').PageData} */
14+
const data = {
15+
main: '',
16+
root: '',
17+
sub: ''
18+
};
19+
data.main;
20+
data.root;
21+
data.sub;

packages/kit/src/core/sync/write_types/test/layout-advanced/_expected/$types.d.ts

-35
This file was deleted.

packages/kit/src/core/sync/write_types/test/layout-advanced/_expected/(main)/$types.d.ts

-43
This file was deleted.

packages/kit/src/core/sync/write_types/test/layout-advanced/_expected/(main)/sub/$types.d.ts

-38
This file was deleted.

packages/kit/src/core/sync/write_types/test/layout/+layout.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export function load() {
1+
/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').LayoutLoad} */
2+
export function load({ data }) {
3+
data.server;
4+
// @ts-expect-error
5+
data.shared;
26
return {
37
shared: 'shared'
48
};

packages/kit/src/core/sync/write_types/test/layout/+layout.server.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').LayoutServerLoad} */
12
export function load() {
23
return {
34
server: 'server'
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1-
export function load() {
1+
/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').PageLoad} */
2+
export function load({ data }) {
3+
data.pageServer;
4+
// @ts-expect-error
5+
data.pageShared;
26
return {
37
pageShared: 'pageShared'
48
};
59
}
10+
11+
/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/layout/$types').PageData} */
12+
const data = {
13+
shared: 'asd',
14+
pageShared: 'asd'
15+
};
16+
data.shared;
17+
data.pageShared;

0 commit comments

Comments
 (0)