import { walk , childKeys } from 'estree-walker' ;
import { getLocator } from 'locate-character' ;
import Stats from '../Stats' ;
import { globals , reserved , is_valid } from '../utils/names' ;
import { namespaces , valid_namespaces } from '../utils/namespaces' ;
import create_module from './create_module' ;
import {
create_scopes ,
extract_names ,
Scope ,
extract_identifiers ,
} from './utils/scope' ;
import Stylesheet from './css/Stylesheet' ;
import { test } from '../config' ;
import Fragment from './nodes/Fragment' ;
import internal_exports from './internal_exports' ;
import { Ast , CompileOptions , Var , Warning } from '../interfaces' ;
import error from '../utils/error' ;
import get_code_frame from '../utils/get_code_frame' ;
import flatten_reference from './utils/flatten_reference' ;
import is_used_as_reference from './utils/is_used_as_reference' ;
import is_reference from 'is-reference' ;
import TemplateScope from './nodes/shared/TemplateScope' ;
import fuzzymatch from '../utils/fuzzymatch' ;
import get_object from './utils/get_object' ;
import Slot from './nodes/Slot' ;
import { Node , ImportDeclaration , Identifier , Program , ExpressionStatement , AssignmentExpression , Literal } from 'estree' ;
import add_to_set from './utils/add_to_set' ;
import check_graph_for_cycles from './utils/check_graph_for_cycles' ;
import { print , x , b } from 'code-red' ;
interface ComponentOptions {
namespace ? : string ;
tag? : string ;
immutable? : boolean ;
accessors? : boolean ;
preserveWhitespace? : boolean ;
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys . EachBlock = childKeys . IfBlock = [ 'children' , 'else' ] ;
childKeys . Attribute = [ 'value' ] ;
childKeys . ExportNamedDeclaration = [ 'declaration' , 'specifiers' ] ;
export default class Component {
stats : Stats ;
warnings : Warning [ ] ;
ignores : Set < string > ;
ignore_stack : Array < Set < string > > = [ ] ;
ast : Ast ;
original_ast : Ast ;
source : string ;
name : Identifier ;
compile_options : CompileOptions ;
fragment : Fragment ;
module _scope : Scope ;
instance_scope : Scope ;
instance_scope_map : WeakMap < Node , Scope > ;
component_options : ComponentOptions ;
namespace : string ;
tag : string ;
accessors : boolean ;
vars : Var [ ] = [ ] ;
var_lookup : Map < string , Var > = new Map ( ) ;
imports : ImportDeclaration [ ] = [ ] ;
hoistable_nodes : Set < Node > = new Set ( ) ;
node_for_declaration : Map < string , Node > = new Map ( ) ;
partly_hoisted : Array < ( Node | Node [ ] ) > = [ ] ;
fully_hoisted : Array < ( Node | Node [ ] ) > = [ ] ;
reactive_declarations : Array < {
assignees : Set < string > ;
dependencies : Set < string > ;
node : Node ;
declaration : Node ;
} > = [ ] ;
reactive_declaration_nodes : Set < Node > = new Set ( ) ;
has_reactive_assignments = false ;
injected_reactive_declaration_vars : Set < string > = new Set ( ) ;
helpers : Map < string , Identifier > = new Map ( ) ;
globals : Map < string , Identifier > = new Map ( ) ;
indirect_dependencies : Map < string , Set < string > > = new Map ( ) ;
file : string ;
locate : ( c : number ) = > { line : number ; column : number } ;
stylesheet : Stylesheet ;
aliases : Map < string , Identifier > = new Map ( ) ;
used_names : Set < string > = new Set ( ) ;
globally_used_names : Set < string > = new Set ( ) ;
slots : Map < string , Slot > = new Map ( ) ;
slot_outlets : Set < string > = new Set ( ) ;
constructor (
ast : Ast ,
source : string ,
name : string ,
compile_options : CompileOptions ,
stats : Stats ,
warnings : Warning [ ]
) {
this . name = { type : 'Identifier' , name } ;
this . stats = stats ;
this . warnings = warnings ;
this . ast = ast ;
this . source = source ;
this . compile_options = compile_options ;
// the instance JS gets mutated, so we park
// a copy here for later. TODO this feels gross
this . original_ast = {
html : ast.html ,
css : ast.css ,
instance : ast.instance && JSON . parse ( JSON . stringify ( ast . instance ) ) ,
module : ast.module
} ;
this . file =
compile_options . filename &&
( typeof process !== 'undefined'
? compile_options . filename
. replace ( process . cwd ( ) , '' )
. replace ( /^[/\\]/ , '' )
: compile_options . filename ) ;
this . locate = getLocator ( this . source , { offsetLine : 1 } ) ;
// styles
this . stylesheet = new Stylesheet (
source ,
ast ,
compile_options . filename ,
compile_options . dev
) ;
this . stylesheet . validate ( this ) ;
this . component_options = process_component_options (
this ,
this . ast . html . children
) ;
this . namespace =
namespaces [ this . component_options . namespace ] ||
this . component_options . namespace ;
if ( compile_options . customElement ) {
if (
this . component_options . tag === undefined &&
compile_options . tag === undefined
) {
const svelteOptions = ast . html . children . find (
child = > child . name === 'svelte:options'
) || { start : 0 , end : 0 } ;
this . warn ( svelteOptions , {
code : 'custom-element-no-tag' ,
message : ` No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/> ` ,
} ) ;
}
this . tag = this . component_options . tag || compile_options . tag ;
} else {
this . tag = this . name . name ;
}
this . walk_module_js_pre_template ( ) ;
this . walk_instance_js_pre_template ( ) ;
this . fragment = new Fragment ( this , ast . html ) ;
this . name = this . get_unique_name ( name ) ;
this . walk_module_js_post_template ( ) ;
this . walk_instance_js_post_template ( ) ;
if ( ! compile_options . customElement ) this . stylesheet . reify ( ) ;
this . stylesheet . warn_on_unused_selectors ( this ) ;
}
add_var ( variable : Var ) {
this . vars . push ( variable ) ;
this . var_lookup . set ( variable . name , variable ) ;
}
add_reference ( name : string ) {
const variable = this . var_lookup . get ( name ) ;
if ( variable ) {
variable . referenced = true ;
} else if ( name === '$$props' ) {
this . add_var ( {
name ,
injected : true ,
referenced : true ,
} ) ;
} else if ( name [ 0 ] === '$' ) {
this . add_var ( {
name ,
injected : true ,
referenced : true ,
mutated : true ,
writable : true ,
} ) ;
const subscribable_name = name . slice ( 1 ) ;
this . add_reference ( subscribable_name ) ;
const variable = this . var_lookup . get ( subscribable_name ) ;
if ( variable ) variable . subscribable = true ;
} else {
this . used_names . add ( name ) ;
}
}
alias ( name : string ) {
if ( ! this . aliases . has ( name ) ) {
this . aliases . set ( name , this . get_unique_name ( name ) ) ;
}
return this . aliases . get ( name ) ;
}
global ( name : string ) {
const alias = this . alias ( name ) ;
this . globals . set ( name , alias ) ;
return alias ;
}
generate ( result? : Node [ ] ) {
let js = null ;
let css = null ;
if ( result ) {
const { compile_options , name } = this ;
const { format = 'esm' } = compile_options ;
const banner = ` ${ this . file ? ` ${ this . file } ` : ` ` } generated by Svelte v ${ '__VERSION__' } ` ;
const program : any = { type : 'Program' , body : result } ;
walk ( program , {
enter : ( node , parent , key ) = > {
if ( node . type === 'Identifier' ) {
if ( node . name [ 0 ] === '@' ) {
if ( node . name [ 1 ] === '_' ) {
const alias = this . global ( node . name . slice ( 2 ) ) ;
node . name = alias . name ;
} else {
let name = node . name . slice ( 1 ) ;
if ( compile_options . dev ) {
if ( internal_exports . has ( ` ${ name } _dev ` ) ) {
name += '_dev' ;
} else if ( internal_exports . has ( ` ${ name } Dev ` ) ) {
name += 'Dev' ;
}
}
const alias = this . alias ( name ) ;
this . helpers . set ( name , alias ) ;
node . name = alias . name ;
}
}
else if ( node . name [ 0 ] !== '#' && ! is_valid ( node . name ) ) {
// this hack allows x`foo.${bar}` where bar could be invalid
const literal : Literal = { type : 'Literal' , value : node.name } ;
if ( parent . type === 'Property' && key === 'key' ) {
parent . key = literal ;
}
else if ( parent . type === 'MemberExpression' && key === 'property' ) {
parent . property = literal ;
parent . computed = true ;
}
}
}
}
} ) ;
const referenced_globals = Array . from (
this . globals ,
( [ name , alias ] ) = > name !== alias . name && { name , alias }
) . filter ( Boolean ) ;
if ( referenced_globals . length ) {
this . helpers . set ( 'globals' , this . alias ( 'globals' ) ) ;
}
const imported_helpers = Array . from ( this . helpers , ( [ name , alias ] ) = > ( {
name ,
alias ,
} ) ) ;
create_module (
program ,
format ,
name ,
banner ,
compile_options . sveltePath ,
imported_helpers ,
referenced_globals ,
this . imports ,
this . vars
. filter ( variable = > variable . module && variable . export_name )
. map ( variable = > ( {
name : variable.name ,
as : variable . export_name ,
} ) )
) ;
css = compile_options . customElement
? { code : null , map : null }
: this . stylesheet . render ( compile_options . cssOutputFilename , true ) ;
js = print ( program , {
sourceMapSource : compile_options.filename
} ) ;
js . map . sources = [
compile_options . filename ? get_relative_path ( compile_options . outputFilename || '' , compile_options . filename ) : null
] ;
js . map . sourcesContent = [
this . source
] ;
}
return {
js ,
css ,
ast : this.original_ast ,
warnings : this.warnings ,
vars : this.vars
. filter ( v = > ! v . global && ! v . internal )
. map ( v = > ( {
name : v.name ,
export_name : v.export_name || null ,
injected : v.injected || false ,
module : v.module || false ,
mutated : v.mutated || false ,
reassigned : v.reassigned || false ,
referenced : v.referenced || false ,
writable : v.writable || false ,
referenced_from_script : v.referenced_from_script || false ,
} ) ) ,
stats : this.stats.render ( ) ,
} ;
}
get_unique_name ( name : string ) : Identifier {
if ( test ) name = ` ${ name } $ ` ;
let alias = name ;
for (
let i = 1 ;
reserved . has ( alias ) ||
this . var_lookup . has ( alias ) ||
this . used_names . has ( alias ) ||
this . globally_used_names . has ( alias ) ;
alias = ` ${ name } _ ${ i ++ } `
) ;
this . used_names . add ( alias ) ;
return { type : 'Identifier' , name : alias } ;
}
get_unique_name_maker() {
const local_used_names = new Set ( ) ;
function add ( name : string ) {
local_used_names . add ( name ) ;
}
reserved . forEach ( add ) ;
internal_exports . forEach ( add ) ;
this . var_lookup . forEach ( ( _value , key ) = > add ( key ) ) ;
return ( name : string ) : Identifier = > {
if ( test ) name = ` ${ name } $ ` ;
let alias = name ;
for (
let i = 1 ;
this . used_names . has ( alias ) || local_used_names . has ( alias ) ;
alias = ` ${ name } _ ${ i ++ } `
) ;
local_used_names . add ( alias ) ;
this . globally_used_names . add ( alias ) ;
return {
type : 'Identifier' ,
name : alias
} ;
} ;
}
error (
pos : {
start : number ;
end : number ;
} ,
e : {
code : string ;
message : string ;
}
) {
error ( e . message , {
name : 'ValidationError' ,
code : e.code ,
source : this.source ,
start : pos.start ,
end : pos.end ,
filename : this.compile_options.filename ,
} ) ;
}
warn (
pos : {
start : number ;
end : number ;
} ,
warning : {
code : string ;
message : string ;
}
) {
if ( this . ignores && this . ignores . has ( warning . code ) ) {
return ;
}
const start = this . locate ( pos . start ) ;
const end = this . locate ( pos . end ) ;
const frame = get_code_frame ( this . source , start . line - 1 , start . column ) ;
this . warnings . push ( {
code : warning.code ,
message : warning.message ,
frame ,
start ,
end ,
pos : pos.start ,
filename : this.compile_options.filename ,
toString : ( ) = >
` ${ warning . message } ( ${ start . line } : ${ start . column } ) \ n ${ frame } ` ,
} ) ;
}
extract_imports ( node ) {
this . imports . push ( node ) ;
}
extract_exports ( node ) {
if ( node . type === 'ExportDefaultDeclaration' ) {
this . error ( node , {
code : ` default-export ` ,
message : ` A component cannot have a default export ` ,
} ) ;
}
if ( node . type === 'ExportNamedDeclaration' ) {
if ( node . source ) {
this . error ( node , {
code : ` not-implemented ` ,
message : ` A component currently cannot have an export ... from ` ,
} ) ;
}
if ( node . declaration ) {
if ( node . declaration . type === 'VariableDeclaration' ) {
node . declaration . declarations . forEach ( declarator = > {
extract_names ( declarator . id ) . forEach ( name = > {
const variable = this . var_lookup . get ( name ) ;
variable . export_name = name ;
if ( variable . writable && ! ( variable . referenced || variable . referenced_from_script ) ) {
this . warn ( declarator , {
code : ` unused-export-let ` ,
message : ` ${ this . name . name } has unused export property ' ${ name } '. If it is for external reference only, please consider using \` export const ' ${ name } ' \` `
} ) ;
}
} ) ;
} ) ;
} else {
const { name } = node . declaration . id ;
const variable = this . var_lookup . get ( name ) ;
variable . export_name = name ;
}
return node . declaration ;
} else {
node . specifiers . forEach ( specifier = > {
const variable = this . var_lookup . get ( specifier . local . name ) ;
if ( variable ) {
variable . export_name = specifier . exported . name ;
if ( variable . writable && ! ( variable . referenced || variable . referenced_from_script ) ) {
this . warn ( specifier , {
code : ` unused-export-let ` ,
message : ` ${ this . name . name } has unused export property ' ${ specifier . exported . name } '. If it is for external reference only, please consider using \` export const ' ${ specifier . exported . name } ' \` `
} ) ;
}
}
} ) ;
return null ;
}
}
}
extract_javascript ( script ) {
if ( ! script ) return null ;
return script . content . body . filter ( node = > {
if ( ! node ) return false ;
if ( this . hoistable_nodes . has ( node ) ) return false ;
if ( this . reactive_declaration_nodes . has ( node ) ) return false ;
if ( node . type === 'ImportDeclaration' ) return false ;
if ( node . type === 'ExportDeclaration' && node . specifiers . length > 0 )
return false ;
return true ;
} ) ;
}
walk_module_js_pre_template() {
const component = this ;
const script = this . ast . module ;
if ( ! script ) return ;
walk ( script . content , {
enter ( node ) {
if ( node . type === 'LabeledStatement' && node . label . name === '$' ) {
component . warn ( node as any , {
code : 'module-script-reactive-declaration' ,
message : '$: has no effect in a module script' ,
} ) ;
}
} ,
} ) ;
const { scope , globals } = create_scopes ( script . content ) ;
this . module _scope = scope ;
scope . declarations . forEach ( ( node , name ) = > {
if ( name [ 0 ] === '$' ) {
this . error ( node as any , {
code : 'illegal-declaration' ,
message : ` The $ prefix is reserved, and cannot be used for variable and import names ` ,
} ) ;
}
const writable = node . type === 'VariableDeclaration' && ( node . kind === 'var' || node . kind === 'let' ) ;
this . add_var ( {
name ,
module : true ,
hoistable : true ,
writable
} ) ;
} ) ;
globals . forEach ( ( node , name ) = > {
if ( name [ 0 ] === '$' ) {
this . error ( node as any , {
code : 'illegal-subscription' ,
message : ` Cannot reference store value inside <script context="module"> ` ,
} ) ;
} else {
this . add_var ( {
name ,
global : true ,
hoistable : true
} ) ;
}
} ) ;
}
walk_instance_js_pre_template() {
const script = this . ast . instance ;
if ( ! script ) return ;
// inject vars for reactive declarations
script . content . body . forEach ( node = > {
if ( node . type !== 'LabeledStatement' ) return ;
if ( node . body . type !== 'ExpressionStatement' ) return ;
const { expression } = node . body ;
if ( expression . type !== 'AssignmentExpression' ) return ;
extract_names ( expression . left ) . forEach ( name = > {
if ( ! this . var_lookup . has ( name ) && name [ 0 ] !== '$' ) {
this . injected_reactive_declaration_vars . add ( name ) ;
}
} ) ;
} ) ;
const { scope : instance_scope , map , globals } = create_scopes (
script . content
) ;
this . instance_scope = instance_scope ;
this . instance_scope_map = map ;
instance_scope . declarations . forEach ( ( node , name ) = > {
if ( name [ 0 ] === '$' ) {
this . error ( node as any , {
code : 'illegal-declaration' ,
message : ` The $ prefix is reserved, and cannot be used for variable and import names ` ,
} ) ;
}
const writable = node . type === 'VariableDeclaration' && ( node . kind === 'var' || node . kind === 'let' ) ;
this . add_var ( {
name ,
initialised : instance_scope.initialised_declarations.has ( name ) ,
hoistable : /^Import/ . test ( node . type ) ,
writable
} ) ;
this . node_for_declaration . set ( name , node ) ;
} ) ;
globals . forEach ( ( node , name ) = > {
if ( this . var_lookup . has ( name ) ) return ;
if ( this . injected_reactive_declaration_vars . has ( name ) ) {
this . add_var ( {
name ,
injected : true ,
writable : true ,
reassigned : true ,
initialised : true ,
} ) ;
} else if ( name === '$$props' ) {
this . add_var ( {
name ,
injected : true ,
} ) ;
} else if ( name [ 0 ] === '$' ) {
if ( name === '$' || name [ 1 ] === '$' ) {
this . error ( node as any , {
code : 'illegal-global' ,
message : ` ${ name } is an illegal variable name `
} ) ;
}
this . add_var ( {
name ,
injected : true ,
mutated : true ,
writable : true ,
} ) ;
this . add_reference ( name . slice ( 1 ) ) ;
const variable = this . var_lookup . get ( name . slice ( 1 ) ) ;
if ( variable ) {
variable . subscribable = true ;
variable . referenced_from_script = true ;
}
} else {
this . add_var ( {
name ,
global : true ,
hoistable : true
} ) ;
}
} ) ;
this . track_references_and_mutations ( ) ;
}
walk_module_js_post_template() {
const script = this . ast . module ;
if ( ! script ) return ;
const { body } = script . content ;
let i = body . length ;
while ( -- i >= 0 ) {
const node = body [ i ] ;
if ( node . type === 'ImportDeclaration' ) {
this . extract_imports ( node ) ;
body . splice ( i , 1 ) ;
}
if ( /^Export/ . test ( node . type ) ) {
const replacement = this . extract_exports ( node ) ;
if ( replacement ) {
body [ i ] = replacement ;
} else {
body . splice ( i , 1 ) ;
}
}
}
}
walk_instance_js_post_template() {
const script = this . ast . instance ;
if ( ! script ) return ;
this . post_template_walk ( ) ;
this . hoist_instance_declarations ( ) ;
this . extract_reactive_declarations ( ) ;
}
post_template_walk() {
const script = this . ast . instance ;
if ( ! script ) return ;
const component = this ;
const { content } = script ;
const { instance_scope , instance_scope_map : map } = this ;
let scope = instance_scope ;
const toRemove = [ ] ;
const remove = ( parent , prop , index ) = > {
toRemove . unshift ( [ parent , prop , index ] ) ;
} ;
const toInsert = new Map ( ) ;
walk ( content , {
enter ( node , parent , prop , index ) {
if ( map . has ( node ) ) {
scope = map . get ( node ) ;
}
if ( node . type === 'ImportDeclaration' ) {
component . extract_imports ( node ) ;
// TODO: to use actual remove
remove ( parent , prop , index ) ;
return this . skip ( ) ;
}
if ( /^Export/ . test ( node . type ) ) {
const replacement = component . extract_exports ( node ) ;
if ( replacement ) {
this . replace ( replacement ) ;
} else {
// TODO: to use actual remove
remove ( parent , prop , index ) ;
}
return this . skip ( ) ;
}
component . warn_on_undefined_store_value_references ( node , parent , scope ) ;
if ( component . compile_options . dev ) {
const to_insert_for_loop_protect = component . loop_protect ( node , prop , index ) ;
if ( to_insert_for_loop_protect ) {
if ( ! Array . isArray ( parent [ prop ] ) ) {
parent [ prop ] = {
type : 'BlockStatement' ,
body : [ to_insert_for_loop_protect . node , node ] ,
} ;
} else {
// can't insert directly, will screw up the index in the for-loop of estree-walker
if ( ! toInsert . has ( parent ) ) {
toInsert . set ( parent , [ ] ) ;
}
toInsert . get ( parent ) . push ( to_insert_for_loop_protect ) ;
}
}
}
} ,
leave ( node ) {
if ( map . has ( node ) ) {
scope = scope . parent ;
}
if ( toInsert . has ( node ) ) {
const nodes_to_insert = toInsert . get ( node ) ;
for ( const { index , prop , node : node_to_insert } of nodes_to_insert . reverse ( ) ) {
node [ prop ] . splice ( index , 0 , node_to_insert ) ;
}
toInsert . delete ( node ) ;
}
} ,
} ) ;
for ( const [ parent , prop , index ] of toRemove ) {
if ( parent ) {
if ( index !== null ) {
parent [ prop ] . splice ( index , 1 ) ;
} else {
delete parent [ prop ] ;
}
}
}
}
track_references_and_mutations() {
const script = this . ast . instance ;
if ( ! script ) return ;
const component = this ;
const { content } = script ;
const { instance_scope , instance_scope_map : map } = this ;
let scope = instance_scope ;
walk ( content , {
enter ( node , parent ) {
if ( map . has ( node ) ) {
scope = map . get ( node ) ;
}
if ( node . type === 'AssignmentExpression' || node . type === 'UpdateExpression' ) {
const assignee = node . type === 'AssignmentExpression' ? node.left : node.argument ;
const names = extract_names ( assignee ) ;
const deep = assignee . type === 'MemberExpression' ;
names . forEach ( name = > {
if ( scope . find_owner ( name ) === instance_scope ) {
const variable = component . var_lookup . get ( name ) ;
variable [ deep ? 'mutated' : 'reassigned' ] = true ;
}
} ) ;
}
if ( is_used_as_reference ( node , parent ) ) {
const object = get_object ( node ) ;
if ( scope . find_owner ( object . name ) === instance_scope ) {
const variable = component . var_lookup . get ( object . name ) ;
variable . referenced_from_script = true ;
}
}
} ,
leave ( node ) {
if ( map . has ( node ) ) {
scope = scope . parent ;
}
} ,
} ) ;
}
warn_on_undefined_store_value_references ( node , parent , scope ) {
if (
node . type === 'LabeledStatement' &&
node . label . name === '$' &&
parent . type !== 'Program'
) {
this . warn ( node as any , {
code : 'non-top-level-reactive-declaration' ,
message : '$: has no effect outside of the top-level' ,
} ) ;
}
if ( is_reference ( node as Node , parent as Node ) ) {
const object = get_object ( node ) ;
const { name } = object ;
if ( name [ 0 ] === '$' && ! scope . has ( name ) ) {
this . warn_if_undefined ( name , object , null ) ;
}
}
}
loop_protect ( node , prop , index ) {
if ( node . type === 'WhileStatement' ||
node . type === 'ForStatement' ||
node . type === 'DoWhileStatement' ) {
const id = this . get_unique_name ( 'LP' ) ;
this . add_var ( {
name : id.name ,
internal : true ,
} ) ;
const before = b ` const ${ id } = Date.now(); ` ;
const inside = b `
if ( Date . now ( ) - $ { id } > 100 ) {
throw new Error ( 'Infinite loop detected' ) ;
}
` ;
// wrap expression statement with BlockStatement
if ( node . body . type !== 'BlockStatement' ) {
node . body = {
type : 'BlockStatement' ,
body : [ node . body ] ,
} ;
}
node . body . body . push ( inside [ 0 ] ) ;
return { index , prop , node : before [ 0 ] } ;
}
return null ;
}
invalidate ( name , value ? ) {
const variable = this . var_lookup . get ( name ) ;
if ( variable && ( variable . subscribable && ( variable . reassigned || variable . export_name ) ) ) {
return x ` ${ ` $ $ subscribe_ ${ name } ` } ( $ $ invalidate(' ${ name } ', ${ value || name } )) ` ;
}
if ( name [ 0 ] === '$' && name [ 1 ] !== '$' ) {
return x ` ${ name . slice ( 1 ) } .set( ${ value || name } ) ` ;
}
if (
variable &&
! variable . referenced &&
! variable . is_reactive_dependency &&
! variable . export_name &&
! name . startsWith ( '$$' )
) {
return value || name ;
}
if ( value ) {
return x ` $ $ invalidate(' ${ name } ', ${ value } ) ` ;
}
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set ( [ name ] ) ;
deps . forEach ( name = > {
const reactive_declarations = this . reactive_declarations . filter ( x = >
x . assignees . has ( name )
) ;
reactive_declarations . forEach ( declaration = > {
declaration . dependencies . forEach ( name = > {
deps . add ( name ) ;
} ) ;
} ) ;
} ) ;
return Array . from ( deps )
. map ( n = > x ` $ $ invalidate(' ${ n } ', ${ n } ) ` )
. reduce ( ( lhs , rhs ) = > x ` ${ lhs } , ${ rhs } } ` ) ;
}
rewrite_props ( get_insert : ( variable : Var ) = > Node [ ] ) {
if ( ! this . ast . instance ) return ;
const component = this ;
const { instance_scope , instance_scope_map : map } = this ;
let scope = instance_scope ;
walk ( this . ast . instance . content , {
enter ( node , parent , key , index ) {
if ( /Function/ . test ( node . type ) ) {
return this . skip ( ) ;
}
if ( map . has ( node ) ) {
scope = map . get ( node ) ;
}
if ( node . type === 'VariableDeclaration' ) {
if ( node . kind === 'var' || scope === instance_scope ) {
node . declarations . forEach ( declarator = > {
if ( declarator . id . type !== 'Identifier' ) {
const inserts = [ ] ;
extract_names ( declarator . id ) . forEach ( name = > {
const variable = component . var_lookup . get ( name ) ;
if ( variable . export_name ) {
// TODO is this still true post-#3539?
component . error ( declarator as any , {
code : 'destructured-prop' ,
message : ` Cannot declare props in destructured declaration ` ,
} ) ;
}
if ( variable . subscribable ) {
inserts . push ( get_insert ( variable ) ) ;
}
} ) ;
if ( inserts . length ) {
parent [ key ] . splice ( index + 1 , 0 , . . . inserts ) ;
}
return ;
}
const { name } = declarator . id ;
const variable = component . var_lookup . get ( name ) ;
if ( variable . export_name && variable . writable ) {
const insert = variable . subscribable
? get_insert ( variable )
: null ;
parent [ key ] . splice ( index + 1 , 0 , insert ) ;
declarator . id = {
type : 'ObjectPattern' ,
properties : [ {
type : 'Property' ,
method : false ,
shorthand : false ,
computed : false ,
kind : 'init' ,
key : { type : 'Identifier' , name : variable.export_name } ,
value : declarator.init
? {
type : 'AssignmentPattern' ,
left : declarator.id ,
right : declarator.init
}
: declarator . id
} ]
} ;
declarator . init = x ` $ $ props ` ;
} else if ( variable . subscribable ) {
const insert = get_insert ( variable ) ;
parent [ key ] . splice ( index + 1 , 0 , . . . insert ) ;
}
} ) ;
}
}
} ,
leave ( node , parent , _key , index ) {
if ( map . has ( node ) ) {
scope = scope . parent ;
}
if ( node . type === 'ExportNamedDeclaration' && node . declaration ) {
( parent as Program ) . body [ index ] = node . declaration ;
}
} ,
} ) ;
}
hoist_instance_declarations() {
// we can safely hoist variable declarations that are
// initialised to literals, and functions that don't
// reference instance variables other than other
// hoistable functions. TODO others?
const {
hoistable_nodes ,
var_lookup ,
injected_reactive_declaration_vars ,
} = this ;
const top_level_function_declarations = new Map ( ) ;
const { body } = this . ast . instance . content ;
for ( let i = 0 ; i < body . length ; i += 1 ) {
const node = body [ i ] ;
if ( node . type === 'VariableDeclaration' ) {
const all_hoistable = node . declarations . every ( d = > {
if ( ! d . init ) return false ;
if ( d . init . type !== 'Literal' ) return false ;
const { name } = d . id as Identifier ;
const v = this . var_lookup . get ( name ) ;
if ( v . reassigned ) return false ;
if ( v . export_name ) return false ;
if ( this . var_lookup . get ( name ) . reassigned ) return false ;
if (
this . vars . find (
variable = > variable . name === name && variable . module
)
)
return false ;
return true ;
} ) ;
if ( all_hoistable ) {
node . declarations . forEach ( d = > {
const variable = this . var_lookup . get ( ( d . id as Identifier ) . name ) ;
variable . hoistable = true ;
} ) ;
hoistable_nodes . add ( node ) ;
body . splice ( i -- , 1 ) ;
this . fully_hoisted . push ( node ) ;
}
}
if (
node . type === 'ExportNamedDeclaration' &&
node . declaration &&
node . declaration . type === 'FunctionDeclaration'
) {
top_level_function_declarations . set ( node . declaration . id . name , node ) ;
}
if ( node . type === 'FunctionDeclaration' ) {
top_level_function_declarations . set ( node . id . name , node ) ;
}
}
const checked = new Set ( ) ;
const walking = new Set ( ) ;
const is_hoistable = fn_declaration = > {
if ( fn_declaration . type === 'ExportNamedDeclaration' ) {
fn_declaration = fn_declaration . declaration ;
}
const instance_scope = this . instance_scope ;
let scope = this . instance_scope ;
const map = this . instance_scope_map ;
let hoistable = true ;
// handle cycles
walking . add ( fn_declaration ) ;
walk ( fn_declaration , {
enter ( node , parent ) {
if ( ! hoistable ) return this . skip ( ) ;
if ( map . has ( node ) ) {
scope = map . get ( node ) ;
}
if ( is_reference ( node as Node , parent as Node ) ) {
const { name } = flatten_reference ( node ) ;
const owner = scope . find_owner ( name ) ;
if ( injected_reactive_declaration_vars . has ( name ) ) {
hoistable = false ;
} else if ( name [ 0 ] === '$' && ! owner ) {
hoistable = false ;
} else if ( owner === instance_scope ) {
const variable = var_lookup . get ( name ) ;
if ( variable . reassigned || variable . mutated ) hoistable = false ;
if ( name === fn_declaration . id . name ) return ;
if ( variable . hoistable ) return ;
if ( top_level_function_declarations . has ( name ) ) {
const other_declaration = top_level_function_declarations . get (
name
) ;
if ( walking . has ( other_declaration ) ) {
hoistable = false ;
} else if (
other_declaration . type === 'ExportNamedDeclaration' &&
walking . has ( other_declaration . declaration )
) {
hoistable = false ;
} else if ( ! is_hoistable ( other_declaration ) ) {
hoistable = false ;
}
} else {
hoistable = false ;
}
}
this . skip ( ) ;
}
} ,
leave ( node ) {
if ( map . has ( node ) ) {
scope = scope . parent ;
}
} ,
} ) ;
checked . add ( fn_declaration ) ;
walking . delete ( fn_declaration ) ;
return hoistable ;
} ;
for ( const [ name , node ] of top_level_function_declarations ) {
if ( is_hoistable ( node ) ) {
const variable = this . var_lookup . get ( name ) ;
variable . hoistable = true ;
hoistable_nodes . add ( node ) ;
const i = body . indexOf ( node ) ;
body . splice ( i , 1 ) ;
this . fully_hoisted . push ( node ) ;
}
}
}
extract_reactive_declarations() {
const component = this ;
const unsorted_reactive_declarations = [ ] ;
this . ast . instance . content . body . forEach ( node = > {
if ( node . type === 'LabeledStatement' && node . label . name === '$' ) {
this . reactive_declaration_nodes . add ( node ) ;
const assignees = new Set ( ) ;
const assignee_nodes = new Set ( ) ;
const dependencies = new Set ( ) ;
let scope = this . instance_scope ;
const map = this . instance_scope_map ;
walk ( node . body , {
enter ( node , parent ) {
if ( map . has ( node ) ) {
scope = map . get ( node ) ;
}
if ( node . type === 'AssignmentExpression' ) {
const left = get_object ( node . left ) ;
extract_identifiers ( left ) . forEach ( node = > {
assignee_nodes . add ( node ) ;
assignees . add ( node . name ) ;
} ) ;
if ( node . operator !== '=' ) {
dependencies . add ( left . name ) ;
}
} else if ( node . type === 'UpdateExpression' ) {
const identifier = get_object ( node . argument ) ;
assignees . add ( identifier . name ) ;
} else if ( is_reference ( node as Node , parent as Node ) ) {
const identifier = get_object ( node ) ;
if ( ! assignee_nodes . has ( identifier ) ) {
const { name } = identifier ;
const owner = scope . find_owner ( name ) ;
const variable = component . var_lookup . get ( name ) ;
if ( variable ) variable . is_reactive_dependency = true ;
const is_writable_or_mutated =
variable && ( variable . writable || variable . mutated ) ;
if (
( ! owner || owner === component . instance_scope ) &&
( name [ 0 ] === '$' || is_writable_or_mutated )
) {
dependencies . add ( name ) ;
}
}
this . skip ( ) ;
}
} ,
leave ( node ) {
if ( map . has ( node ) ) {
scope = scope . parent ;
}
} ,
} ) ;
const { expression } = node . body as ExpressionStatement ;
const declaration = expression && ( expression as AssignmentExpression ) . left ;
unsorted_reactive_declarations . push ( {
assignees ,
dependencies ,
node ,
declaration ,
} ) ;
}
} ) ;
const lookup = new Map ( ) ;
let seen ;
unsorted_reactive_declarations . forEach ( declaration = > {
declaration . assignees . forEach ( name = > {
if ( ! lookup . has ( name ) ) {
lookup . set ( name , [ ] ) ;
}
// TODO warn or error if a name is assigned to in
// multiple reactive declarations?
lookup . get ( name ) . push ( declaration ) ;
} ) ;
} ) ;
const cycle = check_graph_for_cycles ( unsorted_reactive_declarations . reduce ( ( acc , declaration ) = > {
declaration . assignees . forEach ( v = > {
declaration . dependencies . forEach ( w = > {
if ( ! declaration . assignees . has ( w ) ) {
acc . push ( [ v , w ] ) ;
}
} ) ;
} ) ;
return acc ;
} , [ ] ) ) ;
if ( cycle && cycle . length ) {
const declarationList = lookup . get ( cycle [ 0 ] ) ;
const declaration = declarationList [ 0 ] ;
this . error ( declaration . node , {
code : 'cyclical-reactive-declaration' ,
message : ` Cyclical dependency detected: ${ cycle . join ( ' → ' ) } `
} ) ;
}
const add_declaration = declaration = > {
if ( this . reactive_declarations . indexOf ( declaration ) !== - 1 ) {
return ;
}
seen . add ( declaration ) ;
declaration . dependencies . forEach ( name = > {
if ( declaration . assignees . has ( name ) ) return ;
const earlier_declarations = lookup . get ( name ) ;
if ( earlier_declarations )
earlier_declarations . forEach ( declaration = > {
add_declaration ( declaration ) ;
} ) ;
} ) ;
this . reactive_declarations . push ( declaration ) ;
} ;
unsorted_reactive_declarations . forEach ( declaration = > {
seen = new Set ( ) ;
add_declaration ( declaration ) ;
} ) ;
}
qualify ( name ) {
if ( name === ` $ $ props ` ) return x ` #ctx. $ $ props ` ;
let [ head , . . . tail ] = name . split ( '.' ) ;
const variable = this . var_lookup . get ( head ) ;
if ( variable ) {
this . add_reference ( name ) ; // TODO we can probably remove most other occurrences of this
if ( ! variable . hoistable ) {
tail . unshift ( head ) ;
head = '#ctx' ;
}
}
return [ head , . . . tail ] . reduce ( ( lhs , rhs ) = > x ` ${ lhs } . ${ rhs } ` ) ;
}
warn_if_undefined ( name : string , node , template_scope : TemplateScope ) {
if ( name [ 0 ] === '$' ) {
if ( name === '$' || name [ 1 ] === '$' && name !== '$$props' ) {
this . error ( node , {
code : 'illegal-global' ,
message : ` ${ name } is an illegal variable name `
} ) ;
}
this . has_reactive_assignments = true ; // TODO does this belong here?
if ( name === '$$props' ) return ;
name = name . slice ( 1 ) ;
}
if ( this . var_lookup . has ( name ) && ! this . var_lookup . get ( name ) . global ) return ;
if ( template_scope && template_scope . names . has ( name ) ) return ;
if ( globals . has ( name ) ) return ;
let message = ` ' ${ name } ' is not defined ` ;
if ( ! this . ast . instance )
message += ` . Consider adding a <script> block with 'export let ${ name } ' to declare a prop ` ;
this . warn ( node , {
code : 'missing-declaration' ,
message ,
} ) ;
}
push_ignores ( ignores ) {
this . ignores = new Set ( this . ignores || [ ] ) ;
add_to_set ( this . ignores , ignores ) ;
this . ignore_stack . push ( this . ignores ) ;
}
pop_ignores() {
this . ignore_stack . pop ( ) ;
this . ignores = this . ignore_stack [ this . ignore_stack . length - 1 ] ;
}
}
function process_component_options ( component : Component , nodes ) {
const component_options : ComponentOptions = {
immutable : component.compile_options.immutable || false ,
accessors :
'accessors' in component . compile_options
? component . compile_options . accessors
: ! ! component . compile_options . customElement ,
preserveWhitespace : ! ! component . compile_options . preserveWhitespace ,
} ;
const node = nodes . find ( node = > node . name === 'svelte:options' ) ;
function get_value ( attribute , code , message ) {
const { value } = attribute ;
const chunk = value [ 0 ] ;
if ( ! chunk ) return true ;
if ( value . length > 1 ) {
component . error ( attribute , { code , message } ) ;
}
if ( chunk . type === 'Text' ) return chunk . data ;
if ( chunk . expression . type !== 'Literal' ) {
component . error ( attribute , { code , message } ) ;
}
return chunk . expression . value ;
}
if ( node ) {
node . attributes . forEach ( attribute = > {
if ( attribute . type === 'Attribute' ) {
const { name } = attribute ;
switch ( name ) {
case 'tag' : {
const code = 'invalid-tag-attribute' ;
const message = ` 'tag' must be a string literal ` ;
const tag = get_value ( attribute , code , message ) ;
if ( typeof tag !== 'string' && tag !== null )
component . error ( attribute , { code , message } ) ;
if ( tag && ! /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/ . test ( tag ) ) {
component . error ( attribute , {
code : ` invalid-tag-property ` ,
message : ` tag name must be two or more words joined by the '-' character ` ,
} ) ;
}
if ( tag && ! component . compile_options . customElement ) {
component . warn ( attribute , {
code : 'missing-custom-element-compile-options' ,
message : ` The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option? `
} ) ;
}
component_options . tag = tag ;
break ;
}
case 'namespace' : {
const code = 'invalid-namespace-attribute' ;
const message = ` The 'namespace' attribute must be a string literal representing a valid namespace ` ;
const ns = get_value ( attribute , code , message ) ;
if ( typeof ns !== 'string' )
component . error ( attribute , { code , message } ) ;
if ( valid_namespaces . indexOf ( ns ) === - 1 ) {
const match = fuzzymatch ( ns , valid_namespaces ) ;
if ( match ) {
component . error ( attribute , {
code : ` invalid-namespace-property ` ,
message : ` Invalid namespace ' ${ ns } ' (did you mean ' ${ match } '?) ` ,
} ) ;
} else {
component . error ( attribute , {
code : ` invalid-namespace-property ` ,
message : ` Invalid namespace ' ${ ns } ' ` ,
} ) ;
}
}
component_options . namespace = ns ;
break ;
}
case 'accessors' :
case 'immutable' :
case 'preserveWhitespace' : {
const code = ` invalid- ${ name } -value ` ;
const message = ` ${ name } attribute must be true or false ` ;
const value = get_value ( attribute , code , message ) ;
if ( typeof value !== 'boolean' )
component . error ( attribute , { code , message } ) ;
component_options [ name ] = value ;
break ;
}
default :
component . error ( attribute , {
code : ` invalid-options-attribute ` ,
message : ` <svelte:options> unknown attribute ` ,
} ) ;
}
} else {
component . error ( attribute , {
code : ` invalid-options-attribute ` ,
message : ` <svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes ` ,
} ) ;
}
} ) ;
}
return component_options ;
}
function get_relative_path ( from : string , to : string ) {
const from_parts = from . split ( /[/\\]/ ) ;
const to_parts = to . split ( /[/\\]/ ) ;
from_parts . pop ( ) ; // get dirname
while ( from_parts [ 0 ] === to_parts [ 0 ] ) {
from_parts . shift ( ) ;
to_parts . shift ( ) ;
}
if ( from_parts . length ) {
let i = from_parts . length ;
while ( i -- ) from_parts [ i ] = '..' ;
}
return from_parts . concat ( to_parts ) . join ( '/' ) ;
}