Reproduction: https://github.com/igneosaur/sveltejs-template/tree/terser-compress-await-bug
@igneosaur opened https://github.com/sveltejs/template/issues/59 thinking it was terser.
If you clone and change sveltejs from 3.9.1 to 3.9.0 it works fine.
Well it's presumably _something_ to do with minification, because I can't reproduce the exception with npm run dev, only with npm run build/npm run start.
It looks to me like a terser bug involving invalid inlining of a function call.
let current_component;
// ...
function get_current_component() {
if (!current_component)
throw new Error(`Function called outside component initialization`);
return current_component;
}
is turning into
const n = function () {
if (!n) throw new Error('Function called outside component initialization');
return n
}();
I haven't tracked down what about the output changed between Svelte 3.9.0 and 3.9.1 to cause this to happen, but it does appear to be a bug in Terser.
Opened here https://github.com/terser/terser/issues/443, @Conduitry.
I spent a while last night trying to whittle this down to a reasonably sized repro on https://try.terser.org but was unable to.
var app = (function () {
'use strict';
function noop() { }
function assign(tar, src) {
// @ts-ignore
for (const k in src)
tar[k] = src[k];
return tar;
}
function is_promise(value) {
return value && typeof value === 'object' && typeof value.then === 'function';
}
function run(fn) {
return fn();
}
function blank_object() {
return Object.create(null);
}
function run_all(fns) {
fns.forEach(run);
}
function is_function(thing) {
return typeof thing === 'function';
}
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function append(target, node) {
target.appendChild(node);
}
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function element(name) {
return document.createElement(name);
}
function text(data) {
return document.createTextNode(data);
}
function empty() {
return text('');
}
function children(element) {
return Array.from(element.childNodes);
}
function set_data(text, data) {
data = '' + data;
if (text.data !== data)
text.data = data;
}
let current_component;
function set_current_component(component) {
current_component = component;
}
function get_current_component() {
if (!current_component)
throw new Error(`Function called outside component initialization`);
return current_component;
}
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
function flush() {
const seen_callbacks = new Set();
do {
// first, call beforeUpdate functions
// and update components
while (dirty_components.length) {
const component = dirty_components.shift();
set_current_component(component);
update(component.$$);
}
while (binding_callbacks.length)
binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
callback();
// ...so guard against infinite loops
seen_callbacks.add(callback);
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
}
function update($$) {
if ($$.fragment) {
$$.update($$.dirty);
run_all($$.before_update);
$$.fragment.p($$.dirty, $$.ctx);
$$.dirty = null;
$$.after_update.forEach(add_render_callback);
}
}
const outroing = new Set();
let outros;
function group_outros() {
outros = {
r: 0,
c: [],
p: outros // parent group
};
}
function check_outros() {
if (!outros.r) {
run_all(outros.c);
}
outros = outros.p;
}
function transition_in(block, local) {
if (block && block.i) {
outroing.delete(block);
block.i(local);
}
}
function transition_out(block, local, detach, callback) {
if (block && block.o) {
if (outroing.has(block))
return;
outroing.add(block);
outros.c.push(() => {
outroing.delete(block);
if (callback) {
if (detach)
block.d(1);
callback();
}
});
block.o(local);
}
}
function handle_promise(promise, info) {
const token = info.token = {};
function update(type, index, key, value) {
if (info.token !== token)
return;
info.resolved = key && { [key]: value };
const child_ctx = assign(assign({}, info.ctx), info.resolved);
const block = type && (info.current = type)(child_ctx);
if (info.block) {
if (info.blocks) {
info.blocks.forEach((block, i) => {
if (i !== index && block) {
group_outros();
transition_out(block, 1, 1, () => {
info.blocks[i] = null;
});
check_outros();
}
});
}
else {
info.block.d(1);
}
block.c();
transition_in(block, 1);
block.m(info.mount(), info.anchor);
flush();
}
info.block = block;
if (info.blocks)
info.blocks[index] = block;
}
if (is_promise(promise)) {
const current_component = get_current_component();
promise.then(value => {
set_current_component(current_component);
update(info.then, 1, info.value, value);
set_current_component(null);
}, error => {
set_current_component(current_component);
update(info.catch, 2, info.error, error);
set_current_component(null);
});
// if we previously had a then/catch block, destroy it
if (info.current !== info.pending) {
update(info.pending, 0);
return true;
}
}
else {
if (info.current !== info.then) {
update(info.then, 1, info.value, promise);
return true;
}
info.resolved = { [info.value]: promise };
}
}
function mount_component(component, target, anchor) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment.m(target, anchor);
// onMount happens before the initial afterUpdate
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
}
else {
// Edge case - component was destroyed immediately,
// most likely as a result of a binding initialising
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
after_update.forEach(add_render_callback);
}
function destroy_component(component, detaching) {
if (component.$$.fragment) {
run_all(component.$$.on_destroy);
component.$$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
component.$$.on_destroy = component.$$.fragment = null;
component.$$.ctx = {};
}
}
function make_dirty(component, key) {
if (!component.$$.dirty) {
dirty_components.push(component);
schedule_update();
component.$$.dirty = blank_object();
}
component.$$.dirty[key] = true;
}
function init(component, options, instance, create_fragment, not_equal, prop_names) {
const parent_component = current_component;
set_current_component(component);
const props = options.props || {};
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props: prop_names,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty: null
};
let ready = false;
$$.ctx = instance
? instance(component, props, (key, value) => {
if ($$.ctx && not_equal($$.ctx[key], $$.ctx[key] = value)) {
if ($$.bound[key])
$$.bound[key](value);
if (ready)
make_dirty(component, key);
}
})
: props;
$$.update();
ready = true;
run_all($$.before_update);
$$.fragment = create_fragment($$.ctx);
if (options.target) {
if (options.hydrate) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment.l(children(options.target));
}
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment.c();
}
if (options.intro)
transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor);
flush();
}
set_current_component(parent_component);
}
class SvelteComponent {
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1)
callbacks.splice(index, 1);
};
}
$set() {
// overridden by instance, if it has props
}
}
/* src/Await.svelte generated by Svelte v3.9.1 */
// (15:0) {:catch error}
function create_catch_block(ctx) {
var p;
return {
c() {
p = element("p");
p.textContent = "Error";
},
m(target, anchor) {
insert(target, p, anchor);
},
p: noop,
d(detaching) {
if (detaching) {
detach(p);
}
}
};
}
// (13:0) {:then data}
function create_then_block(ctx) {
var h1, t0, t1, t2;
return {
c() {
h1 = element("h1");
t0 = text("Hello ");
t1 = text(ctx.name);
t2 = text("!");
},
m(target, anchor) {
insert(target, h1, anchor);
append(h1, t0);
append(h1, t1);
append(h1, t2);
},
p(changed, ctx) {
if (changed.name) {
set_data(t1, ctx.name);
}
},
d(detaching) {
if (detaching) {
detach(h1);
}
}
};
}
// (11:13) <p>Awaiting...</p> {:then data}
function create_pending_block(ctx) {
var p;
return {
c() {
p = element("p");
p.textContent = "Awaiting...";
},
m(target, anchor) {
insert(target, p, anchor);
},
p: noop,
d(detaching) {
if (detaching) {
detach(p);
}
}
};
}
function create_fragment(ctx) {
var await_block_anchor, promise;
let info = {
ctx,
current: null,
token: null,
pending: create_pending_block,
then: create_then_block,
catch: create_catch_block,
value: 'data',
error: 'error'
};
handle_promise(promise = ctx.data, info);
return {
c() {
await_block_anchor = empty();
info.block.c();
},
m(target, anchor) {
insert(target, await_block_anchor, anchor);
info.block.m(target, info.anchor = anchor);
info.mount = () => await_block_anchor.parentNode;
info.anchor = await_block_anchor;
},
p(changed, new_ctx) {
ctx = new_ctx;
info.block.p(changed, assign(assign({}, ctx), info.resolved));
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(await_block_anchor);
}
info.block.d(detaching);
info.token = null;
info = null;
}
};
}
function instance($$self, $$props, $$invalidate) {
let { name } = $$props;
const waitOneSec = () =>
new Promise(resolve => setTimeout(() => resolve(true), 1000));
const asyncCall = async () => await waitOneSec();
const data = asyncCall();
$$self.$set = $$props => {
if ('name' in $$props) $$invalidate('name', name = $$props.name);
};
return { name, data };
}
class Await extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
}
}
const app = new Await({
target: document.body,
props: {
name: 'world'
}
});
return app;
}());
Something about this is making Terser think it's safe to inline the get_current_component() call in handle_promise() when it is in fact not safe to do so.
This happened to me with rollup 1.16.4; It does not with rollup 1.21.2
I think it's safe to close this now that the above repro has been pared down over in the Terser issue.
The Terser bug was fixed in 4.3.2.
Most helpful comment
It looks to me like a terser bug involving invalid inlining of a function call.
is turning into
I haven't tracked down what about the output changed between Svelte 3.9.0 and 3.9.1 to cause this to happen, but it does appear to be a bug in Terser.