Merge pull request #447 from sveltejs/gh-440

Remove unnecessary template IIFEs
pull/448/head
Rich Harris 8 years ago committed by GitHub
commit 3af7a7c849

@ -4,6 +4,8 @@ import isReference from '../utils/isReference.js';
import flattenReference from '../utils/flattenReference.js';
import globalWhitelist from '../utils/globalWhitelist.js';
import reservedNames from '../utils/reservedNames.js';
import namespaces from '../utils/namespaces.js';
import { removeNode, removeObjectKey } from '../utils/removeNode.js';
import getIntro from './shared/utils/getIntro.js';
import getOutro from './shared/utils/getOutro.js';
import processCss from './shared/processCss.js';
@ -21,6 +23,7 @@ export default class Generator {
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.importedComponents = new Map();
this.bindingGroups = [];
@ -257,67 +260,42 @@ export default class Generator {
};
}
parseJs () {
parseJs ( ssr ) {
const { source } = this;
const { js } = this.parsed;
const imports = this.imports;
const computations = [];
let defaultExport = null;
const templateProperties = {};
let namespace = null;
let hasJs = !!js;
if ( js ) {
this.addSourcemapLocations( js.content );
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
// imports need to be hoisted out of the IIFE
for ( let i = 0; i < js.content.body.length; i += 1 ) {
const node = js.content.body[i];
for ( let i = 0; i < body.length; i += 1 ) {
const node = 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;
removeNode( this.code, js.content, node );
imports.push( node );
this.code.remove( a, b );
node.specifiers.forEach( specifier => {
this.importedNames.add( specifier.local.name );
});
}
}
defaultExport = js.content.body.find( node => node.type === 'ExportDefaultDeclaration' );
const defaultExport = body.find( node => node.type === 'ExportDefaultDeclaration' );
if ( defaultExport ) {
const finalNode = js.content.body[ js.content.body.length - 1 ];
if ( defaultExport === finalNode ) {
// export is last property, we can just return it
this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` );
} else {
const { declarations } = annotateWithScopes( js );
let template = 'template';
for ( let i = 1; declarations.has( template ); template = `template_${i++}` );
this.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 );
this.code.appendLeft( finalNode.end, `\n\n${indentation}return ${template};` );
}
defaultExport.declaration.properties.forEach( prop => {
templateProperties[ prop.key.name ] = prop;
});
this.code.prependRight( js.content.start, `var ${this.alias( 'template' )} = (function () {` );
} else {
this.code.prependRight( js.content.start, '(function () {' );
}
this.code.appendLeft( js.content.end, '}());' );
[ 'helpers', 'events', 'components' ].forEach( key => {
if ( templateProperties[ key ] ) {
templateProperties[ key ].value.properties.forEach( prop => {
@ -353,11 +331,102 @@ export default class Generator {
templateProperties.computed.value.properties.forEach( prop => visit( prop.key.name ) );
}
if ( templateProperties.namespace ) {
const ns = templateProperties.namespace.value.value;
namespace = namespaces[ ns ] || ns;
removeObjectKey( this.code, defaultExport.declaration, 'namespace' );
}
if ( templateProperties.components ) {
let hasNonImportedComponent = false;
templateProperties.components.value.properties.forEach( property => {
const key = property.key.name;
const value = source.slice( property.value.start, property.value.end );
if ( this.importedNames.has( value ) ) {
this.importedComponents.set( key, value );
} else {
hasNonImportedComponent = true;
}
});
if ( hasNonImportedComponent ) {
// remove the specific components that were imported, as we'll refer to them directly
Array.from( this.importedComponents.keys() ).forEach( key => {
removeObjectKey( this.code, templateProperties.components.value, key );
});
} else {
// remove the entire components portion of the export
removeObjectKey( this.code, defaultExport.declaration, 'components' );
}
}
// Remove these after version 2
if ( templateProperties.onrender ) {
const { key } = templateProperties.onrender;
this.code.overwrite( key.start, key.end, 'oncreate', true );
templateProperties.oncreate = templateProperties.onrender;
}
if ( templateProperties.onteardown ) {
const { key } = templateProperties.onteardown;
this.code.overwrite( key.start, key.end, 'ondestroy', true );
templateProperties.ondestroy = templateProperties.onteardown;
}
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy
if ( ssr ) {
if ( templateProperties.oncreate ) removeNode( this.code, defaultExport.declaration, templateProperties.oncreate );
if ( templateProperties.ondestroy ) removeNode( this.code, defaultExport.declaration, templateProperties.ondestroy );
if ( templateProperties.methods ) removeNode( this.code, defaultExport.declaration, templateProperties.methods );
if ( templateProperties.events ) removeNode( this.code, defaultExport.declaration, templateProperties.events );
}
// now that we've analysed the default export, we can determine whether or not we need to keep it
let hasDefaultExport = !!defaultExport;
if ( defaultExport && defaultExport.declaration.properties.length === 0 ) {
hasDefaultExport = false;
removeNode( this.code, js.content, defaultExport );
}
// if we do need to keep it, then we need to generate a return statement
if ( hasDefaultExport ) {
const finalNode = body[ body.length - 1 ];
if ( defaultExport === finalNode ) {
// export is last property, we can just return it
this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` );
} else {
const { declarations } = annotateWithScopes( js );
let template = 'template';
for ( let i = 1; declarations.has( template ); template = `template_${i++}` );
this.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 );
this.code.appendLeft( finalNode.end, `\n\n${indentation}return ${template};` );
}
}
// user code gets wrapped in an IIFE
if ( js.content.body.length ) {
const prefix = hasDefaultExport ? `var ${this.alias( 'template' )} = (function () {` : `(function () {`;
this.code.prependRight( js.content.start, prefix ).appendLeft( js.content.end, '}());' );
}
// if there's no need to include user code, remove it altogether
else {
this.code.remove( js.content.start, js.content.end );
hasJs = false;
}
}
return {
computations,
defaultExport,
hasJs,
namespace,
templateProperties
};
}

@ -1,8 +1,6 @@
import deindent from '../../utils/deindent.js';
import getBuilders from './utils/getBuilders.js';
import CodeBuilder from '../../utils/CodeBuilder.js';
import namespaces from '../../utils/namespaces.js';
import removeObjectKey from '../../utils/removeObjectKey.js';
import visitors from './visitors/index.js';
import Generator from '../Generator.js';
import * as shared from '../../shared/index.js';
@ -17,8 +15,6 @@ class DomGenerator extends Generator {
this.builders = {
metaBindings: new CodeBuilder()
};
this.importedComponents = new Map();
}
addElement ( name, renderStatement, needsIdentifier = false ) {
@ -155,50 +151,7 @@ export default function dom ( parsed, source, options ) {
const generator = new DomGenerator( parsed, source, name, visitors, options );
const { computations, defaultExport, templateProperties } = generator.parseJs();
// Remove these after version 2
if ( templateProperties.onrender ) {
const { key } = templateProperties.onrender;
generator.code.overwrite( key.start, key.end, 'oncreate', true );
templateProperties.oncreate = templateProperties.onrender;
}
if ( templateProperties.onteardown ) {
const { key } = templateProperties.onteardown;
generator.code.overwrite( key.start, key.end, 'ondestroy', true );
templateProperties.ondestroy = templateProperties.onteardown;
}
let namespace = null;
if ( templateProperties.namespace ) {
const ns = templateProperties.namespace.value.value;
namespace = namespaces[ ns ] || ns;
removeObjectKey( generator, defaultExport.declaration, 'namespace' );
}
if ( templateProperties.components ) {
let hasNonImportedComponent = false;
templateProperties.components.value.properties.forEach( property => {
const key = property.key.name;
const value = source.slice( property.value.start, property.value.end );
if ( generator.importedNames.has( value ) ) {
generator.importedComponents.set( key, value );
} else {
hasNonImportedComponent = true;
}
});
if ( hasNonImportedComponent ) {
// remove the specific components that were imported, as we'll refer to them directly
Array.from( generator.importedComponents.keys() ).forEach( key => {
removeObjectKey( generator, templateProperties.components.value, key );
});
} else {
// remove the entire components portion of the export
removeObjectKey( generator, defaultExport.declaration, 'components' );
}
}
const { computations, hasJs, templateProperties, namespace } = generator.parseJs();
const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] );
const component = getUniqueName( 'component' );
@ -273,7 +226,7 @@ export default function dom ( parsed, source, options ) {
${generator.helper( 'dispatchObservers' )}( this, this._observers.post, newState, oldState );
` );
if ( parsed.js ) {
if ( hasJs ) {
builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}

@ -40,7 +40,7 @@ export default function ssr ( parsed, source, options ) {
const generator = new SsrGenerator( parsed, source, name, visitors, options );
const { computations, templateProperties } = generator.parseJs();
const { computations, hasJs, templateProperties } = generator.parseJs( true );
const builders = {
main: new CodeBuilder(),
@ -117,7 +117,9 @@ export default function ssr ( parsed, source, options ) {
` );
templateProperties.components.value.properties.forEach( prop => {
builders.renderCss.addLine( `addComponent( ${generator.alias( 'template' )}.components.${prop.key.name} );` );
const { name } = prop.key;
const expression = generator.importedComponents.get( name ) || `${generator.alias( 'template' )}.components.${name}`;
builders.renderCss.addLine( `addComponent( ${expression} );` );
});
}
@ -129,7 +131,7 @@ export default function ssr ( parsed, source, options ) {
};
` );
if ( parsed.js ) {
if ( hasJs ) {
builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}

@ -50,7 +50,7 @@ export default {
}))
.join( ', ' );
const expression = node.name === ':Self' ? generator.name : `${generator.alias( 'template' )}.components.${node.name}`;
const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`;
bindings.forEach( binding => {
generator.addBinding( binding, expression );

@ -0,0 +1,55 @@
const keys = {
ObjectExpression: 'properties',
Program: 'body'
};
const offsets = {
ObjectExpression: [ 1, -1 ],
Program: [ 0, 0 ]
};
export function removeNode ( code, parent, node ) {
const key = keys[ parent.type ];
const offset = offsets[ parent.type ];
if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` );
const list = parent[ key ];
const i = list.indexOf( node );
if ( i === -1 ) throw new Error( 'node not in list' );
let a;
let b;
if ( list.length === 1 ) {
// remove everything, leave {}
a = parent.start + offset[0];
b = parent.end + offset[1];
} else if ( i === 0 ) {
// remove everything before second node, including comments
a = parent.start + offset[0];
while ( /\s/.test( code.original[a] ) ) a += 1;
b = list[i].end;
while ( /[\s,]/.test( code.original[b] ) ) b += 1;
} else {
// remove the end of the previous node to the end of this one
a = list[ i - 1 ].end;
b = node.end;
}
code.remove( a, b );
list.splice( i, 1 );
return;
}
export function removeObjectKey ( code, node, key ) {
if ( node.type !== 'ObjectExpression' ) return;
let i = node.properties.length;
while ( i-- ) {
const property = node.properties[i];
if ( property.key.type === 'Identifier' && property.key.name === key ) {
removeNode( code, node, property );
}
}
}

@ -1,33 +0,0 @@
export default function removeObjectKey ( generator, node, key ) {
if ( node.type !== 'ObjectExpression' ) return;
let i = node.properties.length;
while ( i-- ) {
const property = node.properties[i];
if ( property.key.type === 'Identifier' && property.key.name === key ) {
let a;
let b;
if ( node.properties.length === 1 ) {
// remove everything, leave {}
a = node.start + 1;
b = node.end - 1;
} else if ( i === 0 ) {
// remove everything before second property, including comments
a = node.start + 1;
while ( /\s/.test( generator.code.original[a] ) ) a += 1;
b = node.properties[i].end;
while ( /[\s,]/.test( generator.code.original[b] ) ) b += 1;
} else {
// remove the end of the previous property to the end of this one
a = node.properties[ i - 1 ].end;
b = property.end;
}
generator.code.remove( a, b );
node.properties.splice( i, 1 );
return;
}
}
}

@ -1,7 +1,8 @@
import assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import { addLineNumbers, exists, loadConfig, setupHtmlEqual, svelte, tryToLoadJson } from '../helpers.js';
import { addLineNumbers, loadConfig, setupHtmlEqual, svelte, tryToLoadJson } from '../helpers.js';
function tryToReadFile ( file ) {
try {
@ -12,6 +13,10 @@ function tryToReadFile ( file ) {
}
}
function capitalize ( str ) {
return str[0].toUpperCase() + str.slice( 1 );
}
describe( 'ssr', () => {
before( () => {
require( process.env.COVERAGE ?
@ -24,24 +29,51 @@ describe( 'ssr', () => {
fs.readdirSync( 'test/server-side-rendering/samples' ).forEach( dir => {
if ( dir[0] === '.' ) return;
const solo = exists( `test/server-side-rendering/samples/${dir}/solo` );
// add .solo to a sample directory name to only run that test, or
// .show to always show the output. or both
const solo = /\.solo/.test( dir );
let show = /\.show/.test( dir );
if ( solo && process.env.CI ) {
throw new Error( 'Forgot to remove `solo: true` from test' );
}
( solo ? it.only : it )( dir, () => {
const component = require( `./samples/${dir}/main.html` );
dir = path.resolve( 'test/server-side-rendering/samples', dir );
const component = require( `${dir}/main.html` );
const expectedHtml = tryToReadFile( `${dir}/_expected.html` );
const expectedCss = tryToReadFile( `${dir}/_expected.css` ) || '';
const data = tryToLoadJson( `${dir}/data.json` );
let html;
let css;
let error;
const expectedHtml = tryToReadFile( `test/server-side-rendering/samples/${dir}/_expected.html` );
const expectedCss = tryToReadFile( `test/server-side-rendering/samples/${dir}/_expected.css` ) || '';
try {
html = component.render( data );
css = component.renderCss().css;
} catch ( e ) {
show = true;
error = e;
}
if ( show ) {
fs.readdirSync( dir ).forEach( file => {
if ( file[0] === '_' ) return;
const source = fs.readFileSync( `${dir}/${file}`, 'utf-8' );
const name = capitalize( file.slice( 0, -path.extname( file ).length ) );
const { code } = svelte.compile( source, { generate: 'ssr', name });
console.group( file );
console.log( addLineNumbers( code ) );
console.groupEnd();
});
}
const data = tryToLoadJson( `test/server-side-rendering/samples/${dir}/data.json` );
const html = component.render( data );
const { css } = component.renderCss();
if ( error ) throw error;
fs.writeFileSync( `test/server-side-rendering/samples/${dir}/_actual.html`, html );
if ( css ) fs.writeFileSync( `test/server-side-rendering/samples/${dir}/_actual.css`, css );
fs.writeFileSync( `${dir}/_actual.html`, html );
if ( css ) fs.writeFileSync( `${dir}/_actual.css`, css );
assert.htmlEqual( html, expectedHtml );
assert.equal( css.replace( /^\s+/gm, '' ), expectedCss.replace( /^\s+/gm, '' ) );

Loading…
Cancel
Save