You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/src/generators/Generator.js

365 lines
9.9 KiB

import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker';
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';
import getIntro from './shared/utils/getIntro.js';
import getOutro from './shared/utils/getOutro.js';
import annotateWithScopes from './annotateWithScopes.js';
export default class Generator {
constructor ( parsed, source, name, names, visitors, options ) {
this.parsed = parsed;
this.source = source;
this.name = name;
this.names = names;
this.visitors = visitors;
this.options = options;
this.imports = [];
this.helpers = {};
this.components = {};
this.events = {};
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = {};
this.elementDepth = 0;
this.code = new MagicString( source );
this.getUniqueName = counter( names );
this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
this.usesRefs = false;
this._callbacks = {};
}
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;
let scope = annotateWithScopes( expression );
walk( expression, {
enter ( node, parent, key ) {
if ( node._scope ) {
scope = node._scope;
return;
}
if ( isReference( node, parent ) ) {
const { name } = flattenReference( node );
if ( scope.has( name ) ) return;
if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) {
code.prependRight( node.start, `template.helpers.` );
}
else if ( name === 'event' && isEventHandler ) {
// noop
}
else if ( name in contexts ) {
const context = contexts[ name ];
if ( context !== name ) {
// this is true for 'reserved' names like `root` and `component`
code.overwrite( node.start, node.start + name.length, context, true );
}
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 {
// handle shorthand properties
if ( parent && parent.type === 'Property' && parent.shorthand ) {
if ( key === 'key' ) {
code.appendLeft( node.start, `${name}: ` );
return;
}
}
if ( globalWhitelist[ name ] ) {
code.prependRight( node.start, `( '${name}' in root ? root.` );
code.appendLeft( node.object ? node.object.end : node.end, ` : ${name} )` );
} else {
code.prependRight( node.start, `root.` );
}
dependencies.push( name );
if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' );
}
this.skip();
}
},
leave ( node ) {
if ( node._scope ) scope = scope.parent;
}
});
dependencies.forEach( name => {
this.expectedProperties[ name ] = true;
});
return {
dependencies,
contexts: usedContexts,
snippet: `[✂${expression.start}-${expression.end}✂]`,
string: this.code.slice( expression.start, expression.end )
};
}
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 );
}
}
generate ( result, options, { name, format } ) {
if ( this.imports.length ) {
const statements = [];
this.imports.forEach( ( declaration, i ) => {
if ( format === 'es' ) {
statements.push( this.source.slice( declaration.start, declaration.end ) );
return;
}
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}`;
declaration.name = name; // hacky but makes life a bit easier later
namedImports.forEach( specifier => {
statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` );
});
if ( defaultImport ) {
statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` );
}
});
result = `${statements.join( '\n' )}\n\n${result}`;
}
const pattern = /\[✂(\d+)-(\d+)$/;
const parts = result.split( '✂]' );
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString ( str ) {
compiled.addSource({
content: new MagicString( str )
});
}
const intro = getIntro( format, options, this.imports );
if ( intro ) addString( intro );
const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if ( !parts.length ) {
compiled.addSource({
filename,
content: new MagicString( this.source ).remove( 0, this.source.length )
});
}
parts.forEach( str => {
const chunk = str.replace( pattern, '' );
if ( chunk ) addString( chunk );
const match = pattern.exec( str );
const snippet = this.code.snip( +match[1], +match[2] );
compiled.addSource({
filename,
content: snippet
});
});
addString( finalChunk );
addString( '\n\n' + getOutro( format, name, options, this.imports ) );
return {
code: compiled.toString(),
map: compiled.generateMap({ includeContent: true, file: options.outputFilename })
};
}
getUniqueNameMaker () {
return counter( this.names );
}
parseJs () {
const { source } = this;
const { js } = this.parsed;
const imports = this.imports;
const computations = [];
const templateProperties = {};
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( 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 => {
templateProperties[ prop.key.name ] = prop;
});
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 ( templateProperties[ key ] ) {
templateProperties[ key ].value.properties.forEach( prop => {
this[ key ][ prop.key.name ] = prop.value;
});
}
});
if ( templateProperties.computed ) {
const dependencies = new Map();
templateProperties.computed.value.properties.forEach( prop => {
const key = prop.key.name;
const value = prop.value;
const deps = value.params.map( param => param.type === 'AssignmentPattern' ? param.left.name : 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 );
computations.push({ key, deps });
}
templateProperties.computed.value.properties.forEach( prop => visit( prop.key.name ) );
}
}
return {
computations,
templateProperties
};
}
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 ( visitor.type === 'Element' ) {
this.elementDepth += 1;
}
if ( node.children ) {
node.children.forEach( child => {
this.visit( child );
});
}
if ( visitor.type === 'Element' ) {
this.elementDepth -= 1;
}
if ( visitor.leave ) visitor.leave( this, node );
}
}