From b85fcba41a2debeb875a7e04ab0baf0729d438b3 Mon Sep 17 00:00:00 2001 From: Sheng Hau Date: Fri, 31 May 2024 18:11:57 +0800 Subject: [PATCH] chore: typescript migration (#345) * chore: move code into /src - add npm scripts to build and start - add tsconfig.json * fix: relative paths * chore: update markdown docs * fix: move config and license files out from src * fix: npm lint * chore: rename all .js files to .ts * fix: move Dockerfile and gitlab file out of src * update package json copyfiles and typescript dev dependency --------- Co-authored-by: Joshua Lai Co-authored-by: younglim --- .gitignore | 1 + INTEGRATION.md | 2 +- README.md | 37 +- .../{mockFunctions.js => mockFunctions.ts} | 0 __mocks__/{mockIssues.js => mockIssues.ts} | 0 __tests__/{logs.test.js => logs.test.ts} | 0 ...esults.test.js => mergeAxeResults.test.ts} | 0 __tests__/{utils.test.js => utils.test.ts} | 0 exclusions.txt | 1 - gitlab-pipeline-template.yml | 2 +- package-lock.json | 56 +- package.json | 16 +- scripts/verapdf-auto-install-macos.xml | 16 - scripts/verapdf-auto-install-windows.xml | 16 - cli.js => src/cli.ts | 58 +- combine.js => src/combine.ts | 0 .../constants/__tests__/common.test.ts | 0 {constants => src/constants}/axeTypes.json | 0 .../constants/cliFunctions.ts | 20 +- .../common.js => src/constants/common.ts | 3402 +++++++++-------- .../constants/constants.ts | 857 +++-- {constants => src/constants}/errorMeta.json | 0 .../constants/itemTypeDescription.ts | 0 .../purpleAi.js => src/constants/purpleAi.ts | 0 .../constants/questions.ts | 0 .../constants/sampleData.ts | 0 {constants => src/constants}/wcagLinks.json | 0 .../__tests__/commonCrawlerFunc.test.ts | 0 .../crawlers/commonCrawlerFunc.ts | 0 .../crawlers/crawlDomain.ts | 0 .../crawlers/crawlIntelligentSitemap.ts | 0 .../crawlers/crawlSitemap.ts | 0 .../utils.js => src/crawlers/custom/utils.ts | 0 .../crawlers/pdfScanFunc.ts | 0 .../runCustom.js => src/crawlers/runCustom.ts | 0 index.js => src/index.ts | 64 +- logs.js => src/logs.ts | 0 mergeAxeResults.js => src/mergeAxeResults.ts | 0 npmIndex.js => src/npmIndex.ts | 0 .../playwrightAxeGenerator.ts | 1896 ++++----- .../runCustomFlowFromGUI.ts | 0 .../screenshotFunc/htmlScreenshotFunc.ts | 0 .../screenshotFunc/pdfScreenshotFunc.ts | 2 +- {scripts => src/scripts}/a11y_shell.cmd | 0 {scripts => src/scripts}/a11y_shell.command | 0 {scripts => src/scripts}/a11y_shell.sh | 0 {scripts => src/scripts}/a11y_shell_ps.ps1 | 108 +- .../scripts}/install_purple_dependencies.cmd | 0 .../install_purple_dependencies.command | 0 .../scripts}/install_purple_dependencies.ps1 | 0 .../partials/components/categorySelector.ejs | 0 .../components/categorySelectorDropdown.ejs | 0 .../partials/components/pagesScannedModal.ejs | 0 .../ejs/partials/components/reportSearch.ejs | 0 .../ejs/partials/components/ruleOffcanvas.ejs | 0 .../ejs/partials/components/scanAbout.ejs | 0 .../components/screenshotLightbox.ejs | 0 .../partials/components/summaryScanAbout.ejs | 0 .../components/summaryScanResults.ejs | 0 .../ejs/partials/components/summaryTable.ejs | 0 .../components/summaryWcagCompliance.ejs | 0 .../ejs/partials/components/topFive.ejs | 0 .../partials/components/wcagCompliance.ejs | 0 .../static}/ejs/partials/footer.ejs | 0 .../static}/ejs/partials/header.ejs | 0 {static => src/static}/ejs/partials/main.ejs | 0 .../ejs/partials/scripts/bootstrap.ejs | 0 .../categorySelectorDropdownScript.ejs | 0 .../ejs/partials/scripts/categorySummary.ejs | 0 .../ejs/partials/scripts/highlightjs.ejs | 0 .../static}/ejs/partials/scripts/popper.ejs | 0 .../ejs/partials/scripts/reportSearch.ejs | 0 .../ejs/partials/scripts/ruleOffcanvas.ejs | 0 .../partials/scripts/screenshotLightbox.ejs | 0 .../partials/scripts/summaryScanResults.ejs | 0 .../ejs/partials/scripts/summaryTable.ejs | 0 .../static}/ejs/partials/scripts/utils.ejs | 0 .../static}/ejs/partials/styles/bootstrap.ejs | 0 .../ejs/partials/styles/highlightjs.ejs | 0 .../static}/ejs/partials/styles/styles.ejs | 0 .../ejs/partials/styles/summaryBootstrap.ejs | 0 .../static}/ejs/partials/summaryHeader.ejs | 0 .../static}/ejs/partials/summaryMain.ejs | 0 {static => src/static}/ejs/report.ejs | 0 {static => src/static}/ejs/summary.ejs | 0 {static => src/static}/mustache/.prettierrc | 0 .../mustache/Attention Deficit.mustache | 0 .../static}/mustache/Blind.mustache | 0 .../static}/mustache/Cognitive.mustache | 0 .../static}/mustache/Colorblindness.mustache | 0 {static => src/static}/mustache/Deaf.mustache | 0 .../static}/mustache/Deafblind.mustache | 0 .../static}/mustache/Dyslexia.mustache | 0 .../static}/mustache/Low Vision.mustache | 0 .../static}/mustache/Mobility.mustache | 0 .../mustache/Sighted Keyboard Users.mustache | 0 .../static}/mustache/report.mustache | 0 src/types/print-message.d.ts | 28 + utils.js => src/utils.ts | 2 +- tsconfig.json | 11 + 100 files changed, 3370 insertions(+), 3225 deletions(-) rename __mocks__/{mockFunctions.js => mockFunctions.ts} (100%) rename __mocks__/{mockIssues.js => mockIssues.ts} (100%) rename __tests__/{logs.test.js => logs.test.ts} (100%) rename __tests__/{mergeAxeResults.test.js => mergeAxeResults.test.ts} (100%) rename __tests__/{utils.test.js => utils.test.ts} (100%) delete mode 100644 exclusions.txt delete mode 100644 scripts/verapdf-auto-install-macos.xml delete mode 100644 scripts/verapdf-auto-install-windows.xml rename cli.js => src/cli.ts (90%) rename combine.js => src/combine.ts (100%) rename constants/__tests__/common.test.js => src/constants/__tests__/common.test.ts (100%) rename {constants => src/constants}/axeTypes.json (100%) rename constants/cliFunctions.js => src/constants/cliFunctions.ts (87%) rename constants/common.js => src/constants/common.ts (84%) rename constants/constants.js => src/constants/constants.ts (91%) rename {constants => src/constants}/errorMeta.json (100%) rename constants/itemTypeDescription.js => src/constants/itemTypeDescription.ts (100%) rename constants/purpleAi.js => src/constants/purpleAi.ts (100%) rename constants/questions.js => src/constants/questions.ts (100%) rename constants/sampleData.js => src/constants/sampleData.ts (100%) rename {constants => src/constants}/wcagLinks.json (100%) rename crawlers/__tests__/commonCrawlerFunc.test.js => src/crawlers/__tests__/commonCrawlerFunc.test.ts (100%) rename crawlers/commonCrawlerFunc.js => src/crawlers/commonCrawlerFunc.ts (100%) rename crawlers/crawlDomain.js => src/crawlers/crawlDomain.ts (100%) rename crawlers/crawlIntelligentSitemap.js => src/crawlers/crawlIntelligentSitemap.ts (100%) rename crawlers/crawlSitemap.js => src/crawlers/crawlSitemap.ts (100%) rename crawlers/custom/utils.js => src/crawlers/custom/utils.ts (100%) rename crawlers/pdfScanFunc.js => src/crawlers/pdfScanFunc.ts (100%) rename crawlers/runCustom.js => src/crawlers/runCustom.ts (100%) rename index.js => src/index.ts (68%) rename logs.js => src/logs.ts (100%) rename mergeAxeResults.js => src/mergeAxeResults.ts (100%) rename npmIndex.js => src/npmIndex.ts (100%) rename playwrightAxeGenerator.js => src/playwrightAxeGenerator.ts (97%) rename runCustomFlowFromGUI.js => src/runCustomFlowFromGUI.ts (100%) rename screenshotFunc/htmlScreenshotFunc.js => src/screenshotFunc/htmlScreenshotFunc.ts (100%) rename screenshotFunc/pdfScreenshotFunc.js => src/screenshotFunc/pdfScreenshotFunc.ts (99%) rename {scripts => src/scripts}/a11y_shell.cmd (100%) rename {scripts => src/scripts}/a11y_shell.command (100%) rename {scripts => src/scripts}/a11y_shell.sh (100%) rename {scripts => src/scripts}/a11y_shell_ps.ps1 (96%) rename {scripts => src/scripts}/install_purple_dependencies.cmd (100%) rename {scripts => src/scripts}/install_purple_dependencies.command (100%) rename {scripts => src/scripts}/install_purple_dependencies.ps1 (100%) rename {static => src/static}/ejs/partials/components/categorySelector.ejs (100%) rename {static => src/static}/ejs/partials/components/categorySelectorDropdown.ejs (100%) rename {static => src/static}/ejs/partials/components/pagesScannedModal.ejs (100%) rename {static => src/static}/ejs/partials/components/reportSearch.ejs (100%) rename {static => src/static}/ejs/partials/components/ruleOffcanvas.ejs (100%) rename {static => src/static}/ejs/partials/components/scanAbout.ejs (100%) rename {static => src/static}/ejs/partials/components/screenshotLightbox.ejs (100%) rename {static => src/static}/ejs/partials/components/summaryScanAbout.ejs (100%) rename {static => src/static}/ejs/partials/components/summaryScanResults.ejs (100%) rename {static => src/static}/ejs/partials/components/summaryTable.ejs (100%) rename {static => src/static}/ejs/partials/components/summaryWcagCompliance.ejs (100%) rename {static => src/static}/ejs/partials/components/topFive.ejs (100%) rename {static => src/static}/ejs/partials/components/wcagCompliance.ejs (100%) rename {static => src/static}/ejs/partials/footer.ejs (100%) rename {static => src/static}/ejs/partials/header.ejs (100%) rename {static => src/static}/ejs/partials/main.ejs (100%) rename {static => src/static}/ejs/partials/scripts/bootstrap.ejs (100%) rename {static => src/static}/ejs/partials/scripts/categorySelectorDropdownScript.ejs (100%) rename {static => src/static}/ejs/partials/scripts/categorySummary.ejs (100%) rename {static => src/static}/ejs/partials/scripts/highlightjs.ejs (100%) rename {static => src/static}/ejs/partials/scripts/popper.ejs (100%) rename {static => src/static}/ejs/partials/scripts/reportSearch.ejs (100%) rename {static => src/static}/ejs/partials/scripts/ruleOffcanvas.ejs (100%) rename {static => src/static}/ejs/partials/scripts/screenshotLightbox.ejs (100%) rename {static => src/static}/ejs/partials/scripts/summaryScanResults.ejs (100%) rename {static => src/static}/ejs/partials/scripts/summaryTable.ejs (100%) rename {static => src/static}/ejs/partials/scripts/utils.ejs (100%) rename {static => src/static}/ejs/partials/styles/bootstrap.ejs (100%) rename {static => src/static}/ejs/partials/styles/highlightjs.ejs (100%) rename {static => src/static}/ejs/partials/styles/styles.ejs (100%) rename {static => src/static}/ejs/partials/styles/summaryBootstrap.ejs (100%) rename {static => src/static}/ejs/partials/summaryHeader.ejs (100%) rename {static => src/static}/ejs/partials/summaryMain.ejs (100%) rename {static => src/static}/ejs/report.ejs (100%) rename {static => src/static}/ejs/summary.ejs (100%) rename {static => src/static}/mustache/.prettierrc (100%) rename {static => src/static}/mustache/Attention Deficit.mustache (100%) rename {static => src/static}/mustache/Blind.mustache (100%) rename {static => src/static}/mustache/Cognitive.mustache (100%) rename {static => src/static}/mustache/Colorblindness.mustache (100%) rename {static => src/static}/mustache/Deaf.mustache (100%) rename {static => src/static}/mustache/Deafblind.mustache (100%) rename {static => src/static}/mustache/Dyslexia.mustache (100%) rename {static => src/static}/mustache/Low Vision.mustache (100%) rename {static => src/static}/mustache/Mobility.mustache (100%) rename {static => src/static}/mustache/Sighted Keyboard Users.mustache (100%) rename {static => src/static}/mustache/report.mustache (100%) create mode 100644 src/types/print-message.d.ts rename utils.js => src/utils.ts (99%) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index bcabcc4a..06ba95c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +dist node_modules .vscode .a11y_storage diff --git a/INTEGRATION.md b/INTEGRATION.md index 7317a6e8..7ef77cdf 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -393,7 +393,7 @@ You will see Purple A11y results generated in results folder. console.log('Cookies retrieved.\n'); // where -m "..." are the headers needed in the format "header1 value1, header2 value2" etc // where -u ".../loginSuccess/" is the destination page after login - const command = `node cli.js -c website -u "https://authenticationtest.com/loginSuccess/" -p 1 -k "Your Name:email@domain.com" -m "${formattedCookies}"`; + const command = `npm run cli -- -c website -u "https://authenticationtest.com/loginSuccess/" -p 1 -k "Your Name:email@domain.com" -m "${formattedCookies}"`; console.log(`Executing PurpleA11y scan command:\n> ${command}\n`); runPurpleA11yScan(command); }) diff --git a/README.md b/README.md index 768607ac..f3ef5796 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If you wish to use Purple A11y as a NodeJS module that can be integrated with en ### Portable Purple A11y -Portable Purple A11y is the recommended way to run Purple A11y as it reduces the difficulty for installation. Refer to [Installation Guide](/INSTALLATION.md) for step-by-step instructions. +Portable Purple A11y is the recommended way to run Purple A11y as it reduces the difficulty for installation. Refer to [Installation Guide](./INSTALLATION.md) for step-by-step instructions. ### Manual Installation @@ -37,6 +37,7 @@ node -v - If you do not have node, or if there is a need to manage your node versions, you can consider using [Node Version Manager (NVM)](https://github.com/nvm-sh/nvm). - Make sure NVM is pointing to a node version >= 15.10.0. Please refer to [Usage of Node Version Manager (NVM)](<#usage-of-node-version-manager-(NVM)>) - Install the required NPM packages with `npm install`. +- Build the project with `npm run build` before you try to run it with `npm start`. #### Usage of Node Version Manager (NVM) @@ -58,7 +59,7 @@ Please refer to [Troubleshooting section](#troubleshooting) for more information Purple A11y can perform the following to scan the target URL. -- To **run** Purple A11y in **terminal**, run `node index`. Questions will be prompted to assist you in providing the right inputs. +- To **run** Purple A11y in **terminal**, run `npm start`. Questions will be prompted to assist you in providing the right inputs. - Results will be compiled in JSON format, followed by generating a HTML report. > NOTE: For your initial scan, there may be some loading time required before use. Purple-A11y will also ask for your name and email address and collect your app usage data to personalise your experience. Your information fully complies with [GovTech’s Privacy Policy](https://www.tech.gov.sg/privacy/). @@ -69,7 +70,7 @@ Purple A11y can perform the following to scan the target URL. > - Windows (PowerShell): `rm "$env:APPDATA\Purple A11y\userData.txt"` > - MacOS (Terminal): `rm "$HOME/Library/Application Support/Purple A11y/userData.txt"` -If `userData.txt` does not exists just run `node index`. +If `userData.txt` does not exists just run `npm start`. ### Scan Selection @@ -77,7 +78,7 @@ If `userData.txt` does not exists just run `node index`. You can interact via your arrow keys. ```shell -% node index +% npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience.│ @@ -96,7 +97,7 @@ You can interact via your arrow keys. Headless mode would allow you to run the scan in the background. If you would like to observe the scraping process, please enter `n` ```shell - % node index + % npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience. │ @@ -111,7 +112,7 @@ Headless mode would allow you to run the scan in the background. If you would li ### Sitemap Scan ```shell -% node index +% npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience. │ @@ -147,7 +148,7 @@ If the sitemap URL provided is invalid, an error message will be prompted for yo ### Website Scan ```shell -% node index +% npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience. │ @@ -171,7 +172,7 @@ If the website URL provided is invalid, an error message will be prompted for yo ### Customised Mobile Device Scan ``` shell -% node index +% npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience. │ @@ -196,7 +197,7 @@ Custom flow allows you to record a series of actions in the browser and re-play 1. Start by choosing the `Custom flow` in the menu selection. ```shell -% node index +% npm start ┌────────────────────────────────────────────────────────────┐ │ Purple A11y (ver ) │ │ We recommend using Chrome browser for the best experience. │ @@ -241,11 +242,11 @@ npx playwright@1.27.1 install ### CLI Mode CLI mode is designed to be run in continuous integration (CI) environment. - Run `node cli.js` for a set of command-line parameters available. + Run `npm run cli` for a set of command-line parameters available. ```shell -Usage: node cli.js -c -d -w -d -w -u OPTIONS Options: @@ -319,11 +320,11 @@ Options: ss to restricted resources. [string] Examples: - To scan sitemap of website:', 'node cli.js -c [ 1 | sitemap ] -u + To scan sitemap of website:', 'npm run cli -- -c [ 1 | sitemap ] -u [ -d | -w ] - To scan a website', 'node cli.js -c [ 2 | website ] -u [ -d [ -d | -w ] - To start a custom flow scan', 'node cli.js -c [ 3 | custom ] -u [ + To start a custom flow scan', 'npm run cli -- -c [ 3 | custom ] -u [ -d | -w ] ``` @@ -457,7 +458,7 @@ Please note that ```-d``` and ```-w``` are mutually exclusive. If none are speci For example, to conduct a website scan to the URL "http://localhost:8000" and write to "a11y-scan-results.zip" with an 'iPad (gen 7) landscape' screen, run ```shell -node cli.js -c 2 -o a11y-scan-results.zip -u http://localhost:8000 -d 'iPad (gen 7) landscape' +npm run cli -- -c 2 -o a11y-scan-results.zip -u http://localhost:8000 -d 'iPad (gen 7) landscape' ``` If the site you want to scan has a query string wrap the link in single quotes when entered into the CLI. @@ -465,7 +466,7 @@ If the site you want to scan has a query string wrap the link in single quotes w For example, to conduct a website scan to the URL "http://localhost:8000" and write to "a11y-scan-results.zip" with a custom screen width '360', run ```shell -node cli.js -c 2 -o a11y-scan-results.zip -u "http://localhost:8000" -w 360 +npm run cli -- -c 2 -o a11y-scan-results.zip -u "http://localhost:8000" -w 360 ``` ## Report @@ -477,7 +478,7 @@ A report will be downloaded into the current working directory. Each Issue has its own severity "Must Fix" / "Good to Fix" based on the [WCAG Conformance](https://www.w3.org/TR/WCAG21/). -For details on which accessibility scan results triggers a "Must Fix" / "Good to Fix" findings, you may refer to [Scan Issue Details](https://github.com/GovTechSG/purple-a11y/blob/master/DETAILS.md). +For details on which accessibility scan results triggers a "Must Fix" / "Good to Fix" findings, you may refer to [Scan Issue Details](./DETAILS.md). ## Troubleshooting @@ -544,7 +545,7 @@ zsh: abort node index.js If you find a scan takes too long to complete due to large website, or there are too many pages in a sitemap to scan, you may choose to limit number of pages scanned. -To do this, run CLI mode `node cli.js` with the needed settings and specify `-p 10` where `10` is the number of pages you wish to scan. +To do this, run CLI mode `npm run cli --` with the needed settings and specify `-p 10` where `10` is the number of pages you wish to scan. ### I am a new developer and I have some knowledge gap. diff --git a/__mocks__/mockFunctions.js b/__mocks__/mockFunctions.ts similarity index 100% rename from __mocks__/mockFunctions.js rename to __mocks__/mockFunctions.ts diff --git a/__mocks__/mockIssues.js b/__mocks__/mockIssues.ts similarity index 100% rename from __mocks__/mockIssues.js rename to __mocks__/mockIssues.ts diff --git a/__tests__/logs.test.js b/__tests__/logs.test.ts similarity index 100% rename from __tests__/logs.test.js rename to __tests__/logs.test.ts diff --git a/__tests__/mergeAxeResults.test.js b/__tests__/mergeAxeResults.test.ts similarity index 100% rename from __tests__/mergeAxeResults.test.js rename to __tests__/mergeAxeResults.test.ts diff --git a/__tests__/utils.test.js b/__tests__/utils.test.ts similarity index 100% rename from __tests__/utils.test.js rename to __tests__/utils.test.ts diff --git a/exclusions.txt b/exclusions.txt deleted file mode 100644 index ee224a5b..00000000 --- a/exclusions.txt +++ /dev/null @@ -1 +0,0 @@ -\.*login.singpass.gov.sg\.* \ No newline at end of file diff --git a/gitlab-pipeline-template.yml b/gitlab-pipeline-template.yml index 36a490a8..8a85712f 100644 --- a/gitlab-pipeline-template.yml +++ b/gitlab-pipeline-template.yml @@ -35,7 +35,7 @@ a11y-scan: # Start in the app dir - cd /app # Run accessibility scan - - PURPLE_A11Y_VERBOSE=true node cli.js -b chromium -c "$A11Y_SCAN_TYPE" -d "$A11Y_SCAN_DEVICE" -o "$CI_PROJECT_DIR/$A11Y_SCAN_ARTIFACT_NAME" -u "$A11Y_SCAN_URL" -p "$A11Y_SCAN_MAX_NUM_PAGES" -a "$A11Y_INCLUDE_SCREENSHOTS" -f "$A11Y_SCAN_SAFE_MODE" -k "$A11Y_SCAN_NAME_EMAIL" || true + - PURPLE_A11Y_VERBOSE=true npm run build && npm run cli -- -b chromium -c "$A11Y_SCAN_TYPE" -d "$A11Y_SCAN_DEVICE" -o "$CI_PROJECT_DIR/$A11Y_SCAN_ARTIFACT_NAME" -u "$A11Y_SCAN_URL" -p "$A11Y_SCAN_MAX_NUM_PAGES" -a "$A11Y_INCLUDE_SCREENSHOTS" -f "$A11Y_SCAN_SAFE_MODE" -k "$A11Y_SCAN_NAME_EMAIL" || true # Move the results directory to artifacts - results_directory=$(find results -mindepth 1 -maxdepth 1 -type d -print -quit) # Get the first directory within ./results - if [ -n "$results_directory" ]; then mv "$results_directory" "$CI_PROJECT_DIR/artifacts"; fi # Move the directory to ./artifacts diff --git a/package-lock.json b/package-lock.json index a083ad28..259c290d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,13 +35,16 @@ "devDependencies": { "@eslint/eslintrc": "^3.0.2", "@eslint/js": "^9.2.0", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^9.0.7", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-import": "^2.27.4", "eslint-plugin-prettier": "^5.0.0", "globals": "^15.2.0", - "jest": "^29.3.1" + "jest": "^29.3.1", + "typescript": "^5.4.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2306,6 +2309,16 @@ "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==" }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2320,6 +2333,16 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2360,6 +2383,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", @@ -2382,6 +2414,15 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -8819,6 +8860,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", diff --git a/package.json b/package.json index 7333468c..23e0dd25 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.9.60", "type": "module", "imports": { - "#root/*.js": "./*.js" + "#root/*.js": "./dist/*.js" }, "dependencies": { "@json2csv/node": "^7.0.3", @@ -33,13 +33,16 @@ "devDependencies": { "@eslint/eslintrc": "^3.0.2", "@eslint/js": "^9.2.0", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^9.0.7", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-import": "^2.27.4", "eslint-plugin-prettier": "^5.0.0", "globals": "^15.2.0", - "jest": "^29.3.1" + "jest": "^29.3.1", + "typescript": "^5.4.5" }, "overrides": { "node-fetch": "^2.3.0", @@ -48,9 +51,14 @@ "tough-cookie": "^5.0.0-rc.2" }, "scripts": { + "build": "npm run copyfiles && tsc", + "build:watch": "npm run build -- --watch", + "copyfiles": "mkdir -p dist/static/ejs && mkdir -p dist && cp exclusions.txt dist/ && cp -R src/static/ejs/* dist/static/ejs", + "start": "node dist/index.js", + "cli": "node dist/cli.js", "test": "node --experimental-vm-modules ./node_modules/.bin/jest", - "lint": "eslint . --ext js --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint . --fix --ext js --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --fix --report-unused-disable-directives --max-warnings 0" }, "author": "", "license": "MIT", diff --git a/scripts/verapdf-auto-install-macos.xml b/scripts/verapdf-auto-install-macos.xml deleted file mode 100644 index 41dc97db..00000000 --- a/scripts/verapdf-auto-install-macos.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - /tmp/verapdf - - - - - - - - - - - diff --git a/scripts/verapdf-auto-install-windows.xml b/scripts/verapdf-auto-install-windows.xml deleted file mode 100644 index e0aebeab..00000000 --- a/scripts/verapdf-auto-install-windows.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - C:\Windows\Temp\verapdf - - - - - - - - - - - diff --git a/cli.js b/src/cli.ts similarity index 90% rename from cli.js rename to src/cli.ts index ab787a68..4ca825ec 100644 --- a/cli.js +++ b/src/cli.ts @@ -30,6 +30,7 @@ import playwrightAxeGenerator from './playwrightAxeGenerator.js'; import { silentLogger } from './logs.js'; import { fileURLToPath } from 'url'; import path from 'path'; +import { Answers } from './index.js'; const appVersion = getVersion(); const yargs = _yargs(hideBin(process.argv)); @@ -38,19 +39,19 @@ const options = yargs .version(false) .usage( `Purple A11y version: ${appVersion} -Usage: node cli.js -c -d -w -u OPTIONS`, +Usage: npm run cli -- -c -d -w -u OPTIONS`, ) .strictOptions(true) .options(cliOptions) .example([ [ - `To scan sitemap of website:', 'node cli.js -c [ 1 | sitemap ] -u [ -d | -w ]`, + `To scan sitemap of website:', 'npm run cli -- -c [ 1 | sitemap ] -u [ -d | -w ]`, ], [ - `To scan a website', 'node cli.js -c [ 2 | website ] -u [ -d | -w ]`, + `To scan a website', 'npm run cli -- -c [ 2 | website ] -u [ -d | -w ]`, ], [ - `To start a custom flow scan', 'node cli.js -c [ 3 | custom ] -u [ -d | -w ]`, + `To start a custom flow scan', 'npm run cli -- -c [ 3 | custom ] -u [ -d | -w ]`, ], ]) .coerce('c', option => { @@ -177,10 +178,7 @@ Usage: node cli.js -c -d -w -u OPTIONS`, try { return validateFilePath(option, __dirname); } catch (err) { - printMessage( - [`Invalid blacklistedPatternsFilename file path. ${validationErrors}`], - messageOptions, - ); + printMessage([`Invalid blacklistedPatternsFilename file path. ${err}`], messageOptions); process.exit(1); } }) @@ -227,7 +225,7 @@ Usage: node cli.js -c -d -w -u OPTIONS`, const headerValues = option.split(', '); const allHeaders = {}; - headerValues.map(headerValue => { + headerValues.map((headerValue: string) => { const headerValuePair = headerValue.split(/ (.*)/s); if (headerValuePair.length < 2) { printMessage( @@ -240,7 +238,7 @@ Usage: node cli.js -c -d -w -u OPTIONS`, } allHeaders[headerValuePair[0]] = headerValuePair[1]; // {"header": "value", "header2": "value2", ...} }); - + return allHeaders; }) .check(argvs => { @@ -258,17 +256,17 @@ Usage: node cli.js -c -d -w -u OPTIONS`, .conflicts('d', 'w') .epilogue('').argv; -const scanInit = async argvs => { +const scanInit = async (argvs: Answers): Promise => { let isNewCustomFlow = false; if (constants.scannerTypes[argvs.scanner] === constants.scannerTypes.custom2) { argvs.scanner = constants.scannerTypes.custom; isNewCustomFlow = true; } else { + argvs.headless = argvs.headless === 'yes'; + argvs.followRobots = argvs.followRobots === 'yes'; + argvs.safeMode = argvs.safeMode === 'yes'; argvs.scanner = constants.scannerTypes[argvs.scanner]; } - argvs.headless = argvs.headless === 'yes'; - argvs.followRobots = argvs.followRobots === 'yes'; - argvs.safeMode = argvs.safeMode === 'yes'; argvs.browserToRun = constants.browserTypes[argvs.browserToRun]; // let chromeDataDir = null; @@ -359,20 +357,22 @@ const scanInit = async argvs => { // File clean up after url check // files will clone a second time below if url check passes - process.env.PURPLE_A11Y_VERBOSE ? deleteClonedProfiles(data.browser,data.randomToken): deleteClonedProfiles(data.browser) //first deletion + process.env.PURPLE_A11Y_VERBOSE + ? deleteClonedProfiles(data.browser, data.randomToken) + : deleteClonedProfiles(data.browser); //first deletion if (argvs.exportDirectory) { constants.exportDirectory = argvs.exportDirectory; } - if (process.env.RUNNING_FROM_PH_GUI || process.env.PURPLE_A11Y_VERBOSE){ + if (process.env.RUNNING_FROM_PH_GUI || process.env.PURPLE_A11Y_VERBOSE) { let randomTokenMessage = { type: 'randomToken', - payload: `${data.randomToken}` + payload: `${data.randomToken}`, + }; + if (process.send) { + process.send(JSON.stringify(randomTokenMessage)); } - if (process.send){ - process.send(JSON.stringify(randomTokenMessage)); - } } setHeadlessMode(data.browser, data.isHeadless); @@ -400,7 +400,9 @@ const scanInit = async argvs => { } // Delete cloned directory - process.env.PURPLE_A11Y_VERBOSE ? deleteClonedProfiles(data.browser,data.randomToken): deleteClonedProfiles(data.browser) //second deletion + process.env.PURPLE_A11Y_VERBOSE + ? deleteClonedProfiles(data.browser, data.randomToken) + : deleteClonedProfiles(data.browser); //second deletion // Delete dataset and request queues await cleanUp(data.randomToken); @@ -412,10 +414,9 @@ scanInit(options).then(async storagePath => { // Take option if set if (typeof options.zip === 'string') { constants.cliZipFileName = options.zip; - - if (!options.zip.endsWith('.zip')){ - constants.cliZipFileName += '.zip'; + if (!options.zip.endsWith('.zip')) { + constants.cliZipFileName += '.zip'; } } @@ -437,16 +438,13 @@ scanInit(options).then(async storagePath => { if (process.send && process.env.PURPLE_A11Y_VERBOSE && process.env.REPORT_BREAKDOWN != '1') { let zipFileNameMessage = { type: 'zipFileName', - payload: `${constants.cliZipFileName}` - } + payload: `${constants.cliZipFileName}`, + }; process.send(JSON.stringify(zipFileNameMessage)); - } - printMessage(messageToDisplay); - process.exit(0); }) @@ -455,4 +453,4 @@ scanInit(options).then(async storagePath => { }); }); -export { options }; \ No newline at end of file +export { options }; diff --git a/combine.js b/src/combine.ts similarity index 100% rename from combine.js rename to src/combine.ts diff --git a/constants/__tests__/common.test.js b/src/constants/__tests__/common.test.ts similarity index 100% rename from constants/__tests__/common.test.js rename to src/constants/__tests__/common.test.ts diff --git a/constants/axeTypes.json b/src/constants/axeTypes.json similarity index 100% rename from constants/axeTypes.json rename to src/constants/axeTypes.json diff --git a/constants/cliFunctions.js b/src/constants/cliFunctions.ts similarity index 87% rename from constants/cliFunctions.js rename to src/constants/cliFunctions.ts index 8e1ad8cb..77ffae7c 100644 --- a/constants/cliFunctions.js +++ b/src/constants/cliFunctions.ts @@ -1,3 +1,4 @@ +import { Options } from 'yargs'; import constants from './constants.js'; export const messageOptions = { @@ -11,10 +12,11 @@ export const alertMessageOptions = { borderColor: 'red', }; -export const cliOptions = { +export const cliOptions: { [key: string]: Options } = { c: { alias: 'scanner', - describe: 'Type of scan, 1) sitemap, 2) website crawl, 3) custom flow, 4) custom flow 2.0, 5) intelligent', + describe: + 'Type of scan, 1) sitemap, 2) website crawl, 3) custom flow, 4) custom flow 2.0, 5) intelligent', choices: Object.keys(constants.scannerTypes), demandOption: true, }, @@ -51,7 +53,8 @@ export const cliOptions = { }, f: { alias: 'safeMode', - describe: 'Option to disable dynamically clicking of page buttons and links to find links, which resolve issues on some websites. Defaults to no.', + describe: + 'Option to disable dynamically clicking of page buttons and links to find links, which resolve issues on some websites. Defaults to no.', type: 'string', choices: ['yes', 'no'], requiresArg: true, @@ -77,7 +80,8 @@ export const cliOptions = { }, s: { alias: 'strategy', - describe: 'Crawls up to general (same parent) domains, or only specific hostname. Defaults to "same-domain".', + describe: + 'Crawls up to general (same parent) domains, or only specific hostname. Defaults to "same-domain".', choices: ['same-domain', 'same-hostname'], requiresArg: true, demandOption: false, @@ -109,7 +113,7 @@ export const cliOptions = { type: 'number', demandOption: false, }, - + i: { alias: 'fileTypes', describe: 'File types to include in the scan. Defaults to html-only.', @@ -164,10 +168,10 @@ export const cliOptions = { }, }; -export const configureReportSetting = isEnabled => { +export const configureReportSetting = (isEnabled: boolean): void => { if (isEnabled) { - process.env.REPORT_BREAKDOWN = 1; + process.env.REPORT_BREAKDOWN = '1'; } else { - process.env.REPORT_BREAKDOWN = 0; + process.env.REPORT_BREAKDOWN = '0'; } }; diff --git a/constants/common.js b/src/constants/common.ts similarity index 84% rename from constants/common.js rename to src/constants/common.ts index adde253f..6305acf4 100644 --- a/constants/common.js +++ b/src/constants/common.ts @@ -1,1689 +1,1713 @@ -/* eslint-disable consistent-return */ -/* eslint-disable no-console */ -/* eslint-disable camelcase */ -/* eslint-disable no-use-before-define */ -import validator from 'validator'; -import axios from 'axios'; -import { JSDOM } from 'jsdom'; -import * as cheerio from 'cheerio'; -import crawlee, { Request } from 'crawlee'; -import { parseString } from 'xml2js'; -import fs from 'fs'; -import path from 'path'; -import safe from 'safe-regex'; -import * as https from 'https'; -import os from 'os'; -import { minimatch } from 'minimatch'; -import { Glob, globSync } from 'glob'; -import { devices, webkit } from 'playwright'; -import printMessage from 'print-message'; -import constants, { - getDefaultChromeDataDir, - getDefaultEdgeDataDir, - getDefaultChromiumDataDir, - proxy, - formDataFields, -} from './constants.js'; -import { silentLogger } from '../logs.js'; -import { isUrlPdf } from '../crawlers/commonCrawlerFunc.js'; -import { randomThreeDigitNumberString } from '../utils.js'; - -// validateDirPath validates a provided directory path -// returns null if no error -export const validateDirPath = dirPath => { - if (typeof dirPath !== 'string') { - return 'Please provide string value of directory path.'; - } - - try { - fs.accessSync(dirPath); - if (!fs.statSync(dirPath).isDirectory()) { - return 'Please provide a directory path.'; - } - - return null; - } catch (error) { - return 'Please ensure path provided exists.'; - } -}; - -export const validateCustomFlowLabel = customFlowLabel => { - const containsReserveWithDot = constants.reserveFileNameKeywords.some(char => - customFlowLabel.toLowerCase().includes(`${char.toLowerCase()}.`), - ); - const containsForbiddenCharacters = constants.forbiddenCharactersInDirPath.some(char => - customFlowLabel.includes(char), - ); - const exceedsMaxLength = customFlowLabel.length > 80; - - if (containsForbiddenCharacters) { - const displayForbiddenCharacters = constants.forbiddenCharactersInDirPath - .toString() - .replaceAll(',', ' , '); - return { - isValid: false, - errorMessage: `Invalid label. Cannot contain ${displayForbiddenCharacters}`, - }; - } - if (exceedsMaxLength) { - return { isValid: false, errorMessage: `Invalid label. Cannot exceed 80 characters.` }; - } - if (containsReserveWithDot) { - const displayReserveKeywords = constants.reserveFileNameKeywords - .toString() - .replaceAll(',', ' , '); - return { - isValid: false, - errorMessage: `Invalid label. Cannot have '.' appended to ${displayReserveKeywords} as they are reserved keywords.`, - }; - } - return { isValid: true }; -}; - -// validateFilePath validates a provided file path -// returns null if no error -export const validateFilePath = (filePath, cliDir) => { - if (typeof filePath !== 'string') { - throw new Error('Please provide string value of file path.'); - } - - const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(cliDir, filePath); - try { - fs.accessSync(absolutePath); - if (!fs.statSync(absolutePath).isFile()) { - throw new Error('Please provide a file path.'); - } - - if (path.extname(absolutePath) !== '.txt') { - throw new Error('Please provide a file with txt extension.'); - } - - return absolutePath; - } catch (error) { - throw new Error('Please ensure path provided exists.'); - } -}; - -export const getBlackListedPatterns = blacklistedPatternsFilename => { - let exclusionsFile = null; - if (blacklistedPatternsFilename) { - exclusionsFile = blacklistedPatternsFilename; - } else if (fs.existsSync('exclusions.txt')) { - exclusionsFile = 'exclusions.txt'; - } - - if (!exclusionsFile) { - return null; - } - - const rawPatterns = fs.readFileSync(exclusionsFile).toString(); - const blacklistedPatterns = rawPatterns - .split('\n') - .map(p => p.trim()) - .filter(p => p !== ''); - - const unsafe = blacklistedPatterns.filter(pattern => !safe(pattern)); - if (unsafe.length > 0) { - const unsafeExpressionsError = `Unsafe expressions detected: ${unsafe} Please revise ${exclusionsFile}`; - throw new Error(unsafeExpressionsError); - } - - return blacklistedPatterns; -}; - -export const isBlacklistedFileExtensions = (url, blacklistedFileExtensions) => { - const urlExtension = url.split('.').pop(); - return blacklistedFileExtensions.includes(urlExtension); -}; - -const document = new JSDOM('').window; - -const httpsAgent = new https.Agent({ - // Run in environments with custom certificates - rejectUnauthorized: false, - keepAlive: true, -}); - -export const messageOptions = { - border: false, - marginTop: 2, - marginBottom: 2, -}; - -const urlOptions = { - protocols: ['http', 'https'], - require_protocol: true, - require_tld: false, -}; - -const queryCheck = s => document.createDocumentFragment().querySelector(s); -export const isSelectorValid = selector => { - try { - queryCheck(selector); - } catch (e) { - return false; - } - return true; -}; - -// Refer to NPM validator's special characters under sanitizers for escape() -const blackListCharacters = '\\<>&\'"'; - -export const isValidXML = async content => { - // fs.writeFileSync('sitemapcontent.txt', content); - let status; - let parsedContent = ''; - parseString(content, (err, result) => { - if (result) { - status = true; - parsedContent = result; - } - if (err) { - status = false; - } - }); - return { status, parsedContent }; -}; - -export const isSkippedUrl = (pageUrl, whitelistedDomains) => { - const matched = - whitelistedDomains.filter(p => { - const pattern = p.replace(/[\n\r]+/g, ''); - - // is url - if (pattern.startsWith('http') && pattern === pageUrl) { - return true; - } - - // is regex (default) - return new RegExp(pattern).test(pageUrl); - }).length > 0; - - return matched; -}; - -export const isFileSitemap = async filePath => { - if (filePath.startsWith('file:///')) { - if (os.platform() === 'win32') { - filePath = filePath.match(/^file:\/\/\/([A-Z]:\/[^?#]+)/)?.[1]; - } else { - filePath = filePath.match(/^file:\/\/(\/[^?#]+)/)?.[1]; - } - } - - if (!fs.existsSync(filePath)) { - return null; - } - - const file = fs.readFileSync(filePath, 'utf8'); - const isLocalSitemap = await isSitemapContent(file); - return isLocalSitemap ? filePath : null; -}; - -export const getUrlMessage = scanner => { - switch (scanner) { - case constants.scannerTypes.website: - case constants.scannerTypes.custom: - case constants.scannerTypes.custom2: - return 'Please enter URL of website: '; - case constants.scannerTypes.sitemap: - return 'Please enter URL or file path to sitemap, or drag and drop a sitemap file here: '; - - default: - return 'Invalid option'; - } -}; - -export const isInputValid = inputString => { - if (!validator.isEmpty(inputString)) { - const removeBlackListCharacters = validator.escape(inputString); - - if (validator.isAscii(removeBlackListCharacters)) { - return true; - } - } - - return false; -}; - -export const sanitizeUrlInput = url => { - // Sanitize that there is no blacklist characters - const sanitizeUrl = validator.blacklist(url, blackListCharacters); - const data = {}; - if (validator.isURL(sanitizeUrl, urlOptions)) { - data.isValid = true; - } else { - data.isValid = false; - } - - data.url = sanitizeUrl; - return data; -}; - -const requestToUrl = async (url, isNewCustomFlow, extraHTTPHeaders) => { - // User-Agent is modified to emulate a browser to handle cases where some sites ban non browser agents, resulting in a 403 error - const res = {}; - const parsedUrl = new URL(url); - await axios - .get(parsedUrl, { - headers: { - ...extraHTTPHeaders, - 'User-Agent': devices['Desktop Chrome HiDPI'].userAgent, - 'Host': parsedUrl.host - }, - auth: { - username: decodeURIComponent(parsedUrl.username), - password: decodeURIComponent(parsedUrl.password), - }, - httpsAgent, - timeout: 5000, - }) - .then(async response => { - const redirectUrl = response.request.res.responseUrl; - res.status = constants.urlCheckStatuses.success.code; - let data; - if (typeof response.data === 'string' || response.data instanceof String) { - data = response.data; - } else if (typeof response.data === 'object' && response.data !== null) { - try { - data = JSON.stringify(response.data); - } catch (error) { - console.log("Error converting object to JSON:", error); - } - } else { - console.log("Unsupported data type:", typeof response.data); - } - let modifiedHTML = data.replace(/