diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts
index 363f5118ec..0971b7552a 100644
--- a/src/generators/Generator.ts
+++ b/src/generators/Generator.ts
@@ -161,9 +161,9 @@ export default class Generator {
this.computations = [];
this.templateProperties = {};
- this.name = this.alias(name);
this.walkJs(dom);
+ this.name = this.alias(name);
if (options.customElement === true) {
this.customElement = {
@@ -731,6 +731,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 === ':Head') { // TODO do this in parse?
+ node.type = 'Head';
+ node.__proto__ = nodes.Head.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..036b4e9014 100644
--- a/src/generators/dom/Block.ts
+++ b/src/generators/dom/Block.ts
@@ -132,10 +132,11 @@ export default class Block {
) {
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
- this.builders.claim.addLine(`${name} = ${claimStatement};`);
+ this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`);
if (parentNode) {
this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`);
+ if (parentNode === 'document.head') this.builders.unmount.addLine(`@detachNode(${name});`);
} else {
this.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
this.builders.unmount.addLine(`@detachNode(${name});`);
@@ -203,7 +204,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 +216,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/AwaitBlock.ts b/src/generators/nodes/AwaitBlock.ts
index f47f5de61b..8dccd62e83 100644
--- a/src/generators/nodes/AwaitBlock.ts
+++ b/src/generators/nodes/AwaitBlock.ts
@@ -68,7 +68,7 @@ export default class AwaitBlock extends Node {
) {
const name = this.var;
- const anchor = this.getOrCreateAnchor(block, parentNode);
+ const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const params = block.params.join(', ');
@@ -143,9 +143,11 @@ export default class AwaitBlock extends Node {
${await_block}.c();
`);
- block.builders.claim.addBlock(deindent`
- ${await_block}.l(${parentNodes});
- `);
+ if (parentNodes) {
+ block.builders.claim.addBlock(deindent`
+ ${await_block}.l(${parentNodes});
+ `);
+ }
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts
index 8115ac808b..35100e16a1 100644
--- a/src/generators/nodes/Component.ts
+++ b/src/generators/nodes/Component.ts
@@ -248,7 +248,7 @@ export default class Component extends Node {
block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
- const anchor = this.getOrCreateAnchor(block, parentNode);
+ const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const params = block.params.join(', ');
@@ -281,9 +281,11 @@ export default class Component extends Node {
`if (${name}) ${name}._fragment.c();`
);
- block.builders.claim.addLine(
- `if (${name}) ${name}._fragment.l(${parentNodes});`
- );
+ if (parentNodes) {
+ block.builders.claim.addLine(
+ `if (${name}) ${name}._fragment.l(${parentNodes});`
+ );
+ }
block.builders.mount.addLine(
`if (${name}) ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
@@ -350,9 +352,11 @@ export default class Component extends Node {
block.builders.create.addLine(`${name}._fragment.c();`);
- block.builders.claim.addLine(
- `${name}._fragment.l(${parentNodes});`
- );
+ if (parentNodes) {
+ block.builders.claim.addLine(
+ `${name}._fragment.l(${parentNodes});`
+ );
+ }
block.builders.mount.addLine(
`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts
index 809eaf9397..688c964fac 100644
--- a/src/generators/nodes/EachBlock.ts
+++ b/src/generators/nodes/EachBlock.ts
@@ -154,7 +154,7 @@ export default class EachBlock extends Node {
block.addElement(
anchor,
`@createComment()`,
- `@createComment()`,
+ parentNodes && `@createComment()`,
parentNode
);
}
@@ -263,7 +263,7 @@ export default class EachBlock extends Node {
this.block.addElement(
this.block.first,
`@createComment()`,
- `@createComment()`,
+ parentNodes && `@createComment()`,
null
);
}
@@ -293,13 +293,15 @@ export default class EachBlock extends Node {
}
`);
- block.builders.claim.addBlock(deindent`
- var ${iteration} = ${head};
- while (${iteration}) {
- ${iteration}.l(${parentNodes});
- ${iteration} = ${iteration}.next;
- }
- `);
+ if (parentNodes) {
+ block.builders.claim.addBlock(deindent`
+ var ${iteration} = ${head};
+ while (${iteration}) {
+ ${iteration}.l(${parentNodes});
+ ${iteration} = ${iteration}.next;
+ }
+ `);
+ }
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
@@ -481,11 +483,13 @@ export default class EachBlock extends Node {
}
`);
- block.builders.claim.addBlock(deindent`
- for (var #i = 0; #i < ${iterations}.length; #i += 1) {
- ${iterations}[#i].l(${parentNodes});
- }
- `);
+ if (parentNodes) {
+ block.builders.claim.addBlock(deindent`
+ for (var #i = 0; #i < ${iterations}.length; #i += 1) {
+ ${iterations}[#i].l(${parentNodes});
+ }
+ `);
+ }
block.builders.mount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts
index 33c21d63f0..e4c47cb52f 100644
--- a/src/generators/nodes/Element.ts
+++ b/src/generators/nodes/Element.ts
@@ -163,7 +163,7 @@ export default class Element extends Node {
const childState = {
parentNode: this.var,
- parentNodes: block.getUniqueName(`${this.var}_nodes`)
+ parentNodes: parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e.
, parentNodes is null
};
const name = this.var;
@@ -175,25 +175,32 @@ export default class Element extends Node {
parentNode;
block.addVariable(name);
+ const renderStatement = getRenderStatement(this.generator, this.namespace, this.name);
block.builders.create.addLine(
- `${name} = ${getRenderStatement(
- this.generator,
- this.namespace,
- this.name
- )};`
+ `${name} = ${renderStatement};`
);
if (this.generator.hydratable) {
- block.builders.claim.addBlock(deindent`
- ${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
- var ${childState.parentNodes} = @children(${name});
- `);
+ if (parentNodes) {
+ block.builders.claim.addBlock(deindent`
+ ${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
+ var ${childState.parentNodes} = @children(${name});
+ `);
+ } else {
+ block.builders.claim.addLine(
+ `${name} = ${renderStatement};`
+ );
+ }
}
if (initialMountNode) {
block.builders.mount.addLine(
`@appendNode(${name}, ${initialMountNode});`
);
+
+ if (initialMountNode === 'document.head') {
+ block.builders.unmount.addLine(`@detachNode(${name});`);
+ }
} else {
block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
@@ -394,9 +401,11 @@ export default class Element extends Node {
block.builders.mount.addBlock(this.initialUpdate);
}
- block.builders.claim.addLine(
- `${childState.parentNodes}.forEach(@detachNode);`
- );
+ if (childState.parentNodes) {
+ block.builders.claim.addLine(
+ `${childState.parentNodes}.forEach(@detachNode);`
+ );
+ }
function toHTML(node: Element | Text) {
if (node.type === 'Text') return node.data;
diff --git a/src/generators/nodes/Head.ts b/src/generators/nodes/Head.ts
new file mode 100644
index 0000000000..be72087aca
--- /dev/null
+++ b/src/generators/nodes/Head.ts
@@ -0,0 +1,32 @@
+import deindent from '../../utils/deindent';
+import { stringify } from '../../utils/stringify';
+import Node from './shared/Node';
+import Block from '../dom/Block';
+import Attribute from './Attribute';
+
+export default class Head extends Node {
+ type: 'Head';
+ attributes: Attribute[];
+
+ init(
+ block: Block,
+ stripWhitespace: boolean,
+ nextSibling: Node
+ ) {
+ this.initChildren(block, true, null);
+ }
+
+ build(
+ block: Block,
+ parentNode: string,
+ parentNodes: string
+ ) {
+ const { generator } = this;
+
+ this.var = 'document.head';
+
+ this.children.forEach((child: Node) => {
+ child.build(block, 'document.head', null);
+ });
+ }
+}
diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts
index b3978e5356..3cec2245d9 100644
--- a/src/generators/nodes/IfBlock.ts
+++ b/src/generators/nodes/IfBlock.ts
@@ -133,15 +133,17 @@ export default class IfBlock extends Node {
block.builders.create.addLine(`${if_name}${name}.c();`);
- block.builders.claim.addLine(
- `${if_name}${name}.l(${parentNodes});`
- );
+ if (parentNodes) {
+ block.builders.claim.addLine(
+ `${if_name}${name}.l(${parentNodes});`
+ );
+ }
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
- `@createComment()`,
+ parentNodes && `@createComment()`,
parentNode
);
}
diff --git a/src/generators/nodes/MustacheTag.ts b/src/generators/nodes/MustacheTag.ts
index 35baccdc2b..21b3b30df2 100644
--- a/src/generators/nodes/MustacheTag.ts
+++ b/src/generators/nodes/MustacheTag.ts
@@ -22,7 +22,7 @@ export default class MustacheTag extends Tag {
block.addElement(
this.var,
`@createText(${init})`,
- `@claimText(${parentNodes}, ${init})`,
+ parentNodes && `@claimText(${parentNodes}, ${init})`,
parentNode
);
}
diff --git a/src/generators/nodes/RawMustacheTag.ts b/src/generators/nodes/RawMustacheTag.ts
index 8b81453033..6e9e5e664e 100644
--- a/src/generators/nodes/RawMustacheTag.ts
+++ b/src/generators/nodes/RawMustacheTag.ts
@@ -61,7 +61,7 @@ export default class RawMustacheTag extends Tag {
block.addElement(
anchorBefore,
`@createElement('noscript')`,
- `@createElement('noscript')`,
+ parentNodes && `@createElement('noscript')`,
parentNode
);
}
@@ -70,7 +70,7 @@ export default class RawMustacheTag extends Tag {
block.addElement(
anchorAfter,
`@createElement('noscript')`,
- `@createElement('noscript')`,
+ parentNodes && `@createElement('noscript')`,
parentNode
);
}
diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts
index 28d56f14c4..c00b00f4fe 100644
--- a/src/generators/nodes/Text.ts
+++ b/src/generators/nodes/Text.ts
@@ -53,7 +53,7 @@ export default class Text extends Node {
block.addElement(
this.var,
`@createText(${stringify(this.data)})`,
- `@claimText(${parentNodes}, ${stringify(this.data)})`,
+ parentNodes && `@claimText(${parentNodes}, ${stringify(this.data)})`,
parentNode
);
}
diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts
index 0c8b174698..274735043d 100644
--- a/src/generators/nodes/index.ts
+++ b/src/generators/nodes/index.ts
@@ -10,6 +10,7 @@ import Element from './Element';
import ElseBlock from './ElseBlock';
import EventHandler from './EventHandler';
import Fragment from './Fragment';
+import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import PendingBlock from './PendingBlock';
@@ -33,6 +34,7 @@ const nodes: Record = {
ElseBlock,
EventHandler,
Fragment,
+ Head,
IfBlock,
MustacheTag,
PendingBlock,
diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts
index 0658524bf5..3461ddd6f2 100644
--- a/src/generators/nodes/shared/Node.ts
+++ b/src/generators/nodes/shared/Node.ts
@@ -138,7 +138,7 @@ export default class Node {
if (this.parent) return this.parent.findNearest(selector);
}
- getOrCreateAnchor(block: Block, parentNode: string) {
+ getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
@@ -150,7 +150,7 @@ export default class Node {
block.addElement(
anchor,
`@createComment()`,
- `@createComment()`,
+ parentNodes && `@createComment()`,
parentNode
);
}
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
index 22242999ed..0ccee8cdd5 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 = { head: '', 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,
+ head: result.head,
+ 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,15 +150,26 @@ export default function ssr(
return \`${generator.renderCode}\`;
};
+ ${name}.css = {
+ code: ${css ? stringify(css) : `''`},
+ map: ${cssMap ? stringify(cssMap.toString()) : 'null'}
+ };
+
+ var warned = false;
${name}.renderCss = function() {
+ if (!warned) {
+ console.error('Component.renderCss(...) is deprecated and will be removed in v2 — use Component.render(...).css instead');
+ warned = true;
+ }
+
var components = [];
${generator.stylesheet.hasStyles &&
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/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts
index 731f73b5fa..a1645d0860 100644
--- a/src/generators/server-side-rendering/visitors/Element.ts
+++ b/src/generators/server-side-rendering/visitors/Element.ts
@@ -5,22 +5,8 @@ import visit from '../visit';
import { SsrGenerator } from '../index';
import Element from '../../nodes/Element';
import Block from '../Block';
-import { escape } from '../../../utils/stringify';
import { Node } from '../../../interfaces';
-
-function stringifyAttributeValue(block: Block, chunks: Node[]) {
- return chunks
- .map((chunk: Node) => {
- if (chunk.type === 'Text') {
- return escape(chunk.data).replace(/"/g, '"');
- }
-
- block.contextualise(chunk.expression);
- const { snippet } = chunk.metadata;
- return '${' + snippet + '}';
- })
- .join('');
-}
+import stringifyAttributeValue from './shared/stringifyAttributeValue';
export default function visitElement(
generator: SsrGenerator,
diff --git a/src/generators/server-side-rendering/visitors/Head.ts b/src/generators/server-side-rendering/visitors/Head.ts
new file mode 100644
index 0000000000..e7b4681f90
--- /dev/null
+++ b/src/generators/server-side-rendering/visitors/Head.ts
@@ -0,0 +1,19 @@
+import { SsrGenerator } from '../index';
+import Block from '../Block';
+import { Node } from '../../../interfaces';
+import stringifyAttributeValue from './shared/stringifyAttributeValue';
+import visit from '../visit';
+
+export default function visitDocument(
+ generator: SsrGenerator,
+ block: Block,
+ node: Node
+) {
+ generator.append('${(__result.head += `');
+
+ node.children.forEach((child: Node) => {
+ visit(generator, block, child);
+ });
+
+ generator.append('`, "")}');
+}
\ No newline at end of file
diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts
index 73d51043b5..c6b43b5d0f 100644
--- a/src/generators/server-side-rendering/visitors/index.ts
+++ b/src/generators/server-side-rendering/visitors/index.ts
@@ -3,6 +3,7 @@ import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element';
+import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
@@ -16,6 +17,7 @@ export default {
Component,
EachBlock,
Element,
+ Head,
IfBlock,
MustacheTag,
RawMustacheTag,
diff --git a/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts b/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts
new file mode 100644
index 0000000000..22eb0eff76
--- /dev/null
+++ b/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts
@@ -0,0 +1,17 @@
+import Block from '../../Block';
+import { escape } from '../../../../utils/stringify';
+import { Node } from '../../../../interfaces';
+
+export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
+ return chunks
+ .map((chunk: Node) => {
+ if (chunk.type === 'Text') {
+ return escape(chunk.data).replace(/"/g, '"');
+ }
+
+ block.contextualise(chunk.expression);
+ const { snippet } = chunk.metadata;
+ return '${' + snippet + '}';
+ })
+ .join('');
+}
\ No newline at end of file
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
index 90d9f5cd2c..1e39a51059 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',
+ ':Head'
+]);
const specials = new Map([
[
@@ -86,22 +87,25 @@ export default function tag(parser: Parser) {
const name = readTagName(parser);
- if (name in metaTags) {
- if (name in parser.metaTags) {
- if (isClosingTag && parser.current().children.length) {
+ if (metaTags.has(name)) {
+ if (isClosingTag) {
+ if (name === ':Window' && parser.current().children.length) {
parser.error(
- `<${name}> cannot have children`,
+ `<:Window> cannot have children`,
parser.current().children[0].start
);
}
+ } else {
+ if (name in parser.metaTags) {
+ parser.error(`A component can only have one <${name}> tag`, start);
+ }
- parser.error(`A component can only have one <${name}> tag`, start);
- }
-
- parser.metaTags[name] = true;
+ if (parser.stack.length > 1) {
+ console.log(parser.stack);
+ parser.error(`<${name}> tags cannot be inside elements or blocks`, start);
+ }
- if (parser.stack.length > 1) {
- parser.error(`<${name}> tags cannot be inside elements or blocks`, start);
+ parser.metaTags[name] = true;
}
}
@@ -252,7 +256,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/shared/index.js b/src/shared/index.js
index fc87e66eb2..e2efb5b4c3 100644
--- a/src/shared/index.js
+++ b/src/shared/index.js
@@ -185,7 +185,7 @@ export function _mount(target, anchor) {
}
export function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
export function isPromise(value) {
diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts
index 9a3b573505..bf94b63c52 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 validateHead from './validateHead';
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],
+ [':Head', validateHead]
+]);
export default function validateHtml(validator: Validator, html: Node) {
const refs = new Map();
diff --git a/src/validate/html/validateHead.ts b/src/validate/html/validateHead.ts
new file mode 100644
index 0000000000..7883e452f9
--- /dev/null
+++ b/src/validate/html/validateHead.ts
@@ -0,0 +1,8 @@
+import { Validator } from '../index';
+import { Node } from '../../interfaces';
+
+export default function validateHead(validator: Validator, node: Node, refs: Map, refCallees: Node[]) {
+ if (node.attributes.length) {
+ validator.error(`<:Head> should not have any attributes or directives`, node.start);
+ }
+}
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/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
index 6c2eadf148..c1aaf7b0f6 100644
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -175,7 +175,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/component-static/expected-bundle.js b/test/js/samples/component-static/expected-bundle.js
index 7045cd9ff8..5868dd94e5 100644
--- a/test/js/samples/component-static/expected-bundle.js
+++ b/test/js/samples/component-static/expected-bundle.js
@@ -151,7 +151,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js
index 0a2f23d100..cc822269ce 100644
--- a/test/js/samples/computed-collapsed-if/expected-bundle.js
+++ b/test/js/samples/computed-collapsed-if/expected-bundle.js
@@ -151,7 +151,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js
index 57181b1064..123e8b07fa 100644
--- a/test/js/samples/css-media-query/expected-bundle.js
+++ b/test/js/samples/css-media-query/expected-bundle.js
@@ -171,7 +171,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js
index ce3509fa69..d1a113a6ab 100644
--- a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js
+++ b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js
@@ -163,7 +163,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/do-use-dataset/expected-bundle.js b/test/js/samples/do-use-dataset/expected-bundle.js
index e82c4bf620..9a48d8db96 100644
--- a/test/js/samples/do-use-dataset/expected-bundle.js
+++ b/test/js/samples/do-use-dataset/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js
index 9a982ad043..0602ad1ed4 100644
--- a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js
+++ b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js
@@ -171,7 +171,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/dont-use-dataset-in-svg/expected-bundle.js b/test/js/samples/dont-use-dataset-in-svg/expected-bundle.js
index 253506e4c9..b595d14633 100644
--- a/test/js/samples/dont-use-dataset-in-svg/expected-bundle.js
+++ b/test/js/samples/dont-use-dataset-in-svg/expected-bundle.js
@@ -171,7 +171,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
index 79439f1af0..685e073f38 100644
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -183,7 +183,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js
index c061742725..6f51e1b8d2 100644
--- a/test/js/samples/event-handlers-custom/expected-bundle.js
+++ b/test/js/samples/event-handlers-custom/expected-bundle.js
@@ -163,7 +163,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js
index 673464486c..a4cf23cfed 100644
--- a/test/js/samples/if-block-no-update/expected-bundle.js
+++ b/test/js/samples/if-block-no-update/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js
index fc54ee85a7..639f037528 100644
--- a/test/js/samples/if-block-simple/expected-bundle.js
+++ b/test/js/samples/if-block-simple/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js
index fa6efd1cec..8fa1f7bfd4 100644
--- a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js
+++ b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/inline-style-optimized-url/expected-bundle.js b/test/js/samples/inline-style-optimized-url/expected-bundle.js
index 2efac93cd5..fb61583a46 100644
--- a/test/js/samples/inline-style-optimized-url/expected-bundle.js
+++ b/test/js/samples/inline-style-optimized-url/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/inline-style-optimized/expected-bundle.js b/test/js/samples/inline-style-optimized/expected-bundle.js
index d69bc32b94..1cf474d1b0 100644
--- a/test/js/samples/inline-style-optimized/expected-bundle.js
+++ b/test/js/samples/inline-style-optimized/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/inline-style-unoptimized/expected-bundle.js b/test/js/samples/inline-style-unoptimized/expected-bundle.js
index 1765fc1728..c4152305da 100644
--- a/test/js/samples/inline-style-unoptimized/expected-bundle.js
+++ b/test/js/samples/inline-style-unoptimized/expected-bundle.js
@@ -167,7 +167,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js
index a4b7837682..bd62e84ac5 100644
--- a/test/js/samples/input-without-blowback-guard/expected-bundle.js
+++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js
@@ -171,7 +171,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js
index b7cfc1320a..8eeda86d6f 100644
--- a/test/js/samples/legacy-input-type/expected-bundle.js
+++ b/test/js/samples/legacy-input-type/expected-bundle.js
@@ -169,7 +169,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/legacy-quote-class/expected-bundle.js b/test/js/samples/legacy-quote-class/expected-bundle.js
index d71ad3e74b..9e454ba444 100644
--- a/test/js/samples/legacy-quote-class/expected-bundle.js
+++ b/test/js/samples/legacy-quote-class/expected-bundle.js
@@ -186,7 +186,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js
index 9ab9d72d9d..80729e9179 100644
--- a/test/js/samples/media-bindings/expected-bundle.js
+++ b/test/js/samples/media-bindings/expected-bundle.js
@@ -179,7 +179,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js
index a7dd66c128..3c6ff9f91f 100644
--- a/test/js/samples/non-imported-component/expected-bundle.js
+++ b/test/js/samples/non-imported-component/expected-bundle.js
@@ -165,7 +165,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
index 5e3648119d..4357b4a202 100644
--- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
@@ -151,7 +151,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js
index 1e35598d5a..9ff9831ae4 100644
--- a/test/js/samples/setup-method/expected-bundle.js
+++ b/test/js/samples/setup-method/expected-bundle.js
@@ -151,7 +151,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
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 514e40d54a..42f630e0e9 100644
--- a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js
+++ b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js
@@ -9,12 +9,47 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options = {}) {
+ var components = new Set();
+
+ function addComponent(component) {
+ components.add(component);
+ }
+
+ var result = { head: '', 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,
+ head: result.head,
+ 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
+};
+
+var warned = false;
SvelteComponent.renderCss = function() {
+ if (!warned) {
+ console.error('Component.renderCss(...) is deprecated and will be removed in v2 — use Component.render(...).css instead');
+ warned = true;
+ }
+
var components = [];
return {
diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js
index 735d359a1f..7168662072 100644
--- a/test/js/samples/ssr-no-oncreate-etc/expected.js
+++ b/test/js/samples/ssr-no-oncreate-etc/expected.js
@@ -11,12 +11,47 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options = {}) {
+ var components = new Set();
+
+ function addComponent(component) {
+ components.add(component);
+ }
+
+ var result = { head: '', 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,
+ head: result.head,
+ 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
+};
+
+var warned = false;
SvelteComponent.renderCss = function() {
+ if (!warned) {
+ console.error('Component.renderCss(...) is deprecated and will be removed in v2 — use Component.render(...).css instead');
+ warned = true;
+ }
+
var components = [];
return {
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
index 47d5a50224..c97c52ab12 100644
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -175,7 +175,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/js/samples/window-binding-scroll/expected-bundle.js b/test/js/samples/window-binding-scroll/expected-bundle.js
index aedda719ed..d44d545cbf 100644
--- a/test/js/samples/window-binding-scroll/expected-bundle.js
+++ b/test/js/samples/window-binding-scroll/expected-bundle.js
@@ -171,7 +171,7 @@ function _mount(target, anchor) {
}
function _unmount() {
- this._fragment.u();
+ if (this._fragment) this._fragment.u();
}
var proto = {
diff --git a/test/runtime/index.js b/test/runtime/index.js
index 1ead0a11cb..fa3482337b 100644
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -52,7 +52,7 @@ describe("runtime", () => {
throw new Error("Forgot to remove `solo: true` from test");
}
- (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, () => {
+ (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers${hydrate ? ' , hydration' : ''})`, () => {
if (failed.has(dir)) {
// this makes debugging easier, by only printing compiled output once
throw new Error('skipping test, already failed');
@@ -183,7 +183,9 @@ describe("runtime", () => {
}
if (config.test) {
- return config.test(assert, component, target, window, raf);
+ return Promise.resolve(config.test(assert, component, target, window, raf)).then(() => {
+ component.destroy();
+ });
} else {
component.destroy();
assert.equal(target.innerHTML, "");
diff --git a/test/runtime/samples/head-title-dynamic/_config.js b/test/runtime/samples/head-title-dynamic/_config.js
new file mode 100644
index 0000000000..5155554a0e
--- /dev/null
+++ b/test/runtime/samples/head-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/head-title-dynamic/main.html b/test/runtime/samples/head-title-dynamic/main.html
new file mode 100644
index 0000000000..ae2b8c1caa
--- /dev/null
+++ b/test/runtime/samples/head-title-dynamic/main.html
@@ -0,0 +1,4 @@
+<:Head>
+ a {{adjective}} title
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/head-title-static/_config.js b/test/runtime/samples/head-title-static/_config.js
new file mode 100644
index 0000000000..3757effa54
--- /dev/null
+++ b/test/runtime/samples/head-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/head-title-static/main.html b/test/runtime/samples/head-title-static/main.html
new file mode 100644
index 0000000000..9ab533140f
--- /dev/null
+++ b/test/runtime/samples/head-title-static/main.html
@@ -0,0 +1,4 @@
+<:Head>
+ changed
+
+
\ No newline at end of file
diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js
index b6bd109c0e..385f781b6e 100644
--- a/test/server-side-rendering/index.js
+++ b/test/server-side-rendering/index.js
@@ -59,18 +59,25 @@ describe("ssr", () => {
const data = tryToLoadJson(`${dir}/data.json`);
- const html = component.render(data);
- const css = component.renderCss().css;
+ const { html, css, head } = 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, "")
);
+ if (fs.existsSync(`${dir}/_expected-head.html`)) {
+ fs.writeFileSync(`${dir}/_actual-head.html`, head);
+ assert.htmlEqual(
+ head,
+ fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8')
+ );
+ }
+
if (show) showOutput(dir, { generate: 'ssr' });
} catch (err) {
showOutput(dir, { generate: 'ssr' });
@@ -105,7 +112,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/head-title/_actual-head.html b/test/server-side-rendering/samples/head-title/_actual-head.html
new file mode 100644
index 0000000000..b0a06af0cc
--- /dev/null
+++ b/test/server-side-rendering/samples/head-title/_actual-head.html
@@ -0,0 +1,2 @@
+
+ a custom title
diff --git a/test/server-side-rendering/samples/head-title/_expected-head.html b/test/server-side-rendering/samples/head-title/_expected-head.html
new file mode 100644
index 0000000000..7d696352f9
--- /dev/null
+++ b/test/server-side-rendering/samples/head-title/_expected-head.html
@@ -0,0 +1 @@
+a custom title
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/head-title/_expected.html b/test/server-side-rendering/samples/head-title/_expected.html
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/server-side-rendering/samples/head-title/data.json b/test/server-side-rendering/samples/head-title/data.json
new file mode 100644
index 0000000000..eab238e816
--- /dev/null
+++ b/test/server-side-rendering/samples/head-title/data.json
@@ -0,0 +1,3 @@
+{
+ "adjective": "custom"
+}
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/head-title/main.html b/test/server-side-rendering/samples/head-title/main.html
new file mode 100644
index 0000000000..18c0bbbabd
--- /dev/null
+++ b/test/server-side-rendering/samples/head-title/main.html
@@ -0,0 +1,3 @@
+<:Head>
+ a {{adjective}} title
+
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}}
-
+
+
+