Skip to content

Commit 5b3e1e6

Browse files
authored
[feat] svelte-kit package: Generate type definitions (#1646)
1 parent 939188e commit 5b3e1e6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+490
-24
lines changed

.changeset/odd-islands-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Add types generation to svelte-kit package command

documentation/docs/12-packaging.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ A SvelteKit component library has the exact same structure as a SvelteKit app, e
1010

1111
Running `svelte-kit package` will take the contents of `src/lib` and generate a `package` directory (which can be [configured](#configuration-package)) containing the following:
1212

13-
- All the files in `src/lib`, unless you [configure](#configuration-package) custom `include`/`exclude` options. Svelte components will be preprocessed (but note the [caveats](#packaging-caveats) below)
13+
- All the files in `src/lib`, unless you [configure](#configuration-package) custom `include`/`exclude` options. Svelte components will be preprocessed, TypeScript files will be transpiled to JavaScript.
14+
- Type definitions (`d.ts` files) which are generated for Svelte, JavaScript and TypeScript files. You need to install `typescript >= 4.0.0` and `svelte2tsx >= 0.4.1` for this. Type definitions are placed next to their implementation, hand-written `d.ts` files are copied over as is.
1415
- A `package.json` that copies the `name`, `version`, `description`, `keywords`, `homepage`, `bugs`, `license`, `author`, `contributors`, `funding`, `repository`, `dependencies`, `private` and `publishConfig` fields from the root of the project, and adds a `"type": "module"` and an `"exports"` field
1516

1617
The `"exports"` field contains the package's entry points. By default, all files in `src/lib` will be treated as an entry point unless they start with (or live in a directory that starts with) an underscore, but you can [configure](#configuration-package) this behaviour. If you have a `src/lib/index.js` or `src/lib/index.svelte` file, it will be treated as the package root.
@@ -37,8 +38,4 @@ The `package` above is referring to the directory name generated, change accordi
3738

3839
### Caveats
3940

40-
This is a relatively experimental feature and is not yet fully implemented:
41-
42-
- if a preprocessor is specified, `.svelte` files are transformed (meaning they can contain TypeScript, for example), but `.d.ts` files are not generated
43-
- `.ts` files are not currently transformed, and will cause the process to fail
44-
- all other files are copied across as-is
41+
This is a relatively experimental feature and is not yet fully implemented. All files except Svelte files (preprocessed) and TypeScript files (transpiled to JavaScript) are copied across as-is.

documentation/docs/14-configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ const config = {
4343
ssr: true,
4444
target: null,
4545
trailingSlash: 'never',
46+
package: {
47+
dir: 'package',
48+
exports: {
49+
include: ['**'],
50+
exclude: ['_*', '**/_*']
51+
},
52+
files: {
53+
include: ['**'],
54+
exclude: []
55+
}
56+
},
4657
vite: () => ({})
4758
},
4859

@@ -149,6 +160,14 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs to rou
149160

150161
> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](#hooks-handle) function.
151162
163+
### package
164+
165+
Options related to [creating a package](#packaging).
166+
167+
- `dir` - output directory
168+
- `exports` - contains a `includes` and a `excludes` array which specifies which files to mark as exported from the `exports` field of the `package.json`
169+
- `files` - contains a `includes` and a `excludes` array which specifies which files to process and copy over when packaging
170+
152171
### vite
153172

154173
A [Vite config object](https://vitejs.dev/config), or a function that returns one. Not all configuration options can be set, since SvelteKit depends on certain values being configured internally.

packages/kit/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"selfsigned": "^1.10.11",
3434
"sirv": "^1.0.12",
3535
"svelte": "^3.38.2",
36-
"svelte-check": "^1.5.2",
36+
"svelte2tsx": "~0.4.1",
37+
"svelte-check": "^2.2.0",
3738
"tiny-glob": "^0.2.8",
3839
"typescript": "^4.2.4",
3940
"uvu": "^0.5.1"
@@ -53,8 +54,8 @@
5354
"scripts": {
5455
"build": "rollup -c",
5556
"dev": "rollup -cw",
56-
"lint": "eslint --ignore-path .gitignore \"src/**/*.{ts,mjs,js,svelte}\" && npm run check-format",
57-
"check": "tsc && svelte-check",
57+
"lint": "eslint --ignore-path .gitignore --ignore-pattern \"src/core/make_package/test/**\" \"src/**/*.{ts,mjs,js,svelte}\" && npm run check-format",
58+
"check": "tsc && svelte-check --ignore \"src/core/make_package/test\"",
5859
"format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore",
5960
"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
6061
"prepublishOnly": "npm run build",

packages/kit/rollup.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const external = [].concat(
1010
Object.keys(pkg.dependencies || {}),
1111
Object.keys(pkg.peerDependencies || {}),
1212
Object.keys(process.binding('natives')),
13-
'typescript'
13+
'typescript',
14+
'svelte2tsx'
1415
);
1516

1617
export default [

packages/kit/src/core/config/options.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const noop = () => {};
1+
const identity = (/** @type {any} */ id) => id;
22

33
/** @typedef {import('./types').ConfigDefinition} ConfigDefinition */
44

@@ -7,7 +7,7 @@ const options = {
77
compilerOptions: {
88
type: 'leaf',
99
default: null,
10-
validate: noop
10+
validate: identity
1111
},
1212

1313
extensions: {
@@ -169,7 +169,7 @@ const options = {
169169
preprocess: {
170170
type: 'leaf',
171171
default: null,
172-
validate: noop
172+
validate: identity
173173
}
174174
};
175175

packages/kit/src/core/make_package/index.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as fs from 'fs';
2+
import globrex from 'globrex';
3+
import { createRequire } from 'module';
24
import * as path from 'path';
35
import { preprocess } from 'svelte/compiler';
4-
import globrex from 'globrex';
5-
import { mkdirp, rimraf } from '../filesystem';
6+
import { mkdirp, rimraf } from '../filesystem/index.js';
67

78
/**
89
* @param {import('types/config').ValidatedConfig} config
@@ -11,8 +12,14 @@ import { mkdirp, rimraf } from '../filesystem';
1112
export async function make_package(config, cwd = process.cwd()) {
1213
rimraf(path.join(cwd, config.kit.package.dir));
1314

15+
// Generate type definitions first so hand-written types can overwrite generated ones
16+
await emit_dts(config);
17+
1418
const files_filter = create_filter(config.kit.package.files);
15-
const exports_filter = create_filter(config.kit.package.exports);
19+
const exports_filter = create_filter({
20+
...config.kit.package.exports,
21+
exclude: [...config.kit.package.exports.exclude, '*.d.ts']
22+
});
1623

1724
const files = walk(config.kit.files.lib);
1825

@@ -57,12 +64,22 @@ export async function make_package(config, cwd = process.cwd()) {
5764

5865
if (svelte_ext) {
5966
// it's a Svelte component
60-
// TODO how to emit types?
6167
out_file = file.slice(0, -svelte_ext.length) + '.svelte';
6268
out_contents = config.preprocess
6369
? (await preprocess(source, config.preprocess, { filename })).code
6470
: source;
65-
} else if (ext === '.ts' && !file.endsWith('.d.ts')) {
71+
} else if (ext === '.ts' && file.endsWith('.d.ts')) {
72+
// TypeScript's declaration emit won't copy over the d.ts files, so we do it here
73+
out_file = file;
74+
out_contents = source;
75+
if (fs.existsSync(path.join(cwd, config.kit.package.dir, out_file))) {
76+
console.warn(
77+
'Found already existing file from d.ts generation for ' +
78+
out_file +
79+
'. This file will be overwritten.'
80+
);
81+
}
82+
} else if (ext === '.ts') {
6683
out_file = file.slice(0, -'.ts'.length) + '.js';
6784
out_contents = await transpile_ts(filename, source);
6885
} else {
@@ -134,7 +151,7 @@ function load_tsconfig(filename, ts) {
134151
const { error, config } = ts.readConfigFile(tsconfig_filename, ts.sys.readFile);
135152

136153
if (error) {
137-
throw new Error('Malformed tsconfig');
154+
throw new Error('Malformed tsconfig\n' + JSON.stringify(error, null, 2));
138155
}
139156

140157
// Do this so TS will not search for initial files which might take a while
@@ -200,3 +217,30 @@ function write(file, contents) {
200217
mkdirp(path.dirname(file));
201218
fs.writeFileSync(file, contents);
202219
}
220+
221+
/**
222+
* @param {import('types/config').ValidatedConfig} config
223+
*/
224+
export async function emit_dts(config) {
225+
const require = createRequire(import.meta.url);
226+
const emit = await try_load_svelte2tsx();
227+
emit({
228+
libRoot: config.kit.files.lib,
229+
svelteShimsPath: require.resolve('svelte2tsx/svelte-shims.d.ts'),
230+
declarationDir: config.kit.package.dir
231+
});
232+
}
233+
234+
async function try_load_svelte2tsx() {
235+
try {
236+
const svelte2tsx = (await import('svelte2tsx')).emitDts;
237+
if (!svelte2tsx) {
238+
throw new Error('Old svelte2tsx version');
239+
}
240+
return svelte2tsx;
241+
} catch (e) {
242+
throw new Error(
243+
'You need to install svelte2tsx >=0.4.1 if you want to generate type definitions'
244+
);
245+
}
246+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
/**
4+
* @type {string}
5+
*/
6+
export const astring;
7+
8+
const dispatch = createEventDispatcher();
9+
dispatch('event', true);
10+
</script>
11+
12+
<slot {astring} />
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/** @typedef {typeof __propDef.props} TestProps */
2+
/** @typedef {typeof __propDef.events} TestEvents */
3+
/** @typedef {typeof __propDef.slots} TestSlots */
4+
export default class Test extends SvelteComponentTyped<
5+
{
6+
astring: string;
7+
},
8+
{
9+
event: CustomEvent<any>;
10+
} & {
11+
[evt: string]: CustomEvent<any>;
12+
},
13+
{
14+
default: {
15+
astring: string;
16+
};
17+
}
18+
> {
19+
get astring(): string;
20+
}
21+
export type TestProps = typeof __propDef.props;
22+
export type TestEvents = typeof __propDef.events;
23+
export type TestSlots = typeof __propDef.slots;
24+
import { SvelteComponentTyped } from 'svelte';
25+
declare const __propDef: {
26+
props: {
27+
astring: string;
28+
};
29+
events: {
30+
event: CustomEvent<any>;
31+
} & {
32+
[evt: string]: CustomEvent<any>;
33+
};
34+
slots: {
35+
default: {
36+
astring: string;
37+
};
38+
};
39+
};
40+
export {};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
/**
3+
* @type {import('./foo').Foo}
4+
*/
5+
export let foo;
6+
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/** @typedef {typeof __propDef.props} Test2Props */
2+
/** @typedef {typeof __propDef.events} Test2Events */
3+
/** @typedef {typeof __propDef.slots} Test2Slots */
4+
export default class Test2 extends SvelteComponentTyped<
5+
{
6+
foo: boolean;
7+
},
8+
{
9+
[evt: string]: CustomEvent<any>;
10+
},
11+
{}
12+
> {}
13+
export type Test2Props = typeof __propDef.props;
14+
export type Test2Events = typeof __propDef.events;
15+
export type Test2Slots = typeof __propDef.slots;
16+
import { SvelteComponentTyped } from 'svelte';
17+
declare const __propDef: {
18+
props: {
19+
foo: import('./foo').Foo;
20+
};
21+
events: {
22+
[evt: string]: CustomEvent<any>;
23+
};
24+
slots: {};
25+
};
26+
export {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Foo = boolean;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Test } from './Test.svelte';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Test } from './Test.svelte';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "javascript",
3+
"version": "1.0.0",
4+
"description": "package-javascript-test",
5+
"type": "module",
6+
"exports": {
7+
"./package.json": "./package.json",
8+
"./index.js": "./index.js",
9+
"./Test.svelte": "./Test.svelte",
10+
"./Test2.svelte": "./Test2.svelte",
11+
".": "./index.js"
12+
}
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"checkJs": true,
4+
"paths": {
5+
"$lib/*": ["src/lib/*"]
6+
}
7+
},
8+
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "javascript",
3+
"version": "1.0.0",
4+
"description": "package-javascript-test"
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
%svelte.head%
8+
</head>
9+
<body>
10+
<div id="svelte">%svelte.body%</div>
11+
</body>
12+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
/**
4+
* @type {string}
5+
*/
6+
export const astring;
7+
8+
const dispatch = createEventDispatcher();
9+
dispatch('event', true);
10+
</script>
11+
12+
<slot {astring} />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
/**
3+
* @type {import('./foo').Foo}
4+
*/
5+
export let foo;
6+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Foo = boolean;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Test } from './Test.svelte';

packages/kit/src/core/make_package/test/fixtures/javascript/svelte.config.js

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from 'svelte';
3+
export const astring;
4+
const dispatch = createEventDispatcher();
5+
dispatch('event', true);
6+
</script>
7+
8+
<slot {astring} />

0 commit comments

Comments
 (0)