Skip to content

Commit

Permalink
Add the option to provide a prefix to generated class names (#1717)
Browse files Browse the repository at this point in the history
* chore: extend types to support hashPrefix options

* feat: add prefix to generated hash rules

* tests: add tests around hashPrefix

* docs: add section around hashPrefix

* chore: add changeset

* tests: remove .only from tests, whoops

* feat: add hashPrefix to the group name hash so it does not break when used with `ax`

* refactor: hashPrefix -> classHashPrefix

* feat: update validator regex and move validation to top level method

* docs: update description for classHashPrefix

* docs: sort

* chore: remove incorrect jsdoc

* refactor: use .test instead of .match

* chore: update changeset description
  • Loading branch information
guilhermehto authored Oct 14, 2024
1 parent 2750e28 commit 4fb5c6e
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .changeset/empty-pumpkins-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@compiled/parcel-transformer': minor
'@compiled/webpack-loader': minor
'@compiled/babel-plugin': minor
'@compiled/css': minor
---

Adds a new option that can be passed to the babel plugin called `classHashPrefix`. Its value is used to add a prefix to the class names when generating their hashes.
32 changes: 32 additions & 0 deletions packages/babel-plugin/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,38 @@ describe('babel plugin', () => {
expect(actual).toInclude('c_MyDiv');
});

it('should add a prefix to style hash classHashPrefix is present', () => {
// changes to css/src/plugins/atomicify-rules can break this due to how the class name is hashed
const hashedClassName = '_1lv61fwx';
const actual = transform(
`
import { styled } from '@compiled/react';
const MyDiv = styled.div\`
font-size: 12px;
\`;
`,
{ classHashPrefix: 'myprefix' }
);

expect(actual).toInclude(hashedClassName);
});

it('should throw if a given classHashPrefix is not a valid css identifier', () => {
expect(() => {
transform(
`
import { styled } from '@compiled/react';
const MyDiv = styled.div\`
font-size: 12px;
\`;
`,
{ classHashPrefix: '$invalid%' }
);
}).toThrow();
});

it('should compress class name for styled component', () => {
const actual = transform(
`
Expand Down
6 changes: 6 additions & 0 deletions packages/babel-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ export interface PluginOptions {
* Defaults to `true`.
*/
sortAtRules?: boolean;

/**
* Adds a defined prefix to the generated classes' hashes.
* Useful in micro frontend environments to avoid clashing/specificity issues.
*/
classHashPrefix?: string;
}

export interface State extends PluginPass {
Expand Down
25 changes: 24 additions & 1 deletion packages/css/src/plugins/atomicify-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ interface PluginOpts {
selectors?: string[];
atRule?: string;
parentNode?: Container;
classHashPrefix?: string;
}

/**
* Returns true if a given string is a valid CSS identifier
*
* @param value the value to test
* @returns `true` if given value is valid, `false` if not
*
*/
const isCssIdentifierValid = (value: string): boolean => {
const validCssIdentifierRegex = /^[a-zA-Z\-_]+[a-zA-Z\-_0-9]*$/;
return validCssIdentifierRegex.test(value);
};

/**
* Returns an atomic rule class name using this form:
*
Expand All @@ -24,7 +37,9 @@ interface PluginOpts {
*/
const atomicClassName = (node: Declaration, opts: PluginOpts) => {
const selectors = opts.selectors ? opts.selectors.join('') : '';
const group = hash(`${opts.atRule}${selectors}${node.prop}`).slice(0, 4);
const prefix = opts.classHashPrefix ?? '';
const group = hash(`${prefix}${opts.atRule}${selectors}${node.prop}`).slice(0, 4);

const value = node.important ? node.value + node.important : node.value;
const valueHash = hash(value).slice(0, 4);

Expand Down Expand Up @@ -260,8 +275,16 @@ const atomicifyAtRule = (node: AtRule, opts: PluginOpts): AtRule => {
* Preconditions:
*
* 1. No nested rules allowed - normalize them with the `parent-orphaned-pseudos` and `nested` plugins first.
*
* @throws Throws an error if `opts.classHashPrefix` contains invalid css class/id characters
*/
export const atomicifyRules = (opts: PluginOpts = {}): Plugin => {
if (opts.classHashPrefix && !isCssIdentifierValid(opts.classHashPrefix)) {
throw new Error(
`${opts.classHashPrefix} isn't a valid CSS identifier. Accepted characters are ^[a-zA-Z\-_]+[a-zA-Z\-_0-9]*$`
);
}

return {
postcssPlugin: 'atomicify-rules',
OnceExit(root) {
Expand Down
2 changes: 2 additions & 0 deletions packages/css/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface TransformOpts {
increaseSpecificity?: boolean;
sortAtRules?: boolean;
sortShorthand?: boolean;
classHashPrefix?: string;
}

/**
Expand Down Expand Up @@ -49,6 +50,7 @@ export const transformCss = (
atomicifyRules({
classNameCompressionMap: opts.classNameCompressionMap,
callback: (className: string) => classNames.push(className),
classHashPrefix: opts.classHashPrefix,
}),
...(opts.increaseSpecificity ? [increaseSpecificity()] : []),
sortAtomicStyleSheet({
Expand Down
6 changes: 6 additions & 0 deletions packages/parcel-transformer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,10 @@ export interface ParcelTransformerOpts extends BabelPluginOpts {
* When set, extract styles to an external CSS file
*/
extractStylesToDirectory?: { source: string; dest: string };

/**
* Adds a defined prefix to the generated classes' hashes.
* Useful in micro frontend environments to avoid clashing/specificity issues.
*/
classHashPrefix?: string;
}
6 changes: 6 additions & 0 deletions packages/webpack-loader/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,10 @@ export interface CompiledExtractPluginOptions {
* Defaults to `false`.
*/
sortShorthand?: boolean;

/**
* Adds a defined prefix to the generated classes' hashes.
* Useful in micro frontend environments to avoid clashing/specificity issues.
*/
classHashPrefix?: string;
}
12 changes: 12 additions & 0 deletions website/packages/docs/src/pages/pkg-babel-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ If you choose to configure Compiled through Webpack or Parcel (recommended), you

- `addComponentName`
- `cache`
- `classHashPrefix`
- `classNameCompressionMap`
- `extensions`
- `importReact`
Expand Down Expand Up @@ -78,6 +79,17 @@ Will cache the result of statically evaluated imports.
* Type: `boolean | 'single-pass'`
* Default: `true`

#### classHashPrefix

Adds a prefix to the generated hashed css rule names. The valued passed to it gets hashed in conjunction with the rest of the rule declaration.

This is useful when `@compiled` is being used in a micro frontend environment by multiple packages and you want to avoid specificity issues.

The currently accepted regex for this value is `^[a-zA-Z\-_]+[a-zA-Z\-_0-9]*$`.

- Type: `string`
- Default: `undefined`

#### classNameCompressionMap

Don't use this!
Expand Down
1 change: 1 addition & 0 deletions website/packages/docs/src/pages/pkg-parcel-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Example usage:
The following options are passed directly to `@compiled/babel-plugin`:

- `addComponentName`
- `classHashPrefix`
- `classNameCompressionMap`
- `importReact`
- `importSources`
Expand Down
1 change: 1 addition & 0 deletions website/packages/docs/src/pages/pkg-webpack-loader.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ module.exports = {
`@compiled/webpack-loader` also accepts the following options. These are not used in the loader itself, but instead they are passed directly to the underlying `@compiled/babel-plugin`.

- `addComponentName`
- `classHashPrefix`
- `classNameCompressionMap`
- `importReact`
- `importSources`
Expand Down

0 comments on commit 4fb5c6e

Please sign in to comment.