Skip to content

Commit

Permalink
feat: Add support namedExports
Browse files Browse the repository at this point in the history
  • Loading branch information
dungjk committed May 27, 2024
1 parent e6519ee commit 2e5dedc
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 6 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`<br>
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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"dependencies": {
"resolve": "^1.22.8",
"safe-identifier": "^0.4.2",
"sass": "^1.71.1"
},
"devDependencies": {
Expand All @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'> & {

Expand Down Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -63,7 +63,7 @@ export function sassPlugin(options: SassPluginOptions = {}): Plugin {
pluginData: {resolveDir}
}))

onLoad({filter: /./, namespace}, ({path, pluginData: {resolveDir}}) => ({
onLoad({filter: /./, namespace }, ({path, pluginData: {resolveDir}}) => ({
contents: cssChunks[path],
resolveDir,
loader: 'css'
Expand Down Expand Up @@ -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 || [])],
Expand Down
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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);
};
32 changes: 31 additions & 1 deletion test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ describe('e2e tests', function () {
})
})
]
})
});

const bundle = readTextFile('./out/index.js')

Expand All @@ -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')

Expand Down
23 changes: 23 additions & 0 deletions test/fixture/named-exports/build.js
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions test/fixture/named-exports/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bootstrap Example</title>
<link href="out/index.css" rel="stylesheet">
</head>
<body>
<script src="out/index.js" type="text/javascript"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions test/fixture/named-exports/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions test/fixture/named-exports/src/common.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.message {
font-family: Roboto, sans-serif;
}
5 changes: 5 additions & 0 deletions test/fixture/named-exports/src/example.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.message {
color: white;
background-color: red;
font-size: 24px;
}
7 changes: 7 additions & 0 deletions test/fixture/named-exports/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { message as stylesMessage } from "./example.module.scss";
import { message as commonMessage } from "./common.module.scss";

document.body.insertAdjacentHTML(
"afterbegin",
`<div class="${stylesMessage} ${commonMessage}">Hello World</div>`,
);
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2e5dedc

Please sign in to comment.