Snowpack: Allow option for CSS to be exported as string

Created on 7 Jul 2020  Â·  13Comments  Â·  Source: snowpackjs/snowpack

Original Discussion: https://www.pika.dev/npm/snowpack/discuss/445
/cc @afberg, @drwpow


Current behavior

Currently, Snowpack allows the following syntax:

import './styles.css';

This creates a styles.proxy.css.js file which takes the contents of styles.css and appends it to the <head> of the HTML.

Desired behavior

Allow for an alternate syntax:

import styles from './styles.css';

This would NOT inject the styles into <head>, rather, only create a default export that exposes the contents of styles.css as a string for use in JS.

good first issue help wanted

Most helpful comment

This issue appears to be related to #206

I would like to use SnowPack for a LitElement based project but there is no way to load external SCSS/CSS as raw text. In Rollup I could compile the following Typescript:

import styleCSS from './mystyle.scss';
const style = css([`${styleCSS}`] as any)

using a Rollup configuration of

import postcss from 'rollup-plugin-postcss';
...
 postcss({
            inject: false,
            modules: false,
            use: ['sass'],
        })

but this same configuration doesn't work with Snowpack because it greedly processes the import statements before the Rollup plugin can process the source files.

While the .proxy.js document.head.appendChild(styleEl) style inclusion may be convenient for many kinds of web apps it is ineffective for any application using the Shadow DOM which ignores light DOM document styling.

If the built in Snowpack CSS import functionality cannot be customized it would be great if it could be disabled all together so that a build plugin could replace it.

All 13 comments

Hmmm we would have to be careful since using CSS Modules lib, I would expect this to be an object.

we would have to be careful since using CSS Modules lib, I would expect this to be an object

Good point. Though is that something Snowpack can even do now? I was under the impression we couldn’t. But that’s something I’d love to have because I’m a big fan of CSS Modules.

When we were thinking about improvements to the plugin system, now plugins can return an object with all the files like so:

{
  ".module.css": "// the CSS code",
  ".proxy.module.css.js": "// the JS import",
  ".d.ts": "// the TypeScript definition for the modules",
}

For example, if the original file was styles.module.css, this plugin would export 3 files: styles.module.css, styles.proxy.module.css.js, and styles.d.ts (all in the same folder for simplicity).

But the thing a plugin _can’t_ do is tell Snowpack to replace the import in JS with that .proxy.module.css.js path. Currently we just handle that manually, but in the new plugin world, the CSS Module stuff would be handled in the plugin and that .proxy.module.css.js reference created would be that named export object with CSS Module classnames.

What are your thoughts on a way to tell Snowpack that the .proxy.module.css.js file is the one to replace the import with? Or should Snowpack just assume to replace imports because of that .js ending extension in the plugin output? Does this make any sense? 😅

Actually maybe this will be simpler: should we handle CSS Modules with Option A or B (or propose an Option C)?

Option A

The new plugin API:

{
   input: ['.module.css'], // this is a CSS Modules plugin that only cares about these files
   output: ['.module.css', '.proxy.module.css.js', '.d.ts'],
+  import: '.proxy.module.css.js' // <- new key to tell Snowpack to update imports
   build(…) { /* the plugin stuff */ }
}

Don’t care about the import name itself; just as a general concept.

Option B

We don’t change the plugin API at all; Snowpack just uses the first *.js plugin output if it finds one, and updates imports if necessary. If there is no *.js output, Snowpack does nothing.

I think either of these solutions would be new behavior, but I think it would at least provide a way forward to allow for CSS Modules.

tbh, I'm pretty nervous about this one! Lets land the new plugins, let them bake a bit, and then revisit.

In general, we need to be very careful about mixing the concept of "how a file is built from source -> output" and "how a file is loaded" (aka "import proxies"). These are two different things, even if they feel similar.

Right now, Snowpack fully owns the "import proxy" experience. It is not at all customizable. Lots of internal logic relies on it's exact behavior, so I also feel strongly that this shouldn't be completely customizable. A good compromise would be to let developers fall back to a raw-text import for any proxy type. Something like:

import './styles.css';
import rawCss from './styles.css?loader=text';

Or, let users override our default behavior via config:

// note: this feels a bit like Webpack's loader config
assetImportLoader: {
  ".css": "text"
}

This issue appears to be related to #206

I would like to use SnowPack for a LitElement based project but there is no way to load external SCSS/CSS as raw text. In Rollup I could compile the following Typescript:

import styleCSS from './mystyle.scss';
const style = css([`${styleCSS}`] as any)

using a Rollup configuration of

import postcss from 'rollup-plugin-postcss';
...
 postcss({
            inject: false,
            modules: false,
            use: ['sass'],
        })

but this same configuration doesn't work with Snowpack because it greedly processes the import statements before the Rollup plugin can process the source files.

While the .proxy.js document.head.appendChild(styleEl) style inclusion may be convenient for many kinds of web apps it is ineffective for any application using the Shadow DOM which ignores light DOM document styling.

If the built in Snowpack CSS import functionality cannot be customized it would be great if it could be disabled all together so that a build plugin could replace it.

Thanks for the feedback! After thinking about it a bit more, our new plugin system should actually support this already via either:

  • *.css - Use a Snowpack plugin to build all CSS files in your project to JS as a single default export
  • *.export.css - Alternatively, use a Snowpack plugin to build only a certain set of CSS files that match this file name (so that normal .css files are handled normally).

An official PostCSS plugin should be able to handle this by default.

Also @drwpow we support CSS Modules today!

I think we have a solution outlined here that does a good job of decoupling "how do I want this non-JS file to be imported into JS" from "how do I want to build this file". Will continue the discussion there.

The proxy() hook solution was removed from #647 and the recommendation to use a load/resolve hook is inaccurate since the wrapImportProxy functionality greedily intercepts all.css file import references and injects a proxy.

Will there be another attempt at delivering a long term solution for inlining CSS string content into Snowpack builds?

The current way to do this would be to define a CSS load() plugin, which converted CSS to the JS module format of your choice. If this doesn't exist as a community plugin (or even an official plugin) it should and we'd love help with it! Here's the basic plugin pseudo-implementation:

// plugin.js
module.exports = function plugin(snowpackConfig, options = {}) {

  return {
    name: 'plugin-css-export-string',
    resolve: {
      input: options.input || ['.css'],
      output: ['.js'],
    },
    async load({filePath}) {
      const contents = await fs.readFile(filePath, 'utf-8');
      return `export default ${JSON.stringify(contents)};`;
    },
  };
};

This would work in both dev and build, by converting the CSS file completely to a JS file in your final built app.

The limitation of this system is that only one plugin could claim the CSS file, so you couldn't use this plugin in combination with another load plugin like PostCSS.

It sounds like this use-case is common enough, maybe we want to build it into Snowpack directly, no plugin needed? Happy to re-open to discuss.

so, how to do that now, with postcss support? Please reopen - this is critical for web-components.

@FredKSchott I tried to get your pseudo-code above to work with postcss but failed. I could get a simple css file loaded and exported as a string (after changing it to return {".js": { code: ``export default ${JSON.stringify(contents)};``}}, otherwise snowpack would replace the import with .css.proxy.js and this would resolve to the old proxy implementation). Without postCSS or something similar available it's not really useful though, as constructable style sheets don't allow @import as far as I know. I'd second that this should be re-opened.

@11111000000 I made a hack like this now
````js
const fs = require("fs").promises;

const postcss = require("postcss");
const postcssEasyImport = require("postcss-easy-import");
const cssnano = require("cssnano")({
preset: "default",
});

module.exports = function plugin() {
return {
name: "plugin-css-string-loader",
resolve: {
input: [".css"],
output: [".js"],
},
load: async ({ filePath }) => {
const input = await fs.readFile(filePath, { encoding: "utf-8" });
const result = await postcss([postcssEasyImport(), cssnano]).process(
input,
{
from: filePath,
to: filePath,
map: { inline: true },
}
);
return {
".js": { code: export default ${JSON.stringify(result.css)}; },
};
},
};
};
````

Mention it in your snowpack.config.js:

json plugins: [ "@snowpack/plugin-typescript", "./cssloader.cjs", ....

Seems to work in dev+build now. I'm using it like this:
ts import { unsafeCSS } from "lit-element"; import cssString from "./global_styles.css"; export const globalStyles = unsafeCSS(cssString); if (globalStyles.styleSheet !== null) { document.adoptedStyleSheets = [globalStyles.styleSheet]; }

Then I can import globalStyles in my components:

````ts
import { customElement, html, LitElement, TemplateResult } from "lit-element";
import { globalStyles } from "./style";

@customElement("app-root")
export class Root extends LitElement {
static styles = [globalStyles];
....
````

I added these typings in types/static.d.ts:
````
declare module "*.css";
interface CSSStyleSheet {
replace(text: string): Promise;
replaceSync(text: string): void;
}

interface Document {
adoptedStyleSheets: readonly CSSStyleSheet[];
}

interface ShadowRoot {
adoptedStyleSheets: readonly CSSStyleSheet[];
}
````

If you don't want to use adoptedStyleSheets the *.css line is enough. (And remove the document.adoptedStyleSheet line above). I'm trying to share StyleSheets between lightdom and shadowdoms to reduce overhead.

Just using postCSS for bundling my @imports together, but this could be easily adapted to do SASS, for example.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

daniele-orlando picture daniele-orlando  Â·  6Comments

backspaces picture backspaces  Â·  5Comments

FredKSchott picture FredKSchott  Â·  6Comments

gr2m picture gr2m  Â·  6Comments

wenyanqi picture wenyanqi  Â·  5Comments