react-ui-codemod
Advanced tools
Comparing version 1.3.0-beta.5 to 1.3.0-beta.6
/* eslint-disable import/no-default-export */ | ||
import { API, FileInfo } from 'jscodeshift'; | ||
import { API, FileInfo, ObjectExpression } from 'jscodeshift'; | ||
import { Collection } from 'jscodeshift/src/Collection'; | ||
/** | ||
* Transform skips to new format | ||
* @see https://github.com/creevey/creevey/pull/206 | ||
*/ | ||
const transformSkips = (api: API, collection: Collection<any>): boolean => { | ||
@@ -10,39 +15,52 @@ const j = api.jscodeshift; | ||
collection | ||
// { creevey: ... } | ||
.find(j.ObjectProperty, (node) => node.key.name === 'creevey') | ||
.forEach((path) => { | ||
const skip = path.node.value.properties.find((p) => p.key.name === 'skip'); | ||
if (skip) { | ||
const { value } = skip; | ||
if (path.node.value.type === "ObjectExpression") { | ||
// { creevey: {...} } | ||
const skip = path.node.value.properties.find((p) => p.type === "ObjectProperty" && p.key.type === "Identifier" && p.key.name === 'skip'); | ||
if (skip && skip.type === "ObjectProperty") { | ||
// { creevey: { skip: ... } } | ||
const { value } = skip; | ||
if (value.type === 'ArrayExpression') { | ||
const { elements } = value; | ||
if (value.type === 'ArrayExpression') { | ||
// { creevey: { skip: [...] } } | ||
const { elements } = value; | ||
if (elements.length === 1 && elements[0].type === 'BooleanLiteral') { | ||
skip.value = elements[0]; | ||
modified = true; | ||
} else if (elements.length !== 0) { | ||
const props = []; | ||
elements.forEach((e, i) => { | ||
let reason = ''; | ||
if (e.type === 'ObjectExpression') { | ||
const reasonProp = e.properties.find((p) => p.key.name === 'reason'); | ||
if (reasonProp) { | ||
reason = reasonProp.value.value; | ||
} | ||
// j(reasonProp).remove(); | ||
} | ||
if (!reason) { | ||
let defaultExport = path.parent; | ||
while (defaultExport) { | ||
if (defaultExport.value.type === 'ExportDefaultDeclaration') { | ||
break; | ||
if (elements.length === 1 && elements[0] && elements[0].type === 'BooleanLiteral') { | ||
// { creevey: { skip: [true|false] } } | ||
skip.value = elements[0]; | ||
modified = true; | ||
} else if (elements.length !== 0) { | ||
// { creevey: { skip: [..., ...] } } | ||
const props: ObjectExpression['properties'] = []; | ||
elements.forEach((e, i) => { | ||
let reason = ''; | ||
if (e) { | ||
if (e.type === 'ObjectExpression') { | ||
// { creevey: { skip: [{ ... }, ] } } | ||
const reasonProp = e.properties.find((p) => p.type === "ObjectProperty" && p.key.type === "Identifier" && p.key.name === 'reason'); | ||
// { creevey: { skip: [{ reason: ..., }, ] } } | ||
if (reasonProp && reasonProp.type === "ObjectProperty" && reasonProp.value.type === "StringLiteral") { | ||
// { creevey: { skip: [{ reason: "...", }, ] } } | ||
reason = reasonProp.value.value; | ||
} | ||
// j(reasonProp).remove(); | ||
} | ||
defaultExport = defaultExport.parent; | ||
if (!reason) { | ||
let defaultExport = path.parent; | ||
while (defaultExport) { | ||
if (defaultExport.value.type === 'ExportDefaultDeclaration') { | ||
break; | ||
} | ||
defaultExport = defaultExport.parent; | ||
} | ||
reason = (defaultExport ? 'kind' : 'story') + '-skip-' + i; | ||
} | ||
props.push(j.property('init', j.literal(reason), e as any)); | ||
} | ||
reason = (defaultExport ? 'kind' : 'story') + '-skip-' + i; | ||
} | ||
props.push(j.property('init', j.literal(reason), e)); | ||
}); | ||
skip.value = j.objectExpression(props); | ||
modified = true; | ||
}); | ||
skip.value = j.objectExpression(props); | ||
modified = true; | ||
} | ||
} | ||
@@ -49,0 +67,0 @@ } |
/* eslint-disable import/no-default-export */ | ||
import path from 'path'; | ||
import { writeFileSync, mkdirSync, existsSync } from 'fs'; | ||
import { exec } from 'child_process'; | ||
import { API, FileInfo } from 'jscodeshift'; | ||
import { API, FileInfo, ObjectExpression, VariableDeclarator } from 'jscodeshift'; | ||
import { Collection } from 'jscodeshift/src/Collection'; | ||
import { CreeveyStoryParams } from 'creevey'; | ||
import prettier from 'prettier'; | ||
import type { CreeveyStoryParams } from 'creevey'; | ||
type Stories = Record<string, CreeveyStoryParams>; | ||
type StringifiedTests = Record<string, string>; | ||
type StringifiedCreeveyParams = Record<keyof CreeveyStoryParams, string>; | ||
type ExtractedCreeveyParams = Partial<Omit<StringifiedCreeveyParams, 'tests'> & { tests: StringifiedTests }>; | ||
type ExtractedStories = Record<string, ExtractedCreeveyParams>; | ||
type GlobalVariables = Record<string, unknown>; | ||
let vars = {}; | ||
/** | ||
* Remove variable declaration from code by its name | ||
*/ | ||
const removeVariable = (api: API, c: Collection<any>, name: string): void => { | ||
@@ -19,23 +24,66 @@ const j = api.jscodeshift; | ||
const processVariable = (api: API, c: Collection<any>, name: string): void => { | ||
/** | ||
* Extract properties from a variable and store them globally | ||
*/ | ||
const processVariable = (api: API, c: Collection<any>, name: string, vars: GlobalVariables): void => { | ||
const properties = getPropertiesFromVarible(api, c, name); | ||
if (properties.length) { | ||
vars[name] = propertiesToStringsObj(api, properties, c); | ||
vars[name] = stringifyTests(api, properties, c, vars); | ||
} | ||
}; | ||
const propertiesToStringsObj = (api: API, properties: Collection<any>, collection: Collection<any>): any => { | ||
/** | ||
* Convert tests defined as object properties to strings. | ||
* | ||
* E.g., properties of | ||
* { | ||
* testA: () => { void 0 }, | ||
* testA: function(){ void 0 }, | ||
* ['testC']() { void 0 }, | ||
* ...varX | ||
* } | ||
* | ||
* results in | ||
* | ||
* { | ||
* testA: 'void 0', | ||
* testB: 'void 0', | ||
* testC: 'void 0', | ||
* varX: 'varX' | ||
* } | ||
*/ | ||
const stringifyTests = (api: API, properties: ObjectExpression['properties'], collection: Collection<any>, globalVars: GlobalVariables): StringifiedTests => { | ||
const j = api.jscodeshift; | ||
const result = {}; | ||
const result: StringifiedTests = {}; | ||
properties.forEach((p) => { | ||
if (p.type === 'ObjectMethod') { | ||
const testName = p.key.name || p.key.value; | ||
if (p.type === 'ObjectMethod' || p.type === 'ObjectProperty') { | ||
// { a() {} } or { a: ... } | ||
let testName = ''; | ||
let testSource: string | string[] = ''; | ||
if (p.key.type === "Identifier") { | ||
// { a() {} } or { a: ... } | ||
testName = p.key.name; | ||
} else if (p.key.type === "StringLiteral") { | ||
// { ['a']() {} } or { ['a']: ... } | ||
testName = p.key.value; | ||
} | ||
const testSource = j(p.body.body).toSource(); | ||
result[testName] = Array.isArray(testSource) ? testSource.join(' ') : testSource; | ||
} else if (p.type === 'SpreadElement') { | ||
if (testName) { | ||
if (p.type === "ObjectMethod") { | ||
// { a() {} } | ||
testSource = j(p.body.body).toSource(); | ||
} else if ( p.value.type === "ArrowFunctionExpression" || p.value.type === "FunctionExpression") { | ||
// { a: () => {} } or { a: function() {} } | ||
testSource = p.value.body.type === "BlockStatement" ? j(p.value.body.body).toSource() : j(p.value.body).toSource(); | ||
} | ||
} | ||
if (testSource) { | ||
result[testName] = Array.isArray(testSource) ? testSource.join(' ') : testSource; | ||
} | ||
} else if (p.type === 'SpreadElement' && p.argument.type === "Identifier" ) { | ||
// { ...varA } | ||
const n = p.argument.name; | ||
result[n] = n; | ||
processVariable(api, collection, n); | ||
processVariable(api, collection, n, globalVars); | ||
} | ||
@@ -47,17 +95,25 @@ }); | ||
const getPropertiesFromVarible = (api: API, c: Collection<any>, name: string): any[] => { | ||
/** | ||
* Extract properties of object defined as variable | ||
*/ | ||
const getPropertiesFromVarible = (api: API, c: Collection<any>, name: string): ObjectExpression['properties'] => { | ||
const j = api.jscodeshift; | ||
// const name = ... | ||
const vs = c.find(j.VariableDeclarator, (node) => node.id.name === name); | ||
if (vs.size() === 1) { | ||
const v = vs.get(0).node; | ||
if (v.init.type !== 'ObjectExpression') { | ||
console.log(`VAR (${name}) IS NOT AN OBJECT!`, v.init); | ||
} else { | ||
return v.init.properties; | ||
const v: VariableDeclarator = vs.get(0).node; | ||
if (v.init) { | ||
if (v.init.type !== 'ObjectExpression') { | ||
// const name = ?? | ||
console.log(`The type of the "${name}" is not supported. Expected an ObjectExpression, but got ${'ObjectExpression'}.`, v.init); | ||
} else { | ||
// const name = {...} | ||
return v.init.properties; | ||
} | ||
} | ||
} else { | ||
console.log(`NUMBER OF VARS (${name}) IS UNEXPECTED (${vs.size()})!`); | ||
// either found no vars or too many of them | ||
console.log(`Can't get the definition: "${name}". Found: ${vs.size()}.`); | ||
} | ||
@@ -68,9 +124,14 @@ | ||
const extractTests = (api: API, collection: Collection<any>, vars: any): { kind: string; stories: Stories } => { | ||
/** | ||
* Extract creevey tests from stories to separate files | ||
*/ | ||
const extractTests = (api: API, collection: Collection<any>): { kind: string; stories: ExtractedStories, vars: GlobalVariables } => { | ||
const j = api.jscodeshift; | ||
let kind = ''; | ||
let kindName = ''; | ||
let kindTests = {}; | ||
const stories: Stories = {}; | ||
const stories: ExtractedStories = {}; | ||
const vars: GlobalVariables = {}; | ||
// extract kind's title and creevey params from default export | ||
collection | ||
@@ -82,37 +143,48 @@ .find(j.ExportDefaultDeclaration) | ||
if (node.key.name === 'title') { | ||
kind = node.value.value; | ||
} | ||
if (node.key.name === 'creevey') { | ||
let testsIndex = -1; | ||
node.value.properties.forEach((p, i) => { | ||
if (p.key.name === 'tests') { | ||
testsIndex = i; | ||
if (node.key.type === "Identifier") { | ||
if (node.key.name === 'title' && node.value.type === 'StringLiteral') { | ||
// export default { title: '...' } | ||
kindName = node.value.value; | ||
} | ||
if (node.key.name === 'creevey' && node.value.type === "ObjectExpression") { | ||
// export default { creevey: { ... } } | ||
let testsIndex = -1; | ||
node.value.properties.forEach((p, i) => { | ||
if (p.type === "ObjectProperty" && p.key.type === "Identifier" && p.key.name === 'tests') { | ||
// export default { creevey: { tests: { ... } } } | ||
testsIndex = i; | ||
if (p.value.type === 'Identifier') { | ||
const variableName = p.value.name; | ||
kindTests = { | ||
[variableName]: variableName, | ||
}; | ||
processVariable(api, collection, variableName); | ||
} else if (p.value.type === 'ObjectExpression') { | ||
kindTests = propertiesToStringsObj(api, p.value.properties, collection); | ||
} else { | ||
console.log(kind + ': TESTS IS SOMETHING UNKNOWN: ', p.value.type); | ||
if (p.value.type === 'Identifier') { | ||
// export default { creevey: { tests: variableName } } | ||
const variableName = p.value.name; | ||
kindTests = { | ||
[variableName]: variableName, | ||
}; | ||
processVariable(api, collection, variableName, vars); | ||
} else if (p.value.type === 'ObjectExpression') { | ||
// export default { creevey: { tests: {...} } } | ||
kindTests = stringifyTests(api, p.value.properties, collection, vars); | ||
} else { | ||
console.log(`${kindName}: tests are not an ObjectExpression (${p.value.type}`); | ||
} | ||
} | ||
}); | ||
if (testsIndex > -1) { | ||
// remove tests from kind params | ||
// but remain the other creevey params | ||
node.value.properties.splice(testsIndex, 1); | ||
} | ||
}); | ||
if (testsIndex > -1) { | ||
node.value.properties.splice(testsIndex, 1); | ||
} | ||
} | ||
}); | ||
if (kind) { | ||
// find named exports and init stories hash | ||
if (kindName) { | ||
collection | ||
.find(j.ExportNamedDeclaration) | ||
.find(j.VariableDeclarator) | ||
.find(j.Identifier) | ||
.forEach((path) => { | ||
stories[path.node.id.name] = null; | ||
stories[path.node.name] = {}; | ||
}); | ||
@@ -124,38 +196,50 @@ } | ||
Object.keys(stories).forEach((story) => { | ||
stories[story] = stories[story] || {}; | ||
const creeveyObj = code | ||
// find assignment of story parameters: Story.parameters = ... | ||
.find(j.ExpressionStatement, (node) => | ||
node.expression.left && node.expression.left.object ? node.expression.left.object.name === story : false, | ||
) | ||
// find creevey parameters: Story.parameters = { creevey: ... } | ||
.find(j.ObjectProperty, (node) => node.key.name === 'creevey') | ||
.forEach((path) => { | ||
let hasTests = false; | ||
path.node.value.properties.forEach((p) => { | ||
const paramName = p.key.name; | ||
if (paramName === 'tests') { | ||
let props = []; | ||
hasTests = true; | ||
if (p.value.type === 'Identifier') { | ||
const variableName = p.value.name; | ||
stories[story].tests = { | ||
[variableName]: variableName, | ||
}; | ||
processVariable(api, collection, variableName); | ||
} else if (p.value.type === 'ObjectExpression') { | ||
props = p.value.properties; | ||
} else { | ||
hasTests = false; | ||
console.log(kind + ': TESTS IS SOMETHING UNKNOWN: ', p.value.type); | ||
} | ||
if (path.node.value.type === "ObjectExpression") { | ||
// Story.parameters = { creevey: { ... } } | ||
path.node.value.properties.forEach((p) => { | ||
if (p.type === 'ObjectProperty' && p.key.type === "Identifier") { | ||
const paramName = p.key.name as keyof ExtractedCreeveyParams; | ||
if (paramName === 'tests') { | ||
// Story.parameters = { creevey: { tests: ... } } | ||
let props: ObjectExpression['properties'] = []; | ||
hasTests = true; | ||
if (props.length) { | ||
stories[story].tests = propertiesToStringsObj(api, props, collection); | ||
if (p.value.type === 'Identifier') { | ||
// Story.parameters = { creevey: { tests: variableName } } | ||
const variableName = p.value.name; | ||
stories[story].tests = { | ||
[variableName]: variableName, | ||
}; | ||
processVariable(api, collection, variableName, vars); | ||
} else if (p.value.type === 'ObjectExpression') { | ||
// Story.parameters = { creevey: { tests: {...} } } | ||
props = p.value.properties; | ||
} else { | ||
hasTests = false; | ||
console.log(`${kindName}.${story}: tests are not an ObjectExpression or a variable (${p.value.type}.`); | ||
} | ||
if (props.length) { | ||
stories[story].tests = stringifyTests(api, props, collection, vars); | ||
} | ||
} else { | ||
// Story.parameters = { creevey: { paramName: ... } } | ||
stories[story][paramName] = j(p.value).toSource(); | ||
} | ||
} | ||
} else { | ||
stories[story][paramName] = j(p.value).toSource(); | ||
} | ||
}); | ||
}); | ||
} | ||
if (hasTests) { | ||
// remove extracted creevey parameters from story | ||
path.replace(); | ||
@@ -165,2 +249,4 @@ } | ||
// if a story doesn't have any more parameters | ||
// then remove the whole parameters assignment | ||
if ( | ||
@@ -184,2 +270,5 @@ creeveyObj.size() && | ||
// if there are some kind-level tests | ||
// add them to every story as a variable | ||
// to unwrap later | ||
if (Object.keys(kindTests).length) { | ||
@@ -193,2 +282,4 @@ vars['kindTests'] = kindTests; | ||
// if there is no tests in extracted creevey params | ||
// then ignore these params and leave them in a story file | ||
Object.keys(stories).forEach((key) => { | ||
@@ -200,2 +291,4 @@ if (!stories[key].tests) { | ||
// remove variables that used to contain tests | ||
Object.keys(vars).forEach((name) => { | ||
@@ -205,15 +298,19 @@ removeVariable(api, collection, name); | ||
return { kind, stories }; | ||
return { kind: kindName, stories, vars }; | ||
}; | ||
/** | ||
* Generate a file with extracted tests | ||
*/ | ||
const createTestFile = ( | ||
filePath: string, | ||
kind: string, | ||
stories: Stories, | ||
vars: any, | ||
{ testsPath = '../__creevey__/', prettier: usePrettier = true }: TransformOptions, | ||
stories: ExtractedStories, | ||
vars: GlobalVariables, | ||
{ testsPath = '../__creevey__/', prettier = true }: TransformOptions, | ||
): void => { | ||
const mainTemplate = (kind, stories, vars) => ` | ||
const mainTemplate = (kind: string, stories: ExtractedStories, vars: GlobalVariables) => ` | ||
import { story, kind, test } from 'creevey'; | ||
${/* if there are variables containing tests, defined them at the top */''} | ||
${Object.keys(vars).reduce( | ||
@@ -223,3 +320,3 @@ (r, v) => ` | ||
const ${v} = () => { | ||
${generateTests(vars[v])} | ||
${generateTests(vars[v] as StringifiedTests)} | ||
} | ||
@@ -235,7 +332,7 @@ `, | ||
const generateStories = (stories, vars) => { | ||
const storyTemplate = (story, params, vars) => { | ||
const generateStories = (stories: ExtractedStories, vars: GlobalVariables) => { | ||
const storyTemplate = (story: string, params: ExtractedCreeveyParams, vars: GlobalVariables) => { | ||
const { tests, ...rest } = params; | ||
const restParams = Object.keys(rest); | ||
const storyParams = restParams.length ? `{ ${restParams.map((key) => `${key}: ${rest[key]}`).join(', ')} }` : ``; | ||
const storyParams = restParams.length ? `{ ${restParams.map((key) => `${key}: ${rest[key as keyof typeof rest]}`).join(', ')} }` : ``; | ||
return ` | ||
@@ -245,3 +342,3 @@ story("${story}", (${storyParams ? `{ setStoryParameters }` : ``}) => { | ||
${generateTests(tests, vars)} | ||
${tests ? generateTests(tests, vars) : ``} | ||
}); | ||
@@ -252,5 +349,5 @@ `; | ||
return Object.keys(stories).reduce( | ||
(result, story) => ` | ||
(result, storyName) => ` | ||
${result} | ||
${storyTemplate(story, stories[story], vars)} | ||
${storyTemplate(storyName, stories[storyName], vars)} | ||
`, | ||
@@ -261,4 +358,7 @@ ``, | ||
const generateTests = (tests, vars?) => { | ||
const testsTemplate = (test, fn, vars) => { | ||
const generateTests = (tests: StringifiedTests, vars?: GlobalVariables): string => { | ||
if (!tests) { | ||
return ''; | ||
} | ||
const testsTemplate = (test: string, fn: string, vars?: GlobalVariables) => { | ||
if (vars && vars[test]) { | ||
@@ -278,5 +378,5 @@ return ` | ||
return Object.keys(tests).reduce( | ||
(result, test) => ` | ||
(result, testName) => ` | ||
${result} | ||
${testsTemplate(test, tests[test], vars)} | ||
${testsTemplate(testName, tests[testName], vars)} | ||
`, | ||
@@ -287,10 +387,4 @@ ``, | ||
const fileText = mainTemplate(kind, stories, vars); | ||
const file = mainTemplate(kind, stories, vars); | ||
const file = usePrettier | ||
? prettier.format(fileText, { | ||
parser: 'typescript', | ||
}) | ||
: fileText; | ||
const dir = path.isAbsolute(testsPath) | ||
@@ -306,2 +400,4 @@ ? path.normalize(testsPath + '/') | ||
// handle possible stories hierarchy by taking only the last kind's subtitle | ||
// "KindTitle/Subtitle_1/Subtitle_2" => "Subtitle_2" | ||
const testFilePath = path.join(dir + kind.split('/').pop() + '.creevey' + ext); | ||
@@ -312,2 +408,6 @@ | ||
writeFileSync(testFilePath, file); | ||
if (prettier) { | ||
exec(`prettier --write ${testFilePath}`); | ||
} | ||
}; | ||
@@ -329,9 +429,6 @@ | ||
export default function transform(file: FileInfo, api: API, options: TransformOptions) { | ||
const { testsPath } = options; | ||
const j = api.jscodeshift; | ||
const result = j(file.source); | ||
vars = {}; | ||
const { kind, stories } = extractTests(api, result, vars); | ||
const { kind, stories, vars } = extractTests(api, result); | ||
@@ -342,3 +439,6 @@ if (!Object.keys(stories).length) { | ||
createTestFile(file.path, kind, stories, vars, options); | ||
if (file.path) { | ||
// file.path is undefined in tests, so prevent the error | ||
createTestFile(file.path, kind, stories, vars, options); | ||
} | ||
@@ -345,0 +445,0 @@ return result.toSource(); |
{ | ||
"name": "react-ui-codemod", | ||
"version": "1.3.0-beta.5", | ||
"version": "1.3.0-beta.6", | ||
"main": "index.js", | ||
@@ -11,3 +11,3 @@ "license": "MIT", | ||
"ast-types": "0.13.2", | ||
"jscodeshift": "^0.7.0", | ||
"jscodeshift": "^0.11.0", | ||
"prettier": "^2" | ||
@@ -14,0 +14,0 @@ }, |
120077
2884
+ Added@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(transitive)
+ Added@babel/plugin-proposal-optional-chaining@7.21.0(transitive)
+ Added@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(transitive)
+ Added@babel/plugin-syntax-optional-chaining@7.8.3(transitive)
+ Addedast-types@0.14.2(transitive)
+ Addedelectron-to-chromium@1.5.99(transitive)
+ Addedflow-parser@0.261.1(transitive)
+ Addedjscodeshift@0.11.0(transitive)
+ Addedrecast@0.20.5(transitive)
+ Addedtslib@2.8.1(transitive)
- Removed@babel/plugin-proposal-object-rest-spread@7.20.7(transitive)
- Removed@babel/plugin-syntax-object-rest-spread@7.8.3(transitive)
- Removedast-types@0.13.3(transitive)
- Removedelectron-to-chromium@1.5.97(transitive)
- Removedflow-parser@0.259.1(transitive)
- Removedjscodeshift@0.7.1(transitive)
- Removedprivate@0.1.8(transitive)
- Removedrecast@0.18.10(transitive)
Updatedjscodeshift@^0.11.0