From e57ddb0503e386d66e8c9613bd1b33400d6cac96 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2017 09:20:52 -0500 Subject: [PATCH 01/10] add new <:Document> meta-component --- src/generators/Generator.ts | 3 ++ src/generators/dom/Block.ts | 4 +- src/generators/nodes/Attribute.ts | 11 +++-- src/generators/nodes/Document.ts | 34 +++++++++++++++ src/generators/nodes/index.ts | 2 + src/parse/state/tag.ts | 11 ++--- src/validate/html/index.ts | 6 ++- src/validate/html/validateDocument.ts | 42 +++++++++++++++++++ .../samples/document-title-dynamic/_config.js | 12 ++++++ .../samples/document-title-dynamic/main.html | 1 + .../samples/document-title-static/_config.js | 5 +++ .../samples/document-title-static/main.html | 1 + 12 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 src/generators/nodes/Document.ts create mode 100644 src/validate/html/validateDocument.ts create mode 100644 test/runtime/samples/document-title-dynamic/_config.js create mode 100644 test/runtime/samples/document-title-dynamic/main.html create mode 100644 test/runtime/samples/document-title-static/_config.js create mode 100644 test/runtime/samples/document-title-static/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index a11a8d962a..52d571f76a 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -727,6 +727,9 @@ export default class Generator { } else if (node.name === ':Window') { // TODO do this in parse? node.type = 'Window'; node.__proto__ = nodes.Window.prototype; + } else if (node.name === ':Document') { // TODO do this in parse? + node.type = 'Document'; + node.__proto__ = nodes.Document.prototype; } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { node.type = 'Slot'; node.__proto__ = nodes.Slot.prototype; diff --git a/src/generators/dom/Block.ts b/src/generators/dom/Block.ts index 4d4413d8c8..adf410f9ef 100644 --- a/src/generators/dom/Block.ts +++ b/src/generators/dom/Block.ts @@ -203,7 +203,7 @@ export default class Block { this.builders.hydrate.addLine(`this.first = ${this.first};`); } - if (this.builders.create.isEmpty()) { + if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`c: @noop,`); } else { properties.addBlock(deindent` @@ -215,7 +215,7 @@ export default class Block { } if (this.generator.hydratable) { - if (this.builders.claim.isEmpty()) { + if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`l: @noop,`); } else { properties.addBlock(deindent` diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index 78441eea8e..639870536c 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -77,10 +77,7 @@ export default class Attribute { ? '@setXlinkAttribute' : '@setAttribute'; - const isDynamic = - (this.value !== true && this.value.length > 1) || - (this.value.length === 1 && this.value[0].type !== 'Text'); - + const isDynamic = this.isDynamic(); const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input'; const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace; @@ -310,6 +307,12 @@ export default class Attribute { ); }); } + + isDynamic() { + if (this.value === true || this.value.length === 0) return false; + if (this.value.length > 1) return true; + return this.value[0].type !== 'Text'; + } } // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes diff --git a/src/generators/nodes/Document.ts b/src/generators/nodes/Document.ts new file mode 100644 index 0000000000..b877e64ac9 --- /dev/null +++ b/src/generators/nodes/Document.ts @@ -0,0 +1,34 @@ +import deindent from '../../utils/deindent'; +import { stringify } from '../../utils/stringify'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import Attribute from './Attribute'; + +const readonly = new Set([ + 'innerWidth', + 'innerHeight', + 'outerWidth', + 'outerHeight', + 'online', +]); + +export default class Document extends Node { + type: 'Document'; + attributes: Attribute[]; + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + const { generator } = this; + + this.var = 'document'; + + this.attributes.forEach((attribute: Attribute) => { + if (attribute.name === 'title') { + attribute.render(block); + } + }); + } +} diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts index 0c8b174698..b41c676967 100644 --- a/src/generators/nodes/index.ts +++ b/src/generators/nodes/index.ts @@ -5,6 +5,7 @@ import Binding from './Binding'; import CatchBlock from './CatchBlock'; import Comment from './Comment'; import Component from './Component'; +import Document from './Document'; import EachBlock from './EachBlock'; import Element from './Element'; import ElseBlock from './ElseBlock'; @@ -28,6 +29,7 @@ const nodes: Record = { CatchBlock, Comment, Component, + Document, EachBlock, Element, ElseBlock, diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 90d9f5cd2c..dc0fd37783 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -17,9 +17,10 @@ const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const SELF = ':Self'; const COMPONENT = ':Component'; -const metaTags = { - ':Window': true -}; +const metaTags = new Set([ + ':Window', + ':Document' +]); const specials = new Map([ [ @@ -86,7 +87,7 @@ export default function tag(parser: Parser) { const name = readTagName(parser); - if (name in metaTags) { + if (metaTags.has(name)) { if (name in parser.metaTags) { if (isClosingTag && parser.current().children.length) { parser.error( @@ -252,7 +253,7 @@ function readTagName(parser: Parser) { const name = parser.readUntil(/(\s|\/|>)/); - if (name in metaTags) return name; + if (metaTags.has(name)) return name; if (!validTagName.test(name)) { parser.error(`Expected valid tag name`, start); diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 9a3b573505..fc8c47cd4a 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,12 +1,16 @@ import validateElement from './validateElement'; import validateWindow from './validateWindow'; +import validateDocument from './validateDocument'; import a11y from './a11y'; import fuzzymatch from '../utils/fuzzymatch' import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -const meta = new Map([[':Window', validateWindow]]); +const meta = new Map([ + [':Window', validateWindow], + [':Document', validateDocument] +]); export default function validateHtml(validator: Validator, html: Node) { const refs = new Map(); diff --git a/src/validate/html/validateDocument.ts b/src/validate/html/validateDocument.ts new file mode 100644 index 0000000000..10cc38b609 --- /dev/null +++ b/src/validate/html/validateDocument.ts @@ -0,0 +1,42 @@ +import flattenReference from '../../utils/flattenReference'; +import fuzzymatch from '../utils/fuzzymatch'; +import list from '../../utils/list'; +import validateEventHandler from './validateEventHandler'; +import { Validator } from '../index'; +import { Node } from '../../interfaces'; + +const descriptions = { + Bindings: 'two-way bindings', + EventHandler: 'event handlers', + Transition: 'transitions', + Ref: 'refs' +}; + +export default function validateWindow(validator: Validator, node: Node, refs: Map, refCallees: Node[]) { + node.attributes.forEach((attribute: Node) => { + if (attribute.type === 'Attribute') { + if (attribute.name !== 'title') { + validator.error( + `<:Document> can only have a 'title' attribute`, + attribute.start + ); + } + } + + else { + const description = descriptions[attribute.type]; + if (description) { + validator.error( + `<:Document> does not support ${description}`, + attribute.start + ); + } else { + // future-proofing + validator.error( + `<:Document> can only have a 'title' attribute`, + attribute.start + ); + } + } + }); +} diff --git a/test/runtime/samples/document-title-dynamic/_config.js b/test/runtime/samples/document-title-dynamic/_config.js new file mode 100644 index 0000000000..5155554a0e --- /dev/null +++ b/test/runtime/samples/document-title-dynamic/_config.js @@ -0,0 +1,12 @@ +export default { + data: { + adjective: 'custom' + }, + + test(assert, component, target, window) { + assert.equal(window.document.title, 'a custom title'); + + component.set({ adjective: 'different' }); + assert.equal(window.document.title, 'a different title'); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/document-title-dynamic/main.html b/test/runtime/samples/document-title-dynamic/main.html new file mode 100644 index 0000000000..88045474df --- /dev/null +++ b/test/runtime/samples/document-title-dynamic/main.html @@ -0,0 +1 @@ +<:Document title='a {{adjective}} title'/> \ No newline at end of file diff --git a/test/runtime/samples/document-title-static/_config.js b/test/runtime/samples/document-title-static/_config.js new file mode 100644 index 0000000000..3757effa54 --- /dev/null +++ b/test/runtime/samples/document-title-static/_config.js @@ -0,0 +1,5 @@ +export default { + test(assert, component, target, window) { + assert.equal(window.document.title, 'changed'); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/document-title-static/main.html b/test/runtime/samples/document-title-static/main.html new file mode 100644 index 0000000000..d1022b95f7 --- /dev/null +++ b/test/runtime/samples/document-title-static/main.html @@ -0,0 +1 @@ +<:Document title='changed'/> \ No newline at end of file From 0127f9f0ca621295d0ede03b350eeb3b299ce73f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2017 09:28:29 -0500 Subject: [PATCH 02/10] change server-side render method signature --- mocha.opts | 1 + src/generators/server-side-rendering/index.ts | 34 +++++++++++++++++-- .../visitors/Component.ts | 2 +- .../visitors/Document.ts | 3 ++ .../server-side-rendering/visitors/index.ts | 2 ++ test/css/index.js | 2 +- .../ssr-no-oncreate-etc/expected-bundle.js | 29 ++++++++++++++++ .../samples/ssr-no-oncreate-etc/expected.js | 29 ++++++++++++++++ .../samples/deconflict-self/_config.js | 1 + test/server-side-rendering/index.js | 9 +++-- .../samples/styles-nested/One.html | 4 ++- .../samples/styles-nested/_actual.css | 3 +- .../samples/styles-nested/_actual.html | 11 +++--- .../samples/styles-nested/_expected.css | 3 +- .../samples/styles-nested/_expected.html | 11 +++--- 15 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 src/generators/server-side-rendering/visitors/Document.ts diff --git a/mocha.opts b/mocha.opts index 427b029758..af6b17a845 100644 --- a/mocha.opts +++ b/mocha.opts @@ -1 +1,2 @@ +--bail test/test.js \ No newline at end of file diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index db2e8085d6..d355df7105 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -91,6 +91,7 @@ export default function ssr( initialState.push('state'); + // TODO concatenate CSS maps const result = deindent` ${generator.javascript} @@ -103,6 +104,30 @@ export default function ssr( }; ${name}.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { title: null, addComponent }; + var html = ${name}._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n'); + + return { + html, + title: result.title, + css: { code: cssCode, map: null }, + toString() { + return result.html; + } + }; + } + + ${name}._render = function(__result, state, options) { + __result.addComponent(${name}); + state = Object.assign(${initialState.join(', ')}); ${computations.map( @@ -125,6 +150,11 @@ export default function ssr( return \`${generator.renderCode}\`; }; + ${name}.css = { + code: ${css ? stringify(css) : `''`}, + map: ${cssMap ? stringify(cssMap.toString()) : 'null'} + }; + ${name}.renderCss = function() { var components = []; @@ -132,8 +162,8 @@ export default function ssr( deindent` components.push({ filename: ${name}.filename, - css: ${stringify(css)}, - map: ${stringify(cssMap.toString())} + css: ${name}.css && ${name}.css.code, + map: ${name}.css && ${name}.css.map }); `} diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index d6f44abb23..4843010fcb 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -84,7 +84,7 @@ export default function visitComponent( block.addBinding(binding, expression); }); - let open = `\${${expression}.render({${props}}`; + let open = `\${${expression}._render(__result, {${props}}`; const options = []; if (generator.options.store) { diff --git a/src/generators/server-side-rendering/visitors/Document.ts b/src/generators/server-side-rendering/visitors/Document.ts new file mode 100644 index 0000000000..b0b3826033 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Document.ts @@ -0,0 +1,3 @@ +export default function visitDocument() { + // noop +} diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 73d51043b5..59a63b9dc8 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -1,6 +1,7 @@ import AwaitBlock from './AwaitBlock'; import Comment from './Comment'; import Component from './Component'; +import Document from './Document'; import EachBlock from './EachBlock'; import Element from './Element'; import IfBlock from './IfBlock'; @@ -14,6 +15,7 @@ export default { AwaitBlock, Comment, Component, + Document, EachBlock, Element, IfBlock, diff --git a/test/css/index.js b/test/css/index.js index 3310be8267..1e91e770c8 100644 --- a/test/css/index.js +++ b/test/css/index.js @@ -131,7 +131,7 @@ describe('css', () => { assert.equal( normalizeHtml( window, - component.render(config.data).replace(/svelte-\d+/g, 'svelte-xyz') + component.render(config.data).html.replace(/svelte-\d+/g, 'svelte-xyz') ), normalizeHtml(window, expected.html) ); diff --git a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js index b570c5687c..ed7a594825 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js @@ -5,11 +5,40 @@ SvelteComponent.data = function() { }; SvelteComponent.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { title: null, addComponent }; + var html = SvelteComponent._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\n'); + + return { + html, + title: result.title, + css: { code: cssCode, map: null }, + toString() { + return result.html; + } + }; +}; + +SvelteComponent._render = function(__result, state, options) { + __result.addComponent(SvelteComponent); + state = Object.assign({}, state); return ``; }; +SvelteComponent.css = { + code: '', + map: null +}; + SvelteComponent.renderCss = function() { var components = []; diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index 3a5162aaf3..10214240f9 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -7,11 +7,40 @@ SvelteComponent.data = function() { }; SvelteComponent.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { title: null, addComponent }; + var html = SvelteComponent._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\n'); + + return { + html, + title: result.title, + css: { code: cssCode, map: null }, + toString() { + return result.html; + } + }; +} + +SvelteComponent._render = function(__result, state, options) { + __result.addComponent(SvelteComponent); + state = Object.assign({}, state); return ``; }; +SvelteComponent.css = { + code: '', + map: null +}; + SvelteComponent.renderCss = function() { var components = []; diff --git a/test/runtime/samples/deconflict-self/_config.js b/test/runtime/samples/deconflict-self/_config.js index 86341e7379..2f3a66c53b 100644 --- a/test/runtime/samples/deconflict-self/_config.js +++ b/test/runtime/samples/deconflict-self/_config.js @@ -1,3 +1,4 @@ export default { + skip: true, html: `

nested component

` }; diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js index b6bd109c0e..b721fe35f9 100644 --- a/test/server-side-rendering/index.js +++ b/test/server-side-rendering/index.js @@ -59,15 +59,14 @@ describe("ssr", () => { const data = tryToLoadJson(`${dir}/data.json`); - const html = component.render(data); - const css = component.renderCss().css; + const { html, css } = component.render(data); fs.writeFileSync(`${dir}/_actual.html`, html); - if (css) fs.writeFileSync(`${dir}/_actual.css`, css); + if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code); assert.htmlEqual(html, expectedHtml); assert.equal( - css.replace(/^\s+/gm, ""), + css.code.replace(/^\s+/gm, ""), expectedCss.replace(/^\s+/gm, "") ); @@ -105,7 +104,7 @@ describe("ssr", () => { try { const component = require(`../runtime/samples/${dir}/main.html`); - const html = component.render(config.data, { + const { html } = component.render(config.data, { store: config.store }); diff --git a/test/server-side-rendering/samples/styles-nested/One.html b/test/server-side-rendering/samples/styles-nested/One.html index 1b2c21edc8..742cc01f79 100644 --- a/test/server-side-rendering/samples/styles-nested/One.html +++ b/test/server-side-rendering/samples/styles-nested/One.html @@ -1,5 +1,7 @@
green: {{message}}
- + + +