mirror of https://github.com/sveltejs/svelte
1531 lines
38 KiB
1531 lines
38 KiB
import { walk, childKeys } from 'estree-walker';
|
|
import { getLocator } from 'locate-character';
|
|
import Stats from '../Stats';
|
|
import { globals, reserved, is_valid } from '../utils/names';
|
|
import { namespaces, valid_namespaces } from '../utils/namespaces';
|
|
import create_module from './create_module';
|
|
import {
|
|
create_scopes,
|
|
extract_names,
|
|
Scope,
|
|
extract_identifiers,
|
|
} from './utils/scope';
|
|
import Stylesheet from './css/Stylesheet';
|
|
import { test } from '../config';
|
|
import Fragment from './nodes/Fragment';
|
|
import internal_exports from './internal_exports';
|
|
import { Ast, CompileOptions, Var, Warning } from '../interfaces';
|
|
import error from '../utils/error';
|
|
import get_code_frame from '../utils/get_code_frame';
|
|
import flatten_reference from './utils/flatten_reference';
|
|
import is_used_as_reference from './utils/is_used_as_reference';
|
|
import is_reference from 'is-reference';
|
|
import TemplateScope from './nodes/shared/TemplateScope';
|
|
import fuzzymatch from '../utils/fuzzymatch';
|
|
import get_object from './utils/get_object';
|
|
import Slot from './nodes/Slot';
|
|
import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal } from 'estree';
|
|
import add_to_set from './utils/add_to_set';
|
|
import check_graph_for_cycles from './utils/check_graph_for_cycles';
|
|
import { print, x, b } from 'code-red';
|
|
|
|
interface ComponentOptions {
|
|
namespace?: string;
|
|
tag?: string;
|
|
immutable?: boolean;
|
|
accessors?: boolean;
|
|
preserveWhitespace?: boolean;
|
|
}
|
|
|
|
// 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 = childKeys.IfBlock = ['children', 'else'];
|
|
childKeys.Attribute = ['value'];
|
|
childKeys.ExportNamedDeclaration = ['declaration', 'specifiers'];
|
|
|
|
export default class Component {
|
|
stats: Stats;
|
|
warnings: Warning[];
|
|
ignores: Set<string>;
|
|
ignore_stack: Array<Set<string>> = [];
|
|
|
|
ast: Ast;
|
|
original_ast: Ast;
|
|
source: string;
|
|
name: Identifier;
|
|
compile_options: CompileOptions;
|
|
fragment: Fragment;
|
|
module_scope: Scope;
|
|
instance_scope: Scope;
|
|
instance_scope_map: WeakMap<Node, Scope>;
|
|
|
|
component_options: ComponentOptions;
|
|
namespace: string;
|
|
tag: string;
|
|
accessors: boolean;
|
|
|
|
vars: Var[] = [];
|
|
var_lookup: Map<string, Var> = new Map();
|
|
|
|
imports: ImportDeclaration[] = [];
|
|
|
|
hoistable_nodes: Set<Node> = new Set();
|
|
node_for_declaration: Map<string, Node> = new Map();
|
|
partly_hoisted: Array<(Node | Node[])> = [];
|
|
fully_hoisted: Array<(Node | Node[])> = [];
|
|
reactive_declarations: Array<{
|
|
assignees: Set<string>;
|
|
dependencies: Set<string>;
|
|
node: Node;
|
|
declaration: Node;
|
|
}> = [];
|
|
reactive_declaration_nodes: Set<Node> = new Set();
|
|
has_reactive_assignments = false;
|
|
injected_reactive_declaration_vars: Set<string> = new Set();
|
|
helpers: Map<string, Identifier> = new Map();
|
|
globals: Map<string, Identifier> = new Map();
|
|
|
|
indirect_dependencies: Map<string, Set<string>> = new Map();
|
|
|
|
file: string;
|
|
locate: (c: number) => { line: number; column: number };
|
|
|
|
stylesheet: Stylesheet;
|
|
|
|
aliases: Map<string, Identifier> = new Map();
|
|
used_names: Set<string> = new Set();
|
|
globally_used_names: Set<string> = new Set();
|
|
|
|
slots: Map<string, Slot> = new Map();
|
|
slot_outlets: Set<string> = new Set();
|
|
|
|
constructor(
|
|
ast: Ast,
|
|
source: string,
|
|
name: string,
|
|
compile_options: CompileOptions,
|
|
stats: Stats,
|
|
warnings: Warning[]
|
|
) {
|
|
this.name = { type: 'Identifier', name };
|
|
|
|
this.stats = stats;
|
|
this.warnings = warnings;
|
|
this.ast = ast;
|
|
this.source = source;
|
|
this.compile_options = compile_options;
|
|
|
|
// the instance JS gets mutated, so we park
|
|
// a copy here for later. TODO this feels gross
|
|
this.original_ast = {
|
|
html: ast.html,
|
|
css: ast.css,
|
|
instance: ast.instance && JSON.parse(JSON.stringify(ast.instance)),
|
|
module: ast.module
|
|
};
|
|
|
|
this.file =
|
|
compile_options.filename &&
|
|
(typeof process !== 'undefined'
|
|
? compile_options.filename
|
|
.replace(process.cwd(), '')
|
|
.replace(/^[/\\]/, '')
|
|
: compile_options.filename);
|
|
this.locate = getLocator(this.source, { offsetLine: 1 });
|
|
|
|
// styles
|
|
this.stylesheet = new Stylesheet(
|
|
source,
|
|
ast,
|
|
compile_options.filename,
|
|
compile_options.dev
|
|
);
|
|
this.stylesheet.validate(this);
|
|
|
|
this.component_options = process_component_options(
|
|
this,
|
|
this.ast.html.children
|
|
);
|
|
this.namespace =
|
|
namespaces[this.component_options.namespace] ||
|
|
this.component_options.namespace;
|
|
|
|
if (compile_options.customElement) {
|
|
if (
|
|
this.component_options.tag === undefined &&
|
|
compile_options.tag === undefined
|
|
) {
|
|
const svelteOptions = ast.html.children.find(
|
|
child => child.name === 'svelte:options'
|
|
) || { start: 0, end: 0 };
|
|
this.warn(svelteOptions, {
|
|
code: 'custom-element-no-tag',
|
|
message: `No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>`,
|
|
});
|
|
}
|
|
this.tag = this.component_options.tag || compile_options.tag;
|
|
} else {
|
|
this.tag = this.name.name;
|
|
}
|
|
|
|
this.walk_module_js_pre_template();
|
|
this.walk_instance_js_pre_template();
|
|
|
|
this.fragment = new Fragment(this, ast.html);
|
|
this.name = this.get_unique_name(name);
|
|
|
|
this.walk_module_js_post_template();
|
|
this.walk_instance_js_post_template();
|
|
|
|
if (!compile_options.customElement) this.stylesheet.reify();
|
|
|
|
this.stylesheet.warn_on_unused_selectors(this);
|
|
}
|
|
|
|
add_var(variable: Var) {
|
|
this.vars.push(variable);
|
|
this.var_lookup.set(variable.name, variable);
|
|
}
|
|
|
|
add_reference(name: string) {
|
|
const variable = this.var_lookup.get(name);
|
|
|
|
if (variable) {
|
|
variable.referenced = true;
|
|
} else if (name === '$$props') {
|
|
this.add_var({
|
|
name,
|
|
injected: true,
|
|
referenced: true,
|
|
});
|
|
} else if (name[0] === '$') {
|
|
this.add_var({
|
|
name,
|
|
injected: true,
|
|
referenced: true,
|
|
mutated: true,
|
|
writable: true,
|
|
});
|
|
|
|
const subscribable_name = name.slice(1);
|
|
this.add_reference(subscribable_name);
|
|
|
|
const variable = this.var_lookup.get(subscribable_name);
|
|
if (variable) variable.subscribable = true;
|
|
} else {
|
|
this.used_names.add(name);
|
|
}
|
|
}
|
|
|
|
alias(name: string) {
|
|
if (!this.aliases.has(name)) {
|
|
this.aliases.set(name, this.get_unique_name(name));
|
|
}
|
|
|
|
return this.aliases.get(name);
|
|
}
|
|
|
|
global(name: string) {
|
|
const alias = this.alias(name);
|
|
this.globals.set(name, alias);
|
|
return alias;
|
|
}
|
|
|
|
generate(result?: Node[]) {
|
|
let js = null;
|
|
let css = null;
|
|
|
|
if (result) {
|
|
const { compile_options, name } = this;
|
|
const { format = 'esm' } = compile_options;
|
|
|
|
const banner = `${this.file ? `${this.file} ` : ``}generated by Svelte v${'__VERSION__'}`;
|
|
|
|
const program: any = { type: 'Program', body: result };
|
|
|
|
walk(program, {
|
|
enter: (node, parent, key) => {
|
|
if (node.type === 'Identifier') {
|
|
if (node.name[0] === '@') {
|
|
if (node.name[1] === '_') {
|
|
const alias = this.global(node.name.slice(2));
|
|
node.name = alias.name;
|
|
} else {
|
|
let name = node.name.slice(1);
|
|
|
|
if (compile_options.dev) {
|
|
if (internal_exports.has(`${name}_dev`)) {
|
|
name += '_dev';
|
|
} else if (internal_exports.has(`${name}Dev`)) {
|
|
name += 'Dev';
|
|
}
|
|
}
|
|
|
|
const alias = this.alias(name);
|
|
this.helpers.set(name, alias);
|
|
node.name = alias.name;
|
|
}
|
|
}
|
|
|
|
else if (node.name[0] !== '#' && !is_valid(node.name)) {
|
|
// this hack allows x`foo.${bar}` where bar could be invalid
|
|
const literal: Literal = { type: 'Literal', value: node.name };
|
|
|
|
if (parent.type === 'Property' && key === 'key') {
|
|
parent.key = literal;
|
|
}
|
|
|
|
else if (parent.type === 'MemberExpression' && key === 'property') {
|
|
parent.property = literal;
|
|
parent.computed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const referenced_globals = Array.from(
|
|
this.globals,
|
|
([name, alias]) => name !== alias.name && { name, alias }
|
|
).filter(Boolean);
|
|
if (referenced_globals.length) {
|
|
this.helpers.set('globals', this.alias('globals'));
|
|
}
|
|
const imported_helpers = Array.from(this.helpers, ([name, alias]) => ({
|
|
name,
|
|
alias,
|
|
}));
|
|
|
|
create_module(
|
|
program,
|
|
format,
|
|
name,
|
|
banner,
|
|
compile_options.sveltePath,
|
|
imported_helpers,
|
|
referenced_globals,
|
|
this.imports,
|
|
this.vars
|
|
.filter(variable => variable.module && variable.export_name)
|
|
.map(variable => ({
|
|
name: variable.name,
|
|
as: variable.export_name,
|
|
}))
|
|
);
|
|
|
|
css = compile_options.customElement
|
|
? { code: null, map: null }
|
|
: this.stylesheet.render(compile_options.cssOutputFilename, true);
|
|
|
|
js = print(program, {
|
|
sourceMapSource: compile_options.filename
|
|
});
|
|
|
|
js.map.sources = [
|
|
compile_options.filename ? get_relative_path(compile_options.outputFilename || '', compile_options.filename) : null
|
|
];
|
|
|
|
js.map.sourcesContent = [
|
|
this.source
|
|
];
|
|
}
|
|
|
|
return {
|
|
js,
|
|
css,
|
|
ast: this.original_ast,
|
|
warnings: this.warnings,
|
|
vars: this.vars
|
|
.filter(v => !v.global && !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,
|
|
referenced_from_script: v.referenced_from_script || false,
|
|
})),
|
|
stats: this.stats.render(),
|
|
};
|
|
}
|
|
|
|
get_unique_name(name: string): Identifier {
|
|
if (test) name = `${name}$`;
|
|
let alias = name;
|
|
for (
|
|
let i = 1;
|
|
reserved.has(alias) ||
|
|
this.var_lookup.has(alias) ||
|
|
this.used_names.has(alias) ||
|
|
this.globally_used_names.has(alias);
|
|
alias = `${name}_${i++}`
|
|
);
|
|
this.used_names.add(alias);
|
|
return { type: 'Identifier', name: alias };
|
|
}
|
|
|
|
get_unique_name_maker() {
|
|
const local_used_names = new Set();
|
|
|
|
function add(name: string) {
|
|
local_used_names.add(name);
|
|
}
|
|
|
|
reserved.forEach(add);
|
|
internal_exports.forEach(add);
|
|
this.var_lookup.forEach((_value, key) => add(key));
|
|
|
|
return (name: string): Identifier => {
|
|
if (test) name = `${name}$`;
|
|
let alias = name;
|
|
for (
|
|
let i = 1;
|
|
this.used_names.has(alias) || local_used_names.has(alias);
|
|
alias = `${name}_${i++}`
|
|
);
|
|
local_used_names.add(alias);
|
|
this.globally_used_names.add(alias);
|
|
|
|
return {
|
|
type: 'Identifier',
|
|
name: alias
|
|
};
|
|
};
|
|
}
|
|
|
|
error(
|
|
pos: {
|
|
start: number;
|
|
end: number;
|
|
},
|
|
e: {
|
|
code: string;
|
|
message: string;
|
|
}
|
|
) {
|
|
error(e.message, {
|
|
name: 'ValidationError',
|
|
code: e.code,
|
|
source: this.source,
|
|
start: pos.start,
|
|
end: pos.end,
|
|
filename: this.compile_options.filename,
|
|
});
|
|
}
|
|
|
|
warn(
|
|
pos: {
|
|
start: number;
|
|
end: number;
|
|
},
|
|
warning: {
|
|
code: string;
|
|
message: string;
|
|
}
|
|
) {
|
|
if (this.ignores && this.ignores.has(warning.code)) {
|
|
return;
|
|
}
|
|
|
|
const start = this.locate(pos.start);
|
|
const end = this.locate(pos.end);
|
|
|
|
const frame = get_code_frame(this.source, start.line - 1, start.column);
|
|
|
|
this.warnings.push({
|
|
code: warning.code,
|
|
message: warning.message,
|
|
frame,
|
|
start,
|
|
end,
|
|
pos: pos.start,
|
|
filename: this.compile_options.filename,
|
|
toString: () =>
|
|
`${warning.message} (${start.line}:${start.column})\n${frame}`,
|
|
});
|
|
}
|
|
|
|
extract_imports(node) {
|
|
this.imports.push(node);
|
|
}
|
|
|
|
extract_exports(node) {
|
|
if (node.type === 'ExportDefaultDeclaration') {
|
|
this.error(node, {
|
|
code: `default-export`,
|
|
message: `A component cannot have a default export`,
|
|
});
|
|
}
|
|
|
|
if (node.type === 'ExportNamedDeclaration') {
|
|
if (node.source) {
|
|
this.error(node, {
|
|
code: `not-implemented`,
|
|
message: `A component currently cannot have an export ... from`,
|
|
});
|
|
}
|
|
if (node.declaration) {
|
|
if (node.declaration.type === 'VariableDeclaration') {
|
|
node.declaration.declarations.forEach(declarator => {
|
|
extract_names(declarator.id).forEach(name => {
|
|
const variable = this.var_lookup.get(name);
|
|
variable.export_name = name;
|
|
if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
|
|
this.warn(declarator, {
|
|
code: `unused-export-let`,
|
|
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const '${name}'\``
|
|
});
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
const { name } = node.declaration.id;
|
|
|
|
const variable = this.var_lookup.get(name);
|
|
variable.export_name = name;
|
|
}
|
|
|
|
return node.declaration;
|
|
} else {
|
|
node.specifiers.forEach(specifier => {
|
|
const variable = this.var_lookup.get(specifier.local.name);
|
|
|
|
if (variable) {
|
|
variable.export_name = specifier.exported.name;
|
|
|
|
if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
|
|
this.warn(specifier, {
|
|
code: `unused-export-let`,
|
|
message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const '${specifier.exported.name}'\``
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
extract_javascript(script) {
|
|
if (!script) return null;
|
|
|
|
return script.content.body.filter(node => {
|
|
if (!node) return false;
|
|
if (this.hoistable_nodes.has(node)) return false;
|
|
if (this.reactive_declaration_nodes.has(node)) return false;
|
|
if (node.type === 'ImportDeclaration') return false;
|
|
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0)
|
|
return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
walk_module_js_pre_template() {
|
|
const component = this;
|
|
const script = this.ast.module;
|
|
if (!script) return;
|
|
|
|
walk(script.content, {
|
|
enter(node) {
|
|
if (node.type === 'LabeledStatement' && node.label.name === '$') {
|
|
component.warn(node as any, {
|
|
code: 'module-script-reactive-declaration',
|
|
message: '$: has no effect in a module script',
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
const { scope, globals } = create_scopes(script.content);
|
|
this.module_scope = scope;
|
|
|
|
scope.declarations.forEach((node, name) => {
|
|
if (name[0] === '$') {
|
|
this.error(node as any, {
|
|
code: 'illegal-declaration',
|
|
message: `The $ prefix is reserved, and cannot be used for variable and import names`,
|
|
});
|
|
}
|
|
|
|
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
|
|
|
|
this.add_var({
|
|
name,
|
|
module: true,
|
|
hoistable: true,
|
|
writable
|
|
});
|
|
});
|
|
|
|
globals.forEach((node, name) => {
|
|
if (name[0] === '$') {
|
|
this.error(node as any, {
|
|
code: 'illegal-subscription',
|
|
message: `Cannot reference store value inside <script context="module">`,
|
|
});
|
|
} else {
|
|
this.add_var({
|
|
name,
|
|
global: true,
|
|
hoistable: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
walk_instance_js_pre_template() {
|
|
const script = this.ast.instance;
|
|
if (!script) return;
|
|
|
|
// inject vars for reactive declarations
|
|
script.content.body.forEach(node => {
|
|
if (node.type !== 'LabeledStatement') return;
|
|
if (node.body.type !== 'ExpressionStatement') return;
|
|
|
|
const { expression } = node.body;
|
|
if (expression.type !== 'AssignmentExpression') return;
|
|
|
|
extract_names(expression.left).forEach(name => {
|
|
if (!this.var_lookup.has(name) && name[0] !== '$') {
|
|
this.injected_reactive_declaration_vars.add(name);
|
|
}
|
|
});
|
|
});
|
|
|
|
const { scope: instance_scope, map, globals } = create_scopes(
|
|
script.content
|
|
);
|
|
this.instance_scope = instance_scope;
|
|
this.instance_scope_map = map;
|
|
|
|
instance_scope.declarations.forEach((node, name) => {
|
|
if (name[0] === '$') {
|
|
this.error(node as any, {
|
|
code: 'illegal-declaration',
|
|
message: `The $ prefix is reserved, and cannot be used for variable and import names`,
|
|
});
|
|
}
|
|
|
|
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
|
|
|
|
this.add_var({
|
|
name,
|
|
initialised: instance_scope.initialised_declarations.has(name),
|
|
hoistable: /^Import/.test(node.type),
|
|
writable
|
|
});
|
|
|
|
this.node_for_declaration.set(name, node);
|
|
});
|
|
|
|
globals.forEach((node, name) => {
|
|
if (this.var_lookup.has(name)) return;
|
|
|
|
if (this.injected_reactive_declaration_vars.has(name)) {
|
|
this.add_var({
|
|
name,
|
|
injected: true,
|
|
writable: true,
|
|
reassigned: true,
|
|
initialised: true,
|
|
});
|
|
} else if (name === '$$props') {
|
|
this.add_var({
|
|
name,
|
|
injected: true,
|
|
});
|
|
} else if (name[0] === '$') {
|
|
if (name === '$' || name[1] === '$') {
|
|
this.error(node as any, {
|
|
code: 'illegal-global',
|
|
message: `${name} is an illegal variable name`
|
|
});
|
|
}
|
|
|
|
this.add_var({
|
|
name,
|
|
injected: true,
|
|
mutated: true,
|
|
writable: true,
|
|
});
|
|
|
|
this.add_reference(name.slice(1));
|
|
|
|
const variable = this.var_lookup.get(name.slice(1));
|
|
if (variable) {
|
|
variable.subscribable = true;
|
|
variable.referenced_from_script = true;
|
|
}
|
|
} else {
|
|
this.add_var({
|
|
name,
|
|
global: true,
|
|
hoistable: true
|
|
});
|
|
}
|
|
});
|
|
|
|
this.track_references_and_mutations();
|
|
}
|
|
|
|
walk_module_js_post_template() {
|
|
const script = this.ast.module;
|
|
if (!script) return;
|
|
|
|
const { body } = script.content;
|
|
let i = body.length;
|
|
while (--i >= 0) {
|
|
const node = body[i];
|
|
if (node.type === 'ImportDeclaration') {
|
|
this.extract_imports(node);
|
|
body.splice(i, 1);
|
|
}
|
|
|
|
if (/^Export/.test(node.type)) {
|
|
const replacement = this.extract_exports(node);
|
|
if (replacement) {
|
|
body[i] = replacement;
|
|
} else {
|
|
body.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk_instance_js_post_template() {
|
|
const script = this.ast.instance;
|
|
if (!script) return;
|
|
|
|
this.post_template_walk();
|
|
|
|
this.hoist_instance_declarations();
|
|
this.extract_reactive_declarations();
|
|
}
|
|
|
|
post_template_walk() {
|
|
const script = this.ast.instance;
|
|
if (!script) return;
|
|
|
|
const component = this;
|
|
const { content } = script;
|
|
const { instance_scope, instance_scope_map: map } = this;
|
|
|
|
let scope = instance_scope;
|
|
|
|
const toRemove = [];
|
|
const remove = (parent, prop, index) => {
|
|
toRemove.unshift([parent, prop, index]);
|
|
};
|
|
|
|
const toInsert = new Map();
|
|
|
|
walk(content, {
|
|
enter(node, parent, prop, index) {
|
|
if (map.has(node)) {
|
|
scope = map.get(node);
|
|
}
|
|
|
|
if (node.type === 'ImportDeclaration') {
|
|
component.extract_imports(node);
|
|
// TODO: to use actual remove
|
|
remove(parent, prop, index);
|
|
return this.skip();
|
|
}
|
|
|
|
if (/^Export/.test(node.type)) {
|
|
const replacement = component.extract_exports(node);
|
|
if (replacement) {
|
|
this.replace(replacement);
|
|
} else {
|
|
// TODO: to use actual remove
|
|
remove(parent, prop, index);
|
|
}
|
|
return this.skip();
|
|
}
|
|
|
|
component.warn_on_undefined_store_value_references(node, parent, scope);
|
|
|
|
if (component.compile_options.dev) {
|
|
const to_insert_for_loop_protect = component.loop_protect(node, prop, index);
|
|
if (to_insert_for_loop_protect) {
|
|
if (!Array.isArray(parent[prop])) {
|
|
parent[prop] = {
|
|
type: 'BlockStatement',
|
|
body: [to_insert_for_loop_protect.node, node],
|
|
};
|
|
} else {
|
|
// can't insert directly, will screw up the index in the for-loop of estree-walker
|
|
if (!toInsert.has(parent)) {
|
|
toInsert.set(parent, []);
|
|
}
|
|
toInsert.get(parent).push(to_insert_for_loop_protect);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
leave(node) {
|
|
if (map.has(node)) {
|
|
scope = scope.parent;
|
|
}
|
|
if (toInsert.has(node)) {
|
|
const nodes_to_insert = toInsert.get(node);
|
|
for (const { index, prop, node: node_to_insert } of nodes_to_insert.reverse()) {
|
|
node[prop].splice(index, 0, node_to_insert);
|
|
}
|
|
toInsert.delete(node);
|
|
}
|
|
},
|
|
});
|
|
|
|
for (const [parent, prop, index] of toRemove) {
|
|
if (parent) {
|
|
if (index !== null) {
|
|
parent[prop].splice(index, 1);
|
|
} else {
|
|
delete parent[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
track_references_and_mutations() {
|
|
const script = this.ast.instance;
|
|
if (!script) return;
|
|
|
|
const component = this;
|
|
const { content } = script;
|
|
const { instance_scope, instance_scope_map: map } = this;
|
|
|
|
let scope = instance_scope;
|
|
|
|
walk(content, {
|
|
enter(node, parent) {
|
|
if (map.has(node)) {
|
|
scope = map.get(node);
|
|
}
|
|
|
|
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
|
|
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
|
|
const names = extract_names(assignee);
|
|
|
|
const deep = assignee.type === 'MemberExpression';
|
|
|
|
names.forEach(name => {
|
|
if (scope.find_owner(name) === instance_scope) {
|
|
const variable = component.var_lookup.get(name);
|
|
variable[deep ? 'mutated' : 'reassigned'] = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (is_used_as_reference(node, parent)) {
|
|
const object = get_object(node);
|
|
if (scope.find_owner(object.name) === instance_scope) {
|
|
const variable = component.var_lookup.get(object.name);
|
|
variable.referenced_from_script = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
leave(node) {
|
|
if (map.has(node)) {
|
|
scope = scope.parent;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
warn_on_undefined_store_value_references(node, parent, scope) {
|
|
if (
|
|
node.type === 'LabeledStatement' &&
|
|
node.label.name === '$' &&
|
|
parent.type !== 'Program'
|
|
) {
|
|
this.warn(node as any, {
|
|
code: 'non-top-level-reactive-declaration',
|
|
message: '$: has no effect outside of the top-level',
|
|
});
|
|
}
|
|
|
|
if (is_reference(node as Node, parent as Node)) {
|
|
const object = get_object(node);
|
|
const { name } = object;
|
|
|
|
if (name[0] === '$' && !scope.has(name)) {
|
|
this.warn_if_undefined(name, object, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
loop_protect(node, prop, index) {
|
|
if (node.type === 'WhileStatement' ||
|
|
node.type === 'ForStatement' ||
|
|
node.type === 'DoWhileStatement') {
|
|
const id = this.get_unique_name('LP');
|
|
this.add_var({
|
|
name: id.name,
|
|
internal: true,
|
|
});
|
|
|
|
const before = b`const ${id} = Date.now();`;
|
|
const inside = b`
|
|
if (Date.now() - ${id} > 100) {
|
|
throw new Error('Infinite loop detected');
|
|
}
|
|
`;
|
|
// wrap expression statement with BlockStatement
|
|
if (node.body.type !== 'BlockStatement') {
|
|
node.body = {
|
|
type: 'BlockStatement',
|
|
body: [node.body],
|
|
};
|
|
}
|
|
node.body.body.push(inside[0]);
|
|
return { index, prop, node: before[0] };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
invalidate(name, value?) {
|
|
const variable = this.var_lookup.get(name);
|
|
|
|
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
|
|
return x`${`$$subscribe_${name}`}($$invalidate('${name}', ${value || name}))`;
|
|
}
|
|
|
|
if (name[0] === '$' && name[1] !== '$') {
|
|
return x`${name.slice(1)}.set(${value || name})`;
|
|
}
|
|
|
|
if (
|
|
variable &&
|
|
!variable.referenced &&
|
|
!variable.is_reactive_dependency &&
|
|
!variable.export_name &&
|
|
!name.startsWith('$$')
|
|
) {
|
|
return value || name;
|
|
}
|
|
|
|
if (value) {
|
|
return x`$$invalidate('${name}', ${value})`;
|
|
}
|
|
|
|
// if this is a reactive declaration, invalidate dependencies recursively
|
|
const deps = new Set([name]);
|
|
|
|
deps.forEach(name => {
|
|
const reactive_declarations = this.reactive_declarations.filter(x =>
|
|
x.assignees.has(name)
|
|
);
|
|
reactive_declarations.forEach(declaration => {
|
|
declaration.dependencies.forEach(name => {
|
|
deps.add(name);
|
|
});
|
|
});
|
|
});
|
|
|
|
return Array.from(deps)
|
|
.map(n => x`$$invalidate('${n}', ${n})`)
|
|
.reduce((lhs, rhs) => x`${lhs}, ${rhs}}`);
|
|
}
|
|
|
|
rewrite_props(get_insert: (variable: Var) => Node[]) {
|
|
if (!this.ast.instance) return;
|
|
|
|
const component = this;
|
|
const { instance_scope, instance_scope_map: map } = this;
|
|
let scope = instance_scope;
|
|
|
|
walk(this.ast.instance.content, {
|
|
enter(node, parent, key, index) {
|
|
if (/Function/.test(node.type)) {
|
|
return this.skip();
|
|
}
|
|
|
|
if (map.has(node)) {
|
|
scope = map.get(node);
|
|
}
|
|
|
|
if (node.type === 'VariableDeclaration') {
|
|
if (node.kind === 'var' || scope === instance_scope) {
|
|
node.declarations.forEach(declarator => {
|
|
if (declarator.id.type !== 'Identifier') {
|
|
const inserts = [];
|
|
|
|
extract_names(declarator.id).forEach(name => {
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
if (variable.export_name) {
|
|
// TODO is this still true post-#3539?
|
|
component.error(declarator as any, {
|
|
code: 'destructured-prop',
|
|
message: `Cannot declare props in destructured declaration`,
|
|
});
|
|
}
|
|
|
|
if (variable.subscribable) {
|
|
inserts.push(get_insert(variable));
|
|
}
|
|
});
|
|
|
|
if (inserts.length) {
|
|
parent[key].splice(index + 1, 0, ...inserts);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const { name } = declarator.id;
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
if (variable.export_name && variable.writable) {
|
|
const insert = variable.subscribable
|
|
? get_insert(variable)
|
|
: null;
|
|
|
|
parent[key].splice(index + 1, 0, insert);
|
|
|
|
declarator.id = {
|
|
type: 'ObjectPattern',
|
|
properties: [{
|
|
type: 'Property',
|
|
method: false,
|
|
shorthand: false,
|
|
computed: false,
|
|
kind: 'init',
|
|
key: { type: 'Identifier', name: variable.export_name },
|
|
value: declarator.init
|
|
? {
|
|
type: 'AssignmentPattern',
|
|
left: declarator.id,
|
|
right: declarator.init
|
|
}
|
|
: declarator.id
|
|
}]
|
|
};
|
|
|
|
declarator.init = x`$$props`;
|
|
} else if (variable.subscribable) {
|
|
const insert = get_insert(variable);
|
|
parent[key].splice(index + 1, 0, ...insert);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
leave(node, parent, _key, index) {
|
|
if (map.has(node)) {
|
|
scope = scope.parent;
|
|
}
|
|
|
|
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
(parent as Program).body[index] = node.declaration;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
hoist_instance_declarations() {
|
|
// we can safely hoist variable declarations that are
|
|
// initialised to literals, and functions that don't
|
|
// reference instance variables other than other
|
|
// hoistable functions. TODO others?
|
|
|
|
const {
|
|
hoistable_nodes,
|
|
var_lookup,
|
|
injected_reactive_declaration_vars,
|
|
} = this;
|
|
|
|
const top_level_function_declarations = new Map();
|
|
|
|
const { body } = this.ast.instance.content;
|
|
|
|
for (let i = 0; i < body.length; i += 1) {
|
|
const node = body[i];
|
|
|
|
if (node.type === 'VariableDeclaration') {
|
|
const all_hoistable = node.declarations.every(d => {
|
|
if (!d.init) return false;
|
|
if (d.init.type !== 'Literal') return false;
|
|
|
|
const { name } = d.id as Identifier;
|
|
|
|
const v = this.var_lookup.get(name);
|
|
if (v.reassigned) return false;
|
|
if (v.export_name) return false;
|
|
|
|
if (this.var_lookup.get(name).reassigned) return false;
|
|
if (
|
|
this.vars.find(
|
|
variable => variable.name === name && variable.module
|
|
)
|
|
)
|
|
return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
if (all_hoistable) {
|
|
node.declarations.forEach(d => {
|
|
const variable = this.var_lookup.get((d.id as Identifier).name);
|
|
variable.hoistable = true;
|
|
});
|
|
|
|
hoistable_nodes.add(node);
|
|
|
|
body.splice(i--, 1);
|
|
this.fully_hoisted.push(node);
|
|
}
|
|
}
|
|
|
|
if (
|
|
node.type === 'ExportNamedDeclaration' &&
|
|
node.declaration &&
|
|
node.declaration.type === 'FunctionDeclaration'
|
|
) {
|
|
top_level_function_declarations.set(node.declaration.id.name, node);
|
|
}
|
|
|
|
if (node.type === 'FunctionDeclaration') {
|
|
top_level_function_declarations.set(node.id.name, node);
|
|
}
|
|
}
|
|
|
|
const checked = new Set();
|
|
const walking = new Set();
|
|
|
|
const is_hoistable = fn_declaration => {
|
|
if (fn_declaration.type === 'ExportNamedDeclaration') {
|
|
fn_declaration = fn_declaration.declaration;
|
|
}
|
|
|
|
const instance_scope = this.instance_scope;
|
|
let scope = this.instance_scope;
|
|
const map = this.instance_scope_map;
|
|
|
|
let hoistable = true;
|
|
|
|
// handle cycles
|
|
walking.add(fn_declaration);
|
|
|
|
walk(fn_declaration, {
|
|
enter(node, parent) {
|
|
if (!hoistable) return this.skip();
|
|
|
|
if (map.has(node)) {
|
|
scope = map.get(node);
|
|
}
|
|
|
|
if (is_reference(node as Node, parent as Node)) {
|
|
const { name } = flatten_reference(node);
|
|
const owner = scope.find_owner(name);
|
|
|
|
if (injected_reactive_declaration_vars.has(name)) {
|
|
hoistable = false;
|
|
} else if (name[0] === '$' && !owner) {
|
|
hoistable = false;
|
|
} else if (owner === instance_scope) {
|
|
const variable = var_lookup.get(name);
|
|
|
|
if (variable.reassigned || variable.mutated) hoistable = false;
|
|
|
|
if (name === fn_declaration.id.name) return;
|
|
|
|
if (variable.hoistable) return;
|
|
|
|
if (top_level_function_declarations.has(name)) {
|
|
const other_declaration = top_level_function_declarations.get(
|
|
name
|
|
);
|
|
|
|
if (walking.has(other_declaration)) {
|
|
hoistable = false;
|
|
} else if (
|
|
other_declaration.type === 'ExportNamedDeclaration' &&
|
|
walking.has(other_declaration.declaration)
|
|
) {
|
|
hoistable = false;
|
|
} else if (!is_hoistable(other_declaration)) {
|
|
hoistable = false;
|
|
}
|
|
} else {
|
|
hoistable = false;
|
|
}
|
|
}
|
|
|
|
this.skip();
|
|
}
|
|
},
|
|
|
|
leave(node) {
|
|
if (map.has(node)) {
|
|
scope = scope.parent;
|
|
}
|
|
},
|
|
});
|
|
|
|
checked.add(fn_declaration);
|
|
walking.delete(fn_declaration);
|
|
|
|
return hoistable;
|
|
};
|
|
|
|
for (const [name, node] of top_level_function_declarations) {
|
|
if (is_hoistable(node)) {
|
|
const variable = this.var_lookup.get(name);
|
|
variable.hoistable = true;
|
|
hoistable_nodes.add(node);
|
|
|
|
const i = body.indexOf(node);
|
|
body.splice(i, 1);
|
|
this.fully_hoisted.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
extract_reactive_declarations() {
|
|
const component = this;
|
|
|
|
const unsorted_reactive_declarations = [];
|
|
|
|
this.ast.instance.content.body.forEach(node => {
|
|
if (node.type === 'LabeledStatement' && node.label.name === '$') {
|
|
this.reactive_declaration_nodes.add(node);
|
|
|
|
const assignees = new Set();
|
|
const assignee_nodes = new Set();
|
|
const dependencies = new Set();
|
|
|
|
let scope = this.instance_scope;
|
|
const map = this.instance_scope_map;
|
|
|
|
walk(node.body, {
|
|
enter(node, parent) {
|
|
if (map.has(node)) {
|
|
scope = map.get(node);
|
|
}
|
|
|
|
if (node.type === 'AssignmentExpression') {
|
|
const left = get_object(node.left);
|
|
|
|
extract_identifiers(left).forEach(node => {
|
|
assignee_nodes.add(node);
|
|
assignees.add(node.name);
|
|
});
|
|
|
|
if (node.operator !== '=') {
|
|
dependencies.add(left.name);
|
|
}
|
|
} else if (node.type === 'UpdateExpression') {
|
|
const identifier = get_object(node.argument);
|
|
assignees.add(identifier.name);
|
|
} else if (is_reference(node as Node, parent as Node)) {
|
|
const identifier = get_object(node);
|
|
if (!assignee_nodes.has(identifier)) {
|
|
const { name } = identifier;
|
|
const owner = scope.find_owner(name);
|
|
const variable = component.var_lookup.get(name);
|
|
if (variable) variable.is_reactive_dependency = true;
|
|
const is_writable_or_mutated =
|
|
variable && (variable.writable || variable.mutated);
|
|
if (
|
|
(!owner || owner === component.instance_scope) &&
|
|
(name[0] === '$' || is_writable_or_mutated)
|
|
) {
|
|
dependencies.add(name);
|
|
}
|
|
}
|
|
|
|
this.skip();
|
|
}
|
|
},
|
|
|
|
leave(node) {
|
|
if (map.has(node)) {
|
|
scope = scope.parent;
|
|
}
|
|
},
|
|
});
|
|
|
|
const { expression } = node.body as ExpressionStatement;
|
|
const declaration = expression && (expression as AssignmentExpression).left;
|
|
|
|
unsorted_reactive_declarations.push({
|
|
assignees,
|
|
dependencies,
|
|
node,
|
|
declaration,
|
|
});
|
|
}
|
|
});
|
|
|
|
const lookup = new Map();
|
|
let seen;
|
|
|
|
unsorted_reactive_declarations.forEach(declaration => {
|
|
declaration.assignees.forEach(name => {
|
|
if (!lookup.has(name)) {
|
|
lookup.set(name, []);
|
|
}
|
|
|
|
// TODO warn or error if a name is assigned to in
|
|
// multiple reactive declarations?
|
|
lookup.get(name).push(declaration);
|
|
});
|
|
});
|
|
|
|
const cycle = check_graph_for_cycles(unsorted_reactive_declarations.reduce((acc, declaration) => {
|
|
declaration.assignees.forEach(v => {
|
|
declaration.dependencies.forEach(w => {
|
|
if (!declaration.assignees.has(w)) {
|
|
acc.push([v, w]);
|
|
}
|
|
});
|
|
});
|
|
return acc;
|
|
}, []));
|
|
|
|
if (cycle && cycle.length) {
|
|
const declarationList = lookup.get(cycle[0]);
|
|
const declaration = declarationList[0];
|
|
this.error(declaration.node, {
|
|
code: 'cyclical-reactive-declaration',
|
|
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
|
|
});
|
|
}
|
|
|
|
const add_declaration = declaration => {
|
|
if (this.reactive_declarations.indexOf(declaration) !== -1) {
|
|
return;
|
|
}
|
|
|
|
seen.add(declaration);
|
|
|
|
declaration.dependencies.forEach(name => {
|
|
if (declaration.assignees.has(name)) return;
|
|
const earlier_declarations = lookup.get(name);
|
|
if (earlier_declarations)
|
|
earlier_declarations.forEach(declaration => {
|
|
add_declaration(declaration);
|
|
});
|
|
});
|
|
|
|
this.reactive_declarations.push(declaration);
|
|
};
|
|
|
|
unsorted_reactive_declarations.forEach(declaration => {
|
|
seen = new Set();
|
|
add_declaration(declaration);
|
|
});
|
|
}
|
|
|
|
qualify(name) {
|
|
if (name === `$$props`) return x`#ctx.$$props`;
|
|
|
|
let [head, ...tail] = name.split('.');
|
|
|
|
const variable = this.var_lookup.get(head);
|
|
|
|
if (variable) {
|
|
this.add_reference(name); // TODO we can probably remove most other occurrences of this
|
|
|
|
if (!variable.hoistable) {
|
|
tail.unshift(head);
|
|
head = '#ctx';
|
|
}
|
|
}
|
|
|
|
return [head, ...tail].reduce((lhs, rhs) => x`${lhs}.${rhs}`);
|
|
}
|
|
|
|
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
|
|
if (name[0] === '$') {
|
|
if (name === '$' || name[1] === '$' && name !== '$$props') {
|
|
this.error(node, {
|
|
code: 'illegal-global',
|
|
message: `${name} is an illegal variable name`
|
|
});
|
|
}
|
|
|
|
this.has_reactive_assignments = true; // TODO does this belong here?
|
|
|
|
if (name === '$$props') return;
|
|
|
|
name = name.slice(1);
|
|
}
|
|
|
|
if (this.var_lookup.has(name) && !this.var_lookup.get(name).global) return;
|
|
if (template_scope && template_scope.names.has(name)) return;
|
|
if (globals.has(name)) return;
|
|
|
|
let message = `'${name}' is not defined`;
|
|
if (!this.ast.instance)
|
|
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
|
|
|
|
this.warn(node, {
|
|
code: 'missing-declaration',
|
|
message,
|
|
});
|
|
}
|
|
|
|
push_ignores(ignores) {
|
|
this.ignores = new Set(this.ignores || []);
|
|
add_to_set(this.ignores, ignores);
|
|
this.ignore_stack.push(this.ignores);
|
|
}
|
|
|
|
pop_ignores() {
|
|
this.ignore_stack.pop();
|
|
this.ignores = this.ignore_stack[this.ignore_stack.length - 1];
|
|
}
|
|
}
|
|
|
|
function process_component_options(component: Component, nodes) {
|
|
const component_options: ComponentOptions = {
|
|
immutable: component.compile_options.immutable || false,
|
|
accessors:
|
|
'accessors' in component.compile_options
|
|
? component.compile_options.accessors
|
|
: !!component.compile_options.customElement,
|
|
preserveWhitespace: !!component.compile_options.preserveWhitespace,
|
|
};
|
|
|
|
const node = nodes.find(node => node.name === 'svelte:options');
|
|
|
|
function get_value(attribute, code, message) {
|
|
const { value } = attribute;
|
|
const chunk = value[0];
|
|
|
|
if (!chunk) return true;
|
|
|
|
if (value.length > 1) {
|
|
component.error(attribute, { code, message });
|
|
}
|
|
|
|
if (chunk.type === 'Text') return chunk.data;
|
|
|
|
if (chunk.expression.type !== 'Literal') {
|
|
component.error(attribute, { code, message });
|
|
}
|
|
|
|
return chunk.expression.value;
|
|
}
|
|
|
|
if (node) {
|
|
node.attributes.forEach(attribute => {
|
|
if (attribute.type === 'Attribute') {
|
|
const { name } = attribute;
|
|
|
|
switch (name) {
|
|
case 'tag': {
|
|
const code = 'invalid-tag-attribute';
|
|
const message = `'tag' must be a string literal`;
|
|
const tag = get_value(attribute, code, message);
|
|
|
|
if (typeof tag !== 'string' && tag !== null)
|
|
component.error(attribute, { code, message });
|
|
|
|
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
|
|
component.error(attribute, {
|
|
code: `invalid-tag-property`,
|
|
message: `tag name must be two or more words joined by the '-' character`,
|
|
});
|
|
}
|
|
|
|
if (tag && !component.compile_options.customElement) {
|
|
component.warn(attribute, {
|
|
code: 'missing-custom-element-compile-options',
|
|
message: `The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?`
|
|
});
|
|
}
|
|
|
|
component_options.tag = tag;
|
|
break;
|
|
}
|
|
|
|
case 'namespace': {
|
|
const code = 'invalid-namespace-attribute';
|
|
const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
|
|
const ns = get_value(attribute, code, message);
|
|
|
|
if (typeof ns !== 'string')
|
|
component.error(attribute, { code, message });
|
|
|
|
if (valid_namespaces.indexOf(ns) === -1) {
|
|
const match = fuzzymatch(ns, valid_namespaces);
|
|
if (match) {
|
|
component.error(attribute, {
|
|
code: `invalid-namespace-property`,
|
|
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`,
|
|
});
|
|
} else {
|
|
component.error(attribute, {
|
|
code: `invalid-namespace-property`,
|
|
message: `Invalid namespace '${ns}'`,
|
|
});
|
|
}
|
|
}
|
|
|
|
component_options.namespace = ns;
|
|
break;
|
|
}
|
|
|
|
case 'accessors':
|
|
case 'immutable':
|
|
case 'preserveWhitespace': {
|
|
const code = `invalid-${name}-value`;
|
|
const message = `${name} attribute must be true or false`;
|
|
const value = get_value(attribute, code, message);
|
|
|
|
if (typeof value !== 'boolean')
|
|
component.error(attribute, { code, message });
|
|
|
|
component_options[name] = value;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
component.error(attribute, {
|
|
code: `invalid-options-attribute`,
|
|
message: `<svelte:options> unknown attribute`,
|
|
});
|
|
}
|
|
} else {
|
|
component.error(attribute, {
|
|
code: `invalid-options-attribute`,
|
|
message: `<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes`,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return component_options;
|
|
}
|
|
|
|
function get_relative_path(from: string, to: string) {
|
|
const from_parts = from.split(/[/\\]/);
|
|
const to_parts = to.split(/[/\\]/);
|
|
|
|
from_parts.pop(); // get dirname
|
|
|
|
while (from_parts[0] === to_parts[0]) {
|
|
from_parts.shift();
|
|
to_parts.shift();
|
|
}
|
|
|
|
if (from_parts.length) {
|
|
let i = from_parts.length;
|
|
while (i--) from_parts[i] = '..';
|
|
}
|
|
|
|
return from_parts.concat(to_parts).join('/');
|
|
} |