diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e1539093..dadb5cd406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,65 @@ # Svelte changelog -## Unreleased +## 3.41.0 -* Expose `svelte/ssr` which exports lifecycle methods as no-ops ([#6416](https://github.com/sveltejs/svelte/pull/6416)) +* Support `export { ... } from` syntax in components ([#2214](https://github.com/sveltejs/svelte/issues/2214)) +* Support `export let { ... } =` syntax in components ([#5612](https://github.com/sveltejs/svelte/issues/5612)) +* Support `{#await ... then/catch}` without a variable for the resolved/rejected value ([#6270](https://github.com/sveltejs/svelte/issues/6270)) + +## 3.40.3 + +* Fix `` data when a transition is cancelled before completing ([#5394](https://github.com/sveltejs/svelte/issues/5394)) +* Fix destructuring into variables beginning with `$` so that they result in store updates ([#5653](https://github.com/sveltejs/svelte/issues/5653)) +* Fix `in:` transition configuration not properly updating when it's changed after its initial creation ([#6505](https://github.com/sveltejs/svelte/issues/6505)) +* Fix applying `:global()` for `>` selector combinator ([#6550](https://github.com/sveltejs/svelte/issues/6550)) +* Fix mounting component at detached DOM node ([#6567](https://github.com/sveltejs/svelte/issues/6567)) + +## 3.40.2 + +* Fix dynamic `autofocus={...}` attribute handling ([#4995](https://github.com/sveltejs/svelte/issues/4995)) +* Add filename to combined source map if needed ([#6089](https://github.com/sveltejs/svelte/pull/6089)) +* In AST, parse empty attribute values as an empty string ([#6286](https://github.com/sveltejs/svelte/issues/6286)) +* Fix tracking whether transition has started ([#6399](https://github.com/sveltejs/svelte/pull/6399)) +* Fix incorrect scoping of `:global()` selectors ([#6550](https://github.com/sveltejs/svelte/issues/6550)) + +## 3.40.1 + +* Fix store reactivity regression when using reactive statements ([#6557](https://github.com/sveltejs/svelte/issues/6557)) + +## 3.40.0 + +* Support rendering a component in a shadow DOM ([#5869](https://github.com/sveltejs/svelte/issues/5869)) +* Fix `:root` selector being erroneously scoped to component ([#4767](https://github.com/sveltejs/svelte/issues/4767)) +* Fix `.end` in AST for expressions inside attributes ([#6258](https://github.com/sveltejs/svelte/issues/6258)) +* Fix one-way ` -``` \ No newline at end of file +``` + +> Press and hold the `control` key for selecting multiple options. diff --git a/site/package-lock.json b/site/package-lock.json index 8303a89e35..3637e2ffd2 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -3434,9 +3434,9 @@ } }, "svelte": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.37.0.tgz", - "integrity": "sha512-TRF30F4W4+d+Jr2KzUUL1j8Mrpns/WM/WacxYlo5MMb2E5Qy2Pk1Guj6GylxsW9OnKQl1tnF8q3hG/hQ3h6VUA==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.39.0.tgz", + "integrity": "sha512-dcJCongL0cRkZWe9q+fde0T4HX8PksBywz2+EGDVIrdYdJaxTzrJu0RVeuDtL8Mx2hs4yn3W8zKPScuzG63hTg==", "dev": true }, "svelte-json-tree": { diff --git a/site/package.json b/site/package.json index 5c8d9e0ad3..e63c530846 100644 --- a/site/package.json +++ b/site/package.json @@ -55,7 +55,7 @@ "rollup-plugin-terser": "^7.0.0", "sapper": "^0.28.10", "shelljs": "^0.8.3", - "svelte": "^3.37.0" + "svelte": "^3.39.0" }, "engines": { "node": ">=10.0.0" diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 9001d09f68..6a8d5b178e 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -10,7 +10,6 @@ import { Scope, extract_identifiers } from './utils/scope'; -import { Node as PeriscopicNode } from 'periscopic'; import Stylesheet from './css/Stylesheet'; import { test } from '../config'; import Fragment from './nodes/Fragment'; @@ -20,15 +19,15 @@ 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 is_reference, { NodeWithPropertyDefinition } 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 { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration } 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'; +import { print, b } from 'code-red'; import { is_reserved_keyword } from './utils/reserved_keywords'; import { apply_preprocessor_sourcemap } from '../utils/mapped_code'; import Element from './nodes/Element'; @@ -36,6 +35,7 @@ import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types import { clone } from '../utils/clone'; import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; +import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; interface ComponentOptions { namespace?: string; @@ -70,6 +70,8 @@ export default class Component { var_lookup: Map = new Map(); imports: ImportDeclaration[] = []; + exports_from: ExportNamedDeclaration[] = []; + instance_exports_from: ExportNamedDeclaration[] = []; hoistable_nodes: Set = new Set(); node_for_declaration: Map = new Map(); @@ -171,12 +173,17 @@ export default class Component { } this.walk_module_js(); + + this.push_ignores(this.ast.instance ? extract_ignores_above_position(this.ast.instance.start, this.ast.html.children) : []); this.walk_instance_js_pre_template(); + this.pop_ignores(); this.fragment = new Fragment(this, ast.html); this.name = this.get_unique_name(name); + this.push_ignores(this.ast.instance ? extract_ignores_above_position(this.ast.instance.start, this.ast.html.children) : []); this.walk_instance_js_post_template(); + this.pop_ignores(); this.elements.forEach(element => this.stylesheet.apply(element)); if (!compile_options.customElement) this.stylesheet.reify(); @@ -267,6 +274,14 @@ export default class Component { } else { let name = node.name.slice(1); + if (compile_options.hydratable) { + if (internal_exports.has(`${name}_hydration`)) { + name += '_hydration'; + } else if (internal_exports.has(`${name}Hydration`)) { + name += 'Hydration'; + } + } + if (compile_options.dev) { if (internal_exports.has(`${name}_dev`)) { name += '_dev'; @@ -320,7 +335,8 @@ export default class Component { .map(variable => ({ name: variable.name, as: variable.export_name - })) + })), + this.exports_from ); css = compile_options.customElement @@ -429,14 +445,18 @@ export default class Component { message: string; } ) { - error(e.message, { - name: 'ValidationError', - code: e.code, - source: this.source, - start: pos.start, - end: pos.end, - filename: this.compile_options.filename - }); + if (this.compile_options.errorMode === 'warn') { + this.warn(pos, e); + } else { + error(e.message, { + name: 'ValidationError', + code: e.code, + source: this.source, + start: pos.start, + end: pos.end, + filename: this.compile_options.filename + }); + } } warn( @@ -475,14 +495,27 @@ export default class Component { this.imports.push(node); } - extract_exports(node) { + extract_exports(node, module_script = false) { + const ignores = extract_svelte_ignore_from_comments(node); + if (ignores.length) this.push_ignores(ignores); + const result = this._extract_exports(node, module_script); + if (ignores.length) this.pop_ignores(); + return result; + } + + private _extract_exports(node: ExportDefaultDeclaration | ExportNamedDeclaration | ExportAllDeclaration, module_script) { if (node.type === 'ExportDefaultDeclaration') { - this.error(node, compiler_errors.default_export); + return this.error(node as any, compiler_errors.default_export); } if (node.type === 'ExportNamedDeclaration') { if (node.source) { - this.error(node, compiler_errors.not_implemented); + if (module_script) { + this.exports_from.push(node); + } else { + this.instance_exports_from.push(node); + } + return null; } if (node.declaration) { if (node.declaration.type === 'VariableDeclaration') { @@ -491,7 +524,7 @@ export default class Component { const variable = this.var_lookup.get(name); variable.export_name = name; if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { - this.warn(declarator, compiler_warnings.unused_export_let(this.name.name, name)); + this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name)); } }); }); @@ -511,7 +544,7 @@ export default class Component { variable.export_name = specifier.exported.name; if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { - this.warn(specifier, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name)); + this.warn(specifier as any, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name)); } } }); @@ -552,7 +585,7 @@ export default class Component { scope.declarations.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_declaration); + return this.error(node as any, compiler_errors.illegal_declaration); } const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); @@ -567,7 +600,7 @@ export default class Component { globals.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_subscription); + return this.error(node as any, compiler_errors.illegal_subscription); } else { this.add_var({ name, @@ -587,7 +620,7 @@ export default class Component { } if (/^Export/.test(node.type)) { - const replacement = this.extract_exports(node); + const replacement = this.extract_exports(node, true); if (replacement) { body[i] = replacement; } else { @@ -625,7 +658,7 @@ export default class Component { instance_scope.declarations.forEach((node, name) => { if (name[0] === '$') { - this.error(node as any, compiler_errors.illegal_declaration); + return this.error(node as any, compiler_errors.illegal_declaration); } const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); @@ -641,8 +674,17 @@ export default class Component { this.node_for_declaration.set(name, node); }); - globals.forEach((node, name) => { + // NOTE: add store variable first, then only $store value + // as `$store` will mark `store` variable as referenced and subscribable + const global_keys = Array.from(globals.keys()); + const sorted_globals = [ + ...global_keys.filter(key => key[0] !== '$'), + ...global_keys.filter(key => key[0] === '$') + ]; + + sorted_globals.forEach(name => { if (this.var_lookup.has(name)) return; + const node = globals.get(name); if (this.injected_reactive_declaration_vars.has(name)) { this.add_var({ @@ -659,7 +701,7 @@ export default class Component { }); } else if (name[0] === '$') { if (name === '$' || name[1] === '$') { - this.error(node as any, compiler_errors.illegal_global(name)); + return this.error(node as any, compiler_errors.illegal_global(name)); } this.add_var({ @@ -717,7 +759,7 @@ export default class Component { let generator_count = 0; walk(content, { - enter(node: Node, parent, prop, index) { + enter(node: Node, parent: Node, prop, index) { if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && node.generator === true) { generator_count++; } @@ -802,7 +844,7 @@ export default class Component { if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; - const names = extract_names(assignee as PeriscopicNode); + const names = extract_names(assignee as Node); const deep = assignee.type === 'MemberExpression'; @@ -845,7 +887,7 @@ export default class Component { this.warn(node as any, compiler_warnings.non_top_level_reactive_declaration); } - if (is_reference(node, parent)) { + if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { const object = get_object(node); const { name } = object; @@ -856,7 +898,7 @@ export default class Component { if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) { if (!((/Function/.test(parent.type) && prop === 'params') || (parent.type === 'VariableDeclarator' && prop === 'id'))) { - this.error(node as any, compiler_errors.contextual_store); + return this.error(node as any, compiler_errors.contextual_store); } } } @@ -901,7 +943,7 @@ export default class Component { let scope = instance_scope; walk(this.ast.instance.content, { - enter(node: Node, parent, key, index) { + enter(node: Node) { if (/Function/.test(node.type)) { return this.skip(); } @@ -910,75 +952,130 @@ export default class Component { scope = map.get(node); } + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + return this.replace(node.declaration); + } + if (node.type === 'VariableDeclaration') { + // NOTE: `var` does not follow block scoping 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, compiler_errors.destructured_prop); + const inserts = []; + const props = []; + + function add_new_props(exported, local, default_value) { + props.push({ + type: 'Property', + method: false, + shorthand: false, + computed: false, + kind: 'init', + key: exported, + value: default_value + ? { + type: 'AssignmentPattern', + left: local, + right: default_value } + : local + }); + } + // transform + // ``` + // export let { x, y = 123 } = OBJ, z = 456 + // ``` + // into + // ``` + // let { x: x$, y: y$ = 123 } = OBJ; + // let { x = x$, y = y$, z = 456 } = $$props; + // ``` + for (let index = 0; index < node.declarations.length; index++) { + const declarator = node.declarations[index]; + if (declarator.id.type !== 'Identifier') { + function get_new_name(local) { + const variable = component.var_lookup.get(local.name); if (variable.subscribable) { inserts.push(get_insert(variable)); } - }); - if (inserts.length) { - parent[key].splice(index + 1, 0, ...inserts); + if (variable.export_name && variable.writable) { + const alias_name = component.get_unique_name(local.name); + add_new_props({ type: 'Identifier', name: variable.export_name }, local, alias_name); + return alias_name; + } + return local; } - return; - } - - const { name } = declarator.id; - const variable = component.var_lookup.get(name); + function rename_identifiers(param: Node) { + switch (param.type) { + case 'ObjectPattern': { + const handle_prop = (prop: Property | RestElement) => { + if (prop.type === 'RestElement') { + rename_identifiers(prop); + } else if (prop.value.type === 'Identifier') { + prop.value = get_new_name(prop.value); + } else { + rename_identifiers(prop.value); + } + }; + + param.properties.forEach(handle_prop); + break; + } + case 'ArrayPattern': { + const handle_element = (element: Node, index: number, array: Node[]) => { + if (element) { + if (element.type === 'Identifier') { + array[index] = get_new_name(element); + } else { + rename_identifiers(element); + } + } + }; + + param.elements.forEach(handle_element); + break; + } + + case 'RestElement': + param.argument = get_new_name(param.argument); + break; + + case 'AssignmentPattern': + param.left = get_new_name(param.left); + break; + } + } - if (variable.export_name && variable.writable) { - 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`; + rename_identifiers(declarator.id); + } else { + const { name } = declarator.id; + const variable = component.var_lookup.get(name); + const is_props = variable.export_name && variable.writable; + if (is_props) { + add_new_props({ type: 'Identifier', name: variable.export_name }, declarator.id, declarator.init); + node.declarations.splice(index--, 1); + } + if (variable.subscribable && (is_props || declarator.init)) { + inserts.push(get_insert(variable)); + } } + } - if (variable.subscribable && declarator.init) { - const insert = get_insert(variable); - parent[key].splice(index + 1, 0, ...insert); - } - }); + this.replace(b` + ${node.declarations.length ? node : null} + ${ props.length > 0 && b`let { ${ props } } = $$props;`} + ${inserts} + ` as any); + return this.skip(); } } }, - leave(node: Node, parent, _key, index) { + leave(node: Node) { if (map.has(node)) { scope = scope.parent; } - - if (node.type === 'ExportNamedDeclaration' && node.declaration) { - (parent as Program).body[index] = node.declaration; - } } }); } @@ -1081,7 +1178,7 @@ export default class Component { scope = map.get(node); } - if (is_reference(node as Node, parent as Node)) { + if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { const { name } = flatten_reference(node); const owner = scope.find_owner(name); @@ -1169,12 +1266,16 @@ export default class Component { }> = []; this.ast.instance.content.body.forEach(node => { + const ignores = extract_svelte_ignore_from_comments(node); + if (ignores.length) this.push_ignores(ignores); + 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(); + const module_dependencies = new Set(); let scope = this.instance_scope; const map = this.instance_scope_map; @@ -1199,7 +1300,7 @@ export default class Component { } 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)) { + } else if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { const identifier = get_object(node); if (!assignee_nodes.has(identifier)) { const { name } = identifier; @@ -1211,7 +1312,7 @@ export default class Component { variable.is_reactive_dependency = true; if (variable.module) { should_add_as_dependency = false; - component.warn(node as any, compiler_warnings.module_script_variable_reactive_declaration(name)); + module_dependencies.add(name); } } const is_writable_or_mutated = @@ -1236,6 +1337,10 @@ export default class Component { } }); + if (module_dependencies.size > 0 && dependencies.size === 0) { + component.warn(node.body as any, compiler_warnings.module_script_variable_reactive_declaration(Array.from(module_dependencies))); + } + const { expression } = node.body as ExpressionStatement; const declaration = expression && (expression as AssignmentExpression).left; @@ -1246,6 +1351,8 @@ export default class Component { declaration }); } + + if (ignores.length) this.pop_ignores(); }); const lookup = new Map(); @@ -1276,7 +1383,7 @@ export default class Component { if (cycle && cycle.length) { const declarationList = lookup.get(cycle[0]); const declaration = declarationList[0]; - this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle)); + return this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle)); } const add_declaration = declaration => { @@ -1299,7 +1406,7 @@ export default class Component { warn_if_undefined(name: string, node, template_scope: TemplateScope) { if (name[0] === '$') { if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) { - this.error(node, compiler_errors.illegal_global(name)); + return this.error(node, compiler_errors.illegal_global(name)); } this.has_reactive_assignments = true; // TODO does this belong here? @@ -1348,13 +1455,13 @@ function process_component_options(component: Component, nodes) { if (!chunk) return true; if (value.length > 1) { - component.error(attribute, { code, message }); + return component.error(attribute, { code, message }); } if (chunk.type === 'Text') return chunk.data; if (chunk.expression.type !== 'Literal') { - component.error(attribute, { code, message }); + return component.error(attribute, { code, message }); } return chunk.expression.value; @@ -1370,11 +1477,11 @@ function process_component_options(component: Component, nodes) { const tag = get_value(attribute, compiler_errors.invalid_tag_attribute); if (typeof tag !== 'string' && tag !== null) { - component.error(attribute, compiler_errors.invalid_tag_attribute); + return component.error(attribute, compiler_errors.invalid_tag_attribute); } if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) { - component.error(attribute, compiler_errors.invalid_tag_property); + return component.error(attribute, compiler_errors.invalid_tag_property); } if (tag && !component.compile_options.customElement) { @@ -1389,12 +1496,12 @@ function process_component_options(component: Component, nodes) { const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute); if (typeof ns !== 'string') { - component.error(attribute, compiler_errors.invalid_namespace_attribute); + return component.error(attribute, compiler_errors.invalid_namespace_attribute); } if (valid_namespaces.indexOf(ns) === -1) { const match = fuzzymatch(ns, valid_namespaces); - component.error(attribute, compiler_errors.invalid_namespace_property(ns, match)); + return component.error(attribute, compiler_errors.invalid_namespace_property(ns, match)); } component_options.namespace = ns; @@ -1407,7 +1514,7 @@ function process_component_options(component: Component, nodes) { const value = get_value(attribute, compiler_errors.invalid_attribute_value(name)); if (typeof value !== 'boolean') { - component.error(attribute, compiler_errors.invalid_attribute_value(name)); + return component.error(attribute, compiler_errors.invalid_attribute_value(name)); } component_options[name] = value; @@ -1415,10 +1522,10 @@ function process_component_options(component: Component, nodes) { } default: - component.error(attribute, compiler_errors.invalid_options_attribute_unknown); + return component.error(attribute, compiler_errors.invalid_options_attribute_unknown); } } else { - component.error(attribute, compiler_errors.invalid_options_attribute); + return component.error(attribute, compiler_errors.invalid_options_attribute); } }); } diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 7ef06c0bbb..54263c3eb9 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -174,10 +174,6 @@ export default { code: 'default-export', message: 'A component cannot have a default export' }, - not_implemented: { - code: 'not-implemented', - message: 'A component currently cannot have an export ... from' - }, illegal_declaration: { code: 'illegal-declaration', message: 'The $ prefix is reserved, and cannot be used for variable and import names' @@ -190,10 +186,6 @@ export default { code: 'illegal-global', message: `${name} is an illegal variable name` }), - destructured_prop: { - code: 'destructured-prop', - message: 'Cannot declare props in destructured declaration' - }, cyclical_reactive_declaration: (cycle: string[]) => ({ code: 'cyclical-reactive-declaration', message: `Cyclical dependency detected: ${cycle.join(' → ')}` diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 3b8ba929fa..bfcd779fc7 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -20,9 +20,9 @@ export default { code: 'non-top-level-reactive-declaration', message: '$: has no effect outside of the top-level' }, - module_script_variable_reactive_declaration: (name: string) => ({ + module_script_variable_reactive_declaration: (names: string[]) => ({ code: 'module-script-reactive-declaration', - message: `"${name}" is declared in a module script and will not be reactive` + message: `${names.map(name => `"${name}"`).join(', ')} ${names.length > 1 ? 'are' : 'is'} declared in a module script and will not be reactive` }), missing_declaration: (name: string, has_script: boolean) => ({ code: 'missing-declaration', diff --git a/src/compiler/compile/create_module.ts b/src/compiler/compile/create_module.ts index 80e6308263..037b2b396e 100644 --- a/src/compiler/compile/create_module.ts +++ b/src/compiler/compile/create_module.ts @@ -1,7 +1,7 @@ import list from '../utils/list'; import { ModuleFormat } from '../interfaces'; import { b, x } from 'code-red'; -import { Identifier, ImportDeclaration } from 'estree'; +import { Identifier, ImportDeclaration, ExportNamedDeclaration } from 'estree'; const wrappers = { esm, cjs }; @@ -19,20 +19,21 @@ export default function create_module( helpers: Array<{ name: string; alias: Identifier }>, globals: Array<{ name: string; alias: Identifier }>, imports: ImportDeclaration[], - module_exports: Export[] + module_exports: Export[], + exports_from: ExportNamedDeclaration[] ) { const internal_path = `${sveltePath}/internal`; helpers.sort((a, b) => (a.name < b.name) ? -1 : 1); globals.sort((a, b) => (a.name < b.name) ? -1 : 1); + + const formatter = wrappers[format]; - if (format === 'esm') { - return esm(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports); + if (!formatter) { + throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`); } - if (format === 'cjs') return cjs(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports); - - throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`); + return formatter(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports, exports_from); } function edit_source(source, sveltePath) { @@ -76,7 +77,8 @@ function esm( helpers: Array<{ name: string; alias: Identifier }>, globals: Array<{ name: string; alias: Identifier }>, imports: ImportDeclaration[], - module_exports: Export[] + module_exports: Export[], + exports_from: ExportNamedDeclaration[] ) { const import_declaration = { type: 'ImportDeclaration', @@ -94,6 +96,9 @@ function esm( imports.forEach(node => { node.source.value = edit_source(node.source.value, sveltePath); }); + exports_from.forEach(node => { + node.source!.value = edit_source(node.source!.value, sveltePath); + }); const exports = module_exports.length > 0 && { type: 'ExportNamedDeclaration', @@ -110,6 +115,7 @@ function esm( ${import_declaration} ${internal_globals} ${imports} + ${exports_from} ${program.body} @@ -127,7 +133,8 @@ function cjs( helpers: Array<{ name: string; alias: Identifier }>, globals: Array<{ name: string; alias: Identifier }>, imports: ImportDeclaration[], - module_exports: Export[] + module_exports: Export[], + exports_from: ExportNamedDeclaration[] ) { const internal_requires = { type: 'VariableDeclaration', @@ -183,6 +190,13 @@ function cjs( const exports = module_exports.map(x => b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`); + const user_exports_from = exports_from.map(node => { + const init = x`require("${edit_source(node.source.value, sveltePath)}")`; + return node.specifiers.map(specifier => { + return b`exports.${specifier.exported} = ${init}.${specifier.local};`; + }); + }); + program.body = b` /* ${banner} */ @@ -190,6 +204,7 @@ function cjs( ${internal_requires} ${internal_globals} ${user_requires} + ${user_exports_from} ${program.body} diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 8dfb135c3a..d9868f4530 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -47,8 +47,9 @@ export default class Selector { this.local_blocks = this.blocks.slice(0, i); const host_only = this.blocks.length === 1 && this.blocks[0].host; + const root_only = this.blocks.length === 1 && this.blocks[0].root; - this.used = this.local_blocks.length === 0 || host_only; + this.used = this.local_blocks.length === 0 || host_only || root_only; } apply(node: Element) { @@ -138,7 +139,7 @@ export default class Selector { for (let i = start; i < end; i += 1) { if (this.blocks[i].global) { - component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global); + return component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global); } } @@ -226,7 +227,8 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: Array<{ return false; } else if (block.combinator.name === '>') { - if (apply_selector(blocks, get_element_parent(node), to_encapsulate)) { + const has_global_parent = blocks.every(block => block.global); + if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) { to_encapsulate.push({ node, block }); return true; } @@ -273,18 +275,16 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN const selector = block.selectors[i]; const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1'); - if (selector.type === 'PseudoClassSelector' && name === 'host') { + if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) { return BlockAppliesToNode.NotPossible; } - if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { - continue; + if (block.selectors.length === 1 && selector.type === 'PseudoClassSelector' && name === 'global') { + return BlockAppliesToNode.NotPossible; } - if (selector.type === 'PseudoClassSelector' && name === 'global') { - // TODO shouldn't see this here... maybe we should enforce that :global(...) - // cannot be sandwiched between non-global selectors? - return BlockAppliesToNode.NotPossible; + if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { + continue; } if (selector.type === 'ClassSelector') { @@ -582,6 +582,7 @@ function loop_child(children: INode[], adjacent_only: boolean) { class Block { host: boolean; + root: boolean; combinator: CssNode; selectors: CssNode[] start: number; @@ -591,6 +592,7 @@ class Block { constructor(combinator: CssNode) { this.combinator = combinator; this.host = false; + this.root = false; this.selectors = []; this.start = null; @@ -604,6 +606,7 @@ class Block { this.start = selector.start; this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host'; } + this.root = this.root || selector.type === 'PseudoClassSelector' && selector.name === 'root'; this.selectors.push(selector); this.end = selector.end; diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index 76ffe94397..ed1682378c 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -7,6 +7,7 @@ import Component from '../Component'; import { CssNode } from './interfaces'; import hash from '../utils/hash'; import compiler_warnings from '../compiler_warnings'; +import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore'; function remove_css_prefix(name: string): string { return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, ''); @@ -447,10 +448,13 @@ export default class Stylesheet { } warn_on_unused_selectors(component: Component) { + const ignores = !this.ast.css ? [] : extract_ignores_above_position(this.ast.css.start, this.ast.html.children); + component.push_ignores(ignores); this.children.forEach(child => { child.warn_on_unused_selector((selector: Selector) => { component.warn(selector.node, compiler_warnings.css_unused_selector(this.source.slice(selector.node.start, selector.node.end))); }); }); + component.pop_ignores(); } } diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index e40ff37e92..96b24bceee 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -14,6 +14,7 @@ const valid_options = [ 'filename', 'sourcemap', 'generate', + 'errorMode', 'varsReport', 'outputFilename', 'cssOutputFilename', diff --git a/src/compiler/compile/nodes/Animation.ts b/src/compiler/compile/nodes/Animation.ts index 3d8be92fa8..ac9dddd275 100644 --- a/src/compiler/compile/nodes/Animation.ts +++ b/src/compiler/compile/nodes/Animation.ts @@ -22,12 +22,14 @@ export default class Animation extends Node { if (parent.animation) { component.error(this, compiler_errors.duplicate_animation); + return; } const block = parent.parent; if (!block || block.type !== 'EachBlock' || !block.key) { // TODO can we relax the 'immediate child' rule? component.error(this, compiler_errors.invalid_animation_immediate); + return; } (block as EachBlock).has_animation = true; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 3584707ed5..1efc1a3038 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -37,6 +37,7 @@ export default class Binding extends Node { if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { component.error(info, compiler_errors.invalid_directive_value); + return; } this.name = info.name; @@ -50,9 +51,11 @@ export default class Binding extends Node { // make sure we track this as a mutable ref if (scope.is_let(name)) { component.error(this, compiler_errors.invalid_binding_let); + return; } else if (scope.names.has(name)) { if (scope.is_await(name)) { component.error(this, compiler_errors.invalid_binding_await); + return; } scope.dependencies_for_name.get(name).forEach(name => { @@ -66,12 +69,14 @@ export default class Binding extends Node { if (!variable || variable.global) { component.error(this.expression.node as any, compiler_errors.binding_undeclared(name)); + return; } variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; if (info.expression.type === 'Identifier' && !variable.writable) { component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale); + return; } } diff --git a/src/compiler/compile/nodes/Comment.ts b/src/compiler/compile/nodes/Comment.ts index 4e871d17f4..79580d0563 100644 --- a/src/compiler/compile/nodes/Comment.ts +++ b/src/compiler/compile/nodes/Comment.ts @@ -3,8 +3,6 @@ import Component from '../Component'; import Node from './shared/Node'; import TemplateScope from './shared/TemplateScope'; -const pattern = /^\s*svelte-ignore\s+([\s\S]+)\s*$/m; - export default class Comment extends Node { type: 'Comment'; data: string; @@ -13,8 +11,6 @@ export default class Comment extends Node { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); this.data = info.data; - - const match = pattern.exec(this.data); - this.ignores = match ? match[1].split(/[^\S]/).map(x => x.trim()).filter(Boolean) : []; + this.ignores = info.ignores; } } diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index 4e6b809239..c03128c388 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -63,6 +63,7 @@ export default class EachBlock extends AbstractBlock { if (this.children.length !== 1) { const child = this.children.find(child => !!(child as Element).animation); component.error((child as Element).animation, compiler_errors.invalid_animation_sole); + return; } } diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 675cbb16e7..07f948741e 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -144,6 +144,7 @@ export default class Element extends Node { const value_attribute = info.attributes.find(node => node.name === 'value'); if (value_attribute) { component.error(value_attribute, compiler_errors.textarea_duplicate_value); + return; } // this is an egregious hack, but it's the easiest way to get