docs(site-2): Action, TypeScript (#8588)

pull/8671/head
Puru Vijay 2 years ago committed by GitHub
parent 9f0fe3070f
commit 4cde4eaada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -455,6 +455,8 @@ An action can have a parameter. If the returned value has an `update` method, it
<div use:foo={bar} /> <div use:foo={bar} />
``` ```
Read more in the [`svelte/action`](/docs/svelte-action) page.
## transition:_fn_ ## transition:_fn_
```svelte ```svelte
@ -526,6 +528,10 @@ The function is called repeatedly _before_ the transition begins, with different
/** @type {boolean} */ /** @type {boolean} */
export let visible; export let visible;
/**
* @param {HTMLElement} node
* @param {{ delay?: number, duration?: number, easing?: (t: number) => number }} params
*/
function whoosh(node, params) { function whoosh(node, params) {
const existingTransform = getComputedStyle(node).transform.replace('none', ''); const existingTransform = getComputedStyle(node).transform.replace('none', '');
@ -548,9 +554,14 @@ A custom transition function can also return a `tick` function, which is called
> If it's possible to use `css` instead of `tick`, do so — CSS animations can run off the main thread, preventing jank on slower devices. > If it's possible to use `css` instead of `tick`, do so — CSS animations can run off the main thread, preventing jank on slower devices.
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
export let visible = false; export let visible = false;
/**
* @param {HTMLElement} node
* @param {{ speed?: number }} params
*/
function typewriter(node, { speed = 1 }) { function typewriter(node, { speed = 1 }) {
const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE; const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
@ -740,9 +751,7 @@ The function is called repeatedly _before_ the animation begins, with different
/** /**
* @param {HTMLElement} node * @param {HTMLElement} node
* @param {Object} states * @param {{ from: DOMRect; to: DOMRect }} states
* @param {DOMRect} states.from
* @param {DOMRect} states.to
* @param {any} params * @param {any} params
*/ */
function whizz(node, { from, to }, params) { function whizz(node, { from, to }, params) {
@ -770,14 +779,13 @@ A custom animation function can also return a `tick` function, which is called _
> If it's possible to use `css` instead of `tick`, do so — CSS animations can run off the main thread, preventing jank on slower devices. > If it's possible to use `css` instead of `tick`, do so — CSS animations can run off the main thread, preventing jank on slower devices.
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
/** /**
* @param {HTMLElement} node * @param {HTMLElement} node
* @param {Object} states * @param {{ from: DOMRect; to: DOMRect }} states
* @param {DOMRect} states.from
* @param {DOMRect} states.to
* @param {any} params * @param {any} params
*/ */
function whizz(node, { from, to }, params) { function whizz(node, { from, to }, params) {

@ -2,7 +2,79 @@
title: svelte/action title: svelte/action
--- ---
TODO Actions are functions that are called when an element is created. They can return an object with a `destroy` method that is called after the element is unmounted:
```svelte
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/action').Action} */
function foo(node) {
// the node has been mounted in the DOM
return {
destroy() {
// the node has been removed from the DOM
}
};
}
</script>
<div use:foo />
```
An action can have a parameter. If the returned value has an `update` method, it will be called immediately after Svelte has applied updates to the markup whenever that parameter changes.
> Don't worry that we're redeclaring the `foo` function for every component instance — Svelte will hoist any functions that don't depend on local state out of the component definition.
```svelte
<!--- file: App.svelte --->
<script>
export let bar;
/** @type {import('svelte/action').Action} */
function foo(node, bar) {
// the node has been mounted in the DOM
return {
update(bar) {
// the value of `bar` has changed
},
destroy() {
// the node has been removed from the DOM
}
};
}
</script>
<div use:foo={bar} />
```
## Attributes
Sometimes actions emit custom events and apply custom attributes to the element they are applied to. To support this, actions typed with `Action` or `ActionReturn` type can have a last parameter, `Attributes`:
```svelte
<!--- file: App.svelte --->
<script>
/**
* @type {import('svelte/action').Action<HTMLDivElement, { prop: any }, { 'on:emit': (e: CustomEvent<string>) => void }>} */
function foo(node, { prop }) {
// the node has been mounted in the DOM
//...LOGIC
node.dispatchEvent(new CustomEvent('emit', { detail: 'hello' }));
return {
destroy() {
// the node has been removed from the DOM
}
};
}
</script>
<div use:foo={{ prop: 'someValue' }} on:emit={handleEmit} />
```
## Types ## Types

@ -2,6 +2,193 @@
title: TypeScript title: TypeScript
--- ---
You can use TypeScript within Svelte components. IDE extensions like the [Svelte VSCode extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) will help you catch errors right in your editor, and [`svelte-check`](https://www.npmjs.com/package/svelte-check) does the same on the command line, which you can integrate into your CI.
## Setup
To use TypeScript within Svelte components, you need to add a preprocessor that will turn TypeScript into JavaScript.
### Using SvelteKit or Vite
The easiest way to get started is scaffolding a new SvelteKit project by typing `npm create svelte@latest`, following the prompts and chosing the TypeScript option.
```ts
/// file: svelte.config.js
// @noErrors
import { vitePreprocess } from '@sveltejs/kit/vite';
const config = {
preprocess: vitePreprocess()
};
export default config;
```
If you don't need or want all the features SvelteKit has to offer, you can scaffold a Svelte-flavoured Vite project instead by typing `npm create vite@latest` and selecting the `svelte-ts` option.
```ts
/// file: svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess()
};
export default config;
```
In both cases, a `svelte.config.js` with `vitePreprocess` will be added. Vite/SvelteKit will read from this config file.
### Other build tools
If you're using tools like Rollup or Webpack instead, install their respective Svelte plugins. For Rollup that's [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) and for Webpack that's [svelte-loader](https://github.com/sveltejs/svelte-loader). For both, you need to install `typescript` and `svelte-preprocess` and add the preprocessor to the plugin config (see the respective READMEs for more info). If you're starting a new project, you can also use the [rollup](https://github.com/sveltejs/template) or [webpack](https://github.com/sveltejs/template-webpack) template to scaffold the setup from a script.
> If you're starting a new project, we recommend using SvelteKit or Vite instead
## `<script lang="ts">`
To use TypeScript inside your Svelte components, add `lang="ts"` to your `script` tags:
```svelte
<script lang="ts">
let name: string = 'world';
function greet(name: string) {
alert(`Hello, ${name}!`);
}
</script>
```
### Props
Props can be typed directly on the `export let` statement:
```svelte
<script lang="ts">
export let name: string;
</script>
```
### Slots
Slot and slot prop types are inferred from the types of the slot props passed to them:
```svelte
<script lang="ts">
export let name: string;
</script>
<slot {name} />
<!-- Later -->
<Comp let:name>
<!-- ^ Inferred as string -->
{name}
</Comp>
```
### Events
Events can be typed with `createEventDispatcher`:
```svelte
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
event: never; // does not accept a payload
type: string; // has a required string payload
click: string | null; // has an optional string payload
}>();
function handleClick() {
dispatch('even');
dispatch('click', 'hello');
}
function handleType() {
dispatch('even');
dispatch('type', Math.random() > 0.5 ? 'world' : null);
}
</script>
<button on:click={handleClick} on:keydown={handleType}>Click</button>
```
## Enhancing built-in DOM types
Svelte provides a best effort of all the HTML DOM types that exist. Sometimes you may want to use experimental attributes or custom events coming from an action. In these cases, TypeScript will throw a type error, saying that it does not know these types. If it's a non-experimental standard attribute/event, this may very well be a missing typing from our [HTML typings](https://github.com/sveltejs/svelte/blob/master/elements/index.d.ts). In that case, you are welcome to open an issue and/or a PR fixing it.
In case this is a custom or experimental attribute/event, you can enhance the typings like this:
```ts
/// file: additional-svelte-typings.d.ts
declare namespace svelteHTML {
// enhance elements
interface IntrinsicElements {
'my-custom-element': { someattribute: string; 'on:event': (e: CustomEvent<any>) => void };
}
// enhance attributes
interface HTMLAttributes<T> {
// If you want to use on:beforeinstallprompt
'on:beforeinstallprompt'?: (event: any) => any;
// If you want to use myCustomAttribute={..} (note: all lowercase)
mycustomattribute?: any; // You can replace any with something more specific if you like
}
}
```
Then make sure that `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect.
## Experimental advanced typings
A few features are missing from taking full advantage of TypeScript in more advanced use cases like typing that a component implements a certain interface, explicitly typing slots, or using generics. These things are possible using experimental advanced type capabilities. See [this RFC](https://github.com/dummdidumm/rfcs/blob/ts-typedefs-within-svelte-components/text/ts-typing-props-slots-events.md) for more information on how to make use of them.
> The API is experimental and may change at any point
## Limitations
### No TS in markup
You cannot use TypeScript in your template's markup. For example, the following does not work:
```svelte
<script lang="ts">
let count = 10;
</script>
<h1>Count as string: {count as string}!</h1> <!-- ❌ Does not work -->
{#if count > 4}
{@const countString: string = count} <!-- ❌ Does not work -->
{countString}
{/if}
```
### Reactive Declarations
You cannot type your reactive declarations with TypeScript in the way you type a variable. For example, the following does not work:
```svelte
<script lang="ts">
let count = 0;
$: doubled: number = count * 2; // ❌ Does not work
</script>
```
You cannot add a `: TYPE` because it's invalid syntax in this position. Instead, you can use the `as` or move the definition to a `let` statement just above:
```svelte
<script lang="ts">
let count = 0;
$: option1 = (count * 2) as number;
let option2: number;
$: option2 = count * 2;
</script>
```
## Types ## Types
> TYPES: svelte > TYPES: svelte

@ -159,12 +159,18 @@ importers:
'@sveltejs/site-kit': '@sveltejs/site-kit':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(@sveltejs/kit@1.18.0)(svelte@) version: 5.2.1(@sveltejs/kit@1.18.0)(svelte@)
'@sveltejs/vite-plugin-svelte':
specifier: ^2.3.0
version: 2.3.0(svelte@)(vite@4.3.8)
'@types/marked': '@types/marked':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0 version: 5.0.0
'@types/node': '@types/node':
specifier: ^20.2.1 specifier: ^20.2.1
version: 20.2.3 version: 20.2.3
'@types/prettier':
specifier: ^2.7.2
version: 2.7.2
degit: degit:
specifier: ^2.8.4 specifier: ^2.8.4
version: 2.8.4 version: 2.8.4
@ -219,6 +225,9 @@ importers:
svelte-check: svelte-check:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2(postcss@8.4.23)(sass@1.62.1)(svelte@) version: 3.3.2(postcss@8.4.23)(sass@1.62.1)(svelte@)
svelte-preprocess:
specifier: ^5.0.3
version: 5.0.3(postcss@8.4.23)(sass@1.62.1)(svelte@)(typescript@5.0.4)
tiny-glob: tiny-glob:
specifier: ^0.2.9 specifier: ^0.2.9
version: 0.2.9 version: 0.2.9
@ -1510,7 +1519,7 @@ packages:
svelte: ^3.54.0 svelte: ^3.54.0
vite: ^4.0.0 vite: ^4.0.0
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 2.2.0(svelte@)(vite@4.3.8) '@sveltejs/vite-plugin-svelte': 2.3.0(svelte@)(vite@4.3.8)
'@types/cookie': 0.5.1 '@types/cookie': 0.5.1
cookie: 0.5.0 cookie: 0.5.0
devalue: 4.3.2 devalue: 4.3.2
@ -1590,13 +1599,29 @@ packages:
svelte-local-storage-store: 0.4.0(svelte@) svelte-local-storage-store: 0.4.0(svelte@)
dev: true dev: true
/@sveltejs/vite-plugin-svelte@2.2.0(svelte@)(vite@4.3.8): /@sveltejs/vite-plugin-svelte-inspector@1.0.1(@sveltejs/vite-plugin-svelte@2.3.0)(svelte@)(vite@4.3.8):
resolution: {integrity: sha512-KDtdva+FZrZlyug15KlbXuubntAPKcBau0K7QhAIqC5SAy0uDbjZwoexDRx0L0J2T4niEfC6FnA9GuQQJKg+Aw==} resolution: {integrity: sha512-8ZXgDbAL1b2o7WHxnPsbkxTzZiZhMwOsCI/GFti3zFlh8unqJtUsgwRQV/XSULFcqkbZXz5v6MqMLSUpl3VKaA==}
engines: {node: ^14.18.0 || >= 16} engines: {node: ^14.18.0 || >= 16}
peerDependencies: peerDependencies:
'@sveltejs/vite-plugin-svelte': ^2.2.0
svelte: ^3.54.0 svelte: ^3.54.0
vite: ^4.0.0 vite: ^4.0.0
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 2.3.0(svelte@)(vite@4.3.8)
debug: 4.3.4
svelte: 'link:'
vite: 4.3.8(@types/node@20.2.3)(sass@1.62.1)
transitivePeerDependencies:
- supports-color
/@sveltejs/vite-plugin-svelte@2.3.0(svelte@)(vite@4.3.8):
resolution: {integrity: sha512-NbgDn5/auWfGYFip7DheDj49/JLE6VugdtdLJjnQASYxXqrQjl81xaZzQsoSAxWk+j2mOkmPFy56gV2i63FUnA==}
engines: {node: ^14.18.0 || >= 16}
peerDependencies:
svelte: ^3.54.0
vite: ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 1.0.1(@sveltejs/vite-plugin-svelte@2.3.0)(svelte@)(vite@4.3.8)
debug: 4.3.4 debug: 4.3.4
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
@ -1681,6 +1706,10 @@ packages:
resolution: {integrity: sha512-e7jZ6I9uyRGsg7MNwQcarmBvRlbGb9DibbocE9crVnxqsy6C23RMxLWbJ2CQ3vgCW7taoL1L+F02EcjA6ld7XA==} resolution: {integrity: sha512-e7jZ6I9uyRGsg7MNwQcarmBvRlbGb9DibbocE9crVnxqsy6C23RMxLWbJ2CQ3vgCW7taoL1L+F02EcjA6ld7XA==}
dev: false dev: false
/@types/prettier@2.7.2:
resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==}
dev: true
/@types/pug@2.0.6: /@types/pug@2.0.6:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: true dev: true

@ -30,8 +30,10 @@
"@sveltejs/adapter-vercel": "^3.0.0", "@sveltejs/adapter-vercel": "^3.0.0",
"@sveltejs/kit": "^1.18.0", "@sveltejs/kit": "^1.18.0",
"@sveltejs/site-kit": "^5.2.1", "@sveltejs/site-kit": "^5.2.1",
"@sveltejs/vite-plugin-svelte": "^2.3.0",
"@types/marked": "^5.0.0", "@types/marked": "^5.0.0",
"@types/node": "^20.2.1", "@types/node": "^20.2.1",
"@types/prettier": "^2.7.2",
"degit": "^2.8.4", "degit": "^2.8.4",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"jimp": "^0.22.8", "jimp": "^0.22.8",
@ -50,6 +52,7 @@
"shiki-twoslash": "^3.1.2", "shiki-twoslash": "^3.1.2",
"svelte": "workspace:*", "svelte": "workspace:*",
"svelte-check": "^3.3.2", "svelte-check": "^3.3.2",
"svelte-preprocess": "^5.0.3",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"ts-morph": "^18.0.0", "ts-morph": "^18.0.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",

@ -0,0 +1 @@
export type * from 'svelte/internal';

@ -4,7 +4,14 @@ import prettier from 'prettier';
import ts from 'typescript'; import ts from 'typescript';
import { get_bundled_types } from './compile-types.js'; import { get_bundled_types } from './compile-types.js';
/** @typedef {{ name: string; comment: string; markdown: string; snippet: string; children: Extracted[] }} Extracted */ /** @typedef {{
* name: string;
* comment: string;
* markdown?: string;
* snippet: string;
* deprecated: string | null;
* children: Extracted[] }
* } Extracted */
/** @type {Array<{ name: string; comment: string; exports: Extracted[]; types: Extracted[]; exempt?: boolean; }>} */ /** @type {Array<{ name: string; comment: string; exports: Extracted[]; types: Extracted[]; exempt?: boolean; }>} */
const modules = []; const modules = [];
@ -44,29 +51,41 @@ function get_types(code, statements) {
let start = statement.pos; let start = statement.pos;
let comment = ''; let comment = '';
/** @type {string | null} */
let deprecated_notice = null;
// @ts-ignore i think typescript is bad at typescript // @ts-ignore i think typescript is bad at typescript
if (statement.jsDoc) { if (statement.jsDoc) {
// @ts-ignore // @ts-ignore
comment = statement.jsDoc[0].comment; const jsDoc = statement.jsDoc[0];
comment = jsDoc.comment;
if (jsDoc?.tags?.[0]?.tagName?.escapedText === 'deprecated') {
deprecated_notice = jsDoc.tags[0].comment;
}
// @ts-ignore // @ts-ignore
start = statement.jsDoc[0].end; start = jsDoc.end;
} }
const i = code.indexOf('export', start); const i = code.indexOf('export', start);
start = i + 6; start = i + 6;
/** @type {string[]} */ /** @type {Extracted[]} */
const children = []; let children = [];
let snippet_unformatted = code.slice(start, statement.end).trim(); let snippet_unformatted = code.slice(start, statement.end).trim();
if (ts.isInterfaceDeclaration(statement)) { if (ts.isInterfaceDeclaration(statement) || ts.isClassDeclaration(statement)) {
if (statement.members.length > 0) { if (statement.members.length > 0) {
for (const member of statement.members) { for (const member of statement.members) {
// @ts-ignore
children.push(munge_type_element(member)); children.push(munge_type_element(member));
} }
children = children.filter(Boolean);
// collapse `interface Foo {/* lots of stuff*/}` into `interface Foo {…}` // collapse `interface Foo {/* lots of stuff*/}` into `interface Foo {…}`
const first = statement.members.at(0); const first = statement.members.at(0);
const last = statement.members.at(-1); const last = statement.members.at(-1);
@ -100,7 +119,13 @@ function get_types(code, statements) {
? exports ? exports
: types; : types;
collection.push({ name, comment, snippet, children }); collection.push({
name,
comment,
snippet,
children,
deprecated: deprecated_notice
});
} }
} }
@ -118,6 +143,8 @@ function munge_type_element(member, depth = 1) {
// @ts-ignore // @ts-ignore
const doc = member.jsDoc?.[0]; const doc = member.jsDoc?.[0];
if (/do not use!/i.test(doc?.comment)) return;
/** @type {string[]} */ /** @type {string[]} */
const children = []; const children = [];
@ -150,6 +177,14 @@ function munge_type_element(member, depth = 1) {
const type = tag.tagName.escapedText; const type = tag.tagName.escapedText;
switch (tag.tagName.escapedText) { switch (tag.tagName.escapedText) {
case 'private':
bullets.push(`- <span class="tag">private</span> ${tag.comment}`);
break;
case 'readonly':
bullets.push(`- <span class="tag">readonly</span> ${tag.comment}`);
break;
case 'param': case 'param':
bullets.push(`- \`${tag.name.getText()}\` ${tag.comment}`); bullets.push(`- \`${tag.name.getText()}\` ${tag.comment}`);
break; break;
@ -162,6 +197,10 @@ function munge_type_element(member, depth = 1) {
bullets.push(`- <span class="tag">returns</span> ${tag.comment}`); bullets.push(`- <span class="tag">returns</span> ${tag.comment}`);
break; break;
case 'deprecated':
bullets.push(`- <span class="tag deprecated">deprecated</span> ${tag.comment}`);
break;
default: default:
console.log(`unhandled JSDoc tag: ${type}`); // TODO indicate deprecated stuff console.log(`unhandled JSDoc tag: ${type}`); // TODO indicate deprecated stuff
} }
@ -289,58 +328,32 @@ $: {
if (!module_with_SvelteComponent) break $; if (!module_with_SvelteComponent) break $;
const svelte_comp_part = module_with_SvelteComponent?.types.filter( const svelte_comp_part = module_with_SvelteComponent?.types.find(
(t) => t.name === 'SvelteComponent' (t) => t.name === 'SvelteComponent'
); );
if (svelte_comp_part?.[1]) { if (!svelte_comp_part) break $;
// Take the comment from [0], and insert into [1]. Then delete [0]
svelte_comp_part[1].comment = svelte_comp_part?.[0].comment;
delete svelte_comp_part[0];
svelte_comp_part.reverse();
svelte_comp_part.length = 1;
module_with_SvelteComponent.types = module_with_SvelteComponent?.types.filter( const internal_module = bundled_types.get('svelte/internal');
(t) => t.name !== 'SvelteComponent' if (!internal_module) break $;
);
module_with_SvelteComponent.types.push(svelte_comp_part[0]); const internal_types = get_types(internal_module.code, internal_module.ts_source_file.statements);
module_with_SvelteComponent.types.sort((a, b) => (a.name < b.name ? -1 : 1));
}
}
// Fix the duplicate/messed up types const svelte_comp_dev_internal = internal_types.types.find(
// !NOTE: This relies on mutation of `modules` (t) => t.name === 'SvelteComponentDev'
$: {
const module_with_SvelteComponentTyped = modules.find((m) =>
m.types.filter((t) => t.name === 'SvelteComponentTyped')
); );
if (!module_with_SvelteComponentTyped) break $; if (!svelte_comp_dev_internal) break $;
const svelte_comp_typed_part = module_with_SvelteComponentTyped?.types.filter( svelte_comp_part.children = svelte_comp_dev_internal.children;
(t) => t.name === 'SvelteComponentTyped' svelte_comp_part.comment = svelte_comp_dev_internal.comment;
); svelte_comp_part.snippet = svelte_comp_dev_internal.snippet;
if (svelte_comp_typed_part?.[1]) {
// Take the comment from [1], and insert into [0]. Then delete [1]
svelte_comp_typed_part[0].comment = svelte_comp_typed_part?.[1].comment;
delete svelte_comp_typed_part[1];
svelte_comp_typed_part.length = 1;
module_with_SvelteComponentTyped.types = module_with_SvelteComponentTyped?.types.filter(
(t) => t.name !== 'SvelteComponentTyped'
);
module_with_SvelteComponentTyped.types.push(svelte_comp_typed_part[0]);
module_with_SvelteComponentTyped.types.sort((a, b) => (a.name < b.name ? -1 : 1));
}
} }
// Remove $$_attributes from ActionReturn // Remove $$_attributes from ActionReturn
$: { $: {
const module_with_ActionReturn = modules.find((m) => const module_with_ActionReturn = modules.find((m) =>
m.types.find((t) => t.name === 'ActionReturn') m.types.find((t) => t?.name === 'ActionReturn')
); );
const new_children = const new_children =

@ -10,6 +10,7 @@ type Child = {
name: string; name: string;
snippet: string; snippet: string;
comment: string; comment: string;
deprecated?: string;
bullets?: string[]; bullets?: string[];
children?: Child[]; children?: Child[];
}; };

@ -2,6 +2,7 @@ import MagicString from 'magic-string';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { format } from 'prettier';
import { createShikiHighlighter, renderCodeToHTML, runTwoSlash } from 'shiki-twoslash'; import { createShikiHighlighter, renderCodeToHTML, runTwoSlash } from 'shiki-twoslash';
import ts from 'typescript'; import ts from 'typescript';
import { import {
@ -142,12 +143,16 @@ export async function render_markdown(
return html; return html;
}, },
codespan: (text) => { codespan: (text) => {
return '<code>' + type_regex return (
? text.replace(type_regex, (_, prefix, name) => { '<code>' +
const link = `<a href="${type_links.get(name)}">${name}</a>`; (type_regex
return `${prefix || ''}${link}`; ? text.replace(type_regex, (_, prefix, name) => {
}) const link = `<a href="${type_links.get(name)}">${name}</a>`;
: text + '</code>'; return `${prefix || ''}${link}`;
})
: text) +
'</code>'
);
} }
}); });
} }
@ -286,6 +291,7 @@ function convert_to_ts(js_code, indent = '', offset = '') {
for (const comment of node.jsDoc) { for (const comment of node.jsDoc) {
let modified = false; let modified = false;
let count = 0;
for (const tag of comment.tags ?? []) { for (const tag of comment.tags ?? []) {
if (ts.isJSDocTypeTag(tag)) { if (ts.isJSDocTypeTag(tag)) {
const [name, generics] = get_type_info(tag); const [name, generics] = get_type_info(tag);
@ -299,16 +305,19 @@ function convert_to_ts(js_code, indent = '', offset = '') {
const is_async = node.modifiers?.some( const is_async = node.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword
); );
const type = generics !== undefined ? `${name}<${generics}>` : name;
code.overwrite( code.overwrite(
node.getStart(), node.getStart(),
node.name.getEnd(), node.name.getEnd(),
`${is_export ? 'export ' : ''}const ${node.name.getText()} = (${ `${is_export ? 'export ' : ''}const ${node.name.getText()}: ${type} = (${
is_async ? 'async ' : '' is_async ? 'async ' : ''
}` }`
); );
code.appendLeft(node.body.getStart(), '=> '); code.appendLeft(node.body.getStart(), '=> ');
const type = generics !== undefined ? `${name}${generics}` : name; code.appendLeft(node.body.getEnd(), ')');
code.appendLeft(node.body.getEnd(), `) satisfies ${type};`);
modified = true; modified = true;
} else if ( } else if (
@ -333,11 +342,30 @@ function convert_to_ts(js_code, indent = '', offset = '') {
// 'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText() // 'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText()
// ); // );
// } // }
const [name] = get_type_info(tag);
code.appendLeft(node.parameters[0].getEnd(), `: ${name}`); const sanitised_param = tag
.getFullText()
.replace(/\s+/g, '')
.replace(/(^\*|\*$)/g, '');
const [, param_type] = /@param{(.+)}(.+)/.exec(sanitised_param);
let param_count = 0;
for (const param of node.parameters) {
if (count !== param_count) {
param_count++;
continue;
}
code.appendLeft(param.getEnd(), `:${param_type}`);
param_count++;
}
modified = true; modified = true;
} }
count++;
} }
if (modified) { if (modified) {
@ -372,7 +400,19 @@ function convert_to_ts(js_code, indent = '', offset = '') {
code.appendLeft(insertion_point, offset + import_statements + '\n'); code.appendLeft(insertion_point, offset + import_statements + '\n');
} }
const transformed = code.toString(); let transformed = format(code.toString(), {
printWidth: 100,
parser: 'typescript',
useTabs: true,
singleQuote: true
});
// Indent transformed's each line by 2
transformed = transformed
.split('\n')
.map((line) => indent.repeat(1) + line)
.join('\n');
return transformed === js_code ? undefined : transformed.replace(/\n\s*\n\s*\n/g, '\n\n'); return transformed === js_code ? undefined : transformed.replace(/\n\s*\n\s*\n/g, '\n\n');
/** @param {ts.JSDocTypeTag | ts.JSDocParameterTag} tag */ /** @param {ts.JSDocTypeTag | ts.JSDocParameterTag} tag */
@ -380,7 +420,15 @@ function convert_to_ts(js_code, indent = '', offset = '') {
const type_text = tag.typeExpression.getText(); const type_text = tag.typeExpression.getText();
let name = type_text.slice(1, -1); // remove { } let name = type_text.slice(1, -1); // remove { }
const import_match = /import\('(.+?)'\)\.(\w+)(<{?[\n\* \w:;,]+}?>)?/.exec(type_text); const single_line_name = format(name, {
printWidth: 1000,
parser: 'typescript',
semi: false,
singleQuote: true
}).replace('\n', '');
const import_match = /import\('(.+?)'\)\.(\w+)(?:<(.+)>)?$/s.exec(single_line_name);
if (import_match) { if (import_match) {
const [, from, _name, generics] = import_match; const [, from, _name, generics] = import_match;
name = _name; name = _name;
@ -476,8 +524,12 @@ export function replace_export_type_placeholders(content, modules) {
.map((t) => { .map((t) => {
let children = t.children.map((val) => stringify(val, 'dts')).join('\n\n'); let children = t.children.map((val) => stringify(val, 'dts')).join('\n\n');
const deprecated = t.deprecated
? ` <blockquote class="tag deprecated">${transform(t.deprecated)}</blockquote>`
: '';
const markdown = `<div class="ts-block">${fence(t.snippet, 'dts')}` + children + `</div>`; const markdown = `<div class="ts-block">${fence(t.snippet, 'dts')}` + children + `</div>`;
return `### [TYPE]: ${t.name}\n\n${t.comment}\n\n${markdown}\n\n`; return `### [TYPE]: ${t.name}\n\n${deprecated}\n\n${t.comment ?? ''}\n\n${markdown}\n\n`;
}) })
.join('')}`; .join('')}`;
}) })

@ -56,7 +56,7 @@ export function content() {
const rank = +metadata.rank || undefined; const rank = +metadata.rank || undefined;
blocks.push({ blocks.push({
breadcrumbs: [...breadcrumbs, removeMarkdown(metadata.title ?? '')], breadcrumbs: [...breadcrumbs, removeMarkdown(remove_TYPE(metadata.title) ?? '')],
href: category.href([slug]), href: category.href([slug]),
content: plaintext(intro), content: plaintext(intro),
rank rank
@ -72,7 +72,11 @@ export function content() {
const intro = subsections.shift().trim(); const intro = subsections.shift().trim();
blocks.push({ blocks.push({
breadcrumbs: [...breadcrumbs, removeMarkdown(metadata.title), removeMarkdown(h2)], breadcrumbs: [
...breadcrumbs,
removeMarkdown(remove_TYPE(metadata.title)),
remove_TYPE(removeMarkdown(h2))
],
href: category.href([slug, normalizeSlugify(h2)]), href: category.href([slug, normalizeSlugify(h2)]),
content: plaintext(intro), content: plaintext(intro),
rank rank
@ -85,9 +89,9 @@ export function content() {
blocks.push({ blocks.push({
breadcrumbs: [ breadcrumbs: [
...breadcrumbs, ...breadcrumbs,
removeMarkdown(metadata.title), removeMarkdown(remove_TYPE(metadata.title)),
removeMarkdown(h2), removeMarkdown(remove_TYPE(h2)),
removeMarkdown(h3) removeMarkdown(remove_TYPE(h3))
], ],
href: category.href([slug, normalizeSlugify(h2), normalizeSlugify(h3)]), href: category.href([slug, normalizeSlugify(h2), normalizeSlugify(h3)]),
content: plaintext(lines.join('\n').trim()), content: plaintext(lines.join('\n').trim()),
@ -101,6 +105,11 @@ export function content() {
return blocks; return blocks;
} }
/** @param {string} str */
function remove_TYPE(str) {
return str?.replace(/^\[TYPE\]:\s+(.+)/, '$1') ?? '';
}
/** @param {string} markdown */ /** @param {string} markdown */
function plaintext(markdown) { function plaintext(markdown) {
/** @param {unknown} text */ /** @param {unknown} text */

@ -17,7 +17,7 @@
<svelte:head> <svelte:head>
<title>{data.page.title} • Docs • Svelte</title> <title>{data.page.title} • Docs • Svelte</title>
<meta name="twitter:title" content="Svelte docs" /> <meta name="twitter:title" content="{data.page.title} • Docs • Svelte" />
<meta name="twitter:description" content="{data.page.title} • Svelte documentation" /> <meta name="twitter:description" content="{data.page.title} • Svelte documentation" />
<meta name="Description" content="{data.page.title} • Svelte documentation" /> <meta name="Description" content="{data.page.title} • Svelte documentation" />
</svelte:head> </svelte:head>

Loading…
Cancel
Save