Merge branch 'master' into v3-readme

pull/1953/head
Conduitry 7 years ago
commit f1f9e38ee1

@ -22,6 +22,7 @@
],
"scripts": {
"test": "mocha --opts mocha.opts",
"test:unit": "mocha --require sucrase/register --recursive ./**/__test__.ts",
"quicktest": "mocha --opts mocha.opts",
"precoverage": "c8 mocha --opts mocha.coverage.opts",
"coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html",

@ -1,6 +1,3 @@
import { Warning } from './interfaces';
import Component from './compile/Component';
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
const t = process.hrtime();
@ -31,14 +28,11 @@ export default class Stats {
currentChildren: Timing[];
timings: Timing[];
stack: Timing[];
warnings: Warning[];
constructor() {
this.startTime = now();
this.stack = [];
this.currentChildren = this.timings = [];
this.warnings = [];
}
start(label) {
@ -67,46 +61,13 @@ export default class Stats {
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
}
render(component: Component) {
render() {
const timings = Object.assign({
total: now() - this.startTime
}, collapseTimings(this.timings));
// TODO would be good to have this info even
// if options.generate is false
const imports = component && component.imports.map(node => {
return {
source: node.source.value,
specifiers: node.specifiers.map(specifier => {
return {
name: (
specifier.type === 'ImportDefaultSpecifier' ? 'default' :
specifier.type === 'ImportNamespaceSpecifier' ? '*' :
specifier.imported.name
),
as: specifier.local.name
};
})
}
});
return {
timings,
warnings: this.warnings,
vars: component.vars.filter(variable => !variable.global && !variable.implicit && !variable.internal).map(variable => ({
name: variable.name,
export_name: variable.export_name || null,
injected: variable.injected || false,
module: variable.module || false,
mutated: variable.mutated || false,
reassigned: variable.reassigned || false,
referenced: variable.referenced || false,
writable: variable.writable || false
}))
timings
};
}
warn(warning) {
this.warnings.push(warning);
}
}

@ -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, Var } from '../interfaces';
import { Node, Ast, CompileOptions, Var, Warning } from '../interfaces';
import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame';
import flattenReference from '../utils/flattenReference';
@ -40,6 +40,7 @@ childKeys.ExportNamedDeclaration = ['declaration', 'specifiers'];
export default class Component {
stats: Stats;
warnings: Warning[];
ast: Ast;
source: string;
@ -93,11 +94,13 @@ export default class Component {
source: string,
name: string,
compileOptions: CompileOptions,
stats: Stats
stats: Stats,
warnings: Warning[]
) {
this.name = name;
this.stats = stats;
this.warnings = warnings;
this.ast = ast;
this.source = source;
this.compileOptions = compileOptions;
@ -161,7 +164,7 @@ export default class Component {
if (!compileOptions.customElement) this.stylesheet.reify();
this.stylesheet.warnOnUnusedSelectors(stats);
this.stylesheet.warnOnUnusedSelectors(this);
}
add_var(variable: Var) {
@ -183,7 +186,11 @@ export default class Component {
writable: true
});
this.add_reference(name.slice(1));
const subscribable_name = name.slice(1);
this.add_reference(subscribable_name);
const variable = this.var_lookup.get(subscribable_name);
variable.subscribable = true;
} else if (!this.ast.instance) {
this.add_var({
name,
@ -213,106 +220,127 @@ export default class Component {
return this.aliases.get(name);
}
helper(name: string) {
this.helpers.add(name);
return this.alias(name);
}
generate(result: string) {
const { compileOptions, name } = this;
const { format = 'esm' } = compileOptions;
let js = null;
let css = null;
const banner = `/* ${this.file ? `${this.file} ` : ``}generated by Svelte v${"__VERSION__"} */`;
if (result) {
const { compileOptions, name } = this;
const { format = 'esm' } = compileOptions;
// TODO use same regex for both
result = result.replace(compileOptions.generate === 'ssr' ? /(@+|#+)(\w*(?:-\w*)?)/g : /(@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (internal_exports.has(name)) {
if (compileOptions.dev && internal_exports.has(`${name}Dev`)) name = `${name}Dev`;
this.helpers.add(name);
}
const banner = `/* ${this.file ? `${this.file} ` : ``}generated by Svelte v${"__VERSION__"} */`;
return this.alias(name);
}
// TODO use same regex for both
result = result.replace(compileOptions.generate === 'ssr' ? /(@+|#+)(\w*(?:-\w*)?)/g : /(@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (internal_exports.has(name)) {
if (compileOptions.dev && internal_exports.has(`${name}Dev`)) name = `${name}Dev`;
this.helpers.add(name);
}
return sigil.slice(1) + name;
});
return this.alias(name);
}
const importedHelpers = Array.from(this.helpers)
.sort()
.map(name => {
const alias = this.alias(name);
return { name, alias };
return sigil.slice(1) + name;
});
const module = wrapModule(
result,
format,
name,
compileOptions,
banner,
compileOptions.sveltePath,
importedHelpers,
this.imports,
this.vars.filter(variable => variable.module && variable.export_name).map(variable => ({
name: variable.name,
as: variable.export_name
})),
this.source
);
const importedHelpers = Array.from(this.helpers)
.sort()
.map(name => {
const alias = this.alias(name);
return { name, alias };
});
const parts = module.split('✂]');
const finalChunk = parts.pop();
const module = wrapModule(
result,
format,
name,
compileOptions,
banner,
compileOptions.sveltePath,
importedHelpers,
this.imports,
this.vars.filter(variable => variable.module && variable.export_name).map(variable => ({
name: variable.name,
as: variable.export_name
})),
this.source
);
const compiled = new Bundle({ separator: '' });
const parts = module.split('✂]');
const finalChunk = parts.pop();
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
const compiled = new Bundle({ separator: '' });
const { filename } = compileOptions;
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
const { filename } = compileOptions;
const pattern = /\[✂(\d+)-(\d+)$/;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
const pattern = /\[✂(\d+)-(\d+)$/;
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
const match = pattern.exec(str);
const match = pattern.exec(str);
const snippet = this.code.snip(+match[1], +match[2]);
const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({
filename,
content: snippet,
compiled.addSource({
filename,
content: snippet,
});
});
});
addString(finalChunk);
addString(finalChunk);
const css = compileOptions.customElement ?
{ code: null, map: null } :
this.stylesheet.render(compileOptions.cssOutputFilename, true);
css = compileOptions.customElement ?
{ code: null, map: null } :
this.stylesheet.render(compileOptions.cssOutputFilename, true);
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: compileOptions.outputFilename,
})
};
js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: compileOptions.outputFilename,
})
};
}
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this)
ast: this.ast,
warnings: this.warnings,
vars: this.vars.filter(v => !v.global && !v.implicit && !v.internal).map(v => ({
name: v.name,
export_name: v.export_name || null,
injected: v.injected || false,
module: v.module || false,
mutated: v.mutated || false,
reassigned: v.reassigned || false,
referenced: v.referenced || false,
writable: v.writable || false
})),
stats: this.stats.render()
};
}
@ -393,7 +421,7 @@ export default class Component {
const frame = getCodeFrame(this.source, start.line - 1, start.column);
this.stats.warn({
this.warnings.push({
code: warning.code,
message: warning.message,
frame,
@ -641,6 +669,9 @@ export default class Component {
});
this.add_reference(name.slice(1));
const variable = this.var_lookup.get(name.slice(1));
variable.subscribable = true;
} else {
this.add_var({
name,
@ -738,7 +769,15 @@ export default class Component {
});
}
rewrite_props() {
invalidate(name, value = name) {
const variable = this.var_lookup.get(name);
if (variable && (variable.subscribable && variable.reassigned)) {
return `$$subscribe_${name}(), $$invalidate('${name}', ${value})`;
}
return `$$invalidate('${name}', ${value})`;
}
rewrite_props(get_insert: (variable: Var) => string) {
const component = this;
const { code, instance_scope, instance_scope_map: map, componentOptions } = this;
let scope = instance_scope;
@ -759,72 +798,97 @@ export default class Component {
if (node.type === 'VariableDeclaration') {
if (node.kind === 'var' || scope === instance_scope) {
let has_exports = false;
let has_only_exports = true;
node.declarations.forEach((declarator, i) => {
const next = node.declarations[i + 1];
node.declarations.forEach(declarator => {
extractNames(declarator.id).forEach(name => {
const variable = component.var_lookup.get(name);
if (declarator.id.type !== 'Identifier') {
const inserts = [];
if (name === componentOptions.props_object) {
if (variable.export_name) {
component.error(declarator, {
code: 'exported-options-props',
message: `Cannot export props binding`
});
}
extractNames(declarator.id).forEach(name => {
const variable = component.var_lookup.get(name);
if (declarator.id.type !== 'Identifier') {
if (variable.export_name || name === componentOptions.props_object) {
component.error(declarator, {
code: 'todo',
message: `props binding in destructured declaration is not yet supported`
code: 'destructured-prop',
message: `Cannot declare props in destructured declaration`
});
}
// can't use the @ trick here, because we're
// manipulating the underlying magic string
component.helpers.add('exclude_internal_props');
const exclude_internal_props = component.alias('exclude_internal_props');
const suffix = code.original[declarator.end] === ';'
? ` = ${exclude_internal_props}($$props)`
: ` = ${exclude_internal_props}($$props);`
if (variable.subscribable) {
inserts.push(get_insert(variable));
}
});
if (declarator.id.end === declarator.end) {
code.appendLeft(declarator.end, suffix);
if (inserts.length > 0) {
if (next) {
code.overwrite(declarator.end, next.start, `; ${inserts.join('; ')}; ${node.kind} `);
} else {
code.overwrite(declarator.id.end, declarator.end, suffix);
code.appendLeft(declarator.end, `; ${inserts.join('; ')}`);
}
}
return;
}
const { name } = declarator.id;
const variable = component.var_lookup.get(name);
if (name === componentOptions.props_object) {
if (variable.export_name) {
has_exports = true;
component.error(declarator, {
code: 'exported-options-props',
message: `Cannot export props binding`
});
}
// can't use the @ trick here, because we're
// manipulating the underlying magic string
const exclude_internal_props = component.helper('exclude_internal_props');
const suffix = code.original[declarator.end] === ';'
? ` = ${exclude_internal_props}($$props)`
: ` = ${exclude_internal_props}($$props);`
if (declarator.id.end === declarator.end) {
code.appendLeft(declarator.end, suffix);
} else {
has_only_exports = false;
code.overwrite(declarator.id.end, declarator.end, suffix);
}
});
});
}
if (has_only_exports) {
if (current_group && current_group[current_group.length - 1].kind !== node.kind) {
if (variable.export_name) {
if (variable.subscribable) {
coalesced_declarations.push({
kind: node.kind,
declarators: [declarator],
insert: get_insert(variable)
});
} else {
if (current_group && current_group.kind !== node.kind) {
current_group = null;
}
if (!current_group) {
current_group = { kind: node.kind, declarators: [], insert: null };
coalesced_declarations.push(current_group);
}
current_group.declarators.push(declarator);
}
} else {
current_group = null;
}
// rewrite as a group, later
if (!current_group) {
current_group = [];
coalesced_declarations.push(current_group);
}
if (variable.subscribable) {
let insert = get_insert(variable);
current_group.push(node);
} else {
if (has_exports) {
// rewrite in place
throw new Error('TODO rewrite prop declaration in place');
if (next) {
code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `);
} else {
code.appendLeft(declarator.end, `; ${insert}`);
}
}
}
current_group = null;
}
});
}
} else {
if (node.type !== 'ExportNamedDeclaration') {
@ -845,31 +909,25 @@ export default class Component {
let combining = false;
group.forEach(node => {
node.declarations.forEach(declarator => {
const { id, init } = declarator;
group.declarators.forEach(declarator => {
const { id } = declarator;
if (id.type === 'Identifier') {
const value = init
? this.code.slice(id.start, init.end)
: this.code.slice(id.start, id.end);
if (combining) {
code.overwrite(c, id.start, ', ');
} else {
code.appendLeft(id.start, '{ ');
combining = true;
}
} else {
throw new Error('TODO destructured declarations');
}
if (combining) {
code.overwrite(c, id.start, ', ');
} else {
code.appendLeft(id.start, '{ ');
combining = true;
}
c = declarator.end;
});
c = declarator.end;
});
if (combining) {
const suffix = code.original[c] === ';' ? ` } = $$props` : ` } = $$props;`;
const insert = group.insert
? `; ${group.insert}`
: '';
const suffix = code.original[c] === ';' ? ` } = $$props${insert}` : ` } = $$props${insert};`;
code.appendLeft(c, suffix);
}
});

@ -1,14 +1,11 @@
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import Selector from './Selector';
import getCodeFrame from '../../utils/getCodeFrame';
import hash from '../../utils/hash';
import removeCSSPrefix from '../../utils/removeCSSPrefix';
import Element from '../nodes/Element';
import { Node, Ast, Warning } from '../../interfaces';
import { Node, Ast } from '../../interfaces';
import Component from '../Component';
import Stats from '../../Stats';
const isKeyframesNode = (node: Node) => removeCSSPrefix(node.name) === 'keyframes'
@ -392,33 +389,14 @@ export default class Stylesheet {
});
}
warnOnUnusedSelectors(stats: Stats) {
let locator;
const handler = (selector: Selector) => {
const pos = selector.node.start;
if (!locator) locator = getLocator(this.source, { offsetLine: 1 });
const start = locator(pos);
const end = locator(selector.node.end);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
const message = `Unused CSS selector`;
stats.warn({
code: `css-unused-selector`,
message,
frame,
start,
end,
pos,
filename: this.filename,
toString: () => `${message} (${start.line}:${start.column})\n${frame}`,
});
};
warnOnUnusedSelectors(component: Component) {
this.children.forEach(child => {
child.warnOnUnusedSelector(handler);
child.warnOnUnusedSelector((selector: Selector) => {
component.warn(selector.node, {
code: `css-unused-selector`,
message: `Unused CSS selector`
});
});
});
}
}

@ -3,7 +3,7 @@ import Stats from '../Stats';
import parse from '../parse/index';
import renderDOM from './render-dom/index';
import renderSSR from './render-ssr/index';
import { CompileOptions, Ast } from '../interfaces';
import { CompileOptions, Ast, Warning } from '../interfaces';
import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch';
@ -24,7 +24,7 @@ const valid_options = [
'preserveComments'
];
function validate_options(options: CompileOptions, stats: Stats) {
function validate_options(options: CompileOptions, warnings: Warning[]) {
const { name, filename } = options;
Object.keys(options).forEach(key => {
@ -43,7 +43,7 @@ function validate_options(options: CompileOptions, stats: Stats) {
if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`;
stats.warn({
warnings.push({
code: `options-lowercase-name`,
message,
filename,
@ -74,10 +74,11 @@ export default function compile(source: string, options: CompileOptions = {}) {
options = assign({ generate: 'dom', dev: false }, options);
const stats = new Stats();
const warnings = [];
let ast: Ast;
validate_options(options, stats);
validate_options(options, warnings);
stats.start('parse');
ast = parse(source, options);
@ -89,17 +90,16 @@ export default function compile(source: string, options: CompileOptions = {}) {
source,
options.name || get_name(options.filename) || 'SvelteComponent',
options,
stats
stats,
warnings
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(component), js: null, css: null };
}
const js = options.generate === 'ssr'
? renderSSR(component, options)
: renderDOM(component, options);
const js = options.generate === false
? null
: options.generate === 'ssr'
? renderSSR(component, options)
: renderDOM(component, options);
return component.generate(js);
}

@ -122,8 +122,16 @@ export default class Expression {
const { name, nodes } = flattenReference(node);
if (scope.has(name)) return;
if (globalWhitelist.has(name) && !component.var_lookup.has(name)) return;
if (name[0] === '$' && template_scope.names.has(name.slice(1))) {
component.error(node, {
code: `contextual-store`,
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
});
}
if (template_scope.is_let(name)) {
if (!function_expression) {
dependencies.add(name);
@ -290,7 +298,7 @@ export default class Expression {
if (dirty.length) component.has_reactive_assignments = true;
code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; '));
code.overwrite(node.start, node.end, dirty.map(n => component.invalidate(n)).join('; '));
} else {
names.forEach(name => {
if (scope.declarations.has(name)) return;
@ -356,7 +364,7 @@ export default class Expression {
let body = code.slice(node.body.start, node.body.end).trim();
if (node.body.type !== 'BlockStatement') {
if (pending_assignments.size > 0) {
const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ');
const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
pending_assignments = new Set();
component.has_reactive_assignments = true;
@ -431,7 +439,7 @@ export default class Expression {
const insert = (
(has_semi ? ' ' : '; ') +
Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ')
Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ')
);
if (/^(Break|Continue|Return)Statement/.test(node.type)) {

@ -79,12 +79,13 @@ export default function dom(
${component.componentOptions.props && deindent`
if (!${component.componentOptions.props}) ${component.componentOptions.props} = {};
@assign(${component.componentOptions.props}, $$props);
$$invalidate('${component.componentOptions.props_object}', ${component.componentOptions.props_object});
${component.invalidate(component.componentOptions.props_object)};
`}
${writable_props.map(prop =>
`if ('${prop.export_name}' in $$props) $$invalidate('${prop.name}', ${prop.name} = $$props.${prop.export_name});`)}
`if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};`
)}
${renderer.slots.size > 0 &&
`if ('$$scope' in $$props) $$invalidate('$$scope', $$scope = $$props.$$scope);`}
`if ('$$scope' in $$props) ${component.invalidate('$$scope', `$$scope = $$props.$$scope`)};`}
}
`
: null;
@ -175,7 +176,7 @@ export default function dom(
if (dirty.length) component.has_reactive_assignments = true;
code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; '));
code.overwrite(node.start, node.end, dirty.map(n => component.invalidate(n)).join('; '));
} else {
names.forEach(name => {
const owner = scope.findOwner(name);
@ -204,7 +205,7 @@ export default function dom(
if (pending_assignments.size > 0) {
if (node.type === 'ArrowFunctionExpression') {
const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join(';');
const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
pending_assignments = new Set();
code.prependRight(node.body.start, `{ const $$result = `);
@ -214,7 +215,7 @@ export default function dom(
}
else if (/Statement/.test(node.type)) {
const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ');
const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
if (/^(Break|Continue|Return)Statement/.test(node.type)) {
if (node.argument) {
@ -240,7 +241,25 @@ export default function dom(
throw new Error(`TODO this should not happen!`);
}
component.rewrite_props();
component.rewrite_props(({ name, reassigned }) => {
const value = `$${name}`;
const callback = `$value => { ${value} = $$value; $$invalidate('${value}', ${value}) }`;
if (reassigned) {
return `$$subscribe_${name}()`;
}
const subscribe = component.helper('subscribe');
let insert = `${subscribe}($$self, ${name}, $${callback})`;
if (component.compileOptions.dev) {
const validate_store = component.helper('validate_store');
insert = `${validate_store}(${name}, '${name}'); ${insert}`;
}
return insert;
});
}
const args = ['$$self'];
@ -301,22 +320,45 @@ export default function dom(
addToSet(all_reactive_dependencies, d.dependencies);
});
const user_code = component.javascript || (
!component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props)
? [
component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`,
filtered_props.length > 0 && `let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`
].filter(Boolean).join('\n')
: null
);
let user_code;
if (component.javascript) {
user_code = component.javascript;
} else {
if (!component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props)) {
const statements = [];
if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`);
if (filtered_props.length > 0) statements.push(`let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`);
const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores
reactive_stores.forEach(({ name }) => {
if (component.compileOptions.dev) {
statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`);
}
statements.push(`@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });`);
});
user_code = statements.join('\n');
}
}
const reactive_store_subscriptions = reactive_stores
.filter(store => {
const variable = component.var_lookup.get(store.name.slice(1));
return variable.hoistable;
})
.map(({ name }) => deindent`
let ${name};
${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
$$self.$$.on_destroy.push(${name.slice(1)}.subscribe($$value => { ${name} = $$value; $$invalidate('${name}', ${name}); }));
`)
.join('\n\n');
@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });
`);
const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter(store => {
const variable = component.var_lookup.get(store.name.slice(1));
return variable.reassigned;
})
.map(({ name }) => `$$self.$$.on_destroy.push(() => $$unsubscribe_${name.slice(1)}());`);
if (has_definition) {
const reactive_declarations = component.reactive_declarations.map(d => {
@ -343,8 +385,26 @@ export default function dom(
return variable.injected;
});
const reactive_store_declarations = reactive_stores.map(variable => {
const $name = variable.name;
const name = $name.slice(1);
const store = component.var_lookup.get(name);
if (store.reassigned) {
return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`
}
return $name;
});
builder.addBlock(deindent`
function ${definition}(${args.join(', ')}) {
${reactive_store_declarations.length > 0 && `let ${reactive_store_declarations.join(', ')};`}
${reactive_store_subscriptions}
${resubscribable_reactive_store_unsubscribers}
${user_code}
${renderer.slots.size && `let { ${[...renderer.slots].map(name => `$$slot_${sanitize(name)}`).join(', ')}, $$scope } = $$props;`}
@ -353,8 +413,6 @@ export default function dom(
${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
${reactive_store_subscriptions}
${set && `$$self.$set = ${set};`}
${reactive_declarations.length > 0 && deindent`

@ -465,7 +465,7 @@ export default class ElementWrapper extends Wrapper {
this.renderer.component.partly_hoisted.push(deindent`
function ${handler}(${contextual_dependencies.size > 0 ? `{ ${Array.from(contextual_dependencies).join(', ')} }` : ``}) {
${group.bindings.map(b => b.handler.mutation)}
${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `$$invalidate('${dep}', ${dep});`)}
${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `${this.renderer.component.invalidate(dep)};`)}
}
`);
@ -532,7 +532,7 @@ export default class ElementWrapper extends Wrapper {
renderer.component.partly_hoisted.push(deindent`
function ${name}(${['$$node', 'check'].concat(args).join(', ')}) {
${handler.snippet ? `if ($$node || (!$$node && ${handler.snippet} === check)) ` : ''}${handler.mutation}
$$invalidate('${object}', ${object});
${renderer.component.invalidate(object)};
}
`);

@ -266,7 +266,7 @@ export default class InlineComponentWrapper extends Wrapper {
component.partly_hoisted.push(deindent`
function ${fn}($$component) {
${lhs} = $$component;
${object && `$$invalidate('${object}', ${object});`}
${object && component.invalidate(object)}
}
`);
@ -341,7 +341,7 @@ export default class InlineComponentWrapper extends Wrapper {
const body = deindent`
function ${name}(${args.join(', ')}) {
${lhs} = value;
return $$invalidate('${dependencies[0]}', ${dependencies[0]});
return ${component.invalidate(dependencies[0])}
}
`;

@ -3,6 +3,8 @@ import Component from '../Component';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../../utils/stringify';
import Renderer from './Renderer';
import { walk } from 'estree-walker';
import { extractNames } from '../../utils/annotateWithScopes';
export default function ssr(
component: Component,
@ -22,29 +24,53 @@ export default function ssr(
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);
let user_code;
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
const reactive_store_values = reactive_stores
.map(({ name }) => {
const assignment = `${name} = @get_store_value(${name.slice(1)});`;
return component.compileOptions.dev
? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}`
: assignment;
});
// TODO remove this, just use component.vars everywhere
const props = component.vars.filter(variable => !variable.module && variable.export_name && variable.export_name !== component.componentOptions.props_object);
let user_code;
if (component.javascript) {
component.rewrite_props();
component.rewrite_props(({ name }) => {
const value = `$${name}`;
const get_store_value = component.helper('get_store_value');
let insert = `${value} = ${get_store_value}(${name})`;
if (component.compileOptions.dev) {
const validate_store = component.helper('validate_store');
insert = `${validate_store}(${name}, '${name}'); ${insert}`;
}
return insert;
});
user_code = component.javascript;
} else if (!component.ast.instance && !component.ast.module && (props.length > 0 || component.componentOptions.props)) {
user_code = [
component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`,
props.length > 0 && `let { ${props.map(prop => prop.export_name).join(', ')} } = $$props;`
].filter(Boolean).join('\n');
}
const statements = [];
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
const reactive_store_values = reactive_stores.map(({ name }) => {
const assignment = `const ${name} = @get_store_value(${name.slice(1)});`;
if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`);
if (props.length > 0) statements.push(`let { ${props.map(x => x.name).join(', ')} } = $$props;`);
return component.compileOptions.dev
? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}`
: assignment;
});
reactive_stores.forEach(({ name }) => {
if (component.compileOptions.dev) {
statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`);
}
statements.push(`${name} = @get_store_value(${name.slice(1)});`);
});
user_code = statements.join('\n');
}
// TODO only do this for props with a default value
const parent_bindings = component.javascript
@ -83,6 +109,7 @@ export default function ssr(
return \`${renderer.code}\`;`;
const blocks = [
reactive_stores.length > 0 && `let ${reactive_stores.map(store => store.name).join(', ')};`,
user_code,
parent_bindings.join('\n'),
css.code && `$$result.css.add(#css);`,

@ -89,4 +89,5 @@ export interface Var {
internal?: boolean; // event handlers, bindings
initialised?: boolean;
hoistable?: boolean;
subscribable?: boolean;
}

@ -250,3 +250,9 @@ export function addResizeListener(element, fn) {
export function toggleClass(element, name, toggle) {
element.classList[toggle ? 'add' : 'remove'](name);
}
export function custom_event(type, detail) {
const e = document.createEvent('CustomEvent');
e.initCustomEvent(type, false, false, detail);
return e;
}

@ -1,3 +1,5 @@
import { custom_event } from './dom';
export let current_component;
export function set_current_component(component) {
@ -34,7 +36,7 @@ export function createEventDispatcher() {
if (callbacks) {
// TODO are there situations where events could be dispatched
// in a server (non-DOM) environment?
const event = new window.CustomEvent(type, { detail });
const event = custom_event(type, detail);
callbacks.slice().forEach(fn => {
fn.call(component, event);
});

@ -1,6 +1,7 @@
import { identity as linear, noop, run_all } from './utils.js';
import { loop } from './loop.js';
import { create_rule, delete_rule } from './style_manager.js';
import { custom_event } from './dom.js';
let promise;
@ -238,14 +239,14 @@ export function create_bidirectional_transition(node, fn, params, intro) {
if (b) tick(0, 1);
running_program = init(program, duration);
node.dispatchEvent(new window.CustomEvent(`${running_program.b ? 'intro' : 'outro'}start`));
node.dispatchEvent(custom_event(`${running_program.b ? 'intro' : 'outro'}start`));
loop(now => {
if (pending_program && now > pending_program.start) {
running_program = init(pending_program, duration);
pending_program = null;
node.dispatchEvent(new window.CustomEvent(`${running_program.b ? 'intro' : 'outro'}start`));
node.dispatchEvent(custom_event(`${running_program.b ? 'intro' : 'outro'}start`));
if (css) {
clear_animation();
@ -256,7 +257,7 @@ export function create_bidirectional_transition(node, fn, params, intro) {
if (running_program) {
if (now >= running_program.end) {
tick(t = running_program.b, 1 - t);
node.dispatchEvent(new window.CustomEvent(`${running_program.b ? 'intro' : 'outro'}end`));
node.dispatchEvent(custom_event(`${running_program.b ? 'intro' : 'outro'}end`));
if (!pending_program) {
// we're done

@ -47,6 +47,10 @@ export function validate_store(store, name) {
}
}
export function subscribe(component, store, callback) {
component.$$.on_destroy.push(store.subscribe(callback));
}
export function create_slot(definition, ctx, fn) {
if (definition) {
const slot_ctx = get_slot_context(definition, ctx, fn);

@ -34,6 +34,45 @@ describe('deindent', () => {
assert.equal(deindented, `before\n\tline one\n\tline two\nafter`);
});
it('removes newlines before an empty expression', () => {
const deindented = deindent`
{
some text
${null}
}`;
assert.equal(deindented, `{\n\tsome text\n}`);
});
it('removes newlines after an empty expression', () => {
const deindented = deindent`
{
${null}
some text
}`;
assert.equal(deindented, `{\n\tsome text\n}`);
});
it('removes newlines around empty expressions', () => {
const deindented = deindent`
{
${null}
some text
${null}
some text
${null}
}`;
assert.equal(deindented, `{\n\tsome text\n\n\tsome text\n}`);
});
});
describe('CodeBuilder', () => {

@ -39,7 +39,7 @@ export default function deindent(
current_indentation = get_current_indentation(result);
}
return result.trim().replace(/\t+$/gm, '');
return result.trim().replace(/\t+$/gm, '').replace(/{\n\n/gm, '{\n');
}
function get_current_indentation(str: string) {

@ -68,8 +68,8 @@ describe('css', () => {
assert.equal(dom.css.code, ssr.css.code);
const dom_warnings = dom.stats.warnings.map(normalize_warning);
const ssr_warnings = ssr.stats.warnings.map(normalize_warning);
const dom_warnings = dom.warnings.map(normalize_warning);
const ssr_warnings = ssr.warnings.map(normalize_warning);
assert.deepEqual(dom_warnings, ssr_warnings);
assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);

@ -0,0 +1,9 @@
export default {
html: `
<p>42</p>
`,
async test({ assert, component }) {
assert.equal(component.initial_foo, 42);
}
};

@ -0,0 +1,7 @@
<script>
import { writable } from 'svelte/store';
const foo = writable(42), bar = 'something else';
export let initial_foo = $foo;
</script>
<p>{initial_foo}</p>

@ -0,0 +1,9 @@
export default {
html: `
<p>42</p>
`,
async test({ assert, component }) {
assert.equal(component.initial_foo, 42);
}
};

@ -0,0 +1,7 @@
<script>
import { writable } from 'svelte/store';
let foo = writable(42);
export let initial_foo = $foo;
</script>
<p>{initial_foo}</p>

@ -0,0 +1,76 @@
import { writable } from '../../../../store.js';
const todos = [
writable({ done: false, text: 'write docs' }),
writable({ done: false, text: 'implement contextual stores' }),
writable({ done: false, text: 'go outside' })
];
export default {
error: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`,
props: {
todos
},
html: `
<label>
<input type=checkbox>
[todo] write docs
</label>
<label>
<input type=checkbox>
[todo] implement contextual stores
</label>
<label>
<input type=checkbox>
[todo] go outside
</label>
`,
async test({ assert, component, target, window }) {
const inputs = target.querySelectorAll('input');
const change = new window.MouseEvent('change');
inputs[1].checked = true;
await inputs[1].dispatchEvent(change);
assert.htmlEqual(target.innerHTML, `
<label>
<input type=checkbox>
[todo] write docs
</label>
<label>
<input type=checkbox>
[done] implement contextual stores
</label>
<label>
<input type=checkbox>
[todo] go outside
</label>
`);
await todos[0].update(todo => ({ done: !todo.done, text: todo.text }));
assert.htmlEqual(target.innerHTML, `
<label>
<input type=checkbox>
[done] write docs
</label>
<label>
<input type=checkbox>
[done] implement contextual stores
</label>
<label>
<input type=checkbox>
[todo] go outside
</label>
`);
}
};

@ -0,0 +1,6 @@
{#each todos as todo}
<label>
<input type=checkbox on:change={e => todo.update(t => ({ done: e.target.checked, text: t.text }))}>
{$todo.done ? '[done]' : '[todo]'} {$todo.text}
</label>
{/each}

@ -0,0 +1,36 @@
export default {
html: `
<h1>0</h1>
<button>+1</button>
<button>reset</button>
`,
async test({ assert, component, target, window }) {
const buttons = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await buttons[0].dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<h1>1</h1>
<button>+1</button>
<button>reset</button>
`);
await buttons[1].dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<h1>0</h1>
<button>+1</button>
<button>reset</button>
`);
await buttons[0].dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<h1>1</h1>
<button>+1</button>
<button>reset</button>
`);
}
};

@ -0,0 +1,8 @@
<script>
import { writable } from 'svelte/store';
let foo = writable(0);
</script>
<h1>{$foo}</h1>
<button on:click="{() => foo.update(n => n + 1)}">+1</button>
<button on:click="{() => foo = writable(0)}">reset</button>

@ -19,8 +19,6 @@ describe('stats', () => {
const filename = `test/stats/samples/${dir}/input.svelte`;
const input = fs.readFileSync(filename, 'utf-8').replace(/\s+$/, '');
const expectedWarnings =
tryToLoadJson(`test/stats/samples/${dir}/warnings.json`) || [];
const expectedError = tryToLoadJson(
`test/stats/samples/${dir}/error.json`
);
@ -31,10 +29,6 @@ describe('stats', () => {
try {
result = svelte.compile(input, config.options);
config.test(assert, result.stats);
if (result.stats.warnings.length || expectedWarnings.length) {
// TODO check warnings are added to stats.warnings
}
} catch (e) {
error = e;
}

@ -1,5 +0,0 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, []);
},
};

@ -1,5 +0,0 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, []);
},
};

@ -1,5 +0,0 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, []);
},
};

@ -1,5 +0,0 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, []);
},
};

@ -1,5 +0,0 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, []);
},
};

@ -24,21 +24,19 @@ describe("validate", () => {
let error;
try {
const { stats } = svelte.compile(input, {
let { warnings } = svelte.compile(input, {
dev: config.dev,
legacy: config.legacy,
generate: false
});
const warnings = stats.warnings.map(w => ({
assert.deepEqual(warnings.map(w => ({
code: w.code,
message: w.message,
pos: w.pos,
start: w.start,
end: w.end
}));
assert.deepEqual(warnings, expected_warnings);
})), expected_warnings);
} catch (e) {
error = e;
}
@ -78,12 +76,12 @@ describe("validate", () => {
});
it("warns if options.name is not capitalised", () => {
const { stats } = svelte.compile("<div></div>", {
const { warnings } = svelte.compile("<div></div>", {
name: "lowercase",
generate: false
});
assert.deepEqual(stats.warnings.map(w => ({
assert.deepEqual(warnings.map(w => ({
code: w.code,
message: w.message
})), [{
@ -93,11 +91,11 @@ describe("validate", () => {
});
it("does not warn if options.name begins with non-alphabetic character", () => {
const { stats } = svelte.compile("<div></div>", {
const { warnings } = svelte.compile("<div></div>", {
name: "_",
generate: false
});
assert.deepEqual(stats.warnings, []);
assert.deepEqual(warnings, []);
});
});

@ -0,0 +1,60 @@
import * as fs from 'fs';
import * as assert from 'assert';
import { svelte, loadConfig, tryToLoadJson } from '../helpers.js';
describe('vars', () => {
fs.readdirSync('test/vars/samples').forEach(dir => {
if (dir[0] === '.') return;
// add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir);
const skip = /\.skip/.test(dir);
if (solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}
(solo ? it.only : skip ? it.skip : it)(dir, () => {
const config = loadConfig(`./vars/samples/${dir}/_config.js`);
const filename = `test/vars/samples/${dir}/input.svelte`;
const input = fs.readFileSync(filename, 'utf-8').replace(/\s+$/, '');
const expectedError = tryToLoadJson(
`test/vars/samples/${dir}/error.json`
);
let result;
let error;
try {
result = svelte.compile(input, config.options);
config.test(assert, result.vars);
} catch (e) {
error = e;
}
if (error || expectedError) {
if (error && !expectedError) {
throw error;
}
if (expectedError && !error) {
throw new Error(`Expected an error: ${expectedError.message}`);
}
assert.equal(error.message, expectedError.message);
assert.deepEqual(error.start, expectedError.start);
assert.deepEqual(error.end, expectedError.end);
assert.equal(error.pos, expectedError.pos);
}
});
});
it('returns a vars object when options.generate is false', () => {
const { vars } = svelte.compile('', {
generate: false
});
assert.ok(Array.isArray(vars));
});
});

@ -0,0 +1,5 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, []);
},
};

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'console',
injected: false,

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'foo',
injected: false,

@ -0,0 +1,5 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, []);
},
};

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'a',
injected: false,

@ -0,0 +1,5 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, []);
},
};

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'x',
export_name: null,

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'count',
export_name: null,

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'count',
export_name: null,

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'name',
export_name: 'name',

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'foo',
export_name: null,

@ -1,6 +1,6 @@
export default {
test(assert, stats) {
assert.deepEqual(stats.vars, [
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'foo',
export_name: null,

@ -0,0 +1,5 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, []);
},
};

@ -0,0 +1,5 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, []);
},
};
Loading…
Cancel
Save