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.
492 lines
15 KiB
492 lines
15 KiB
import MagicString, { Bundle } from 'magic-string';
|
|
import { walk } from 'estree-walker';
|
|
import isReference from '../utils/isReference';
|
|
import flattenReference from '../utils/flattenReference';
|
|
import globalWhitelist from '../utils/globalWhitelist';
|
|
import reservedNames from '../utils/reservedNames';
|
|
import namespaces from '../utils/namespaces';
|
|
import { removeNode, removeObjectKey } from '../utils/removeNode';
|
|
import getIntro from './shared/utils/getIntro';
|
|
import getOutro from './shared/utils/getOutro';
|
|
import processCss from './shared/processCss';
|
|
import annotateWithScopes from '../utils/annotateWithScopes';
|
|
import DomBlock from './dom/Block';
|
|
import SsrBlock from './server-side-rendering/Block';
|
|
import { Node, Parsed, CompileOptions } from '../interfaces';
|
|
|
|
const test = typeof global !== 'undefined' && global.__svelte_test;
|
|
|
|
export default class Generator {
|
|
parsed: Parsed;
|
|
source: string;
|
|
name: string;
|
|
options: CompileOptions;
|
|
|
|
imports: Node[];
|
|
helpers: Set<string>;
|
|
components: Set<string>;
|
|
events: Set<string>;
|
|
transitions: Set<string>;
|
|
importedComponents: Map<string, string>;
|
|
|
|
bindingGroups: string[];
|
|
expectedProperties: Set<string>;
|
|
css: string;
|
|
cssId: string;
|
|
usesRefs: boolean;
|
|
|
|
importedNames: Set<string>;
|
|
aliases: Map<string, string>;
|
|
usedNames: Set<string>;
|
|
|
|
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) {
|
|
this.parsed = parsed;
|
|
this.source = source;
|
|
this.name = name;
|
|
this.options = options;
|
|
|
|
this.imports = [];
|
|
this.helpers = new Set();
|
|
this.components = new Set();
|
|
this.events = new Set();
|
|
this.transitions = new Set();
|
|
this.importedComponents = new Map();
|
|
|
|
this.bindingGroups = [];
|
|
|
|
// track which properties are needed, so we can provide useful info
|
|
// in dev mode
|
|
this.expectedProperties = new Set();
|
|
|
|
this.code = new MagicString( source );
|
|
this.css = parsed.css ? processCss( parsed, this.code ) : null;
|
|
this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
|
|
this.usesRefs = false;
|
|
|
|
// allow compiler to deconflict user's `import { get } from 'whatever'` and
|
|
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
|
|
this.importedNames = new Set();
|
|
this.aliases = new Map();
|
|
this.usedNames = new Set( [ name ] );
|
|
}
|
|
|
|
addSourcemapLocations ( node: Node ) {
|
|
walk( node, {
|
|
enter: ( node: Node ) => {
|
|
this.code.addSourcemapLocation( node.start );
|
|
this.code.addSourcemapLocation( node.end );
|
|
}
|
|
});
|
|
}
|
|
|
|
alias ( name: string ) {
|
|
if ( !this.aliases.has( name ) ) {
|
|
this.aliases.set( name, this.getUniqueName( name ) );
|
|
}
|
|
|
|
return this.aliases.get( name );
|
|
}
|
|
|
|
contextualise ( block: DomBlock | SsrBlock, expression: Node, context: string, isEventHandler: boolean ) {
|
|
this.addSourcemapLocations( expression );
|
|
|
|
const usedContexts: string[] = [];
|
|
|
|
const { code, helpers } = this;
|
|
const { contexts, indexes } = block;
|
|
|
|
let scope = annotateWithScopes( expression ); // TODO this already happens in findDependencies
|
|
let lexicalDepth = 0;
|
|
|
|
const self = this;
|
|
|
|
walk( expression, {
|
|
enter ( node: Node, parent: Node, key: string ) {
|
|
if ( /^Function/.test( node.type ) ) lexicalDepth += 1;
|
|
|
|
if ( node._scope ) {
|
|
scope = node._scope;
|
|
return;
|
|
}
|
|
|
|
if ( node.type === 'ThisExpression' ) {
|
|
if ( lexicalDepth === 0 && context ) code.overwrite( node.start, node.end, context, true );
|
|
}
|
|
|
|
else if ( isReference( node, parent ) ) {
|
|
const { name } = flattenReference( node );
|
|
if ( scope.has( name ) ) return;
|
|
|
|
if ( name === 'event' && isEventHandler ) {
|
|
// noop
|
|
}
|
|
|
|
else if ( contexts.has( name ) ) {
|
|
const contextName = contexts.get( name );
|
|
if ( contextName !== name ) {
|
|
// this is true for 'reserved' names like `state` and `component`
|
|
code.overwrite( node.start, node.start + name.length, contextName, true );
|
|
}
|
|
|
|
if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name );
|
|
}
|
|
|
|
else if ( helpers.has( name ) ) {
|
|
code.prependRight( node.start, `${self.alias( 'template' )}.helpers.` );
|
|
}
|
|
|
|
else if ( indexes.has( name ) ) {
|
|
const context = indexes.get( 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.has( name ) ) {
|
|
code.prependRight( node.start, `( '${name}' in state ? state.` );
|
|
code.appendLeft( node.object ? node.object.end : node.end, ` : ${name} )` );
|
|
} else {
|
|
code.prependRight( node.start, `state.` );
|
|
}
|
|
|
|
if ( !~usedContexts.indexOf( 'state' ) ) usedContexts.push( 'state' );
|
|
}
|
|
|
|
this.skip();
|
|
}
|
|
},
|
|
|
|
leave ( node: Node ) {
|
|
if ( /^Function/.test( node.type ) ) lexicalDepth -= 1;
|
|
if ( node._scope ) scope = scope.parent;
|
|
}
|
|
});
|
|
|
|
return {
|
|
dependencies: expression._dependencies, // TODO probably a better way to do this
|
|
contexts: usedContexts,
|
|
snippet: `[✂${expression.start}-${expression.end}✂]`
|
|
};
|
|
}
|
|
|
|
findDependencies ( contextDependencies: Map<string, string[]>, indexes: Map<string, string>, expression: Node ) {
|
|
if ( expression._dependencies ) return expression._dependencies;
|
|
|
|
let scope = annotateWithScopes( expression );
|
|
const dependencies: string[] = [];
|
|
|
|
const generator = this; // can't use arrow functions, because of this.skip()
|
|
|
|
walk( expression, {
|
|
enter ( node: Node, parent: Node ) {
|
|
if ( node._scope ) {
|
|
scope = node._scope;
|
|
return;
|
|
}
|
|
|
|
if ( isReference( node, parent ) ) {
|
|
const { name } = flattenReference( node );
|
|
if ( scope.has( name ) || generator.helpers.has( name ) ) return;
|
|
|
|
if ( contextDependencies.has( name ) ) {
|
|
dependencies.push( ...contextDependencies.get( name ) );
|
|
} else if ( !indexes.has( name ) ) {
|
|
dependencies.push( name );
|
|
}
|
|
|
|
this.skip();
|
|
}
|
|
},
|
|
|
|
leave ( node: Node ) {
|
|
if ( node._scope ) scope = scope.parent;
|
|
}
|
|
});
|
|
|
|
dependencies.forEach( name => {
|
|
if ( !globalWhitelist.has( name ) ) {
|
|
this.expectedProperties.add( name );
|
|
}
|
|
});
|
|
|
|
return ( expression._dependencies = dependencies );
|
|
}
|
|
|
|
generate ( result, options, { name, format } ) {
|
|
if ( this.imports.length ) {
|
|
const statements: string[] = [];
|
|
|
|
this.imports.forEach( ( declaration, i ) => {
|
|
if ( format === 'es' ) {
|
|
statements.push( this.source.slice( declaration.start, declaration.end ) );
|
|
return;
|
|
}
|
|
|
|
const defaultImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' );
|
|
const namespaceImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportNamespaceSpecifier' );
|
|
const namedImports = declaration.specifiers.filter( ( x: Node ) => 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: Node ) => {
|
|
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: string ) {
|
|
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: string ) => {
|
|
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 }),
|
|
css: this.css
|
|
};
|
|
}
|
|
|
|
getUniqueName ( name: string ) {
|
|
if ( test ) name = `${name}$`;
|
|
let alias = name;
|
|
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ); alias = `${name}_${i++}` );
|
|
this.usedNames.add( alias );
|
|
return alias;
|
|
}
|
|
|
|
getUniqueNameMaker ( params ) {
|
|
const localUsedNames = new Set( params );
|
|
return name => {
|
|
if ( test ) name = `${name}$`;
|
|
let alias = name;
|
|
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ) || localUsedNames.has( alias ); alias = `${name}_${i++}` );
|
|
localUsedNames.add( alias );
|
|
return alias;
|
|
};
|
|
}
|
|
|
|
parseJs ( ssr: boolean = false ) {
|
|
const { source } = this;
|
|
const { js } = this.parsed;
|
|
|
|
const imports = this.imports;
|
|
const computations = [];
|
|
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 < body.length; i += 1 ) {
|
|
const node = body[i];
|
|
if ( node.type === 'ImportDeclaration' ) {
|
|
removeNode( this.code, js.content, node );
|
|
imports.push( node );
|
|
|
|
node.specifiers.forEach( ( specifier: Node ) => {
|
|
this.importedNames.add( specifier.local.name );
|
|
});
|
|
}
|
|
}
|
|
|
|
const defaultExport = body.find( ( node: Node ) => node.type === 'ExportDefaultDeclaration' );
|
|
|
|
if ( defaultExport ) {
|
|
defaultExport.declaration.properties.forEach( ( prop: Node ) => {
|
|
templateProperties[ prop.key.name ] = prop;
|
|
});
|
|
}
|
|
|
|
[ 'helpers', 'events', 'components', 'transitions' ].forEach( key => {
|
|
if ( templateProperties[ key ] ) {
|
|
templateProperties[ key ].value.properties.forEach( ( prop: node ) => {
|
|
this[ key ].add( prop.key.name );
|
|
});
|
|
}
|
|
});
|
|
|
|
if ( templateProperties.computed ) {
|
|
const dependencies = new Map();
|
|
|
|
templateProperties.computed.value.properties.forEach( ( prop: Node ) => {
|
|
const key = prop.key.name;
|
|
const value = prop.value;
|
|
|
|
const deps = value.params.map( ( param: Node ) => 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: Node ) => 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: Node ) => {
|
|
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,
|
|
hasJs,
|
|
namespace,
|
|
templateProperties
|
|
};
|
|
}
|
|
}
|