diff --git a/change/@griffel-babel-preset-38bd5966-ba05-454d-8a03-ca57f93eb42b.json b/change/@griffel-babel-preset-38bd5966-ba05-454d-8a03-ca57f93eb42b.json new file mode 100644 index 0000000000..b0536e5bb5 --- /dev/null +++ b/change/@griffel-babel-preset-38bd5966-ba05-454d-8a03-ca57f93eb42b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add support for makeResetStyles transforms", + "packageName": "@griffel/babel-preset", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@griffel-webpack-loader-2fb0d186-b51f-4f73-8784-982a16c6f786.json b/change/@griffel-webpack-loader-2fb0d186-b51f-4f73-8784-982a16c6f786.json new file mode 100644 index 0000000000..553a6f593b --- /dev/null +++ b/change/@griffel-webpack-loader-2fb0d186-b51f-4f73-8784-982a16c6f786.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add support for makeResetStyles transforms", + "packageName": "@griffel/webpack-loader", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/babel-preset/__fixtures__/assets-reset-styles/blank.jpg b/packages/babel-preset/__fixtures__/assets-reset-styles/blank.jpg new file mode 100644 index 0000000000..1cda9a53dc Binary files /dev/null and b/packages/babel-preset/__fixtures__/assets-reset-styles/blank.jpg differ diff --git a/packages/babel-preset/__fixtures__/assets-reset-styles/code.ts b/packages/babel-preset/__fixtures__/assets-reset-styles/code.ts new file mode 100644 index 0000000000..a214c515eb --- /dev/null +++ b/packages/babel-preset/__fixtures__/assets-reset-styles/code.ts @@ -0,0 +1,11 @@ +import { makeResetStyles } from '@griffel/react'; + +import blank from './blank.jpg'; +import blankDuplicate from './blank.jpg'; +import empty from './empty.jpg'; + +export const useStyles = makeResetStyles({ + backgroundImage: `url(${blank})`, + ':hover': { backgroundImage: `url(${blankDuplicate})` }, + ':focus': { backgroundImage: `url(${empty})` }, +}); diff --git a/packages/babel-preset/__fixtures__/assets-reset-styles/empty.jpg b/packages/babel-preset/__fixtures__/assets-reset-styles/empty.jpg new file mode 100644 index 0000000000..1cda9a53dc Binary files /dev/null and b/packages/babel-preset/__fixtures__/assets-reset-styles/empty.jpg differ diff --git a/packages/babel-preset/__fixtures__/assets-reset-styles/output.ts b/packages/babel-preset/__fixtures__/assets-reset-styles/output.ts new file mode 100644 index 0000000000..f582824f65 --- /dev/null +++ b/packages/babel-preset/__fixtures__/assets-reset-styles/output.ts @@ -0,0 +1,11 @@ +import _asset2 from './empty.jpg'; +import _asset from './blank.jpg'; +import { __resetStyles } from '@griffel/react'; +import blank from './blank.jpg'; +import blankDuplicate from './blank.jpg'; +import empty from './empty.jpg'; +export const useStyles = __resetStyles('ra9m047', null, [ + `.ra9m047{background-image:url(${_asset});}`, + `.ra9m047:hover{background-image:url(${_asset});}`, + `.ra9m047:focus{background-image:url(${_asset2});}`, +]); diff --git a/packages/babel-preset/__fixtures__/require-reset-styles/code.ts b/packages/babel-preset/__fixtures__/require-reset-styles/code.ts new file mode 100644 index 0000000000..2194b3342b --- /dev/null +++ b/packages/babel-preset/__fixtures__/require-reset-styles/code.ts @@ -0,0 +1,6 @@ +const react_make_styles_1 = require('@griffel/react'); + +export const useStyles = react_make_styles_1.makeResetStyles({ + fontSize: '14px', + lineHeight: 1, +}); diff --git a/packages/babel-preset/__fixtures__/require-reset-styles/output.ts b/packages/babel-preset/__fixtures__/require-reset-styles/output.ts new file mode 100644 index 0000000000..ad0df6815d --- /dev/null +++ b/packages/babel-preset/__fixtures__/require-reset-styles/output.ts @@ -0,0 +1,5 @@ +const react_make_styles_1 = require('@griffel/react'); + +export const useStyles = react_make_styles_1.__resetStyles('r1qlvv59', null, [ + '.r1qlvv59{font-size:14px;line-height:1;}', +]); diff --git a/packages/babel-preset/__fixtures__/reset-styles/code.ts b/packages/babel-preset/__fixtures__/reset-styles/code.ts new file mode 100644 index 0000000000..3f2173edcf --- /dev/null +++ b/packages/babel-preset/__fixtures__/reset-styles/code.ts @@ -0,0 +1,6 @@ +import { makeResetStyles } from '@griffel/react'; + +export const useStyles = makeResetStyles({ + color: 'red', + paddingLeft: '4px', +}); diff --git a/packages/babel-preset/__fixtures__/reset-styles/output.ts b/packages/babel-preset/__fixtures__/reset-styles/output.ts new file mode 100644 index 0000000000..4101e2fc9a --- /dev/null +++ b/packages/babel-preset/__fixtures__/reset-styles/output.ts @@ -0,0 +1,5 @@ +import { __resetStyles } from '@griffel/react'; +export const useStyles = __resetStyles('rjefjbm', 'r7z97ji', [ + '.rjefjbm{color:red;padding-left:4px;}', + '.r7z97ji{color:red;padding-right:4px;}', +]); diff --git a/packages/babel-preset/src/transformPlugin.test.ts b/packages/babel-preset/src/transformPlugin.test.ts index 33bfe43660..e78fce6636 100644 --- a/packages/babel-preset/src/transformPlugin.test.ts +++ b/packages/babel-preset/src/transformPlugin.test.ts @@ -144,6 +144,20 @@ pluginTester({ }, }, + // Reset styles + // + // + { + title: 'reset: default', + fixture: path.resolve(fixturesDir, 'reset-styles', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'reset-styles', 'output.ts'), + }, + { + title: 'reset: assets', + fixture: path.resolve(fixturesDir, 'assets-reset-styles', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'assets-reset-styles', 'output.ts'), + }, + // Imports // // @@ -182,6 +196,12 @@ pluginTester({ }, }, + { + title: 'imports: require() for makeResetStyles', + fixture: path.resolve(fixturesDir, 'require-reset-styles', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'require-reset-styles', 'output.ts'), + }, + // Errors // // diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts index f02cd803e5..d0e1044443 100644 --- a/packages/babel-preset/src/transformPlugin.ts +++ b/packages/babel-preset/src/transformPlugin.ts @@ -2,7 +2,13 @@ import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core'; import { declare } from '@babel/helper-plugin-utils'; import { Module } from '@linaria/babel-preset'; import shakerEvaluator from '@linaria/shaker'; -import { resolveStyleRulesForSlots, CSSRulesByBucket, StyleBucketName, GriffelStyle } from '@griffel/core'; +import { + resolveStyleRulesForSlots, + CSSRulesByBucket, + StyleBucketName, + GriffelStyle, + resolveResetStyleRules, +} from '@griffel/core'; import * as path from 'path'; import { normalizeStyleRules } from './assets/normalizeStyleRules'; @@ -12,45 +18,54 @@ import { evaluatePaths } from './utils/evaluatePaths'; import { BabelPluginOptions } from './types'; import { validateOptions } from './validateOptions'; +type FunctionKinds = 'makeStyles' | 'makeResetStyles'; + type BabelPluginState = PluginPass & { importDeclarationPaths?: NodePath[]; requireDeclarationPath?: NodePath; - definitionPaths?: NodePath[]; + definitionPaths?: { functionKind: FunctionKinds; path: NodePath }[]; calleePaths?: NodePath[]; }; -function getDefinitionPathFromMakeStylesCallExpression( +function getDefinitionPathFromCallExpression( + functionKind: FunctionKinds, callExpression: NodePath, -): NodePath { +): NodePath { const argumentPaths = callExpression.get('arguments') as NodePath[]; const hasValidArguments = Array.isArray(argumentPaths) && argumentPaths.length === 1; if (!hasValidArguments) { - throw new Error('makeStyles() function accepts only a single param'); + throw callExpression.buildCodeFrameError(`${functionKind}() function accepts only a single param`); } const definitionsPath = argumentPaths[0]; if (!definitionsPath.isObjectExpression()) { - throw definitionsPath.buildCodeFrameError('makeStyles() function accepts only an object as a param'); + throw definitionsPath.buildCodeFrameError(`${functionKind}() function accepts only an object as a param`); } return definitionsPath; } /** - * Checks that passed callee imports makesStyles(). + * Gets a kind of passed callee. */ -function isMakeStylesCallee( - path: NodePath, +function getCalleeFunctionKind( + path: NodePath, modules: NonNullable, -): path is NodePath { - if (path.isIdentifier()) { - return Boolean(modules.find(module => path.referencesImport(module.moduleSource, module.importName))); +): FunctionKinds | null { + for (const module of modules) { + if (path.referencesImport(module.moduleSource, module.importName)) { + return 'makeStyles'; + } + + if (path.referencesImport(module.moduleSource, module.resetImportName || 'makeResetStyles')) { + return 'makeResetStyles'; + } } - return false; + return null; } /** @@ -164,39 +179,69 @@ export const transformPlugin = declare, PluginObj p.path), + pluginOptions, + ); state.definitionPaths.forEach(definitionPath => { - const callExpressionPath = definitionPath.findParent(parentPath => + const callExpressionPath = definitionPath.path.findParent(parentPath => parentPath.isCallExpression(), ) as NodePath; - const evaluationResult = definitionPath.evaluate(); + const evaluationResult = definitionPath.path.evaluate(); if (!evaluationResult.confident) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const deoptPath = (evaluationResult as any).deopt as NodePath | undefined; - throw (deoptPath || definitionPath).buildCodeFrameError( + throw (deoptPath || definitionPath.path).buildCodeFrameError( 'Evaluation of a code fragment failed, this is a bug, please report it', ); } - const stylesBySlots: Record = evaluationResult.value; - const [classnamesMapping, cssRulesByBucket] = resolveStyleRulesForSlots( - // Heads up! - // Style rules should be normalized *before* they will be resolved to CSS rules to have deterministic - // results across different build targets. - normalizeStyleRules( - path, - pluginOptions.projectRoot, - // Presence of "state.filename" is validated on `Program.enter()` - state.filename as string, - stylesBySlots, - ), - ); - const uniqueCSSRules = dedupeCSSRules(cssRulesByBucket); - - (callExpressionPath.get('arguments.0') as NodePath).remove(); - callExpressionPath.pushContainer('arguments', [astify(classnamesMapping), astify(uniqueCSSRules)]); + if (definitionPath.functionKind === 'makeStyles') { + const stylesBySlots: Record = evaluationResult.value; + const [classnamesMapping, cssRulesByBucket] = resolveStyleRulesForSlots( + // Heads up! + // Style rules should be normalized *before* they will be resolved to CSS rules to have deterministic + // results across different build targets. + normalizeStyleRules( + path, + pluginOptions.projectRoot, + // Presence of "state.filename" is validated on `Program.enter()` + state.filename as string, + stylesBySlots, + ), + ); + const uniqueCSSRules = dedupeCSSRules(cssRulesByBucket); + + (callExpressionPath.get('arguments.0') as NodePath).remove(); + callExpressionPath.pushContainer('arguments', [astify(classnamesMapping), astify(uniqueCSSRules)]); + } + + if (definitionPath.functionKind === 'makeResetStyles') { + const styles: GriffelStyle = evaluationResult.value; + const [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules( + // Heads up! + // Style rules should be normalized *before* they will be resolved to CSS rules to have deterministic + // results across different build targets. + normalizeStyleRules( + path, + pluginOptions.projectRoot, + // Presence of "state.filename" is validated on `Program.enter()` + state.filename as string, + styles, + ), + ); + + (callExpressionPath.get('arguments.0') as NodePath).remove(); + callExpressionPath.pushContainer('arguments', [ + astify(ltrClassName), + astify(rtlClassName), + astify(cssRules), + ]); + } replaceAssetsWithImports(pluginOptions.projectRoot, state.filename!, programPath, callExpressionPath); }); @@ -208,19 +253,19 @@ export const transformPlugin = declare, PluginObj { if (specifier.isImportSpecifier()) { - // TODO: should use generated modifier to avoid collisions - const importedPath = specifier.get('imported'); - const importIdentifierPath = pluginOptions.modules.find(module => { - return ( - module.moduleSource === source.node.value && - // 👆 "moduleSource" should match "importDeclarationPath.source" to skip unrelated ".importName" - importedPath.isIdentifier({ name: module.importName }) - ); - }); - if (importIdentifierPath) { - specifier.replaceWith(t.identifier('__styles')); + for (const module of pluginOptions.modules) { + if (module.moduleSource !== source.node.value) { + // 👆 "moduleSource" should match "importDeclarationPath.source" to skip unrelated ".importName" + continue; + } + + if (importedPath.isIdentifier({ name: module.importName })) { + specifier.replaceWith(t.identifier('__styles')); + } else if (importedPath.isIdentifier({ name: module.resetImportName || 'makeResetStyles' })) { + specifier.replaceWith(t.identifier('__resetStyles')); + } } } }); @@ -228,6 +273,11 @@ export const transformPlugin = declare, PluginObj { + if (calleePath.node.name === 'makeResetStyles') { + calleePath.replaceWith(t.identifier('__resetStyles')); + return; + } + calleePath.replaceWith(t.identifier('__styles')); }); } @@ -261,12 +311,17 @@ export const transformPlugin = declare, PluginObj, PluginObj, PluginObj); }, }, diff --git a/packages/babel-preset/src/types.ts b/packages/babel-preset/src/types.ts index 6f00b361c7..5ac91d61fd 100644 --- a/packages/babel-preset/src/types.ts +++ b/packages/babel-preset/src/types.ts @@ -3,7 +3,11 @@ import type { EvalRule } from '@linaria/babel-preset'; export type BabelPluginOptions = { /** Defines set of modules and imports handled by a transformPlugin. */ - modules?: { moduleSource: string; importName: string }[]; + modules?: { + moduleSource: string; + importName: string; + resetImportName?: string; + }[]; /** * If you need to specify custom Babel configuration, you can pass them here. These options will be used by the diff --git a/packages/webpack-loader/__fixtures__/reset/code.ts b/packages/webpack-loader/__fixtures__/reset/code.ts new file mode 100644 index 0000000000..1c0a4ac18e --- /dev/null +++ b/packages/webpack-loader/__fixtures__/reset/code.ts @@ -0,0 +1,7 @@ +import { makeResetStyles } from '@griffel/react'; +import { tokens } from './tokens'; + +export const useStyles = makeResetStyles({ + color: tokens.colorBrandStroke1, + paddingLeft: '4px', +}); diff --git a/packages/webpack-loader/__fixtures__/reset/output.ts b/packages/webpack-loader/__fixtures__/reset/output.ts new file mode 100644 index 0000000000..908b646013 --- /dev/null +++ b/packages/webpack-loader/__fixtures__/reset/output.ts @@ -0,0 +1,6 @@ +import { __resetStyles } from '@griffel/react'; +import { tokens } from './tokens'; +export const useStyles = __resetStyles('rgwtdav', 'r133dp1', [ + '.rgwtdav{color:var(--colorBrandStroke1);padding-left:4px;}', + '.r133dp1{color:var(--colorBrandStroke1);padding-right:4px;}', +]); diff --git a/packages/webpack-loader/__fixtures__/reset/tokens.ts b/packages/webpack-loader/__fixtures__/reset/tokens.ts new file mode 100644 index 0000000000..7728554d5f --- /dev/null +++ b/packages/webpack-loader/__fixtures__/reset/tokens.ts @@ -0,0 +1,3 @@ +export const tokens = { + colorBrandStroke1: 'var(--colorBrandStroke1)', +}; diff --git a/packages/webpack-loader/project.json b/packages/webpack-loader/project.json index 2836557336..8c0863fbf7 100644 --- a/packages/webpack-loader/project.json +++ b/packages/webpack-loader/project.json @@ -16,8 +16,7 @@ "options": { "jestConfig": "packages/webpack-loader/jest.config.js", "passWithNoTests": true - }, - "dependsOn": [{ "target": "build", "projects": "dependencies" }] + } }, "build": { "executor": "@nrwl/node:package", diff --git a/packages/webpack-loader/src/webpackLoader.test.ts b/packages/webpack-loader/src/webpackLoader.test.ts index 4e260b81c3..5c24d16806 100644 --- a/packages/webpack-loader/src/webpackLoader.test.ts +++ b/packages/webpack-loader/src/webpackLoader.test.ts @@ -28,6 +28,9 @@ async function compileSourceWithWebpack(entryPath: string, options: CompileOptio path: path.resolve(__dirname), filename: 'bundle.js', }, + externals: { + '@griffel/react': 'Griffel', + }, module: { rules: [ @@ -43,10 +46,6 @@ async function compileSourceWithWebpack(entryPath: string, options: CompileOptio }, resolve: { - alias: { - '@griffel/core': path.resolve(__dirname, '..', '..', '..', 'dist', 'packages', 'react', 'index.esm.js'), - '@griffel/react': path.resolve(__dirname, '..', '..', '..', 'dist', 'packages', 'react', 'index.esm.js'), - }, extensions: ['.js', '.ts'], }, }; @@ -175,28 +174,56 @@ function testFixture(fixtureName: string, options: CompileOptions = {}) { } describe('shouldTransformSourceCode', () => { - it('handles defaults', () => { - expect(shouldTransformSourceCode(`import { makeStyles } from "@griffel/react"`, undefined)).toBe(true); - expect(shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, undefined)).toBe(false); + describe('handles defaults', () => { + it('makeStyles', () => { + expect(shouldTransformSourceCode(`import { makeStyles } from "@griffel/react"`, undefined)).toBe(true); + expect(shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, undefined)).toBe(false); + }); + + it('makeResetStyles', () => { + expect(shouldTransformSourceCode(`import { makeResetStyles } from "@griffel/react"`, undefined)).toBe(true); + expect(shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, undefined)).toBe(false); + }); }); - it('handles options', () => { - expect( - shouldTransformSourceCode(`import { makeStyles } from "@griffel/react"`, [ - { moduleSource: '@griffel/react', importName: 'makeStyles' }, - ]), - ).toBe(true); - expect( - shouldTransformSourceCode(`import { createStyles } from "make-styles"`, [ - { moduleSource: 'make-styles', importName: 'createStyles' }, - ]), - ).toBe(true); - - expect( - shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, [ - { moduleSource: '@griffel/react', importName: 'makeStyles' }, - ]), - ).toBe(false); + describe('handles options', () => { + it('makeStyles', () => { + expect( + shouldTransformSourceCode(`import { makeStyles } from "@griffel/react"`, [ + { moduleSource: '@griffel/react', importName: 'makeStyles' }, + ]), + ).toBe(true); + expect( + shouldTransformSourceCode(`import { createStyles } from "make-styles"`, [ + { moduleSource: 'make-styles', importName: 'createStyles' }, + ]), + ).toBe(true); + + expect( + shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, [ + { moduleSource: '@griffel/react', importName: 'makeStyles' }, + ]), + ).toBe(false); + }); + + it('makeResetStyles', () => { + expect( + shouldTransformSourceCode(`import { makeResetStyles } from "@griffel/react"`, [ + { moduleSource: '@griffel/react', importName: 'makeStyles', resetImportName: 'makeResetStyles' }, + ]), + ).toBe(true); + expect( + shouldTransformSourceCode(`import { createResetStyles } from "make-styles"`, [ + { moduleSource: 'make-styles', importName: 'makeStyles', resetImportName: 'createResetStyles' }, + ]), + ).toBe(true); + + expect( + shouldTransformSourceCode(`import { Button } from "@fluentui/react"`, [ + { moduleSource: '@griffel/react', importName: 'makeStyles', resetImportName: 'makeResetStyles' }, + ]), + ).toBe(false); + }); }); }); @@ -204,6 +231,7 @@ describe('webpackLoader', () => { // Integration fixtures for base functionality, all scenarios are tested in "@griffel/babel-preset" testFixture('object'); testFixture('function'); + testFixture('reset'); // Integration fixtures for config functionality testFixture('config-modules', { @@ -211,10 +239,8 @@ describe('webpackLoader', () => { modules: [{ moduleSource: 'react-make-styles', importName: 'makeStyles' }], }, webpackConfig: { - resolve: { - alias: { - 'react-make-styles': '@griffel/react', - }, + externals: { + 'react-make-styles': 'Griffel', }, }, }); diff --git a/packages/webpack-loader/src/webpackLoader.ts b/packages/webpack-loader/src/webpackLoader.ts index 08bccd1664..2057889260 100644 --- a/packages/webpack-loader/src/webpackLoader.ts +++ b/packages/webpack-loader/src/webpackLoader.ts @@ -16,9 +16,11 @@ export function shouldTransformSourceCode( modules: WebpackLoaderOptions['modules'] | undefined, ): boolean { // Fallback to "makeStyles" if options were not provided - const imports = modules ? modules.map(module => module.importName).join('|') : 'makeStyles'; + const imports = modules + ? modules.flatMap(module => [module.importName, module.resetImportName || 'makeResetStyles']).join('|') + : 'makeStyles|makeResetStyles'; - return new RegExp(`\\b(${imports})`).test(sourceCode); + return new RegExp(`\\b(${imports}|makeResetStyles)`).test(sourceCode); } /**