import MagicString, { Bundle } from 'magic-string'; import { walk, childKeys } from 'estree-walker'; import { getLocator } from 'locate-character'; import Stats from '../Stats'; import reservedNames from '../utils/reservedNames'; import { namespaces, validNamespaces } from '../utils/namespaces'; import { removeNode } from '../utils/removeNode'; import wrapModule from './wrapModule'; import { createScopes, extractNames, Scope } from '../utils/annotateWithScopes'; 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, Warning } from '../interfaces'; import error from '../utils/error'; import getCodeFrame from '../utils/getCodeFrame'; import flattenReference from '../utils/flattenReference'; import isReference from 'is-reference'; import TemplateScope from './nodes/shared/TemplateScope'; import fuzzymatch from '../utils/fuzzymatch'; import { remove_indentation, add_indentation } from '../utils/indentation'; import getObject from '../utils/getObject'; import globalWhitelist from '../utils/globalWhitelist'; type ComponentOptions = { namespace?: string; tag?: string; immutable?: 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[]; ast: Ast; source: string; code: MagicString; name: string; compileOptions: CompileOptions; fragment: Fragment; module_scope: Scope; instance_scope: Scope; instance_scope_map: WeakMap<Node, Scope>; componentOptions: ComponentOptions; namespace: string; tag: string; vars: Var[] = []; var_lookup: Map<string, Var> = new Map(); imports: Node[] = []; module_javascript: string; javascript: string; hoistable_nodes: Set<Node> = new Set(); node_for_declaration: Map<string, Node> = new Map(); partly_hoisted: string[] = []; fully_hoisted: string[] = []; reactive_declarations: Array<{ assignees: Set<string>, dependencies: Set<string>, node: Node, injected: boolean }> = []; reactive_declaration_nodes: Set<Node> = new Set(); has_reactive_assignments = false; injected_reactive_declaration_vars: Set<string> = new Set(); helpers: Set<string> = new Set(); indirectDependencies: Map<string, Set<string>> = new Map(); file: string; locate: (c: number) => { line: number, column: number }; // TODO this does the same as component.locate! remove one or the other locator: (search: number, startIndex?: number) => { line: number, column: number }; stylesheet: Stylesheet; aliases: Map<string, string> = new Map(); usedNames: Set<string> = new Set(); constructor( ast: Ast, source: string, name: string, compileOptions: CompileOptions, stats: Stats, warnings: Warning[] ) { this.name = name; this.stats = stats; this.warnings = warnings; this.ast = ast; this.source = source; this.compileOptions = compileOptions; this.file = compileOptions.filename && ( typeof process !== 'undefined' ? compileOptions.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : compileOptions.filename ); this.locate = getLocator(this.source); this.code = new MagicString(source); // styles this.stylesheet = new Stylesheet(source, ast, compileOptions.filename, compileOptions.dev); this.stylesheet.validate(this); this.componentOptions = process_component_options(this, this.ast.html.children); this.namespace = namespaces[this.componentOptions.namespace] || this.componentOptions.namespace; if (compileOptions.customElement === true && !this.componentOptions.tag) { throw new Error(`No tag name specified`); // TODO better error } this.tag = compileOptions.customElement ? compileOptions.customElement === true ? this.componentOptions.tag : compileOptions.customElement as string : this.name; this.walk_module_js(); this.walk_instance_js_pre_template(); this.name = this.getUniqueName(name); this.fragment = new Fragment(this, ast.html); this.walk_instance_js_post_template(); if (!compileOptions.customElement) this.stylesheet.reify(); this.stylesheet.warnOnUnusedSelectors(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, implicit: 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); variable.subscribable = true; } else if (!this.ast.instance) { this.add_var({ name, export_name: name, implicit: true, mutated: false, referenced: true, writable: true }); } } addSourcemapLocations(node: Node) { walk(node, { enter: (node: Node) => { this.code.addSourcemapLocation(node.start); this.code.addSourcemapLocation(node.end); }, }); } alias(name: string) { if (!this.aliases.has(name)) { this.aliases.set(name, this.getUniqueName(name)); } return this.aliases.get(name); } helper(name: string) { this.helpers.add(name); return this.alias(name); } generate(result: string) { let js = null; let css = null; if (result) { const { compileOptions, name } = this; const { format = 'esm' } = compileOptions; const banner = `/* ${this.file ? `${this.file} ` : ``}generated by Svelte v${"__VERSION__"} */`; // 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 this.alias(name); } return sigil.slice(1) + name; }); const importedHelpers = Array.from(this.helpers) .sort() .map(name => { const alias = this.alias(name); return { name, alias }; }); 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 parts = module.split('✂]'); const finalChunk = parts.pop(); const compiled = new Bundle({ separator: '' }); function addString(str: string) { compiled.addSource({ content: new MagicString(str), }); } const { filename } = compileOptions; // 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); const match = pattern.exec(str); const snippet = this.code.snip(+match[1], +match[2]); compiled.addSource({ filename, content: snippet, }); }); addString(finalChunk); css = compileOptions.customElement ? { code: null, map: null } : this.stylesheet.render(compileOptions.cssOutputFilename, true); js = { code: compiled.toString(), map: compiled.generateMap({ includeContent: true, file: compileOptions.outputFilename, }) }; } return { js, css, ast: this.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 })), stats: this.stats.render() }; } getUniqueName(name: string) { if (test) name = `${name}$`; let alias = name; for ( let i = 1; reservedNames.has(alias) || this.var_lookup.has(alias) || this.usedNames.has(alias); alias = `${name}_${i++}` ); this.usedNames.add(alias); return alias; } getUniqueNameMaker() { const localUsedNames = new Set(); function add(name: string) { localUsedNames.add(name); } reservedNames.forEach(add); this.var_lookup.forEach((value, key) => add(key)); return (name: string) => { if (test) name = `${name}$`; let alias = name; for ( let i = 1; this.usedNames.has(alias) || localUsedNames.has(alias); alias = `${name}_${i++}` ); localUsedNames.add(alias); return 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.compileOptions.filename }); } warn( pos: { start: number, end: number }, warning: { code: string, message: string } ) { if (!this.locator) { this.locator = getLocator(this.source, { offsetLine: 1 }); } const start = this.locator(pos.start); const end = this.locator(pos.end); const frame = getCodeFrame(this.source, start.line - 1, start.column); this.warnings.push({ code: warning.code, message: warning.message, frame, start, end, pos: pos.start, filename: this.compileOptions.filename, toString: () => `${warning.message} (${start.line + 1}:${start.column})\n${frame}`, }); } extract_imports(content) { const { code } = this; content.body.forEach(node => { if (node.type === 'ImportDeclaration') { // imports need to be hoisted out of the IIFE removeNode(code, content.start, content.end, content.body, node); this.imports.push(node); } }); } extract_exports(content) { const { code } = this; content.body.forEach(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.declaration) { if (node.declaration.type === 'VariableDeclaration') { node.declaration.declarations.forEach(declarator => { extractNames(declarator.id).forEach(name => { const variable = this.var_lookup.get(name); variable.export_name = name; }); }); } else { const { name } = node.declaration.id; const variable = this.var_lookup.get(name); variable.export_name = name; } code.remove(node.start, node.declaration.start); } else { removeNode(code, content.start, content.end, content.body, node); node.specifiers.forEach(specifier => { const variable = this.var_lookup.get(specifier.local.name); if (variable) { variable.export_name = specifier.exported.name; } else { // TODO what happens with `export { Math }` or some other global? } }); } } }); } extract_javascript(script) { const nodes_to_include = script.content.body.filter(node => { 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; }); if (nodes_to_include.length === 0) return null; let a = script.content.start; while (/\s/.test(this.source[a])) a += 1; let b = a; let result = ''; script.content.body.forEach((node, i) => { if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) { if (a !== b) result += `[✂${a}-${b}✂]`; a = node.end; } b = node.end; }); // while (/\s/.test(this.source[a - 1])) a -= 1; b = script.content.end; while (/\s/.test(this.source[b - 1])) b -= 1; if (a < b) result += `[✂${a}-${b}✂]`; return result || null; } walk_module_js() { const script = this.ast.module; if (!script) return; this.addSourcemapLocations(script.content); let { scope, globals } = createScopes(script.content); this.module_scope = scope; scope.declarations.forEach((node, name) => { if (name[0] === '$') { this.error(node, { code: 'illegal-declaration', message: `The $ prefix is reserved, and cannot be used for variable and import names` }); } this.add_var({ name, module: true, hoistable: true, writable: node.kind === 'var' || node.kind === 'let' }); }); globals.forEach((node, name) => { if (name[0] === '$') { this.error(node, { code: 'illegal-subscription', message: `Cannot reference store value inside <script context="module">` }) } else { this.add_var({ name, global: true }); } }); this.extract_imports(script.content); this.extract_exports(script.content); remove_indentation(this.code, script.content); this.module_javascript = this.extract_javascript(script); } walk_instance_js_pre_template() { const script = this.ast.instance; if (!script) return; this.addSourcemapLocations(script.content); // inject vars for reactive declarations script.content.body.forEach(node => { if (node.type !== 'LabeledStatement') return; if (node.body.type !== 'ExpressionStatement') return; if (node.body.expression.type !== 'AssignmentExpression') return; const { type, name } = node.body.expression.left; if (type === 'Identifier' && !this.var_lookup.has(name)) { this.injected_reactive_declaration_vars.add(name); } }); let { scope: instance_scope, map, globals } = createScopes(script.content); this.instance_scope = instance_scope; this.instance_scope_map = map; instance_scope.declarations.forEach((node, name) => { if (name[0] === '$') { this.error(node, { code: 'illegal-declaration', message: `The $ prefix is reserved, and cannot be used for variable and import names` }); } this.add_var({ name, initialised: instance_scope.initialised_declarations.has(name), hoistable: /^Import/.test(node.type), writable: node.kind === 'var' || node.kind === 'let' }); 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, implicit: true }); } else if (name[0] === '$') { 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)); variable.subscribable = true; } else { this.add_var({ name, global: true }); } }); this.extract_imports(script.content); this.extract_exports(script.content); this.track_mutations(); } walk_instance_js_post_template() { const script = this.ast.instance; if (!script) return; this.hoist_instance_declarations(); this.extract_reactive_declarations(); this.extract_reactive_store_references(); this.javascript = this.extract_javascript(script); } // TODO merge this with other walks that are independent track_mutations() { const component = this; const { instance_scope, instance_scope_map: map } = this; let scope = instance_scope; walk(this.ast.instance.content, { enter(node, parent) { if (map.has(node)) { scope = map.get(node); } let names; let deep = false; if (node.type === 'AssignmentExpression') { deep = node.left.type === 'MemberExpression'; names = deep ? [getObject(node.left).name] : extractNames(node.left); } else if (node.type === 'UpdateExpression') { names = [getObject(node.argument).name]; } if (names) { names.forEach(name => { if (scope.findOwner(name) === instance_scope) { const variable = component.var_lookup.get(name); variable[deep ? 'mutated' : 'reassigned'] = true; } }); } }, leave(node) { if (map.has(node)) { scope = scope.parent; } } }) } extract_reactive_store_references() { // TODO this pattern happens a lot... can we abstract it // (or better still, do fewer AST walks)? const component = this; let { instance_scope: scope, instance_scope_map: map } = this; walk(this.ast.instance.content, { enter(node, parent) { if (map.has(node)) { scope = map.get(node); } if (isReference(node, parent)) { const object = getObject(node); const { name } = object; if (name[0] === '$' && !scope.has(name)) { component.warn_if_undefined(object, null); } } }, leave(node) { if (map.has(node)) { scope = scope.parent; } } }); } 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; const coalesced_declarations = []; let current_group; walk(this.ast.instance.content, { enter(node, parent) { if (/Function/.test(node.type)) { current_group = null; 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, i) => { const next = node.declarations[i + 1]; if (declarator.id.type !== 'Identifier') { const inserts = []; extractNames(declarator.id).forEach(name => { const variable = component.var_lookup.get(name); if (variable.export_name) { component.error(declarator, { code: 'destructured-prop', message: `Cannot declare props in destructured declaration` }); } if (variable.subscribable) { inserts.push(get_insert(variable)); } }); if (inserts.length > 0) { if (next) { code.overwrite(declarator.end, next.start, `; ${inserts.join('; ')}; ${node.kind} `); } else { code.appendLeft(declarator.end, `; ${inserts.join('; ')}`); } } return; } const { name } = declarator.id; const variable = component.var_lookup.get(name); if (variable.export_name) { if (current_group && current_group.kind !== node.kind) { current_group = null; } const insert = variable.subscribable ? get_insert(variable) : null; if (!current_group || (current_group.insert && insert)) { current_group = { kind: node.kind, declarators: [declarator], insert }; coalesced_declarations.push(current_group); } else if (insert) { current_group.insert = insert current_group.declarators.push(declarator); } else { current_group.declarators.push(declarator); } if (variable.name !== variable.export_name) { code.prependRight(declarator.id.start, `${variable.export_name}:`) } if (next) { const next_variable = component.var_lookup.get(next.id.name) const new_declaration = !next_variable.export_name || (current_group.insert && next_variable.subscribable) if (new_declaration) { code.overwrite(declarator.end, next.start, ` ${node.kind} `); } } } else { current_group = null; if (variable.subscribable) { let insert = get_insert(variable); if (next) { code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `); } else { code.appendLeft(declarator.end, `; ${insert}`); } } } }); } } else { if (node.type !== 'ExportNamedDeclaration') { if (!parent) current_group = null; } } }, leave(node) { if (map.has(node)) { scope = scope.parent; } } }); coalesced_declarations.forEach(group => { let c = 0; let combining = false; group.declarators.forEach(declarator => { const { id } = declarator; if (combining) { code.overwrite(c, id.start, ', '); } else { code.appendLeft(id.start, '{ '); combining = true; } c = declarator.end; }); if (combining) { const insert = group.insert ? `; ${group.insert}` : ''; const suffix = code.original[c] === ';' ? ` } = $$props${insert}` : ` } = $$props${insert};`; code.appendLeft(c, suffix); } }); } 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 } = this; const top_level_function_declarations = new Map(); this.ast.instance.content.body.forEach(node => { if (node.type === 'VariableDeclaration') { const all_hoistable = node.declarations.every(d => { if (!d.init) return false; if (d.init.type !== 'Literal') return false; const v = this.var_lookup.get(d.id.name) if (v.reassigned) return false if (v.export_name && v.export_name !== v.name) return false if (this.var_lookup.get(d.id.name).reassigned) return false; if (this.vars.find(variable => variable.name === d.id.name && variable.module)) return false; return true; }); if (all_hoistable) { node.declarations.forEach(d => { const variable = this.var_lookup.get(d.id.name); variable.hoistable = true; }); hoistable_nodes.add(node); this.fully_hoisted.push(`[✂${node.start}-${node.end}✂]`); } } 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(); let 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; let map = this.instance_scope_map; let hoistable = true; // handle cycles walking.add(fn_declaration); walk(fn_declaration, { enter(node, parent) { if (map.has(node)) { scope = map.get(node); } if (isReference(node, parent)) { const { name } = flattenReference(node); const owner = scope.findOwner(name); if (name[0] === '$' && !owner) { hoistable = false; } else if (owner === instance_scope) { if (name === fn_declaration.id.name) return; const variable = var_lookup.get(name); 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 (!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 (!checked.has(node) && is_hoistable(node)) { const variable = this.var_lookup.get(name); variable.hoistable = true; hoistable_nodes.add(node); remove_indentation(this.code, node); this.fully_hoisted.push(`[✂${node.start}-${node.end}✂]`); } } } 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 dependencies = new Set(); let scope = this.instance_scope; let map = this.instance_scope_map; walk(node.body, { enter(node, parent) { if (map.has(node)) { scope = map.get(node); } if (node.type === 'AssignmentExpression') { const { name } = getObject(node.left) assignees.add(name); dependencies.delete(name); } else if (node.type === 'UpdateExpression') { const { name } = getObject(node.argument); assignees.add(name); dependencies.delete(name); } else if (isReference(node, parent)) { const { name } = getObject(node); const owner = scope.findOwner(name); if ((!owner || owner === component.instance_scope) && (name[0] === '$' || component.var_lookup.has(name)) && !assignees.has(name)) { dependencies.add(name); } this.skip(); } }, leave(node) { if (map.has(node)) { scope = scope.parent; } } }); add_indentation(this.code, node.body, 2); unsorted_reactive_declarations.push({ assignees, dependencies, node, injected: ( node.body.type === 'ExpressionStatement' && node.body.expression.type === 'AssignmentExpression' && node.body.expression.left.type === 'Identifier' && this.var_lookup.get(node.body.expression.left.name).injected ) }); } }); 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 add_declaration = declaration => { if (seen.has(declaration)) { this.error(declaration.node, { code: 'cyclical-reactive-declaration', message: 'Cyclical dependency detected' }); } if (this.reactive_declarations.indexOf(declaration) !== -1) { return; } seen.add(declaration); if (declaration.dependencies.size === 0) { this.error(declaration.node, { code: 'invalid-reactive-declaration', message: 'Invalid reactive declaration — must depend on local state' }); } 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 `ctx.$$props`; const variable = this.var_lookup.get(name); if (!variable) return name; if (variable && variable.hoistable) return name; this.add_reference(name); // TODO we can probably remove most other occurrences of this return `ctx.${name}`; } warn_if_undefined(node, template_scope: TemplateScope, allow_implicit?: boolean) { let { name } = node; if (name[0] === '$') { name = name.slice(1); this.has_reactive_assignments = true; } if (allow_implicit && !this.ast.instance && !this.ast.module) return; if (this.var_lookup.has(name)) return; if (template_scope && template_scope.names.has(name)) return; if (globalWhitelist.has(name)) return; this.warn(node, { code: 'missing-declaration', message: `'${name}' is not defined` }); } } function process_component_options(component: Component, nodes) { const componentOptions: ComponentOptions = { immutable: component.compileOptions.immutable || false }; 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') component.error(attribute, { code, message }); if (!/^[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` }); } componentOptions.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 (validNamespaces.indexOf(ns) === -1) { const match = fuzzymatch(ns, validNamespaces); 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}'` }); } } componentOptions.namespace = ns; break; } case 'immutable': const code = `invalid-immutable-value`; const message = `immutable attribute must be true or false` const value = get_value(attribute, code, message); if (typeof value !== 'boolean') component.error(attribute, { code, message }); componentOptions.immutable = 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' and 'immutable' attributes` }); } }); } return componentOptions; }