server-rendered CSS (#153)

pull/154/head
Rich Harris 8 years ago
parent 37edc19627
commit bac02481b7

@ -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 );
}

@ -393,7 +393,16 @@ export default function generate ( parsed, source, options, names ) {
}
if ( parsed.css ) {
topLevelStatements.push( processCss( parsed ) );
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() );

@ -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,6 +4,7 @@ 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;
@ -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' );

@ -3,5 +3,11 @@ import compile from './compile.js';
require.extensions[ '.html' ] = function ( module, filename ) {
const { code } = compile( fs.readFileSync( filename, 'utf-8' ) );
return module._compile( code, filename );
try {
return module._compile( code, filename );
} catch ( err ) {
console.log( code );
throw err;
}
};

@ -132,6 +132,75 @@ function flatten ( node ) {
return { name, keypath: parts.join( '.' ) };
}
function spaces ( i ) {
let result = '';
while ( i-- ) result += ' ';
return result;
}
// largely borrowed from Ractive https://github.com/ractivejs/ractive/blob/2ec648aaf5296bb88c21812e947e0e42fcc456e3/src/Ractive/config/custom/css/transform.js
const selectorsPattern = /(?:^|\})?\s*([^\{\}]+)\s*\{/g;
const commentsPattern = /\/\*.*?\*\//g;
const selectorUnitPattern = /((?:(?:\[[^\]+]\])|(?:[^\s\+\>~:]))+)((?:::?[^\s\+\>\~\(:]+(?:\([^\)]+\))?)*\s*[\s\+\>\~]?)\s*/g;
const excludePattern = /^(?:@|\d+%)/;
function transformSelector ( selector, parent ) {
const selectorUnits = [];
let match;
while ( match = selectorUnitPattern.exec( selector ) ) {
selectorUnits.push({
str: match[0],
base: match[1],
modifiers: match[2]
});
}
// For each simple selector within the selector, we need to create a version
// that a) combines with the id, and b) is inside the id
const base = selectorUnits.map( unit => unit.str );
const transformed = [];
let i = selectorUnits.length;
while ( i-- ) {
const appended = base.slice();
// Pseudo-selectors should go after the attribute selector
const unit = selectorUnits[i];
appended[i] = unit.base + parent + unit.modifiers || '';
const prepended = base.slice();
prepended[i] = parent + ' ' + prepended[i];
transformed.push( appended.join( ' ' ), prepended.join( ' ' ) );
}
return transformed.join( ', ' );
}
function transformCss ( css, hash ) {
const attr = `[svelte-${hash}]`;
return css
.replace( commentsPattern, '' )
.replace( selectorsPattern, ( match, $1 ) => {
// don't transform at-rules and keyframe declarations
if ( excludePattern.test( $1 ) ) return match;
const selectors = $1.split( ',' ).map( selector => selector.trim() );
const transformed = selectors
.map( selector => transformSelector( selector, attr ) )
.join( ', ' ) + ' ';
return match.replace( $1, transformed );
});
}
function process ( parsed ) {
return transformCss( spaces( parsed.css.content.start ) + parsed.css.content.styles, parsed.hash );
}
const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
function compile ( source, filename ) {
@ -240,6 +309,8 @@ function compile ( source, filename ) {
};
}
let elementDepth = 0;
const stringifiers = {
Component ( node ) {
const props = node.attributes.map( attribute => {
@ -314,12 +385,18 @@ 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;
@ -405,10 +482,6 @@ 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 || {};`
];
@ -452,10 +525,55 @@ function compile ( source, filename ) {
`return rendered;`
);
const renderCssStatements = [
`var components = [];`
];
if ( parsed.css ) {
renderCssStatements.push( deindent`
components.push({
filename: exports.filename,
css: ${JSON.stringify( process( 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' );
@ -496,6 +614,12 @@ function compile ( source, filename ) {
require.extensions[ '.html' ] = function ( module, filename ) {
const { code } = compile( fs.readFileSync( filename, 'utf-8' ) );
return module._compile( code, filename );
try {
return module._compile( code, filename );
} catch ( err ) {
console.log( code );
throw err;
}
};
//# sourceMappingURL=register.js.map

File diff suppressed because one or more lines are too long

@ -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>

@ -71,6 +71,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 ) {
@ -519,14 +528,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