sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.71k stars 4.23k forks source link

Application won't compile - "cannot read property 'n' of undefined" since 3.16.1 #4077

Closed antony closed 4 years ago

antony commented 4 years ago

Describe the bug

Upgrading an existing app to Svelte 3.16.1 causes client compilation to fail with the error Cannot read property 'n' of undefined.

I'm the second person to see this in the discord, it seems. I don't know where the error comes from or what the causes is (but I do see that the variable n is used a lot in recent Svelte commits - https://github.com/sveltejs/svelte/commit/bb5cf9ada7706fed9bb86467d2ae78c76f88b9d0).

Logs

ant@xeno  ~/Projects/beyonk-dashboard   master ●  npm run dev

beyonk-dashboard@0.3.0 dev /home/ant/Projects/beyonk-dashboard PORT=1233 NODE_CONFIG_ENV=${NODE_ENV} sapper dev

✗ client Cannot read property 'n' of undefined

To Reproduce I'm afraid I simply don't know, at present.

Expected behavior The client should compile as normal.

Information about your Svelte project:

Severity Blocker. It's broken, and my app won't start.

Additional context The app was previously using 3.15, and trying 3.16.0 wouldn't start either, due to an issue with reduce with no initial value.

Conduitry commented 4 years ago

Presumably #4063 was an incomplete fix. Please do update with a repro if you find one.

ghost commented 4 years ago

This script should try to compile all components in the project and allow the full stack trace and breakpoints using the built-in Node debugger.

Not sure if it needs tweaking for sapper though.

const svelte = require('svelte/compiler');
const fs     = require('fs');
const glob   = require('glob');

function do_compile(err,data) {
    if (err) {
        console.log("Couldn't read file:", err);
        process.exit(-1);
    }

    let result = svelte.compile(data);
}

function call_svelte(err, files) {
    if (err) {
        console.log("Glob error:", err);
        process.exit(-1);
    }

    files.forEach(f => {
        fs.readFile(f, {encoding:"utf8"}, do_compile);
    });
}

glob('src/*.svelte', call_svelte);
glob('src/**/*.svelte', call_svelte);
mrkishi commented 4 years ago

I believe this line is the issue:

https://github.com/sveltejs/svelte/blob/8a6abc9215b10e93db90c0d33c2b2f99d098641e/src/compiler/compile/render_dom/Renderer.ts#L256

We could temporarily fix it by checking for bitmask[0], but I don't know what causes this to happen in the first place... I believe the issue is that context_overflow is set after the Block and FragmentWrapper are created and they use dirty, but don't they also append to the context? So idk the right approach here.

ghost commented 4 years ago

@mrkishi,

This line should probably just initialize bitmask to contain an object since we'll always need at least one of them.

https://github.com/sveltejs/svelte/blob/8a6abc9215b10e93db90c0d33c2b2f99d098641e/src/compiler/compile/render_dom/Renderer.ts#L209

Should be:

const bitmask: BitMasks = [{ n: 0, names: [] }]; 

Burning question is still how did the bitmask object get initialized not containing an index 0 in the first place!

mrkishi commented 4 years ago

Contrary to what I assumed earlier, I see that renderer.add_to_context() is called in a few places after initialization, during render. That'd explain why context_overflow is false even though there are more than 31 variables in the context.

dimfeld commented 4 years ago

Here's a reproduction: https://svelte.dev/repl/5357a38fcfc6441ebcf162fa5d307a12?version=3.16.1

This just imports the svelte-select library. I haven't reduced this to a minimal test case, but hopefully it helps you figure this out. It appears to be failing on the file List.svelte inside svelte-select.

<script>
    import Select from 'svelte-select';
</script>
antony commented 4 years ago

I took the code from @dimfeld 's repro (thanks!) and took the List.svelte component out and started to trim it back.

This is as minimal as I can get it without making the error disappear:

https://svelte.dev/repl/909f45b7a2234ad2804d3dcecd59a54b?version=3.16.1

It seems to occur when you have exactly 31 variables declared and a component declaration.

Note:

solidsnail commented 4 years ago



The error goes away if you write `<A></A>`
antony commented 4 years ago
  • In continuation to what @antony said above, the issue is also caused by a carriage return of an empty component, like so:
<A>

</A>

The error goes away if you write <A></A>

This is what I meant by point #3 above. It's not the sole cause though. You also need exactly 31 variable declarations.

Conduitry commented 4 years ago

Great, thanks for the minimal repro, folks!

edit: It looks like the necessary condition for this to manifest is actually that there be some content passed to the component. The carriage return isn't important, but it does count as content to be passed to the child component.

Conduitry commented 4 years ago

One way to address this looks like it would be to replace

if (!bitmask[i]) bitmask[i] = { n: 0, names: [] };

with

if (bitmask.length <= i) {
    for (let j = bitmask.length; j <= i; j++) {
        bitmask[j] = { n: 0, names: [] };
    }
}

so that we fill in the missing elements in the bitmask array, but I'm not sure whether this is just covering up a bug elsewhere.

Conduitry commented 4 years ago

Just was chatting with @mrkishi and it looks like the issue is that context_overflow is getting set too early, before everything has been added to the context. Here is where I think we are calling this.add_to_context() after we've already set context_overflow.

The obvious solution is to defer calculating context_overflow, or to make it a method and call that instead. I don't think that'll be masking a bug, as in my tests the new does_context_overflow() method wasn't getting called at all until after that call to add_to_context(). This does, however, reveal a bug in code-red related to the /* comments */ in the rendered that would have to be addressed first if we went that route.

Edit: I think it'll be safe just to move this.context_overflow = this.context.length > 31; after this.fragment.render(this.block, null, x#nodesas Identifier);. That's after the calls that add '$$scope' to the context and before the places where we're checking the value of context_overflow. There's still the code-red issue to address.

Rich-Harris commented 4 years ago

Fully admit that this part of the code is completely bewildering. The idea behind setting context_overflow before fragment.render(...) is that rendering will add a bunch of stuff to context that couldn't be part of a changeset — e.g. if you have exactly 31 variables, there's no overflow, but if you have this inside the component...

{#each things as thing}
  <p>{thing}</p>
{/each}

...then thing will get added to context, making its length 32. But because dirty is only tracking top-level changes, it doesn't affect whether or not there's an overflow.

Certainly possible — likely, even — that there's a much neater way to do all this without causing unnecessary overflows.

Conduitry commented 4 years ago

I also just noticed that the code-red issue I was seeing would be fixed in #4078, so I'm glad I don't have to worry about that. If I rebase moving this.context_overflow's assignment onto that branch, I don't have any problems.

But I also see now why that was an issue at all - we were now being overly cautious in saying there was a context overflow in many situations. Blargh. I dunno. I guess the issue is that some of the things added during render really do need to be part of the context and others do not.

Rich-Harris commented 4 years ago

Hmm. Well I'll merge #4078 as-is and cut a release, because I need it for work. We could certainly move the assignment down to guarantee correctness, and then worry about eliminating false positives later

Conduitry commented 4 years ago

Sounds reasonable. I'll rebase and re-open #4084 after you merge #4078.

ghost commented 4 years ago

I'd also initialize bitmask with the first index populated since we'll always need it anyway...

const bitmask: BitMasks = [{ n: 0, names: [] }];