diff --git a/changelog.md b/changelog.md index 126833a..b3a041f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +## V3.0.0 +This version has some breaking changes: +- drop postcss-module, as a result most of postcss-module configurations are removed as well +- remove `v2` feature flag + +Other changes: +- full support of `compose` +- code refactor +- export both `commonjs` & `es` module + ## V2.7.1 - support esbuild@^0.17 diff --git a/index.d.ts b/index.d.ts index 163c232..b544ba9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,21 +1,58 @@ import type { Plugin, PluginBuild } from 'esbuild'; declare interface BuildOptions { - /** force to build modules-css files even if `bundle` is disabled in esbuild, default is `false` */ + /** + * force to build css modules files even if `bundle` is disabled in esbuild + * @default false + */ force?: boolean; - /** inline images imported in css as data url even if `bundle` is false */ + /** + * inline images imported in css as data url even if `bundle` is false + * @default false + */ forceInlineImages?: boolean; /** * emit typescript declaration file for css modules class names * - `.css.d.ts` : emit `xxx.css.d.ts` * - `.d.css.ts` : emit `xxx.d.css.ts` (from typescript@5, see https://www.typescriptlang.org/tsconfig#allowArbitraryExtensions) * - `true` : emit both `xxx.css.d.ts` and `xxx.d.css.ts` + * @default false */ emitDeclarationFile?: boolean | '.d.css.ts' | '.css.d.ts'; + /** + * set to false to not inject generated css into page; + * @description + * if set to `true`, the generated css will be injected into `head`; + * could be a string of css selector of the element to inject into, + * e.g. + * + * ``` js + * { + * inject: '#some-element-id' + * } + * + * ``` + * the plugin will try to get `shadowRoot` of the found element, and append css to the + * `shadowRoot`, if no shadowRoot then append to the found element, if no element found then append to `document.head`. + * + * could be a function with params content & digest (return a string of js code to inject css into page), + * e.g. + * + * ```js + * { + * inject: (content, digest) => `console.log(${content}, ${digest})` + * } + * ``` + * @default false + */ inject?: boolean | string | ((css: string, digest: string) => string); + /** + * Regex to filter certain CSS files. + * @default /\.modules?\.css$/i + */ filter?: RegExp; /** - * refer to: https://github.com/parcel-bundler/parcel-css/releases/tag/v1.9.0 + * @see https://lightningcss.dev/css-modules.html#local-css-variables */ dashedIndents?: boolean; /** @@ -23,30 +60,33 @@ declare interface BuildOptions { * [name] - the base name of the CSS file, without the extension * [hash] - a hash of the full file path * [local] - the original class name + * @see https://lightningcss.dev/css-modules.html#custom-naming-patterns */ pattern?: string; /** * localsConvention - * default is `camelCaseOnly` - * **cameCase** : `.some-class-name` ==> `someClassName`, the original class name will not to be removed from the locals - * **camelCaseOnly**: `.some-class-name` ==> `someClassName`, the original class name will be removed from the locals - * **pascalCase** : `.some-class-name` ==> `SomeClassName`, the original class name will not to be removed from the locals - * **pascalCaseOnly**: `.some-class-name` ==> `SomeClassName`, the original class name will be removed from the locals + * - **cameCase** : `.some-class-name` ==> `someClassName`, the original class name will not to be removed from the locals + * - **camelCaseOnly**: `.some-class-name` ==> `someClassName`, the original class name will be removed from the locals + * - **pascalCase** : `.some-class-name` ==> `SomeClassName`, the original class name will not to be removed from the locals + * - **pascalCaseOnly**: `.some-class-name` ==> `SomeClassName`, the original class name will be removed from the locals + * @default "camelCaseOnly" */ localsConvention?: 'camelCase' | 'pascalCase' | 'camelCaseOnly' | 'pascalCaseOnly'; /** * namedExports - * @default false - * @description * e.g.: * ``` * export const someClassName = '.some-class-name__hauajsk'; * ``` + * @default false * Notes: * - `someClassName` can not be a js key word like `const`, `var` & etc. * - cannot be used with `inject` */ namedExports?: boolean; + /** + * optional package detail + */ package?: { name: string; main?: string; diff --git a/license b/license index 1fdc001..04b927c 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-present indooorsman, https://me.csser.top +Copyright (c) 2021-present indooorsman(Chuanye, Wang), https://me.csser.top Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 33860a3..0b4cc6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "esbuild-css-modules-plugin", - "version": "3.0.0-dev.20", + "version": "3.0.0", "description": "A esbuild plugin to bundle css modules into js(x)/ts(x).", "main": "./index.cjs", "module": "./index.js", diff --git a/readme.md b/readme.md index b5883a2..dbb89de 100644 --- a/readme.md +++ b/readme.md @@ -1,26 +1,26 @@ # esbuild-css-modules-plugin -[![npm version](https://img.shields.io/npm/v/esbuild-css-modules-plugin/v3-dev)](https://www.npmjs.com/package/esbuild-css-modules-plugin/v/v3-dev) +[![npm version](https://img.shields.io/npm/v/esbuild-css-modules-plugin)](https://www.npmjs.com/package/esbuild-css-modules-plugin) [![Test](https://github.com/indooorsman/esbuild-css-modules-plugin/actions/workflows/test.yml/badge.svg)](https://github.com/indooorsman/esbuild-css-modules-plugin/actions/workflows/test.yml) A esbuild plugin to bundle css modules into js(x)/ts(x). Works both with `bundle: false` and `bundle: true`. -If build with `bundle: false`, `xxx.modules.css` will be transformed to `xxx.modules.js`. +If build with `bundle: false`, `xxx.modules.css` will be transformed to `xxx.modules.css.js`. -See [`./test/test.js`](https://github.com/indooorsman/esbuild-css-modules-plugin/blob/v3/test/test.js) for examples. +See [`./test/test.js`](https://github.com/indooorsman/esbuild-css-modules-plugin/blob/main/test/test.js) for examples. ## Install ```shell -npm i -D esbuild-css-modules-plugin@v3-dev +npm i -D esbuild-css-modules-plugin ``` or ```shell -yarn add -D esbuild-css-modules-plugin@v3-dev +yarn add -D esbuild-css-modules-plugin ``` ## Usage @@ -32,78 +32,12 @@ import CssModulesPlugin from 'esbuild-css-modules-plugin'; esbuild.build({ plugins: [ CssModulesPlugin({ - /** optional, force to build modules-css files even if `bundle` is disabled in esbuild. default is `false` */ - force: false, - /** optional, inline images imported in css as data url even if `bundle` is false. default is `false` */ - forceInlineImages: false, - /** optional, generate typescript declaration file for css file to `outdir` of esbuild config. default is `false` */ - emitDeclarationFile: false, - /** - * optional - * @see https://lightningcss.dev/css-modules.html#local-css-variables - */ - dashedIndents: false, - /** - * optional, pattern of class names - * The currently supported segments are: - * [name] - the base name of the CSS file, without the extension - * [hash] - a hash of the full file path - * [local] - the original class name - * @see https://lightningcss.dev/css-modules.html#custom-naming-patterns - */ - pattern: '[name]_[local]_[hash]', - /** - * optional, localsConvention - * default is `camelCaseOnly` - * **cameCase** : `.some-class-name` ==> `someClassName`, the original class name will not to be removed from the locals - * **camelCaseOnly**: `.some-class-name` ==> `someClassName`, the original class name will be removed from the locals - * **pascalCase** : `.some-class-name` ==> `SomeClassName`, the original class name will not to be removed from the locals - * **pascalCaseOnly**: `.some-class-name` ==> `SomeClassName`, the original class name will be removed from the locals - */ - localsConvention: 'camelCase' | 'pascalCase' | 'camelCaseOnly' | 'pascalCaseOnly', - /** - * optional, enable named exports - * @default false - * @description - * e.g.: - * ``` - * export const someClassName = '.some-class-name__hauajsk'; - * ``` - * Notes: - * - `someClassName` can **NOT** be a js key word like `const`, `var` & etc. - * - can **NOT** be used with `inject` - */ - namedExports: false, - // optional, package info - package: { - name: 'my-lib', - main: 'index.cjs', - module: 'index.js', - version: '3.0.0' - }, - /** - * optional. set to false to not inject generated css into page; - * if set to `true`, the generated css will be injected into `head`; - * could be a string of css selector of the element to inject into, - * e.g. - * - * ``` - * inject: '#some-element-id' // the plugin will try to get `shadowRoot` of the found element, and append css to the - * `shadowRoot`, if no shadowRoot then append to the found element, if no element found then append to document.head - * - * ``` - * - * could be a function with params content & digest (return a string of js code to inject css into page), - * e.g. - * - * ``` - * inject: (content, digest) => `console.log(${content}, ${digest})` - * ``` - */ - inject: false, - - /** Optional. Regex to filter certain CSS files. default is `/\.modules?\.css$/i` */ - filter: /\.modules?\.css$/i + // @see https://github.com/indooorsman/esbuild-css-modules-plugin/blob/main/index.d.ts for more details + force: true, + emitDeclarationFile: true, + localsConvention: 'camelCaseOnly', + namedExports: true, + inject: false }) ] }); diff --git a/test/app.jsx b/test/app.jsx index 8afbf1a..f1fce6f 100644 --- a/test/app.jsx +++ b/test/app.jsx @@ -1,12 +1,13 @@ import React from 'react'; import ReactDom from 'react-dom'; import klass from './styles/app.modules.css'; +import klass2 from './styles/app-filter.css'; import { HelloWorld } from './components/hello.world'; const App = () => { return ( -
+
); diff --git a/test/components/filter.world.jsx b/test/components/filter.world.jsx index ab3db8f..7a287de 100644 --- a/test/components/filter.world.jsx +++ b/test/components/filter.world.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import * as styles from '../styles/app-filter.css'; -import * as styles2 from '../styles/deep/styles/hello-filter.css'; +import styles from '../styles/app-filter.css'; +import styles2 from '../styles/deep/styles/hello-filter.css'; export const HelloWorld = () => ( <> diff --git a/test/components/named-exports.world.jsx b/test/components/named-exports.world.jsx deleted file mode 100644 index 5c8241c..0000000 --- a/test/components/named-exports.world.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import * as styles from '../styles/app.modules.css'; -import * as styles2 from '../styles/deep/styles/hello.modules.css'; - -export const HelloWorld = () => ( - <> -

Hello World!

-

hi...

- -); diff --git a/test/named-exports.jsx b/test/named-exports.jsx deleted file mode 100644 index 415f41c..0000000 --- a/test/named-exports.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ReactDom from 'react-dom'; - -import { HelloWorld } from './components/named-exports.world'; - -const App = () => { - return ; -}; - -ReactDom.render(, document.body); diff --git a/test/package.json b/test/package.json index 51b0159..a70aa97 100644 --- a/test/package.json +++ b/test/package.json @@ -1,6 +1,6 @@ { "name": "esbuild-css-modules-plugin-test", - "version": "3.0.0-dev", + "version": "3.0.0", "private": true, "type": "module" } \ No newline at end of file diff --git a/test/test.js b/test/test.js index 56e74de..ebf22ce 100644 --- a/test/test.js +++ b/test/test.js @@ -2,125 +2,140 @@ import esbuild from 'esbuild'; import cssModulesPlugin from '../index.js'; (async () => { - // await esbuild.build({ - // entryPoints: { - // ['custom-entry-name']: 'app.jsx', - // ['named-exports']: 'named-exports.jsx' - // }, - // entryNames: '[name]-[hash]', - // format: 'esm', - // target: ['esnext'], - // bundle: true, - // minify: false, - // sourcemap: true, - // publicPath: 'https://my.domain/static/', - // external: ['react', 'react-dom'], - // outdir: './dist/bundle-v2-inject', - // write: true, - // loader: { - // '.jpg': 'file' - // }, - // plugins: [cssModulesPlugin({ - // inject: '#my-custom-element-with-shadow-dom', - // generateTsFile: true - // })], - // logLevel: 'debug' - // }); - // console.log('[test][esbuild:bundle:v2] done, please check `test/dist/bundle-v2-inject`', '\n'); + await esbuild.build({ + entryPoints: ['app.jsx'], + entryNames: '[name]-[hash]', + format: 'esm', + target: ['esnext'], + bundle: true, + minify: false, + publicPath: 'https://my.domain/static/', + external: ['react', 'react-dom'], + outdir: './dist/bundle-inject', + write: true, + loader: { + '.jpg': 'file' + }, + plugins: [ + cssModulesPlugin({ + inject: '#my-custom-element-with-shadow-dom', + emitDeclarationFile: true + }) + ], + logLevel: 'debug' + }); + console.log('[test][esbuild:bundle:inject] done, please check `test/dist/bundle-inject`', '\n'); - // await esbuild.build({ - // entryPoints: { - // ['custom-entry-name']: 'app.jsx', - // ['named-exports']: 'named-exports.jsx' - // }, - // entryNames: '[name]-[hash]', - // format: 'esm', - // target: ['esnext'], - // bundle: true, - // minify: false, - // sourcemap: 'external', - // publicPath: 'https://my.domain/static/', - // external: ['react', 'react-dom'], - // outdir: './dist/bundle-v2-custom-inject', - // write: true, - // loader: { - // '.jpg': 'dataurl' - // }, - // plugins: [cssModulesPlugin({ - // inject: (css, digest) => { - // return ` - // const styleId = 'style_${digest}'; - // if (!document.getElementById(styleId)) { - // const styleEle = document.createElement('style'); - // styleEle.id = styleId; - // styleEle.textContent = \`${css.replace(/\n/g, '')}\`; - // document.head.appendChild(styleEle); - // } - // ` - // } - // })], - // logLevel: 'debug' - // }); - // console.log('[test][esbuild:bundle:v2] done, please check `test/dist/bundle-v2-custom-inject`', '\n'); + await esbuild.build({ + entryPoints: { + ['custom-entry-name']: 'app.jsx' + }, + entryNames: '[name]-[hash]', + format: 'esm', + target: ['esnext'], + bundle: true, + minify: false, + publicPath: 'https://my.domain/static/', + external: ['react', 'react-dom'], + outdir: './dist/bundle-custom-inject', + write: true, + loader: { + '.jpg': 'dataurl' + }, + plugins: [ + cssModulesPlugin({ + inject: (css, digest) => { + return ` + const styleId = 'style-' + ${digest}; + if (!document.getElementById(styleId)) { + const styleEle = document.createElement('style'); + styleEle.id = styleId; + styleEle.textContent = ${css}; + document.head.appendChild(styleEle); + } + `; + } + }) + ], + logLevel: 'debug' + }); + console.log( + '[test][esbuild:bundle:custom:inject] done, please check `test/dist/bundle-custom-inject`', + '\n' + ); + + await esbuild.build({ + entryPoints: ['filter.jsx'], + entryNames: '[name]-[hash]', + format: 'esm', + target: ['esnext'], + bundle: true, + minify: false, + sourcemap: false, + publicPath: 'https://my.domain/static/', + packages: 'external', + outdir: './dist/bundle-custom-filter', + write: true, + loader: { + '.jpg': 'file' + }, + plugins: [ + cssModulesPlugin({ + inject: false, + namedExports: true, + filter: /\.css$/i + }) + ], + logLevel: 'debug' + }); + console.log('[test][esbuild:bundle:custom:filter] done, please check `test/dist/bundle-custom-filter`', '\n'); - // await esbuild.build({ - // entryPoints: ['app.jsx', 'named-exports.jsx'], - // entryNames: '[name]-[hash]', - // format: 'esm', - // target: ['esnext'], - // bundle: true, - // minify: false, - // sourcemap: false, - // publicPath: 'https://my.domain/static/', - // external: ['react', 'react-dom'], - // outdir: './dist/bundle-v2-no-inject', - // write: true, - // loader: { - // '.jpg': 'file' - // }, - // plugins: [ - // cssModulesPlugin({ - // inject: false - // }) - // ], - // logLevel: 'debug' - // }); - // console.log('[test][esbuild:bundle:v2] done, please check `test/dist/bundle-v2-no-inject`', '\n'); + await esbuild.build({ + entryPoints: ['app.jsx'], + entryNames: '[name]-[hash]', + format: 'esm', + target: ['esnext'], + bundle: true, + minify: false, + sourcemap: false, + publicPath: 'https://my.domain/static/', + external: ['react', 'react-dom'], + outdir: './dist/bundle-no-inject', + write: true, + loader: { + '.jpg': 'file' + }, + plugins: [ + cssModulesPlugin({ + inject: false, + namedExports: true + }) + ], + logLevel: 'debug' + }); + console.log('[test][esbuild:bundle:no:inject] done, please check `test/dist/bundle-no-inject`', '\n'); /** @type {import('esbuild').BuildOptions} */ const buildOptions = { entryPoints: [ - 'app.jsx', - 'components/hello.world.jsx', - 'styles/app.modules.css', - 'styles/deep/styles/hello.modules.css' + './app.jsx', + './components/hello.world.jsx', + './styles/**/*.modules.css', ], entryNames: '[dir]/[name]', assetNames: '[dir]/[name]', format: 'esm', target: ['esnext'], bundle: false, - // external: ['react', 'react-dom'], minify: false, sourcemap: false, - publicPath: 'https://my.cdn/static/', - outdir: './dist/bundle-v3', + outdir: './dist/no-bundle', write: true, - loader: { - '.jpg': 'file' - }, plugins: [ cssModulesPlugin({ - // force: true, - // namedExports: true - // inject: true - // inject: (css, digest) => { - // return `console.log(${css}, ${digest});` - // }, emitDeclarationFile: true, - // forceInlineImages: true, force: true, - // namedExports: true, + forceInlineImages: true, inject: '#my-styles-container' }) ], @@ -128,7 +143,5 @@ import cssModulesPlugin from '../index.js'; }; await esbuild.build(buildOptions); - // const ctx = await esbuild.context(buildOptions); - // await ctx.watch(); - console.log('[test][esbuild:bundle:v3] done, please check `test/dist/bundle-v3`', '\n'); + console.log('[test][esbuild:no:bundle] done, please check `test/dist/no-bundle`', '\n'); })(); diff --git a/tsconfig.json b/tsconfig.json index 28b9717..6410e8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "target": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, + "allowArbitraryExtensions": true, "esModuleInterop": true }, "include": [