solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.05k stars 914 forks source link

No reactivity with solid + electron-webpack #196

Closed benrbray closed 4 years ago

benrbray commented 4 years ago

Followup to my previous issue. I was able to modify the electron-webpack config to run tsx files through babel with babel-preset-solid active, but unfortunately I'm getting no reactivity in my application.

I've created two minimal working examples:

Even though I'm running the same code and nearly the same webpack config (merged with the electron-webpack defaults), the Electron version has no reactivity (the counter remains at zero). There are no errors in the console. However, calling createState and createEffect manually still works as expected.

The following files are most important:

I'm new to solid, webpack, and Electron, so it's been quite difficult to pinpoint the issue. My best guess is that I have a problem with my webpack configuration, or that there is an extra transpilation happening which transpiles the JSX before babel-preset-solid has a chance to do its thing.

I will update here as I learn more on my own, but any additional help resolving the issue would be appreciated! Thanks!

ryansolid commented 4 years ago

I think that is a good guess. If we are just losing reactivity only in the views it is probably one of two things. Either they must be doing some sort of default hyperscript transformation. Although that would usually lead to a runtime error since it wouldn't be able to find h or React.createElement etc..

The other possibility is there are 2 copies of Solid in your node modules for some reason solid-js/dom that babel plugin writes into your compiled files is a different Solid. Solid's reactive system depends on a singleton reactive scope tracking. So 2 copies of modules might be tracking separately (ie.. the signals are coming from one runtime and the DOM effects from another). Why it would do that is a bit lost on me but check for duplicate copies of Solid in your node modules.

I hope that sets you on the right path. Other than that seeing the compiled output could help. Let me know if that reveals anything. Otherwise when I get a chance I can see if I can download the repo locally and try to help you debug.

benrbray commented 4 years ago

Thanks for the advice! I verified that there is only one copy of solid-js in my node_modules folder, so that doesn't seem to be the issue.

To pinpoint the issue, I made a new branch in the Electron MWE that uses plain JS/JSX rather than TypeScript. Unfortunately the issue still persists, so I took a look at the compiled code. (it should appear in dist/renderer/renderer.js after running npm run compile).

Here's the original JSX:

jsx:

const [count, setCount] = createSignal(0),
timer = setInterval(() => setCount(count() + 1), 1000);
onCleanup(() => clearInterval(timer));
// ...
const AppContent = () => {
    return (<div id="content">Counter: {count()}</div>);
}

Compiled versions are shown below, with and without electron. As you can see, they are compiled in slightly different ways, but as far as I can tell, the electron version should still work, as it's calling createEffect as part of the insert() function.

I'll keep investigating, but since it seems that babel-preset-solid is running as expected, I'm more or less out of ideas. Thanks in advance for any help you can provide!

compiled (from jsx, no electron):

/* harmony import */ var solid_js_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! solid-js/dom */ "./node_modules/solid-js/dist/dom/index.js");
//...
const [count, setCount] = Object(solid_js__WEBPACK_IMPORTED_MODULE_1__["createSignal"])(0),
      timer = setInterval(() => setCount(count() + 1), 1000);
Object(solid_js__WEBPACK_IMPORTED_MODULE_1__["onCleanup"])(() => clearInterval(timer)); // components
//...
const AppContent = () => {
  return (() => {
    const _el$2 = _tmpl$2.cloneNode(true),
          _el$3 = _el$2.firstChild;

    Object(solid_js_dom__WEBPACK_IMPORTED_MODULE_0__["insert"])(_el$2, () => count(), null);

    return _el$2;
  })();
};

compiled (from jsx, with electron-webpack): (view full source)

Object(external_solid_js_["createEffect"])(() => console.log("\n\nfilePath:", state.filePath, "\n\n\n"));
const [count, setCount] = Object(external_solid_js_["createSignal"])(0),
      timer = setInterval(() => setCount(count() + 1), 1000);
Object(external_solid_js_["onCleanup"])(() => clearInterval(timer)); // components
//...
const AppContent = () => {
  return (() => {
    const _el$2 = _tmpl$2.cloneNode(true),
          _el$3 = _el$2.firstChild;

    insert(_el$2, () => count(), null);

    return _el$2;
  })();
};
// where `insert` is defined as...
function insert(parent, accessor, marker, initial) {
  if (marker !== undefined && !initial) initial = [];
  if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
  createEffect(current => insertExpression(parent, accessor(), current, marker), initial);
}
ryansolid commented 4 years ago

Thanks I see the problem but I'm trying to figure out how to fix it. It looks like solid-js is marked as external which is what is being used in by your user code, but a version is also being bundled in your build probably coming in from solid-js/dom. So you have the 2 copies of Solid problem. I'm gathering the intention is not to bundle solid-js/dom. So it should be added to externals in the same way solid-js is. Or we need to remove solid-js from the externals and have it all use the bundled versions. I just didn't see that in your custom config.

This sort of tipped me off https://github.com/benrbray/solidjs-electron-webpack-mwe/blob/jsonly/dist/renderer/renderer.js#L2218 And: https://github.com/benrbray/solidjs-electron-webpack-mwe/blob/jsonly/dist/renderer/renderer.js#L92

webpack require(0) is external solid-js webpack require(2) appears to be solid-js and solid-js/dom bundled right into the build

EDIT: I see nothing for externals in your config so if I had to guess this has to do with node module resolution. I wonder if this is something webpack target electron-renderer does. I wonder if it is requiring the CJS version of the packages instead of the ES versions in different scenarios.

benrbray commented 4 years ago

Thanks for the tip about externals! I am not manually defining any externals, but when I print the config.externals defined by electron-webpack, solid-js does indeed appear in the list:

externals : ["solid-js", "source-map-support", "electron", "webpack", "electron-devtools-installer"]

So, this is definitely unexpected behavior arising from either electron-webpack or webpack itself. After some digging, I found an option for whitelisting modules in electron-webpack:

Since webpack is set to target the electron environment, all modules are treated as externals. Unfortunately, there can be a few situations where this behavior may not be expected by some modules. For the case of some Vue UI libraries that provide raw *.vue components, they will needed to be white-listed. This ensures that vue-loader is able to compile them as the UI library originally expected.

Adding the following to my package.json did the trick:

"electronWebpack": {
  "whiteListedModules": ["solid-js"]
}

Some relevant links about the electron-renderer webpack target for future visitors:

Thanks very much for all your help, this issue has been quite the headache! I appreciate the quick response time, on a Saturday no less! I'll push my solution to the mwe repo for any future visitors.

ryansolid commented 4 years ago

That's great. Although I suspect if the default is to external that the preferred solution might be add solid-js/dom to the externals rather than whitelist solid-js. I wonder if they construct the list from package.json. In either case I'm glad it works.