├── bin └── solid-codemod.js ├── .gitignore ├── src ├── transforms │ ├── solid@v2 │ │ ├── jsx-classlist-to-class │ │ │ ├── test │ │ │ │ ├── expected.tsx │ │ │ │ └── input.tsx │ │ │ └── index.js │ │ ├── jsx-array-map-to-for │ │ │ ├── test │ │ │ │ ├── expected.tsx │ │ │ │ └── input.tsx │ │ │ └── index.ts │ │ └── jsx-properties-to-attributes │ │ │ ├── test │ │ │ ├── input.tsx │ │ │ └── expected.tsx │ │ │ └── index.js │ └── shared.js ├── data │ └── solid@v2 │ │ ├── solid-jsx-types-to-json.js │ │ └── solid-markup.js ├── bin │ ├── run-transformer.js │ └── solid-codemod.js └── utils.js ├── tsconfig.json ├── release.mjs ├── LICENSE ├── package.json ├── .github └── ISSUE_TEMPLATE │ └── feature_request.yml └── README.md /bin/solid-codemod.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../src/bin/solid-codemod.js' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | output.js 4 | output.jsx 5 | output.ts 6 | output.tsx 7 | data/test-data.json -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-classlist-to-class/test/expected.tsx: -------------------------------------------------------------------------------- 1 | const test = [ 2 |
, 3 |
, 7 |
, 11 | ] 12 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-classlist-to-class/test/input.tsx: -------------------------------------------------------------------------------- 1 | const test = [ 2 |
, 3 |
, 7 |
, 11 | ] 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "strict": true, 6 | 7 | "target": "ESNext", 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "jsx": "preserve", 13 | "jsxImportSource": "solid-js", 14 | "types": ["solid-js", "node", "@types/jscodeshift"], 15 | "noEmit": true, 16 | "isolatedModules": true, 17 | 18 | "resolveJsonModule": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-array-map-to-for/test/expected.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For } from 'solid-js' 2 | 3 | const test = [ 4 |
5 | {el1 =>
{el1.name}
}
6 |
, 7 |
8 | {el2 =>
{el2.name}
}
9 | {el3 =>
{el4.name}
}
10 |
, 11 | ] 12 | 13 | export const Looper = () => { 14 | return ( 15 |
16 | {el =>
{el.name}
}
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-array-map-to-for/test/input.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js' 2 | 3 | const test = [ 4 |
5 | {array1.map(el1 => ( 6 |
{el1.name}
7 | ))} 8 |
, 9 |
10 | {array2.map(el2 => ( 11 |
{el2.name}
12 | ))} 13 | {array3.map(el3 => ( 14 |
{el4.name}
15 | ))} 16 |
, 17 | ] 18 | 19 | export const Looper = () => { 20 | return ( 21 |
22 | {array.map(el => ( 23 |
{el.name}
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /release.mjs: -------------------------------------------------------------------------------- 1 | import { execSync as $ } from 'child_process' 2 | 3 | // bump version number 4 | $('npm version minor --git-tag-version false') 5 | 6 | // read version number 7 | import('./package.json', { 8 | with: { type: 'json' }, 9 | }).then(json => { 10 | const version = json.default.version 11 | 12 | // git add, commit with version number 13 | $('git add --all') 14 | $('git commit -m "v' + version + '"') 15 | $('git tag "v' + version + '" -m "v' + version + '"') 16 | 17 | // git push / npm publish 18 | $('git push --all') 19 | $('git push origin --tags') 20 | $('npm publish') 21 | }) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 David Di Biase, Tito Bouzout, SolidJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-codemod", 3 | "version": "2.4.0", 4 | "description": "Codemod scripts for SolidJS JavaScript library", 5 | "homepage": "https://github.com/solidjs-community/solid-codemod", 6 | "license": "MIT", 7 | "type": "module", 8 | "bin": { 9 | "solid-codemod": "bin/solid-codemod.js" 10 | }, 11 | "scripts": { 12 | "test": "solid-codemod", 13 | "release": "node ./release.mjs" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/solidjs-community/solid-codemod.git" 18 | }, 19 | "dependencies": { 20 | "@types/jscodeshift": "^17.3.0", 21 | "@types/node": "^22.15.21", 22 | "chalk": "^5.4.1", 23 | "cli-diff": "^1.0.0", 24 | "jscodeshift": "^17.3.0", 25 | "prettier": "^3.5.3", 26 | "solid-js": "^2.0.0-experimental.5", 27 | "typescript": "^5.8.3" 28 | }, 29 | "prettier": { 30 | "printWidth": 70, 31 | "useTabs": true, 32 | "semi": false, 33 | "singleQuote": true, 34 | "quoteProps": "as-needed", 35 | "jsxSingleQuote": false, 36 | "trailingComma": "all", 37 | "bracketSpacing": true, 38 | "bracketSameLine": false, 39 | "arrowParens": "avoid", 40 | "proseWrap": "never", 41 | "endOfLine": "lf", 42 | "singleAttributePerLine": true, 43 | "embeddedLanguageFormatting": "off" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/data/solid@v2/solid-jsx-types-to-json.js: -------------------------------------------------------------------------------- 1 | import { read, write } from '../../utils.js' 2 | 3 | const test = false 4 | 5 | // data 6 | 7 | const destination = './jsx.json' 8 | 9 | const o = JSON.parse( 10 | !test 11 | ? await fetch( 12 | 'https://raw.githubusercontent.com/potahtml/namespace-jsx-project/refs/heads/master/jsx/data.json', 13 | ).then(x => x.text()) 14 | : read('./test-data.json'), 15 | ) 16 | 17 | const solidV2Key = 'Solid Next' 18 | 19 | const result = { 20 | tags: {}, 21 | attributes: { 22 | global: {}, 23 | custom: { 24 | ref: true, 25 | children: true, 26 | }, 27 | }, 28 | } 29 | 30 | // tags data 31 | 32 | for (const ns in o.elements) { 33 | for (const tag in o.elements[ns]) { 34 | result.tags[tag] = result.tags[tag] || {} 35 | for (const [key, entry] of Object.entries( 36 | o.elements[ns][tag].keys, 37 | )) { 38 | const value = entry.values[solidV2Key] 39 | if (value) { 40 | result.tags[tag][key] = value 41 | } 42 | } 43 | } 44 | } 45 | 46 | // globals data 47 | 48 | for (const interfaceName of [ 49 | 'Element', 50 | 'HTMLElement', 51 | 'MathMLElement', 52 | 'SVGElement', 53 | ]) { 54 | for (const [key, entry] of Object.entries( 55 | o.keys[interfaceName].keys, 56 | )) { 57 | const value = entry.values[solidV2Key] 58 | if (value) { 59 | result.attributes.global[key] = value 60 | } 61 | } 62 | } 63 | 64 | // save 65 | 66 | write(destination, JSON.stringify(result, null, 2)) 67 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-classlist-to-class/index.js: -------------------------------------------------------------------------------- 1 | import { SolidMarkupV2 } from '../../../data/solid@v2/solid-markup.js' 2 | 3 | import { 4 | log, 5 | getAttributeNameAndValueFromJSXAttribute, 6 | getTagNameFromJSXElement, 7 | } from '../../shared.js' 8 | 9 | /** 10 | * This transform provides the following when reliable possible: 11 | * 12 | * - Trasnforms `classList` to `class` 13 | * 14 | * @param {import('jscodeshift').FileInfo} file 15 | * @param {import('jscodeshift').API} api 16 | */ 17 | export default function transformer(file, api) { 18 | const j = api.jscodeshift 19 | const root = j(file.source) 20 | 21 | root.find(j.JSXElement).forEach(path => { 22 | const tagName = getTagNameFromJSXElement(api, path) 23 | 24 | if (!SolidMarkupV2.isKnownTag(tagName)) { 25 | // skip JSX Components and unknown tags 26 | return 27 | } 28 | 29 | const attributes = path.node.openingElement.attributes 30 | 31 | attributes.slice().forEach(attr => { 32 | if (attr.type === 'JSXAttribute') { 33 | // get name and value 34 | let [attributeName, attributeValue] = 35 | getAttributeNameAndValueFromJSXAttribute(api, attr) 36 | 37 | if (attributeName === 'classList') { 38 | const newName = 'class' 39 | 40 | log( 41 | file, 42 | `renamed attribute ´${attributeName}´ to ´${newName}´ on tag ´${tagName}´`, 43 | ) 44 | attributeName = newName 45 | attr.name = j.jsxIdentifier(newName) 46 | } 47 | } 48 | }) 49 | }) 50 | 51 | return root.toSource() 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/run-transformer.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | import { spawn } from 'node:child_process' 3 | 4 | const require = createRequire(import.meta.url) 5 | const jscodeshiftExecutable = require.resolve('.bin/jscodeshift') 6 | 7 | /** 8 | * To make `process.(stdout/stdin).write` display colors on `wsl` and 9 | * possibly other places. Although I am not sure if this is my 10 | * personal config that needs to be set to display colors, instead of 11 | * forcing the colors. 12 | */ 13 | process.env.FORCE_COLOR = '1' 14 | 15 | export async function runTransformer(transformer, files, write) { 16 | const parser = /(tsx|ts)$/.test(files[0]) ? 'tsx' : 'babel' 17 | 18 | const args = [ 19 | jscodeshiftExecutable, 20 | '--verbose=0', 21 | '--ignore-pattern=**/node_modules/**', 22 | '--parser', 23 | parser, 24 | '--transform', 25 | transformer, 26 | '--stdin', 27 | ] 28 | 29 | if (!write) { 30 | args.push('--dry') 31 | } 32 | 33 | parser === 'tsx' 34 | ? args.push('--extensions=tsx,ts,jsx,js') 35 | : args.push('--extensions=jsx,js') 36 | 37 | const child = spawn('node', args) 38 | 39 | child.stdout.on('data', chunk => { 40 | if (chunk.toString().includes('ERR')) { 41 | process.stdout.write(chunk) 42 | process.exit(1) 43 | } 44 | queueMicrotask(() => process.stdout.write(chunk)) 45 | }) 46 | child.stderr.on('data', chunk => 47 | queueMicrotask(() => process.stderr.write(chunk)), 48 | ) 49 | 50 | child.stdin.write(files.join('\n')) 51 | child.stdin.end() 52 | 53 | return new Promise((resolve, reject) => { 54 | child.on('exit', resolve) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: These issues are for **concrete and actionable proposals**. If you just have 3 | a general problem that you would like to brainstorm, open a Discussion instead. 4 | title: "[Feature]: " 5 | labels: ["enhancement"] 6 | 7 | body: 8 | - type: textarea 9 | attributes: 10 | label: Input code 11 | description: | 12 | Provide a minimal code for which you need transformation to be supported. 13 | placeholder: | 14 | ```ts 15 | import React, { useState, useEffect } from "react"; 16 | import { render } from "react-dom"; 17 | 18 | const CountingComponent = () => { 19 | const [count, setCount] = useState(0); 20 | useEffect(() => { 21 | const interval = setInterval(() => setCount(count + 1), 1000); 22 | return () => { 23 | clearInterval(interval); 24 | }; 25 | }, []); 26 | return
Count value is {count}
; 27 | } 28 | 29 | render(() => , document.getElementById("app")); 30 | ``` 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: Expected Output 37 | description: The expected transformed output for the provided code snippet. 38 | placeholder: | 39 | ```ts 40 | import { createSignal, onCleanup, onMount } from "solid-js"; 41 | import { render } from "solid-js/web"; 42 | 43 | const CountingComponent = () => { 44 | const [count, setCount] = createSignal(0); 45 | onMount(() => { 46 | const interval = setInterval(() => setCount(count + 1), 1000); 47 | onCleanup(() => clearInterval(interval)); 48 | }); 49 | return
Count value is {count()}
; 50 | }; 51 | 52 | render(() => , document.getElementById("app")); 53 | ``` 54 | 55 | - type: textarea 56 | attributes: 57 | label: Additional context 58 | description: | 59 | Add any other context about the problem here. 60 | placeholder: | 61 | Example transform to convert useState/useEffect of ReactJS to createSignal/onCleanup of SolidJS. 62 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { execSync as $ } from 'child_process' 4 | import { fileURLToPath } from 'url' 5 | 6 | import chalk from 'chalk' 7 | import diff from 'cli-diff' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | export const root = path.dirname(path.dirname(__filename)) 11 | 12 | export const log = string => console.log('\n' + string) 13 | export const red = string => log(chalk.redBright(string)) 14 | export const blueBright = string => log(chalk.cyanBright(string)) 15 | export const blue = string => log(chalk.cyan(string)) 16 | export const green = string => log(chalk.greenBright(string)) 17 | 18 | export const read = path => 19 | fs.readFileSync(path, { encoding: 'utf8' }) 20 | 21 | export const write = (path, content) => 22 | fs.writeFileSync(path, content) 23 | 24 | export const copy = (source, destination) => 25 | fs.copyFileSync(source, destination) 26 | 27 | export const exists = path => fs.existsSync(path) 28 | 29 | export const remove = path => { 30 | try { 31 | fs.rmSync(path) 32 | } catch (e) {} 33 | } 34 | 35 | /** @returns {string[]} */ 36 | export const getFiles = (...args) => 37 | /** @type {string[]} */ 38 | [ 39 | ...new Set( 40 | args 41 | .flat(Infinity) 42 | .map(a => getFilesRecurse(a)) 43 | .flat(Infinity), 44 | ), 45 | ].filter(x => /\.(tsx|ts|jsx|js)$/.test(x)) 46 | 47 | /** @returns {string[]} */ 48 | const getFilesRecurse = (source, files = []) => { 49 | if (!fs.existsSync(source)) { 50 | throw new Error(`path "${source}" doesn't exists`) 51 | } 52 | 53 | if (/(\.git|node_modules)/.test(source)) { 54 | } else if (fs.statSync(source).isDirectory()) { 55 | for (const file of fs.readdirSync(source)) { 56 | getFilesRecurse(path.join(source, file), files) 57 | } 58 | } else { 59 | files.push(source) 60 | } 61 | return files 62 | } 63 | 64 | /** @returns {string[]} */ 65 | export const getDirectories = source => 66 | /** @type {string[]} */ fs 67 | .readdirSync(source, { withFileTypes: true }) 68 | .filter(dirent => dirent.isDirectory()) 69 | .map(dirent => dirent.name) 70 | 71 | export const prettier = file => 72 | $( 73 | `prettier "${file}" --write --config "${root}/package.json" --no-editorconfig --ignore-path="false"`, 74 | ) 75 | 76 | export const diffFiles = (a, b) => (diff.default || diff)(a, b) 77 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-array-map-to-for/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ASTPath, 3 | CallExpression, 4 | FunctionExpression, 5 | ImportDeclaration, 6 | MemberExpression, 7 | FileInfo, 8 | API, 9 | } from 'jscodeshift' 10 | 11 | import { 12 | log, 13 | getAttributeNameAndValueFromJSXAttribute, 14 | getTagNameFromJSXElement, 15 | } from '../../shared.js' 16 | 17 | /** 18 | * This transform provides the following when reliable possible: 19 | * 20 | * - Trasnforms `array.map` to `() 27 | 28 | // Replace JSX expression {.map(fn)} with {fn} 29 | root 30 | .find(j.JSXExpressionContainer) 31 | .filter(path => { 32 | // We only care about JSX expression containers whose parents are JSX elements. 33 | // (in other words, exclude attribute expressions.) 34 | if ( 35 | path.parent.node.type === 'JSXElement' && 36 | path.node.expression.type === 'CallExpression' 37 | ) { 38 | const call = path.node.expression 39 | if ( 40 | call.callee.type === 'MemberExpression' && 41 | call.callee.property.type === 'Identifier' && 42 | call.callee.property.name === 'map' 43 | ) { 44 | return true 45 | } 46 | } 47 | return false 48 | }) 49 | .replaceWith(path => { 50 | const call = path.node.expression as CallExpression 51 | const callee = call.callee as MemberExpression 52 | importsRequired.add('For') 53 | return j.jsxElement.from({ 54 | openingElement: j.jsxOpeningElement(j.jsxIdentifier('For'), [ 55 | j.jsxAttribute.from({ 56 | name: j.jsxIdentifier('each'), 57 | value: j.jsxExpressionContainer.from({ 58 | expression: callee.object, 59 | }), 60 | }), 61 | ]), 62 | closingElement: j.jsxClosingElement(j.jsxIdentifier('For')), 63 | selfClosing: false, 64 | children: [ 65 | j.jsxExpressionContainer.from({ 66 | expression: call.arguments.at(0) as FunctionExpression, 67 | }), 68 | ], 69 | }) 70 | }) 71 | 72 | // See whether we need to add any imports 73 | if (importsRequired.size > 0) { 74 | // Look for solid-js import 75 | let lastImport: ASTPath | null = null 76 | let solidImport = root 77 | .find(j.ImportDeclaration) 78 | .map(path => { 79 | lastImport = path 80 | return path 81 | }) 82 | .filter(path => path.node.source.value === 'solid-js') 83 | .forEach(path => { 84 | if (!path.node.specifiers) { 85 | path.node.specifiers = [] 86 | } 87 | path.node.specifiers?.forEach(spec => { 88 | if (spec.type === 'ImportSpecifier') { 89 | importsRequired.delete(spec.imported.name) 90 | } 91 | }) 92 | 93 | for (const symbol of importsRequired) { 94 | path.node.specifiers.push( 95 | j.importSpecifier.from({ 96 | imported: j.jsxIdentifier(symbol), 97 | }), 98 | ) 99 | } 100 | }) 101 | 102 | if (solidImport.length == 0) { 103 | // Insert new solid import statement. 104 | const solidImport = j.importDeclaration.from({ 105 | source: j.stringLiteral('solid-js'), 106 | specifiers: Array.from(importsRequired).map(symbol => 107 | j.importSpecifier.from({ 108 | imported: j.jsxIdentifier(symbol), 109 | }), 110 | ), 111 | }) 112 | lastImport!.insertAfter(solidImport) 113 | } 114 | } 115 | 116 | return root.toSource() 117 | } 118 | -------------------------------------------------------------------------------- /src/transforms/solid@v2/jsx-properties-to-attributes/test/input.tsx: -------------------------------------------------------------------------------- 1 | const o = { 2 | // unfortunally we cannot change this 3 | autoFocus: true, 4 | } 5 | 6 | const quack = 'duck' 7 | 8 | const test = [ 9 |