@bigbinary/neeto-commons-frontend 2.0.72 → 2.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ module.exports = {
22
22
  "never",
23
23
  {
24
24
  ignorePackages: true,
25
- pattern: { json: "always", mp3: "always", ico: "always" },
25
+ pattern: { json: "always", ico: "always" },
26
26
  },
27
27
  ],
28
28
  },
@@ -32,7 +32,7 @@ module.exports = {
32
32
  // We need this for the import/extensions rule to work: https://github.com/import-js/eslint-plugin-import#importextensions
33
33
  "import/resolver": {
34
34
  node: {
35
- extensions: [".js", ".jsx", ".ts", ".tsx", ".svg", ".json", ".mp3"],
35
+ extensions: [".js", ".jsx", ".ts", ".tsx", ".svg", ".json"],
36
36
  },
37
37
  },
38
38
  },
@@ -3,7 +3,7 @@ const path = require("path");
3
3
  const { mergeDeepLeft } = require("ramda");
4
4
 
5
5
  const rootOfTheProject = path.join(__dirname, "../../../../../../");
6
- const pathToTranslationFile = "src/translations/en.json";
6
+ const pathToTranslationsDir = "src/translations";
7
7
  const pathToResolveFile = "resolve.js";
8
8
  const pathToJsConfigFile = "jsconfig.json";
9
9
 
@@ -19,7 +19,6 @@ const commonResolve = loadJS(
19
19
  "node_modules/@bigbinary/neeto-commons-frontend/configs/nanos/webpack/resolve.js"
20
20
  );
21
21
  const projectResolve = loadJS(pathToResolveFile);
22
- const en = loadJS(pathToTranslationFile);
23
22
  const resolve = mergeDeepLeft(projectResolve, commonResolve);
24
23
 
25
24
  module.exports = {
@@ -77,7 +76,10 @@ module.exports = {
77
76
  "@bigbinary/neeto/no-axios-import-outside-apis": ["error", resolve.alias],
78
77
  "@bigbinary/neeto/no-missing-localization": [
79
78
  "error",
80
- { name: "en.json", lng: "en", content: en },
79
+ {
80
+ translationsDir: path.join(rootOfTheProject, pathToTranslationsDir),
81
+ languages: ["en"],
82
+ },
81
83
  ],
82
84
  "@bigbinary/neeto/prefix-neeto-ui-import-alias": ["error", resolve.alias],
83
85
  "@bigbinary/neeto/use-webpack-alias": ["error", resolve.alias],
@@ -0,0 +1,12 @@
1
+ const RAILS_MOUNTED_COMPONENTS_REGEX = /app\/javascript\/src\/[^/]*\.(js|jsx)$/;
2
+ const PACKS_FILES_REGEX = /app\/javascript\/packs\/[^/]*\.(js|jsx)$/;
3
+
4
+ const TYPES = {
5
+ CLASS_DECLARATION: "ClassDeclaration",
6
+ FUNCTION_DECLARATION: "FunctionDeclaration",
7
+ IMPORT_DEFAULT_SPECIFIER: "ImportDefaultSpecifier",
8
+ IMPORT_NAMESPACE_SPECIFIER: "ImportNamespaceSpecifier",
9
+ VARIABLE_DECLARATION: "VariableDeclaration",
10
+ };
11
+
12
+ module.exports = { RAILS_MOUNTED_COMPONENTS_REGEX, PACKS_FILES_REGEX, TYPES };
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const { exec, execSync } = require("child_process");
5
+ const fs = require("fs");
6
+ const pathLib = require("path");
7
+
8
+ const generate = require("@babel/generator").default;
9
+ const babelParser = require("@babel/parser");
10
+ const traverse = require("@babel/traverse").default;
11
+ const {
12
+ isNotEmpty,
13
+ existsBy,
14
+ } = require("@bigbinary/neeto-commons-frontend/pure");
15
+ const enhancedResolve = require("enhanced-resolve");
16
+ const { isEmpty } = require("ramda");
17
+
18
+ const {
19
+ RAILS_MOUNTED_COMPONENTS_REGEX,
20
+ TYPES,
21
+ PACKS_FILES_REGEX,
22
+ } = require("./constants");
23
+
24
+ // @ts-ignore
25
+ // eslint-disable-next-line import/extensions
26
+ const webpackConfig = require("../../../../../../config/webpack/webpack.config.js");
27
+
28
+ let modifiedFiles = [];
29
+ let importedItems = [];
30
+ let unusedFiles = [];
31
+ let iterationCount = 1;
32
+
33
+ const javascriptDir = pathLib.resolve(
34
+ __dirname,
35
+ "../../../../../../app/javascript"
36
+ );
37
+
38
+ const resolver = enhancedResolve.create.sync({
39
+ alias: webpackConfig.resolve.alias,
40
+ modules: webpackConfig.resolve.modules,
41
+ extensions: webpackConfig.resolve.extensions,
42
+ conditionNames: ["require", "import", "node", "default", "types"],
43
+ });
44
+
45
+ const isRailsMountedOrPacksFiles = filePath =>
46
+ RAILS_MOUNTED_COMPONENTS_REGEX.test(filePath) ||
47
+ PACKS_FILES_REGEX.test(filePath);
48
+
49
+ const isInImportedList = (item, filePath) =>
50
+ existsBy({ item, filePath }, importedItems);
51
+
52
+ const findImports = filePath => {
53
+ const code = fs.readFileSync(filePath, "utf8");
54
+ const ast = babelParser.parse(code, {
55
+ sourceType: "module",
56
+ plugins: ["jsx"],
57
+ });
58
+
59
+ traverse(ast, {
60
+ ImportDeclaration(path) {
61
+ const { node } = path;
62
+
63
+ const currentDir = pathLib.dirname(filePath);
64
+ const importSource = node.source.value;
65
+
66
+ const resolvedPath = resolver(currentDir, importSource);
67
+ const isNodeModule = resolvedPath.includes("node_modules");
68
+ if (isNodeModule) return;
69
+
70
+ if (isEmpty(node.specifiers)) {
71
+ importedItems.push({ item: "*", filePath: resolvedPath });
72
+ }
73
+
74
+ node.specifiers.forEach(specifier => {
75
+ if (specifier.type === TYPES.IMPORT_NAMESPACE_SPECIFIER) {
76
+ const binding = path.scope.getBinding(specifier.local.name);
77
+ binding?.referencePaths?.forEach(refPath => {
78
+ const item = refPath.parentPath?.node?.property?.name;
79
+ importedItems.push({ item, filePath: resolvedPath });
80
+ });
81
+ } else if (specifier.type === TYPES.IMPORT_DEFAULT_SPECIFIER) {
82
+ importedItems.push({ item: "default", filePath: resolvedPath });
83
+ } else {
84
+ const item = specifier.imported.name;
85
+ importedItems.push({ item, filePath: resolvedPath });
86
+ }
87
+ });
88
+ },
89
+ });
90
+ };
91
+
92
+ const removeUnusedExports = filePath => {
93
+ if (isRailsMountedOrPacksFiles(filePath)) return;
94
+
95
+ if (!existsBy({ filePath }, importedItems)) {
96
+ unusedFiles.push(filePath);
97
+
98
+ return;
99
+ }
100
+
101
+ let isPathModified = false;
102
+ const code = fs.readFileSync(filePath, "utf8");
103
+ const ast = babelParser.parse(code, {
104
+ sourceType: "module",
105
+ plugins: ["jsx"],
106
+ });
107
+
108
+ traverse(ast, {
109
+ ExportNamedDeclaration(path) {
110
+ const { node } = path;
111
+ if (node.declaration) {
112
+ if (node.declaration.type === TYPES.VARIABLE_DECLARATION) {
113
+ node.declaration.declarations.forEach(declaration => {
114
+ const exportedItem = declaration.id.name;
115
+ if (isInImportedList(exportedItem, filePath)) return;
116
+
117
+ path.replaceWith(path.node.declaration);
118
+ isPathModified = true;
119
+ });
120
+ } else if (
121
+ node.declaration.type === TYPES.FUNCTION_DECLARATION ||
122
+ node.declaration.type === TYPES.CLASS_DECLARATION
123
+ ) {
124
+ const exportedItem = node.declaration.id.name;
125
+ if (isInImportedList(exportedItem, filePath)) return;
126
+
127
+ path.replaceWith(path.node.declaration);
128
+ isPathModified = true;
129
+ }
130
+ } else {
131
+ path.get("specifiers").forEach(specifierPath => {
132
+ const exportedItem = specifierPath.node.exported.name;
133
+ if (isInImportedList(exportedItem, filePath)) return;
134
+
135
+ node.specifiers.length === 1 ? path.remove() : specifierPath.remove();
136
+ isPathModified = true;
137
+ });
138
+ }
139
+ },
140
+ ExportDefaultDeclaration(path) {
141
+ const exportedItem = "default";
142
+ if (isInImportedList(exportedItem, filePath)) return;
143
+
144
+ path.remove();
145
+ isPathModified = true;
146
+ },
147
+ });
148
+
149
+ if (!isPathModified) return;
150
+
151
+ const newCode = generate(ast, {
152
+ retainLines: true,
153
+ retainFunctionParens: true,
154
+ }).code;
155
+
156
+ fs.writeFileSync(filePath, newCode);
157
+ modifiedFiles.push(filePath);
158
+ };
159
+
160
+ const traverseDirectoryAndExecuteFunc = (dir, func) => {
161
+ const files = fs.readdirSync(dir);
162
+
163
+ files.forEach(file => {
164
+ const filePath = pathLib.join(dir, file);
165
+ const stats = fs.statSync(filePath);
166
+
167
+ if (stats.isFile() && [".js", ".jsx"].includes(pathLib.extname(filePath))) {
168
+ func(filePath);
169
+ } else if (stats.isDirectory()) {
170
+ traverseDirectoryAndExecuteFunc(filePath, func);
171
+ }
172
+ });
173
+ };
174
+
175
+ const runLintersAndRemoveUnusedFiles = () => {
176
+ const commands = [];
177
+
178
+ if (isNotEmpty(unusedFiles)) commands.push(`rm -f ${unusedFiles.join(" ")}`);
179
+
180
+ if (isNotEmpty(modifiedFiles)) {
181
+ commands.push(`npx prettier --write ${modifiedFiles.join(" ")}`);
182
+ commands.push(`npx eslint --fix ${modifiedFiles.join(" ")}`);
183
+ }
184
+
185
+ // script will be executed recursively until we have no modified files or unused files
186
+ exec(commands.join(" && "), () => run());
187
+ };
188
+
189
+ const restoreEslintNeetoVersion = () => {
190
+ execSync("git restore package.json yarn.lock", { stdio: "ignore" });
191
+ execSync("yarn", { stdio: "ignore" });
192
+ };
193
+
194
+ const reportNonFixableEslintErrors = () => {
195
+ const modifiedFiles = execSync("git diff --name-only --diff-filter=M")
196
+ .toString()
197
+ .split("\n")
198
+ .join(" ");
199
+
200
+ if (!modifiedFiles) {
201
+ console.log("✅ Script executed successfully!\n");
202
+
203
+ return;
204
+ }
205
+
206
+ exec(`npx eslint ${modifiedFiles} --quiet`, (_, stdout) => {
207
+ if (stdout) {
208
+ console.log(stdout);
209
+ console.log(
210
+ "⭕️ Auto-fixing is not implemented for the above eslint errors. Please fix them manually.\n"
211
+ );
212
+ } else {
213
+ console.log("- No errors found.\n\n✅ Script executed successfully!\n");
214
+ }
215
+ });
216
+ };
217
+
218
+ const postExecution = () => {
219
+ console.log("\n4. Restoring @bigbinary/eslint-plugin-neeto version.");
220
+ restoreEslintNeetoVersion();
221
+
222
+ console.log("\n5. Checking if there are any non-fixable eslint errors.");
223
+ reportNonFixableEslintErrors();
224
+ };
225
+
226
+ const installCustomEslintNeetoVersion = () =>
227
+ execSync(
228
+ "yarn add @bigbinary/eslint-plugin-neeto@1.0.43-no-unused-vars-autofix",
229
+ { stdio: "ignore" }
230
+ );
231
+
232
+ const run = () => {
233
+ console.log(`\nIteration: ${iterationCount++}`);
234
+ console.log(".............");
235
+
236
+ modifiedFiles = [];
237
+ importedItems = [];
238
+ unusedFiles = [];
239
+
240
+ console.log("* Collecting all the imports.");
241
+ traverseDirectoryAndExecuteFunc(javascriptDir, findImports);
242
+
243
+ console.log("* Removing exports which are not imported.");
244
+ traverseDirectoryAndExecuteFunc(javascriptDir, removeUnusedExports);
245
+
246
+ const filesCountMsg = `\nModified files: ${modifiedFiles.length}\nUnused files: ${unusedFiles.length}`;
247
+
248
+ const isExecutionCompleted = isEmpty(modifiedFiles) && isEmpty(unusedFiles);
249
+ if (isExecutionCompleted) {
250
+ console.log(filesCountMsg);
251
+ postExecution();
252
+ } else {
253
+ console.log(
254
+ "* Running linters on modified files and removing unused files."
255
+ );
256
+ runLintersAndRemoveUnusedFiles();
257
+ console.log(filesCountMsg);
258
+ }
259
+ };
260
+
261
+ // Script execution starts here
262
+ console.log("\nPlease wait, this may take a few minutes...");
263
+
264
+ console.log(
265
+ "\n1. Installing @bigbinary/eslint-plugin-neeto custom release required for this script."
266
+ );
267
+ installCustomEslintNeetoVersion();
268
+
269
+ console.log(
270
+ "\n2. Executing script recursively until we have no modified or unused files."
271
+ );
272
+ run();
@@ -0,0 +1,11 @@
1
+ const I18NEXT_PLURALS = ["zero", "one", "two", "few", "many", "other"];
2
+
3
+ /*
4
+ Matches the following:
5
+ - t(`some.${key}`), t(key), t(getKey()), t(key, { count: 1 })
6
+ - i18nKey={`some.${key}`}, i18nKey={key}, i18nKey={getKey()}
7
+ */
8
+ const INTERPOLATED_TRANSLATION_KEY_REGEX =
9
+ /(?<![a-z0-9])t\((?:`.*`|[a-z0-9\-_()]*)(?:,.*)?\)|i18nKey={(`.*`|[a-z0-9-_()]*)}/gi;
10
+
11
+ module.exports = { I18NEXT_PLURALS, INTERPOLATED_TRANSLATION_KEY_REGEX };
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const { Command } = require("commander");
8
+ const { isEmpty, path: rPath } = require("ramda");
9
+
10
+ const {
11
+ I18NEXT_PLURALS,
12
+ INTERPOLATED_TRANSLATION_KEY_REGEX,
13
+ } = require("./constants");
14
+
15
+ const isI18nextPlural = key =>
16
+ I18NEXT_PLURALS.some(pluralKey => key.endsWith(`_${pluralKey}`));
17
+
18
+ const logUnusedKeysAndInterpolatedKeys = (
19
+ unusedTranslationKeys,
20
+ transFuncWithInterpolatedKeys
21
+ ) => {
22
+ console.log("\nUnused translation keys");
23
+ console.log("-----------------------");
24
+
25
+ if (isEmpty(unusedTranslationKeys)) {
26
+ console.log("No unused keys found.");
27
+ } else {
28
+ const unusedKeyCount = unusedTranslationKeys.length;
29
+ unusedTranslationKeys.forEach(key => console.log(key));
30
+ console.log(`\nDeleted ${unusedKeyCount} unused keys successfully.`);
31
+ }
32
+
33
+ console.log("\n\nTranslation functions with interpolated keys");
34
+ console.log("--------------------------------------------");
35
+
36
+ if (isEmpty(transFuncWithInterpolatedKeys)) {
37
+ console.log("No translation functions with interpolated keys found.\n");
38
+ } else {
39
+ transFuncWithInterpolatedKeys.forEach(hash => {
40
+ console.log(`Code: ${hash.code}`);
41
+ console.log(`File path: ${hash.filePath}\n`);
42
+ });
43
+
44
+ console.log(
45
+ "Please verify whether the keys required by the above translation functions have been removed or not. If removed, please restore them.\n"
46
+ );
47
+ }
48
+ };
49
+
50
+ const deleteUnusedKeysFromData = (translationData, unusedTranslationKeys) => {
51
+ unusedTranslationKeys.forEach(translationKey => {
52
+ const keys = translationKey.split(".");
53
+ const lastKey = keys.pop();
54
+
55
+ if (isEmpty(keys)) {
56
+ delete translationData[lastKey];
57
+ } else {
58
+ const currentData = rPath(keys, translationData);
59
+ if (currentData) delete currentData[lastKey];
60
+ }
61
+ });
62
+ };
63
+
64
+ const deleteEmptyValuesFromData = translationData => {
65
+ for (const [key, value] of Object.entries(translationData)) {
66
+ if (typeof value === "object") {
67
+ if (isEmpty(value)) delete translationData[key];
68
+ else deleteEmptyValuesFromData(value);
69
+ }
70
+ }
71
+ };
72
+
73
+ const deleteUsedKeysFromKeysList = (dirPath, keys) => {
74
+ fs.readdirSync(dirPath).forEach(fileName => {
75
+ const filePath = path.join(dirPath, fileName);
76
+ if (fs.statSync(filePath).isFile()) {
77
+ const extension = path.extname(filePath);
78
+ if (![".js", ".jsx", ".rb", ".jbuilder"].includes(extension)) return;
79
+
80
+ const data = fs.readFileSync(filePath, "utf8");
81
+ for (let i = 0; i < keys.length; i++) {
82
+ const key = keys[i];
83
+ const isKeyFound =
84
+ data.includes(key) ||
85
+ (isI18nextPlural(key) && data.includes(key.replace(/_[^_]*$/, "")));
86
+
87
+ if (isKeyFound) {
88
+ keys.splice(i, 1);
89
+ i--;
90
+ }
91
+ }
92
+ } else if (fs.statSync(filePath).isDirectory()) {
93
+ deleteUsedKeysFromKeysList(filePath, keys);
94
+ }
95
+ });
96
+ };
97
+
98
+ const getUnusedKeys = (searchPath, translationKeys) => {
99
+ const keys = [...translationKeys];
100
+ deleteUsedKeysFromKeysList(searchPath, keys);
101
+
102
+ return keys;
103
+ };
104
+
105
+ const findTransFuncWithInterpolatedKeys = dirPath => {
106
+ const transFuncWithInterpolatedKeys = [];
107
+
108
+ fs.readdirSync(dirPath).forEach(fileName => {
109
+ const filePath = path.join(dirPath, fileName);
110
+ if (fs.statSync(filePath).isFile()) {
111
+ const extension = path.extname(filePath);
112
+ if (![".js", ".jsx"].includes(extension)) return;
113
+
114
+ const data = fs.readFileSync(filePath, "utf8");
115
+ const relativePath = path.relative(process.cwd(), filePath);
116
+ const matchedCodes = data.match(INTERPOLATED_TRANSLATION_KEY_REGEX);
117
+
118
+ matchedCodes?.forEach(code => {
119
+ transFuncWithInterpolatedKeys.push({
120
+ code,
121
+ filePath: relativePath,
122
+ });
123
+ });
124
+ } else if (fs.statSync(filePath).isDirectory()) {
125
+ transFuncWithInterpolatedKeys.push(
126
+ ...findTransFuncWithInterpolatedKeys(filePath)
127
+ );
128
+ }
129
+ });
130
+
131
+ return transFuncWithInterpolatedKeys;
132
+ };
133
+
134
+ const generateTranslationKeys = (data, parentKey = null) => {
135
+ const translationKeys = [];
136
+
137
+ for (const [key, value] of Object.entries(data)) {
138
+ const currentKey = parentKey ? `${parentKey}.${key}` : key;
139
+
140
+ if (typeof value === "object") {
141
+ translationKeys.push(...generateTranslationKeys(value, currentKey));
142
+ } else {
143
+ translationKeys.push(currentKey);
144
+ }
145
+ }
146
+
147
+ return translationKeys;
148
+ };
149
+
150
+ const getCommandLineOptions = () => {
151
+ const program = new Command();
152
+ program
153
+ .option("--translation-path <path>", "Path to translation file")
154
+ .option("--search-path <path>", "Path to search for unused keys")
155
+ .parse(process.argv);
156
+
157
+ return program.opts();
158
+ };
159
+
160
+ // Script execution starts here
161
+ const {
162
+ translationPath = "app/javascript/src/translations/en.json",
163
+ searchPath = "app",
164
+ } = getCommandLineOptions();
165
+
166
+ console.log("\nPlease wait, this may take a few seconds...");
167
+
168
+ const translationData = JSON.parse(fs.readFileSync(translationPath, "utf8"));
169
+ const translationKeys = generateTranslationKeys(translationData);
170
+ const unusedTranslationKeys = getUnusedKeys(searchPath, translationKeys);
171
+
172
+ deleteUnusedKeysFromData(translationData, unusedTranslationKeys);
173
+ deleteEmptyValuesFromData(translationData);
174
+
175
+ fs.writeFileSync(
176
+ translationPath,
177
+ `${JSON.stringify(translationData, null, 2)}\n`
178
+ );
179
+
180
+ const transFuncWithInterpolatedKeys =
181
+ findTransFuncWithInterpolatedKeys(searchPath);
182
+
183
+ logUnusedKeysAndInterpolatedKeys(
184
+ unusedTranslationKeys,
185
+ transFuncWithInterpolatedKeys
186
+ );
@@ -20,20 +20,6 @@ module.exports = [
20
20
  },
21
21
  },
22
22
  },
23
- {
24
- test: /\.mp3$/,
25
- use: [
26
- "babel-loader",
27
- {
28
- loader: "file-loader",
29
- options: {
30
- query: {
31
- name: "static/media/[name].[hash:8].[ext]",
32
- },
33
- },
34
- },
35
- ],
36
- },
37
23
  {
38
24
  test: /\.m?js$/,
39
25
  resolve: {
@@ -6,6 +6,11 @@ declare namespace Cypress {
6
6
  typeFast(selector: string, text: string): Void;
7
7
  typeAndEnter(selector: string, text: string): Void;
8
8
  verifyToastMessage(message: string): Void;
9
+
10
+ /**
11
+ * @param closeToastr Default value is true
12
+ */
13
+ verifyToastIcon(closeToastr: boolean): Void;
9
14
  continueOnAlert(
10
15
  alias:
11
16
  | string
@@ -40,8 +45,19 @@ declare namespace Cypress {
40
45
  clickDropdownOption(optionText: string, dropdownSelector: string): Void;
41
46
  getText(selector: string): Chainable<JQuery<HTMLElement>>;
42
47
  getValue(selector: string): Chainable<JQuery<HTMLElement>>;
48
+
49
+ /**
50
+ * @deprecated Use `withinIframe` instead.
51
+ */
43
52
  getIframe(iframeSelector: string): Chainable<JQuery<HTMLElement>>;
44
53
 
54
+ /**
55
+ * Selects and performs commands within an iframe element based on the given selector.
56
+ * @param iframeSelector The selector to locate the iframe element.
57
+ * @param callbackFn The callback function containing the commands to execute within the iframe.
58
+ */
59
+ withinIframe(iframeSelector: string, callbackFn: void): Void;
60
+
45
61
  /**
46
62
  * Command to open the url in the same window after clicking on a button that opens the url in a new tab
47
63
  */
@@ -76,9 +92,9 @@ declare namespace Cypress {
76
92
  navigateToPreviewPage(selector: string, requestCount: number): Void;
77
93
  saveChanges(aliasName: string, requestCount: number): Void;
78
94
  verifyAttribute(props: {
79
- selector: string,
80
- attr: string,
81
- value: string
95
+ selector: string;
96
+ attr: string;
97
+ value: string;
82
98
  }): Void;
83
99
  verifyUnsavedAlertModal(cancelButton: string): Void;
84
100
  verifyTooltip(selector: string, text: string): Void;