@@ -18,6 +18,7 @@ import { Ast, CompileOptions, Var, Warning } from '../interfaces';
1818import error from '../utils/error' ;
1919import get_code_frame from '../utils/get_code_frame' ;
2020import flatten_reference from './utils/flatten_reference' ;
21+ import is_used_as_reference from './utils/is_used_as_reference' ;
2122import is_reference from 'is-reference' ;
2223import TemplateScope from './nodes/shared/TemplateScope' ;
2324import fuzzymatch from '../utils/fuzzymatch' ;
@@ -168,12 +169,13 @@ export default class Component {
168169 this . tag = this . name . name ;
169170 }
170171
171- this . walk_module_js ( ) ;
172+ this . walk_module_js_pre_template ( ) ;
172173 this . walk_instance_js_pre_template ( ) ;
173174
174175 this . fragment = new Fragment ( this , ast . html ) ;
175176 this . name = this . get_unique_name ( name ) ;
176177
178+ this . walk_module_js_post_template ( ) ;
177179 this . walk_instance_js_post_template ( ) ;
178180
179181 if ( ! compile_options . customElement ) this . stylesheet . reify ( ) ;
@@ -346,6 +348,7 @@ export default class Component {
346348 reassigned : v . reassigned || false ,
347349 referenced : v . referenced || false ,
348350 writable : v . writable || false ,
351+ referenced_from_script : v . referenced_from_script || false ,
349352 } ) ) ,
350353 stats : this . stats . render ( ) ,
351354 } ;
@@ -447,63 +450,64 @@ export default class Component {
447450 } ) ;
448451 }
449452
450- extract_imports ( content ) {
451- for ( let i = 0 ; i < content . body . length ; i += 1 ) {
452- const node = content . body [ i ] ;
453-
454- if ( node . type === 'ImportDeclaration' ) {
455- content . body . splice ( i -- , 1 ) ;
456- this . imports . push ( node ) ;
457- }
458- }
453+ extract_imports ( node ) {
454+ this . imports . push ( node ) ;
459455 }
460456
461- extract_exports ( content ) {
462- let i = content . body . length ;
463- while ( i -- ) {
464- const node = content . body [ i ] ;
457+ extract_exports ( node ) {
458+ if ( node . type === 'ExportDefaultDeclaration' ) {
459+ this . error ( node , {
460+ code : `default-export` ,
461+ message : `A component cannot have a default export` ,
462+ } ) ;
463+ }
465464
466- if ( node . type === 'ExportDefaultDeclaration' ) {
465+ if ( node . type === 'ExportNamedDeclaration' ) {
466+ if ( node . source ) {
467467 this . error ( node , {
468- code : `default-export ` ,
469- message : `A component cannot have a default export` ,
468+ code : `not-implemented ` ,
469+ message : `A component currently cannot have an export ... from ` ,
470470 } ) ;
471471 }
472-
473- if ( node . type === 'ExportNamedDeclaration' ) {
474- if ( node . source ) {
475- this . error ( node , {
476- code : `not-implemented` ,
477- message : `A component currently cannot have an export ... from` ,
472+ if ( node . declaration ) {
473+ if ( node . declaration . type === 'VariableDeclaration' ) {
474+ node . declaration . declarations . forEach ( declarator => {
475+ extract_names ( declarator . id ) . forEach ( name => {
476+ const variable = this . var_lookup . get ( name ) ;
477+ variable . export_name = name ;
478+ if ( variable . writable && ! ( variable . referenced || variable . referenced_from_script ) ) {
479+ this . warn ( declarator , {
480+ code : `unused-export-let` ,
481+ message : `${ this . name . name } has unused export property '${ name } '. If it is for external reference only, please consider using \`export const '${ name } '\``
482+ } ) ;
483+ }
484+ } ) ;
478485 } ) ;
486+ } else {
487+ const { name } = node . declaration . id ;
488+
489+ const variable = this . var_lookup . get ( name ) ;
490+ variable . export_name = name ;
479491 }
480- if ( node . declaration ) {
481- if ( node . declaration . type === 'VariableDeclaration' ) {
482- node . declaration . declarations . forEach ( declarator => {
483- extract_names ( declarator . id ) . forEach ( name => {
484- const variable = this . var_lookup . get ( name ) ;
485- variable . export_name = name ;
486- } ) ;
487- } ) ;
488- } else {
489- const { name } = node . declaration . id ;
490492
491- const variable = this . var_lookup . get ( name ) ;
492- variable . export_name = name ;
493- }
493+ return node . declaration ;
494+ } else {
495+ node . specifiers . forEach ( specifier => {
496+ const variable = this . var_lookup . get ( specifier . local . name ) ;
494497
495- content . body [ i ] = node . declaration ;
496- } else {
497- node . specifiers . forEach ( specifier => {
498- const variable = this . var_lookup . get ( specifier . local . name ) ;
498+ if ( variable ) {
499+ variable . export_name = specifier . exported . name ;
499500
500- if ( variable ) {
501- variable . export_name = specifier . exported . name ;
501+ if ( variable . writable && ! ( variable . referenced || variable . referenced_from_script ) ) {
502+ this . warn ( specifier , {
503+ code : `unused-export-let` ,
504+ 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 } '\``
505+ } ) ;
502506 }
503- } ) ;
507+ }
508+ } ) ;
504509
505- content . body . splice ( i , 1 ) ;
506- }
510+ return null ;
507511 }
508512 }
509513 }
@@ -522,7 +526,7 @@ export default class Component {
522526 } ) ;
523527 }
524528
525- walk_module_js ( ) {
529+ walk_module_js_pre_template ( ) {
526530 const component = this ;
527531 const script = this . ast . module ;
528532 if ( ! script ) return ;
@@ -573,9 +577,6 @@ export default class Component {
573577 } ) ;
574578 }
575579 } ) ;
576-
577- this . extract_imports ( script . content ) ;
578- this . extract_exports ( script . content ) ;
579580 }
580581
581582 walk_instance_js_pre_template ( ) {
@@ -657,7 +658,10 @@ export default class Component {
657658 this . add_reference ( name . slice ( 1 ) ) ;
658659
659660 const variable = this . var_lookup . get ( name . slice ( 1 ) ) ;
660- if ( variable ) variable . subscribable = true ;
661+ if ( variable ) {
662+ variable . subscribable = true ;
663+ variable . referenced_from_script = true ;
664+ }
661665 } else {
662666 this . add_var ( {
663667 name,
@@ -667,46 +671,83 @@ export default class Component {
667671 }
668672 } ) ;
669673
670- this . extract_imports ( script . content ) ;
671- this . extract_exports ( script . content ) ;
672- this . track_mutations ( ) ;
674+ this . track_references_and_mutations ( ) ;
675+ }
676+
677+ walk_module_js_post_template ( ) {
678+ const script = this . ast . module ;
679+ if ( ! script ) return ;
680+
681+ const { body } = script . content ;
682+ let i = body . length ;
683+ while ( -- i >= 0 ) {
684+ const node = body [ i ] ;
685+ if ( node . type === 'ImportDeclaration' ) {
686+ this . extract_imports ( node ) ;
687+ body . splice ( i , 1 ) ;
688+ }
689+
690+ if ( / ^ E x p o r t / . test ( node . type ) ) {
691+ const replacement = this . extract_exports ( node ) ;
692+ if ( replacement ) {
693+ body [ i ] = replacement ;
694+ } else {
695+ body . splice ( i , 1 ) ;
696+ }
697+ }
698+ }
673699 }
674700
675701 walk_instance_js_post_template ( ) {
676702 const script = this . ast . instance ;
677703 if ( ! script ) return ;
678704
679- this . warn_on_undefined_store_value_references ( ) ;
705+ this . post_template_walk ( ) ;
706+
680707 this . hoist_instance_declarations ( ) ;
681708 this . extract_reactive_declarations ( ) ;
682709 }
683710
684- // TODO merge this with other walks that are independent
685- track_mutations ( ) {
711+ post_template_walk ( ) {
712+ const script = this . ast . instance ;
713+ if ( ! script ) return ;
714+
686715 const component = this ;
716+ const { content } = script ;
687717 const { instance_scope, instance_scope_map : map } = this ;
688718
689719 let scope = instance_scope ;
690720
691- walk ( this . ast . instance . content , {
692- enter ( node ) {
721+ const toRemove = [ ] ;
722+ const remove = ( parent , prop , index ) => {
723+ toRemove . unshift ( [ parent , prop , index ] ) ;
724+ } ;
725+
726+ walk ( content , {
727+ enter ( node , parent , prop , index ) {
693728 if ( map . has ( node ) ) {
694729 scope = map . get ( node ) ;
695730 }
696731
697- if ( node . type === 'AssignmentExpression' || node . type === 'UpdateExpression' ) {
698- const assignee = node . type === 'AssignmentExpression' ? node . left : node . argument ;
699- const names = extract_names ( assignee ) ;
700-
701- const deep = assignee . type === 'MemberExpression' ;
732+ if ( node . type === 'ImportDeclaration' ) {
733+ component . extract_imports ( node ) ;
734+ // TODO: to use actual remove
735+ remove ( parent , prop , index ) ;
736+ return this . skip ( ) ;
737+ }
702738
703- names . forEach ( name => {
704- if ( scope . find_owner ( name ) === instance_scope ) {
705- const variable = component . var_lookup . get ( name ) ;
706- variable [ deep ? 'mutated' : 'reassigned' ] = true ;
707- }
708- } ) ;
739+ if ( / ^ E x p o r t / . test ( node . type ) ) {
740+ const replacement = component . extract_exports ( node ) ;
741+ if ( replacement ) {
742+ this . replace ( replacement ) ;
743+ } else {
744+ // TODO: to use actual remove
745+ remove ( parent , prop , index ) ;
746+ }
747+ return this . skip ( ) ;
709748 }
749+
750+ component . warn_on_undefined_store_value_references ( node , parent , scope ) ;
710751 } ,
711752
712753 leave ( node ) {
@@ -715,37 +756,53 @@ export default class Component {
715756 }
716757 } ,
717758 } ) ;
759+
760+ for ( const [ parent , prop , index ] of toRemove ) {
761+ if ( parent ) {
762+ if ( index !== null ) {
763+ parent [ prop ] . splice ( index , 1 ) ;
764+ } else {
765+ delete parent [ prop ] ;
766+ }
767+ }
768+ }
718769 }
719770
720- warn_on_undefined_store_value_references ( ) {
721- // TODO this pattern happens a lot... can we abstract it
722- // (or better still, do fewer AST walks)?
771+ track_references_and_mutations ( ) {
772+ const script = this . ast . instance ;
773+ if ( ! script ) return ;
774+
723775 const component = this ;
724- let { instance_scope : scope , instance_scope_map : map } = this ;
776+ const { content } = script ;
777+ const { instance_scope, instance_scope_map : map } = this ;
725778
726- walk ( this . ast . instance . content , {
779+ let scope = instance_scope ;
780+
781+ walk ( content , {
727782 enter ( node , parent ) {
728783 if ( map . has ( node ) ) {
729784 scope = map . get ( node ) ;
730785 }
731786
732- if (
733- node . type === 'LabeledStatement' &&
734- node . label . name === '$' &&
735- parent . type !== 'Program'
736- ) {
737- component . warn ( node as any , {
738- code : 'non-top-level-reactive-declaration' ,
739- message : '$: has no effect outside of the top-level' ,
787+ if ( node . type === 'AssignmentExpression' || node . type === 'UpdateExpression' ) {
788+ const assignee = node . type === 'AssignmentExpression' ? node . left : node . argument ;
789+ const names = extract_names ( assignee ) ;
790+
791+ const deep = assignee . type === 'MemberExpression' ;
792+
793+ names . forEach ( name => {
794+ if ( scope . find_owner ( name ) === instance_scope ) {
795+ const variable = component . var_lookup . get ( name ) ;
796+ variable [ deep ? 'mutated' : 'reassigned' ] = true ;
797+ }
740798 } ) ;
741799 }
742800
743- if ( is_reference ( node as Node , parent as Node ) ) {
801+ if ( is_used_as_reference ( node , parent ) ) {
744802 const object = get_object ( node ) ;
745- const { name } = object ;
746-
747- if ( name [ 0 ] === '$' && ! scope . has ( name ) ) {
748- component . warn_if_undefined ( name , object , null ) ;
803+ if ( scope . find_owner ( object . name ) === instance_scope ) {
804+ const variable = component . var_lookup . get ( object . name ) ;
805+ variable . referenced_from_script = true ;
749806 }
750807 }
751808 } ,
@@ -758,6 +815,28 @@ export default class Component {
758815 } ) ;
759816 }
760817
818+ warn_on_undefined_store_value_references ( node , parent , scope ) {
819+ if (
820+ node . type === 'LabeledStatement' &&
821+ node . label . name === '$' &&
822+ parent . type !== 'Program'
823+ ) {
824+ this . warn ( node as any , {
825+ code : 'non-top-level-reactive-declaration' ,
826+ message : '$: has no effect outside of the top-level' ,
827+ } ) ;
828+ }
829+
830+ if ( is_reference ( node as Node , parent as Node ) ) {
831+ const object = get_object ( node ) ;
832+ const { name } = object ;
833+
834+ if ( name [ 0 ] === '$' && ! scope . has ( name ) ) {
835+ this . warn_if_undefined ( name , object , null ) ;
836+ }
837+ }
838+ }
839+
761840 invalidate ( name , value ?) {
762841 const variable = this . var_lookup . get ( name ) ;
763842
0 commit comments