Merge pull request #154 from sveltejs/gh-153

Server-rendered CSS
pull/156/head
Rich Harris 10 years ago committed by GitHub
commit f978bbe977

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -1,18 +1,6 @@
import deindent from '../../utils/deindent.js';
import spaces from '../../utils/spaces.js';
import transform from './transform.js';
export default function process ( parsed ) {
const scoped = transform( spaces( parsed.css.content.start ) + parsed.css.content.styles, parsed.hash );
return deindent`
let addedCss = false;
function addCss () {
var style = document.createElement( 'style' );
style.textContent = ${JSON.stringify( scoped )};
document.head.appendChild( style );
addedCss = true;
}
`;
return transform( spaces( parsed.css.content.start ) + parsed.css.content.styles, parsed.hash );
}

@ -392,8 +392,17 @@ export default function generate ( parsed, source, options, names ) {
topLevelStatements.push( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}
if ( parsed.css ) {
topLevelStatements.push( processCss( parsed ) );
if ( parsed.css && options.css !== false ) {
topLevelStatements.push( deindent`
let addedCss = false;
function addCss () {
var style = document.createElement( 'style' );
style.textContent = ${JSON.stringify( processCss( parsed ) )};
document.head.appendChild( style );
addedCss = true;
}
` );
}
topLevelStatements.push( ...renderers.reverse() );
@ -402,7 +411,7 @@ export default function generate ( parsed, source, options, names ) {
const initStatements = [];
if ( parsed.css ) {
if ( parsed.css && options.css !== false ) {
initStatements.push( `if ( !addedCss ) addCss();` );
}

@ -25,6 +25,8 @@ export default function parse ( template ) {
throw new TypeError( 'Template must be a string' );
}
template = template.replace( /\s+$/, '' );
const parser = {
index: 0,
template,

@ -4,10 +4,11 @@ import deindent from '../utils/deindent.js';
import isReference from '../utils/isReference.js';
import flattenReference from '../utils/flattenReference.js';
import MagicString, { Bundle } from 'magic-string';
import processCss from '../generate/css/process.js';
const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
export default function compile ( source, filename ) {
export default function compile ( source, { filename }) {
const parsed = parse( source, {} );
validate( parsed, source, {} );
@ -113,6 +114,8 @@ export default function compile ( source, filename ) {
};
}
let elementDepth = 0;
const stringifiers = {
Component ( node ) {
const props = node.attributes.map( attribute => {
@ -187,12 +190,18 @@ export default function compile ( source, filename ) {
element += str;
});
if ( parsed.css && elementDepth === 0 ) {
element += ` svelte-${parsed.hash}`;
}
if ( voidElementNames.test( node.name ) ) {
element += '>';
} else if ( node.children.length === 0 ) {
element += '/>';
} else {
elementDepth += 1;
element += '>' + node.children.map( stringify ).join( '' ) + `</${node.name}>`;
elementDepth -= 1;
}
return element;
@ -278,10 +287,6 @@ export default function compile ( source, filename ) {
topLevelStatements.push( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}
if ( parsed.css ) {
throw new Error( 'TODO handle css' );
}
const renderStatements = [
templateProperties.data ? `data = Object.assign( template.data(), data || {} );` : `data = data || {};`
];
@ -325,10 +330,55 @@ export default function compile ( source, filename ) {
`return rendered;`
);
const renderCssStatements = [
`var components = [];`
];
if ( parsed.css ) {
renderCssStatements.push( deindent`
components.push({
filename: exports.filename,
css: ${JSON.stringify( processCss( parsed ) )},
map: null // TODO
});
` );
}
if ( templateProperties.components ) {
renderCssStatements.push( deindent`
var seen = {};
function addComponent ( component ) {
var result = component.renderCss();
result.components.forEach( x => {
if ( seen[ x.filename ] ) return;
seen[ x.filename ] = true;
components.push( x );
});
}
` );
renderCssStatements.push( templateProperties.components.properties.map( prop => `addComponent( template.components.${prop.key.name} );` ).join( '\n' ) );
}
renderCssStatements.push( deindent`
return {
css: components.map( x => x.css ).join( '\\n' ),
map: null,
components
};
` );
topLevelStatements.push( deindent`
exports.filename = ${JSON.stringify( filename )};
exports.render = function ( data, options ) {
${renderStatements.join( '\n\n' )}
};
exports.renderCss = function () {
${renderCssStatements.join( '\n\n' )}
};
` );
const rendered = topLevelStatements.join( '\n\n' );

@ -2,6 +2,6 @@ import * as fs from 'fs';
import compile from './compile.js';
require.extensions[ '.html' ] = function ( module, filename ) {
const { code } = compile( fs.readFileSync( filename, 'utf-8' ) );
const { code } = compile( fs.readFileSync( filename, 'utf-8' ), { filename });
return module._compile( code, filename );
};

@ -1,501 +0,0 @@
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var fs = require('fs');
var ___compiler_svelte_js = require('../compiler/svelte.js');
var MagicString = require('magic-string');
var MagicString__default = _interopDefault(MagicString);
function walk ( ast, ref) {
var enter = ref.enter;
var leave = ref.leave;
visit( ast, null, enter, leave );
}
var context = {
skip: function () { return context.shouldSkip = true; },
shouldSkip: false
};
var childKeys = {};
var toString = Object.prototype.toString;
function isArray ( thing ) {
return toString.call( thing ) === '[object Array]';
}
function visit ( node, parent, enter, leave, prop, index ) {
if ( !node ) return;
if ( enter ) {
context.shouldSkip = false;
enter.call( context, node, parent, prop, index );
if ( context.shouldSkip ) return;
}
var keys = childKeys[ node.type ] || (
childKeys[ node.type ] = Object.keys( node ).filter( function (key) { return typeof node[ key ] === 'object'; } )
);
for ( var i = 0; i < keys.length; i += 1 ) {
var key = keys[i];
var value = node[ key ];
if ( isArray( value ) ) {
for ( var j = 0; j < value.length; j += 1 ) {
visit( value[j], node, enter, leave, key, j );
}
}
else if ( value && value.type ) {
visit( value, node, enter, leave, key, null );
}
}
if ( leave ) {
leave( node, parent, prop, index );
}
}
const start = /\n(\t+)/;
function deindent ( strings, ...values ) {
const indentation = start.exec( strings[0] )[1];
const pattern = new RegExp( `^${indentation}`, 'gm' );
let result = strings[0].replace( start, '' ).replace( pattern, '' );
let trailingIndentation = getTrailingIndentation( result );
for ( let i = 1; i < strings.length; i += 1 ) {
const value = String( values[ i - 1 ] ).replace( /\n/g, `\n${trailingIndentation}` );
result += value + strings[i].replace( pattern, '' );
trailingIndentation = getTrailingIndentation( result );
}
return result.trim();
}
function getTrailingIndentation ( str ) {
let i = str.length;
while ( str[ i - 1 ] === ' ' || str[ i - 1 ] === '\t' ) i -= 1;
return str.slice( i, str.length );
}
function isReference ( node, parent ) {
if ( node.type === 'MemberExpression' ) {
return !node.computed && isReference( node.object, node );
}
if ( node.type === 'Identifier' ) {
// the only time we could have an identifier node without a parent is
// if it's the entire body of a function without a block statement
// i.e. an arrow function expression like `a => a`
if ( !parent ) return true;
// TODO is this right?
if ( parent.type === 'MemberExpression' || parent.type === 'MethodDefinition' ) {
return parent.computed || node === parent.object;
}
// disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }`
if ( parent.type === 'Property' ) return parent.computed || node === parent.value;
// disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return false;
// disregard the `bar` in `export { foo as bar }`
if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
return true;
}
}
function flatten ( node ) {
const parts = [];
while ( node.type === 'MemberExpression' ) {
if ( node.computed ) return null;
parts.unshift( node.property.name );
node = node.object;
}
if ( node.type !== 'Identifier' ) return null;
const name = node.name;
parts.unshift( name );
return { name, keypath: parts.join( '.' ) };
}
const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
function compile ( source, filename ) {
const parsed = ___compiler_svelte_js.parse( source, {} );
___compiler_svelte_js.validate( parsed, source, {} );
const code = new MagicString__default( source );
const templateProperties = {};
const components = {};
const helpers = {};
const imports = [];
if ( parsed.js ) {
walk( parsed.js.content, {
enter ( node ) {
code.addSourcemapLocation( node.start );
code.addSourcemapLocation( node.end );
}
});
// imports need to be hoisted out of the IIFE
for ( let i = 0; i < parsed.js.content.body.length; i += 1 ) {
const node = parsed.js.content.body[i];
if ( node.type === 'ImportDeclaration' ) {
let a = node.start;
let b = node.end;
while ( /[ \t]/.test( source[ a - 1 ] ) ) a -= 1;
while ( source[b] === '\n' ) b += 1;
//imports.push( source.slice( a, b ).replace( /^\s/, '' ) );
imports.push( node );
code.remove( a, b );
}
}
const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' );
if ( defaultExport ) {
const finalNode = parsed.js.content.body[ parsed.js.content.body.length - 1 ];
if ( defaultExport === finalNode ) {
// export is last property, we can just return it
code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` );
} else {
// TODO ensure `template` isn't already declared
code.overwrite( defaultExport.start, defaultExport.declaration.start, `var template = ` );
let i = defaultExport.start;
while ( /\s/.test( source[ i - 1 ] ) ) i--;
const indentation = source.slice( i, defaultExport.start );
code.appendLeft( finalNode.end, `\n\n${indentation}return template;` );
}
defaultExport.declaration.properties.forEach( prop => {
templateProperties[ prop.key.name ] = prop.value;
});
code.prependRight( parsed.js.content.start, 'var template = (function () {' );
} else {
code.prependRight( parsed.js.content.start, '(function () {' );
}
code.appendLeft( parsed.js.content.end, '}());' );
if ( templateProperties.helpers ) {
templateProperties.helpers.properties.forEach( prop => {
helpers[ prop.key.name ] = prop.value;
});
}
if ( templateProperties.components ) {
templateProperties.components.properties.forEach( prop => {
components[ prop.key.name ] = prop.value;
});
}
}
let scope = new Set();
const scopes = [ scope ];
function contextualise ( expression ) {
walk( expression, {
enter ( node, parent ) {
if ( isReference( node, parent ) ) {
const { name } = flatten( node );
if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) {
code.prependRight( node.start, `template.helpers.` );
return;
}
if ( !scope.has( name ) ) {
code.prependRight( node.start, `data.` );
}
this.skip();
}
}
});
return {
snippet: `[✂${expression.start}-${expression.end}✂]`,
string: code.slice( expression.start, expression.end )
};
}
const stringifiers = {
Component ( node ) {
const props = node.attributes.map( attribute => {
let value;
if ( attribute.value === true ) {
value = `true`;
} else if ( attribute.value.length === 0 ) {
value = `''`;
} else if ( attribute.value.length === 1 ) {
const chunk = attribute.value[0];
if ( chunk.type === 'Text' ) {
value = isNaN( parseFloat( chunk.data ) ) ? JSON.stringify( chunk.data ) : chunk.data;
} else {
const { snippet } = contextualise( chunk.expression );
value = snippet;
}
} else {
value = '`' + attribute.value.map( stringify ).join( '' ) + '`';
}
return `${attribute.name}: ${value}`;
}).join( ', ' );
let params = `{${props}}`;
if ( node.children.length ) {
params += `, { yield: () => \`${node.children.map( stringify ).join( '' )}\` }`;
}
return `\${template.components.${node.name}.render(${params})}`;
},
EachBlock ( node ) {
const { snippet } = contextualise( node.expression );
scope = new Set();
scope.add( node.context );
if ( node.index ) scope.add( node.index );
scopes.push( scope );
const block = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \`${ node.children.map( stringify ).join( '' )}\` ).join( '' )}`;
scopes.pop();
scope = scopes[ scopes.length - 1 ];
return block;
},
Element ( node ) {
if ( node.name in components ) {
return stringifiers.Component( node );
}
let element = `<${node.name}`;
node.attributes.forEach( attribute => {
let str = ` ${attribute.name}`;
if ( attribute.value !== true ) {
str += `="` + attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return chunk.data;
}
const { snippet } = contextualise( chunk.expression );
return '${' + snippet + '}';
}).join( '' ) + `"`;
}
element += str;
});
if ( voidElementNames.test( node.name ) ) {
element += '>';
} else if ( node.children.length === 0 ) {
element += '/>';
} else {
element += '>' + node.children.map( stringify ).join( '' ) + `</${node.name}>`;
}
return element;
},
IfBlock ( node ) {
const { snippet } = contextualise( node.expression ); // TODO use snippet, for sourcemap support
const consequent = node.children.map( stringify ).join( '' );
const alternate = node.else ? node.else.children.map( stringify ).join( '' ) : '';
return '${ ' + snippet + ' ? `' + consequent + '` : `' + alternate + '` }';
},
MustacheTag ( node ) {
const { snippet } = contextualise( node.expression ); // TODO use snippet, for sourcemap support
return '${' + snippet + '}';
},
Text ( node ) {
return node.data.replace( /\${/g, '\\${' );
},
YieldTag () {
return `\${options.yield()}`;
}
};
function stringify ( node ) {
const stringifier = stringifiers[ node.type ];
if ( !stringifier ) {
throw new Error( `Not implemented: ${node.type}` );
}
return stringifier( node );
}
function createBlock ( node ) {
const str = stringify( node );
if ( str.slice( 0, 2 ) === '${' ) return str.slice( 2, -1 );
return '`' + str + '`';
}
const blocks = parsed.html.children.map( node => {
return deindent`
rendered += ${createBlock( node )};
`;
});
const topLevelStatements = [];
const importBlock = imports
.map( ( declaration, i ) => {
const defaultImport = declaration.specifiers.find( x => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' );
const namespaceImport = declaration.specifiers.find( x => x.type === 'ImportNamespaceSpecifier' );
const namedImports = declaration.specifiers.filter( x => x.type === 'ImportSpecifier' && x.imported.name !== 'default' );
const name = ( defaultImport || namespaceImport ) ? ( defaultImport || namespaceImport ).local.name : `__import${i}`;
const statements = [
`var ${name} = require( '${declaration.source.value}' );`
];
namedImports.forEach( specifier => {
statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name};` );
});
if ( defaultImport ) {
statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` );
}
return statements.join( '\n' );
})
.filter( Boolean )
.join( '\n' );
if ( parsed.js ) {
if ( imports.length ) {
topLevelStatements.push( importBlock );
}
topLevelStatements.push( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}
if ( parsed.css ) {
throw new Error( 'TODO handle css' );
}
const renderStatements = [
templateProperties.data ? `data = Object.assign( template.data(), data || {} );` : `data = data || {};`
];
if ( templateProperties.computed ) {
const statements = [];
const dependencies = new Map();
templateProperties.computed.properties.forEach( prop => {
const key = prop.key.name;
const value = prop.value;
const deps = value.params.map( param => param.name );
dependencies.set( key, deps );
});
const visited = new Set();
function visit ( key ) {
if ( !dependencies.has( key ) ) return; // not a computation
if ( visited.has( key ) ) return;
visited.add( key );
const deps = dependencies.get( key );
deps.forEach( visit );
statements.push( deindent`
data.${key} = template.computed.${key}( ${deps.map( dep => `data.${dep}` ).join( ', ' )} );
` );
}
templateProperties.computed.properties.forEach( prop => visit( prop.key.name ) );
renderStatements.push( statements.join( '\n' ) );
}
renderStatements.push(
`var rendered = '';`,
blocks.join( '\n\n' ),
`return rendered;`
);
topLevelStatements.push( deindent`
exports.render = function ( data, options ) {
${renderStatements.join( '\n\n' )}
};
` );
const rendered = topLevelStatements.join( '\n\n' );
const pattern = /\[✂(\d+)-(\d+)$/;
const parts = rendered.split( '✂]' );
const finalChunk = parts.pop();
const compiled = new MagicString.Bundle({ separator: '' });
function addString ( str ) {
compiled.addSource({
content: new MagicString__default( str )
});
}
parts.forEach( str => {
const chunk = str.replace( pattern, '' );
if ( chunk ) addString( chunk );
const match = pattern.exec( str );
const snippet = code.snip( +match[1], +match[2] );
compiled.addSource({
filename,
content: snippet
});
});
addString( finalChunk );
return {
code: compiled.toString()
};
}
require.extensions[ '.html' ] = function ( module, filename ) {
const { code } = compile( fs.readFileSync( filename, 'utf-8' ) );
return module._compile( code, filename );
};
//# sourceMappingURL=register.js.map

File diff suppressed because one or more lines are too long

@ -0,0 +1,7 @@
<p>test</p>
<style>
p {
color: red;
}
</style>

@ -0,0 +1,12 @@
export default {
compileOptions: {
css: false
},
test ( assert, component, target, window ) {
const [ control, test ] = target.querySelectorAll( 'p' );
assert.equal( window.getComputedStyle( control ).color, '' );
assert.equal( window.getComputedStyle( test ).color, '' );
}
};

@ -0,0 +1,10 @@
<p>control</p>
<Widget/>
<script>
import Widget from './Widget.html';
export default {
components: { Widget }
};
</script>

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

@ -0,0 +1,16 @@
<div>green: {{message}}</div>
<Two message='{{message}}'/>
<style>
div {
color: green;
}
</style>
<script>
import Two from './Two.html';
export default {
components: { Two }
};
</script>

@ -0,0 +1,7 @@
<div>blue: {{message}}</div>
<style>
div {
color: blue;
}
</style>

@ -0,0 +1,14 @@
div[svelte-4188175681], [svelte-4188175681] div {
color: red;
}
div[svelte-146600313], [svelte-146600313] div {
color: green;
}
div[svelte-1506185237], [svelte-1506185237] div {
color: blue;
}

@ -0,0 +1,5 @@
<div svelte-4188175681>red</div>
<div svelte-146600313>green: foo</div>
<div svelte-1506185237>blue: foo</div>
<div svelte-146600313>green: bar</div>
<div svelte-1506185237>blue: bar</div>

@ -0,0 +1,11 @@
div[svelte-4188175681], [svelte-4188175681] div {
color: red;
}
div[svelte-146600313], [svelte-146600313] div {
color: green;
}
div[svelte-1506185237], [svelte-1506185237] div {
color: blue;
}

@ -0,0 +1,5 @@
<div svelte-4188175681>red</div>
<div svelte-146600313>green: foo</div>
<div svelte-1506185237>blue: foo</div>
<div svelte-146600313>green: bar</div>
<div svelte-1506185237>blue: bar</div>

@ -0,0 +1,17 @@
<div>red</div>
<One message='foo'/>
<One message='bar'/>
<style>
div {
color: red;
}
</style>
<script>
import One from './One.html';
export default {
components: { One }
};
</script>

@ -0,0 +1,4 @@
div[svelte-281576708], [svelte-281576708] div {
color: red;
}

@ -0,0 +1 @@
<div svelte-281576708>red</div>

@ -0,0 +1,3 @@
div[svelte-281576708], [svelte-281576708] div {
color: red;
}

@ -0,0 +1 @@
<div svelte-281576708>red</div>

@ -0,0 +1,7 @@
<div>red</div>
<style>
div {
color: red;
}
</style>

@ -23,9 +23,11 @@ const svelte = process.env.COVERAGE ?
const cache = {};
let showCompiledCode = false;
let compileOptions = null;
require.extensions[ '.html' ] = function ( module, filename ) {
const code = cache[ filename ] || ( cache[ filename ] = svelte.compile( fs.readFileSync( filename, 'utf-8' ) ).code );
const options = Object.assign({ filename }, compileOptions );
const code = cache[ filename ] || ( cache[ filename ] = svelte.compile( fs.readFileSync( filename, 'utf-8' ), options ).code );
if ( showCompiledCode ) console.log( addLineNumbers( code ) ); // eslint-disable-line no-console
return module._compile( code, filename );
@ -71,6 +73,15 @@ function tryToLoadJson ( file ) {
}
}
function tryToReadFile ( file ) {
try {
return fs.readFileSync( file, 'utf-8' );
} catch ( err ) {
if ( err.code !== 'ENOENT' ) throw err;
return null;
}
}
describe( 'svelte', () => {
before( () => {
function cleanChildren ( node ) {
@ -241,6 +252,7 @@ describe( 'svelte', () => {
let compiled;
showCompiledCode = config.show;
compileOptions = config.compileOptions || {};
try {
const source = fs.readFileSync( `test/generator/${dir}/main.html`, 'utf-8' );
@ -519,14 +531,18 @@ describe( 'svelte', () => {
( solo ? it.only : it )( dir, () => {
const component = require( `./server-side-rendering/${dir}/main.html` );
const expected = fs.readFileSync( `test/server-side-rendering/${dir}/_expected.html`, 'utf-8' );
const expectedHtml = tryToReadFile( `test/server-side-rendering/${dir}/_expected.html` );
const expectedCss = tryToReadFile( `test/server-side-rendering/${dir}/_expected.css` ) || '';
const data = tryToLoadJson( `test/server-side-rendering/${dir}/data.json` );
const actual = component.render( data );
const html = component.render( data );
const { css } = component.renderCss();
fs.writeFileSync( `test/server-side-rendering/${dir}/_actual.html`, actual );
fs.writeFileSync( `test/server-side-rendering/${dir}/_actual.html`, html );
if ( css ) fs.writeFileSync( `test/server-side-rendering/${dir}/_actual.css`, css );
assert.htmlEqual( actual, expected );
assert.htmlEqual( html, expectedHtml );
assert.equal( css.replace( /^\s+/gm, '' ), expectedCss.replace( /^\s+/gm, '' ) );
});
});
});

Loading…
Cancel
Save