Merge pull request #2680 from sveltejs/site/database-with-fallback

add database entries for new gists, update REPL URLs
pull/2572/head
Rich Harris 6 years ago committed by GitHub
commit d654b34959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,7 +6,7 @@ exports.up = DB => {
name character varying(255),
username character varying(255) not null,
avatar text,
github_token character varying(255) not null,
github_token character varying(255),
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone
);

@ -1360,6 +1360,11 @@
}
}
},
"@polka/redirect": {
"version": "1.0.0-next.0",
"resolved": "https://registry.npmjs.org/@polka/redirect/-/redirect-1.0.0-next.0.tgz",
"integrity": "sha512-ym6ooqMr09+cV+y52p5kszJ0jYcX+nJfm8POrQb7QYowvpPPuneZ71EclHrQSB7a50lcytgR/xtL6AUFdvyEkg=="
},
"@polka/send": {
"version": "1.0.0-next.2",
"resolved": "https://registry.npmjs.org/@polka/send/-/send-1.0.0-next.2.tgz",

@ -15,6 +15,7 @@
"testsrc": "mocha -r esm test/**"
},
"dependencies": {
"@polka/redirect": "^1.0.0-next.0",
"@polka/send": "^1.0.0-next.2",
"devalue": "^1.1.0",
"do-not-zip": "^1.0.0",

@ -28,7 +28,7 @@
}
if (gist) {
fetch(`gist/${gist}`).then(r => r.json()).then(data => {
fetch(`repl/${gist}.json`).then(r => r.json()).then(data => {
const { id, description, files } = data;
name = description;

@ -6,16 +6,16 @@ const cache = new Map();
export function get(req, res) {
const { slug } = req.params;
try {
let example = cache.get(slug);
if (!example || process.env.NODE_ENV !== 'production') {
example = get_example(slug);
cache.set(slug, example);
if (example) cache.set(slug, example);
}
if (example) {
send(res, 200, example);
} catch (err) {
} else {
send(res, 404, {
error: 'not found'
});

@ -35,7 +35,7 @@ export function get_example(slug) {
const dir = lookup.get(slug);
const title = titles.get(slug);
if (!dir || !title) throw { status: 404, message: 'not found' };
if (!dir || !title) return null;
const files = fs.readdirSync(`content/examples/${dir}`)
.filter(name => name[0] !== '.' && name !== 'meta.json')

@ -1,65 +0,0 @@
import send from '@polka/send';
import { body } from './_utils.js';
import { query } from '../../utils/db';
import { isUser } from '../../backend/auth';
export async function get(req, res) {
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
`, [req.params.id]); // via filename pattern
if (!row) {
return send(res, 404, { error: 'Gist not found' });
}
send(res, 200, {
uid: row.uid,
name: row.name,
files: row.files,
owner: row.owner
});
}
export async function patch(req, res) {
const user = await isUser(req, res);
if (!user) return; // response already sent
let id, uid=req.params.id;
try {
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.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' });
id = row.id;
} catch (err) {
console.error('PATCH /gists @ select', err);
return send(res, 500);
}
try {
const obj = await body(req);
obj.updated_at = 'now()';
let k, cols=[], vals=[];
for (k in obj) {
cols.push(k);
vals.push(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);
send(res, 200, {
uid: row.uid,
name: row.name,
files: row.files,
owner: user.uid,
});
} catch (err) {
console.error('PATCH /gists @ update', err);
send(res, 500, { error: err.message });
}
}

@ -1,5 +1,5 @@
<script>
import { user, logout } from '../../../../user.js';
import { user, logout } from '../../../../../user.js';
let showMenu = false;
let name;

@ -3,10 +3,10 @@
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 { user } from '../../../../user.js';
import { enter } from '../../../../utils/events.js';
import { isMac } from '../../../../utils/compat.js';
import downloadBlob from '../../../_utils/downloadBlob.js';
import { user } from '../../../../../user.js';
import { enter } from '../../../../../utils/events.js';
import { isMac } from '../../../../../utils/compat.js';
const dispatch = createEventDispatcher();
@ -54,12 +54,15 @@
const { components } = repl.toJSON();
try {
const r = await fetch(`gist/create`, {
const r = await fetch(`repl/create.json`, {
method: 'POST',
headers: { Authorization },
body: JSON.stringify({
name,
components
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
@ -107,16 +110,16 @@
const files = {};
const { components } = repl.toJSON();
components.forEach(module => {
const text = module.source.trim();
if (!text.length) return; // skip empty file
files[`${module.name}.${module.type}`] = text;
});
const r = await fetch(`gist/${gist.uid}`, {
const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH',
headers: { Authorization },
body: JSON.stringify({ name, files })
body: JSON.stringify({
name,
files: components.map(component => ({
name: `${component.name}.${component.type}`,
source: component.source
}))
})
});
if (r.status < 200 || r.status >= 300) {
@ -197,8 +200,7 @@ export default app;` });
<Icon name="download" />
</button>
{#if $user}
<button class="icon" disabled="{saving || !$user}" on:click={fork} title="fork">
<button class="icon" disabled="{saving || !$user}" on:click={() => fork(false)} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
@ -213,13 +215,6 @@ export default app;` });
<Icon name="save" />
{/if}
</button>
{/if}
{#if gist}
<a class="icon no-underline" href={gist.html_url} title="link to gist">
<Icon name="link" />
</a>
{/if}
{#if $user}
<UserMenu />

@ -0,0 +1,135 @@
import send from '@polka/send';
import redirect from '@polka/redirect';
import body from '../_utils/body.js';
import * as httpie from 'httpie';
import { query, find } from '../../../utils/db';
import { isUser } from '../../../backend/auth';
import { get_example } from '../../examples/_examples.js';
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
async function import_gist(req, res) {
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 query(`
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...
const [gist] = await query(`
insert into gists(uid, user_id, name, files)
values ($1, $2, $3, $4) returning *`, [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?
const example = get_example(req.params.id);
if (example) {
return send(res, 200, {
relaxed: true,
uid: req.params.id,
name: example.title,
files: example.files,
owner: null
});
}
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
`, [req.params.id]); // via filename pattern
if (!row) {
return import_gist(req, res);
}
send(res, 200, {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: row.owner
});
}
export async function patch(req, res) {
const user = await isUser(req, res);
if (!user) return; // response already sent
let id, uid=req.params.id;
try {
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.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' });
id = row.id;
} catch (err) {
console.error('PATCH /gists @ select', err);
return send(res, 500);
}
try {
const obj = await body(req);
obj.updated_at = 'now()';
let k, cols=[], 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);
send(res, 200, {
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,
});
} catch (err) {
console.error('PATCH /gists @ update', err);
send(res, 500, { error: err.message });
}
}

@ -0,0 +1,223 @@
<script context="module">
export function preload({ params, query }) {
return {
version: query.version || '3',
id: params.id
};
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import { process_example } from '../../../utils/examples';
import { user } from '../../../user.js';
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 = process.browser ? window.innerWidth : 1000;
let checked = false;
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 rgx = /(js|svelte)$/i;
const components = data.files.map(file => {
let [name, type] = file.name.split('.');
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 (process.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}`);
}
$: svelteUrl = process.browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
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;
$: relaxed = is_relaxed_gist || ($user && gist && $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;
}
.viewport {
width: 100%;
height: 100%;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
}
.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;
}
.pane { width: 100%; height: 100% }
.loading {
text-align: center;
color: var(--second);
font-weight: 400;
margin: 2em 0 0 0;
opacity: 0;
animation: fade-in .4s;
animation-delay: .2s;
animation-fill-mode: both;
}
@keyframes fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
.input {
padding: 2.4em 0 0 0;
}
</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
on:forked={handle_fork}
/>
{#if process.browser}
<div class="viewport" class:offset={checked}>
<Repl
bind:this={repl}
{svelteUrl}
{rollupUrl}
{relaxed}
fixed={mobile}
injectedJS={mapbox_setup}
/>
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
{/if}
</div>

@ -1,4 +1,4 @@
export function body(req) {
export default function body(req) {
return new Promise((fulfil, reject) => {
let str = '';

@ -1,5 +1,5 @@
import send from '@polka/send';
import { body } from './_utils.js';
import body from './_utils/body.js';
import { query } from '../../utils/db';
import { isUser } from '../../backend/auth';
@ -8,21 +8,14 @@ export async function post(req, res) {
if (!user) return; // response already sent
try {
const { name, components } = await body(req);
const files = {};
components.forEach(component => {
const text = component.source.trim();
if (!text.length) return; // skip empty file
files[`${component.name}.${component.type}`] = text;
});
const { name, files } = await body(req);
const [row] = await query(`
insert into gists(user_id, name, files)
values ($1, $2, $3) returning *`, [user.id, name, files]);
values ($1, $2, $3) returning *`, [user.id, name, JSON.stringify(files)]);
send(res, 201, {
uid: row.uid,
uid: row.uid.replace(/-/g, ''),
name: row.name,
files: row.files,
owner: user.uid,

@ -1,238 +1,16 @@
<script context="module">
export function preload({ query }) {
if (/^[^>]?[12]/.test(query.version)) {
const search = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
return this.redirect(302, `https://v2.svelte.dev/repl?${search}`);
}
const { gist, example, version } = query;
return {
version: query.version || '3',
gist_id: query.gist,
example: query.example || 'hello-world'
};
// redirect to v2 REPL if appropriate
if (/^[^>]?[12]/.test(version)) {
const q = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
return this.redirect(302, `https://v2.svelte.dev/repl?${q}`);
}
</script>
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { process_example } from '../../utils/examples';
import InputOutputToggle from '../../components/Repl/InputOutputToggle.svelte';
import AppControls from './_components/AppControls/index.svelte';
export let version;
export let example;
export let gist_id;
let repl;
let gist;
let name = 'Loading...';
let zen_mode = false;
let relaxed = false;
let width = process.browser ? window.innerWidth : 1000;
let checked = false;
$: if (typeof history !== 'undefined') {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
if (gist_id) params.push(`gist=${gist_id}`);
else if (example) params.push(`example=${example}`);
const url = params.length > 0
? `repl?${params.join('&')}`
: 'repl';
const id = gist || example || 'hello-world';
const q = version ? `?version=${version}` : ``;
history.replaceState({}, 'x', url);
this.redirect(301, `repl/${id}${q}`);
}
onMount(() => {
if (version !== 'local') {
fetch(`https://unpkg.com/svelte@${version || '3'}/package.json`)
.then(r => r.json())
.then(pkg => {
version = pkg.version;
});
}
if (gist_id) {
relaxed = false;
fetch(`gist/${gist_id}`).then(r => {
if (r.ok) {
r.json().then(data => {
gist = data;
name = data.name;
const components = [];
const files = data.files;
const rgx = /(js|svelte)$/i;
Object.keys(files).forEach(key => {
let [name, type] = key.split('.');
if (type === 'html') type = 'svelte';
if (rgx.test(type)) {
components.push({ name, type, source:files[key] });
}
});
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')
}
});
} else {
relaxed = true;
fetch(`examples/${example}.json`).then(async response => {
if (response.ok) {
const data = await response.json();
name = data.title;
const components = process_example(data.files);
repl.set({ components });
gist = null;
}
});
}
});
function handle_fork(event) {
example = null;
console.log('> handle_fork', event);
gist = event.detail.gist;
gist_id = gist.uid;
}
$: svelteUrl = process.browser && version === 'local' ?
`${location.origin}/repl/local` :
`https://unpkg.com/svelte@${version}`;
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;
</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;
}
.viewport {
width: 100%;
height: 100%;
}
.mobile .viewport {
width: 200%;
height: calc(100% - 42px);
transition: transform 0.3s;
}
.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;
}
.pane { width: 100%; height: 100% }
.loading {
text-align: center;
color: var(--second);
font-weight: 400;
margin: 2em 0 0 0;
opacity: 0;
animation: fade-in .4s;
animation-delay: .2s;
animation-fill-mode: both;
}
@keyframes fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
.input {
padding: 2.4em 0 0 0;
}
</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
on:forked={handle_fork}
/>
{#if process.browser}
<div class="viewport" class:offset={checked}>
<Repl
bind:this={repl}
{svelteUrl}
{rollupUrl}
{relaxed}
fixed={mobile}
injectedJS={mapbox_setup}
/>
</div>
{#if mobile}
<InputOutputToggle bind:checked/>
{/if}
{/if}
</div>

Loading…
Cancel
Save