[site] migrate to SvelteKit (#6811)

Co-authored-by: Conduitry <git@chor.date>
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
pull/6865/head
Ben McCann 3 years ago committed by GitHub
parent 883c47b45d
commit 42076a7502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

10
.gitignore vendored

@ -24,15 +24,13 @@ _actual*.*
_output _output
/types /types
/site/cypress/screenshots/ /site/.svelte-kit/
/site/__sapper__/ /site/build/
/site/.env /site/.env
/site/.sessions
/site/static/svelte-app.json /site/static/svelte-app.json
/site/static/contributors.jpg /site/static/contributors.jpg
/site/static/donors.jpg /site/static/donors.jpg
/site/static/workers /site/static/workers/
/site/scripts/svelte-app /site/scripts/svelte-app/
/site/scripts/community
/site/src/routes/_contributors.js /site/src/routes/_contributors.js
/site/src/routes/_donors.js /site/src/routes/_donors.js

@ -2,8 +2,6 @@
!/Dockerfile !/Dockerfile
!/package.json !/package.json
!/package-lock.json !/package-lock.json
!/__sapper__ !/build
/__sapper__/*
!/__sapper__/build
!/static !/static
!/content !/content

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

@ -1,11 +0,0 @@
sudo: false
language: node_js
node_js:
- "stable"
env:
global:
- BUILD_TIMEOUT=10000
install:
- npm install
- npm install cypress

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

@ -5,10 +5,10 @@ PROJECT := svelte-dev
IMAGE := gcr.io/$(PROJECT)/$(SERVICE):$(HASH) IMAGE := gcr.io/$(PROJECT)/$(SERVICE):$(HASH)
sapper: sveltekit:
@echo "\n~> updating template & contributors list" @echo "\n~> updating template & contributors list"
@npm run update @npm run update
@echo "\n~> building Sapper app" @echo "\n~> building SvelteKit app"
@npm run build @npm run build
@ -17,6 +17,6 @@ docker:
@gcloud builds submit --project $(PROJECT) -t $(IMAGE) @gcloud builds submit --project $(PROJECT) -t $(IMAGE)
deploy: sapper docker deploy: sveltekit docker
@echo "\n~> deploying $(SERVICE) to Cloud Run servers" @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 @gcloud run deploy $(SERVICE) --project $(PROJECT) --allow-unauthenticated --platform managed --region us-central1 --image $(IMAGE) --memory=512Mi

@ -51,7 +51,7 @@ In order for the REPL's GitHub integration to work properly when running locally
## Building the site ## Building the site
To build the website, run `npm run build`. The output can be found in `__sapper__/build`. To build the website, run `npm run build`. The output can be found in `build`.
## Testing ## Testing

@ -1,17 +0,0 @@
version: "{build}"
shallow_clone: true
init:
- git config --global core.autocrlf false
build: off
environment:
matrix:
# node.js
- nodejs_version: stable
install:
- ps: Install-Product node $env:nodejs_version
- npm install

@ -0,0 +1,15 @@
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"

8531
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,61 +4,40 @@
"description": "Docs and examples for Svelte", "description": "Docs and examples for Svelte",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/update.js && npm run copy-workers && sapper dev", "dev": "node scripts/update.js && npm run copy-workers && svelte-kit dev",
"copy-workers": "node scripts/copy-workers.js", "copy-workers": "node scripts/copy-workers.js",
"migrate": "node-pg-migrate -r dotenv/config", "migrate": "node-pg-migrate -r dotenv/config",
"build": "node scripts/update.js && npm run copy-workers && sapper build --legacy", "build": "node scripts/update.js && npm run copy-workers && svelte-kit build",
"update": "node scripts/update.js --force=true", "update": "node scripts/update.js --force=true",
"start": "node __sapper__/build", "start": "node build",
"test": "mocha -r esm test/**", "test": "uvu test",
"deploy": "make deploy" "deploy": "make deploy"
}, },
"dependencies": { "dependencies": {
"@polka/redirect": "^1.0.0-next.7",
"@polka/send": "^1.0.0-next.7",
"cookie": "^0.4.0", "cookie": "^0.4.0",
"devalue": "^2.0.0", "devalue": "^2.0.0",
"do-not-zip": "^1.0.0", "do-not-zip": "^1.0.0",
"flru": "^1.0.2", "flru": "^1.0.2",
"httpie": "^1.1.2", "httpie": "^1.1.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"marked": "^1.0.0", "marked": "^3.0.5",
"pg": "^8.7.1", "pg": "^8.7.1",
"polka": "^1.0.0-next.9",
"prism-svelte": "^0.4.3", "prism-svelte": "^0.4.3",
"prismjs": "^1.25.0", "prismjs": "^1.25.0"
"sirv": "^1.0.0",
"yootils": "0.0.16"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.6.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/runtime": "^7.6.0",
"@rollup/plugin-babel": "^5.0.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.2.0",
"@sindresorhus/slugify": "^0.9.1", "@sindresorhus/slugify": "^0.9.1",
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"@sveltejs/site-kit": "^1.4.0", "@sveltejs/site-kit": "^1.4.0",
"@sveltejs/svelte-repl": "^0.2.1", "@sveltejs/svelte-repl": "^0.3.0",
"degit": "^2.1.4", "degit": "^2.1.4",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esm": "^3.2.25",
"jimp": "^0.8.0", "jimp": "^0.8.0",
"mocha": "^6.2.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-pg-migrate": "^3.22.0", "node-pg-migrate": "^6.0.0",
"rollup": "^2.30.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"sapper": "^0.29.3",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"svelte": "^3.39.0" "svelte": "^3.39.0",
}, "uvu": "^0.5.2"
"engines": {
"node": ">=10.0.0"
} }
} }

@ -1,122 +0,0 @@
import 'dotenv/config';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import resolve from '@rollup/plugin-node-resolve';
import svelte from 'rollup-plugin-svelte';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup.js';
import pkg from './package.json';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;
if (!dev && !process.env.MAPBOX_ACCESS_TOKEN) {
throw new Error('MAPBOX_ACCESS_TOKEN is missing. Please add the token in the .env file before generating the production build.');
}
const onwarn = (warning, onwarn) => (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning);
const dedupe = importee => importee === 'svelte' || importee.startsWith('svelte/');
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode),
'process.env.MAPBOX_ACCESS_TOKEN': JSON.stringify(process.env.MAPBOX_ACCESS_TOKEN)
}),
svelte({
compilerOptions: {
dev,
hydratable: true
}
}),
resolve({
browser: true,
dedupe
}),
commonjs(),
json(),
legacy && babel({
extensions: ['.js', '.mjs', '.html', '.svelte'],
babelHelpers: 'runtime',
exclude: ['node_modules/@babel/**'],
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead'
}]
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-transform-runtime', {
useESModules: true
}]
]
}),
!dev && terser({
module: true
})
],
preserveEntrySignatures: false,
onwarn
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
compilerOptions: {
dev,
generate: 'ssr',
hydratable: true
},
emitCss: false
}),
resolve({
dedupe
}),
commonjs(),
json()
],
external: [
'yootils',
'codemirror',
...Object.keys(pkg.dependencies || {}).concat(
require('module').builtinModules || Object.keys(process.binding('natives'))
)
],
preserveEntrySignatures: 'strict',
onwarn
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
commonjs(),
!dev && terser()
],
preserveEntrySignatures: false,
onwarn
}
};

@ -0,0 +1,26 @@
<!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,6 +0,0 @@
import '@sveltejs/site-kit/base.css';
import * as sapper from '@sapper/app';
sapper.start({
target: document.querySelector('#sapper')
});

@ -1,6 +1,7 @@
<script> <script>
import Repl from '@sveltejs/svelte-repl'; import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/env';
import { process_example } from '../../utils/examples'; import { process_example } from '../../utils/examples';
import InputOutputToggle from './InputOutputToggle.svelte'; import InputOutputToggle from './InputOutputToggle.svelte';
@ -12,7 +13,7 @@
let repl; let repl;
let name = 'loading...'; let name = 'loading...';
let width = process.browser let width = browser
? window.innerWidth - 32 ? window.innerWidth - 32
: 1000; : 1000;
@ -73,7 +74,7 @@
$: if (embedded) document.title = `${name} • Svelte REPL`; $: if (embedded) document.title = `${name} • Svelte REPL`;
$: svelteUrl = process.browser && version === 'local' ? $: svelteUrl = browser && version === 'local' ?
`${location.origin}/repl/local` : `${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`; `https://unpkg.com/svelte@${version}`;
@ -116,7 +117,7 @@
<div class="repl-outer" bind:clientWidth={width} class:mobile> <div class="repl-outer" bind:clientWidth={width} class:mobile>
<div class="viewport" class:offset={checked}> <div class="viewport" class:offset={checked}>
{#if process.browser} {#if browser}
<Repl <Repl
bind:this={repl} bind:this={repl}
workersUrl="workers" workersUrl="workers"

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

@ -0,0 +1,31 @@
// 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,6 +1,13 @@
<script> <script context="module">
const dev = process.env.NODE_ENV === 'development'; /** @type {import('@sveltejs/kit').ErrorLoad} */
export function load({ error, status }) {
return {
props: { error, status }
};
}
</script>
<script>
export let status; export let status;
export let error; export let error;
@ -54,7 +61,7 @@
<p class="error">Encountered a {status} error</p> <p class="error">Encountered a {status} error</p>
{/if} {/if}
{#if dev && error.stack} {#if import.meta.env.DEV && error.stack}
<pre>{error.stack}</pre> <pre>{error.stack}</pre>
{:else} {:else}
{#if status >= 500} {#if status >= 500}

@ -1,13 +1,12 @@
<script> <script>
import '@sveltejs/site-kit/base.css';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
import { stores } from '@sapper/app'; import { page, navigating, session } from '$app/stores';
import { Icon, Icons, Nav, NavItem } from '@sveltejs/site-kit'; import { Icon, Icons, Nav, NavItem } from '@sveltejs/site-kit';
import PreloadingIndicator from '../components/PreloadingIndicator.svelte'; import PreloadingIndicator from '../components/PreloadingIndicator.svelte';
export let segment; export let segment;
const { page, preloading, session } = stores();
setContext('app', { setContext('app', {
login: () => { login: () => {
const login_window = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400'); const login_window = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
@ -31,7 +30,7 @@
<Icons/> <Icons/>
{#if $preloading} {#if $navigating && $navigating.to}
<PreloadingIndicator/> <PreloadingIndicator/>
{/if} {/if}

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

@ -1,22 +1,22 @@
<script context="module"> <script context="module">
export async function preload(page, { user }) { export async function load({ fetch, page, session: { user }}) {
let apps = []; let apps = [];
let offset = null; let offset = null;
if (user) { if (user) {
let url = 'apps.json'; let url = 'apps.json';
if (page.query.offset) { if (page.query.get('offset')) {
url += `?offset=${encodeURIComponent(page.query.offset)}`; url += `?offset=${encodeURIComponent(page.query.get('offset'))}`;
} }
const r = await this.fetch(url, { const r = await fetch(url, {
credentials: 'include' credentials: 'include'
}); });
if (!r.ok) return this.error(r.status, await r.text()); if (!r.ok) return { status: r.status, body: await r.text() };
({ apps, offset } = await r.json()); ({ apps, offset } = await r.json());
} }
return { user, apps, offset }; return { props: { user, apps, offset }};
} }
</script> </script>

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

@ -1,4 +1,3 @@
import send from '@polka/send';
import devalue from 'devalue'; import devalue from 'devalue';
import * as cookie from 'cookie'; import * as cookie from 'cookie';
import * as httpie from 'httpie'; import * as httpie from 'httpie';
@ -6,11 +5,11 @@ import { parse, stringify } from 'querystring';
import { sanitize_user, create_user, create_session } from '../../utils/auth'; import { sanitize_user, create_user, create_session } from '../../utils/auth';
import { oauth, secure, client_id, client_secret } from './_config.js'; import { oauth, secure, client_id, client_secret } from './_config.js';
export async function get(req, res) { export async function get({ query }) {
try { try {
// Trade "code" for "access_token" // Trade "code" for "access_token"
const r1 = await httpie.post(`${oauth}/access_token?` + stringify({ const r1 = await httpie.post(`${oauth}/access_token?` + stringify({
code: req.query.code, code: query.get('code'),
client_id, client_id,
client_secret, client_secret,
})); }));
@ -27,28 +26,29 @@ export async function get(req, res) {
const user = await create_user(r2.data, access_token); const user = await create_user(r2.data, access_token);
const session = await create_session(user); const session = await create_session(user);
res.writeHead(200, { return {
'Set-Cookie': cookie.serialize('sid', session.uid, { headers: {
maxAge: 31536000, 'Set-Cookie': cookie.serialize('sid', session.uid, {
path: '/', maxAge: 31536000,
httpOnly: true, path: '/',
secure httpOnly: true,
}), secure
'Content-Type': 'text/html; charset=utf-8' }),
}); 'Content-Type': 'text/html; charset=utf-8'
},
res.end(` body: `
<script> <script>
window.opener.postMessage({ window.opener.postMessage({
user: ${devalue(sanitize_user(user))} user: ${devalue(sanitize_user(user))}
}, window.location.origin); }, window.location.origin);
</script> </script>
`); `
};
} catch (err) { } catch (err) {
console.error('GET /auth/callback', err); console.error('GET /auth/callback', err);
send(res, 500, err.data, { return {
'Content-Type': err.headers['content-type'], status: 500,
'Content-Length': err.headers['content-length'] body: err.data
}); };
} }
} }

@ -1,19 +1,28 @@
import send from '@polka/send';
import { stringify } from 'querystring'; import { stringify } from 'querystring';
import { oauth, baseurl, client_id } from './_config.js'; import { oauth, baseurl, client_id } from './_config.js';
export const get = client_id export const get = client_id
? (req, res) => { ? () => {
const Location = `${oauth}/authorize?` + stringify({ const Location = `${oauth}/authorize?` + stringify({
scope: 'read:user', scope: 'read:user',
client_id, client_id,
redirect_uri: `${baseurl}/auth/callback`, redirect_uri: `${baseurl}/auth/callback`,
}); });
send(res, 302, Location, { Location }); return {
status: 302,
headers: {
Location
}
};
} }
: (req, res) => { : () => {
send(res, 500, ` 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;"> <body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1> <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> <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>
@ -21,7 +30,6 @@ export const get = client_id
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p> <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> <p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body> </body>
`, { `
'Content-Type': 'text/html; charset=utf-8' };
}); };
};

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

@ -1,9 +1,8 @@
import send from '@polka/send';
import get_posts from './_posts.js'; import get_posts from './_posts.js';
let lookup; let lookup;
export function get(req, res) { export function get({ params }) {
if (!lookup || process.env.NODE_ENV !== 'production') { if (!lookup || process.env.NODE_ENV !== 'production') {
lookup = new Map(); lookup = new Map();
get_posts().forEach(post => { get_posts().forEach(post => {
@ -11,12 +10,14 @@ export function get(req, res) {
}); });
} }
const post = lookup.get(req.params.slug); const post = lookup.get(params.slug);
if (post) { if (post) {
res.setHeader('Cache-Control', `max-age=${5 * 60 * 1e3}`); // 5 minutes return {
send(res, 200, post); body: post,
} else { headers: {
send(res, 404, { message: 'not found' }); 'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
}
};
} }
} }

@ -1,7 +1,9 @@
<script context="module"> <script context="module">
export async function preload({ params }) { export async function load({ fetch, page: { params } }) {
const res = await this.fetch(`blog/${params.slug}.json`); const res = await fetch(`/blog/${params.slug}.json`);
return res.ok ? { post: await res.json() } : this.error(404, 'Not found'); if (res.ok) {
return { props: { post: await res.json() }};
}
} }
</script> </script>

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

@ -1,7 +1,11 @@
<script context="module"> <script context="module">
export async function preload() { export async function load({ fetch }) {
const posts = await this.fetch(`blog.json`).then(r => r.json()); const posts = await fetch(`blog.json`).then(r => r.json());
return { posts }; return {
props: {
posts
}
};
} }
</script> </script>
@ -22,7 +26,7 @@
<div class='posts stretch'> <div class='posts stretch'>
{#each posts as post} {#each posts as post}
<article class='post' data-pubdate={post.metadata.dateString}> <article class='post' data-pubdate={post.metadata.dateString}>
<a class="no-underline" sapper:prefetch href='blog/{post.slug}' title='Read the article »'> <a class="no-underline" sveltekit:prefetch href='blog/{post.slug}' title='Read the article »'>
<h2>{post.metadata.title}</h2> <h2>{post.metadata.title}</h2>
<p>{post.metadata.description}</p> <p>{post.metadata.description}</p>
</a> </a>

@ -1,4 +1,3 @@
import send from '@polka/send';
import get_posts from '../blog/_posts.js'; import get_posts from '../blog/_posts.js';
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','); const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
@ -46,9 +45,12 @@ const rss = `
</rss> </rss>
`.replace(/>[^\S]+/gm, '>').replace(/[^\S]+</gm, '<').trim(); `.replace(/>[^\S]+/gm, '>').replace(/[^\S]+</gm, '<').trim();
export function get(req, res) { export function get() {
send(res, 200, rss, { return {
'Cache-Control': `max-age=${30 * 60 * 1e3}`, body: rss,
'Content-Type': 'application/rss+xml' headers: {
}); 'Cache-Control': `max-age=${30 * 60 * 1e3}`,
'Content-Type': 'application/rss+xml'
}
};
} }

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

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

@ -6,8 +6,8 @@
'4_Prefix_stores_with_$_to_access_their_values': 'accessing stores ($)' '4_Prefix_stores_with_$_to_access_their_values': 'accessing stores ($)'
}; };
export async function preload() { export async function load({ fetch }) {
const sections = await this.fetch(`docs.json`).then(r => r.json()); const sections = await fetch(`docs.json`).then(r => r.json());
for (const section of sections) { for (const section of sections) {
for (const subsection of section.subsections) { for (const subsection of section.subsections) {
const { slug } = subsection; const { slug } = subsection;
@ -18,7 +18,11 @@
} }
} }
return { sections }; return {
props: {
sections
}
};
} }
</script> </script>

@ -1,10 +1,9 @@
import send from '@polka/send';
import { get_example } from './_examples.js'; import { get_example } from './_examples.js';
const cache = new Map(); const cache = new Map();
export function get(req, res) { export function get({ params }) {
const { slug } = req.params; const { slug } = params;
let example = cache.get(slug); let example = cache.get(slug);
@ -14,10 +13,8 @@ export function get(req, res) {
} }
if (example) { if (example) {
send(res, 200, example); return {
} else { body: example
send(res, 404, { };
error: 'not found'
});
} }
} }

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

@ -1,7 +1,7 @@
<!-- FIXME sometimes it adds a trailing slash when landing --> <!-- FIXME sometimes it adds a trailing slash when landing -->
<script context="module"> <script context="module">
export async function preload() { export async function load({ fetch }) {
const sections = await this.fetch(`examples.json`).then(r => r.json()); const sections = await fetch(`examples.json`).then(r => r.json());
const title_by_slug = sections.reduce((acc, {examples}) => { const title_by_slug = sections.reduce((acc, {examples}) => {
examples.forEach(({slug, title}) => { examples.forEach(({slug, title}) => {
acc[slug] = title; acc[slug] = title;
@ -10,13 +10,18 @@
return acc; return acc;
}, {}); }, {});
return {sections, title_by_slug}; return {
props: {
sections,
title_by_slug
}
};
} }
</script> </script>
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '@sapper/app'; import { goto } from '$app/navigation';
import Repl from '@sveltejs/svelte-repl'; import Repl from '@sveltejs/svelte-repl';
import ScreenToggle from '../../components/ScreenToggle.svelte'; import ScreenToggle from '../../components/ScreenToggle.svelte';

@ -1,9 +1,8 @@
import send from '@polka/send';
import get_faqs from './_faqs.js'; import get_faqs from './_faqs.js';
let json; let json;
export function get(req, res) { export function get() {
if (!json || process.env.NODE_ENV !== 'production') { if (!json || process.env.NODE_ENV !== 'production') {
const faqs = get_faqs() const faqs = get_faqs()
.map(faq => { .map(faq => {
@ -17,8 +16,11 @@ export function get(req, res) {
json = JSON.stringify(faqs); json = JSON.stringify(faqs);
} }
send(res, 200, json, { return {
'Content-Type': 'application/json', body: json,
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes headers: {
}); 'Content-Type': 'application/json',
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
}
};
} }

@ -1,7 +1,11 @@
<script context="module"> <script context="module">
export async function preload() { export async function load({ fetch }) {
const faqs = await this.fetch(`faq.json`).then(r => r.json()); const faqs = await fetch(`faq.json`).then(r => r.json());
return { faqs }; return {
props: {
faqs
}
};
} }
</script> </script>

@ -87,7 +87,7 @@ npm run dev
<p style="flex: 1">See the <a href="blog/the-easiest-way-to-get-started">quickstart guide</a> for more information.</p> <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 sapper:prefetch href="tutorial">Learn Svelte</a></p> <p class="cta"><a sveltekit:prefetch href="tutorial">Learn Svelte</a></p>
</div> </div>
</Blurb> </Blurb>

@ -1,7 +1,6 @@
<script> <script>
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { stores } from '@sapper/app'; import { session } from '$app/stores';
const { session } = stores();
const { logout } = getContext('app'); const { logout } = getContext('app');

@ -1,6 +1,6 @@
<script> <script>
import { createEventDispatcher, getContext } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
import { stores } from '@sapper/app'; import { session } from '$app/stores';
import UserMenu from './UserMenu.svelte'; import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit'; import { Icon } from '@sveltejs/site-kit';
import * as doNotZip from 'do-not-zip'; import * as doNotZip from 'do-not-zip';
@ -9,7 +9,6 @@
import { isMac } from '../../../../../utils/compat.js'; import { isMac } from '../../../../../utils/compat.js';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const { session } = stores();
const { login } = getContext('app'); const { login } = getContext('app');
export let repl; export let repl;
@ -44,6 +43,9 @@
const r = await fetch(`repl/create.json`, { const r = await fetch(`repl/create.json`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
name, name,
files: components.map(component => ({ files: components.map(component => ({
@ -99,6 +101,9 @@
const r = await fetch(`repl/${gist.uid}.json`, { const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include', credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ body: JSON.stringify({
name, name,
files: components.map(component => ({ files: components.map(component => ({

@ -1,127 +1,146 @@
import send from '@polka/send';
import body from '../_utils/body.js';
import * as httpie from 'httpie'; import * as httpie from 'httpie';
import { query, find } from '../../../utils/db'; import { query, find } from '../../../utils/db';
import { get_example } from '../../examples/_examples.js'; import { get_example } from '../../examples/_examples.js';
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env; const GITHUB_CLIENT_ID = process.env['GITHUB_CLIENT_ID'];
const GITHUB_CLIENT_SECRET = process.env['GITHUB_CLIENT_SECRET'];
async function import_gist(req, res) { export async function get({ path, params }) {
const base = `https://api.github.com/gists/${req.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)
`, [req.params.id, user.id, data.description, JSON.stringify(files)]);
send(res, 200, {
uid: req.params.id,
name: data.description,
files,
owner: data.owner.id
});
} catch (err) {
send(res, err.statusCode, { error: err.message });
}
}
export async function get(req, res) {
// is this an example? // is this an example?
const example = get_example(req.params.id); const example = get_example(params.id);
if (example) { if (example) {
return send(res, 200, { return {
relaxed: true, body: {
uid: req.params.id, relaxed: true,
name: example.title, uid: params.id,
files: example.files, name: example.title,
owner: null files: example.files,
}); owner: null
}
};
} }
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// In dev, proxy requests to load particular REPLs to the real server. // In dev, proxy requests to load particular REPLs to the real server.
// This avoids needing to connect to the real database server. // This avoids needing to connect to the real database server.
req.pipe( try {
require('https').request({ host: 'svelte.dev', path: req.url }) const res_proxy = await httpie.get(`https://svelte.dev${path}`);
).once('response', res_proxy => { return {
res_proxy.pipe(res); body: res_proxy.data,
res.writeHead(res_proxy.statusCode, res_proxy.headers); status: res_proxy.statusCode,
}).once('error', () => res.end()); headers: res_proxy.headers
return; };
} catch (err) {
return {
status: err.statusCode,
body: { error: err.message }
};
}
} }
const [row] = await query(` const [row] = await query(`
select g.*, u.uid as owner from gists g select g.*, u.uid as owner from gists g
left join users u on g.user_id = u.id left join users u on g.user_id = u.id
where g.uid = $1 limit 1 where g.uid = $1 limit 1
`, [req.params.id]); // via filename pattern `, [params.id]); // via filename pattern
if (!row) { if (!row) {
return import_gist(req, res); 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 }
};
}
} }
send(res, 200, { return {
uid: row.uid.replace(/-/g, ''), body: {
name: row.name, uid: row.uid.replace(/-/g, ''),
files: row.files, name: row.name,
owner: row.owner files: row.files,
}); owner: row.owner
}
};
} }
export async function patch(req, res) { export async function patch({ params, locals, body }) {
const { user } = req; const { user } = locals;
if (!user) return; if (!user) return;
let id; let id;
const uid = req.params.id; const uid = params.id;
try { try {
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]); const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
if (!row) return send(res, 404, { error: 'Gist not found' }); if (!row) {
if (row.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' }); 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; id = row.id;
} catch (err) { } catch (err) {
console.error('PATCH /gists @ select', err); console.error('PATCH /gists @ select', err);
return send(res, 500); return { status: 500 };
} }
try { try {
const obj = await body(req); const obj = body;
obj.updated_at = 'now()'; obj.updated_at = 'now()';
let k; let k;
const cols = []; const cols = [];
@ -136,14 +155,19 @@ export async function patch(req, res) {
const [row] = await query(`update gists ${set} where id = ${id} returning *`, vals); const [row] = await query(`update gists ${set} where id = ${id} returning *`, vals);
send(res, 200, { return {
uid: row.uid.replace(/-/g, ''), body: {
name: row.name, uid: row.uid.replace(/-/g, ''),
files: row.files, name: row.name,
owner: user.uid, files: row.files,
}); owner: user.uid,
}
};
} catch (err) { } catch (err) {
console.error('PATCH /gists @ update', err); console.error('PATCH /gists @ update', err);
send(res, 500, { error: err.message }); return {
status: 500,
body: { error: err.message }
};
} }
} }

@ -1,8 +1,10 @@
<script context="module"> <script context="module">
export function preload({ params, query }) { export function load({ page: { params, query }}) {
return { return {
version: query.version || '3', props: {
id: params.id version: query.get('version') || '3',
id: params.id
}
}; };
} }
</script> </script>
@ -10,21 +12,22 @@
<script> <script>
import Repl from '@sveltejs/svelte-repl'; import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto, stores } from '@sapper/app'; 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 InputOutputToggle from '../../../components/Repl/InputOutputToggle.svelte';
import AppControls from './_components/AppControls/index.svelte'; import AppControls from './_components/AppControls/index.svelte';
export let version; export let version;
export let id; export let id;
const { session } = stores();
let repl; let repl;
let gist; let gist;
let name = 'Loading...'; let name = 'Loading...';
let zen_mode = false; let zen_mode = false;
let is_relaxed_gist = false; let is_relaxed_gist = false;
let width = process.browser ? window.innerWidth : 1000; let width = browser ? window.innerWidth : 1000;
let checked = false; let checked = false;
function update_query_string(version) { function update_query_string(version) {
@ -33,8 +36,8 @@
if (version !== 'latest') params.push(`version=${version}`); if (version !== 'latest') params.push(`version=${version}`);
const url = params.length > 0 const url = params.length > 0
? `repl/${id}?${params.join('&')}` ? `/repl/${id}?${params.join('&')}`
: `repl/${id}`; : `/repl/${id}`;
history.replaceState({}, 'x', url); history.replaceState({}, 'x', url);
} }
@ -48,7 +51,7 @@
} }
// TODO handle `relaxed` logic // TODO handle `relaxed` logic
fetch(`repl/${id}.json`).then(r => { fetch(`/repl/${id}.json`).then(r => {
if (r.ok) { if (r.ok) {
r.json().then(data => { r.json().then(data => {
gist = data; gist = data;
@ -82,7 +85,7 @@
}); });
} }
$: if (process.browser) fetch_gist(id); $: if (browser) fetch_gist(id);
onMount(() => { onMount(() => {
if (version !== 'local') { if (version !== 'local') {
@ -100,15 +103,12 @@
goto(`/repl/${gist.uid}?version=${version}`); goto(`/repl/${gist.uid}?version=${version}`);
} }
$: svelteUrl = process.browser && version === 'local' ? $: svelteUrl = browser && version === 'local' ?
`${location.origin}/repl/local` : `${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`; `https://unpkg.com/svelte@${version}`;
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`; const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
// needed for context API example
const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;`;
$: mobile = width < 540; $: mobile = width < 540;
$: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner); $: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner);
@ -192,7 +192,7 @@
on:forked={handle_fork} on:forked={handle_fork}
/> />
{#if process.browser} {#if browser}
<div class="viewport" class:offset={checked}> <div class="viewport" class:offset={checked}>
<Repl <Repl
bind:this={repl} bind:this={repl}

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

@ -1,17 +1,20 @@
<script context="module"> <script context="module">
export function preload({ query }) { export function load({ page: { query }}) {
return { return {
version: query.version, props: {
gist: query.gist, version: query.get('version') || '3',
example: query.example gist: query.get('gist'),
example: query.get('example')
}
}; };
} }
</script> </script>
<script> <script>
import { browser } from '$app/env';
import ReplWidget from '../../components/Repl/ReplWidget.svelte'; import ReplWidget from '../../components/Repl/ReplWidget.svelte';
export let version = '3'; export let version;
export let gist; export let gist;
export let example; export let example;
</script> </script>
@ -41,7 +44,7 @@
</svelte:head> </svelte:head>
<div class="repl-outer"> <div class="repl-outer">
{#if process.browser} {#if browser}
<ReplWidget {version} {gist} {example} embedded={true}/> <ReplWidget {version} {gist} {example} embedded={true}/>
{/if} {/if}
</div> </div>

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

@ -1,17 +0,0 @@
import { createReadStream } from 'fs';
export function get(req, res) {
const path = req.params.file.join('/');
if (process.env.NODE_ENV !== 'development' || ('/' + path).includes('/.')) {
res.writeHead(403);
res.end();
return;
}
createReadStream('../' + path)
.on('error', () => {
res.writeHead(403);
res.end();
})
.pipe(res);
res.writeHead(200, { 'Content-Type': 'text/javascript' });
}

@ -0,0 +1,11 @@
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,5 +1,5 @@
<script> <script>
import { goto } from '@sapper/app'; import { goto } from '$app/navigation';
import { Icon } from '@sveltejs/site-kit'; import { Icon } from '@sveltejs/site-kit';
export let sections; export let sections;
@ -64,7 +64,7 @@
</style> </style>
<nav> <nav>
<a sapper:prefetch aria-label="Previous tutorial step" class="no-underline" href="tutorial/{(selected.prev || selected).slug}" class:disabled={!selected.prev}> <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" /> <Icon name="arrow-left" />
</a> </a>
@ -88,7 +88,7 @@
</select> </select>
</div> </div>
<a sapper:prefetch aria-label="Next tutorial step" class="no-underline" href="tutorial/{(selected.next || selected).slug}" class:disabled={!selected.next}> <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" /> <Icon name="arrow-right" />
</a> </a>
</nav> </nav>

@ -1,7 +1,6 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import marked from 'marked'; import marked from 'marked';
import send from '@polka/send';
import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown'; import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown';
import { highlight } from '../../../utils/highlight'; import { highlight } from '../../../utils/highlight';
@ -85,8 +84,8 @@ function get_tutorial(slug) {
}; };
} }
export function get(req, res) { export function get({ params }) {
const { slug } = req.params; const { slug } = params;
let tut = cache.get(slug); let tut = cache.get(slug);
if (!tut || process.env.NODE_ENV !== 'production') { if (!tut || process.env.NODE_ENV !== 'production') {
@ -95,8 +94,8 @@ export function get(req, res) {
} }
if (tut) { if (tut) {
send(res, 200, tut); return {
} else { body: tut
send(res, 404, { message: 'not found' }); };
} }
} }

@ -1,14 +1,19 @@
<script context="module"> <script context="module">
export async function preload({ params }) { export async function load({ fetch, page: { params }}) {
const res = await this.fetch(`tutorial/${params.slug}.json`); const res = await fetch(`/tutorial/${params.slug}.json`);
if (!res.ok) { if (!res.ok) {
return this.redirect(301, `tutorial/basics`); return {
status: 301,
redirect: '/tutorial/basics'
}
} }
return { return {
slug: params.slug, props: {
chapter: await res.json() slug: params.slug,
chapter: await res.json()
}
}; };
} }
</script> </script>
@ -16,6 +21,7 @@
<script> <script>
import Repl from '@sveltejs/svelte-repl'; import Repl from '@sveltejs/svelte-repl';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { browser } from '$app/env';
import ScreenToggle from '../../../components/ScreenToggle.svelte'; import ScreenToggle from '../../../components/ScreenToggle.svelte';
import TableOfContents from './_TableOfContents.svelte'; import TableOfContents from './_TableOfContents.svelte';
@ -36,7 +42,7 @@
let scrollable; let scrollable;
const lookup = new Map(); const lookup = new Map();
let width = process.browser ? window.innerWidth : 1000; let width = browser ? window.innerWidth : 1000;
let offset = 0; let offset = 0;
sections.forEach(section => { sections.forEach(section => {
@ -50,7 +56,7 @@
lookup.set(chapter.slug, obj); lookup.set(chapter.slug, obj);
if (process.browser) { // pending https://github.com/sveltejs/svelte/issues/2135 if (browser) { // pending https://github.com/sveltejs/svelte/issues/2135
if (prev) prev.next = obj; if (prev) prev.next = obj;
prev = obj; prev = obj;
} }

@ -0,0 +1,19 @@
<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,15 +0,0 @@
<script context="module">
export async function preload() {
const sections = await this.fetch(`tutorial.json`).then(r => r.json());
return { sections };
}
</script>
<script>
import { setContext } from 'svelte';
export let sections;
setContext('tutorial', { sections });
</script>
<slot></slot>

@ -1,5 +1,4 @@
import * as fs from 'fs'; import * as fs from 'fs';
import send from '@polka/send';
import { extract_frontmatter } from '@sveltejs/site-kit/utils/markdown'; import { extract_frontmatter } from '@sveltejs/site-kit/utils/markdown';
let json; let json;
@ -48,16 +47,21 @@ function get_sections() {
return sections; return sections;
} }
export function get(req, res) { export function get() {
try { try {
if (!json || process.env.NODE_ENV !== 'production') { if (!json || process.env.NODE_ENV !== 'production') {
json = get_sections(); json = get_sections();
} }
send(res, 200, json); return {
body: json
};
} catch (err) { } catch (err) {
send(res, 500, { return {
message: err.message status: 500,
}); body: {
message: err.message
}
};
} }
} }

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

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

@ -1,43 +0,0 @@
import polka from 'polka';
import send from '@polka/send';
import sirv from 'sirv';
import * as sapper from '@sapper/server';
import { sanitize_user, authenticate } from './utils/auth';
if (!process.env.PORT) {
process.env.PORT = 3000;
}
const { PORT } = process.env;
const app = polka({
onError: (err, req, res) => {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error }, {
'content-type': 'text/plain'
});
}
});
if (process.env.PGHOST) {
app.use(authenticate());
}
app.use(
sirv('static', {
dev: process.env.NODE_ENV === 'development',
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.hasHeader('Cache-Control') || res.setHeader('Cache-Control', 'max-age=600'); // 10min default
}
}),
sapper.middleware({
session: req => ({
user: sanitize_user(req.user)
})
})
);
app.listen(PORT);

@ -1,87 +0,0 @@
import { timestamp, files, shell } from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
// `shell` is an array of all the files generated by Rollup,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(files.filter(file => {
const basename = file.split('/').pop();
if (basename[0] === '.') return false; // .DS_Store and friends
if (basename.endsWith('.mp3')) return false; // TODO others?
return true;
}));
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve static files and Rollup-generated assets from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/index.html'));
return;
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch (err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

@ -1,40 +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'>
%sapper.base%
<link href=global.css rel=stylesheet>
<link href=prism.css rel=stylesheet>
<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'>
<!-- Sapper generates a <style> tag containing critical CSS
for the current page. CSS for the rest of the app is
lazily loaded when it precaches secondary pages -->
%sapper.styles%
<!-- This contains the contents of the <svelte:head> component, if
the current page has one -->
%sapper.head%
</head>
<body>
<!-- The application will be rendered inside this element,
because `app/client.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `app/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
</body>
</html>

@ -1,4 +1,3 @@
import * as cookie from 'cookie';
import flru from 'flru'; import flru from 'flru';
import { find, query } from './db'; import { find, query } from './db';
@ -37,7 +36,7 @@ export const delete_session = async sid => {
session_cache.set(sid, null); session_cache.set(sid, null);
}; };
const get_user = async sid => { export const get_user = async sid => {
if (!sid) return null; if (!sid) return null;
if (!session_cache.has(sid)) { if (!session_cache.has(sid)) {
@ -51,15 +50,3 @@ const get_user = async sid => {
return session_cache.get(sid); return session_cache.get(sid);
}; };
export const authenticate = () => {
// this is a convenient time to clear out expired sessions
query(`delete from sessions where expiry < now()`);
return async (req, res, next) => {
req.cookies = cookie.parse(req.headers.cookie || '');
req.user = await get_user(req.cookies.sid);
next();
};
};

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

@ -1,7 +1,7 @@
import { langs } from '@sveltejs/site-kit/utils/markdown.js'; import { langs } from '@sveltejs/site-kit/utils/markdown.js';
import PrismJS from 'prismjs'; import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-bash.js';
import 'prismjs/components/prism-diff'; import 'prismjs/components/prism-diff.js';
import 'prism-svelte'; import 'prism-svelte';
export function highlight(source, lang) { export function highlight(source, lang) {

@ -1,5 +1,5 @@
import slugify from '@sindresorhus/slugify'; import slugify from '@sindresorhus/slugify';
import {SLUG_SEPARATOR} from '../../config'; import {SLUG_SEPARATOR} from '../../config.js';
/* url-safe processor */ /* url-safe processor */

@ -0,0 +1,36 @@
import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
export default {
kit: {
adapter: adapter(),
target: '#svelte',
prerender: {
enabled: false
},
vite: () => ({
optimizeDeps: {
include: [
'codemirror',
'codemirror/mode/javascript/javascript.js',
'codemirror/mode/handlebars/handlebars.js',
'codemirror/mode/htmlmixed/htmlmixed.js',
'codemirror/mode/xml/xml.js',
'codemirror/mode/css/css.js',
'codemirror/mode/markdown/markdown.js',
'codemirror/addon/edit/closebrackets.js',
'codemirror/addon/edit/closetag.js',
'codemirror/addon/edit/continuelist.js',
'codemirror/addon/comment/comment.js',
'codemirror/addon/fold/foldcode.js',
'codemirror/addon/fold/foldgutter.js',
'codemirror/addon/fold/brace-fold.js',
'codemirror/addon/fold/xml-fold.js',
'codemirror/addon/fold/indent-fold.js',
'codemirror/addon/fold/markdown-fold.js',
'codemirror/addon/fold/comment-fold.js'
]
}
})
}
};

@ -1,423 +1,430 @@
import {strict as assert} from 'assert'; import * as uvu from 'uvu';
import {urlsafeSlugProcessor, unicodeSafeProcessor} from '../../src/utils/slug'; import * as assert from 'uvu/assert';
import {SLUG_SEPARATOR as _} from '../../config'; import {urlsafeSlugProcessor, unicodeSafeProcessor} from '../../src/utils/slug.js';
import {SLUG_SEPARATOR as _} from '../../config.js';
describe('slug', () => { function run(name, func) {
describe('urlsafeSlugProcessor', () => { const suite = uvu.suite(name);
describe('ascii', () => { func(suite);
it('space separated words', () => { suite.run();
assert.equal( }
urlsafeSlugProcessor('Text expressions'),
`Text${_}expressions` run('urlsafeSlugProcessor -> ascii', it => {
); it('space separated words', () => {
}); assert.equal(
it('numbered text', () => { urlsafeSlugProcessor('Text expressions'),
assert.equal( `Text${_}expressions`
urlsafeSlugProcessor('1. export creates'), );
`1${_}export${_}creates` });
); it('numbered text', () => {
}); assert.equal(
it('punctuated text', () => { urlsafeSlugProcessor('1. export creates'),
assert.equal( `1${_}export${_}creates`
urlsafeSlugProcessor('svelte.VERSION'), );
`svelte${_}VERSION` });
); it('punctuated text', () => {
}); assert.equal(
it('text starting with the dollar sign', () => { urlsafeSlugProcessor('svelte.VERSION'),
assert.equal( `svelte${_}VERSION`
urlsafeSlugProcessor('$destroy method'), );
`$destroy${_}method` });
); it('text starting with the dollar sign', () => {
}); assert.equal(
it('numbered text containing the dollar sign', () => { urlsafeSlugProcessor('$destroy method'),
assert.equal( `$destroy${_}method`
urlsafeSlugProcessor('1. export $destroy'), );
`1${_}export${_}$destroy` });
); it('numbered text containing the dollar sign', () => {
}); assert.equal(
it('text containing the equal char', () => { urlsafeSlugProcessor('1. export $destroy'),
assert.equal( `1${_}export${_}$destroy`
urlsafeSlugProcessor('script context=module'), );
`script${_}context${_}module` });
); it('text containing the equal char', () => {
}); assert.equal(
it('text containing the colon char', () => { urlsafeSlugProcessor('script context=module'),
assert.equal( `script${_}context${_}module`
urlsafeSlugProcessor('svelte:body'), );
`svelte${_}body` });
); it('text containing the colon char', () => {
}); assert.equal(
it('text containing the slash char', () => { urlsafeSlugProcessor('svelte:body'),
assert.equal( `svelte${_}body`
urlsafeSlugProcessor('svelte/motion'), );
`svelte${_}motion` });
); it('text containing the slash char', () => {
}); assert.equal(
it('text containing the comma char', () => { urlsafeSlugProcessor('svelte/motion'),
assert.equal( `svelte${_}motion`
urlsafeSlugProcessor('svelte, motion'), );
`svelte${_}motion` });
); it('text containing the comma char', () => {
}); assert.equal(
}); urlsafeSlugProcessor('svelte, motion'),
describe('unicode', () => { `svelte${_}motion`
it('should translate symbols to English', () => { );
assert.equal( });
urlsafeSlugProcessor('Ich ♥ Deutsch'), });
`Ich${_}love${_}Deutsch`
); run('urlsafeSlugProcessor - unicode', it => {
}); it('should translate symbols to English', () => {
it('should remove emoji', () => { assert.equal(
assert.equal( urlsafeSlugProcessor('Ich ♥ Deutsch'),
urlsafeSlugProcessor('Ich 😍 Deutsch'), `Ich${_}love${_}Deutsch`
`Ich${_}Deutsch` );
); });
}); it('should remove emoji', () => {
}); assert.equal(
describe('cyricllic', () => { urlsafeSlugProcessor('Ich 😍 Deutsch'),
it('space separated words', () => { `Ich${_}Deutsch`
assert.equal( );
urlsafeSlugProcessor('Всплытие и перехват событий'), });
`Vsplytie${_}i${_}perehvat${_}sobytij` });
);
}); run('urlsafeSlugProcessor -> cyrillic', it => {
it('numbered text', () => { it('space separated words', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('1 Всплытие и перехват событий'), urlsafeSlugProcessor('Всплытие и перехват событий'),
`1${_}Vsplytie${_}i${_}perehvat${_}sobytij` `Vsplytie${_}i${_}perehvat${_}sobytij`
); );
}); });
it('punctuated text', () => { it('numbered text', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('.Всплытие.и.перехват событий'), urlsafeSlugProcessor('1 Всплытие и перехват событий'),
`Vsplytie${_}i${_}perehvat${_}sobytij` `1${_}Vsplytie${_}i${_}perehvat${_}sobytij`
); );
}); });
it('text starting with the dollar sign', () => { it('punctuated text', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('$Всплытие $ перехват событий'), urlsafeSlugProcessor('.Всплытие.и.перехват событий'),
`$Vsplytie${_}$${_}perehvat${_}sobytij` `Vsplytie${_}i${_}perehvat${_}sobytij`
); );
}); });
it('text containing the dollar sign', () => { it('text starting with the dollar sign', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('Всплытие$перехват'), urlsafeSlugProcessor('$Всплытие $ перехват событий'),
`Vsplytie$perehvat` `$Vsplytie${_}$${_}perehvat${_}sobytij`
); );
}); });
it('text containing the equal char', () => { it('text containing the dollar sign', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('Всплытие = перехват=событий'), urlsafeSlugProcessor('Всплытие$перехват'),
`Vsplytie${_}perehvat${_}sobytij` `Vsplytie$perehvat`
); );
}); });
it('text containing the colon char', () => { it('text containing the equal char', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('Всплытие : перехват:событий'), urlsafeSlugProcessor('Всплытие = перехват=событий'),
`Vsplytie${_}perehvat${_}sobytij` `Vsplytie${_}perehvat${_}sobytij`
); );
}); });
it('text containing the slash char', () => { it('text containing the colon char', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('Всплытие / перехват/событий'), urlsafeSlugProcessor('Всплытие : перехват:событий'),
`Vsplytie${_}perehvat${_}sobytij` `Vsplytie${_}perehvat${_}sobytij`
); );
}); });
it('text containing the comma char', () => { it('text containing the slash char', () => {
assert.equal( assert.equal(
urlsafeSlugProcessor('Всплытие, перехват'), urlsafeSlugProcessor('Всплытие / перехват/событий'),
`Vsplytie${_}perehvat` `Vsplytie${_}perehvat${_}sobytij`
); );
}); });
}); it('text containing the comma char', () => {
describe('ascii + cyricllic', () => { assert.equal(
it('space separated words', () => { urlsafeSlugProcessor('Всплытие, перехват'),
assert.equal( `Vsplytie${_}perehvat`
urlsafeSlugProcessor('Всплытие и export перехват событий'), );
`Vsplytie${_}i${_}export${_}perehvat${_}sobytij` });
); });
});
it('ascii word concatenated to a cyricllic word', () => { run('urlsafeSlugProcessor -> ascii + cyrillic', it => {
assert.equal( it('space separated words', () => {
urlsafeSlugProcessor('exportВсплытие'), assert.equal(
'exportVsplytie' urlsafeSlugProcessor('Всплытие и export перехват событий'),
); `Vsplytie${_}i${_}export${_}perehvat${_}sobytij`
}); );
it('cyricllic word concatenated to an ascii word', () => { });
assert.equal( it('ascii word concatenated to a cyrillic word', () => {
urlsafeSlugProcessor('Всплытиеexport'), assert.equal(
`Vsplytieexport` urlsafeSlugProcessor('exportВсплытие'),
); 'exportVsplytie'
}); );
it('numbered text', () => { });
assert.equal( it('cyrillic word concatenated to an ascii word', () => {
urlsafeSlugProcessor('1 export Всплытие и перехват событий'), assert.equal(
`1${_}export${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('Всплытиеexport'),
); `Vsplytieexport`
}); );
it('punctuated text', () => { });
assert.equal( it('numbered text', () => {
urlsafeSlugProcessor('.Всплытие.export.и.перехват событий'), assert.equal(
`Vsplytie${_}export${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('1 export Всплытие и перехват событий'),
); `1${_}export${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
it('text starting with the dollar sign, followed by ascii char', () => { });
assert.equal( it('punctuated text', () => {
urlsafeSlugProcessor('$exportВсплытие перехват событий'), assert.equal(
`$exportVsplytie${_}perehvat${_}sobytij` urlsafeSlugProcessor('.Всплытие.export.и.перехват событий'),
); `Vsplytie${_}export${_}i${_}perehvat${_}sobytij`
}); );
it('text starting with the dollar sign, followed by unicode char', () => { });
assert.equal( it('text starting with the dollar sign, followed by ascii char', () => {
urlsafeSlugProcessor('$Всплытие export перехват событий'), assert.equal(
`$Vsplytie${_}export${_}perehvat${_}sobytij` urlsafeSlugProcessor('$exportВсплытие перехват событий'),
); `$exportVsplytie${_}perehvat${_}sobytij`
}); );
it('text containing the dollar sign, followed by ascii char', () => { });
assert.equal( it('text starting with the dollar sign, followed by unicode char', () => {
urlsafeSlugProcessor('export $destroy a component prop Всплытие и перехват событий'), assert.equal(
`export${_}$destroy${_}a${_}component${_}prop${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('$Всплытие export перехват событий'),
); `$Vsplytie${_}export${_}perehvat${_}sobytij`
}); );
it('text containing the dollar sign, followed by unicode char', () => { });
assert.equal( it('text containing the dollar sign, followed by ascii char', () => {
urlsafeSlugProcessor('Всплытие export $Всплытие a component prop Всплытие и перехват событий'), assert.equal(
`Vsplytie${_}export${_}$Vsplytie${_}a${_}component${_}prop${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('export $destroy a component prop Всплытие и перехват событий'),
); `export${_}$destroy${_}a${_}component${_}prop${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
it('text containing the equal char', () => { });
assert.equal( it('text containing the dollar sign, followed by unicode char', () => {
urlsafeSlugProcessor('script context=module Всплытие=и перехват событий'), assert.equal(
`script${_}context${_}module${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('Всплытие export $Всплытие a component prop Всплытие и перехват событий'),
); `Vsplytie${_}export${_}$Vsplytie${_}a${_}component${_}prop${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
it('text containing the colon char', () => { });
assert.equal( it('text containing the equal char', () => {
urlsafeSlugProcessor('svelte:body Всплытие и:перехват событий'), assert.equal(
`svelte${_}body${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('script context=module Всплытие=и перехват событий'),
); `script${_}context${_}module${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
it('text containing the slash char', () => { });
assert.equal( it('text containing the colon char', () => {
urlsafeSlugProcessor('svelte/motion Всплытие и / перехват/событий'), assert.equal(
`svelte${_}motion${_}Vsplytie${_}i${_}perehvat${_}sobytij` urlsafeSlugProcessor('svelte:body Всплытие и:перехват событий'),
); `svelte${_}body${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
it('text containing the comma char', () => { });
assert.equal( it('text containing the slash char', () => {
urlsafeSlugProcessor('Всплытие, export'), assert.equal(
`Vsplytie${_}export` urlsafeSlugProcessor('svelte/motion Всплытие и / перехват/событий'),
); `svelte${_}motion${_}Vsplytie${_}i${_}perehvat${_}sobytij`
}); );
}); });
it('text containing the comma char', () => {
assert.equal(
urlsafeSlugProcessor('Всплытие, export'),
`Vsplytie${_}export`
);
}); });
});
run('unicodeSafeProcessor (preserve unicode) -> ascii', it => {
it('space separated words', () => {
assert.equal(
unicodeSafeProcessor('Text expressions'),
`Text${_}expressions`
);
});
it('numbered text', () => {
assert.equal(
unicodeSafeProcessor('1. export creates'),
`1${_}export${_}creates`
);
});
it('punctuated text', () => {
assert.equal(
unicodeSafeProcessor('svelte.VERSION'),
`svelte${_}VERSION`
);
});
it('text starting with the dollar sign', () => {
assert.equal(
unicodeSafeProcessor('$destroy method'),
`$destroy${_}method`
);
});
it('numbered text containing the dollar sign', () => {
assert.equal(
unicodeSafeProcessor('1. export $destroy'),
`1${_}export${_}$destroy`
);
});
it('text containing the equal char', () => {
assert.equal(
unicodeSafeProcessor('script context=module'),
`script${_}context${_}module`
);
});
it('text containing the colon char', () => {
assert.equal(
unicodeSafeProcessor('svelte:body'),
`svelte${_}body`
);
});
it('text containing the slash char', () => {
assert.equal(
unicodeSafeProcessor('svelte/motion'),
`svelte${_}motion`
);
});
it('text containing the comma char', () => {
assert.equal(
unicodeSafeProcessor('svelte, motion'),
`svelte${_}motion`
);
});
});
run('unicodeSafeProcessor (preserve unicode) -> unicode', it => {
it('should preserve symbols', () => {
assert.equal(
unicodeSafeProcessor('Ich ♥ Deutsch'),
`Ich${_}love${_}Deutsch`
);
});
it('should remove emoji', () => {
assert.equal(
unicodeSafeProcessor('Ich 😍 Deutsch'),
`Ich${_}Deutsch`
);
});
});
describe('unicodeSafeProcessor (preserve unicode)', () => { run('unicodeSafeProcessor (preserve unicode) -> cyrillic', it => {
describe('ascii', () => { it('space separated words', () => {
it('space separated words', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие и перехват событий'),
unicodeSafeProcessor('Text expressions'), `Всплытие${_}и${_}перехват${_}событий`
`Text${_}expressions` );
); });
}); it('numbered text', () => {
it('numbered text', () => { assert.equal(
assert.equal( unicodeSafeProcessor('1 Всплытие и перехват событий'),
unicodeSafeProcessor('1. export creates'), `1${_}Всплытие${_}и${_}перехват${_}событий`
`1${_}export${_}creates` );
); });
}); it('punctuated text', () => {
it('punctuated text', () => { assert.equal(
assert.equal( unicodeSafeProcessor('.Всплытие.и.перехват событий'),
unicodeSafeProcessor('svelte.VERSION'), `Всплытие${_}и${_}перехват${_}событий`
`svelte${_}VERSION` );
); });
}); it('text starting with the dollar sign', () => {
it('text starting with the dollar sign', () => { assert.equal(
assert.equal( unicodeSafeProcessor('$Всплытие $ перехват событий'),
unicodeSafeProcessor('$destroy method'), `$${_}Всплытие${_}$${_}перехват${_}событий`
`$destroy${_}method` );
); });
}); it('text containing the dollar sign', () => {
it('numbered text containing the dollar sign', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие$перехват'),
unicodeSafeProcessor('1. export $destroy'), `Всплытие${_}$${_}перехват`
`1${_}export${_}$destroy` );
); });
}); it('text containing the equal char', () => {
it('text containing the equal char', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие = перехват=событий'),
unicodeSafeProcessor('script context=module'), `Всплытие${_}перехват${_}событий`
`script${_}context${_}module` );
); });
}); it('text containing the colon char', () => {
it('text containing the colon char', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие : перехват:событий'),
unicodeSafeProcessor('svelte:body'), `Всплытие${_}перехват${_}событий`
`svelte${_}body` );
); });
}); it('text containing the slash char', () => {
it('text containing the slash char', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие / перехват/событий'),
unicodeSafeProcessor('svelte/motion'), `Всплытие${_}перехват${_}событий`
`svelte${_}motion` );
); });
}); it('text containing the comma char', () => {
it('text containing the comma char', () => { assert.equal(
assert.equal( unicodeSafeProcessor('Всплытие, перехват'),
unicodeSafeProcessor('svelte, motion'), `Всплытие${_}перехват`
`svelte${_}motion` );
); });
}); });
});
describe('unicode', () => { run('unicodeSafeProcessor (preserve unicode) -> ascii + cyrillic', it => {
it('should preserve symbols', () => { it('space separated words', () => {
assert.equal( assert.equal(
unicodeSafeProcessor('Ich ♥ Deutsch'), unicodeSafeProcessor('Всплытие и export перехват событий'),
`Ich${_}love${_}Deutsch` `Всплытие${_}и${_}export${_}перехват${_}событий`
); );
}); });
it('should remove emoji', () => { it('ascii word concatenated to a cyrillic word', () => {
assert.equal( assert.equal(
unicodeSafeProcessor('Ich 😍 Deutsch'), unicodeSafeProcessor('exportВсплытие'),
`Ich${_}Deutsch` `export${_}Всплытие`
); );
}); });
}); it('cyrillic word concatenated to an ascii word', () => {
describe('cyricllic', () => { assert.equal(
it('space separated words', () => { unicodeSafeProcessor('Всплытиеexport'),
assert.equal( `Всплытие${_}export`
unicodeSafeProcessor('Всплытие и перехват событий'), );
`Всплытие${_}и${_}перехват${_}событий` });
); it('numbered text', () => {
}); assert.equal(
it('numbered text', () => { unicodeSafeProcessor('1 export Всплытие и перехват событий'),
assert.equal( `1${_}export${_}Всплытие${_}и${_}перехват${_}событий`
unicodeSafeProcessor('1 Всплытие и перехват событий'), );
`1${_}Всплытие${_}и${_}перехват${_}событий` });
); it('punctuated text', () => {
}); assert.equal(
it('punctuated text', () => { unicodeSafeProcessor('.Всплытие.export.и.перехват событий'),
assert.equal( `Всплытие${_}export${_}и${_}перехват${_}событий`
unicodeSafeProcessor('.Всплытие.и.перехват событий'), );
`Всплытие${_}и${_}перехват${_}событий` });
); it('text starting with the dollar sign, followed by ascii char', () => {
}); assert.equal(
it('text starting with the dollar sign', () => { unicodeSafeProcessor('$exportВсплытие перехват событий'),
assert.equal( `$export${_}Всплытие${_}перехват${_}событий`
unicodeSafeProcessor('$Всплытие $ перехват событий'), );
`$${_}Всплытие${_}$${_}перехват${_}событий` });
); it('text starting with the dollar sign, followed by unicode char', () => {
}); assert.equal(
it('text containing the dollar sign', () => { unicodeSafeProcessor('$Всплытие export перехват событий'),
assert.equal( `$${_}Всплытие${_}export${_}перехват${_}событий`
unicodeSafeProcessor('Всплытие$перехват'), );
`Всплытие${_}$${_}перехват` });
); it('text containing the dollar sign, followed by ascii char', () => {
}); assert.equal(
it('text containing the equal char', () => { unicodeSafeProcessor('export $destroy a component prop Всплытие и перехват событий'),
assert.equal( `export${_}$destroy${_}a${_}component${_}prop${_}Всплытие${_}и${_}перехват${_}событий`
unicodeSafeProcessor('Всплытие = перехват=событий'), );
`Всплытие${_}перехват${_}событий` });
); it('text containing the dollar sign, followed by unicode char', () => {
}); assert.equal(
it('text containing the colon char', () => { unicodeSafeProcessor('Всплытие export $Всплытие a component prop Всплытие и перехват событий'),
assert.equal( `Всплытие${_}export${_}$${_}Всплытие${_}a${_}component${_}prop${_}Всплытие${_}и${_}перехват${_}событий`
unicodeSafeProcessor('Всплытие : перехват:событий'), );
`Всплытие${_}перехват${_}событий` });
); it('text containing the equal char', () => {
}); assert.equal(
it('text containing the slash char', () => { unicodeSafeProcessor('script context=module Всплытие=и перехват событий'),
assert.equal( `script${_}context${_}module${_}Всплытие${_}и${_}перехват${_}событий`
unicodeSafeProcessor('Всплытие / перехват/событий'), );
`Всплытие${_}перехват${_}событий` });
); it('text containing the colon char', () => {
}); assert.equal(
it('text containing the comma char', () => { unicodeSafeProcessor('svelte:body Всплытие и:перехват событий'),
assert.equal( `svelte${_}body${_}Всплытие${_}и${_}перехват${_}событий`
unicodeSafeProcessor('Всплытие, перехват'), );
`Всплытие${_}перехват` });
); it('text containing the slash char', () => {
}); assert.equal(
}); unicodeSafeProcessor('svelte/motion Всплытие и / перехват/событий'),
describe('ascii + cyricllic', () => { `svelte${_}motion${_}Всплытие${_}и${_}перехват${_}событий`
it('space separated words', () => { );
assert.equal( });
unicodeSafeProcessor('Всплытие и export перехват событий'), it('text containing the comma char', () => {
`Всплытие${_}и${_}export${_}перехват${_}событий` assert.equal(
); unicodeSafeProcessor('Всплытие, export'),
}); `Всплытие${_}export`
it('ascii word concatenated to a cyricllic word', () => { );
assert.equal(
unicodeSafeProcessor('exportВсплытие'),
`export${_}Всплытие`
);
});
it('cyricllic word concatenated to an ascii word', () => {
assert.equal(
unicodeSafeProcessor('Всплытиеexport'),
`Всплытие${_}export`
);
});
it('numbered text', () => {
assert.equal(
unicodeSafeProcessor('1 export Всплытие и перехват событий'),
`1${_}export${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('punctuated text', () => {
assert.equal(
unicodeSafeProcessor('.Всплытие.export.и.перехват событий'),
`Всплытие${_}export${_}и${_}перехват${_}событий`
);
});
it('text starting with the dollar sign, followed by ascii char', () => {
assert.equal(
unicodeSafeProcessor('$exportВсплытие перехват событий'),
`$export${_}Всплытие${_}перехват${_}событий`
);
});
it('text starting with the dollar sign, followed by unicode char', () => {
assert.equal(
unicodeSafeProcessor('$Всплытие export перехват событий'),
`$${_}Всплытие${_}export${_}перехват${_}событий`
);
});
it('text containing the dollar sign, followed by ascii char', () => {
assert.equal(
unicodeSafeProcessor('export $destroy a component prop Всплытие и перехват событий'),
`export${_}$destroy${_}a${_}component${_}prop${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('text containing the dollar sign, followed by unicode char', () => {
assert.equal(
unicodeSafeProcessor('Всплытие export $Всплытие a component prop Всплытие и перехват событий'),
`Всплытие${_}export${_}$${_}Всплытие${_}a${_}component${_}prop${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('text containing the equal char', () => {
assert.equal(
unicodeSafeProcessor('script context=module Всплытие=и перехват событий'),
`script${_}context${_}module${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('text containing the colon char', () => {
assert.equal(
unicodeSafeProcessor('svelte:body Всплытие и:перехват событий'),
`svelte${_}body${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('text containing the slash char', () => {
assert.equal(
unicodeSafeProcessor('svelte/motion Всплытие и / перехват/событий'),
`svelte${_}motion${_}Всплытие${_}и${_}перехват${_}событий`
);
});
it('text containing the comma char', () => {
assert.equal(
unicodeSafeProcessor('Всплытие, export'),
`Всплытие${_}export`
);
});
});
}); });
}); });

Loading…
Cancel
Save