Repository: ben-rogerson/twin.macro
Branch: master
Commit: e477fca539e7
Files: 391
Total size: 2.6 MB
Directory structure:
gitextract_n8pigfyt/
├── .babelrc
├── .eslintrc.js
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── config.yml
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .nvmrc
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docs/
│ ├── advanced-theming.md
│ ├── arbitrary-values.md
│ ├── customizing-config.md
│ ├── fonts.md
│ ├── group.md
│ ├── index.md
│ ├── options.md
│ ├── prop-styling-guide.md
│ ├── screen-import.md
│ └── styled-component-guide.md
├── jest.config.ts
├── package.json
├── sandbox/
│ └── in.tsx
├── src/
│ ├── core/
│ │ ├── constants.ts
│ │ ├── createCoreContext.ts
│ │ ├── extractRuleStyles.ts
│ │ ├── getGlobalStyles.ts
│ │ ├── getStyles.ts
│ │ ├── index.ts
│ │ ├── lib/
│ │ │ ├── configHelpers.ts
│ │ │ ├── convertClassName.ts
│ │ │ ├── createAssert.ts
│ │ │ ├── createTheme.ts
│ │ │ ├── defaultTailwindConfig.ts
│ │ │ ├── expandVariantGroups.ts
│ │ │ ├── getStitchesPath.ts
│ │ │ ├── logging.ts
│ │ │ ├── twinConfig.ts
│ │ │ ├── userPresets.ts
│ │ │ └── util/
│ │ │ ├── camelize.ts
│ │ │ ├── deepMerge.ts
│ │ │ ├── escapeRegex.ts
│ │ │ ├── formatProp.ts
│ │ │ ├── get.ts
│ │ │ ├── isEmpty.ts
│ │ │ ├── isObject.ts
│ │ │ ├── isShortCss.ts
│ │ │ ├── replaceThemeValue.ts
│ │ │ ├── sassifySelector.ts
│ │ │ ├── splitOnFirst.ts
│ │ │ ├── toArray.ts
│ │ │ └── twImports.ts
│ │ └── types/
│ │ └── index.ts
│ ├── macro/
│ │ ├── className.ts
│ │ ├── css.ts
│ │ ├── dataProp.ts
│ │ ├── globalStyles.ts
│ │ ├── lib/
│ │ │ ├── astHelpers.ts
│ │ │ ├── util/
│ │ │ │ ├── get.ts
│ │ │ │ └── isEmpty.ts
│ │ │ └── validateImports.ts
│ │ ├── screen.ts
│ │ ├── shortCss.ts
│ │ ├── styled.ts
│ │ ├── theme.ts
│ │ ├── tw.ts
│ │ ├── twin.ts
│ │ └── types/
│ │ └── index.ts
│ ├── macro.ts
│ └── suggestions/
│ ├── index.ts
│ ├── lib/
│ │ ├── colors.ts
│ │ ├── extractors.ts
│ │ ├── getClassSuggestions.ts
│ │ ├── getPackageVersions.ts
│ │ ├── makeColor.ts
│ │ ├── validateVariants.ts
│ │ └── validators.ts
│ └── types/
│ └── index.ts
├── tests/
│ ├── @applyInPlugins.test.ts
│ ├── __fixtures__/
│ │ ├── !general.tsx
│ │ ├── !important.tsx
│ │ ├── !imports.tsx
│ │ ├── !namelessImport.tsx
│ │ ├── !ordering.tsx
│ │ ├── !properties.tsx
│ │ ├── !variantGrouping.tsx
│ │ ├── !variants.tsx
│ │ ├── .eslintrc.js
│ │ ├── addBase/
│ │ │ ├── addBase.tsx
│ │ │ └── tailwind.config.js
│ │ ├── arbitraryProperties/
│ │ │ └── arbitraryProperties.tsx
│ │ ├── arbitraryVariants/
│ │ │ ├── arbitraryVariants.tsx
│ │ │ └── config.json
│ │ ├── autoCssProp/
│ │ │ └── autoCssProp.tsx
│ │ ├── colorFunctions/
│ │ │ ├── colorFunctions.tsx
│ │ │ └── tailwind.config.js
│ │ ├── comments/
│ │ │ ├── comments.tsx
│ │ │ └── config.json
│ │ ├── config/
│ │ │ ├── config.tsx
│ │ │ └── tailwind.config.js
│ │ ├── configTS/
│ │ │ ├── configTS.tsx
│ │ │ └── tailwind.config.ts
│ │ ├── content/
│ │ │ ├── content.tsx
│ │ │ └── tailwind.config.js
│ │ ├── cssPropEmotion/
│ │ │ ├── autoCssProp.tsx
│ │ │ └── autoCssPropWithStyled.tsx
│ │ ├── cssPropStyledComponents/
│ │ │ ├── autoCssProp.tsx
│ │ │ ├── autoCssPropWithStyled.tsx
│ │ │ └── config.json
│ │ ├── darkLightModeArray/
│ │ │ ├── darkLightModeArray.tsx
│ │ │ └── tailwind.config.js
│ │ ├── directionalBorders/
│ │ │ └── directionalBorders.tsx
│ │ ├── fluidContainer/
│ │ │ ├── fluidContainer.tsx
│ │ │ └── tailwind.config.js
│ │ ├── globalStyles/
│ │ │ ├── config.json
│ │ │ ├── globalStyles.tsx
│ │ │ └── tailwind.config.js
│ │ ├── group/
│ │ │ └── group.tsx
│ │ ├── includeClassNames/
│ │ │ ├── config.json
│ │ │ └── includeClassNames.tsx
│ │ ├── lineClamp/
│ │ │ └── lineClamp.tsx
│ │ ├── negative/
│ │ │ ├── negative.tsx
│ │ │ └── tailwind.config.js
│ │ ├── peers/
│ │ │ └── peers.tsx
│ │ ├── pluginAspectRatio/
│ │ │ ├── pluginAspectRatio.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginDaisyUi/
│ │ │ ├── pluginDaisyUi.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginExamples/
│ │ │ ├── pluginExamples.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginForms/
│ │ │ ├── pluginForms.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginFormsClassStrategy/
│ │ │ ├── pluginTypography.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginGapFallback/
│ │ │ ├── pluginGapFallback.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginTypography/
│ │ │ ├── pluginTypography.tsx
│ │ │ └── tailwind.config.js
│ │ ├── pluginUserParentSelector/
│ │ │ ├── pluginUserParentSelector.tsx
│ │ │ └── tailwind.config.js
│ │ ├── plugins/
│ │ │ ├── config.json
│ │ │ ├── plugins.tsx
│ │ │ └── tailwind.config.js
│ │ ├── prefix/
│ │ │ ├── config.json
│ │ │ ├── prefix.tsx
│ │ │ └── tailwind.config.js
│ │ ├── preflight/
│ │ │ ├── preflight.tsx
│ │ │ └── tailwind.config.js
│ │ ├── presets/
│ │ │ ├── presets.tsx
│ │ │ └── tailwind.config.js
│ │ ├── sassyPseudo/
│ │ │ ├── config.json
│ │ │ ├── sassyPseudo.tsx
│ │ │ └── tailwind.config.js
│ │ ├── screenImport/
│ │ │ ├── screenImport.tsx
│ │ │ └── tailwind.config.js
│ │ ├── screens/
│ │ │ ├── screens.tsx
│ │ │ └── tailwind.config.js
│ │ ├── separator/
│ │ │ ├── separator.tsx
│ │ │ └── tailwind.config.js
│ │ ├── shortCss/
│ │ │ ├── config.json
│ │ │ └── shortCss.tsx
│ │ ├── stitches/
│ │ │ ├── config.json
│ │ │ ├── stitches.config.js
│ │ │ ├── stitchesDotSyntax.tsx
│ │ │ ├── stitchesGlobals.tsx
│ │ │ ├── stitchesImports.tsx
│ │ │ └── stitchesProps.tsx
│ │ ├── themeValuesToString/
│ │ │ ├── tailwind.config.js
│ │ │ └── themeValuesToString.tsx
│ │ ├── userPluginOrdering/
│ │ │ ├── tailwind.config.js
│ │ │ └── userPluginOrdering.tsx
│ │ ├── utilitiesAccessibility/
│ │ │ └── screenReaders.tsx
│ │ ├── utilitiesBackgrounds/
│ │ │ ├── backgroundAttachment.tsx
│ │ │ ├── backgroundClip.tsx
│ │ │ ├── backgroundColor.tsx
│ │ │ ├── backgroundImage.tsx
│ │ │ ├── backgroundOpacity.tsx
│ │ │ ├── backgroundOrigin.tsx
│ │ │ ├── backgroundPosition.tsx
│ │ │ ├── backgroundRepeat.tsx
│ │ │ ├── backgroundSize.tsx
│ │ │ ├── gradientColorStops.tsx
│ │ │ └── tailwind.config.js
│ │ ├── utilitiesBorders/
│ │ │ ├── borderColor.tsx
│ │ │ ├── borderOpacity.tsx
│ │ │ ├── borderRadius.tsx
│ │ │ ├── borderStyle.tsx
│ │ │ ├── borderWidth.tsx
│ │ │ ├── divideColor.tsx
│ │ │ ├── divideOpacity.tsx
│ │ │ ├── divideStyle.tsx
│ │ │ ├── divideWidth.tsx
│ │ │ ├── outlineColor.tsx
│ │ │ ├── outlineOffset.tsx
│ │ │ ├── outlineStyle.tsx
│ │ │ ├── outlineWidth.tsx
│ │ │ ├── ringColor.tsx
│ │ │ ├── ringMisc.tsx
│ │ │ ├── ringOffsetColor.tsx
│ │ │ ├── ringOffsetWidth.tsx
│ │ │ ├── ringOpacity.tsx
│ │ │ ├── ringWidth.tsx
│ │ │ └── tailwind.config.js
│ │ ├── utilitiesEffects/
│ │ │ ├── backgroundBlendMode.tsx
│ │ │ ├── boxShadow.tsx
│ │ │ ├── boxShadowColor.tsx
│ │ │ ├── mixBlendMode.tsx
│ │ │ └── opacity.tsx
│ │ ├── utilitiesFilters/
│ │ │ ├── backdropBlur.tsx
│ │ │ ├── backdropBrightness.tsx
│ │ │ ├── backdropContrast.tsx
│ │ │ ├── backdropGrayscale.tsx
│ │ │ ├── backdropHueRotate.tsx
│ │ │ ├── backdropInvert.tsx
│ │ │ ├── backdropOpacity.tsx
│ │ │ ├── backdropSaturate.tsx
│ │ │ ├── backdropSepia.tsx
│ │ │ ├── blur.tsx
│ │ │ ├── brightness.tsx
│ │ │ ├── contrast.tsx
│ │ │ ├── dropShadow.tsx
│ │ │ ├── grayscale.tsx
│ │ │ ├── hueRotate.tsx
│ │ │ ├── invert.tsx
│ │ │ ├── misc.tsx
│ │ │ ├── saturate.tsx
│ │ │ └── sepia.tsx
│ │ ├── utilitiesLayout/
│ │ │ ├── aspectRatio.tsx
│ │ │ ├── boxDecorationBreak.tsx
│ │ │ ├── boxSizing.tsx
│ │ │ ├── breakAfter.tsx
│ │ │ ├── breakBefore.tsx
│ │ │ ├── breakInside.tsx
│ │ │ ├── clear.tsx
│ │ │ ├── columns.tsx
│ │ │ ├── container.tsx
│ │ │ ├── display.tsx
│ │ │ ├── float.tsx
│ │ │ ├── isolation.tsx
│ │ │ ├── objectFit.tsx
│ │ │ ├── objectPosition.tsx
│ │ │ ├── overflow.tsx
│ │ │ ├── overscrollBehavior.tsx
│ │ │ ├── position.tsx
│ │ │ ├── tailwind.config.js
│ │ │ ├── topRightBottomLeft.tsx
│ │ │ ├── visibility.tsx
│ │ │ └── zIndex.tsx
│ │ ├── utilitiesSpacing/
│ │ │ ├── margin.tsx
│ │ │ ├── padding.tsx
│ │ │ └── spaceBetween.tsx
│ │ ├── utilitiesSvg/
│ │ │ ├── fill.tsx
│ │ │ ├── stroke.tsx
│ │ │ ├── strokeWidth.tsx
│ │ │ └── tailwind.config.js
│ │ ├── utilitiesTransforms/
│ │ │ ├── misc.tsx
│ │ │ ├── rotate.tsx
│ │ │ ├── scale.tsx
│ │ │ ├── skew.tsx
│ │ │ ├── tailwind.config.js
│ │ │ ├── transformOrigin.tsx
│ │ │ └── translate.tsx
│ │ ├── utilitiesTransitionsAnimation/
│ │ │ ├── animation.tsx
│ │ │ ├── misc.tsx
│ │ │ ├── transitionDelay.tsx
│ │ │ ├── transitionDuration.tsx
│ │ │ ├── transitionProperty.tsx
│ │ │ └── transitionTimingFunction.tsx
│ │ ├── utiltiesFlexboxGrid/
│ │ │ ├── alignContent.tsx
│ │ │ ├── alignItems.tsx
│ │ │ ├── alignSelf.tsx
│ │ │ ├── flex.tsx
│ │ │ ├── flexBasis.tsx
│ │ │ ├── flexDirection.tsx
│ │ │ ├── flexGrow.tsx
│ │ │ ├── flexShrink.tsx
│ │ │ ├── flexWrap.tsx
│ │ │ ├── gap.tsx
│ │ │ ├── gridAutoColumns.tsx
│ │ │ ├── gridAutoFlow.tsx
│ │ │ ├── gridAutoRows.tsx
│ │ │ ├── gridColumn.tsx
│ │ │ ├── gridRow.tsx
│ │ │ ├── gridTemplateColumns.tsx
│ │ │ ├── gridTemplateRows.tsx
│ │ │ ├── justifyContent.tsx
│ │ │ ├── justifyItems.tsx
│ │ │ ├── justifySelf.tsx
│ │ │ ├── misc.tsx
│ │ │ ├── order.tsx
│ │ │ ├── placeContent.tsx
│ │ │ ├── placeItems.tsx
│ │ │ └── placeSelf.tsx
│ │ ├── utiltiesInteractivity/
│ │ │ ├── accentColor.tsx
│ │ │ ├── appearance.tsx
│ │ │ ├── caretColor.tsx
│ │ │ ├── cursor.tsx
│ │ │ ├── pointerEvents.tsx
│ │ │ ├── resize.tsx
│ │ │ ├── scrollBehavior.tsx
│ │ │ ├── scrollMargin.tsx
│ │ │ ├── scrollPadding.tsx
│ │ │ ├── scrollSnapAlign.tsx
│ │ │ ├── scrollSnapStop.tsx
│ │ │ ├── scrollSnapType.tsx
│ │ │ ├── tailwind.config.js
│ │ │ ├── touchAction.tsx
│ │ │ ├── userSelect.tsx
│ │ │ └── willChange.tsx
│ │ ├── utiltiesSizing/
│ │ │ ├── height.tsx
│ │ │ ├── maxHeight.tsx
│ │ │ ├── maxWidth.tsx
│ │ │ ├── minHeight.tsx
│ │ │ ├── minWidth.tsx
│ │ │ └── width.tsx
│ │ ├── utiltiesTables/
│ │ │ ├── borderCollapse.tsx
│ │ │ ├── borderSpacing.tsx
│ │ │ ├── captionSide.tsx
│ │ │ └── tableLayout.tsx
│ │ ├── utiltiesTypography/
│ │ │ ├── fontFamily.tsx
│ │ │ ├── fontSize.tsx
│ │ │ ├── fontSmoothing.tsx
│ │ │ ├── fontStyle.tsx
│ │ │ ├── fontVariantNumeric.tsx
│ │ │ ├── fontWeight.tsx
│ │ │ ├── hyphens.tsx
│ │ │ ├── letterSpacing.tsx
│ │ │ ├── lineHeight.tsx
│ │ │ ├── listStyleImage.tsx
│ │ │ ├── listStylePosition.tsx
│ │ │ ├── listStyleType.tsx
│ │ │ ├── placeholderColor.tsx
│ │ │ ├── placeholderOpacity.tsx
│ │ │ ├── tailwind.config.js
│ │ │ ├── textAlign.tsx
│ │ │ ├── textColor.tsx
│ │ │ ├── textDecoration.tsx
│ │ │ ├── textDecorationColor.tsx
│ │ │ ├── textDecorationStyle.tsx
│ │ │ ├── textDecorationThickness.tsx
│ │ │ ├── textIndent.tsx
│ │ │ ├── textOpacity.tsx
│ │ │ ├── textOverflow.tsx
│ │ │ ├── textTransform.tsx
│ │ │ ├── textUnderlineOffset.tsx
│ │ │ ├── verticalAlign.tsx
│ │ │ ├── whitespace.tsx
│ │ │ └── wordBreak.tsx
│ │ ├── variables/
│ │ │ ├── tailwind.config.js
│ │ │ └── variables.tsx
│ │ ├── variantOrdering/
│ │ │ └── variantOrdering.tsx
│ │ └── visitedOpacity/
│ │ └── visitedOpacity.tsx
│ ├── __snapshots__/
│ │ └── plugin.test.js.snap
│ ├── animations.test.ts
│ ├── arbitraryProperties.test.ts
│ ├── arbitraryValues.test.ts
│ ├── arbitraryVariants.test.ts
│ ├── config.test.ts
│ ├── containerQueries.test.ts
│ ├── dividers.test.ts
│ ├── escaping.test.ts
│ ├── fontSize.test.ts
│ ├── minMaxScreenVariants.test.ts
│ ├── plugin.test.js
│ ├── presetEmotion.test.ts
│ ├── presetGoober.test.ts
│ ├── presetSolid.test.ts
│ ├── presetStitches.test.ts
│ ├── presetStyledComponents.test.ts
│ ├── screens.test.ts
│ ├── stitches.config.js
│ ├── types/
│ │ ├── index.ts
│ │ └── types.d.ts
│ └── util/
│ ├── customMatchers.ts
│ └── run.ts
├── tsconfig.json
└── types/
├── index.d.ts
├── macro.d.ts
├── tests/
│ ├── __fixtures__/
│ │ ├── config/
│ │ │ └── tailwind.config.d.ts
│ │ └── configTS/
│ │ └── tailwind.config.d.ts
│ ├── basic/
│ │ ├── index.tsx
│ │ ├── noDefaultImport.tsx
│ │ └── tsconfig.json
│ ├── emotion/
│ │ ├── index.tsx
│ │ └── tsconfig.json
│ └── styled-components/
│ ├── index.tsx
│ └── tsconfig.json
├── tsconfig.base.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-syntax-jsx",
"babel-plugin-macros"
]
}
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
},
settings: {
version: 'detect',
},
plugins: [
'@typescript-eslint',
'chai-friendly',
'jest',
'import',
'prettier',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:unicorn/recommended',
'xo/browser',
'xo-typescript/space',
'xo-react/space',
'plugin:jest/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier',
],
overrides: [
{
files: ['src/**/*.ts', 'tests/**/*.ts'],
rules: {
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0, maxBOF: 0 }],
'no-undef': 2,
'no-console': 'error',
'capitalized-comments': 0,
'func-style': ['error', 'declaration'],
'no-constant-binary-expression': 0,
'import/no-unresolved': 'error',
'import/no-relative-parent-imports': 'error',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'unicorn/filename-case': ['error', { case: 'camelCase' }],
'unicorn/prefer-optional-catch-binding': 0, // Doubleup
'unicorn/consistent-destructuring': 0,
'unicorn/prefer-node-protocol': 0,
'unicorn/import-style': 0,
'unicorn/prefer-array-flat': 0,
'unicorn/no-array-for-each': 0,
'unicorn/prevent-abbreviations': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
},
],
},
},
{
files: ['tests/**/*test.ts'],
rules: {
'@typescript-eslint/no-require-imports': 0,
'unicorn/prefer-module': 0,
'jest/no-conditional-expect': 0,
},
},
{
files: ['types/tests/**/*.ts', 'types/tests/**/*.tsx'],
rules: {
'import/no-unassigned-import': 0,
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-unsafe-call': 0,
},
},
{
files: ['**/types/**/*.ts'],
rules: {
'import/no-relative-parent-imports': 0,
'unicorn/prefer-export-from': 0,
},
},
],
ignorePatterns: [
'/tests/plugin.test.js',
'/tests/stitches.config.js',
'/tests/__fixtures__',
'/types/macro.d.ts',
'/types',
'/.eslintrc.js',
'/macro.js',
'/sandbox',
'/jest.config.ts',
],
globals: { JSX: true, AriaAttributes: true, process: true },
env: { browser: false, node: true, es6: true },
}
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: ben-rogerson
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: 'Bug report'
about: 'Report a reproducible bug'
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Help / Question / Feature request
url: https://github.com/ben-rogerson/twin.macro/discussions/new
about: 'If you have a question, need help or have an idea please ask a question in the discussion forum'
================================================
FILE: .github/workflows/main.yml
================================================
name: Notification on push
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
================================================
FILE: .gitignore
================================================
node_modules
/macro.js
/macro.js.map
/utils.umd.js
/utils.umd.js.map
.vscode
yarn.lock
# artifacts from type tests
*.tsbuildinfo
types/build
types/core
types/macro
types/suggestions
types/src
types/tests/types
types/tests/util
types/tests/*.ts
.eslintcache
coverage
sandbox/out.tsx
================================================
FILE: .nvmrc
================================================
16.14.0
================================================
FILE: .prettierrc
================================================
{
"endOfLine": "lf",
"semi": false,
"singleQuote": true,
"bracketSpacing": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"arrowParens": "avoid"
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at info@benrogerson.dev. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: LICENSE
================================================
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
The magic of Tailwind with the flexibility of css-in-js.
---
Style jsx elements using Tailwind classes:
```js
import 'twin.macro'
const Input = () =>
```
Nest Twin’s `tw` import within a css prop to add conditional styles:
```js
import tw from 'twin.macro'
const Input = ({ hasHover }) => (
)
```
Or mix sass styles with the css import:
```js
import tw, { css } from 'twin.macro'
const hoverStyles = css`
&:hover {
border-color: black;
${tw`text-black`}
}
`
const Input = ({ hasHover }) => (
)
```
### Styled Components
You can also use the tw import to create and style new components:
```js
import tw from 'twin.macro'
const Input = tw.input`border hover:border-black`
```
And clone and style existing components:
```js
const PurpleInput = tw(Input)`border-purple-500`
```
Switch to the styled import to add conditional styling:
```js
import tw, { styled } from 'twin.macro'
const StyledInput = styled.input(({ hasBorder }) => [
`color: black;`,
hasBorder && tw`border-purple-500`,
])
const Input = () =>
```
Or use backticks to mix with sass styles:
```js
import tw, { styled } from 'twin.macro'
const StyledInput = styled.input`
color: black;
${({ hasBorder }) => hasBorder && tw`border-purple-500`}
`
const Input = () =>
```
## How it works
When babel runs over your javascript or typescript files at compile time, twin grabs your classes and converts them into css objects.
These css objects are then passed into your chosen css-in-js library without the need for an extra client-side bundle:
```js
import tw from 'twin.macro'
tw`text-sm md:text-lg`
// ↓ ↓ ↓ ↓ ↓ ↓
{
fontSize: '0.875rem',
'@media (min-width: 768px)': {
fontSize: '1.125rem',
},
}
```
## Features
**👌 Simple imports** - Twin collapses imports from common styling libraries into a single import:
```diff
- import styled from '@emotion/styled'
- import css from '@emotion/react'
+ import { styled, css } from 'twin.macro'
```
**🐹 Adds no size to your build** - Twin converts the classes you’ve used into css objects using Babel and then compiles away, leaving no runtime code
**🍱 Apply variants to multiple classes at once with variant groups**
```js
import 'twin.macro'
const interactionStyles = () => (
)
const mediaStyles = () =>
const pseudoElementStyles = () =>
const stackedVariants = () =>
const groupsInGroups = () =>
```
**🛎 Helpful suggestions for mistypings** - Twin chimes in with class and variant values from your Tailwind config:
```bash
✕ ml-1.25 was not found
Try one of these classes:
- ml-1.5 > 0.375rem
- ml-1 > 0.25rem
- ml-10 > 2.5rem
```
**🖌️ Use the theme import to add values from your tailwind config**
```js
import { css, theme } from 'twin.macro'
const Input = () =>
```
See more examples [using the theme import →](https://github.com/ben-rogerson/twin.macro/pull/106)
**💡 Works with the official tailwind vscode plugin** - Avoid having to look up your classes with auto-completions straight from your Tailwind config - [setup instructions →](https://github.com/ben-rogerson/twin.macro/discussions/227)
**💥 Add !important to any class with a trailing or leading bang!**
```js
||
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
```
Add !important to multiple classes with bracket groups:
```js
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
```
## Get started
Twin works with many modern stacks - take a look at these examples to get started:
### App build tools and libraries
- **Parcel** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/react-styled-components) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/react-emotion) / [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/react-emotion-typescript)
- **Webpack** [styled-components (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/webpack-styled-components-typescript) / [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/webpack-emotion-typescript)
- **Preact** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/preact-styled-components) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/preact-emotion) / [goober](https://github.com/ben-rogerson/twin.examples/tree/master/preact-goober)
- **Create React App** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/cra-styled-components) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/cra-emotion)
- **Vite** [styled-components (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/vite-styled-components-typescript) / [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/vite-emotion-typescript) / [solid (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/vite-solid-typescript)
- **Jest / React Testing Library** [styled-components (ts) / emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/jest-testing-typescript)
### Advanced frameworks
- **Next.js** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/next-styled-components) / [styled-components (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/next-styled-components-typescript) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/next-emotion) / [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/next-emotion-typescript) / [stitches (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/next-stitches-typescript)
- **T3 App** [styled-components (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/t3-styled-components-typescript) /
[emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/t3-emotion-typescript)
- **Blitz.js** [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/blitz-emotion-typescript)
- **Gatsby** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/gatsby-styled-components) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/gatsby-emotion)
### Component libraries
- **Storybook** [styled-components (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/storybook-styled-components-typescript) / [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/storybook-emotion) / [emotion (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/storybook-emotion-typescript)
- **yarn/npm workspaces + Next.js + shared ui components** [styled-components](https://github.com/ben-rogerson/twin.examples/tree/master/component-library-styled-components)
- **Yarn workspaces + Rollup** [emotion](https://github.com/ben-rogerson/twin.examples/tree/master/component-library-emotion)
- [**HeadlessUI** (ts)](https://github.com/ben-rogerson/twin.examples/tree/master/headlessui-typescript)
## Community
[Drop into our Discord server](https://discord.gg/Xj6x9z7) for announcements, help and styling chat.
## Resources
- 🔥 [Docs: The prop styling guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/prop-styling-guide.md) - A must-read guide to level up on prop styling
- 🔥 [Docs: The styled component guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/styled-component-guide.md) - A must-read guide on getting productive with styled components
- [Docs: Options](https://github.com/ben-rogerson/twin.macro/blob/master/docs/options.md) - Learn about the features you can tweak via the twin config
- [Plugin: babel-plugin-twin](https://github.com/ben-rogerson/babel-plugin-twin) - Use the tw and css props without adding an import
- [Example: Advanced theming](https://github.com/ben-rogerson/twin.macro/blob/master/docs/advanced-theming.md) - Add custom theming the right way using css variables
- [Example: React + Tailwind breakpoint syncing](https://gist.github.com/ben-rogerson/b4b406dffcc18ae02f8a6c8c97bb58a8) - Sync your tailwind.config.js breakpoints with react
- [Helpers: Twin VSCode snippets](https://gist.github.com/ben-rogerson/c6b62508e63b3e3146350f685df2ddc9) - For devs who want to type less
- [Plugins: VSCode plugins](https://github.com/ben-rogerson/twin.macro/discussions/227) - VScode plugins that work with twin
- [Article: "Why I Love Tailwind" by Max Stoiber](https://mxstbr.com/thoughts/tailwind) - Max (inventor of styled-components) shares his thoughts on twin
## Special thanks
This project stemmed from [babel-plugin-tailwind-components](https://github.com/bradlc/babel-plugin-tailwind-components) so a big shout out goes to [Brad Cornes](https://github.com/bradlc) for the amazing work he produced. Styling with tailwind.macro has been such a pleasure.
---
[Consider donating some 🍕 if you enjoy!](https://www.buymeacoffee.com/benrogerson)
================================================
FILE: docs/advanced-theming.md
================================================
# Theming with css variables
These examples show how to create themes using css variables rather than relying on the default `dark:` variant supplied in tailwind.
This technique is the preferred way to add a dark/light theme and allows you to add more themes when needed.
- [react + emotion](https://codesandbox.io/s/github/alexperronnet/codesandbox-examples/tree/master/react/twin-emotion-dark-mode-variables)
- [react + styled-components](https://codesandbox.io/s/github/alexperronnet/codesandbox-examples/tree/master/react/twin-styled-components-dark-mode-variables)
- [gatsby + emotion](https://codesandbox.io/s/github/alexperronnet/codesandbox-examples/tree/master/gatsby/twin-emotion-dark-mode-variables)
- [gatsby + styled-components](https://codesandbox.io/s/github/alexperronnet/codesandbox-examples/tree/master/gatsby/twin-styled-components-dark-mode-variables)
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/arbitrary-values.md
================================================
# Arbitrary values
Twin supports the same arbitrary values syntax popularized by Tailwind’s [jit ("Just-in-Time") mode](https://tailwindcss.com/docs/just-in-time-mode).
```js
tw`top-[calc(100vh - 2rem)]`
// ↓ ↓ ↓ ↓ ↓ ↓
;({ top: 'calc(100vh - 2rem)' })
```
Arbitrary values use square brackets to allow custom css values instead of classes built from your tailwind.config.js.
This is a good solution for those unique “once off” values that every project requires which you may not want to add to your tailwind.config.js.
## Supported classes
Generally the rule is: Dynamic classes - like `bg-red-500` - support arbitrary values, while static classes like `block` don’t.
> For fully custom css properties and values use [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties).
## Spaces in values
In Tailwind, when we add classes within the className prop/attribute, values cannot have spaces in them.
```js
// Spaced values won’t work in Tailwind
;
```
But with twin, spaces are okay because Twin is not restricted by the spacing rules of the className prop:
```js
// Twin supports values with spaces
;
// Classes can be added on multiple lines when using template literals
;
```
And we can also use Arbitrary values within variant groups:
```js
;
```
## Dynamic values
Just like Tailwind, values can't be dynamically added because Twin doesn’t have the ability to read the variables before converting to a css object:
```js
// Dynamic values without the tw call won’t work
;
```
You’ll need to use a full tw class definition to make dynamic values possible:
```js
// Dynamic values work when constructed like this
;
```
## Resources
- [The PR for arbitrary values](https://github.com/ben-rogerson/twin.macro/pull/447)
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/customizing-config.md
================================================
# Customizing the Tailwind config
For style customizations, add a `tailwind.config.js` in your project root.
> It’s important to know that you don’t need a `tailwind.config.js` to use Twin. You already have access to every class with every variant.
Choose from one of the following configs:
- a) Start with an empty config:
```js
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {},
},
},
plugins: [],
}
```
- b) Start with a [full config](https://raw.githubusercontent.com/tailwindcss/tailwindcss/master/stubs/defaultConfig.stub.js):
```bash
# cd into your project folder then:
npx tailwindcss-cli@latest init --full
```
## Plugins
You can use all Tailwind plugins with twin, some popular ones are [tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography) and [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms).
## Resources
- Official [Tailwind theme docs](https://tailwindcss.com/docs/theme)
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/fonts.md
================================================
# Fonts
You can add `@font-face` definitions either [in the global styles provider](#add-the-font-face-in-the-global-styles-provider) or [in a traditional .css file](#add-the-font-face-in-a-traditional-css-file).
## Add `@font-face` in the Global styles provider
An option is to add the font with the global provider that comes with your css-in-js library. Here are some examples:
### Styled-components
```js
// styles/GlobalStyles.js
import React from 'react'
import { createGlobalStyle } from 'styled-components'
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro'
const CustomStyles = createGlobalStyle`
@font-face {
font-family: 'Foo';
src: url('/path/to/exampleFont.woff') format('woff');
font-style: normal;
font-weight: 400;
/* https://styled-components.com/docs/faqs#how-do-i-fix-flickering-text-after-server-side-rendering */
font-display: fallback;
}
`
const GlobalStyles = () => (
<>
>
)
export default GlobalStyles
```
[createGlobalStyle docs →](https://styled-components.com/docs/api#createglobalstyle)
### Emotion
```js
// styles/GlobalStyles.js
import React from 'react'
import { Global, css } from '@emotion/react'
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro'
const customStyles = css`
@font-face {
font-family: 'Foo';
src: url('/path/to/exampleFont.woff') format('woff');
font-style: normal;
font-weight: 400;
/* https://styled-components.com/docs/faqs#how-do-i-fix-flickering-text-after-server-side-rendering */
font-display: fallback;
}
`
const GlobalStyles = () => (
<>
>
)
export default GlobalStyles
```
[Global docs →](https://emotion.sh/docs/globals)
### Goober
```js
// styles/GlobalStyles.js
import React from 'react'
import { createGlobalStyle } from 'styled-components'
import tw, { GlobalStyles as BaseStyles } from 'twin.macro'
const CustomStyles = createGlobalStyle`
@font-face {
font-family: 'Foo';
src: url('/path/to/exampleFont.woff') format('woff');
font-style: normal;
font-weight: 400;
/* https://styled-components.com/docs/faqs#how-do-i-fix-flickering-text-after-server-side-rendering */
font-display: fallback;
}
`
const GlobalStyles = () => (
<>
>
)
export default GlobalStyles
```
[createGlobalStyle docs →](https://goober.js.org/api/createGlobalStyles)
## Add the `@font-face` in a .css file and import it
This method may help to remove text flickering in some frameworks.
```css
/* styles/fonts.css */
@font-face {
font-family: 'Foo';
src: url('/path/to/exampleFont.woff') format('woff');
font-style: normal;
font-weight: 400;
font-display: fallback;
}
```
```js
// index.js / _app.js
import '../styles/fonts.css'
// ...
```
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/group.md
================================================
# Using the group className
There’s a couple of Tailwind classes that need to be added to React elements as a `className`.
These classes are the `peer` and the `group` classes.
A className is used so variants like `group-hover:` and `peer-hover:` can use the className as an anchor to allow their styles to work.
Here’s how we use the `group` classes in twin:
```js
import 'twin.macro'
export default () => (
)
```
When working in emotion and styled-components without the `group` classes, the equivalent looks like this:
```js
import tw, { styled } from 'twin.macro'
const Group = tw.button``
Group.Child1 = styled.div`
${Group}:hover & {
${tw`bg-black`}
}
`
Group.Child2 = styled.div`
${Group}:hover & {
${tw`font-bold`}
}
`
export default () => (
Child 1Child 2
)
```
Not as great right?
Here’s some ways you can improve upon that:
## Attrs in 💅 styled‑components
In styled-components we have a `styled` function called `attrs`.
Here’s what the docs have to say about it:
> The rule of thumb is to use attrs when you want every instance of a styled component to have that prop, and pass props directly when every instance needs a different one. - [styled-components docs](https://styled-components.com/docs/faqs#when-to-use-attrs)
But we can also put it to use to define the `group` class in Tailwind.
Rather than adding `className="group"` directly onto your jsx element, the class can be more tightly coupled with your styles:
```js
import tw, { styled } from 'twin.macro'
const Group = styled.button.attrs({ className: 'group' })``
Group.Child1 = tw.div`group-hover:bg-black`
Group.Child2 = tw.div`group-hover:font-bold`
export default () => (
Child 1Child 2
)
```
## Attrs in emotion
Unfortunately emotion [doesn’t have any plans](https://github.com/emotion-js/emotion/issues/821) to add `attrs` so the easiest option is to add `className="group"` directly on the jsx element:
```js
import tw from 'twin.macro'
const Group = tw.button``
Group.Child1 = tw.div`group-hover:bg-black`
Group.Child2 = tw.div`group-hover:font-bold`
export default () => (
Child 1Child 2
)
```
But if you’d like similar functionality to the attr function in styled-components then you could add the className using a [Higher-Order Component (HOC)](https://reactjs.org/docs/higher-order-components.html):
```js
import tw from 'twin.macro'
const withAttrs = (Component, attrs) => props =>
const Button = tw.button``
const Group = withAttrs(Button, { className: 'group' })
Group.Child1 = tw.div`group-hover:bg-black`
Group.Child2 = tw.div`group-hover:font-bold`
export default () => (
Child 1Child 2
)
```
You could also use `defaultProps` but it’s [going to be deprecated at some stage](https://twitter.com/dan_abramov/status/1133878326358171650), which is a shame because it’s a really nice way to add the className:
```js
import tw from 'twin.macro'
const Group = tw.button``
Group.defaultProps = { className: 'group' }
Group.Child1 = tw.div`group-hover:bg-black`
Group.Child2 = tw.div`group-hover:font-bold`
export default () => (
Child 1Child 2
)
```
## Resources
- [Quick Start Guide to Attrs in styled-components](https://scalablecss.com/styled-components-attrs/)
- [Emotion issue: .attrs equivalent](https://github.com/emotion-js/emotion/issues/821)
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/index.md
================================================
[](#documentation)
# Documentation
[](#usage)
## Usage
- [The prop styling guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/prop-styling-guide.md)
- [Styled component guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/styled-component-guide.md)
[](#configuration)
## Configuration
- [Twin config options](https://github.com/ben-rogerson/twin.macro/blob/master/docs/options.md)
- [Customizing the tailwind config](https://github.com/ben-rogerson/twin.macro/blob/master/docs/customizing-config.md)
- [Fonts](https://github.com/ben-rogerson/twin.macro/blob/master/docs/fonts.md)
[](#theming)
## Theming
- [Theming with css variables](https://github.com/ben-rogerson/twin.macro/blob/master/docs/advanced-theming.md)
[](#classes)
## More
- [group](https://github.com/ben-rogerson/twin.macro/blob/master/docs/group.md)
================================================
FILE: docs/options.md
================================================
[](#twin-config-options)
# Twin config options
These options are available in your [twin config](#twin-config-location):
| Name | Default | Description |
| --------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| config | `"tailwind.config.js"` | The path to your Tailwind config. Also takes a config object. |
| preset | `"emotion"` | The css-in-js library behind the scenes. Also supported: `"styled-components"` `"goober"` `"stitches"` `"solid"` |
| dataTwProp | `true` | Add a prop to jsx components in development showing the original tailwind classes. Use `"all"` to keep the prop in production. |
| debug | `false` | Display information in your terminal about the Tailwind class conversions. |
| disableShortCss | `true` | Disable converting short css within the tw import/prop. |
| hasLogColors | `true` | Disable log colors to remove the glyphs when the color display is not supported |
| includeClassNames | `false` | Check className attributes for tailwind classes to convert. |
| dataCsProp | `true` | Add a prop to your elements in development so you can see the original cs prop classes, eg: ``. |
| disableCsProp | `true` | Disable twin from reading values specified in the cs prop. |
| sassyPseudo | `false` | Some css-in-js frameworks require the `&` in selectors like `&:hover`, this option ensures it’s added. |
| moveKeyframesToGlobalStyles | `false` | `@keyframes` are added next to the `animation-x` classes - this option can move them to global styles instead. |
### Options
---
config
```js
config: 'tailwind.config.js', // Path to the tailwind config
```
Set a custom location by specifying a path to your tailwind.config.js file.
**Passing in a config**: The config option also accepts a config object:
```js
// babel-plugin-macros.config.js
const tailwindConfig = {
theme: {
extend: {
colors: {
primary: '#ff0000',
},
},
},
}
module.exports = {
twin: {
config: tailwindConfig,
},
}
```
This can be useful in component libraries, tests, or just to remove the need for a tailwind.config.js file.
**Monorepos / Workspaces**: The tailwind.config.js is commonly added as a shared file in the project root so you may need to add a `path.resolve` on the pathname in the twin config:
```js
// babel-plugin-macros.config.js
const path = require('path')
module.exports = {
twin: {
config: path.resolve(__dirname, '../../', 'tailwind.config.js'),
},
}
```
---
preset
```js
preset: 'emotion', // Set the css-in-js library to use with twin
```
Supports: `'emotion'` / `'styled-components'` / `'goober'` / `'stitches'`.
The preset option primarily assigns the library imports for `css`, `styled` and `GlobalStyles`.
---
dataTwProp
```js
dataTwProp: false, // Set the display of the data-tw prop on jsx elements
```
The `data-tw` prop gets added to your elements while in development so you can see the original tailwind classes:
```js
```
If you add the value `all`, twin will add the data-tw prop in production as well as development.
---
debug
```js
debug: true, // Display information about class conversions
```
When debug mode is on, twin displays logs on class conversions.
This feedback only displays in development.
##
---
hasLogColors
```js
hasLogColors: false, // Disable log colors (removes those glyphs in your console/overlay)
```
Sometimes the display of errors and suggestions are pretty poor due to lack of support for custom colors. Use this setting to disable the colors so you can actually read the messages.
---
disableShortCss
```js
disableShortCss: false, // Enable converting short css within the tw import/prop
```
When set to `true`, this will throw an error if short css is added within the tw import or tw prop.
Disable short css completely with `dataCsProp: false`.
---
includeClassNames
```js
includeClassNames: true, // Check className props for tailwind classes to convert
```
When a tailwind class is found in a className prop, it’s plucked out, converted and delivered to the css-in-js library.
- Unmatched classes are skipped and preserved within the className
- Suggestions aren’t shown for unmatched classes like they are for the tw prop
- The tw and css props can be used on the same jsx element
- Limitation: classNames with conditional props or variables aren’t touched, eg: ``
---
dataCsProp
```js
dataCsProp: false, // JSX prop twin adds that shows the original cs prop classes
```
If you add short css within the `cs` prop then twin will add a `data-cs` prop to preserve the css you added.
This option controls the display of the prop.
Shows in development only.
---
disableCsProp
```js
disableCsProp: true, // Whether to read short css values added in a `cs` prop
```
If you're using the cs prop for something else or don’t want other developers using the feature you can disable it with this option.
---
sassyPseudo
```js
sassyPseudo: true, // Prefix pseudo selectors with a `&`
```
Some css-in-js frameworks require the `&` in selectors like `&:hover`, this option ensures it’s added.
---
moveKeyframesToGlobalStyles
```js
moveKeyframesToGlobalStyles: true, // Avoid @keyframes next to animation-x classes
```
Add `@keyframes` matching an `animation-x` class to global styles instead of alongside the `animation-x` class.
In stitches this gets set to `true` to make animations work.
---
[](#twin-config-location)
## Twin config location
Twin’s config can be added in a couple of different files.
a) Either in `babel-plugin-macros.config.js`:
```js
// babel-plugin-macros.config.js
module.exports = {
twin: {
// add options here
},
}
```
b) Or in `package.json`:
```js
// package.json
"babelMacros": {
"twin": {
// add options here
}
},
```
---
[‹ Documentation index](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/prop-styling-guide.md
================================================
# The prop styling guide
## Basic styling
Use Twin’s tw prop to add Tailwind classes onto jsx elements:
```js
import 'twin.macro'
const Component = () => (
)
```
- Use the tw prop when conditional styles aren’t needed
- Any import from `twin.macro` activates the tw prop
- Remove the need for an import with [babel-plugin-twin](https://github.com/ben-rogerson/babel-plugin-twin)
## Conditional styling
To add conditional styles, nest the styles in an array and use the `css` prop:
```js
import tw from 'twin.macro'
const Component = ({ hasBg }) => (
)
```
- Twin doesn’t own the css prop, the prop comes from your css-in-js library
- Adding values to an array makes it easier to define base styles, conditionals and vanilla css
- Use multiple lines to organize styles within the backticks ([template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))
## Overriding styles
Use the `tw` prop after the css prop to add any overriding styles:
```js
import tw from 'twin.macro'
const Component = () => (
Has black text
)
```
## Keeping jsx clean
It’s no secret that when tailwind class sets become larger, they obstruct the readability of other jsx props.
To clean up the jsx, lift the styles out and group them as named entries in an object:
```js
import tw from 'twin.macro'
const styles = {
container: ({ hasBg }) => [
tw`flex w-full`, // Add base styles first
hasBg && tw`bg-black`, // Then add conditional styles
],
column: tw`w-1/2`,
}
const Component = ({ hasBg }) => (
)
```
TypeScript example
```js
import tw from 'twin.macro'
interface ContainerProps {
hasBg?: boolean;
}
const styles = {
container: ({ hasBg }: ContainerProps) => [
tw`flex w-full`, // Add base styles first
hasBg && tw`bg-black`, // Then add conditional styles
],
column: tw`w-1/2`,
}
const Component = ({ hasBg }: ContainerProps) => (
)
```
## Variants with many values
When a variant has many values (eg: `variant="light/dark/etc"`), name the class set in an object and use a prop to grab the entry containing the styles:
```js
import tw from 'twin.macro'
const containerVariants = {
// Named class sets
light: tw`bg-white text-black`,
dark: tw`bg-black text-white`,
crazy: tw`bg-yellow-500 text-red-500`,
}
const styles = {
container: ({ variant = 'dark' }) => [
tw`flex w-full`,
containerVariants[variant], // Grab the variant style via a prop
],
column: tw`w-1/2`,
}
const Component = ({ variant }) => (
)
```
TypeScript example
Use the `TwStyle` import to type tw blocks:
```tsx
import tw, { TwStyle } from 'twin.macro'
type WrapperVariant = 'light' | 'dark' | 'crazy'
interface ContainerProps {
variant?: WrapperVariant
}
const containerVariants: Record = {
// Named class sets
light: tw`bg-white text-black`,
dark: tw`bg-black text-white`,
crazy: tw`bg-yellow-500 text-red-500`,
}
const styles = {
container: ({ variant = 'dark' }: ContainerProps) => [
tw`flex w-full`,
containerVariants[variant], // Grab the variant style via a prop
],
column: tw`w-1/2`,
}
const Component = ({ variant }: ContainerProps) => (
)
```
## Interpolation workaround
Due to Babel limitations, tailwind classes and arbitrary properties can’t have any part of them dynamically created.
So interpolated values like this won’t work:
```js
// Won't work with tailwind classes
// Won't work with arbitrary properties
```
This is because babel doesn’t know the values of the variables and so twin can’t make a conversion to css.
Instead, define the classes in objects and grab them using props:
```js
import tw from 'twin.macro'
const styles = { sm: tw`mt-2`, lg: tw`mt-4` }
const Component = ({ spacing = 'sm' }) =>
```
Or combine vanilla css with twins `theme` import:
```js
import { theme } from 'twin.macro'
// Use theme values from your tailwind config
const styles = { sm: theme`spacing.2`, lg: theme`spacing.4` }
const Component = ({ spacing = 'sm' }) => (
)
```
Or we can always fall back to vanilla css, which can interpolate anything:
```js
import 'twin.macro'
const Component = ({ width = 5 }) =>
```
## Custom selectors (Arbitrary variants)
Use square-bracketed arbitrary variants to style elements with a custom selector:
```js
import tw from 'twin.macro'
const buttonStyles = tw`
bg-black
[> i]:block
[> span]:(text-blue-500 w-10)
`
const Component = () => (
)
```
More examples
```js
// Style the current element based on a theming/scoping className
;
// Style the current element based on a dynamic className
const Component = ({ isLarge }) => (
...
)
```
## Custom class values (Arbitrary values)
Custom values can be added to many tailwind classes by using square brackets to define the custom value:
```js
;
// ↓ ↓ ↓ ↓ ↓ ↓
```
[Read more about Arbitrary values →](https://github.com/ben-rogerson/twin.macro/blob/master/docs/arbitrary-values.md)
## Custom css
Basic css is added using [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties) or within vanilla css which supports more advanced use cases like dynamic/interpolated values.
### Simple css styling
To add simple custom styling, use [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties):
```js
// Set css variables
// Set vendor prefixes
// Set grid areas
```
Use arbitrary properties with variants or twins grouping features:
```js
```
Arbitrary properties also work with the `tw` import:
```js
import tw from 'twin.macro'
;
```
- Add a bang to make the custom css !important: `![grid-area:1 / 1 / 4 / 2]`
- Arbitrary properties can have camelCase properties: `[gridArea:1 / 1 / 4 / 2]`
### Advanced css styling
The css prop accepts a sass-like syntax, allowing both custom css and tailwind styles with values that can come from your tailwind config:
```js
import tw, { css, theme } from 'twin.macro'
const Components = () => (
)
```
But it’s often cleaner to use an object to add styles as it avoids the interpolation cruft seen above:
```js
import tw, { css, theme } from 'twin.macro'
const Components = () => (
)
```
## Learn more
- [Styled component guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/styled-component-guide.md) - A must-read guide on getting productive with styled-components
## Resources
- [babel-plugin-twin](https://github.com/ben-rogerson/babel-plugin-twin) - Use the tw and css props without adding an import
- [React + Tailwind breakpoint syncing](https://gist.github.com/ben-rogerson/b4b406dffcc18ae02f8a6c8c97bb58a8) - Sync your tailwind.config.js breakpoints with react
- [Twin VSCode snippits](https://gist.github.com/ben-rogerson/c6b62508e63b3e3146350f685df2ddc9) - For devs who want to type less
- [Twin VSCode extensions](https://github.com/ben-rogerson/twin.macro/discussions/227) - For faster class suggestions and feedback
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/screen-import.md
================================================
# Screen import
The screen import creates media queries for custom css that sync with your tailwind config screen values (sm, md, lg, etc).
**Usage with the css prop**
```js
import tw, { screen, css } from 'twin.macro'
const styles = [
screen`sm`({ display: 'block', ...tw`inline` }),
]
```
**Usage with styled components**
```js
import tw, { styled, screen, css } from 'twin.macro'
const Component = styled.div(() => [
screen`sm`({ display: 'block', ...tw`inline` }),
])
```
## Screen as a key
Without the styles, the screen import just creates a media query, so you can use it as a key:
```js
// ↓ ↓ ↓ ↓ ↓ ↓
```
## Relaxed usage
The screen import can be used in different ways:
```js
screen`sm`({ ... })
screen('sm')({ ... })
screen(`sm`)({ ... })
screen.sm({ ... }) // Dot syntax can’t be used when the screen begins with a number, eg: screen.2xl
```
## Custom media queries
Since the screen import always adds a min-width query, it’s not suitable for constructing custom media queries.
So to add custom media queries, use the theme import instead.
**With the css prop**
```js
import tw, { theme } from 'twin.macro'
const styles = {
[`@media (max-width: ${theme`screens.sm`})`]: {
display: 'block',
...tw`inline`,
},
}
;
```
**With a styled component**
```js
import tw, { styled, theme } from 'twin.macro'
const Component = styled.div({
[`@media (max-width: ${theme`screens.sm`})`]: {
display: 'block',
...tw`inline`,
},
})
```
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: docs/styled-component-guide.md
================================================
# Styled component guide
## Basic styling
Use Twin’s `tw` import to create and style new components with Tailwind classes:
```js
import tw from 'twin.macro'
const Wrapper = tw.section`flex w-full`
const Column = tw.div`w-1/2`
const Component = () => (
)
```
## Conditional styling
To add conditional styles, nest the styles in an array and use the `styled` import:
```js
import tw, { styled } from 'twin.macro'
const Container = styled.div(({ hasBg }) => [
tw`flex w-full`, // Add base styles first
hasBg && tw`bg-black`, // Then add conditional styles
])
const Column = tw.div`w-1/2`
const Component = ({ hasBg }) => (
)
```
TypeScript example
```tsx
import tw, { styled } from 'twin.macro'
interface ContainerProps {
hasBg?: string
}
const Container = styled.div(({ hasBg }) => [
tw`flex w-full`, // Add base styles first
hasBg && tw`bg-black`, // Then add conditional styles
])
const Column = tw.div`w-1/2`
const Component = ({ hasBg }: ContainerProps) => (
)
```
- Adding styles in an array makes it easier to separate base styles, conditionals and vanilla css
- The `styled` import comes from the css-in-js library, twin just exports it
- Use multiple lines to organize styles within the backticks ([template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))
## Variants with many values
When a variant has many values (eg: `variant="light/dark/etc"`), name the class set in an object and use a prop to grab the entry containing the styles. Note that you must return a function as follows:
```js
import tw, { styled } from 'twin.macro'
const containerVariants = {
// Named class sets
light: tw`bg-white text-black`,
dark: tw`bg-black text-white`,
crazy: tw`bg-yellow-500 text-red-500`,
}
const Container = styled.section(() => [
// Return a function here
tw`flex w-full`,
({ variant = 'dark' }) => containerVariants[variant], // Grab the variant style via a prop
])
const Column = tw.div`w-1/2`
const Component = () => (
)
```
TypeScript example
Use the `TwStyle` import to type tw blocks:
```tsx
import tw, { styled, TwStyle } from 'twin.macro'
type ContainerVariant = 'light' | 'dark' | 'crazy'
interface ContainerProps {
variant?: ContainerVariant
}
// Use the `TwStyle` import to type tw blocks
const containerVariants: Record = {
// Named class sets
light: tw`bg-white text-black`,
dark: tw`bg-black text-white`,
crazy: tw`bg-yellow-500 text-red-500`,
}
const Container = styled.section(() => [
// Return a function here
tw`flex w-full`,
({ variant = 'dark' }) => containerVariants[variant], // Grab the variant style via a prop
])
const Column = tw.div`w-1/2`
const Component = () => (
)
```
## Interpolation workaround
Due to Babel limitations, tailwind classes and arbitrary properties can’t have any part of them dynamically created.
So interpolated values like this won’t work:
```js
const Component = styled.div(({ spacing }) => [
tw`mt-${spacing === 'sm' ? 2 : 4}`, // Won't work with tailwind classes
`[margin-top:${spacing === 'sm' ? 2 : 4}rem]`, // Won't work with arbitrary properties
])
```
This is because babel doesn’t know the values of the variables and so twin can’t make a conversion to css.
Instead, define the classes in objects and grab them using props:
```js
import tw, { styled } from 'twin.macro'
const styles = { sm: tw`mt-2`, lg: tw`mt-4` }
const Card = styled.div(({ spacing }) => styles[spacing])
const Component = ({ spacing = 'sm' }) =>
```
Or combine vanilla css with twins `theme` import:
```js
import { styled, theme } from 'twin.macro'
// Use theme values from your tailwind config
const styles = { sm: theme`spacing.2`, lg: theme`spacing.4` }
const Card = styled.div(({ spacing }) => ({ marginTop: styles[spacing] }))
const Component = ({ spacing = 'sm' }) =>
```
Or you can always fall back to “vanilla css” which can interpolate anything:
```js
import { styled } from 'twin.macro'
const Card = styled.div(({ spacing }) => ({
marginTop: `${spacing === 'sm' ? 2 : 4}rem`,
}))
const Component = ({ spacing = 'sm' }) =>
```
## Overriding styles
You can use the `tw` jsx prop to override styles in the styled-component:
```tsx
import tw from 'twin.macro'
const Text = tw.div`text-white`
const Component = () => Has black text
```
## Extending components
Wrap components using the component extending feature to copy/override styles from another component:
```tsx
import tw, { styled } from 'twin.macro'
const Container = tw.div`bg-black text-white`
// Extend with tw: for basic styling
const BlueContainer = tw(Container)`bg-blue-500`
// Or extend with styled: For conditionals
const RedContainer = styled(Container)(({ hasBorder }) => [
tw`bg-red-500 text-black`,
hasBorder && tw`border`,
])
// Extending more than once like this is not recommended
const BlueContainerBold = tw(BlueContainer)`font-bold`
const Component = () => (
<>
>
)
```
## Changing elements
Reuse styled components with a different element using the `as` prop:
```tsx
import tw from 'twin.macro'
const Heading = tw.h1`font-bold` // or styled.h1(tw`font-bold`)
const Component = () => (
<>
I am a H1I am a H2 with the same style
>
)
```
## Custom selectors (Arbitrary variants)
Use square-bracketed arbitrary variants to style elements with a custom selector:
```js
import tw from 'twin.macro'
const Button = tw.button`
bg-black
[> i]:block
[> span]:(text-blue-500 w-10)
`
const Component = () => (
)
```
More examples
```js
// Style the current element based on a theming/scoping className
const Theme = tw.div`[.dark-theme &]:(bg-black text-white)`
;
Dark theme
// Add custom group selectors
const Text = tw.div`[.group:disabled &]:text-gray-500`
;
// Add custom height queries
const SmallHeightOnly = tw.div`[@media (min-height: 800px)]:hidden`
;Burger menu
// Use custom at-rules like @supports
const Grid = tw.div`[@supports (display: grid)]:grid`
;A grid
// Style the component based on a dynamic className
const Text = tw.div`text-base [&.is-large]:text-lg`
const Container = ({ isLarge }) => (
...
)
```
## Custom class values (Arbitrary values)
Custom values can be added to many tailwind classes by using square brackets to define the custom value:
```js
tw.div`top-[calc(100vh - 2rem)]`
// ↓ ↓ ↓ ↓ ↓ ↓
styled.div({ top: 'calc(100vh - 2rem)' })
```
[Read more about Arbitrary values →](https://github.com/ben-rogerson/twin.macro/blob/master/docs/arbitrary-values.md)
## Custom css
Basic css is added using [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties) or within vanilla css which supports more advanced use cases like dynamic/interpolated values.
### Simple css styling
To add simple custom styling, use [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties):
```js
// Set css variables
tw.div`[--my-width-variable:calc(100vw - 10rem)]`
// Set vendor prefixes
tw.div`[-webkit-line-clamp:3]`
// Set grid areas
tw.div`[grid-area:1 / 1 / 4 / 2]`
```
Use arbitrary properties with variants or twins grouping features:
```js
tw.div`block md:(relative [grid-area:1 / 1 / 4 / 2])`
```
Use a theme value to grab a value from your tailwind.config:
```js
tw.div`[color:theme('colors.gray.300')]`
tw.div`[margin:theme('spacing[2.5]')]`
tw.div`[box-shadow: 5px 10px theme('colors.black')]`
```
- Add a bang to make the custom css !important: `![grid-area:1 / 1 / 4 / 2]`
- Arbitrary properties can have camelCase properties: `[gridArea:1 / 1 / 4 / 2]`
### Advanced css styling
The styled import accepts a sass-like syntax, allowing both custom css and tailwind styles with values that can come from your tailwind config:
```js
import tw, { styled, css, theme } from 'twin.macro'
const Input = styled.div`
${css`
-webkit-tap-highlight-color: transparent; /* add css styles */
background-color: ${theme`colors.red.500`}; /* add values from your tailwind config */
${tw`text-blue-500 border-2`}; /* tailwind classes */
&::selection {
${tw`text-purple-500`}; /* style custom css selectors with tailwind classes */
}
`}
`
const Component = () =>
```
- Prefix css styles with the `css` import to apply css highlighting in your editor
- Add semicolons to the end of each line
It can be cleaner to use an object to add styles as it avoids the interpolation cruft seen in the last example:
```js
import tw, { styled, theme } from 'twin.macro'
const Input = styled.div({
WebkitTapHighlightColor: 'transparent', // css properties are camelCased
backgroundColor: theme`colors.red.500`, // values don’t require interpolation
...tw`text-blue-500 border-2`, // merge tailwind classes into the container
'&::selection': tw`text-purple-500`, // allows single-line tailwind selector styling
})
const Component = () =>
```
### Mixing css with tailwind classes
Mix tailwind classes and custom css in an array:
```js
import tw, { styled } from 'twin.macro'
const Input = styled.div(({ tapColor }) => [
tw`block`,
`-webkit-tap-highlight-color: ${tapColor};`,
])
const Component = () =>
```
When you move the styles out of jsx, prefix them with the `css` import:
```js
import tw, { styled, css } from 'twin.macro'
const widthStyles = ({ tapColor }) => css`
-webkit-tap-highlight-color: ${tapColor};
`
const Input = styled.div(({ tapColor }) => [
tw`block`,
widthStyles({ tapColor }),
])
const Component = () =>
```
## Learn more
- [Prop styling guide](https://github.com/ben-rogerson/twin.macro/blob/master/docs/prop-styling-guide.md) - A must-read guide to level up on prop styling
## Resources
- [babel-plugin-twin](https://github.com/ben-rogerson/babel-plugin-twin) - Use the tw and css props without adding an import
- [React + Tailwind breakpoint syncing](https://gist.github.com/ben-rogerson/b4b406dffcc18ae02f8a6c8c97bb58a8) - Sync your tailwind.config.js breakpoints with react
- [Twin VSCode snippits](https://gist.github.com/ben-rogerson/c6b62508e63b3e3146350f685df2ddc9) - For devs who want to type less
- [Twin VSCode extensions](https://github.com/ben-rogerson/twin.macro/discussions/227) - For faster class suggestions and feedback
---
[‹ Documentation](https://github.com/ben-rogerson/twin.macro/blob/master/docs/index.md)
================================================
FILE: jest.config.ts
================================================
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': 'ts-jest',
},
transformIgnorePatterns: ['/node_modules/', '/types'],
setupFilesAfterEnv: ['/tests/util/customMatchers.ts'],
}
================================================
FILE: package.json
================================================
{
"name": "twin.macro",
"version": "3.4.1",
"description": "Twin blends the magic of Tailwind with the flexibility of css-in-js",
"main": "macro.js",
"types": "types/index.d.ts",
"scripts": {
"dev": "concurrently npm:dev:* -p none",
"dev:macro": "NODE_ENV=dev nodemon -q --watch 'src/**/*.ts' --watch package.json -x \"npm run build:macro\" --delay .01",
"dev:sandbox": "NODE_ENV=dev nodemon -q --watch sandbox/in.tsx --watch package.json --watch macro.js -x \"npm run build:sandbox\" --delay .01",
"build": "npm run build:macro",
"build:macro": "microbundle -i src/macro.ts -f cjs -o ./macro.js --target node",
"build:sandbox": "babel sandbox/in.tsx --out-file sandbox/out.tsx",
"test": "npm run build && jest && npm run test:types",
"test:types": "tsc -b ./types/tsconfig.json",
"test:update": "npm run build && jest --u",
"prepublishOnly": "npm run build"
},
"nodemonConfig": {
"ignore": [],
"watch": [
"src"
],
"ext": "ts",
"delay": "0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"engines": {
"node": ">=16.14.0"
},
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint src --cache --fix",
"jest plugin.test.js"
],
"*.{js,ts,jsx,tsx,json,md}": [
"prettier --write"
]
},
"files": [
"macro.js",
"types/index.d.ts"
],
"repository": {
"type": "git",
"url": "git+https://github.com/ben-rogerson/twin.macro.git"
},
"keywords": [
"emotion",
"styled-components",
"stitches",
"goober",
"tailwind",
"tailwindcss",
"css-in-js",
"babel-plugin",
"babel-plugin-macros"
],
"author": "Ben Rogerson ",
"license": "MIT",
"bugs": {
"url": "https://github.com/ben-rogerson/twin.macro/issues"
},
"homepage": "https://github.com/ben-rogerson/twin.macro#readme",
"peerDependencies": {
"tailwindcss": ">=3.3.1"
},
"dependencies": {
"@babel/template": "^7.22.15",
"babel-plugin-macros": "^3.1.0",
"chalk": "4.1.2",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "^6.0.13"
},
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"@types/babel-plugin-macros": "^2.8.5",
"@types/didyoumean": "^1.2.0",
"@types/dlv": "^1.1.2",
"@types/jest": "^29.2.2",
"@types/lodash.flatmap": "^4.5.7",
"@types/lodash.get": "^4.4.7",
"@types/lodash.merge": "^4.6.7",
"@types/react": "^18.0.25",
"@types/string-similarity": "^4.0.0",
"@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"babel-plugin-tester": "^10.1.0",
"concurrently": "^7.5.0",
"daisyui": "^2.38.0",
"escalade": "^3.1.1",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-xo": "^0.42.0",
"eslint-config-xo-react": "^0.27.0",
"eslint-config-xo-space": "^0.33.0",
"eslint-config-xo-typescript": "^0.53.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.4",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unicorn": "^44.0.2",
"glob-all": "^3.3.0",
"husky": "4.3.8",
"jest": "^29.2.2",
"lint-staged": "^13.0.3",
"microbundle": "^0.15.1",
"nodemon": "^2.0.20",
"prettier": "^2.8.7",
"react": "^18.2.0",
"string-similarity": "^4.0.4",
"styled-components": "^5.3.6",
"tailwindcss-typography": "3.1.0",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
}
}
================================================
FILE: sandbox/in.tsx
================================================
/**
* Twin Sandbox
* A place to test the output twin creates.
* Good for general testing or for developing new features.
*
* Getting started
* 1. Run the script: `npm run dev`
* 2. Make a change to this file or to a file in the `src` folder
* 3. Check `sandbox/out.tsx` for the macro output
*/
// @ts-nocheck
import tw, { globalStyles, css, styled, screen, theme } from '../macro'
// Tw block
tw`bg-black/25`
// Styled component
tw.div`bg-black/25`
// Inline tw
;
================================================
FILE: src/core/constants.ts
================================================
const CLASS_SEPARATOR = /\S+/g
const DEFAULTS_UNIVERSAL = '*, ::before, ::after'
const EMPTY_CSS_VARIABLE_VALUE = 'var(--tw-empty,/*!*/ /*!*/)'
const PRESERVED_ATRULE_TYPES = new Set([
'charset',
'counter-style',
'document',
'font-face',
'font-feature-values',
'import',
'keyframes',
'namespace',
])
const LAYER_DEFAULTS = 'defaults'
const LINEFEED = /\n/g
const WORD_CHARACTER = /\w/
const SPACE_ID = '_'
const SPACE = /\s/
const SPACES = /\s+/g
export {
CLASS_SEPARATOR,
DEFAULTS_UNIVERSAL,
EMPTY_CSS_VARIABLE_VALUE,
PRESERVED_ATRULE_TYPES,
LAYER_DEFAULTS,
LINEFEED,
SPACE_ID,
SPACE,
SPACES,
WORD_CHARACTER,
}
================================================
FILE: src/core/createCoreContext.ts
================================================
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { getTailwindConfig, getConfigTwinValidated } from './lib/configHelpers'
import getStitchesPath from './lib/getStitchesPath'
import userPresets from './lib/userPresets'
import createTheme from './lib/createTheme'
import createAssert from './lib/createAssert'
import { createDebug } from './lib/logging'
import { createContext } from './lib/util/twImports'
import type {
PresetItem,
GetPackageUsed,
PossiblePresets,
CoreContext,
CreateCoreContext,
TwinConfig,
Assert,
} from './types'
function packageCheck(
packageToCheck: PossiblePresets,
params: GetPackageConfig,
hasNoFallback?: boolean
): boolean {
if (params.config && params.config.preset === packageToCheck) return true
if (hasNoFallback) return false
return (
params.styledImport.from.includes(packageToCheck) ||
params.cssImport.from.includes(packageToCheck)
)
}
type GetPackageConfig = {
config?: TwinConfig
cssImport: PresetItem
styledImport: PresetItem
}
function getPackageUsed(params: GetPackageConfig): GetPackageUsed {
return {
isEmotion: packageCheck('emotion', params),
isStyledComponents: packageCheck('styled-components', params, true),
isGoober: packageCheck('goober', params),
isSolid: packageCheck('solid', params),
isStitches: packageCheck('stitches', params),
}
}
type ConfigParameters = {
sourceRoot?: string
filename: string
config?: TwinConfig
assert: Assert
}
function getStyledConfig({
sourceRoot,
filename,
config,
}: ConfigParameters): PresetItem {
const usedConfig =
(config?.styled && config) ||
(config?.preset && userPresets[config.preset]) ||
userPresets.emotion
if (typeof usedConfig.styled === 'string') {
return { import: 'default', from: usedConfig.styled }
}
if (config && config.preset === 'stitches') {
const stitchesPath = getStitchesPath({ sourceRoot, filename, config })
if (stitchesPath && usedConfig.styled) {
// Overwrite the stitches import data with the path from the current file
usedConfig.styled.from = stitchesPath
}
}
return usedConfig.styled as PresetItem
}
function getCssConfig({
sourceRoot,
filename,
config,
}: ConfigParameters): PresetItem {
const usedConfig =
(config?.css && config) ||
(config?.preset && userPresets[config.preset]) ||
userPresets.emotion
if (typeof usedConfig.css === 'string') {
return { import: 'css', from: usedConfig.css }
}
if (config && config.preset === 'stitches') {
const stitchesPath = getStitchesPath({ sourceRoot, filename, config })
if (stitchesPath && usedConfig.css) {
// Overwrite the stitches import data with the path from the current file
usedConfig.css.from = stitchesPath
}
}
return usedConfig.css as PresetItem
}
function getGlobalConfig(config: TwinConfig): PresetItem {
const usedConfig =
(config.global && config) ||
(config.preset && userPresets[config.preset]) ||
userPresets.emotion
return usedConfig.global as PresetItem
}
function createCoreContext(params: CreateCoreContext): CoreContext {
const { sourceRoot, filename, config, isDev = false, CustomError } = params
const assert = createAssert(CustomError, false, config?.hasLogColors)
const configParameters = {
sourceRoot,
assert,
filename: filename ?? '',
config,
}
const styledImport = getStyledConfig(configParameters)
const cssImport = getCssConfig(configParameters)
const tailwindConfig =
params.tailwindConfig ?? getTailwindConfig(configParameters)
const packageUsed = getPackageUsed({ config, cssImport, styledImport })
const twinConfig = getConfigTwinValidated(config, { ...packageUsed, isDev })
const importConfig = {
styled: styledImport,
css: cssImport,
global: getGlobalConfig(config ?? {}),
}
return {
isDev,
assert,
debug: createDebug(isDev, twinConfig),
theme: createTheme(tailwindConfig),
tailwindContext: createContext(tailwindConfig),
packageUsed,
tailwindConfig,
twinConfig,
CustomError,
importConfig,
}
}
export default createCoreContext
================================================
FILE: src/core/extractRuleStyles.ts
================================================
import camelize from './lib/util/camelize'
import deepMerge from './lib/util/deepMerge'
import get from './lib/util/get'
import replaceThemeValue from './lib/util/replaceThemeValue'
import sassifySelector from './lib/util/sassifySelector'
import { splitAtTopLevelOnly, unescape } from './lib/util/twImports'
import {
DEFAULTS_UNIVERSAL,
EMPTY_CSS_VARIABLE_VALUE,
PRESERVED_ATRULE_TYPES,
LAYER_DEFAULTS,
LINEFEED,
} from './constants'
import getStyles from './getStyles'
import type { ExtractRuleStyles, CssObject, TransformDecl } from './types'
import type * as P from 'postcss'
const ESC_COMMA = /\\2c/g
const ESC_DIGIT = /\\3(\d)/g
const UNDERSCORE_ESCAPING = /\\+(_)/g
const SLASH_DOT_ESCAPING = /\\\./g
const BACKSLASH_ESCAPING = /\\\\/g
function transformImportant(value: string, params: TransformDecl): string {
if (params.passChecks === true) return value
if (!params.hasImportant) return value
// Avoid adding important if the rule doesn't respect it
if (params.hasImportant && params.options?.respectImportant === false)
return value
return `${value} !important`
}
function transformEscaping(value: string): string {
return (
value
.replace(UNDERSCORE_ESCAPING, '$1')
// Remove slash dot encoding in values
// eg: calc(\\.5 * .25rem)
.replace(SLASH_DOT_ESCAPING, '.')
// Fix the duplicate escaping babel delivers
.replace(BACKSLASH_ESCAPING, '\\')
)
}
const transformValueTasks = [
replaceThemeValue,
transformImportant,
transformEscaping,
]
function transformDeclValue(value: string, params: TransformDecl): string {
const valueOriginal = value
for (const task of transformValueTasks) {
value = task(value, params)
}
if (value !== valueOriginal)
params.debug('converted theme/important', {
old: valueOriginal,
new: value,
})
return value
}
function extractFromRule(
rule: P.Rule,
params: ExtractRuleStyles
): [string, CssObject] {
const selectorForUnescape = rule.selector.replace(ESC_DIGIT, '$1') // Remove digit escaping
const selector = unescape(selectorForUnescape).replace(LINEFEED, ' ')
return [selector, extractRuleStyles(rule.nodes, params)] as [
string,
CssObject
]
}
function extractSelectorFromAtRule(
name: string,
value: string,
params: ExtractRuleStyles
): string | undefined {
if (name === LAYER_DEFAULTS) {
if (params.includeUniversalStyles === false) return
return DEFAULTS_UNIVERSAL
}
const val = value.replace(ESC_COMMA, ',')
// Handle @screen usage in plugins, eg: `@screen md`
if (name === 'screen') {
const screenConfig = get(params, 'tailwindConfig.theme.screens') as Record<
string,
string
>
return `@media (min-width: ${screenConfig[val]})`
}
return `@${name} ${val}`.trim()
}
const ruleTypes = {
decl(decl: P.Declaration, params: ExtractRuleStyles): CssObject | undefined {
const property = decl.prop.startsWith('--')
? decl.prop
: camelize(decl.prop)
const value =
decl.prop.startsWith('--') && decl.value === ' '
? EMPTY_CSS_VARIABLE_VALUE // valid empty value in js, unlike ` `
: transformDeclValue(decl.value, { ...params, decl, property })
if (value === null) return
// `background-clip: text` is still in "unofficial" phase and needs a
// prefix in Firefox, Chrome and Safari.
// https://caniuse.com/background-img-opts
if (
property === 'backgroundClip' &&
(value === 'text' || value === 'text !important')
)
return {
WebkitBackgroundClip: value,
[property]: value,
}
return { [property]: value }
},
// General styles, eg: `{ display: block }`
rule(rule: P.Rule, params: ExtractRuleStyles): CssObject | undefined {
if (!rule.selector) {
if (rule.nodes) {
const styles = extractRuleStyles(rule.nodes, params)
params.debug('rule has no selector, returning nodes', styles)
return styles
}
params.debug('no selector found in rule', rule, 'error')
return
}
let [selector, styles] = extractFromRule(rule, params)
if (selector && styles === null) return
if (params.passChecks) {
const out = selector ? { [selector]: styles } : styles
params.debug('style pass return', out)
return out
}
params.debug('styles extracted', { selector, styles })
// As classes aren't used in css-in-js we split the selector into
// multiple selectors and strip the ones that don't affect the current
// element, eg: In `.this, .sub`, .sub is stripped as it has no target
const selectorList = [...splitAtTopLevelOnly(selector, ',')].filter(s => {
// Match the selector as a class
const result = params.selectorMatchReg?.test(s)
// Only keep selectors if they contain a `&` || aren’t
// targeting multiple elements with classes
if (!result && (s.includes('&') || !s.includes('.'))) return true
return result
})
if (selectorList.length === 0) {
params.debug('no selector match', selector, 'warn')
return
}
if (selectorList.length === 1)
params.debug('matched whole selector', selectorList[0])
if (selectorList.length > 1)
params.debug('matched multiple selectors', selectorList)
selector = selectorList
.map(s =>
sassifySelector(
s,
params as ExtractRuleStyles & {
selectorMatchReg: RegExp
sassyPseudo: boolean
}
)
)
.filter(Boolean)
.join(',')
params.debug('sassified key', selector || styles)
if (!selector) return styles
return { [selector]: styles }
},
// At-rules, eg: `@media __` && `@screen md`
atrule(atrule: P.AtRule, params: ExtractRuleStyles): CssObject | undefined {
const selector = extractSelectorFromAtRule(
atrule.name,
atrule.params,
params
)
if (!selector) {
params.debug(
'no atrule selector found, removed',
{ name: atrule.name, params: atrule.params },
'warn'
)
return
}
// Add @apply support in plugins
if (selector.startsWith('@apply ')) {
const { styles, unmatched } = getStyles(
selector.slice(7),
params.coreContext
)
params.coreContext.assert(unmatched.length === 0, ({ color }) => {
const extraMessage =
selector === `@apply ${unmatched.join(' ')}`
? '.'
: ` as:\n\`${selector}\``
return `${color(
`✕ ${color(unmatched.join(' '), 'errorLight')} ${
unmatched.length > 1 ? 'classes' : 'class'
} can’t be used.\n\nThis is defined in a tailwind plugin${extraMessage}`
)}`
})
return styles
}
// Strip keyframes from animate-* classes
if (
selector.startsWith('@keyframes ') &&
!params.passChecks &&
params.twinConfig.moveKeyframesToGlobalStyles
)
return
if (PRESERVED_ATRULE_TYPES.has(atrule.name)) {
params.debug(`${atrule.name} pass given`, selector)
// Rules that pass checks have no further style transformations
params.passChecks = true
}
const styles = extractRuleStyles(atrule.nodes, params)
if (!styles) return
let ruleset = { [selector]: styles }
if (selector === DEFAULTS_UNIVERSAL) {
// Add a cloned backdrop style
ruleset = { ...ruleset, '::backdrop': styles }
params.debug('universal default', styles)
}
params.debug('atrule', selector)
return ruleset
},
}
type Styles = CssObject | undefined
function extractRuleStyles(nodes: P.Node[], params: ExtractRuleStyles): Styles {
const styles: Styles[] = nodes
.map((rule): CssObject | undefined => {
const handler = ruleTypes[rule.type as keyof typeof ruleTypes]
if (!handler) return
return handler(rule as never, params)
})
.filter(Boolean)
if (styles.length === 0) return undefined
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return deepMerge(styles[0], ...styles.slice(1))
}
export default extractRuleStyles
================================================
FILE: src/core/getGlobalStyles.ts
================================================
import deepMerge from './lib/util/deepMerge'
import extractRuleStyles from './extractRuleStyles'
import { LAYER_DEFAULTS } from './constants'
import type { CoreContext, CssObject, Candidate } from './types'
function getGlobalStyles(params: CoreContext): CssObject | undefined {
const candidates = [...params.tailwindContext.candidateRuleMap]
const globalPluginStyles = candidates
.flatMap(([, candidate]: [unknown, Candidate[]]) => {
const out = candidate.map(([data, rule]) => {
if (data.layer !== LAYER_DEFAULTS) return
return extractRuleStyles([rule], {
...params,
coreContext: params,
passChecks: true,
})
})
if (out.length === 0) return
return out
})
.filter(Boolean)
const [globalKey, preflightRules]: [string, Candidate[]] = candidates[0]
// @ts-expect-error TOFIX: Fix tuple type error
if (globalKey.trim() !== '*') return deepMerge(...globalPluginStyles)
// @ts-expect-error TOFIX: Fix tuple type error
if (!Array.isArray(preflightRules)) return deepMerge(...globalPluginStyles)
const preflightStyles = preflightRules.flatMap(([, rule]) =>
extractRuleStyles([rule], {
...params,
coreContext: params,
passChecks: true,
})
)
return deepMerge(
// @ts-expect-error TOFIX: Fix tuple type error
...preflightStyles,
...globalPluginStyles,
...globalKeyframeStyles(params)
)
}
function globalKeyframeStyles(
params: CoreContext
): Array> {
if (params.twinConfig.moveKeyframesToGlobalStyles === false) return []
const keyframes = params.theme('keyframes')
if (!keyframes) return []
return Object.entries(keyframes).map(
([name, frames]: [string, Record]) => ({
[`@keyframes ${name}`]: frames,
})
)
}
export default getGlobalStyles
================================================
FILE: src/core/getStyles.ts
================================================
import extractRuleStyles from './extractRuleStyles'
import createAssert from './lib/createAssert'
import expandVariantGroups from './lib/expandVariantGroups'
import deepMerge from './lib/util/deepMerge'
import { resolveMatches, splitAtTopLevelOnly } from './lib/util/twImports'
import escapeRegex from './lib/util/escapeRegex'
import convertClassName from './lib/convertClassName'
import { WORD_CHARACTER } from './constants'
import type {
CoreContext,
CssObject,
ExtractRuleStyles,
AssertContext,
TailwindMatch,
TailwindContext,
TailwindConfig,
Assert,
} from './types'
const IMPORTANT_OUTSIDE_BRACKETS =
/(:!|^!)(?=(?:(?:(?!\)).)*\()|[^()]*$)(?=(?:(?:(?!]).)*\[)|[^[\]]*$)/
const COMMENTS_MULTI_LINE = /(?
extractRuleStyles([rule], { ...params, options: data.options })
)
.filter(Boolean)
if (rulesets.length === 0) {
params.debug('no node rulesets found', {}, 'error')
return
}
// @ts-expect-error Avoid tuple type error
return deepMerge(...rulesets)
}
// When removing a multiline comment, determine if a space is left or not
// eg: You'd want a space left in this situation: tw`class1/* comment */class2`
function multilineReplaceWith(
match: string,
index: number,
input: string
): ' ' | '' {
const charBefore = input[index - 1]
const directPrefixMatch = charBefore && WORD_CHARACTER.exec(charBefore)
const charAfter = input[Number(index) + Number(match.length)]
const directSuffixMatch = charAfter && WORD_CHARACTER.exec(charAfter)
return directPrefixMatch?.[0] && directSuffixMatch && directSuffixMatch[0]
? ' '
: ''
}
function validateClasses(
classes: string,
{
assert,
tailwindConfig,
}: { tailwindConfig: TailwindConfig; assert: CoreContext['assert'] }
): boolean {
// TOFIX: Avoid counting brackets within arbitrary values
assert(
(classes.match(ALL_BRACKET_SQUARE_LEFT) ?? []).length ===
(classes.match(ALL_BRACKET_SQUARE_RIGHT) ?? []).length,
({ color }: AssertContext) =>
`${color(
`✕ Unbalanced square brackets found in classes:\n\n${color(
classes,
'errorLight'
)}`
)}`
)
// TOFIX: Avoid counting brackets within arbitrary values
assert(
(classes.match(ALL_BRACKET_ROUND_LEFT) ?? []).length ===
(classes.match(ALL_BRACKET_ROUND_RIGHT) ?? []).length,
({ color }: AssertContext) =>
`${color(
`✕ Unbalanced round brackets found in classes:\n\n${color(
classes,
'errorLight'
)}`
)}`
)
for (const className of splitAtTopLevelOnly(classes, ' ')) {
// Check for missing class attached to a variant
const classCheck = className.replace(ESCAPE_CHARACTERS, ' ').trim()
assert(
!classCheck.endsWith(tailwindConfig.separator ?? ':'),
({ color }: AssertContext) =>
`${color(
`✕ The variant ${String(
color(classCheck, 'errorLight')
)} doesn’t look right`
)}\n\nUpdate to ${String(
color(`${classCheck}block`, 'success')
)} or ${String(color(`${classCheck}(block mt-4)`, 'success'))}`
)
}
return true
}
const tasks: Array<
(classes: string, tailwindConfig: TailwindConfig, assert: Assert) => string
> = [
(classes): string => classes.replace(CLASS_DIVIDER_PIPE, ' '),
(classes): string =>
classes.replace(COMMENTS_MULTI_LINE, multilineReplaceWith),
(classes): string => classes.replace(COMMENTS_SINGLE_LINE, ''),
(classes, tailwindConfig, assert): string =>
expandVariantGroups(classes, { assert, tailwindConfig }), // Expand grouped variants to individual classes
]
function sortBigSign(bigIntValue: bigint): number {
return Number(bigIntValue > 0n) - Number(bigIntValue < 0n)
}
function getOrderedClassList(
tailwindContext: TailwindContext,
convertedClassList: string[],
classList: string[],
assert: CoreContext['assert']
): Array<[order: bigint, className: string, preservedClassName: string]> {
assert(typeof tailwindContext?.getClassOrder === 'function', ({ color }) =>
color('Twin requires a newer version of tailwindcss, please update')
) // `getClassOrder` was added in tailwindcss@3.0.23
let orderedClassList
try {
orderedClassList = tailwindContext
.getClassOrder(convertedClassList)
.map(([className, order], index): [bigint, string, string] => [
order || 0n,
className,
classList[index],
])
.sort(([a], [z]) => sortBigSign(a - z))
} catch (error: unknown) {
assert(
false,
({ color }) =>
`${color(
String(error).replace('with \\ may', 'with a single \\ may') // Improve error
)}\n\n${color('Found in:')} ${color(
convertedClassList.join(' '),
'errorLight'
)}`
)
}
return orderedClassList as Array<[bigint, string, string]>
}
function getStyles(
classes: string,
params: CoreContext
): { styles: CssObject | undefined; unmatched: string[]; matched: string[] } {
const assert = createAssert(
params.CustomError,
params.isSilent,
params.twinConfig.hasLogColors
)
params.debug('string in', classes)
assert(
![null, 'null', undefined].includes(classes),
({ color }: AssertContext) =>
`${color(
`✕ Your classes need to be complete strings for Twin to detect them correctly`
)}\n\nRead more at https://twinredirect.page.link/template-literals`
)
const result = validateClasses(classes, {
tailwindConfig: params.tailwindConfig,
assert,
})
if (!result) return { styles: undefined, matched: [], unmatched: [] }
for (const task of tasks) {
classes = task(classes, params.tailwindConfig, assert)
}
params.debug('classes after format', classes)
const matched: string[] = []
const unmatched: string[] = []
const styles: CssObject[] = []
const commonContext = {
assert,
theme: params.theme,
debug: params.debug,
}
const convertedClassNameContext = {
...commonContext,
tailwindConfig: params.tailwindConfig,
isShortCssOnly: params.isShortCssOnly,
disableShortCss: params.twinConfig.disableShortCss,
}
const classList = [...splitAtTopLevelOnly(classes, ' ')]
const convertedClassList = classList.map(c =>
convertClassName(c, convertedClassNameContext)
)
const orderedClassList = getOrderedClassList(
params.tailwindContext,
convertedClassList,
classList,
assert
)
const commonMatchContext = {
...commonContext,
includeUniversalStyles: false,
coreContext: params,
twinConfig: params.twinConfig,
tailwindConfig: params.tailwindConfig,
tailwindContext: params.tailwindContext,
sassyPseudo: params.twinConfig.sassyPseudo,
}
for (const [, convertedClassName, className] of orderedClassList) {
const matches = [
...resolveMatches(convertedClassName, params.tailwindContext),
]
const results = getStylesFromMatches(matches, {
...commonMatchContext,
hasImportant: IMPORTANT_OUTSIDE_BRACKETS.test(
escapeRegex(convertedClassName)
),
selectorMatchReg: new RegExp(
// This regex specifies a list of characters allowed for the character
// immediately after the class ends - this avoids matching other classes
// eg: Input 'btn' will avoid matching '.btn-primary' in `.btn + .btn-primary`
`(${escapeRegex(`.${convertedClassName}`)})(?=[\\[.# >~+*:$\\)]|$)`
),
original: convertedClassName,
})
if (!results) {
params.debug('🔥 No matching rules found', className, 'error')
// Allow tw``/tw="" to pass through
if (className !== '') unmatched.push(className)
// If non-match and is on silent mode: Continue next iteration
if (params.isSilent) continue
// If non-match: Stop iteration and return
// (This "for of" loop returns to the parent function)
return { styles: undefined, matched, unmatched }
}
matched.push(className)
params.debug('✨ ruleset out', results, 'success')
styles.push(results)
}
if (styles.length === 0) return { styles: undefined, matched, unmatched }
// @ts-expect-error Avoid tuple type error
const mergedStyles = deepMerge(...styles)
return { styles: mergedStyles, matched, unmatched }
}
export default getStyles
================================================
FILE: src/core/index.ts
================================================
export { default as createCoreContext } from './createCoreContext'
export { default as getGlobalStyles } from './getGlobalStyles'
export { default as getStyles } from './getStyles'
export { splitAtTopLevelOnly } from './lib/util/twImports'
================================================
FILE: src/core/lib/configHelpers.ts
================================================
import { resolve, dirname } from 'path'
import { existsSync } from 'fs'
import escalade from 'escalade/sync'
import { configTwinValidators, configDefaultsTwin } from './twinConfig'
import defaultTwinConfig from './defaultTailwindConfig'
import { resolveTailwindConfig, getAllConfigs } from './util/twImports'
import isObject from './util/isObject'
import { logGeneralError } from './logging'
import type {
TwinConfig,
TwinConfigAll,
GetConfigTwinValidatedParameters,
TailwindConfig,
Assert,
AssertContext,
} from 'core/types'
import loadConfig from 'tailwindcss/loadConfig'
type Validator = [(value: unknown) => boolean, string]
type GetTailwindConfig = {
sourceRoot?: string
filename: string
config?: TwinConfig
assert: Assert
}
function getTailwindConfig({
sourceRoot,
filename,
config,
assert,
}: GetTailwindConfig): TailwindConfig {
sourceRoot = sourceRoot ?? '.'
const baseDirectory = filename ? dirname(filename) : process.cwd()
const userTailwindConfig = config?.config
if (isObject(userTailwindConfig))
return resolveTailwindConfig([
// User config
...getAllConfigs(userTailwindConfig as Record),
// Default config
...getAllConfigs(defaultTwinConfig),
])
const configPath = userTailwindConfig
? resolve(sourceRoot, userTailwindConfig)
: escalade(baseDirectory, (_, names) => {
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
}) ?? ''
const configExists = Boolean(configPath && existsSync(configPath))
if (userTailwindConfig)
assert(configExists, ({ color }: AssertContext) =>
[
`${String(
color(
`✕ The tailwind config ${color(
String(userTailwindConfig),
'errorLight'
)} wasn’t found`
)
)}`,
`Update the \`config\` option in your twin config`,
].join('\n\n')
)
const configs = [
// User config
...(configExists ? getAllConfigs(loadConfig(configPath)) : []),
// Default config
...getAllConfigs(defaultTwinConfig),
]
const tailwindConfig = resolveTailwindConfig(configs)
return tailwindConfig
}
function runConfigValidator([item, value]: [
keyof typeof configTwinValidators,
string | boolean
]): boolean {
const validatorConfig: Validator = configTwinValidators[item]
if (!validatorConfig) return true
const [validator, errorMessage] = validatorConfig
if (typeof validator !== 'function') return false
if (!validator(value)) {
throw new Error(logGeneralError(String(errorMessage)))
}
return true
}
function getConfigTwin(
config: TwinConfig | undefined,
params: GetConfigTwinValidatedParameters
): TwinConfigAll {
const output: TwinConfigAll = {
...configDefaultsTwin(params),
...config,
}
return output
}
function getConfigTwinValidated(
config: TwinConfig | undefined,
params: GetConfigTwinValidatedParameters
): TwinConfigAll {
const twinConfig = getConfigTwin(config, params)
// eslint-disable-next-line unicorn/no-array-reduce
return Object.entries(twinConfig).reduce((result, item) => {
const validatedItem = item as [
keyof typeof configTwinValidators,
string | boolean
]
return {
...result,
...(runConfigValidator(validatedItem) && {
[validatedItem[0]]: validatedItem[1],
}),
}
}, {}) as TwinConfigAll
}
export { getTailwindConfig, getConfigTwinValidated }
================================================
FILE: src/core/lib/convertClassName.ts
================================================
import replaceThemeValue from './util/replaceThemeValue'
import isShortCss from './util/isShortCss'
import splitOnFirst from './util/splitOnFirst'
import { splitAtTopLevelOnly } from './util/twImports'
import type { AssertContext, CoreContext, TailwindConfig } from 'core/types'
// eslint-disable-next-line import/no-relative-parent-imports
import { SPACE_ID, SPACES } from '../constants'
const ALL_COMMAS = /,/g
const ALL_AMPERSANDS = /&/g
const ENDING_AMP_THEN_WHITESPACE = /&[\s_]*$/
const ALL_CLASS_DOTS = /(?[_~])[\w-])/g
const BASIC_SELECTOR_TYPES = /^#|^\\.|[^\W_]/
type ConvertShortCssToArbitraryPropertyParameters = {
disableShortCss: CoreContext['twinConfig']['disableShortCss']
origClassName: string
} & Pick
function convertShortCssToArbitraryProperty(
className: string,
{
tailwindConfig,
assert,
disableShortCss,
isShortCssOnly,
origClassName,
}: ConvertShortCssToArbitraryPropertyParameters
): string {
const splitArray = [
...splitAtTopLevelOnly(className, tailwindConfig.separator ?? ':'),
]
const lastValue = splitArray.slice(-1)[0]
let [property, value] = splitOnFirst(lastValue, '[')
value = value.slice(0, -1).trim()
let preSelector = ''
if (property.startsWith('!')) {
property = property.slice(1)
preSelector = '!'
}
const template = `${preSelector}[${[
property,
value === '' ? "''" : value,
].join(tailwindConfig.separator ?? ':')}]`
splitArray.splice(-1, 1, template)
const arbitraryProperty = splitArray.join(tailwindConfig.separator ?? ':')
const isShortCssDisabled = disableShortCss && !isShortCssOnly
assert(!isShortCssDisabled, ({ color }) =>
[
`${String(
color(
`✕ ${String(
color(origClassName, 'errorLight')
)} uses twin’s deprecated short-css syntax`
)
)}`,
`Update to ${String(color(arbitraryProperty, 'success'))}`,
`To ignore this notice, add this to your twin config:\n{ "disableShortCss": false }`,
`Read more at https://twinredirect.page.link/short-css`,
].join('\n\n')
)
return arbitraryProperty
}
type ConvertClassNameParameters = {
disableShortCss: CoreContext['twinConfig']['disableShortCss']
} & Pick<
CoreContext,
'tailwindConfig' | 'theme' | 'assert' | 'debug' | 'isShortCssOnly'
>
function checkForVariantSupport({
className,
tailwindConfig,
assert,
}: { className: string } & Pick<
CoreContext,
'tailwindConfig' | 'assert'
>): void {
const pieces = [
...splitAtTopLevelOnly(className, tailwindConfig.separator ?? ':'),
]
const hasMultipleVariants = pieces.length > 2 // One is the class name
const hasACommaInVariants = pieces.some(
p => splitAtTopLevelOnly(p.slice(1, -1), ',').length > 1
)
const hasIssue = hasMultipleVariants && hasACommaInVariants
assert(
!hasIssue,
({ color }: AssertContext) =>
`${color(
`✕ The variants on ${String(
color(className, 'errorLight')
)} are invalid tailwind and twin classes`
)}\n\n${color(
`To fix, either reduce all variants into a single arbitrary variant:`,
'success'
)}\nFrom: \`[.this, .that]:first:block\`\nTo: \`[.this:first, .that:first]:block\`\n\n${color(
`Or split the class into separate classes instead of using commas:`,
'success'
)}\nFrom: \`[.this, .that]:first:block\`\nTo: \`[.this]:first:block [.that]:first:block\`\n\nRead more at https://twinredirect.page.link/arbitrary-variants-with-commas`
)
}
// Convert a twin class to a tailwindcss friendly class
function convertClassName(
className: string,
{
tailwindConfig,
theme,
isShortCssOnly,
disableShortCss,
assert,
debug,
}: ConvertClassNameParameters
): string {
checkForVariantSupport({ className, tailwindConfig, assert })
const origClassName = className
// Convert spaces to class friendly underscores
className = className.replace(SPACES, SPACE_ID)
// Move the bang to the front of the class
if (className.endsWith('!')) {
debug('trailing bang found', className)
const splitArray = [
...splitAtTopLevelOnly(
className.slice(0, -1),
tailwindConfig.separator ?? ':'
),
]
// Place a ! before the class
splitArray.splice(-1, 1, `!${splitArray[splitArray.length - 1]}`)
className = splitArray.join(tailwindConfig.separator ?? ':')
}
// Convert short css to an arbitrary property, eg: `[display:block]`
// (Short css is deprecated)
if (isShortCss(className, tailwindConfig)) {
debug('short css found', className)
className = convertShortCssToArbitraryProperty(className, {
tailwindConfig,
assert,
disableShortCss,
isShortCssOnly,
origClassName,
})
}
// Replace theme values throughout the class
className = replaceThemeValue(className, { assert, theme })
// Add missing parent selectors and collapse arbitrary variants
className = sassifyArbitraryVariants(className, { tailwindConfig })
debug('class after format', className)
return className
}
function isArbitraryVariant(variant: string): boolean {
return variant.startsWith('[') && variant.endsWith(']')
}
function unbracket(variant: string): string {
return variant.slice(1, -1)
}
function sassifyArbitraryVariants(
fullClassName: string,
{ tailwindConfig }: { tailwindConfig: TailwindConfig }
): string {
const splitArray = [
...splitAtTopLevelOnly(fullClassName, tailwindConfig.separator ?? ':'),
]
const variants = splitArray.slice(0, -1)
const className = splitArray.slice(-1)[0]
if (variants.length === 0) return fullClassName
// Collapse arbitrary variants when they don't contain `&`.
// `[> div]:[.nav]:(flex block)` -> `[> div_.nav]:flex [> div_.nav]:block`
const collapsed = [] as string[]
variants.forEach((variant, index) => {
// We can’t match the selector if there's a character right next to the parent selector (eg: `[§ion]:block`) otherwise we'd accidentally replace `.step` in classes like this:
// Bad: `.steps-primary .steps` -> `&-primary &`
// Good: `.steps-primary .steps` -> `.steps-primary &`
// So here we replace it with crazy brackets to identify and unwrap it later
if (isArbitraryVariant(variant))
variant = variant.replace(ALL_WRAPPABLE_PARENT_SELECTORS, '(((&)))')
if (
index === 0 ||
!isArbitraryVariant(variant) ||
!isArbitraryVariant(variants[index - 1])
)
return collapsed.push(variant)
const prev = collapsed[collapsed.length - 1]
if (variant.includes('&')) {
const prevHasParent = prev.includes('&')
// Merge with current
if (prevHasParent) {
const mergedWithCurrent = variant.replace(
ALL_AMPERSANDS,
unbracket(prev)
)
const isLast = index === variants.length - 1
collapsed[index - 1] = isLast
? mergedWithCurrent.replace(ALL_AMPERSANDS, '')
: mergedWithCurrent
return
}
// Merge with previous
if (!prevHasParent) {
const mergedWithPrev = `[${unbracket(variant).replace(
ALL_AMPERSANDS,
unbracket(prev)
)}]`
collapsed[collapsed.length - 1] = mergedWithPrev
return
}
}
// Parentless variants are merged into the previous arbitrary variant
const mergedWithPrev = `[${[
unbracket(prev).replace(ENDING_AMP_THEN_WHITESPACE, ''),
unbracket(variant),
].join('_')}]`
collapsed[collapsed.length - 1] = mergedWithPrev
})
// The supplied class requires the reversal of it's variants as resolveMatches adds them in reverse order
const reversedVariantList = [...collapsed].slice().reverse()
const allVariants = reversedVariantList.map((v, idx) => {
if (!isArbitraryVariant(v)) return v
const unwrappedVariant = unbracket(v)
// Unescaped dots incorrectly add the prefix within arbitrary variants (only when`prefix` is set in tailwind config)
// eg: tw`[.a]:first:tw-block` -> `.tw-a &:first-child`
.replace(ALL_CLASS_DOTS, '\\.')
// Unescaped ats will throw a conversion error
.replace(ALL_CLASS_ATS, '\\@')
const variantList = unwrappedVariant.startsWith('@')
? [unwrappedVariant]
: // Arbitrary variants with commas are split, handled as separate selectors then joined
[...splitAtTopLevelOnly(unwrappedVariant, ',')]
const out = variantList
.map(variant =>
addParentSelector(variant, collapsed[idx - 1], collapsed[idx + 1] ?? '')
)
// Tailwindcss removes everything from a comma onwards in arbitrary variants, so we need to encode to preserve them
// Underscore is needed to distance the code from another possible number
// Eg: [path[fill='rgb(51,100,51)']]:[fill:white]
.join('\\2c_')
.replace(ALL_COMMAS, '\\2c_')
return `[${out}]`
})
return [...allVariants, className].join(tailwindConfig.separator ?? ':')
}
function addParentSelector(
selector: string,
prev: string,
next: string
): string {
// Preserve selectors with a parent selector and media queries
if (selector.includes('&') || selector.startsWith('@')) return selector
// Arbitrary variants
// Pseudo elements get an auto parent selector prefixed
if (selector.startsWith(':')) return `&${selector}`
// Variants that start with a class/id get treated as a child
if (BASIC_SELECTOR_TYPES.test(selector) && !prev) return `& ${selector}`
// When there's more than one variant and it's at the end then prefix it
if (!next && prev) return `&${selector}`
return `& ${selector}`
}
export default convertClassName
================================================
FILE: src/core/lib/createAssert.ts
================================================
import type { AssertContext } from 'core/types'
import { makeColor } from './logging'
function createAssert(
CustomError = Error,
isSilent = false,
hasLogColors = true
) {
return (
expression: boolean | string | (({ color }: AssertContext) => string),
message: string | (({ color }: AssertContext) => string)
): void => {
if (isSilent) return
if (typeof expression === 'string') {
throw new CustomError(`\n\n${expression}\n`)
}
const messageContext = { color: makeColor(hasLogColors) }
if (typeof expression === 'function') {
throw new CustomError(`\n\n${expression(messageContext)}\n`)
}
if (expression) return
if (typeof message === 'string') {
throw new CustomError(`\n\n${message}\n`)
}
if (typeof message === 'function') {
throw new CustomError(`\n\n${message(messageContext)}\n`)
}
}
}
export default createAssert
================================================
FILE: src/core/lib/createTheme.ts
================================================
import dlv from 'dlv'
import { transformThemeValue, toPath } from './util/twImports'
import isObject from './util/isObject'
import type { TailwindConfig } from 'core/types'
function createTheme(
tailwindConfig: TailwindConfig
): (
dotSeparatedItem: string,
extra?: string
) => Record | boolean | number {
function getConfigValue(path: string[], defaultValue?: string): unknown {
return dlv(tailwindConfig, path, defaultValue)
}
function resolveThemeValue(
path: string,
defaultValue?: string,
options = {}
): number | boolean | Record {
let [pathRoot, ...subPaths] = toPath(path)
// Retain dots in spacing values, eg: `ml-[theme(spacing.0.5)]`
if (
pathRoot === 'spacing' &&
subPaths.length === 2 &&
subPaths.every(x => !Number.isNaN(Number(x)))
) {
subPaths = [subPaths.join('.')]
}
const value = getConfigValue(
path ? ['theme', pathRoot, ...subPaths] : ['theme'],
defaultValue
)
return sassifyValues(transformThemeValue(pathRoot)(value, options))
}
const out = Object.assign(
(path: string, defaultValue?: string) =>
resolveThemeValue(path, defaultValue),
{
withAlpha: (path: string, opacityValue?: string) =>
resolveThemeValue(path, undefined, { opacityValue }),
}
)
return out
}
function sassifyValues(
values: Record
): Record {
if (!isObject(values)) return values
const transformed: Array<[string, unknown]> = Object.entries(values).map(
([k, v]: [string, unknown]) => [
k,
(isObject(v) && sassifyValues(v)) ||
(typeof v === 'number' && String(v)) ||
v,
]
)
return Object.fromEntries(transformed)
}
export default createTheme
================================================
FILE: src/core/lib/defaultTailwindConfig.ts
================================================
import toArray from './util/toArray'
import type { PluginAPI } from 'tailwindcss/types/config'
const AMPERSAND_AFTER = /&(.+)/g
const AMPERSAND = /&/g
function stripAmpersands(string: string): string {
return typeof string === 'string'
? string.replace(AMPERSAND, '').trim()
: string
}
const EXTRA_VARIANTS = [
['all', '& *'],
['all-child', '& > *'],
['sibling', '& ~ *'],
['hocus', ['&:hover', '&:focus']],
'link',
'read-write',
['svg', '& svg'],
['even-of-type', '&:nth-of-type(even)'],
['odd-of-type', '&:nth-of-type(odd)'],
]
const EXTRA_NOT_VARIANTS = [
// Positional
['first', '&:first-child'],
['last', '&:last-child'],
['only', '&:only-child'],
['odd', '&:nth-child(odd)'],
['even', '&:nth-child(even)'],
'first-of-type',
'last-of-type',
'only-of-type',
// State
'target',
['open', '&[open]'],
// Forms
'default',
'checked',
'indeterminate',
'placeholder-shown',
'autofill',
'optional',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'read-only',
// Content
'empty',
// Interactive
'focus-within',
'hover',
'focus',
'focus-visible',
'active',
'enabled',
'disabled',
]
function defaultVariants({ config, addVariant }: PluginAPI): void {
const extraVariants = EXTRA_VARIANTS.flatMap(v => {
let [name, selector] = toArray(v)
selector = selector || `&:${String(name)}`
const variant = [name, selector]
// Create a :not() version of the selectors above
const notVariant = [
`not-${String(name)}`,
(toArray(selector) as string[]).map(
(s: string) => `&:not(${stripAmpersands(s)})`
),
]
return [variant, notVariant]
})
// Create :not() versions of these selectors
const notPseudoVariants = EXTRA_NOT_VARIANTS.map(v => {
const [name, selector] = toArray(v)
const notConfig = [
`not-${name as string}`,
(toArray(selector || `&:${name as string}`) as string[]).map(
s => `&:not(${stripAmpersands(s)})`
),
]
return notConfig
})
const variants = [...extraVariants, ...notPseudoVariants]
for (const [name, selector] of variants) {
addVariant(name as string, toArray(selector) as string[])
}
for (const [name, selector] of variants) {
const groupSelector = (toArray(selector) as string[]).map(s =>
s.replace(AMPERSAND_AFTER, ':merge(.group)$1 &')
)
addVariant(`group-${name as string}`, groupSelector)
}
for (const [name, selector] of variants) {
const peerSelector = (toArray(selector) as string[]).map(s =>
s.replace(AMPERSAND_AFTER, ':merge(.peer)$1 ~ &')
)
addVariant(`peer-${name as string}`, peerSelector)
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/any-pointer
addVariant('any-pointer-none', '@media (any-pointer: none)')
addVariant('any-pointer-fine', '@media (any-pointer: fine)')
addVariant('any-pointer-coarse', '@media (any-pointer: coarse)')
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer
addVariant('pointer-none', '@media (pointer: none)')
addVariant('pointer-fine', '@media (pointer: fine)')
addVariant('pointer-coarse', '@media (pointer: coarse)')
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/any-hover
addVariant('any-hover-none', '@media (any-hover: none)')
addVariant('any-hover', '@media (any-hover: hover)')
// https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
addVariant('can-hover', '@media (hover: hover)')
addVariant('cant-hover', '@media (hover: none)')
addVariant('screen', '@media screen')
// Light mode
// eslint-disable-next-line unicorn/prefer-spread
let [mode, className = '.light'] = ([] as Array).concat(
config('lightMode', 'media')
)
if (mode === false) mode = 'media'
if (mode === 'class') {
addVariant('light', `${String(className)} &`)
} else if (mode === 'media') {
addVariant('light', '@media (prefers-color-scheme: light)')
}
// eslint-disable-next-line unicorn/prefer-spread
;[mode, className = '.light'] = ([] as string[]).concat(
config('lightMode', 'media')
)
if (mode === 'class') {
addVariant('light', `${className} &`)
} else if (mode === 'media') {
addVariant('light', '@media (prefers-color-scheme: light)')
}
}
const defaultTailwindConfig = {
presets: [
{
content: [''], // Silence empty content warning
theme: {
extend: {
content: { DEFAULT: '' }, // Add a `content` class
zIndex: { 1: '1' }, // Add a handy small zIndex (`z-1` / `-z-1`)
},
},
plugins: [defaultVariants], // Add extra variants
},
],
}
export default defaultTailwindConfig
================================================
FILE: src/core/lib/expandVariantGroups.ts
================================================
import type { Assert, AssertContext, TailwindConfig } from 'core/types'
import { splitAtTopLevelOnly } from './util/twImports'
const BRACKETED = /^\(.*?\)$/
const BRACKETED_MAYBE_IMPORTANT = /\)!?$/
const ESCAPE_CHARACTERS = /\n|\t/g
type Context = {
variants?: string
beforeImportant?: string
afterImportant?: string
tailwindConfig: TailwindConfig
assert: Assert
}
function spreadVariantGroups(classes: string, context: Context): string[] {
const pieces = [
...splitAtTopLevelOnly(
classes.trim(),
context.tailwindConfig.separator ?? ':'
),
] as string[]
let groupedClasses = pieces.pop()
if (!groupedClasses) return [] // type guard
// Check for too many dividers used
// Added here instead of "validateClasses" as it's less error prone to check here
context.assert(
!pieces.includes(''),
({ color }: AssertContext) =>
`${color(
`✕ ${String(color(classes, 'errorLight'))} has too many dividers`
)}\n\nUpdate to ${String(
color(
`${pieces
.filter(Boolean)
.join(context.tailwindConfig.separator ?? ':')}`,
'success'
)
)}`
)
let beforeImportant = context?.beforeImportant ?? ''
let afterImportant = context?.afterImportant ?? ''
if (!beforeImportant && groupedClasses.startsWith('!')) {
groupedClasses = groupedClasses.slice(1)
beforeImportant = '!'
}
if (!afterImportant && groupedClasses.endsWith('!')) {
groupedClasses = groupedClasses.slice(0, -1)
afterImportant = '!'
}
// Remove () brackets and split
const unwrapped = BRACKETED.test(groupedClasses)
? groupedClasses.slice(1, -1)
: groupedClasses
const classList = [...splitAtTopLevelOnly(unwrapped, ' ')].filter(Boolean)
const group = classList
.map(className => {
if (
BRACKETED_MAYBE_IMPORTANT.test(className) &&
// Avoid infinite loop due to lack of separator, eg: `[em](block)`
!className.includes('](')
) {
const ctx = { ...context, beforeImportant, afterImportant }
return expandVariantGroups(
[...pieces, className].join(context.tailwindConfig.separator ?? ':'),
ctx
)
}
return [...pieces, [beforeImportant, className, afterImportant].join('')]
.filter(Boolean)
.join(context.tailwindConfig.separator ?? ':')
})
.filter(Boolean)
return group
}
function expandVariantGroups(classes: string, context: Context): string {
const classList = [
...splitAtTopLevelOnly(classes.replace(ESCAPE_CHARACTERS, ' ').trim(), ' '),
]
if (classList.length === 1 && ['', '()'].includes(classList[0])) return ''
const expandedClasses = classList.flatMap(item =>
spreadVariantGroups(item, context)
)
return expandedClasses.join(' ')
}
export default expandVariantGroups
================================================
FILE: src/core/lib/getStitchesPath.ts
================================================
import { resolve, relative, parse } from 'path'
import { existsSync } from 'fs'
import { logGeneralError } from './logging'
import toArray from './util/toArray'
import type { TwinConfig } from 'core/types'
function getFirstValue(
list: ListItem[],
getValue: (
params: ListItem,
options: { index: number; isLast: boolean }
) => unknown
): [unknown, ListItem | undefined] {
let firstValue
const listLength = list.length - 1
const listItem = list.find((listItem, index) => {
const isLast = index === listLength
firstValue = getValue(listItem, { index, isLast })
return Boolean(firstValue)
})
return [firstValue, listItem]
}
function checkExists(
fileName: string | string[],
sourceRoot: string
): string | undefined {
const [, value] = getFirstValue(
toArray(fileName) as string[],
existingFileName => existsSync(resolve(sourceRoot, `./${existingFileName}`))
)
return value
}
function getRelativePath(comparePath: string, filename: string): string {
const pathName = parse(filename).dir
return relative(pathName, comparePath)
}
function getStitchesPath({
sourceRoot,
filename,
config,
}: {
sourceRoot?: string
filename: string
config: TwinConfig
}): string {
sourceRoot = sourceRoot ?? '.'
const configPathCheck = config.stitchesConfig ?? [
'stitches.config.ts',
'stitches.config.js',
]
const configPath = checkExists(configPathCheck, sourceRoot)
if (!configPath)
throw new Error(
logGeneralError(
`Couldn’t find the Stitches config at ${
config.stitchesConfig
? `“${String(config.stitchesConfig)}”`
: 'the project root'
}.\nUse the twin config: stitchesConfig="PATH_FROM_PROJECT_ROOT" to set the location.`
)
)
return getRelativePath(configPath, filename)
}
export default getStitchesPath
================================================
FILE: src/core/lib/logging.ts
================================================
import chalk from 'chalk'
import type {
MakeColor,
ColorType,
ColorValue,
TwinConfigAll,
} from 'core/types'
const colors = {
error: chalk.hex('#ff8383'),
errorLight: chalk.hex('#ffd3d3'),
warn: chalk.yellowBright,
success: chalk.greenBright,
highlight: chalk.yellowBright,
subdued: chalk.hex('#999'),
}
function makeColor(hasColor: boolean): MakeColor {
return (message: string, type: keyof typeof colors = 'error') => {
if (!hasColor) return message
return colors[type](message)
}
}
function spaced(string: string): string {
return `\n\n${string}\n`
}
function warning(string: string): string {
return colors.error(`✕ ${string}`)
}
function logGeneralError(error: string | [ColorValue, string]): string {
return Array.isArray(error)
? spaced(
`${warning(
typeof error[0] === 'function' ? error[0](colors) : error[0]
)}\n\n${error[1]}`
)
: spaced(warning(error))
}
function createDebug(isDev: boolean, twinConfig: TwinConfigAll) {
return (
reference: string,
data: unknown,
type: ColorType = 'subdued'
): void => {
if (!isDev) return
if (!twinConfig.debug) return
const log = `${String(colors[type]('-'))} ${reference} ${String(
colors[type](JSON.stringify(data))
)}`
// eslint-disable-next-line no-console
console.log(log)
}
}
export { makeColor, spaced, warning, colors, logGeneralError, createDebug }
================================================
FILE: src/core/lib/twinConfig.ts
================================================
import type { GetPackageUsed, TwinConfigAll } from 'core/types'
const TWIN_CONFIG_DEFAULTS = {
allowStyleProp: false,
autoCssProp: false,
config: undefined,
convertHtmlElementToStyled: false,
convertStyledDotToParam: false,
convertStyledDotToFunction: false,
css: { import: '', from: '' },
dataCsProp: false,
dataTwProp: false,
debug: false,
disableCsProp: true,
disableShortCss: true,
global: { import: '', from: '' },
hasLogColors: true,
includeClassNames: false,
moveTwPropToStyled: false,
moveKeyframesToGlobalStyles: false,
preset: undefined,
sassyPseudo: false,
stitchesConfig: undefined,
styled: { import: '', from: '' },
} as const
// Defaults for different css-in-js libraries
const configDefaultsStyledComponents = {
sassyPseudo: true, // Sets selectors like hover to &:hover
} as const
const configDefaultsGoober = {
sassyPseudo: true, // Sets selectors like hover to &:hover
} as const
const configDefaultsSolid = {
sassyPseudo: true, // Sets selectors like hover to &:hover
moveTwPropToStyled: true, // Move the tw prop to a styled definition
convertHtmlElementToStyled: true, // Add a styled definition on css prop elements
convertStyledDotToFunction: true, // Convert styled.[element] to a default syntax
} as const
const configDefaultsStitches = {
sassyPseudo: true, // Sets selectors like hover to &:hover
convertStyledDotToParam: true, // Convert styled.[element] to a default syntax
moveTwPropToStyled: true, // Move the tw prop to a styled definition
convertHtmlElementToStyled: true, // Add a styled definition on css prop elements
stitchesConfig: undefined, // Set the path to the stitches config
moveKeyframesToGlobalStyles: true, // Stitches doesn't support inline @keyframes
} as const
function configDefaultsTwin({
isSolid,
isStyledComponents,
isGoober,
isStitches,
isDev,
}: GetPackageUsed & { isDev: boolean }): TwinConfigAll {
return {
...TWIN_CONFIG_DEFAULTS,
...(isSolid && configDefaultsSolid),
...(isStyledComponents && configDefaultsStyledComponents),
...(isGoober && configDefaultsGoober),
...(isStitches && configDefaultsStitches),
dataTwProp: isDev,
dataCsProp: isDev,
}
}
function isBoolean(value: unknown): boolean {
return typeof value === 'boolean'
}
const allowedPresets = [
'styled-components',
'emotion',
'goober',
'stitches',
'solid',
]
type ConfigTwinValidators = Record<
keyof typeof TWIN_CONFIG_DEFAULTS & 'disableColorVariables',
[(value: unknown) => boolean, string]
>
const configTwinValidators: ConfigTwinValidators = {
preset: [
(value: unknown): boolean =>
value === undefined ||
(typeof value === 'string' && allowedPresets.includes(value)),
`The config “preset” can only be:\n${allowedPresets
.map(p => `'${p}'`)
.join(', ')}`,
],
allowStyleProp: [
isBoolean,
'The config “allowStyleProp” can only be a boolean',
],
autoCssProp: [
(value: unknown): boolean => !value,
'The “autoCssProp” feature has been removed from twin.macro@2.8.2+\nThis means the css prop must be added by styled-components instead.\nSetup info at https://twinredirect.page.link/auto-css-prop\n\nRemove the “autoCssProp” item from your config to avoid this message.',
],
convertStyledDot: [
(value: unknown): boolean => !value,
'The “convertStyledDot” feature was changed to “convertStyledDotParam”.',
],
disableColorVariables: [
(value: unknown): boolean => !value,
'The disableColorVariables feature has been removed from twin.macro@3+\n\nRemove the disableColorVariables item from your config to avoid this message.',
],
sassyPseudo: [isBoolean, 'The config “sassyPseudo” can only be a boolean'],
dataTwProp: [
(value: unknown): boolean => isBoolean(value) || value === 'all',
'The config “dataTwProp” can only be true, false or "all"',
],
dataCsProp: [
(value: unknown): boolean => isBoolean(value) || value === 'all',
'The config “dataCsProp” can only be true, false or "all"',
],
includeClassNames: [
isBoolean,
'The config “includeClassNames” can only be a boolean',
],
disableCsProp: [
isBoolean,
'The config “disableCsProp” can only be a boolean',
],
convertStyledDotToParam: [
isBoolean,
'The config “convertStyledDotToParam” can only be a boolean',
],
convertStyledDotToFunction: [
isBoolean,
'The config “convertStyledDotToFunction” can only be a boolean',
],
moveTwPropToStyled: [
isBoolean,
'The config “moveTwPropToStyled” can only be a boolean',
],
convertHtmlElementToStyled: [
isBoolean,
'The config “convertHtmlElementToStyled” can only be a boolean',
],
}
export { configDefaultsTwin, configTwinValidators, TWIN_CONFIG_DEFAULTS }
================================================
FILE: src/core/lib/userPresets.ts
================================================
/**
* Config presets
*
* To change the preset, add the following in `package.json`:
* `{ "babelMacros": { "twin": { "preset": "styled-components" } } }`
*
* Or in `babel-plugin-macros.config.js`:
* `module.exports = { twin: { preset: "styled-components" } }`
*/
const userPresets = {
'styled-components': {
styled: { import: 'default', from: 'styled-components' },
css: { import: 'css', from: 'styled-components' },
global: { import: 'createGlobalStyle', from: 'styled-components' },
},
emotion: {
styled: { import: 'default', from: '@emotion/styled' },
css: { import: 'css', from: '@emotion/react' },
global: { import: 'Global', from: '@emotion/react' },
},
goober: {
styled: { import: 'styled', from: 'goober' },
css: { import: 'css', from: 'goober' },
global: { import: 'createGlobalStyles', from: 'goober/global' },
},
stitches: {
styled: { import: 'styled', from: 'stitches.config' },
css: { import: 'css', from: 'stitches.config' },
global: { import: 'global', from: 'stitches.config' },
},
solid: {
styled: { import: 'styled', from: 'solid-styled-components' },
css: { import: 'css', from: 'solid-styled-components' },
global: { import: 'createGlobalStyles', from: 'solid-styled-components' },
},
}
export default userPresets
================================================
FILE: src/core/lib/util/camelize.ts
================================================
const CAMEL_FIND = /\W+(.)/g
export default function camelize(string: string): string {
return string?.replace(CAMEL_FIND, (_, chr: string) => chr.toUpperCase())
}
================================================
FILE: src/core/lib/util/deepMerge.ts
================================================
import deepMerge from 'lodash.merge'
// eslint-disable-next-line unicorn/prefer-export-from
export default deepMerge
================================================
FILE: src/core/lib/util/escapeRegex.ts
================================================
const REGEX_SPECIAL_CHARACTERS = /[$()*+./?[\\\]^{|}-]/g
export default function escapeRegex(string: string): string {
return string.replace(REGEX_SPECIAL_CHARACTERS, '\\$&')
}
================================================
FILE: src/core/lib/util/formatProp.ts
================================================
// eslint-disable-next-line import/no-relative-parent-imports
import { SPACE_ID, LINEFEED } from '../../constants'
const EXTRA_WHITESPACE = /\s\s+/g
export default function formatProp(classes: string): string {
return (
classes
// Normalize spacing
.replace(EXTRA_WHITESPACE, ' ')
// Remove newline characters
.replace(LINEFEED, ' ')
// Replace the space id
.replace(SPACE_ID, ' ')
.trim()
)
}
================================================
FILE: src/core/lib/util/get.ts
================================================
import get from 'lodash.get'
// eslint-disable-next-line unicorn/prefer-export-from
export default get
================================================
FILE: src/core/lib/util/isEmpty.ts
================================================
export default function isEmpty(value: unknown): boolean {
return (
value === undefined ||
value === null ||
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'string' && value.trim().length === 0)
)
}
================================================
FILE: src/core/lib/util/isObject.ts
================================================
export default function isObject(
value: unknown
): value is Record {
// eslint-disable-next-line eqeqeq, no-eq-null
return value != null && typeof value === 'object' && !Array.isArray(value)
}
================================================
FILE: src/core/lib/util/isShortCss.ts
================================================
import { splitAtTopLevelOnly } from './twImports'
import type { TailwindConfig } from 'core/types'
export default function isShortCss(
fullClassName: string,
tailwindConfig: TailwindConfig
): boolean {
const classPieces = [
...splitAtTopLevelOnly(fullClassName, tailwindConfig.separator ?? ':'),
]
const className = classPieces.slice(-1)[0]
if (!className.includes('[')) return false
// Replace brackets before splitting on them as the split function already
// reads brackets to determine where the top level is
const splitAtArbitrary = [
...splitAtTopLevelOnly(className.replace(/\[/g, '∀'), '∀'),
]
// Normal class
if (splitAtArbitrary[0].endsWith('-')) return false
// Important suffix
if (splitAtArbitrary[0].endsWith('!')) return false
// Arbitrary property
if (splitAtArbitrary[0] === '') return false
// Slash opacity, eg: bg-red-500/fromConfig/[.555]
if (splitAtArbitrary[0].endsWith('/')) return false
return true
}
================================================
FILE: src/core/lib/util/replaceThemeValue.ts
================================================
import type { AssertContext, CoreContext } from 'core/types'
const MATCH_THEME = /theme\((.+?)\)/
const MATCH_QUOTES = /["'`]/g
function replaceThemeValue(
value: string,
{
assert,
theme,
}: { assert: CoreContext['assert']; theme: CoreContext['theme'] }
): string {
const match = MATCH_THEME.exec(value)
if (!match) return value
const themeFunction = match[0]
const themeParameters = match[1].replace(MATCH_QUOTES, '').trim()
const [main, second] = themeParameters.split(',')
let themeValue = theme(main, second)
assert(Boolean(themeValue), ({ color }: AssertContext) =>
color(
`✕ ${color(
themeParameters,
'errorLight'
)} doesn’t match a theme value from the config`
)
)
// Account for the 'DEFAULT' key
if (typeof themeValue === 'object' && 'DEFAULT' in themeValue) {
themeValue = themeValue.DEFAULT as typeof themeValue
}
// Escape spaces in the value - without this we get an incorrect order
// in class groups like this:
// tw`w-[calc(100%-theme('spacing.1'))] w-[calc(100%-theme('spacing[0.5]'))]`
// theme: { spacing: { 0.5: "calc(.5 * .25rem)", 1: "calc(1 * .25rem)" } }
const stringValue = String(themeValue).replace(/\./g, '\\.')
const replacedValue = value.replace(themeFunction, stringValue)
return replacedValue
}
export default replaceThemeValue
================================================
FILE: src/core/lib/util/sassifySelector.ts
================================================
import type { ExtractRuleStyles } from 'core/types'
const SELECTOR_PARENT_CANDIDATE = /^[ #.[]/
const SELECTOR_SPECIAL_STARTS = /^ [>@]/
const SELECTOR_ROOT = /(^| ):root(?!\w)/g
const UNDERSCORE_ESCAPING = /\\+(_)/g
const WRAPPED_PARENT_SELECTORS = /(\({3}&(.*?)\){3})/g
type OptionalSassifyContext = {
selectorMatchReg: RegExp
sassyPseudo: boolean
original?: string
}
type SassifySelectorTasks = Array<
(selector: string, params: OptionalSassifyContext) => string
>
const sassifySelectorTasks: SassifySelectorTasks = [
(selector): string => selector.trim(),
// Prefix with the parent selector when sassyPseudo is enabled,
// otherwise just replace the class with the parent selector
(selector, { selectorMatchReg, sassyPseudo, original }): string => {
const out = selector.replace(
selectorMatchReg,
(match, __, offset: number) => {
if (selector === match) return ''
if (
/\w/.test(selector[offset - 1]) &&
selector[offset + match.length] === ':'
) {
if (sassyPseudo && selector[offset - 1] === undefined) return '&'
return '' // Cover [section&]:hover:block / .btn.loading&:before
}
return offset === 0 ? '' : '&'
}
)
// Fix certain matches not covered by the previous task, eg: `first:[section]:m-1`
// (Arbitrary variants targeting html elements)
if (original && out === selector && selector.includes(`.${original}`))
return selector.replace(`.${original}`, '')
return out
},
// Unwrap the pre-wrapped parent selectors (pre-wrapping avoids matching issues against word characters, eg: `[§ion]:block`)
(selector): string => selector.replace(WRAPPED_PARENT_SELECTORS, '&$2'),
// Remove unneeded escaping from the selector
(selector): string => selector.replace(UNDERSCORE_ESCAPING, '$1'),
// Prefix classes/ids/attribute selectors with a parent selector so styles
// are applied to the current element rather than its children
(selector): string => {
if (selector.includes('&')) return selector
const addParentSelector = SELECTOR_PARENT_CANDIDATE.test(selector)
if (!addParentSelector) return selector
// Fix: ` > :not([hidden]) ~ :not([hidden])` / ` > *`
// Fix: `[@page]:x`
if (SELECTOR_SPECIAL_STARTS.test(selector)) return selector
return `&${selector}`
},
// Fix the spotty `:root` support in emotion/styled-components
(selector): string => selector.replace(SELECTOR_ROOT, '*:root'),
// Escape selectors containing forward slashes, eg: group-hover/link:bg-black
(selector): string => selector.replace(/\//g, '\\/'),
(selector): string => selector.trim(),
]
function sassifySelector(
selector: string,
params: ExtractRuleStyles & OptionalSassifyContext
): string {
// Remove the selector if it only contains the parent selector
if (selector === '&') {
params.debug('selector not required', selector)
return ''
}
for (const task of sassifySelectorTasks) {
selector = task(selector, params)
}
return selector
}
export default sassifySelector
================================================
FILE: src/core/lib/util/splitOnFirst.ts
================================================
// Split a string at a value and return an array of the two parts
export default function splitOnFirst(input: string, delim: string): string[] {
return (([first, ...rest]): [string, string] => [first, rest.join(delim)])(
input.split(delim)
)
}
================================================
FILE: src/core/lib/util/toArray.ts
================================================
export default function toArray(array: T): T | [T] {
if (Array.isArray(array)) return array
return [array]
}
================================================
FILE: src/core/lib/util/twImports.ts
================================================
import type { Config } from 'tailwindcss'
import type { TailwindConfig, TailwindContext, TailwindMatch } from 'core/types'
// @ts-expect-error Types added below
import { toPath as toPathRaw } from 'tailwindcss/lib/util/toPath'
// @ts-expect-error Types added below
import { resolveMatches as resolveMatchesRaw } from 'tailwindcss/lib/lib/generateRules'
// @ts-expect-error Types added below
import { createContext as createContextRaw } from 'tailwindcss/lib/lib/setupContextUtils'
// @ts-expect-error Types added below
import { default as defaultTailwindConfigRaw } from 'tailwindcss/stubs/config.full'
// @ts-expect-error Types added below
import { default as transformThemeValueRaw } from 'tailwindcss/lib/util/transformThemeValue'
// @ts-expect-error Types added below
import { default as resolveTailwindConfigRaw } from 'tailwindcss/lib/util/resolveConfig'
// @ts-expect-error Types added below
import { default as getAllConfigsRaw } from 'tailwindcss/lib/util/getAllConfigs'
// @ts-expect-error Types added below
import { splitAtTopLevelOnly as splitAtTopLevelOnlyRaw } from 'tailwindcss/lib/util/splitAtTopLevelOnly'
// @ts-expect-error Types added below
import unescapeRaw from 'postcss-selector-parser/dist/util/unesc'
const toPath = toPathRaw as (path: string[] | string) => string[]
const createContext = createContextRaw as (config: Config) => TailwindContext
const defaultTailwindConfig = defaultTailwindConfigRaw as Config
const resolveMatches = resolveMatchesRaw as (
candidate: string,
context: TailwindContext
) => TailwindMatch[]
const transformThemeValue = transformThemeValueRaw as (
themeValue: string
) => (
value: unknown,
options: Record
) => Record
const resolveTailwindConfig = resolveTailwindConfigRaw as (
config: unknown[]
) => TailwindConfig
const getAllConfigs = getAllConfigsRaw as (
config: Record
) => TailwindConfig[]
const splitAtTopLevelOnly = splitAtTopLevelOnlyRaw as (
input: string,
separator: string
) => string[]
const unescape = unescapeRaw as (string: string) => string
export {
toPath,
createContext,
defaultTailwindConfig,
resolveMatches,
transformThemeValue,
resolveTailwindConfig,
getAllConfigs,
splitAtTopLevelOnly,
unescape,
}
================================================
FILE: src/core/types/index.ts
================================================
import type { MacroParams } from 'babel-plugin-macros'
import type { NodePath, types as T } from '@babel/core'
import type * as P from 'postcss'
import type { Config as TailwindConfig } from 'tailwindcss'
import type { colors } from '../lib/logging'
import type userPresets from '../lib/userPresets'
type KeyValuePair = Record
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface RecursiveKeyValuePair {
[key: string]: V | RecursiveKeyValuePair
}
export type CssObject = RecursiveKeyValuePair
// Make all properties in T optional
type Partial = {
[P in keyof T]?: T[P]
}
export type ColorValue = (c: typeof colors) => string
export type ColorType = keyof typeof colors
export type MakeColor = (message: string, type?: keyof typeof colors) => string
export type PresetItem = { import: string; from: string }
export type PresetConfig = {
styled: PresetItem
css: PresetItem
global: PresetItem
}
export type TwinConfigAll = {
preset?: keyof typeof userPresets
allowStyleProp: boolean
autoCssProp: boolean
dataTwProp: boolean | 'all'
sassyPseudo: boolean
debug: boolean
includeClassNames: boolean
dataCsProp: boolean | 'all'
disableCsProp: boolean
disableShortCss: boolean
config?: string | Partial
convertStyledDotToParam?: boolean
convertStyledDotToFunction?: boolean
moveTwPropToStyled?: boolean
moveKeyframesToGlobalStyles?: boolean
convertHtmlElementToStyled?: boolean
hasLogColors?: boolean
stitchesConfig?: string
} & PresetConfig
export type Candidate = [
data: { layer: string },
rule: P.Rule | P.AtRule | P.Declaration
]
export type TailwindContext = {
getClassOrder: (
classes: string[]
) => Array<[className: string, order: bigint]>
candidateRuleMap: Array<[string, Candidate[]]>
variantMap: Array>
}
export type AssertContext = {
color: MakeColor
}
export type Assert = (
expression: boolean | string,
message: ({ color }: AssertContext) => string
) => void
export type CoreContext = {
isDev: boolean
assert: Assert
debug: (reference: string, data: unknown, type?: ColorType) => void
theme: (
dotSeparatedItem: string,
extra?: string
) => Record | boolean | number
tailwindContext: TailwindContext
packageUsed: GetPackageUsed
tailwindConfig: TailwindConfig
twinConfig: TwinConfigAll
CustomError: typeof Error
importConfig: PresetConfig
isShortCssOnly?: boolean
isSilent?: boolean
options?: TailwindMatchOptions
}
export type ExtractRuleStyles = {
includeUniversalStyles?: boolean
original?: string
hasImportant?: boolean
selectorMatchReg?: RegExp
passChecks?: boolean
sassyPseudo?: TwinConfigAll['sassyPseudo']
coreContext: CoreContext
} & Pick<
CoreContext,
| 'assert'
| 'debug'
| 'theme'
| 'tailwindConfig'
| 'tailwindContext'
| 'options'
| 'twinConfig'
>
export type TransformDecl = {
decl: P.Declaration
property: string
} & ExtractRuleStyles
export type CreateCoreContext = {
isDev?: boolean
config?: TwinConfig
sourceRoot?: string
filename?: string
tailwindConfig?: TailwindConfig
CustomError: typeof Error
}
export type PossiblePresets = keyof typeof userPresets
export type GetPackageUsed = {
isEmotion: boolean
isStyledComponents: boolean
isGoober: boolean
isStitches: boolean
isSolid: boolean
}
export type TailwindMatchOptions = {
preserveSource?: boolean
respectPrefix?: boolean
respectImportant?: boolean
values?: Record
}
export type TailwindMatch = [
{ options?: TailwindMatchOptions; layer?: string },
P.Rule | P.AtRule | P.Declaration
]
export type GetConfigTwinValidatedParameters = GetPackageUsed & {
isDev: boolean
}
export type TwinConfig = Partial
export type { T, NodePath, MacroParams, TailwindConfig, KeyValuePair }
================================================
FILE: src/macro/className.ts
================================================
// eslint-disable-next-line import/no-relative-parent-imports
import { getStyles } from '../core'
import { addDataTwPropToPath, addDataPropToExistingPath } from './dataProp'
import isEmpty from './lib/util/isEmpty'
import {
astify,
getParentJSX,
getAttributeNames,
getCssAttributeData,
} from './lib/astHelpers'
import type { JSXAttributeHandler, T, NodePath } from './types'
function makeJsxAttribute(
[key, value]: [string, T.Expression | T.JSXEmptyExpression],
t: typeof T
): T.JSXAttribute {
return t.jsxAttribute(t.jsxIdentifier(key), t.jsxExpressionContainer(value))
}
function handleClassNameProperty({
path,
t,
state,
coreContext,
}: JSXAttributeHandler): void {
if (!coreContext.twinConfig.includeClassNames) return
if (path.node.name.name !== 'className') return
const nodeValue = path.node.value
if (!nodeValue) return
// Ignore className if it cannot be resolved
if ((nodeValue as T.JSXExpressionContainer).expression) return
const rawClasses = (nodeValue as T.StringLiteral).value
if (!rawClasses) return
const { styles, unmatched, matched } = getStyles(rawClasses, {
...coreContext,
isSilent: true,
})
if (matched.length === 0) return
const astStyles = astify(isEmpty(styles) ? {} : styles, t)
// When classes can't be matched we add them back into the className (it exists as a few properties)
const unmatchedClasses = unmatched.join(' ')
if (!path.node.value) return
;(path.node.value as T.StringLiteral).value = unmatchedClasses
if (path.node.value.extra) {
path.node.value.extra.rawValue = unmatchedClasses
path.node.value.extra.raw = `"${unmatchedClasses}"`
}
const jsxPath = getParentJSX(path)
const attributes = jsxPath.get('attributes')
const { attribute: cssAttribute } = getCssAttributeData(attributes)
if (!cssAttribute) {
const attribute = makeJsxAttribute(['css', astStyles], t)
if (unmatchedClasses) {
path.insertAfter(attribute)
} else {
path.replaceWith(attribute)
}
const pathParameters = {
t,
path,
state,
attributes,
coreContext,
rawClasses: matched.join(' '),
}
addDataTwPropToPath(pathParameters)
return
}
const cssExpression = (cssAttribute as NodePath)
.get('value')
.get('expression') as NodePath
const attributeNames = getAttributeNames(jsxPath)
const isBeforeCssAttribute =
attributeNames.indexOf('className') - attributeNames.indexOf('css') < 0
if (cssExpression.isArrayExpression()) {
// The existing css prop is an array, eg: css={[...]}
if (isBeforeCssAttribute) {
cssExpression.unshiftContainer('elements', astStyles)
} else {
cssExpression.pushContainer('elements', astStyles)
}
} else {
// The existing css prop is not an array, eg: css={{ ... }} / css={`...`}
const existingCssAttribute = cssExpression.node
coreContext.assert(Boolean(existingCssAttribute), ({ color }) =>
color(
`✕ An empty css prop (css="") isn’t supported alongside the className prop`
)
)
const styleArray = isBeforeCssAttribute
? [astStyles, existingCssAttribute]
: [existingCssAttribute, astStyles]
cssExpression.replaceWith(t.arrayExpression(styleArray))
}
if (!unmatchedClasses) path.remove()
addDataPropToExistingPath({
t,
attributes,
rawClasses: matched.join(' '),
path: jsxPath,
state,
coreContext,
})
}
export { handleClassNameProperty }
================================================
FILE: src/macro/css.ts
================================================
import { addImport, makeStyledComponent } from './lib/astHelpers'
import isEmpty from './lib/util/isEmpty'
import type {
T,
AdditionalHandlerParameters,
HandlerParameters,
NodePath,
} from './types'
function updateCssReferences({
references,
state,
}: AdditionalHandlerParameters): void {
if (state.existingCssIdentifier) return
const cssReferences = references.css
if (isEmpty(cssReferences)) return
cssReferences.forEach(path => {
// @ts-expect-error Setting value on target
path.node.name = state.cssIdentifier.name
})
}
function addCssImport({
references,
program,
t,
state,
coreContext,
}: AdditionalHandlerParameters): void {
if (!state.isImportingCss) {
const shouldImport =
!isEmpty(references.css) && !state.existingCssIdentifier
if (!shouldImport) return
}
if (state.existingCssIdentifier) return
if (!coreContext.importConfig.css) return
addImport({
types: t,
program,
name: coreContext.importConfig.css.import,
mod: coreContext.importConfig.css.from,
identifier: state.cssIdentifier,
})
}
function convertHtmlElementToStyled(
params: HandlerParameters & { path: NodePath }
): void {
const { path, t, coreContext } = params
if (!coreContext.twinConfig.convertHtmlElementToStyled) return
const jsxPath = path.get('openingElement')
makeStyledComponent({
...params,
jsxPath,
secondArg: t.objectExpression([]),
fromProp: 'css',
})
}
export { updateCssReferences, addCssImport, convertHtmlElementToStyled }
================================================
FILE: src/macro/dataProp.ts
================================================
import type { AddDataPropToExistingPath, T } from './types'
const SPACE_ID = '_'
const EXTRA_WHITESPACE = /\s\s+/g
const LINEFEED = /\n/g
function formatProp(classes: string): string {
return (
classes
// Normalize spacing
.replace(EXTRA_WHITESPACE, ' ')
// Remove newline characters
.replace(LINEFEED, ' ')
// Replace the space id
.replace(SPACE_ID, ' ')
.trim()
)
}
function addDataTwPropToPath({
t,
attributes,
rawClasses,
path,
state,
coreContext,
propName = 'data-tw',
}: AddDataPropToExistingPath): void {
const dataTwPropAllEnvironments =
propName === 'data-tw' && coreContext.twinConfig.dataTwProp === 'all'
const dataCsPropAllEnvironments =
propName === 'data-cs' && coreContext.twinConfig.dataCsProp === 'all'
if (!state.isDev && !dataTwPropAllEnvironments && !dataCsPropAllEnvironments)
return
if (propName === 'data-tw' && !coreContext.twinConfig.dataTwProp) return
if (propName === 'data-cs' && !coreContext.twinConfig.dataCsProp) return
// A for in loop looping over attributes and removing the one we want
for (const p of attributes) {
if (p.type === 'JSXSpreadAttribute') continue
const nodeName = p.node as T.JSXAttribute
if (nodeName?.name && nodeName.name.name === propName) p.remove()
}
const classes = formatProp(rawClasses)
// Add the attribute
path.insertAfter(
t.jsxAttribute(t.jsxIdentifier(propName), t.stringLiteral(classes))
)
}
function addDataPropToExistingPath({
t,
attributes,
rawClasses,
path,
state,
coreContext,
propName = 'data-tw',
}: AddDataPropToExistingPath): void {
const dataTwPropAllEnvironments =
propName === 'data-tw' && coreContext.twinConfig.dataTwProp === 'all'
const dataCsPropAllEnvironments =
propName === 'data-cs' && coreContext.twinConfig.dataCsProp === 'all'
if (!state.isDev && !dataTwPropAllEnvironments && !dataCsPropAllEnvironments)
return
if (propName === 'data-tw' && !coreContext.twinConfig.dataTwProp) return
if (propName === 'data-cs' && !coreContext.twinConfig.dataCsProp) return
// Append to the existing debug attribute
const dataProperty = attributes.find(
p =>
(p.node as T.JSXAttribute)?.name &&
(p.node as T.JSXAttribute).name.name === propName
)
if (dataProperty) {
try {
// Existing data prop
if (
((dataProperty.node as T.JSXAttribute).value as T.StringLiteral).value
) {
;(
(dataProperty.node as T.JSXAttribute).value as T.StringLiteral
).value = `${[
((dataProperty.node as T.JSXAttribute).value as T.StringLiteral)
.value,
rawClasses,
]
.filter(Boolean)
.join(' | ')}`
return
}
// New data prop
const attribute = (dataProperty.node as T.JSXAttribute)
.value as T.JSXExpressionContainer
// @ts-expect-error Setting value on target
attribute.expression.value = `${[
// @ts-expect-error Okay with value not on all expression types
(dataProperty.node.value as T.JSXExpressionContainer).expression.value,
rawClasses,
]
.filter(Boolean)
.join(' | ')}`
} catch (_: unknown) {}
return
}
const classes = formatProp(rawClasses)
// Add a new attribute
path.pushContainer(
// @ts-expect-error Key is never
'attributes',
t.jSXAttribute(
t.jSXIdentifier(propName),
t.jSXExpressionContainer(t.stringLiteral(classes))
)
)
}
export { addDataTwPropToPath, addDataPropToExistingPath }
================================================
FILE: src/macro/globalStyles.ts
================================================
// eslint-disable-next-line import/no-relative-parent-imports
import { getGlobalStyles } from '../core'
import template from '@babel/template'
import {
addImport,
generateUid,
generateTaggedTemplateExpression,
} from './lib/astHelpers'
import type {
CoreContext,
AdditionalHandlerParameters,
NodePath,
State,
T,
CssObject,
} from './types'
const KEBAB_CANDIDATES = /([\da-z]|(?=[A-Z]))([A-Z])/g
type AddGlobalStylesImport = {
program: NodePath
t: typeof T
identifier: T.Identifier
coreContext: CoreContext
}
function addGlobalStylesImport({
program,
t,
identifier,
coreContext,
}: AddGlobalStylesImport): void {
addImport({
types: t,
program,
identifier,
name: coreContext.importConfig.global.import,
mod: coreContext.importConfig.global.from,
})
}
export type DeclarationParameters = {
t: typeof T
state: State
globalUid: T.Identifier
stylesUid: T.Identifier
styles: string | undefined
}
function getGlobalDeclarationTte({
t,
stylesUid,
globalUid,
styles,
}: DeclarationParameters): T.VariableDeclaration {
return t.variableDeclaration('const', [
t.variableDeclarator(
globalUid,
generateTaggedTemplateExpression({ t, identifier: stylesUid, styles })
),
])
}
function getGlobalDeclarationProperty(
params: DeclarationParameters
): T.VariableDeclaration {
const { t, stylesUid, globalUid, state, styles } = params
const ttExpression = generateTaggedTemplateExpression({
t,
identifier: state.cssIdentifier as T.Identifier,
styles,
})
const openingElement = t.jsxOpeningElement(
t.jsxIdentifier(stylesUid.name),
[
t.jsxAttribute(
t.jsxIdentifier('styles'),
t.jsxExpressionContainer(ttExpression)
),
],
true
)
const closingElement = t.jsxClosingElement(t.jsxIdentifier('close'))
const arrowFunctionExpression = t.arrowFunctionExpression(
[],
t.jsxElement(openingElement, closingElement, [], true)
)
const code = t.variableDeclaration('const', [
t.variableDeclarator(globalUid, arrowFunctionExpression),
])
return code
}
function kebabize(string: string): string {
return string.replace(KEBAB_CANDIDATES, '$1-$2').toLowerCase()
}
function convert(k: string, v: string | number): string {
return typeof v === 'string'
? ` ${kebabize(k)}: ${v};`
: `${k} {
${convertCssObjectToString(v)}
}`
}
function convertCssObjectToString(
cssObject: CssObject | string | number | undefined
): string {
if (!cssObject) return ''
return Object.entries(cssObject)
.map(([k, v]) => convert(k, v))
.join('\n')
}
function handleGlobalStylesFunction(params: AdditionalHandlerParameters): void {
const { references } = params
if (references.GlobalStyles) handleGlobalStylesJsx(params)
if (references.globalStyles) handleGlobalStylesVariable(params)
}
function handleGlobalStylesVariable(params: AdditionalHandlerParameters): void {
const { references } = params
if (references.globalStyles.length === 0) return
const styles = getGlobalStyles(params.coreContext)
references.globalStyles.forEach(path => {
const templateStyles = `(${JSON.stringify(styles)})` // `template` requires () wrapping
const convertedStyles = template(templateStyles, {
placeholderPattern: false,
})()
path.replaceWith(convertedStyles as NodePath)
})
}
function handleGlobalStylesJsx(params: AdditionalHandlerParameters): void {
const { references, program, t, state, coreContext } = params
if (references.GlobalStyles.length === 0) return
coreContext.assert(
references.GlobalStyles.length < 2,
({ color }) =>
`${color(
`✕ Only one can be added per file`
)}\n\nNeed something custom?\nUse the \`globalStyles\` import for a style object you can work with`
)
const path = references.GlobalStyles[0]
const parentPath = path.findParent(x => x.isJSXElement())
coreContext.assert(
Boolean(parentPath),
({ color }) =>
`${color(
`✕ The \`GlobalStyles\` import must be added as a JSX element`
)}\neg: \`\`\n\nNeed something custom?\nUse the \`globalStyles\` import for a style object you can work with`
)
const globalStyles = getGlobalStyles(params.coreContext)
const styles = convertCssObjectToString(globalStyles)
const globalUid = generateUid('GlobalStyles', program)
const stylesUid = generateUid('globalImport', program)
const declarationData = { t, globalUid, stylesUid, styles, state }
if (coreContext.packageUsed.isStyledComponents) {
const declaration = getGlobalDeclarationTte(declarationData)
program.unshiftContainer('body', declaration)
path.replaceWith(t.jSXIdentifier(globalUid.name))
}
if (coreContext.packageUsed.isEmotion) {
const declaration = getGlobalDeclarationProperty(declarationData)
program.unshiftContainer('body', declaration)
path.replaceWith(t.jSXIdentifier(globalUid.name))
// Check if the css import has already been imported
// https://github.com/ben-rogerson/twin.macro/issues/313
state.isImportingCss = !state.existingCssIdentifier
}
if (coreContext.packageUsed.isGoober || coreContext.packageUsed.isSolid) {
const declaration = getGlobalDeclarationTte(declarationData)
program.unshiftContainer('body', declaration)
path.replaceWith(t.jSXIdentifier(globalUid.name))
}
coreContext.assert(
Boolean(!coreContext.packageUsed.isStitches),
({ color }) =>
`${color(
`✕ The ${color(
'GlobalStyles',
'errorLight'
)} import can’t be used with stitches`
)}\n\nUse the ${color(`globalStyles`, 'success')} import instead`
)
addGlobalStylesImport({
identifier: stylesUid,
t,
program,
coreContext,
})
}
export { handleGlobalStylesFunction }
================================================
FILE: src/macro/lib/astHelpers.ts
================================================
import get from './util/get'
import type {
T,
State,
NodePath,
CoreContext,
ImportDeclarationHandler,
} from 'macro/types'
function addImport({
types: t,
program,
mod,
name,
identifier,
}: {
types: typeof T
program: NodePath
mod: string
name: string
identifier: T.Identifier
}): void {
const importName =
name === 'default'
? [t.importDefaultSpecifier(identifier)]
: name
? [t.importSpecifier(identifier, t.identifier(name))]
: []
program.unshiftContainer(
'body',
t.importDeclaration(importName, t.stringLiteral(mod))
)
}
/**
* Convert plain js into babel ast
*/
function astify(
literal: unknown,
t: typeof T
):
| T.NullLiteral
| T.UnaryExpression
| T.NumericLiteral
| T.BooleanLiteral
| T.StringLiteral
| T.Expression {
if (literal === null) {
return t.nullLiteral()
}
switch (typeof literal) {
case 'function': {
return t.unaryExpression('void', t.numericLiteral(0), true)
}
case 'number': {
return t.numericLiteral(literal)
}
case 'boolean': {
return t.booleanLiteral(literal)
}
case 'undefined': {
return t.unaryExpression('void', t.numericLiteral(0), true)
}
case 'string': {
return t.stringLiteral(literal)
}
default: {
if (Array.isArray(literal)) {
return t.arrayExpression(literal.map(x => astify(x, t)))
}
return t.objectExpression(
objectExpressionElements(literal as Record, t)
)
}
}
}
function objectExpressionElements(
literal: Record,
t: typeof T
): T.ObjectProperty[] {
return Object.keys(literal)
.filter(k => typeof literal[k] !== 'undefined')
.map(
(k: string): T.ObjectProperty =>
t.objectProperty(t.stringLiteral(k), astify(literal[k], t))
)
}
function setStyledIdentifier({
state,
path,
coreContext,
}: ImportDeclarationHandler): void {
const importFromStitches =
coreContext.packageUsed.isStitches &&
coreContext.importConfig.styled.from.includes(path.node.source.value)
const importFromLibrary =
path.node.source.value === coreContext.importConfig.styled.from
if (!importFromLibrary && !importFromStitches) return
// Look for an existing import that matches the config,
// if found then reuse it for the rest of the function calls
path.node.specifiers.some(specifier => {
if (
specifier.type === 'ImportDefaultSpecifier' &&
coreContext.importConfig.styled.import === 'default' &&
// fixes an issue in gatsby where the styled-components plugin has run
// before twin. fix is to ignore import aliases which babel creates
// https://github.com/ben-rogerson/twin.macro/issues/192
!specifier.local.name.startsWith('_')
) {
state.styledIdentifier = specifier.local
state.existingStyledIdentifier = true
return true
}
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === coreContext.importConfig.styled.import
) {
state.styledIdentifier = specifier.local
state.existingStyledIdentifier = true
return true
}
state.existingStyledIdentifier = false
return false
})
}
function setCssIdentifier({
state,
path,
coreContext,
}: ImportDeclarationHandler): void {
const importFromStitches =
coreContext.packageUsed.isStitches &&
coreContext.importConfig.css.from.includes(path.node.source.value)
const isLibraryImport =
path.node.source.value === coreContext.importConfig.css.from
if (!isLibraryImport && !importFromStitches) return
// Look for an existing import that matches the config,
// if found then reuse it for the rest of the function calls
path.node.specifiers.some(specifier => {
if (
specifier.type === 'ImportDefaultSpecifier' &&
coreContext.importConfig.css.import === 'default'
) {
state.cssIdentifier = specifier.local
state.existingCssIdentifier = true
return true
}
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === coreContext.importConfig.css.import
) {
state.cssIdentifier = specifier.local
state.existingCssIdentifier = true
return true
}
state.existingCssIdentifier = false
return false
})
}
function getStringFromTTE(path: NodePath): string {
let getRawValue = false
let rawValue = ''
// Convert basic interpolated variables defined in the same file
const evaluatedValue = (path.get('quasi').evaluate().value as string) ?? ''
if (evaluatedValue === '') getRawValue = true
// Evaluating strips escaping, so if there's a square bracket we know it's an
// arbitrary value/property/variant and should grab the raw value
if (evaluatedValue.includes('[')) getRawValue = true
if (getRawValue)
rawValue = (path.get('quasi.quasis') as Array>)
.map(q => q.node.value.raw)
.join('')
// Trigger error due to non-evaluated value, eg:`w-[${sizes.width}]`
if (evaluatedValue.length === 0 && rawValue.length > 0) return 'null'
// Return raw classes with escaping, eg: [content\!]:block
if (rawValue.length > evaluatedValue.length) return rawValue
return evaluatedValue
}
// Parse tagged template arrays (``)
function parseTte(
path: NodePath,
{ t, state }: { t: typeof T; state: State }
): { string: string; path: NodePath } | undefined {
const cloneNode = t.cloneNode || t.cloneDeep
const tagType = path.node.tag.type
if (
tagType !== 'Identifier' &&
tagType !== 'MemberExpression' &&
tagType !== 'CallExpression'
)
return
const string = getStringFromTTE(path)
// Grab the path location before changing it
const stringLoc = path.get('quasi').node.loc
if (tagType === 'CallExpression') {
replaceWithLocation(
path.get('tag').get('callee') as NodePath,
// @ts-expect-error Source type doesn’t include `Identifier` as possible type
cloneNode(state.styledIdentifier)
)
state.isImportingStyled = true
} else if (tagType === 'MemberExpression') {
replaceWithLocation(
path.get('tag').get('object') as NodePath,
// @ts-expect-error Source type doesn’t include `Identifier` as possible type
cloneNode(state.styledIdentifier)
)
state.isImportingStyled = true
}
if (tagType === 'CallExpression' || tagType === 'MemberExpression') {
replaceWithLocation(
path,
t.callExpression(cloneNode(path.node.tag), [
t.identifier('__twPlaceholder'),
]) as unknown as NodePath
)
path = (
path.get('arguments') as Array>
)[0]
}
path.node.loc = stringLoc // Restore the original path location
return { string, path }
}
function replaceWithLocation(
path: NodePath,
replacement: NodePath | T.Expression | T.ExpressionStatement
): [NodePath] | EmptyArray[] {
const { loc } = path.node
const newPaths = replacement ? path.replaceWith(replacement) : []
if (Array.isArray(newPaths) && newPaths.length > 0) {
newPaths.forEach(p => {
p.node.loc = loc
})
}
return newPaths
}
function generateUid(name: string, program: NodePath): T.Identifier {
return program.scope.generateUidIdentifier(name)
}
function getParentJSX(path: NodePath): NodePath {
return path.findParent(p =>
p.isJSXOpeningElement()
) as NodePath
}
function getAttributeNames(jsxPath: NodePath): string[] {
const attributes = jsxPath.get('attributes') as Array<
NodePath
>
const attributeNames = attributes.map(p => p.node.name?.name) as string[]
return attributeNames
}
function getCssAttributeData(
attributes: NodeType[]
): {
index: number
hasCssAttribute: boolean
attribute: NodeType | undefined
} {
if (!String(attributes))
return { index: 0, hasCssAttribute: false, attribute: undefined }
const index = attributes.findIndex(
attribute =>
attribute?.isJSXAttribute() &&
((attribute.get('name.name') as NodePath).node as unknown as string) ===
'css'
)
return { index, hasCssAttribute: index >= 0, attribute: attributes[index] }
}
function getFunctionValue(
path: NodePath
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { parent: NodePath; input: any } | undefined {
if (path.parent.type !== 'CallExpression') return
const parent = path.findParent(x => x.isCallExpression())
if (!parent) return
const argument = (parent.get('arguments') as NodePath[])[0] || ''
return {
parent,
input: argument.evaluate && (argument.evaluate().value as string),
}
}
function getTaggedTemplateValue(
path: Path
): { parent: NodePath; input: string } | undefined {
if (path.parent.type !== 'TaggedTemplateExpression') return
const parent = path.findParent(x =>
x.isTaggedTemplateExpression()
) as NodePath
if (!parent) return
if (parent.node.tag.type !== 'Identifier') return
return { parent, input: parent.get('quasi').evaluate().value as string }
}
function getMemberExpression(
path: NodePath
): { parent: NodePath; input: string } | undefined {
if (path.parent.type !== 'MemberExpression') return
const parent = path.findParent(x =>
x.isMemberExpression()
) as NodePath
if (!parent) return
return {
parent,
// @ts-expect-error name doesn't exist on node
input: parent.get('property').node.name as string,
}
}
function generateTaggedTemplateExpression({
t,
identifier,
styles,
}: {
t: typeof T
identifier: T.Identifier
styles: string | undefined
}): T.TaggedTemplateExpression {
const backtickStyles = t.templateElement({
raw: `${styles ?? ''}`,
cooked: `${styles ?? ''}`,
})
const ttExpression = t.taggedTemplateExpression(
identifier,
t.templateLiteral([backtickStyles], [])
)
return ttExpression
}
function isComponent(name: string): boolean {
return name.slice(0, 1).toUpperCase() === name.slice(0, 1)
}
const jsxSingleDotError = `The css prop + tw props can only be added to jsx elements with a single dot in their name (or no dot at all).`
function getFirstStyledArgument(
jsxPath: NodePath,
t: typeof T,
assert: CoreContext['assert']
): T.MemberExpression | T.Identifier | T.StringLiteral {
const path = get(jsxPath, 'node.name.name') as string
if (path)
return isComponent(path) ? t.identifier(path) : t.stringLiteral(path)
const dotComponent = get(jsxPath, 'node.name') as string
assert(Boolean(dotComponent), () => jsxSingleDotError)
// Element name has dots in it
const objectName = get(dotComponent, 'object.name') as string
assert(Boolean(objectName), () => jsxSingleDotError)
const propertyName = get(dotComponent, 'property.name') as string
assert(Boolean(propertyName), () => jsxSingleDotError)
return t.memberExpression(
t.identifier(objectName),
t.identifier(propertyName)
)
}
type MakeStyledComponent = {
t: typeof T
secondArg: T.Expression | T.StringLiteral | T.Identifier
jsxPath: NodePath
program: NodePath
state: State
coreContext: CoreContext
fromProp: 'tw' | 'css'
}
type CreateStyledProps = Pick<
MakeStyledComponent,
'jsxPath' | 't' | 'secondArg'
> & {
stateStyled: T.Identifier
constName: T.Identifier
firstArg: T.MemberExpression | T.Identifier | T.StringLiteral
}
function createStyledPropsForTw({
t,
stateStyled,
firstArg,
secondArg,
constName,
}: CreateStyledProps): T.VariableDeclaration {
const callee = t.callExpression(stateStyled, [firstArg])
const declarations = [
t.variableDeclarator(constName, t.callExpression(callee, [secondArg])),
]
return t.variableDeclaration('const', declarations)
}
function createStyledPropsForCss(
args: CreateStyledProps
): T.VariableDeclaration | undefined {
const cssPropAttribute = args.jsxPath
.get('attributes')
.find(
p =>
p.isJSXAttribute() &&
p.get('name').isJSXIdentifier() &&
p.get('name')?.node.name === 'css'
)
const cssPropValue = cssPropAttribute?.get(
'value'
) as NodePath
const expression = cssPropValue?.node?.expression
if (!expression || expression.type === 'JSXEmptyExpression') return
cssPropAttribute?.remove()
return createStyledPropsForTw({ ...args, secondArg: expression })
}
function makeStyledComponent({
t,
secondArg,
jsxPath,
program,
state,
coreContext,
fromProp,
}: MakeStyledComponent): void {
const constName = program.scope.generateUidIdentifier('TwComponent')
if (!state.styledIdentifier) {
state.styledIdentifier = generateUid('styled', program)
state.isImportingStyled = true
}
const firstArg = getFirstStyledArgument(jsxPath, t, coreContext.assert)
let styledDefinition = null
const stateStyled: T.Identifier = state.styledIdentifier
if (coreContext.packageUsed.isSolid) {
const params = { jsxPath, t, stateStyled, firstArg, secondArg, constName }
styledDefinition =
fromProp === 'tw'
? createStyledPropsForTw(params)
: createStyledPropsForCss(params)
} else {
const args = [firstArg, secondArg].filter(Boolean)
const init = t.callExpression(stateStyled, args)
const declarations = [t.variableDeclarator(constName, init)]
styledDefinition = t.variableDeclaration('const', declarations)
}
if (!styledDefinition) return
const rootParentPath = jsxPath.findParent(p =>
p.parentPath ? p.parentPath.isProgram() : false
) as NodePath
if (rootParentPath) rootParentPath.insertBefore(styledDefinition)
if (t.isMemberExpression(firstArg)) {
// Replace components with a dot, eg: Dialog.blah
const id = t.jsxIdentifier(constName.name)
jsxPath.get('name').replaceWith(id)
if (jsxPath.node.selfClosing) return
;(jsxPath.parentPath.get('closingElement.name') as NodePath).replaceWith(id)
} else {
;(jsxPath.node.name as T.JSXIdentifier).name = constName.name
if (jsxPath.node.selfClosing) return
// @ts-expect-error Untyped name replacement
jsxPath.parentPath.node.closingElement.name.name = constName.name
}
}
function getJsxAttributes(
path: NodePath
): Array> {
const attributes = path.get('openingElement.attributes') as Array<
NodePath
>
return attributes.filter(a => a.isJSXAttribute())
}
export {
addImport,
astify,
parseTte,
replaceWithLocation,
setStyledIdentifier,
setCssIdentifier,
generateUid,
getParentJSX,
getAttributeNames,
getCssAttributeData,
getFunctionValue,
getTaggedTemplateValue,
getMemberExpression,
generateTaggedTemplateExpression,
makeStyledComponent,
getJsxAttributes,
}
================================================
FILE: src/macro/lib/util/get.ts
================================================
import get from 'lodash.get'
// eslint-disable-next-line unicorn/prefer-export-from
export default get
================================================
FILE: src/macro/lib/util/isEmpty.ts
================================================
function isEmpty(value: unknown): boolean {
return (
value === undefined ||
value === null ||
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'string' && value.trim().length === 0)
)
}
export default isEmpty
================================================
FILE: src/macro/lib/validateImports.ts
================================================
import type { CoreContext, MacroParams } from 'macro/types'
const validImports = new Set([
'default',
'styled',
'css',
'theme',
'screen',
'TwStyle',
'TwComponent',
'ThemeStyle',
'GlobalStyles',
'globalStyles',
])
export default function validateImports(
imports: MacroParams['references'],
coreContext: CoreContext
): void {
const importTwAsNamedNotDefault = Object.keys(imports).find(
reference => reference === 'tw'
)
coreContext.assert(
!importTwAsNamedNotDefault,
({ color }) =>
`${color(
`✕ import { tw } from 'twin.macro'`
)}\n\nUse the default export for \`tw\`:\n\n${color(
`import tw from 'twin.macro'`,
'success'
)}`
)
const unsupportedImport = Object.keys(imports).find(
reference => !validImports.has(reference)
)
coreContext.assert(
!unsupportedImport,
({ color }) =>
`${color(
`✕ Twin doesn't recognize { ${String(unsupportedImport)} }`
)}\n\nTry one of these imports:\n\nimport ${color(
'tw',
'success'
)}, { ${color('styled', 'success')}, ${color('css', 'success')}, ${color(
'theme',
'success'
)}, ${color('screen', 'success')}, ${color(
'GlobalStyles',
'success'
)}, ${color('globalStyles', 'success')} } from 'twin.macro'`
)
}
================================================
FILE: src/macro/screen.ts
================================================
import {
replaceWithLocation,
astify,
getFunctionValue,
getTaggedTemplateValue,
getMemberExpression,
} from './lib/astHelpers'
import type {
AdditionalHandlerParameters,
T,
NodePath,
CoreContext,
} from './types'
type GetDirectReplacement = Pick<
HandleDefinition,
'mediaQuery' | 'parent' | 't'
>
function getDirectReplacement({
mediaQuery,
parent,
t,
}: GetDirectReplacement): Expression {
return {
newPath: parent,
replacement: astify(mediaQuery, t),
}
}
type ScreenValues =
| string
| { raw?: string; min?: string; max?: string }
| Array<{ raw?: string; min?: string; max?: string }>
type GetMediaQuery = {
input: string | string[]
screens: Record
assert: CoreContext['assert']
}
type Expression = {
newPath: NodePath
replacement: T.TemplateLiteral | T.ObjectExpression | T.Expression
}
type HandleDefinition = {
mediaQuery: string
parent: NodePath
type: string
t: typeof T
}
function handleDefinition({
mediaQuery,
parent,
type,
t,
}: HandleDefinition): undefined | (() => Expression) {
return {
TaggedTemplateExpression(): {
newPath: NodePath
replacement: T.TemplateLiteral
} {
const newPath = parent.findParent(x =>
x.isTaggedTemplateExpression()
) as NodePath
const query = [`${mediaQuery} { `, ` }`]
const quasis = [
t.templateElement({ raw: query[0], cooked: query[0] }, false),
t.templateElement({ raw: query[1], cooked: query[1] }, true),
]
const expressions = [newPath.get('quasi').node]
const replacement = t.templateLiteral(quasis, expressions)
return { newPath, replacement }
},
CallExpression(): { newPath: NodePath; replacement: T.ObjectExpression } {
const newPath = parent.findParent(x =>
x.isCallExpression()
) as NodePath
const value = newPath.get('arguments')[0].node as T.Expression
const replacement = t.objectExpression([
t.objectProperty(t.stringLiteral(mediaQuery), value),
])
return { newPath, replacement }
},
ObjectProperty(): Expression {
// Remove brackets around keys so merges work with tailwind screens
// styled.div({ [screen`2xl`]: tw`block`, ...tw`2xl:inline` })
// https://github.com/ben-rogerson/twin.macro/issues/379
// @ts-expect-error unsure of parent type
parent.parent.computed = false
return getDirectReplacement({ mediaQuery, parent, t })
},
ExpressionStatement: () => getDirectReplacement({ mediaQuery, parent, t }),
ArrowFunctionExpression: () =>
getDirectReplacement({ mediaQuery, parent, t }),
ArrayExpression: () => getDirectReplacement({ mediaQuery, parent, t }),
BinaryExpression: () => getDirectReplacement({ mediaQuery, parent, t }),
LogicalExpression: () => getDirectReplacement({ mediaQuery, parent, t }),
ConditionalExpression: () =>
getDirectReplacement({ mediaQuery, parent, t }),
VariableDeclarator: () => getDirectReplacement({ mediaQuery, parent, t }),
TemplateLiteral: () => getDirectReplacement({ mediaQuery, parent, t }),
TSAsExpression: () => getDirectReplacement({ mediaQuery, parent, t }),
}[type]
}
function getMediaQuery({ input, screens, assert }: GetMediaQuery): string {
const _input =
typeof input === 'string' ? input.split(',').map(s => s.trim()) : input
const _screens = _input.map(s => screens[s])
_input.forEach(i => {
assert(
Boolean(screens[i]),
({ color }) =>
`${color(
`${
input
? `✕ ${color(i, 'errorLight')} wasn’t found in your`
: 'Specify a screen value from your'
} tailwind config`
)}\n\nTry one of these values:\n\n${Object.entries(screens)
.map(
([k, v]) =>
`${color('-', 'subdued')} screen(${color(
`'${k}'`,
'success'
)})({ ... }) (${String(v)})`
)
.join('\n')}`
)
})
const mediaQuery = _screens
.map(screen => {
if (typeof screen === 'string') return `(min-width: ${screen})`
if (!Array.isArray(screen) && typeof screen.raw === 'string')
return screen.raw
return (Array.isArray(screen) ? screen : [screen])
.map(range =>
[
typeof range.min === 'string' ? `(min-width: ${range.min})` : null,
typeof range.max === 'string' ? `(max-width: ${range.max})` : null,
]
.filter(Boolean)
.join(' and ')
)
.join(', ')
})
.join(', ')
return mediaQuery ? `@media ${mediaQuery}` : ''
}
function handleScreenFunction({
references,
t,
coreContext,
}: AdditionalHandlerParameters): void {
if (!references.screen) return
const screens = coreContext.theme('screens') as Record
references.screen.forEach(path => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { input, parent } = getTaggedTemplateValue(path) ?? // screen.lg``
getFunctionValue(path) ?? // screen.lg({ })
getMemberExpression(path) ?? {
// screen`lg`
input: null,
parent: null,
}
const definition = handleDefinition({
type: (parent as NodePath).parent.type,
mediaQuery: getMediaQuery({
input: input as string,
screens,
assert: coreContext.assert,
}),
parent: parent as NodePath,
t,
})
coreContext.assert(
Boolean(definition),
({ color }) =>
`${color(
`✕ The screen import doesn’t support that syntax`
)}\n\nTry using it like this: ${color(
[Object.keys(screens)[0]].map(f => `screen("${f}")`).join(''),
'success'
)}`
)
const { newPath, replacement } = (definition as () => Expression)()
replaceWithLocation(newPath, replacement)
})
}
export { handleScreenFunction }
================================================
FILE: src/macro/shortCss.ts
================================================
/* eslint-disable @typescript-eslint/no-unsafe-call */
// eslint-disable-next-line import/no-relative-parent-imports
import { getStyles } from '../core'
import isEmpty from './lib/util/isEmpty'
import { addDataTwPropToPath, addDataPropToExistingPath } from './dataProp'
import {
astify,
getParentJSX,
getAttributeNames,
getCssAttributeData,
} from './lib/astHelpers'
import type { NodePath, T, JSXAttributeHandler } from './types'
function handleCsProperty({
path,
t,
state,
coreContext,
}: JSXAttributeHandler): void {
if (coreContext.twinConfig.disableCsProp) return
if (!path.node || path.node.name.name !== 'cs') return
const nodeValue = path.node.value
const nodeExpression = (nodeValue as T.JSXExpressionContainer).expression
// Allow cs={"property[value]"}
const expressionValue =
nodeExpression &&
nodeExpression.type === 'StringLiteral' &&
nodeExpression.value
if (nodeExpression)
coreContext.assert(
Boolean(expressionValue),
({ color }) =>
`${color(
`✕ Only complete classes can be used with the "cs" prop`
)}\n\nTry using it like this: ${color(
'',
'success'
)}\n\nRead more at https://twinredirect.page.link/cs-classes`
)
const rawClasses =
expressionValue || (nodeValue as T.StringLiteral).value || ''
const { styles } = getStyles(rawClasses, {
isShortCssOnly: true,
...coreContext,
})
const astStyles = astify(isEmpty(styles) ? {} : styles, t)
const jsxPath = getParentJSX(path)
const attributes = jsxPath.get('attributes')
const { attribute: cssAttribute } = getCssAttributeData(attributes)
if (!cssAttribute) {
// Replace the tw prop with the css prop
path.replaceWith(
t.jsxAttribute(
t.jsxIdentifier('css'),
t.jsxExpressionContainer(astStyles)
)
)
addDataTwPropToPath({
t,
attributes,
rawClasses,
path,
state,
coreContext,
propName: 'data-cs',
})
return
}
// The expression is the value as a NodePath
const attributeValuePath = cssAttribute.get('value')
// If it's not {} or "", get out of here
if (
!attributeValuePath ||
// @ts-expect-error The type checking functions don't exist on NodePath
(!attributeValuePath.isJSXExpressionContainer() &&
// @ts-expect-error The type checking functions don't exist on NodePath
!attributeValuePath.isStringLiteral())
)
return
// @ts-expect-error The type checking functions don't exist on NodePath
const existingCssAttribute = attributeValuePath.isStringLiteral()
? (attributeValuePath as unknown as NodePath)
: // @ts-expect-error get doesn’t exist on the types
(attributeValuePath.get(
'expression'
) as NodePath)
const attributeNames = getAttributeNames(jsxPath)
const isBeforeCssAttribute =
attributeNames.indexOf('cs') - attributeNames.indexOf('css') < 0
if (existingCssAttribute.isArrayExpression()) {
// The existing css prop is an array, eg: css={[...]}
if (isBeforeCssAttribute) {
// @ts-expect-error unshiftContainer doesn't exist on NodePath
existingCssAttribute.unshiftContainer('elements', astStyles)
} else {
// @ts-expect-error pushContainer doesn't exist on NodePath
existingCssAttribute.pushContainer('elements', astStyles)
}
} else {
// css prop is either:
// TemplateLiteral
//
// or an ObjectExpression
//
// or ArrowFunctionExpression/FunctionExpression
//
(...)} cs="..." />
const existingCssAttributeNode = existingCssAttribute.node
// The existing css prop is an array, eg: css={[...]}
const styleArray = isBeforeCssAttribute
? [astStyles, existingCssAttributeNode]
: [existingCssAttributeNode, astStyles]
const arrayExpression = t.arrayExpression(styleArray as T.Expression[])
const { parent } = existingCssAttribute
const replacement =
parent.type === 'JSXAttribute'
? t.jsxExpressionContainer(arrayExpression)
: arrayExpression
existingCssAttribute.replaceWith(replacement)
}
path.remove() // remove the cs prop
addDataPropToExistingPath({
t,
attributes,
rawClasses,
path: jsxPath,
state,
coreContext,
propName: 'data-cs',
})
}
export { handleCsProperty }
================================================
FILE: src/macro/styled.ts
================================================
import { addImport, replaceWithLocation } from './lib/astHelpers'
import isEmpty from './lib/util/isEmpty'
import get from './lib/util/get'
import type { T, NodePath, AdditionalHandlerParameters } from './types'
function updateStyledReferences({
references,
state,
}: AdditionalHandlerParameters): void {
if (state.existingStyledIdentifier) return
const styledReferences = references.styled
if (isEmpty(styledReferences)) return
styledReferences.forEach(path => {
// @ts-expect-error Setting values is untyped
path.node.name = state.styledIdentifier.name
})
}
function addStyledImport({
references,
program,
t,
state,
coreContext,
}: AdditionalHandlerParameters): void {
if (!state.isImportingStyled) {
const shouldImport =
!isEmpty(references.styled) && !state.existingStyledIdentifier
if (!shouldImport) return
}
if (state.existingStyledIdentifier) return
addImport({
types: t,
program,
name: coreContext.importConfig.styled.import,
mod: coreContext.importConfig.styled.from,
identifier: state.styledIdentifier,
})
}
function moveDotElement({
path,
t,
moveToParam = true,
}: {
path: NodePath
t: typeof T
moveToParam: boolean
}): void {
if (path.parent.type !== 'MemberExpression') return
const parentCallExpression = path.findParent(x =>
x.isCallExpression()
) as NodePath
if (!parentCallExpression) return
const styledName = get(path, 'parentPath.node.property.name') as string
const styledArgs = get(parentCallExpression, 'node.arguments.0') as
| T.Expression
| T.SpreadElement
| T.JSXNamespacedName
| T.ArgumentPlaceholder
| T.ArrowFunctionExpression
let replacement
if (moveToParam) {
// `styled('div', {})`
const args = [t.stringLiteral(styledName), styledArgs].filter(Boolean)
replacement = t.callExpression((path as NodePath).node, args)
} else {
// `styled('div')({})`
const callee = t.callExpression((path as NodePath).node, [
t.stringLiteral(styledName),
])
replacement = t.expressionStatement(t.callExpression(callee, [styledArgs]))
}
replaceWithLocation(parentCallExpression, replacement)
}
function handleStyledFunction({
references,
t,
coreContext,
}: AdditionalHandlerParameters): void {
if (
!coreContext.twinConfig.convertStyledDotToParam &&
!coreContext.twinConfig.convertStyledDotToFunction
)
return
if (isEmpty(references)) return
const defaultRefs = references.default || []
const styledRefs = references.styled || []
const refs = [...defaultRefs, ...styledRefs].filter(Boolean)
refs.forEach((path: NodePath): void => {
// convert tw.div`` & styled.div`` to styled('div', {}) / styled('div')({})
moveDotElement({
path,
t,
moveToParam: coreContext.twinConfig.convertStyledDotToParam ?? true,
})
})
}
export { updateStyledReferences, addStyledImport, handleStyledFunction }
================================================
FILE: src/macro/theme.ts
================================================
import {
replaceWithLocation,
astify,
getFunctionValue,
getTaggedTemplateValue,
} from './lib/astHelpers'
import type { AssertContext } from 'core/types'
import type { AdditionalHandlerParameters, NodePath } from 'macro/types'
function handleThemeFunction({
references,
t,
coreContext,
}: AdditionalHandlerParameters): void {
if (!references.theme) return
references.theme.forEach((path): never[] | [Node | NodePath] => {
const ttValue = getTaggedTemplateValue(path) ??
getFunctionValue(path) ?? { input: null, parent: null }
const { input, parent } = ttValue as {
parent: NodePath
input?: string
}
if (input !== '')
coreContext.assert(
Boolean(input),
({ color }: AssertContext) =>
`${color(`✕ The theme value doesn’t look right`)}\n\nTry ${color(
'theme`colors.black`',
'success'
)} or ${color(`theme('colors.black')`, 'success')}`
)
coreContext.assert(
Boolean(parent),
({ color }: AssertContext) =>
`${color(
`✕ The theme value ${color(
input as string,
'errorLight'
)} doesn’t look right`
)}\n\nTry ${color('theme`colors.black`', 'success')} or ${color(
`theme('colors.black')`,
'success'
)}`
)
const themeValue = coreContext.theme(input as string)
coreContext.assert(Boolean(themeValue), ({ color }: AssertContext) =>
color(
`✕ ${color(
input as string,
'errorLight'
)} doesn’t match a theme value from the config`
)
)
return replaceWithLocation(parent, astify(themeValue, t))
})
}
export { handleThemeFunction }
================================================
FILE: src/macro/tw.ts
================================================
// eslint-disable-next-line import/no-relative-parent-imports
import { getStyles } from '../core'
// eslint-disable-next-line import/no-relative-parent-imports
import getSuggestions from '../suggestions'
import {
astify,
getParentJSX,
parseTte,
replaceWithLocation,
getAttributeNames,
getCssAttributeData,
makeStyledComponent,
} from './lib/astHelpers'
import isEmpty from './lib/util/isEmpty'
import { addDataTwPropToPath, addDataPropToExistingPath } from './dataProp'
import type {
AdditionalHandlerParameters,
CoreContext,
JSXAttributeHandler,
NodePath,
State,
T,
} from './types'
type MoveTwPropToStyled = {
t: typeof T
state: State
program: NodePath
astStyles: T.Expression
jsxPath: NodePath
coreContext: CoreContext
}
function moveTwPropToStyled(params: MoveTwPropToStyled): void {
const { jsxPath, astStyles } = params
makeStyledComponent({ ...params, secondArg: astStyles, fromProp: 'tw' })
// Remove the tw attribute
const tagAttributes = jsxPath.node.attributes
const twAttributeIndex = tagAttributes.findIndex(
n => n.type === 'JSXAttribute' && n.name && n.name.name === 'tw'
)
if (twAttributeIndex < 0) return
jsxPath.node.attributes.splice(twAttributeIndex, 1)
}
type MergeIntoCssAttribute = {
t: typeof T
path: NodePath
astStyles: T.Expression
cssAttribute: NodePath | undefined
}
function mergeIntoCssAttribute({
t,
path,
astStyles,
cssAttribute,
}: MergeIntoCssAttribute): void {
if (!cssAttribute) return
// The expression is the value as a NodePath
const attributeValuePath = cssAttribute.get('value')
// If it's not {} or "", get out of here
if (
!attributeValuePath ||
(!attributeValuePath.isJSXExpressionContainer() &&
!attributeValuePath.isStringLiteral())
)
return
const existingCssAttribute = attributeValuePath.isStringLiteral()
? (attributeValuePath as unknown as NodePath)
: // @ts-expect-error get doesn’t exist on the types
(attributeValuePath.get(
'expression'
) as NodePath)
const attributeNames = getAttributeNames(path)
const isBeforeCssAttribute =
attributeNames.indexOf('tw') - attributeNames.indexOf('css') < 0
if (existingCssAttribute.isArrayExpression()) {
// The existing css prop is an array, eg: css={[...]}
if (isBeforeCssAttribute) {
const attribute = existingCssAttribute as NodePath<
T.StringLiteral | T.JSXExpressionContainer
>
// @ts-expect-error never in arg0?
attribute.unshiftContainer('elements', astStyles)
} else {
const attribute = existingCssAttribute as NodePath<
T.StringLiteral | T.JSXExpressionContainer
>
// @ts-expect-error never in arg0?
attribute.pushContainer('elements', astStyles)
}
} else {
// css prop is either:
// TemplateLiteral
//
// or an ObjectExpression
//
// or ArrowFunctionExpression/FunctionExpression
//
(...)} tw="..." />
const existingCssAttributeNode = existingCssAttribute.node
// The existing css prop is an array, eg: css={[...]}
const styleArray = isBeforeCssAttribute
? [astStyles, existingCssAttributeNode]
: [existingCssAttributeNode, astStyles]
const arrayExpression = t.arrayExpression(styleArray as T.Expression[])
const { parent } = existingCssAttribute
const replacement =
parent.type === 'JSXAttribute'
? t.jsxExpressionContainer(arrayExpression)
: arrayExpression
existingCssAttribute.replaceWith(replacement)
}
}
function handleTwProperty({
path,
t,
program,
state,
coreContext,
}: JSXAttributeHandler): void {
if (!path.node || path.node.name.name !== 'tw') return
state.hasTwAttribute = true
const nodeValue = path.node.value
if (!nodeValue) return
const nodeExpression = (nodeValue as T.JSXExpressionContainer).expression
// Handle `tw={"block"}`
const expressionValue =
nodeExpression &&
nodeExpression.type === 'StringLiteral' &&
nodeExpression.value
if (expressionValue === '') return // Allow `tw={""}`
// Feedback for unsupported usage
if (nodeExpression)
coreContext.assert(
Boolean(expressionValue),
({ color }) =>
`${color(
`✕ Only plain strings can be used with the "tw" prop`
)}\n\nTry using it like this: ${color(
``,
'success'
)} or ${color(
``,
'success'
)}\n\nRead more at https://twinredirect.page.link/template-literals`
)
const rawClasses =
expressionValue || (nodeValue as T.StringLiteral).value || ''
const { styles, unmatched } = getStyles(rawClasses, coreContext)
if (unmatched.length > 0) {
getSuggestions(unmatched, {
CustomError: coreContext.CustomError,
tailwindContext: coreContext.tailwindContext,
tailwindConfig: coreContext.tailwindConfig,
hasLogColors: coreContext.twinConfig.hasLogColors,
})
return
}
const astStyles = astify(isEmpty(styles) ? {} : styles, t)
const jsxPath = getParentJSX(path)
const attributes = jsxPath.get('attributes')
const { attribute: cssAttribute } = getCssAttributeData(attributes)
if (coreContext.twinConfig.moveTwPropToStyled) {
moveTwPropToStyled({ astStyles, jsxPath, t, program, state, coreContext })
addDataTwPropToPath({ t, attributes, rawClasses, path, state, coreContext })
return
}
if (!cssAttribute) {
// Replace the tw prop with the css prop
path.replaceWith(
t.jsxAttribute(
t.jsxIdentifier('css'),
t.jsxExpressionContainer(astStyles)
)
)
addDataTwPropToPath({ t, attributes, rawClasses, path, state, coreContext })
return
}
// Merge tw styles into an existing css prop
mergeIntoCssAttribute({
cssAttribute: cssAttribute as NodePath,
path: jsxPath,
astStyles,
t,
})
path.remove() // remove the tw prop
addDataPropToExistingPath({
t,
attributes,
rawClasses,
path: jsxPath,
coreContext,
state,
})
}
function handleTwFunction({
references,
t,
state,
coreContext,
}: AdditionalHandlerParameters): void {
const defaultImportReferences = references.default || references.tw || []
defaultImportReferences.forEach(path => {
/**
* Gotcha: After twin changes a className/tw/cs prop path then the reference
* becomes stale and needs to be refreshed with crawl()
*/
const { parentPath } = path
if (!(parentPath as NodePath).isTaggedTemplateExpression())
path.scope.crawl()
const parent = path.findParent(x =>
x.isTaggedTemplateExpression()
) as NodePath
if (!parent) return
// Check if the style attribute is being used
if (!coreContext.twinConfig.allowStyleProp) {
const jsxAttribute = parent.findParent(x =>
x.isJSXAttribute()
) as NodePath
const attributeName =
// @ts-expect-error No `get` on resulting path
jsxAttribute && (jsxAttribute.get('name').get('name').node as string)
coreContext.assert(
attributeName !== 'style',
({ color }) =>
`${color(
`✕ Tailwind styles shouldn’t be added within a \`style={...}\` prop`
)}\n\nUse the tw or css prop instead: ${color(
'',
'success'
)} or ${color(
'',
'success'
)}\n\nDisable this error by adding this in your twin config: \`{ "allowStyleProp": true }\`\nRead more at https://twinredirect.page.link/style-prop`
)
}
const parsed = parseTte(parent, { t, state })
if (!parsed) return
const rawClasses = parsed.string
// Add tw-prop for css attributes
const jsxPath = path.findParent(p =>
p.isJSXOpeningElement()
) as NodePath
if (jsxPath) {
const attributes = jsxPath.get('attributes')
const pathData = {
t,
attributes,
rawClasses,
path: jsxPath,
coreContext,
state,
}
addDataPropToExistingPath(pathData)
}
const { styles, unmatched } = getStyles(rawClasses, coreContext)
if (unmatched.length > 0) {
getSuggestions(unmatched, {
CustomError: coreContext.CustomError,
tailwindContext: coreContext.tailwindContext,
tailwindConfig: coreContext.tailwindConfig,
hasLogColors: coreContext.twinConfig.hasLogColors,
})
return
}
const astStyles = astify(isEmpty(styles) ? {} : styles, t)
replaceWithLocation(parsed.path, astStyles)
})
}
export { handleTwProperty, handleTwFunction }
================================================
FILE: src/macro/twin.ts
================================================
// eslint-disable-next-line import/no-relative-parent-imports
import { createCoreContext } from '../core'
import { MacroError } from 'babel-plugin-macros'
import {
setStyledIdentifier,
setCssIdentifier,
generateUid,
getCssAttributeData,
getJsxAttributes,
} from './lib/astHelpers'
import validateImports from './lib/validateImports'
import {
updateCssReferences,
addCssImport,
convertHtmlElementToStyled,
} from './css'
import {
updateStyledReferences,
addStyledImport,
handleStyledFunction,
} from './styled'
import { handleThemeFunction } from './theme'
import { handleScreenFunction } from './screen'
import { handleGlobalStylesFunction } from './globalStyles'
import { handleTwProperty, handleTwFunction } from './tw'
import { handleCsProperty } from './shortCss'
import { handleClassNameProperty } from './className'
import type { MacroParams } from 'babel-plugin-macros'
import type { State } from './types'
const macroTasks = [
handleTwFunction,
handleGlobalStylesFunction, // GlobalStyles import
updateStyledReferences, // Styled import
handleStyledFunction, // Convert tw.div`` & styled.div`` to styled('div', {}) (stitches)
updateCssReferences, // Update any usage of existing css imports
handleThemeFunction, // Theme import
handleScreenFunction, // Screen import
addStyledImport,
addCssImport, // Gotcha: Must be after addStyledImport or issues with theme`` style transpile
]
function twinMacro(params: MacroParams): void {
const t = params.babel.types
const program = params.state.file.path
const isDev =
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'dev' ||
false
const coreContext = createCoreContext({
isDev,
config: params.config,
filename: params.state.filename ?? '',
sourceRoot: params.state.file.opts.sourceRoot ?? '',
CustomError: MacroError as typeof Error,
})
validateImports(params.references, coreContext)
const state: State = {
isDev,
babel: params.babel,
config: params.config,
tailwindConfigIdentifier: generateUid('tailwindConfig', program),
tailwindUtilsIdentifier: generateUid('tailwindUtils', program),
styledIdentifier: undefined,
cssIdentifier: undefined,
hasCssAttribute: false,
}
const handlerParameters = { t, program, state, coreContext }
program.traverse({
ImportDeclaration(path) {
setStyledIdentifier({ ...handlerParameters, path })
setCssIdentifier({ ...handlerParameters, path })
},
JSXElement(path) {
const jsxAttributes = getJsxAttributes(path)
const { index, hasCssAttribute } = getCssAttributeData(jsxAttributes)
state.hasCssAttribute = state.hasCssAttribute || hasCssAttribute
const attributePaths = index > 1 ? jsxAttributes.reverse() : jsxAttributes
for (const path of attributePaths) {
handleClassNameProperty({ ...handlerParameters, path })
handleTwProperty({ ...handlerParameters, path })
handleCsProperty({ ...handlerParameters, path })
}
if (hasCssAttribute)
convertHtmlElementToStyled({ ...handlerParameters, path })
},
})
if (state.styledIdentifier === undefined)
state.styledIdentifier = generateUid('styled', program)
if (state.cssIdentifier === undefined)
state.cssIdentifier = generateUid('css', program)
for (const task of macroTasks) {
// @ts-expect-error TOFIX: Adjust types for altered state
task({ ...handlerParameters, references: params.references })
}
program.scope.crawl()
}
export default twinMacro
================================================
FILE: src/macro/types/index.ts
================================================
import type { NodePath, types as T } from '@babel/core'
import type { MacroParams } from 'babel-plugin-macros'
import type { CoreContext, CssObject } from '../../core/types'
import type { Config as TailwindConfig } from 'tailwindcss'
type Identifiers = {
styledIdentifier?: T.Identifier
cssIdentifier?: T.Identifier
}
type StateBase = {
babel: MacroParams['babel']
config: MacroParams['config']
existingCssIdentifier?: boolean
existingStyledIdentifier?: boolean
hasCssAttribute: boolean
hasTwAttribute?: boolean
isDev: boolean
isImportingStyled?: boolean
isImportingCss?: boolean
tailwindConfigIdentifier: T.Identifier
tailwindUtilsIdentifier: T.Identifier
}
export type State = StateBase & Identifiers
export type HandlerParameters = {
t: typeof T
state: State
program: NodePath
coreContext: CoreContext
}
export type AddDataPropToExistingPath = {
path: NodePath
attributes: Array>
rawClasses: string
propName?: string
} & Pick
export type JSXAttributeHandler = HandlerParameters & {
path: NodePath
}
export type ImportDeclarationHandler = HandlerParameters & {
path: NodePath
}
export type AdditionalHandlerParameters = {
t: typeof T
references: MacroParams['references']
state: StateBase & {
styledIdentifier: T.Identifier
cssIdentifier: T.Identifier
}
program: NodePath
coreContext: CoreContext
}
export type { NodePath, CoreContext, T, MacroParams, CssObject, TailwindConfig }
================================================
FILE: src/macro.ts
================================================
import { createMacro } from 'babel-plugin-macros'
import twinMacro from './macro/twin'
export default createMacro(twinMacro, { configName: 'twin' })
================================================
FILE: src/suggestions/index.ts
================================================
import { MacroError } from 'babel-plugin-macros'
import { validators } from './lib/validators'
import { getClassSuggestions } from './lib/getClassSuggestions'
import { makeColor } from './lib/makeColor'
import {
extractClassCandidates,
extractVariantCandidates,
} from './lib/extractors'
import { getPackageVersions } from './lib/getPackageVersions'
import type {
ClassErrorContext,
MakeColor,
Options,
TailwindContext,
TailwindConfig,
} from './types'
// eslint-disable-next-line import/no-relative-parent-imports
import { createCoreContext, getStyles, splitAtTopLevelOnly } from '../core'
const ALL_SPACE_IDS = /{{SPACE}}/g
const OPTION_DEFAULTS = {
CustomError: Error,
tailwindContext: undefined,
tailwindConfig: undefined,
hasLogColors: true,
suggestionNumber: 5,
}
function getVariantSuggestions(
variants: string[],
className: string,
context: ClassErrorContext
): string | undefined {
const coreContext = createCoreContext({
tailwindConfig: context?.tailwindConfig,
CustomError: MacroError as typeof Error,
})
const { unmatched } = getStyles(className, coreContext)
if (unmatched.length > 0) return
const unmatchedVariants = variants.filter(v => {
if (v.startsWith('[')) return v
return !context.variants.has(v)
})
if (unmatchedVariants.length === 0) return
const problemVariant = unmatchedVariants[0]
return [
`${context.color(
`✕ Variant ${context.color(problemVariant, 'errorLight')} ${
problemVariant.startsWith('[') ? 'can’t be used' : 'was not found'
}`
)}`,
].join('\n\n')
}
function getClassError(rawClass: string, context: ClassErrorContext): string {
const input = rawClass.replace(ALL_SPACE_IDS, ' ')
const classPieces = [
...splitAtTopLevelOnly(input, context.tailwindConfig.separator ?? ':'),
]
for (const validator of validators) {
const error = validator(classPieces, context)
if (error) return error
}
const className = classPieces.slice(-1).join('')
const variants = classPieces.slice(0, -1)
// Check if variants or classes with match issues
if (variants.length > 0) {
const variantSuggestions = getVariantSuggestions(
variants,
className,
context
)
if (variantSuggestions) return variantSuggestions
}
return getClassSuggestions(className, context)
}
export type ErrorContext = {
CustomError: typeof Error
tailwindContext: TailwindContext
tailwindConfig: TailwindConfig
hasLogColors: boolean
suggestionNumber: number
}
function createErrorContext(
color: MakeColor,
context: ErrorContext
): ClassErrorContext {
return {
color,
candidates: extractClassCandidates(context.tailwindContext),
variants: extractVariantCandidates(context.tailwindContext),
suggestionNumber: context.suggestionNumber,
CustomError: context.CustomError,
tailwindConfig: context.tailwindConfig,
tailwindContext: context.tailwindContext,
}
}
function getSuggestions(classList: string[], options: Options): void {
const context = { ...OPTION_DEFAULTS, ...options }
const color = makeColor(context.hasLogColors)
const classErrorContext = createErrorContext(color, context)
const errorText = classList
.map(c => getClassError(c, classErrorContext))
.join('\n\n')
const { twinVersion } = getPackageVersions()
const helpText = [
`${twinVersion ? `twin.macro@${twinVersion}` : 'twinVersion'}`,
`https://twinredirect.page.link/docs`,
`https://tailwindcss.com/docs`,
].join('\n')
throw new context.CustomError(
`\n\n${errorText}\n\n${color(helpText, 'subdued')}\n`
)
}
export default getSuggestions
================================================
FILE: src/suggestions/lib/colors.ts
================================================
import chalk from 'chalk'
const colors = {
error: chalk.hex('#ff8383'),
errorLight: chalk.hex('#ffd3d3'),
warn: chalk.yellowBright,
success: chalk.greenBright,
highlight: chalk.yellowBright,
subdued: chalk.hex('#999'),
}
export default colors
================================================
FILE: src/suggestions/lib/extractors.ts
================================================
import type { TailwindContext, TailwindMatch } from 'suggestions/types'
export function extractClassCandidates(
tailwindContext: TailwindContext
): Set<[string, TailwindMatch[]]> {
const candidates = new Set<[string, TailwindMatch[]]>()
for (const candidate of tailwindContext.candidateRuleMap) {
if (String(candidate[0]) !== '*') candidates.add(candidate)
}
return candidates
}
export function extractVariantCandidates(
tailwindContext: TailwindContext
): Set {
const candidates = new Set()
for (const candidate of tailwindContext.variantMap) {
if (candidate[0]) candidates.add(candidate[0])
}
return candidates
}
================================================
FILE: src/suggestions/lib/getClassSuggestions.ts
================================================
import stringSimilarity from 'string-similarity'
import type { ClassErrorContext } from 'suggestions/types'
const RATING_MINIMUM = 0.2
type RateCandidate = [number, string, string]
function rateCandidate(
classData: [string, string],
className: string,
matchee: string
): RateCandidate | undefined {
const [classEnd, value] = classData
const candidate = `${[className, classEnd === 'DEFAULT' ? '' : classEnd]
.filter(Boolean)
.join('-')}`
const rating = Number(stringSimilarity.compareTwoStrings(matchee, candidate))
if (rating < RATING_MINIMUM) return
const classValue = `${String(
(typeof value === 'string' && (value.length === 0 ? `''` : value)) ??
(Array.isArray(value) && value.join(', ')) ??
value
)}${classEnd === 'DEFAULT' ? ' (DEFAULT)' : ''}`
return [rating, candidate, classValue]
}
function extractCandidates(
candidates: ClassErrorContext['candidates'],
matchee: string
): RateCandidate[] {
const results = [] as RateCandidate[]
for (const [className, classOptionSet] of candidates) {
for (const classOption of classOptionSet) {
const { options } = classOption[0]
if (options?.values) {
// Dynamic classes like mt-xxx, bg-xxx
for (const value of Object.entries(options?.values)) {
const rated = rateCandidate(value, className, matchee)
// eslint-disable-next-line max-depth
if (rated) results.push(rated)
}
} else {
// Non-dynamic classes like fixed, block
const rated = rateCandidate(['', className], className, matchee)
if (rated) results.push(rated)
}
}
}
return results
}
export function getClassSuggestions(
matchee: string,
context: ClassErrorContext
): string {
const { color } = context
const candidates = extractCandidates(context.candidates, matchee)
const errorText = `${context.color(
`✕ ${context.color(matchee, 'errorLight')} was not found`,
'error'
)}`
if (candidates.length === 0) return errorText
candidates.sort(
([a]: [number, string, string], [b]: [number, string, string]) => b - a
)
const [firstSuggestion, secondSuggestion = []] = candidates
const [firstRating, firstCandidate, firstClassValue] = firstSuggestion
const [secondRating] = secondSuggestion as RateCandidate
const hasWinningSuggestion =
((secondSuggestion as RateCandidate).length > 0 &&
firstRating - secondRating > 0.12) ??
false
if (candidates.length === 1 || hasWinningSuggestion) {
const valueText =
firstClassValue === firstCandidate ? '' : ` (${firstClassValue})`
return [
errorText,
`Did you mean ${color(firstCandidate, 'success')} ?${valueText}`,
].join('\n\n')
}
const suggestions = candidates
.slice(0, context.suggestionNumber)
.map(
([, suggestion, value]: [number, string, string]): string =>
`${color('-', 'subdued')} ${color(suggestion, 'highlight')} ${
value === 'false' ? '' : `${color('>', 'subdued')} ${value}`
}`
)
return [errorText, 'Try one of these classes:', suggestions.join('\n')].join(
'\n\n'
)
}
================================================
FILE: src/suggestions/lib/getPackageVersions.ts
================================================
export type JSONPrimitive = string | number | boolean | undefined
export type JSONValue = JSONPrimitive | JSONObject
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface JSONObject extends Record {}
export function getPackageVersions(): Record {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, unicorn/prefer-module
const packageJson = require('./package.json') as JSONObject
const versions = { twinVersion: packageJson.version as string }
return versions
}
================================================
FILE: src/suggestions/lib/makeColor.ts
================================================
import colors from './colors'
import type { MakeColor } from 'suggestions/types'
export function makeColor(hasColor: boolean): MakeColor {
return (message: string, type: keyof typeof colors = 'error') => {
if (!hasColor) return message
return colors[type](message)
}
}
================================================
FILE: src/suggestions/lib/validateVariants.ts
================================================
import stringSimilarity from 'string-similarity'
import type { ClassErrorContext } from 'suggestions/types'
export function validateVariants(
variantMatch: string,
context: ClassErrorContext
): string | undefined {
if (!variantMatch) return
if (variantMatch.startsWith('[')) return
const variantCandidates = [...context.variants]
// Exact variant match
if (variantCandidates.includes(variantMatch)) return
const results = variantCandidates
.map((variant: string): [string, number] | undefined => {
const rating = variantMatch
? Number(stringSimilarity.compareTwoStrings(variant, variantMatch))
: 0
if (rating < 0.2) return
return [variant, rating]
})
.filter(Boolean) as Array<[string, number]>
const errorText = `${context.color(
`✕ Variant ${context.color(`${variantMatch}`, 'errorLight')} was not found`,
'error'
)}`
if (results.length === 0) return errorText
const suggestions = results
.sort(([, a]: [string, number], [, b]: [string, number]) => b - a)
.slice(0, 4)
.map(
([i]: [string, number]): string =>
`${i}${context.tailwindConfig.separator ?? ':'}`
)
const showMore = results.length > 2 && results[0][1] - results[1][1] < 0.1
const suggestionText =
suggestions.length > 0
? [
`Did you mean ${context.color(
suggestions.slice(0, 1).join(''),
'success'
)} ?`,
showMore &&
`More variants\n${suggestions
.slice(1)
.map(v => `${context.color('-', 'subdued')} ${v}`)
.join('\n')}`,
]
.filter(Boolean)
.join('\n\n')
: ''
return [errorText, suggestionText].join('\n\n')
}
================================================
FILE: src/suggestions/lib/validators.ts
================================================
import { validateVariants } from './validateVariants'
import type { ClassErrorContext } from 'suggestions/types'
const validators = [
// Validate the group class
(pieces: string[], context: ClassErrorContext): undefined | string => {
const className = pieces.slice(-1).join('')
if (/^!?group\/\S/.test(className)) {
return `${context.color(
`✕ ${context.color(
className,
'errorLight'
)} must be added as a className:`,
'error'
)}\n\n
\n \n
`
}
if (!pieces.includes('group')) return
return `${context.color(
`✕ ${context.color('group', 'errorLight')} must be added as a className:`,
'error'
)}\n\n
\n \n
\n\nRead more at https://twinredirect.page.link/group`
},
// Validate the peer class
(pieces: string[], context: ClassErrorContext): undefined | string => {
const className = pieces.slice(-1).join('')
if (/^!?peer\/\S/.test(className)) {
return `${context.color(
`✕ ${context.color(
className,
'errorLight'
)} must be added as a className:`,
'error'
)}\n\n
\n \n
`
}
if (!pieces.includes('peer')) return
return `${context.color(
`✕ ${context.color('peer', 'errorLight')} must be added as a className:`,
'error'
)}\n\n
\n\n\nRead more at https://twinredirect.page.link/peer`
},
// Validate the opacity
(pieces: string[], context: ClassErrorContext): undefined | string => {
const className = pieces.slice(-1).join('')
const opacityMatch = /\/(\w+)$/.exec(className)
if (!opacityMatch) return
const opacityConfig = context.tailwindConfig.theme?.opacity ?? {}
if (opacityConfig[opacityMatch[1] as keyof typeof opacityConfig]) return
const choices = Object.entries(opacityConfig)
.map(
([k, v]: [string, string]): string =>
`${context.color('-', 'subdued')} ${context.color(
k,
'success'
)} ${context.color('>', 'subdued')} ${v}`
)
.join('\n')
return `${context.color(
`✕ ${context.color(
className,
'errorLight'
)} doesn’t have an opacity from your config`,
'error'
)}\n\nTry one of these opacity values:\n\n${choices}`
},
// Validate the lead class (from the official typography plugin)
(pieces: string[], context: ClassErrorContext): undefined | string => {
if (!pieces.includes('lead')) return
return `${context.color(
`✕ ${context.color('lead', 'errorLight')} must be added as a className:`,
'error'
)}\n\n
...
`
},
// Validate the not-prose class (from the official typography plugin)
(pieces: string[], context: ClassErrorContext): undefined | string => {
if (!pieces.includes('not-prose')) return
return `${context.color(
`✕ ${context.color(
'not-prose',
'errorLight'
)} must be added as a className:`,
'error'
)}\n\n