feat(site-2): TS in docs (#8452)

* Commit

* Push working type generation code

* Fix type gen script invocation

* Delete type-info.js

* Change build script

* Recreate package lock

* mkdir generated

* Add type references to some pages

* Add TS-able snippets to docs examples

* Fix some stuff

* Add types to individual functions

* Add to store page

* Refactor compile-types.js

* Move ts sourcefile stuff to compile-types

* Remove commented code

* Half attempt at export aliases

* Add

* Fix, add disclaimer for svelte/internal

* Disable /docs prerendering

* Remove internals page

* Remove redundant stuff

* Make search work with it

* Fix compile types logic

* Add file: comment,

* Add two-slash to docs pages

* Get type links working

* Remove console.log

* Add action module

* Fix actions logic

* Make compile-types logic more robust

* Bump site-kit

* Fix gitignore

* Use moduleResolution bundler

* Don't apply shiki-twoslash to auto generated code snippets from render

* Recreate package lock

* Make TSbpage undraft

* Fix svelte component types

* Remove console.log

* No more sveltekit

* Make regex smarter

* Update sites/svelte.dev/scripts/type-gen/compile-types.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* Rebuild package lock

* Remove commented out code

* Update deps

* Remove $$_attributes

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/8585/head
Puru Vijay 1 year ago committed by GitHub
parent b9ea60cff4
commit 9bee59cc4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -52,10 +52,12 @@ In development mode (see the [compiler options](/docs/svelte-compiler#svelte-com
If you export a `const`, `class` or `function`, it is readonly from outside the component. Functions are valid prop values, however, as shown below.
```svelte
<!--- file: App.svelte --->
<script>
// these are readonly
export const thisIs = 'readonly';
/** @param {string} name */
export function greet(name) {
alert(`hello ${name}!`);
}
@ -70,7 +72,9 @@ Readonly props can be accessed as properties on the element, tied to the compone
You can use reserved words as prop names.
```svelte
<!--- file: App.svelte --->
<script>
/** @type {string} */
let className;
// creates a `class` property, even
@ -155,10 +159,12 @@ Any top-level statement (i.e. not inside a block or a function) can be made reac
Only values which directly appear within the `$:` block will become dependencies of the reactive statement. For example, in the code below `total` will only update when `x` changes, but not `y`.
```svelte
<!--- file: App.svelte --->
<script>
let x = 0;
let y = 0;
/** @param {number} value */
function yPlusAValue(value) {
return value + y;
}
@ -179,9 +185,10 @@ It is important to note that the reactive blocks are ordered via simple static a
let x = 0;
let y = 0;
const setY = (value) => {
/** @param {number} value */
function setY(value) {
y = value;
};
}
$: yDependent = y;
$: setY(x);
@ -193,7 +200,9 @@ Moving the line `$: yDependent = y` below `$: setY(x)` will cause `yDependent` t
If a statement consists entirely of an assignment to an undeclared variable, Svelte will inject a `let` declaration on your behalf.
```svelte
<!--- file: App.svelte --->
<script>
/** @type {number} */
export let num;
// we don't need to declare `squared` and `cubed`
@ -232,7 +241,8 @@ Local variables (that do not represent store values) must _not_ have a `$` prefi
#### Store contract
```js
```ts
// @noErrors
store = { subscribe: (subscription: (value: any) => void) => (() => void), set?: (value: any) => void }
```

@ -17,9 +17,11 @@ on:eventname|modifiers={handler}
Use the `on:` directive to listen to DOM events.
```svelte
<!--- file: App.svelte --->
<script>
let count = 0;
/** @param {MouseEvent} event */
function handleClick(event) {
count += 1;
}
@ -70,12 +72,14 @@ If the `on:` directive is used without a value, the component will _forward_ the
It's possible to have multiple event listeners for the same event:
```svelte
<!--- file: App.svelte --->
<script>
let counter = 0;
function increment() {
counter = counter + 1;
}
/** @param {MouseEvent} event */
function track(event) {
trackEvent(event);
}
@ -274,8 +278,11 @@ bind:group={variable}
Inputs that work together can use `bind:group`.
```svelte
<!--- file: App.svelte --->
<script>
let tortilla = 'Plain';
/** @type {Array<string>} */
let fillings = [];
</script>
@ -302,9 +309,11 @@ bind:this={dom_node}
To get a reference to a DOM node, use `bind:this`.
```svelte
<!--- file: App.svelte --->
<script>
import { onMount } from 'svelte';
/** @type {HTMLCanvasElement} */
let canvasElement;
onMount(() => {
@ -390,7 +399,8 @@ use:action
use:action={parameters}
```
```js
```ts
// @noErrors
action = (node: HTMLElement, parameters: any) => {
update?: (parameters: any) => void,
destroy?: () => void
@ -400,7 +410,9 @@ action = (node: HTMLElement, parameters: any) => {
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
@ -420,9 +432,11 @@ An action can have a parameter. If the returned value has an `update` method, it
> Don't worry about the fact 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
@ -460,6 +474,7 @@ transition:fn|local={params}
```
```js
// @noErrors
transition = (node: HTMLElement, params: any, options: { direction: 'in' | 'out' | 'both' }) => {
delay?: number,
duration?: number,
@ -504,9 +519,11 @@ The `t` argument passed to `css` is a value between `0` and `1` after the `easin
The function is called repeatedly _before_ the transition begins, with different `t` and `u` arguments.
```svelte
<!--- file: App.svelte --->
<script>
import { elasticOut } from 'svelte/easing';
/** @type {boolean} */
export let visible;
function whoosh(node, params) {
@ -657,6 +674,7 @@ animate:name={params}
```
```js
// @noErrors
animation = (node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) => {
delay?: number,
duration?: number,
@ -667,6 +685,7 @@ animation = (node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) =>
```
```ts
// @noErrors
DOMRect {
bottom: number,
height: number,
@ -712,10 +731,20 @@ The `t` argument passed to `css` is a value that goes from `0` and `1` after the
The function is called repeatedly _before_ the animation begins, with different `t` and `u` arguments.
<!-- TODO: Types -->
```svelte
<!--- file: App.svelte --->
<script>
import { cubicOut } from 'svelte/easing';
/**
* @param {HTMLElement} node
* @param {Object} states
* @param {DOMRect} states.from
* @param {DOMRect} states.to
* @param {any} params
*/
function whizz(node, { from, to }, params) {
const dx = from.left - to.left;
const dy = from.top - to.top;
@ -744,6 +773,13 @@ A custom animation function can also return a `tick` function, which is called _
<script>
import { cubicOut } from 'svelte/easing';
/**
* @param {HTMLElement} node
* @param {Object} states
* @param {DOMRect} states.from
* @param {DOMRect} states.to
* @param {any} params
*/
function whizz(node, { from, to }, params) {
const dx = from.left - to.left;
const dy = from.top - to.top;

@ -156,6 +156,7 @@ It cannot appear at the top level of your markup; it must be inside an if or eac
```svelte
<script>
/** @type {number} */
export let count;
</script>
@ -198,6 +199,8 @@ If `this` is the name of a [void element](https://developer.mozilla.org/en-US/do
```svelte
<script>
let tag = 'div';
/** @type {(e: MouseEvent) => void} */
export let handler;
</script>
@ -220,6 +223,7 @@ Unlike `<svelte:self>`, this element may only appear at the top level of your co
```svelte
<script>
/** @param {KeyboardEvent} event */
function handleKeydown(event) {
alert(`pressed the ${event.key} key`);
}

@ -6,13 +6,7 @@ The `svelte` package exposes [lifecycle functions](/tutorial/onmount) and the [c
## `onMount`
```js
onMount(callback: () => void)
```
```js
onMount(callback: () => () => void)
```
> EXPORT_SNIPPET: svelte#onMount
The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. It must be called during the component's initialisation (but doesn't need to live _inside_ the component; it can be called from an external module).
@ -48,9 +42,7 @@ If a function is returned from `onMount`, it will be called when the component i
## `beforeUpdate`
```js
beforeUpdate(callback: () => void)
```
> EXPORT_SNIPPET: svelte#beforeUpdate
Schedules a callback to run immediately before the component is updated after any state change.
@ -68,9 +60,7 @@ Schedules a callback to run immediately before the component is updated after an
## `afterUpdate`
```js
afterUpdate(callback: () => void)
```
> EXPORT_SNIPPET: svelte#afterUpdate
Schedules a callback to run immediately after the component has been updated.
@ -88,9 +78,7 @@ Schedules a callback to run immediately after the component has been updated.
## `onDestroy`
```js
onDestroy(callback: () => void)
```
> EXPORT_SNIPPET: svelte#onDestroy
Schedules a callback to run immediately before the component is unmounted.
@ -108,9 +96,7 @@ Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the onl
## `tick`
```js
promise: Promise = tick();
```
> EXPORT_SNIPPET: svelte#tick
Returns a promise that resolves once any pending state changes have been applied, or in the next microtask if there are none.
@ -128,9 +114,7 @@ Returns a promise that resolves once any pending state changes have been applied
## `setContext`
```js
setContext(key: any, context: any)
```
> EXPORT_SNIPPET: svelte#setContext
Associates an arbitrary `context` object with the current component and the specified `key` and returns that object. The context is then available to children of the component (including slotted content) with `getContext`.
@ -148,9 +132,7 @@ Like lifecycle functions, this must be called during component initialisation.
## `getContext`
```js
context: any = getContext(key: any)
```
> EXPORT_SNIPPET: svelte#getContext
Retrieves the context that belongs to the closest parent component with the specified `key`. Must be called during component initialisation.
@ -164,9 +146,7 @@ Retrieves the context that belongs to the closest parent component with the spec
## `hasContext`
```js
hasContext: boolean = hasContext(key: any)
```
> EXPORT_SNIPPET: svelte#hasContext
Checks whether a given `key` has been set in the context of a parent component. Must be called during component initialisation.
@ -182,9 +162,7 @@ Checks whether a given `key` has been set in the context of a parent component.
## `getAllContexts`
```js
contexts: Map<any, any> = getAllContexts()
```
> EXPORT_SNIPPET: svelte#getAllContexts
Retrieves the whole context map that belongs to the closest parent component. Must be called during component initialisation. Useful, for example, if you programmatically create a component and want to pass the existing context to it.
@ -198,9 +176,7 @@ Retrieves the whole context map that belongs to the closest parent component. Mu
## `createEventDispatcher`
```js
dispatch: ((name: string, detail?: any, options?: DispatchOptions) => boolean) = createEventDispatcher();
```
> EXPORT_SNIPPET: svelte#createEventDispatcher
Creates an event dispatcher that can be used to dispatch [component events](/docs/component-directives#on-eventname). Event dispatchers are functions that can take two arguments: `name` and `detail`.
@ -246,3 +222,7 @@ Events can be cancelable by passing a third parameter to the dispatch function.
}
</script>
```
## Types
> TYPES: svelte

@ -10,13 +10,7 @@ This makes it possible to wrap almost any other reactive state handling library
## `writable`
```js
store = writable(value?: any)
```
```js
store = writable(value?: any, start?: (set: (value: any) => void) => () => void)
```
> EXPORT_SNIPPET: svelte/store#writable
Function that creates a store which has values that can be set from 'outside' components. It gets created as an object with additional `set` and `update` methods.
@ -25,6 +19,7 @@ Function that creates a store which has values that can be set from 'outside' co
`update` is a method that takes one argument which is a callback. The callback takes the existing store value as its argument and returns the new value to be set to the store.
```js
/// file: store.js
import { writable } from 'svelte/store';
const count = writable(0);
@ -41,6 +36,7 @@ count.update((n) => n + 1); // logs '2'
If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store. It must return a `stop` function that is called when the subscriber count goes from one to zero.
```js
/// file: store.js
import { writable } from 'svelte/store';
const count = writable(0, () => {
@ -61,16 +57,15 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh
## `readable`
```js
store = readable(value?: any, start?: (set: (value: any) => void) => () => void)
```
> EXPORT_SNIPPET: svelte/store#readable
Creates a store whose value cannot be set from 'outside', the first argument is the store's initial value, and the second argument to `readable` is the same as the second argument to `writable`.
```js
/// file: store.js
import { readable } from 'svelte/store';
const time = readable(null, (set) => {
const time = readable(new Date(), (set) => {
set(new Date());
const interval = setInterval(() => {
@ -83,27 +78,24 @@ const time = readable(null, (set) => {
## `derived`
```js
store = derived(a, callback: (a: any) => any)
```
> EXPORT_SNIPPET: svelte/store#derived
```js
store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any)
```
Derives a store from one or more other stores. The callback runs initially when the first subscriber subscribes and then whenever the store dependencies change.
```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
```
In the simplest version, `derived` takes a single store, and the callback returns a derived value.
```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any)
```
```ts
// @filename: ambient.d.ts
import { type Writable } from 'svelte/store';
Derives a store from one or more other stores. The callback runs initially when the first subscriber subscribes and then whenever the store dependencies change.
declare global {
const a: Writable<number>;
}
In the simplest version, `derived` takes a single store, and the callback returns a derived value.
export {};
```js
// @filename: index.ts
// ---cut---
import { derived } from 'svelte/store';
const doubled = derived(a, ($a) => $a * 2);
@ -114,6 +106,17 @@ The callback can set a value asynchronously by accepting a second argument, `set
In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called.
```js
// @filename: ambient.d.ts
import { type Writable } from 'svelte/store';
declare global {
const a: Writable<number>;
}
export {};
// @filename: index.ts
// ---cut---
import { derived } from 'svelte/store';
const delayed = derived(
@ -121,13 +124,24 @@ const delayed = derived(
($a, set) => {
setTimeout(() => set($a), 1000);
},
'one moment...'
2000
);
```
If you return a function from the callback, it will be called when a) the callback runs again, or b) the last subscriber unsubscribes.
```js
// @filename: ambient.d.ts
import { type Writable } from 'svelte/store';
declare global {
const frequency: Writable<number>;
}
export {};
// @filename: index.ts
// ---cut---
import { derived } from 'svelte/store';
const tick = derived(
@ -141,13 +155,26 @@ const tick = derived(
clearInterval(interval);
};
},
'one moment...'
2000
);
```
In both cases, an array of arguments can be passed as the first argument instead of a single store.
```js
```ts
// @filename: ambient.d.ts
import { type Writable } from 'svelte/store';
declare global {
const a: Writable<number>;
const b: Writable<number>;
}
export {};
// @filename: index.ts
// ---cut---
import { derived } from 'svelte/store';
const summed = derived([a, b], ([$a, $b]) => $a + $b);
@ -159,14 +186,12 @@ const delayed = derived([a, b], ([$a, $b], set) => {
## `readonly`
```js
readableStore = readonly(writableStore);
```
> EXPORT_SNIPPET: svelte/store#readonly
This simple helper function makes a store readonly. You can still subscribe to the changes from the original one using this new readable store.
```js
import { readonly } from 'svelte/store';
import { readonly, writable } from 'svelte/store';
const writableStore = writable(1);
const readableStore = readonly(writableStore);
@ -174,21 +199,35 @@ const readableStore = readonly(writableStore);
readableStore.subscribe(console.log);
writableStore.set(2); // console: 2
// @errors: 2339
readableStore.set(2); // ERROR
```
## `get`
```js
value: any = get(store);
```
> EXPORT_SNIPPET: svelte/store#get
Generally, you should read the value of a store by subscribing to it and using the value as it changes over time. Occasionally, you may need to retrieve the value of a store to which you're not subscribed. `get` allows you to do so.
> This works by creating a subscription, reading the value, then unsubscribing. It's therefore not recommended in hot code paths.
```js
// @filename: ambient.d.ts
import { type Writable } from 'svelte/store';
declare global {
const store: Writable<string>;
}
export {};
// @filename: index.ts
// ---cut---
import { get } from 'svelte/store';
const value = get(store);
```
## Types
> TYPES: svelte/store

@ -6,9 +6,7 @@ The `svelte/motion` module exports two functions, `tweened` and `spring`, for cr
## `tweened`
```js
store = tweened(value: any, options)
```
> EXPORT_SNIPPET: svelte/motion#tweened
Tweened stores update their values over a fixed duration. The following options are available:
@ -46,7 +44,19 @@ Out of the box, Svelte will interpolate between two numbers, two arrays or two o
If the initial value is `undefined` or `null`, the first value change will take effect immediately. This is useful when you have tweened values that are based on props, and don't want any motion when the component first renders.
```js
```ts
// @filename: ambient.d.ts
declare global {
var $size: number;
var big: number;
}
export {};
// @filename: motion.ts
// ---cut---
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const size = tweened(undefined, {
duration: 300,
easing: cubicOut
@ -81,9 +91,7 @@ The `interpolate` option allows you to tween between _any_ arbitrary values. It
## `spring`
```js
store = spring(value: any, options)
```
> EXPORT_SNIPPET: svelte/motion#spring
A `spring` store gradually changes to its target value based on its `stiffness` and `damping` parameters. Whereas `tweened` stores change their values over a fixed duration, `spring` stores change over a duration that is determined by their existing velocity, allowing for more natural-seeming motion in many situations. The following options are available:
@ -94,6 +102,8 @@ A `spring` store gradually changes to its target value based on its `stiffness`
All of the options above can be changed while the spring is in motion, and will take immediate effect.
```js
import { spring } from 'svelte/motion';
const size = spring(100);
size.stiffness = 0.3;
size.damping = 0.4;
@ -105,6 +115,8 @@ As with [`tweened`](/docs/svelte-motion#tweened) stores, `set` and `update` retu
Both `set` and `update` can take a second argument — an object with `hard` or `soft` properties. `{ hard: true }` sets the target value immediately; `{ soft: n }` preserves existing momentum for `n` seconds before settling. `{ soft: true }` is equivalent to `{ soft: 0.5 }`.
```js
import { spring } from 'svelte/motion';
const coords = spring({ x: 50, y: 50 });
// updates the value immediately
coords.set({ x: 100, y: 200 }, { hard: true });
@ -135,7 +147,23 @@ coords.update(
If the initial value is `undefined` or `null`, the first value change will take effect immediately, just as with `tweened` values (see above).
```js
```ts
// @filename: ambient.d.ts
declare global {
var $size: number;
var big: number;
}
export {};
// @filename: motion.ts
// ---cut---
import { spring } from 'svelte/motion';
const size = spring();
$: $size = big ? 100 : 10;
```
## Types
> TYPES: svelte/motion

@ -6,6 +6,8 @@ The `svelte/transition` module exports seven functions: `fade`, `blur`, `fly`, `
## `fade`
> EXPORT_SNIPPET: svelte/transition#fade
```svelte
transition:fade={params}
```
@ -40,6 +42,8 @@ You can see the `fade` transition in action in the [transition tutorial](/tutori
## `blur`
> EXPORT_SNIPPET: svelte/transition#blur
```svelte
transition:blur={params}
```
@ -74,6 +78,8 @@ Animates a `blur` filter alongside an element's opacity.
## `fly`
> EXPORT_SNIPPET: svelte/transition#fly
```svelte
transition:fly={params}
```
@ -117,6 +123,8 @@ You can see the `fly` transition in action in the [transition tutorial](/tutoria
## `slide`
> EXPORT_SNIPPET: svelte/transition#slide
```svelte
transition:slide={params}
```
@ -154,6 +162,8 @@ Slides an element in and out.
## `scale`
> EXPORT_SNIPPET: svelte/transition#scale
```svelte
transition:scale={params}
```
@ -191,6 +201,8 @@ Animates the opacity and scale of an element. `in` transitions animate from an e
## `draw`
> EXPORT_SNIPPET: svelte/transition#draw
```svelte
transition:draw={params}
```
@ -236,6 +248,8 @@ The `speed` parameter is a means of setting the duration of the transition relat
## `crossfade`
> EXPORT_SNIPPET: svelte/transition#crossfade
The `crossfade` function creates a pair of [transitions](/docs/element-directives#transition-fn) called `send` and `receive`. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the `fallback` transition is used.
`crossfade` accepts the following parameters:
@ -262,3 +276,7 @@ The `crossfade` function creates a pair of [transitions](/docs/element-directive
<small in:send={{ key }} out:receive={{ key }}>small elem</small>
{/if}
```
## Types
> TYPES: svelte/transition

@ -6,6 +6,8 @@ The `svelte/animate` module exports one function for use with Svelte [animations
## `flip`
> EXPORT_SNIPPET: svelte/animate#flip
```svelte
animate:flip={params}
```
@ -39,3 +41,7 @@ You can see a full example on the [animations tutorial](/tutorial/animate)
</div>
{/each}
```
## Types
> TYPES: svelte/animate

@ -1,6 +0,0 @@
---
title: Actions
draft: true
---
TODO

@ -0,0 +1,9 @@
---
title: svelte/action
---
TODO
## Types
> TYPES: svelte/action

@ -8,23 +8,23 @@ Nonetheless, it's useful to understand how to use the compiler, since bundler pl
## `svelte.compile`
```js
result: {
js,
css,
ast,
warnings,
vars,
stats
} = svelte.compile(source: string, options?: {...})
```
> EXPORT_SNIPPET: svelte/compiler#compile
This is where the magic happens. `svelte.compile` takes your component source code, and turns it into a JavaScript module that exports a class.
```js
import svelte from 'svelte/compiler';
// @filename: ambient.d.ts
declare global {
var source: string
}
export {}
// @filename: index.ts
// ---cut---
import { compile } from 'svelte/compiler';
const result = svelte.compile(source, {
const result = compile(source, {
// options
});
```
@ -83,8 +83,18 @@ The following options can be passed to the compiler. None are required:
The returned `result` object contains the code for your component, along with useful bits of metadata.
```js
const { js, css, ast, warnings, vars, stats } = svelte.compile(source);
```ts
// @filename: ambient.d.ts
declare global {
const source: string;
}
export {};
// @filename: main.ts
import { compile } from 'svelte/compiler';
// ---cut---
const { js, css, ast, warnings, vars, stats } = compile(source);
```
- `js` and `css` are objects with the following properties:
@ -147,56 +157,33 @@ compiled: {
## `svelte.parse`
```js
ast: object = svelte.parse(
source: string,
options?: {
filename?: string,
customElement?: boolean
}
)
```
> EXPORT_SNIPPET: svelte/compiler#parse
The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it. Note that the returned AST is not considered public API, so breaking changes could occur at any point in time.
```js
import svelte from 'svelte/compiler';
// @filename: ambient.d.ts
declare global {
var source: string;
}
export {};
// @filename: main.ts
// ---cut---
import { parse } from 'svelte/compiler';
const ast = svelte.parse(source, { filename: 'App.svelte' });
const ast = parse(source, { filename: 'App.svelte' });
```
## `svelte.preprocess`
> EXPORT_SNIPPET: svelte/compiler#preprocess
A number of [community-maintained preprocessing plugins](https://sveltesociety.dev/tools#preprocessors) are available to allow you to use Svelte with tools like TypeScript, PostCSS, SCSS, and Less.
You can write your own preprocessor using the `svelte.preprocess` API.
```js
result: {
code: string,
dependencies: Array<string>
} = await svelte.preprocess(
source: string,
preprocessors: Array<{
markup?: (input: { content: string, filename: string }) => Promise<{
code: string,
dependencies?: Array<string>
}>,
script?: (input: { content: string, markup: string, attributes: Record<string, string>, filename: string }) => Promise<{
code: string,
dependencies?: Array<string>
}>,
style?: (input: { content: string, markup: string, attributes: Record<string, string>, filename: string }) => Promise<{
code: string,
dependencies?: Array<string>
}>
}>,
options?: {
filename?: string
}
)
```
The `preprocess` function provides convenient hooks for arbitrarily transforming component source code. For example, it can be used to convert a `<style lang="sass">` block into vanilla CSS.
The first argument is the component source code. The second is an array of _preprocessors_ (or a single preprocessor, if you only have one), where a preprocessor is an object with `markup`, `script` and `style` functions, each of which is optional.
@ -208,10 +195,19 @@ The `markup` function receives the entire component source text, along with the
> Preprocessor functions should additionally return a `map` object alongside `code` and `dependencies`, where `map` is a sourcemap representing the transformation.
```js
import svelte from 'svelte/compiler';
// @filename: ambient.d.ts
declare global {
var source: string;
}
export {};
// @filename: main.ts
// ---cut---
import { preprocess } from 'svelte/compiler';
import MagicString from 'magic-string';
const { code } = await svelte.preprocess(
const { code } = await preprocess(
source,
{
markup: ({ content, filename }) => {
@ -237,10 +233,21 @@ The `script` and `style` functions receive the contents of `<script>` and `<styl
If a `dependencies` array is returned, it will be included in the result object. This is used by packages like [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte) and [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) to watch additional files for changes, in the case where your `<style>` tag has an `@import` (for example).
```js
```ts
// @filename: ambient.d.ts
declare global {
var source: string;
}
export {};
// @filename: main.ts
// @errors: 2322 2345
/// <reference types="@types/node" />
// ---cut---
import { dirname } from 'node:path';
import { preprocess } from 'svelte/compiler';
import sass from 'sass';
import { dirname } from 'path';
const { code, dependencies } = await preprocess(
source,
@ -268,9 +275,18 @@ const { code, dependencies } = await preprocess(
Multiple preprocessors can be used together. The output of the first becomes the input to the second. `markup` functions run first, then `script` and `style`.
```js
import svelte from 'svelte/compiler';
// @filename: ambient.d.ts
declare global {
var source: string;
}
const { code } = await svelte.preprocess(
export {};
// @filename: main.ts
// ---cut---
import { preprocess } from 'svelte/compiler';
const { code } = await preprocess(
source,
[
{
@ -304,21 +320,28 @@ const { code } = await svelte.preprocess(
## `svelte.walk`
```js
walk(ast: Node, {
enter(node: Node, parent: Node, prop: string, index: number)?: void,
leave(node: Node, parent: Node, prop: string, index: number)?: void
})
```
> EXPORT_SNIPPET: svelte/compiler#walk
The `walk` function provides a way to walk the abstract syntax trees generated by the parser, using the compiler's own built-in instance of [estree-walker](https://github.com/Rich-Harris/estree-walker).
The walker takes an abstract syntax tree to walk and an object with two optional methods: `enter` and `leave`. For each node, `enter` is called (if present). Then, unless `this.skip()` is called during `enter`, each of the children are traversed, and then `leave` is called on the node.
```js
import svelte from 'svelte/compiler';
// @filename: ambient.d.ts
declare global {
var ast: import('estree').Node;
function do_something(node: import('estree').Node): void;
function do_something_else(node: import('estree').Node): void;
function should_skip_children(node: import('estree').Node): boolean;
}
export {};
svelte.walk(ast, {
// @filename: main.ts
// ---cut---
import { walk } from 'svelte/compiler';
walk(ast, {
enter(node, parent, prop, index) {
do_something(node);
if (should_skip_children(node)) {
@ -333,9 +356,11 @@ svelte.walk(ast, {
## `svelte.VERSION`
> EXPORT_SNIPPET: svelte/compiler#VERSION
The current version, as set in package.json.
```js
import svelte from 'svelte/compiler';
console.log(`running svelte version ${svelte.VERSION}`);
import { VERSION } from 'svelte/compiler';
console.log(`running svelte version ${VERSION}`);
```

@ -4,13 +4,33 @@ title: 'Client-side component API'
## Creating a component
```js
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var options: ComponentConstructorOptions<Record<string, any>>;
}
// @filename: index.ts
// ---cut---
const component = new Component(options);
```
A client-side component — that is, a component compiled with `generate: 'dom'` (or the `generate` option left unspecified) is a JavaScript class.
```js
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare module './App.svelte' {
class Component extends SvelteComponent {}
export default Component;
}
// @filename: index.ts
// ---cut---
import App from './App.svelte';
const app = new App({
@ -42,7 +62,18 @@ Whereas children of `target` are normally left alone, `hydrate: true` will cause
The existing DOM doesn't need to match the component — Svelte will 'repair' the DOM as it goes.
```js
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare module './App.svelte' {
class Component extends SvelteComponent {}
export default Component;
}
// @filename: index.ts
// @errors: 2322
// ---cut---
import App from './App.svelte';
const app = new App({
@ -53,7 +84,20 @@ const app = new App({
## `$set`
```js
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
var props: Record<string, any>;
}
export {};
// @filename: index.ts
// ---cut---
component.$set(props);
```
@ -61,22 +105,60 @@ Programmatically sets props on an instance. `component.$set({ x: 1 })` is equiva
Calling this method schedules an update for the next microtask — the DOM is _not_ updated synchronously.
```js
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
}
export {};
// @filename: index.ts
// ---cut---
component.$set({ answer: 42 });
```
## `$on`
```js
component.$on(event, callback);
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
var ev: string;
var callback: (event: CustomEvent) => void;
}
export {};
// @filename: index.ts
// ---cut---
component.$on(ev, callback);
```
Causes the `callback` function to be called whenever the component dispatches an `event`.
A function is returned that will remove the event listener when called.
```js
const off = app.$on('selected', (event) => {
```ts
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
}
export {};
// @filename: index.ts
// ---cut---
const off = component.$on('selected', (event) => {
console.log(event.detail.selection);
});
@ -86,6 +168,18 @@ off();
## `$destroy`
```js
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
}
export {}
// @filename: index.ts
// ---cut---
component.$destroy();
```
@ -94,10 +188,35 @@ Removes a component from the DOM and triggers any `onDestroy` handlers.
## Component props
```js
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
}
export {}
// @filename: index.ts
// ---cut---
component.prop;
```
```js
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
var value: unknown;
}
export {}
// @filename: index.ts
// ---cut---
component.prop = value;
```
@ -106,6 +225,19 @@ If a component is compiled with `accessors: true`, each instance will have gette
By default, `accessors` is `false`, unless you're compiling as a custom element.
```js
console.log(app.count);
app.count += 1;
// @filename: ambiend.d.ts
import { SvelteComponent, ComponentConstructorOptions } from 'svelte';
declare global {
class Component extends SvelteComponent {}
var component: Component;
var props: Record<string, any>;
}
export {}
// @filename: index.ts
// ---cut---
console.log(component.count);
component.count += 1;
```

@ -3,6 +3,7 @@ title: 'Server-side component API'
---
```js
// @noErrors
const result = Component.render(...)
```
@ -13,6 +14,7 @@ A server-side component exposes a `render` method that can be called with option
You can import a Svelte component directly into Node using [`svelte/register`](/docs/svelte-register).
```js
// @noErrors
require('svelte/register');
const App = require('./App.svelte').default;
@ -36,6 +38,7 @@ The `options` object takes in the following options:
| `context` | `new Map()` | A `Map` of root-level context key-value pairs to supply to the component |
```js
// @noErrors
const { head, html, css } = App.render(
// props
{ answer: 42 },

@ -18,6 +18,7 @@ Svelte components can also be compiled to custom elements (aka web components) u
Alternatively, use `tag={null}` to indicate that the consumer of the custom element should name it.
```js
// @noErrors
import MyElement from './MyElement.svelte';
customElements.define('my-element', MyElement);
@ -38,6 +39,7 @@ By default, custom elements are compiled with `accessors: true`, which means tha
To prevent this, add `accessors={false}` to `<svelte:options>`.
```js
// @noErrors
const el = document.querySelector('my-element');
// get the current value of the 'name' prop

@ -1,6 +1,7 @@
---
title: TypeScript
draft: true
---
TODO
## Types
> TYPES: svelte

@ -5,11 +5,12 @@ title: 'svelte/register'
To render Svelte components in Node.js without bundling, use `require('svelte/register')`. After that, you can use `require` to include any `.svelte` file.
```js
// @noErrors
require('svelte/register');
const App = require('./App.svelte').default;
...
// ...
const { html, css, head } = App.render({ answer: 42 });
```
@ -19,6 +20,7 @@ const { html, css, head } = App.render({ answer: 42 });
To set compile options, or to use a custom file extension, call the `register` hook as a function:
```js
// @noErrors
require('svelte/register')({
extensions: ['.customextension'], // defaults to ['.html', '.svelte']
preserveComments: true

@ -11,3 +11,4 @@
/src/routes/_components/Supporters/donors.js
.vercel
examples-data.js
type-info.js

File diff suppressed because it is too large Load Diff

@ -4,8 +4,9 @@
"description": "Docs and examples for Svelte",
"type": "module",
"scripts": {
"dev": "node scripts/generate_examples.js && node scripts/update.js && vite dev",
"build": "node scripts/generate_examples.js && node scripts/update.js && vite build",
"dev": "node scripts/update.js && vite dev",
"build": "node scripts/update.js && npm run generate && vite build",
"generate": "node scripts/type-gen/index.js && node scripts/generate_examples.js",
"update": "node scripts/update.js --force=true",
"preview": "vite preview",
"start": "node build",
@ -31,23 +32,24 @@
"@sveltejs/kit": "^1.16.3",
"@sveltejs/site-kit": "^5.0.4",
"@types/marked": "^4.3.0",
"@types/prismjs": "^1.26.0",
"degit": "^2.8.4",
"dotenv": "^16.0.3",
"jimp": "^0.22.7",
"jimp": "^0.22.8",
"marked": "^5.0.1",
"node-fetch": "^3.3.1",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.0",
"prism-svelte": "^0.5.0",
"prismjs": "^1.29.0",
"satori": "^0.7.4",
"rollup": "^3.21.6",
"rollup-plugin-dts": "^5.3.0",
"sass": "^1.62.1",
"satori": "^0.8.0",
"satori-html": "^0.3.2",
"shelljs": "^0.8.5",
"shiki": "^0.14.2",
"shiki-twoslash": "^3.1.2",
"svelte": "^3.59.1",
"svelte-check": "^3.3.2",
"ts-morph": "^18.0.0",
"typescript": "^5.0.4",
"vite": "^4.3.5",
"vite-imagetools": "^5.0.3"

@ -5,7 +5,9 @@ const examples_data = get_examples_data(
new URL('../../../site/content/examples', import.meta.url).pathname
);
fs.mkdirSync(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
try {
fs.mkdirSync(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
} catch {}
fs.writeFileSync(
new URL('../src/lib/generated/examples-data.js', import.meta.url),

@ -0,0 +1,127 @@
// @ts-check
import MagicString from 'magic-string';
import fs from 'node:fs';
import { rollup } from 'rollup';
import dts from 'rollup-plugin-dts';
import { VERSION } from 'svelte/compiler';
import { Project, SyntaxKind } from 'ts-morph';
import ts from 'typescript';
export async function get_bundled_types() {
const dtsSources = fs.readdirSync(new URL('./dts-sources', import.meta.url));
/** @type {Map<string, {code: string, ts_source_file: ts.SourceFile}>} */
const codes = new Map();
for (const file of dtsSources) {
const bundle = await rollup({
input: new URL(`./dts-sources/${file}`, import.meta.url).pathname,
plugins: [dts({ respectExternal: true })]
});
const moduleName = (file === 'index.d.ts' ? 'svelte' : `svelte/${file}`).replace('.d.ts', '');
const code = await bundle.generate({ format: 'esm' }).then(({ output }) => output[0].code);
const inlined_export_declaration_code = inlineExportDeclarations(code);
codes.set(moduleName, {
code: inlined_export_declaration_code,
ts_source_file: ts.createSourceFile(
'index.d.ts',
inlined_export_declaration_code,
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS
)
});
// !IMPORTANT: This is for debugging purposes only.
// !Do not remove until Svelte d.ts files are stable during v4/v5
write_to_node_modules('before', file, code);
write_to_node_modules('after', file, inlined_export_declaration_code);
}
return codes;
}
/**
* @param {string} str
*/
function inlineExportDeclarations(str) {
const project = new Project();
const source_file = project.createSourceFile('index.d.ts', str, { overwrite: true });
// There's only gonna be one because of the output of dts-plugin
const exportDeclaration = source_file.getExportDeclarations()[0];
const exportedSymbols = exportDeclaration
.getNamedExports()
.map((e) => e.getAliasNode()?.getText() ?? e.getNameNode().getText());
// console.log(exportedSymbols);
if (exportedSymbols.length === 0) return str;
const aliasedExportedSymbols = new Map();
const namedExports = exportDeclaration.getNamedExports();
namedExports.forEach((namedExport) => {
if (namedExport.getAliasNode()) {
const alias = namedExport.getAliasNode()?.getText();
const originalName = namedExport.getNameNode().getText();
aliasedExportedSymbols.set(alias, originalName);
}
});
for (const [exported, og] of aliasedExportedSymbols) {
source_file.forEachDescendant((node) => {
if (node.getKind() === ts.SyntaxKind.Identifier && node.getText() === og) {
node.replaceWithText(exported);
}
});
}
{
// Get the symbols and their declarations
const exportedSymbols = exportDeclaration
.getNamedExports()
.map((exp) => exp.getSymbolOrThrow());
/** @type {import('ts-morph').ExportSpecifier[]} */
// @ts-ignore
const exportedDeclarations = exportedSymbols.flatMap((symbol) => symbol.getDeclarations());
// Add 'export' keyword to the declarations
exportedDeclarations.forEach((declaration) => {
if (!declaration.getFirstDescendantByKind(SyntaxKind.ExportKeyword)) {
for (const target of declaration.getLocalTargetDeclarations()) {
if (target.isKind(SyntaxKind.VariableDeclaration)) {
target.getVariableStatement()?.setIsExported(true);
} else {
// @ts-ignore
target.setIsExported(true);
}
}
}
});
}
exportDeclaration.remove();
// In case it is export declare VERSION = '__VERSION__', replace it with svelte's real version
const stringified = source_file.getFullText().replace('__VERSION__', VERSION);
return stringified;
}
/**
* @param {'before' | 'after'} label
* @param {string} filename
* @param {string} code
*/
function write_to_node_modules(label, filename, code) {
const folder = new URL(`../../node_modules/.type-gen/${label}`, import.meta.url).pathname;
try {
fs.mkdirSync(folder, { recursive: true });
} catch {}
fs.writeFileSync(`${folder}/${filename}`, code);
}

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

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

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

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

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

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

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

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

@ -0,0 +1,366 @@
// @ts-check
import fs from 'fs';
import prettier from 'prettier';
import ts from 'typescript';
import { get_bundled_types } from './compile-types.js';
/** @typedef {{ name: string; comment: string; markdown: string; snippet: string; children: Extracted[] }} Extracted */
/** @type {Array<{ name: string; comment: string; exports: Extracted[]; types: Extracted[]; exempt?: boolean; }>} */
const modules = [];
/**
* @param {string} code
* @param {ts.NodeArray<ts.Statement>} statements
*/
function get_types(code, statements) {
/** @type {Extracted[]} */
const exports = [];
/** @type {Extracted[]} */
const types = [];
if (statements) {
for (const statement of statements) {
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
const export_modifier = modifiers?.find((modifier) => modifier.kind === 93);
if (!export_modifier) continue;
if (
ts.isClassDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isModuleDeclaration(statement) ||
ts.isVariableStatement(statement) ||
ts.isFunctionDeclaration(statement)
) {
const name_node = ts.isVariableStatement(statement)
? statement.declarationList.declarations[0]
: statement;
// @ts-ignore no idea why it's complaining here
const name = name_node.name?.escapedText;
let start = statement.pos;
let comment = '';
// @ts-ignore i think typescript is bad at typescript
if (statement.jsDoc) {
// @ts-ignore
comment = statement.jsDoc[0].comment;
// @ts-ignore
start = statement.jsDoc[0].end;
}
const i = code.indexOf('export', start);
start = i + 6;
/** @type {string[]} */
const children = [];
let snippet_unformatted = code.slice(start, statement.end).trim();
if (ts.isInterfaceDeclaration(statement)) {
if (statement.members.length > 0) {
for (const member of statement.members) {
children.push(munge_type_element(member));
}
// collapse `interface Foo {/* lots of stuff*/}` into `interface Foo {…}`
const first = statement.members.at(0);
const last = statement.members.at(-1);
let body_start = first.pos - start;
while (snippet_unformatted[body_start] !== '{') body_start -= 1;
let body_end = last.end - start;
while (snippet_unformatted[body_end] !== '}') body_end += 1;
snippet_unformatted =
snippet_unformatted.slice(0, body_start + 1) +
'/*…*/' +
snippet_unformatted.slice(body_end);
}
}
const snippet = prettier
.format(snippet_unformatted, {
parser: 'typescript',
printWidth: 80,
useTabs: true,
singleQuote: true,
trailingComma: 'none'
})
.replace(/\s*(\/\*…\*\/)\s*/g, '/*…*/')
.trim();
const collection =
ts.isVariableStatement(statement) || ts.isFunctionDeclaration(statement)
? exports
: types;
collection.push({ name, comment, snippet, children });
}
}
types.sort((a, b) => (a.name < b.name ? -1 : 1));
exports.sort((a, b) => (a.name < b.name ? -1 : 1));
}
return { types, exports };
}
/**
* @param {ts.TypeElement} member
*/
function munge_type_element(member, depth = 1) {
// @ts-ignore
const doc = member.jsDoc?.[0];
/** @type {string[]} */
const children = [];
const name = member.name?.escapedText;
let snippet = member.getText();
for (let i = 0; i < depth; i += 1) {
snippet = snippet.replace(/^\t/gm, '');
}
if (
ts.isPropertySignature(member) &&
ts.isTypeLiteralNode(member.type) &&
member.type.members.some((member) => member.jsDoc?.[0].comment)
) {
let a = 0;
while (snippet[a] !== '{') a += 1;
snippet = snippet.slice(0, a + 1) + '/*…*/}';
for (const child of member.type.members) {
children.push(munge_type_element(child, depth + 1));
}
}
/** @type {string[]} */
const bullets = [];
for (const tag of doc?.tags ?? []) {
const type = tag.tagName.escapedText;
switch (tag.tagName.escapedText) {
case 'param':
bullets.push(`- \`${tag.name.getText()}\` ${tag.comment}`);
break;
case 'default':
bullets.push(`- <span class="tag">default</span> \`${tag.comment}\``);
break;
case 'returns':
bullets.push(`- <span class="tag">returns</span> ${tag.comment}`);
break;
default:
console.log(`unhandled JSDoc tag: ${type}`); // TODO indicate deprecated stuff
}
}
return {
name,
snippet,
comment: (doc?.comment ?? '')
.replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */')
.replace(/^( )+/gm, (match, spaces) => {
return '\t'.repeat(match.length / 2);
}),
bullets,
children
};
}
const bundled_types = await get_bundled_types();
{
const module = bundled_types.get('svelte');
if (!module) throw new Error('Could not find svelte');
modules.push({
name: 'svelte',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/compiler');
if (!module) throw new Error('Could not find svelte/compiler');
modules.push({
name: 'svelte/compiler',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/action');
if (!module) throw new Error('Could not find svelte/action');
modules.push({
name: 'svelte/action',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/animate');
if (!module) throw new Error('Could not find svelte/animate');
modules.push({
name: 'svelte/animate',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/easing');
if (!module) throw new Error('Could not find svelte/easing');
modules.push({
name: 'svelte/easing',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/motion');
if (!module) throw new Error('Could not find svelte/motion');
modules.push({
name: 'svelte/motion',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/store');
if (!module) throw new Error('Could not find svelte/store');
modules.push({
name: 'svelte/store',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
{
const module = bundled_types.get('svelte/transition');
if (!module) throw new Error('Could not find svelte/transition');
modules.push({
name: 'svelte/transition',
comment: '',
...get_types(module.code, module.ts_source_file.statements)
});
}
modules.sort((a, b) => (a.name < b.name ? -1 : 1));
// Fix the duplicate/messed up types
// !NOTE: This relies on mutation of `modules`
$: {
const module_with_SvelteComponent = modules.find((m) =>
m.types.filter((t) => t.name === 'SvelteComponent')
);
if (!module_with_SvelteComponent) break $;
const svelte_comp_part = module_with_SvelteComponent?.types.filter(
(t) => t.name === 'SvelteComponent'
);
if (svelte_comp_part?.[1]) {
// 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(
(t) => t.name !== 'SvelteComponent'
);
module_with_SvelteComponent.types.push(svelte_comp_part[0]);
module_with_SvelteComponent.types.sort((a, b) => (a.name < b.name ? -1 : 1));
}
}
// Fix the duplicate/messed up types
// !NOTE: This relies on mutation of `modules`
$: {
const module_with_SvelteComponentTyped = modules.find((m) =>
m.types.filter((t) => t.name === 'SvelteComponentTyped')
);
if (!module_with_SvelteComponentTyped) break $;
const svelte_comp_typed_part = module_with_SvelteComponentTyped?.types.filter(
(t) => t.name === 'SvelteComponentTyped'
);
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
$: {
const module_with_ActionReturn = modules.find((m) =>
m.types.find((t) => t.name === 'ActionReturn')
);
const new_children =
module_with_ActionReturn?.types[1].children.filter((c) => c.name !== '$$_attributes') || [];
if (!module_with_ActionReturn) break $;
module_with_ActionReturn.types[1].children = new_children;
}
try {
fs.mkdirSync(new URL('../../src/lib/generated', import.meta.url), { recursive: true });
} catch {}
fs.writeFileSync(
new URL('../../src/lib/generated/type-info.js', import.meta.url),
`
/* This file is generated by running \`node scripts/extract-types.js\`
in the packages/kit directory do not edit it */
export const modules = ${JSON.stringify(modules, null, ' ')};
`.trim()
);

@ -11,6 +11,22 @@
<meta name="twitter:site" content="@sveltejs" />
<meta name="twitter:creator" content="@sveltejs" />
<!-- add inline style and blocking script to prevent content flash/jump -->
<style>
.ts-version {
display: none;
}
.prefers-ts .js-version {
display: none;
}
.prefers-ts .ts-version {
display: block;
}
.no-js .ts-toggle {
display: none;
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-prefetch>

@ -1,14 +1,37 @@
// import 'prism-svelte';
// import 'prismjs/components/prism-bash.js';
// import 'prismjs/components/prism-diff.js';
// import 'prismjs/components/prism-typescript.js';
import { createShikiHighlighter } from 'shiki-twoslash';
import { SHIKI_LANGUAGE_MAP, normalizeSlugify, transform } from '../markdown';
// import { render, replace_placeholders } from './render.js';
// import { parse_route_id } from '../../../../../../packages/kit/src/utils/routing.js';
import { modules } from '$lib/generated/type-info';
import { createHash } from 'crypto';
import fs from 'fs';
import MagicString from 'magic-string';
import { createShikiHighlighter, renderCodeToHTML, runTwoSlash } from 'shiki-twoslash';
import ts from 'typescript';
import { SHIKI_LANGUAGE_MAP, escape, normalizeSlugify, slugify, transform } from '../markdown';
import { replace_placeholders } from './render.js';
const METADATA_REGEX = /(?:<!---\s*([\w-]+):\s*(.*?)\s*--->|\/\/\/\s*([\w-]+):\s*(.*))\n/gm;
const snippet_cache = new URL('../../../../node_modules/.snippets', import.meta.url).pathname;
if (!fs.existsSync(snippet_cache)) {
fs.mkdirSync(snippet_cache, { recursive: true });
}
const type_regex = new RegExp(
`(import\\(&apos;svelte&apos;\\)\\.)?\\b(${modules
.flatMap((module) => module.types)
.map((type) => type.name)
.join('|')})\\b`,
'g'
);
const type_links = new Map();
modules.forEach((module) => {
const slug = slugify(module.name);
module.types.forEach((type) => {
const link = `/docs/${slug}#type-${slugify(type.name)}`;
type_links.set(type.name, link);
});
});
/**
* @param {import('./types').DocsData} docs_data
@ -23,20 +46,23 @@ export async function get_parsed_docs(docs_data, slug) {
const highlighter = await createShikiHighlighter({ theme: 'css-variables' });
const placeholders_replaced_content = replace_placeholders(page.content);
return {
...page,
content: parse({
file: page.file,
body: generate_ts_from_js(page.content),
body: generate_ts_from_js(placeholders_replaced_content),
code: (source, language, current) => {
const hash = createHash('sha256');
hash.update(source + language + current);
const digest = hash.digest().toString('base64').replace(/\//g, '-');
// TODO: cache
// if (fs.existsSync(`${snippet_cache}/${digest}.html`)) {
// return fs.readFileSync(`${snippet_cache}/${digest}.html`, 'utf-8');
// }
try {
if (fs.existsSync(`${snippet_cache}/${digest}.html`)) {
return fs.readFileSync(`${snippet_cache}/${digest}.html`, 'utf-8');
}
} catch {}
/** @type {Record<string, string>} */
const options = {};
@ -44,10 +70,13 @@ export async function get_parsed_docs(docs_data, slug) {
let html = '';
source = source
.replace(/^\/\/\/ (.+?): (.+)\n/gm, (_, key, value) => {
options[key] = value;
return '';
})
.replace(
/(?:<!---\s*([\w-]+):\s*(.*?)\s*--->|\/\/\/\s*([\w-]+):\s*(.*))\n/gm,
(_, key, value) => {
options[key] = value;
return '';
}
)
.replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => {
if (prefix && language !== 'diff') return match;
@ -69,123 +98,123 @@ export async function get_parsed_docs(docs_data, slug) {
version_class = 'js-version';
}
// TODO: Replace later
html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] });
// if (source.includes('$env/')) {
// // TODO we're hardcoding static env vars that are used in code examples
// // in the types, which isn't... totally ideal, but will do for now
// injected.push(
// `declare module '$env/dynamic/private' { export const env: Record<string, string> }`,
// `declare module '$env/dynamic/public' { export const env: Record<string, string> }`,
// `declare module '$env/static/private' { export const API_KEY: string }`,
// `declare module '$env/static/public' { export const PUBLIC_BASE_URL: string }`
// );
// }
// if (source.includes('./$types') && !source.includes('@filename: $types.d.ts')) {
// const params = parse_route_id(options.file || `+page.${language}`)
// .params.map((param) => `${param.name}: string`)
// .join(', ');
// injected.push(
// `// @filename: $types.d.ts`,
// `import type * as Kit from '@sveltejs/kit';`,
// `export type PageLoad = Kit.Load<{${params}}>;`,
// `export type PageServerLoad = Kit.ServerLoad<{${params}}>;`,
// `export type LayoutLoad = Kit.Load<{${params}}>;`,
// `export type LayoutServerLoad = Kit.ServerLoad<{${params}}>;`,
// `export type RequestHandler = Kit.RequestHandler<{${params}}>;`,
// `export type Action = Kit.Action<{${params}}>;`,
// `export type Actions = Kit.Actions<{${params}}>;`
// );
// }
// // special case — we need to make allowances for code snippets coming
// // from e.g. ambient.d.ts
// if (file.endsWith('30-modules.md')) {
// injected.push('// @errors: 7006 7031');
// }
// // another special case
// if (source.includes('$lib/types')) {
// injected.push(`declare module '$lib/types' { export interface User {} }`);
// }
// if (injected.length) {
// const injected_str = injected.join('\n');
// if (source.includes('// @filename:')) {
// source = source.replace('// @filename:', `${injected_str}\n\n// @filename:`);
// } else {
// source = source.replace(
// /^(?!\/\/ @)/m,
// `${injected_str}\n\n// @filename: index.${language}\n// ---cut---\n`
// );
// }
// }
// const twoslash = runTwoSlash(source, language, {
// defaultCompilerOptions: {
// allowJs: true,
// checkJs: true,
// target: 'es2021',
// },
// });
// html = renderCodeToHTML(
// twoslash.code,
// 'ts',
// { twoslash: true },
// {},
// highlighter,
// twoslash
// );
// } catch (e) {
// console.error(`Error compiling snippet in ${file}`);
// console.error(e.code);
// throw e;
// }
// // we need to be able to inject the LSP attributes as HTML, not text, so we
// // turn &lt; into &amp;lt;
// html = html.replace(
// /<data-lsp lsp='([^']*)'([^>]*)>(\w+)<\/data-lsp>/g,
// (match, lsp, attrs, name) => {
// if (!lsp) return name;
// return `<data-lsp lsp='${lsp.replace(/&/g, '&amp;')}'${attrs}>${name}</data-lsp>`;
// }
// );
// // preserve blank lines in output (maybe there's a more correct way to do this?)
// html = html.replace(/<div class='line'><\/div>/g, '<div class="line"> </div>');
// } else if (language === 'diff') {
// const lines = source.split('\n').map((content) => {
// let type = null;
// if (/^[\+\-]/.test(content)) {
// type = content[0] === '+' ? 'inserted' : 'deleted';
// content = content.slice(1);
// }
// return {
// type,
// content: escape(content),
// };
// });
// html = `<pre class="language-diff"><code>${lines
// .map((line) => {
// if (line.type) return `<span class="${line.type}">${line.content}\n</span>`;
// return line.content + '\n';
// })
// .join('')}</code></pre>`;
// } else {
// const plang = languages[language];
// const highlighted = plang
// ? PrismJS.highlight(source, PrismJS.languages[plang], language)
// : source.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
// html = `<pre class='language-${plang}'><code>${highlighted}</code></pre>`;
// }
if (language === 'dts') {
html = renderCodeToHTML(
source,
'ts',
{ twoslash: false },
{ themeName: 'css-variables' },
highlighter
);
} else if (language === 'js' || language === 'ts') {
try {
const injected = [];
if (/(svelte)/.test(source) || page.file.includes('typescript')) {
injected.push(
`// @filename: ambient.d.ts`,
`/// <reference types="svelte" />`,
`/// <reference types="svelte/action" />`,
`/// <reference types="svelte/compiler" />`,
`/// <reference types="svelte/easing" />`,
`/// <reference types="svelte/motion" />`,
`/// <reference types="svelte/transition" />`,
`/// <reference types="svelte/store" />`,
`/// <reference types="svelte/action" />`
);
}
if (page.file.includes('svelte-compiler')) {
injected.push('// @esModuleInterop');
}
if (page.file.includes('svelte.md')) {
injected.push('// @errors: 2304');
}
// Actions JSDoc examples are invalid. Too many errors, edge cases
if (page.file.includes('svelte-action')) {
injected.push('// @noErrors');
}
if (page.file.includes('typescript')) {
injected.push('// @errors: 2304');
}
if (injected.length) {
const injected_str = injected.join('\n');
if (source.includes('// @filename:')) {
source = source.replace('// @filename:', `${injected_str}\n\n// @filename:`);
} else {
source = source.replace(
/^(?!\/\/ @)/m,
`${injected_str}\n\n// @filename: index.${language}\n` + ` // ---cut---\n`
);
}
}
const twoslash = runTwoSlash(source, language, {
defaultCompilerOptions: {
allowJs: true,
checkJs: true,
target: ts.ScriptTarget.ES2022
}
});
html = renderCodeToHTML(
twoslash.code,
'ts',
{ twoslash: true },
// @ts-ignore Why shiki-twoslash requires a theme name?
{},
highlighter,
twoslash
);
} catch (e) {
console.error(`Error compiling snippet in ${page.file}`);
console.error(e.code);
throw e;
}
// we need to be able to inject the LSP attributes as HTML, not text, so we
// turn &lt; into &amp;lt;
html = html.replace(
/<data-lsp lsp='([^']*)'([^>]*)>(\w+)<\/data-lsp>/g,
(match, lsp, attrs, name) => {
if (!lsp) return name;
return `<data-lsp lsp='${lsp.replace(/&/g, '&amp;')}'${attrs}>${name}</data-lsp>`;
}
);
// preserve blank lines in output (maybe there's a more correct way to do this?)
html = html.replace(/<div class='line'><\/div>/g, '<div class="line"> </div>');
} else if (language === 'diff') {
const lines = source.split('\n').map((content) => {
let type = null;
if (/^[\+\-]/.test(content)) {
type = content[0] === '+' ? 'inserted' : 'deleted';
content = content.slice(1);
}
return {
type,
content: escape(content)
};
});
html = `<pre class="language-diff"><code>${lines
.map((line) => {
if (line.type) return `<span class="${line.type}">${line.content}\n</span>`;
return line.content + '\n';
})
.join('')}</code></pre>`;
} else {
const highlighted = highlighter.codeToHtml(source, {
lang: SHIKI_LANGUAGE_MAP[language]
});
html = highlighted.replace(/<div class='line'><\/div>/g, '<div class="line"> </div>');
}
if (options.file) {
html = `<div class="code-block"><span class="filename">${options.file}</span>${html}</div>`;
@ -195,18 +224,20 @@ export async function get_parsed_docs(docs_data, slug) {
html = html.replace(/class=('|")/, `class=$1${version_class} `);
}
// type_regex.lastIndex = 0;
type_regex.lastIndex = 0;
html = html
// .replace(type_regex, (match, prefix, name) => {
// if (options.link === 'false' || name === current) {
// // we don't want e.g. RequestHandler to link to RequestHandler
// return match;
// }
// const link = `<a href="${type_links.get(name)}">${name}</a>`;
// return `${prefix || ''}${link}`;
// })
.replace(type_regex, (match, prefix, name, pos, str) => {
const char_after = str.slice(pos + match.length, pos + match.length + 1);
if (options.link === 'false' || name === current || /(\$|\d|\w)/.test(char_after)) {
// we don't want e.g. RequestHandler to link to RequestHandler
return match;
}
const link = `<a href="${type_links.get(name)}">${name}</a>`;
return `${prefix || ''}${link}`;
})
.replace(
/^(\s+)<span class="token comment">([\s\S]+?)<\/span>\n/gm,
(match, intro_whitespace, content) => {
@ -226,17 +257,16 @@ export async function get_parsed_docs(docs_data, slug) {
)
.replace(/\/\*…\*\//g, '…');
// fs.writeFileSync(`${snippet_cache}/${digest}.html`, html);
fs.writeFileSync(`${snippet_cache}/${digest}.html`, html);
return html;
},
codespan: (text) => {
return (
'<code>' +
text +
// text.replace(type_regex, (match, prefix, name) => {
// const link = `<a href="${type_links.get(name)}">${name}</a>`;
// return `${prefix || ''}${link}`;
// }) +
text.replace(type_regex, (match, prefix, name) => {
const link = `<a href="${type_links.get(name)}">${name}</a>`;
return `${prefix || ''}${link}`;
}) +
'</code>'
);
}
@ -279,9 +309,16 @@ function parse({ body, code, codespan }) {
headings[level] = normalized;
headings.length = level;
const slug = normalizeSlugify(raw);
const type_heading_match = /^\[TYPE\]:\s+(.+)/.exec(raw);
return `<h${level} id="${slug}">${html}<a href="#${slug}" class="permalink"><span class="visually-hidden">permalink</span></a></h${level}>`;
const slug = normalizeSlugify(type_heading_match ? `type-${type_heading_match[1]}` : raw);
return `<h${level} id="${slug}">${html
.replace(/<\/?code>/g, '')
.replace(
/^\[TYPE\]:\s+(.+)/,
'$1'
)}<a href="#${slug}" class="permalink"><span class="visually-hidden">permalink</span></a></h${level}>`;
},
code: (source, language) => code(source, language, current),
codespan
@ -313,7 +350,7 @@ export function generate_ts_from_js(markdown) {
return match.replace('js', 'original-js') + '\n```generated-ts\n' + ts + '\n```';
})
.replaceAll(/```svelte\n([\s\S]+?)\n```/g, (match, code) => {
if (!code.includes('/// file:')) {
if (!METADATA_REGEX.test(code)) {
// No named file -> assume that the code is not meant to be shown in two versions
return match;
}
@ -333,7 +370,7 @@ export function generate_ts_from_js(markdown) {
return (
match.replace('svelte', 'original-svelte') +
'\n```generated-svelte\n' +
code.replace(outer, `<script lang="ts">${ts}</script>`) +
code.replace(outer, `<script lang="ts">\n\t${ts.trim()}\n</script>`) +
'\n```'
);
});
@ -404,7 +441,7 @@ function convert_to_ts(js_code, indent = '', offset = '') {
if (variable_statement.name.getText() === 'actions') {
code.appendLeft(variable_statement.getEnd(), ` satisfies ${name}`);
} else {
code.appendLeft(variable_statement.name.getEnd(), `: ${name}`);
code.appendLeft(variable_statement.name.getEnd(), `: ${name}${generics ?? ''}`);
}
modified = true;
@ -412,11 +449,11 @@ function convert_to_ts(js_code, indent = '', offset = '') {
throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code);
}
} else if (ts.isJSDocParameterTag(tag) && ts.isFunctionDeclaration(node)) {
if (node.parameters.length !== 1) {
throw new Error(
'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText()
);
}
// if (node.parameters.length !== 1) {
// throw new Error(
// 'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText()
// );
// }
const [name] = get_type_info(tag);
code.appendLeft(node.parameters[0].getEnd(), `: ${name}`);

@ -0,0 +1,182 @@
import { modules } from '$lib/generated/type-info.js';
/** @param {string} content */
export function replace_placeholders(content) {
return content
.replace(/> EXPANDED_TYPES: (.+?)#(.+)$/gm, (_, name, id) => {
const module = modules.find((module) => module.name === name);
if (!module) throw new Error(`Could not find module ${name}`);
const type = module.types.find((t) => t.name === id);
return (
type.comment +
type.children
.map((child) => {
let section = `### ${child.name}`;
if (child.bullets) {
section += `\n\n<div class="ts-block-property-bullets">\n\n${child.bullets.join(
'\n'
)}\n\n</div>`;
}
section += `\n\n${child.comment}`;
if (child.children) {
section += `\n\n<div class="ts-block-property-children">\n\n${child.children
.map(stringify)
.join('\n')}\n\n</div>`;
}
return section;
})
.join('\n\n')
);
})
.replace(/> TYPES: (.+?)(?:#(.+))?$/gm, (_, name, id) => {
const module = modules.find((module) => module.name === name);
if (!module) throw new Error(`Could not find module ${name}`);
if (id) {
const type = module.types.find((t) => t.name === id);
return (
`<div class="ts-block">${fence(type.snippet, 'dts')}` +
type.children.map(stringify).join('\n\n') +
`</div>`
);
}
return `${module.comment}\n\n${module.types
.map((t) => {
let children = t.children.map((val) => stringify(val, 'dts')).join('\n\n');
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`;
})
.join('')}`;
})
.replace(/> EXPORT_SNIPPET: (.+?)#(.+)?$/gm, (_, name, id) => {
const module = modules.find((module) => module.name === name);
if (!module) throw new Error(`Could not find module ${name} for EXPORT_SNIPPET clause`);
if (!id) {
throw new Error(`id is required for module ${name}`);
}
const exported = module.exports.filter((t) => t.name === id);
return exported
.map((exportVal) => `<div class="ts-block">${fence(exportVal.snippet, 'dts')}</div>`)
.join('\n\n');
})
.replace('> MODULES', () => {
return modules
.map((module) => {
if (module.exports.length === 0 && !module.exempt) return '';
let import_block = '';
if (module.exports.length > 0) {
// deduplication is necessary for now, because of `error()` overload
const exports = Array.from(new Set(module.exports.map((x) => x.name)));
let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
if (declaration.length > 80) {
declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
}
import_block = fence(declaration, 'js');
}
return `## ${module.name}\n\n${import_block}\n\n${module.comment}\n\n${module.exports
.map((type) => {
const markdown =
`<div class="ts-block">${fence(type.snippet)}` +
type.children.map(stringify).join('\n\n') +
`</div>`;
return `### ${type.name}\n\n${type.comment}\n\n${markdown}`;
})
.join('\n\n')}`;
})
.join('\n\n');
})
.replace(/> EXPORTS: (.+)/, (_, name) => {
const module = modules.find((module) => module.name === name);
if (!module) throw new Error(`Could not find module ${name} for EXPORTS: clause`);
if (module.exports.length === 0 && !module.exempt) return '';
let import_block = '';
if (module.exports.length > 0) {
// deduplication is necessary for now, because of `error()` overload
const exports = Array.from(new Set(module.exports.map((x) => x.name)));
let declaration = `import { ${exports.join(', ')} } from '${module.name}';`;
if (declaration.length > 80) {
declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`;
}
import_block = fence(declaration, 'js');
}
return `${import_block}\n\n${module.comment}\n\n${module.exports
.map((type) => {
const markdown =
`<div class="ts-block">${fence(type.snippet, 'dts')}` +
type.children.map((val) => stringify(val, 'dts')).join('\n\n') +
`</div>`;
return `### ${type.name}\n\n${type.comment}\n\n${markdown}`;
})
.join('\n\n')}`;
});
}
/**
* @param {string} code
* @param {keyof typeof import('../markdown/index').SHIKI_LANGUAGE_MAP} lang
*/
function fence(code, lang = 'ts') {
return (
'\n\n```' +
lang +
'\n' +
(['js', 'ts'].includes(lang) ? '// @noErrors\n' : '') +
code +
'\n```\n\n'
);
}
/**
* @param {import('./types').Type} member
* @param {keyof typeof import('../markdown').SHIKI_LANGUAGE_MAP} [lang]
*/
function stringify(member, lang = 'ts') {
const bullet_block =
member.bullets.length > 0
? `\n\n<div class="ts-block-property-bullets">\n\n${member.bullets.join('\n')}</div>`
: '';
const child_block =
member.children.length > 0
? `\n\n<div class="ts-block-property-children">${member.children
.map((val) => stringify(val, lang))
.join('\n')}</div>`
: '';
return (
`<div class="ts-block-property">${fence(member.snippet, lang)}` +
`<div class="ts-block-property-details">\n\n` +
bullet_block +
'\n\n' +
member.comment
.replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */')
.replace(/^( )+/gm, (match, spaces) => {
return '\t'.repeat(match.length / 2);
}) +
child_block +
'\n</div></div>'
);
}

@ -24,6 +24,7 @@ export const SHIKI_LANGUAGE_MAP = {
svelte: 'svelte',
sv: 'svelte',
js: 'javascript',
dts: 'typescript',
css: 'css',
diff: 'diff',
ts: 'typescript',

@ -0,0 +1,67 @@
<script>
import { tick } from 'svelte';
export let html = '';
export let x = 0;
export let y = 0;
let width = 1;
let tooltip;
// bit of a gross hack but it works — this prevents the
// tooltip from disappearing off the side of the screen
$: if (html && tooltip) {
tick().then(() => {
width = tooltip.getBoundingClientRect().width;
});
}
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div
on:mouseenter
on:mouseleave
class="tooltip-container"
style="left: {x}px; top: {y}px; --offset: {Math.min(-10, window.innerWidth - (x + width + 10))}px"
>
<div bind:this={tooltip} class="tooltip">
<span>{@html html}</span>
</div>
</div>
<style>
.tooltip-container {
--bg: var(--sk-theme-2);
--arrow-size: 0.4rem;
position: absolute;
transform: translate(var(--offset), calc(2rem + var(--arrow-size)));
}
.tooltip {
margin: 0 2rem 0 0;
background-color: var(--bg);
color: #fff;
text-align: left;
padding: 0.4rem 0.6rem;
border-radius: var(--sk-border-radius);
font-family: var(--sk-font-mono);
font-size: 1.2rem;
white-space: pre-wrap;
z-index: 100;
filter: drop-shadow(2px 4px 6px #67677866);
}
.tooltip::after {
content: '';
position: absolute;
left: calc(-1 * var(--offset) - var(--arrow-size));
top: calc(-2 * var(--arrow-size));
border: var(--arrow-size) solid transparent;
border-bottom-color: var(--bg);
}
.tooltip :global(a) {
color: white;
text-decoration: underline;
}
</style>

@ -0,0 +1,60 @@
import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte';
export function setup() {
onMount(() => {
let tooltip;
let timeout;
function over(event) {
if (event.target.tagName === 'DATA-LSP') {
clearTimeout(timeout);
if (!tooltip) {
tooltip = new Tooltip({
target: document.body
});
tooltip.$on('mouseenter', () => {
clearTimeout(timeout);
});
tooltip.$on('mouseleave', () => {
clearTimeout(timeout);
tooltip.$destroy();
tooltip = null;
});
}
const rect = event.target.getBoundingClientRect();
const html = event.target.getAttribute('lsp');
const x = (rect.left + rect.right) / 2 + window.scrollX;
const y = rect.top + window.scrollY;
tooltip.$set({
html,
x,
y
});
}
}
function out(event) {
if (event.target.tagName === 'DATA-LSP') {
timeout = setTimeout(() => {
tooltip.$destroy();
tooltip = null;
}, 200);
}
}
window.addEventListener('mouseover', over);
window.addEventListener('mouseout', out);
return () => {
window.removeEventListener('mouseover', over);
window.removeEventListener('mouseout', out);
};
});
}

@ -1,3 +1,4 @@
import { replace_placeholders } from '$lib/server/docs/render';
import {
extract_frontmatter,
normalizeSlugify,
@ -41,7 +42,7 @@ export function content() {
const filepath = `${base}/${category.slug}/${file}`;
// const markdown = replace_placeholders(fs.readFileSync(filepath, 'utf-8'));
const markdown = fs.readFileSync(filepath, 'utf-8');
const markdown = replace_placeholders(fs.readFileSync(filepath, 'utf-8'));
const { body, metadata } = extract_frontmatter(markdown);

@ -1,6 +1,7 @@
<script>
import { page } from '$app/stores';
import Contents from './Contents.svelte';
import { TSToggle } from '@sveltejs/site-kit/components';
export let data;
@ -21,6 +22,10 @@
<div class="toc-container">
<Contents contents={data.sections} />
</div>
<div class="ts-toggle">
<TSToggle />
</div>
</div>
<style>
@ -71,12 +76,12 @@
.toc-container {
background: var(--sk-back-3);
}
/*
.ts-toggle {
width: 100%;
border-top: 1px solid var(--sk-back-4);
background-color: var(--sk-back-3);
} */
}
@media (min-width: 832px) {
.toc-container {
@ -103,14 +108,14 @@
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
}
/* .ts-toggle {
.ts-toggle {
position: fixed;
width: var(--sidebar-width);
bottom: 0;
z-index: 1;
margin-right: 0;
border-right: 1px solid var(--sk-back-5);
} */
}
}
@media (min-width: 1200px) {

@ -0,0 +1,2 @@
// This page now exists solely for redirect, prerendering triggers the `handleMissingID`
export const prerender = false;

@ -1,15 +1,75 @@
<script>
import { page } from '$app/stores';
import OnThisPage from './OnThisPage.svelte';
import * as hovers from '$lib/utils/hovers';
export let data;
$: pages = data.sections.flatMap((section) => section.pages);
$: index = pages.findIndex(({ path }) => path === $page.url.pathname);
$: prev = pages[index - 1];
$: next = pages[index + 1];
hovers.setup();
</script>
<svelte:head>
<title>{data.page.title} - Svelte</title>
<title>{data.page.title} • Docs • Svelte</title>
<meta name="twitter:title" content="Svelte docs" />
<meta name="twitter:description" content="{data.page.title} • Svelte documentation" />
<meta name="Description" content="{data.page.title} • Svelte documentation" />
</svelte:head>
<div class="text">
{@html data.page.content}
</div>
<div class="controls">
<div>
<span class:faded={!prev}>previous</span>
{#if prev}
<a href={prev.path}>{prev.title}</a>
{/if}
</div>
<div>
<span class:faded={!next}>next</span>
{#if next}
<a href={next.path}>{next.title}</a>
{/if}
</div>
</div>
<OnThisPage details={data.page} />
<style>
.controls {
max-width: calc(var(--sk-line-max-width) + 1rem);
border-top: 1px solid var(--sk-back-4);
padding: 1rem 0 0 0;
display: grid;
grid-template-columns: 1fr 1fr;
margin: 6rem 0 0 0;
}
.controls > :first-child {
text-align: left;
}
.controls > :last-child {
text-align: right;
}
.controls span {
display: block;
font-size: 1.2rem;
text-transform: uppercase;
font-weight: 600;
color: var(--sk-text-3);
}
.controls span.faded {
opacity: 0.4;
}
</style>

@ -92,13 +92,13 @@
containerEl.scrollBy({
top: top - max,
left: 0,
behavior: 'smooth',
behavior: 'smooth'
});
} else if (bottom < min) {
containerEl.scrollBy({
top: bottom - min,
left: 0,
behavior: 'smooth',
behavior: 'smooth'
});
}
}

@ -6,7 +6,7 @@
</script>
<svelte:head>
<title>Search • SvelteKit</title>
<title>Search • Svelte</title>
</svelte:head>
<main>

Loading…
Cancel
Save