|
|
import MagicString from 'magic-string';
|
|
|
import CodeBuilder from '../utils/CodeBuilder.js';
|
|
|
import { walk } from 'estree-walker';
|
|
|
import deindent from '../utils/deindent.js';
|
|
|
import isReference from '../utils/isReference.js';
|
|
|
import counter from './shared/utils/counter.js';
|
|
|
import flattenReference from '../utils/flattenReference.js';
|
|
|
import globalWhitelist from '../utils/globalWhitelist.js';
|
|
|
|
|
|
export default class Generator {
|
|
|
constructor ( parsed, source, names, visitors ) {
|
|
|
this.parsed = parsed;
|
|
|
this.source = source;
|
|
|
this.names = names;
|
|
|
this.visitors = visitors;
|
|
|
|
|
|
this.imports = [];
|
|
|
this.templateProperties = {};
|
|
|
this.helpers = {};
|
|
|
this.components = {};
|
|
|
this.events = {};
|
|
|
|
|
|
this.renderers = [];
|
|
|
this.code = new MagicString( source );
|
|
|
this.getUniqueName = counter( names );
|
|
|
this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
|
|
|
this.usesRefs = false;
|
|
|
|
|
|
this._callbacks = {};
|
|
|
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
addElement ( name, renderStatement, needsIdentifier = false ) {
|
|
|
const isToplevel = this.current.localElementDepth === 0;
|
|
|
if ( needsIdentifier || isToplevel ) {
|
|
|
this.current.builders.init.addLine(
|
|
|
`var ${name} = ${renderStatement};`
|
|
|
);
|
|
|
|
|
|
this.createMountStatement( name );
|
|
|
} else {
|
|
|
this.current.builders.init.addLine(
|
|
|
`${this.current.target}.appendChild( ${renderStatement} );`
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if ( isToplevel ) {
|
|
|
this.current.builders.detach.addLine(
|
|
|
`${name}.parentNode.removeChild( ${name} );`
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
addRenderer ( fragment ) {
|
|
|
if ( fragment.autofocus ) {
|
|
|
fragment.builders.init.addLine( `${fragment.autofocus}.focus();` );
|
|
|
}
|
|
|
|
|
|
// minor hack – we need to ensure that any {{{triples}}} are detached
|
|
|
// first, so we append normal detach statements to detachRaw
|
|
|
fragment.builders.detachRaw.addBlock( fragment.builders.detach );
|
|
|
|
|
|
if ( !fragment.builders.detachRaw.isEmpty() ) {
|
|
|
fragment.builders.teardown.addBlock( deindent`
|
|
|
if ( detach ) {
|
|
|
${fragment.builders.detachRaw}
|
|
|
}
|
|
|
` );
|
|
|
}
|
|
|
|
|
|
this.renderers.push( deindent`
|
|
|
function ${fragment.name} ( ${fragment.params}, component ) {
|
|
|
${fragment.builders.init}
|
|
|
|
|
|
return {
|
|
|
mount: function ( target, anchor ) {
|
|
|
${fragment.builders.mount}
|
|
|
},
|
|
|
|
|
|
update: function ( changed, ${fragment.params} ) {
|
|
|
${fragment.builders.update}
|
|
|
},
|
|
|
|
|
|
teardown: function ( detach ) {
|
|
|
${fragment.builders.teardown}
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
` );
|
|
|
}
|
|
|
|
|
|
addSourcemapLocations ( node ) {
|
|
|
walk( node, {
|
|
|
enter: node => {
|
|
|
this.code.addSourcemapLocation( node.start );
|
|
|
this.code.addSourcemapLocation( node.end );
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
contextualise ( expression, isEventHandler ) {
|
|
|
const usedContexts = [];
|
|
|
const dependencies = [];
|
|
|
|
|
|
const { code, helpers } = this;
|
|
|
const { contextDependencies, contexts, indexes } = this.current;
|
|
|
|
|
|
walk( expression, {
|
|
|
enter ( node, parent ) {
|
|
|
if ( isReference( node, parent ) ) {
|
|
|
const { name } = flattenReference( node );
|
|
|
|
|
|
if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) {
|
|
|
code.prependRight( node.start, `template.helpers.` );
|
|
|
}
|
|
|
|
|
|
else if ( name === 'event' && isEventHandler ) {
|
|
|
// noop
|
|
|
}
|
|
|
|
|
|
else if ( contexts[ name ] ) {
|
|
|
dependencies.push( ...contextDependencies[ name ] );
|
|
|
if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name );
|
|
|
}
|
|
|
|
|
|
else if ( indexes[ name ] ) {
|
|
|
const context = indexes[ name ];
|
|
|
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
if ( globalWhitelist[ name ] ) {
|
|
|
code.prependRight( node.start, `( '${name}' in root ? root.` );
|
|
|
code.appendLeft( node.object.end, ` : ${name} )` );
|
|
|
} else {
|
|
|
code.prependRight( node.start, `root.` );
|
|
|
}
|
|
|
|
|
|
dependencies.push( name );
|
|
|
if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' );
|
|
|
}
|
|
|
|
|
|
this.skip();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
dependencies,
|
|
|
contexts: usedContexts,
|
|
|
snippet: `[✂${expression.start}-${expression.end}✂]`,
|
|
|
string: this.code.slice( expression.start, expression.end )
|
|
|
};
|
|
|
}
|
|
|
|
|
|
createAnchor ( _name, description = '' ) {
|
|
|
const name = `${_name}_anchor`;
|
|
|
const statement = `document.createComment( ${JSON.stringify( description )} )`;
|
|
|
this.addElement( name, statement, true );
|
|
|
return name;
|
|
|
}
|
|
|
|
|
|
createMountStatement ( name ) {
|
|
|
if ( this.current.target === 'target' ) {
|
|
|
this.current.builders.mount.addLine(
|
|
|
`target.insertBefore( ${name}, anchor );`
|
|
|
);
|
|
|
} else {
|
|
|
this.current.builders.init.addLine(
|
|
|
`${this.current.target}.appendChild( ${name} );` );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
fire ( eventName, data ) {
|
|
|
const handlers = eventName in this._callbacks && this._callbacks[ eventName ].slice();
|
|
|
if ( !handlers ) return;
|
|
|
|
|
|
for ( let i = 0; i < handlers.length; i += 1 ) {
|
|
|
handlers[i].call( this, data );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
generateBlock ( node, name ) {
|
|
|
this.push({
|
|
|
name,
|
|
|
target: 'target',
|
|
|
localElementDepth: 0,
|
|
|
builders: this.getBuilders(),
|
|
|
getUniqueName: this.getUniqueNameMaker()
|
|
|
});
|
|
|
// walk the children here
|
|
|
node.children.forEach( node => this.visit( node ) );
|
|
|
this.addRenderer( this.current );
|
|
|
this.pop();
|
|
|
// unset the children, to avoid them being visited again
|
|
|
node.children = [];
|
|
|
}
|
|
|
|
|
|
getBuilders () {
|
|
|
return {
|
|
|
init: new CodeBuilder(),
|
|
|
mount: new CodeBuilder(),
|
|
|
update: new CodeBuilder(),
|
|
|
detach: new CodeBuilder(),
|
|
|
detachRaw: new CodeBuilder(),
|
|
|
teardown: new CodeBuilder()
|
|
|
};
|
|
|
}
|
|
|
|
|
|
getUniqueNameMaker () {
|
|
|
return counter( this.names );
|
|
|
}
|
|
|
|
|
|
init () {
|
|
|
const { imports, source } = this;
|
|
|
const { js } = this.parsed;
|
|
|
if ( js ) {
|
|
|
this.addSourcemapLocations( js.content );
|
|
|
|
|
|
// 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];
|
|
|
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 );
|
|
|
this.code.remove( a, b );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const defaultExport = js.content.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 {
|
|
|
// TODO ensure `template` isn't already declared
|
|
|
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 => {
|
|
|
this.templateProperties[ prop.key.name ] = prop.value;
|
|
|
});
|
|
|
|
|
|
this.code.prependRight( js.content.start, 'var template = (function () {' );
|
|
|
} else {
|
|
|
this.code.prependRight( js.content.start, '(function () {' );
|
|
|
}
|
|
|
|
|
|
this.code.appendLeft( js.content.end, '}());' );
|
|
|
|
|
|
[ 'helpers', 'events', 'components' ].forEach( key => {
|
|
|
if ( this.templateProperties[ key ] ) {
|
|
|
this.templateProperties[ key ].properties.forEach( prop => {
|
|
|
this[ key ][ prop.key.name ] = prop.value;
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
on ( eventName, handler ) {
|
|
|
const handlers = this._callbacks[ eventName ] || ( this._callbacks[ eventName ] = [] );
|
|
|
handlers.push( handler );
|
|
|
}
|
|
|
|
|
|
pop () {
|
|
|
const tail = this.current;
|
|
|
this.current = tail.parent;
|
|
|
|
|
|
return tail;
|
|
|
}
|
|
|
|
|
|
push ( fragment ) {
|
|
|
const newFragment = Object.assign( {}, this.current, fragment, {
|
|
|
parent: this.current
|
|
|
});
|
|
|
|
|
|
this.current = newFragment;
|
|
|
}
|
|
|
|
|
|
visit ( node ) {
|
|
|
const visitor = this.visitors[ node.type ];
|
|
|
if ( !visitor ) throw new Error( `Not implemented: ${node.type}` );
|
|
|
|
|
|
if ( visitor.enter ) visitor.enter( this, node );
|
|
|
|
|
|
if ( node.children ) {
|
|
|
node.children.forEach( child => {
|
|
|
this.visit( child );
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if ( visitor.leave ) visitor.leave( this, node );
|
|
|
}
|
|
|
}
|