Merge branch 'master' of https://github.com/sveltejs/svelte into multi-class-comma

pull/3419/head
Marcelo Junior 6 years ago
commit d35f316030

@ -1,5 +1,20 @@
# Svelte changelog # Svelte changelog
## 3.9.1
* Only update style properties if necessary ([#3433](https://github.com/sveltejs/svelte/issues/3433))
* Only update if/await blocks if necessary ([#2355](https://github.com/sveltejs/svelte/issues/2355))
* Set context correctly inside await blocks ([#2443](https://github.com/sveltejs/svelte/issues/2443))
* Handle `!important` inline styles ([#1834](https://github.com/sveltejs/svelte/issues/1834))
* Make index references reactive in event handlers inside keyed each blocks ([#2569](https://github.com/sveltejs/svelte/issues/2569))
## 3.9.0
* Support `is` attribute on elements, with a warning ([#3182](https://github.com/sveltejs/svelte/issues/3182))
* Handle missing slot prop ([#3322](https://github.com/sveltejs/svelte/issues/3322))
* Don't set undefined/null input values, unless previous value exists ([#1233](https://github.com/sveltejs/svelte/issues/1233))
* Fix style attribute optimisation bailout ([#1830](https://github.com/sveltejs/svelte/issues/1830))
## 3.8.1 ## 3.8.1
* Set SVG namespace for slotted elements ([#3321](https://github.com/sveltejs/svelte/issues/3321)) * Set SVG namespace for slotted elements ([#3321](https://github.com/sveltejs/svelte/issues/3321))

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.8.1", "version": "3.9.1",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -98,6 +98,5 @@
], ],
"sourceMap": true, "sourceMap": true,
"instrument": true "instrument": true
}, }
"dependencies": {}
} }

@ -6,7 +6,3 @@ DATABASE_URL=
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
MAPBOX_ACCESS_TOKEN= MAPBOX_ACCESS_TOKEN=
JWT_EXP=30d
JWT_ALG=HS512
JWT_KEY=

@ -907,7 +907,68 @@ app.count += 1;
### Custom element API ### Custom element API
* TODO ---
Svelte components can also be compiled to custom elements (aka web components) using the `customElements: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](docs#svelte_options).
```html
<svelte:options tag="my-element">
<script>
export let name = 'world';
</script>
<h1>Hello {name}!</h1>
<slot></slot>
```
---
Alternatively, use `tag={null}` to indicate that the consumer of the custom element should name it.
```js
import MyElement from './MyElement.svelte';
customElements.define('my-element', MyElement);
```
---
Once a custom element has been defined, it can be used as a regular DOM element:
```js
document.body.innerHTML = `
<my-element>
<p>This is some slotted content</p>
</my-element>
`;
```
---
By default, custom elements are compiled with `accessors: true`, which means that any [props](docs#Attributes_and_props) are exposed as properties of the DOM element (as well as being readable/writable as attributes, where possible).
To prevent this, add `accessors={false}` to `<svelte:options>`.
```js
const el = document.querySelector('my-element');
// get the current value of the 'name' prop
console.log(el.name);
// set a new value, updating the shadow DOM
el.name = 'everybody';
```
Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as [most frameworks](https://custom-elements-everywhere.com/). There are, however, some important differences to be aware of:
* Styles are *encapsulated*, rather than merely *scoped*. This means that any non-component styles (such as you might have in a `global.css` file) will not apply to the custom element, including styles with the `:global(...)` modifier
* Instead of being extracted out as a separate .css file, styles are inlined into the component as a JavaScript string
* Custom elements are not generally suitable for server-side rendering, as the shadow DOM is invisible until JavaScript loads
* In Svelte, slotted content renders *lazily*. In the DOM, it renders *eagerly*. In other words, it will always be created even if the component's `<slot>` element is inside an `{#if ...}` block. Similarly, including a `<slot>` in an `{#each ...}` block will not cause the slotted content to be rendered multiple times
* The `let:` directive has no effect
* Polyfills are required to support older browsers
### Server-side component API ### Server-side component API

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

@ -1647,6 +1647,11 @@
"safe-buffer": "~5.1.1" "safe-buffer": "~5.1.1"
} }
}, },
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"core-js": { "core-js": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
@ -1906,6 +1911,11 @@
"is-buffer": "~2.0.3" "is-buffer": "~2.0.3"
} }
}, },
"flru": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz",
"integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog=="
},
"for-each": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",

@ -15,8 +15,10 @@
"dependencies": { "dependencies": {
"@polka/redirect": "^1.0.0-next.0", "@polka/redirect": "^1.0.0-next.0",
"@polka/send": "^1.0.0-next.3", "@polka/send": "^1.0.0-next.3",
"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",
"httpie": "^1.1.2", "httpie": "^1.1.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"marked": "^0.7.0", "marked": "^0.7.0",

@ -1,150 +0,0 @@
import polka from 'polka';
import devalue from 'devalue';
import send from '@polka/send';
import { get, post } from 'httpie';
import { parse, stringify } from 'querystring';
import { decode, sign, verify } from './token';
import { find, query } from '../utils/db';
const {
BASEURL,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
} = process.env;
const OAuth = 'https://github.com/login/oauth';
function exit(res, code, msg='') {
const error = msg.charAt(0).toUpperCase() + msg.substring(1);
send(res, code, { error });
}
function onError(err, req, res) {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
}
/**
* Middleware to determine User validity
*/
export async function isUser(req, res) {
const abort = exit.bind(null, res, 401);
const auth = req.headers.authorization;
if (!auth) return abort('Missing Authorization header');
const [scheme, token] = auth.split(' ');
if (scheme !== 'Bearer' || !token) return abort('Invalid Authorization format');
let data;
const decoded = decode(token, { complete:true });
if (!decoded || !decoded.header) return abort('Invalid token');
try {
data = await verify(token);
} catch (err) {
return abort(err.message);
}
const { uid, username } = data;
if (!uid || !username) return abort('Invalid token payload');
try {
const row = await find(`select * from users where uid = $1 and username = $2 limit 1`, [uid, username]);
return row || abort('Invalid token');
} catch (err) {
console.error('Auth.isUser', err);
return send(res, 500, 'Unknown error occurred');
}
}
export function toUser(obj={}) {
const { uid, username, name, avatar } = obj;
const token = sign({ uid, username });
return { uid, username, name, avatar, token };
}
export function API() {
const app = polka({ onError });
if (GITHUB_CLIENT_ID) {
app.get('/auth/login', (req, res) => {
try {
const Location = `${OAuth}/authorize?` + stringify({
scope: 'read:user',
client_id: GITHUB_CLIENT_ID,
redirect_uri: `${BASEURL}/auth/callback`,
});
send(res, 302, Location, { Location });
} catch (err) {
console.error('GET /auth/login', err);
send(res, 500);
}
});
app.get('/auth/callback', async (req, res) => {
try {
// Trade "code" for "access_token"
const r1 = await post(`${OAuth}/access_token?` + stringify({
code: req.query.code,
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
}));
// Now fetch User details
const { access_token } = parse(r1.data);
const r2 = await get('https://api.github.com/user', {
headers: {
'User-Agent': 'svelte.dev',
Authorization: `token ${access_token}`
}
});
const { id, name, avatar_url, login } = r2.data;
// Upsert `users` table
const [user] = await query(`
insert into users(uid, name, username, avatar, github_token)
values ($1, $2, $3, $4, $5) on conflict (uid) do update
set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now())
returning *
`, [id, name, login, avatar_url, access_token]);
send(res, 200, `
<script>
window.opener.postMessage({
user: ${devalue(toUser(user))}
}, window.location.origin);
</script>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
} catch (err) {
console.error('GET /auth/callback', err);
send(res, 500, err.data, {
'Content-Type': err.headers['content-type'],
'Content-Length': err.headers['content-length']
});
}
});
} else {
// Print "Misconfigured" error
app.get('/auth/login', (req, res) => {
send(res, 500, `
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
});
}
return app;
}

@ -1,22 +0,0 @@
import * as jwt from 'jsonwebtoken';
const { JWT_KEY, JWT_ALG, JWT_EXP } = process.env;
const CONFIG = {
expiresIn: JWT_EXP,
issuer: 'https://svelte.dev',
audience: 'https://svelte.dev',
algorithm: JWT_ALG,
};
export const decode = jwt.decode;
export function sign(obj, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.sign(obj, JWT_KEY, opts, cb);
}
export function verify(str, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.verify(str, JWT_KEY, opts, cb);
}

@ -53,6 +53,7 @@
<a target="_blank" rel="noopener" href="https://deck.nl"><img src="organisations/deck.svg" alt="Deck logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://deck.nl"><img src="organisations/deck.svg" alt="Deck logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://from-now-on.com"><img src="organisations/from-now-on.png" alt="From-Now-On logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://from-now-on.com"><img src="organisations/from-now-on.png" alt="From-Now-On logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://godaddy.com"><img src="organisations/godaddy.svg" alt="GoDaddy logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://godaddy.com"><img src="organisations/godaddy.svg" alt="GoDaddy logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="http://healthtree.org/"><img src="organisations/healthtree.png" alt="HealthTree logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://itslearning.com"><img src="organisations/itslearning.svg" alt="itslearning logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://itslearning.com"><img src="organisations/itslearning.svg" alt="itslearning logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="http://mustlab.ru"><img src="organisations/mustlab.png" alt="Mustlab logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="http://mustlab.ru"><img src="organisations/mustlab.png" alt="Mustlab logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://www.nesta.org.uk"><img src="organisations/nesta.svg" alt="Nesta logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://www.nesta.org.uk"><img src="organisations/nesta.svg" alt="Nesta logo" loading="lazy"></a>
@ -69,6 +70,6 @@
<a target="_blank" rel="noopener" href="https://thunderdome.dev"><img src="organisations/thunderdome.svg" alt="Thunderdome logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://thunderdome.dev"><img src="organisations/thunderdome.svg" alt="Thunderdome logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://m.tokopedia.com"><img src="organisations/tokopedia.png" alt="Tokopedia logo" srcset="organisations/tokopedia.2x.png 2x, organisations/tokopedia.3x.png 3x" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://m.tokopedia.com"><img src="organisations/tokopedia.png" alt="Tokopedia logo" srcset="organisations/tokopedia.2x.png 2x, organisations/tokopedia.3x.png 3x" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://webdesq.net"><img src="organisations/webdesq.svg" alt="Webdesq logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://webdesq.net"><img src="organisations/webdesq.svg" alt="Webdesq logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="http://healthtree.org/"><img src="organisations/healthtree.png" alt="HealthTree logo" loading="lazy"></a> <a target="_blank" rel="noopener" href="https://zevvle.com/"><img src="organisations/zevvle.svg" alt="Zevvle logo" loading="lazy"></a>
<a target="_blank" rel="noopener" href="https://github.com/sveltejs/svelte/blob/master/site/src/routes/_components/WhosUsingSvelte.svelte" class="add-yourself"><span>+ your company?</span></a> <a target="_blank" rel="noopener" href="https://github.com/sveltejs/svelte/blob/master/site/src/routes/_components/WhosUsingSvelte.svelte" class="add-yourself"><span>+ your company?</span></a>
</div> </div>

@ -1,11 +1,32 @@
<script> <script>
import { setContext } from 'svelte';
import { stores } from '@sapper/app'; import { stores } from '@sapper/app';
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';
const { page, preloading } = stores();
export let segment; export let segment;
const { page, preloading, session } = stores();
setContext('app', {
login: () => {
const login_window = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
window.addEventListener('message', function handler(event) {
login_window.close();
window.removeEventListener('message', handler);
$session.user = event.data.user;
});
},
logout: async () => {
const r = await fetch(`/auth/logout`, {
credentials: 'include'
});
if (r.ok) $session.user = null;
}
});
</script> </script>
<Icons/> <Icons/>

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

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

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

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

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

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

@ -1,8 +0,0 @@
import send from '@polka/send';
import { isUser, toUser } from '../../backend/auth';
export async function get(req, res) {
const user = await isUser(req, res);
res.setHeader('Cache-Control', 'private, no-cache, no-store');
return send(res, 200, user ? toUser(user) : null);
}

@ -1,18 +1,23 @@
<script> <script>
import { user, logout } from '../../../../../user.js'; import { getContext } from 'svelte';
import { stores } from '@sapper/app';
const { session } = stores();
const { logout } = getContext('app');
let showMenu = false; let showMenu = false;
let name; let name;
$: name = $user.name || $user.username; $: name = $session.user.name || $session.user.username;
</script> </script>
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}"> <div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
<span>{name}</span> <span>{name}</span>
<img alt="{name} avatar" src="{$user.avatar}"> <img alt="{name} avatar" src="{$session.user.avatar}">
{#if showMenu} {#if showMenu}
<div class="menu"> <div class="menu">
<a href="apps">Your saved apps</a>
<button on:click={logout}>Log out</button> <button on:click={logout}>Log out</button>
</div> </div>
{/if} {/if}
@ -64,7 +69,7 @@
.menu { .menu {
position: absolute; position: absolute;
width: calc(100% + 1.6rem); width: calc(100% + 1.6rem);
min-width: 6em; min-width: 10em;
top: 3rem; top: 3rem;
right: -1.6rem; right: -1.6rem;
background-color: var(--second); background-color: var(--second);
@ -72,17 +77,25 @@
z-index: 99; z-index: 99;
text-align: left; text-align: left;
border-radius: 0.4rem; border-radius: 0.4rem;
display: flex;
flex-direction: column;
} }
.menu button { .menu button, .menu a {
background-color: transparent; background-color: transparent;
font-family: var(--font); font-family: var(--font);
font-size: 1.6rem; font-size: 1.6rem;
/* opacity: 0.7; */ opacity: 0.7;
padding: 0.4rem 0;
text-decoration: none;
text-align: left;
border: none;
color: inherit;
} }
.menu button:hover { .menu button:hover, .menu a:hover {
opacity: 1; opacity: 1;
color: inherit;
} }
@media (min-width: 600px) { @media (min-width: 600px) {

@ -1,14 +1,16 @@
<script> <script>
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
import { stores } from '@sapper/app';
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';
import downloadBlob from '../../../_utils/downloadBlob.js'; import downloadBlob from '../../../_utils/downloadBlob.js';
import { user } from '../../../../../user.js';
import { enter } from '../../../../../utils/events.js'; import { enter } from '../../../../../utils/events.js';
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');
export let repl; export let repl;
export let gist; export let gist;
@ -25,8 +27,7 @@
return new Promise(f => setTimeout(f, ms)); return new Promise(f => setTimeout(f, ms));
} }
$: Authorization = $user && `Bearer ${$user.token}`; $: canSave = $session.user && gist && gist.owner === $session.user.uid;
$: canSave = $user && gist && gist.owner === $user.uid;
function handleKeydown(event) { function handleKeydown(event) {
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) { if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
@ -35,19 +36,6 @@
} }
} }
function login(event) {
event.preventDefault();
const loginWindow = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
const handleLogin = event => {
loginWindow.close();
user.set(event.data.user);
window.removeEventListener('message', handleLogin);
};
window.addEventListener('message', handleLogin);
}
async function fork(intentWasSave) { async function fork(intentWasSave) {
saving = true; saving = true;
@ -56,7 +44,7 @@
try { try {
const r = await fetch(`repl/create.json`, { const r = await fetch(`repl/create.json`, {
method: 'POST', method: 'POST',
headers: { Authorization }, credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
name, name,
files: components.map(component => ({ files: components.map(component => ({
@ -111,7 +99,7 @@
const r = await fetch(`repl/${gist.uid}.json`, { const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization }, credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
name, name,
files: components.map(component => ({ files: components.map(component => ({
@ -199,7 +187,7 @@ export default app;` });
<Icon name="download" /> <Icon name="download" />
</button> </button>
<button class="icon" disabled="{saving || !$user}" on:click={() => fork(false)} title="fork"> <button class="icon" disabled="{saving || !$session.user}" on:click={() => fork(false)} title="fork">
{#if justForked} {#if justForked}
<Icon name="check" /> <Icon name="check" />
{:else} {:else}
@ -207,7 +195,7 @@ export default app;` });
{/if} {/if}
</button> </button>
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save"> <button class="icon" disabled="{saving || !$session.user}" on:click={save} title="save">
{#if justSaved} {#if justSaved}
<Icon name="check" /> <Icon name="check" />
{:else} {:else}
@ -215,10 +203,10 @@ export default app;` });
{/if} {/if}
</button> </button>
{#if $user} {#if $session.user}
<UserMenu/> <UserMenu/>
{:else} {:else}
<button class="icon" on:click={login}> <button class="icon" on:click|preventDefault={login}>
<Icon name="log-in" /> <Icon name="log-in" />
<span>&nbsp;Log in to save</span> <span>&nbsp;Log in to save</span>
</button> </button>

@ -2,7 +2,6 @@ import send from '@polka/send';
import body from '../_utils/body.js'; 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 { isUser } from '../../../backend/auth';
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, GITHUB_CLIENT_SECRET } = process.env;
@ -24,7 +23,7 @@ async function import_gist(req, res) {
if (!user) { if (!user) {
const { id, name, login, avatar_url } = data.owner; const { id, name, login, avatar_url } = data.owner;
[user] = await query(` user = await find(`
insert into users(uid, name, username, avatar) insert into users(uid, name, username, avatar)
values ($1, $2, $3, $4) values ($1, $2, $3, $4)
returning * returning *
@ -44,9 +43,10 @@ async function import_gist(req, res) {
}); });
// add gist to database... // add gist to database...
const [gist] = await query(` await query(`
insert into gists(uid, user_id, name, files) insert into gists(uid, user_id, name, files)
values ($1, $2, $3, $4) returning *`, [req.params.id, user.id, data.description, JSON.stringify(files)]); values ($1, $2, $3, $4)
`, [req.params.id, user.id, data.description, JSON.stringify(files)]);
send(res, 200, { send(res, 200, {
uid: req.params.id, uid: req.params.id,
@ -92,8 +92,8 @@ export async function get(req, res) {
} }
export async function patch(req, res) { export async function patch(req, res) {
const user = await isUser(req, res); const { user } = req;
if (!user) return; // response already sent if (!user) return;
let id, uid = req.params.id; let id, uid = req.params.id;

@ -10,14 +10,15 @@
<script> <script>
import Repl from '@sveltejs/svelte-repl'; import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '@sapper/app'; import { goto, stores } from '@sapper/app';
import { user } from '../../../user.js';
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...';
@ -107,7 +108,7 @@
$: mobile = width < 540; $: mobile = width < 540;
$: relaxed = is_relaxed_gist || ($user && gist && $user.uid === gist.owner); $: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner);
</script> </script>
<style> <style>

@ -1,10 +1,9 @@
import send from '@polka/send'; import send from '@polka/send';
import body from './_utils/body.js'; import body from './_utils/body.js';
import { query } from '../../utils/db'; import { query } from '../../utils/db';
import { isUser } from '../../backend/auth';
export async function post(req, res) { export async function post(req, res) {
const user = await isUser(req, res); const { user } = req;
if (!user) return; // response already sent if (!user) return; // response already sent
try { try {

@ -1,11 +1,22 @@
import polka from 'polka';
import send from '@polka/send';
import sirv from 'sirv'; import sirv from 'sirv';
import * as sapper from '@sapper/server'; import * as sapper from '@sapper/server';
import { API } from './backend/auth'; import { sanitize_user, authenticate } from './utils/auth';
const { PORT = 3000 } = process.env; const { PORT = 3000 } = process.env;
API() const app = polka({
.use( onError: (err, req, res) => {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
}
});
app.use(
authenticate(),
sirv('static', { sirv('static', {
dev: process.env.NODE_ENV === 'development', dev: process.env.NODE_ENV === 'development',
setHeaders(res) { setHeaders(res) {
@ -15,7 +26,10 @@ API()
}), }),
sapper.middleware({ sapper.middleware({
// session: req => ({
user: sanitize_user(req.user)
}) })
) })
.listen(PORT); );
app.listen(PORT);

@ -1,33 +0,0 @@
import { writable } from 'svelte/store';
export const user = writable(null);
if (process.browser) {
const storageKey = 'svelte-dev:token';
// On load, get the last-known user token (if any)
// Note: We can skip this all by writing User data?
const token = localStorage.getItem(storageKey);
// Write changes to localStorage
user.subscribe(obj => {
if (obj) {
localStorage.setItem(storageKey, obj.token);
} else {
localStorage.removeItem(storageKey);
}
});
if (token) {
// If token, refresh the User data from API
const headers = { Authorization: `Bearer ${token}` };
fetch('/auth/me.json', { headers })
.then(r => r.ok ? r.json() : null)
.then(user.set);
}
}
export function logout() {
user.set(null);
}

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

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2219.27 506.75"><path d="M431.45,2755.61L543.53,2584c9.12-13.74,13.75-30,13.75-47.54a95.29,95.29,0,0,0-24.77-64.64c-16.92-18.56-39.14-30.17-64.25-30.17h-232L120,2627.61H290.57l-109.84,168.6a83.29,83.29,0,0,0-13.82,46.68c0,23.67,8.53,45.24,25.35,63.64,23.17,25.35,51.13,32.36,67.2,32.36l250.4,0.5,108.49-183.77H431.45Zm53.15,129-3.41,5.41-217-1.38-2.25.08a44.72,44.72,0,0,1-44.67-44.67,48.4,48.4,0,0,1,6.43-23.43l0.57-.9,150.94-228.06,5.46-8.77,1.83-3.3H208.41l54.79-84H468.26c11.5,0,20.45,3,28.17,11.49,8.1,8.88,12,18.2,12,29.46,0,7.92-1.8,14.24-5.5,19.81L346.8,2792.24l-5.46,7.92-1.83,2.45h194l-10.24,17.61C506.76,2848.2,492.66,2871.85,484.6,2884.64Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M784.72,2832.35a20.51,20.51,0,0,1-4.81-13.36,16.91,16.91,0,0,1,2.14-8.55l146.42-235.13H792.73a7.31,7.31,0,0,1-7.48-7.48v-24.58a7.31,7.31,0,0,1,7.48-7.48H965.87a16,16,0,0,1,12.56,5.88,19.26,19.26,0,0,1,5.08,12.83,16.87,16.87,0,0,1-2.67,9.08L834.41,2798.68H978.7a7.31,7.31,0,0,1,7.48,7.48v24.58a7.31,7.31,0,0,1-7.48,7.48H797A15.16,15.16,0,0,1,784.72,2832.35Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M1097.33,2822.73a113.59,113.59,0,0,1-42.22-42.48,115.42,115.42,0,0,1-15.5-58.51v-76.42a115.46,115.46,0,0,1,15.5-58.51,115.81,115.81,0,0,1,200.93,0,115.54,115.54,0,0,1,15.5,58.51v36.34a19.16,19.16,0,0,1-19.24,19.24h-171v23.51a73.34,73.34,0,0,0,9.89,37.41,72.54,72.54,0,0,0,27,27,73.34,73.34,0,0,0,37.41,9.89h77.49a7.31,7.31,0,0,1,7.48,7.48v24.58a7.31,7.31,0,0,1-7.48,7.48h-77.49A114,114,0,0,1,1097.33,2822.73Zm132.53-160.31v-19.77a74.58,74.58,0,0,0-111.69-64.39,73.77,73.77,0,0,0-36.87,64.39v19.77h148.56Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M1426.23,2836.35a9.37,9.37,0,0,1-4-5.08l-100.46-283.76a9.09,9.09,0,0,1-.53-3.74,7.69,7.69,0,0,1,2.4-5.61,8.32,8.32,0,0,1,6.15-2.41h24.58a11.31,11.31,0,0,1,6.15,1.87,9.47,9.47,0,0,1,4,5.08l84.43,243.68,82.83-243.68a9.39,9.39,0,0,1,4-5.08,11.29,11.29,0,0,1,6.15-1.87h23.51a8.2,8.2,0,0,1,7.21,3.47q2.41,3.48.8,8.28L1473,2831.28a9.44,9.44,0,0,1-4,5.08,11.24,11.24,0,0,1-6.15,1.87h-30.46A11.22,11.22,0,0,1,1426.23,2836.35Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M1724.42,2836.35a9.37,9.37,0,0,1-4-5.08l-100.46-283.76a9.09,9.09,0,0,1-.53-3.74,7.69,7.69,0,0,1,2.4-5.61,8.32,8.32,0,0,1,6.15-2.41h24.58a11.31,11.31,0,0,1,6.15,1.87,9.47,9.47,0,0,1,4,5.08l84.43,243.68L1830,2542.71a9.39,9.39,0,0,1,4-5.08,11.29,11.29,0,0,1,6.15-1.87h23.51a8.2,8.2,0,0,1,7.21,3.47q2.41,3.48.8,8.28l-100.46,283.76a9.44,9.44,0,0,1-4,5.08,11.24,11.24,0,0,1-6.15,1.87h-30.46A11.22,11.22,0,0,1,1724.42,2836.35Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M1956.35,2832.61a18.55,18.55,0,0,1-5.61-13.63V2472.17H1901a7.31,7.31,0,0,1-7.48-7.48v-24.58a7.31,7.31,0,0,1,7.48-7.48h72.14a19.16,19.16,0,0,1,19.24,19.24v346.81h56.64a7.31,7.31,0,0,1,7.48,7.48v24.58a7.31,7.31,0,0,1-7.48,7.48H1970A18.55,18.55,0,0,1,1956.35,2832.61Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/><path d="M2165,2822.73a113.59,113.59,0,0,1-42.22-42.48,115.42,115.42,0,0,1-15.5-58.51v-76.42a115.46,115.46,0,0,1,15.5-58.51,115.81,115.81,0,0,1,200.93,0,115.55,115.55,0,0,1,15.5,58.51v36.34a19.16,19.16,0,0,1-19.24,19.24H2149v23.51a73.34,73.34,0,0,0,9.89,37.41,72.54,72.54,0,0,0,27,27,73.34,73.34,0,0,0,37.41,9.89h77.49a7.31,7.31,0,0,1,7.48,7.48v24.58a7.31,7.31,0,0,1-7.48,7.48h-77.49A114,114,0,0,1,2165,2822.73Zm132.53-160.31v-19.77a74.58,74.58,0,0,0-111.69-64.39,73.77,73.77,0,0,0-36.87,64.39v19.77h148.56Z" transform="translate(-119.96 -2432.63)" fill="#da291c"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -415,6 +415,13 @@ export default class Element extends Node {
} }
} }
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: `The 'is' attribute is not supported cross-browser and should be avoided`
});
}
attribute_map.set(attribute.name, attribute); attribute_map.set(attribute.name, attribute);
}); });

@ -147,7 +147,10 @@ export default class Expression {
contextual_dependencies.add(name); contextual_dependencies.add(name);
if (!lazy) { const owner = template_scope.get_owner(name);
const is_index = owner.type === 'EachBlock' && owner.key && name === owner.index;
if (!lazy || is_index) {
template_scope.dependencies_for_name.get(name).forEach(name => dependencies.add(name)); template_scope.dependencies_for_name.get(name).forEach(name => dependencies.add(name));
} }
} else { } else {

@ -188,7 +188,6 @@ export default class AwaitBlockWrapper extends Wrapper {
conditions.push( conditions.push(
`(${dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` `(${dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
); );
}
conditions.push( conditions.push(
`${promise} !== (${promise} = ${snippet})`, `${promise} !== (${promise} = ${snippet})`,
@ -212,6 +211,13 @@ export default class AwaitBlockWrapper extends Wrapper {
${conditions.join(' && ')} ${conditions.join(' && ')}
`); `);
} }
} else {
if (this.pending.block.has_update_method) {
block.builders.update.add_block(deindent`
${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved));
`);
}
}
if (this.pending.block.has_outro_method) { if (this.pending.block.has_outro_method) {
block.builders.outro.add_block(deindent` block.builders.outro.add_block(deindent`

@ -208,6 +208,10 @@ function get_dom_updater(
return `${element.var}.checked = ${condition};`; return `${element.var}.checked = ${condition};`;
} }
if (binding.node.name === 'value') {
return `@set_input_value(${element.var}, ${binding.snippet});`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`; return `${element.var}.${binding.node.name} = ${binding.snippet};`;
} }

@ -10,6 +10,7 @@ import Text from '../../../nodes/Text';
export interface StyleProp { export interface StyleProp {
key: string; key: string;
value: Array<Text|Expression>; value: Array<Text|Expression>;
important: boolean;
} }
export default class StyleAttributeWrapper extends AttributeWrapper { export default class StyleAttributeWrapper extends AttributeWrapper {
@ -35,8 +36,7 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
} else { } else {
const snippet = chunk.render(); const snippet = chunk.render();
add_to_set(prop_dependencies, chunk.dependencies); add_to_set(prop_dependencies, chunk.dynamic_dependencies());
return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet; return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet;
} }
}) })
@ -51,7 +51,7 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
block.builders.update.add_conditional( block.builders.update.add_conditional(
condition, condition,
`@set_style(${this.parent.var}, "${prop.key}", ${value});` `@set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''});`
); );
} }
} else { } else {
@ -59,7 +59,7 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
} }
block.builders.hydrate.add_line( block.builders.hydrate.add_line(
`@set_style(${this.parent.var}, "${prop.key}", ${value});` `@set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''});`
); );
}); });
} }
@ -97,7 +97,7 @@ function optimize_style(value: Array<Text|Expression>) {
const result = get_style_value(chunks); const result = get_style_value(chunks);
props.push({ key, value: result.value }); props.push({ key, value: result.value, important: result.important });
chunks = result.chunks; chunks = result.chunks;
} }
@ -110,8 +110,9 @@ function get_style_value(chunks: Array<Text | Expression>) {
let in_url = false; let in_url = false;
let quote_mark = null; let quote_mark = null;
let escaped = false; let escaped = false;
let closed = false;
while (chunks.length) { while (chunks.length && !closed) {
const chunk = chunks.shift(); const chunk = chunks.shift();
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
@ -132,6 +133,7 @@ function get_style_value(chunks: Array<Text | Expression>) {
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') { } else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
in_url = true; in_url = true;
} else if (char === ';' && !in_url && !quote_mark) { } else if (char === ';' && !in_url && !quote_mark) {
closed = true;
break; break;
} }
@ -167,9 +169,19 @@ function get_style_value(chunks: Array<Text | Expression>) {
} }
} }
let important = false;
const last_chunk = value[value.length - 1];
if (last_chunk && last_chunk.type === 'Text' && /\s*!important\s*$/.test(last_chunk.data)) {
important = true;
last_chunk.data = last_chunk.data.replace(/\s*!important\s*$/, '');
if (!last_chunk.data) value.pop();
}
return { return {
chunks, chunks,
value value,
important
}; };
} }

@ -383,6 +383,11 @@ export default class ElementWrapper extends Wrapper {
return `@_document.createElementNS("${namespace}", "${name}")`; return `@_document.createElementNS("${namespace}", "${name}")`;
} }
const is = this.attributes.find(attr => attr.node.name === 'is');
if (is) {
return `@element_is("${name}", ${is.render_chunks().join(' + ')});`;
}
return `@element("${name}")`; return `@element("${name}")`;
} }

@ -7,6 +7,7 @@ import create_debugging_comment from './shared/create_debugging_comment';
import ElseBlock from '../../nodes/ElseBlock'; import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment'; import FragmentWrapper from './Fragment';
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import { walk } from 'estree-walker';
function is_else_if(node: ElseBlock) { function is_else_if(node: ElseBlock) {
return ( return (
@ -17,7 +18,9 @@ function is_else_if(node: ElseBlock) {
class IfBlockBranch extends Wrapper { class IfBlockBranch extends Wrapper {
block: Block; block: Block;
fragment: FragmentWrapper; fragment: FragmentWrapper;
condition: string; dependencies?: string[];
condition?: string;
snippet?: string;
is_dynamic: boolean; is_dynamic: boolean;
var = null; var = null;
@ -32,13 +35,35 @@ class IfBlockBranch extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.condition = (node as IfBlock).expression && (node as IfBlock).expression.render(block); const { expression } = (node as IfBlock);
const is_else = !expression;
if (expression) {
const dependencies = expression.dynamic_dependencies();
// TODO is this the right rule? or should any non-reference count?
// const should_cache = !is_reference(expression.node, null) && dependencies.length > 0;
let should_cache = false;
walk(expression.node, {
enter(node) {
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
should_cache = true;
}
}
});
if (should_cache) {
this.condition = block.get_unique_name(`show_if`);
this.snippet = expression.render(block);
this.dependencies = dependencies;
} else {
this.condition = expression.render(block);
}
}
this.block = block.child({ this.block = block.child({
comment: create_debugging_comment(node, parent.renderer.component), comment: create_debugging_comment(node, parent.renderer.component),
name: parent.renderer.component.get_unique_name( name: parent.renderer.component.get_unique_name(is_else ? `create_else_block` : `create_if_block`)
(node as IfBlock).expression ? `create_if_block` : `create_else_block`
)
}); });
this.fragment = new FragmentWrapper(renderer, this.block, node.children, parent, strip_whitespace, next_sibling); this.fragment = new FragmentWrapper(renderer, this.block, node.children, parent, strip_whitespace, next_sibling);
@ -157,6 +182,10 @@ export default class IfBlockWrapper extends Wrapper {
const detaching = (parent_node && parent_node !== '@_document.head') ? '' : 'detaching'; const detaching = (parent_node && parent_node !== '@_document.head') ? '' : 'detaching';
if (this.node.else) { if (this.node.else) {
this.branches.forEach(branch => {
if (branch.snippet) block.add_variable(branch.condition);
});
if (has_outros) { if (has_outros) {
this.render_compound_with_outros(block, parent_node, parent_nodes, dynamic, vars, detaching); this.render_compound_with_outros(block, parent_node, parent_nodes, dynamic, vars, detaching);
@ -212,16 +241,18 @@ export default class IfBlockWrapper extends Wrapper {
/* eslint-disable @typescript-eslint/indent,indent */ /* eslint-disable @typescript-eslint/indent,indent */
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
function ${select_block_type}(ctx) { function ${select_block_type}(changed, ctx) {
${this.branches ${this.branches.map(({ dependencies, condition, snippet, block }) => condition
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`) ? deindent`
.join('\n')} ${dependencies && `if ((${condition} == null) || ${dependencies.map(n => `changed.${n}`).join(' || ')}) ${condition} = !!(${snippet})`}
if (${condition}) return ${block.name};`
: `return ${block.name};`)}
} }
`); `);
/* eslint-enable @typescript-eslint/indent,indent */ /* eslint-enable @typescript-eslint/indent,indent */
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
var ${current_block_type} = ${select_block_type}(ctx); var ${current_block_type} = ${select_block_type}(null, ctx);
var ${name} = ${current_block_type_and}${current_block_type}(ctx); var ${name} = ${current_block_type_and}${current_block_type}(ctx);
`); `);
@ -245,7 +276,7 @@ export default class IfBlockWrapper extends Wrapper {
if (dynamic) { if (dynamic) {
block.builders.update.add_block(deindent` block.builders.update.add_block(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) { if (${current_block_type} === (${current_block_type} = ${select_block_type}(changed, ctx)) && ${name}) {
${name}.p(changed, ctx); ${name}.p(changed, ctx);
} else { } else {
${change_block} ${change_block}
@ -253,7 +284,7 @@ export default class IfBlockWrapper extends Wrapper {
`); `);
} else { } else {
block.builders.update.add_block(deindent` block.builders.update.add_block(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) { if (${current_block_type} !== (${current_block_type} = ${select_block_type}(changed, ctx))) {
${change_block} ${change_block}
} }
`); `);
@ -293,10 +324,12 @@ export default class IfBlockWrapper extends Wrapper {
var ${if_blocks} = []; var ${if_blocks} = [];
function ${select_block_type}(ctx) { function ${select_block_type}(changed, ctx) {
${this.branches ${this.branches.map(({ dependencies, condition, snippet }, i) => condition
.map(({ condition }, i) => `${condition ? `if (${condition}) ` : ''}return ${i};`) ? deindent`
.join('\n')} ${dependencies && `if ((${condition} == null) || ${dependencies.map(n => `changed.${n}`).join(' || ')}) ${condition} = !!(${snippet})`}
if (${condition}) return ${String(i)};`
: `return ${i};`)}
${!has_else && `return -1;`} ${!has_else && `return -1;`}
} }
`); `);
@ -304,12 +337,12 @@ export default class IfBlockWrapper extends Wrapper {
if (has_else) { if (has_else) {
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
${current_block_type_index} = ${select_block_type}(ctx); ${current_block_type_index} = ${select_block_type}(null, ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
`); `);
} else { } else {
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
if (~(${current_block_type_index} = ${select_block_type}(ctx))) { if (~(${current_block_type_index} = ${select_block_type}(null, ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
} }
`); `);
@ -363,7 +396,7 @@ export default class IfBlockWrapper extends Wrapper {
if (dynamic) { if (dynamic) {
block.builders.update.add_block(deindent` block.builders.update.add_block(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx); ${current_block_type_index} = ${select_block_type}(changed, ctx);
if (${current_block_type_index} === ${previous_block_index}) { if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx); ${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else { } else {
@ -373,7 +406,7 @@ export default class IfBlockWrapper extends Wrapper {
} else { } else {
block.builders.update.add_block(deindent` block.builders.update.add_block(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx); ${current_block_type_index} = ${select_block_type}(changed, ctx);
if (${current_block_type_index} !== ${previous_block_index}) { if (${current_block_type_index} !== ${previous_block_index}) {
${change_block} ${change_block}
} }
@ -395,6 +428,8 @@ export default class IfBlockWrapper extends Wrapper {
) { ) {
const branch = this.branches[0]; const branch = this.branches[0];
if (branch.snippet) block.add_variable(branch.condition, branch.snippet);
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
var ${name} = (${branch.condition}) && ${branch.block.name}(ctx); var ${name} = (${branch.condition}) && ${branch.block.name}(ctx);
`); `);
@ -431,6 +466,10 @@ export default class IfBlockWrapper extends Wrapper {
} }
`; `;
if (branch.snippet) {
block.builders.update.add_block(`if (${branch.dependencies.map(n => `changed.${n}`).join(' || ')}) ${branch.condition} = ${branch.snippet}`);
}
// no `p()` here — we don't want to update outroing nodes, // no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching // as that will typically result in glitching
if (branch.block.has_outro_method) { if (branch.block.has_outro_method) {

@ -8,7 +8,7 @@ export default function(node: Slot, renderer: Renderer, options: RenderOptions)
const slot_data = get_slot_data(node.values, true); const slot_data = get_slot_data(node.values, true);
const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : ''; const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : '{}';
renderer.append(`\${$$slots${prop} ? $$slots${prop}(${arg}) : \``); renderer.append(`\${$$slots${prop} ? $$slots${prop}(${arg}) : \``);

@ -1,6 +1,7 @@
import { assign, is_promise } from './utils'; import { assign, is_promise } from './utils';
import { check_outros, group_outros, transition_in, transition_out } from './transitions'; import { check_outros, group_outros, transition_in, transition_out } from './transitions';
import { flush } from './scheduler'; import { flush } from './scheduler';
import { get_current_component, set_current_component } from './lifecycle';
export function handle_promise(promise, info) { export function handle_promise(promise, info) {
const token = info.token = {}; const token = info.token = {};
@ -40,10 +41,15 @@ export function handle_promise(promise, info) {
} }
if (is_promise(promise)) { if (is_promise(promise)) {
const current_component = get_current_component();
promise.then(value => { promise.then(value => {
set_current_component(current_component);
update(info.then, 1, info.value, value); update(info.then, 1, info.value, value);
set_current_component(null);
}, error => { }, error => {
set_current_component(current_component);
update(info.catch, 2, info.error, error); update(info.catch, 2, info.error, error);
set_current_component(null);
}); });
// if we previously had a then/catch block, destroy it // if we previously had a then/catch block, destroy it

@ -20,6 +20,10 @@ export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
return document.createElement<K>(name); return document.createElement<K>(name);
} }
export function element_is<K extends keyof HTMLElementTagNameMap>(name: K, is: string) {
return document.createElement<K>(name, { is });
}
export function object_without_properties<T, K extends keyof T>(obj: T, exclude: K[]) { export function object_without_properties<T, K extends keyof T>(obj: T, exclude: K[]) {
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
const target = {} as Pick<T, Exclude<keyof T, K>>; const target = {} as Pick<T, Exclude<keyof T, K>>;
@ -165,6 +169,12 @@ export function set_data(text, data) {
if (text.data !== data) text.data = data; if (text.data !== data) text.data = data;
} }
export function set_input_value(input, value) {
if (value != null || input.value) {
input.value = value;
}
}
export function set_input_type(input, type) { export function set_input_type(input, type) {
try { try {
input.type = type; input.type = type;
@ -173,8 +183,8 @@ export function set_input_type(input, type) {
} }
} }
export function set_style(node, key, value) { export function set_style(node, key, value, important) {
node.style.setProperty(key, value); node.style.setProperty(key, value, important ? 'important' : '');
} }
export function select_option(select, value) { export function select_option(select, value) {

@ -6,7 +6,7 @@ export function set_current_component(component) {
current_component = component; current_component = component;
} }
function get_current_component() { export function get_current_component() {
if (!current_component) throw new Error(`Function called outside component initialization`); if (!current_component) throw new Error(`Function called outside component initialization`);
return current_component; return current_component;
} }

@ -0,0 +1,17 @@
export default {
warnings: [{
code: "avoid-is",
message: "The 'is' attribute is not supported cross-browser and should be avoided",
pos: 97,
start: {
character: 97,
column: 8,
line: 7
},
end: {
character: 114,
column: 25,
line: 7
}
}]
};

@ -0,0 +1,2 @@
class FancyButton extends HTMLButtonElement {}
customElements.define('fancy-button', FancyButton, { extends: 'button' });

@ -0,0 +1,7 @@
<svelte:options tag="custom-element"/>
<script>
import './fancy-button.js';
</script>
<button is="fancy-button">click me</button>

@ -0,0 +1,15 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';
export default function (target) {
new CustomElement({
target
});
assert.equal(target.innerHTML, '<custom-element></custom-element>');
const el = target.querySelector('custom-element');
const button = el.shadowRoot.querySelector('button');
assert.ok(button instanceof customElements.get('fancy-button'));
}

@ -57,12 +57,12 @@ function create_if_block(ctx) {
function create_fragment(ctx) { function create_fragment(ctx) {
var if_block_anchor; var if_block_anchor;
function select_block_type(ctx) { function select_block_type(changed, ctx) {
if (ctx.foo) return create_if_block; if (ctx.foo) return create_if_block;
return create_else_block; return create_else_block;
} }
var current_block_type = select_block_type(ctx); var current_block_type = select_block_type(null, ctx);
var if_block = current_block_type(ctx); var if_block = current_block_type(ctx);
return { return {
@ -77,7 +77,7 @@ function create_fragment(ctx) {
}, },
p(changed, ctx) { p(changed, ctx) {
if (current_block_type !== (current_block_type = select_block_type(ctx))) { if (current_block_type !== (current_block_type = select_block_type(changed, ctx))) {
if_block.d(1); if_block.d(1);
if_block = current_block_type(ctx); if_block = current_block_type(ctx);
if (if_block) { if (if_block) {

@ -0,0 +1,47 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal,
set_style
} from "svelte/internal";
function create_fragment(ctx) {
var div;
return {
c() {
div = element("div");
set_style(div, "color", color);
},
m(target, anchor) {
insert(target, div, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(div);
}
}
};
}
let color = 'red';
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, []);
}
}
export default Component;

@ -0,0 +1,5 @@
<script>
let color = 'red';
</script>
<div style="color: {color}"></div>

@ -0,0 +1,87 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
append,
attr,
detach,
element,
init,
insert,
listen,
noop,
run_all,
safe_not_equal,
set_input_value,
space
} from "svelte/internal";
function create_fragment(ctx) {
var form, input, t, button, dispose;
return {
c() {
form = element("form");
input = element("input");
t = space();
button = element("button");
button.textContent = "Store";
attr(input, "type", "text");
input.required = true;
dispose = [
listen(input, "input", ctx.input_input_handler),
listen(form, "submit", ctx.handleSubmit)
];
},
m(target, anchor) {
insert(target, form, anchor);
append(form, input);
set_input_value(input, ctx.test);
append(form, t);
append(form, button);
},
p(changed, ctx) {
if (changed.test && (input.value !== ctx.test)) set_input_value(input, ctx.test);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(form);
}
run_all(dispose);
}
};
}
function instance($$self, $$props, $$invalidate) {
let test = undefined;
function handleSubmit(event) {
event.preventDefault();
console.log('value', test);
}
function input_input_handler() {
test = this.value;
$$invalidate('test', test);
}
return { test, handleSubmit, input_input_handler };
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, []);
}
}
export default Component;

@ -0,0 +1,13 @@
<script>
let test = undefined;
function handleSubmit(event) {
event.preventDefault();
console.log('value', test);
}
</script>
<form on:submit={handleSubmit}>
<input bind:value={test} type=text required>
<button>Store</button>
</form>

@ -10,6 +10,7 @@ import {
noop, noop,
run_all, run_all,
safe_not_equal, safe_not_equal,
set_input_value,
to_number to_number
} from "svelte/internal"; } from "svelte/internal";
@ -30,11 +31,11 @@ function create_fragment(ctx) {
m(target, anchor) { m(target, anchor) {
insert(target, input, anchor); insert(target, input, anchor);
input.value = ctx.value; set_input_value(input, ctx.value);
}, },
p(changed, ctx) { p(changed, ctx) {
if (changed.value) input.value = ctx.value; if (changed.value) set_input_value(input, ctx.value);
}, },
i: noop, i: noop,

@ -0,0 +1,16 @@
import { sleep } from './sleep.js';
export default {
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
return sleep(50).then(() => {
assert.htmlEqual(target.innerHTML, `
<p>the answer is 42</p>
<p>count: 1</p>
`);
});
}
};

@ -0,0 +1,19 @@
<script>
import { sleep } from './sleep.js';
let count = 0;
const get_promise = () => {
return sleep(10).then(() => {
count += 1;
return 42;
});
};
</script>
{#await get_promise()}
<p>loading...</p>
{:then value}
<p>the answer is {value}</p>
<p>count: {count}</p>
{/await}

@ -0,0 +1,11 @@
export let stopped = false;
export const stop = () => stopped = true;
export const sleep = ms => new Promise(f => {
if (stopped) return;
setTimeout(() => {
if (stopped) return;
f();
}, ms);
});

@ -0,0 +1,5 @@
<script>
export let thing;
</script>
<p>{thing}</p>

@ -0,0 +1,5 @@
export default {
html: `
<p>undefined</p>
`
};

@ -0,0 +1,10 @@
<script>
import Foo from './Foo.svelte';
import Bar from './Bar.svelte';
const things = { '1': 'one' };
</script>
<Foo let:id>
<Bar thing={things[id]}/>
</Foo>

@ -0,0 +1,6 @@
<script>
import { getContext } from 'svelte';
const num = getContext('test');
</script>
<p>Context value: {num}</p>

@ -0,0 +1,13 @@
export default {
html: `
<p>...waiting</p>
`,
async test({ assert, component, target }) {
await component.promise;
assert.htmlEqual(target.innerHTML, `
<p>Context value: 123</p>
`);
}
};

@ -0,0 +1,14 @@
<script>
import { setContext } from 'svelte';
import Child from './Child.svelte';
setContext('test', 123);
export let promise = Promise.resolve();
</script>
{#await promise}
<p>...waiting</p>
{:then}
<Child />
{/await}

@ -0,0 +1,18 @@
export default {
html: `
<button>remove</button>
<button>remove</button>
<button>remove</button>
`,
async test({ assert, target, window }) {
const click = new window.MouseEvent('click');
await target.querySelectorAll('button')[1].dispatchEvent(click);
await target.querySelectorAll('button')[1].dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>remove</button>
`);
}
};

@ -0,0 +1,16 @@
<script>
let list = ["a", "b", "c"];
const remove = index => {
list.splice(index, 1);
list = list;
};
</script>
{#each list as value, index (value)}
{#if value}
<button on:click="{e => remove(index)}">
remove
</button>
{/if}
{/each}

@ -0,0 +1,22 @@
let count = 0;
export default {
props: {
foo: 'potato',
fn: () => {
count += 1;
return true;
}
},
html: `<p>potato</p>`,
test({ assert, component, target }) {
assert.equal(count, 1);
component.foo = 'soup';
assert.equal(count, 1);
assert.htmlEqual(target.innerHTML, `<p>soup</p>`);
}
};

@ -0,0 +1,8 @@
<script>
export let fn;
export let foo;
</script>
{#if fn()}
<p>{foo}</p>
{/if}

@ -0,0 +1,37 @@
let a = true;
let count_a = 0;
let count_b = 0;
export default {
props: {
foo: 'potato',
fn: () => {
count_a += 1;
return a;
},
other_fn: () => {
count_b += 1;
return true;
}
},
html: `<p>potato</p>`,
test({ assert, component, target }) {
assert.equal(count_a, 1);
assert.equal(count_b, 0);
a = false;
component.foo = 'soup';
assert.equal(count_a, 2);
assert.equal(count_b, 1);
assert.htmlEqual(target.innerHTML, `<p>SOUP</p>`);
component.foo = 'salad';
assert.equal(count_a, 3);
assert.equal(count_b, 1);
assert.htmlEqual(target.innerHTML, `<p>SALAD</p>`);
}
};

@ -0,0 +1,11 @@
<script>
export let fn;
export let other_fn;
export let foo;
</script>
{#if fn(foo)}
<p>{foo}</p>
{:else if other_fn()}
<p>{foo.toUpperCase()}</p>
{/if}

@ -0,0 +1,18 @@
export default {
html: `
<p class="svelte-y94hdy" style="color: red !important; font-size: 20px !important; opacity: 1;">red</p>
`,
test({ assert, component, target, window }) {
const p = target.querySelector('p');
let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.fontSize, '20px');
component.color = 'green';
styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green');
}
};

@ -0,0 +1,12 @@
<script>
export let color = `red`;
</script>
<p style="color: {color} !important; font-size: 20px !important; opacity: 1;">{color}</p>
<style>
p {
color: blue !important;
font-size: 10px !important;
}
</style>

@ -0,0 +1,20 @@
export default {
html: `
<p style="opacity: 0.5; color: red">color: red</p>
`,
test({ assert, component, target, window }) {
const p = target.querySelector('p');
let styles = window.getComputedStyle(p);
assert.equal(styles.opacity, '0.5');
assert.equal(styles.color, 'red');
component.styles = 'font-size: 20px';
styles = window.getComputedStyle(p);
assert.equal(styles.opacity, '0.5');
assert.equal(styles.color, '');
assert.equal(styles.fontSize, '20px');
}
};

@ -0,0 +1,5 @@
<script>
export let styles = `color: red`;
</script>
<p style="opacity: 0.5; {styles}">{styles}</p>
Loading…
Cancel
Save