Remove site - new home at sveltejs/sites (#6994)

pull/7015/head
Ben McCann 3 years ago committed by GitHub
parent 52549c466b
commit c2847b7d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

11
.gitignore vendored

@ -23,14 +23,3 @@ node_modules
_actual*.*
_output
/types
/site/.svelte-kit/
/site/build/
/site/.env
/site/static/svelte-app.json
/site/static/contributors.jpg
/site/static/donors.jpg
/site/static/workers/
/site/scripts/svelte-app/
/site/src/routes/_contributors.js
/site/src/routes/_donors.js

@ -1,7 +0,0 @@
/*
!/Dockerfile
!/package.json
!/package-lock.json
!/build
!/static
!/content

@ -1,13 +0,0 @@
NODE_ENV=
PORT=3000
BASEURL=http://localhost:3000
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
VITE_MAPBOX_ACCESS_TOKEN=
PGHOST=localhost
PGPORT=5432
PGUSER=username
PGPASSWORD=password
PGDATABASE=database_name

@ -1,58 +0,0 @@
module.exports = {
root: true,
rules: {
indent: [2, 'tab', { SwitchCase: 1 }],
semi: [2, 'always'],
'keyword-spacing': [2, { before: true, after: true }],
'space-before-blocks': [2, 'always'],
'no-mixed-spaces-and-tabs': [2, 'smart-tabs'],
'no-cond-assign': 0,
'no-unused-vars': 2,
'object-shorthand': [2, 'always'],
'no-const-assign': 2,
'no-class-assign': 2,
'no-this-before-super': 2,
'no-var': 2,
'no-unreachable': 2,
'valid-typeof': 2,
'quote-props': [2, 'as-needed'],
'one-var': [2, 'never'],
'prefer-arrow-callback': 2,
'prefer-const': [2, { destructuring: 'all' }],
'arrow-spacing': 2,
'no-inner-declarations': 0,
'require-atomic-updates': 0
},
env: {
es6: true,
browser: true,
node: true,
mocha: true
},
extends: [
'eslint:recommended',
'plugin:import/errors',
'plugin:import/warnings'
],
plugins: ['svelte3'],
overrides: [
{
files: ['*.svelte'],
processor: 'svelte3/svelte3'
}
],
parserOptions: {
ecmaVersion: 9,
sourceType: 'module'
},
settings: {
'import/core-modules': ['svelte'],
'svelte3/compiler': (() => {
try {
return require('svelte/compiler');
} catch (e) {
return null;
}
})()
}
};

@ -1 +0,0 @@
#!include:.dockerignore

@ -1,22 +0,0 @@
# IMPORTANT: Don't use this Dockerfile in your own projects without also looking at the .dockerignore file.
# Without an appropriate .dockerignore, this Dockerfile will copy a large number of unneeded files into your image.
FROM mhart/alpine-node:14
# install dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
###
# Only copy over the Node pieces we need
# ~> Saves 35MB
###
FROM mhart/alpine-node:slim-14
WORKDIR /app
COPY --from=0 /app .
COPY . .
EXPOSE 3000
CMD ["node", "build"]

@ -1,22 +0,0 @@
HASH := `git rev-parse --short HEAD`
SERVICE := svelte-website
PROJECT := svelte-dev
IMAGE := gcr.io/$(PROJECT)/$(SERVICE):$(HASH)
sveltekit:
@echo "\n~> updating template & contributors list"
@npm run update
@echo "\n~> building SvelteKit app"
@npm run build
docker:
@echo "\n~> building docker image"
@gcloud builds submit --project $(PROJECT) -t $(IMAGE)
deploy: sveltekit docker
@echo "\n~> deploying $(SERVICE) to Cloud Run servers"
@gcloud run deploy $(SERVICE) --project $(PROJECT) --allow-unauthenticated --platform managed --region us-central1 --image $(IMAGE) --memory=512Mi

@ -1,84 +0,0 @@
## Running locally
Set up the site sub-project:
```bash
git clone https://github.com/sveltejs/svelte.git
cd site
npm ci
npm run dev
```
and navigate to [localhost:3000](http://localhost:3000).
The first time you run the site locally, it will update the list of Contributors and REPL dependencies. After this it won't run again unless you force it by running:
```bash
npm run update
```
## Running using the local copy of Svelte
By default, the REPL will fetch the most recent version of Svelte from https://unpkg.com/svelte. When running the site locally, you can also use your local copy of Svelte.
To produce the proper browser-compatible UMD build of the compiler, you will need to run `npm run build` (or `npm run dev`) in the root of this repository with the `PUBLISH` environment variable set to any non-empty string:
```bash
git clone https://github.com/sveltejs/svelte.git
cd svelte
npm ci
PUBLISH=1 npm run build
cd site
npm ci
npm run dev
```
Then visit the REPL at [localhost:3000/repl?version=local](http://localhost:3000/repl?version=local). Please note that the local REPL only works with `npm run dev` and not when building the site for production usage.
## REPL GitHub integration
In order for the REPL's GitHub integration to work properly when running locally, you will need to:
- [create a GitHub OAuth app](https://github.com/settings/developers):
- set `Authorization callback URL` to `http://localhost:3000/auth/callback`;
- set `Application name` as you like, and `Homepage URL` as `http://localhost:3000/`;
- create the app and take note of `Client ID` and `Client Secret`
- in this repo, create `site/.env` containing:
```
GITHUB_CLIENT_ID=[your app's Client ID]
GITHUB_CLIENT_SECRET=[your app's Client Secret]
BASEURL=http://localhost:3000
```
## Building the site
To build the website, run `npm run build`. The output can be found in `build`.
## Testing
Tests can be run using `npm run test`.
## Linking `@sveltejs/site-kit` and `@sveltejs/svelte-repl`
This site depends on `@sveltejs/site-kit` (a collection of styles, components and icons used in common by *.svelte.dev websites), and `@sveltejs/svelte-repl`.
In order to work on features that depend on those packages, you need to [link](https://docs.npmjs.com/cli/link) their repositories:
- `cd <somewhere>`
- `git clone https://github.com/sveltejs/site-kit`
- `git clone https://github.com/sveltejs/svelte-repl`
- `cd <somewhere>/site-kit`
- `npm link`
- `cd <somewhere>/svelte-repl`
- `npm link`
- `cd <svelte-repo>/site`
- `npm link @sveltejs/site-kit`
- `npm link @sveltejs/svelte-repl`
## Translating the API docs
Anchors are automatically generated using headings in the documentation and by default (for the english language) they are latinised to make sure the URL is always conforming to RFC3986.
If we need to translate the API documentation to a language using unicode chars, we can setup this app to export the correct anchors by setting up `SLUG_PRESERVE_UNICODE` to `true` in `config.js`.

@ -1,2 +0,0 @@
export const SLUG_PRESERVE_UNICODE = false;
export const SLUG_SEPARATOR = '_';

@ -1,15 +0,0 @@
version: "3.0"
services:
postgres:
image: postgres:13.4
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${PGDATABASE}
- POSTGRES_USER=${PGUSER}
- POSTGRES_PASSWORD=${PGPASSWORD}
adminer:
image: adminer
restart: always
ports:
- "4000:8080"

@ -1,25 +0,0 @@
exports.up = DB => {
DB.sql(`
create table if not exists users (
id serial primary key,
uid character varying(255) not null unique,
name character varying(255),
username character varying(255) not null,
avatar text,
github_token character varying(255),
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone
);
create unique index if not exists users_pkey ON users(id int4_ops);
create unique index if not exists users_uid_key ON users(uid text_ops);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists users cascade;
drop index if exists users_uid_key;
drop index if exists users_pkey;
`);
};

@ -1,24 +0,0 @@
exports.up = DB => {
DB.sql(`
create table if not exists gists (
id serial primary key,
uid uuid NOT NULL DEFAULT gen_random_uuid(),
user_id integer REFERENCES users(id) not null,
name character varying(255) not null,
files json not null,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone
);
create unique index if not exists gists_pkey ON gists(id int4_ops);
create index if not exists gists_user_id_key ON gists(user_id int4_ops);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists gists cascade;
drop index if exists gists_user_id_key;
drop index if exists gists_pkey;
`);
};

@ -1,15 +0,0 @@
exports.up = DB => {
DB.sql(`
create table if not exists sessions (
uid uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
user_id integer REFERENCES users(id) not null,
expiry timestamp without time zone DEFAULT now() + interval '1 year'
);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists sessions;
`);
};

3976
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,44 +0,0 @@
{
"name": "svelte.dev",
"version": "1.0.0",
"description": "Docs and examples for Svelte",
"type": "module",
"scripts": {
"dev": "node scripts/update.js && npm run copy-workers && svelte-kit dev",
"copy-workers": "node scripts/copy-workers.js",
"migrate": "node-pg-migrate -r dotenv/config",
"build": "node scripts/update.js && npm run copy-workers && svelte-kit build",
"update": "node scripts/update.js --force=true",
"preview": "svelte-kit preview",
"start": "node build",
"test": "uvu test",
"deploy": "make deploy"
},
"dependencies": {
"cookie": "^0.4.0",
"devalue": "^2.0.0",
"do-not-zip": "^1.0.0",
"flru": "^1.0.2",
"httpie": "^1.1.2",
"jsonwebtoken": "^8.5.1",
"marked": "^4.0.6",
"pg": "^8.7.1",
"prism-svelte": "^0.4.3",
"prismjs": "^1.25.0"
},
"devDependencies": {
"@sindresorhus/slugify": "^0.9.1",
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"@sveltejs/site-kit": "^1.4.1",
"@sveltejs/svelte-repl": "^0.4.1",
"degit": "^2.1.4",
"dotenv": "^10.0.0",
"jimp": "^0.8.0",
"node-fetch": "^2.6.1",
"node-pg-migrate": "^6.0.0",
"shelljs": "^0.8.3",
"svelte": "^3.39.0",
"uvu": "^0.5.2"
}
}

@ -1,4 +0,0 @@
import sh from 'shelljs';
sh.rm('-rf', 'static/workers');
sh.cp('-r', 'node_modules/@sveltejs/svelte-repl/workers', 'static');

@ -1,67 +0,0 @@
import fs from 'fs';
import puppeteer from 'puppeteer';
import Jimp from 'jimp';
import c from 'kleur';
const slugs = [];
fs.readdirSync(`content/examples`).forEach(group_dir => {
fs.readdirSync(`content/examples/${group_dir}`).filter(file => file !== 'meta.json').map(example_dir => {
const slug = example_dir.replace(/^\d+-/, '');
slugs.push(slug);
});
});
async function main() {
const browser = await puppeteer.launch({
defaultViewport: {
width: 400 * 10 / 4,
height: 400 + 42,
deviceScaleFactor: 2
}
});
const page = await browser.newPage();
for (const slug of slugs) {
try {
const output_file = `static/examples/thumbnails/${slug}.jpg`;
if (fs.existsSync(output_file)) {
console.log(c.gray(`skipping ${slug}`));
continue;
}
console.log(slug);
await page.goto(`http://localhost:3000/repl/embed?example=${slug}`);
await page.waitForSelector('iframe.inited[title=Result]');
await page.waitFor(1500);
const iframe = await page.$('iframe.inited[title=Result]');
const buffer = await iframe.screenshot();
const image = await Jimp.read(buffer);
console.log(image.bitmap.width, image.bitmap.height);
image.crop(3, 3, image.bitmap.width - 6, image.bitmap.height - 6);
image.autocrop();
// image.scale(0.25);
if (image.bitmap.width > 200 || image.bitmap.height > 200) {
const scale = Math.min(
200 / image.bitmap.width,
200 / image.bitmap.height
);
image.scale(scale);
}
await image.quality(75).write(output_file);
} catch (err) {
console.log(c.bold().red(`failed to screenshot ${slug}`));
console.log(err);
}
}
await browser.close();
}
main();

@ -1,77 +0,0 @@
import sander from 'sander';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(`${__dirname}/../..`);
function extract_frontmatter(markdown) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
const frontMatter = match[1];
const content = markdown.slice(match[0].length);
const metadata = {};
frontMatter.split('\n').forEach(pair => {
const colonIndex = pair.indexOf(':');
metadata[pair.slice(0, colonIndex).trim()] = pair
.slice(colonIndex + 1)
.trim();
});
return { metadata, content };
}
const tutorial_sections = sander.readdirSync(`content/tutorial`).map(section_dir => {
const section_slug = section_dir.replace(/^\d+-/, '');
const meta = JSON.parse(sander.readFileSync(`content/tutorial/${section_dir}/meta.json`, { encoding: 'utf-8' }));
const chapters = sander.readdirSync(`content/tutorial/${section_dir}`).map(chapter_dir => {
const app_dir = `content/tutorial/${section_dir}/${chapter_dir}/app-b`;
if (!sander.existsSync(app_dir)) return;
const markdown = sander.readFileSync(`content/tutorial/${section_dir}/${chapter_dir}/text.md`, { encoding: 'utf-8' });
const { metadata } = extract_frontmatter(markdown);
const chapter_slug = chapter_dir.replace(/^\d+-/, '');
return {
slug: chapter_slug,
title: metadata.title,
files: sander.readdirSync(app_dir).map(name => {
return {
name,
source: sander.readFileSync(`${app_dir}/${name}`, { encoding: 'utf-8' })
};
})
};
}).filter(Boolean);
return {
slug: section_slug,
title: meta.title,
chapters
};
}).filter(section => section.chapters.length > 0);
const pad = i => i < 10 ? `0${i}` : i;
tutorial_sections.forEach((section, i) => {
const section_dir = `${pad(i)}-${section.slug}`;
sander.writeFileSync(`content/examples/${section_dir}/meta.json`, JSON.stringify({
title: section.title
}, null, '\t'));
section.chapters.forEach((chapter, i) => {
const chapter_dir = `${pad(i)}-${chapter.slug}`;
sander.writeFileSync(`content/examples/${section_dir}/${chapter_dir}/meta.json`, JSON.stringify({
title: chapter.title
}, null, '\t'));
chapter.files.forEach(file => {
sander.writeFileSync(`content/examples/${section_dir}/${chapter_dir}/${file.name}`, file.source);
});
});
});

@ -1,68 +0,0 @@
import 'dotenv/config';
import fs from 'fs';
import fetch from 'node-fetch';
import Jimp from 'jimp';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const force = process.env.FORCE_UPDATE === 'true';
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(__dirname);
const outputFile = `../src/routes/_contributors.js`;
if (!force && fs.existsSync(outputFile)) {
console.info(`[update/contributors] ${outputFile} exists. Skipping`);
process.exit(0);
}
const base = `https://api.github.com/repos/sveltejs/svelte/contributors`;
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
const MAX = 20;
const SIZE = 128;
async function main() {
const contributors = [];
let page = 1;
while (true) {
const res = await fetch(`${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}&per_page=100&page=${page++}`);
const list = await res.json();
if (list.length === 0) break;
contributors.push(...list);
}
const authors = contributors
.filter(({ login }) => !login.includes('[bot]'))
.sort((a, b) => b.contributions - a.contributions)
.slice(0, MAX);
const sprite = new Jimp(SIZE * authors.length, SIZE);
for (let i = 0; i < authors.length; i += 1) {
const author = authors[i];
console.log(`${i + 1} / ${authors.length}: ${author.login}`);
const image_data = await fetch(author.avatar_url);
const buffer = await image_data.arrayBuffer();
const image = await Jimp.read(buffer);
image.resize(SIZE, SIZE);
sprite.composite(image, i * SIZE, 0);
}
await sprite.quality(80).write(`../static/contributors.jpg`);
// TODO: Optimizing the static/contributors.jpg image should probably get automated as well
console.log('remember to additionally optimize the resulting /static/contributors.jpg image file via e.g. https://squoosh.app ');
const str = `[\n\t${authors.map(a => `'${a.login}'`).join(',\n\t')}\n]`;
fs.writeFileSync(outputFile, `export default ${str};`);
}
main();

@ -1,65 +0,0 @@
import 'dotenv/config';
import fs from 'fs';
import fetch from 'node-fetch';
import Jimp from 'jimp';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const force = process.env.FORCE_UPDATE === 'true';
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(__dirname);
const outputFile = `../src/routes/_donors.js`;
if (!force && fs.existsSync(outputFile)) {
console.info(`[update/donors] ${outputFile} exists. Skipping`);
process.exit(0);
}
const MAX = 20;
const SIZE = 128;
async function main() {
const res = await fetch('https://opencollective.com/svelte/members/all.json');
const donors = await res.json();
const unique = new Map();
donors.forEach(d => unique.set(d.profile, d));
let backers = [...unique.values()]
.filter(({ role }) => role === 'BACKER')
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
.slice(0, 3 * MAX);
const included = [];
for (let i = 0; included.length < MAX; i += 1) {
const backer = backers[i];
console.log(`${included.length + 1} / ${MAX}: ${backer.name}`);
try {
const image_data = await fetch(backer.image);
const buffer = await image_data.arrayBuffer();
const image = await Jimp.read(buffer);
image.resize(SIZE, SIZE);
included.push({ backer, image });
} catch( err) {
console.log(`Skipping ${backer.name}: no image data`);
}
}
const sprite = new Jimp(SIZE * included.length, SIZE);
for (let i = 0; i < included.length; i += 1) {
sprite.composite(included[i].image, i * SIZE, 0);
}
await sprite.quality(80).write(`../static/donors.jpg`);
// TODO: Optimizing the static/donors.jpg image should probably get automated as well
console.log('remember to additionally optimize the resulting /static/donors.jpg image file via e.g. https://squoosh.app ');
const str = `[\n\t${included.map(a => `${JSON.stringify(a.backer.name)}`).join(',\n\t')}\n]`;
fs.writeFileSync(outputFile, `export default ${str};`);
}
main();

@ -1,9 +0,0 @@
import sh from 'shelljs';
sh.env['FORCE_UPDATE'] = process.argv.includes('--force=true');
Promise.all([
sh.exec('node ./scripts/get_contributors.js'),
sh.exec('node ./scripts/get_donors.js'),
sh.exec('node ./scripts/update_template.js')
]);

@ -1,36 +0,0 @@
import sh from 'shelljs';
import fs from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
const force = process.env.FORCE_UPDATE === 'true';
const __dirname = dirname(fileURLToPath(import.meta.url));
sh.cd(path.join(__dirname, '..'));
const outputFile = 'static/svelte-app.json';
if (!force && fs.existsSync(outputFile)) {
console.info(`[update/template] ${outputFile} exists. Skipping`);
process.exit(0);
}
// fetch svelte app
sh.rm('-rf','scripts/svelte-app');
sh.exec('npx degit sveltejs/template scripts/svelte-app');
// remove src (will be recreated client-side) and node_modules
sh.rm('-rf', 'scripts/svelte-app/src');
sh.rm('-rf', 'scripts/svelte-app/node_modules');
// build svelte-app.json
const appPath = 'scripts/svelte-app';
const files = [];
for (const path of sh.find(appPath).filter(p => fs.lstatSync(p).isFile()) ) {
const bytes = fs.readFileSync(path);
const string = bytes.toString();
const data = bytes.compare(Buffer.from(string)) === 0 ? string : [...bytes];
files.push({ path: path.slice(appPath.length + 1), data });
}
fs.writeFileSync(outputFile, JSON.stringify(files));

@ -1,26 +0,0 @@
<!doctype html>
<html lang='en' class="theme-default typo-default">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name='theme-color' content='#ff3e00'>
<base href='/'>
<link rel='stylesheet' href='global.css'>
<link rel='stylesheet' href='prism.css'>
<link rel='manifest' href='manifest.json'>
<link rel='icon' type='image/png' href='favicon.png'>
<meta name='twitter:card' content='summary_large_image'>
<meta name='twitter:site' content='@sveltejs'>
<meta name='twitter:creator' content='@sveltejs'>
<meta name='twitter:image' content='https://svelte.dev/images/twitter-card.png'>
<meta name='og:image' content='https://svelte.dev/images/twitter-card.png'>
%svelte.head%
</head>
<body>
<div id='svelte'>%svelte.body%</div>
</body>
</html>

@ -1,59 +0,0 @@
<script>
import { onMount } from 'svelte';
export let once = false;
export let top = 0;
export let bottom = 0;
export let left = 0;
export let right = 0;
let intersecting = false;
let container;
onMount(() => {
if (typeof IntersectionObserver !== 'undefined') {
const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;
const observer = new IntersectionObserver(entries => {
intersecting = entries[0].isIntersecting;
if (intersecting && once) {
observer.unobserve(container);
}
}, {
rootMargin
});
observer.observe(container);
return () => observer.unobserve(container);
}
function handler() {
const bcr = container.getBoundingClientRect();
intersecting = (
(bcr.bottom + bottom) > 0 &&
(bcr.right + right) > 0 &&
(bcr.top - top) < window.innerHeight &&
(bcr.left - left) < window.innerWidth
);
if (intersecting && once) {
window.removeEventListener('scroll', handler);
}
}
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
});
</script>
<style>
div {
width: 100%;
height: 100%;
}
</style>
<div bind:this={container}>
<slot {intersecting}></slot>
</div>

@ -1,11 +0,0 @@
<script>
import { onMount } from 'svelte';
let constructor;
onMount(async () => {
constructor = await $$props.this();
});
</script>
<svelte:component this={constructor} {...$$props}/>

@ -1,63 +0,0 @@
<script>
import { onMount } from 'svelte';
let p = 0;
let visible = false;
onMount(() => {
function next() {
visible = true;
p += 0.1;
const remaining = 1 - p;
if (remaining > 0.15) setTimeout(next, 500 / remaining);
}
setTimeout(next, 250);
});
</script>
<style>
.progress-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
z-index: 999;
}
.progress {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: var(--prime);
transition: width 0.4s;
}
.fade {
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(255,255,255,0.3);
pointer-events: none;
z-index: 998;
animation: fade 0.4s;
}
@keyframes fade {
from { opacity: 0 }
to { opacity: 1 }
}
</style>
{#if visible}
<div class="progress-container">
<div class="progress" style="width: {p * 100}%"></div>
</div>
{/if}
{#if p >= 0.4}
<div class="fade"></div>
{/if}

@ -1,27 +0,0 @@
<script>
export let checked;
</script>
<style>
.input-output-toggle {
display: grid;
user-select: none;
flex: 0;
grid-template-columns: 1fr 40px 1fr;
grid-gap: 0.5em;
align-items: center;
width: 100%;
height: 42px;
border-top: 1px solid var(--second);
}
input { display: block }
span { color: #ccc }
.active { color: #555 }
</style>
<label class="input-output-toggle">
<span class:active={!checked} style="text-align: right">input</span>
<input type="checkbox" bind:checked>
<span class:active={checked}>output</span>
</label>

@ -1,136 +0,0 @@
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { browser } from '$app/env';
import { process_example } from '../../utils/examples';
import InputOutputToggle from './InputOutputToggle.svelte';
export let version = '3';
export let gist = null;
export let example = null;
export let embedded = false;
let repl;
let name = 'loading...';
let width = browser
? window.innerWidth - 32
: 1000;
let checked = false;
onMount(() => {
if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version}/package.json`)
.then(r => r.json())
.then(pkg => {
version = pkg.version;
});
}
if (gist) {
fetch(`repl/${gist}.json`).then(r => r.json()).then(data => {
const { description, files } = data;
name = description;
const components = Object.keys(files)
.map(file => {
const dot = file.lastIndexOf('.');
if (!~dot) return;
const source = files[file].content;
return {
name: file.slice(0, dot),
type: file.slice(dot + 1),
source
};
})
.filter(x => x.type === 'svelte' || x.type === 'js')
.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
return a.name < b.name ? -1 : 1;
});
repl.set({ components });
});
} else if (example) {
fetch(`examples/${example}.json`).then(async response => {
if (response.ok) {
const data = await response.json();
repl.set({
components: process_example(data.files)
});
}
});
}
});
$: if (embedded) document.title = `${name} • Svelte REPL`;
$: svelteUrl = browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
$: mobile = width < 560;
</script>
<style>
.repl-outer {
position: relative;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--back);
overflow: hidden;
box-sizing: border-box;
--pane-controls-h: 4.2rem;
}
.viewport {
width: 100%;
height: 100%;
flex: 1;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
}
.mobile .offset {
transform: translate(-50%, 0);
}
</style>
<div class="repl-outer" bind:clientWidth={width} class:mobile>
<div class="viewport" class:offset={checked}>
{#if browser}
<Repl
bind:this={repl}
workersUrl="workers"
fixed={mobile}
{svelteUrl}
{rollupUrl}
embedded
relaxed
/>
{/if}
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
</div>

@ -1,46 +0,0 @@
<script>
export let labels;
export let offset = 0;
</script>
<style>
.toggle {
position: fixed;
bottom: 0;
width: 100%;
height: 4.6rem;
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid var(--second);
background-color: white;
}
button {
margin: 0 .15em;
width: 4em;
height: 1em;
padding: .3em .4em;
border-radius: var(--border-r);
line-height: 1em;
box-sizing: content-box;
color: #888;
border: 1px solid var(--back-light);
}
.selected {
background-color: var(--prime);
color: white;
}
</style>
<div class="toggle">
{#each labels as label, index}
<button
class:selected={offset === index}
on:click={() => offset = index}
>
{label}
</button>
{/each}
</div>

@ -1,5 +0,0 @@
// REPL props
export const svelteUrl = `https://unpkg.com/svelte@3`;
export const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
export const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = '${import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}';`;

@ -1,31 +0,0 @@
// need to do this first before importing database, etc.
import dotenv from 'dotenv';
dotenv.config();
import * as cookie from 'cookie';
import { get_user, sanitize_user } from './utils/auth';
import { query } from './utils/db';
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
if (process.env['PGHOST']) {
// this is a convenient time to clear out expired sessions
query('delete from sessions where expiry < now()');
request.locals.cookies = cookie.parse(request.headers.cookie || '');
request.locals.user = await get_user(request.locals.cookies.sid);
}
const response = await resolve(request);
return response;
}
/** @type {import('@sveltejs/kit').GetSession} */
export function getSession(request) {
return request.locals.user
? {
user: sanitize_user(request.locals.user)
}
: {};
}

@ -1,78 +0,0 @@
<script context="module">
/** @type {import('@sveltejs/kit').ErrorLoad} */
export function load({ error, status }) {
return {
props: { error, status }
};
}
</script>
<script>
export let status;
export let error;
// we don't want to use <svelte:window bind:online> here,
// because we only care about the online state when
// the page first loads
const online = typeof navigator !== 'undefined'
? navigator.onLine
: true;
</script>
<style>
.container {
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
}
h1, p { margin: 0 auto }
h1 {
font-size: 2.8em;
font-weight: 300;
margin: 0 0 0.5em 0;
}
p { margin: 1em auto }
.error {
background-color: var(--second);
color: white;
padding: 12px 16px;
font: 600 16px/1.7 var(--font);
border-radius: 2px;
}
/* @media (min-width: 480px) {
h1 { font-size: 4em }
} */
</style>
<svelte:head>
<title>{status}</title>
</svelte:head>
<div class="container">
{#if online}
<h1>Yikes!</h1>
{#if error.message}
<p class="error">{status}: {error.message}</p>
{:else}
<p class="error">Encountered a {status} error</p>
{/if}
{#if import.meta.env.DEV && error.stack}
<pre>{error.stack}</pre>
{:else}
{#if status >= 500}
<p>Please try reloading the page.</p>
{/if}
<p>If the error persists, please drop by the <a rel="external" href="chat">Discord chatroom</a> and let us know, or raise an issue on <a href="https://github.com/sveltejs/svelte">GitHub</a>. Thanks!</p>
{/if}
{:else}
<h1>It looks like you're offline</h1>
<p>Reload the page once you've found the internet.</p>
{/if}
</div>

@ -1,70 +0,0 @@
<script>
import '@sveltejs/site-kit/base.css';
import { setContext } from 'svelte';
import { page, navigating, session } from '$app/stores';
import { Icon, Icons, Nav, NavItem } from '@sveltejs/site-kit';
import PreloadingIndicator from '../components/PreloadingIndicator.svelte';
export let segment;
setContext('app', {
login: () => {
const login_window = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
window.addEventListener('message', function handler(event) {
login_window.close();
window.removeEventListener('message', handler);
$session.user = event.data.user;
});
},
logout: async () => {
const r = await fetch(`/auth/logout`, {
credentials: 'include'
});
if (r.ok) $session.user = null;
}
});
</script>
<Icons/>
{#if $navigating && $navigating.to}
<PreloadingIndicator/>
{/if}
{#if $page.path !== '/repl/embed'}
<Nav {segment} {page} logo="svelte-logo-horizontal.svg">
<NavItem segment="tutorial">Tutorial</NavItem>
<NavItem segment="docs">Docs</NavItem>
<NavItem segment="examples">Examples</NavItem>
<NavItem segment="repl">REPL</NavItem>
<NavItem segment="blog">Blog</NavItem>
<NavItem segment="faq">FAQ</NavItem>
<NavItem external="https://kit.svelte.dev">SvelteKit</NavItem>
<NavItem external="chat" title="Discord Chat">
<Icon name="message-square"/>
</NavItem>
<NavItem external="https://github.com/sveltejs/svelte" title="GitHub Repo">
<Icon name="github"/>
</NavItem>
</Nav>
{/if}
<main>
<slot></slot>
</main>
<style>
main {
position: relative;
margin: 0 auto;
/* padding: var(--nav-h) var(--side-nav) 0 var(--side-nav); */
padding: var(--nav-h) 0 0 0;
overflow-x: hidden;
}
</style>

@ -1,34 +0,0 @@
<script>
import contributors from '../_contributors.js';
</script>
<style>
#contributors {
margin: 0.4em 0;
}
.contributor {
width: 4.2em;
height: 4.2em;
border-radius: 50%;
text-indent: -9999px;
display: inline-block;
background: no-repeat url(/contributors.jpg);
background-size: auto 102%;
margin: 0 1.5em 1.5em 0;
border: 2px solid var(--second);
}
</style>
<div id="contributors">
{#each contributors as contributor, i}
<a
class="contributor"
style="background-position: {(100 * i) / (contributors.length - 1)}% 0"
href="https://github.com/{contributor}">
{contributor}
</a>
{/each}
</div>
<p>And so <a href="https://github.com/sveltejs/svelte/graphs/contributors">many more →</a></p>

@ -1,35 +0,0 @@
<script>
import donors from '../_donors.js';
</script>
<style>
#donors {
margin: 0.4em 0;
}
.donor {
width: 4.2em;
height: 4.2em;
border-radius: 50%;
text-indent: -9999px;
display: inline-block;
background: no-repeat url(/donors.jpg);
background-size: auto 102%;
margin: 0 1.5em 1.5em 0;
border: 2px solid var(--second);
}
</style>
<div id="donors">
{#each donors as donor, i}
<a
class="donor"
style="background-position: {(100 * i) / (donors.length - 1)}% 0"
alt="{donor}"
href="https://opencollective.com/svelte">
{donor}
</a>
{/each}
</div>
<p>And so <a href="https://opencollective.com/svelte">many more →</a></p>

@ -1,52 +0,0 @@
<script>
import { Section } from '@sveltejs/site-kit';
import IntersectionObserver from '../../components/IntersectionObserver.svelte';
import ReplWidget from '../../components/Repl/ReplWidget.svelte';
export let id;
</script>
<style>
.example {
width: 100%;
}
.example :global(a) {
color: inherit;
}
.example > :global(p) {
margin: 4.4rem 2.4rem 2.4rem 0;
}
.repl-container {
width: 100%;
height: 420px;
border-radius: var(--border-r);
overflow: hidden;
}
@media (min-width: 920px) {
.example {
display: grid;
grid-template-columns: 1fr 3fr;
grid-gap: 0.5em;
align-items: start;
}
}
</style>
<Section>
<div class="example">
<slot></slot>
<div class="repl-container">
<IntersectionObserver once let:intersecting top={400}>
{#if intersecting}
<!-- <Lazy this={loadReplWidget} example={id}/> -->
<ReplWidget example={id}/>
{/if}
</IntersectionObserver>
</div>
</div>
</Section>

@ -1,80 +0,0 @@
export const companies = [
{
href: "https://1password.com",
filename: "1password.svg",
alt: "1Password logo",
},
{
href: "https://www.alaskaair.com/",
style: "background-color: #01426a;",
filename: "alaskaairlines.svg",
alt: "Alaska Airlines logo",
},
{
href: "https://avast.com",
filename: "avast.svg",
alt: "Avast logo",
},
{
href: "https://chess.com",
style: "background-color: #312e2b;",
filename: "chess.svg",
alt: "Chess.com logo",
},
{
href: "https://fusioncharts.com",
filename: "fusioncharts.svg",
alt: "FusionCharts logo",
},
{
href: "https://godaddy.com",
filename: "godaddy.svg",
alt: "GoDaddy logo",
},
{
href: "https://www.ibm.com/",
filename: "ibm.svg",
alt: "IBM logo",
},
{
href: "https://media.lesechos.fr/infographie",
filename: "les-echos.svg",
alt: "Les Echos",
},
{
href: "https://www.philips.co.uk",
filename: "philips.svg",
alt: "Philips logo",
},
{
href: "https://global.rakuten.com/corp/",
filename: "rakuten.svg",
alt: "Rakuten logo",
},
{
href: "https://razorpay.com",
filename: "razorpay.svg",
alt: "Razorpay logo",
},
{
href: "https://www.se.com",
style: " background-color: #3dcd58; ",
filename: "Schneider_Electric.svg",
alt: "Schneider Electric",
},
{
href: "https://squareup.com",
filename: "square.svg",
alt: "Square",
},
{
href: "https://nytimes.com",
filename: "nyt.svg",
alt: "The New York Times logo",
},
{
href: "https://transloadit.com",
filename: "transloadit.svg",
alt: "Transloadit",
},
];

@ -1,66 +0,0 @@
<script>
import { companies } from './WhosUsingSvelte.js';
const randomizer = ({ prominent }) => Math.random();
const doSort = (a, b) => randomizer(b) - randomizer(a);
const sortedCompanies = companies.sort(doSort);
</script>
<div class="logos">
{#each sortedCompanies as { href, filename, alt, style, picture, span }, index}
<a target="_blank" rel="noopener" {href} style={style || ""}>
{#if picture}
<picture>
{#each picture as { type, srcset }}
<source {type} {srcset} />
{/each}
<img src="/whos-using-svelte/{filename}" {alt} loading="lazy" />
</picture>
{:else}
<img src="/whos-using-svelte/{filename}" {alt} loading="lazy" />
{#if span}
<span>{span}</span>
{/if}
{/if}
</a>
{/each}
</div>
<style>
.logos {
margin: 1em 0 0 0;
display: flex;
flex-wrap: wrap;
}
a {
height: 40px;
margin: 0 0.5em 0.5em 0;
display: flex;
align-items: center;
border: 2px solid var(--second);
padding: 0;
border-radius: 20px;
color: var(--text);
}
picture,
img {
height: 100%;
padding: 5px 10px;
transition: transform 0.2s;
min-width: 0; /* Avoid image overflow in Safari */
}
picture:hover,
img:hover {
transform: scale(1.2);
}
@media (min-width: 540px) {
a {
height: 60px;
border-radius: 30px;
}
picture,
img {
padding: 10px 20px;
}
}
</style>

@ -1,25 +0,0 @@
import { query as db_query } from '../../utils/db';
export async function get({ query, locals }) {
if (locals.user) {
const page_size = 100;
const offset = query.get('offset') ? parseInt(query.get('offset')) : 0;
const rows = await db_query(`
select g.uid, g.name, coalesce(g.updated_at, g.created_at) as updated_at
from gists g
where g.user_id = $1
order by id desc
limit ${page_size + 1}
offset $2
`, [locals.user.id, offset]);
rows.forEach(row => {
row.uid = row.uid.replace(/-/g, '');
});
const more = rows.length > page_size;
return { body: { apps: rows.slice(0, page_size), offset: more ? offset + page_size : null }};
} else {
return { status: 401 };
}
}

@ -1,140 +0,0 @@
<script context="module">
export async function load({ fetch, page, session: { user }}) {
let apps = [];
let offset = null;
if (user) {
let url = 'apps.json';
if (page.query.get('offset')) {
url += `?offset=${encodeURIComponent(page.query.get('offset'))}`;
}
const r = await fetch(url, {
credentials: 'include'
});
if (!r.ok) return { status: r.status, body: await r.text() };
({ apps, offset } = await r.json());
}
return { props: { user, apps, offset }};
}
</script>
<script>
import { getContext } from 'svelte';
export let user;
export let apps;
export let offset;
const { login, logout } = getContext('app');
const formatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
const format = str => formatter.format(new Date(str));
</script>
<svelte:head>
<title>Your apps • Svelte</title>
</svelte:head>
<div class="apps">
{#if user}
<header>
<h1>Your apps</h1>
<div class="user">
<img class="avatar" alt="{user.name || user.username} avatar" src="{user.avatar}">
<span>
{user.name || user.username}
(<a on:click|preventDefault={logout} href="auth/logout">log out</a>)
</span>
</div>
</header>
<ul>
{#each apps as app}
<li>
<a href="repl/{app.uid}">
<h2>{app.name}</h2>
<span>updated {format(app.updated_at)}</span>
</a>
</li>
{/each}
</ul>
{#if offset !== null}
<div><a href="apps?offset={offset}">Next page...</a></div>
{/if}
{:else}
<p>Please <a on:click|preventDefault={login} href="auth/login">log in</a> to see your saved apps.</p>
{/if}
</div>
<style>
.apps {
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
max-width: var(--main-width);
margin: 0 auto;
}
header {
margin: 0 0 1em 0;
}
h1 {
font-size: 4rem;
font-weight: 400;
}
.user {
display: flex;
padding: 0 0 0 3.2rem;
position: relative;
margin: 1rem 0 5rem 0;
color: var(--text);
}
.avatar {
position: absolute;
left: 0;
top: 0.1rem;
width: 2.4rem;
height: 2.4rem;
border: 1px solid rgba(0,0,0,0.3);
border-radius: 0.2rem;
}
ul {
list-style: none;
}
li {
margin: 0 0 1em 0;
}
h2 {
color: var(--text);
font-size: var(--h3);
font-weight: 400;
}
li a {
border: none;
}
li a:hover h2 {
color: var(--flash);
}
li span {
font-size: 14px;
color: #999;
}
</style>

@ -1,6 +0,0 @@
export const oauth = 'https://github.com/login/oauth';
export const baseurl = process.env['BASEURL'];
export const secure = baseurl && baseurl.startsWith('https:');
export const client_id = process.env['GITHUB_CLIENT_ID'];
export const client_secret = process.env['GITHUB_CLIENT_SECRET'];

@ -1,54 +0,0 @@
import devalue from 'devalue';
import * as cookie from 'cookie';
import * as httpie from 'httpie';
import { parse, stringify } from 'querystring';
import { sanitize_user, create_user, create_session } from '../../utils/auth';
import { oauth, secure, client_id, client_secret } from './_config.js';
export async function get({ query }) {
try {
// Trade "code" for "access_token"
const r1 = await httpie.post(`${oauth}/access_token?` + stringify({
code: query.get('code'),
client_id,
client_secret,
}));
// Now fetch User details
const { access_token } = parse(r1.data);
const r2 = await httpie.get('https://api.github.com/user', {
headers: {
'User-Agent': 'svelte.dev',
Authorization: `token ${access_token}`
}
});
const user = await create_user(r2.data, access_token);
const session = await create_session(user);
return {
headers: {
'Set-Cookie': cookie.serialize('sid', session.uid, {
maxAge: 31536000,
path: '/',
httpOnly: true,
secure
}),
'Content-Type': 'text/html; charset=utf-8'
},
body: `
<script>
window.opener.postMessage({
user: ${devalue(sanitize_user(user))}
}, window.location.origin);
</script>
`
};
} catch (err) {
console.error('GET /auth/callback', err);
return {
status: 500,
body: err.data
};
}
}

@ -1,35 +0,0 @@
import { stringify } from 'querystring';
import { oauth, baseurl, client_id } from './_config.js';
export const get = client_id
? () => {
const Location = `${oauth}/authorize?` + stringify({
scope: 'read:user',
client_id,
redirect_uri: `${baseurl}/auth/callback`,
});
return {
status: 302,
headers: {
Location
}
};
}
: () => {
return {
status: 500,
headers: {
'Content-Type': 'text/html; charset=utf-8'
},
body: `
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`
};
};

@ -1,18 +0,0 @@
import * as cookie from 'cookie';
import { secure } from './_config.js';
import { delete_session } from '../../utils/auth.js';
export async function get(request) {
await delete_session(request.locals.cookies.sid);
return {
headers: {
'Set-Cookie': cookie.serialize('sid', '', {
maxAge: -1,
path: '/',
httpOnly: true,
secure
})
}
};
}

@ -1,23 +0,0 @@
import get_posts from './_posts.js';
let lookup;
export function get({ params }) {
if (!lookup || process.env.NODE_ENV !== 'production') {
lookup = new Map();
get_posts().forEach(post => {
lookup.set(post.slug, post);
});
}
const post = lookup.get(params.slug);
if (post) {
return {
body: post,
headers: {
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
}
};
}
}

@ -1,177 +0,0 @@
<script context="module">
export async function load({ fetch, page: { params } }) {
const res = await fetch(`/blog/${params.slug}.json`);
if (res.ok) {
return { props: { post: await res.json() }};
}
}
</script>
<script>
export let post;
</script>
<svelte:head>
<title>{post.metadata.title}</title>
<meta name="twitter:title" content={post.metadata.title}>
<meta name="twitter:description" content={post.metadata.description}>
<meta name="Description" content={post.metadata.description}>
</svelte:head>
<article class='post listify'>
<h1>{post.metadata.title}</h1>
<p class='standfirst'>{post.metadata.description}</p>
<p class='byline'><a href='{post.metadata.authorURL}'>{post.metadata.author}</a> <time datetime='{post.metadata.pubdate}'>{post.metadata.dateString}</time></p>
{@html post.html}
</article>
<style>
.post {
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
max-width: var(--main-width);
margin: 0 auto;
}
h1 {
font-size: 4rem;
font-weight: 400;
}
.standfirst {
font-size: var(--h4);
color: var(--second);
margin: 0 0 1em 0;
}
.byline {
margin: 0 0 6rem 0;
padding: 1.6rem 0 0 0;
border-top: var(--border-w) solid #6767785b;
font-size: var(--h6);
text-transform: uppercase;
}
.byline a {
/* border-bottom: none; */
/* font-weight: 600; */
}
.byline a:hover {
/* border-bottom: 2px solid var(--prime); */
}
.post h1 {
color: var(--second);
max-width: 20em;
margin: 0 0 .8rem 0;
}
.post :global(h2) {
margin: 2em 0 0.5em 0;
/* color: var(--second); */
color: var(--text);
font-size: var(--h3);
font-weight: 300;
}
.post :global(figure) {
margin: 1.6rem 0 3.2rem 0;
}
.post :global(figure) :global(img) {
max-width: 100%;
}
.post :global(figcaption) {
color: var(--second);
text-align: left;
}
.post :global(video) {
width: 100%;
}
.post :global(blockquote) {
max-width: none;
border-left: 4px solid #eee;
background: #f9f9f9;
border-radius: 0 var(--border-r) var(--border-r) 0;
}
.post :global(code) {
padding: .3rem .8rem .3rem;
margin: 0 0.2rem;
top: -.1rem;
background: var(--back-api);
}
.post :global(pre) :global(code) {
padding: 0;
margin: 0;
top: 0;
background: transparent;
}
.post :global(aside) {
float: right;
margin: 0 0 1em 1em;
width: 16rem;
color: var(--second);
z-index: 2;
}
.post :global(.max) {
width: 100%;
}
.post :global(iframe) {
width: 100%;
height: 420px;
margin: 2em 0;
border-radius: var(--border-r);
border: 0.8rem solid var(--second);
}
.post :global(.anchor) {
top: calc((var(--h3) - 24px) / 2);
}
.post :global(a) {
padding: 0;
transition: none;
}
.post :global(a):not(:hover) {
border: none;
}
@media (max-width: 768px) {
.post :global(.anchor) {
transform: scale(0.6);
opacity: 1;
left: -1.0em;
}
}
@media (min-width: 910px) {
.post :global(.max) {
width: calc(100vw - 2 * var(--side-nav));
margin: 0 calc(var(--main-width) / 2 - 50vw);
text-align: center;
}
.post :global(.max) > :global(*) {
width: 100%;
max-width: 1200px;
}
.post :global(iframe) {
width: 100%;
max-width: 1100px;
margin: 2em auto;
}
}
</style>

@ -1,59 +0,0 @@
import fs from 'fs';
import path from 'path';
import { extract_frontmatter, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
import { marked } from 'marked';
import { makeSlugProcessor } from '../../utils/slug';
import { highlight } from '../../utils/highlight';
import { SLUG_PRESERVE_UNICODE } from '../../../config';
const makeSlug = makeSlugProcessor(SLUG_PRESERVE_UNICODE);
export default function get_posts() {
return fs
.readdirSync('content/blog')
.map(file => {
if (path.extname(file) !== '.md') return;
const match = /^(\d+-\d+-\d+)-(.+)\.md$/.exec(file);
if (!match) throw new Error(`Invalid filename '${file}'`);
const [, pubdate, slug] = match;
const markdown = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { content, metadata } = extract_frontmatter(markdown);
const date = new Date(`${pubdate} EDT`); // cheeky hack
metadata.pubdate = pubdate;
metadata.dateString = date.toDateString();
const renderer = new marked.Renderer();
renderer.link = link_renderer;
renderer.code = highlight;
renderer.heading = (text, level, rawtext) => {
const fragment = makeSlug(rawtext);
return `
<h${level}>
<span id="${fragment}" class="offset-anchor"></span>
<a href="blog/${slug}#${fragment}" class="anchor" aria-hidden="true"></a>
${text}
</h${level}>`;
};
const html = marked(
content.replace(/^\t+/gm, match => match.split('\t').join(' ')),
{ renderer }
);
return {
html,
metadata,
slug
};
})
.sort((a, b) => a.metadata.pubdate < b.metadata.pubdate ? 1 : -1);
}

@ -1,26 +0,0 @@
import get_posts from './_posts.js';
let json;
export function get() {
if (!json || process.env.NODE_ENV !== 'production') {
const posts = get_posts()
.filter(post => !post.metadata.draft)
.map(post => {
return {
slug: post.slug,
metadata: post.metadata
};
});
json = JSON.stringify(posts);
}
return {
body: json,
headers: {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
}
}
}

@ -1,94 +0,0 @@
<script context="module">
export async function load({ fetch }) {
const posts = await fetch(`blog.json`).then(r => r.json());
return {
props: {
posts
}
};
}
</script>
<script>
export let posts;
</script>
<svelte:head>
<title>Blog • Svelte</title>
<link rel="alternate" type="application/rss+xml" title="Svelte blog" href="https://svelte.dev/blog/rss.xml">
<meta name="twitter:title" content="Svelte blog">
<meta name="twitter:description" content="Articles about Svelte and UI development">
<meta name="Description" content="Articles about Svelte and UI development">
</svelte:head>
<h1 class="visually-hidden">Blog</h1>
<div class='posts stretch'>
{#each posts as post}
<article class='post' data-pubdate={post.metadata.dateString}>
<a class="no-underline" sveltekit:prefetch href='blog/{post.slug}' title='Read the article »'>
<h2>{post.metadata.title}</h2>
<p>{post.metadata.description}</p>
</a>
</article>
{/each}
</div>
<style>
.posts {
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
min-height: calc(100vh - var(--nav-h));
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
max-width: var(--main-width);
margin: 0 auto;
}
h2 {
display: inline-block;
margin: 3.2rem 0 0.4rem 0;
color: var(--text);
max-width: 18em;
font-size: var(--h3);
font-weight: 400;
}
.post:first-child {
margin: 0 0 2rem 0;
padding: 0 0 4rem 0;
border-bottom: var(--border-w) solid #6767785b; /* based on --second */
}
.post:first-child h2 {
font-size: 4rem;
font-weight: 400;
color: var(--second);
}
.post:first-child::before,
.post:nth-child(2)::before {
content: 'Latest post • ' attr(data-pubdate);
color: var(--flash);
font-size: var(--h6);
font-weight: 400;
letter-spacing: .05em;
text-transform: uppercase;
}
.post:nth-child(2)::before {
content: 'Older posts';
}
.post p {
font-size: var(--h5);
max-width: 30em;
color: var(--second);
}
.post > a { display: block }
.posts a:hover,
.posts a:hover > h2 {
color: var(--flash)
}
</style>

@ -1,56 +0,0 @@
import get_posts from '../blog/_posts.js';
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
function formatPubdate(str) {
const [y, m, d] = str.split('-');
return `${d} ${months[+m]} ${y} 12:00 +0000`;
}
function escapeHTML(html) {
const chars = {
'"' : 'quot',
"'": '#39',
'&': 'amp',
'<' : 'lt',
'>' : 'gt'
};
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
}
const rss = `
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Svelte blog</title>
<link>https://svelte.dev/blog</link>
<description>News and information about the magical disappearing UI framework</description>
<image>
<url>https://svelte.dev/favicon.png</url>
<title>Svelte</title>
<link>https://svelte.dev/blog</link>
</image>
${get_posts().filter(post => !post.metadata.draft).map(post => `
<item>
<title>${escapeHTML(post.metadata.title)}</title>
<link>https://svelte.dev/blog/${post.slug}</link>
<description>${escapeHTML(post.metadata.description)}</description>
<pubDate>${formatPubdate(post.metadata.pubdate)}</pubDate>
</item>
`).join('')}
</channel>
</rss>
`.replace(/>[^\S]+/gm, '>').replace(/[^\S]+</gm, '<').trim();
export function get() {
return {
body: rss,
headers: {
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
'Content-Type': 'application/rss+xml'
}
};
}

@ -1,6 +0,0 @@
export function get() {
return {
status: 302,
headers: { Location: 'https://discord.gg/yy75DKs' },
};
}

@ -1,135 +0,0 @@
import fs from 'fs';
import path from 'path';
import { SLUG_PRESERVE_UNICODE, SLUG_SEPARATOR } from '../../../config';
import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
import { make_session_slug_processor } from '@sveltejs/site-kit/utils/slug';
import { highlight } from '../../utils/highlight';
import { marked } from 'marked';
const blockTypes = [
'blockquote',
'html',
'heading',
'hr',
'list',
'listitem',
'paragraph',
'table',
'tablerow',
'tablecell'
];
export default function() {
const make_slug = make_session_slug_processor({
preserve_unicode: SLUG_PRESERVE_UNICODE,
separator: SLUG_SEPARATOR
});
return fs
.readdirSync(`content/docs`)
.filter(file => file[0] !== '.' && path.extname(file) === '.md')
.map(file => {
const markdown = fs.readFileSync(`content/docs/${file}`, 'utf-8');
const { content, metadata } = extract_frontmatter(markdown);
const section_slug = make_slug(metadata.title);
const subsections = [];
const renderer = new marked.Renderer();
let block_open = false;
renderer.link = link_renderer;
renderer.hr = () => {
block_open = true;
return '<div class="side-by-side"><div class="copy">';
};
renderer.code = (source, lang) => {
source = source.replace(/^ +/gm, match =>
match.split(' ').join('\t')
);
const lines = source.split('\n');
const meta = extract_metadata(lines[0], lang);
let prefix = '';
let className = 'code-block';
if (meta) {
source = lines.slice(1).join('\n');
const filename = meta.filename || (lang === 'html' && 'App.svelte');
if (filename) {
prefix = `<span class='filename'>${prefix} ${filename}</span>`;
className += ' named';
}
}
if (meta && meta.hidden) return '';
const html = `<div class='${className}'>${prefix}${highlight(source, lang)}</div>`;
if (block_open) {
block_open = false;
return `</div><div class="code">${html}</div></div>`;
}
return html;
};
renderer.heading = (text, level, rawtext) => {
let slug;
const match = /<a href="([^"]+)"[^>]*>(.+)<\/a>/.exec(text);
if (match) {
slug = match[1];
text = match[2];
} else {
slug = make_slug(rawtext);
}
if (level === 3 || level === 4) {
const title = text
.replace(/<\/?code>/g, '')
.replace(/\.(\w+)(\((.+)?\))?/, (m, $1, $2, $3) => {
if ($3) return `.${$1}(...)`;
if ($2) return `.${$1}()`;
return `.${$1}`;
});
subsections.push({ slug, title, level });
}
return `
<h${level}>
<span id="${slug}" class="offset-anchor" ${level > 4 ? 'data-scrollignore' : ''}></span>
<a href="docs#${slug}" class="anchor" aria-hidden="true"></a>
${text}
</h${level}>`;
};
blockTypes.forEach(type => {
const fn = renderer[type];
renderer[type] = function() {
return fn.apply(this, arguments);
};
});
const html = marked(content, { renderer });
const hashes = {};
return {
html: html.replace(/@@(\d+)/g, (m, id) => hashes[id] || m),
metadata,
subsections,
slug: section_slug,
file,
};
});
}

@ -1,13 +0,0 @@
import get_sections from './_sections.js';
let json;
export function get() {
if (!json || process.env.NODE_ENV !== 'production') {
json = get_sections();
}
return {
body: json
};
}

@ -1,44 +0,0 @@
<script context="module">
const title_replacements = {
'1_export_creates_a_component_prop': 'props',
'2_Assignments_are_reactive': 'reactive assignments',
'3_$_marks_a_statement_as_reactive': 'reactive statements ($:)',
'4_Prefix_stores_with_$_to_access_their_values': 'accessing stores ($)'
};
export async function load({ fetch }) {
const sections = await fetch(`docs.json`).then(r => r.json());
for (const section of sections) {
for (const subsection of section.subsections) {
const { slug } = subsection;
// show abbreviated title in the table of contents
if (slug in title_replacements) {
subsection.title = title_replacements[slug];
}
}
}
return {
props: {
sections
}
};
}
</script>
<script>
import { Docs } from '@sveltejs/site-kit';
export let sections;
</script>
<svelte:head>
<title>API Docs • Svelte</title>
<meta name="twitter:title" content="Svelte API docs">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Cybernetically enhanced web apps">
</svelte:head>
<h1 class="visually-hidden">API Docs</h1>
<Docs {sections}/>

@ -1,20 +0,0 @@
import { get_example } from './_examples.js';
const cache = new Map();
export function get({ params }) {
const { slug } = params;
let example = cache.get(slug);
if (!example || process.env.NODE_ENV !== 'production') {
example = get_example(slug);
if (example) cache.set(slug, example);
}
if (example) {
return {
body: example
};
}
}

@ -1,116 +0,0 @@
<script>
export let sections = [];
export let active_section = null;
export let isLoading = false;
</script>
<style>
.examples-toc {
overflow-y: auto;
height: 100%;
border-right: 1px solid var(--second);
background-color: var(--second);
color: white;
padding: 3rem 3rem 0 3rem;
}
.examples-toc li {
display: block;
line-height: 1.2;
margin: 0 0 4.8rem 0;
}
.section-title {
display: block;
padding: 0 0 0.8rem 0;
font: 400 var(--h6) var(--font);
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
}
div {
display: flex;
flex-direction: row;
padding: 0.2rem 3rem;
margin: 0 -3rem;
}
div.active {
background: rgba(0, 0, 0, 0.15) calc(100% - 3rem) 47% no-repeat
url(/icons/arrow-right.svg);
background-size: 1em 1em;
color: white;
}
div.active.loading {
background: rgba(0, 0, 0, 0.1) calc(100% - 3rem) 47% no-repeat
url(/icons/loading.svg);
background-size: 1em 1em;
color: white;
}
a {
display: flex;
flex: 1 1 auto;
position: relative;
color: var(--sidebar-text);
border-bottom: none;
font-size: 1.6rem;
align-items: center;
justify-content: start;
padding: 0;
}
a:hover {
color: white;
}
.repl-link {
flex: 0 1 auto;
font-size: 1.2rem;
font-weight: 700;
margin-right: 2.5rem;
}
.thumbnail {
background-color: white;
object-fit: contain;
width: 5rem;
height: 5rem;
border-radius: 2px;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.13);
margin: 0.2em 0.5em 0.2em 0;
}
</style>
<ul class="examples-toc">
{#each sections as section}
<li>
<span class="section-title">{section.title}</span>
{#each section.examples as example}
<div
class="row"
class:active={example.slug === active_section}
class:loading={isLoading}>
<a
href="examples#{example.slug}"
class="row"
class:active={example.slug === active_section}
class:loading={isLoading}>
<img
class="thumbnail"
alt="{example.title} thumbnail"
src="examples/thumbnails/{example.slug}.jpg" />
<span>{example.title}</span>
</a>
{#if example.slug === active_section}
<a href="repl/{example.slug}" class="repl-link">REPL</a>
{/if}
</div>
{/each}
</li>
{/each}
</ul>

@ -1,50 +0,0 @@
import fs from 'fs';
let lookup;
const titles = new Map();
export function get_examples() {
lookup = new Map();
return fs.readdirSync(`content/examples`).map(group_dir => {
const metadata = JSON.parse(fs.readFileSync(`content/examples/${group_dir}/meta.json`, 'utf-8'));
return {
title: metadata.title,
examples: fs.readdirSync(`content/examples/${group_dir}`).filter(file => file !== 'meta.json').map(example_dir => {
const slug = example_dir.replace(/^\d+-/, '');
if (lookup.has(slug)) throw new Error(`Duplicate example slug "${slug}"`);
lookup.set(slug, `${group_dir}/${example_dir}`);
const metadata = JSON.parse(fs.readFileSync(`content/examples/${group_dir}/${example_dir}/meta.json`, 'utf-8'));
titles.set(slug, metadata.title);
return {
slug,
title: metadata.title
};
})
};
});
}
export function get_example(slug) {
if (!lookup || !lookup.has(slug)) get_examples();
const dir = lookup.get(slug);
const title = titles.get(slug);
if (!dir || !title) return null;
const files = fs.readdirSync(`content/examples/${dir}`)
.filter(name => name[0] !== '.' && name !== 'meta.json')
.map(name => {
return {
name,
source: fs.readFileSync(`content/examples/${dir}/${name}`, 'utf-8')
};
});
return { title, files };
}

@ -1,22 +0,0 @@
import { get_examples } from './_examples.js';
let cached;
export function get() {
if (!cached || process.env.NODE_ENV !== 'production') {
cached = get_examples().filter(section => section.title);
}
try {
return {
body: cached
};
} catch(err) {
return {
status: e.status || 500,
body: {
message: e.message
}
};
}
}

@ -1,183 +0,0 @@
<!-- FIXME sometimes it adds a trailing slash when landing -->
<script context="module">
export async function load({ fetch }) {
const sections = await fetch(`examples.json`).then(r => r.json());
const title_by_slug = sections.reduce((acc, {examples}) => {
examples.forEach(({slug, title}) => {
acc[slug] = title;
});
return acc;
}, {});
return {
props: {
sections,
title_by_slug
}
};
}
</script>
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import Repl from '@sveltejs/svelte-repl';
import ScreenToggle from '../../components/ScreenToggle.svelte';
import {
mapbox_setup, // see site/content/examples/15-context/00-context-api
rollupUrl,
svelteUrl
} from '../../config';
import { process_example } from '../../utils/examples';
import { getFragment } from '@sveltejs/site-kit/utils/navigation';
import TableOfContents from './_TableOfContents.svelte';
export let sections;
export let title_by_slug;
let active_slug;
let width;
let offset = 1;
let repl;
let isLoading = false;
const cache = {};
function showExampleCodeOnChange() {
offset = 1;
}
$: title = title_by_slug[active_slug] || '';
$: first_slug = sections[0].examples[0].slug;
$: mobile = width < 768; // note: same as per media query below
$: replOrientation = (mobile || width > 1080) ? 'columns' : 'rows';
$: if (repl && active_slug) {
if (active_slug in cache) {
repl.set({ components: cache[active_slug] });
showExampleCodeOnChange();
} else {
isLoading = true;
fetch(`examples/${active_slug}.json`)
.then(async response => {
if (response.ok) {
const {files} = await response.json();
return process_example(files);
}
})
.then(components => {
cache[active_slug] = components;
repl.set({components});
showExampleCodeOnChange();
isLoading = false;
})
.catch(() => {
isLoading = false;
});
}
}
onMount(() => {
const onhashchange = () => {
active_slug = getFragment();
};
window.addEventListener('hashchange', onhashchange, false);
const fragment = getFragment();
if (fragment) {
active_slug = fragment;
} else {
active_slug = first_slug;
goto(`examples#${active_slug}`);
}
return () => {
window.removeEventListener('hashchange', onhashchange, false);
};
});
</script>
<svelte:head>
<title>{title} {title ? '•' : ''} Svelte Examples</title>
<meta name="twitter:title" content="Svelte examples">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Interactive example Svelte apps">
</svelte:head>
<h1 class="visually-hidden">Examples</h1>
<div class='examples-container' bind:clientWidth={width}>
<div class="viewport offset-{offset}">
<TableOfContents {sections} active_section={active_slug} {isLoading} />
<div class="repl-container" class:loading={isLoading}>
<Repl
bind:this={repl}
workersUrl="workers"
{svelteUrl}
{rollupUrl}
orientation={replOrientation}
fixed={mobile}
relaxed
injectedJS={mapbox_setup}
/>
</div>
</div>
{#if mobile}
<ScreenToggle bind:offset labels={['index', 'input', 'output']}/>
{/if}
</div>
<style>
.examples-container {
position: relative;
height: calc(100vh - var(--nav-h));
overflow: hidden;
padding: 0 0 42px 0;
box-sizing: border-box;
}
.viewport {
display: grid;
width: 300%;
height: 100%;
grid-template-columns: 33.333% 66.666%;
transition: transform .3s;
grid-auto-rows: 100%;
}
.repl-container.loading {
opacity: 0.6;
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.repl-container :global(.tab-content),
.repl-container :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.repl-container :global(.tab-content) {
visibility: hidden;
}
.repl-container :global(.tab-content.visible) {
visibility: visible;
}
.offset-1 { transform: translate(-33.333%, 0); }
.offset-2 { transform: translate(-66.666%, 0); }
@media (min-width: 768px) {
.examples-container { padding: 0 }
.viewport {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: var(--sidebar-mid-w) auto;
grid-auto-rows: 100%;
transition: none;
}
.offset-1, .offset-2 { transform: none; }
}
</style>

@ -1,58 +0,0 @@
import fs from 'fs';
import path from 'path';
import { extract_frontmatter, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
import { marked } from 'marked';
import { makeSlugProcessor } from '../../utils/slug';
import { highlight } from '../../utils/highlight';
import { SLUG_PRESERVE_UNICODE } from '../../../config';
const makeSlug = makeSlugProcessor(SLUG_PRESERVE_UNICODE);
export default function get_faqs() {
return fs
.readdirSync('content/faq')
.map(file => {
if (path.extname(file) !== '.md') return;
const match = /^([0-9]+)-(.+)\.md$/.exec(file);
if (!match) throw new Error(`Invalid filename '${file}'`);
const [, order, slug] = match;
const markdown = fs.readFileSync(`content/faq/${file}`, 'utf-8');
const { content, metadata } = extract_frontmatter(markdown);
const renderer = new marked.Renderer();
renderer.link = link_renderer;
renderer.code = highlight;
renderer.heading = (text, level, rawtext) => {
const fragment = makeSlug(rawtext);
return `
<h${level}>
<span id="${fragment}" class="offset-anchor"></span>
<a href="faq#${fragment}" class="anchor" aria-hidden="true"></a>
${text}
</h${level}>`;
};
const answer = marked(
content.replace(/^\t+/gm, match => match.split('\t').join(' ')),
{ renderer }
);
const fragment = makeSlug(slug);
return {
fragment,
order,
answer,
metadata
};
})
.sort((a, b) => a.order - b.order);
}

@ -1,26 +0,0 @@
import get_faqs from './_faqs.js';
let json;
export function get() {
if (!json || process.env.NODE_ENV !== 'production') {
const faqs = get_faqs()
.map(faq => {
return {
fragment: faq.fragment,
answer: faq.answer,
metadata: faq.metadata
};
});
json = JSON.stringify(faqs);
}
return {
body: json,
headers: {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
}
};
}

@ -1,93 +0,0 @@
<script context="module">
export async function load({ fetch }) {
const faqs = await fetch(`faq.json`).then(r => r.json());
return {
props: {
faqs
}
};
}
</script>
<script>
const description = "Frequently Asked Questions about Svelte"
export let faqs;
</script>
<svelte:head>
<title>Frequently Asked Questions • Svelte</title>
<meta name="twitter:title" content="Svelte FAQ">
<meta name="twitter:description" content={description}>
<meta name="Description" content={description}>
</svelte:head>
<div class='faqs stretch'>
<h1>Frequently Asked Questions</h1>
{#each faqs as faq}
<article class='faq'>
<h2>
<span id={faq.fragment} class="offset-anchor"></span>
<a class="anchor" href='faq#{faq.fragment}' title='{faq.question}'>&nbsp;</a>
{faq.metadata.question}
</h2>
<p>{@html faq.answer}</p>
</article>
{/each}
</div>
<style>
.faqs {
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
min-height: calc(100vh - var(--nav-h));
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
max-width: var(--main-width);
margin: 0 auto;
}
h2 {
display: inline-block;
margin: 3.2rem 0 1rem 0;
color: var(--text);
max-width: 18em;
font-size: var(--h3);
font-weight: 400;
}
.faq:first-child {
margin: 0 0 2rem 0;
padding: 0 0 4rem 0;
border-bottom: var(--border-w) solid #6767785b; /* based on --second */
}
.faq:first-child h2 {
font-size: 4rem;
font-weight: 400;
color: var(--second);
}
.faq p {
font-size: var(--h5);
max-width: 30em;
color: var(--second);
}
:global(.faqs .faq ul) {
margin-left: 3.2rem;
}
.faqs :global(.anchor) {
top: calc((var(--h3) - 24px) / 2);
}
@media (max-width: 768px) {
.faqs :global(.anchor) {
transform: scale(0.6);
opacity: 1;
left: -1.0em;
}
}
</style>

@ -1,132 +0,0 @@
<script>
import { Blurb, Hero, Section } from '@sveltejs/site-kit';
import Contributors from './_components/Contributors.svelte';
import Donors from './_components/Donors.svelte';
import Example from './_components/Example.svelte';
import WhosUsingSvelte from './_components/WhosUsingSvelte.svelte';
// import Lazy from '../components/Lazy.svelte';
// TODO this causes a Sapper CSS bug...
// function loadReplWidget() {
// console.log('lazy loading');
// return import('../components/Repl/ReplWidget.svelte').then(mod => mod.default);
// }
</script>
<style>
/* darken text for accessibility */
/* TODO does this belong elsewhere? */
:global(.back-light) {
--text: hsl(36, 3%, 44%);
}
.examples {
background: var(--second);
color: white;
overflow: hidden;
}
</style>
<svelte:head>
<title>Svelte • Cybernetically enhanced web apps</title>
<meta name="twitter:title" content="Svelte">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Cybernetically enhanced web apps">
</svelte:head>
<h1 class="visually-hidden">Svelte</h1>
<Hero
title="Svelte"
tagline="Cybernetically enhanced web apps"
outline="svelte-logo-outline.svg"
logotype="svelte-logotype.svg"
/>
<Blurb>
<a href="blog/write-less-code" slot="one">
<h2>Write less code</h2>
<p>Build boilerplate-free components using languages you already know — HTML, CSS and JavaScript</p>
<span class="learn-more">learn more</span>
</a>
<a href="blog/virtual-dom-is-pure-overhead" slot="two">
<h2>No virtual DOM</h2>
<p>Svelte compiles your code to tiny, framework-less vanilla JS — your app starts fast and stays fast</p>
<span class="learn-more">learn more</span>
</a>
<a href="blog/svelte-3-rethinking-reactivity" slot="three">
<h2>Truly reactive</h2>
<p>No more complex state management libraries — Svelte brings reactivity to JavaScript itself</p>
<span class="learn-more">learn more</span>
</a>
<div class="description" slot="what">
<p>Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the <em>browser</em>, Svelte shifts that work into a <em>compile step</em> that happens when you build your app.</p>
<p>Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.</p>
<p>We're proud that Svelte was recently voted the <a href="https://insights.stackoverflow.com/survey/2021#section-most-loved-dreaded-and-wanted-web-frameworks">most loved web framework</a> with the <a href="https://2020.stateofjs.com/en-US/technologies/front-end-frameworks/">most satisfied developers</a> in a pair of industry surveys. We think you'll love it too. <a href="blog/svelte-3-rethinking-reactivity">Read the introductory blog post</a> to learn more.</p>
</div>
<div style="grid-area: start; display: flex; flex-direction: column; min-width: 0" slot="how">
<pre class="language-bash" style="margin: 0 0 1em 0; min-width: 0; min-height: 0">
npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project
<span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span>
cd my-svelte-project
<span class="token comment"># to use <a href="blog/svelte-and-typescript">TypeScript</a> run:</span>
<span class="token comment"># node scripts/setupTypeScript.js</span>
npm install
npm run dev
</pre>
<p style="flex: 1">See the <a href="blog/the-easiest-way-to-get-started">quickstart guide</a> for more information.</p>
<p class="cta"><a sveltekit:prefetch href="tutorial">Learn Svelte</a></p>
</div>
</Blurb>
<div class="examples">
<Example id="hello-world">
<p>Svelte components are built on top of HTML. Just add data.</p>
</Example>
<Example id="nested-components">
<p>CSS is component-scoped by default — no more style collisions or specificity wars. Or you can <a href="/blog/svelte-css-in-js">use your favourite CSS-in-JS library</a>.</p>
</Example>
<Example id="reactive-assignments">
<p>Trigger efficient, granular updates by assigning to local variables. The compiler does the rest.</p>
</Example>
<Example id="svg-transitions">
<p>Build beautiful UIs with a powerful, performant transition engine built right into the framework.</p>
</Example>
</div>
<Section>
<h3>Who's using Svelte?</h3>
<WhosUsingSvelte/>
</Section>
<Section>
<h3>Supporters</h3>
<p>Svelte is free and open source software, made possible by the work of hundreds of volunteers and donors. <a href="https://github.com/sveltejs/svelte">Join us</a> or <a href="https://opencollective.com/svelte">give</a>!</p>
<h4>Contributors</h4>
<Contributors/>
<p></p>
<h4>Donors</h4>
<Donors/>
</Section>

@ -1,114 +0,0 @@
<script>
import { getContext } from 'svelte';
import { session } from '$app/stores';
const { logout } = getContext('app');
let showMenu = false;
let name;
$: name = $session.user.name || $session.user.username;
</script>
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
<span>{name}</span>
<img alt="{name} avatar" src="{$session.user.avatar}">
{#if showMenu}
<div class="menu">
<a href="apps">Your saved apps</a>
<button on:click={logout}>Log out</button>
</div>
{/if}
</div>
<style>
.user {
position: relative;
display: inline-block;
padding: 0em 1.2rem 0 1.6rem;
height: 0.8em;
line-height: 1;
z-index: 99;
}
.user::after {
/* embiggen hit zone, so log out menu doesn't disappear */
position: absolute;
content: '';
width: 100%;
height: 3.2rem;
left: 0;
top: 0;
}
span {
/* position: relative; padding: 0 2em 0 0; */
line-height: 1;
display: none;
font-family: var(--font);
font-size: 1.6rem;
opacity: 0.7;
}
.user:hover span {
opacity: 1;
}
img {
position: absolute;
top: -0.05em;
right: 0;
width: 2.1rem;
height: 2.1rem;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 0.2rem;
}
.menu {
position: absolute;
width: calc(100% + 1.6rem);
min-width: 10em;
top: 3rem;
right: -1.6rem;
background-color: var(--second);
padding: 0.8rem 1.6rem;
z-index: 99;
text-align: left;
border-radius: 0.4rem;
display: flex;
flex-direction: column;
}
.menu button, .menu a {
background-color: transparent;
font-family: var(--font);
font-size: 1.6rem;
opacity: 0.7;
padding: 0.4rem 0;
text-decoration: none;
text-align: left;
border: none;
color: inherit;
}
.menu button:hover, .menu a:hover {
opacity: 1;
color: inherit;
}
@media (min-width: 600px) {
.user {
padding: 0em 3.2rem 0 1.6rem;
}
img {
width: 2.4rem;
height: 2.4rem;
}
span {
display: inline-block;
}
}
</style>

@ -1,315 +0,0 @@
<script>
import { createEventDispatcher, getContext } from 'svelte';
import { session } from '$app/stores';
import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit';
import * as doNotZip from 'do-not-zip';
import downloadBlob from '../../../_utils/downloadBlob.js';
import { enter } from '../../../../../utils/events.js';
import { isMac } from '../../../../../utils/compat.js';
const dispatch = createEventDispatcher();
const { login } = getContext('app');
export let repl;
export let gist;
export let name;
export let zen_mode;
export let modified_count;
let saving = false;
let downloading = false;
let justSaved = false;
let justForked = false;
function wait(ms) {
return new Promise(f => setTimeout(f, ms));
}
$: canSave = $session.user && gist && gist.owner === $session.user.uid;
function handleKeydown(event) {
if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
save();
}
}
async function fork(intentWasSave) {
saving = true;
const { components } = repl.toJSON();
try {
const r = await fetch(`repl/create.json`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
const gist = await r.json();
dispatch('forked', { gist });
modified_count = 0;
repl.markSaved();
if (intentWasSave) {
justSaved = true;
await wait(600);
justSaved = false;
} else {
justForked = true;
await wait(600);
justForked = false;
}
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function save() {
if (saving) return;
if (!canSave) {
fork(true);
return;
}
saving = true;
try {
// Send all files back to API
// ~> Any missing files are considered deleted!
const { components } = repl.toJSON();
const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
await r.json();
modified_count = 0;
repl.markSaved();
justSaved = true;
await wait(600);
justSaved = false;
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function download() {
downloading = true;
const { components, imports } = repl.toJSON();
const files = await (await fetch('/svelte-app.json')).json();
if (imports.length > 0) {
const idx = files.findIndex(({ path }) => path === 'package.json');
const pkg = JSON.parse(files[idx].data);
const { devDependencies } = pkg;
imports.forEach(mod => {
const match = /^(@[^/]+\/)?[^@/]+/.exec(mod);
devDependencies[match[0]] = 'latest';
});
pkg.devDependencies = devDependencies;
files[idx].data = JSON.stringify(pkg, null, ' ');
}
files.push(...components.map(component => ({ path: `src/${component.name}.${component.type}`, data: component.source })));
files.push({
path: `src/main.js`, data: `import App from './App.svelte';
var app = new App({
target: document.body
});
export default app;` });
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
downloading = false;
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app-controls">
<input
bind:value={name}
on:focus="{e => e.target.select()}"
use:enter="{e => e.target.blur()}"
>
<div style="text-align: right; margin-right:.4rem">
<button class="icon" on:click="{() => zen_mode = !zen_mode}" title="fullscreen editor">
{#if zen_mode}
<Icon name="close" />
{:else}
<Icon name="maximize" />
{/if}
</button>
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
<Icon name="download" />
</button>
<button class="icon" disabled="{saving || !$session.user}" on:click={() => fork(false)} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
<Icon name="git-branch" />
{/if}
</button>
<button class="icon" disabled="{saving || !$session.user}" on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
<Icon name="save" />
{#if modified_count}
<div class="badge">{modified_count}</div>
{/if}
{/if}
</button>
{#if $session.user}
<UserMenu/>
{:else}
<button class="icon" on:click|preventDefault={login}>
<Icon name="log-in" />
<span>&nbsp;Log in to save</span>
</button>
{/if}
</div>
</div>
<style>
.app-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: var(--app-controls-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: .6rem var(--side-nav);
background-color: var(--second);
color: white;
white-space: nowrap;
flex: 0;
}
.icon {
position: relative;
top: -0.1rem;
display: inline-block;
padding: 0.2em;
opacity: .7;
transition: opacity .3s;
font-family: var(--font);
font-size: 1.6rem;
color: white;
/* width: 1.6em;
height: 1.6em; */
line-height: 1;
margin: 0 0 0 0.2em;
}
.icon:hover { opacity: 1 }
.icon:disabled { opacity: .3 }
.icon[title^='fullscreen'] { display: none }
input {
background: transparent;
border: none;
color: currentColor;
font-family: var(--font);
font-size: 1.6rem;
opacity: 0.7;
outline: none;
flex: 1;
margin: 0 0.2em 0 .4rem;
padding-top: 0.2em;
border-bottom: 1px solid transparent;
}
input:hover {
border-bottom: 1px solid currentColor;
opacity: 1;
}
input:focus {
border-bottom: 1px solid currentColor;
opacity: 1;
}
button span {
display: none;
}
.badge {
background: #ff3e00;
border-radius: 100%;
font-size: 10px;
padding: 0;
width: 15px;
height: 15px;
line-height: 15px;
position: absolute;
top: 10px;
right: 0px;
}
@media (min-width: 600px) {
.icon[title^='fullscreen'] { display: inline }
button span {
display: inline-block;
}
}
</style>

@ -1,173 +0,0 @@
import * as httpie from 'httpie';
import { query, find } from '../../../utils/db';
import { get_example } from '../../examples/_examples.js';
const GITHUB_CLIENT_ID = process.env['GITHUB_CLIENT_ID'];
const GITHUB_CLIENT_SECRET = process.env['GITHUB_CLIENT_SECRET'];
export async function get({ path, params }) {
// is this an example?
const example = get_example(params.id);
if (example) {
return {
body: {
relaxed: true,
uid: params.id,
name: example.title,
files: example.files,
owner: null
}
};
}
if (process.env.NODE_ENV === 'development') {
// In dev, proxy requests to load particular REPLs to the real server.
// This avoids needing to connect to the real database server.
try {
const res_proxy = await httpie.get(`https://svelte.dev${path}`);
return {
body: res_proxy.data,
status: res_proxy.statusCode,
headers: res_proxy.headers
};
} catch (err) {
return {
status: err.statusCode,
body: { error: err.message }
};
}
}
const [row] = await query(`
select g.*, u.uid as owner from gists g
left join users u on g.user_id = u.id
where g.uid = $1 limit 1
`, [params.id]); // via filename pattern
if (!row) {
const base = `https://api.github.com/gists/${params.id}`;
const url = `${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}`;
try {
const { data } = await httpie.get(url, {
headers: {
'User-Agent': 'https://svelte.dev'
}
});
// create owner if necessary...
let user = await find(`select * from users where uid = $1`, [data.owner.id]);
if (!user) {
const { id, name, login, avatar_url } = data.owner;
user = await find(`
insert into users(uid, name, username, avatar)
values ($1, $2, $3, $4)
returning *
`, [id, name, login, avatar_url]);
}
delete data.files['README.md'];
delete data.files['meta.json'];
const files = Object.keys(data.files).map(key => {
const name = key.replace(/\.html$/, '.svelte');
return {
name,
source: data.files[key].content
};
});
// add gist to database...
await query(`
insert into gists(uid, user_id, name, files)
values ($1, $2, $3, $4)
`, [params.id, user.id, data.description, JSON.stringify(files)]);
return {
body: {
uid: params.id,
name: data.description,
files,
owner: data.owner.id
}
};
} catch (err) {
return {
status: err.statusCode,
body: { error: err.message }
};
}
}
return {
body: {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: row.owner
}
};
}
export async function patch({ params, locals, body }) {
const { user } = locals;
if (!user) return;
let id;
const uid = params.id;
try {
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
if (!row) {
return {
status: 404,
body: {
error: 'Gist not found'
}
};
}
if (row.user_id !== user.id) {
return { status: 403, body: { error: 'Item does not belong to you' }};
}
id = row.id;
} catch (err) {
console.error('PATCH /gists @ select', err);
return { status: 500 };
}
try {
const obj = body;
obj.updated_at = 'now()';
let k;
const cols = [];
const vals = [];
for (k in obj) {
cols.push(k);
vals.push(k === 'files' ? JSON.stringify(obj[k]) : obj[k]);
}
const tmp = vals.map((x, i) => `$${i + 1}`).join(',');
const set = `set (${cols.join(',')}) = (${tmp})`;
const [row] = await query(`update gists ${set} where id = ${id} returning *`, vals);
return {
body: {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,
}
};
} catch (err) {
console.error('PATCH /gists @ update', err);
return {
status: 500,
body: { error: err.message }
};
}
}

@ -1,221 +0,0 @@
<script context="module">
export function load({ page: { params, query }}) {
return {
props: {
version: query.get('version') || '3',
id: params.id
}
};
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { browser } from '$app/env';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { mapbox_setup } from '../../../config';
import InputOutputToggle from '../../../components/Repl/InputOutputToggle.svelte';
import AppControls from './_components/AppControls/index.svelte';
export let version;
export let id;
let repl;
let gist;
let name = 'Loading...';
let zen_mode = false;
let is_relaxed_gist = false;
let width = browser ? window.innerWidth : 1000;
let checked = false;
let modified_count = 0;
function update_query_string(version) {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
const url = params.length > 0
? `/repl/${id}?${params.join('&')}`
: `/repl/${id}`;
history.replaceState({}, 'x', url);
}
$: if (typeof history !== 'undefined') update_query_string(version);
function fetch_gist(id) {
if (gist && gist.uid === id) {
// if the id changed because we just forked, don't refetch
return;
}
// TODO handle `relaxed` logic
fetch(`/repl/${id}.json`).then(r => {
if (r.ok) {
r.json().then(data => {
gist = data;
name = data.name;
is_relaxed_gist = data.relaxed;
const components = data.files.map(file => {
const dot = file.name.lastIndexOf(".");
let name = file.name.slice(0, dot);
let type = file.name.slice(dot + 1);
if (type === 'html') type = 'svelte'; // TODO do this on the server
return { name, type, source: file.source };
});
components.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
return a.name < b.name ? -1 : 1;
});
repl.set({ components });
});
} else {
console.warn('TODO: 404 Gist');
}
});
}
$: if (browser) fetch_gist(id);
onMount(() => {
if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version || '3'}/package.json`)
.then(r => r.json())
.then(pkg => {
version = pkg.version;
});
}
});
function handle_fork(event) {
console.log('> handle_fork', event);
gist = event.detail.gist;
goto(`/repl/${gist.uid}?version=${version}`);
}
function handle_change(event) {
modified_count = event.detail.components.filter(c => c.modified).length;
}
$: svelteUrl = browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
$: mobile = width < 540;
$: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner);
</script>
<style>
.repl-outer {
position: relative;
height: calc(100vh - var(--nav-h));
--app-controls-h: 5.6rem;
--pane-controls-h: 4.2rem;
overflow: hidden;
background-color: var(--back);
padding: var(--app-controls-h) 0 0 0;
/* margin: 0 calc(var(--side-nav) * -1); */
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.viewport {
width: 100%;
height: 100%;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
flex: 1;
}
.mobile .offset {
transform: translate(-50%, 0);
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.viewport :global(.tab-content),
.viewport :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.viewport :global(.tab-content) {
visibility: hidden;
}
.viewport :global(.tab-content.visible) {
visibility: visible;
}
.zen-mode {
position: fixed;
width: 100%;
height: 100%;
top: 0;
z-index: 111;
}
@keyframes fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
</style>
<svelte:head>
<title>{name} • REPL • Svelte</title>
<meta name="twitter:title" content="Svelte REPL">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Interactive Svelte playground">
</svelte:head>
<svelte:window bind:innerWidth={width}/>
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}" class:mobile>
<AppControls
{gist}
{repl}
bind:name
bind:zen_mode
bind:modified_count
on:forked={handle_fork}
/>
{#if browser}
<div class="viewport" class:offset={checked}>
<Repl
bind:this={repl}
workersUrl="workers"
{svelteUrl}
{rollupUrl}
{relaxed}
fixed={mobile}
injectedJS={mapbox_setup}
on:change={handle_change}
on:add={handle_change}
on:remove={handle_change}
/>
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
{/if}
</div>

@ -1,19 +0,0 @@
export default function body(req) {
return new Promise((fulfil, reject) => {
let str = '';
req.on('error', reject);
req.on('data', chunk => {
str += chunk;
});
req.on('end', () => {
try {
fulfil(JSON.parse(str));
} catch (err) {
reject(err);
}
});
});
}

@ -1,11 +0,0 @@
export default (blob, filename) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
link.remove();
};

@ -1,31 +0,0 @@
import { query } from '../../utils/db';
export async function post({ locals, body }) {
const { user } = locals;
if (!user) return; // response already sent
try {
const { name, files } = body;
const [row] = await query(`
insert into gists(user_id, name, files)
values ($1, $2, $3) returning *`, [user.id, name, JSON.stringify(files)]);
return {
status: 201,
body: {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,
}
};
} catch (err) {
return {
status: 500,
body: {
error: err.message
}
};
}
}

@ -1,50 +0,0 @@
<script context="module">
export function load({ page: { query }}) {
return {
props: {
version: query.get('version') || '3',
gist: query.get('gist'),
example: query.get('example')
}
};
}
</script>
<script>
import { browser } from '$app/env';
import ReplWidget from '../../components/Repl/ReplWidget.svelte';
export let version;
export let gist;
export let example;
</script>
<style>
.repl-outer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--back);
overflow: hidden;
box-sizing: border-box;
--pane-controls-h: 4.2rem;
display: flex;
flex-direction: column;
}
</style>
<svelte:head>
<title>REPL • Svelte</title>
<meta name="twitter:title" content="Svelte REPL">
<meta name="twitter:description" content="Cybernetically enhanced web apps">
<meta name="Description" content="Interactive Svelte playground">
</svelte:head>
<div class="repl-outer">
{#if browser}
<ReplWidget {version} {gist} {example} embedded={true}/>
{/if}
</div>

@ -1,23 +0,0 @@
<script context="module">
export function load({ page: { query } }) {
const gist = query.get('gist');
const example = query.get('example');
const version = query.get('version');
// redirect to v2 REPL if appropriate
if (/^[^>]?[12]/.test(version)) {
return {
status: 302,
redirect: `https://v2.svelte.dev/repl?${query}`
};
}
const id = gist || example || 'hello-world';
const q = version ? `?version=${version}` : ``;
return {
status: 301,
redirect: `/repl/${id}${q}`
};
}
</script>

@ -1,11 +0,0 @@
import { readFileSync } from 'fs';
export function get({ params: { path } }) {
if (process.env.NODE_ENV !== 'development' || ('/' + path).includes('/.')) {
return { status: 403 };
}
return {
headers: { 'Content-Type': 'text/javascript' },
body: readFileSync('../' + path)
};
}

@ -1,94 +0,0 @@
<script>
import { goto } from '$app/navigation';
import { Icon } from '@sveltejs/site-kit';
export let sections;
export let slug;
export let selected;
function navigate(e) {
goto(`tutorial/${e.target.value}`);
}
</script>
<style>
nav {
display: grid;
grid-template-columns: 2.5em 1fr 2.5em;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
div {
position: relative;
padding: 1em 0.5em;
font-weight: 300;
font-size: var(--h6);
color: white;
}
a {
display: block;
padding: 0.7em 0;
text-align: center;
opacity: 0.75;
color: white;
}
a:hover {
opacity: 1;
}
a.disabled, a.disabled:hover, a.disabled:active {
color: white;
opacity: 0.3;
}
span {
white-space: nowrap;
position: relative;
top: 0.3em;
}
strong { opacity: 0.7 }
select {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0.0001;
cursor: pointer;
-webkit-appearance: none;
}
</style>
<nav>
<a sveltekit:prefetch aria-label="Previous tutorial step" class="no-underline" href="tutorial/{(selected.prev || selected).slug}" class:disabled={!selected.prev}>
<Icon name="arrow-left" />
</a>
<div>
<span>
<strong>
<span style="position: relative; top: -0.1em; margin: 0 0.5em 0 0"><Icon name="menu"/></span>
{selected.section.title} /
</strong>
{selected.chapter.title}
</span>
<select value={slug} on:change={navigate}>
{#each sections as section, i}
<optgroup label="{i + 1}. {section.title}">
{#each section.chapters as chapter, i}
<option value={chapter.slug}>{String.fromCharCode(i + 97)}. {chapter.title}</option>
{/each}
</optgroup>
{/each}
</select>
</div>
<a sveltekit:prefetch aria-label="Next tutorial step" class="no-underline" href="tutorial/{(selected.next || selected).slug}" class:disabled={!selected.next}>
<Icon name="arrow-right" />
</a>
</nav>

@ -1,101 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { marked } from 'marked';
import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown';
import { highlight } from '../../../utils/highlight';
const cache = new Map();
function find_tutorial(slug) {
const sections = fs.readdirSync(`content/tutorial`);
for (const section of sections) {
const chapters = fs.readdirSync(`content/tutorial/${section}`).filter(dir => /^\d+/.test(dir));
for (const chapter of chapters) {
if (slug === chapter.replace(/^\d+-/, '')) {
return { section, chapter };
}
}
}
}
function get_tutorial(slug) {
const found = find_tutorial(slug);
if (!found) return found;
const dir = `content/tutorial/${found.section}/${found.chapter}`;
const markdown = fs.readFileSync(`${dir}/text.md`, 'utf-8');
const app_a = fs.readdirSync(`${dir}/app-a`);
const app_b = fs.existsSync(`${dir}/app-b`) && fs.readdirSync(`${dir}/app-b`);
const { content } = extract_frontmatter(markdown);
const renderer = new marked.Renderer();
renderer.link = link_renderer;
renderer.code = (source, lang) => {
source = source.replace(/^ +/gm, match =>
match.split(' ').join('\t')
);
const lines = source.split('\n');
const meta = extract_metadata(lines[0], lang);
let prefix = '';
let className = 'code-block';
if (meta) {
source = lines.slice(1).join('\n');
const filename = meta.filename || (lang === 'html' && 'App.svelte');
if (filename) {
prefix = `<span class='filename'>${prefix} ${filename}</span>`;
className += ' named';
}
}
return `<div class='${className}'>${prefix}${highlight(source, lang)}</div>`;
};
let html = marked(content, { renderer });
if (found.chapter.startsWith('01')) {
const meta = JSON.parse(fs.readFileSync(`content/tutorial/${found.section}/meta.json`));
html = `<h2>${meta.title}</h2>\n${html}`;
}
function get_file(stage, file) {
const ext = path.extname(file);
const name = file.slice(0, -ext.length);
const type = ext.slice(1);
return {
name,
type,
source: fs.readFileSync(`${dir}/${stage}/${file}`, 'utf-8')
};
}
return {
html,
app_a: app_a.map(file => get_file('app-a', file)),
app_b: app_b && app_b.map(file => get_file('app-b', file))
};
}
export function get({ params }) {
const { slug } = params;
let tut = cache.get(slug);
if (!tut || process.env.NODE_ENV !== 'production') {
tut = get_tutorial(slug);
cache.set(slug, tut);
}
if (tut) {
return {
body: tut
};
}
}

@ -1,323 +0,0 @@
<script context="module">
export async function load({ fetch, page: { params }}) {
const res = await fetch(`/tutorial/${params.slug}.json`);
if (!res.ok) {
return {
status: 301,
redirect: '/tutorial/basics'
}
}
return {
props: {
slug: params.slug,
chapter: await res.json()
}
};
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { getContext } from 'svelte';
import { browser } from '$app/env';
import ScreenToggle from '../../../components/ScreenToggle.svelte';
import TableOfContents from './_TableOfContents.svelte';
import {
mapbox_setup, // needed for context API tutorial
rollupUrl,
svelteUrl
} from '../../../config';
export let slug;
export let chapter;
const { sections } = getContext('tutorial');
let repl;
let prev;
let scrollable;
const lookup = new Map();
let width = browser ? window.innerWidth : 1000;
let offset = 0;
sections.forEach(section => {
section.chapters.forEach(chapter => {
const obj = {
slug: chapter.slug,
section,
chapter,
prev
};
lookup.set(chapter.slug, obj);
if (browser) { // pending https://github.com/sveltejs/svelte/issues/2135
if (prev) prev.next = obj;
prev = obj;
}
});
});
// TODO is there a non-hacky way to trigger scroll when chapter changes?
$: if (scrollable) chapter, scrollable.scrollTo(0, 0);
// TODO: this will need to be changed to the master branch, and probably should be dynamic instead of included
// here statically
const tutorial_repo_link = 'https://github.com/sveltejs/svelte/tree/master/site/content/tutorial';
$: selected = lookup.get(slug);
$: improve_link = `${tutorial_repo_link}/${selected.chapter.section_dir}/${selected.chapter.chapter_dir}`;
const clone = file => ({
name: file.name,
type: file.type,
source: file.source
});
$: if (repl) {
completed = false;
repl.set({
components: chapter.app_a.map(clone)
});
}
$: mobile = width < 768;
function reset() {
repl.update({
components: chapter.app_a.map(clone)
});
}
function complete() {
repl.update({
components: chapter.app_b.map(clone)
});
}
let completed = false;
function handle_change(event) {
completed = event.detail.components.every((file, i) => {
const expected = chapter.app_b[i];
return expected && (
file.name === expected.name &&
file.type === expected.type &&
file.source.trim().replace(/\s+$/gm, '') === expected.source.trim().replace(/\s+$/gm, '')
);
});
}
</script>
<style>
.tutorial-outer {
position: relative;
height: calc(100vh - var(--nav-h));
overflow: hidden;
padding: 0 0 42px 0;
box-sizing: border-box;
}
.viewport {
display: grid;
width: 300%;
height: 100%;
grid-template-columns: 33.333% 66.666%;
transition: transform .3s;
grid-auto-rows: 100%;
}
.offset-1 { transform: translate(-33.333%, 0); }
.offset-2 { transform: translate(-66.666%, 0); }
@media (min-width: 768px) {
.tutorial-outer { padding: 0 }
.viewport {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: minmax(33.333%, var(--sidebar-large-w)) auto;
grid-auto-rows: 100%;
transition: none;
}
.offset-1, .offset-2 { transform: none; }
}
.tutorial-text {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--second);
background-color: var(--second);
color: var(--sidebar-text);
}
.chapter-markup {
padding: 3.2rem 4rem;
overflow: auto;
flex: 1;
height: 0;
}
.chapter-markup :global(h2) {
margin: 4rem 0 1.6rem 0;
font-size: var(--h3);
line-height: 1;
font-weight: 400;
color: white;
}
.chapter-markup :global(h2:first-child) {
margin-top: .4rem;
}
.chapter-markup :global(a) {
color: var(--sidebar-text);
}
.chapter-markup :global(a:hover) {
color: white;
}
.chapter-markup :global(ul) {
padding: 0 0 0 2em;
}
.chapter-markup :global(blockquote) {
background-color: rgba(0,0,0,.17);
color: var(--sidebar-text);
}
.chapter-markup::-webkit-scrollbar {
background-color: var(--second);
width: 8px;
}
.chapter-markup::-webkit-scrollbar-thumb {
background-color: rgba(255,255,255,.7);
border-radius: 1em;
}
.chapter-markup :global(p) > :global(code),
.chapter-markup :global(ul) :global(code) {
color: var(--sidebar-text);
background: rgba(0,0,0,.12);
padding: .2em .4em .3em;
white-space: nowrap;
position: relative;
top: -0.1em;
}
.controls {
border-top: 1px solid rgba(255,255,255,.15);
padding: 1em 0 0 0;
display: flex;
}
.show {
background: var(--prime);
padding: .3em .7em;
border-radius: var(--border-r);
top: .1em;
position: relative;
font-size: var(--h5);
font-weight: 300;
color: rgba(255,255,255,0.7);
}
.show:hover {
color: white;
}
a.next {
padding-right: 1.2em;
background: no-repeat 100% 50% url(/icons/arrow-right.svg);
background-size: 1em 1em;
margin-left: auto;
}
.improve-chapter {
padding: 1em 0 .5em 0;
}
.improve-chapter a {
font-size: 14px;
text-decoration: none;
opacity: .3;
padding: 0 .1em 0 1.2em;
background: no-repeat 0 50% url(/icons/edit.svg);
background-size: 1em 1em;
}
.improve-chapter a:hover {
opacity: 1;
}
</style>
<svelte:head>
<title>{selected.section.title} / {selected.chapter.title} • Svelte Tutorial</title>
<meta name="twitter:title" content="Svelte tutorial">
<meta name="twitter:description" content="{selected.section.title} / {selected.chapter.title}">
<meta name="Description" content="{selected.section.title} / {selected.chapter.title}">
</svelte:head>
<svelte:window bind:innerWidth={width}/>
<div class="tutorial-outer">
<div class="viewport offset-{offset}">
<div class="tutorial-text">
<div class="table-of-contents">
<TableOfContents {sections} {slug} {selected}/>
</div>
<div class="chapter-markup" bind:this={scrollable}>
{@html chapter.html}
<div class="controls">
{#if chapter.app_b}
<!-- TODO disable this button when the contents of the REPL
matches the expected end result -->
<button class="show" on:click="{() => completed ? reset() : complete()}">
{completed ? 'Reset' : 'Show me'}
</button>
{/if}
{#if selected.next}
<a class="next" href="tutorial/{selected.next.slug}">Next</a>
{/if}
</div>
<div class="improve-chapter">
<a class="no-underline" href={improve_link}>Edit this chapter</a>
</div>
</div>
</div>
<div class="tutorial-repl">
<Repl
bind:this={repl}
workersUrl="workers"
{svelteUrl}
{rollupUrl}
orientation={mobile ? 'columns' : 'rows'}
fixed={mobile}
on:change={handle_change}
injectedJS={mapbox_setup}
relaxed
/>
</div>
</div>
{#if mobile}
<ScreenToggle bind:offset labels={['tutorial', 'input', 'output']}/>
{/if}
</div>

@ -1,19 +0,0 @@
<script context="module">
export async function load({ fetch }) {
const sections = await fetch(`/tutorial.json`).then(r => r.json());
return {
props: {
sections
}
};
}
</script>
<script>
import { setContext } from 'svelte';
export let sections;
setContext('tutorial', { sections });
</script>
<slot></slot>

@ -1,67 +0,0 @@
import * as fs from 'fs';
import { extract_frontmatter } from '@sveltejs/site-kit/utils/markdown';
let json;
function get_sections() {
const slugs = new Set();
const sections = fs.readdirSync(`content/tutorial`)
.filter(dir => /^\d+/.test(dir))
.map(dir => {
let meta;
try {
meta = JSON.parse(fs.readFileSync(`content/tutorial/${dir}/meta.json`, 'utf-8'));
} catch (err) {
throw new Error(`Error reading metadata for ${dir}`);
}
return {
title: meta.title,
chapters: fs.readdirSync(`content/tutorial/${dir}`)
.filter(dir => /^\d+/.test(dir))
.map(tutorial => {
try {
const md = fs.readFileSync(`content/tutorial/${dir}/${tutorial}/text.md`, 'utf-8');
const { metadata } = extract_frontmatter(md);
const slug = tutorial.replace(/^\d+-/, '');
if (slugs.has(slug)) throw new Error(`Duplicate slug: ${slug}`);
slugs.add(slug);
return {
slug,
title: metadata.title,
section_dir: dir,
chapter_dir: tutorial,
};
} catch (err) {
throw new Error(`Error building tutorial ${dir}/${tutorial}: ${err.message}`);
}
})
};
});
return sections;
}
export function get() {
try {
if (!json || process.env.NODE_ENV !== 'production') {
json = get_sections();
}
return {
body: json
};
} catch (err) {
return {
status: 500,
body: {
message: err.message
}
};
}
}

@ -1,8 +0,0 @@
<script context="module">
export function load() {
return {
status: 301,
redirect: '/tutorial/basics'
};
}
</script>

@ -1,23 +0,0 @@
export async function get(req) {
let { min = '0', max = '100' } = req.query;
min = +min;
max = +max;
// simulate a long delay
await new Promise((res) => setTimeout(res, 1000));
// fail sometimes
if (Math.random() < 0.333) {
return {
status: 400,
headers: { 'Access-Control-Allow-Origin': '*' },
body: `Failed to generate random number. Please try again`
};
}
const num = min + Math.round(Math.random() * (max - min));
return {
headers: { 'Access-Control-Allow-Origin': '*' },
body: String(num)
};
}

@ -1,52 +0,0 @@
import flru from 'flru';
import { find, query } from './db';
export const sanitize_user = obj => obj && ({
uid: obj.uid,
username: obj.username,
name: obj.name,
avatar: obj.avatar
});
const session_cache = flru(1000);
export const create_user = async (gh_user, access_token) => {
return await find(`
insert into users(uid, name, username, avatar, github_token)
values ($1, $2, $3, $4, $5) on conflict (uid) do update
set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now())
returning id, uid, username, name, avatar
`, [gh_user.id, gh_user.name, gh_user.login, gh_user.avatar_url, access_token]);
};
export const create_session = async user => {
const session = await find(`
insert into sessions(user_id)
values ($1)
returning uid
`, [user.id]);
session_cache.set(session.uid, user);
return session;
};
export const delete_session = async sid => {
await query(`delete from sessions where uid = $1`, [sid]);
session_cache.set(sid, null);
};
export const get_user = async sid => {
if (!sid) return null;
if (!session_cache.has(sid)) {
session_cache.set(sid, await find(`
select users.id, users.uid, users.username, users.name, users.avatar
from sessions
left join users on sessions.user_id = users.id
where sessions.uid = $1 and expiry > now()
`, [sid]));
}
return session_cache.get(sid);
};

@ -1 +0,0 @@
export const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';

@ -1,12 +0,0 @@
import pg from 'pg';
// Uses `PG*` ENV vars
export const DB = process.env['PGHOST'] ? new pg.Pool() : null;
export function query(text, values=[]) {
return DB.query(text, values).then(r => r.rows);
}
export function find(text, values=[]) {
return query(text, values).then(arr => arr[0]);
}

@ -1,19 +0,0 @@
export function keyEvent(code) {
return function (node, callback) {
node.addEventListener('keydown', handleKeydown);
function handleKeydown(event) {
if (event.keyCode === code) {
callback.call(this, event);
}
}
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
}
};
};
}
export const enter = keyEvent(13);

@ -1,16 +0,0 @@
export function process_example(files) {
return files
.map(file => {
const [name, type] = file.name.split('.');
return { name, type, source: file.source };
})
.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type === b.type) return a.name < b.name ? -1 : 1;
if (a.type === 'svelte') return -1;
if (b.type === 'svelte') return 1;
});
}

@ -1,16 +0,0 @@
import { langs } from '@sveltejs/site-kit/utils/markdown.js';
import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash.js';
import 'prismjs/components/prism-diff.js';
import 'prism-svelte';
export function highlight(source, lang) {
const plang = langs[lang] || '';
const highlighted = plang ? PrismJS.highlight(
source,
PrismJS.languages[plang],
lang,
) : source.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]);
return `<pre class='language-${plang}'><code>${highlighted}</code></pre>`;
}

@ -1,78 +0,0 @@
import slugify from '@sindresorhus/slugify';
import {SLUG_SEPARATOR} from '../../config.js';
/* url-safe processor */
export const urlsafeSlugProcessor = string =>
slugify(string, {
customReplacements: [ // runs before any other transformations
['$', 'DOLLAR'], // `$destroy` & co
['-', 'DASH'], // conflicts with `separator`
],
separator: SLUG_SEPARATOR,
decamelize: false,
lowercase: false
})
.replace(/DOLLAR/g, '$')
.replace(/DASH/g, '-');
/* unicode-preserver processor */
const alphaNumRegex = /[a-zA-Z0-9]/;
const unicodeRegex = /\p{Letter}/u;
const isNonAlphaNumUnicode =
string => !alphaNumRegex.test(string) && unicodeRegex.test(string);
export const unicodeSafeProcessor = string =>
string.split('')
.reduce((accum, char, index, array) => {
const type = isNonAlphaNumUnicode(char) ? 'pass' : 'process';
if (index === 0) {
accum.current = {type, string: char};
} else if (type === accum.current.type) {
accum.current.string += char;
} else {
accum.chunks.push(accum.current);
accum.current = {type, string: char};
}
if (index === array.length - 1) {
accum.chunks.push(accum.current);
}
return accum;
}, {chunks: [], current: {type: '', string: ''}})
.chunks
.reduce((accum, chunk) => {
const processed = chunk.type === 'process'
? urlsafeSlugProcessor(chunk.string)
: chunk.string;
processed.length > 0 && accum.push(processed);
return accum;
}, [])
.join(SLUG_SEPARATOR);
/* processor */
export const makeSlugProcessor = (preserveUnicode = false) => preserveUnicode
? unicodeSafeProcessor
: urlsafeSlugProcessor;
/* session processor */
export const makeSessionSlugProcessor = (preserveUnicode = false) => {
const processor = makeSlugProcessor(preserveUnicode);
const seen = new Set();
return string => {
const slug = processor(string);
if (seen.has(slug)) throw new Error(`Duplicate slug ${slug}`);
seen.add(slug);
return slug;
};
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save