diff --git a/CHANGELOG.md b/CHANGELOG.md index 634f85fb3e..c286390f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Svelte changelog +## Unreleased + +* 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 `{: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)) + ## 3.16.0 * Use bitmasks to track changes ([#3945](https://github.com/sveltejs/svelte/pull/3945)) diff --git a/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md b/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md index 8799fdacce..cca200e966 100644 --- a/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md +++ b/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md @@ -53,7 +53,7 @@ What happens if we use the new model as a starting point? The same 'hello world' app that took 204kb with React and Next weighs just 7kb with Sapper. That number is likely to fall further in the future as we explore the space of optimisation possibilities, such as not shipping any JavaScript *at all* for pages that aren't interactive, beyond the tiny Sapper runtime that handles client-side routing. -What about a more 'real world' example? Conveniently, the [RealWorld](https://github.com/gothinkster/realworld) project, which challenges frameworks to develop an implementation of a Medium clone, gives us a way to find out. The [Sapper implementation](http://svelte-realworld.now.sh/) takes 39.6kb (11.8kb zipped) to render an interactive homepage. +What about a more 'real world' example? Conveniently, the [RealWorld](https://github.com/gothinkster/realworld) project, which challenges frameworks to develop an implementation of a Medium clone, gives us a way to find out. The [Sapper implementation](https://github.com/sveltejs/realworld) takes 39.6kb (11.8kb zipped) to render an interactive homepage. diff --git a/site/src/routes/_components/WhosUsingSvelte.svelte b/site/src/routes/_components/WhosUsingSvelte.svelte index c1b80c0fe1..f27f1684e6 100644 --- a/site/src/routes/_components/WhosUsingSvelte.svelte +++ b/site/src/routes/_components/WhosUsingSvelte.svelte @@ -45,9 +45,11 @@
+ Absolute Web logo Bekchy logo Beyonk logo buy.* logo + Cashfree logo Chess.com logo Comigo logo Datawrapper logo diff --git a/site/src/routes/repl/[id]/index.svelte b/site/src/routes/repl/[id]/index.svelte index 4d05ac28ea..59b8e6359a 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/site/static/organisations/absoluteweb.svg b/site/static/organisations/absoluteweb.svg new file mode 100644 index 0000000000..e2aeb9421e --- /dev/null +++ b/site/static/organisations/absoluteweb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/static/organisations/cashfree.svg b/site/static/organisations/cashfree.svg new file mode 100644 index 0000000000..d0952dd71c --- /dev/null +++ b/site/static/organisations/cashfree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index cd97c8dd8e..4402a4a947 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -344,7 +344,7 @@ export default class Component { }; } - get_unique_name(name: string): Identifier { + get_unique_name(name: string, scope?: Scope): Identifier { if (test) name = `${name}$`; let alias = name; for ( @@ -352,7 +352,8 @@ export default class Component { reserved.has(alias) || this.var_lookup.has(alias) || this.used_names.has(alias) || - this.globally_used_names.has(alias); + this.globally_used_names.has(alias) || + (scope && scope.has(alias)); alias = `${name}_${i++}` ); this.used_names.add(alias); @@ -465,7 +466,7 @@ export default class Component { extract_names(declarator.id).forEach(name => { const variable = this.var_lookup.get(name); variable.export_name = name; - if (variable.writable && !(variable.referenced || variable.referenced_from_script)) { + if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { this.warn(declarator, { code: `unused-export-let`, message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const '${name}'\`` @@ -488,7 +489,7 @@ export default class Component { if (variable) { variable.export_name = specifier.exported.name; - if (variable.writable && !(variable.referenced || variable.referenced_from_script)) { + if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { this.warn(specifier, { code: `unused-export-let`, message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const '${specifier.exported.name}'\`` @@ -707,8 +708,7 @@ export default class Component { const remove = (parent, prop, index) => { to_remove.unshift([parent, prop, index]); }; - - const to_insert = new Map(); + let scope_updated = false; walk(content, { enter(node, parent, prop, index) { @@ -735,37 +735,21 @@ export default class Component { } component.warn_on_undefined_store_value_references(node, parent, scope); + }, + leave(node) { + // do it on leave, to prevent infinite loop if (component.compile_options.dev && component.compile_options.loopGuardTimeout > 0) { - const to_insert_for_loop_protect = component.loop_protect(node, prop, index, component.compile_options.loopGuardTimeout); - if (to_insert_for_loop_protect) { - if (!Array.isArray(parent[prop])) { - parent[prop] = { - type: 'BlockStatement', - body: [to_insert_for_loop_protect.node, node], - }; - } else { - // can't insert directly, will screw up the index in the for-loop of estree-walker - if (!to_insert.has(parent)) { - to_insert.set(parent, []); - } - to_insert.get(parent).push(to_insert_for_loop_protect); - } + const to_replace_for_loop_protect = component.loop_protect(node, scope, component.compile_options.loopGuardTimeout); + if (to_replace_for_loop_protect) { + this.replace(to_replace_for_loop_protect); + scope_updated = true; } } - }, - leave(node) { if (map.has(node)) { scope = scope.parent; - } - if (to_insert.has(node)) { - const nodes_to_insert = to_insert.get(node); - for (const { index, prop, node: node_to_insert } of nodes_to_insert.reverse()) { - node[prop].splice(index, 0, node_to_insert); - } - to_insert.delete(node); - } + } }, }); @@ -778,6 +762,12 @@ export default class Component { } } } + + if (scope_updated) { + const { scope, map } = create_scopes(script.content); + this.instance_scope = scope; + this.instance_scope_map = map; + } } track_references_and_mutations() { @@ -849,15 +839,12 @@ export default class Component { } } - loop_protect(node, prop, index, timeout) { + loop_protect(node, scope: Scope, timeout: number): Node | null { if (node.type === 'WhileStatement' || node.type === 'ForStatement' || node.type === 'DoWhileStatement') { - const guard = this.get_unique_name('guard'); - this.add_var({ - name: guard.name, - internal: true, - }); + const guard = this.get_unique_name('guard', scope); + this.used_names.add(guard.name); const before = b`const ${guard} = @loop_guard(${timeout})`; const inside = b`${guard}();`; @@ -870,7 +857,14 @@ export default class Component { }; } node.body.body.push(inside[0]); - return { index, prop, node: before[0] }; + + return { + type: 'BlockStatement', + body: [ + before[0], + node, + ], + }; } return null; } @@ -1289,7 +1283,7 @@ export default class Component { if (this.var_lookup.has(name) && !this.var_lookup.get(name).global) return; if (template_scope && template_scope.names.has(name)) return; - if (globals.has(name)) return; + if (globals.has(name) && node.type !== 'InlineComponent') return; let message = `'${name}' is not defined`; if (!this.ast.instance) diff --git a/src/compiler/compile/create_module.ts b/src/compiler/compile/create_module.ts index 632206652a..80e6308263 100644 --- a/src/compiler/compile/create_module.ts +++ b/src/compiler/compile/create_module.ts @@ -152,28 +152,34 @@ function cjs( const internal_globals = get_internal_globals(globals, helpers); - const user_requires = imports.map(node => ({ - type: 'VariableDeclaration', - kind: 'const', - declarations: [{ - type: 'VariableDeclarator', - id: node.specifiers[0].type === 'ImportNamespaceSpecifier' - ? { type: 'Identifier', name: node.specifiers[0].local.name } - : { - type: 'ObjectPattern', - properties: node.specifiers.map(s => ({ - type: 'Property', - method: false, - shorthand: false, - computed: false, - key: s.type === 'ImportSpecifier' ? s.imported : { type: 'Identifier', name: 'default' }, - value: s.local, - kind: 'init' - })) - }, - init: x`require("${edit_source(node.source.value, sveltePath)}")` - }] - })); + const user_requires = imports.map(node => { + const init = x`require("${edit_source(node.source.value, sveltePath)}")`; + if (node.specifiers.length === 0) { + return b`${init};`; + } + return { + type: 'VariableDeclaration', + kind: 'const', + declarations: [{ + type: 'VariableDeclarator', + id: node.specifiers[0].type === 'ImportNamespaceSpecifier' + ? { type: 'Identifier', name: node.specifiers[0].local.name } + : { + type: 'ObjectPattern', + properties: node.specifiers.map(s => ({ + type: 'Property', + method: false, + shorthand: false, + computed: false, + key: s.type === 'ImportSpecifier' ? s.imported : { type: 'Identifier', name: 'default' }, + value: s.local, + kind: 'init' + })) + }, + init + }] + }; + }); const exports = module_exports.map(x => b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`); diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 8ce7495fd7..25c5312e65 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -71,6 +71,8 @@ export default class AwaitBlockWrapper extends Wrapper { this.not_static_content(); block.add_dependencies(this.node.expression.dependencies); + if (this.node.value) block.renderer.add_to_context(this.node.value, true); + if (this.node.error) block.renderer.add_to_context(this.node.error, true); let is_dynamic = false; let has_intros = false; @@ -118,9 +120,6 @@ export default class AwaitBlockWrapper extends Wrapper { if (has_outros) { block.add_outro(); } - - if (this.node.value) block.renderer.add_to_context(this.node.value, true); - if (this.node.error) block.renderer.add_to_context(this.node.error, true); } render( @@ -206,7 +205,7 @@ export default class AwaitBlockWrapper extends Wrapper { } else { const #child_ctx = #ctx.slice(); - #child_ctx[${value_index}] = ${info}.resolved; + ${this.node.value && x`#child_ctx[${value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); @@ -220,7 +219,7 @@ export default class AwaitBlockWrapper extends Wrapper { block.chunks.update.push(b` { const #child_ctx = #ctx.slice(); - #child_ctx[${value_index}] = ${info}.resolved; + ${this.node.value && x`#child_ctx[${value_index}] = ${info}.resolved;`} ${info}.block.p(#child_ctx, #dirty); } `); diff --git a/test/js/samples/debug-no-dependencies/expected.js b/test/js/samples/debug-no-dependencies/expected.js index 76e17a2e81..2238e169aa 100644 --- a/test/js/samples/debug-no-dependencies/expected.js +++ b/test/js/samples/debug-no-dependencies/expected.js @@ -151,4 +151,4 @@ class Component extends SvelteComponentDev { } } -export default Component; +export default Component; \ No newline at end of file diff --git a/test/js/samples/loop-protect/expected.js b/test/js/samples/loop-protect/expected.js index c2a99098fb..127addf1d1 100644 --- a/test/js/samples/loop-protect/expected.js +++ b/test/js/samples/loop-protect/expected.js @@ -1,8 +1,13 @@ /* generated by Svelte vX.Y.Z */ import { SvelteComponentDev, + add_location, + binding_callbacks, + detach_dev, dispatch_dev, + element, init, + insert_dev, loop_guard, noop, safe_not_equal @@ -11,16 +16,27 @@ import { const file = undefined; function create_fragment(ctx) { + let div; + const block = { - c: noop, + c: function create() { + div = element("div"); + add_location(div, file, 22, 0, 288); + }, l: function claim(nodes) { throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option"); }, - m: noop, + m: function mount(target, anchor) { + insert_dev(target, div, anchor); + /*div_binding*/ ctx[1](div); + }, p: noop, i: noop, o: noop, - d: noop + d: function destroy(detaching) { + if (detaching) detach_dev(div); + /*div_binding*/ ctx[1](null); + } }; dispatch_dev("SvelteRegisterBlock", { @@ -34,57 +50,93 @@ function create_fragment(ctx) { return block; } +function foo() { + const guard = "foo"; + + { + const guard_1 = loop_guard(100); + + while (true) { + console.log(guard); + guard_1(); + } + } +} + function instance($$self, $$props, $$invalidate) { - const guard = loop_guard(100); + let node; + + { + const guard = loop_guard(100); - while (true) { - foo(); - guard(); + while (true) { + foo(); + guard(); + } } - const guard_1 = loop_guard(100); + { + const guard_2 = loop_guard(100); - for (; ; ) { - foo(); - guard_1(); + for (; ; ) { + foo(); + guard_2(); + } } - const guard_2 = loop_guard(100); + { + const guard_3 = loop_guard(100); - while (true) { - foo(); - guard_2(); + while (true) { + foo(); + guard_3(); + } } - const guard_4 = loop_guard(100); + { + const guard_5 = loop_guard(100); - do { - foo(); - guard_4(); - } while (true); + do { + foo(); + guard_5(); + } while (true); + } - $$self.$capture_state = () => ({ foo }); - $$self.$inject_state = noop; + function div_binding($$value) { + binding_callbacks[$$value ? "unshift" : "push"](() => { + $$invalidate(0, node = $$value); + }); + } + + $$self.$capture_state = () => ({ node, foo, console }); + + $$self.$inject_state = $$props => { + if ("node" in $$props) $$invalidate(0, node = $$props.node); + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } $: { - const guard_3 = loop_guard(100); + const guard_4 = loop_guard(100); while (true) { foo(); - guard_3(); + guard_4(); } } $: { - const guard_5 = loop_guard(100); + const guard_6 = loop_guard(100); do { foo(); - guard_5(); + guard_6(); } while (true); } - return []; + return [node, div_binding]; } class Component extends SvelteComponentDev { diff --git a/test/js/samples/loop-protect/input.svelte b/test/js/samples/loop-protect/input.svelte index c39ea75f72..daac6ab1c4 100644 --- a/test/js/samples/loop-protect/input.svelte +++ b/test/js/samples/loop-protect/input.svelte @@ -1,4 +1,13 @@ \ No newline at end of file + + +
\ No newline at end of file diff --git a/test/runtime/samples/await-then-if/_config.js b/test/runtime/samples/await-then-if/_config.js new file mode 100644 index 0000000000..b26688d9f3 --- /dev/null +++ b/test/runtime/samples/await-then-if/_config.js @@ -0,0 +1,25 @@ +let fulfil; + +const thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + props: { + thePromise + }, + + html: ` + loading... + `, + + async test({ assert, component, target }) { + fulfil([]); + + await thePromise; + + assert.htmlEqual(target.innerHTML, ` +

promise array is empty

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-if/main.svelte b/test/runtime/samples/await-then-if/main.svelte new file mode 100644 index 0000000000..d292067b30 --- /dev/null +++ b/test/runtime/samples/await-then-if/main.svelte @@ -0,0 +1,13 @@ + + +{#await thePromise} + loading... +{:then r} + {#if r.length < 1} +

promise array is empty

+ {:else} +

promise array is not empty

+ {/if} +{/await} \ No newline at end of file diff --git a/test/runtime/samples/await-then-no-context/main.svelte b/test/runtime/samples/await-then-no-context/main.svelte new file mode 100644 index 0000000000..c729512149 --- /dev/null +++ b/test/runtime/samples/await-then-no-context/main.svelte @@ -0,0 +1,12 @@ + + +{#await promise} +
waiting
+{:then} + {#each test as t} +
t
+ {/each} +{/await} diff --git a/test/runtime/samples/loop-protect-inner-function/_config.js b/test/runtime/samples/loop-protect-inner-function/_config.js new file mode 100644 index 0000000000..862d4f4c0f --- /dev/null +++ b/test/runtime/samples/loop-protect-inner-function/_config.js @@ -0,0 +1,7 @@ +export default { + html: '
', + compileOptions: { + dev: true, + loopGuardTimeout: 100, + } +}; diff --git a/test/runtime/samples/loop-protect-inner-function/main.svelte b/test/runtime/samples/loop-protect-inner-function/main.svelte new file mode 100644 index 0000000000..cf5350efde --- /dev/null +++ b/test/runtime/samples/loop-protect-inner-function/main.svelte @@ -0,0 +1,7 @@ + +
\ No newline at end of file diff --git a/test/validator/samples/missing-component-global/input.svelte b/test/validator/samples/missing-component-global/input.svelte new file mode 100644 index 0000000000..5d17448bd4 --- /dev/null +++ b/test/validator/samples/missing-component-global/input.svelte @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/test/validator/samples/missing-component-global/warnings.json b/test/validator/samples/missing-component-global/warnings.json new file mode 100644 index 0000000000..c9de56619a --- /dev/null +++ b/test/validator/samples/missing-component-global/warnings.json @@ -0,0 +1,15 @@ +[{ + "code": "missing-declaration", + "message": "'String' is not defined. Consider adding a +{$q}