Skip to content

Commit

Permalink
feat(styled): customizable names of generated css variables (closes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber committed Oct 13, 2022
1 parent 4c2efaa commit 87ffe61
Show file tree
Hide file tree
Showing 18 changed files with 334 additions and 59 deletions.
10 changes: 10 additions & 0 deletions .changeset/stupid-otters-obey.md
@@ -0,0 +1,10 @@
---
'@linaria/atomic': patch
'@linaria/core': patch
'@linaria/react': patch
'@linaria/tags': patch
'@linaria/testkit': patch
'@linaria/utils': patch
---

The new `variableNameSlug` option that allows to customize css variable names (closes #1053).
29 changes: 28 additions & 1 deletion docs/CONFIGURATION.md
Expand Up @@ -26,7 +26,7 @@ module.exports = {

Enabling this will add a display name to generated class names, e.g. `.Title_abcdef` instead of `.abcdef'. It is disabled by default to generate smaller CSS files.

- `classNameSlug: string | (hash: string, title: string, args: ClassNameSlugVars) => string` (default: `default`):
- `classNameSlug: string | ((hash: string, title: string, args: ClassNameSlugVars) => string)` (default: `default`):

Using this will provide an interface to customize the output of the CSS class name. Example:

Expand Down Expand Up @@ -58,6 +58,33 @@ module.exports = {
- `hash`: The hash of the content.
- `title`: The name of the class.

- `variableNameSlug: string | ((context: IVariableContext) => string)` (default: `default`):

Using this will provide an interface to customize the output of the CSS variable name. Example:

variableNameSlug: '[componentName]-[valueSlug]-[index]',

Would generate a variable name such as `--Title-absdjfsdf-0` instead of the `@react/styled`'s default `--absdjfsdf-0`.

You may also use a function to define the slug. The function will be evaluated at build time and must return a string:

variableNameSlug: (context) => `${context.valueSlug}__${context.componentName}__${context.precedingCss.match(/([\w-]+)\s*:\s*$/)[1]}`,

Would generate the variable name `--absdjfsdf__Title__flex-direction`.

**note** invalid characters will be replaced with an underscore (`_`).

### Variables

- `componentName` - the displayName of the component.
- `componentSlug` - the component slug.
- `index` - the index of the css variable in the current component.
- `precedingCss` - the preceding part of the css for the variable, i.e. `flex: 1; flex-direction: `.
- `preprocessor` - the preprocessor used to process the tag (e.g. 'StyledProcessor' or 'AtomicStyledProcessor').
- `source` - the string source of the css property value.
- `unit` - the unit.
- `valueSlug` - the value slug.

- `rules: EvalRule[]`

The set of rules that defines how the matched files will be transformed during the evaluation.
Expand Down
16 changes: 12 additions & 4 deletions packages/atomic/src/processors/styled.ts
Expand Up @@ -5,7 +5,6 @@ import type { IProps } from '@linaria/react/processors/styled';
import StyledProcessor from '@linaria/react/processors/styled';
import { hasMeta } from '@linaria/tags';
import type { Rules, ValueCache } from '@linaria/tags';
import { slugify } from '@linaria/utils';

import atomize from './helpers/atomize';

Expand Down Expand Up @@ -67,9 +66,18 @@ export default class AtomicStyledProcessor extends StyledProcessor {
return props;
}

// eslint-disable-next-line class-methods-use-this
protected override getVariableId(value: string): string {
protected override getVariableId(
source: string,
unit: string,
precedingCss: string
): string {
const id = this.getCustomVariableId(source, unit, precedingCss);
if (id) {
return id;
}

const context = this.getVariableContext(source, unit, precedingCss);
// id is based on the slugified value
return slugify(value);
return context.valueSlug;
}
}
6 changes: 5 additions & 1 deletion packages/core/src/processors/css.ts
Expand Up @@ -5,7 +5,11 @@ import { TaggedTemplateProcessor } from '@linaria/tags';

export default class CssProcessor extends TaggedTemplateProcessor {
// eslint-disable-next-line class-methods-use-this
public override addInterpolation(node: unknown, source: string): string {
public override addInterpolation(
node: unknown,
precedingCss: string,
source: string
): string {
throw new Error(
`css tag cannot handle '${source}' as an interpolated value`
);
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Expand Up @@ -7,6 +7,7 @@
"@emotion/is-prop-valid": "^0.8.8",
"@linaria/core": "workspace:^",
"@linaria/tags": "workspace:^",
"@linaria/utils": "workspace:^",
"ts-invariant": "^0.10.3"
},
"devDependencies": {
Expand Down
75 changes: 65 additions & 10 deletions packages/react/src/processors/styled.ts
Expand Up @@ -7,18 +7,22 @@ import type {
} from '@babel/types';

import type {
Rules,
WrappedNode,
ValueCache,
Params,
Rules,
TailProcessorParams,
ValueCache,
WrappedNode,
} from '@linaria/tags';
import {
TaggedTemplateProcessor,
ValueType,
buildSlug,
hasMeta,
TaggedTemplateProcessor,
validateParams,
ValueType,
toValidCSSIdentifier,
} from '@linaria/tags';
import type { IVariableContext } from '@linaria/utils';
import { slugify } from '@linaria/utils';

const isNotNull = <T>(x: T | null): x is T => x !== null;

Expand Down Expand Up @@ -90,10 +94,11 @@ export default class StyledProcessor extends TaggedTemplateProcessor {

public override addInterpolation(
node: Expression,
precedingCss: string,
source: string,
unit = ''
): string {
const id = this.getVariableId(source + unit);
const id = this.getVariableId(source, unit, precedingCss);

this.interpolations.push({
id,
Expand Down Expand Up @@ -215,6 +220,22 @@ export default class StyledProcessor extends TaggedTemplateProcessor {
return res(this.component.source);
}

protected getCustomVariableId(
source: string,
unit: string,
precedingCss: string
) {
const context = this.getVariableContext(source, unit, precedingCss);
const customSlugFn = this.options.variableNameSlug;
if (!customSlugFn) {
return undefined;
}

return typeof customSlugFn === 'function'
? customSlugFn(context)
: buildSlug(customSlugFn, context);
}

protected getProps(): IProps {
const propsObj: IProps = {
name: this.displayName,
Expand Down Expand Up @@ -271,12 +292,46 @@ export default class StyledProcessor extends TaggedTemplateProcessor {
return t.objectExpression(propExpressions);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getVariableId(value: string): string {
protected getVariableContext(
source: string,
unit: string,
precedingCss: string
): IVariableContext {
const getIndex = () => {
// eslint-disable-next-line no-plusplus
return this.#variableIdx++;
};

return {
componentName: this.displayName,
componentSlug: this.slug,
get index() {
return getIndex();
},
precedingCss,
processor: this.constructor.name,
source,
unit,
valueSlug: slugify(source + unit),
};
}

protected getVariableId(
source: string,
unit: string,
precedingCss: string
): string {
const value = source + unit;
if (!this.#variablesCache.has(value)) {
const id = this.getCustomVariableId(source, unit, precedingCss);
if (id) {
return toValidCSSIdentifier(id);
}

const context = this.getVariableContext(source, unit, precedingCss);

// make the variable unique to this styled component
// eslint-disable-next-line no-plusplus
this.#variablesCache.set(value, `${this.slug}-${this.#variableIdx++}`);
this.#variablesCache.set(value, `${this.slug}-${context.index}`);
}

return this.#variablesCache.get(value)!;
Expand Down
2 changes: 2 additions & 0 deletions packages/tags/src/TaggedTemplateProcessor.ts
Expand Up @@ -43,12 +43,14 @@ export default abstract class TaggedTemplateProcessor extends BaseProcessor {
/**
* It is called for each resolved expression in a template literal.
* @param node
* @param precedingCss
* @param source
* @param unit
* @return chunk of CSS that should be added to extracted CSS
*/
public abstract addInterpolation(
node: Expression,
precedingCss: string,
source: string,
unit?: string
): string;
Expand Down
2 changes: 2 additions & 0 deletions packages/tags/src/index.ts
@@ -1,8 +1,10 @@
export * from './BaseProcessor';
export * from './types';
export { buildSlug } from './utils/buildSlug';
export { default as isSerializable } from './utils/isSerializable';
export * from './utils/types';
export * from './utils/validateParams';
export { default as BaseProcessor } from './BaseProcessor';
export { default as TaggedTemplateProcessor } from './TaggedTemplateProcessor';
export { default as hasMeta } from './utils/hasMeta';
export { default as toValidCSSIdentifier } from './utils/toValidCSSIdentifier';
12 changes: 12 additions & 0 deletions packages/tags/src/utils/buildSlug.ts
@@ -0,0 +1,12 @@
const PLACEHOLDER = /\[(.*?)]/g;

const isValidArgName = <TArgs>(
key: string | number | symbol,
args: TArgs
): key is keyof TArgs => key in args;

export function buildSlug<TArgs>(pattern: string, args: TArgs) {
return pattern.replace(PLACEHOLDER, (_, name: string) =>
isValidArgName(name, args) ? String(args[name]) : ''
);
}
26 changes: 4 additions & 22 deletions packages/tags/src/utils/getClassNameAndSlug.ts
Expand Up @@ -4,14 +4,10 @@ import { debug } from '@linaria/logger';
import type { ClassNameSlugVars } from '@linaria/utils';
import { slugify } from '@linaria/utils';

import { buildSlug } from './buildSlug';
import toValidCSSIdentifier from './toValidCSSIdentifier';
import type { IFileContext, IOptions } from './types';

const isSlugVar = (
key: string,
slugVars: ClassNameSlugVars
): key is keyof ClassNameSlugVars => key in slugVars;

export default function getClassNameAndSlug(
displayName: string,
idx: number,
Expand Down Expand Up @@ -58,23 +54,9 @@ export default function getClassNameAndSlug(
}

if (typeof options.classNameSlug === 'string') {
const { classNameSlug } = options;

// Variables that were used in the config for `classNameSlug`
const optionVariables = classNameSlug.match(/\[.*?]/g) || [];
let cnSlug = classNameSlug;

for (let i = 0, l = optionVariables.length; i < l; i++) {
const v = optionVariables[i].slice(1, -1); // Remove the brackets around the variable name

// Replace the var if it key and value exist otherwise place an empty string
cnSlug = cnSlug.replace(
`[${v}]`,
isSlugVar(v, slugVars) ? slugVars[v] : ''
);
}

className = toValidCSSIdentifier(cnSlug);
className = toValidCSSIdentifier(
buildSlug(options.classNameSlug, slugVars)
);
}

debug(
Expand Down
7 changes: 6 additions & 1 deletion packages/tags/src/utils/templateProcessor.ts
Expand Up @@ -117,14 +117,19 @@ export default function templateProcessor(

const varId = tagProcessor.addInterpolation(
item.ex,
cssText,
item.source,
unit
);
cssText += `var(--${varId})`;

cssText += next.value.cooked?.substring(unit?.length ?? 0) ?? '';
} else {
const varId = tagProcessor.addInterpolation(item.ex, item.source);
const varId = tagProcessor.addInterpolation(
item.ex,
cssText,
item.source
);
cssText += `var(--${varId})`;
}
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion packages/tags/src/utils/types.ts
@@ -1,10 +1,11 @@
import type { TransformOptions } from '@babel/core';

import type { ClassNameFn } from '@linaria/utils';
import type { ClassNameFn, VariableNameFn } from '@linaria/utils';

export interface IOptions {
classNameSlug?: string | ClassNameFn;
displayName: boolean;
variableNameSlug?: string | VariableNameFn;
}

export type IFileContext = Pick<TransformOptions, 'root' | 'filename'>;

0 comments on commit 87ffe61

Please sign in to comment.