diff --git a/packages/babel/package.json b/packages/babel/package.json index adbec2172..ce8961abf 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -6,6 +6,7 @@ "dependencies": { "@babel/core": "^7.18.9", "@babel/generator": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", "@babel/template": "^7.18.6", "@babel/traverse": "^7.18.9", "@babel/types": "^7.18.9", diff --git a/packages/babel/src/helper-module-imports.d.ts b/packages/babel/src/helper-module-imports.d.ts new file mode 100644 index 000000000..b27fcf17e --- /dev/null +++ b/packages/babel/src/helper-module-imports.d.ts @@ -0,0 +1,11 @@ +declare module '@babel/helper-module-imports' { + import type { NodePath } from '@babel/traverse'; + import type { Identifier } from '@babel/types'; + + function addNamed( + path: NodePath, + name: string, + importedSource: string, + opts?: { nameHint: string } + ): Identifier; +} diff --git a/packages/babel/src/utils/getTagProcessor.ts b/packages/babel/src/utils/getTagProcessor.ts index bf41a8c7e..29797a3c3 100644 --- a/packages/babel/src/utils/getTagProcessor.ts +++ b/packages/babel/src/utils/getTagProcessor.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { basename, dirname, join } from 'path'; import { types as t } from '@babel/core'; +import { addNamed } from '@babel/helper-module-imports'; import type { NodePath } from '@babel/traverse'; import type { Expression, SourceLocation, Identifier } from '@babel/types'; import findUp from 'find-up'; @@ -306,9 +307,15 @@ function getBuilderForIdentifier( }); }; + const astService = { + ...t, + addNamedImport: (name: string, importedSource: string) => + addNamed(path, name, importedSource), + }; + return (...args: BuilderArgs) => new Processor( - t, + astService, params, (tagPath as NodePath).node, tagPath.node.loc ?? null, diff --git a/packages/griffel/babel.config.js b/packages/griffel/babel.config.js new file mode 100644 index 000000000..c9ad680b1 --- /dev/null +++ b/packages/griffel/babel.config.js @@ -0,0 +1,3 @@ +const config = require('../../babel.config'); + +module.exports = config; diff --git a/packages/griffel/package.json b/packages/griffel/package.json new file mode 100644 index 000000000..911271592 --- /dev/null +++ b/packages/griffel/package.json @@ -0,0 +1,55 @@ +{ + "name": "@linaria/griffel", + "description": "Blazing fast zero-runtime CSS in JS library", + "version": "3.0.0-beta.21", + "bugs": "https://github.com/callstack/linaria/issues", + "dependencies": { + "@griffel/core": "^1.5.0", + "@linaria/logger": "workspace:^", + "@linaria/tags": "workspace:^", + "@linaria/utils": "workspace:^", + "ts-invariant": "^0.10.3" + }, + "devDependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": "^12.16.0 || >=13.7.0" + }, + "files": [ + "esm/", + "lib/", + "processors/", + "types/" + ], + "homepage": "https://github.com/callstack/linaria#readme", + "keywords": [ + "css", + "css-in-js", + "linaria", + "react", + "styled-components" + ], + "license": "MIT", + "linaria": { + "tags": { + "makeStyles": "./lib/processors/makeStyles.js" + } + }, + "main": "lib/index.js", + "module": "esm/index.js", + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:callstack/linaria.git", + "scripts": { + "build": "npm run build:lib && npm run build:esm && npm run build:declarations", + "build:declarations": "tsc --emitDeclarationOnly --outDir types", + "build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "typecheck": "tsc --noEmit --composite false", + "watch": "npm run build --watch" + }, + "sideEffects": false, + "types": "types/index.d.ts" +} diff --git a/packages/griffel/processors/makeStyles.js b/packages/griffel/processors/makeStyles.js new file mode 100644 index 000000000..33a4817a9 --- /dev/null +++ b/packages/griffel/processors/makeStyles.js @@ -0,0 +1,5 @@ +Object.defineProperty(exports, '__esModule', { + value: true, +}); + +exports.default = require('../lib/processors/makeStyles').default; diff --git a/packages/griffel/src/index.ts b/packages/griffel/src/index.ts new file mode 100644 index 000000000..ae8a76343 --- /dev/null +++ b/packages/griffel/src/index.ts @@ -0,0 +1 @@ +export { default as makeStyles } from './makeStyles'; diff --git a/packages/griffel/src/makeStyles.ts b/packages/griffel/src/makeStyles.ts new file mode 100644 index 000000000..d6dc9db41 --- /dev/null +++ b/packages/griffel/src/makeStyles.ts @@ -0,0 +1,5 @@ +export default function makeStyles( + stylesBySlots: Record +): () => Record { + throw new Error('Cannot be called in runtime'); +} diff --git a/packages/griffel/src/processors/makeStyles.ts b/packages/griffel/src/processors/makeStyles.ts new file mode 100644 index 000000000..ecd17cd91 --- /dev/null +++ b/packages/griffel/src/processors/makeStyles.ts @@ -0,0 +1,107 @@ +/* eslint-disable class-methods-use-this */ +import type { Expression } from '@babel/types'; +import { resolveStyleRulesForSlots } from '@griffel/core'; +import type { + StylesBySlots, + CSSClassesMapBySlot, + CSSRulesByBucket, +} from '@griffel/core/types'; + +import type { Rules, ValueCache, ProcessorParams } from '@linaria/tags'; +import { BaseProcessor, isCallParam } from '@linaria/tags'; + +export default class MakeStylesProcessor extends BaseProcessor { + #cssClassMap: CSSClassesMapBySlot | undefined; + + #cssRulesByBucket: CSSRulesByBucket | undefined; + + readonly #slotsExpName: string; + + constructor(...args: ProcessorParams) { + super(...args); + + const callParam = this.params.find(isCallParam); + if (!callParam || this.params.length !== 1) { + throw new Error('Invalid usage of `makeStyles` tag'); + } + + this.#slotsExpName = callParam[1].ex.name; + } + + public override addInterpolation(): string { + throw new Error('Not implemented'); + } + + public override get asSelector(): string { + throw new Error('The result of makeStyles cannot be used as a selector.'); + } + + public override build(valueCache: ValueCache) { + const slots = valueCache.get(this.#slotsExpName) as StylesBySlots; + [this.#cssClassMap, this.#cssRulesByBucket] = + resolveStyleRulesForSlots(slots); + } + + public override doEvaltimeReplacement(): void { + this.replacer(this.value, false); + } + + public override doRuntimeReplacement(): void { + if (!this.#cssClassMap || !this.#cssRulesByBucket) { + throw new Error( + 'Styles are not extracted yet. Please call `build` first.' + ); + } + + const t = this.astService; + + const importedStyles = t.addNamedImport('__styles', '@griffel/react'); + + const cssClassMap = t.objectExpression( + Object.entries(this.#cssClassMap).map(([slot, classesMap]) => { + return t.objectProperty( + t.identifier(slot), + t.objectExpression( + Object.entries(classesMap).map(([className, classValue]) => + t.objectProperty( + t.identifier(className), + Array.isArray(classValue) + ? t.arrayExpression(classValue.map((i) => t.stringLiteral(i))) + : t.stringLiteral(classValue) + ) + ) + ) + ); + }) + ); + + const cssRulesByBucket = t.objectExpression( + Object.entries(this.#cssRulesByBucket).map(([bucket, rules]) => { + return t.objectProperty( + t.identifier(bucket), + t.arrayExpression( + rules.map((rule) => t.stringLiteral(rule as string)) + ) + ); + }) + ); + + const stylesCall = t.callExpression(importedStyles, [ + cssClassMap, + cssRulesByBucket, + ]); + this.replacer(stylesCall, true); + } + + protected get tagExpression(): Expression { + throw new Error('Not implemented'); + } + + public override get value(): Expression { + return this.astService.nullLiteral(); + } + + extractRules(): Rules { + throw new Error('Not implemented'); + } +} diff --git a/packages/griffel/tsconfig.json b/packages/griffel/tsconfig.json new file mode 100644 index 000000000..6d0f1dbc8 --- /dev/null +++ b/packages/griffel/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { "paths": {}, "rootDir": "src/" }, + "references": [{ "path": "../core" }, { "path": "../logger" }, { "path": "../react" }, { "path": "../utils" }] +} diff --git a/packages/tags/src/BaseProcessor.ts b/packages/tags/src/BaseProcessor.ts index 1a2e20b07..71a7d9f27 100644 --- a/packages/tags/src/BaseProcessor.ts +++ b/packages/tags/src/BaseProcessor.ts @@ -1,21 +1,25 @@ /* eslint-disable class-methods-use-this */ import type { types as t } from '@babel/core'; -import type { SourceLocation, Expression, TemplateElement } from '@babel/types'; +import type { + SourceLocation, + Expression, + TemplateElement, + Identifier, +} from '@babel/types'; import type { ExpressionValue, IInterpolation, IPlaceholder, - Param, Params, Rules, Value, ValueCache, Artifact, - TemplateParam, } from './types'; import getClassNameAndSlug from './utils/getClassNameAndSlug'; import hasMeta from './utils/hasMeta'; +import { isTemplateParam } from './utils/params'; import templateProcessor from './utils/templateProcessor'; import { isCSSable } from './utils/toCSS'; import type { IFileContext, IOptions } from './utils/types'; @@ -24,9 +28,6 @@ export { Expression }; export type ProcessorParams = ConstructorParameters; -const isTemplateParam = (param: Param): param is TemplateParam => - param[0] === 'template'; - export default abstract class BaseProcessor { public readonly artifacts: Artifact[] = []; @@ -45,7 +46,9 @@ export default abstract class BaseProcessor { | undefined; public constructor( - protected readonly astService: typeof t, + protected readonly astService: typeof t & { + addNamedImport: (name: string, source: string) => Identifier; + }, protected readonly params: Params, protected readonly tagExp: Expression, public readonly location: SourceLocation | null, @@ -74,7 +77,7 @@ export default abstract class BaseProcessor { return this.params.find(isTemplateParam)?.[1] ?? []; } - public build(values: ValueCache): Artifact[] { + public build(values: ValueCache) { if (this.artifacts.length > 0) { // FIXME: why it was called twice? throw new Error('Tag is already built'); @@ -84,8 +87,6 @@ export default abstract class BaseProcessor { if (artifact) { this.artifacts.push(['css', artifact]); } - - return this.artifacts; } public isValidValue(value: unknown): value is Value { diff --git a/packages/tags/src/index.ts b/packages/tags/src/index.ts index 6f6005c0e..5550df231 100644 --- a/packages/tags/src/index.ts +++ b/packages/tags/src/index.ts @@ -1,6 +1,7 @@ export * from './BaseProcessor'; export * from './types'; export { default as isSerializable } from './utils/isSerializable'; +export * from './utils/params'; export * from './utils/types'; export { default as BaseProcessor } from './BaseProcessor'; export { default as hasMeta } from './utils/hasMeta'; diff --git a/packages/tags/src/utils/params.ts b/packages/tags/src/utils/params.ts new file mode 100644 index 000000000..d724f0398 --- /dev/null +++ b/packages/tags/src/utils/params.ts @@ -0,0 +1,10 @@ +import type { Param, TemplateParam, CallParam, MemberParam } from '../types'; + +export const isCallParam = (param: Param): param is CallParam => + param[0] === 'call'; + +export const isMemberParam = (param: Param): param is MemberParam => + param[0] === 'member'; + +export const isTemplateParam = (param: Param): param is TemplateParam => + param[0] === 'template'; diff --git a/packages/testkit/package.json b/packages/testkit/package.json index cb30f3f06..1abec3bcf 100644 --- a/packages/testkit/package.json +++ b/packages/testkit/package.json @@ -22,6 +22,7 @@ "@babel/types": "^7.18.9", "@linaria/atomic": "workspace:^", "@linaria/core": "workspace:^", + "@linaria/griffel": "workspace:^", "@linaria/logger": "workspace:^", "@linaria/utils": "workspace:^", "@types/babel__core": "^7.1.19", diff --git a/packages/testkit/src/__snapshots__/babel.test.ts.snap b/packages/testkit/src/__snapshots__/babel.test.ts.snap index 14a505930..d8555ee3f 100644 --- a/packages/testkit/src/__snapshots__/babel.test.ts.snap +++ b/packages/testkit/src/__snapshots__/babel.test.ts.snap @@ -1854,6 +1854,32 @@ CSS: } } +Dependencies: NA + +`; + +exports[`strategy shaker should process griffel makeStyles 1`] = ` +"import { __styles as _styles } from \\"@griffel/react\\"; +export const useStyles = /*#__PURE__*/_styles({ + root: { + mc9l5x: \\"f22iagw\\", + Bi91k9c: \\"faf35ka\\", + Bb9khzn: \\"f17t1d3d\\", + Btk3f3y: \\"fh8e7tb\\" + } +}, { + d: [\\".f22iagw{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}\\", \\".fh8e7tb .foo:hover{color:green;}\\"], + h: [\\".faf35ka:hover{color:red;}\\"], + f: [\\".f17t1d3d:focus:hover{color:blue;}\\"] +});" +`; + +exports[`strategy shaker should process griffel makeStyles 2`] = ` + +CSS: + + + Dependencies: NA `; diff --git a/packages/testkit/src/babel.test.ts b/packages/testkit/src/babel.test.ts index bd17bf198..99f3ad3a7 100644 --- a/packages/testkit/src/babel.test.ts +++ b/packages/testkit/src/babel.test.ts @@ -2424,4 +2424,27 @@ describe('strategy shaker', () => { expect(code).toMatchSnapshot(); expect(metadata).toMatchSnapshot(); }); + + it('should process griffel makeStyles', async () => { + const { code, metadata } = await transform( + dedent` + import { makeStyles } from '@linaria/griffel'; + + export const useStyles = makeStyles({ + root: { + display: 'flex', + + ':hover': { color: 'red' }, + ':focus': { ':hover': { color: 'blue' } }, + + '& .foo': { ':hover': { color: 'green' } }, + }, + }); + `, + [evaluator] + ); + + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55cd69bfb..e7199e191 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,6 +198,7 @@ importers: specifiers: '@babel/core': ^7.18.9 '@babel/generator': ^7.18.9 + '@babel/helper-module-imports': ^7.18.6 '@babel/template': ^7.18.6 '@babel/traverse': ^7.18.9 '@babel/types': ^7.18.9 @@ -226,6 +227,7 @@ importers: dependencies: '@babel/core': 7.18.9 '@babel/generator': 7.18.9 + '@babel/helper-module-imports': 7.18.6 '@babel/template': 7.18.6 '@babel/traverse': 7.18.9 '@babel/types': 7.18.9 @@ -319,6 +321,23 @@ importers: packages/extractor: specifiers: {} + packages/griffel: + specifiers: + '@babel/types': ^7.18.9 + '@griffel/core': ^1.5.0 + '@linaria/logger': workspace:^ + '@linaria/tags': workspace:^ + '@linaria/utils': workspace:^ + ts-invariant: ^0.10.3 + dependencies: + '@griffel/core': 1.5.0 + '@linaria/logger': link:../logger + '@linaria/tags': link:../tags + '@linaria/utils': link:../utils + ts-invariant: 0.10.3 + devDependencies: + '@babel/types': 7.18.9 + packages/interop: specifiers: '@babel/core': ^7.18.9 @@ -518,6 +537,7 @@ importers: '@linaria/babel-preset': workspace:^ '@linaria/core': workspace:^ '@linaria/extractor': workspace:^ + '@linaria/griffel': workspace:^ '@linaria/logger': workspace:^ '@linaria/react': workspace:^ '@linaria/shaker': workspace:^ @@ -555,6 +575,7 @@ importers: '@babel/types': 7.18.9 '@linaria/atomic': link:../atomic '@linaria/core': link:../core + '@linaria/griffel': link:../griffel '@linaria/logger': link:../logger '@linaria/utils': link:../utils '@types/babel__core': 7.1.19 @@ -1066,7 +1087,7 @@ packages: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.18.8 + '@babel/types': 7.18.9 /@babel/helper-module-transforms/7.18.0: resolution: {integrity: sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==} @@ -2733,6 +2754,10 @@ packages: engines: {node: '>=10.0.0'} dev: true + /@emotion/hash/0.8.0: + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + dev: false + /@emotion/is-prop-valid/0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} dependencies: @@ -2760,6 +2785,16 @@ packages: - supports-color dev: true + /@griffel/core/1.5.0: + resolution: {integrity: sha512-NC0J3k4qxQq7fLicrlbbPIC85Gw7LgTUm0c9SWGRv7dsj1tXE6y2+eHAhJwA5230KkUvACPgwJRCaBikFxv+4Q==} + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.1.0 + rtl-css-js: 1.15.0 + stylis: 4.1.1 + tslib: 2.4.0 + dev: false + /@humanwhocodes/config-array/0.9.5: resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==} engines: {node: '>=10.10.0'} @@ -5239,7 +5274,6 @@ packages: /csstype/3.1.0: resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} - dev: true /csv-generate/3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -8154,7 +8188,7 @@ packages: '@babel/generator': 7.18.2 '@babel/plugin-syntax-typescript': 7.17.12_@babel+core@7.18.9 '@babel/traverse': 7.18.2 - '@babel/types': 7.18.4 + '@babel/types': 7.18.9 '@jest/expect-utils': 28.1.0 '@jest/transform': 28.1.0 '@jest/types': 28.1.0 @@ -10397,6 +10431,12 @@ packages: fsevents: 2.3.2 dev: true + /rtl-css-js/1.15.0: + resolution: {integrity: sha512-99Cu4wNNIhrI10xxUaABHsdDqzalrSRTie4GeCmbGVuehm4oj+fIy8fTzB+16pmKe8Bv9rl+hxIBez6KxExTew==} + dependencies: + '@babel/runtime': 7.18.3 + dev: false + /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -11052,6 +11092,10 @@ packages: resolution: {integrity: sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==} dev: false + /stylis/4.1.1: + resolution: {integrity: sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==} + dev: false + /sugarss/2.0.0: resolution: {integrity: sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==} dependencies: