Skip to content

Commit

Permalink
add globalCss processor
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp committed Apr 22, 2024
1 parent ed00c14 commit 43b91a3
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 7 deletions.
5 changes: 5 additions & 0 deletions packages/pigment-css-react/exports/globalCss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Object.defineProperty(exports, '__esModule', {
value: true,
});

exports.default = require('../processors/globalCss').GlobalCssProcessor;
22 changes: 22 additions & 0 deletions packages/pigment-css-react/src/globalCss.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { CSSObjectNoCallback } from './base';
import type { ThemeArgs } from './theme';

type Primitve = string | null | undefined | boolean | number;

type CssArg = ((themeArgs: ThemeArgs) => CSSObjectNoCallback) | CSSObjectNoCallback;
type CssFn = (themeArgs: ThemeArgs) => string | number;

interface GlobalCss {
/**
* @returns {string} The generated css class name to be referenced.
*/
(arg: TemplateStringsArray, ...templateArgs: (Primitve | CssFn)[]): string;
/**
* @returns {string} The generated css class name to be referenced.
*/
(...arg: CssArg[]): string;
}

declare const globalCss: GlobalCss;

export default globalCss;
5 changes: 5 additions & 0 deletions packages/pigment-css-react/src/globalCss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function globalCss() {
throw new Error(
`${process.env.PACKAGE_NAME}: You were trying to call "globalCss" function without configuring your bundler. Make sure to install the bundler specific plugin and use it. @pigment-css/vite-plugin for Vite integration or @pigment-css/nextjs-plugin for Next.js integration.`,
);
}
167 changes: 167 additions & 0 deletions packages/pigment-css-react/src/processors/globalCss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import type { Expression } from '@babel/types';
import type {
CallParam,
TemplateParam,
Params,
TailProcessorParams,
ValueCache,
} from '@wyw-in-js/processor-utils';
import { serializeStyles, Interpolation } from '@emotion/serialize';
import { type Replacements, type Rules, ValueType } from '@wyw-in-js/shared';
import type { CSSInterpolation } from '@emotion/css';
import { validateParams } from '@wyw-in-js/processor-utils';
import BaseProcessor from './base-processor';
import type { IOptions } from './styled';
import { cache } from '../utils/emotion';
import { getGlobalSelector } from '../utils/preprocessor';

export type Primitive = string | number | boolean | null | undefined;

export type TemplateCallback = (params: Record<string, unknown> | undefined) => string | number;

export class GlobalCssProcessor extends BaseProcessor {
callParam: CallParam | TemplateParam;

constructor(params: Params, ...args: TailProcessorParams) {
super([params[0]], ...args);
if (params.length < 2) {
throw BaseProcessor.SKIP;
}
validateParams(
params,
['callee', ['call', 'template']],
`Invalid use of ${this.tagSource.imported} tag.`,
);

const [, callParams] = params;
if (callParams[0] === 'call') {
this.dependencies.push(callParams[1]);
} else if (callParams[0] === 'template') {
callParams[1].forEach((element) => {
if ('kind' in element && element.kind !== ValueType.CONST) {
this.dependencies.push(element);
}
});
}
this.callParam = callParams;
}

build(values: ValueCache) {
if (this.artifacts.length > 0) {
throw new Error(`MUI: "${this.tagSource.imported}" is already built`);
}

const [callType] = this.callParam;

if (callType === 'template') {
this.handleTemplate(this.callParam, values);
} else {
this.handleCall(this.callParam, values);
}
}

private handleTemplate([, callArgs]: TemplateParam, values: ValueCache) {
const templateStrs: string[] = [];
// @ts-ignore @TODO - Fix this. No idea how to initialize a Tagged String array.
templateStrs.raw = [];
const templateExpressions: Primitive[] = [];
const { themeArgs } = this.options as IOptions;

callArgs.forEach((item) => {
if ('kind' in item) {
switch (item.kind) {
case ValueType.FUNCTION: {
const value = values.get(item.ex.name) as TemplateCallback;
templateExpressions.push(value(themeArgs));
break;
}
case ValueType.CONST:
templateExpressions.push(item.value);
break;
case ValueType.LAZY: {
const evaluatedValue = values.get(item.ex.name);
if (typeof evaluatedValue === 'function') {
templateExpressions.push(evaluatedValue(themeArgs));
} else {
templateExpressions.push(evaluatedValue as Primitive);
}
break;
}
default:
break;
}
} else if (item.type === 'TemplateElement') {
templateStrs.push(item.value.cooked as string);
// @ts-ignore
templateStrs.raw.push(item.value.raw);
}
});
this.generateArtifacts(templateStrs, ...templateExpressions);
}

generateArtifacts(styleObjOrTaggged: CSSInterpolation | string[], ...args: Primitive[]) {
const { styles: cssText } = serializeStyles(
args.length > 0
? [styleObjOrTaggged as Interpolation<{}>, ...args]
: [styleObjOrTaggged as Interpolation<{}>],
cache.registered,
);

const rules: Rules = {
[this.asSelector]: {
className: this.className,
cssText,
displayName: this.displayName,
start: this.location?.start ?? null,
},
};
const sourceMapReplacements: Replacements = [
{
length: cssText.length,
original: {
start: {
column: this.location?.start.column ?? 0,
line: this.location?.start.line ?? 0,
},
end: {
column: this.location?.end.column ?? 0,
line: this.location?.end.line ?? 0,
},
},
},
];
this.artifacts.push(['css', [rules, sourceMapReplacements]]);
}

private handleCall([, callArg]: CallParam, values: ValueCache) {
let styleObj: CSSInterpolation;
if (callArg.kind === ValueType.LAZY) {
styleObj = values.get(callArg.ex.name) as CSSInterpolation;
} else if (callArg.kind === ValueType.FUNCTION) {
const { themeArgs } = this.options as IOptions;
const value = values.get(callArg.ex.name) as (
args: Record<string, unknown> | undefined,
) => CSSInterpolation;
styleObj = value(themeArgs);
}
if (styleObj) {
this.generateArtifacts(styleObj);
}
}

doEvaltimeReplacement() {
this.replacer(this.value, false);
}

doRuntimeReplacement() {
this.doEvaltimeReplacement();
}

get asSelector() {
return getGlobalSelector(this.className);
}

get value(): Expression {
return this.astService.nullLiteral();
}
}
18 changes: 12 additions & 6 deletions packages/pigment-css-react/src/utils/preprocessor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Element } from 'stylis';
import { serialize, compile, stringify, middleware } from 'stylis';
import { serialize, compile, stringify, middleware, namespace } from 'stylis';
import rtlPlugin from 'stylis-plugin-rtl';
import { type PluginCustomOptions } from './cssFnValueToVariable';

Expand All @@ -21,9 +21,9 @@ function globalSelector(element: Element) {

function getSerializer(includeRtl?: boolean) {
if (!includeRtl) {
return middleware([globalSelector, stringify]);
return middleware([globalSelector, namespace, stringify]);
}
return middleware([globalSelector, rtlPlugin, stringify]);
return middleware([globalSelector, namespace, rtlPlugin, stringify]);
}

const serializer = getSerializer();
Expand All @@ -34,6 +34,10 @@ const stylis = (css: string, serializerParam = serializer) =>

const defaultGetDirSelector = (dir: 'ltr' | 'rtl') => `[dir=${dir}]`;

export function getGlobalSelector(asSelector: string) {
return `$$GLOBAL-${asSelector}`;
}

export function preprocessor(
selector: string,
cssText: string,
Expand All @@ -45,14 +49,16 @@ export function preprocessor(
getDirSelector = defaultGetDirSelector,
} = options || {};
let css = '';
if (cssText.startsWith('@keyframes')) {
const isGlobal = selector.startsWith(getGlobalSelector(''));

if (!isGlobal && cssText.startsWith('@keyframes')) {
css += stylis(cssText.replace('@keyframes', `@keyframes ${selector}`));
return css;
}
css += stylis(`${selector}{${cssText}}`);
css += stylis(!isGlobal ? `${selector}{${cssText}}` : cssText);
if (generateForBothDir) {
css += stylis(
`${getDirSelector(defaultDirection === 'ltr' ? 'rtl' : 'ltr')} ${selector}{${cssText}}`,
`${getDirSelector(defaultDirection === 'ltr' ? 'rtl' : 'ltr')} ${!isGlobal ? `${selector}{${cssText}}` : cssText}`,
serializerRtl,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { globalCss } from '@pigment-css/react';

const green = 'green';

globalCss`
* {
box-sizing: border-box;
}
@font-face {
font-family: 'Patrick Hand SC';
font-style: normal;
font-weight: 400;
color: ${green};
src: local('Patrick Hand SC'),
local('PatrickHandSC-Regular'),
url(https://fonts.gstatic.com/s/patrickhandsc/v4/OYFWCgfCR-7uHIovjUZXsZ71Uis0Qeb9Gqo8IZV7ckE.woff2)
format('woff2');
unicode-range: U+0100-024f, U+1-1eff,
U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
U+A720-A7FF;
}
`;

let inputGlobalStyles = globalCss({
'@keyframes mui-auto-fill': { from: { display: 'block' } },
'@keyframes mui-auto-fill-cancel': { from: { display: 'block' } },
});
if (typeof inputGlobalStyles === 'function') {
inputGlobalStyles = inputGlobalStyles();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}
@font-face {
font-family: 'Patrick Hand SC';
font-style: normal;
font-weight: 400;
color: green;
src:
local('Patrick Hand SC'),
local('PatrickHandSC-Regular'),
url(https://fonts.gstatic.com/s/patrickhandsc/v4/OYFWCgfCR-7uHIovjUZXsZ71Uis0Qeb9Gqo8IZV7ckE.woff2)
format('woff2');
unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF;
}
@keyframes mui-auto-fill {
from {
display: block;
}
}
@keyframes mui-auto-fill-cancel {
from {
display: block;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
null;
let inputGlobalStyles = null;
if (typeof inputGlobalStyles === 'function') {
inputGlobalStyles = inputGlobalStyles();
}
13 changes: 13 additions & 0 deletions packages/pigment-css-react/tests/globalCss/globalCss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from 'node:path';
import { runTransformation, expect } from '../testUtils';

describe('Pigment CSS - globalCss', () => {
it.only('basics', async () => {
const { output, fixture } = await runTransformation(
path.join(__dirname, 'fixtures/globalCss.input.js'),
);

expect(output.js).to.equal(fixture.js);
expect(output.css).to.equal(fixture.css);
});
});
10 changes: 9 additions & 1 deletion packages/pigment-css-react/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Options, defineConfig } from 'tsup';
import config from '../../tsup.config';

const processors = ['styled', 'sx', 'keyframes', 'generateAtomics', 'css', 'createUseThemeProps'];
const processors = [
'styled',
'sx',
'keyframes',
'generateAtomics',
'css',
'createUseThemeProps',
'globalCss',
];
const external = ['react', 'react-is', 'prop-types'];

const baseConfig: Options = {
Expand Down

0 comments on commit 43b91a3

Please sign in to comment.