Skip to content
Draft
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require('eslint-plugin-escompat');

module.exports = {
parser: '@babel/eslint-parser',
env : {
Expand Down
11,132 changes: 5,683 additions & 5,449 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"livereload": "grunt livereload",
"serve": "grunt serve",
"start": "grunt start",
"eslint": "eslint --fix \"src/**/*.jsx\" src/"
"eslint": "eslint --fix \"src/**/*.jsx\" src/",
"optimize-svgs": "node scripts/optimize-svgs.js"
},
"repository": {
"type": "git",
Expand All @@ -21,8 +22,8 @@
"homepage": "https://github.com/deriv-com/smarttrader",
"devDependencies": {
"@babel/core": "7.24.0",
"@babel/eslint-parser": "7.23.10",
"@babel/parser": "7.25.0",
"@babel/eslint-parser": "^7.23.10",
"@babel/parser": "^7.25.0",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.24.7",
Expand Down Expand Up @@ -53,6 +54,7 @@
"eslint-config-binary": "1.0.2",
"eslint-config-prettier": "2.9.0",
"eslint-import-resolver-alias": "1.1.1",
"eslint-plugin-escompat": "^3.11.4",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-react": "7.28.0",
"estraverse": "4.2.0",
Expand All @@ -67,7 +69,7 @@
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-cssmin": "5.0.0",
"grunt-contrib-watch": "1.1.0",
"grunt-eslint": "24.3.0",
"grunt-eslint": "^24.3.0",
"grunt-gh-pages": "4.0.0",
"grunt-hashres": "^0.4.1",
"grunt-mocha-test": "0.13.3",
Expand Down Expand Up @@ -142,8 +144,8 @@
},
"overrides": {
"braces": "^3.0.3",
"@babel/parser": "7.25.0",
"@babel/plugin-proposal-optional-chaining": "7.22.5"
"@babel/plugin-proposal-optional-chaining": "7.22.5",
"browserslist": "4.24.4"
},
"engines": {
"node": "18.x"
Expand Down
44 changes: 42 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ In order to make sure that no strings are missed by the extractor code while pus
1. Refactor the code so that the first argument passed to the `localize()` method is a string literal.
i.e.
```js
const text = localize(is_started ? 'Sell at market' : 'Sell');
const text = localize(is_started ? "Sell at market" : "Sell");
```
would change to:
```js
const text = is_started ? localize('Sell at market') : localize('Sell');
const text = is_started ? localize("Sell at market") : localize("Sell");
```
2. If there is no way to have the string literal in js code (i.e. API texts which are not translated), add them to `scripts/js_texts/static_strings_app.js`.

Expand All @@ -66,3 +66,43 @@ During the translation update process, the source file `messages.pot` will be up

- The list of paths to include in `sitemap.xml` is here: [config/sitemap_urls.js](config/sitemap_urls.js)
- Once the paths are updated in the above file, run `./scripts/sitemap.js` or `grunt shell:sitemap` to generate new `sitemap.xml` files in `src/root_files` according to each section.

## SVG Optimization

The project enforces that all SVG files follow a specific format to ensure optimal performance and compatibility. The test `should be valid svgs` in `scripts/__tests__/svg_test.js` checks that SVG files are properly formatted.

### SVG Validation Rules

- SVGs should be on a single line (not multi-line)
- Must have proper opening `<svg>` and closing `</svg>` tags
- Must follow the pattern: `/(?!\n)(<svg)(.*)(>).*(<\/\s?svg)>/i`

### How to Optimize SVGs

If you have SVG files that don't pass the validation, use the `optimize-svgs.js` script to fix them:

```bash
# Using npm script
npm run optimize-svgs

# Or directly
node scripts/optimize-svgs.js
```

The script:

1. Identifies all unoptimized SVG files in the project
2. Asks for confirmation before optimization
3. Uses SVGO (SVG Optimizer) to optimize the files
4. Reports results

### Ignoring SVGs

If specific SVG files should be excluded from optimization for valid reasons, add them to the `ignored_files` array in:

- `scripts/__tests__/svg_test.js`
- `scripts/optimize-svgs.js`

Currently ignored files:

- `src/images/pages/regulation/map.svg`
95 changes: 74 additions & 21 deletions scripts/__tests__/svg_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,88 @@ let changed_files = [];

describe('check svg file format', () => {
const fetchFiles = async (command) => {
const { stdout, stderr } = await exec(command);
if (stderr) {
throw new Error(stderr);
try {
const { stdout, stderr } = await exec(command);
if (stderr && !stderr.includes('Not a git repository')) {
throw new Error(stderr);
}
return stdout.split('\n').filter(dir => dir.length);
} catch (err) {
// If git command fails, it might be a non-git environment (e.g. CI)
// Just return an empty array in that case
if (err.message.includes('Not a git repository')) {
return [];
}
throw err;
}

return stdout.split('\n').filter(dir => dir.length);
};

it('should be valid svgs', async () => {
// Increase timeout to 10 seconds to give git operations more time
it('should be valid svgs', async function() {
this.timeout(10000); // 10 seconds

try {
await exec('git fetch origin master --depth 1');
changed_files = [
...await fetchFiles('git diff --name-only -- *.svg'),
...await fetchFiles('git diff HEAD origin/master --name-only -- *.svg'),
];
} catch (err) {
console.error(err);
}
// Get all changed SVG files using git
// This might fail if not in a git repo, but we'll handle that
try {
await exec('git fetch origin master --depth 1');
changed_files = [
...await fetchFiles('git diff --name-only -- *.svg'),
...await fetchFiles('git diff HEAD origin/master --name-only -- *.svg'),
];
} catch (err) {
// If git commands fail, fallback to checking all SVG files
if (err.message.includes('Not a git repository')) {
// Find all SVG files in the project
const { stdout } = await exec('find src -name "*.svg" 2>/dev/null');
changed_files = stdout.split('\n').filter(dir => dir.length);
} else {
expect.fail(`Error fetching changed files: ${err.message}`);
}
}

// If no files were found through git, find all SVGs in source directory
if (changed_files.length === 0) {
const { stdout } = await exec('find src -name "*.svg" 2>/dev/null');
changed_files = stdout.split('\n').filter(dir => dir.length);
}

changed_files.filter(item =>
!ignored_files.some(ignored => path.resolve(common.root_path, ignored) === item) &&
fs.existsSync(path.resolve(item)))
.forEach(item => {
// Filter files and check each one
const files_to_check = changed_files.filter(item =>
!ignored_files.some(ignored => path.resolve(common.root_path, ignored) === item) &&
fs.existsSync(path.resolve(item))
);

if (files_to_check.length === 0) {
// Skip test if no applicable files found
this.skip();
return;
}

const unoptimized_files = [];

files_to_check.forEach(item => {
const stats = fs.statSync(path.resolve(item));
if (stats.isSymbolicLink()) return;

const file = fs.readFileSync(path.resolve(item), 'utf-8');
expect(file, `Unoptimized svg at ${item}\n Please run the following command on your terminal and commit the result: \n svgo ${item} \n`)
.to
.match(/(?!\n)(<svg)(.*)(>).*(<\/\s?svg)>/i);

if (!(/(?!\n)(<svg)(.*)(>).*(<\/\s?svg)>/i).test(file)) {
unoptimized_files.push(item);
}
});

if (unoptimized_files.length > 0) {
const file_list = unoptimized_files.map(f => `- ${f}`).join('\n');
const error_message = `The following SVG files need to be optimized:\n${file_list}\n\nPlease run the following command to optimize these files:\nnode scripts/optimize-svgs.js\n`;

expect.fail(error_message);
}
} catch (err) {
if (err.name !== 'AssertionError') {
expect.fail(`Unexpected error: ${err.message}`);
}
throw err;
}
});
});
102 changes: 102 additions & 0 deletions scripts/optimize-svgs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node
/* eslint-disable no-console */

/**
* This script finds and optimizes SVG files that don't match the required format.
* It helps fix the "should be valid svgs" test failures.
*/

const fs = require('fs');
const util = require('util');
const exec = util.promisify(require('child_process').exec);

const ignored_files = [
'src/images/pages/regulation/map.svg',
];

const findSvgFiles = async () => {
try {
// Find all SVG files in the project
const { stdout } = await exec('find . -name "*.svg" -not -path "*/node_modules/*" -not -path "*/\\.git/*"');
return stdout.split('\n').filter(filepath => filepath.length);
} catch (err) {
console.error('Error finding SVG files:', err.message);
return [];
}
};

const optimizeSvg = async (filepath) => {
try {
console.log(`Optimizing: ${filepath}`);
await exec(`npx svgo "${filepath}"`);
return true;
} catch (err) {
console.error(`Failed to optimize ${filepath}:`, err.message);
return false;
}
};

const isSvgOptimized = (content) => /(?!\n)(<svg)(.*)(>).*(<\/\s?svg)>/i.test(content);

const main = async () => {
const files = await findSvgFiles();
const unoptimized_files = [];

files.forEach(filepath => {
// Skip ignored files
if (ignored_files.some(ignored => filepath.includes(ignored))) {
return;
}

try {
const stats = fs.statSync(filepath);
if (stats.isSymbolicLink()) return;

const content = fs.readFileSync(filepath, 'utf-8');
if (!isSvgOptimized(content)) {
unoptimized_files.push(filepath);
}
} catch (err) {
console.error(`Error processing ${filepath}:`, err.message);
}
});

if (unoptimized_files.length === 0) {
console.log('All SVG files are already optimized!');
return;
}

console.log(`Found ${unoptimized_files.length} unoptimized SVG file(s):`);
unoptimized_files.forEach(file => console.log(`- ${file}`));

const answer = await promptYesNo('\nWould you like to optimize these files now? (y/n) ');
if (answer) {
console.log('\nOptimizing SVG files...');
const results = await Promise.all(
unoptimized_files.map(async file => ({
file,
success: await optimizeSvg(file),
}))
);

const success_count = results.filter(r => r.success).length;
console.log(`\nSuccessfully optimized ${success_count} of ${unoptimized_files.length} file(s).`);
}
};

const promptYesNo = async (question) => {
process.stdout.write(question);

return new Promise(resolve => {
process.stdin.once('data', (data) => {
const answer = data.toString().trim().toLowerCase();
resolve(answer === 'y' || answer === 'yes');
});
});
};

// Run the script
main().catch(err => {
console.error('Unexpected error:', err);
process.exit(1);
});
Loading
Loading