From 2eb881dcb695b30cdb8ef37b69c1194d93ce32af Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Fri, 4 Jan 2019 22:02:12 -0500 Subject: [PATCH 01/13] prevent invalid svelte: tags - fixes #1948 --- src/parse/state/tag.ts | 34 ++++++++++++++----- .../error-svelte-selfdestructive/error.json | 10 ++++++ .../error-svelte-selfdestructive/input.html | 3 ++ 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 test/parser/samples/error-svelte-selfdestructive/error.json create mode 100644 test/parser/samples/error-svelte-selfdestructive/input.html diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 43e685017b..5465f6f3e1 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -5,16 +5,20 @@ import { decodeCharacterReferences } from '../utils/html'; import isVoidElementName from '../../utils/isVoidElementName'; import { Parser } from '../index'; import { Node } from '../../interfaces'; +import fuzzymatch from '../../utils/fuzzymatch'; +import list from '../../utils/list'; const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const metaTags = new Map([ - ['svelte:document', 'Document'], - ['svelte:window', 'Window'], + ['svelte:head', 'Head'], ['svelte:meta', 'Meta'], - ['svelte:head', 'Head'] + ['svelte:window', 'Window'], + ['svelte:document', 'Document'] ]); +const valid_meta_tags = [...metaTags.keys(), 'svelte:self', 'svelte:component']; + const specials = new Map([ [ 'script', @@ -32,8 +36,8 @@ const specials = new Map([ ], ]); -const SELF = 'svelte:self'; -const COMPONENT = 'svelte:component'; +const SELF = /^svelte:self[\s\/>]/; +const COMPONENT = /^svelte:component[\s\/>]/; // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission const disallowedContents = new Map([ @@ -278,7 +282,7 @@ export default function tag(parser: Parser) { function readTagName(parser: Parser) { const start = parser.index; - if (parser.eat(SELF)) { + if (parser.read(SELF)) { // check we're inside a block, otherwise this // will cause infinite recursion let i = parser.stack.length; @@ -295,19 +299,31 @@ function readTagName(parser: Parser) { if (!legal) { parser.error({ code: `invalid-self-placement`, - message: `<${SELF}> components can only exist inside if-blocks or each-blocks` + message: ` components can only exist inside if-blocks or each-blocks` }, start); } - return SELF; + return 'svelte:self'; } - if (parser.eat(COMPONENT)) return COMPONENT; + if (parser.read(COMPONENT)) return 'svelte:component'; const name = parser.readUntil(/(\s|\/|>)/); if (metaTags.has(name)) return name; + if (name.startsWith('svelte:')) { + const match = fuzzymatch(name.slice(7), valid_meta_tags); + + let message = `Valid tag names are ${list(valid_meta_tags)}`; + if (match) message += ` (did you mean '${match}'?)`; + + parser.error({ + code: 'invalid-tag-name', + message + }, start); + } + if (!validTagName.test(name)) { parser.error({ code: `invalid-tag-name`, diff --git a/test/parser/samples/error-svelte-selfdestructive/error.json b/test/parser/samples/error-svelte-selfdestructive/error.json new file mode 100644 index 0000000000..1b4dc7fdec --- /dev/null +++ b/test/parser/samples/error-svelte-selfdestructive/error.json @@ -0,0 +1,10 @@ +{ + "code": "invalid-tag-name", + "message": "Valid tag names are svelte:head, svelte:meta, svelte:window, svelte:document, svelte:self or svelte:component", + "pos": 10, + "start": { + "character": 10, + "line": 2, + "column": 2 + } +} \ No newline at end of file diff --git a/test/parser/samples/error-svelte-selfdestructive/input.html b/test/parser/samples/error-svelte-selfdestructive/input.html new file mode 100644 index 0000000000..6bf1e83aa4 --- /dev/null +++ b/test/parser/samples/error-svelte-selfdestructive/input.html @@ -0,0 +1,3 @@ +{#if x} + +{/if} \ No newline at end of file From 0bdf4a45363a1410db0b85eedc4eef6b5d40fa9b Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Fri, 4 Jan 2019 22:09:21 -0500 Subject: [PATCH 02/13] oops --- src/parse/state/tag.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 1ed7b3d1e8..64108057ce 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -283,6 +283,8 @@ function readTagName(parser: Parser) { const start = parser.index; if (parser.read(SELF)) { + parser.index -= 1; + // check we're inside a block, otherwise this // will cause infinite recursion let i = parser.stack.length; @@ -306,7 +308,10 @@ function readTagName(parser: Parser) { return 'svelte:self'; } - if (parser.read(COMPONENT)) return 'svelte:component'; + if (parser.read(COMPONENT)) { + parser.index -= 1; + return 'svelte:component'; + } const name = parser.readUntil(/(\s|\/|>)/); From e682d5ca5488a20ff8c55bdb4fc4f0bdb81afa4f Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Fri, 4 Jan 2019 22:32:32 -0500 Subject: [PATCH 03/13] nicer --- src/parse/state/tag.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 64108057ce..d2cc9cc9ba 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -36,8 +36,8 @@ const specials = new Map([ ], ]); -const SELF = /^svelte:self[\s\/>]/; -const COMPONENT = /^svelte:component[\s\/>]/; +const SELF = /^svelte:self(?=[\s\/>])/; +const COMPONENT = /^svelte:component(?=[\s\/>])/; // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission const disallowedContents = new Map([ @@ -283,8 +283,6 @@ function readTagName(parser: Parser) { const start = parser.index; if (parser.read(SELF)) { - parser.index -= 1; - // check we're inside a block, otherwise this // will cause infinite recursion let i = parser.stack.length; @@ -308,10 +306,7 @@ function readTagName(parser: Parser) { return 'svelte:self'; } - if (parser.read(COMPONENT)) { - parser.index -= 1; - return 'svelte:component'; - } + if (parser.read(COMPONENT)) return 'svelte:component'; const name = parser.readUntil(/(\s|\/|>)/); From fc5d929e761123e0eb2c8a715d10bffdb7d0d0a8 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Fri, 4 Jan 2019 22:57:40 -0500 Subject: [PATCH 04/13] set parent correctly for children of await blocks - should fix #1939 --- src/compile/render-dom/wrappers/AwaitBlock.ts | 2 +- .../samples/await-containing-if/_config.js | 35 +++++++++++++++++++ .../samples/await-containing-if/main.html | 11 ++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/runtime/samples/await-containing-if/_config.js create mode 100644 test/runtime/samples/await-containing-if/main.html diff --git a/src/compile/render-dom/wrappers/AwaitBlock.ts b/src/compile/render-dom/wrappers/AwaitBlock.ts index ca1fdcc64e..cbc03f8ce9 100644 --- a/src/compile/render-dom/wrappers/AwaitBlock.ts +++ b/src/compile/render-dom/wrappers/AwaitBlock.ts @@ -80,7 +80,7 @@ export default class AwaitBlockWrapper extends Wrapper { status, renderer, block, - parent, + this, child, stripWhitespace, nextSibling diff --git a/test/runtime/samples/await-containing-if/_config.js b/test/runtime/samples/await-containing-if/_config.js new file mode 100644 index 0000000000..9e24e4f614 --- /dev/null +++ b/test/runtime/samples/await-containing-if/_config.js @@ -0,0 +1,35 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + props: { + thePromise, + show: true + }, + + html: ` +

loading...

+ `, + + test({ assert, component, target }) { + fulfil(42); + + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+ `); + + component.show = false; + assert.htmlEqual(target.innerHTML, `
`); + + component.show = true; + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-containing-if/main.html b/test/runtime/samples/await-containing-if/main.html new file mode 100644 index 0000000000..cab07e6499 --- /dev/null +++ b/test/runtime/samples/await-containing-if/main.html @@ -0,0 +1,11 @@ +
+ {#await thePromise} +

loading...

+ {:then theValue} + {#if show} +

the value is {theValue}

+ {/if} + {:catch theError} +

oh no! {theError.message}

+ {/await} +
\ No newline at end of file From f8d69e296fd18ff170ed9245f26588ffbbb060d4 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Sat, 5 Jan 2019 14:12:42 -0500 Subject: [PATCH 05/13] replace with (#1846) --- src/compile/nodes/{Document.ts => Body.ts} | 4 ++-- src/compile/nodes/shared/mapChildren.ts | 6 +++--- .../render-dom/wrappers/{Document.ts => Body.ts} | 12 ++++++------ src/compile/render-dom/wrappers/Fragment.ts | 6 +++--- src/compile/render-ssr/Renderer.ts | 4 ++-- src/parse/state/tag.ts | 6 +++--- .../samples/error-svelte-selfdestructive/error.json | 4 ++-- test/runtime/samples/document-event/_config.js | 8 ++++---- test/runtime/samples/document-event/main.html | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) rename src/compile/nodes/{Document.ts => Body.ts} (87%) rename src/compile/render-dom/wrappers/{Document.ts => Body.ts} (60%) diff --git a/src/compile/nodes/Document.ts b/src/compile/nodes/Body.ts similarity index 87% rename from src/compile/nodes/Document.ts rename to src/compile/nodes/Body.ts index 48e985291c..74c63b1f57 100644 --- a/src/compile/nodes/Document.ts +++ b/src/compile/nodes/Body.ts @@ -1,8 +1,8 @@ import Node from './shared/Node'; import EventHandler from './EventHandler'; -export default class Document extends Node { - type: 'Document'; +export default class Body extends Node { + type: 'Body'; handlers: EventHandler[]; constructor(component, parent, scope, info) { diff --git a/src/compile/nodes/shared/mapChildren.ts b/src/compile/nodes/shared/mapChildren.ts index 775f72dc16..21df3e2918 100644 --- a/src/compile/nodes/shared/mapChildren.ts +++ b/src/compile/nodes/shared/mapChildren.ts @@ -1,6 +1,6 @@ import AwaitBlock from '../AwaitBlock'; +import Body from '../Body'; import Comment from '../Comment'; -import Document from '../Document'; import EachBlock from '../EachBlock'; import Element from '../Element'; import Head from '../Head'; @@ -19,8 +19,8 @@ import Node from './Node'; function getConstructor(type): typeof Node { switch (type) { case 'AwaitBlock': return AwaitBlock; + case 'Body': return Body; case 'Comment': return Comment; - case 'Document': return Document; case 'EachBlock': return EachBlock; case 'Element': return Element; case 'Head': return Head; @@ -50,4 +50,4 @@ export default function mapChildren(component, parent, scope, children: any[]) { return node; }); -} \ No newline at end of file +} diff --git a/src/compile/render-dom/wrappers/Document.ts b/src/compile/render-dom/wrappers/Body.ts similarity index 60% rename from src/compile/render-dom/wrappers/Document.ts rename to src/compile/render-dom/wrappers/Body.ts index a87a486f6c..678c790053 100644 --- a/src/compile/render-dom/wrappers/Document.ts +++ b/src/compile/render-dom/wrappers/Body.ts @@ -1,22 +1,22 @@ import Block from '../Block'; import Wrapper from './shared/Wrapper'; import deindent from '../../../utils/deindent'; -import Document from '../../nodes/Document'; +import Body from '../../nodes/Body'; -export default class DocumentWrapper extends Wrapper { - node: Document; +export default class BodyWrapper extends Wrapper { + node: Body; render(block: Block, parentNode: string, parentNodes: string) { this.node.handlers.forEach(handler => { const snippet = handler.render(block); block.builders.init.addBlock(deindent` - document.addEventListener("${handler.name}", ${snippet}); + document.body.addEventListener("${handler.name}", ${snippet}); `); block.builders.destroy.addBlock(deindent` - document.removeEventListener("${handler.name}", ${snippet}); + document.body.removeEventListener("${handler.name}", ${snippet}); `); }); } -} \ No newline at end of file +} diff --git a/src/compile/render-dom/wrappers/Fragment.ts b/src/compile/render-dom/wrappers/Fragment.ts index 56c7e2d6f0..aad4c74c80 100644 --- a/src/compile/render-dom/wrappers/Fragment.ts +++ b/src/compile/render-dom/wrappers/Fragment.ts @@ -1,7 +1,7 @@ import Wrapper from './shared/Wrapper'; import AwaitBlock from './AwaitBlock'; +import Body from './Body'; import DebugTag from './DebugTag'; -import Document from './Document'; import EachBlock from './EachBlock'; import Element from './Element'; import Head from './Head'; @@ -21,8 +21,8 @@ import Block from '../Block'; const wrappers = { AwaitBlock, + Body, Comment: null, - Document, DebugTag, EachBlock, Element, @@ -140,4 +140,4 @@ export default class FragmentWrapper { this.nodes[i].render(block, parentNode, parentNodes); } } -} \ No newline at end of file +} diff --git a/src/compile/render-ssr/Renderer.ts b/src/compile/render-ssr/Renderer.ts index 9e98979e26..ad7d44459e 100644 --- a/src/compile/render-ssr/Renderer.ts +++ b/src/compile/render-ssr/Renderer.ts @@ -19,9 +19,9 @@ function noop(){} const handlers: Record = { AwaitBlock, + Body: noop, Comment, DebugTag, - Document: noop, EachBlock, Element, Head, @@ -64,4 +64,4 @@ export default class Renderer { handler(node, this, options); }); } -} \ No newline at end of file +} diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index d2cc9cc9ba..6e53ab03e0 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -14,7 +14,7 @@ const metaTags = new Map([ ['svelte:head', 'Head'], ['svelte:meta', 'Meta'], ['svelte:window', 'Window'], - ['svelte:document', 'Document'] + ['svelte:body', 'Body'] ]); const valid_meta_tags = [...metaTags.keys(), 'svelte:self', 'svelte:component']; @@ -101,7 +101,7 @@ export default function tag(parser: Parser) { const slug = metaTags.get(name).toLowerCase(); if (isClosingTag) { if ( - (name === 'svelte:window' || name === 'svelte:document') && + (name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length ) { parser.error({ @@ -540,4 +540,4 @@ function readSequence(parser: Parser, done: () => boolean) { code: `unexpected-eof`, message: `Unexpected end of input` }); -} \ No newline at end of file +} diff --git a/test/parser/samples/error-svelte-selfdestructive/error.json b/test/parser/samples/error-svelte-selfdestructive/error.json index 1b4dc7fdec..bdef619538 100644 --- a/test/parser/samples/error-svelte-selfdestructive/error.json +++ b/test/parser/samples/error-svelte-selfdestructive/error.json @@ -1,10 +1,10 @@ { "code": "invalid-tag-name", - "message": "Valid tag names are svelte:head, svelte:meta, svelte:window, svelte:document, svelte:self or svelte:component", + "message": "Valid tag names are svelte:head, svelte:meta, svelte:window, svelte:body, svelte:self or svelte:component", "pos": 10, "start": { "character": 10, "line": 2, "column": 2 } -} \ No newline at end of file +} diff --git a/test/runtime/samples/document-event/_config.js b/test/runtime/samples/document-event/_config.js index 98e619af0f..f5e4b46f4f 100644 --- a/test/runtime/samples/document-event/_config.js +++ b/test/runtime/samples/document-event/_config.js @@ -1,13 +1,13 @@ export default { - test({ assert, component, target, window }) { + test({ assert, component, window }) { assert.deepEqual(component.events, []); const event1 = new window.Event('mouseenter'); - window.document.dispatchEvent(event1); + window.document.body.dispatchEvent(event1); assert.deepEqual(component.events, ['enter']); const event2 = new window.Event('mouseleave'); - window.document.dispatchEvent(event2); + window.document.body.dispatchEvent(event2); assert.deepEqual(component.events, ['enter', 'leave']); }, -}; \ No newline at end of file +}; diff --git a/test/runtime/samples/document-event/main.html b/test/runtime/samples/document-event/main.html index 6e7ab3a47c..078ae1ee02 100644 --- a/test/runtime/samples/document-event/main.html +++ b/test/runtime/samples/document-event/main.html @@ -6,4 +6,4 @@ } - \ No newline at end of file + From a035676ef054094f189dd7dd7721d80adf6124cd Mon Sep 17 00:00:00 2001 From: Conduitry Date: Wed, 9 Jan 2019 11:09:48 -0500 Subject: [PATCH 06/13] guide: update docs for --- site/content/guide/06-special-components.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/site/content/guide/06-special-components.md b/site/content/guide/06-special-components.md index 47c31c3733..56ee2b4964 100644 --- a/site/content/guide/06-special-components.md +++ b/site/content/guide/06-special-components.md @@ -114,11 +114,9 @@ You can also bind to certain values — so far `innerWidth`, `outerWidth`, `inne ``` -### `` +### `` -TODO REPLACE THIS WITH svelte:body - -The `` tag, just like ``, gives you a convenient way to declaratively add event listeners to the `document` object. This is useful for listening to events that don't fire on `window`, such as `mouseenter` and `mouseleave`. +The `` tag, just like ``, gives you a convenient way to declaratively add event listeners to the `document.body` object. This is useful for listening to events that don't fire on `window`, such as `mouseenter` and `mouseleave`. ### `` From 7bc461f6ee538e583ac409e65f7ed2d2cd54739c Mon Sep 17 00:00:00 2001 From: Conduitry Date: Wed, 9 Jan 2019 11:29:25 -0500 Subject: [PATCH 07/13] site: update readme and tidy package.json --- site/README.md | 83 +++++++++++++---------------------------------- site/package.json | 10 +++--- 2 files changed, 27 insertions(+), 66 deletions(-) diff --git a/site/README.md b/site/README.md index eb3c7013ce..591e3f0cfb 100644 --- a/site/README.md +++ b/site/README.md @@ -1,75 +1,36 @@ -# sapper-template-rollup +## Running locally -A version of the default [Sapper](https://github.com/sveltejs/sapper) template that uses Rollup instead of webpack. To clone it and get started: +Set up the project: ```bash -npx degit sveltejs/sapper-template#rollup my-app -cd my-app -npm install # or yarn! -npm run dev +git clone https://github.com/sveltejs/svelte.git +cd svelte/site +npm ci +npm run update ``` -Open up [localhost:3000](http://localhost:3000) and start clicking around. - -Consult [sapper.svelte.technology](https://sapper.svelte.technology) for help getting started. - -*[Click here for the webpack version of this template](https://github.com/sveltejs/sapper-template)* - -## Structure - -Sapper expects to find three directories in the root of your project — `app`, `assets` and `routes`. - - -### app - -The [app](app) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file. +Start the server with `npm run dev`, and navigate to [localhost:3000](http://localhost:3000). +## Using a local copy of Svelte -### assets +By default, the REPL will fetch the most recent version of Svelte from https://unpkg.com/svelte. If you need to test a local version of Svelte, you can do so by linking it and navigating to [localhost:3000/repl?version=local](http://localhost:3000/repl?version=local): -The [assets](assets) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv). - -In your [service-worker.js](app/service-worker.js) file, you can import these as `assets` from the generated manifest... +```bash +cd /path/to/svelte +npm link +npm run dev # rebuild Svelte on changes -```js -import { assets } from './manifest/service-worker.js'; +cd /path/to/svelte/site +npm link svelte +npm run dev ``` -...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files). - - -### routes - -This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*. - -**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.) +## REPL GitHub integration -**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example. +In order for the REPL's GitHub integration to work properly when running locally, you will need to create a GitHub OAuth app. Set its authorization callback URL to `http://localhost:3000/auth/callback`, and in this project, create `site/.env` containing: -There are three simple rules for naming the files that define your routes: - -* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route -* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`. -* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route - - -## Rollup config - -Sapper uses Rollup to provide code-splitting and dynamic imports, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like. - - -## Production mode and deployment - -To start a production version of your app, run `npm run build && npm start`. - -You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands: - -```bash -npm install -g now -now ``` - - -## Bugs and feedback - -Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues). +GITHUB_CLIENT_ID=[your app's client id] +GITHUB_CLIENT_SECRET=[your app's client secret] +BASEURL=http://localhost:3000 +``` diff --git a/site/package.json b/site/package.json index 7ecd42b767..2e633b774c 100644 --- a/site/package.json +++ b/site/package.json @@ -1,17 +1,17 @@ { - "name": "TODO", - "description": "TODO", - "version": "0.0.1", + "name": "svelte.technology", + "version": "1.0.0", + "description": "Docs and examples for Svelte", "scripts": { "dev": "sapper dev", "sapper": "sapper build --legacy", - "update_template": "sh ./scripts/update_template.sh", + "update": "sh scripts/update_template.sh && node scripts/get-contributors.js", "start": "node __sapper__/build", "cy:run": "cypress run", "cy:open": "cypress open", "test": "run-p --race dev cy:run", "deploy": "npm run stage && now alias", - "prestage": "npm run update_template && node scripts/get-contributors.js && npm run sapper", + "prestage": "npm run update && npm run sapper", "stage": "now" }, "dependencies": { From 2b1be6f8ae03dc9f86ac2277e5783c637379e1cb Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:07:42 +1000 Subject: [PATCH 08/13] convert update_template.sh to js for windows support --- site/package.json | 3 ++- site/scripts/update_template.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 site/scripts/update_template.js diff --git a/site/package.json b/site/package.json index 2e633b774c..b9390f3c39 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "sapper dev", "sapper": "sapper build --legacy", - "update": "sh scripts/update_template.sh && node scripts/get-contributors.js", + "update": "node scripts/update_template.js && node scripts/get-contributors.js", "start": "node __sapper__/build", "cy:run": "cypress run", "cy:open": "cypress open", @@ -29,6 +29,7 @@ "passport-github": "^1.1.0", "prismjs": "^1.15.0", "session-file-store": "^1.2.0", + "shelljs": "^0.8.3", "sirv": "^0.2.0", "yootils": "0.0.14" }, diff --git a/site/scripts/update_template.js b/site/scripts/update_template.js new file mode 100644 index 0000000000..23e2333bab --- /dev/null +++ b/site/scripts/update_template.js @@ -0,0 +1,26 @@ +const sh = require('shelljs'); +const fs = require('fs') + +sh.cd(__dirname+'/../') + +// fetch svelte app +sh.rm('-rf','scripts/svelte-app') +sh.exec('npx degit sveltejs/template scripts/svelte-app') + +// update repl-viewer.css based on template +sh.cp('scripts/svelte-app/public/global.css', 'static/repl-viewer.css') + +// remove src (will be recreated client-side) and node_modules +sh.rm('-rf', 'scripts/svelte-app/src') +sh.rm('-rf', 'scripts/svelte-app/node_modules') + +// build svelte-app.json +const appPath = 'scripts/svelte-app' +let files = [] + +for (const path of sh.find(appPath).filter(p => fs.lstatSync(p).isFile()) ) { + files.push({ path: path.slice(appPath.length + 1), data: fs.readFileSync(path).toString() }); +} + +fs.writeFileSync('static/svelte-app.json', JSON.stringify(files)); + From 5ff3e7f52a65c66df1ce2c41eac9114c1fb4bbcb Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:07:58 +1000 Subject: [PATCH 09/13] remove redundant bash based update script --- site/scripts/update_template.sh | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100755 site/scripts/update_template.sh diff --git a/site/scripts/update_template.sh b/site/scripts/update_template.sh deleted file mode 100755 index 59ab3cb0cd..0000000000 --- a/site/scripts/update_template.sh +++ /dev/null @@ -1,15 +0,0 @@ -cd `dirname $0`/.. - -# fetch svelte-app -rm -rf scripts/svelte-app -node_modules/.bin/degit sveltejs/template scripts/svelte-app - -# update repl-viewer.css based on template -cp scripts/svelte-app/public/global.css static/repl-viewer.css - -# remove src (will be recreated client-side) and node_modules -rm -rf scripts/svelte-app/src -rm -rf scripts/svelte-app/node_modules - -# build svelte-app.json -node scripts/build-svelte-app-json.js `find scripts/svelte-app -type f` From 9b7d0152bc8b5d33a14bf67ea1d8bc3a8e1bde08 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Thu, 10 Jan 2019 17:39:10 +1000 Subject: [PATCH 10/13] handle markdown on windows machines --- site/src/utils/_process_markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/utils/_process_markdown.js b/site/src/utils/_process_markdown.js index a79d759cbb..f760018a3b 100644 --- a/site/src/utils/_process_markdown.js +++ b/site/src/utils/_process_markdown.js @@ -1,5 +1,5 @@ export default function process_markdown(markdown) { - const match = /---\n([\s\S]+?)\n---/.exec(markdown); + const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown); const frontMatter = match[1]; const content = markdown.slice(match[0].length); From 2c537e7cb43dea55de2f47d2e031c9d98a9b7e69 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Fri, 11 Jan 2019 13:19:00 +1000 Subject: [PATCH 11/13] POC for postmessage Repl --- .../repl/_components/Output/Viewer.html | 260 +++++++++++------- site/static/repl-runner.js | 136 +++++++++ 2 files changed, 293 insertions(+), 103 deletions(-) create mode 100644 site/static/repl-runner.js diff --git a/site/src/routes/repl/_components/Output/Viewer.html b/site/src/routes/repl/_components/Output/Viewer.html index ef377b8b3c..9cfe5a5942 100644 --- a/site/src/routes/repl/_components/Output/Viewer.html +++ b/site/src/routes/repl/_components/Output/Viewer.html @@ -14,9 +14,14 @@ export let error; export function setProp(prop, value) { - if (component) { - component[prop] = value; - } + if (!refs.child) return; + refs.child.contentWindow.postMessage({ + action: 'set_prop', + args: { + prop: prop, + value: value + } + },'*'); } let component; @@ -81,9 +86,84 @@ onMount(() => { refs.child.addEventListener('load', () => { const iframe = refs.child; - const body = iframe.contentDocument.body; - const evalInIframe = iframe.contentWindow.eval; + let evalId = 1; + let fetchId = 1; + let fetchHandler = null; + let pendingResults = new Map(); + + const evalInIframe = function(scriptToEval) { + let id = evalId++; + let promise = new Promise((resolve,reject) => { + iframe.contentWindow.postMessage({ + action: "eval", + args: { + evalId: id, + script: scriptToEval + } + }, '*') + pendingResults.set(id, { resolve: resolve, reject: reject }); + }); + return promise; + } + + const handleReplMessage = (ev) => { + console.log("got message in parent", ev.data); + let action = ev.data.action; + if (action == "evalError") { + let { message, stack, evalId } = ev.data.args; + let e = new Error(message); + e.stack = e.stack; + let resultHandler = pendingResults.get(evalId); + if (resultHandler) { + pendingResults.delete(evalId); + resultHandler.reject(e); + } else { + console.err("error evaluating script in iframe", e); + } + } + + if (action == "evalOk") { + let { evalId } = ev.data.args; + let resultHandler = pendingResults.get(evalId); + if (resultHandler) { + pendingResults.delete(evalId); + resultHandler.resolve(); + } + } + + if (action == "prop_update") { + let { prop, value } = ev.data.args; + dispatch('binding', { prop, value }); + values_store.update(values => Object.assign({}, values, { + [prop]: value + })); + } + + if (action == "fetch_complete") { + console.log(fetchHandler, ev.data.args); + if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) { + fetchHandler.resolve() + fetchHandler = null; + } + } + if (action == "fetch_error") { + if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) { + fetchHandler.reject(new Error(ev.data.args.message)); + fetchHandler = null; + } + } + if (action == "fetch_progress") { + if (fetchHandler && fetchHandler.fetchId == ev.data.args.fetchId) { + pendingImports = ev.data.args.remaining; + } + } + } + + window.addEventListener("message", handleReplMessage, false); + + // iframe.contentWindow.eval; + /* TODO // intercept links, so that we can use #hashes inside the iframe body.addEventListener('click', event => { if (event.which !== 1) return; @@ -109,6 +189,7 @@ window.open(el.href, '_blank'); }); + */ let promise = null; let updating = false; @@ -121,11 +202,14 @@ const imports = []; const missingImports = bundle.imports.filter(x => !importCache[x]); + const removeStyles = () => { - const styles = iframe.contentDocument.querySelectorAll('style.svelte'); - let i = styles.length; - while (i--) styles[i].parentNode.removeChild(styles[i]); + evalInIframe(` + const styles = document.querySelectorAll('style.svelte'); + let i = styles.length; + while (i--) styles[i].parentNode.removeChild(styles[i]); + `) }; const ready = () => { @@ -137,14 +221,7 @@ toDestroy.$destroy(); toDestroy = null; } - - bundle.imports.forEach(x => { - const module = importCache[x]; - const name = bundle.importMap.get(x); - - iframe.contentWindow[name] = module; - }); - + if (ssr) { // this only gets generated if component uses lifecycle hooks pending = true; createHtml(); @@ -155,54 +232,45 @@ }; const createHtml = () => { - try { - evalInIframe(`${ssr.code} - var rendered = SvelteComponent.render(${JSON.stringify($values_store)}); - - if (rendered.css.code) { - var style = document.createElement('style'); - style.className = 'svelte'; - style.textContent = rendered.css.code; - document.head.appendChild(style); - } - - document.body.innerHTML = rendered.html; - `) - } catch (e) { - const loc = getLocationFromStack(e.stack, ssr.map); - if (loc) { - e.filename = loc.source; - e.loc = { line: loc.line, column: loc.column }; + + evalInIframe(`${ssr.code} + var rendered = SvelteComponent.render(${JSON.stringify($values_store)}); + + if (rendered.css.code) { + var style = document.createElement('style'); + style.className = 'svelte'; + style.textContent = rendered.css.code; + document.head.appendChild(style); } - error = e; - } - }; + document.body.innerHTML = rendered.html; + `).catch( e => { + + const loc = getLocationFromStack(e.stack, ssr.map); + if (loc) { + e.filename = loc.source; + e.loc = { line: loc.line, column: loc.column }; + } + error = e; + }); + }; + const createComponent = () => { // remove leftover styles from SSR renderer if (ssr) removeStyles(); - try { - evalInIframe(`${dom.code} - document.body.innerHTML = ''; - window.location.hash = ''; - window._svelteTransitionManager = null; - - var component = new SvelteComponent({ - target: document.body, - props: ${JSON.stringify($values_store)} - });`); - - component = window.app = window.component = iframe.contentWindow.component; - - // component.on('state', ({ current }) => { - // if (updating) return; - // updating = true; - // this.fire('data', { current }); - // updating = false; - // }); - } catch (e) { + evalInIframe(`${dom.code} + document.body.innerHTML = ''; + window.location.hash = ''; + window._svelteTransitionManager = null; + + window.component = new SvelteComponent({ + target: document.body, + props: ${JSON.stringify($values_store)} + });`) + .catch(e=> { + // TODO show in UI component = null; @@ -213,35 +281,31 @@ } error = e; - } + }); }; - - pendingImports = missingImports.length; - - if (missingImports.length) { - let cancelled = false; - - promise = Promise.all( - missingImports.map(id => fetchImport(id, iframe.contentWindow.curl).then(module => { - pendingImports -= 1; - return module; - })) - ); - promise.cancel = () => cancelled = true; - - promise - .then(() => { - if (cancelled) return; - ready(); - }) - .catch(e => { - if (cancelled) return; - error = e; - }); - } else { + + + new Promise((resolve, reject)=> { + fetchHandler = { + fetchId: fetchId++, + resolve: resolve, + reject: reject + } + iframe.contentWindow.postMessage({ + action: "fetch_imports", + args: { + bundle: bundle, + fetchId: fetchHandler.fetchId + } + }, '*'); + }) + .then(() => { ready(); - } - + }) + .catch(e => { + error = e; + }); + run = () => { pending = false; @@ -252,7 +316,7 @@ bundle_handler = bundle => { if (!bundle) return; // TODO can this ever happen? - if (promise) promise.cancel(); + if (fetchHandler) fetchHandler = null; toDestroy = component; component = null; @@ -261,23 +325,12 @@ }; props_handler = props => { - if (!component) { - // TODO can this happen? - console.error(`no component to bind to`); - return; - } - - props.forEach(prop => { - // TODO should there be a public API for binding? - // e.g. `component.$watch(prop, handler)`? - // (answer: probably) - component.$$.bound[prop] = value => { - dispatch('binding', { prop, value }); - values_store.update(values => Object.assign({}, values, { - [prop]: value - })); - }; - }); + iframe.contentWindow.postMessage({ + action:"bind_props", + args: { + props: [...props] + } + },'*') }; }); }); @@ -346,7 +399,7 @@
- diff --git a/site/static/repl-runner.js b/site/static/repl-runner.js new file mode 100644 index 0000000000..d346ad1fc5 --- /dev/null +++ b/site/static/repl-runner.js @@ -0,0 +1,136 @@ + + + +const importCache = {}; + + + +function fetchImport(id) { + return new Promise((fulfil, reject) => { + curl([`https://bundle.run/${id}`]).then(module => { + importCache[id] = module; + fulfil(module); + }, err => { + console.error(err.stack); + reject(new Error(`Error loading ${id} from bundle.run`)); + }); + }); +} + +function fetchImports(bundle, progressFunc) { + const missingImports = bundle.imports.filter(x => !importCache[x]); + let pendingImports = missingImports.length; + + if (missingImports.length) { + let promise = Promise.all( + missingImports.map(id => fetchImport(id).then(() => { + pendingImports -= 1; + if (progressFunc) progressFunc(pendingImports); + })) + ); + + return promise + } else { + return P.resolve(); + } +} + + + +function handleMessage(ev) { + if (ev.data.action == "eval") { + let { script, evalId } = ev.data.args; + try { + eval(script); + parent.postMessage({ + action: "evalOk", + args: { + evalId: evalId + } + }, ev.origin); + } catch (e) { + parent.postMessage({ + action: "evalError", + args: { + evalId: evalId, + stack: e.stack, + message: e.message + } + }, ev.origin); + } + } + + if (ev.data.action == "bind_props") { + let { props } = ev.data.args + + if (!window.component) { + // TODO can this happen? + console.error(`no component to bind to`); + return; + } + + props.forEach(prop => { + // TODO should there be a public API for binding? + // e.g. `component.$watch(prop, handler)`? + // (answer: probably) + window.component.$$.bound[prop] = value => { + parent.postMessage({ + action: "propUpdate", + args: { + prop: prop, + value: value + } + }, ev.origin); + }; + }); + } + + if (ev.data.action == "set_prop") { + if (!window.component) { + return; + } + let { prop, value } = ev.data.args; + component[prop] = value; + } + + if (ev.data.action == "fetch_imports") { + let { bundle, fetchId } = ev.data.args; + fetchImports(bundle, (remaining) => { + parent.postMessage({ + action: "fetch_progress", + args: { + fetchId: fetchId, + remaining: remaining + } + }, ev.origin); + }) + .then(() => { + bundle.imports.forEach(x=> { + const module = importCache[x]; + const name = bundle.importMap.get(x); + window[name] = module; + }); + + parent.postMessage({ + action: "fetch_complete", + args: { + fetchId: fetchId + } + }, ev.origin); + }) + .catch(e => { + parent.postMessage({ + action: "fetch_error", + args: { + fetchId: fetchId, + message: e.message + } + }, ev.origin); + }) + } +} + +window.addEventListener("message", handleMessage, false) + +console.log("repl-runner initialized"); + From 5eafdd446f265900bfd7f13087a6e5804e0b5dc8 Mon Sep 17 00:00:00 2001 From: David Pershouse Date: Sat, 12 Jan 2019 01:46:33 +1000 Subject: [PATCH 12/13] abstract repl interaction code --- .../repl/_components/Output/Viewer.html | 244 +++++------------- site/src/routes/repl/_utils/replProxy.js | 87 +++++++ site/static/repl-runner.js | 107 ++++---- 3 files changed, 201 insertions(+), 237 deletions(-) create mode 100644 site/src/routes/repl/_utils/replProxy.js diff --git a/site/src/routes/repl/_components/Output/Viewer.html b/site/src/routes/repl/_components/Output/Viewer.html index 9cfe5a5942..e62e0d695b 100644 --- a/site/src/routes/repl/_components/Output/Viewer.html +++ b/site/src/routes/repl/_components/Output/Viewer.html @@ -1,6 +1,7 @@ - -{#if visible} -
delayed
-{/if} \ No newline at end of file diff --git a/test/runtime/samples/transition-css-duration/_config.js b/test/runtime/samples/transition-css-duration/_config.js index ed25357681..28a78944bb 100644 --- a/test/runtime/samples/transition-css-duration/_config.js +++ b/test/runtime/samples/transition-css-duration/_config.js @@ -4,8 +4,10 @@ export default { const div = target.querySelector('div'); raf.tick(25); + component.visible = false; + raf.tick(26); assert.ok(~div.style.animation.indexOf('25ms')); }, }; diff --git a/test/runtime/samples/transition-css-in-out-in/_config.js b/test/runtime/samples/transition-css-in-out-in/_config.js new file mode 100644 index 0000000000..10719280e9 --- /dev/null +++ b/test/runtime/samples/transition-css-in-out-in/_config.js @@ -0,0 +1,20 @@ +export default { + test({ assert, component, target, window, raf }) { + component.visible = true; + const div = target.querySelector('div'); + + assert.equal(div.style.animation, `__svelte_3809512021_0 100ms linear 0ms 1 both`); + + raf.tick(50); + component.visible = false; + + // both in and out styles + assert.equal(div.style.animation, `__svelte_3809512021_0 100ms linear 0ms 1 both, __svelte_3750847757_0 100ms linear 0ms 1 both`); + + raf.tick(75); + component.visible = true; + + // reset original styles + assert.equal(div.style.animation, `__svelte_3809512021_1 100ms linear 0ms 1 both`); + }, +}; diff --git a/test/runtime/samples/transition-css-in-out-in/main.html b/test/runtime/samples/transition-css-in-out-in/main.html new file mode 100644 index 0000000000..5108452a39 --- /dev/null +++ b/test/runtime/samples/transition-css-in-out-in/main.html @@ -0,0 +1,25 @@ + + +{#if visible} +
+{/if} \ No newline at end of file diff --git a/test/runtime/samples/transition-js-aborted-outro-in-each/_config.js b/test/runtime/samples/transition-js-aborted-outro-in-each/_config.js index 75242d21af..e78862e86c 100644 --- a/test/runtime/samples/transition-js-aborted-outro-in-each/_config.js +++ b/test/runtime/samples/transition-js-aborted-outro-in-each/_config.js @@ -7,7 +7,7 @@ export default { ] }, - test({ assert, component, target, window, raf }) { + test({ assert, component, target, raf }) { const { things } = component; component.things = []; diff --git a/test/runtime/samples/transition-js-aborted-outro/_config.js b/test/runtime/samples/transition-js-aborted-outro/_config.js index 87ad32be07..d61407e1bd 100644 --- a/test/runtime/samples/transition-js-aborted-outro/_config.js +++ b/test/runtime/samples/transition-js-aborted-outro/_config.js @@ -3,7 +3,7 @@ export default { visible: true, }, - test({ assert, component, target, window, raf }) { + test({ assert, component, target, raf }) { component.visible = false; const span = target.querySelector('span'); diff --git a/test/runtime/samples/transition-js-await-block/_config.js b/test/runtime/samples/transition-js-await-block/_config.js index 5cddc8a95c..2e01dd7f76 100644 --- a/test/runtime/samples/transition-js-await-block/_config.js +++ b/test/runtime/samples/transition-js-await-block/_config.js @@ -13,7 +13,7 @@ export default { intro: true, - test({ assert, component, target, window, raf }) { + test({ assert, target, raf }) { let p = target.querySelector('p'); assert.equal(p.className, 'pending'); diff --git a/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js b/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js index 6036be6055..aa0bd9e1aa 100644 --- a/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js +++ b/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js @@ -3,7 +3,7 @@ export default { name: 'world' }, - test({ assert, component, target, window, raf }) { + test({ assert, component, target, raf }) { global.count = 0; component.visible = true; diff --git a/test/runtime/samples/transition-js-if-block-in-each-block-bidi-3/_config.js b/test/runtime/samples/transition-js-if-block-in-each-block-bidi-3/_config.js index 261731c700..04df38a09c 100644 --- a/test/runtime/samples/transition-js-if-block-in-each-block-bidi-3/_config.js +++ b/test/runtime/samples/transition-js-if-block-in-each-block-bidi-3/_config.js @@ -12,9 +12,7 @@ export default {
5
`, - test({ assert, component, target, window, raf }) { - const divs = target.querySelectorAll('div'); - + test({ assert, component, target, raf }) { raf.tick(100); component.threshold = 4; diff --git a/test/runtime/samples/transition-js-if-else-block-intro/_config.js b/test/runtime/samples/transition-js-if-else-block-intro/_config.js index 2bf1483dac..c5eccf50e5 100644 --- a/test/runtime/samples/transition-js-if-else-block-intro/_config.js +++ b/test/runtime/samples/transition-js-if-else-block-intro/_config.js @@ -1,7 +1,7 @@ export default { intro: true, - test({ assert, component, target, window, raf }) { + test({ assert, component, target, raf }) { assert.equal(target.querySelector('div'), component.no); assert.equal(component.no.foo, 0); diff --git a/test/runtime/samples/transition-js-nested-each-delete/_config.js b/test/runtime/samples/transition-js-nested-each-delete/_config.js index c2a886db9a..041cbebc66 100644 --- a/test/runtime/samples/transition-js-nested-each-delete/_config.js +++ b/test/runtime/samples/transition-js-nested-each-delete/_config.js @@ -4,7 +4,7 @@ export default { things: ['a', 'b', 'c'] }, - test({ assert, component, target, window, raf }) { + test({ assert, component, target, raf }) { assert.htmlEqual(target.innerHTML, `
a
b