histoire-dev / histoire

⚡ Fast and beautiful interactive component playgrounds, powered by Vite
https://histoire.dev
MIT License
3.14k stars 184 forks source link

Isolate global styles #339

Open xaddict opened 1 year ago

xaddict commented 1 year ago

Describe the bug

When using a histoire.setup.js or style block inside a Vue 3 SFC to import global styles, these styles bleed out into the Histoire styling, overriding color, inputs and theming.

I don't think my application styling should inform Histoire's styling, right?

I suggest to either:

Reproduction

https://stackblitz.com/edit/histoire-vue3-starter-cbmzxo?file=src%2Findex.css

System Info

System:
    OS: macOS 14.3.1
    CPU: Apple M2
    Memory: 24.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 21.6.2 - ~/.nvm/versions/node/v21.6.2/bin/node
    Yarn: 1.22.19 - ~/.yarn/bin/yarn
    npm: 10.5.0 - ~/.nvm/versions/node/v21.6.2/bin/npm
  Browsers:
    Brave Browser: 102.1.39.111
    Chrome: 106.0.5249.119
    Edge: 106.0.1370.52
    Firefox: 106.0.1
    Firefox Developer Edition: 125.0
    Firefox Nightly: 107.0a1
    Safari: 16.0
    Safari Technology Preview: 16.4
  npmPackages:
    @histoire/plugin-vue: ^0.17.15 => 0.17.15 
    @vitejs/plugin-vue: ^5.0.4 => 5.0.4
    histoire: ^0.17.15 => 0.17.15 
    vite: ^5.2.8 => 5.2.8

Used Package Manager

Validations

stackblitz[bot] commented 1 year ago

Fix this issue in StackBlitz Codeflow Start a new pull request in StackBlitz Codeflow.

tillsanders commented 1 year ago

I have the same issue: https://github.com/histoire-dev/histoire/issues/66#issuecomment-1282343948

I would love to see this be addressed in some way. I'm open to sponsoring a little to help make this happen :)

troyere commented 1 year ago

Not strictly related, but there's no iframe at all in grid mode.

Akryum commented 1 year ago

support the [scoped] attribute on style blocks inside SFCs

This is already supported :)

xaddict commented 1 year ago

Yes that’s not the problem. Global styles leaking into histoire’s UI are the problem

Met vriendelijke groet, Luuk Lamers On 10 Feb 2023 at 20:31 +0100, Guillaume Chau @.***>, wrote:

support the [scoped] attribute on style blocks inside SFCs This is already supported :) — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

Akryum commented 1 year ago

Yeah it's a tough issue with no obvious solution (iframe comes with a lot of tradeoffs)

tillsanders commented 1 year ago

Would it be possible to run the stories inside a shadow DOM? That would isolate the styles completely.

xaddict commented 1 year ago

I don't think so, as I have a global stylesheet that gets imported through the main javascript file. I can't import the stylesheet in every story.

PhotonBursted commented 1 year ago

After playing around a bit I noticed some things that may help this along. Perhaps sharing them sparks some new inspiration to get this resolved.

The styles imported in histoire.setup.js are included in the main Histoire app, as well as in the sandbox. It looks like they are "lazily loaded"; the styles from the sandbox only start "bleeding" once the first story is opened. The sandbox itself is an iframe which contains a rendering of the app with the styles included separately.

This can be confirmed by opening the Stackblitz in the original post, opening the app in a new tab, and playing around with it there. It starts out with three <style> tags in the app's <head>, which turns into four once a story is opened. After manually deleting the last one the Histoire app is perfectly displayed, while the styles in the sandbox are retained.

I guess this means the feature is somehow halfway there already? It definitely seems to be possible, at least.

qexk commented 1 year ago

This is an issue that prevents histoire from being used in a ton of projects, unfortunately. I hope this’ll be solved soon.

Thanks to @PhotonBursted’s comment, I wrote this in my histoire.setup.ts file and it seems stable enough:

const isIframe = window.self !== window.top;
document.head
    .querySelectorAll("style[type='text/css']:not([data-vite-dev-id*='histoire'])")
    .forEach((style) => isIframe || style.remove());

It’s dirty but it works with v0.15.9

reslear commented 1 year ago

@Aksamyt but work only in dev mode

for using const isIframe = window.self !== window.top; good idea:

// src/histoire.setup.ts
import { defineSetupVue3 } from '@histoire/plugin-vue'
import { IonicVue } from '@ionic/vue'

export const setupVue3 = defineSetupVue3(async ({ app, story, variant }) => {
  // https://github.com/histoire-dev/histoire/issues/339#issuecomment-1522329599
  const isIframe = window.self !== window.top

  if (isIframe) {
    await import('@/theme/styles')
    app.use(IonicVue)
  }
})
// src/theme/styles.ts
import '@ionic/vue/css/core.css'
import '@ionic/vue/css/normalize.css'
import '@ionic/vue/css/structure.css'
import '@ionic/vue/css/typography.css'
import '@ionic/vue/css/padding.css'
import '@ionic/vue/css/float-elements.css'
import '@ionic/vue/css/text-alignment.css'
import '@ionic/vue/css/text-transformation.css'
import '@ionic/vue/css/flex-utils.css'
import '@ionic/vue/css/display.css'

import './variables.css'

And additional styles bundled in @ionic/vue package.

BUT

Screenshot 2023-05-23 at 17 37 59

Dynamic CSS load in one bundle:

  1. Not separated is used:
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        ionic: ['@ionic/vue', '@ionic/vue-router'],
      },
    },
  },
},

proof:

https://github.com/histoire-dev/histoire/blob/9209381665f82d2d7647776b0080bbbea64df39c/packages/histoire/src/node/build.ts#L142-L143

  1. cssCodeSplit: false proof:

https://github.com/histoire-dev/histoire/blob/9209381665f82d2d7647776b0080bbbea64df39c/packages/histoire/src/node/build.ts#L165

  1. is patching package:
pnpm patch histoire@0.16.1

and remove code in 1 step, and set true in 2 step.

--- a/dist/node/build.js
+++ b/dist/node/build.js
@@ -114,29 +114,19 @@ export async function build(ctx) {
         config(config) {
             // Don't externalize
             config.build.rollupOptions.external = [];
-            // Force chunk strategy
-            config.build.rollupOptions.output = {
-                manualChunks(id) {
-                    if (!id.includes('@histoire/app') && id.includes('node_modules')) {
-                        for (const test of ctx.config.build?.excludeFromVendorsChunk ?? []) {
-                            if ((typeof test === 'string' && id.includes(test)) || (test instanceof RegExp && test.test(id))) {
-                                // Excluded from vendor chunk
-                                return;
-                            }
-                        }
-                        return 'vendor';
-                    }
-                },
-            };
+
             // Force vite build options
             Object.assign(config.build, {
                 outDir: ctx.config.outDir,
                 emptyOutDir: true,
-                cssCodeSplit: false,
+                cssCodeSplit: true,
                 minify: false,
                 // Don't build in SSR mode
                 ssr: false,
             });

We are getting the expected effect. But then the HTML generation breaks down, and this is the basic logic

need @Akryum help, because now using ionic is not fully production ready.

noclat commented 1 year ago

Hi, any updates about isolating Histoire UI and global story styles? That issue defeats Histoire's purpose (sandboxing).

skstuder commented 7 months ago

Was so excited at how fast we got this working, but now I am embarrassed to show anyone how bad it looks, because the global styles leak and make Histoire look terrible.

SteinRobert commented 6 months ago
const isIframe = window.self !== window.top;
document.head
    .querySelectorAll("style[type='text/css']:not([data-vite-dev-id*='histoire'])")
    .forEach((style) => isIframe || style.remove());

Also hacky - however works for now - took this and added it to setupCode:

export default defineConfig({
  plugins: [
    HstVue(),
    HstNuxt(),
  ],
  setupCode: [`const isIframe = window.self !== window.top;
        const sandbox = window.location.pathname === '/__sandbox.html';
  document.head
      .querySelectorAll("style[type='text/css']:not([data-vite-dev-id*='histoire'])")
      .forEach((style) => (!isIframe && !sandbox) && style.remove());`]
})

This does not work with variants, since these don't use an iFrame.