From 6abbf371319ddabf390beab870cca66cbd31ab14 Mon Sep 17 00:00:00 2001 From: dungjk Date: Fri, 26 Apr 2024 15:04:33 +0700 Subject: [PATCH 1/2] feat: Add support namedExports --- README.md | 36 +++++++++++++++++++ package.json | 5 +-- src/index.ts | 6 ++++ src/plugin.ts | 20 +++++++++-- src/utils.ts | 8 +++++ test/e2e.test.ts | 32 ++++++++++++++++- test/fixture/named-exports/build.js | 23 ++++++++++++ test/fixture/named-exports/index.html | 11 ++++++ test/fixture/named-exports/package.json | 14 ++++++++ .../named-exports/src/common.module.scss | 3 ++ .../named-exports/src/example.module.scss | 5 +++ test/fixture/named-exports/src/index.js | 7 ++++ yarn.lock | 5 +++ 13 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 test/fixture/named-exports/build.js create mode 100644 test/fixture/named-exports/index.html create mode 100644 test/fixture/named-exports/package.json create mode 100644 test/fixture/named-exports/src/common.module.scss create mode 100644 test/fixture/named-exports/src/example.module.scss create mode 100644 test/fixture/named-exports/src/index.js diff --git a/README.md b/README.md index e91530a..d4e685e 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,42 @@ In order for `quietDeps` to correctly identify external dependencies the `url` o > The `url` option creates problems when importing source SASS files from 3rd party modules in which case the best workaround is to avoid `quietDeps` and [mute the logger](https://sass-lang.com/documentation/js-api/interfaces/StringOptionsWithImporter#logger) if that's a big issue. +### namedExports + +Type: `boolean` `function`
+Default: `false` + +Use named exports alongside default export. + +You can supply a function to control how exported named is generated: + +```js +namedExports(name) { + // Maybe you simply want to convert dash to underscore + return name.replace(/-/g, '_') +} +``` + +If you set it to `true`, the following will happen when importing specific classNames: + +- dashed class names will be transformed by replacing all the dashes to `$` sign wrapped underlines, eg. `--` => `$__$` +- js protected names used as your style class names, will be transformed by wrapping the names between `$` signs, eg. `switch` => `$switch$` + +All transformed names will be logged in your terminal like: + +```bash +Exported "new" as "$new$" in test/fixtures/named-exports/style.css +``` + +The original will not be removed, it's still available on `default` export: + +```js +import style, { class$_$name, class$__$name, $switch$ } from './style.css' +console.log(style['class-name'] === class$_$name) // true +console.log(style['class--name'] === class$__$name) // true +console.log(style['switch'] === $switch$) // true +``` + ### pnpm There's a working example of using `pnpm` with `@material` design diff --git a/package.json b/package.json index f557ec1..eff1a8a 100755 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "resolve": "^1.22.8", + "safe-identifier": "^0.4.2", "sass": "^1.71.1" }, "devDependencies": { @@ -52,10 +53,10 @@ "postcss": "^8.4.35", "postcss-modules": "^6.0.0", "postcss-url": "^10.1.3", + "sass-embedded": "^1.71.1", "source-map": "^0.7.4", "ts-node": "^10.9.2", - "typescript": "^5.3.3", - "sass-embedded": "^1.71.1" + "typescript": "^5.3.3" }, "peerDependencies": { "esbuild": ">=0.20.1", diff --git a/src/index.ts b/src/index.ts index f0b38ce..14f2ae6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import {StringOptions} from 'sass' import {sassPlugin} from './plugin' export type Type = 'css' | 'local-css' | 'style' | 'css-text' | 'lit-css' | ((cssText: string, nonce?: string) => string) +export type NamedExport = boolean | ((name: string) => string); export type SassPluginOptions = StringOptions<'sync'|'async'> & { @@ -81,6 +82,11 @@ export type SassPluginOptions = StringOptions<'sync'|'async'> & { * To enable the sass-embedded compiler */ embedded?: boolean + + /** + * Use named exports alongside default export. + */ + namedExports?: NamedExport; } export default sassPlugin diff --git a/src/plugin.ts b/src/plugin.ts index 5f1b57e..3dd3bc5 100755 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,7 @@ import {OnLoadResult, Plugin} from 'esbuild' import {dirname} from 'path' import {SassPluginOptions} from './index' -import {getContext, makeModule, modulesPaths, parseNonce, posixRelative, DEFAULT_FILTER} from './utils' +import {getContext, makeModule, modulesPaths, parseNonce, posixRelative, DEFAULT_FILTER, ensureClassName} from './utils' import {useCache} from './cache' import {createRenderer} from './render' @@ -106,8 +106,24 @@ export function sassPlugin(options: SassPluginOptions = {}): Plugin { errors: [{text: `unsupported type '${type}' for postCSS modules`}] } } + + let exportConstants = ""; + if (options.namedExports && pluginData.exports) { + const json = JSON.parse(pluginData.exports); + const getClassName = + typeof options.namedExports === "function" + ? options.namedExports + : ensureClassName; + Object.keys(json).forEach((name) => { + const newName = getClassName(name); + exportConstants += `export const ${newName} = ${JSON.stringify( + json[name] + )};\n`; + }); + } + return { - contents: `${contents}export default ${pluginData.exports};`, + contents: `${contents}${exportConstants}export default ${pluginData.exports};`, loader: 'js', resolveDir, watchFiles: [...watchFiles, ...(out.watchFiles || [])], diff --git a/src/utils.ts b/src/utils.ts index 1cccdc3..f33aa24 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,7 @@ import {Syntax} from 'sass' import {parse, relative, resolve} from 'path' import {existsSync} from 'fs' import {SyncOpts} from 'resolve' +import {identifier} from 'safe-identifier' const cwd = process.cwd() @@ -226,3 +227,10 @@ export function createResolver(options: SassPluginOptions = {}, loadPaths: strin } } } + +const escapeClassNameDashes = (name: string) => + name.replace(/-+/g, (match) => `$${match.replace(/-/g, "_")}$`); +export const ensureClassName = (name: string) => { + const escaped = escapeClassNameDashes(name); + return identifier(escaped); +}; \ No newline at end of file diff --git a/test/e2e.test.ts b/test/e2e.test.ts index bdbaa28..0d8267a 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -261,7 +261,7 @@ describe('e2e tests', function () { }) }) ] - }) + }); const bundle = readTextFile('./out/index.js') @@ -280,6 +280,36 @@ describe('e2e tests', function () { `) }) + it('named exports', async function () { + const options = useFixture('named-exports') + + await esbuild.build({ + ...options, + entryPoints: ['./src/index.js'], + outdir: './out', + bundle: true, + format: 'esm', + plugins: [ + sassPlugin({ + transform: postcssModules({ + localsConvention: 'camelCaseOnly' + }), + namedExports: (name) => { + return `${name.replace(/-/g, "_")}`; + }, + }) + ] + }); + + const bundle = readTextFile('./out/index.js') + + expect(bundle).to.containIgnoreSpaces('class="${message} ${message2}') + + expect(bundle).to.containIgnoreSpaces(`var message = "_message_1vmzm_1"`) + + expect(bundle).to.containIgnoreSpaces(`var message2 = "_message_bxgcs_1";`) + }) + it('css modules & lit-element together', async function () { const options = useFixture('multiple') diff --git a/test/fixture/named-exports/build.js b/test/fixture/named-exports/build.js new file mode 100644 index 0000000..f46aca6 --- /dev/null +++ b/test/fixture/named-exports/build.js @@ -0,0 +1,23 @@ +const esbuild = require('esbuild') +const {sassPlugin, postcssModules} = require('../../../lib') +const {cleanFixture, logSuccess, logFailure} = require('../utils') + +cleanFixture(__dirname) + +esbuild.build({ + entryPoints: ['./src/index.js'], + outdir: './out', + bundle: true, + format: 'esm', + plugins: [ + sassPlugin({ + transform: postcssModules({ + generateScopedName: '[hash:base64:8]--[local]', + localsConvention: 'camelCaseOnly' + }), + namedExports: (name) => { + return `${name.replace(/-/g, "_")}`; + }, + }) + ] +}).then(logSuccess, logFailure) \ No newline at end of file diff --git a/test/fixture/named-exports/index.html b/test/fixture/named-exports/index.html new file mode 100644 index 0000000..e3ed70a --- /dev/null +++ b/test/fixture/named-exports/index.html @@ -0,0 +1,11 @@ + + + + + Bootstrap Example + + + + + + \ No newline at end of file diff --git a/test/fixture/named-exports/package.json b/test/fixture/named-exports/package.json new file mode 100644 index 0000000..847492a --- /dev/null +++ b/test/fixture/named-exports/package.json @@ -0,0 +1,14 @@ +{ + "name": "css-modules-fixture", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.12", + "postcss": "^8.4.18", + "postcss-modules": "^5.0.0" + }, + "scripts": { + "build": "node ./build", + "serve": "node ../serve css-modules" + } +} diff --git a/test/fixture/named-exports/src/common.module.scss b/test/fixture/named-exports/src/common.module.scss new file mode 100644 index 0000000..2825bde --- /dev/null +++ b/test/fixture/named-exports/src/common.module.scss @@ -0,0 +1,3 @@ +.message { + font-family: Roboto, sans-serif; +} \ No newline at end of file diff --git a/test/fixture/named-exports/src/example.module.scss b/test/fixture/named-exports/src/example.module.scss new file mode 100644 index 0000000..826dd30 --- /dev/null +++ b/test/fixture/named-exports/src/example.module.scss @@ -0,0 +1,5 @@ +.message { + color: white; + background-color: red; + font-size: 24px; +} \ No newline at end of file diff --git a/test/fixture/named-exports/src/index.js b/test/fixture/named-exports/src/index.js new file mode 100644 index 0000000..ec5e3a8 --- /dev/null +++ b/test/fixture/named-exports/src/index.js @@ -0,0 +1,7 @@ +import { message as stylesMessage } from "./example.module.scss"; +import { message as commonMessage } from "./common.module.scss"; + +document.body.insertAdjacentHTML( + "afterbegin", + `
Hello World
`, +); diff --git a/yarn.lock b/yarn.lock index db1b772..c80c8ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1524,6 +1524,11 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-identifier@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" + integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== + safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" From 6dba8bd4505d4ec046914be2b2e7cddff22cf240 Mon Sep 17 00:00:00 2001 From: dungjk Date: Mon, 27 May 2024 17:52:06 +0700 Subject: [PATCH 2/2] style: update coding style to align with source code conventions --- src/index.ts | 4 ++-- src/plugin.ts | 8 ++++---- src/utils.ts | 6 +++--- test/fixture/named-exports/build.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 14f2ae6..edf92e4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import {StringOptions} from 'sass' import {sassPlugin} from './plugin' export type Type = 'css' | 'local-css' | 'style' | 'css-text' | 'lit-css' | ((cssText: string, nonce?: string) => string) -export type NamedExport = boolean | ((name: string) => string); +export type NamedExport = boolean | ((name: string) => string) export type SassPluginOptions = StringOptions<'sync'|'async'> & { @@ -86,7 +86,7 @@ export type SassPluginOptions = StringOptions<'sync'|'async'> & { /** * Use named exports alongside default export. */ - namedExports?: NamedExport; + namedExports?: NamedExport } export default sassPlugin diff --git a/src/plugin.ts b/src/plugin.ts index 3dd3bc5..7838fc4 100755 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -109,17 +109,17 @@ export function sassPlugin(options: SassPluginOptions = {}): Plugin { let exportConstants = ""; if (options.namedExports && pluginData.exports) { - const json = JSON.parse(pluginData.exports); + const json = JSON.parse(pluginData.exports) const getClassName = typeof options.namedExports === "function" ? options.namedExports - : ensureClassName; + : ensureClassName Object.keys(json).forEach((name) => { const newName = getClassName(name); exportConstants += `export const ${newName} = ${JSON.stringify( json[name] - )};\n`; - }); + )};\n` + }) } return { diff --git a/src/utils.ts b/src/utils.ts index f33aa24..0cb2f4f 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -229,8 +229,8 @@ export function createResolver(options: SassPluginOptions = {}, loadPaths: strin } const escapeClassNameDashes = (name: string) => - name.replace(/-+/g, (match) => `$${match.replace(/-/g, "_")}$`); + name.replace(/-+/g, (match) => `$${match.replace(/-/g, "_")}$`) export const ensureClassName = (name: string) => { - const escaped = escapeClassNameDashes(name); - return identifier(escaped); + const escaped = escapeClassNameDashes(name) + return identifier(escaped) }; \ No newline at end of file diff --git a/test/fixture/named-exports/build.js b/test/fixture/named-exports/build.js index f46aca6..15ae5bc 100644 --- a/test/fixture/named-exports/build.js +++ b/test/fixture/named-exports/build.js @@ -16,7 +16,7 @@ esbuild.build({ localsConvention: 'camelCaseOnly' }), namedExports: (name) => { - return `${name.replace(/-/g, "_")}`; + return `${name.replace(/-/g, "_")}` }, }) ]