vercel / styled-jsx

Full CSS support for JSX without compromises
http://npmjs.com/styled-jsx
MIT License
7.7k stars 262 forks source link

optimizeForSpeed mode sometimes attempts to insert multiple rules as one #602

Open jaydenseric opened 4 years ago

jaydenseric commented 4 years ago

Do you want to request a feature or report a bug?

Bug.

What is the current behavior?

In optimizeForSpeed mode, sometimes multiple rules are being incorrectly stuffed into .insertRule() at once, here:

https://github.com/zeit/styled-jsx/blob/343795d3b2fef6d3d085921b0d4cf31b4d9be709/src/lib/stylesheet.js#L129-L139

In production the error is swallowed. This made it exceedingly hard for me to figure out why two seperate Next.js projects were having horrific style issues in production; the SSR page would be styled fine, but on client side route transitions, the styled-jsx styles for many of the components would be simply missing.

Here is a spectrum post from when I first encountered this bug a few weeks ago:

https://spectrum.chat/styled-jsx/general/absolutely-stumped-why-next-js-page-styles-broke~3ad888fa-23fc-4e6c-9948-fffa81a4e956

I found the "fix" at the time was to remove styling from a single component:

https://github.com/jaydenseric/device-agnostic-ui-website/commit/a44fca1f988038d4245054a3b1990eef8d053cbb

Fast forward to now, and I have another project that suddenly started bombing out, here is a Zeit Now deployment:

whimsy-app-hhuy9ally.now.sh

If I put a console.error(error) in here, this is an example error:

Screen Shot 2019-11-28 at 8 15 19 pm

Here is a problematic "rule" that was being inserted:

.jsx-3944333314,.jsx-3103593668 ul,.jsx-3103593668 ol{margin-top:1em;margin-bottom:1.5em;padding-left:2em;}.jsx-3944333314:first-child,.jsx-3103593668 ul:first-child,.jsx-3103593668 ol:first-child{margin-top:0;}.jsx-3944333314:last-child,.jsx-3103593668 ul:last-child,.jsx-3103593668 ol:last-child{margin-bottom:0;}.jsx-3944333314>li,.jsx-3103593668 ul > li,.jsx-3103593668 ol > li{margin:0.5em 0;max-width:30em;line-height:var(--daui-line-height);}.jsx-3944333314>li:first-child,.jsx-3103593668 ul > li:first-child,.jsx-3103593668 ul > li:first-child{margin-top:0;}.jsx-3944333314>li:last-child,.jsx-3103593668 ul > li:last-child,.jsx-3103593668 ol > li:last-child{margin-bottom:0;}

It was the result of a few weeks of work without a production deployment, so unfortunately I can't say exactly what change pushed the bug to occur. I haven't lucked out on a miraculous "fix" this time though, so after MANY hours of debugging I'm getting pretty desperate!

If the current behavior is a bug, please provide the steps to reproduce and possibly a minimal demo or testcase in the form of a Next.js app, CodeSandbox URL or similar

I'm sorry I only have complicated, real world examples linked above and I haven't been able to isolate a minimal reproduction.

I have discovered that debugging is a lot easier when running optimizeForSpeed in dev, so traces aren't minified:

{
  "presets": [["next/babel", { "styled-jsx": { "optimizeForSpeed": true } }]]
}

What is the expected behavior?

Styles managed via styled-jsx in a Next.js app render correctly on the client after route transitions.

Environment (include versions)

Did this work in previous versions?

Not sure.

giuseppeg commented 4 years ago

@jaydenseric hey mate I am sorry that you are experiencing this kind of issue! Sounds super frustrating. Before we even try to debug, can you make sure that both the library and the consumer app are transpiled in the same "mode" (either NODE_ENV production or development).

The issue could be that your components library is compiled for development mode and when the Next.js app is deployed to prod it instead uses optimizeForSpeed which cannot insert styles compiled for dev mode.

jaydenseric commented 4 years ago

I'll check, but if that is the case, how come none of the styles are broken right now at https://deviceagnosticui.com, or https://jaydenseric.com?

giuseppeg commented 4 years ago

it seems that you are not using optimizeForSpeed there

Screen Shot 2019-11-28 at 11 16 27 AM

giuseppeg commented 4 years ago

this article explains how to bundle a library that uses styled-jsx to avoid this issue https://medium.com/@tomaszmularczyk89/guide-to-building-a-react-components-library-with-rollup-and-styled-jsx-694ec66bd2

jaydenseric commented 4 years ago

Thanks for the revelation, although this is a real headache to solve elegantly 😓

Bundling published packages is an antipattern, and you can't have require() inside ESM for a conditional export. ESM for tree-shaking is an important feature for a published a component library, so I don't want to drop it. Publishing invalid ESM is undesirable because eventually, maybe even in January when conditional package exports are shipped by Node.js, I'll be publishing ESM intended to be run natively.

I think I might publish two builds at device-agnostic-ui for development and device-agnostic-ui/production for production, then have Babel/Webpack config to setup in Next.js to rewrite the imports accordingly.

jaydenseric commented 4 years ago

Is optimizeForSpeed even worth it? What exactly are the benefits?

I might just continue to publish device-agnostic-ui components and styles transpilled with it false, and in the Next.js setup instructions explain that the Babel config needs to be configured like this:

{
  "presets": [["next/babel", { "styled-jsx": { "optimizeForSpeed": false } }]]
}
giuseppeg commented 4 years ago

optimizeForSpeed uses the CSSOM apis which are way faster than creating script tags.

As for bundling you have a few options:

1) Ship the source code and let the host app build it 2) You can use conditionals, bundlers will treeshake this https://github.com/giuseppeg/style-sheet/blob/aa125093adcef82ca3bd86b58f5f203ef232a4fa/lib/esm/createElement.js (works only if the bundler replaces process.env.NODE_ENV === 'production' with a constant, eg. webpack does that)

giuseppeg commented 4 years ago
  1. won't work in UMD or browser of course
jaydenseric commented 4 years ago

Here is an example of the difference between transpiling published components without and with NODE_ENV=production

For this component:

https://deviceagnosticui.com/components/Blockquote

Here is without:

export var stylesBlockquote = {
  styles: React.createElement(
    _JSXStyle,
    {
      id: '3946479178'
    },
    '.jsx-3946479178,.' +
      stylesHtml.className +
      ' blockquote{margin-top:1.5em;margin-bottom:1.5em;margin-left:1.5em;max-width:22em;line-height:var(--daui-line-height);font-style:italic;font-weight:300;text-rendering:optimizeLegibility;}.jsx-3946479178:first-child,.' +
      stylesHtml.className +
      ':first-child{margin-top:0;}.jsx-3946479178:last-child,.' +
      stylesHtml.className +
      ':last-child{margin-bottom:0;}'
  ),
  className: 'jsx-3946479178'
}

Here is with:

export var stylesBlockquote = {
  styles: React.createElement(
    _JSXStyle,
    {
      id: '3946479178'
    },
    [
      '.jsx-3946479178,.' +
        stylesHtml.className +
        ' blockquote{margin-top:1.5em;margin-bottom:1.5em;margin-left:1.5em;max-width:22em;line-height:var(--daui-line-height);font-style:italic;font-weight:300;text-rendering:optimizeLegibility;}',
      '.jsx-3946479178:first-child,.' +
        stylesHtml.className +
        ':first-child{margin-top:0;}',
      '.jsx-3946479178:last-child,.' +
        stylesHtml.className +
        ':last-child{margin-bottom:0;}'
    ]
  ),
  className: 'jsx-3946479178'
}

The only difference appears to be the rules are one big string in dev, and an array of a string for each rule for prod.

Could styled-jsx be enhanced to automatically .join() an array of rules encountered in development? That way component libraries could be published only transpilled for production.

giuseppeg commented 4 years ago

Could styled-jsx be enhanced to automatically .join() an array of rules encountered in development?

@jaydenseric it is not possible to mix up optimizedForSpeed styles with non optimizedForSpeed. With optimizedForSpeed we insert to a single style tag using the CSSOM API, when it is disabled we create multiple style tags.

We can add a warning here though

https://github.com/zeit/styled-jsx/blob/343795d3b2fef6d3d085921b0d4cf31b4d9be709/src/stylesheet-registry.js#L34-L39

like if this._optimizeForSpeed is true but props.children (the styles) are not an array print a warning

jaydenseric commented 4 years ago

Perhaps you misunderstood the suggestion...

It would be impossible to support importing non optimizedForSpeed styles for use in a production build, because it is hard to cut up a style string into an array of rules at runtime - that should always happen build time.

But, I can't see why the other way around can't be supported. You should be able to import optimizedForSpeed styles for use in a development build, because it would be trivial at runtime to detect the styles are an array of rules, and simply array .join() them together into a single string, just like how non optimizedForSpeed styles are normally.

With this behavior, to write a component/styles library all you need to do is publish a single optimizedForSpeed build. This would make it much easier to publish styled-jsx styles and a native ESM build would be possible.

giuseppeg commented 4 years ago

@jaydenseric that is a bit "magic" but it might work, want to put together a PR?

jaydenseric commented 4 years ago

@giuseppeg I got started on a PR a few weeks ago but had to move onto other stuff. Sorry, but I'm now out of time and money to work on OSS, see https://twitter.com/jaydenseric/status/1206800095280128006. I've been working on OSS pro bono full time since August; time to get back into paid work.

giuseppeg commented 4 years ago

@jaydenseric no worries! I get that as I am not paid to maintain any of these OSS projects as well :)