I have a weird behavior involving parcel and Syncfusion Spreadsheet component. Unfortunately, I think it might be specific to both parcel and/or Syncfusion's component, so it might be tricky figuring out.
I have a React application with the following package.json
:
{
"name": "parcel-test-app",
"version": "1.0.0",
"description": "",
"source": "index.html",
"scripts": {
"cleanup": "if exist dist rmdir /q /s dist",
"build": "npm run cleanup && parcel build --no-optimize",
"watch": "npm run cleanup && parcel watch",
"start": "parcel"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@parcel/transformer-sass": "^2.8.3",
"parcel": "^2.8.3",
"process": "^0.11.10"
},
"dependencies": {
"@syncfusion/ej2-base": "^20.4.44",
"@syncfusion/ej2-icons": "^20.4.42",
"@syncfusion/ej2-popups": "^20.4.44",
"@syncfusion/ej2-react-buttons": "^20.4.44",
"@syncfusion/ej2-react-diagrams": "^20.4.42",
"@syncfusion/ej2-react-dropdowns": "^20.4.43",
"@syncfusion/ej2-react-inputs": "^20.4.42",
"@syncfusion/ej2-react-lists": "^20.4.42",
"@syncfusion/ej2-react-navigations": "^20.4.44",
"@syncfusion/ej2-react-popups": "^20.4.44",
"@syncfusion/ej2-react-richtexteditor": "^20.4.44",
"@syncfusion/ej2-react-splitbuttons": "^20.4.42",
"@syncfusion/ej2-react-spreadsheet": "^20.4.44",
"@syncfusion/ej2-richtexteditor": "^20.4.44",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^4.9.4"
}
}
As you can see, it's React, TypeScript, Parcel and Syncfusion's components.
index.html
is a classic placeholder, which is used by React App
component:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TypeSCript & Syncfusion Spreadsheet</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
import { SyncfusionSpreadsheet } from "./SyncfusionSpreadsheet";
export const App = () => {
return <SyncfusionSpreadsheet />;
};
The only thing I'm doing inside SyncfusionSpreadsheet
is importing CSS files from its npm packages:
// Syncfusion Spreadsheet
import "./node_modules/@syncfusion/ej2-inputs/styles/bootstrap5.css";
import "./node_modules/@syncfusion/ej2-buttons/styles/bootstrap5.css";
import "./node_modules/@syncfusion/ej2-lists/styles/bootstrap5.css";
import "./node_modules/@syncfusion/ej2-dropdowns/styles/bootstrap5.css";
import "./node_modules/@syncfusion/ej2-grids/styles/bootstrap5.css";
import "./node_modules/@syncfusion/ej2-react-spreadsheet/styles/bootstrap5.css";
export const SyncfusionSpreadsheet = () => {
return <div>SyncfusionSpreadsheet</div>;
};
Now the weird part starts. If you use npm run watch
to run parcel in watching mode, you can see that it outputs a CSS bundle with spreadsheet
classes inside (which come from Syncfusion's CSS imported files):
However, if you use npm run build
which uses parcel build
command (--no-optimize
is added to disable minification as per documentation), there's no CSS bundle built at all:
What I've tried
I tried importing the external CSS files in another ways, e.g. in .css or .scss files. However, then I have another issues with parcel throwing errors about duplicated bundles (in real production app, I use multiple parcel's sources in package.json
).
I also tried changing CSS imports to the following form:
import "@syncfusion/ej2-inputs/styles/bootstrap5.css";
but it didn't change anything.
I've been fighting with this issue for a long time and honestly I have no idea what's happening here. Fun part is that with external CSSes imported from other packages (e.g. from Syncfusion's Diagram npm packages) it works fine with both watch
and build
.
Does anyone have any idea what might be happening here? Why parcel outputs different bundles results in watch
and build
?
Tracing it from the source code, we find a few differences:
watch
always builds bundles in development mode, while build
uses productionwatch
has NODE_ENV set default to development
and enables auto-installationwatch
builds in watch mode.watch
mode adds HMR supportwatch
mode doesn't use scope hoistingSurprisingly enough, it's scope hoisting! When the Parcel CLI is explicitly modified to set scope hoisting to false, the bundle then builds the wanted CSS modules.
Scope hoisting has a feature that handles side effects, and would usually load in all of the CSS modules if it wasn't for the fact that our @syncfusion
library has decided to say that their modules are side-effect free.
It seems that Parcel, when it notices that the package.json
has this, decides to ignore all modules that don't have explicit imports when importing, including CSS modules.
This is not a bug: #7825, but rather intended behavior, and also an error on the @syncfusion
maintainers for not saying that their CSS modules are not side-effect free.
In their package.json
, they should have this declaration for their sideEffect
field:
"sideEffects": [
"*.css"
],
However, this is also a chance for Parcel to improve -- what they should have done is raised a warning that there was an "unnecessary import of a pure file" to guide others onto finding this.
I've raised an issue on syncfusion related to this, and am waiting for a response by the maintainers.