attach globals to state object on initialisation

fixes #908
pull/940/head
Rich Harris 7 years ago committed by GitHub
parent 60b883b066
commit 51901442c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -51,7 +51,7 @@
"eslint": "^4.3.0",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"estree-walker": "^0.5.0",
"estree-walker": "^0.5.1",
"glob": "^7.1.1",
"jsdom": "^11.1.0",
"locate-character": "^2.0.0",

@ -1,17 +1,16 @@
import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import getCodeFrame from '../utils/getCodeFrame';
import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference';
import globalWhitelist from '../utils/globalWhitelist';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import wrapModule from './shared/utils/wrapModule';
import annotateWithScopes from '../utils/annotateWithScopes';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import clone from '../utils/clone';
import DomBlock from './dom/Block';
@ -71,6 +70,19 @@ function removeIndentation(
}
}
// 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 = [
'children',
'else'
];
childKeys.IfBlock = [
'children',
'else'
];
export default class Generator {
ast: Parsed;
parsed: Parsed;
@ -156,7 +168,12 @@ export default class Generator {
this.aliases = new Map();
this.usedNames = new Set();
this.parseJs(dom);
this.computations = [];
this.templateProperties = {};
this.walkJs(dom);
this.walkTemplate();
this.name = this.alias(name);
if (options.customElement === true) {
@ -196,12 +213,10 @@ export default class Generator {
context: string,
isEventHandler: boolean
): {
dependencies: string[],
contexts: Set<string>,
indexes: Set<string>,
snippet: string
indexes: Set<string>
} {
this.addSourcemapLocations(expression);
// this.addSourcemapLocations(expression);
const usedContexts: Set<string> = new Set();
const usedIndexes: Set<string> = new Set();
@ -209,7 +224,7 @@ export default class Generator {
const { code, helpers } = this;
const { contexts, indexes } = block;
let scope = annotateWithScopes(expression); // TODO this already happens in findDependencies
let scope: Scope;
let lexicalDepth = 0;
const self = this;
@ -231,7 +246,7 @@ export default class Generator {
});
} else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name)) return;
if (scope && scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop
@ -267,16 +282,7 @@ export default class Generator {
}
}
if (globalWhitelist.has(name)) {
code.prependRight(node.start, `('${name}' in state ? state.`);
code.appendLeft(
node.object ? node.object.end : node.end,
` : ${name})`
);
} else {
code.prependRight(node.start, `state.`);
}
code.prependRight(node.start, `state.`);
usedContexts.add('state');
}
@ -290,73 +296,12 @@ export default class Generator {
},
});
const dependencies: Set<string> = new Set(expression._dependencies || []);
if (expression._dependencies) {
expression._dependencies.forEach((prop: string) => {
if (this.indirectDependencies.has(prop)) {
this.indirectDependencies.get(prop).forEach(dependency => {
dependencies.add(dependency);
});
}
});
}
return {
dependencies: Array.from(dependencies),
contexts: usedContexts,
indexes: usedIndexes,
snippet: `[✂${expression.start}-${expression.end}✂]`,
indexes: usedIndexes
};
}
findDependencies(
contextDependencies: Map<string, string[]>,
indexes: Map<string, string>,
expression: Node
) {
if (expression._dependencies) return expression._dependencies;
let scope = annotateWithScopes(expression);
const dependencies: string[] = [];
const generator = this; // can't use arrow functions, because of this.skip()
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) {
scope = node._scope;
return;
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name) || generator.helpers.has(name)) return;
if (contextDependencies.has(name)) {
dependencies.push(...contextDependencies.get(name));
} else if (!indexes.has(name)) {
dependencies.push(name);
}
this.skip();
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
dependencies.forEach(name => {
if (!globalWhitelist.has(name)) {
this.expectedProperties.add(name);
}
});
return (expression._dependencies = dependencies);
}
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
@ -454,17 +399,19 @@ export default class Generator {
};
}
parseJs(dom: boolean) {
const { code, source } = this;
walkJs(dom: boolean) {
const {
code,
source,
computations,
templateProperties,
imports
} = this;
const { js } = this.parsed;
const imports = this.imports;
const computations: Computation[] = [];
const templateProperties: Record<string, Node> = {};
const componentDefinition = new CodeBuilder();
let namespace = null;
if (js) {
this.addSourcemapLocations(js.content);
@ -637,7 +584,7 @@ export default class Generator {
if (templateProperties.namespace) {
const ns = templateProperties.namespace.value.value;
namespace = namespaces[ns] || ns;
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.onrender) templateProperties.oncreate = templateProperties.onrender; // remove after v2
@ -693,9 +640,132 @@ export default class Generator {
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
}
}
}
walkTemplate() {
const {
code,
expectedProperties,
helpers
} = this;
const { html } = this.parsed;
const contextualise = (node: Node, contextDependencies: Map<string, string[]>, indexes: Set<string>) => {
this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
let scope = annotateWithScopes(node);
const dependencies: Set<string> = new Set();
walk(node, {
enter(node: Node, parent: Node) {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (node._scope) {
scope = node._scope;
return;
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name)) return;
if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else if (!indexes.has(name)) {
dependencies.add(name);
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (node._scope) scope = scope.parent;
}
});
dependencies.forEach(dependency => {
expectedProperties.add(dependency);
});
return {
snippet: `[✂${node.start}-${node.end}✂]`,
dependencies: Array.from(dependencies)
};
}
const contextStack = [];
const indexStack = [];
const dependenciesStack = [];
let contextDependencies = new Map();
const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
let indexes = new Set();
const indexesStack: Set<string>[] = [indexes];
walk(html, {
enter(node: Node, parent: Node) {
if (node.type === 'EachBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.context, node.metadata.dependencies);
if (node.destructuredContexts) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
const name = node.destructuredContexts[i];
const value = `${node.context}[${i}]`;
contextDependencies.set(name, node.metadata.dependencies);
}
}
contextDependenciesStack.push(contextDependencies);
if (node.index) {
indexes = new Set(indexes);
indexes.add(node.index);
indexesStack.push(indexes);
}
}
if (node.type === 'IfBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
}
if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
this.skip();
}
if (node.type === 'Binding') {
node.metadata = contextualise(node.value, contextDependencies, indexes);
this.skip();
}
if (node.type === 'EventHandler' && node.expression) {
node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes);
});
this.skip();
}
},
leave(node: Node, parent: Node) {
if (node.type === 'EachBlock') {
contextDependenciesStack.pop();
contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
this.computations = computations;
this.namespace = namespace;
this.templateProperties = templateProperties;
if (node.index) {
indexesStack.pop();
indexes = indexesStack[indexesStack.length - 1];
}
}
}
});
}
}

@ -10,12 +10,12 @@ export interface BlockOptions {
generator?: DomGenerator;
expression?: Node;
context?: string;
destructuredContexts?: string[];
comment?: string;
key?: string;
contexts?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
contextDependencies?: Map<string, string[]>;
params?: string[];
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
@ -38,7 +38,6 @@ export default class Block {
contexts: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
contextDependencies: Map<string, string[]>;
dependencies: Set<string>;
params: string[];
indexNames: Map<string, string>;
@ -86,7 +85,6 @@ export default class Block {
this.contexts = options.contexts;
this.indexes = options.indexes;
this.changeableIndexes = options.changeableIndexes;
this.contextDependencies = options.contextDependencies;
this.dependencies = new Set();
this.params = options.params;
@ -176,14 +174,6 @@ export default class Block {
);
}
findDependencies(expression: Node) {
return this.generator.findDependencies(
this.contextDependencies,
this.indexes,
expression
);
}
mount(name: string, parentNode: string) {
if (parentNode) {
this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`);

@ -6,6 +6,7 @@ import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import visit from './visit';
import shared from './shared';
@ -184,13 +185,28 @@ export default function dom(
const debugName = `<${generator.customElement ? generator.tag : name}>`;
// generate initial state object
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
initialState.push('{}');
}
initialState.push(`options.data`);
const constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`}
${options.dev && !generator.customElement &&
`if (!options || (!options.target && !options._root)) throw new Error("'target' is a required option");`}
@init(this, options);
${generator.usesRefs && `this.refs = {};`}
this._state = @assign(${templateProperties.data ? '%data()' : '{}'}, options.data);
this._state = @assign(${initialState.join(', ')});
${generator.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&

@ -74,9 +74,7 @@ const preprocessors = {
) => {
cannotUseInnerHTML(node);
node.var = block.getUniqueName('text');
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
},
RawMustacheTag: (
@ -90,9 +88,7 @@ const preprocessors = {
) => {
cannotUseInnerHTML(node);
node.var = block.getUniqueName('raw');
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
},
Text: (
@ -133,8 +129,7 @@ const preprocessors = {
function attachBlocks(node: Node) {
node.var = block.getUniqueName(`if_block`);
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
node._block = block.child({
comment: createDebuggingComment(node, generator),
@ -209,7 +204,7 @@ const preprocessors = {
cannotUseInnerHTML(node);
node.var = block.getUniqueName(`each`);
const dependencies = block.findDependencies(node.expression);
const { dependencies } = node.metadata;
block.addDependencies(dependencies);
const indexNames = new Map(block.indexNames);
@ -235,24 +230,18 @@ const preprocessors = {
const changeableIndexes = new Map(block.changeableIndexes);
if (node.index) changeableIndexes.set(node.index, node.key);
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.context, dependencies);
if (node.destructuredContexts) {
for (const i = 0; i < node.destructuredContexts.length; i++) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
contexts.set(node.destructuredContexts[i], `${context}[${i}]`);
contextDependencies.set(node.destructuredContexts[i], dependencies);
}
}
node._block = block.child({
comment: createDebuggingComment(node, generator),
name: generator.getUniqueName('create_each_block'),
expression: node.expression,
context: node.context,
key: node.key,
contextDependencies,
contexts,
indexes,
changeableIndexes,
@ -319,7 +308,7 @@ const preprocessors = {
if (chunk.type !== 'Text') {
if (node.parent) cannotUseInnerHTML(node.parent);
const dependencies = block.findDependencies(chunk.expression);
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
@ -341,12 +330,10 @@ const preprocessors = {
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
const dependencies = block.findDependencies(arg);
block.addDependencies(dependencies);
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
block.addDependencies(dependencies);
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
generator.hasIntroTransitions = block.hasIntroMethod = true;
@ -382,9 +369,10 @@ const preprocessors = {
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
if (node.name === 'select') {
if (valueAttribute) {
const binding = node.attributes.find((node: Node) => node.type === 'Binding' && node.name === 'value');
if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = block.findDependencies(valueAttribute.value);
const dependencies = binding.metadata.dependencies;
state.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
generator.indirectDependencies.set(prop, new Set());
@ -422,7 +410,6 @@ const preprocessors = {
);
node._state = getChildState(state, {
isTopLevel: false,
parentNode: node.var,
parentNodes: block.getUniqueName(`${node.var}_nodes`),
parentNodeName: node.name,
@ -530,7 +517,6 @@ export default function preprocess(
contexts: new Map(),
indexes: new Map(),
changeableIndexes: new Map(),
contextDependencies: new Map(),
params: ['state'],
indexNames: new Map(),

@ -182,7 +182,7 @@ export default function visitComponent(
`);
beforecreate = deindent`
#component._root._beforecreate.push(function () {
#component._root._beforecreate.push(function() {
var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return;
${setParentFromChildOnInit}
@ -199,7 +199,7 @@ export default function visitComponent(
block.builders.update.addBlock(deindent`
var ${name}_changes = {};
${updates.join('\n')}
${name}._set( ${name}_changes );
${name}._set(${name}_changes);
${bindings.length && `${name_updating} = {};`}
`);
}
@ -219,7 +219,7 @@ export default function visitComponent(
block.builders.create.addLine(`${name}._fragment.c();`);
block.builders.claim.addLine(
`${name}._fragment.l( ${state.parentNodes} );`
`${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
@ -352,7 +352,8 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
}
// simple dynamic attributes
const { dependencies, snippet } = block.contextualise(value.expression);
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
@ -373,15 +374,14 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = block.contextualise(
chunk.expression
);
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach(dependency => {
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
@ -396,9 +396,8 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { snippet, contexts, dependencies } = block.contextualise(
binding.value
);
const { contexts } = block.contextualise(binding.value);
const { dependencies, snippet } = binding.metadata;
const contextual = block.contexts.has(name);

@ -45,7 +45,8 @@ export default function visitEachBlock(
mountOrIntro,
};
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
@ -362,7 +363,7 @@ function unkeyed(
block: Block,
state: State,
node: Node,
snippet,
snippet: string,
{
create_each_block,
each_block_value,
@ -402,8 +403,8 @@ function unkeyed(
}
`);
const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies);
const { dependencies } = node.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});

@ -72,7 +72,9 @@ export default function visitAttribute(
if (attribute.value.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = attribute.value[0];
const { snippet, dependencies, indexes } = block.contextualise(expression);
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = attribute.value[0].metadata;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
@ -94,7 +96,8 @@ export default function visitAttribute(
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { snippet, dependencies, indexes } = block.contextualise(chunk.expression);
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;

@ -36,7 +36,8 @@ export default function visitStyleAttribute(
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { snippet, dependencies, indexes } = block.contextualise(chunk.expression);
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;

@ -92,9 +92,23 @@ export default function addBindings(
let updateCondition: string;
const { name } = getObject(binding.value);
const { snippet, contexts, dependencies } = block.contextualise(
binding.value
);
const { contexts } = block.contextualise(binding.value);
const { snippet } = binding.metadata;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be
// pointing to
// TODO should this happen in preprocess?
const dependencies = binding.metadata.dependencies.slice();
binding.metadata.dependencies.forEach((prop: string) => {
const indirectDependencies = generator.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
});
}
});
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))

@ -15,10 +15,13 @@ export default function addTransitions(
if (!intro && !outro) return;
if (intro) block.contextualise(intro.expression); // TODO remove all these
if (outro) block.contextualise(outro.expression);
if (intro === outro) {
const name = block.getUniqueName(`${node.var}_transition`);
const snippet = intro.expression
? block.contextualise(intro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
block.addVariable(name);
@ -48,7 +51,7 @@ export default function addTransitions(
if (intro) {
block.addVariable(introName);
const snippet = intro.expression
? block.contextualise(intro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -73,7 +76,7 @@ export default function addTransitions(
if (outro) {
block.addVariable(outroName);
const snippet = outro.expression
? block.contextualise(outro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
const fn = `%transitions-${outro.name}`;

@ -38,7 +38,8 @@ export default function visitWindow(
let usesState = false;
attribute.expression.arguments.forEach((arg: Node) => {
const { dependencies } = block.contextualise(arg, null, true);
block.contextualise(arg, null, true);
const { dependencies } = arg.metadata;
if (dependencies.length) usesState = true;
});

@ -24,9 +24,11 @@ function getBranches(
elementStack: Node[],
componentStack: Node[]
) {
block.contextualise(node.expression); // TODO remove
const branches = [
{
condition: block.contextualise(node.expression).snippet,
condition: node.metadata.snippet,
block: node._block.name,
hasUpdateMethod: node._block.hasUpdateMethod,
hasIntroMethod: node._block.hasIntroMethod,
@ -143,7 +145,6 @@ function simple(
var ${name} = (${branch.condition}) && ${branch.block}(${params}, #component);
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
@ -246,7 +247,6 @@ function compound(
var ${name} = ${current_block_type_and}${current_block_type}(${params}, #component);
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
@ -345,7 +345,6 @@ function compoundWithOutros(
`);
}
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';

@ -12,7 +12,8 @@ export default function visitTag(
name: string,
update: (value: string) => string
) {
const { dependencies, indexes, snippet } = block.contextualise(node.expression);
const { indexes } = block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
@ -30,7 +31,7 @@ export default function visitTag(
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;

@ -6,6 +6,7 @@ import preprocess from './preprocess';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Parsed, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from './interfaces';
import { stringify } from '../../utils/stringify';
@ -71,6 +72,22 @@ export default function ssr(
{ css: null, cssMap: null } :
generator.stylesheet.render(options.filename, true);
// generate initial state object
// TODO this doesn't work, because expectedProperties isn't populated
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
initialState.push('{}');
}
initialState.push('state');
const result = deindent`
${generator.javascript}
@ -83,9 +100,7 @@ export default function ssr(
};
${name}.render = function(state, options) {
${templateProperties.data
? `state = Object.assign(%data(), state || {});`
: `state = state || {};`}
state = Object.assign(${initialState.join(', ')});
${computations.map(
({ key, deps }) =>

@ -5,12 +5,6 @@ import { Node } from '../../interfaces';
function noop () {}
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
const preprocessors = {
MustacheTag: noop,
RawMustacheTag: noop,
@ -21,21 +15,15 @@ const preprocessors = {
node: Node,
elementStack: Node[]
) => {
function attachBlocks(node: Node) {
preprocessChildren(generator, node, elementStack);
preprocessChildren(generator, node, elementStack);
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
attachBlocks(node);
},
EachBlock: (

@ -16,7 +16,8 @@ export default function visitComponent(
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'MustacheTag') {
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape( ' + snippet + ')}';
}
}
@ -45,7 +46,8 @@ export default function visitComponent(
if (chunk.type === 'Text') {
value = isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
} else {
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
value = snippet;
}
} else {

@ -8,7 +8,8 @@ export default function visitEachBlock(
block: Block,
node: Node
) {
const { dependencies, snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : node.context} => \``;
generator.append(open);

@ -19,7 +19,8 @@ function stringifyAttributeValue(block: Block, chunks: Node[]) {
return escape(chunk.data).replace(/"/g, '&quot;');
}
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${' + snippet + '}';
})
.join('');

@ -8,7 +8,8 @@ export default function visitIfBlock(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${ ' + snippet + ' ? `');

@ -7,6 +7,8 @@ export default function visitMustacheTag(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${__escape(' + snippet + ')}');
}

@ -7,6 +7,8 @@ export default function visitRawMustacheTag(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${' + snippet + '}');
}

@ -38,7 +38,7 @@ export default function annotateWithScopes(expression: Node) {
return scope;
}
class Scope {
export class Scope {
parent: Scope;
block: boolean;
declarations: Set<string>;

@ -5,7 +5,7 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options) {
state = state || {};
state = Object.assign({}, state);
return ``.trim();
};

@ -7,7 +7,7 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options) {
state = state || {};
state = Object.assign({}, state);
return ``.trim();
};

@ -0,0 +1,61 @@
export default {
html: `
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo ">
<input type="checkbox">
<input type="text">
</div>
`,
data: {
todos: {
first: {
description: 'Buy some milk',
done: true,
},
second: {
description: 'Do the laundry',
done: true,
},
third: {
description: "Find life's true purpose",
done: false,
},
},
},
test(assert, component, target, window) {
const input = document.querySelectorAll('input[type="checkbox"]')[2];
const change = new window.Event('change');
input.checked = true;
input.dispatchEvent(change);
assert.ok(component.get('todos').third.done);
assert.htmlEqual(target.innerHTML, `
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
`);
},
};

@ -0,0 +1,6 @@
{{#each Object.keys(todos) as key}}
<div class='todo {{todos[key].done ? "done": ""}}'>
<input type='checkbox' bind:checked='todos[key].done'>
<input type='text' bind:value='todos[key].description'>
</div>
{{/each}}

@ -7,7 +7,7 @@ export default {
},
html: `<svg><rect x="0" y="0" width="100" height="100"></rect></svg>`,
test ( assert, component, target ) {
const svg = target.querySelector( 'svg' );
const rect = target.querySelector( 'rect' );

@ -954,8 +954,8 @@ estree-walker@^0.3.0:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
estree-walker@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.0.tgz#aae3b57c42deb8010e349c892462f0e71c5dd1aa"
version "0.5.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854"
esutils@^2.0.2:
version "2.0.2"

Loading…
Cancel
Save