diff --git a/.changeset/good-houses-end.md b/.changeset/good-houses-end.md
new file mode 100644
index 00000000000..a5a86c30113
--- /dev/null
+++ b/.changeset/good-houses-end.md
@@ -0,0 +1,5 @@
+---
+'@toptal/picasso-codemod': minor
+---
+
+- add `spacing-values` codemod. Please run the codemod (`npx @toptal/picasso-codemod@latest v38.1.0`) to replace spacing property values of `Container` and `Dropdown` components with BASE-aligned property values according to the https://github.com/toptal/picasso/blob/master/docs/decisions/18-spacings.md. Property values that do not have BASE counterpart or are complex expressions have to be updated manually (non-BASE values have to be replaced with BASE ones after consulting with Design Team), codemod outputs the list of such cases for convenience. Run linter or prettier to align updated code with project code style.
diff --git a/packages/picasso-codemod/README.md b/packages/picasso-codemod/README.md
index f3e8da7e943..1ac01dcb133 100644
--- a/packages/picasso-codemod/README.md
+++ b/packages/picasso-codemod/README.md
@@ -41,6 +41,15 @@ Codemods do not guarantee the code format preservation. Therefore be sure to run
## Included Scripts
+
+### v38.1.0
+
+Replaces spacing property values of `Container` and `Dropdown` components with BASE-aligned property values according to the https://github.com/toptal/picasso/blob/master/docs/decisions/18-spacings.md. Property values that do not have BASE counterpart or are complex expressions have to be updated manually (non-BASE values have to be replaced with BASE ones after consulting with Design Team), codemod outputs the list of such cases for convenience. Run linter or prettier to align updated code with project code style
+
+```sh
+npx @toptal/picasso-codemod v38.1.0@latest
+```
+
### v36.0.0
Replaces all imports of RichTextEditor related components to `@toptal/picasso-rich-text-editor` and updates package.json with a new version of `@toptal/picasso`, `@toptal/picasso-rich-text-editor` and `@toptal/picasso-forms`
diff --git a/packages/picasso-codemod/package.json b/packages/picasso-codemod/package.json
index dbdb227f780..9bc265579ad 100644
--- a/packages/picasso-codemod/package.json
+++ b/packages/picasso-codemod/package.json
@@ -27,7 +27,7 @@
"react": ">=16.12.0 < 19.0.0"
},
"devDependencies": {
- "@types/jscodeshift": "^0.11.5"
+ "@types/jscodeshift": "^0.11.6"
},
"bin": {
"picasso-codemod": "./bin/picasso-codemod.mjs"
diff --git a/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.input.tsx b/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.input.tsx
new file mode 100644
index 00000000000..fa04bd5a59e
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.input.tsx
@@ -0,0 +1,35 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+import React from 'react'
+import { Container } from '@toptal/picasso'
+
+const test = 'large'
+const booleanVariable = true
+const extraProps = { align: 'left '}
+
+export default () => (
+ <>
+ Content
+ Content
+ Content
+ Content
+ Content
+ Content
+
+ Menu item 1
+
+ }
+ >
+ Dropdown
+
+ >
+)
diff --git a/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.output.tsx b/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.output.tsx
new file mode 100644
index 00000000000..1353a09544b
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/__testfixtures__/default.output.tsx
@@ -0,0 +1,37 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+import React from 'react'
+import { Container } from '@toptal/picasso'
+
+import { SPACING_4, SPACING_6, SPACING_12 } from '@toptal/picasso/utils';
+
+const test = 'large'
+const booleanVariable = true
+const extraProps = { align: 'left '}
+
+export default () => (
+ <>
+ Content
+ Content
+ Content
+ Content
+ Content
+ Content
+
+ Menu item 1
+
+ }
+ >
+ Dropdown
+
+ >
+)
diff --git a/packages/picasso-codemod/src/v38.1.0/__tests__/test.ts b/packages/picasso-codemod/src/v38.1.0/__tests__/test.ts
new file mode 100644
index 00000000000..998452930fb
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/__tests__/test.ts
@@ -0,0 +1,3 @@
+import { defineTest } from 'jscodeshift/src/testUtils'
+
+defineTest(__dirname, 'spacing-values', {}, 'default', { parser: 'tsx' })
diff --git a/packages/picasso-codemod/src/v38.1.0/config.ts b/packages/picasso-codemod/src/v38.1.0/config.ts
new file mode 100644
index 00000000000..01fbd2efd80
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/config.ts
@@ -0,0 +1,10 @@
+export const AFFECTED_COMPONENTS = ['Container', 'Dropdown']
+export const AFFECTED_ATTRIBUTES = [
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ 'padded',
+ 'gap',
+ 'offset',
+]
diff --git a/packages/picasso-codemod/src/v38.1.0/index.ts b/packages/picasso-codemod/src/v38.1.0/index.ts
new file mode 100644
index 00000000000..c30363b92f8
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/index.ts
@@ -0,0 +1 @@
+export { default } from './spacing-values'
diff --git a/packages/picasso-codemod/src/v38.1.0/spacing-values.ts b/packages/picasso-codemod/src/v38.1.0/spacing-values.ts
new file mode 100644
index 00000000000..3e0c1e87933
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/spacing-values.ts
@@ -0,0 +1,118 @@
+import type {
+ JSXAttribute,
+ JSXIdentifier,
+ JSXMemberExpression,
+ JSXNamespacedName,
+ JSXSpreadAttribute,
+ Transform,
+} from 'jscodeshift'
+
+import { AFFECTED_ATTRIBUTES, AFFECTED_COMPONENTS } from './config'
+import type { ManuallyFixableCase, TransformationOptions } from './types'
+import {
+ reportManuallyFixableCases,
+ insertSpacingImport,
+ getUpdatedNode,
+} from './utils'
+
+const isJSXAttribute = (
+ attribute: JSXSpreadAttribute | JSXAttribute
+): attribute is JSXAttribute => !!(attribute as any).name?.name
+const isJSXMemberExpression = (
+ element: JSXIdentifier | JSXNamespacedName | JSXMemberExpression
+): element is JSXMemberExpression => !(element as any).name
+
+const transform: Transform = (file, api) => {
+ const spacingsToImport: string[] = []
+ const manuallyFixableCases: ManuallyFixableCase[] = []
+
+ const j = api.jscodeshift
+ const ast = j(file.source)
+
+ ast
+ .find(j.JSXElement)
+ .filter(path => {
+ if (isJSXMemberExpression(path.node.openingElement.name)) {
+ return false
+ }
+
+ if (typeof path.node.openingElement.name.name !== 'string') {
+ return false
+ }
+
+ return AFFECTED_COMPONENTS.includes(path.node.openingElement.name.name)
+ })
+ .forEach(element => {
+ const updatedElementAttributes =
+ element.node.openingElement.attributes?.map(attribute => {
+ if (!isJSXAttribute(attribute)) {
+ return attribute
+ }
+
+ const attributeName = attribute.name.name
+ const ignoreAttribute =
+ !(typeof attributeName === 'string') ||
+ !AFFECTED_ATTRIBUTES.includes(attributeName)
+
+ if (ignoreAttribute) {
+ return attribute
+ }
+
+ // Populate "manuallyFixableCases" and "spacingsToImport" as transformations are happening
+ // deeper and deeper in attribute value node
+ const transformationOptions: TransformationOptions = {
+ api,
+ reportManuallyFixableCase: () => {
+ if (isJSXMemberExpression(element.value.openingElement.name)) {
+ return
+ }
+
+ if (typeof element.value.openingElement.name.name !== 'string') {
+ return
+ }
+
+ manuallyFixableCases.push({
+ componentName: element.value.openingElement.name.name,
+ attributeName,
+ location: `${file.path}:${element.value.loc?.start.line}`,
+ })
+ },
+ addSpacingImport: spacingIdentifier =>
+ spacingsToImport.push(spacingIdentifier),
+ }
+
+ if (attribute.value?.type === 'JSXExpressionContainer') {
+ const updatedNode = getUpdatedNode(
+ attribute.value.expression,
+ transformationOptions
+ )
+
+ attribute.value.expression = updatedNode
+ } else if (attribute.value) {
+ const updatedNode = getUpdatedNode(
+ attribute.value,
+ transformationOptions
+ )
+
+ attribute.value =
+ api.jscodeshift.jsxExpressionContainer(updatedNode)
+ }
+
+ return attribute
+ })
+
+ element.node.openingElement.attributes = updatedElementAttributes
+ })
+
+ if (spacingsToImport.length > 0) {
+ insertSpacingImport(api, ast, spacingsToImport)
+ }
+
+ if (manuallyFixableCases.length > 0) {
+ reportManuallyFixableCases(manuallyFixableCases)
+ }
+
+ return ast.toSource({ quote: 'single' })
+}
+
+export default transform
diff --git a/packages/picasso-codemod/src/v38.1.0/types.ts b/packages/picasso-codemod/src/v38.1.0/types.ts
new file mode 100644
index 00000000000..132a6f4e6f3
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/types.ts
@@ -0,0 +1,13 @@
+import type { API } from 'jscodeshift'
+
+export type TransformationOptions = {
+ api: API
+ reportManuallyFixableCase: () => void
+ addSpacingImport: (spacingIdentifier: string) => void
+}
+
+export type ManuallyFixableCase = {
+ componentName: string
+ attributeName: string
+ location: string
+}
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-number.ts b/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-number.ts
new file mode 100644
index 00000000000..dfab77b9717
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-number.ts
@@ -0,0 +1,32 @@
+import type { TransformationOptions } from '../types'
+
+export const NUMERIC_VALUE_TO_SPACING_CONSTANTS = [
+ { value: 0, name: 'SPACING_0' },
+ { value: 0.25, name: 'SPACING_1' },
+ { value: 0.5, name: 'SPACING_2' },
+ { value: 0.75, name: 'SPACING_3' },
+ { value: 1, name: 'SPACING_4' },
+ { value: 1.5, name: 'SPACING_6' },
+ { value: 2, name: 'SPACING_8' },
+ { value: 2.5, name: 'SPACING_10' },
+ { value: 3, name: 'SPACING_12' },
+]
+
+export const getNodeForNumber = (
+ node: any,
+ { api, reportManuallyFixableCase, addSpacingImport }: TransformationOptions
+) => {
+ const numericValue = node.value
+ const spacing = NUMERIC_VALUE_TO_SPACING_CONSTANTS.find(
+ ({ value }) => value === numericValue
+ )
+
+ if (spacing) {
+ addSpacingImport(spacing.name)
+
+ return api.jscodeshift.identifier(spacing.name)
+ }
+ reportManuallyFixableCase()
+
+ return node
+}
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-size-string-constant.ts b/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-size-string-constant.ts
new file mode 100644
index 00000000000..9f4df31b82c
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/get-node-for-size-string-constant.ts
@@ -0,0 +1,27 @@
+import type { TransformationOptions } from '../types'
+
+export const SIZE_TO_SPACING_CONSTANT: Record = {
+ xsmall: 'SPACING_2',
+ small: 'SPACING_4',
+ medium: 'SPACING_6',
+ large: 'SPACING_8',
+ xlarge: 'SPACING_10',
+}
+
+export const getNodeForSizeStringConstant = (
+ node: any,
+ { api, addSpacingImport }: TransformationOptions
+) => {
+ const spacingConstant = node.value
+ const spacingName = SIZE_TO_SPACING_CONSTANT[spacingConstant]
+
+ if (!spacingName) {
+ throw new Error(
+ `Unable to match "${spacingConstant}" size string constant to BASE spacing`
+ )
+ }
+
+ addSpacingImport(spacingName)
+
+ return api.jscodeshift.identifier(spacingName)
+}
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/get-updated-node.ts b/packages/picasso-codemod/src/v38.1.0/utils/get-updated-node.ts
new file mode 100644
index 00000000000..a45ce638f0c
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/get-updated-node.ts
@@ -0,0 +1,47 @@
+import type {
+ ConditionalExpression,
+ JSXAttribute,
+ JSXExpressionContainer,
+} from 'jscodeshift'
+
+import type { TransformationOptions } from '../types'
+import { getNodeForNumber } from './get-node-for-number'
+import { getNodeForSizeStringConstant } from './get-node-for-size-string-constant'
+
+type NodeType =
+ | JSXExpressionContainer['expression']
+ | NonNullable
+
+export const getUpdatedNode = (
+ node: NodeType,
+ options: TransformationOptions
+) => {
+ const { reportManuallyFixableCase } = options
+
+ if (node.type === 'StringLiteral') {
+ node = getNodeForSizeStringConstant(node, options)
+ } else if (node.type === 'NumericLiteral') {
+ node = getNodeForNumber(node, options)
+ } else if (node.type === 'ObjectExpression') {
+ const updatedProperties = node.properties.map((property: any) => {
+ property.value = getUpdatedNode(property.value, options)
+
+ return property
+ })
+
+ node.properties = updatedProperties
+ } else if (node.type === 'ConditionalExpression') {
+ node.consequent = getUpdatedNode(
+ node.consequent,
+ options
+ ) as ConditionalExpression['consequent']
+ node.alternate = getUpdatedNode(
+ node.alternate,
+ options
+ ) as ConditionalExpression['alternate']
+ } else {
+ reportManuallyFixableCase()
+ }
+
+ return node
+}
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/index.ts b/packages/picasso-codemod/src/v38.1.0/utils/index.ts
new file mode 100644
index 00000000000..0a4513226ea
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/index.ts
@@ -0,0 +1,5 @@
+export { getUpdatedNode } from './get-updated-node'
+export { getNodeForNumber } from './get-node-for-number'
+export { getNodeForSizeStringConstant } from './get-node-for-size-string-constant'
+export { reportManuallyFixableCases } from './report-manually-fixable-cases'
+export { insertSpacingImport } from './insert-spacing-import'
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/insert-spacing-import.ts b/packages/picasso-codemod/src/v38.1.0/utils/insert-spacing-import.ts
new file mode 100644
index 00000000000..e6b529fe2bb
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/insert-spacing-import.ts
@@ -0,0 +1,34 @@
+import type { API, Collection } from 'jscodeshift'
+
+export const insertSpacingImport = (
+ api: API,
+ ast: Collection,
+ spacingsToImport: string[]
+) => {
+ const j = api.jscodeshift
+
+ const newImport = j.importDeclaration(
+ Array.from(new Set(spacingsToImport)).map(spacing =>
+ j.importSpecifier(j.identifier(spacing))
+ ),
+ j.stringLiteral('@toptal/picasso/utils')
+ )
+
+ const anchorDeclaration = ast
+ .find(j.ImportDeclaration)
+ .filter(
+ path =>
+ path.node.source.value === '@toptal/picasso' ||
+ path.node.source.value === '../Container' ||
+ path.node.source.value === '../Dropdown'
+ )
+ .at(0)
+
+ if (anchorDeclaration.length === 0) {
+ throw new Error(
+ `Unable to find @toptal/picasso or Container declaration to insert new spacing import`
+ )
+ }
+
+ anchorDeclaration.get().insertAfter(newImport)
+}
diff --git a/packages/picasso-codemod/src/v38.1.0/utils/report-manually-fixable-cases.ts b/packages/picasso-codemod/src/v38.1.0/utils/report-manually-fixable-cases.ts
new file mode 100644
index 00000000000..c04697ea078
--- /dev/null
+++ b/packages/picasso-codemod/src/v38.1.0/utils/report-manually-fixable-cases.ts
@@ -0,0 +1,10 @@
+import type { ManuallyFixableCase } from '../types'
+
+export const reportManuallyFixableCases = (
+ manuallyFixableCases: ManuallyFixableCase[]
+) =>
+ manuallyFixableCases.forEach(({ componentName, attributeName, location }) => {
+ process.stdout.write(
+ `\x1b[33mManual update required for ${componentName}.${attributeName} in ${location}\x1b[0m\n`
+ )
+ })
diff --git a/yarn.lock b/yarn.lock
index 45d7d96b9dc..63082fead6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6026,10 +6026,10 @@
jest-matcher-utils "^27.0.0"
pretty-format "^27.0.0"
-"@types/jscodeshift@^0.11.5":
- version "0.11.5"
- resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.11.5.tgz#51198aa72ceb66d36ceba3918e1ab445b868f29b"
- integrity sha512-7JV0qdblTeWFigevmwFUgROXX395F+MQx6v0YqPn8Bx0B4Sng6alEejz9PENzgLYpG+zL0O4tGdBzc4gKZH8XA==
+"@types/jscodeshift@^0.11.6":
+ version "0.11.6"
+ resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.11.6.tgz#9ced613c8dd92559000fb671d151685ea8e420c7"
+ integrity sha512-3lJ4DajWkk4MZ1F7q+1C7jE0z0xOtbu0VU/Kg3wdPq2DUvJjySSlu3B5Q/bICrTxugLhONBO7inRUWsymOID/A==
dependencies:
ast-types "^0.14.1"
recast "^0.20.3"
@@ -14273,15 +14273,11 @@ is-text-path@^1.0.1:
text-extensions "^1.0.0"
is-typed-array@^1.1.10, is-typed-array@^1.1.9:
- version "1.1.10"
- resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f"
- integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a"
+ integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==
dependencies:
- available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
- for-each "^0.3.3"
- gopd "^1.0.1"
- has-tostringtag "^1.0.0"
+ which-typed-array "^1.1.11"
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
@@ -15059,7 +15055,7 @@ jsbn@~0.1.0:
jscodeshift@^0.13.1:
version "0.13.1"
- resolved "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.1.tgz"
+ resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.13.1.tgz#69bfe51e54c831296380585c6d9e733512aecdef"
integrity sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==
dependencies:
"@babel/core" "^7.13.16"
@@ -24043,17 +24039,16 @@ which-pm@2.0.0:
load-yaml-file "^0.2.0"
path-exists "^4.0.0"
-which-typed-array@^1.1.9:
- version "1.1.9"
- resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"
- integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
+which-typed-array@^1.1.11, which-typed-array@^1.1.9:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
+ integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.0"
- is-typed-array "^1.1.10"
which@^1.2.9, which@^1.3.1:
version "1.3.1"