-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(codemods): add @suspensive/codemods package
- Loading branch information
Showing
10 changed files
with
516 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { suspensiveTypeScriptConfig } from '@suspensive/eslint-config' | ||
|
||
export default [...suspensiveTypeScriptConfig] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "@suspensive/codemods", | ||
"version": "0.1.0", | ||
"description": "Codemods for @suspensive.", | ||
"keywords": [ | ||
"suspensive", | ||
"codemods" | ||
], | ||
"homepage": "https://suspensive.org", | ||
"bugs": "https://github.com/toss/suspensive/issues", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/toss/suspensive.git", | ||
"directory": "packages/codemods" | ||
}, | ||
"license": "MIT", | ||
"author": "Gwansik Kim <[email protected]> & Jonghyeon Ko <[email protected]>", | ||
"sideEffects": false, | ||
"type": "module", | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"build": "tsup", | ||
"ci:attw": "attw --pack", | ||
"ci:eslint": "eslint \"**/*.{ts,tsx,cts,mts}\"", | ||
"ci:publint": "publint --strict", | ||
"ci:test": "vitest run --coverage --typecheck", | ||
"ci:type": "tsc --noEmit", | ||
"clean": "rimraf ./dist && rimraf ./coverage", | ||
"prepack": "pnpm build", | ||
"test:ui": "vitest --ui --coverage --typecheck" | ||
}, | ||
"bin": "dist/suspensive-codemod.cjs", | ||
"dependencies": { | ||
"commander": "^12.1.0", | ||
"execa": "^4.1.0", | ||
"jscodeshift": "^17.0.0", | ||
"prompts": "^2.4.2" | ||
}, | ||
"devDependencies": { | ||
"@commander-js/extra-typings": "^12.1.0", | ||
"@suspensive/eslint-config": "workspace:*", | ||
"@suspensive/tsconfig": "workspace:*", | ||
"@types/jscodeshift": "^0.12.0", | ||
"@types/prompts": "^2.4.9" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env node | ||
|
||
import { Command } from 'commander' | ||
import packageJson from '../package.json' | ||
import { transformRunner } from './transformRunner' | ||
|
||
const program = new Command(packageJson.name) | ||
|
||
program | ||
.description(packageJson.description) | ||
.version(packageJson.version, '-v, --version', 'Output the current version of @suspensive/codemods') | ||
.argument('[codemod]', 'Codemod slug to run.') | ||
.argument('[path]', 'Path to source directory') | ||
.usage('[codemod] [path]') | ||
.action(transformRunner) | ||
|
||
program.parse(process.argv) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { join } from 'node:path' | ||
import execa from 'execa' | ||
import prompts from 'prompts' | ||
import { jscodeshiftExecutable, onCancel } from './utils' | ||
|
||
export const TRANSFORMER_INQUIRER_CHOICES = [ | ||
{ | ||
title: 'react-query-import', | ||
description: 'Safely migrate @tanstack/react-query', | ||
}, | ||
] | ||
|
||
export const transformerDirectory = join(__dirname, '../', 'dist', 'transforms') | ||
|
||
export async function transformRunner(transform: string, path: string) { | ||
let transformer = transform | ||
let directory = path | ||
|
||
if (transform && !TRANSFORMER_INQUIRER_CHOICES.find((x) => x.title === transform)) { | ||
console.error('Invalid transform choice, pick one of:') | ||
console.error(TRANSFORMER_INQUIRER_CHOICES.map((x) => '- ' + x.title).join('\n')) | ||
process.exit(1) | ||
} | ||
|
||
if (!transform) { | ||
const res = await prompts( | ||
{ | ||
type: 'select', | ||
name: 'transformer', | ||
message: 'Which transform would you like to apply?', | ||
choices: TRANSFORMER_INQUIRER_CHOICES.reverse().map(({ title, description }) => { | ||
return { | ||
title, | ||
description, | ||
value: title, | ||
} | ||
}), | ||
}, | ||
{ onCancel } | ||
) | ||
|
||
transformer = res.transformer as string | ||
} | ||
|
||
if (!path) { | ||
const res = await prompts( | ||
{ | ||
type: 'text', | ||
name: 'path', | ||
message: 'On which files or directory should the codemods be applied?', | ||
initial: '.', | ||
}, | ||
{ onCancel } | ||
) | ||
|
||
directory = res.path as string | ||
} | ||
|
||
let args = [] | ||
|
||
args.push('--no-babel') | ||
args.push('--ignore-pattern=**/node_modules/**') | ||
args.push('--ignore-pattern=**/.next/**') | ||
args.push('--extensions=tsx,ts,jsx,js') | ||
args.push('--parser=ts') | ||
|
||
args = args.concat(['--transform', join(transformerDirectory, `${transformer}.cjs`)]) | ||
args.push(directory) | ||
|
||
console.log(`jscodeshift ${args.join(' ')}`) | ||
|
||
const execaChildProcess = execa(jscodeshiftExecutable, args, { | ||
env: process.stdout.isTTY ? { FORCE_COLOR: 'true' } : {}, | ||
}) | ||
|
||
// "\n" + "a\n" + "b\n" | ||
let lastThreeLineBreaks = '' | ||
|
||
if (execaChildProcess.stdout) { | ||
execaChildProcess.stdout.pipe(process.stdout) | ||
execaChildProcess.stderr?.pipe(process.stderr) | ||
|
||
// The last two lines contain the successful transformation count as "N ok". | ||
// To save memory, we "slide the window" to keep only the last three line breaks. | ||
// We save three line breaks because the EOL is always "\n". | ||
execaChildProcess.stdout.on('data', (chunk: Buffer) => { | ||
lastThreeLineBreaks += chunk.toString('utf-8') | ||
|
||
let cutoff = lastThreeLineBreaks.length | ||
|
||
// Note: the stdout ends with "\n". | ||
// "foo\n" + "bar\n" + "baz\n" -> "\nbar\nbaz\n" | ||
// "\n" + "foo\n" + "bar\n" -> "\nfoo\nbar\n" | ||
|
||
for (let i = 0; i < 3; i++) { | ||
cutoff = lastThreeLineBreaks.lastIndexOf('\n', cutoff) - 1 | ||
} | ||
|
||
if (cutoff > 0 && cutoff < lastThreeLineBreaks.length) { | ||
lastThreeLineBreaks = lastThreeLineBreaks.slice(cutoff + 1) | ||
} | ||
}) | ||
} | ||
|
||
try { | ||
const result = await execaChildProcess | ||
|
||
if (result.failed) { | ||
throw new Error(`jscodeshift exited with code ${result.exitCode}`) | ||
} | ||
} catch (error) { | ||
console.error(error) | ||
process.exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type j from 'jscodeshift' | ||
import { createParserFromPath } from '../utils' | ||
|
||
const IMPORT_TO_CHANGE = [ | ||
'useSuspenseQuery', | ||
'UseSuspenseQueryResult', | ||
'UseSuspenseQueryOptions', | ||
'useSuspenseQueries', | ||
'SuspenseQueriesResults', | ||
'SuspenseQueriesOptions', | ||
'useSuspenseInfiniteQuery', | ||
'UseSuspenseInfiniteQueryResult', | ||
'UseSuspenseInfiniteQueryOptions', | ||
'usePrefetchQuery', | ||
'queryOptions', | ||
'infiniteQueryOption', | ||
] | ||
|
||
export default function transformer(file: j.FileInfo) { | ||
const j = createParserFromPath(file.path) | ||
const root = j(file.source) | ||
|
||
root | ||
.find(j.ImportDeclaration, { | ||
source: { | ||
value: '@suspensive/react-query', | ||
}, | ||
}) | ||
.forEach((path) => { | ||
const importSpecifiers = path.node.specifiers || [] | ||
|
||
const importNamesToChange = importSpecifiers.filter((specifier) => | ||
IMPORT_TO_CHANGE.includes(specifier.local?.name ?? '') | ||
) | ||
const importsNamesRemained = importSpecifiers.filter( | ||
(specifier) => !IMPORT_TO_CHANGE.includes(specifier.local?.name ?? '') | ||
) | ||
|
||
if (importNamesToChange.length > 0) { | ||
const newImportStatement = j.importDeclaration(importNamesToChange, j.stringLiteral('@tanstack/react-query')) | ||
path.insertBefore(newImportStatement) | ||
} | ||
if (importsNamesRemained.length > 0) { | ||
const remainingSpecifiers = importSpecifiers.filter( | ||
(specifier) => !IMPORT_TO_CHANGE.includes(specifier.local?.name ?? '') | ||
) | ||
|
||
const suspensiveReactQueryRemainImportsStatement = j.importDeclaration( | ||
remainingSpecifiers, | ||
j.stringLiteral('@suspensive/react-query') | ||
) | ||
path.insertBefore(suspensiveReactQueryRemainImportsStatement) | ||
} | ||
j(path).remove() | ||
}) | ||
.toSource() | ||
|
||
return root.toSource() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import j from 'jscodeshift' | ||
// @ts-expect-error: Declaration files are not included | ||
import babylonParse from 'jscodeshift/parser/babylon' | ||
// @ts-expect-error: Declaration files are not included | ||
import tsOptions from 'jscodeshift/parser/tsOptions' | ||
|
||
export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift') | ||
|
||
export function onCancel() { | ||
process.exit(1) | ||
} | ||
|
||
export function createParserFromPath(filePath: string): j.JSCodeshift { | ||
const isDeclarationFile = /\.d\.(m|c)?ts$/.test(filePath) | ||
if (isDeclarationFile) { | ||
return j.withParser( | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call | ||
babylonParse({ | ||
...tsOptions, | ||
plugins: [ | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access | ||
...tsOptions.plugins.filter((plugin: string) => plugin !== 'typescript'), | ||
['typescript', { dts: true }], | ||
], | ||
}) | ||
) | ||
} | ||
|
||
// jsx is allowed in .js files, feed them into the tsx parser. | ||
// tsx parser :.js, .jsx, .tsx | ||
// ts parser: .ts, .mts, .cts | ||
const isTsFile = /\.(m|c)?.ts$/.test(filePath) | ||
return isTsFile ? j.withParser('ts') : j.withParser('tsx') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "@suspensive/tsconfig/base.json", | ||
"compilerOptions": { | ||
"types": ["vitest/globals"] | ||
}, | ||
"include": ["**/*.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { defineConfig } from 'tsup' | ||
|
||
export default defineConfig({ | ||
format: 'cjs', | ||
target: ['node18'], | ||
entry: ['src/**/*.{ts,tsx}', '!**/*.{spec,test,test-d,bench}.*'], | ||
outDir: 'dist', | ||
external: ['.bin/jscodeshift'], | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { defineConfig } from 'vitest/config' | ||
import packageJson from './package.json' | ||
|
||
export default defineConfig({ | ||
test: { | ||
name: packageJson.name, | ||
dir: './src', | ||
globals: true, | ||
coverage: { | ||
provider: 'istanbul', | ||
}, | ||
}, | ||
}) |
Oops, something went wrong.