Skip to content

Remove unnecessary template IIFEs #447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 105 additions & 36 deletions src/generators/Generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = [];

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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
};
}
Expand Down
51 changes: 2 additions & 49 deletions src/generators/dom/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,8 +15,6 @@ class DomGenerator extends Generator {
this.builders = {
metaBindings: new CodeBuilder()
};

this.importedComponents = new Map();
}

addElement ( name, renderStatement, needsIdentifier = false ) {
Expand Down Expand Up @@ -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' );
Expand Down Expand Up @@ -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}✂]` );
}

Expand Down
8 changes: 5 additions & 3 deletions src/generators/server-side-rendering/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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} );` );
});
}

Expand All @@ -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}✂]` );
}

Expand Down
2 changes: 1 addition & 1 deletion src/generators/server-side-rendering/visitors/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
55 changes: 55 additions & 0 deletions src/utils/removeNode.js
Original file line number Diff line number Diff line change
@@ -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 );
}
}
}
Loading