diff --git a/.eslintignore b/.eslintignore index a0ca1e55c8..bfe7b1fa95 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ **/_actual.js **/expected.js +_output test/*/samples/*/output.js node_modules diff --git a/.gitignore b/.gitignore index 053f905294..b0e6896dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ node_modules /test/sourcemaps/samples/*/output.css.map /yarn-error.log _actual*.* +_output /types /site/cypress/screenshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c286390f8a..c5fccddcb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,30 @@ # Svelte changelog -## Unreleased +## 3.16.5 + +* Better fix for cascading invalidations and fix some regressions ([#4098](https://github.com/sveltejs/svelte/issues/4098), [#4114](https://github.com/sveltejs/svelte/issues/4114), [#4120](https://github.com/sveltejs/svelte/issues/4120)) + +## 3.16.4 + +* Fix slots with props not propagating through to inner slots ([#4061](https://github.com/sveltejs/svelte/issues/4061)) +* Fix noting autosubscribed stores as `referenced` in `vars` for tooling ([#4081](https://github.com/sveltejs/svelte/issues/4081)) +* Fix cascading invalidations in certain situations ([#4094](https://github.com/sveltejs/svelte/issues/4094)) + +## 3.16.3 + +* Fix bitmask overflow when using slotted components ([#4077](https://github.com/sveltejs/svelte/issues/4077)) +* Remove unnecessary `$$invalidate` calls from init block ([#4018](https://github.com/sveltejs/svelte/issues/4018)) + +## 3.16.2 + +* Handle slot updates when parent component has a bitmask overflow ([#4078](https://github.com/sveltejs/svelte/pull/4078)) + +## 3.16.1 * Fix unused export warning for props used as stores ([#4021](https://github.com/sveltejs/svelte/issues/4021)) * Fix `{:then}` without resolved value containing `{#each}` ([#4022](https://github.com/sveltejs/svelte/issues/4022)) * Fix incorrect code generated with `loopGuardTimeout` ([#4034](https://github.com/sveltejs/svelte/issues/4034)) +* Fix handling of bitmask overflow and globals ([#4037](https://github.com/sveltejs/svelte/issues/4037)) * Fix `{:then}` containing `{#if}` ([#4044](https://github.com/sveltejs/svelte/issues/4044)) * Fix bare `import`s in `format: 'cjs'` output mode ([#4055](https://github.com/sveltejs/svelte/issues/4050)) * Warn when using a known global as a component name ([#4070](https://github.com/sveltejs/svelte/issues/4070)) diff --git a/README.md b/README.md index b5a0e895dc..9460efce76 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ install size - - + build status diff --git a/package-lock.json b/package-lock.json index 4a45a6e5fd..f3eb858358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.15.0", + "version": "3.16.5", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -500,9 +500,9 @@ "dev": true }, "code-red": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.0.26.tgz", - "integrity": "sha512-W4t68vk3xJjmkbuAKfEtaj7E+K82BtV+A4VjBlxHA6gDoSLc+sTB643JdJMSk27vpp5iEqHFuGnHieQGy/GmUQ==", + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.0.27.tgz", + "integrity": "sha512-MSILIi8kkSGag76TW+PRfBP/dN++Ei+uTeiUfqW4Es5teFNrbsAWtyAbPwxKI1vxEsBx64loAfGxS1kVCo1d2g==", "dev": true, "requires": { "acorn": "^7.1.0", diff --git a/package.json b/package.json index 5a75422427..36eaf2e323 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.16.0", + "version": "3.16.5", "description": "Cybernetically enhanced web apps", "module": "index.mjs", "main": "index", @@ -64,7 +64,7 @@ "acorn": "^7.1.0", "agadoo": "^1.1.0", "c8": "^5.0.1", - "code-red": "0.0.26", + "code-red": "0.0.27", "codecov": "^3.5.0", "css-tree": "1.0.0-alpha22", "eslint": "^6.3.0", diff --git a/site/src/routes/examples/_TableOfContents.svelte b/site/src/routes/examples/_TableOfContents.svelte index f89a9741ae..167e889e2a 100644 --- a/site/src/routes/examples/_TableOfContents.svelte +++ b/site/src/routes/examples/_TableOfContents.svelte @@ -29,13 +29,33 @@ font-weight: 700; } + div { + display: flex; + flex-direction: row; + padding: 0.2rem 3rem; + margin: 0 -3rem; + } + + div.active { + background: rgba(0, 0, 0, 0.15) calc(100% - 3rem) 47% no-repeat + url(/icons/arrow-right.svg); + background-size: 1em 1em; + color: white; + } + + div.active.loading { + background: rgba(0, 0, 0, 0.1) calc(100% - 3rem) 47% no-repeat + url(/icons/loading.svg); + background-size: 1em 1em; + color: white; + } + a { display: flex; + flex: 1 1 auto; position: relative; color: var(--sidebar-text); border-bottom: none; - padding: 0.2rem 3rem; - margin: 0 -3rem; font-size: 1.6rem; align-items: center; justify-content: start; @@ -45,18 +65,11 @@ color: white; } - a.active { - background: rgba(0, 0, 0, 0.15) calc(100% - 3rem) 50% no-repeat - url(/icons/arrow-right.svg); - background-size: 1em 1em; - color: white; - } - - a.active.loading { - background: rgba(0, 0, 0, 0.1) calc(100% - 3rem) 50% no-repeat - url(/icons/loading.svg); - background-size: 1em 1em; - color: white; + .repl-link { + flex: 0 1 auto; + font-size: 1.2rem; + font-weight: 700; + margin-right: 2.5rem; } .thumbnail { @@ -72,27 +85,31 @@ diff --git a/site/src/routes/index.svelte b/site/src/routes/index.svelte index cbbdf43e95..edf1e4d36c 100644 --- a/site/src/routes/index.svelte +++ b/site/src/routes/index.svelte @@ -85,7 +85,8 @@
-npx degit sveltejs/template my-svelte-project
+npx degit sveltejs/template my-svelte-project
+# or download and extract this .zip file
 cd my-svelte-project
 
 npm install
diff --git a/site/src/routes/repl/[id]/index.svelte b/site/src/routes/repl/[id]/index.svelte
index 59b8e6359a..4d05ac28ea 100644
--- a/site/src/routes/repl/[id]/index.svelte
+++ b/site/src/routes/repl/[id]/index.svelte
@@ -187,7 +187,7 @@
 
 	{name} • REPL • Svelte
 
-	
+	
 	
 	
 
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
index 4402a4a947..2c7fb64b67 100644
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -203,7 +203,10 @@ export default class Component {
 			const subscribable_name = name.slice(1);
 
 			const variable = this.var_lookup.get(subscribable_name);
-			if (variable) variable.subscribable = true;
+			if (variable) {
+				variable.referenced   = true;
+				variable.subscribable = true;
+			}
 		} else {
 			this.used_names.add(name);
 		}
diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts
index ac52f59471..5d29f71e35 100644
--- a/src/compiler/compile/index.ts
+++ b/src/compiler/compile/index.ts
@@ -33,7 +33,7 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
 	const { name, filename, loopGuardTimeout, dev } = options;
 
 	Object.keys(options).forEach(key => {
-		if (valid_options.indexOf(key) === -1) {
+		if (!valid_options.includes(key)) {
 			const match = fuzzymatch(key, valid_options);
 			let message = `Unrecognized option '${key}'`;
 			if (match) message += ` (did you mean '${match}'?)`;
diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts
index 6de1c92f07..046a90b684 100644
--- a/src/compiler/compile/render_dom/Renderer.ts
+++ b/src/compiler/compile/render_dom/Renderer.ts
@@ -15,6 +15,11 @@ interface ContextMember {
 	priority: number;
 }
 
+type BitMasks = Array<{
+	n: number;
+	names: string[];
+}>;
+
 export default class Renderer {
 	component: Component; // TODO Maybe Renderer shouldn't know about Component?
 	options: CompileOptions;
@@ -81,8 +86,6 @@ export default class Renderer {
 			null
 		);
 
-		this.context_overflow = this.context.length > 31;
-
 		// TODO messy
 		this.blocks.forEach(block => {
 			if (block instanceof Block) {
@@ -94,6 +97,8 @@ export default class Renderer {
 
 		this.fragment.render(this.block, null, x`#nodes` as Identifier);
 
+		this.context_overflow = this.context.length > 31;
+
 		this.context.forEach(member => {
 			const { variable } = member;
 			if (variable) {
@@ -199,65 +204,53 @@ export default class Renderer {
 			? x`$$self.$$.dirty`
 			: x`#dirty`) as Identifier | MemberExpression;
 
-		const get_bitmask = () => names.reduce((bitmask, name) => {
-			const member = renderer.context_lookup.get(name);
-
-			if (!member) return bitmask;
+		const get_bitmask = () => {
+			const bitmask: BitMasks = [];
+			names.forEach((name) => {
+				const member = renderer.context_lookup.get(name);
 
-			if (member.index.value === -1) {
-				throw new Error(`unset index`);
-			}
+				if (!member) return;
 
-			const value = member.index.value as number;
-			const i = (value / 31) | 0;
-			const n = 1 << (value % 31);
+				if (member.index.value === -1) {
+					throw new Error(`unset index`);
+				}
 
-			if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };
+				const value = member.index.value as number;
+				const i = (value / 31) | 0;
+				const n = 1 << (value % 31);
 
-			bitmask[i].n |= n;
-			bitmask[i].names.push(name);
+				if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };
 
+				bitmask[i].n |= n;
+				bitmask[i].names.push(name);
+			});
 			return bitmask;
-		}, Array((this.context.length / 31) | 0).fill(null));
-
-		let operator;
-		let left;
-		let right;
+		};
 
 		return {
-			get type() {
-				// we make the type a getter, even though it's always
-				// a BinaryExpression, because it gives us an opportunity
-				// to lazily create the node. TODO would be better if
-				// context was determined before rendering, so that
-				// this indirection was unnecessary
-
+			// Using a ParenthesizedExpression allows us to create
+			// the expression lazily. TODO would be better if
+			// context was determined before rendering, so that
+			// this indirection was unnecessary
+			type: 'ParenthesizedExpression',
+			get expression() {
 				const bitmask = get_bitmask();
 
+				if (!bitmask.length) {
+					return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression;
+				}
+
 				if (renderer.context_overflow) {
-					const expression = bitmask
+					return bitmask
 						.map((b, i) => ({ b, i }))
 						.filter(({ b }) => b)
 						.map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`)
 						.reduce((lhs, rhs) => x`${lhs} | ${rhs}`);
-
-					({ operator, left, right } = expression);
-				} else {
-					({ operator, left, right } = x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0] ? bitmask[0].n : 0}` as BinaryExpression); // TODO the `: 0` case should never apply
 				}
 
-				return 'BinaryExpression';
-			},
-			get operator() {
-				return operator;
-			},
-			get left() {
-				return left;
-			},
-			get right() {
-				return right;
+				return x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression;
 			}
-		} as Expression;
+		} as any;
 	}
 
 	reference(node: string | Identifier | MemberExpression) {
diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts
index 9fb02ddb8d..a607edaf1e 100644
--- a/src/compiler/compile/render_dom/index.ts
+++ b/src/compiler/compile/render_dom/index.ts
@@ -3,7 +3,7 @@ import Component from '../Component';
 import Renderer from './Renderer';
 import { CompileOptions } from '../../interfaces';
 import { walk } from 'estree-walker';
-import { extract_names } from '../utils/scope';
+import { extract_names, Scope } from '../utils/scope';
 import { invalidate } from './invalidate';
 import Block from './Block';
 import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
@@ -203,11 +203,18 @@ export default function dom(
 	if (component.ast.instance) {
 		let scope = component.instance_scope;
 		const map = component.instance_scope_map;
+		let execution_context: Node | null = null;
 
 		walk(component.ast.instance.content, {
-			enter: (node) => {
+			enter(node) {
 				if (map.has(node)) {
-					scope = map.get(node);
+					scope = map.get(node) as Scope;
+
+					if (!execution_context && !scope.block) {
+						execution_context = node;
+					}
+				} else if (!execution_context && node.type === 'LabeledStatement' && node.label.name === '$') {
+					execution_context = node;
 				}
 			},
 
@@ -216,6 +223,10 @@ export default function dom(
 					scope = scope.parent;
 				}
 
+				if (execution_context === node) {
+					execution_context = null;
+				}
+
 				if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
 					const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
 
@@ -225,7 +236,7 @@ export default function dom(
 					// onto the initial function call
 					const names = new Set(extract_names(assignee));
 
-					this.replace(invalidate(renderer, scope, node, names));
+					this.replace(invalidate(renderer, scope, node, names, execution_context === null));
 				}
 			}
 		});
diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts
index 98fca59028..65fb73afc3 100644
--- a/src/compiler/compile/render_dom/invalidate.ts
+++ b/src/compiler/compile/render_dom/invalidate.ts
@@ -1,42 +1,50 @@
 import { nodes_match } from '../../utils/nodes_match';
 import { Scope } from '../utils/scope';
 import { x } from 'code-red';
-import { Node } from 'estree';
+import { Node, Expression } from 'estree';
 import Renderer from './Renderer';
+import { Var } from '../../interfaces';
 
-export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: Set) {
+export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: Set, main_execution_context: boolean = false) {
 	const { component } = renderer;
 
-	const [head, ...tail] = Array.from(names).filter(name => {
-		const owner = scope.find_owner(name);
-		if (owner && owner !== component.instance_scope) return false;
+	const [head, ...tail] = Array.from(names)
+		.filter(name => {
+			const owner = scope.find_owner(name);
+			return !owner || owner === component.instance_scope;
+		})
+		.map(name => component.var_lookup.get(name))
+		.filter(variable =>	{
+			return variable && (
+				!variable.hoistable &&
+				!variable.global &&
+				!variable.module &&
+				(
+					variable.referenced ||
+					variable.subscribable ||
+					variable.is_reactive_dependency ||
+					variable.export_name ||
+					variable.name[0] === '$'
+				)
+			);
+		}) as Var[];
 
-		const variable = component.var_lookup.get(name);
+	function get_invalidated(variable: Var, node?: Expression) {
+		if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
+			return node || x`${variable.name}`;
+		}
 
-		return variable && (
-			!variable.hoistable &&
-			!variable.global &&
-			!variable.module &&
-			(
-				variable.referenced ||
-				variable.subscribable ||
-				variable.is_reactive_dependency ||
-				variable.export_name ||
-				variable.name[0] === '$'
-			)
-		);
-	});
+		return renderer.invalidate(variable.name);
+	}
 
 	if (head) {
 		component.has_reactive_assignments = true;
 
 		if (node.type === 'AssignmentExpression' && node.operator === '=' && nodes_match(node.left, node.right) && tail.length === 0) {
-			return renderer.invalidate(head);
+			return get_invalidated(head, node);
 		} else {
-			const is_store_value = head[0] === '$';
-			const variable = component.var_lookup.get(head);
-
-			const extra_args = tail.map(name => renderer.invalidate(name));
+			const is_store_value = head.name[0] === '$';
+			const extra_args = tail.map(variable => get_invalidated(variable));
 
 			const pass_value = (
 				extra_args.length > 0 ||
@@ -47,16 +55,18 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
 			if (pass_value) {
 				extra_args.unshift({
 					type: 'Identifier',
-					name: head
+					name: head.name
 				});
 			}
 
 			let invalidate = is_store_value
-				? x`@set_store_value(${head.slice(1)}, ${node}, ${extra_args})`
-				: x`$$invalidate(${renderer.context_lookup.get(head).index}, ${node}, ${extra_args})`;
+				? x`@set_store_value(${head.name.slice(1)}, ${node}, ${extra_args})`
+				: !main_execution_context
+					? x`$$invalidate(${renderer.context_lookup.get(head.name).index}, ${node}, ${extra_args})`
+					: node;
 
-			if (variable.subscribable && variable.reassigned) {
-				const subscribe = `$$subscribe_${head}`;
+			if (head.subscribable && head.reassigned) {
+				const subscribe = `$$subscribe_${head.name}`;
 				invalidate = x`${subscribe}(${invalidate})}`;
 			}
 
diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
index 13c96d0065..631c172576 100644
--- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
@@ -182,11 +182,9 @@ export default class InlineComponentWrapper extends Wrapper {
 			});
 		});
 
-		const non_let_dependencies = Array.from(fragment_dependencies).filter(name => !this.node.scope.is_let(name));
-
 		const dynamic_attributes = this.node.attributes.filter(a => a.get_dependencies().length > 0);
 
-		if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || non_let_dependencies.length > 0)) {
+		if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0)) {
 			updates.push(b`const ${name_changes} = {};`);
 		}
 
@@ -266,9 +264,9 @@ export default class InlineComponentWrapper extends Wrapper {
 			}
 		}
 
-		if (non_let_dependencies.length > 0) {
+		if (fragment_dependencies.size > 0) {
 			updates.push(b`
-				if (${renderer.dirty(non_let_dependencies)}) {
+				if (${renderer.dirty(Array.from(fragment_dependencies))}) {
 					${name_changes}.$$scope = { dirty: #dirty, ctx: #ctx };
 				}`);
 		}
diff --git a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts
index 24ca813684..2adbd3b1d0 100644
--- a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts
+++ b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts
@@ -2,6 +2,7 @@ import Let from '../../../nodes/Let';
 import { x, p } from 'code-red';
 import Block from '../../Block';
 import TemplateScope from '../../../nodes/shared/TemplateScope';
+import { BinaryExpression } from 'estree';
 
 export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]) {
 	if (lets.length === 0) return { block, scope };
@@ -28,21 +29,48 @@ export function get_slot_definition(block: Block, scope: TemplateScope, lets: Le
 		properties: Array.from(names).map(name => p`${block.renderer.context_lookup.get(name).index}: ${name}`)
 	};
 
-	const changes = Array.from(names)
-		.map(name => {
-			const { context_lookup } = block.renderer;
+	const { context_lookup } = block.renderer;
 
-			const literal = {
-				type: 'Literal',
-				get value() {
+	// i am well aware that this code is gross
+	// TODO make it less gross
+	const changes = {
+		type: 'ParenthesizedExpression',
+		get expression() {
+			if (block.renderer.context_overflow) {
+				const grouped = [];
+
+				Array.from(names).forEach(name => {
 					const i = context_lookup.get(name).index.value as number;
-					return 1 << i;
+					const g = Math.floor(i / 31);
+
+					if (!grouped[g]) grouped[g] = [];
+					grouped[g].push({ name, n: i % 31 });
+				});
+
+				const elements = [];
+
+				for (let g = 0; g < grouped.length; g += 1) {
+					elements[g] = grouped[g]
+						? grouped[g]
+							.map(({ name, n }) => x`${name} ? ${1 << n} : 0`)
+							.reduce((lhs, rhs) => x`${lhs} | ${rhs}`)
+						: x`0`;
 				}
-			};
 
-			return x`${name} ? ${literal} : 0`;
-		})
-		.reduce((lhs, rhs) => x`${lhs} | ${rhs}`);
+				return {
+					type: 'ArrayExpression',
+					elements
+				};
+			}
+
+			return Array.from(names)
+				.map(name => {
+					const i = context_lookup.get(name).index.value as number;
+					return x`${name} ? ${1 << i} : 0`;
+				})
+				.reduce((lhs, rhs) => x`${lhs} | ${rhs}`) as BinaryExpression;
+		}
+	};
 
 	return {
 		block,
diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts
index 2f864743a2..04c1264ab1 100644
--- a/src/runtime/internal/scheduler.ts
+++ b/src/runtime/internal/scheduler.ts
@@ -73,8 +73,9 @@ function update($$) {
 	if ($$.fragment !== null) {
 		$$.update();
 		run_all($$.before_update);
-		$$.fragment && $$.fragment.p($$.ctx, $$.dirty);
+		const dirty = $$.dirty;
 		$$.dirty = [-1];
+		$$.fragment && $$.fragment.p($$.ctx, dirty);
 
 		$$.after_update.forEach(add_render_callback);
 	}
diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts
index 7e8769cd88..b844f1dd4c 100644
--- a/src/runtime/internal/utils.ts
+++ b/src/runtime/internal/utils.ts
@@ -77,9 +77,23 @@ export function get_slot_context(definition, ctx, $$scope, fn) {
 }
 
 export function get_slot_changes(definition, $$scope, dirty, fn) {
-	return definition[2] && fn
-		? $$scope.dirty | definition[2](fn(dirty))
-		: $$scope.dirty;
+	if (definition[2] && fn) {
+		const lets = definition[2](fn(dirty));
+
+		if (typeof $$scope.dirty === 'object') {
+			const merged = [];
+			const len = Math.max($$scope.dirty.length, lets.length);
+			for (let i = 0; i < len; i += 1) {
+				merged[i] = $$scope.dirty[i] | lets[i];
+			}
+
+			return merged;
+		}
+
+		return $$scope.dirty | lets;
+	}
+
+	return $$scope.dirty;
 }
 
 export function exclude_internal_props(props) {
diff --git a/test/helpers.js b/test/helpers.js
index ff40ac5f79..2a03e0f436 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -1,6 +1,7 @@
 import * as jsdom from 'jsdom';
 import * as assert from 'assert';
 import * as glob from 'tiny-glob/sync.js';
+import * as path from 'path';
 import * as fs from 'fs';
 import * as colors from 'kleur';
 
@@ -237,3 +238,16 @@ export function useFakeTimers() {
 		}
 	};
 }
+
+export function mkdirp(dir) {
+	const parent = path.dirname(dir);
+	if (parent === dir) return;
+
+	mkdirp(parent);
+
+	try {
+		fs.mkdirSync(dir);
+	} catch (err) {
+		// do nothing
+	}
+}
\ No newline at end of file
diff --git a/test/js/samples/component-store-access-invalidate/expected.js b/test/js/samples/component-store-access-invalidate/expected.js
index 151bd6ed0e..4c7bfd7009 100644
--- a/test/js/samples/component-store-access-invalidate/expected.js
+++ b/test/js/samples/component-store-access-invalidate/expected.js
@@ -43,7 +43,7 @@ function instance($$self, $$props, $$invalidate) {
 	let $foo;
 	const foo = writable(0);
 	component_subscribe($$self, foo, value => $$invalidate(0, $foo = value));
-	return [$foo];
+	return [$foo, foo];
 }
 
 class Component extends SvelteComponent {
diff --git a/test/js/samples/instrumentation-script-main-block/expected.js b/test/js/samples/instrumentation-script-main-block/expected.js
new file mode 100644
index 0000000000..bc80924602
--- /dev/null
+++ b/test/js/samples/instrumentation-script-main-block/expected.js
@@ -0,0 +1,75 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append,
+	detach,
+	element,
+	init,
+	insert,
+	noop,
+	safe_not_equal,
+	set_data,
+	text
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let p;
+	let t0;
+	let t1;
+
+	return {
+		c() {
+			p = element("p");
+			t0 = text("x: ");
+			t1 = text(/*x*/ ctx[0]);
+		},
+		m(target, anchor) {
+			insert(target, p, anchor);
+			append(p, t0);
+			append(p, t1);
+		},
+		p(ctx, [dirty]) {
+			if (dirty & /*x*/ 1) set_data(t1, /*x*/ ctx[0]);
+		},
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(p);
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	let x = 0;
+	let y = 1;
+	x += 1;
+
+	{
+		x += 2;
+	}
+
+	setTimeout(
+		function foo() {
+			$$invalidate(0, x += 10);
+			$$invalidate(1, y += 20);
+		},
+		1000
+	);
+
+	$$self.$$.update = () => {
+		if ($$self.$$.dirty & /*x, y*/ 3) {
+			$: $$invalidate(0, x += y);
+		}
+	};
+
+	return [x];
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, {});
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/instrumentation-script-main-block/input.svelte b/test/js/samples/instrumentation-script-main-block/input.svelte
new file mode 100644
index 0000000000..8c01783710
--- /dev/null
+++ b/test/js/samples/instrumentation-script-main-block/input.svelte
@@ -0,0 +1,19 @@
+
+
+

x: {x}

diff --git a/test/runtime/index.js b/test/runtime/index.js index 60bd70a5d9..408fda40f4 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -3,6 +3,7 @@ import * as path from "path"; import * as fs from "fs"; import { rollup } from 'rollup'; import * as virtual from 'rollup-plugin-virtual'; +import * as glob from 'tiny-glob/sync.js'; import { clear_loops, flush, set_now, set_raf } from "../../internal"; import { @@ -10,7 +11,8 @@ import { loadConfig, loadSvelte, env, - setupHtmlEqual + setupHtmlEqual, + mkdirp } from "../helpers.js"; let svelte$; @@ -90,6 +92,33 @@ describe("runtime", () => { const window = env(); + glob('**/*.svelte', { cwd }).forEach(file => { + if (file[0] === '_') return; + + const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`; + const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`; + + if (fs.existsSync(out)) { + fs.unlinkSync(out); + } + + mkdirp(dir); + + try { + const { js } = compile( + fs.readFileSync(`${cwd}/${file}`, 'utf-8'), + { + ...compileOptions, + filename: file + } + ); + + fs.writeFileSync(out, js.code); + } catch (err) { + // do nothing + } + }); + return Promise.resolve() .then(() => { // hack to support transition tests @@ -195,7 +224,7 @@ describe("runtime", () => { } else { throw err; } - }).catch(err => { + }).catch(err => { failed.add(dir); showOutput(cwd, compileOptions, compile); // eslint-disable-line no-console throw err; diff --git a/test/runtime/samples/bitmask-overflow-2/_config.js b/test/runtime/samples/bitmask-overflow-2/_config.js new file mode 100644 index 0000000000..0b63791292 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-2/_config.js @@ -0,0 +1,3 @@ +export default { + error: `potato is not defined`, +}; \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-2/main.svelte b/test/runtime/samples/bitmask-overflow-2/main.svelte new file mode 100644 index 0000000000..10fa1bd5e2 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-2/main.svelte @@ -0,0 +1,35 @@ + +

{x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16 + x17 + x18 + x19 + x20 + x21 + x22 + x23 + x24 + x25 + x26 + x27 + x28 + x29 + x30 + x31 + x32}

\ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-3/_config.js b/test/runtime/samples/bitmask-overflow-3/_config.js new file mode 100644 index 0000000000..aee7d3237e --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-3/_config.js @@ -0,0 +1,3 @@ +export default { + error: `A is not defined`, +}; \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-3/main.svelte b/test/runtime/samples/bitmask-overflow-3/main.svelte new file mode 100644 index 0000000000..aa2c56a147 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-3/main.svelte @@ -0,0 +1,4 @@ + +foo diff --git a/test/runtime/samples/bitmask-overflow-slot-2/Echo.svelte b/test/runtime/samples/bitmask-overflow-slot-2/Echo.svelte new file mode 100644 index 0000000000..c8905184dc --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot-2/Echo.svelte @@ -0,0 +1,73 @@ + + +

{d1}

+

{d2}

+

{d3}

+

{d4}

+

{d5}

+

{d6}

+

{d7}

+

{d8}

+

{d9}

+

{d10}

+

{d11}

+

{d12}

+

{d13}

+

{d14}

+

{d15}

+

{d16}

+

{d17}

+

{d18}

+

{d19}

+

{d20}

+

{d21}

+

{d22}

+

{d23}

+

{d24}

+

{d25}

+

{d26}

+

{d27}

+

{d28}

+

{d29}

+

{d30}

+

{d31}

+

{d32}

+

{d33}

+ + \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot-2/_config.js b/test/runtime/samples/bitmask-overflow-slot-2/_config.js new file mode 100644 index 0000000000..b01bd81e00 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot-2/_config.js @@ -0,0 +1,96 @@ +export default { + html: ` +

d1

+

d2

+

d3

+

d4

+

d5

+

d6

+

d7

+

d8

+

d9

+

d10

+

d11

+

d12

+

d13

+

d14

+

d15

+

d16

+

d17

+

d18

+

d19

+

d20

+

d21

+

d22

+

d23

+

d24

+

d25

+

d26

+

d27

+

d28

+

d29

+

d30

+

d31

+

2

+

1

+

0:1

+

2:1

+

0

+

1

+

2

+ `, + + test({ assert, component, target }) { + component.reads = {}; + + component._0 = 'a'; + component._1 = 'b'; + component._2 = 'c'; + + assert.htmlEqual(target.innerHTML, ` +

d1

+

d2

+

d3

+

d4

+

d5

+

d6

+

d7

+

d8

+

d9

+

d10

+

d11

+

d12

+

d13

+

d14

+

d15

+

d16

+

d17

+

d18

+

d19

+

d20

+

d21

+

d22

+

d23

+

d24

+

d25

+

d26

+

d27

+

d28

+

d29

+

d30

+

d31

+

c

+

b

+

a:b

+

c:b

+

a

+

b

+

c

+ `); + + assert.deepEqual(component.reads, { + _0: 2, + _1: 2, + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot-2/main.svelte b/test/runtime/samples/bitmask-overflow-slot-2/main.svelte new file mode 100644 index 0000000000..c15b4d5322 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot-2/main.svelte @@ -0,0 +1,30 @@ + + + +

{bar}

+

{dummy}

+

{_0}

+

{_1}

+

{_2}

+
\ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot/Echo.svelte b/test/runtime/samples/bitmask-overflow-slot/Echo.svelte new file mode 100644 index 0000000000..28eaa54060 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/Echo.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot/_config.js b/test/runtime/samples/bitmask-overflow-slot/_config.js new file mode 100644 index 0000000000..9b24d5541f --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/_config.js @@ -0,0 +1,124 @@ +export default { + html: ` +

0

+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

5:36

+

6:37

+

38

+

0

+ `, + + test({ assert, component, target }) { + component.reads = {}; + + component._0 = 'a'; + component._30 = 'b'; + component._31 = 'c'; + component._32 = 'd'; + component._40 = 'e'; + + component._5 = 'f'; + component._6 = 'g'; + component._36 = 'h'; + component._37 = 'i'; + + assert.htmlEqual(target.innerHTML, ` +

a

+

1

+

2

+

3

+

4

+

f

+

g

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

b

+

c

+

d

+

33

+

34

+

35

+

h

+

i

+

38

+

39

+

e

+

f:h

+

g:i

+

38

+

a

+ `); + + assert.deepEqual(component.reads, { + _0: 1, + _5: 3, + _6: 3, + _30: 1, + _31: 1, + _32: 1, + _36: 3, + _37: 3, + _40: 1 + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/bitmask-overflow-slot/main.svelte b/test/runtime/samples/bitmask-overflow-slot/main.svelte new file mode 100644 index 0000000000..89e60ce4b9 --- /dev/null +++ b/test/runtime/samples/bitmask-overflow-slot/main.svelte @@ -0,0 +1,107 @@ + + + +

{read(_0, '_0')}

+

{read(_1, '_1')}

+

{read(_2, '_2')}

+

{read(_3, '_3')}

+

{read(_4, '_4')}

+

{read(_5, '_5')}

+

{read(_6, '_6')}

+

{read(_7, '_7')}

+

{read(_8, '_8')}

+

{read(_9, '_9')}

+

{read(_10, '_10')}

+

{read(_11, '_11')}

+

{read(_12, '_12')}

+

{read(_13, '_13')}

+

{read(_14, '_14')}

+

{read(_15, '_15')}

+

{read(_16, '_16')}

+

{read(_17, '_17')}

+

{read(_18, '_18')}

+

{read(_19, '_19')}

+

{read(_20, '_20')}

+

{read(_21, '_21')}

+

{read(_22, '_22')}

+

{read(_23, '_23')}

+

{read(_24, '_24')}

+

{read(_25, '_25')}

+

{read(_26, '_26')}

+

{read(_27, '_27')}

+

{read(_28, '_28')}

+

{read(_29, '_29')}

+

{read(_30, '_30')}

+

{read(_31, '_31')}

+

{read(_32, '_32')}

+

{read(_33, '_33')}

+

{read(_34, '_34')}

+

{read(_35, '_35')}

+

{read(_36, '_36')}

+

{read(_37, '_37')}

+

{read(_38, '_38')}

+

{read(_39, '_39')}

+

{read(_40, '_40')}

+ +

{read(_5, '_5') + ':' + read(_36, '_36')}

+

{foo}

+

{bar}

+ +

{dummy}

+
\ No newline at end of file diff --git a/test/runtime/samples/component-slot-let-in-slot/Inner.svelte b/test/runtime/samples/component-slot-let-in-slot/Inner.svelte new file mode 100644 index 0000000000..d0ea817d54 --- /dev/null +++ b/test/runtime/samples/component-slot-let-in-slot/Inner.svelte @@ -0,0 +1 @@ + diff --git a/test/runtime/samples/component-slot-let-in-slot/Outer.svelte b/test/runtime/samples/component-slot-let-in-slot/Outer.svelte new file mode 100644 index 0000000000..590a70564a --- /dev/null +++ b/test/runtime/samples/component-slot-let-in-slot/Outer.svelte @@ -0,0 +1,5 @@ + + + diff --git a/test/runtime/samples/component-slot-let-in-slot/_config.js b/test/runtime/samples/component-slot-let-in-slot/_config.js new file mode 100644 index 0000000000..a86d869581 --- /dev/null +++ b/test/runtime/samples/component-slot-let-in-slot/_config.js @@ -0,0 +1,12 @@ +export default { + props: { + prop: 'a', + }, + + html: 'a', + + test({ assert, component, target }) { + component.prop = 'b'; + assert.htmlEqual( target.innerHTML, 'b' ); + } +}; diff --git a/test/runtime/samples/component-slot-let-in-slot/main.svelte b/test/runtime/samples/component-slot-let-in-slot/main.svelte new file mode 100644 index 0000000000..7ebb4ebc21 --- /dev/null +++ b/test/runtime/samples/component-slot-let-in-slot/main.svelte @@ -0,0 +1,10 @@ + + + + {value} + diff --git a/test/runtime/samples/each-block-recursive-with-function-condition/_config.js b/test/runtime/samples/each-block-recursive-with-function-condition/_config.js new file mode 100644 index 0000000000..0795576bae --- /dev/null +++ b/test/runtime/samples/each-block-recursive-with-function-condition/_config.js @@ -0,0 +1,8 @@ +export default { + html: ` +

OK

+

OK

+
one
+
two
+ ` +}; diff --git a/test/runtime/samples/each-block-recursive-with-function-condition/main.svelte b/test/runtime/samples/each-block-recursive-with-function-condition/main.svelte new file mode 100644 index 0000000000..9b62c03cf6 --- /dev/null +++ b/test/runtime/samples/each-block-recursive-with-function-condition/main.svelte @@ -0,0 +1,13 @@ + + +{#each data as datum} + {#if datum.foo && a()} +

OK

+ + {:else} +
{datum.bar}
+ {/if} +{/each} diff --git a/test/runtime/samples/if-block-component-store-function-conditionals/Widget.svelte b/test/runtime/samples/if-block-component-store-function-conditionals/Widget.svelte new file mode 100644 index 0000000000..a74ce95de5 --- /dev/null +++ b/test/runtime/samples/if-block-component-store-function-conditionals/Widget.svelte @@ -0,0 +1 @@ +

OK

\ No newline at end of file diff --git a/test/runtime/samples/if-block-component-store-function-conditionals/_config.js b/test/runtime/samples/if-block-component-store-function-conditionals/_config.js new file mode 100644 index 0000000000..db171f2fd1 --- /dev/null +++ b/test/runtime/samples/if-block-component-store-function-conditionals/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

OK

', +}; diff --git a/test/runtime/samples/if-block-component-store-function-conditionals/main.svelte b/test/runtime/samples/if-block-component-store-function-conditionals/main.svelte new file mode 100644 index 0000000000..765654f0d2 --- /dev/null +++ b/test/runtime/samples/if-block-component-store-function-conditionals/main.svelte @@ -0,0 +1,12 @@ + + +{#if $a || b() } + +{:else} +
fail
+{/if} \ No newline at end of file diff --git a/test/runtime/samples/store-auto-subscribe-event-callback/_config.js b/test/runtime/samples/store-auto-subscribe-event-callback/_config.js new file mode 100644 index 0000000000..747ed3e4ad --- /dev/null +++ b/test/runtime/samples/store-auto-subscribe-event-callback/_config.js @@ -0,0 +1,22 @@ +export default { + html: ` + + Dirty: false + Valid: false + `, + + async test({ assert, target, window }) { + const input = target.querySelector('input'); + + input.value = 'foo'; + const inputEvent = new window.InputEvent('input'); + + await input.dispatchEvent(inputEvent); + + assert.htmlEqual(target.innerHTML, ` + + Dirty: true + Valid: true + `); + }, +}; diff --git a/test/runtime/samples/store-auto-subscribe-event-callback/main.svelte b/test/runtime/samples/store-auto-subscribe-event-callback/main.svelte new file mode 100644 index 0000000000..eca7c202cd --- /dev/null +++ b/test/runtime/samples/store-auto-subscribe-event-callback/main.svelte @@ -0,0 +1,29 @@ + + + + +Dirty: {$validity.dirty} +Valid: {$validity.valid} diff --git a/test/runtime/samples/store-invalidation-while-update-1/_config.js b/test/runtime/samples/store-invalidation-while-update-1/_config.js new file mode 100644 index 0000000000..d0361d2d23 --- /dev/null +++ b/test/runtime/samples/store-invalidation-while-update-1/_config.js @@ -0,0 +1,44 @@ +export default { + html: ` + +
+
simple
+ + `, + + async test({ assert, component, target, window }) { + const input = target.querySelector('input'); + const button = target.querySelector('button'); + + const inputEvent = new window.InputEvent('input'); + const clickEvent = new window.MouseEvent('click'); + + input.value = 'foo'; + await input.dispatchEvent(inputEvent); + + assert.htmlEqual(target.innerHTML, ` + +
foo
+
foo
+ + `); + + await button.dispatchEvent(clickEvent); + assert.htmlEqual(target.innerHTML, ` + +
foo
+
clicked
+ + `); + + input.value = 'bar'; + await input.dispatchEvent(inputEvent); + + assert.htmlEqual(target.innerHTML, ` + +
bar
+
bar
+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-invalidation-while-update-1/main.svelte b/test/runtime/samples/store-invalidation-while-update-1/main.svelte new file mode 100644 index 0000000000..f74808ee2e --- /dev/null +++ b/test/runtime/samples/store-invalidation-while-update-1/main.svelte @@ -0,0 +1,20 @@ + + + +
{v}
+
{$s}
+ diff --git a/test/runtime/samples/store-invalidation-while-update-2/_config.js b/test/runtime/samples/store-invalidation-while-update-2/_config.js new file mode 100644 index 0000000000..422034bc11 --- /dev/null +++ b/test/runtime/samples/store-invalidation-while-update-2/_config.js @@ -0,0 +1,44 @@ +export default { + html: ` +
+
simple
+ + + `, + + async test({ assert, component, target, window }) { + const input = target.querySelector('input'); + const button = target.querySelector('button'); + + const inputEvent = new window.InputEvent('input'); + const clickEvent = new window.MouseEvent('click'); + + input.value = 'foo'; + await input.dispatchEvent(inputEvent); + + assert.htmlEqual(target.innerHTML, ` +
foo
+
foo
+ + + `); + + await button.dispatchEvent(clickEvent); + assert.htmlEqual(target.innerHTML, ` +
foo
+
clicked
+ + + `); + + input.value = 'bar'; + await input.dispatchEvent(inputEvent); + + assert.htmlEqual(target.innerHTML, ` +
bar
+
bar
+ + + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-invalidation-while-update-2/main.svelte b/test/runtime/samples/store-invalidation-while-update-2/main.svelte new file mode 100644 index 0000000000..83e13742ce --- /dev/null +++ b/test/runtime/samples/store-invalidation-while-update-2/main.svelte @@ -0,0 +1,20 @@ + + +
{v}
+
{$s}
+ + diff --git a/test/vars/samples/store-referenced/_config.js b/test/vars/samples/store-referenced/_config.js index cb92b67ea4..bac35d1dba 100644 --- a/test/vars/samples/store-referenced/_config.js +++ b/test/vars/samples/store-referenced/_config.js @@ -8,7 +8,7 @@ export default { module: false, mutated: false, reassigned: false, - referenced: false, + referenced: true, referenced_from_script: false, writable: true },