Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

svelte/store #951

Merged
merged 12 commits into from
Nov 25, 2017
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
src/shared
shared.js
store.js
test/test.js
test/setup.js
**/_actual.js
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"compiler",
"ssr",
"shared.js",
"store.js",
"README.md"
],
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/generators/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@ export default class Generator {
(param: Node) =>
param.type === 'AssignmentPattern' ? param.left.name : param.name
);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
});

Expand Down
19 changes: 16 additions & 3 deletions src/generators/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,23 @@ 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 expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];

const initialState = [];

if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}

if (storeProps.length > 0) {
initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
}

if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}

Expand All @@ -205,6 +213,7 @@ export default function dom(
@init(this, options);
${generator.usesRefs && `this.refs = {};`}
this._state = @assign(${initialState.join(', ')});
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${generator.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Expand All @@ -215,7 +224,11 @@ export default function dom(
${generator.bindingGroups.length &&
`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`}

${templateProperties.ondestroy && `this._handlers.destroy = [%ondestroy]`}
${(templateProperties.ondestroy || storeProps.length) && (
`this._handlers.destroy = [${
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
}];`
)}

${generator.slots.size && `this._slotted = options.slots || {};`}

Expand Down
42 changes: 37 additions & 5 deletions src/generators/dom/visitors/Element/addBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,19 @@ export default function addBindings(

const usesContext = group.bindings.some(binding => binding.handler.usesContext);
const usesState = group.bindings.some(binding => binding.handler.usesState);
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');

const props = new Set();
const storeProps = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.add(prop);
});

binding.handler.storeProps.forEach(prop => {
storeProps.add(prop);
});
}); // TODO use stringifyProps here, once indenting is fixed

// media bindings — awkward special case. The native timeupdate events
Expand All @@ -222,9 +228,11 @@ export default function addBindings(
}
${usesContext && `var context = ${node.var}._svelte;`}
${usesState && `var state = #component.get();`}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
#component.set({ ${Array.from(props).join(', ')} });
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
${needsLock && `${lock} = false;`}
}
`);
Expand Down Expand Up @@ -307,6 +315,13 @@ function getEventHandler(
dependencies: string[],
value: string,
) {
let storeDependencies = [];

if (generator.options.store) {
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
}

if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
Expand All @@ -318,8 +333,10 @@ function getEventHandler(
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: state.${prop}`)
props: dependencies.map(prop => `${prop}: state.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}

Expand All @@ -336,16 +353,31 @@ function getEventHandler(
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: state.${prop}`)
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}

let props;
let storeProps;

if (generator.options.store && name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}

return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props: [`${name}: ${value}`]
props,
storeProps
};
}

Expand Down Expand Up @@ -393,4 +425,4 @@ function isComputed(node: Node) {
}

return false;
}
}
14 changes: 10 additions & 4 deletions src/generators/server-side-rendering/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,22 @@ export default function ssr(
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 expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : [];

const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}

if (storeProps.length > 0) {
initialState.push(`options.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
}

if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}

Expand All @@ -99,7 +105,7 @@ export default function ssr(
return ${templateProperties.data ? `%data()` : `{}`};
};

${name}.render = function(state, options) {
${name}.render = function(state, options = {}) {
state = Object.assign(${initialState.join(', ')});

${computations.map(
Expand Down
11 changes: 10 additions & 1 deletion src/generators/server-side-rendering/visitors/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export default function visitComponent(

let open = `\${${expression}.render({${props}}`;

const options = [];
if (generator.options.store) {
options.push(`store: options.store`);
}

if (node.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
Expand All @@ -95,11 +100,15 @@ export default function visitComponent(
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
.join(', ');

open += `, { slotted: { ${slotted} } }`;
options.push(`slotted: { ${slotted} }`);

generator.appendTargets.pop();
}

if (options.length) {
open += `, { ${options.join(', ')} }`;
}

generator.append(open);
generator.append(')}');
}
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface CompileOptions {
legacy?: boolean;
customElement?: CustomElementOptions | true;
css?: boolean;
store?: boolean;

onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;
Expand Down
12 changes: 10 additions & 2 deletions src/server-side-rendering/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ import * as fs from 'fs';
import * as path from 'path';
import { compile } from '../index.ts';

const compileOptions = {};

function capitalise(name) {
return name[0].toUpperCase() + name.slice(1);
}

export default function register(options) {
const { extensions } = options;

if (extensions) {
_deregister('.html');
extensions.forEach(_register);
}

// TODO make this the default and remove in v2
if ('store' in options) compileOptions.store = options.store;
}

function _deregister(extension) {
Expand All @@ -20,13 +26,15 @@ function _deregister(extension) {

function _register(extension) {
require.extensions[extension] = function(module, filename) {
const {code} = compile(fs.readFileSync(filename, 'utf-8'), {
const options = Object.assign({}, compileOptions, {
filename,
name: capitalise(path.basename(filename)
.replace(new RegExp(`${extension.replace('.', '\\.')}$`), '')),
generate: 'ssr',
generate: 'ssr'
});

const {code} = compile(fs.readFileSync(filename, 'utf-8'), options);

return module._compile(code, filename);
};
}
Expand Down
9 changes: 7 additions & 2 deletions src/shared/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ export function get(key) {
}

export function init(component, options) {
component.options = options;

component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._root = options._root || component;
component._bind = options._bind;

component.options = options;
component.store = component._root.options.store;
}

export function observe(key, callback, options) {
Expand Down Expand Up @@ -187,6 +188,10 @@ export function _unmount() {
this._fragment.u();
}

export function removeFromStore() {
this.store._remove(this);
}

export var proto = {
destroy: destroy,
get: get,
Expand Down
10 changes: 9 additions & 1 deletion src/validate/html/validateEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import flattenReference from '../../utils/flattenReference';
import list from '../../utils/list';
import { Validator } from '../index';
import validate, { Validator } from '../index';
import validCalleeObjects from '../../utils/validCalleeObjects';
import { Node } from '../../interfaces';

Expand Down Expand Up @@ -28,13 +28,21 @@ export default function validateEventHandlerCallee(
return;
}

if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') {
if (!validator.options.store) {
validator.warn('compile with `store: true` in order to call store methods', attribute.expression.start);
}
return;
}

if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
)
return;

const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat(
validator.options.store ? 'store.*' : [],
Array.from(validBuiltins),
Array.from(validator.methods.keys())
);
Expand Down
6 changes: 4 additions & 2 deletions src/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class Validator {
readonly source: string;
readonly filename: string;

options: CompileOptions;
onwarn: ({}) => void;
locator?: (pos: number) => Location;

Expand All @@ -37,8 +38,8 @@ export class Validator {
constructor(parsed: Parsed, source: string, options: CompileOptions) {
this.source = source;
this.filename = options.filename;

this.onwarn = options.onwarn;
this.options = options;

this.namespace = null;
this.defaultExport = null;
Expand Down Expand Up @@ -78,7 +79,7 @@ export default function validate(
stylesheet: Stylesheet,
options: CompileOptions
) {
const { onwarn, onerror, name, filename } = options;
const { onwarn, onerror, name, filename, store } = options;

try {
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
Expand All @@ -99,6 +100,7 @@ export default function validate(
onwarn,
name,
filename,
store
});

if (parsed.js) {
Expand Down
Loading