mirror of https://github.com/sveltejs/svelte
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.
365 lines
9.9 KiB
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 );
|
|
}
|
|
}
|