diff --git a/src/generators/nodes/Action.ts b/src/generators/nodes/Action.ts index fc1958e88d..1e8f4311e7 100644 --- a/src/generators/nodes/Action.ts +++ b/src/generators/nodes/Action.ts @@ -6,13 +6,13 @@ export default class Action extends Node { name: string; expression: Expression; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.name = info.name; this.expression = info.expression - ? new Expression(compiler, this, info.expression) + ? new Expression(compiler, this, scope, info.expression) : null; } } \ No newline at end of file diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index b5bdf5de5e..f774713868 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -28,8 +28,8 @@ export default class Attribute extends Node { dependencies: Set; expression: Node; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.name = info.name; this.isTrue = info.value === true; @@ -41,7 +41,7 @@ export default class Attribute extends Node { : info.value.map(node => { if (node.type === 'Text') return node; - const expression = new Expression(compiler, this, node.expression); + const expression = new Expression(compiler, this, scope, node.expression); addToSet(this.dependencies, expression.dependencies); return expression; diff --git a/src/generators/nodes/Binding.ts b/src/generators/nodes/Binding.ts index 8d2d8f7950..6b1b74bd93 100644 --- a/src/generators/nodes/Binding.ts +++ b/src/generators/nodes/Binding.ts @@ -20,11 +20,11 @@ export default class Binding extends Node { obj: string; prop: string; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.name = info.name; - this.value = new Expression(compiler, this, info.value); + this.value = new Expression(compiler, this, scope, info.value); // const contextual = block.contexts.has(name); const contextual = false; // TODO diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index 1c67672d45..1f9e65a73e 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -24,8 +24,8 @@ export default class Component extends Node { children: Node[]; ref: string; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); compiler.hasComponents = true; @@ -39,15 +39,15 @@ export default class Component extends Node { switch (node.type) { case 'Attribute': // TODO spread - this.attributes.push(new Attribute(compiler, this, node)); + this.attributes.push(new Attribute(compiler, this, scope, node)); break; case 'Binding': - this.bindings.push(new Binding(compiler, this, node)); + this.bindings.push(new Binding(compiler, this, scope, node)); break; case 'EventHandler': - this.handlers.push(new EventHandler(compiler, this, node)); + this.handlers.push(new EventHandler(compiler, this, scope, node)); break; case 'Ref': @@ -63,7 +63,7 @@ export default class Component extends Node { } }); - this.children = mapChildren(compiler, this, info.children); + this.children = mapChildren(compiler, this, scope, info.children); } init( @@ -150,7 +150,7 @@ export default class Component extends Node { ? '{}' : stringifyProps( // this.attributes.map(attr => `${attr.name}: ${attr.value}`) - this.attributes.map(attr => `${attr.name}: "TODO"`) + this.attributes.map(attr => `${attr.name}: ${attr.getValue()}`) ); if (this.attributes.length || this.bindings.length) { diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index 3d7ca27469..a5fc2ce48b 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -21,14 +21,19 @@ export default class EachBlock extends Node { children: Node[]; else?: ElseBlock; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); - this.expression = new Expression(compiler, this, info.expression); + this.expression = new Expression(compiler, this, scope, info.expression); this.context = info.context; this.key = info.key; - this.children = mapChildren(compiler, this, info.children); + this.scope = scope.child(); + + // TODO handle indexes and destructuring + this.scope.add(this.context, this.expression.dependencies); + + this.children = mapChildren(compiler, this, this.scope, info.children); } init( diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 4c1e766c7b..19c538d486 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -33,8 +33,8 @@ export default class Element extends Node { ref: string; namespace: string; - constructor(compiler, parent, info: any) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info: any) { + super(compiler, parent, scope, info); this.name = info.name; const parentElement = parent.findNearest(/^Element/); @@ -53,26 +53,26 @@ export default class Element extends Node { info.attributes.forEach(node => { switch (node.type) { case 'Action': - this.actions.push(new Action(compiler, this, node)); + this.actions.push(new Action(compiler, this, scope, node)); break; case 'Attribute': // special case if (node.name === 'xmlns') this.namespace = node.value[0].data; - this.attributes.push(new Attribute(compiler, this, node)); + this.attributes.push(new Attribute(compiler, this, scope, node)); break; case 'Binding': - this.bindings.push(new Binding(compiler, this, node)); + this.bindings.push(new Binding(compiler, this, scope, node)); break; case 'EventHandler': - this.handlers.push(new EventHandler(compiler, this, node)); + this.handlers.push(new EventHandler(compiler, this, scope, node)); break; case 'Transition': - const transition = new Transition(compiler, this, node); + const transition = new Transition(compiler, this, scope, node); if (node.intro) this.intro = transition; if (node.outro) this.outro = transition; break; @@ -92,7 +92,7 @@ export default class Element extends Node { // TODO break out attributes and directives here - this.children = mapChildren(compiler, this, info.children); + this.children = mapChildren(compiler, this, scope, info.children); } init( diff --git a/src/generators/nodes/ElseBlock.ts b/src/generators/nodes/ElseBlock.ts index 917e3aa026..742fbc7f91 100644 --- a/src/generators/nodes/ElseBlock.ts +++ b/src/generators/nodes/ElseBlock.ts @@ -6,8 +6,8 @@ export default class ElseBlock extends Node { type: 'ElseBlock'; children: Node[]; - constructor(compiler, parent, info) { - super(compiler, parent, info); - this.children = mapChildren(compiler, this, info.children); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.children = mapChildren(compiler, this, scope, info.children); } } \ No newline at end of file diff --git a/src/generators/nodes/EventHandler.ts b/src/generators/nodes/EventHandler.ts index 34bc200b3d..9cd97de382 100644 --- a/src/generators/nodes/EventHandler.ts +++ b/src/generators/nodes/EventHandler.ts @@ -13,8 +13,8 @@ export default class EventHandler extends Node { args: Expression[]; snippet: string; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.name = info.name; this.dependencies = new Set(); @@ -23,7 +23,7 @@ export default class EventHandler extends Node { this.callee = flattenReference(info.expression.callee); this.insertionPoint = info.expression.start; this.args = info.expression.arguments.map(param => { - const expression = new Expression(compiler, this, param); + const expression = new Expression(compiler, this, scope, param); addToSet(this.dependencies, expression.dependencies); return expression; }); diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts index 214c54b43d..a7ba6a094f 100644 --- a/src/generators/nodes/Fragment.ts +++ b/src/generators/nodes/Fragment.ts @@ -4,13 +4,39 @@ import Generator from '../Generator'; import mapChildren from './shared/mapChildren'; import Block from '../dom/Block'; +class TemplateScope { + names: Set; + indexes: Set; + dependenciesForName: Map; + + constructor(parent?: TemplateScope) { + this.names = new Set(parent ? parent.names : []); + this.indexes = new Set(parent ? parent.names : []); + + this.dependenciesForName = new Map(parent ? parent.dependenciesForName : []); + } + + add(name, dependencies) { + this.names.add(name); + this.dependenciesForName.set(name, dependencies); + } + + child() { + return new TemplateScope(this); + } +} + export default class Fragment extends Node { block: Block; children: Node[]; + scope: TemplateScope; constructor(compiler: Generator, info: any) { - super(compiler, null, info); - this.children = mapChildren(compiler, this, info.children); + const scope = new TemplateScope(); + super(compiler, null, scope, info); + + this.scope = scope; + this.children = mapChildren(compiler, this, scope, info.children); } init() { diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts index 2167ea8baf..55dd64bb40 100644 --- a/src/generators/nodes/IfBlock.ts +++ b/src/generators/nodes/IfBlock.ts @@ -25,14 +25,14 @@ export default class IfBlock extends Node { block: Block; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); - this.expression = new Expression(compiler, this, info.expression); - this.children = mapChildren(compiler, this, info.children); + this.expression = new Expression(compiler, this, scope, info.expression); + this.children = mapChildren(compiler, this, scope, info.children); this.else = info.else - ? new ElseBlock(compiler, this, info.else) + ? new ElseBlock(compiler, this, scope, info.else) : null; } diff --git a/src/generators/nodes/Slot.ts b/src/generators/nodes/Slot.ts index 86b1625e69..896a65959c 100644 --- a/src/generators/nodes/Slot.ts +++ b/src/generators/nodes/Slot.ts @@ -31,10 +31,10 @@ export default class Slot extends Element { parentNode: string, parentNodes: string ) { - const { generator } = this; + const { compiler } = this; const slotName = this.getStaticAttributeValue('name') || 'default'; - generator.slots.add(slotName); + compiler.slots.add(slotName); const content_name = block.getUniqueName(`slot_content_${slotName}`); const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`; diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts index d8c468224d..a7ad47d8f9 100644 --- a/src/generators/nodes/Text.ts +++ b/src/generators/nodes/Text.ts @@ -33,8 +33,8 @@ export default class Text extends Node { data: string; shouldSkip: boolean; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.data = info.data; } diff --git a/src/generators/nodes/Transition.ts b/src/generators/nodes/Transition.ts index 9ed9f87dab..00a6b8739d 100644 --- a/src/generators/nodes/Transition.ts +++ b/src/generators/nodes/Transition.ts @@ -6,13 +6,13 @@ export default class Transition extends Node { name: string; expression: Expression; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.name = info.name; this.expression = info.expression - ? new Expression(compiler, this, info.expression) + ? new Expression(compiler, this, scope, info.expression) : null; } } \ No newline at end of file diff --git a/src/generators/nodes/Window.ts b/src/generators/nodes/Window.ts index 564ad1f47d..447d3405e7 100644 --- a/src/generators/nodes/Window.ts +++ b/src/generators/nodes/Window.ts @@ -38,17 +38,17 @@ export default class Window extends Node { handlers: EventHandler[]; bindings: Binding[]; - constructor(compiler, parent, info) { - super(compiler, parent, info); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); this.handlers = []; this.bindings = []; info.attributes.forEach(node => { if (node.type === 'EventHandler') { - this.handlers.push(new EventHandler(compiler, this, node)); + this.handlers.push(new EventHandler(compiler, this, scope, node)); } else if (node.type === 'Binding') { - this.bindings.push(new Binding(compiler, this, node)); + this.bindings.push(new Binding(compiler, this, scope, node)); } }); } diff --git a/src/generators/nodes/shared/Expression.ts b/src/generators/nodes/shared/Expression.ts index 9358059b71..b624a53cc7 100644 --- a/src/generators/nodes/shared/Expression.ts +++ b/src/generators/nodes/shared/Expression.ts @@ -14,7 +14,7 @@ export default class Expression { contexts: Set; indexes: Set; - constructor(compiler, parent, info) { + constructor(compiler, parent, scope, info) { this.compiler = compiler; this.node = info; @@ -27,7 +27,7 @@ export default class Expression { const { code, helpers } = compiler; - let { map, scope } = createScopes(info); + let { map, scope: currentScope } = createScopes(info); const isEventHandler = parent.type === 'EventHandler'; walk(info, { @@ -36,22 +36,23 @@ export default class Expression { code.addSourcemapLocation(node.end); if (map.has(node)) { - scope = map.get(node); + currentScope = map.get(node); return; } if (isReference(node, parent)) { const { name } = flattenReference(node); - if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return; + if (currentScope && currentScope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return; code.prependRight(node.start, 'ctx.'); - if (contextDependencies.has(name)) { - contextDependencies.get(name).forEach(dependency => { + if (scope.names.has(name)) { + scope.dependenciesForName.get(name).forEach(dependency => { dependencies.add(dependency); }); } else if (!indexes.has(name)) { dependencies.add(name); + compiler.expectedProperties.add(name); } this.skip(); @@ -59,7 +60,7 @@ export default class Expression { }, leave(node: Node, parent: Node) { - if (map.has(node)) scope = scope.parent; + if (map.has(node)) currentScope = currentScope.parent; } }); diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts index a07b4fdd8b..e0afb53409 100644 --- a/src/generators/nodes/shared/Node.ts +++ b/src/generators/nodes/shared/Node.ts @@ -16,11 +16,11 @@ export default class Node { canUseInnerHTML: boolean; var: string; - constructor(compiler: Generator, parent, info: any) { - this.start = info.start; - this.end = info.end; + constructor(compiler: Generator, parent, scope, info: any) { this.compiler = compiler; this.parent = parent; + this.start = info.start; + this.end = info.end; this.type = info.type; } diff --git a/src/generators/nodes/shared/Tag.ts b/src/generators/nodes/shared/Tag.ts index 267d93d28d..8416a607c0 100644 --- a/src/generators/nodes/shared/Tag.ts +++ b/src/generators/nodes/shared/Tag.ts @@ -5,9 +5,9 @@ import Block from '../../dom/Block'; export default class Tag extends Node { expression: Expression; - constructor(compiler, parent, info) { - super(compiler, parent, info); - this.expression = new Expression(compiler, this, info.expression); + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.expression = new Expression(compiler, this, scope, info.expression); } renameThisMethod( @@ -19,8 +19,8 @@ export default class Tag extends Node { const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); const shouldCache = ( - this.expression.type !== 'Identifier' || - block.contexts.has(this.expression.name) || + this.expression.node.type !== 'Identifier' || + block.contexts.has(this.expression.node.name) || hasChangeableIndex ); diff --git a/src/generators/nodes/shared/mapChildren.ts b/src/generators/nodes/shared/mapChildren.ts index 829a7ea962..d0df4be94b 100644 --- a/src/generators/nodes/shared/mapChildren.ts +++ b/src/generators/nodes/shared/mapChildren.ts @@ -2,6 +2,7 @@ import Component from '../Component'; import EachBlock from '../EachBlock'; import Element from '../Element'; import IfBlock from '../IfBlock'; +import Slot from '../Slot'; import Text from '../Text'; import MustacheTag from '../MustacheTag'; import Window from '../Window'; @@ -13,6 +14,7 @@ function getConstructor(type): typeof Node { case 'EachBlock': return EachBlock; case 'Element': return Element; case 'IfBlock': return IfBlock; + case 'Slot': return Slot; case 'Text': return Text; case 'MustacheTag': return MustacheTag; case 'Window': return Window; @@ -20,11 +22,11 @@ function getConstructor(type): typeof Node { } } -export default function mapChildren(compiler, parent, children: any[]) { +export default function mapChildren(compiler, parent, scope, children: any[]) { let last = null; return children.map(child => { const constructor = getConstructor(child.type); - const node = new constructor(compiler, parent, child); + const node = new constructor(compiler, parent, scope, child); if (last) last.next = node; node.prev = last; diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index e6a7707004..8fc8f3b098 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -50,21 +50,19 @@ export default function visitComponent( }); function getAttributeValue(attribute) { - if (attribute.value === true) return `true`; - if (attribute.value.length === 0) return `''`; + if (attribute.isTrue) return `true`; + if (attribute.chunks.length === 0) return `''`; - if (attribute.value.length === 1) { - const chunk = attribute.value[0]; + if (attribute.chunks.length === 1) { + const chunk = attribute.chunks[0]; if (chunk.type === 'Text') { return stringify(chunk.data); } - block.contextualise(chunk.expression); - const { snippet } = chunk.metadata; - return snippet; + return chunk.snippet; } - return '`' + attribute.value.map(stringifyAttribute).join('') + '`'; + return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`'; } const props = usesSpread @@ -72,8 +70,7 @@ export default function visitComponent( attributes .map(attribute => { if (attribute.type === 'Spread') { - block.contextualise(attribute.expression); - return attribute.metadata.snippet; + return attribute.expression.snippet; } else { return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`; } diff --git a/src/generators/server-side-rendering/visitors/EachBlock.ts b/src/generators/server-side-rendering/visitors/EachBlock.ts index eaf43f8bd3..79907e2920 100644 --- a/src/generators/server-side-rendering/visitors/EachBlock.ts +++ b/src/generators/server-side-rendering/visitors/EachBlock.ts @@ -8,8 +8,7 @@ export default function visitEachBlock( block: Block, node: Node ) { - block.contextualise(node.expression); - const { snippet } = node.metadata; + const { snippet } = node.expression; const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``; generator.append(open); diff --git a/src/generators/wrapModule.ts b/src/generators/wrapModule.ts index 8fd81d9308..d8fee62f0b 100644 --- a/src/generators/wrapModule.ts +++ b/src/generators/wrapModule.ts @@ -145,12 +145,12 @@ function cjs( helpers: { name: string, alias: string }[], dependencies: Dependency[] ) { - const SHARED = '__shared'; + const helperDeclarations = helpers && ( + helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', ') + ); + const helperBlock = helpers && ( - `var ${SHARED} = require(${JSON.stringify(sharedPath)});\n` + - helpers.map(helper => { - return `var ${helper.alias} = ${SHARED}.${helper.name};`; - }).join('\n') + `var { ${helperDeclarations} } = require(${JSON.stringify(sharedPath)});\n` ); const requireBlock = dependencies.length > 0 && ( diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 2eb8b3d068..c3f82be9d7 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -113,7 +113,8 @@ export default function tag(parser: Parser) { const type = metaTags.has(name) ? metaTags.get(name) - : /[A-Z]/.test(name[0]) ? 'Component' : 'Element'; + : /[A-Z]/.test(name[0]) ? 'Component' + : name === 'slot' ? 'Slot' : 'Element'; const element: Node = { start, diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 26aa8e8e5b..6698cc7c43 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,6 +1,7 @@ import validateElement from './validateElement'; import validateWindow from './validateWindow'; import validateHead from './validateHead'; +import validateSlot from './validateSlot'; import a11y from './a11y'; import fuzzymatch from '../utils/fuzzymatch' import flattenReference from '../../utils/flattenReference'; @@ -29,6 +30,10 @@ export default function validateHtml(validator: Validator, html: Node) { validateHead(validator, node, refs, refCallees); } + else if (node.type === 'Slot') { + validateSlot(validator, node); + } + else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') { validateElement( validator, diff --git a/src/validate/html/validateSlot.ts b/src/validate/html/validateSlot.ts new file mode 100644 index 0000000000..c39d663b25 --- /dev/null +++ b/src/validate/html/validateSlot.ts @@ -0,0 +1,56 @@ +import * as namespaces from '../../utils/namespaces'; +import validateEventHandler from './validateEventHandler'; +import validate, { Validator } from '../index'; +import { Node } from '../../interfaces'; + +export default function validateSlot( + validator: Validator, + node: Node +) { + node.attributes.forEach(attr => { + if (attr.type !== 'Attribute') { + validator.error(attr, { + code: `invalid-slot-directive`, + message: ` cannot have directives` + }); + } + + if (attr.name !== 'name') { + validator.error(attr, { + code: `invalid-slot-attribute`, + message: `"name" is the only attribute permitted on elements` + }); + } + + if (attr.value.length !== 1 || attr.value[0].type !== 'Text') { + validator.error(attr, { + code: `dynamic-slot-name`, + message: ` name cannot be dynamic` + }); + } + + const slotName = attr.value[0].data; + if (slotName === 'default') { + validator.error(attr, { + code: `invalid-slot-name`, + message: `default is a reserved word — it cannot be used as a slot name` + }); + } + + // TODO should duplicate slots be disallowed? Feels like it's more likely to be a + // bug than anything. Perhaps it should be a warning + + // if (validator.slots.has(slotName)) { + // validator.error(`duplicate '${slotName}' element`, nameAttribute.start); + // } + + // validator.slots.add(slotName); + }); + + // if (node.attributes.length === 0) && validator.slots.has('default')) { + // validator.error(node, { + // code: `duplicate-slot`, + // message: `duplicate default element` + // }); + // } +} \ No newline at end of file diff --git a/test/runtime/samples/svg-child-component-declared-namespace/_config.js b/test/runtime/samples/svg-child-component-declared-namespace/_config.js index 2944111fd9..37d94a1609 100644 --- a/test/runtime/samples/svg-child-component-declared-namespace/_config.js +++ b/test/runtime/samples/svg-child-component-declared-namespace/_config.js @@ -7,7 +7,7 @@ export default { }, html: ``, - + test ( assert, component, target ) { const svg = target.querySelector( 'svg' ); const rect = target.querySelector( 'rect' );