Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New component root property may throw errors #6584

Open
CatchABus opened this issue Jul 27, 2021 · 53 comments
Open

New component root property may throw errors #6584

CatchABus opened this issue Jul 27, 2021 · 53 comments

Comments

@CatchABus
Copy link

CatchABus commented Jul 27, 2021

Read this comment first

The reason why this occurs is that you are likely trying to use a pre-compiled (to JS) component that was compiled with a different Svelte version than the one you use, which is not supported. See this comment for more info and solutions:
#6584 (comment)

Describe the bug

I recently experienced issues due to new root property in several svelte plugins.
It seems that this line causes issues: 5cfefeb#diff-da9bae4e28c441de5ba3a074e30775fe69109100b3d921ad8f2592d93cd67b7f

It seems that a null check for parent_component variable is missing at that point.

on_mount: [],
on_destroy: [],
on_disconnect: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : options.context || []), // Here, there is a null check for parent_component variable

// everything else
callbacks: blank_object(),
dirty,
skip_bound: false,
root: options.target || parent_component.$$.root // Here there is no check for parent_component variable

Reproduction

This suddenly occured on certain svelte plugins.

Logs

No response

System Info

System:
    OS: Linux 5.8 Debian GNU/Linux 10 (buster) 10 (buster)
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
    Memory: 517.19 MB / 7.47 GB
    Container: Yes
    Shell: 5.0.3 - /bin/bash
  Binaries:
    Node: 16.0.0 - ~/.n/bin/node
    npm: 7.10.0 - ~/.n/bin/npm
  npmPackages:
    svelte: ^3.31.2 => 3.38.3 
    webpack: ^5.16.0 => 5.44.0

Severity

blocking an upgrade

@mskocik
Copy link

mskocik commented Jul 27, 2021

Experiencing the same issue. I had svelte app, which includes npm svelte component and it throws this error:
image

This is my simplified code snippet:

/** app.js */
<script>
import Example from './example.svelte';

new Example({ target: document.body });
</script>

/** example.svelte **/
<script>
import Svelecte from 'svelecte';
</script>

<Svelecte></Svelecte> <!-- when I add component, it breaks -->

@CatchABus
Copy link
Author

It's strange that context property will check for existence of parent_component but the new root property won't.
@mskocik I had the exact same error from plugins that had bundles using latest svelte version.

@micschwarz
Copy link

micschwarz commented Jul 29, 2021

This also occurs when sharing components with webpacks module federation. (In the current version of svelte, older versions work).
An example is here: https://github.com/micschwarz/svelte-module-federation

@nullbio
Copy link

nullbio commented Jul 30, 2021

I've been racking my brain for the last 3 hours trying to figure out why my app suddenly started exploding... Eventually managed to find this issue. Is there a simple fix to reverting this bug so I can continue development until it's patched? I'm using esbuild-svelte which is pulling in a broken version I guess, so I can't just define an older version of svelte in my package.json as far as I'm aware?

For others trying to Google this issue, the console output for Chrome is:

Uncaught (in promise) TypeError: Cannot read property '$$' of null

And for Firefox:

Uncaught (in promise) TypeError: parent_component is null

@mskocik
Copy link

mskocik commented Jul 30, 2021 via email

@nullbio
Copy link

nullbio commented Jul 30, 2021

Try downgrading svelte version until it works again. Version 3.39.0 worked fine for me.

On 30. 7. 2021 12:01, nullbio wrote: I've been racking my brain for the last 3 hours trying to figure out why my app suddenly started exploding... Is there a simple fix to reverting this bug so I can continue development until it's patched? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#6584 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGCU5FABFDMDYYYU3OTM6PTT2JZ7BANCNFSM5BCL6OBQ.

Thanks. Clearing my package-lock.json, node_modules, and adding "svelte": "3.39.0", to my package.json seems to have worked. Didn't think it would because in my build svelte is a dependency of esbuild-svelte, but I suppose in the land of node it enforces a version for sub-dependencies if you declare one explicitly. News to me. Just leaving this here for anyone else who needs it and is new to all of this like I am.

@pateketrueke
Copy link

This breaks on Snowpack's streaming imports where you don't have the svelte/compiler available and it just downloads ESM modules... so, for componentes that were already generated this way then there's no solution.

Sticking to v3.39.0 is fine if you're bundling all way long, but CDN-based approaches are failing due this.

Until #6646 gets merged CDN users are just blocked.

Thank you!

@robin-shine
Copy link

does this issue solved? the same error occurs to me.

@Conduitry
Copy link
Member

Why is this code ever getting called with neither a target passed in to the constructor nor current_component/parent_component being set?

If it's one Svelte component trying to use another pre-compiled pre-bundled Svelte component, there are already other known issues with that (problems with the transitions scheduler, context not inheriting, probably others), and it's not something that's supported.

What's the use case here? Would the change in #6646 prevent this immediate crash, but still leave the other less-visible issues with apps being bundled with multiple copies of Svelte's internals?

@micschwarz
Copy link

@Conduitry

What's the use case here?

Microfrontends are the use case :)

Would the change in #6646 prevent this immediate crash, but still leave the other less-visible issues with apps being bundled with multiple copies of Svelte's internals?

The change in #6646 would prevent this, yes.
image
Of course the other issues would still exist, but if you use svelte this way, you might be aware of them.

@dummdidumm
Copy link
Member

A possibly related post on Reddit: https://www.reddit.com/r/sveltejs/comments/pjo902/svelte_microfrontend_module_federation_bug/?utm_medium=android_app&utm_source=share

@roblevintennis
Copy link

roblevintennis commented Sep 24, 2021

I'm getting same error and I've tried downgrading to 3.39.0 and using 3.43.0 latest but still get it:

image

My use case is I have a ui component library I'm attempting to prepare for publishing -- I've npm linked to a ui library which I've built out a dist/index.js via rollup -c and when I try to use it with following I get said error:

<script>
	import { Button } from 'agnosticui-svelte';
</script>

<main>
	<Button mode="primary" isBordered>Testing 1234</Button>
</main>
<style></style>

I've used https://github.com/joeattardi/svelte-tabs and built it out a similar way and it doesn't have the issue but it's locked back at 3.7.1:

    "svelte": {
      "version": "3.7.1",
      "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.7.1.tgz",
      "integrity": "sha512-MKjFy3YZ2pNUVPTwLNQ9JAVx6KVwfYnm/vbUF/pKLrfDDZiCBKfWPwnffpwGlnIAn7aS+aJoKV0soj464DOs4Q==",
      "dev": true
    },

I tried rolling all the way back to 3.7.1 in my project, and I still have same exact error. So that tells me perhaps there's something different (and wrong) between what I'm doing and what svelte-tabs.

UPDATE:
I just deleted all my components and copied over the very simple svelte-tabs components and updated my example app to look exactly like his. I can reproduce same exact error with svelte v3.43.0.

Then, I backed down my package.json for the library and for the example app the was npm linked to "svelte": "3.39.0"`, removed both lock files and reinstalled. I was able to run the example. So, there's something specific in my components that still was causing the error even after downgrading; so I'll have to just add my components back one at a time to figure that out. However, it looks like this is definitely a bug on the latest version.

This seems like a blocker for anyone trying to develop a component library.

@gcruchon
Copy link

gcruchon commented Sep 25, 2021

Hello,

I do have the same issue in my app while trying to test a component which contains an "if" statement and a mock module. I did not have the issue back when I was using Svelte 3.35.0. I tried to upgrade yesterday to 3.43.0, but did not succeed. I had to rollback to 3.39.0.

To ease a bit the debug, here's a simplified repo to reproduce the issue: https://github.com/gcruchon/tests/tree/main/svelte-null-parent-component

I'm using last version of all libraries (svelte, jest, svelte-jester, babel, testing-library, ...). I used the svelte template and just added the bug (i.e. testing a mocked "Link" within an "if" statement)

Feel free to comment if I missed something.

JulesFrancoise added a commit to marcellejs/marcelle that referenced this issue Oct 11, 2021
Fixes a bug in compiled components, probably related to sveltejs/svelte#6584
xavhan pushed a commit to xavhan/svelte-vitamin that referenced this issue Oct 20, 2021
@bluwy
Copy link
Member

bluwy commented Oct 21, 2021

Re @mskocik I tested your example in a REPL and I don't see any errors.

Re streaming imports @pateketrueke I'm not familiar with it, but usually using CDN or pre-bundled code for Svelte components, they should always be instantiated with a target to mount to. Svelte doesn't guarantee components to work cross version AFAIK, so target would help "isolate" it.

Re jest mocks @gcruchon that seems like a bug of how Svelte components are mocked and interacting with the component initialization phase. I'm not familiar with how it works under-the-hood, but probably somewhere multiple svelte instances are created when being bundled (?).

I agree with Conduitry here. While the PR that fixes this works, I don't see any reason why the error would happen in the first place. Either options.target is defined for the component to mount to, or it has a parent_component. It shouldn't be dangling around without a reference. If anything, versions above 3.39.0 have surfaced many implementation bugs around the ecosystem, and we should fix them instead. And they largely seem to be components not being compiled/bundled correctly.

@mskocik
Copy link

mskocik commented Oct 21, 2021

@bluwy Cannot duplicate in the REPL neither. But it was happening. It was quite hard to replicate back then. But since when I tried 3.43.1 it worked for me since then.

@gcruchon
Copy link

Re @mskocik that seems like a bug of how Svelte components are mocked and interacting with the component initialization phase. I'm not familiar with how it works under-the-hood, but probably somewhere multiple svelte instances are created when being bundled (?).

Thanks for acknowledging this bug.

If I’m not mocking svelte component in the right way, how would you recommend to mock a svelte component? Using an import of a much simpler .svelte file seems to me pretty simple and straightforward… how come this works without if statement and suddenly fails with an if?

I’m not a specialist of the svelte compiler, so I need more info to investigate. Can someone help me understand and improve the maturity of Svelte to embrace such software craftsmanship practices?

Do you have all what is necessary to reproduce the bug? (I see a label “need repro”)

@bluwy
Copy link
Member

bluwy commented Oct 25, 2021

If I’m not mocking svelte component in the right way, how would you recommend to mock a svelte component?

The way you mock it now is correct, but I'm guessing there's a bug when transforming svelte files in jest. Might be an issue in https://github.com/mihar-22/svelte-jester.

Do you have all what is necessary to reproduce the bug? (I see a label “need repro”)

I'd say a no since the repros given so far are Svelte + [third-party integration] repros. Maybe it's fair as it only happens in these very specific scenarios, but it makes it hard to nail down what actually is the bug in Svelte. Or the real bug resides in that third-party integration.

jacob-8 added a commit to livingtongues/living-dictionaries that referenced this issue Nov 20, 2021
@crisward
Copy link

crisward commented Nov 28, 2021

@nullbio I was also getting this error with Esbuild and it seemed to happen when I linked other libraries that also included svelte. Esbuild wasn't de-duping the svelte import, so I was getting multiple copies inside my bundle. I couldn't find a plugin to de-dupe svelte so added this inline one.

// part of esbuild config
plugins: [
    {
      name: 'dedupe-svelte',
      setup({ onResolve }) {
        const svelte = require.resolve('svelte')
        onResolve({ filter: /^svelte/ }, args => {
          let path = svelte.replace(/svelte\/[^\.]+.js$/,args.path+"/index.mjs")// .mjs for browser path
          return { path }
        })
      }
    },
    sveltePlugin({compileOptions: {css: true}})
  ],

Hope that helps.

@pateketrueke
Copy link

Add svelte field in package.json will resolve it,but i don't want to do this.

Actually that's the solution, the svelte compiler should consume all the .svelte sources to produce correct builds.

If you don't want to follow that rule then it is your fault, not svelte's fault.

Bundlers should be aware of that, and MUST load "svelte": "./path/to/sources" to properly work.

If your bundler does not resolve from the svelte field then it'll fail, and that's not your fault... just pick another bundler that will do his job correctly!

@matpen
Copy link

matpen commented Jun 3, 2022

I second @pateketrueke's opinion. See tjinauyeung/svelte-forms-lib#169 (comment) for a discussion.

@richerfu
Copy link

richerfu commented Jun 6, 2022

Actually that's the solution, the svelte compiler should consume all the .svelte sources to produce correct builds.

It's an amazing rule, and i can't find it in the docs.

So i must provide all of the source files to avoid this problem? If it is, esm/cjs module may be useless?

@pateketrueke
Copy link

So i must provide all of the source files to avoid this problem?

yes, definitely

If it is, esm/cjs module may be useless?

not really, because you can still consume compiled components manually, using the new Component() syntax

it is very useful to have, i.e. a well-crafted component that can operate standalone

@jindrahm
Copy link

I finally have a solution.. building svelte/internal as a separate module and treating svelte and svelte/internal dependencies as external and pointing them to the module.
That makes the internal svelte logic to be shared for all the components.
I updated the repo I created for reproduction of one of the issues I had with the pre-bundled components ..so you can have a look.

@atomcat1978
Copy link

@pateketrueke What if someone uses a svelte component written in typescript, but the project is using pure JS? Then one would have to add typescript just because of the component used? Feels like an overhead definetely.

@ivanhofer
Copy link
Contributor

What if someone uses a svelte component written in typescript, but the project is using pure JS

That's why there exists the sveltekit package feature.

Library authors should ideally use this feature to create the package. Authors can use SCSS, TypeScript and other language variants and in the end normal JavaScript and CSS components will be generated that can be consumed by any other Svelte project.

Here is an example of a TypesScript demo package: https://github.com/ivanhofer/svienna-meetup-package-demo.
You can run npm run package and see the exported files in the package folder. All files are beeing converted to normal JavaScript versions.

If you are using a library that doesn't get published in the right way, you could open an issue and let the authors know that this feature exists.

@atomcat1978
Copy link

Actually, I could resolve this issue with a correct rollup config in my library project. I defined all svelte related stuff as external and excluded from resolve, so it is fetched from within the project that is using my components fro the library. My rollup config looks as follows:

import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'
import pkg from './package.json'
import typescript from '@rollup/plugin-typescript';
import {terser} from 'rollup-plugin-terser'
import resolve from '@rollup/plugin-node-resolve';

const globals = {
  'svelte/internal': 'svelte/internal',
  'svelte': 'svelte',
};

export default
  {
    external: /^svelte.*$/,
    input: 'src/index.ts',
    output: [
      {
        file: pkg.module,
        format: 'es',
        sourcemap: true,
        globals
      },
      {
        file: pkg.main,
        format: 'umd',
        name: 'Autocomplete',
        sourcemap: true,
        globals
      },
    ],
    plugins: [
      svelte({
          preprocess: [
            autoPreprocess()
          ],
          emitCss: false,
        },
      ),
      resolve({
        // Exclude all svelte related stuff
        resolveOnly: [/^(?!svelte.*$)/]
      }),
      typescript({sourceMap: true}),
      terser(),
    ],
  };

Apparently works fine, since all svelte stuff gets provided by the build system of the application using the component.

@Tlahey
Copy link

Tlahey commented Aug 17, 2022

Thanks @atomcat1978, set svelte (and svelte/internal) as a external librairie fix the build issue on client side :)
Potatoes for everyone ! 🥔

@vpalos
Copy link

vpalos commented Jan 5, 2023

Having this same issue:

  1. Built a library with Svelte components and packaged it using svelte-package;
  2. Importing the library in a TypeScript project which uses Rollup to compile the end result.

In this setup, I could instruct Rollup (second project) to use it's own version of Svelte (including for the components inside the library) by using the dedupe option of the @rollup/plugin-node-resolve plugin:

// rollup.config.js
import { visualizer } from "rollup-plugin-visualizer";

import commonjs from "@rollup/plugin-commonjs";
import esbuild from "rollup-plugin-esbuild";
import json from "@rollup/plugin-json";
import polyfills from "rollup-plugin-polyfill-node";
import resolve from "@rollup/plugin-node-resolve";
import svelte from "rollup-plugin-svelte";
import sveltePreprocess from "svelte-preprocess";

export default {
    input: "./src/index.ts",
    output: [
        {
            name: "TPScript",
            file: "dist/script.js",
            format: "umd",
            sourcemap: true,
        },
    ],

    plugins: [
        svelte({
            preprocess: sveltePreprocess({
                sourceMap: true,
            }),
            emitCss: false,
            compilerOptions: {
                sourcemap: true,
            },
        }),

        json(),
        commonjs(),
        polyfills(),
        resolve({
            browser: true,
            dedupe: ["svelte"],
        }),

        esbuild({
            minify: process.env.NODE_ENV === "production",
        }),

        visualizer(),
    ],
};

Posted the whole thing in case it helps anyone, but the relevant bit is just the dedupe: ["svelte"] part.

@kennethnym
Copy link

kennethnym commented Mar 19, 2023

I am still getting this error, even though I made sure the components were precompiled with the same version of svelte as the one the app uses (3.53.1). The components were compiled using esbuild via the esbuild-svelte & svelte-preprocess. AFAIK, esbuild-svelte declares svelte as peerDependency since 0.7.0, and I am using 0.7.3, so it is definitely using the correct svelte version to compile the components.

For context, I am trying to develop a desktop application that has a plugin system that can extend the application's functionality, and one part of it is that plugins can export svelte components that can then be used in the application itself. The application imports the compiled svelte files via the import() syntax. The imported component is then consumed via <svelte:component>. Each <svelte:component> is rendered inside of a div, so I would expect svelte to be able to pick up the parent_component correctly.

@jindrahm
Copy link

@kennethnym It's not that much about the svelte version as it is about having the same context of svelte/internal. Make sure you don't have svelte/internal bundled in each module.

I'm not familiar with esbuild (I use rollup) so I cannot give you a concrete tip to set it up. But I have svelte/internal like separate module which I import into the other modules. In rollup I set to threat svelte/internal as extrenal module and override the default path to it.

@kennethnym
Copy link

I suspected that was the issue. Thanks for the pointer, I have managed to fix the issue by making sure the plugin and the application is loading the same instance of svelte.

@vitmsrk
Copy link

vitmsrk commented Apr 20, 2023

@kennethnym How did you manage to do that?

I'm having the same use case with svelte components as plugins, and trying to make the plugins load svelte/internal stuff from the host application.
I'm looking into Import maps option for the browser, but not sure that will work good enough.

I'm wondering what was your solution. Any hint will be much appreciated 🙏🏻

@jare25
Copy link

jare25 commented Apr 21, 2023

The problem here is instantiating a component compiled with Svelte version X inside an app compiled with Svelte version Y in a declarative way.

The following is not guaranteed to work:

<CompiledComponent />
<svelte:element this={CompiledComponent} />

The following will work:

<script>
   //..import
   let el;
   onMount(() => new CompiledComponent({target: el, props: {..}));
</script>
<div bind:this={el} />

If using a component from a library, ensure that an uncompiled version exists and is used by your build tool. SvelteKit's package command ensures this for example.

Thanks for this!
I'm trying to use shared components, with webpack module federation. Tried with <svelte:component />, but I also get error like others (parent_component).

This is my working RemoteButton component (should add some error handling):

<script>
    import {onMount} from "svelte";

    let el;
    let Component
    let Module

    onMount(async () => {
        Module = (await import('components/Button')).default;
        Component = new Module({target: el, props: $$props});
    });

    $: {
        if (Component) {
            Component.$set($$props)
        }
    }

</script>

<div bind:this={el}></div>

@kennethnym
Copy link

kennethnym commented Apr 22, 2023

@kennethnym How did you manage to do that?

I'm having the same use case with svelte components as plugins, and trying to make the plugins load svelte/internal stuff from the host application. I'm looking into Import maps option for the browser, but not sure that will work good enough.

I'm wondering what was your solution. Any hint will be much appreciated 🙏🏻

@vitmsrk In my case I am using Svelte in a Tauri app, so the way I make it work will probably not be applicable to you. Anyways, this is how I made it work. Hopefully it will be helpful to you.

Basically, you need to make sure that both the internal code and the plugin code uses the same import URL to import svelte. I use Vite to bundle the internal code. Since it doesn't support import rewriting OOTB, I wrote this simple Vite plugin to do exactly that:

/**
 * This vite plugin prevents vite from rewriting import paths,
 * because some libraries are imported through the lib:// protocol
 * instead of being bundled directly into the application.
 *
 * https://github.com/vitejs/vite/issues/6393
 */
function libProtocol() {
  return {
    name: "lib-protocol",
    enforce: "pre",
    configResolved(resolvedConfig) {
      const VALID_ID_PREFIX = "/@id/"
      const reg = new RegExp(`${VALID_ID_PREFIX}(lib:\/.+)`, "g")
      resolvedConfig.plugins.push({
        name: "vite-plugin-ignore-static-import-replace-idprefix",
        transform: (code) => reg.test(code)
          ? code.replace(reg, (m, s1) => s1)
          : code
      })
    },
    resolveId(id, importer) {
      if (id === "svelte" || id === "svelte/internal") {
        return {id: `lib://localhost/svelte_internal.js`, external: true}
      }
      if (id === "@twind/core") {
        return {id: "lib://localhost/twind_core.js", external: true}
      }
      if (id.startsWith("svelte/")) {
        // `svelte/store` gets flattened to `svelte_store.js`
        return {id: `lib://localhost/${
            id.replace("/", "_")
          }.js`, external: true}
      }
    }
  }
}

module.exports = libProtocol

The lib:// is a special protocol that triggers a custom protocol handler that I registered with Tauri. The handler then reads the compiled Svelte code and responds with the content of the compiled source code.

For the plugins, I use esbuild to compile the plugins. When compiling the plugins, I used esbuild-plugin-import-map] to re-write all svelte imports to the same URL that the internal code of the app uses to import svelte:

function rewriteSvelteImport() {
  importMap.load({
    imports: {
      "@powermacro/api": "lib:/localhost/powermacro-api.js",
      "@powermacro/block-api": "lib:/localhost/powermacro-block-api.js",
      "@twind/core": "lib:/localhost/twind_core.js",
      svelte: "lib:/localhost/svelte_internal.js",
      "svelte/internal": "lib:/localhost/svelte_internal.js",
      "svelte/action": "lib:/localhost/svelte_action.js",
      "svelte/animate": "lib:/localhost/svelte_animate.js",
      "svelte/easing": "lib:/localhost/svelte_easing.js",
      "svelte/motion": "lib:/localhost/svelte_motion.js",
      "svelte/store": "lib:/localhost/svelte_store.js",
      "svelte/transition": "lib:/localhost/svelte_transition.js"
    }
  }, ["lib:/"])
  return importMap.plugin()
}

You can safely ignore the single slash lib:/ vs lib:// - it is a weird workaround for a strange quirk of Tauri that I couldn't quite figure out.

Since both the internal code and the plugin code use the same URL to import svelte libraries, the browser will use the same instance of svelte for the import.

I am assuming that your app is run in a browser instead of Tauri. I can see a potential solution here, which is to import svelte through a CDN, then use the same technique that I used to rewrite imports for internal and plugin code.

Edit: note that the esbuild plugin I linked only supports https protocol and nothing else. I forked the plugin to allow for lib:// protocol.

@vitmsrk
Copy link

vitmsrk commented Apr 24, 2023

@kennethnym Thanks very much for the detailed answer.

It looks like I need to make the plugins know where or how they will be used, what I'm actually try to avoid.
What I have tried is mark svelte as external dependency in the plugin code, so it would import from svelte/internal. Then in the browser try to map svelte/internal to the svelte chunk generated by the main app bundler using import maps.
That worked, but import maps aren't yet well supported in all browsers.

I'll try the CDN option with your custom vite plugin.

Feels like I'm close to it, but missing some final pieces of the puzzle.

@titanism
Copy link

For us the bug was that we needed to wrap our Sapper instance with DOMContentLoaded upptime/status-page@9104d06.

@chrisws
Copy link

chrisws commented Sep 1, 2023

You may also encounter this error if your index.html is missing id="app" on the intended DOM element (or whatver ID you used in main.js).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet