diff --git a/CHANGELOG.md b/CHANGELOG.md index bae7373742..5767a556e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # 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 * Set SVG namespace for slotted elements ([#3321](https://github.com/sveltejs/svelte/issues/3321)) diff --git a/package.json b/package.json index 45abeeda37..4a28235a30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.8.1", + "version": "3.9.1", "description": "Cybernetically enhanced web apps", "module": "index.mjs", "main": "index", @@ -98,6 +98,5 @@ ], "sourceMap": true, "instrument": true - }, - "dependencies": {} + } } diff --git a/site/.env.example b/site/.env.example index c59f6f7272..4de2f6111a 100644 --- a/site/.env.example +++ b/site/.env.example @@ -5,8 +5,4 @@ BASEURL= DATABASE_URL= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= -MAPBOX_ACCESS_TOKEN= - -JWT_EXP=30d -JWT_ALG=HS512 -JWT_KEY= +MAPBOX_ACCESS_TOKEN= \ No newline at end of file diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md index 193e880ea6..d5653dd288 100644 --- a/site/content/docs/03-run-time.md +++ b/site/content/docs/03-run-time.md @@ -907,7 +907,68 @@ app.count += 1; ### 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 `` [element](docs#svelte_options). + +```html + + + + +

Hello {name}!

+ +``` + +--- + +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 = ` + +

This is some slotted content

+
+`; +``` + +--- + +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 ``. + +```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 `` element is inside an `{#if ...}` block. Similarly, including a `` 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 diff --git a/site/migrations/002-create-sessions.js b/site/migrations/002-create-sessions.js new file mode 100644 index 0000000000..c24fc69115 --- /dev/null +++ b/site/migrations/002-create-sessions.js @@ -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; + `); +}; diff --git a/site/package-lock.json b/site/package-lock.json index 373c0505cb..a36dea4aeb 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1647,6 +1647,11 @@ "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": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", @@ -1906,6 +1911,11 @@ "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": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", diff --git a/site/package.json b/site/package.json index 0b949f5da7..c9ecacbf0e 100644 --- a/site/package.json +++ b/site/package.json @@ -15,8 +15,10 @@ "dependencies": { "@polka/redirect": "^1.0.0-next.0", "@polka/send": "^1.0.0-next.3", + "cookie": "^0.4.0", "devalue": "^2.0.0", "do-not-zip": "^1.0.0", + "flru": "^1.0.2", "httpie": "^1.1.2", "jsonwebtoken": "^8.5.1", "marked": "^0.7.0", diff --git a/site/src/backend/auth.js b/site/src/backend/auth.js deleted file mode 100644 index 7792f4ee37..0000000000 --- a/site/src/backend/auth.js +++ /dev/null @@ -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, ` - - `, { - '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, ` - -

Missing .env file

-

In order to use GitHub authentication, you will need to register an OAuth application and create a local .env file:

-
GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000
-

The BASEURL variable should match the callback URL specified for your app.

-

See also here

- - `, { - 'Content-Type': 'text/html; charset=utf-8' - }); - }); - } - - return app; -} diff --git a/site/src/backend/token.js b/site/src/backend/token.js deleted file mode 100644 index 9adf7d24f4..0000000000 --- a/site/src/backend/token.js +++ /dev/null @@ -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); -} diff --git a/site/src/routes/_components/WhosUsingSvelte.svelte b/site/src/routes/_components/WhosUsingSvelte.svelte index 9169dbd1cd..043dd8e5ef 100644 --- a/site/src/routes/_components/WhosUsingSvelte.svelte +++ b/site/src/routes/_components/WhosUsingSvelte.svelte @@ -53,6 +53,7 @@ Deck logo From-Now-On logo GoDaddy logo + HealthTree logo itslearning logo Mustlab logo Nesta logo @@ -69,6 +70,6 @@ Thunderdome logo Tokopedia logo Webdesq logo - HealthTree logo + Zevvle logo + your company? diff --git a/site/src/routes/_layout.svelte b/site/src/routes/_layout.svelte index 4adbb9b0dc..55f4861880 100644 --- a/site/src/routes/_layout.svelte +++ b/site/src/routes/_layout.svelte @@ -1,11 +1,32 @@ diff --git a/site/src/routes/apps/index.json.js b/site/src/routes/apps/index.json.js new file mode 100644 index 0000000000..e6a09d52c5 --- /dev/null +++ b/site/src/routes/apps/index.json.js @@ -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); + } +} diff --git a/site/src/routes/apps/index.svelte b/site/src/routes/apps/index.svelte new file mode 100644 index 0000000000..d85f5681f8 --- /dev/null +++ b/site/src/routes/apps/index.svelte @@ -0,0 +1,140 @@ + + + + + + Your apps • Svelte + + +
+ {#if user} +
+

Your apps

+ +
+ {user.name} avatar + + {user.name} + (log out) + +
+
+ + + + {#if offset !== null} + + {/if} + {:else} +

Please log in to see your saved apps.

+ {/if} +
+ + diff --git a/site/src/routes/auth/_config.js b/site/src/routes/auth/_config.js new file mode 100644 index 0000000000..d155e56de8 --- /dev/null +++ b/site/src/routes/auth/_config.js @@ -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; \ No newline at end of file diff --git a/site/src/routes/auth/callback.js b/site/src/routes/auth/callback.js new file mode 100644 index 0000000000..7979b1c21f --- /dev/null +++ b/site/src/routes/auth/callback.js @@ -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(` + + `); + } 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'] + }); + } +} \ No newline at end of file diff --git a/site/src/routes/auth/login.js b/site/src/routes/auth/login.js new file mode 100644 index 0000000000..7240498418 --- /dev/null +++ b/site/src/routes/auth/login.js @@ -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, ` + +

Missing .env file

+

In order to use GitHub authentication, you will need to register an OAuth application and create a local .env file:

+
GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000
+

The BASEURL variable should match the callback URL specified for your app.

+

See also here

+ + `, { + 'Content-Type': 'text/html; charset=utf-8' + }); + }; \ No newline at end of file diff --git a/site/src/routes/auth/logout.js b/site/src/routes/auth/logout.js new file mode 100644 index 0000000000..7e132f0a41 --- /dev/null +++ b/site/src/routes/auth/logout.js @@ -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 + }) + }); +} \ No newline at end of file diff --git a/site/src/routes/auth/me.json.js b/site/src/routes/auth/me.json.js deleted file mode 100644 index f77eaf5b94..0000000000 --- a/site/src/routes/auth/me.json.js +++ /dev/null @@ -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); -} diff --git a/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte b/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte index 1404c1d5fc..de400cbf29 100644 --- a/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte +++ b/site/src/routes/repl/[id]/_components/AppControls/UserMenu.svelte @@ -1,18 +1,23 @@
{name} - {name} avatar + {name} avatar {#if showMenu} {/if} @@ -64,7 +69,7 @@ .menu { position: absolute; width: calc(100% + 1.6rem); - min-width: 6em; + min-width: 10em; top: 3rem; right: -1.6rem; background-color: var(--second); @@ -72,17 +77,25 @@ z-index: 99; text-align: left; border-radius: 0.4rem; + display: flex; + flex-direction: column; } - .menu button { + .menu button, .menu a { background-color: transparent; font-family: var(--font); 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; + color: inherit; } @media (min-width: 600px) { diff --git a/site/src/routes/repl/[id]/_components/AppControls/index.svelte b/site/src/routes/repl/[id]/_components/AppControls/index.svelte index 7770972474..4cfd5d43b2 100644 --- a/site/src/routes/repl/[id]/_components/AppControls/index.svelte +++ b/site/src/routes/repl/[id]/_components/AppControls/index.svelte @@ -1,14 +1,16 @@ \ No newline at end of file diff --git a/test/runtime/samples/inline-style-optimisation-bailout/_config.js b/test/runtime/samples/inline-style-optimisation-bailout/_config.js new file mode 100644 index 0000000000..836a3e2e6e --- /dev/null +++ b/test/runtime/samples/inline-style-optimisation-bailout/_config.js @@ -0,0 +1,20 @@ +export default { + html: ` +

color: red

+ `, + + 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'); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/inline-style-optimisation-bailout/main.svelte b/test/runtime/samples/inline-style-optimisation-bailout/main.svelte new file mode 100644 index 0000000000..35b768547e --- /dev/null +++ b/test/runtime/samples/inline-style-optimisation-bailout/main.svelte @@ -0,0 +1,5 @@ + + +

{styles}

\ No newline at end of file