diff --git a/src/compile/Component.ts b/src/compile/Component.ts index 6dcf84d4dd..e0db009036 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -11,7 +11,7 @@ import Stylesheet from './css/Stylesheet'; import { test } from '../config'; import Fragment from './nodes/Fragment'; import internal_exports from './internal-exports'; -import { Node, Ast, CompileOptions } from '../interfaces'; +import { Node, Ast, CompileOptions, Var } from '../interfaces'; import error from '../utils/error'; import getCodeFrame from '../utils/getCodeFrame'; import flattenReference from '../utils/flattenReference'; @@ -56,6 +56,9 @@ export default class Component { namespace: string; tag: string; + vars: Var[] = []; + var_lookup: Map = new Map(); + imports: Node[] = []; module_javascript: string; javascript: string; @@ -90,7 +93,6 @@ export default class Component { stylesheet: Stylesheet; - userVars: Set = new Set(); aliases: Map = new Map(); usedNames: Set = new Set(); @@ -152,18 +154,57 @@ export default class Component { this.declarations.push(...props); addToSet(this.mutable_props, this.template_references); addToSet(this.writable_declarations, this.template_references); - addToSet(this.userVars, this.template_references); - this.props = props.map(name => ({ - name, - as: name - })); + props.forEach(name => { + this.add_var({ + name, + kind: 'injected', + import_type: null, + imported_as: null, + exported_as: name, + source: null, + mutated: false, + referenced: true, + module: false + }); + }); + + // TODO remove this + this.props = props.map(name => ({ name, as: name })); } // tell the root fragment scope about all of the mutable names we know from the script this.mutable_props.forEach(name => this.fragment.scope.mutables.add(name)); } + 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 (!this.ast.instance) { + this.add_var({ + name, + kind: 'injected', + import_type: null, + imported_as: null, + source: null, + exported_as: null, + module: false, + mutated: true, + referenced: true + }); + } + + // TODO remove this + this.template_references.add(name); + } + addSourcemapLocations(node: Node) { walk(node, { enter: (node: Node) => { @@ -290,7 +331,7 @@ export default class Component { for ( let i = 1; reservedNames.has(alias) || - this.userVars.has(alias) || + this.var_lookup.has(alias) || this.usedNames.has(alias); alias = `${name}_${i++}` ); @@ -306,7 +347,7 @@ export default class Component { } reservedNames.forEach(add); - this.userVars.forEach(add); + this.var_lookup.forEach((value, key) => add(key)); return (name: string) => { if (test) name = `${name}$`; @@ -373,7 +414,54 @@ export default class Component { }); } - extract_imports_and_exports(content, imports, exports) { + extract_imports(content, is_module: boolean) { + const { code } = this; + + content.body.forEach(node => { + if (node.type === 'ImportDeclaration') { + // imports need to be hoisted out of the IIFE + removeNode(code, content.start, content.end, content.body, node); + this.imports.push(node); + + node.specifiers.forEach((specifier: Node) => { + if (specifier.local.name[0] === '$') { + this.error(specifier.local, { + code: 'illegal-declaration', + message: `The $ prefix is reserved, and cannot be used for variable and import names` + }); + } + + const imported_as = specifier.imported + ? specifier.imported.name + : specifier.type === 'ImportDefaultSpecifier' + ? 'default' + : '*'; + + const import_type = specifier.type === 'ImportSpecifier' + ? 'named' + : specifier.type === 'ImportDefaultSpecifier' + ? 'default' + : 'namespace'; + + this.add_var({ + name: specifier.local.name, + kind: 'import', + imported_as, + import_type, + source: node.source.value, + exported_as: null, + module: is_module, + mutated: false, + referenced: false + }); + + this.imported_declarations.add(specifier.local.name); + }); + } + }); + } + + extract_exports(content, is_module: boolean) { const { code } = this; content.body.forEach(node => { @@ -389,44 +477,60 @@ export default class Component { if (node.declaration.type === 'VariableDeclaration') { node.declaration.declarations.forEach(declarator => { extractNames(declarator.id).forEach(name => { - exports.push({ name, as: name }); - this.mutable_props.add(name); + this.add_var({ + name, + kind: node.declaration.kind, + import_type: null, + imported_as: null, + exported_as: name, + source: null, + module: is_module, + mutated: false, + referenced: false + }); + + if (!is_module) this.mutable_props.add(name); }); }); } else { const { name } = node.declaration.id; - exports.push({ name, as: name }); + + const kind = node.declaration.type === 'ClassDeclaration' + ? 'class' + : node.declaration.type === 'FunctionDeclaration' + ? 'function' + : null; + + // sanity check + if (!kind) throw new Error(`Unknown declaration type ${node.declaration.type}`); + + this.add_var({ + name, + kind, + import_type: null, + imported_as: null, + exported_as: name, + source: null, + module: is_module, + mutated: false, + referenced: false + }); } code.remove(node.start, node.declaration.start); } else { removeNode(code, content.start, content.end, content.body, node); node.specifiers.forEach(specifier => { - exports.push({ - name: specifier.local.name, - as: specifier.exported.name - }); + const variable = this.var_lookup.get(specifier.local.name); + + if (variable) { + variable.exported_as = specifier.exported.name; + } else { + // TODO what happens with `export { Math }` or some other global? + } }); } } - - // imports need to be hoisted out of the IIFE - else if (node.type === 'ImportDeclaration') { - removeNode(code, content.start, content.end, content.body, node); - imports.push(node); - - node.specifiers.forEach((specifier: Node) => { - if (specifier.local.name[0] === '$') { - this.error(specifier.local, { - code: 'illegal-declaration', - message: `The $ prefix is reserved, and cannot be used for variable and import names` - }); - } - - this.userVars.add(specifier.local.name); - this.imported_declarations.add(specifier.local.name); - }); - } }); } @@ -485,9 +589,16 @@ export default class Component { } }); - this.extract_imports_and_exports(script.content, this.imports, this.module_exports); + this.extract_imports(script.content, true); + this.extract_exports(script.content, true); remove_indentation(this.code, script.content); this.module_javascript = this.extract_javascript(script); + + // TODO remove this + this.module_exports = this.vars.filter(variable => variable.module && variable.exported_as).map(variable => ({ + name: variable.name, + as: variable.exported_as + })); } walk_instance_js_pre_template() { @@ -507,11 +618,33 @@ export default class Component { message: `The $ prefix is reserved, and cannot be used for variable and import names` }); } - }); - instance_scope.declarations.forEach((node, name) => { - this.userVars.add(name); - this.declarations.push(name); + if (!/Import/.test(node.type)) { + const kind = node.type === 'VariableDeclaration' + ? node.kind + : node.type === 'ClassDeclaration' + ? 'class' + : node.type === 'FunctionDeclaration' + ? 'function' + : null; + + // sanity check + if (!kind) throw new Error(`Unknown declaration type ${node.type}`); + + this.add_var({ + name, + kind, + import_type: null, + imported_as: null, + exported_as: null, + source: null, + module: false, + mutated: false, + referenced: false + }); + + this.declarations.push(name); + } this.node_for_declaration.set(name, node); }); @@ -520,11 +653,28 @@ export default class Component { this.initialised_declarations = instance_scope.initialised_declarations; globals.forEach(name => { - this.userVars.add(name); + this.add_var({ + name, + kind: 'global', + import_type: null, + imported_as: null, + source: null, + exported_as: null, + module: false, + mutated: false, + referenced: false + }); }); this.track_mutations(); - this.extract_imports_and_exports(script.content, this.imports, this.props); + this.extract_imports(script.content, false); + this.extract_exports(script.content, false); + + // TODO remove this, just use component.symbols everywhere + this.props = this.vars.filter(variable => !variable.module && variable.exported_as).map(variable => ({ + name: variable.name, + as: variable.exported_as + })); } walk_instance_js_post_template() { @@ -586,7 +736,7 @@ export default class Component { component.warn_if_undefined(object, null); // cheeky hack - component.template_references.add(name); + component.add_reference(name); } } }, @@ -963,7 +1113,7 @@ export default class Component { if (this.imported_declarations.has(name)) return name; if (this.declarations.indexOf(name) === -1) return name; - this.template_references.add(name); // TODO we can probably remove most other occurrences of this + this.add_reference(name); // TODO we can probably remove most other occurrences of this return `ctx.${name}`; } diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts index fe29c6d1fd..fbf740f0f6 100644 --- a/src/compile/nodes/EventHandler.ts +++ b/src/compile/nodes/EventHandler.ts @@ -57,7 +57,7 @@ export default class EventHandler extends Node { render(block: Block) { if (this.expression) return this.expression.render(block); - this.component.template_references.add(this.handler_name); + this.component.add_reference(this.handler_name); return `ctx.${this.handler_name}`; } } \ No newline at end of file diff --git a/src/compile/nodes/InlineComponent.ts b/src/compile/nodes/InlineComponent.ts index 4d3b7332a6..28c7b2594e 100644 --- a/src/compile/nodes/InlineComponent.ts +++ b/src/compile/nodes/InlineComponent.ts @@ -24,7 +24,7 @@ export default class InlineComponent extends Node { if (info.name !== 'svelte:component' && info.name !== 'svelte:self') { component.warn_if_undefined(info, scope); - component.template_references.add(info.name); + component.add_reference(info.name); } this.name = info.name; diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts index ab0d3f36dd..b01a309e04 100644 --- a/src/compile/nodes/shared/Expression.ts +++ b/src/compile/nodes/shared/Expression.ts @@ -116,7 +116,7 @@ export default class Expression { const owner = template_scope.getOwner(name); const is_let = owner && (owner.type === 'InlineComponent' || owner.type === 'Element'); - if (is_let || component.writable_declarations.has(name) || name[0] === '$' || (component.userVars.has(name) && deep)) { + if (is_let || component.writable_declarations.has(name) || name[0] === '$' || (component.var_lookup.has(name) && deep)) { dynamic_dependencies.add(name); } } else { @@ -153,7 +153,7 @@ export default class Expression { template_scope.dependenciesForName.get(name).forEach(name => add_dependency(name, true)); } else { add_dependency(name, nodes.length > 1); - component.template_references.add(name); + component.add_reference(name); component.warn_if_undefined(nodes[0], template_scope, true); } @@ -241,7 +241,7 @@ export default class Expression { }); } else { dependencies.add(name); - component.template_references.add(name); + component.add_reference(name); } } else if (!is_synthetic && isContextual(component, template_scope, name)) { code.prependRight(node.start, key === 'key' && parent.shorthand @@ -365,7 +365,7 @@ export default class Expression { // function can be hoisted inside the component init component.partly_hoisted.push(fn); component.declarations.push(name); - component.template_references.add(name); + component.add_reference(name); code.overwrite(node.start, node.end, `ctx.${name}`); } @@ -373,7 +373,7 @@ export default class Expression { // we need a combo block/init recipe component.partly_hoisted.push(fn); component.declarations.push(name); - component.template_references.add(name); + component.add_reference(name); code.overwrite(node.start, node.end, name); declarations.push(deindent` diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 03ce9eed9f..cf7286d58d 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -405,7 +405,7 @@ export default class ElementWrapper extends Wrapper { groups.forEach(group => { const handler = renderer.component.getUniqueName(`${this.var}_${group.events.join('_')}_handler`); renderer.component.declarations.push(handler); - renderer.component.template_references.add(handler); + renderer.component.add_reference(handler); // TODO figure out how to handle locks const needsLock = group.bindings.some(binding => binding.needsLock); @@ -506,7 +506,7 @@ export default class ElementWrapper extends Wrapper { if (this_binding) { const name = renderer.component.getUniqueName(`${this.var}_binding`); renderer.component.declarations.push(name); - renderer.component.template_references.add(name); + renderer.component.add_reference(name); const { handler, object } = this_binding; diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts index 75bff825c6..83f41845dc 100644 --- a/src/compile/render-dom/wrappers/InlineComponent/index.ts +++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts @@ -234,7 +234,7 @@ export default class InlineComponentWrapper extends Wrapper { if (binding.name === 'this') { const fn = component.getUniqueName(`${this.var}_binding`); component.declarations.push(fn); - component.template_references.add(fn); + component.add_reference(fn); let lhs; let object; @@ -265,7 +265,7 @@ export default class InlineComponentWrapper extends Wrapper { const name = component.getUniqueName(`${this.var}_${binding.name}_binding`); component.declarations.push(name); - component.template_references.add(name); + component.add_reference(name); const updating = block.getUniqueName(`updating_${binding.name}`); block.addVariable(updating); diff --git a/src/compile/render-dom/wrappers/Window.ts b/src/compile/render-dom/wrappers/Window.ts index c986d3726f..f704c35446 100644 --- a/src/compile/render-dom/wrappers/Window.ts +++ b/src/compile/render-dom/wrappers/Window.ts @@ -119,7 +119,7 @@ export default class WindowWrapper extends Wrapper { } component.declarations.push(handler_name); - component.template_references.add(handler_name); + component.add_reference(handler_name); component.partly_hoisted.push(deindent` function ${handler_name}() { ${props.map(prop => `${prop.name} = window.${prop.value}; $$invalidate('${prop.name}', ${prop.name});`)} diff --git a/src/compile/render-dom/wrappers/shared/addActions.ts b/src/compile/render-dom/wrappers/shared/addActions.ts index 48c2287f87..0dc037bb39 100644 --- a/src/compile/render-dom/wrappers/shared/addActions.ts +++ b/src/compile/render-dom/wrappers/shared/addActions.ts @@ -27,7 +27,7 @@ export default function addActions( ? action.name : `ctx.${action.name}`; - component.template_references.add(action.name); + component.add_reference(action.name); block.builders.mount.addLine( `${name} = ${fn}.call(null, ${target}${snippet ? `, ${snippet}` : ''}) || {};` diff --git a/src/interfaces.ts b/src/interfaces.ts index 13c296f2a0..cc4bd33a6d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -77,4 +77,16 @@ export interface CustomElementOptions { export interface AppendTarget { slots: Record; slotStack: string[] +} + +export interface Var { + name: string; + kind: 'let' | 'var' | 'const' | 'class' | 'function' | 'import' | 'injected' | 'global'; + import_type: 'default' | 'named' | 'namespace'; + imported_as: string; // the `foo` in `import { foo as bar }` + exported_as: string; // the `bar` in `export { foo as bar }` + source: string; + module: boolean; + mutated: boolean; + referenced: boolean; } \ No newline at end of file diff --git a/test/stats/samples/imports/_config.js b/test/stats/samples/imports/_config.js index f7c15183dd..60a8d3f971 100644 --- a/test/stats/samples/imports/_config.js +++ b/test/stats/samples/imports/_config.js @@ -1,17 +1,23 @@ export default { test(assert, stats) { - assert.deepEqual(stats.imports, [ + assert.deepEqual(stats.vars, [ { - source: 'x', - specifiers: [{ name: 'default', as: 'x' }] + kind: 'import', + imported: 'x', + default: true, + source: 'x' }, { - source: 'y', - specifiers: [{ name: 'y', as: 'y' }] + kind: 'import', + imported: 'y', + named: true, + source: 'y' }, { - source: 'z', - specifiers: [{ name: '*', as: 'z' }] + kind: 'import', + imported: 'y', + namespace: true, + source: 'y' } ]); } diff --git a/test/stats/samples/props/_config.js b/test/stats/samples/props/_config.js index 50c01e2ffb..47084d34dd 100644 --- a/test/stats/samples/props/_config.js +++ b/test/stats/samples/props/_config.js @@ -1,5 +1,28 @@ export default { test(assert, stats) { - assert.deepEqual(stats.props.sort(), ['cats', 'name']); + assert.deepEqual(stats.vars, [ + { + name: 'name', + kind: 'let', + exported: 'name', + referenced: true + }, + { + name: 'cats', + kind: 'let', + exported: 'name', + referenced: true + }, + { + name: 'foo', + kind: 'let', + referenced: true + }, + { + name: 'bar', + kind: 'let', + referenced: true + } + ]); } }; \ No newline at end of file diff --git a/test/stats/samples/template-references/_config.js b/test/stats/samples/template-references/_config.js index 0bc844928f..51f83880bf 100644 --- a/test/stats/samples/template-references/_config.js +++ b/test/stats/samples/template-references/_config.js @@ -1,8 +1,24 @@ export default { test(assert, stats) { - assert.equal(stats.templateReferences.size, 3); - assert.ok(stats.templateReferences.has('foo')); - assert.ok(stats.templateReferences.has('Bar')); - assert.ok(stats.templateReferences.has('baz')); + assert.deepEqual(stats.vars, [ + { + name: 'foo', + kind: 'injected', + exported: 'foo', + referenced: true + }, + { + name: 'Bar', + kind: 'injected', + exported: 'Bar', + referenced: true + }, + { + name: 'baz', + kind: 'injected', + exported: 'baz', + referenced: true + } + ]); }, };