warn on missing data

pull/1864/head
Rich Harris 7 years ago
parent 58e532fc08
commit de0830b0c5

@ -15,6 +15,7 @@ import { Node, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import error from '../utils/error'; import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import flattenReference from '../utils/flattenReference'; import flattenReference from '../utils/flattenReference';
import addToSet from '../utils/addToSet';
// We need to tell estree-walker that it should always // We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get // look for an `else` block, otherwise it might get
@ -44,31 +45,32 @@ export default class Component {
properties: Map<string, Node>; properties: Map<string, Node>;
imports: Node[]; imports: Node[] = [];
namespace: string; namespace: string;
hasComponents: boolean; hasComponents: boolean;
javascript: string; javascript: string;
declarations: string[]; declarations: string[] = [];
exports: Array<{ name: string, as: string }>; writable_declarations: Set<string> = new Set();
event_handlers: Array<{ name: string, body: string }>; initialised_declarations: Set<string> = new Set();
props: string[]; exports: Array<{ name: string, as: string }> = [];
event_handlers: Array<{ name: string, body: string }> = [];
code: MagicString; code: MagicString;
indirectDependencies: Map<string, Set<string>>; indirectDependencies: Map<string, Set<string>> = new Map();
expectedProperties: Set<string>; expectedProperties: Set<string> = new Set();
refs: Set<string>; refs: Set<string> = new Set();
file: string; file: string;
locate: (c: number) => { line: number, column: number }; locate: (c: number) => { line: number, column: number };
stylesheet: Stylesheet; stylesheet: Stylesheet;
userVars: Set<string>; userVars: Set<string> = new Set();
templateVars: Map<string, string>; templateVars: Map<string, string> = new Map();
aliases: Map<string, string>; aliases: Map<string, string> = new Map();
usedNames: Set<string>; usedNames: Set<string> = new Set();
init_uses_self = false; init_uses_self = false;
locator: (search: number, startIndex?: number) => { locator: (search: number, startIndex?: number) => {
@ -89,38 +91,17 @@ export default class Component {
this.source = source; this.source = source;
this.options = options; this.options = options;
this.imports = [];
this.declarations = [];
this.exports = [];
this.event_handlers = [];
this.refs = new Set();
this.indirectDependencies = new Map();
this.file = options.filename && ( this.file = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
); );
this.locate = getLocator(this.source); this.locate = getLocator(this.source);
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = new Set();
this.code = new MagicString(source); this.code = new MagicString(source);
// styles // styles
this.stylesheet = new Stylesheet(source, ast, options.filename, options.dev); this.stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
this.stylesheet.validate(this); this.stylesheet.validate(this);
// allow compiler to deconflict user's `import { flush } from 'whatever'` and
// Svelte's builtin `import { flush, ... } from 'svelte/internal.ts'`;
this.userVars = new Set();
this.templateVars = new Map();
this.aliases = new Map();
this.usedNames = new Set();
this.properties = new Map(); this.properties = new Map();
this.walkJs(); this.walkJs();
@ -150,6 +131,7 @@ export default class Component {
if (!this.ast.js) { if (!this.ast.js) {
this.declarations = Array.from(this.expectedProperties); this.declarations = Array.from(this.expectedProperties);
addToSet(this.writable_declarations, this.expectedProperties);
this.exports = this.declarations.map(name => ({ this.exports = this.declarations.map(name => ({
name, name,
@ -390,6 +372,9 @@ export default class Component {
this.declarations.push(name); this.declarations.push(name);
}); });
this.writable_declarations = scope.writable_declarations;
this.initialised_declarations = scope.initialised_declarations;
globals.forEach(name => { globals.forEach(name => {
this.userVars.add(name); this.userVars.add(name);
}); });
@ -401,7 +386,7 @@ export default class Component {
this.error(node, { this.error(node, {
code: `default-export`, code: `default-export`,
message: `A component cannot have a default export` message: `A component cannot have a default export`
}) });
} }
if (node.type === 'ExportNamedDeclaration') { if (node.type === 'ExportNamedDeclaration') {

@ -61,7 +61,8 @@ export default function dom(
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
if (component.customElement) { if (component.customElement) {
const props = component.props || Array.from(component.expectedProperties); // TODO use `export` to determine this
const props = Array.from(component.expectedProperties);
builder.addBlock(deindent` builder.addBlock(deindent`
class ${name} extends HTMLElement { class ${name} extends HTMLElement {
@ -118,8 +119,8 @@ export default function dom(
); );
} }
builder.addBlock(deindent` const body = [
class ${name} extends ${superclass} { deindent`
$$init($$make_dirty) { $$init($$make_dirty) {
${component.init_uses_self && `const $$self = this;`} ${component.init_uses_self && `const $$self = this;`}
${component.javascript || component.exports.map(x => `let ${x.name};`)} ${component.javascript || component.exports.map(x => `let ${x.name};`)}
@ -145,17 +146,59 @@ export default function dom(
$$create_fragment(${component.alias('component')}, ctx) { $$create_fragment(${component.alias('component')}, ctx) {
${block.getContents()} ${block.getContents()}
} }
`
];
if (component.options.dev) {
// TODO check no uunexpected props were passed, as well as
// checking that expected ones were passed
const expected = component.exports
.map(x => x.name)
.filter(name => !component.initialised_declarations.has(name));
if (expected.length) {
const debug_name = `<${component.customElement ? component.tag : name}>`;
body.push(deindent`
$$checkProps() {
const state = this.$$.get_state();
${expected.map(name => deindent`
if (state.${name} === undefined) {
console.warn("${debug_name} was created without expected data property '${name}'");
}
`)}
}
`);
}
}
${component.exports.map(x => deindent` component.exports.forEach(x => {
body.push(deindent`
get ${x.as}() { get ${x.as}() {
return this.$$.get_state().${x.name}; return this.$$.get_state().${x.name};
} }
`);
set ${x.as}(value) { if (component.writable_declarations.has(x.as)) {
this.$set({ ${x.name}: value }); body.push(deindent`
@flush(); set ${x.as}(value) {
} this.$set({ ${x.name}: value });
`)} @flush();
}
`);
} else if (component.options.dev) {
body.push(deindent`
set ${x.as}(value) {
throw new Error("${x.as} is read-only");
}
`);
}
});
builder.addBlock(deindent`
class ${name} extends ${superclass} {
${body.join('\n\n')}
} }
`); `);
} }

@ -116,6 +116,7 @@ export class $$ComponentDev extends $$Component {
} }
super(options); super(options);
this.$$checkProps();
} }
$destroy() { $destroy() {
@ -124,4 +125,8 @@ export class $$ComponentDev extends $$Component {
console.warn(`Component was already destroyed`); console.warn(`Component was already destroyed`);
}; };
} }
$$checkProps() {
// noop by default
}
} }

@ -54,21 +54,28 @@ export function createScopes(expression: Node) {
export class Scope { export class Scope {
parent: Scope; parent: Scope;
block: boolean; block: boolean;
declarations: Set<string>;
declarations: Set<string> = new Set();
writable_declarations: Set<string> = new Set();
initialised_declarations: Set<string> = new Set();
constructor(parent: Scope, block: boolean) { constructor(parent: Scope, block: boolean) {
this.parent = parent; this.parent = parent;
this.block = block; this.block = block;
this.declarations = new Set();
} }
addDeclaration(node: Node) { addDeclaration(node: Node) {
if (node.kind === 'var' && !this.block && this.parent) { if (node.kind === 'var' && !this.block && this.parent) {
this.parent.addDeclaration(node); this.parent.addDeclaration(node);
} else if (node.type === 'VariableDeclaration') { } else if (node.type === 'VariableDeclaration') {
const writable = node.kind !== 'const';
const initialised = !!node.init;
node.declarations.forEach((declarator: Node) => { node.declarations.forEach((declarator: Node) => {
extractNames(declarator.id).forEach(name => { extractNames(declarator.id).forEach(name => {
this.declarations.add(name); this.declarations.add(name);
if (writable) this.writable_declarations.add(name);
if (initialised) this.initialised_declarations.add(name);
}); });
}); });
} else { } else {

@ -1,4 +1,6 @@
export default { export default {
solo: 1,
compileOptions: { compileOptions: {
dev: true dev: true
}, },

Loading…
Cancel
Save