Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document how to declare a PostCSS plugin in an ES6/TypeScript module #1771

Open
nex3 opened this issue Aug 18, 2022 · 22 comments
Open

Document how to declare a PostCSS plugin in an ES6/TypeScript module #1771

nex3 opened this issue Aug 18, 2022 · 22 comments

Comments

@nex3
Copy link
Contributor

nex3 commented Aug 18, 2022

It's currently unclear how to properly declare a PostCSS module using ES6 module syntax (which TypeScript also uses idiomatically). The suggested module.exports isn't idiomatic in these systems.

@ai
Copy link
Member

ai commented Aug 19, 2022

let plugin = () => { … }
plugin.postcss = true

export default postcss

Where do you think we should change docs?

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

That was the first thing I tried, but it doesn't work if you're loading it from TypeScript:

 FAIL  test/index.test.ts
  ● Test suite failed to run

    test/index.test.ts:22:25 - error TS2349: This expression is not callable.
      Type 'typeof import("/home/nweiz/goog/pkg/js/postcss-src/build/index")' has no call signatures.

    22   return await postcss([plugin(options)]).process(input, {from: undefined});
                               ~~~~~~

I also tried

const plugin: PluginCreator = (opts: Options = {}): Plugin => {  };
plugin.postcss = true;

export = plugin;

but this produced

src/index.ts:80:1 - error TS2309: An export assignment cannot be used in a module with other exported elements.                       

80 export = plugin;
   ~~~~~~~~~~~~~~~~

So far, this is the best I've been able to manage:

const plugin: PluginCreator = (opts: Options = {}): Plugin => {  };

export = plugin;

...but this doesn't include the postcss flag, which I assume is desirable.

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

It's worth noting that the export default syntax also doesn't work with postcss-cli:

$ npx postcss-cli -u ../../build/index.js < test.css

> postcss-src@1.0.0 compile
> tsc -p .

Plugin Error (../../build/index.js): TypeError: (intermediate value).default is not a function'

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

Okay, I've found an incantation that works:

// eslint-disable-next-line @typescript-eslint/no-namespace
namespace plugin {
  export interface Options {  }
}

const plugin: PluginCreator<olugin.Options> = (opts = {}) => {  };
plugin.postcss = true;

export = plugin;

It would be nice if there were an alternative way to declare these plugins that was more module-system friendly—allowing the PluginCreator to be defined as the top-level postcss field, for example.

@ai
Copy link
Member

ai commented Aug 19, 2022

These problems are coming from ESM support in TS.

  1. Try export const plugin = () => {} (TS has a problem with default export)
  2. Another way is to be sure that TS generate true and native ESM without any hacks from TS

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

These problems are coming from ESM support in TS.

Sure, no argument there, but that's not really something we can avoid at least until native ESM is widely used everywhere.

  1. Try export const plugin = () => {} (TS has a problem with default export)

This just exports a value with the name plugin, rather than setting the module object's value.

  1. Another way is to be sure that TS generate true and native ESM without any hacks from TS

If I do that I'll prevent anyone but other ESM users from using my plugin.

@ai
Copy link
Member

ai commented Aug 19, 2022

If I do that I'll prevent anyone but other ESM users from using my plugin.

There is no way to have dual CJS/ESM package solution which will work in 100% cases and use default export. You must move to named export.

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

I would happily use named exports—but I'm also trying to make an idiomatic PostCSS plugin 😅. That's why I'm asking for PostCSS to accept a plugin that uses named exports.

@ai
Copy link
Member

ai commented Aug 19, 2022

I think it is better to use CJS for plugins rather adding named export to JSON config loading. I have no idea about good API for named export.

But you can use named export in JS format of config.

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

But, as discussed, the CJS plugin format is difficult and messy to use correctly with TypeScript.

I have no idea about good API for named export.

What about this: if a module defines a member named postcss where typeof postcss === 'function', you treat that member as a PluginCreator.

But you can use named export in JS format of config.

When I'm writing a PostCSS plugin, only having it work for users who load plugins with JS and not for users who load plugins via JSON or postcss-cli just isn't acceptable UX—it might as well be forbidden.

@ai
Copy link
Member

ai commented Aug 19, 2022

What about this: if a module defines a member named postcss where typeof postcss === 'function', you treat that member as a PluginCreator.

import a from "module"

a will not be an object with all module named exports. You need load a module in a special manner.

Also the same { postcss } export will make it more ugly for JS based configs.

import { postcss as postcssNested } from 'postcss-nested`

I rather prefer to wait a little when TS will fix ESM support (will avoid dirty hacks).

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

Are you sure TypeScript is ever going to fix this? Do they even think of this as a bug? My understanding from reading through documentation around this is that ESM's export default is explicitly and intentionally not one-to-one compatible with CJS's export =. What if you're painting the PostCSS API into a corner where it'll never work well with ESM?

@ai
Copy link
Member

ai commented Aug 19, 2022

I am dreaming about the future when TS of exports default return ESM code with exports default without a hacky wrapper.

@nex3
Copy link
Contributor Author

nex3 commented Aug 19, 2022

That's not some far-off future; I can do that today by setting "module": "es6" in my tsconfig.json. But it's not practical to do so because ES6 modules can't be consumed by CJS projects.

The issue here isn't that TypeScript decided to do something weird and idiosyncratic with ES6 syntax; the issue is that the incompatibility between CJS and native-ES6 forces users to transpile until there's a critical mass of ES6 authors out there that it's viable to ship packages that only work for them.

The good news is that you can make that world a little bit closer by making it easier to use transpiled-ES6 with PostCSS. If you don't want to support a top-level postcss function, you could instead check for module.__esModule and, if that's set, use module.default—which is what the ES6-to-CJS polyfill sets for export default.

@ai
Copy link
Member

ai commented Aug 19, 2022

If you don't want to support a top-level postcss function, you could instead check for module.__esModule and, if that's set, use module.default—which is what the ES6-to-CJS polyfill sets for export default.

Hm. Cool idea.

I will look how to implement it.

@ai
Copy link
Member

ai commented Aug 31, 2022

@nex3 Can you test this fix? #1773

@romainmenke
Copy link
Contributor

I am no expert on ESM or CJS but have been following this thread and the pull request because I want to handle this correctly for all plugins we author from https://github.com/csstools/postcss-plugins

For some time we have been using conditional exports and rollup.
We author the plugins as ESM but everything is fed through rollup : https://github.com/csstools/postcss-plugins/blob/main/rollup/presets/package-typescript.js#L12-L13

We also have some tests for these :

We don't currently have tests for Typescript, but I've manually verified this and seems to work. Same for passing plugin options.

Afaik these are true:

  • users can use esm to import and run PostCSS + plugins we create.
  • users can use typescript to import and run PostCSS + plugins we create.
  • users can use commonjs to require and run PostCSS + plugins we create.

Are we affected by this issue but not aware?
Or are we doing something different?

As stated above I am no expert, so the steps I take to verify that it works are limited by my knowledge of ESM and CJS.

@nex3
Copy link
Contributor Author

nex3 commented Nov 3, 2022

@romainmenke It looks like the CJS blob you're distributing is set up not to cause CJS users problems. That does raise the question, though: why even bother distributing ESM, when ESM users could just as easily import your CJS blob?

@romainmenke
Copy link
Contributor

That was from before my time as a maintainer of these plugins so I would have to start digging through old commits.

Most likely this happened :

  • someone had an issue with some plugin in esm, deno, webpack, ...
  • conditional exports were setup with the dual distibution of esm and cjs
  • someone fixed the actual issue by changing/adding the rollup config

I don't dare removing it now :)

@elchininet
Copy link
Contributor

Hi @nex3,

I have just read the thread, and maybe you already solved your issue since then but perhaps I can apport my two cents here. I would not recommend a named export, there are many tools/frameworks in which you need to specify the postcss plugins as strings and they are imported behind the scenes, that will not work if the plugin is exported as a named export. I was exporting postcss-rtlcss as a named export before and I changed it just to support frameworks like NextJS.

If you are still dealing with that, try a setup similar to the one that I created in postcss-rtlcss, maybe it works in your use case.

Regards

undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Aug 17, 2023
Configure project to use ES6 modules to enable top-level await
capabilities. This change helps project to align well with modern JS
standards.

- Set `type` to `module` in `package.json`.
- Use import/export syntax in Cypress configuration files.
- Rename configurations files that do not support modules to use
  the `.cjs` extension:
  - `vue.config.js` to `vue.config.cjs` (vuejs/vue-cli#4477).
  - `babel.config.js` to `babel.config.cjs (babel/babel-loader#894)
  - `.eslintrc.js` to `.eslintrc.cjs` (eslint/eslint#13440,
    eslint/eslint#14137)
  - `postcss.config.js` to `postcss.config.cjs` (postcss/postcss#1771)
undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Aug 17, 2023
Configure project to use ES6 modules to enable top-level await
capabilities. This change helps project to align well with modern JS
standards.

- Set `type` to `module` in `package.json`.
- Use import/export syntax in Cypress configuration files.
- Rename configurations files that do not support modules to use
  the `.cjs` extension:
  - `vue.config.js` to `vue.config.cjs` (vuejs/vue-cli#4477).
  - `babel.config.js` to `babel.config.cjs (babel/babel-loader#894)
  - `.eslintrc.js` to `.eslintrc.cjs` (eslint/eslint#13440,
    eslint/eslint#14137)
  - `postcss.config.js` to `postcss.config.cjs` (postcss/postcss#1771)
- Provide a workaround for Vue CLI & Mocha ES6 modules conflict in
  Vue configuration file (vuejs#7417).
undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Aug 17, 2023
Configure project to use ES6 modules to enable top-level await
capabilities. This change helps project to align well with modern JS
standards.

- Set `type` to `module` in `package.json`.
- Use import/export syntax in Cypress configuration files.
- Rename configurations files that do not support modules to use
  the `.cjs` extension:
  - `vue.config.js` to `vue.config.cjs` (vuejs/vue-cli#4477).
  - `babel.config.js` to `babel.config.cjs (babel/babel-loader#894)
  - `.eslintrc.js` to `.eslintrc.cjs` (eslint/eslint#13440,
    eslint/eslint#14137)
  - `postcss.config.js` to `postcss.config.cjs` (postcss/postcss#1771)
- Provide a workaround for Vue CLI & Mocha ES6 modules conflict in
  Vue configuration file (vuejs/vue-cli#7417).
LarrMarburger added a commit to LarrMarburger/privacy.sexy that referenced this issue Nov 16, 2023
Configure project to use ES6 modules to enable top-level await
capabilities. This change helps project to align well with modern JS
standards.

- Set `type` to `module` in `package.json`.
- Use import/export syntax in Cypress configuration files.
- Rename configurations files that do not support modules to use
  the `.cjs` extension:
  - `vue.config.js` to `vue.config.cjs` (vuejs/vue-cli#4477).
  - `babel.config.js` to `babel.config.cjs (babel/babel-loader#894)
  - `.eslintrc.js` to `.eslintrc.cjs` (eslint/eslint#13440,
    eslint/eslint#14137)
  - `postcss.config.js` to `postcss.config.cjs` (postcss/postcss#1771)
- Provide a workaround for Vue CLI & Mocha ES6 modules conflict in
  Vue configuration file (vuejs/vue-cli#7417).
sungik-choi added a commit to channel-io/bezier-react that referenced this issue Jan 9, 2024
…1887)

<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English** and added an appropriate
**label** to the PR.
- [x] I wrote the commit message in **English** and to follow [**the
Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] I [added the
**changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
about the changes that needed to be released. (or didn't have to)
- [x] I wrote or updated **documentation** related to the changes. (or
didn't have to)
- [x] I wrote or updated **tests** related to the changes. (or didn't
have to)
- [x] I tested the changes in various browsers. (or didn't have to)
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox

## Related Issue
<!-- Please link to issue if one exists -->

- Fixes #1884

## Summary
<!-- Please brief explanation of the changes made -->

Enchance to add `@layer components` at-rule in CSS post-processing

## Details
<!-- Please elaborate description of the changes -->

- 커스텀 PostCSS plugin을 통해 CSS를 갈아끼우는 방식으로 구현했습니다. 구현은 단순합니다.
- 불필요해진 *.module.scss의 `@layer components` at-rule을 제거했습니다.
- `*.module.scss` + `components` 디렉토리 내부에 위치한 파일만 처리하도록 구현했습니다. 여기엔
layout, margin props도 포함되니 이후 디렉터리명 변경에 유의해야합니다.
- 이전/이후 빌드된 CSS 파일이 동일한 것을 확인했습니다.

### Breaking change? (Yes/No)
<!-- If Yes, please describe the impact and migration path for users -->

No

## References
<!-- Please list any other resources or points the reviewer should be
aware of -->

- https://postcss.org/docs/writing-a-postcss-plugin
- 공식 홈페이지는 CJS인데, ESM으로 만들기 위해 참고:
postcss/postcss#1771
@IanVS
Copy link

IanVS commented Mar 19, 2024

Soooo, does anyone know of any docs / blogs about how to author idiomatic postcss plugins using TypeScript? I haven't come across anything from my searches so far, does everyone just write their plugins in CJS JavaScript?

@romainmenke
Copy link
Contributor

You can check out how we do it here : https://github.com/csstools/postcss-plugins

summary:

  • write as modern TypeScript
  • use rollup to generate commonjs and es modules
  • generate bundled types only for es modules

Most basic plugin : https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-base-plugin

Rollup stuff : https://github.com/csstools/postcss-plugins/tree/main/rollup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants