diff --git a/.eslintrc.json b/.eslintrc.json index ef9bcb1..011399d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "eslint-config-ibexa/eslint" + "extends": "eslint-config-ibexa/eslint" } diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f8b35ce --- /dev/null +++ b/babel.config.js @@ -0,0 +1,23 @@ +const { getModifyMethod } = require('./js/helpers.js'); + +module.exports = function (api) { + const modifyPlugins = getModifyMethod('plugins'); + const ibexaPlugins = [ + './js/ibexa-rename-ez-global.js', + './js/ibexa-rename-variables.js', + './js/ibexa-rename-string-values.js', + './js/ibexa-rename-trans-id.js', + './js/ibexa-rename-in-translations.js', + ]; + const finalPlugins = modifyPlugins(ibexaPlugins); + + api.cache(true); + + const presets = []; + const plugins = ['@babel/plugin-syntax-jsx', ...finalPlugins]; + + return { + presets, + plugins, + }; +}; diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..222256c --- /dev/null +++ b/js/README.md @@ -0,0 +1,199 @@ +# Get ready to run +``` +yarn --cwd ./vendor/ibexa/rector/js install +``` +`--cwd` argument should point to directory where JS transform module is installed (for example `./vendor/ibexa/rector/js`). This installs node_modules inside vendor bundle. + +# How to run +``` +yarn --cwd ./vendor/ibexa/rector/js transform +``` +`--cwd` argument should point to directory where JS transform module is installed (for example `./vendor/ibexa/rector/js`). + +## Configuration file +If you need to modify default config, plugins or configuration for rules, use `rector.config.js` from main directory of bundle. +``` +module.exports = { + config: { + paths: [{ + input: 'src/bundle/Resources/public', + output: 'src/bundle/Resources/public', + }], + prettierConfigPath: './prettier.js', + } + plugins: (plugins) => { + // modify enabled plugins + + return plugins; + }, + pluginsConfig: (config) => { + // modify plugins config + + return config; + } +}; +``` + +### config + +#### paths +Array of objects with input and output directories for transformed files. By default it's relative to main bundle root. While transforming, structure of directories is maintained. + +#### prettierConfigPath +Optional. At the end of transform there's mandatory prettier execution, which by default is taken from package https://github.com/ibexa/eslint-config-ibexa/blob/main/prettier.js + +### plugins +Allows to modify enabled plugins (more about plugin below). + +### pluginsConfig +Allows to modify config for plugins. +Example config: +``` +{ + "ibexa-rename-string-values": { + "ez-form-error": "ibexa-form-error", + "ez-selection-settings": { + "to": "ibexa-selection-settings", + "exactMatch": true + }, + "(^|\\s)\\.ez-": { + "to": ".ibexa-", + "regexp": true + }, + "ibexa-field-edit--ez([A-Za-z0-9]+)": { + "to": "ibexa-field-edit--ibexa$1", + "regexp": true + } + } +} +``` +Plugin config is kept as object with key being plugin name (`ibexa-rename-string-values` in example). + +Property key inside this object is string that is supposed to be changed, can be either standard string or regex (`(^|\\s)\\.ez-`, `ezform-error"` in example) + +#### Shorthand expression + +`"ez-form-error": "ibexa-form-error"` - change all `ez-form-error` occurences to `ibexa-form-error` + +#### Full object config properties + +`"to": "ibexa-selection-settings"` - what string should be replaced with + +`"regexp": true/false` - should config use regexp to match original value + +`"exactMatch": true` - should match only full values, using example config, this won't change `ez-selection-settings__field` as `ez-selection-settings` is not exact match. + +#### Special "shared" property +Except named plugins config, there is also possibility to create config for all plugins - its rules are later overwritten by specific plugin config if there is intersection in rules names. +``` + "shared": { + "ez": { + "to": "ibexa", + "exactMatch": true, + } + } +``` + +## Default plugins +### Rename eZ global variables +This plugin changes all `eZ` variables to `ibexa`. + +**Plugin name in config:** `./ibexa-rename-ez-global.js` + +**Example config:** none + +### Rename variables +This plugin allows to change any variable to any other value. + +**Plugin name in config:** `./ibexa-rename-variables.js` + +**Example config:** +``` +{ + "^Ez(.*?)Validator$": { + "to": "Ibexa$1Validator", + "regexp": true + }, + "^EZ_": { + "to": "IBEXA_", + "regexp": true + } +} +``` + +**Example output:** + +`class EzBooleanValidator extends eZ.BaseFieldValidator` => `class IbexaBooleanValidator extends ibexa.BaseFieldValidator` + +`const EZ_INPUT_SELECTOR = 'ezselection-settings__input';` => `const IBEXA_INPUT_SELECTOR = 'ezselection-settings__input';` + +### Rename string values +This plugin allows to change any string value - except translations. Can be used to transform selectors etc. + +**Plugin name in config:** `./ibexa-rename-string-values.js` + +**Example config:** +``` +{ + "(^|\\s)\\.ez-": { + "to": ".ibexa-", + "regexp": true + }, + "ibexa-field-edit--ez([A-Za-z0-9]+)": { + "to": "ibexa-field-edit--ibexa$1", + "regexp": true + }, + "ezselection-settings": "ibexaselection-settings" +} +``` + +**Example output:** + +`const SELECTOR_FIELD = '.ez-field-edit--ezboolean';` => `const SELECTOR_FIELD = ".ibexa-field-edit--ezboolean"` + +`const SELECTOR_FIELD = '.ibexa-field-edit--ezboolean';` => `const SELECTOR_FIELD = '.ibexa-field-edit--ibexaboolean';` + +### Rename translation IDs +This plugin allows to change translation ids. Remember to extract translations afterwards! + +**Plugin name in config:** `./ibexa-rename-trans-id.js` + +**Example config:** +``` +{ + "^ez": { + "to": "ibexa", + "regexp": true + } +} +``` + +**Example output:** + +`'ez_boolean.limitation.pick.ez_error'` => `'ibexa_boolean.limitation.pick.ez_error'` + +### Rename translation strings +This plugin allows to change translations. Remember to extract translations afterwards! + +**Plugin name in config:** `./ibexa-rename-in-translations.js` + +**Example config:** +``` +{ + "to": "ibexa-not-$1--show-modal", + "regexp": true, + "selectors-only": true +} +``` + +**selectors-only config:** + +If this property is set to `true`, this plugin changes only strings inside html tags (like classes and other html parameters). Set to `false` or remove property to change also normal strings as well. + +**Example output with selectors-only=true:** + +`/*@Desc("

Show message

for ez-not-error--show-modal")*/` => `/*@Desc("

Show message

for ez-not-error--show-modal")*/` + +**Example output with selectors-only=false:** + +`/*@Desc("

Show message

for ez-not-error--show-modal")*/` => `/*@Desc("

Show message

for ibexa-not-error--show-modal")*/` \ No newline at end of file diff --git a/js/helpers.js b/js/helpers.js new file mode 100644 index 0000000..93e1056 --- /dev/null +++ b/js/helpers.js @@ -0,0 +1,116 @@ +const fs = require('fs'); +const path = require('path'); + +const CONFIG_FILENAME = 'rector.config.js'; + +let savedMainConfig = null; + +const getMainConfig = () => { + if (savedMainConfig) { + return savedMainConfig; + } + + const { INIT_CWD } = process.env; + const fallbackConfigPath = path.resolve('js', CONFIG_FILENAME); + const customConfigPath = path.resolve(INIT_CWD, CONFIG_FILENAME); + + if (fs.existsSync(customConfigPath)) { + savedMainConfig = require(customConfigPath); + } else if (fs.existsSync(fallbackConfigPath)) { + savedMainConfig = require(fallbackConfigPath); + } else { + throw new Error(`Config file not found: ${customConfigPath} or ${fallbackConfigPath}`); + } + + return savedMainConfig; +}; + +const getPrettierConfigFile = (prettierConfigPath) => { + if (fs.existsSync(prettierConfigPath)) { + return prettierConfigPath; + } + + return require.resolve('eslint-config-ibexa/prettier'); +}; +const getAbsolutePath = (pathToDir) => { + if (path.isAbsolute(pathToDir)) { + return pathToDir; + } + + return path.join(process.env.INIT_CWD, pathToDir); +}; + +const getModifyMethod = (method) => { + const mainConfig = getMainConfig(); + + return mainConfig[method] ?? ((config) => config); +}; + +const getRulesConfig = (name) => { + const modifyConfig = getModifyMethod('config'); + const rulesConfigPath = path.join(__dirname, 'rules.config.json'); + const rawData = fs.readFileSync(rulesConfigPath); + const parsedData = JSON.parse(rawData); + const modifiedData = modifyConfig(parsedData); + const sharedConfig = modifiedData.shared ?? {}; + const namedConfig = modifiedData[name] ?? {}; + + return { + ...sharedConfig, + ...namedConfig, + }; +}; + +const shouldReplace = (original, oldValue, newValueConfig) => { + if (newValueConfig.exactMatch) { + return original === oldValue; + } else if (newValueConfig.regexp) { + return !!original.match(oldValue); + } + + return true; +}; + +const getValues = (oldValue, newValueConfig) => { + if (newValueConfig.regexp) { + return { + oldValue: new RegExp(oldValue, 'g'), + newValue: newValueConfig.to, + }; + } + + return { + oldValue, + newValue: newValueConfig.to ?? newValueConfig, + }; +}; + +const traverse = (moduleConfig, originalValue, replaceData) => { + Object.entries(moduleConfig).forEach(([oldValueRaw, newValueConfig]) => { + if (shouldReplace(originalValue, oldValueRaw, newValueConfig)) { + const { oldValue, newValue } = getValues(oldValueRaw, newValueConfig); + + replaceData(oldValue, newValue, newValueConfig); + } + }); +}; + +const isFunctionArgument = ({ parentPath }, functionName) => { + if (!parentPath?.isCallExpression()) { + return false; + } + + return parentPath.node.callee.property?.name === functionName; +}; + +module.exports = { + getMainConfig, + getPrettierConfigFile, + getAbsolutePath, + getModifyMethod, + getRulesConfig, + shouldReplace, + getValues, + traverse, + isFunctionArgument, +}; diff --git a/js/ibexa-rename-ez-global.js b/js/ibexa-rename-ez-global.js new file mode 100644 index 0000000..e55ae02 --- /dev/null +++ b/js/ibexa-rename-ez-global.js @@ -0,0 +1,16 @@ +module.exports = function ({ types }) { + return { + visitor: { + Identifier(path) { + if (path.node.name === 'eZ') { + path.node.name = types.toIdentifier('ibexa'); + } + }, + JSXIdentifier(path) { + if (path.node.name === 'eZ') { + path.node.name = types.toIdentifier('ibexa'); + } + }, + }, + }; +}; diff --git a/js/ibexa-rename-in-translations.js b/js/ibexa-rename-in-translations.js new file mode 100644 index 0000000..561e6a1 --- /dev/null +++ b/js/ibexa-rename-in-translations.js @@ -0,0 +1,34 @@ +const { getRulesConfig, traverse } = require('./helpers.js'); + +const moduleConfig = getRulesConfig('ibexa-rename-in-translations'); + +module.exports = function () { + return { + visitor: { + Identifier(path) { + if (path.node.name !== 'trans') { + return; + } + const parentCallExpresion = path.findParent((parentPath) => parentPath.isCallExpression()); + const translationComment = parentCallExpresion?.node.arguments[0]?.leadingComments[0]; + + if (translationComment) { + traverse(moduleConfig, translationComment.value, (oldValue, newValue, config) => { + if (config['selectors-only']) { + const regexp = new RegExp(`<[a-zA-Z0-9-_]*? .*?=(?:[\\"\\'])(.*?)\\1.*?>`, 'g'); + const matches = translationComment.value.match(regexp); + + matches?.forEach((match) => { + const valueToReplace = match.replaceAll(oldValue, newValue); + + translationComment.value = translationComment.value.replaceAll(match, valueToReplace); + }); + } else { + translationComment.value = translationComment.value.replaceAll(oldValue, newValue); + } + }); + } + }, + }, + }; +}; diff --git a/js/ibexa-rename-string-values.js b/js/ibexa-rename-string-values.js new file mode 100644 index 0000000..8a50251 --- /dev/null +++ b/js/ibexa-rename-string-values.js @@ -0,0 +1,29 @@ +const { getRulesConfig, traverse, isFunctionArgument } = require('./helpers.js'); + +const moduleConfig = getRulesConfig('ibexa-rename-string-values'); + +module.exports = function () { + return { + visitor: { + TemplateElement(path) { + if (isFunctionArgument(path, 'trans')) { + return; + } + + traverse(moduleConfig, path.node.value.raw, (oldValue, newValue) => { + path.node.value.raw = path.node.value.raw.replace(oldValue, newValue); + path.node.value.cooked = path.node.value.cooked.replace(oldValue, newValue); + }); + }, + StringLiteral(path) { + if (isFunctionArgument(path, 'trans')) { + return; + } + + traverse(moduleConfig, path.node.value, (oldValue, newValue) => { + path.node.value = path.node.value.replace(oldValue, newValue); + }); + }, + }, + }; +}; diff --git a/js/ibexa-rename-trans-id.js b/js/ibexa-rename-trans-id.js new file mode 100644 index 0000000..79b5438 --- /dev/null +++ b/js/ibexa-rename-trans-id.js @@ -0,0 +1,19 @@ +const { getRulesConfig, traverse, isFunctionArgument } = require('./helpers.js'); + +const moduleConfig = getRulesConfig('ibexa-rename-trans-id'); + +module.exports = function () { + return { + visitor: { + StringLiteral(path) { + if (!isFunctionArgument(path, 'trans')) { + return; + } + + traverse(moduleConfig, path.node.value, (oldValue, newValue) => { + path.node.value = path.node.value.replace(oldValue, newValue); + }); + }, + }, + }; +}; diff --git a/js/ibexa-rename-variables.js b/js/ibexa-rename-variables.js new file mode 100644 index 0000000..4e1b0fa --- /dev/null +++ b/js/ibexa-rename-variables.js @@ -0,0 +1,24 @@ +const { getRulesConfig, traverse } = require('./helpers.js'); + +const moduleConfig = getRulesConfig('ibexa-rename-variables'); + +module.exports = function ({ types }) { + return { + visitor: { + Identifier(path) { + traverse(moduleConfig, path.node.name, (oldValue, newValue) => { + const newIdentifier = path.node.name.replace(oldValue, newValue); + + path.node.name = types.toIdentifier(newIdentifier); + }); + }, + JSXIdentifier(path) { + traverse(moduleConfig, path.node.name, (oldValue, newValue) => { + const newIdentifier = path.node.name.replace(oldValue, newValue); + + path.node.name = types.toIdentifier(newIdentifier); + }); + }, + }, + }; +}; diff --git a/js/rector.config.js b/js/rector.config.js new file mode 100644 index 0000000..547e8a5 --- /dev/null +++ b/js/rector.config.js @@ -0,0 +1,23 @@ +module.exports = { + config: { + paths: [ + { + input: 'src/bundle/Resources/public', + output: 'src/bundle/Resources/public', + }, + { + input: 'src/bundle/ui-dev/src/modules', + output: 'src/bundle/ui-dev/src/modules', + }, + ], + // prettierConfigPath: 'pathToPrettierConfigFile', + }, + plugins: (plugins) => { + // modify enabled plugins + return plugins; + }, + pluginsConfig: (config) => { + // modify plugins config + return config; + }, +}; diff --git a/js/rules.config.json b/js/rules.config.json new file mode 100644 index 0000000..df09215 --- /dev/null +++ b/js/rules.config.json @@ -0,0 +1,63 @@ +{ + "ibexa-rename-variables": { + "^Ez(.*?)Validator$": { + "to": "Ibexa$1Validator", + "regexp": true + }, + "^Ez(.*?)Field$": { + "to": "Ibexa$1Field", + "regexp": true + }, + "_EZ_": { + "to": "_IBEXA_", + "regexp": true + }, + "ez": { + "to": "ibexa", + "exactMatch": true + }, + "ezSiteLocationId": { + "to": "ibexaSiteLocationId", + "exactMatch": true + } + }, + "ibexa-rename-string-values": { + "\\.ez-": { + "to": ".ibexa-", + "regexp": true + }, + "#ez-": { + "to": "#ibexa-", + "regexp": true + }, + "^ez-": { + "to": "ibexa-", + "regexp": true + }, + "^ezplatform-": { + "to": "ibexa-", + "regexp": true + }, + "^ezplatform.": { + "to": "ibexa.", + "regexp": true + }, + "ibexa-field-edit--ez([A-Za-z0-9]+)": { + "to": "ibexa-field-edit--ibexa$1", + "regexp": true + } + }, + "ibexa-rename-trans-id": { + "^ez": { + "to": "ibexa", + "regexp": true + } + }, + "ibexa-rename-in-translations": { + "ez-": { + "to": "ibexa-", + "regexp": true, + "selectors-only": true + } + } +} diff --git a/js/transform-script.js b/js/transform-script.js new file mode 100755 index 0000000..af1c090 --- /dev/null +++ b/js/transform-script.js @@ -0,0 +1,25 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const { getMainConfig, getPrettierConfigFile, getAbsolutePath } = require('./helpers'); + +const { config } = getMainConfig(); +const { paths, prettierConfigPath } = config; +const prettierConfigFile = getPrettierConfigFile(prettierConfigPath); + +paths.forEach(({ input, output }) => { + const inputAbsolutePath = getAbsolutePath(input); + const outputAbsolutePath = getAbsolutePath(output); + + if (!fs.existsSync(inputAbsolutePath)) { + return; + } + + let command = `babel ${inputAbsolutePath} -d ${outputAbsolutePath} --retain-lines`; + command += ` && yarn prettier "${outputAbsolutePath}/**/*.js" --config ${prettierConfigFile} --write`; + + try { + execSync(command, { stdio: 'inherit' }); + } catch (err) {} // eslint-disable-line no-empty +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..b197056 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "rector", + "repository": "git@github.com:ibexa/rector.git", + "private": true, + "prettier": "eslint-config-ibexa/prettier", + "dependencies": { + "@babel/cli": "^7.27.0", + "@babel/core": "^7.26.10", + "@babel/plugin-syntax-jsx": "^7.25.9", + "eslint-config-ibexa": "https://github.com/ibexa/eslint-config-ibexa.git#~v1.1.1" + }, + "scripts": { + "test": "yarn prettier-test && yarn eslint-test", + "fix": "yarn prettier-test --write && yarn eslint-test --fix", + "eslint-test": "eslint \"./js/**/*.js\"", + "prettier-test": "yarn prettier \"./js/**/*.{js,scss}\" --check", + "transform": "node ./js/transform-script.js" + } +}