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

feat: Add support namedExports #179

Merged
merged 2 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 18 additions & 2 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 @@ -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
Loading