From 2ec0a850fc943db56fd44cbdbd4483f8d30d3842 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 1 Jul 2017 23:27:02 -0400 Subject: [PATCH] apply css optimisations to SSR --- src/generators/server-side-rendering/index.ts | 3 + .../server-side-rendering/preprocess.ts | 90 +++++++++++++++++++ .../server-side-rendering/visitors/Element.ts | 2 +- test/css/index.js | 41 ++++++--- .../expected.css | 2 +- .../expected.html | 4 +- .../input.html | 6 +- test/helpers.js | 41 ++++++--- 8 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 src/generators/server-side-rendering/preprocess.ts diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 29cfccd67a..92c3599aeb 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -1,6 +1,7 @@ import deindent from '../../utils/deindent'; import Generator from '../Generator'; import Block from './Block'; +import preprocess from './preprocess'; import visit from './visit'; import { removeNode, removeObjectKey } from '../../utils/removeNode'; import { Parsed, Node, CompileOptions } from '../../interfaces'; @@ -24,6 +25,8 @@ export class SsrGenerator extends Generator { // in an SSR context, we don't need to include events, methods, oncreate or ondestroy const { templateProperties, defaultExport } = this; + preprocess(this, parsed.html); + if (templateProperties.oncreate) removeNode( this.code, diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts new file mode 100644 index 0000000000..c82fda9e9b --- /dev/null +++ b/src/generators/server-side-rendering/preprocess.ts @@ -0,0 +1,90 @@ +import { SsrGenerator } from './index'; +import { Node } from '../../interfaces'; + +function noop () {} + +function isElseIf(node: Node) { + return ( + node && node.children.length === 1 && node.children[0].type === 'IfBlock' + ); +} + +const preprocessors = { + MustacheTag: noop, + RawMustacheTag: noop, + Text: noop, + + IfBlock: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + function attachBlocks(node: Node) { + preprocessChildren(generator, node, elementStack); + + if (isElseIf(node.else)) { + attachBlocks(node.else.children[0]); + } else if (node.else) { + preprocessChildren( + generator, + node.else, + elementStack + ); + } + } + + attachBlocks(node); + }, + + EachBlock: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + preprocessChildren(generator, node, elementStack); + + if (node.else) { + preprocessChildren( + generator, + node.else, + elementStack + ); + } + }, + + Element: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + const isComponent = + generator.components.has(node.name) || node.name === ':Self'; + + if (!isComponent) { + generator.applyCss(node, elementStack); + } + + if (node.children.length) { + if (isComponent) { + preprocessChildren(generator, node, elementStack); + } else { + preprocessChildren(generator, node, elementStack.concat(node)); + } + } + }, +}; + +function preprocessChildren( + generator: SsrGenerator, + node: Node, + elementStack: Node[] +) { + node.children.forEach((child: Node, i: number) => { + const preprocessor = preprocessors[child.type]; + if (preprocessor) preprocessor(generator, child, elementStack); + }); +} + +export default function preprocess(generator: SsrGenerator, html: Node) { + preprocessChildren(generator, html, []); +} \ No newline at end of file diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 3540bd12f7..765e16ec8a 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -56,7 +56,7 @@ export default function visitElement( } }); - if (generator.cssId && (!generator.cascade || generator.elementDepth === 0)) { + if (node._needsCssAttribute) { openingTag += ` ${generator.cssId}`; } diff --git a/test/css/index.js b/test/css/index.js index 3c4e427746..e4f31f00a3 100644 --- a/test/css/index.js +++ b/test/css/index.js @@ -1,6 +1,6 @@ import assert from "assert"; import * as fs from "fs"; -import { env, svelte } from "../helpers.js"; +import { env, normalizeHtml, svelte } from "../helpers.js"; function tryRequire(file) { try { @@ -23,27 +23,36 @@ describe("css", () => { } (solo ? it.only : it)(dir, () => { - const config = Object.assign(tryRequire(`./samples/${dir}/_config.js`) || {}, { - format: 'iife', - name: 'SvelteComponent' - }); + const config = tryRequire(`./samples/${dir}/_config.js`) || {}; const input = fs .readFileSync(`test/css/samples/${dir}/input.html`, "utf-8") .replace(/\s+$/, ""); - const actual = svelte.compile(input, config); - fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, actual.css); + const dom = svelte.compile(input, Object.assign(config, { + format: 'iife', + name: 'SvelteComponent' + })); + + const ssr = svelte.compile(input, Object.assign(config, { + format: 'iife', + generate: 'ssr', + name: 'SvelteComponent' + })); + + assert.equal(dom.css, ssr.css); + + fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css); const expected = { html: read(`test/css/samples/${dir}/expected.html`), css: read(`test/css/samples/${dir}/expected.css`) }; - assert.equal(actual.css.trim(), expected.css.trim()); + assert.equal(dom.css.trim(), expected.css.trim()); // verify that the right elements have scoping selectors if (expected.html !== null) { return env().then(window => { - const Component = eval(`(function () { ${actual.code}; return SvelteComponent; }())`); + const Component = eval(`(function () { ${dom.code}; return SvelteComponent; }())`); const target = window.document.querySelector("main"); new Component({ target, data: config.data }); @@ -51,7 +60,19 @@ describe("css", () => { fs.writeFileSync(`test/css/samples/${dir}/_actual.html`, html); - assert.equal(html.trim(), expected.html.trim()); + // dom + assert.equal( + normalizeHtml(window, html), + normalizeHtml(window, expected.html) + ); + + // ssr + const component = eval(`(function () { ${ssr.code}; return SvelteComponent; }())`); + + assert.equal( + normalizeHtml(window, component.render(config.data)), + normalizeHtml(window, expected.html) + ); }); } }); diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css index c57bf43bd4..662e453d83 100644 --- a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css +++ b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css @@ -1,4 +1,4 @@ - [data-foo][svelte-2966013849] { + [autoplay][svelte-240005720] { color: red; } diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html index 77ed0898cf..254afb2a46 100644 --- a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html +++ b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html @@ -1,2 +1,2 @@ -

this is styled

-

this is unstyled

\ No newline at end of file +
+
\ No newline at end of file diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/input.html b/test/css/samples/omit-scoping-attribute-attribute-selector/input.html index eba59af199..6f4549ead8 100644 --- a/test/css/samples/omit-scoping-attribute-attribute-selector/input.html +++ b/test/css/samples/omit-scoping-attribute-attribute-selector/input.html @@ -1,10 +1,10 @@
-

this is styled

-

this is unstyled

+ +
\ No newline at end of file diff --git a/test/helpers.js b/test/helpers.js index 8d9cd3a660..8087b173e4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -69,6 +69,20 @@ export function env() { function cleanChildren(node) { let previous = null; + // sort attributes + const attributes = Array.from(node.attributes).sort((a, b) => { + return a.name < b.name ? -1 : 1; + }); + + attributes.forEach(attr => { + node.removeAttribute(attr.name); + }); + + attributes.forEach(attr => { + node.setAttribute(attr.name, attr.value); + }); + + // recurse [...node.childNodes].forEach(child => { if (child.nodeType === 8) { // comment @@ -114,22 +128,23 @@ function cleanChildren(node) { } } +export function normalizeHtml(window, html) { + const node = window.document.createElement('div'); + node.innerHTML = html + .replace(/>[\s\r\n]+<') + .trim(); + cleanChildren(node, ''); + return node.innerHTML; +} + export function setupHtmlEqual() { return env().then(window => { assert.htmlEqual = (actual, expected, message) => { - window.document.body.innerHTML = actual - .replace(/>[\s\r\n]+<') - .trim(); - cleanChildren(window.document.body, ''); - actual = window.document.body.innerHTML; - - window.document.body.innerHTML = expected - .replace(/>[\s\r\n]+<') - .trim(); - cleanChildren(window.document.body, ''); - expected = window.document.body.innerHTML; - - assert.deepEqual(actual, expected, message); + assert.deepEqual( + normalizeHtml(window, actual), + normalizeHtml(window, expected), + message + ); }; }); }