From 43b91a38797e09239631696f08d90cf59ca22953 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 22 Apr 2024 17:15:21 +0700 Subject: [PATCH 1/6] add globalCss processor --- .../pigment-css-react/exports/globalCss.js | 5 + packages/pigment-css-react/src/globalCss.d.ts | 22 +++ packages/pigment-css-react/src/globalCss.js | 5 + .../src/processors/globalCss.ts | 167 ++++++++++++++++++ .../src/utils/preprocessor.ts | 18 +- .../globalCss/fixtures/globalCss.input.js | 30 ++++ .../globalCss/fixtures/globalCss.output.css | 25 +++ .../globalCss/fixtures/globalCss.output.js | 5 + .../tests/globalCss/globalCss.test.ts | 13 ++ packages/pigment-css-react/tsup.config.ts | 10 +- 10 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 packages/pigment-css-react/exports/globalCss.js create mode 100644 packages/pigment-css-react/src/globalCss.d.ts create mode 100644 packages/pigment-css-react/src/globalCss.js create mode 100644 packages/pigment-css-react/src/processors/globalCss.ts create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss.input.js create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.css create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.js create mode 100644 packages/pigment-css-react/tests/globalCss/globalCss.test.ts diff --git a/packages/pigment-css-react/exports/globalCss.js b/packages/pigment-css-react/exports/globalCss.js new file mode 100644 index 00000000..0cfecb09 --- /dev/null +++ b/packages/pigment-css-react/exports/globalCss.js @@ -0,0 +1,5 @@ +Object.defineProperty(exports, '__esModule', { + value: true, +}); + +exports.default = require('../processors/globalCss').GlobalCssProcessor; diff --git a/packages/pigment-css-react/src/globalCss.d.ts b/packages/pigment-css-react/src/globalCss.d.ts new file mode 100644 index 00000000..2a61b4db --- /dev/null +++ b/packages/pigment-css-react/src/globalCss.d.ts @@ -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; diff --git a/packages/pigment-css-react/src/globalCss.js b/packages/pigment-css-react/src/globalCss.js new file mode 100644 index 00000000..65e0cfca --- /dev/null +++ b/packages/pigment-css-react/src/globalCss.js @@ -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.`, + ); +} diff --git a/packages/pigment-css-react/src/processors/globalCss.ts b/packages/pigment-css-react/src/processors/globalCss.ts new file mode 100644 index 00000000..3c5244fd --- /dev/null +++ b/packages/pigment-css-react/src/processors/globalCss.ts @@ -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 | 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 | 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(); + } +} diff --git a/packages/pigment-css-react/src/utils/preprocessor.ts b/packages/pigment-css-react/src/utils/preprocessor.ts index 48b135fc..da6c09ae 100644 --- a/packages/pigment-css-react/src/utils/preprocessor.ts +++ b/packages/pigment-css-react/src/utils/preprocessor.ts @@ -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'; @@ -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(); @@ -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, @@ -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, ); } diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.input.js b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.input.js new file mode 100644 index 00000000..966b1041 --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.input.js @@ -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(); +} diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.css b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.css new file mode 100644 index 00000000..bea96799 --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.css @@ -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; + } +} diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.js b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.js new file mode 100644 index 00000000..59a5426c --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss.output.js @@ -0,0 +1,5 @@ +null; +let inputGlobalStyles = null; +if (typeof inputGlobalStyles === 'function') { + inputGlobalStyles = inputGlobalStyles(); +} diff --git a/packages/pigment-css-react/tests/globalCss/globalCss.test.ts b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts new file mode 100644 index 00000000..1ad012e5 --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts @@ -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); + }); +}); diff --git a/packages/pigment-css-react/tsup.config.ts b/packages/pigment-css-react/tsup.config.ts index fe170a40..09d5ed38 100644 --- a/packages/pigment-css-react/tsup.config.ts +++ b/packages/pigment-css-react/tsup.config.ts @@ -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 = { From f15e41989a46efbe1f2cfe1867933210a317f173 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 22 Apr 2024 17:17:15 +0700 Subject: [PATCH 2/6] revert unnecessary namespace --- packages/pigment-css-react/src/utils/preprocessor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pigment-css-react/src/utils/preprocessor.ts b/packages/pigment-css-react/src/utils/preprocessor.ts index da6c09ae..816e68d7 100644 --- a/packages/pigment-css-react/src/utils/preprocessor.ts +++ b/packages/pigment-css-react/src/utils/preprocessor.ts @@ -1,5 +1,5 @@ import type { Element } from 'stylis'; -import { serialize, compile, stringify, middleware, namespace } from 'stylis'; +import { serialize, compile, stringify, middleware } from 'stylis'; import rtlPlugin from 'stylis-plugin-rtl'; import { type PluginCustomOptions } from './cssFnValueToVariable'; @@ -21,9 +21,9 @@ function globalSelector(element: Element) { function getSerializer(includeRtl?: boolean) { if (!includeRtl) { - return middleware([globalSelector, namespace, stringify]); + return middleware([globalSelector, stringify]); } - return middleware([globalSelector, namespace, rtlPlugin, stringify]); + return middleware([globalSelector, rtlPlugin, stringify]); } const serializer = getSerializer(); From a0573fee45a67dcfddea12cb8a23d77650ece9e0 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 22 Apr 2024 17:17:57 +0700 Subject: [PATCH 3/6] fix typo --- packages/pigment-css-react/tests/globalCss/globalCss.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pigment-css-react/tests/globalCss/globalCss.test.ts b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts index 1ad012e5..a7c42180 100644 --- a/packages/pigment-css-react/tests/globalCss/globalCss.test.ts +++ b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { runTransformation, expect } from '../testUtils'; describe('Pigment CSS - globalCss', () => { - it.only('basics', async () => { + it('basics', async () => { const { output, fixture } = await runTransformation( path.join(__dirname, 'fixtures/globalCss.input.js'), ); From af389a84df8281423f89769242669ef88446e8e1 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 29 Apr 2024 09:43:27 +0700 Subject: [PATCH 4/6] add theme test case --- .../fixtures/globalCss-theme.input.js | 30 +++++++++++++++++++ .../fixtures/globalCss-theme.output.css | 27 +++++++++++++++++ .../fixtures/globalCss-theme.output.js | 5 ++++ .../tests/globalCss/globalCss.test.ts | 20 +++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.input.js create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.css create mode 100644 packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.js diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.input.js b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.input.js new file mode 100644 index 00000000..c1001433 --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.input.js @@ -0,0 +1,30 @@ +import { globalCss } from '@pigment-css/react'; + +globalCss` +* { + box-sizing: border-box; +} +@font-face { + font-family: 'Patrick Hand SC'; + font-style: normal; + font-weight: 400; + color: ${({ theme }) => theme.palette.primary.main}; + 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(({ theme }) => ({ + '@keyframes mui-auto-fill': { from: { display: 'block', color: 'transparent' } }, + '@keyframes mui-auto-fill-cancel': { + from: { display: 'block', color: theme.palette.primary.main }, + }, +})); +if (typeof inputGlobalStyles === 'function') { + inputGlobalStyles = inputGlobalStyles(); +} diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.css b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.css new file mode 100644 index 00000000..f6c71611 --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.css @@ -0,0 +1,27 @@ +* { + box-sizing: border-box; +} +@font-face { + font-family: 'Patrick Hand SC'; + font-style: normal; + font-weight: 400; + color: red; + 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; + color: transparent; + } +} +@keyframes mui-auto-fill-cancel { + from { + display: block; + color: red; + } +} diff --git a/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.js b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.js new file mode 100644 index 00000000..59a5426c --- /dev/null +++ b/packages/pigment-css-react/tests/globalCss/fixtures/globalCss-theme.output.js @@ -0,0 +1,5 @@ +null; +let inputGlobalStyles = null; +if (typeof inputGlobalStyles === 'function') { + inputGlobalStyles = inputGlobalStyles(); +} diff --git a/packages/pigment-css-react/tests/globalCss/globalCss.test.ts b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts index a7c42180..bbca3b00 100644 --- a/packages/pigment-css-react/tests/globalCss/globalCss.test.ts +++ b/packages/pigment-css-react/tests/globalCss/globalCss.test.ts @@ -10,4 +10,24 @@ describe('Pigment CSS - globalCss', () => { expect(output.js).to.equal(fixture.js); expect(output.css).to.equal(fixture.css); }); + + it('can access theme', async () => { + const { output, fixture } = await runTransformation( + path.join(__dirname, 'fixtures/globalCss-theme.input.js'), + { + themeArgs: { + theme: { + palette: { + primary: { + main: 'red', + }, + }, + }, + }, + }, + ); + + expect(output.js).to.equal(fixture.js); + expect(output.css).to.equal(fixture.css); + }); }); From 1f968f0b87bf46e81b192b3a4cc24acf54017cdf Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 30 May 2024 12:32:58 -0400 Subject: [PATCH 5/6] add globalCss doc --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 116bce95..c2187a31 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,25 @@ function App() { } ``` +### Creating global styles + +Use the `globalCss` API to create global styles: + +```js +import { globalCss } from '@pigment-css/react'; + +globalCss` + body { + margin: 0; + padding: 0; + } +`; +``` + +The `globalCss` function should to be called at the top level of your javascript file, usually from the index of the application. + +Calling inside a function or a component will not work as expected. Also, the extraction of global styles will always take place regardless of conditional rendering. + ### Theming Theming is an **optional** feature that lets you reuse the same values, such as colors, spacing, and typography, across your application. It is a plain object of any structure that you can define in your config file. From 82f38e8893cfb2aa666e5773bab285f760e28c35 Mon Sep 17 00:00:00 2001 From: Siriwat K Date: Fri, 31 May 2024 09:04:03 -0400 Subject: [PATCH 6/6] Update README.md Co-authored-by: Brijesh Bittu Signed-off-by: Siriwat K --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2187a31..d320c1e0 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ globalCss` `; ``` -The `globalCss` function should to be called at the top level of your javascript file, usually from the index of the application. +The `globalCss` function should to be called at the top level of your javascript file, usually from the entry point of the application. Calling inside a function or a component will not work as expected. Also, the extraction of global styles will always take place regardless of conditional rendering.