├── .dockerignore ├── .editorconfig ├── .env ├── .github └── workflows │ └── ci-cd.yml ├── .gitignore ├── .prettierrc ├── DEMO.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_EN.md ├── builder.js ├── docker-compose.yaml ├── docs ├── grafana-dashboard.png ├── rapport-html-detail-page-avec-changement-page.jpeg ├── rapport-html-detail-page.jpeg ├── rapport-html-global-avec-influxdb.jpeg ├── rapport-html-global.jpeg ├── rapport-xlsx-detail-page.png └── rapport-xlsx-global.png ├── grafana-provisioning ├── dashboards │ ├── dashboard.yml │ └── greenit-analysis.json └── datasources │ └── datasource.yml ├── greenit ├── package-lock.json ├── package.json ├── samples └── greenit-url.yml ├── src ├── cli-core │ ├── analysis.js │ ├── influxdb.js │ ├── reportExcel.js │ ├── reportGlobal.js │ ├── reportHtml.js │ ├── template │ │ ├── global.html │ │ └── page.html │ ├── translator.js │ └── utils.js ├── commands │ ├── analyse.js │ └── sitemapParser.js ├── conf │ ├── rules.js │ └── sizes.js ├── greenit-core │ ├── analyseFrameCore.js │ ├── ecoIndex.js │ ├── greenpanel.js │ ├── rules │ │ ├── AddExpiresOrCacheControlHeaders.js │ │ ├── CompressHttp.js │ │ ├── DomainsNumber.js │ │ ├── DontResizeImageInBrowser.js │ │ ├── EmptySrcTag.js │ │ ├── ExternalizeCss.js │ │ ├── ExternalizeJs.js │ │ ├── HttpError.js │ │ ├── HttpRequests.js │ │ ├── ImageDownloadedNotDisplayed.js │ │ ├── JsValidate.js │ │ ├── MaxCookiesLength.js │ │ ├── MinifiedCss.js │ │ ├── MinifiedJs.js │ │ ├── NoCookieForStaticRessources.js │ │ ├── NoRedirect.js │ │ ├── OptimizeBitmapImages.js │ │ ├── OptimizeSvg.js │ │ ├── Plugins.js │ │ ├── PrintStyleSheet.js │ │ ├── SocialNetworkButton.js │ │ ├── StyleSheets.js │ │ ├── UseETags.js │ │ └── UseStandardTypefaces.js │ ├── rulesManager.js │ └── utils.js └── locales │ ├── en.json │ └── fr.json └── tests └── commands ├── analyse.test.js └── reference ├── 1.json ├── 2.json ├── 3.json ├── 4.json ├── 5.json ├── 6.json ├── 7.json ├── 8.json └── globalReport.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | dist/ 4 | script/ 5 | node_modules/ 6 | results/ 7 | results.xlsx 8 | tests/ 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | 12 | [{*.markdown, *.md}] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | [{*.yaml, *.yml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # InfluxDB 2 | # Ne pas modifier les deux variables suivante 3 | INFLUXDB_HOST=greenit-cli-influxdb 4 | INFLUXDB_PORT=8086 5 | 6 | INFLUXDB_ORG_NAME=default 7 | INFLUXDB_USERNAME=default 8 | INFLUXDB_PASSWORD=defaultinfluxdb 9 | INFLUXDB_BUCKET_NAME=db0 10 | INFLUXDB_RETENTION=24w 11 | 12 | # Renseigner ces variables une fois influxdb démarré pour la première fois 13 | INFLUXDB_TOKEN=token 14 | INFLUXDB_ORG_ID=orgId 15 | 16 | # Grafana 17 | GRAFANA_USERNAME=admin 18 | GRAFANA_PASSWORD=admin 19 | 20 | # Image Version 21 | # Attention lors des changements de versions 22 | INFLUXDB_IMAGE_VERSION=influxdb:2.1.1 23 | GRAFANA_IMAGE_VERSION=grafana/grafana:8.4.3 -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image on Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | 22 | - name: Install dependencies 23 | run: npm i 24 | 25 | - name: Run tests 26 | run: npm test 27 | 28 | docker: 29 | runs-on: ubuntu-latest 30 | needs: build 31 | if: startsWith(github.ref, 'refs/tags/v') 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | 36 | - name: Set up Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: '20' 40 | 41 | - name: Extract version from package.json 42 | id: extract_version 43 | run: | 44 | VERSION=$(node -p "require('./package.json').version") 45 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 46 | 47 | - name: Set up Docker Buildx 48 | uses: docker/setup-buildx-action@v3 49 | 50 | - name: Log in to Docker Hub 51 | uses: docker/login-action@v3 52 | with: 53 | username: ${{ secrets.DOCKER_USERNAME }} 54 | password: ${{ secrets.DOCKER_PASSWORD }} 55 | 56 | - name: Build and push Docker image 57 | uses: docker/build-push-action@v5 58 | with: 59 | context: . 60 | push: true 61 | tags: | 62 | ${{ secrets.DOCKER_USERNAME }}/greenit-analysis-cli:latest 63 | ${{ secrets.DOCKER_USERNAME }}/greenit-analysis-cli:${{ steps.extract_version.outputs.VERSION }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .scannerwork 2 | .vscode 3 | node_modules/ 4 | dist/ 5 | **/results/ 6 | **/results_test/ 7 | **/output/ 8 | *.xlsx 9 | *.yaml 10 | !docker-compose.yaml 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /DEMO.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | ## 1. Npm 4 | 5 | 1. Install 6 | ``` 7 | npm i 8 | ``` 9 | 10 | 2. Link 11 | ``` 12 | npm link 13 | ``` 14 | 15 | 3. Analyse - HTML 16 | ``` 17 | greenit analyse samples/greenit-url.yml output/greenit.html --max_tab=1 --timeout=10000 --retry=3 18 | ``` 19 | 20 | ## 2. Docker build 21 | 22 | 1. Build 23 | ``` 24 | docker build -t greenit-analysis-cli . 25 | ``` 26 | 27 | 2. Run 28 | ``` 29 | docker run -it --init --rm --cap-add=SYS_ADMIN \ 30 | -v output:/app/output \ 31 | -e TZ=Europe/Paris \ 32 | --name greenit-analysis-cli-container \ 33 | greenit-analysis-cli \ 34 | greenit analyse samples/greenit-url.yml output/greenit.html \ 35 | --max_tab=1 \ 36 | --timeout=10000 \ 37 | --retry=3 38 | ``` 39 | 40 | ## 3. Docker pull 41 | 42 | 1. Pull image 43 | ``` 44 | docker pull jpreisner/greenit-analysis-cli:latest 45 | ``` 46 | 47 | 2. Run container 48 | ``` 49 | docker run -it --init --rm --cap-add=SYS_ADMIN \ 50 | -v output:/app/output \ 51 | -e TZ=Europe/Paris \ 52 | --name greenit-analysis-cli-container \ 53 | jpreisner/greenit-analysis-cli \ 54 | greenit analyse samples/greenit-url.yml output/greenit.html \ 55 | --max_tab=1 \ 56 | --timeout=10000 \ 57 | --retry=3 58 | ``` 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # See : https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker 2 | FROM node:24-slim 3 | 4 | # Uncomment if you need to configure proxy. 5 | # You can init these variables by using --build-args during docker build 6 | # Example : docker build [...] --build-args http_proxy=http://:@: 7 | #ENV HTTP_PROXY=$http_proxy 8 | #ENV HTTPS_PROXY=$https_proxy 9 | #ENV NO_PROXY=$no_proxy 10 | 11 | RUN apt-get update \ 12 | && apt-get install -y wget gnupg \ 13 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 14 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 15 | && apt-get update \ 16 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 libx11-xcb1 libnss3-tools curl \ 17 | --no-install-recommends \ 18 | && rm -rf /var/lib/apt/lists/* 19 | WORKDIR /app 20 | COPY . . 21 | 22 | # Uncomment if you need to configure proxy. 23 | #RUN npm config set proxy $HTTP_PROXY 24 | 25 | RUN npm i \ 26 | && npm link \ 27 | && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ 28 | && mkdir -p /home/pptruser/Downloads \ 29 | && chown -R pptruser:pptruser /home/pptruser \ 30 | && chown -R pptruser:pptruser /app/ 31 | 32 | USER pptruser 33 | 34 | # To avoid "Error: ENOENT: no such file or directory, open '/app/dist/greenItBundle.js'" 35 | RUN npm i \ 36 | && node node_modules/puppeteer/install.mjs 37 | 38 | ENV URL_PATH="/app/input/url.yaml" 39 | ENV RESULTS_PATH="/app/output/results.html" 40 | CMD greenit analyse $URL_PATH $RESULTS_PATH 41 | -------------------------------------------------------------------------------- /builder.js: -------------------------------------------------------------------------------- 1 | const concat = require('concat-files'); 2 | const glob = require('glob'); 3 | const fs = require('fs'); 4 | 5 | const DIR = './dist'; 6 | 7 | if (!fs.existsSync(DIR)) { 8 | fs.mkdirSync(DIR); 9 | } 10 | 11 | const rules = glob.sync('./src/greenit-core/rules/*.js'); 12 | 13 | //One script to analyse them all 14 | concat( 15 | [ 16 | './src/greenit-core/analyseFrameCore.js', 17 | './src/greenit-core/utils.js', 18 | './src/greenit-core/rulesManager.js', 19 | './src/greenit-core/ecoIndex.js', 20 | ...rules, 21 | './src/greenit-core/greenpanel.js', 22 | ], 23 | './dist/greenItBundle.js', 24 | function (err) { 25 | if (err) throw err; 26 | console.log('build complete'); 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | greenit-cli-influxdb: 3 | image: ${INFLUXDB_IMAGE_VERSION} 4 | container_name: ${INFLUXDB_HOST} 5 | ports: 6 | - '8086:8086' 7 | volumes: 8 | - greenit-cli-influxdb-storage:/var/lib/influxdb2 9 | - ./influxdb/queries:/home/queries:rw 10 | environment: 11 | - DOCKER_INFLUXDB_INIT_MODE=setup 12 | - DOCKER_INFLUXDB_INIT_USERNAME=${INFLUXDB_USERNAME} 13 | - DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_PASSWORD} 14 | - DOCKER_INFLUXDB_INIT_ORG=${INFLUXDB_ORG_NAME} 15 | - DOCKER_INFLUXDB_INIT_BUCKET=${INFLUXDB_BUCKET_NAME} 16 | - DOCKER_INFLUXDB_INIT_RETENTION=${INFLUXDB_RETENTION} 17 | 18 | greenit-cli-grafana: 19 | container_name: greenit-cli-grafana 20 | image: ${GRAFANA_IMAGE_VERSION} 21 | ports: 22 | - '3000:3000' 23 | volumes: 24 | - greenit-cli-grafana-storage:/var/lib/grafana 25 | - ./grafana-provisioning/:/etc/grafana/provisioning 26 | depends_on: 27 | - greenit-cli-influxdb 28 | environment: 29 | - GF_SECURITY_ADMIN_USER=${GRAFANA_USERNAME} 30 | - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} 31 | - GF_AUTH_ANONYMOUS_ENABLED=true 32 | - GF_USERS_ALLOW_SIGN_UP=false 33 | - GF_USERS_ALLOW_ORG_CREATE=false 34 | - INFLUXDB_ORG_ID=${INFLUXDB_ORG_ID} 35 | - INFLUXDB_TOKEN=${INFLUXDB_TOKEN} 36 | - INFLUXDB_BUCKET_NAME=${INFLUXDB_BUCKET_NAME} 37 | - INFLUXDB_HOST=${INFLUXDB_HOST} 38 | - INFLUXDB_PORT=${INFLUXDB_PORT} 39 | 40 | volumes: 41 | greenit-cli-influxdb-storage: 42 | driver: local 43 | greenit-cli-grafana-storage: 44 | driver: local -------------------------------------------------------------------------------- /docs/grafana-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/grafana-dashboard.png -------------------------------------------------------------------------------- /docs/rapport-html-detail-page-avec-changement-page.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-html-detail-page-avec-changement-page.jpeg -------------------------------------------------------------------------------- /docs/rapport-html-detail-page.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-html-detail-page.jpeg -------------------------------------------------------------------------------- /docs/rapport-html-global-avec-influxdb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-html-global-avec-influxdb.jpeg -------------------------------------------------------------------------------- /docs/rapport-html-global.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-html-global.jpeg -------------------------------------------------------------------------------- /docs/rapport-xlsx-detail-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-xlsx-detail-page.png -------------------------------------------------------------------------------- /docs/rapport-xlsx-global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnumr/GreenIT-Analysis-cli/c4fec990458591d48871d9bd0668d79962d1a9a3/docs/rapport-xlsx-global.png -------------------------------------------------------------------------------- /grafana-provisioning/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: InfluxDB 4 | folder: '' 5 | type: file 6 | disableDeletion: false 7 | editable: true 8 | options: 9 | path: /etc/grafana/provisioning/dashboards 10 | 11 | -------------------------------------------------------------------------------- /grafana-provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: InfluxDB2 4 | type: influxdb 5 | access: proxy 6 | url: http://${INFLUXDB_HOST}:${INFLUXDB_PORT} 7 | secureJsonData: 8 | token: ${INFLUXDB_TOKEN} 9 | jsonData: 10 | version: Flux 11 | organization: ${INFLUXDB_ORG_ID} 12 | defaultBucket: ${INFLUXDB_BUCKET_NAME} 13 | editable: true 14 | -------------------------------------------------------------------------------- /greenit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const yargs = require('yargs/yargs'); 4 | const { hideBin } = require('yargs/helpers'); 5 | const sizes = require('./src/conf/sizes.js'); 6 | const analyse_core = require('./src/commands/analyse.js').analyse_core; 7 | 8 | yargs(hideBin(process.argv)) 9 | .command( 10 | 'analyse [url_input_file] [report_output_file]', 11 | 'Run the analysis', 12 | (yargs) => { 13 | yargs 14 | .positional('url_input_file', { 15 | describe: 'YAML file path listing all URL', 16 | default: 'url.yaml', 17 | }) 18 | .positional('report_output_file', { 19 | describe: 'Report output file path', 20 | default: 'results.xlsx', 21 | }) 22 | .option('grafana_link', { 23 | description: 'Grafana link to display in HTML report when using influxdbhtml format', 24 | type: 'string', 25 | default: '', 26 | }) 27 | .option('device', { 28 | alias: 'd', 29 | description: 'Hardware to simulate', 30 | choices: Object.keys(sizes), 31 | default: 'desktop', 32 | }) 33 | .option('format', { 34 | alias: 'f', 35 | type: 'string', 36 | description: 'Report format : Possible choices : xlsx (excel), html, influxdb', 37 | }) 38 | .option('headers', { 39 | alias: 'h', 40 | type: 'string', 41 | description: 'Headers HTTP to configure to analyze url', 42 | }) 43 | .option('headless', { 44 | type: 'boolean', 45 | description: 'Option to enable or disable web browser headless mode', 46 | default: true, 47 | }) 48 | .option('influxdb_bucket', { 49 | type: 'string', 50 | }) 51 | .option('influxdb_hostname', { 52 | type: 'string', 53 | }) 54 | 55 | .option('influxdb_org', { 56 | type: 'string', 57 | }) 58 | .option('influxdb_token', { 59 | type: 'string', 60 | }) 61 | .option('language', { 62 | type: 'string', 63 | description: 'Report language : Possible choices: fr, en', 64 | choices: ['fr', 'en'], 65 | default: 'fr', 66 | }) 67 | .option('login', { 68 | type: 'string', 69 | alias: 'l', 70 | description: 'Path to YAML file with login informations', 71 | }) 72 | .option('max_tab', { 73 | type: 'number', 74 | description: 'Number of concurrent analysis', 75 | default: 40, 76 | }) 77 | .option('mobile', { 78 | type: 'boolean', 79 | description: 'Connection type : mobile or wired', 80 | default: false, 81 | }) 82 | .option('proxy', { 83 | alias: 'p', 84 | type: 'string', 85 | description: 'Path to YAML file with proxy configuration to apply in Chromium', 86 | }) 87 | .option('retry', { 88 | alias: 'r', 89 | type: 'number', 90 | description: 'Number of retry when an analysis of a URL fail', 91 | default: 2, 92 | }) 93 | .option('timeout', { 94 | alias: 't', 95 | type: 'number', 96 | description: 'Timeout for an analysis of a URL in ms', 97 | default: 180000, 98 | }) 99 | .option('worst_pages', { 100 | type: 'number', 101 | description: 'Number of displayed worst pages', 102 | default: 5, 103 | }) 104 | .option('worst_rules', { 105 | type: 'number', 106 | description: 'Number of displayed worst rules', 107 | default: 5, 108 | }); 109 | }, 110 | (argv) => { 111 | analyse_core(argv); 112 | } 113 | ) 114 | .command( 115 | 'parseSitemap [yaml_output_file]', 116 | 'Parse sitemap to a YAML file', 117 | (yargs) => { 118 | yargs 119 | .positional('sitemap_url', { 120 | describe: 'URL to the sitemap.xml file', 121 | }) 122 | .positional('yaml_output_file', { 123 | describe: 'Output file path', 124 | default: 'url.yaml', 125 | }); 126 | }, 127 | (argv) => { 128 | require('./src/commands/sitemapParser.js')(argv); 129 | } 130 | ) 131 | .option('ci', { 132 | type: 'boolean', 133 | description: 'Disable progress bar to work with GitLab CI', 134 | }) 135 | .strict() 136 | .demandCommand() 137 | .help().argv; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greenit-analysis-cli", 3 | "version": "0.2.0", 4 | "description": "", 5 | "main": "index.js", 6 | "bin": { 7 | "greenit": "greenit" 8 | }, 9 | "scripts": { 10 | "postinstall": "node builder.js", 11 | "test": "jest", 12 | "build": "node builder.js", 13 | "start": "node index.js" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@influxdata/influxdb-client": "^1.20.0", 19 | "@influxdata/influxdb-client-apis": "^1.20.0", 20 | "concat-files": "^0.1.1", 21 | "debug": "^4.3.0", 22 | "exceljs": "^4.4.0", 23 | "glob": "^10.3.10", 24 | "mustache": "^4.2.0", 25 | "progress": "^2.0.3", 26 | "puppeteer": "^24.9.0", 27 | "puppeteer-har": "^1.1.2", 28 | "sitemapper": "^3.2.8", 29 | "tough-cookie": "^4.1.3", 30 | "xml2js": "^0.5.0", 31 | "yaml": "^2.6.1", 32 | "yargs": "^17.7.2" 33 | }, 34 | "overrides": { 35 | "chrome-har": { 36 | "debug": "^4.3.0", 37 | "tough-cookie": "^4.1.3" 38 | }, 39 | "sitemapper": { 40 | "xml2js": "^0.5.0" 41 | } 42 | }, 43 | "devDependencies": { 44 | "jest": "^29.7.0", 45 | "jest-expect-message": "^1.1.3" 46 | }, 47 | "jest": { 48 | "setupFilesAfterEnv": [ 49 | "jest-expect-message" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /samples/greenit-url.yml: -------------------------------------------------------------------------------- 1 | # Analyse l'URL collectif.greenit.fr 2 | - name : 'Collectif GreenIT.fr' 3 | url : 'https://collectif.greenit.fr/' 4 | 5 | # Analyse l'URL collectif.greenit.fr/outils.html en spécifiant une condition d'attente via un sélecteur CSS 6 | # Réalise une capture d'écran de la page 7 | - name : 'Les outils du collectif GreenIT.fr avec un waitForSelector' 8 | url : 'https://collectif.greenit.fr/outils.html' 9 | waitForSelector: '#header' 10 | 11 | # Analyse l'URL collectif.greenit.fr/index_en.html en spécifiant une condition d'attente via un XPath 12 | - name : 'Collectif GreenIT.fr en anglais avec un waitForXPath' 13 | url : 'https://collectif.greenit.fr/index_en.html' 14 | waitForXPath: '//section[2]/div/h2' 15 | 16 | - name : 'Collectif GreenIT.fr avec une action de type clic - Avec changement de page' 17 | url : 'https://collectif.greenit.fr/' 18 | screenshot: 'output/screenshots/01-scenario-avec-clic-avant-action.png' 19 | actions: 20 | - name : 'Clic sur Découvrez nos outils' 21 | pageChange: true 22 | type: 'click' 23 | element : 'a[title="Nos outils"]' 24 | timeoutBefore: 1000 25 | waitForSelector: '#header' 26 | screenshot: 'output/screenshots/02-scenario-avec-clic-apres-action.png' 27 | 28 | - name : 'Collectif GreenIT.fr avec une action de type clic - Sans changement de page' 29 | url : 'https://collectif.greenit.fr/' 30 | screenshot: 'output/screenshots/01-scenario-avec-clic-avant-action.png' 31 | actions: 32 | - name : 'Clic sur Découvrez nos outils' 33 | type: 'click' 34 | element : 'a[title="Nos outils"]' 35 | timeoutBefore: 1000 36 | waitForSelector: '#header' 37 | screenshot: 'output/screenshots/02-scenario-avec-clic-apres-action.png' 38 | 39 | 40 | - name : 'ecoconceptionweb.com avec une action de type scroll' 41 | url : 'https://ecoconceptionweb.com/' 42 | actions: 43 | - name : "Scroll auto vers le bas de la page" 44 | type : 'scroll' 45 | 46 | - name : 'ecoconceptionweb.com avec une action de type select' 47 | url : 'https://ecoconceptionweb.com/' 48 | screenshot: 'output/screenshots/10-scenario-avant-select.png' 49 | actions: 50 | - name : "Saisie du choix Proposer dans le select Sujet" 51 | type : 'select' 52 | element : '#subject' 53 | values: ['proposer'] 54 | screenshot: 'output/screenshots/11-scenario-apres-select.png' 55 | timeoutBefore: 2000 56 | 57 | - name : 'Collectif GreenIT.fr - remplissage du formulaire de contact' 58 | url : 'https://collectif.greenit.fr/' 59 | actions: 60 | - name : "Remplir l'email dans le formulaire de contact" 61 | type : 'text' 62 | element: '#form_email' 63 | content: 'john.doe@mail.com' 64 | -------------------------------------------------------------------------------- /src/cli-core/analysis.js: -------------------------------------------------------------------------------- 1 | const PuppeteerHar = require('puppeteer-har'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { harFromMessages } = require('chrome-har'); 5 | const sizes = require('../conf/sizes.js'); 6 | const { createProgressBar } = require('./utils.js'); 7 | const { option } = require('yargs'); 8 | 9 | //Analyse a scenario 10 | async function analyseScenario(browser, pageInformations, options, translator, pageLoadingLabel) { 11 | let scenarioResult = {}; 12 | 13 | const TIMEOUT = options.timeout; 14 | const TAB_ID = options.tabId; 15 | const TRY_NB = options.tryNb || 1; 16 | const DEVICE = options.device || 'desktop'; 17 | const PROXY = options.proxy; 18 | const LANGUAGE = options.language; 19 | 20 | try { 21 | const page = await browser.newPage(); 22 | 23 | // configure proxy in page browser 24 | if (PROXY) { 25 | await page.authenticate({ username: PROXY.user, password: PROXY.password }); 26 | } 27 | 28 | // configure headers http 29 | if (options.headers) { 30 | await page.setExtraHTTPHeaders(options.headers); 31 | } 32 | 33 | await page.setViewport(sizes[DEVICE]); 34 | 35 | // disabling cache 36 | await page.setCacheEnabled(false); 37 | 38 | // Execute actions on page (click, text, ...) 39 | let pages = await startActions(page, pageInformations, TIMEOUT, translator, pageLoadingLabel); 40 | 41 | scenarioResult.pages = pages; 42 | scenarioResult.success = true; 43 | scenarioResult.nbBestPracticesToCorrect = 0; 44 | 45 | // Compute number of times where best practices are not respected 46 | for (let key in scenarioResult.bestPractices) { 47 | if ((scenarioResult.bestPractices[key].complianceLevel || 'A') !== 'A') { 48 | scenarioResult.nbBestPracticesToCorrect++; 49 | } 50 | } 51 | } catch (error) { 52 | console.error(`Error while analyzing URL ${pageInformations.url} : `, error); 53 | scenarioResult.success = false; 54 | } 55 | const date = new Date(); 56 | scenarioResult.date = `${date.toLocaleDateString(LANGUAGE)} ${date.toLocaleTimeString(LANGUAGE)}`; 57 | scenarioResult.pageInformations = pageInformations; 58 | scenarioResult.tryNb = TRY_NB; 59 | scenarioResult.tabId = TAB_ID; 60 | scenarioResult.index = options.index; 61 | scenarioResult.url = pageInformations.url; 62 | 63 | return scenarioResult; 64 | } 65 | 66 | async function waitPageLoading(page, pageInformations, TIMEOUT) { 67 | if (pageInformations.waitForSelector) { 68 | await page.locator(pageInformations.waitForSelector).setTimeout(TIMEOUT).wait(); 69 | } else if (pageInformations.waitForXPath) { 70 | await page.locator(`::-p-xpath(${pageInformations.waitForXPath})`).setTimeout(TIMEOUT).wait(); 71 | } else if (isValidWaitForNavigation(pageInformations.waitForNavigation)) { 72 | await page.waitForNavigation({ waitUntil: pageInformations.waitForNavigation, timeout: TIMEOUT }); 73 | } else if (pageInformations.waitForTimeout) { 74 | await waitForTimeout(pageInformations.waitForTimeout); 75 | } 76 | } 77 | 78 | function waitForTimeout(milliseconds) { 79 | return new Promise((r) => setTimeout(r, milliseconds)); 80 | } 81 | 82 | function isValidWaitForNavigation(waitUntilParam) { 83 | return ( 84 | waitUntilParam && 85 | ('load' === waitUntilParam || 86 | 'domcontentloaded' === waitUntilParam || 87 | 'networkidle0' === waitUntilParam || 88 | 'networkidle2' === waitUntilParam) 89 | ); 90 | } 91 | 92 | /** 93 | * Execute scenario configured actions 94 | * @param {*} page selenium page 95 | * @param {*} actions list of action 96 | * @param {*} TIMEOUT timeout 97 | * @param {*} pptrHar analyze data 98 | * @param {*} name page name 99 | * @returns 100 | */ 101 | async function startActions(page, pageInformations, timeout, translator, pageLoadingLabel) { 102 | //get har file 103 | const pptrHar = new PuppeteerHar(page); 104 | await pptrHar.start(); 105 | 106 | // do first action : go to the URL 107 | await doFirstAction(page, pageInformations, timeout); 108 | 109 | // do initial snapshot of data before actions 110 | let actionResult = await doAnalysis(page, pptrHar, pageLoadingLabel, translator); 111 | 112 | let actionsResultsForAPage = []; 113 | actionsResultsForAPage.push(actionResult); 114 | 115 | let currentPage = {}; 116 | currentPage.name = actionResult.name; 117 | currentPage.bestPractices = actionResult.bestPractices; 118 | currentPage.nbRequest = actionResult.nbRequest; 119 | currentPage.responsesSize = actionResult.responsesSize; 120 | currentPage.responsesSizeUncompress = actionResult.responsesSizeUncompress; 121 | 122 | const pagesResults = []; 123 | const actions = pageInformations.actions; 124 | if (actions) { 125 | for (let index = 0; index < actions.length; index++) { 126 | let action = actions[index]; 127 | let actionName = action.name || index + 1; 128 | 129 | // Add some wait in order to prevent green-it script to cancel future measure 130 | // default timeout : 1000ms 131 | let timeoutBefore = action.timeoutBefore > 0 ? action.timeoutBefore : 1000; 132 | await waitForTimeout(timeoutBefore); 133 | 134 | currentPage.url = page.url(); 135 | 136 | if (action.pageChange) { 137 | // Save page analyse 138 | currentPage.actions = actionsResultsForAPage; 139 | pagesResults.push({ ...currentPage }); 140 | 141 | // Reinit variables 142 | actionsResultsForAPage = []; 143 | currentPage = {}; 144 | currentPage.name = actionName; 145 | currentPage.nbRequest = 0; 146 | currentPage.responsesSize = 0; 147 | currentPage.responsesSizeUncompress = 0; 148 | 149 | // Clean up HAR history 150 | pptrHar.network_events = []; 151 | pptrHar.response_body_promises = []; 152 | } 153 | 154 | try { 155 | // Do asked action 156 | await doAction(page, action, actionName, timeout); 157 | } finally { 158 | if (action.screenshot) { 159 | await takeScreenshot(page, action.screenshot); 160 | } 161 | } 162 | 163 | actionResult = await doAnalysis(page, pptrHar, actionName, translator); 164 | currentPage.bestPractices = actionResult.bestPractices; 165 | 166 | // Statistics of current page = statistics of last action (e.g. statistics sum of all actions) 167 | currentPage.nbRequest = actionResult.nbRequest; 168 | currentPage.responsesSize = actionResult.responsesSize; 169 | currentPage.responsesSizeUncompress = actionResult.responsesSizeUncompress; 170 | 171 | actionsResultsForAPage.push(actionResult); 172 | } 173 | } 174 | 175 | currentPage.url = page.url(); 176 | currentPage.actions = actionsResultsForAPage; 177 | pagesResults.push(currentPage); 178 | 179 | await pptrHar.stop(); 180 | page.close(); 181 | 182 | return pagesResults; 183 | } 184 | 185 | async function doFirstAction(page, pageInformations, timeout) { 186 | try { 187 | //go to url 188 | await page.goto(pageInformations.url, { timeout: timeout }); 189 | 190 | // waiting for page to load 191 | await waitPageLoading(page, pageInformations, timeout); 192 | } finally { 193 | // Take screenshot (even if the page fails to load) 194 | if (pageInformations.screenshot) { 195 | await takeScreenshot(page, pageInformations.screenshot); 196 | } 197 | } 198 | } 199 | 200 | async function doAction(page, action, actionName, timeout) { 201 | if (action.type === 'click') { 202 | await page.click(action.element); 203 | await waitPageLoading(page, action, timeout); 204 | } else if (action.type === 'text') { 205 | await page.type(action.element, action.content, { delay: 100 }); 206 | await waitPageLoading(page, action, timeout); 207 | } else if (action.type === 'select') { 208 | let args = [action.element].concat(action.values); 209 | // equivalent to : page.select(action.element, action.values[0], action.values[1], ...) 210 | await page.select.apply(page, args); 211 | await waitPageLoading(page, action, timeout); 212 | } else if (action.type === 'scroll') { 213 | await scrollToBottom(page); 214 | await waitPageLoading(page, action, timeout); 215 | } else if (action.type === 'press') { 216 | await page.keyboard.press(action.key); 217 | await waitPageLoading(page, action, timeout); 218 | } else { 219 | console.log("Unknown action for '" + actionName + "' : " + action.type); 220 | } 221 | } 222 | 223 | function isNetworkEventGeneratedByAnalysis(initiator) { 224 | return ( 225 | initiator?.type === 'script' && 226 | initiator?.stack?.callFrames?.some((callFrame) => callFrame.url.includes('greenItBundle.js')) 227 | ); 228 | } 229 | 230 | async function doAnalysis(page, pptrHar, name, translator) { 231 | // remove network events generated by the analysis (remove all events that have initiator.type=script generated by greenItBundle.js) 232 | pptrHar.network_events = pptrHar.network_events.filter( 233 | (network_event) => !isNetworkEventGeneratedByAnalysis(network_event?.params?.initiator) 234 | ); 235 | 236 | //get ressources 237 | const harObj = await harStatus(pptrHar); 238 | const client = await page.target().createCDPSession(); 239 | const ressourceTree = await client.send('Page.getResourceTree'); 240 | await client.detach(); 241 | 242 | await injectChromeObjectInPage(page, translator); 243 | 244 | //add script, get run, then remove it to not interfere with the analysis 245 | const script = await page.addScriptTag({ 246 | path: path.join(__dirname, '../../dist/greenItBundle.js'), 247 | }); 248 | await script.evaluate((x) => x.remove()); 249 | 250 | //pass node object to browser 251 | await page.evaluate((x) => (har = x), harObj.log); 252 | await page.evaluate((x) => (resources = x), ressourceTree.frameTree.resources); 253 | 254 | //launch analyse 255 | const result = await page.evaluate(() => launchAnalyse()); 256 | if (name) { 257 | result.name = name; 258 | } 259 | 260 | return result; 261 | } 262 | 263 | async function injectChromeObjectInPage(page, translator) { 264 | // replace chrome.i18n.getMessage call by i18n custom implementation working in page 265 | // fr is default catalog 266 | await page.evaluate( 267 | (language_array) => 268 | (chrome = { 269 | i18n: { 270 | getMessage: function (message, parameters = []) { 271 | return language_array[message].replace(/%s/g, function () { 272 | // parameters is string or array 273 | return Array.isArray(parameters) ? parameters.shift() : parameters; 274 | }); 275 | }, 276 | }, 277 | }), 278 | translator.getCatalog() 279 | ); 280 | } 281 | 282 | /** 283 | * @returns {Promise} 284 | */ 285 | async function harStatus(pptrHar) { 286 | await Promise.all(pptrHar.response_body_promises); 287 | return harFromMessages(pptrHar.page_events.concat(pptrHar.network_events), { 288 | includeTextFromResponseBody: pptrHar.saveResponse, 289 | }); 290 | } 291 | 292 | async function scrollToBottom(page) { 293 | await page.evaluate(async () => { 294 | await new Promise((resolve, reject) => { 295 | var distance = 400; 296 | var timeoutBetweenScroll = 1500; 297 | var totalHeight = 0; 298 | var timer = setInterval(() => { 299 | var scrollHeight = document.body.scrollHeight; 300 | window.scrollBy(0, distance); 301 | totalHeight += distance; 302 | if (totalHeight >= scrollHeight) { 303 | clearInterval(timer); 304 | resolve(); 305 | } 306 | }, timeoutBetweenScroll); 307 | }); 308 | }); 309 | } 310 | 311 | async function takeScreenshot(page, screenshotPath) { 312 | // create screenshot folder if not exists 313 | const folder = path.dirname(screenshotPath); 314 | if (!fs.existsSync(folder)) { 315 | fs.mkdirSync(folder, { recursive: true }); 316 | } 317 | // remove old screenshot 318 | if (fs.existsSync(screenshotPath)) { 319 | fs.unlinkSync(screenshotPath); 320 | } 321 | // take screenshot 322 | await page.screenshot({ path: screenshotPath }); 323 | } 324 | 325 | //handle login 326 | async function login(browser, loginInformations, options) { 327 | //use the tab that opens with the browser 328 | const page = (await browser.pages())[0]; 329 | //go to login page 330 | await page.goto(loginInformations.url); 331 | //ensure page is loaded 332 | await page.waitForSelector(loginInformations.loginButtonSelector); 333 | //simulate user waiting before typing login and password 334 | await waitForTimeout(1000); 335 | //complete fields 336 | for (let index = 0; index < loginInformations.fields.length; index++) { 337 | let field = loginInformations.fields[index]; 338 | await page.type(field.selector, field.value); 339 | await waitForTimeout(500); 340 | } 341 | //simulate user waiting before clicking on button 342 | await waitForTimeout(1000); 343 | //click login button 344 | await page.click(loginInformations.loginButtonSelector); 345 | 346 | if (loginInformations.screenshot) { 347 | await takeScreenshot(page, loginInformations.screenshot); 348 | } 349 | //make sure to not wait for the full authentification procedure 350 | // waiting for page to load 351 | await waitPageLoading(page, loginInformations, options.timeout); 352 | } 353 | 354 | //Core 355 | async function createJsonReports(browser, pagesInformations, options, proxy, headers, translator) { 356 | //Timeout for an analysis 357 | const TIMEOUT = options.timeout; 358 | //Concurent tab 359 | const MAX_TAB = options.max_tab; 360 | //Nb of retry before dropping analysis 361 | const RETRY = options.retry; 362 | //Device to emulate 363 | const DEVICE = options.device; 364 | //Language 365 | const LANGUAGE = options.language; 366 | // JSON output directory 367 | const JSON_SUBRESULTS_DIRECTORY = path.join(__dirname, '../../', path.dirname(options.report_output_file), 'json'); 368 | 369 | //initialise progress bar 370 | const progressBar = createProgressBar(options, pagesInformations.length + 2, 'Analysing', 'Analysing ...'); 371 | let asyncFunctions = []; 372 | let results; 373 | let resultId = 1; 374 | let index = 0; 375 | let reports = []; 376 | let writeList = []; 377 | 378 | let convert = []; 379 | 380 | for (let i = 0; i < MAX_TAB; i++) { 381 | convert[i] = i; 382 | } 383 | 384 | //create directory for subresults 385 | if (fs.existsSync(JSON_SUBRESULTS_DIRECTORY)) { 386 | fs.rmSync(JSON_SUBRESULTS_DIRECTORY, { recursive: true }); 387 | } 388 | fs.mkdirSync(JSON_SUBRESULTS_DIRECTORY, { recursive: true }); 389 | 390 | //Set translator language 391 | const pageLoadingLabel = translator.translate('pageLoading'); 392 | 393 | //Asynchronous analysis with MAX_TAB open simultaneously to json 394 | for (let i = 0; i < MAX_TAB && index < pagesInformations.length; i++) { 395 | asyncFunctions.push( 396 | analyseScenario( 397 | browser, 398 | pagesInformations[index], 399 | { 400 | device: DEVICE, 401 | timeout: TIMEOUT, 402 | tabId: i, 403 | proxy: proxy, 404 | headers: headers, 405 | index: index, 406 | language: LANGUAGE, 407 | }, 408 | translator, 409 | pageLoadingLabel 410 | ) 411 | ); 412 | index++; 413 | } 414 | 415 | while (asyncFunctions.length != 0) { 416 | results = await Promise.race(asyncFunctions); 417 | if (!results.success && results.tryNb <= RETRY) { 418 | asyncFunctions.splice( 419 | convert[results.tabId], 420 | 1, 421 | analyseScenario( 422 | browser, 423 | results.pageInformations, 424 | { 425 | device: DEVICE, 426 | timeout: TIMEOUT, 427 | tabId: results.tabId, 428 | tryNb: results.tryNb + 1, 429 | proxy: proxy, 430 | headers: headers, 431 | index: results.index, 432 | language: LANGUAGE, 433 | }, 434 | translator, 435 | pageLoadingLabel 436 | ) 437 | ); // convert is NEEDED, variable size array 438 | } else { 439 | let filePath = path.resolve(JSON_SUBRESULTS_DIRECTORY, `${resultId}.json`); 440 | writeList.push(fs.promises.writeFile(filePath, JSON.stringify(results))); 441 | reports.push({ name: `${resultId}`, path: filePath }); 442 | if (progressBar) { 443 | progressBar.tick(); 444 | } else { 445 | console.log(`${resultId}/${pagesInformations.length}`); 446 | } 447 | resultId++; 448 | if (index == pagesInformations.length) { 449 | asyncFunctions.splice(convert[results.tabId], 1); // convert is NEEDED, varialbe size array 450 | for (let i = results.tabId + 1; i < convert.length; i++) { 451 | convert[i] = convert[i] - 1; 452 | } 453 | } else { 454 | asyncFunctions.splice( 455 | results.tabId, 456 | 1, 457 | analyseScenario( 458 | browser, 459 | pagesInformations[index], 460 | { 461 | device: DEVICE, 462 | timeout: TIMEOUT, 463 | tabId: results.tabId, 464 | proxy: proxy, 465 | headers: headers, 466 | index, 467 | language: LANGUAGE, 468 | }, 469 | translator, 470 | pageLoadingLabel 471 | ) 472 | ); // No need for convert, fixed size array 473 | index++; 474 | } 475 | } 476 | } 477 | 478 | //wait for all file to be written 479 | await Promise.all(writeList); 480 | //results to xlsx file 481 | if (progressBar) { 482 | progressBar.tick(); 483 | } else { 484 | console.log('Analyse done'); 485 | } 486 | return reports; 487 | } 488 | 489 | module.exports = { 490 | createJsonReports, 491 | login, 492 | }; 493 | -------------------------------------------------------------------------------- /src/cli-core/influxdb.js: -------------------------------------------------------------------------------- 1 | const { InfluxDB, Point, HttpError } = require('@influxdata/influxdb-client'); 2 | const fs = require('fs'); 3 | const { createProgressBar } = require('./utils'); 4 | 5 | async function write(reports, options) { 6 | if (!options.influxdb_hostname) { 7 | throw `You must define an InfluxDB hostname.`; 8 | } 9 | if (!options.influxdb_token) { 10 | throw `You must define an InfluxDB token.`; 11 | } 12 | if (!options.influxdb_bucket) { 13 | throw `You must define an InfluxDB bucket name.`; 14 | } 15 | if (!options.influxdb_org) { 16 | throw `You must define an InfluxDB organisation.`; 17 | } 18 | 19 | const url = options.influxdb_hostname; 20 | 21 | //initialise progress bar 22 | const progressBar = createProgressBar( 23 | options, 24 | reports.length + 2, 25 | 'Push to InfluxDB', 26 | 'Push report to InfluxDB ...' 27 | ); 28 | 29 | // initialise client 30 | const client = new InfluxDB({ url: options.influxdb_hostname, token: options.influxdb_token }); 31 | const writeApi = client.getWriteApi(options.influxdb_org, options.influxdb_bucket); 32 | 33 | // create points from reports 34 | const points = []; 35 | const date = new Date(); 36 | reports.forEach((file) => { 37 | const scenario = JSON.parse(fs.readFileSync(file.path).toString()); 38 | const scenarioName = scenario.pageInformations.name; 39 | 40 | if (scenario.pages) { 41 | scenario.pages.forEach((page) => { 42 | let hostname = scenario.url.split('/')[2]; 43 | 44 | page.actions.forEach((action) => { 45 | let point = new Point('eco_index'); 46 | point 47 | .tag('scenarioName', scenarioName) 48 | .tag('pageName', page.name) 49 | .tag('hostname', hostname) 50 | .tag('actionName', action.name) 51 | .stringField('url', page.name) 52 | .stringField('hostname', hostname) 53 | .stringField('grade', action.grade) 54 | .intField('ecoindex', action.ecoIndex) 55 | .floatField('water', action.waterConsumption) 56 | .floatField('ges', action.greenhouseGasesEmission) 57 | .floatField('domSize', action.domSize) 58 | .floatField('nbRequest', action.nbRequest) 59 | .floatField('responsesSize', action.responsesSize / 1000) 60 | .floatField('responsesSizeUncompress', action.responsesSizeUncompress / 1000) 61 | .stringField('date', date); 62 | 63 | points.push(point); 64 | }); 65 | 66 | if (progressBar) progressBar.tick(); 67 | }); 68 | } 69 | }); 70 | 71 | //upload points and close connexion 72 | writeApi.writePoints(points); 73 | 74 | writeApi 75 | .close() 76 | .then(() => { 77 | if (progressBar) progressBar.tick(); 78 | }) 79 | .catch((e) => { 80 | console.log('Writing to influx failed\n'); 81 | console.error(e); 82 | if (e instanceof HttpError && e.statusCode === 401) { 83 | console.log(`The InfluxDB database: ${bucket} doesn't exist.`); 84 | } 85 | }); 86 | } 87 | 88 | module.exports = { 89 | write, 90 | }; 91 | -------------------------------------------------------------------------------- /src/cli-core/reportExcel.js: -------------------------------------------------------------------------------- 1 | const ExcelJS = require('exceljs'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const utils = require('./utils'); 5 | 6 | //create xlsx report for all the analysed pages and recap on the first sheet 7 | async function create_XLSX_report(reportObject, options, translator) { 8 | const OUTPUT_FILE = path.resolve(options.report_output_file); 9 | const fileList = reportObject.reports; 10 | const globalReport = reportObject.globalReport; 11 | 12 | //initialise progress bar 13 | const progressBar = utils.createProgressBar( 14 | options, 15 | fileList.length + 2, 16 | 'Create Excel report', 17 | 'Creating XLSX report ...' 18 | ); 19 | 20 | const wb = new ExcelJS.Workbook(); 21 | //Creating the recap page 22 | const globalSheet = wb.addWorksheet(globalReport.name); 23 | const globalReport_data = JSON.parse(fs.readFileSync(globalReport.path).toString()); 24 | const globalSheet_data = [ 25 | [translator.translate('date'), globalReport_data.date], 26 | [translator.translate('hostname'), globalReport_data.hostname], 27 | [translator.translate('platform'), globalReport_data.device], 28 | [translator.translate('connection'), globalReport_data.connection], 29 | [translator.translate('grade'), globalReport_data.grade], 30 | [translator.translate('ecoIndex'), globalReport_data.ecoIndex], 31 | [translator.translate('nbPages'), globalReport_data.nbPages], 32 | [translator.translate('timeout'), globalReport_data.timeout], 33 | [translator.translate('nbConcAnalysis'), globalReport_data.maxTab], 34 | [translator.translate('nbAdditionalAttemps'), globalReport_data.retry], 35 | [translator.translate('nbErrors'), globalReport_data.errors.length], 36 | [translator.translate('analysisErrors')], 37 | ]; 38 | globalReport_data.errors.forEach((element) => { 39 | globalSheet_data.push([element.nb, element.url]); 40 | }); 41 | globalSheet_data.push([], [translator.translate('priorityPages')]); 42 | globalReport_data.worstPages.forEach((element) => { 43 | globalSheet_data.push([ 44 | element.nb, 45 | element.url, 46 | translator.translate('grade'), 47 | element.grade, 48 | translator.translate('ecoIndex'), 49 | element.ecoIndex, 50 | ]); 51 | }); 52 | globalSheet_data.push([], [translator.translate('rulesToApply')]); 53 | globalReport_data.worstRules.forEach((elem) => { 54 | globalSheet_data.push([elem]); 55 | }); 56 | //add data to the recap sheet 57 | globalSheet.addRows(globalSheet_data); 58 | globalSheet.getCell('B5').fill = { 59 | type: 'pattern', 60 | pattern: 'solid', 61 | fgColor: { argb: getGradeColor(globalReport_data.grade) }, 62 | }; 63 | 64 | if (progressBar) progressBar.tick(); 65 | 66 | //Creating one report sheet per file 67 | fileList.forEach((file) => { 68 | const sheet_name = file.name; 69 | let obj = JSON.parse(fs.readFileSync(file.path).toString()); 70 | 71 | if (obj.pages) { 72 | obj.pages.forEach((page) => { 73 | let sheet_data; 74 | if (page.actions) { 75 | // Prepare data 76 | sheet_data = [ 77 | [translator.translate('url'), obj.pageInformations.url], 78 | [translator.translate('grade'), page.actions[page.actions.length - 1].grade], 79 | [translator.translate('ecoIndex'), page.actions[page.actions.length - 1].ecoIndex], 80 | [translator.translate('water'), page.actions[page.actions.length - 1].waterConsumption], 81 | [ 82 | translator.translate('greenhouseGasesEmission'), 83 | page.actions[page.actions.length - 1].greenhouseGasesEmission, 84 | ], 85 | [translator.translate('domSize'), page.actions[page.actions.length - 1].domSize], 86 | [ 87 | translator.translate('pageSize'), 88 | `${Math.round(page.actions[page.actions.length - 1].responsesSize / 1000)} (${Math.round( 89 | page.actions[page.actions.length - 1].responsesSizeUncompress / 1000 90 | )})`, 91 | ], 92 | [translator.translate('nbRequests'), page.actions[page.actions.length - 1].nbRequest], 93 | [translator.translate('nbPlugins'), page.actions[page.actions.length - 1].pluginsNumber], 94 | [ 95 | translator.translate('nbCssFiles'), 96 | page.actions[page.actions.length - 1].printStyleSheetsNumber, 97 | ], 98 | [ 99 | translator.translate('nbInlineCss'), 100 | page.actions[page.actions.length - 1].inlineStyleSheetsNumber, 101 | ], 102 | [translator.translate('nbEmptySrc'), page.actions[page.actions.length - 1].emptySrcTagNumber], 103 | [ 104 | translator.translate('nbInlineJs'), 105 | page.actions[page.actions.length - 1].inlineJsScriptsNumber, 106 | ], 107 | ]; 108 | } 109 | 110 | sheet_data.push([], [translator.translate('resizedImage')]); 111 | for (let elem in page.actions[page.actions.length - 1].imagesResizedInBrowser) { 112 | sheet_data.push([page.actions[page.actions.length - 1].imagesResizedInBrowser[elem].src]); 113 | } 114 | 115 | sheet_data.push([], [translator.translate('bestPractices')]); 116 | for (let key in page.bestPractices) { 117 | sheet_data.push([key, page.bestPractices[key].complianceLevel || 'A']); 118 | } 119 | //Create sheet 120 | let sheet = wb.addWorksheet(`${sheet_name} - ${page.name}`); 121 | sheet.addRows(sheet_data); 122 | sheet.getCell('B2').fill = { 123 | type: 'pattern', 124 | pattern: 'solid', 125 | fgColor: { argb: getGradeColor(obj.grade) }, 126 | }; 127 | }); 128 | } else { 129 | // Prepare data 130 | const sheet_data = [ 131 | [translator.translate('url'), obj.pageInformations.url], 132 | [translator.translate('grade'), obj.grade], 133 | [translator.translate('ecoIndex'), obj.ecoIndex], 134 | [translator.translate('water'), obj.waterConsumption], 135 | [translator.translate('greenhouseGasesEmission'), obj.greenhouseGasesEmission], 136 | [translator.translate('domSize'), obj.domSize], 137 | [ 138 | translator.translate('pageSize'), 139 | `${Math.round(obj.responsesSize / 1000)} (${Math.round(obj.responsesSizeUncompress / 1000)})`, 140 | ], 141 | [translator.translate('nbRequests'), obj.nbRequest], 142 | [translator.translate('nbPlugins'), obj.pluginsNumber], 143 | [translator.translate('nbCssFiles'), obj.printStyleSheetsNumber], 144 | [translator.translate('nbInlineCss'), obj.inlineStyleSheetsNumber], 145 | [translator.translate('nbEmptySrc'), obj.emptySrcTagNumber], 146 | [translator.translate('nbInlineJs'), obj.inlineJsScriptsNumber], 147 | ]; 148 | //Create sheet 149 | let sheet = wb.addWorksheet(sheet_name); 150 | sheet.addRows(sheet_data); 151 | sheet.getCell('B2').fill = { 152 | type: 'pattern', 153 | pattern: 'solid', 154 | fgColor: { argb: getGradeColor(obj.grade) }, 155 | }; 156 | } 157 | if (progressBar) progressBar.tick(); 158 | }); 159 | //save report 160 | try { 161 | await wb.xlsx.writeFile(OUTPUT_FILE); 162 | } catch (error) { 163 | throw ` report_output_file : Path "${OUTPUT_FILE}" cannot be reached.`; 164 | } 165 | } 166 | 167 | // Get color code by grade 168 | function getGradeColor(grade) { 169 | if (grade == 'A') return 'ff009b4f'; 170 | if (grade == 'B') return 'ff30b857'; 171 | if (grade == 'C') return 'ffcbda4b'; 172 | if (grade == 'D') return 'fffbe949'; 173 | if (grade == 'E') return 'ffffca3e'; 174 | if (grade == 'F') return 'ffff9349'; 175 | return 'fffe002c'; 176 | } 177 | 178 | module.exports = { 179 | create_XLSX_report, 180 | }; 181 | -------------------------------------------------------------------------------- /src/cli-core/reportGlobal.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { getEcoIndexGrade, getGradeEcoIndex, createProgressBar } = require('./utils'); 4 | 5 | // keep track of worst pages based on ecoIndex 6 | function worstPagesHandler(number) { 7 | return (obj, table) => { 8 | let index; 9 | for (index = 0; index < table.length; index++) { 10 | if (obj.ecoIndex < table[index].ecoIndex) break; 11 | } 12 | let addObj = { 13 | nb: obj.nb, 14 | url: obj.pageInformations.url, 15 | grade: obj.grade, 16 | ecoIndex: obj.ecoIndex, 17 | }; 18 | table.splice(index, 0, addObj); 19 | if (table.length > number) table.pop(); 20 | return table; 21 | }; 22 | } 23 | 24 | //keep track of the least followed rule based on grade 25 | function handleWorstRule(bestPracticesTotal, number) { 26 | let table = []; 27 | for (let key in bestPracticesTotal) { 28 | table.push({ name: key, total: bestPracticesTotal[key] }); 29 | } 30 | return table 31 | .sort((a, b) => a.total - b.total) 32 | .slice(0, number) 33 | .map((obj) => obj.name); 34 | } 35 | 36 | async function create_global_report(reports, options, translator) { 37 | //Timeout for an analysis 38 | const TIMEOUT = options.timeout || 'No data'; 39 | //Concurent tab 40 | const MAX_TAB = options.max_tab || 'No data'; 41 | //Nb of retry before dropping analysis 42 | const RETRY = options.retry || 'No data'; 43 | //Nb of displayed worst pages 44 | const WORST_PAGES = options.worst_pages; 45 | //Nb of displayed worst rules 46 | const WORST_RULES = options.worst_rules; 47 | // JSON output directory 48 | const JSON_SUBRESULTS_DIRECTORY = path.join(__dirname, '../../', path.dirname(options.report_output_file), 'json'); 49 | 50 | const DEVICE = options.device; 51 | const LANGUAGE = options.language; 52 | 53 | let handleWorstPages = worstPagesHandler(WORST_PAGES); 54 | 55 | //initialise progress bar 56 | const progressBar = createProgressBar( 57 | options, 58 | reports.length + 2, 59 | 'Create Global report', 60 | 'Creating global report ...' 61 | ); 62 | 63 | let eco = 0; //future average 64 | let worstEcoIndexes = [null, null]; 65 | let err = []; 66 | let hostname; 67 | let worstPages = []; 68 | let bestPracticesTotal = {}; 69 | let nbBestPracticesToCorrect = 0; 70 | 71 | //Creating one report sheet per file 72 | reports.forEach((file) => { 73 | let obj = JSON.parse(fs.readFileSync(file.path).toString()); 74 | if (!hostname) hostname = obj.pageInformations.url.split('/')[2]; 75 | obj.nb = parseInt(file.name); 76 | //handle potential failed analyse 77 | if (obj.success) { 78 | eco += obj.ecoIndex; 79 | const pageWorstEcoIndexes = getWorstEcoIndexes(obj); 80 | if (!worstEcoIndexes[0] || worstEcoIndexes[0].ecoIndex > pageWorstEcoIndexes[0].ecoIndex) { 81 | // update global worst ecoindex 82 | worstEcoIndexes[0] = { ...pageWorstEcoIndexes[0] }; 83 | } 84 | if (!worstEcoIndexes[1] || worstEcoIndexes[1].ecoIndex > pageWorstEcoIndexes[1].ecoIndex) { 85 | // update global worst ecoindex 86 | worstEcoIndexes[1] = { ...pageWorstEcoIndexes[1] }; 87 | } 88 | 89 | nbBestPracticesToCorrect += obj.nbBestPracticesToCorrect; 90 | handleWorstPages(obj, worstPages); 91 | obj.pages.forEach((page) => { 92 | if (page.bestPractices) { 93 | for (let key in page.bestPractices) { 94 | bestPracticesTotal[key] = bestPracticesTotal[key] || 0; 95 | bestPracticesTotal[key] += getGradeEcoIndex(page.bestPractices[key].complianceLevel || 'A'); 96 | } 97 | } 98 | }); 99 | } else { 100 | err.push({ 101 | nb: obj.nb, 102 | url: obj.pageInformations.url, 103 | grade: obj.grade, 104 | ecoIndex: obj.ecoIndex, 105 | }); 106 | } 107 | if (progressBar) progressBar.tick(); 108 | }); 109 | 110 | let proxy = null; 111 | if (options.proxy?.server) { 112 | const { protocol, hostname: host, port } = new URL(options.proxy.server); 113 | const { user, password } = options.proxy; 114 | const auth = user && password ? { username: user, password } : undefined; 115 | proxy = { 116 | protocol: protocol.slice(0, -1), 117 | host, 118 | port, 119 | auth, 120 | }; 121 | } 122 | //Add info the recap sheet 123 | //Prepare data 124 | const date = new Date(); 125 | eco = reports.length - err.length != 0 ? Math.round(eco / (reports.length - err.length)) : 'No data'; //Average EcoIndex 126 | let globalSheet_data = { 127 | date: `${date.toLocaleDateString(LANGUAGE)} ${date.toLocaleTimeString(LANGUAGE)}`, 128 | hostname: hostname, 129 | device: DEVICE, 130 | connection: options.mobile ? translator.translate('mobile') : translator.translate('wired'), 131 | grade: getEcoIndexGrade(eco), 132 | ecoIndex: eco, 133 | worstEcoIndexes: worstEcoIndexes, 134 | nbScenarios: reports.length, 135 | timeout: parseInt(TIMEOUT), 136 | maxTab: parseInt(MAX_TAB), 137 | retry: parseInt(RETRY), 138 | errors: err, 139 | worstPages: worstPages, 140 | worstRules: handleWorstRule(bestPracticesTotal, WORST_RULES), 141 | nbBestPracticesToCorrect: nbBestPracticesToCorrect, 142 | }; 143 | 144 | if (progressBar) progressBar.tick(); 145 | 146 | //save report 147 | let filePath = path.join(JSON_SUBRESULTS_DIRECTORY, 'globalReport.json'); 148 | try { 149 | fs.writeFileSync(filePath, JSON.stringify(globalSheet_data)); 150 | } catch (error) { 151 | throw ` Global report : Path "${filePath}" cannot be reached.`; 152 | } 153 | return { 154 | globalReport: { 155 | name: 'Global Report', 156 | path: filePath, 157 | }, 158 | reports, 159 | }; 160 | } 161 | 162 | function getWorstEcoIndexes(obj) { 163 | let worstEcoIndexes = [null, null]; 164 | obj.pages.forEach((page) => { 165 | worstEcoIndexes = worstEcoIndexes.map((worstEcoIndex, i) => { 166 | if (page.actions.length === 1 || i === 0) { 167 | // first = last if only one value, otherwise return value of first action 168 | return getWorstEcoIndex(page.actions[0].ecoIndex, worstEcoIndex); 169 | } else if (i === 1) { 170 | // return value of last action 171 | if (page.actions[page.actions.length - 1].ecoIndex) { 172 | return getWorstEcoIndex(page.actions[page.actions.length - 1].ecoIndex, worstEcoIndex); 173 | } 174 | } 175 | return worstEcoIndex; 176 | }); 177 | }); 178 | 179 | return worstEcoIndexes.map((worstEcoIndex) => ({ 180 | ecoIndex: worstEcoIndex, 181 | grade: getEcoIndexGrade(worstEcoIndex), 182 | })); 183 | } 184 | 185 | function getWorstEcoIndex(current, worst) { 186 | if (!worst || worst > current) { 187 | worst = current; 188 | } 189 | return worst; 190 | } 191 | 192 | module.exports = { 193 | create_global_report, 194 | }; 195 | -------------------------------------------------------------------------------- /src/cli-core/reportHtml.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Mustache = require('mustache'); 4 | const rules = require('../conf/rules'); 5 | const utils = require('./utils'); 6 | 7 | /** 8 | * Css class best practices 9 | */ 10 | const cssBestPractices = { 11 | A: 'checkmark-success', 12 | B: 'close-warning', 13 | C: 'close-error', 14 | }; 15 | const bestPracticesKey = [ 16 | 'AddExpiresOrCacheControlHeaders', 17 | 'CompressHttp', 18 | 'DomainsNumber', 19 | 'DontResizeImageInBrowser', 20 | 'EmptySrcTag', 21 | 'ExternalizeCss', 22 | 'ExternalizeJs', 23 | 'HttpError', 24 | 'HttpRequests', 25 | 'ImageDownloadedNotDisplayed', 26 | 'JsValidate', 27 | 'MaxCookiesLength', 28 | 'MinifiedCss', 29 | 'MinifiedJs', 30 | 'NoCookieForStaticRessources', 31 | 'NoRedirect', 32 | 'OptimizeBitmapImages', 33 | 'OptimizeSvg', 34 | 'Plugins', 35 | 'PrintStyleSheet', 36 | 'SocialNetworkButton', 37 | 'StyleSheets', 38 | 'UseETags', 39 | 'UseStandardTypefaces', 40 | ]; 41 | 42 | //create html report for all the analysed pages and recap on the first sheet 43 | async function create_html_report(reportObject, options, translator, grafanaLinkPresent) { 44 | const OUTPUT_FILE = path.resolve(options.report_output_file); 45 | const fileList = reportObject.reports; 46 | const globalReport = reportObject.globalReport; 47 | 48 | // initialise progress bar 49 | const progressBar = utils.createProgressBar( 50 | options, 51 | fileList.length + 2, 52 | 'Create HTML report', 53 | 'Creating HTML report ...' 54 | ); 55 | 56 | // Read all reports 57 | const { allReportsVariables, waterTotal, greenhouseGasesEmissionTotal } = readAllReports( 58 | fileList, 59 | options.grafana_link, 60 | translator 61 | ); 62 | 63 | // Read global report 64 | const globalReportVariables = readGlobalReport( 65 | globalReport.path, 66 | allReportsVariables, 67 | waterTotal, 68 | greenhouseGasesEmissionTotal, 69 | grafanaLinkPresent, 70 | translator 71 | ); 72 | 73 | // write global report 74 | writeGlobalReport(globalReportVariables, OUTPUT_FILE, progressBar, translator); 75 | 76 | // write all reports 77 | const outputFolder = path.dirname(OUTPUT_FILE); 78 | writeAllReports(allReportsVariables, outputFolder, progressBar, translator); 79 | } 80 | 81 | /** 82 | * Use all reports to generate global and detail data 83 | * @param {*} fileList 84 | * @returns 85 | */ 86 | function readAllReports(fileList, grafanaLink, translator) { 87 | // init variables 88 | const allReportsVariables = []; 89 | let waterTotal = 0; 90 | let greenhouseGasesEmissionTotal = 0; 91 | 92 | // Read all json files 93 | fileList.forEach((file) => { 94 | let reportVariables = {}; 95 | const report_data = JSON.parse(fs.readFileSync(file.path).toString()); 96 | 97 | const hostname = report_data.pageInformations.url.split('/')[2]; 98 | const scenarioName = report_data.pageInformations.name || report_data.pageInformations.url; 99 | const pageFilename = report_data.pageInformations.name 100 | ? `${removeForbiddenCharacters(report_data.pageInformations.name)}.html` 101 | : `${report_data.index}.html`; 102 | 103 | if (report_data.success) { 104 | let pages = []; 105 | let nbRequestTotal = 0; 106 | let responsesSizeTotal = 0; 107 | let responsesSizeUncompressTotal = 0; 108 | let domSizeTotal = 0; 109 | let id = 0; 110 | 111 | // Loop over each page (i.e scenario) 112 | report_data.pages.forEach((page) => { 113 | const actions = []; 114 | const analyzePage = {}; 115 | 116 | analyzePage.name = page.name; 117 | analyzePage.url = page.url; 118 | 119 | analyzePage.id = id; 120 | id += 1; 121 | 122 | // Loop on each recorded action 123 | page.actions.forEach((action) => { 124 | const res = {}; 125 | res.name = action.name; 126 | res.ecoIndex = action.ecoIndex; 127 | res.grade = action.grade; 128 | res.waterConsumption = action.waterConsumption; 129 | res.greenhouseGasesEmission = action.greenhouseGasesEmission; 130 | res.nbRequest = action.nbRequest; 131 | res.domSize = action.domSize; 132 | res.responsesSize = action.responsesSize / 1000; 133 | res.responsesSizeUncompress = action.responsesSizeUncompress; 134 | actions.push(res); 135 | }); 136 | 137 | analyzePage.actions = actions; 138 | 139 | const lastAction = actions[actions.length - 1]; 140 | analyzePage.lastEcoIndex = lastAction.ecoIndex; 141 | analyzePage.lastGrade = lastAction.grade; 142 | analyzePage.deltaEcoIndex = actions[0].ecoIndex - lastAction.ecoIndex; 143 | analyzePage.waterConsumption = lastAction.waterConsumption; 144 | analyzePage.greenhouseGasesEmission = lastAction.greenhouseGasesEmission; 145 | analyzePage.domSize = lastAction.domSize; 146 | analyzePage.nbRequest = lastAction.nbRequest; 147 | analyzePage.ecoIndex = lastAction.ecoIndex; 148 | analyzePage.grade = lastAction.grade; 149 | 150 | // update total page measure 151 | nbRequestTotal += lastAction.nbRequest; 152 | responsesSizeTotal += lastAction.responsesSize; 153 | domSizeTotal += lastAction.domSize; 154 | responsesSizeUncompressTotal += lastAction.responsesSizeUncompress; 155 | 156 | const pageBestPractices = extractBestPractices(translator); 157 | 158 | // Manage best practices 159 | let nbBestPracticesToCorrect = 0; 160 | pageBestPractices.forEach((bp) => { 161 | if (page.bestPractices) { 162 | bp.note = cssBestPractices[page.bestPractices[bp.key].complianceLevel || 'A']; 163 | bp.comment = page.bestPractices[bp.key].comment || ''; 164 | bp.errors = page.bestPractices[bp.key].detailComment; 165 | 166 | if ( 167 | cssBestPractices[page.bestPractices[bp.key].complianceLevel || 'A'] !== 'checkmark-success' 168 | ) { 169 | // if error, increment number of incorrect best practices 170 | nbBestPracticesToCorrect += 1; 171 | } 172 | } else { 173 | bp.note = 'A'; 174 | bp.comment = ''; 175 | } 176 | }); 177 | 178 | if (analyzePage.waterConsumption) { 179 | waterTotal += analyzePage.waterConsumption; 180 | } 181 | if (analyzePage.greenhouseGasesEmission) { 182 | greenhouseGasesEmissionTotal += analyzePage.greenhouseGasesEmission; 183 | } 184 | analyzePage.bestPractices = pageBestPractices; 185 | analyzePage.nbBestPracticesToCorrect = nbBestPracticesToCorrect; 186 | analyzePage.nbBestPracticesToCorrectLabel = translator.translateWithArgs( 187 | 'bestPracticesToImplementWithNumber', 188 | nbBestPracticesToCorrect 189 | ); 190 | analyzePage.grafanaLink = `${grafanaLink}&var-scenarioName=${scenarioName}&var-actionName=${analyzePage.name}`; 191 | pages.push(analyzePage); 192 | }); 193 | 194 | // Manage state of global best practices, for each page of the scenario 195 | const bestPractices = manageScenarioBestPratices(pages, translator); 196 | 197 | reportVariables = { 198 | date: report_data.date, 199 | success: report_data.success, 200 | cssRowError: '', 201 | name: scenarioName, 202 | link: `${scenarioName}`, 203 | filename: pageFilename, 204 | header: `${translator.translate('greenItAnalysisReport')} > ${scenarioName}`, 207 | bigEcoIndex: `${report_data.ecoIndex} ${report_data.grade}`, 208 | smallEcoIndex: `${report_data.ecoIndex} ${report_data.grade}`, 209 | grade: report_data.grade, 210 | nbRequest: nbRequestTotal, 211 | responsesSize: Math.round(responsesSizeTotal * 1000) / 1000, 212 | pageSize: `${Math.round(responsesSizeTotal)} (${Math.round(responsesSizeUncompressTotal / 1000)})`, 213 | domSize: domSizeTotal, 214 | pages, 215 | bestPractices, 216 | }; 217 | } else { 218 | reportVariables = { 219 | date: report_data.date, 220 | name: scenarioName, 221 | filename: pageFilename, 222 | success: false, 223 | header: `${translator.translate('greenItAnalysisReport')} > ${scenarioName}`, 226 | cssRowError: 'bg-danger', 227 | nbRequest: 0, 228 | pages: [], 229 | link: `${scenarioName}`, 230 | bestPractices: [], 231 | }; 232 | } 233 | allReportsVariables.push(reportVariables); 234 | }); 235 | 236 | return { allReportsVariables, waterTotal, greenhouseGasesEmissionTotal }; 237 | } 238 | 239 | /** 240 | * Read and generate data for global template 241 | * @param {*} path 242 | * @param {*} allReportsVariables 243 | * @param {*} waterTotal 244 | * @param {*} greenhouseGasesEmissionTotal 245 | * @param {*} grafanaLinkPresent 246 | * @returns 247 | */ 248 | function readGlobalReport( 249 | path, 250 | allReportsVariables, 251 | waterTotal, 252 | greenhouseGasesEmissionTotal, 253 | grafanaLinkPresent, 254 | translator 255 | ) { 256 | const globalReport_data = JSON.parse(fs.readFileSync(path).toString()); 257 | 258 | let ecoIndex = ''; 259 | 260 | if (globalReport_data.worstEcoIndexes) { 261 | try { 262 | globalReport_data.worstEcoIndexes.forEach((worstEcoIndex) => { 263 | ecoIndex = `${ecoIndex} ${ecoIndex === '' ? '' : '/'} ${ 264 | worstEcoIndex.ecoIndex 265 | } ${worstEcoIndex.grade}`; 266 | }); 267 | } catch (err) { 268 | console.error(err); 269 | } 270 | } 271 | 272 | const globalReportVariables = { 273 | date: globalReport_data.date, 274 | hostname: globalReport_data.hostname, 275 | device: globalReport_data.device, 276 | connection: globalReport_data.connection, 277 | ecoIndex: ecoIndex, 278 | grade: globalReport_data.grade, 279 | nbScenarios: globalReport_data.nbScenarios, 280 | waterTotal: Math.round(waterTotal * 100) / 100, 281 | greenhouseGasesEmissionTotal: Math.round(greenhouseGasesEmissionTotal * 100) / 100, 282 | nbErrors: globalReport_data.errors.length, 283 | allReportsVariables, 284 | bestsPractices: constructBestPracticesGlobal(allReportsVariables, translator), 285 | grafanaLinkPresent, 286 | }; 287 | return globalReportVariables; 288 | } 289 | 290 | function constructBestPracticesGlobal(allReportsVariables, translator) { 291 | const bestPracticesGlobal = []; 292 | const bestPractices = extractBestPractices(translator); 293 | 294 | bestPractices.forEach((bestPractice) => { 295 | let note = 'checkmark-success'; 296 | let errors = []; 297 | let success = true; 298 | 299 | allReportsVariables.forEach((scenario) => { 300 | if (scenario.pages) { 301 | scenario.pages.forEach((page) => { 302 | const best = page.bestPractices.filter((bp) => bp.key === bestPractice.key)[0]; 303 | 304 | if (success && best.note === 'close-error') { 305 | success = false; 306 | note = 'close-error'; 307 | } 308 | }); 309 | } 310 | }); 311 | 312 | const bestPracticeGlobal = { 313 | id: bestPractice.id, 314 | description: bestPractice.description, 315 | name: bestPractice.name, 316 | comment: bestPractice.comment, 317 | note: note, 318 | errors: errors, 319 | priority: bestPractice.priority, 320 | impact: bestPractice.impact, 321 | effort: bestPractice.effort, 322 | }; 323 | 324 | bestPracticesGlobal.push(bestPracticeGlobal); 325 | }); 326 | return bestPracticesGlobal; 327 | } 328 | 329 | /** 330 | * Extract best practices from report 331 | * @param {} bestPracticesFromReport 332 | * @returns 333 | */ 334 | function extractBestPractices(translator) { 335 | let bestPractices = []; 336 | let bestPractice; 337 | let rule; 338 | let index = 0; 339 | 340 | bestPracticesKey.forEach((bestPracticeName) => { 341 | rule = rules.find((p) => p.bestPractice === bestPracticeName); 342 | bestPractice = { 343 | key: bestPracticeName, 344 | id: `collapse${index}`, 345 | name: translator.translateRule(bestPracticeName), 346 | description: translator.translateRule(`${bestPracticeName}_DetailDescription`), 347 | priority: rule.priority, 348 | impact: rule.impact, 349 | effort: rule.effort, 350 | notes: [], 351 | pages: [], 352 | comments: [], 353 | }; 354 | index++; 355 | bestPractices.push(bestPractice); 356 | }); 357 | 358 | return bestPractices; 359 | } 360 | 361 | /** 362 | * Manage best practice state for each page 363 | * @param {*} pages 364 | */ 365 | function manageScenarioBestPratices(pages, translator) { 366 | const bestPractices = extractBestPractices(translator); 367 | // loop over each best practice 368 | pages.forEach((page) => { 369 | bestPractices.forEach((bp) => { 370 | if (!bp.pages) { 371 | bp.pages = []; 372 | } 373 | if (!bp.notes) { 374 | bp.notes = []; 375 | } 376 | if (!bp.comments) { 377 | bp.comments = []; 378 | } 379 | 380 | bp.pages.push(page.name); 381 | if (page.bestPractices) { 382 | // Get mapping best practice and update data 383 | const currentBestPractice = page.bestPractices.find((element) => element.key === bp.key); 384 | bp.notes.push(currentBestPractice.note || 'A'); 385 | bp.comments.push(currentBestPractice.comment || ''); 386 | } 387 | }); 388 | }); 389 | return bestPractices; 390 | } 391 | 392 | /** 393 | * Write global report from global template 394 | */ 395 | function writeGlobalReport(globalReportVariables, outputFile, progressBar, translator) { 396 | const globalReportVariablesWithLabels = { 397 | labels: { 398 | header: translator.translate('greenItAnalysisReport'), 399 | executionDate: translator.translate('executionDate'), 400 | hostname: translator.translate('hostname'), 401 | platform: translator.translate('platform'), 402 | connection: translator.translate('connection'), 403 | scenarios: translator.translate('scenarios'), 404 | errors: translator.translate('errors'), 405 | error: translator.translate('error'), 406 | scenario: translator.translate('scenario'), 407 | ecoIndex: translator.translate('ecoIndex'), 408 | shareDueToActions: translator.translate('shareDueToActions'), 409 | greenhouseGasesEmission: translator.translate('greenhouseGasesEmission'), 410 | water: translator.translate('water'), 411 | bestPracticesToImplement: translator.translate('bestPracticesToImplement'), 412 | bestPractices: translator.translate('bestPractices'), 413 | priority: translator.translate('priority'), 414 | allPriorities: translator.translate('allPriorities'), 415 | bestPractice: translator.translate('bestPractice'), 416 | effort: translator.translate('effort'), 417 | impact: translator.translate('impact'), 418 | note: translator.translate('note'), 419 | footerEcoIndex: translator.translate('footerEcoIndex'), 420 | footerBestPractices: translator.translate('footerBestPractices'), 421 | trend: translator.translate('trend'), 422 | }, 423 | tooltips: { 424 | ecoIndex: translator.translate('tooltip_ecoIndex'), 425 | shareDueToActions: translator.translate('tooltip_shareDueToActions'), 426 | bestPracticesToImplement: translator.translate('tooltip_bestPracticesToImplement'), 427 | }, 428 | values: globalReportVariables, 429 | }; 430 | 431 | const template = fs.readFileSync(path.join(__dirname, 'template/global.html')).toString(); 432 | var rendered = Mustache.render(template, globalReportVariablesWithLabels); 433 | fs.writeFileSync(outputFile, rendered); 434 | 435 | if (progressBar) { 436 | progressBar.tick(); 437 | } else { 438 | console.log(`Global report : ${outputFile} created`); 439 | } 440 | } 441 | 442 | /** 443 | * Write scenarios report from page template 444 | */ 445 | function writeAllReports(allReportsVariables, outputFolder, progressBar, translator) { 446 | const labels = { 447 | requests: translator.translate('requests'), 448 | pageSize: translator.translate('pageSize'), 449 | domSize: translator.translate('domSize'), 450 | steps: translator.translate('steps'), 451 | step: translator.translate('step'), 452 | ecoIndex: translator.translate('ecoIndex'), 453 | water: translator.translate('water'), 454 | greenhouseGasesEmission: translator.translate('greenhouseGasesEmission'), 455 | bestPractices: translator.translate('bestPractices'), 456 | bestPractice: translator.translate('bestPractice'), 457 | result: translator.translate('result'), 458 | effort: translator.translate('effort'), 459 | impact: translator.translate('impact'), 460 | priority: translator.translate('priority'), 461 | note: translator.translate('note'), 462 | }; 463 | 464 | const template = fs.readFileSync(path.join(__dirname, 'template/page.html')).toString(); 465 | let reportVariablesWithLabels; 466 | allReportsVariables.forEach((reportVariables) => { 467 | reportVariablesWithLabels = { 468 | labels: labels, 469 | values: reportVariables, 470 | }; 471 | 472 | var rendered = Mustache.render(template, reportVariablesWithLabels); 473 | 474 | fs.writeFileSync(`${outputFolder}/${reportVariables.filename}`, rendered); 475 | 476 | if (progressBar) { 477 | progressBar.tick(); 478 | } else { 479 | console.log(`Global report : ${outputFolder}/${reportVariables.filename} created`); 480 | } 481 | }); 482 | } 483 | 484 | function removeForbiddenCharacters(str) { 485 | str = removeForbiddenCharactersInFile(str); 486 | str = removeAccents(str); 487 | return str; 488 | } 489 | 490 | function removeForbiddenCharactersInFile(str) { 491 | return str.replace(/[/\\?%*:|"<>° ]/g, ''); 492 | } 493 | 494 | function removeAccents(str) { 495 | return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 496 | } 497 | 498 | module.exports = { 499 | create_html_report, 500 | }; 501 | -------------------------------------------------------------------------------- /src/cli-core/template/global.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 20 | 26 | 27 | 153 | 154 | 159 | 160 | {{ labels.header }} 161 | 162 | 163 | 164 | 169 |
170 |
171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |
{{ labels.executionDate }}{{ labels.hostname }}{{ labels.platform }}{{ labels.connection }}
{{ values.date }}{{ values.hostname }}{{ values.device }}{{ values.connection }}
187 |
188 | 189 |
190 |
191 |
192 |
193 |
{{ labels.scenarios }}
194 |
195 |

{{{ values.nbScenarios }}}

196 |
197 |
198 |
199 | 200 |
201 |
202 |
{{ labels.errors }}
203 |
204 |

{{ values.nbErrors }}

205 |
206 |
207 |
208 |
209 |
210 | 211 |
212 |
213 |
214 | 215 | 216 | 217 | 224 | 231 | 232 | 233 | 240 | {{#values.grafanaLinkPresent}} 241 | 242 | {{/values.grafanaLinkPresent}} 243 | 244 | 245 | {{#values.allReportsVariables}} 246 | 247 | 248 | 249 | {{#pages}} 250 | 251 | 252 | {{^success}} 253 | 254 | {{/success}} 255 | {{#success}} 256 | 259 | 260 | 261 | 262 | 263 | {{#values.grafanaLinkPresent}} 264 | 267 | {{/values.grafanaLinkPresent}} 268 | {{/success}} 269 | 270 | {{/pages}} 271 | {{/values.allReportsVariables}} 272 | 273 |
{{ labels.scenario }} 218 | 222 |  {{ labels.ecoIndex }} 223 | 225 | 229 |  {{ labels.shareDueToActions }} 230 | {{ labels.water }}{{ labels.greenhouseGasesEmission }} 234 | 238 |  {{ labels.bestPracticesToImplement }} 239 |
{{{ link }}}
> {{ name }}{{ labels.error }} 257 | {{{ lastEcoIndex }}} {{ lastGrade }} 258 | {{ deltaEcoIndex }}{{ waterConsumption }}{{ greenhouseGasesEmission }}{{ nbBestPracticesToCorrect }} 265 | {{ labels.trend }} 266 |
274 |
275 |
276 |
277 | 278 |
279 |
{{ labels.bestPractices }}
280 |
281 | 284 | 287 | 290 | 293 | 296 | 299 |
300 |
301 |
302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | {{#values.bestsPractices}} 312 | 313 | 320 | 321 | 322 | 323 | 326 | 327 | 328 | 335 | 336 | {{/values.bestsPractices}} 337 | 338 |
{{ labels.bestPractice }}{{ labels.effort }}{{ labels.impact }}{{ labels.priority }}{{ labels.note }}
314 | 318 | {{ name }} 319 | {{ effort }}{{ impact }}{{ priority }} 324 |
325 |
329 |
330 | {{#errors}} {{ scenario }} 331 |
{{{ errors }}}
332 | {{/errors}} 333 |
334 |
339 |
340 |
341 |
342 |
343 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /src/cli-core/template/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 20 | 26 | 27 | 139 | 140 | GreenIT-Analysis report 141 | 142 | 143 | 144 | 152 |
153 |
154 |
155 |
156 |
157 |
{{ labels.requests }}
158 |
159 |

{{ values.nbRequest }}

160 |
161 |
162 |
163 | 164 |
165 |
166 |
{{ labels.pageSize }}
167 |
168 |

{{ values.pageSize }}

169 |
170 |
171 |
172 | 173 |
174 |
175 |
{{ labels.domSize }}
176 |
177 |

{{ values.domSize }}

178 |
179 |
180 |
181 |
182 |
183 | 184 |
185 |
{{ labels.steps }}
186 |
187 |
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | {{#values.pages}} 200 | 201 | 202 | 203 | 204 | {{#actions}} 205 | 206 | 207 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | {{/actions}} 218 | 219 | 224 | 225 | 226 | 261 | 262 | 263 | 264 | 265 | {{/values.pages}} 266 | 267 |
{{ labels.step }}{{ labels.ecoIndex }}{{ labels.water }}{{ labels.greenhouseGasesEmission }}{{ labels.domSize }}{{ labels.requests }}{{ labels.pageSize }}
{{ name }} - {{ url }}
{{ name }} 208 | {{ ecoIndex }} 209 | {{grade}} 210 | {{ waterConsumption }}{{ greenhouseGasesEmission }}{{ domSize }}{{{ nbRequest }}}{{ responsesSize }}
220 | 223 |
227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | {{#bestPractices}} 235 | 236 | 245 | 246 | 249 | 250 | 251 | 256 | 257 | {{/bestPractices}} 258 | 259 |
{{ labels.bestPractice }}{{ labels.result }}{{ labels.note }}
237 | 241 | 244 | {{comment}} 247 |
248 |
252 |
253 |
{{{ errors }}}
254 |
255 |
260 |
268 |
269 |
270 |
271 | 272 |
273 |
{{ labels.bestPractices }}
274 |
275 |
276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | {{#values.bestPractices}} 287 | 288 | 302 | 314 | 315 | 316 | 317 | 329 | 330 | {{/values.bestPractices}} 331 | 332 |
{{ labels.bestPractice }}{{ labels.result }}{{ labels.effort }}{{ labels.impact }}{{ labels.priority }}{{ labels.note }}
289 | 293 | {{ name }} 294 | {{#pages.1}} 295 |
296 | {{#pages}} 297 | > {{ . }} 298 |
299 | {{/pages}} 300 | {{/pages.1}} 301 |
303 | {{^pages.1}} 304 | {{comments.0}} 305 | {{/pages.1}} 306 | {{#pages.1}} 307 |
308 | {{#comments}} 309 | {{ . }} 310 |
311 | {{/comments}} 312 | {{/pages.1}} 313 |
{{ effort }}{{ impact }}{{ priority }} 318 | {{^pages.1}} 319 |
320 | {{/pages.1}} 321 | {{#pages.1}} 322 |
323 | {{#notes}} 324 |
325 |
326 | {{/notes}} 327 | {{/pages.1}} 328 |
333 |
334 |
335 |
336 |
337 | 338 | 339 | -------------------------------------------------------------------------------- /src/cli-core/translator.js: -------------------------------------------------------------------------------- 1 | const fr = require('../locales/fr.json'); 2 | const en = require('../locales/en.json'); 3 | const util = require('util'); 4 | 5 | class Translator { 6 | constructor() { 7 | this.catalog = fr; 8 | } 9 | 10 | getCatalog() { 11 | return this.catalog; 12 | } 13 | 14 | setLocale(locale) { 15 | if (locale === 'fr') { 16 | this.catalog = fr; 17 | } else if (locale === 'en') { 18 | this.catalog = en; 19 | } 20 | } 21 | 22 | translateRule(rule) { 23 | return this.translate('rule_' + rule); 24 | } 25 | 26 | translate(key) { 27 | return this.catalog[key]; 28 | } 29 | 30 | translateWithArgs(key, ...args) { 31 | return util.format(this.catalog[key], args); 32 | } 33 | } 34 | 35 | const translator = new Translator(); 36 | 37 | module.exports = { 38 | translator, 39 | }; 40 | -------------------------------------------------------------------------------- /src/cli-core/utils.js: -------------------------------------------------------------------------------- 1 | const ProgressBar = require('progress'); 2 | 3 | /** 4 | * Initialize new progress bar 5 | */ 6 | function createProgressBar(options, total, progressText, defaultText) { 7 | let progressBar; 8 | if (!options.ci) { 9 | progressBar = new ProgressBar( 10 | ` ${progressText} [:bar] :percent Remaining: :etas Time: :elapseds`, 11 | { 12 | complete: '=', 13 | incomplete: ' ', 14 | width: 40, 15 | total: total, 16 | } 17 | ); 18 | progressBar.tick(); 19 | } else { 20 | console.log(`${defaultText}`); 21 | } 22 | 23 | return progressBar; 24 | } 25 | 26 | //EcoIndex -> Grade 27 | function getEcoIndexGrade(ecoIndex) { 28 | if (ecoIndex > 75) return 'A'; 29 | if (ecoIndex > 65) return 'B'; 30 | if (ecoIndex > 50) return 'C'; 31 | if (ecoIndex > 35) return 'D'; 32 | if (ecoIndex > 20) return 'E'; 33 | if (ecoIndex > 5) return 'F'; 34 | return 'G'; 35 | } 36 | 37 | //Grade -> EcoIndex 38 | function getGradeEcoIndex(grade) { 39 | if (grade == 'A') return 75; 40 | if (grade == 'B') return 65; 41 | if (grade == 'C') return 50; 42 | if (grade == 'D') return 35; 43 | if (grade == 'E') return 20; 44 | if (grade == 'F') return 5; 45 | return 0; 46 | } 47 | 48 | module.exports = { 49 | createProgressBar, 50 | getEcoIndexGrade, 51 | getGradeEcoIndex, 52 | }; 53 | -------------------------------------------------------------------------------- /src/commands/analyse.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const YAML = require('yaml'); 3 | const path = require('path'); 4 | const puppeteer = require('puppeteer'); 5 | const createJsonReports = require('../cli-core/analysis.js').createJsonReports; 6 | const login = require('../cli-core/analysis.js').login; 7 | const create_global_report = require('../cli-core/reportGlobal.js').create_global_report; 8 | const create_XLSX_report = require('../cli-core/reportExcel.js').create_XLSX_report; 9 | const create_html_report = require('../cli-core/reportHtml.js').create_html_report; 10 | const writeToInflux = require('../cli-core/influxdb').write; 11 | const translator = require('../cli-core/translator.js').translator; 12 | 13 | //launch core 14 | async function analyse_core(options) { 15 | const URL_YAML_FILE = path.resolve(options.url_input_file); 16 | //Get list of pages to analyze and its informations 17 | let pagesInformations; 18 | try { 19 | pagesInformations = YAML.parse(fs.readFileSync(URL_YAML_FILE).toString()); 20 | } catch (error) { 21 | throw ` url_input_file : "${URL_YAML_FILE}" is not a valid YAML file: ${error.code} at ${JSON.stringify( 22 | error.linePos 23 | )}.`; 24 | } 25 | 26 | let browserArgs = [ 27 | '--no-sandbox', // can't run inside docker without 28 | '--disable-setuid-sandbox', // but security issues 29 | ]; 30 | 31 | // Add proxy conf in browserArgs 32 | let proxy = {}; 33 | if (options.proxy) { 34 | proxy = readProxy(options.proxy); 35 | browserArgs.push(`--proxy-server=${proxy.server}`); 36 | if (proxy.bypass) { 37 | browserArgs.push(`--proxy-bypass-list=${proxy.bypass}`); 38 | } 39 | } 40 | 41 | // Read headers http file 42 | let headers; 43 | if (options.headers) { 44 | headers = readHeaders(options.headers); 45 | } 46 | 47 | // Get and check report format 48 | const reportFormat = getReportFormat(options.format, options.report_output_file); 49 | if (!reportFormat) { 50 | throw 'Format not supported. Use --format option or report file extension to define a supported extension.'; 51 | } 52 | 53 | //start browser 54 | const browser = await puppeteer.launch({ 55 | headless: options.headless === false ? false : 'new', 56 | args: browserArgs, 57 | // Keep gpu horsepower in headless 58 | ignoreDefaultArgs: ['--disable-gpu'], 59 | ignoreHTTPSErrors: true, 60 | }); 61 | 62 | // init translator 63 | translator.setLocale(options.language); 64 | 65 | //handle analyse 66 | let reports; 67 | try { 68 | //handle login 69 | if (options.login) { 70 | const LOGIN_YAML_FILE = path.resolve(options.login); 71 | let loginInfos; 72 | try { 73 | loginInfos = YAML.parse(fs.readFileSync(LOGIN_YAML_FILE).toString()); 74 | } catch (error) { 75 | throw ` --login : "${LOGIN_YAML_FILE}" is not a valid YAML file: ${error.code} at ${JSON.stringify( 76 | error.linePos 77 | )}.`; 78 | } 79 | await login(browser, loginInfos, options); 80 | } 81 | //analyse 82 | reports = await createJsonReports(browser, pagesInformations, options, proxy, headers, translator); 83 | } finally { 84 | //close browser 85 | await browser.close(); 86 | } 87 | //create report 88 | let reportObj = await create_global_report(reports, { ...options, proxy }, translator); 89 | if (reportFormat === 'influxdbhtml') { 90 | // write in database then generate html report 91 | await writeToInflux(reports, options); 92 | await create_html_report(reportObj, options, translator, true); 93 | } else if (reportFormat === 'html') { 94 | await create_html_report(reportObj, options, translator, false); 95 | } else if (reportFormat === 'influxdb') { 96 | await writeToInflux(reports, options); 97 | } else { 98 | await create_XLSX_report(reportObj, options, translator); 99 | } 100 | } 101 | 102 | function readProxy(proxyFile) { 103 | const PROXY_FILE = path.resolve(proxyFile); 104 | let proxy; 105 | try { 106 | proxy = YAML.parse(fs.readFileSync(PROXY_FILE).toString()); 107 | if (!proxy.server || !proxy.user || !proxy.password) { 108 | throw `proxy_config_file : Bad format "${PROXY_FILE}". Expected server, user and password.`; 109 | } 110 | } catch (error) { 111 | throw ` proxy_config_file : "${PROXY_FILE}" is not a valid YAML file: ${error.code} at ${JSON.stringify( 112 | error.linePos 113 | )}.`; 114 | } 115 | return proxy; 116 | } 117 | 118 | function readHeaders(headersFile) { 119 | const HEADERS_YAML_FILE = path.resolve(headersFile); 120 | let headers; 121 | try { 122 | headers = YAML.parse(fs.readFileSync(HEADERS_YAML_FILE).toString()); 123 | } catch (error) { 124 | throw ` --headers : "${HEADERS_YAML_FILE}" is not a valid YAML file: ${error.code} at ${JSON.stringify( 125 | error.linePos 126 | )}.`; 127 | } 128 | return headers; 129 | } 130 | 131 | function getReportFormat(format, filename) { 132 | // Check if format is defined 133 | const formats = ['xlsx', 'html', 'influxdb', 'influxdbhtml']; 134 | if (format && formats.includes(format.toLowerCase())) { 135 | return format.toLowerCase(); 136 | } 137 | 138 | // Else, check extension 139 | const filenameLC = filename.toLowerCase(); 140 | const extensionFormat = formats.find((format) => filenameLC.endsWith(`.${format}`)); 141 | if (extensionFormat) { 142 | console.log(`No output format specified, defaulting to ${extensionFormat} based on output file name.`); 143 | } 144 | return extensionFormat; 145 | } 146 | 147 | //export method that handle error 148 | function analyse(options) { 149 | analyse_core(options).catch((e) => console.error('ERROR : \n', e)); 150 | } 151 | 152 | module.exports = { 153 | analyse, 154 | analyse_core 155 | }; 156 | -------------------------------------------------------------------------------- /src/commands/sitemapParser.js: -------------------------------------------------------------------------------- 1 | const Sitemapper = require('sitemapper'); 2 | const fs = require('fs'); 3 | const YAML = require('yaml'); 4 | const path = require('path'); 5 | const sitemap = new Sitemapper(); 6 | 7 | module.exports = (options) => { 8 | //handle inputs 9 | const SITEMAP_URL = options.sitemap_url; 10 | const OUTPUT_FILE = path.resolve(options.yaml_output_file); 11 | //parse sitemap 12 | sitemap 13 | .fetch(SITEMAP_URL) 14 | .then(function (res) { 15 | try { 16 | const urls = res.sites.map((site) => { 17 | return { url: site }; 18 | }); 19 | fs.writeFileSync(OUTPUT_FILE, YAML.stringify(urls)); 20 | } catch (error) { 21 | throw ` yaml_output_file : Path "${OUTPUT_FILE}" cannot be reached.`; 22 | } 23 | }) 24 | .catch((e) => console.log('ERROR : \n' + e)); 25 | }; 26 | -------------------------------------------------------------------------------- /src/conf/rules.js: -------------------------------------------------------------------------------- 1 | const rules = [ 2 | { 3 | bestPractice: 'DomainsNumber', 4 | priority: 3, 5 | effort: 3, 6 | impact: 4, 7 | }, 8 | { 9 | bestPractice: 'AddExpiresOrCacheControlHeaders', 10 | priority: 4, 11 | effort: 3, 12 | impact: 4, 13 | }, 14 | { 15 | bestPractice: 'CompressHttp', 16 | priority: 9, 17 | effort: 9, 18 | impact: 9, 19 | }, 20 | { 21 | bestPractice: 'DontResizeImageInBrowser', 22 | priority: 4, 23 | effort: 4, 24 | impact: 4, 25 | }, 26 | { 27 | bestPractice: 'EmptySrcTag', 28 | priority: 9, 29 | effort: 9, 30 | impact: 9, 31 | }, 32 | { 33 | bestPractice: 'ExternalizeCss', 34 | priority: 4, 35 | effort: 4, 36 | impact: 4, 37 | }, 38 | { 39 | bestPractice: 'ExternalizeJs', 40 | priority: 4, 41 | effort: 4, 42 | impact: 4, 43 | }, 44 | { 45 | bestPractice: 'HttpError', 46 | priority: 9, 47 | effort: 9, 48 | impact: 9, 49 | }, 50 | { 51 | bestPractice: 'HttpRequests', 52 | priority: 4, 53 | effort: 3, 54 | impact: 4, 55 | }, 56 | { 57 | bestPractice: 'ImageDownloadedNotDisplayed', 58 | priority: 9, 59 | effort: 9, 60 | impact: 9, 61 | }, 62 | { 63 | bestPractice: 'JsValidate', 64 | priority: 3, 65 | effort: 3, 66 | impact: 2, 67 | }, 68 | { 69 | bestPractice: 'MaxCookiesLength', 70 | priority: 9, 71 | effort: 9, 72 | impact: 9, 73 | }, 74 | { 75 | bestPractice: 'MinifiedCss', 76 | priority: 4, 77 | effort: 3, 78 | impact: 4, 79 | }, 80 | { 81 | bestPractice: 'MinifiedJs', 82 | priority: 4, 83 | effort: 3, 84 | impact: 4, 85 | }, 86 | { 87 | bestPractice: 'NoCookieForStaticRessources', 88 | priority: 9, 89 | effort: 9, 90 | impact: 9, 91 | }, 92 | { 93 | bestPractice: 'NoRedirect', 94 | priority: 3, 95 | effort: 3, 96 | impact: 3, 97 | }, 98 | { 99 | bestPractice: 'OptimizeBitmapImages', 100 | priority: 4, 101 | effort: 4, 102 | impact: 4, 103 | }, 104 | { 105 | bestPractice: 'OptimizeSvg', 106 | priority: 4, 107 | effort: 4, 108 | impact: 4, 109 | }, 110 | { 111 | bestPractice: 'Plugins', 112 | priority: 9, 113 | effort: 9, 114 | impact: 9, 115 | }, 116 | { 117 | bestPractice: 'PrintStyleSheet', 118 | priority: 3, 119 | effort: 4, 120 | impact: 3, 121 | }, 122 | { 123 | bestPractice: 'SocialNetworkButton', 124 | priority: 4, 125 | effort: 4, 126 | impact: 4, 127 | }, 128 | { 129 | bestPractice: 'StyleSheets', 130 | priority: 4, 131 | effort: 4, 132 | impact: 4, 133 | }, 134 | { 135 | bestPractice: 'UseETags', 136 | priority: 9, 137 | effort: 9, 138 | impact: 9, 139 | }, 140 | { 141 | bestPractice: 'UseStandardTypefaces', 142 | priority: 4, 143 | effort: 3, 144 | impact: 4, 145 | }, 146 | ]; 147 | 148 | module.exports = rules; 149 | -------------------------------------------------------------------------------- /src/conf/sizes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | desktop: { 3 | width: 1920, 4 | height: 1080, 5 | isMobile: false, 6 | }, 7 | galaxyS9: { 8 | width: 360, 9 | height: 740, 10 | isMobile: true, 11 | }, 12 | galaxyS20: { 13 | width: 360, 14 | height: 740, 15 | isMobile: true, 16 | }, 17 | iPhone8: { 18 | width: 375, 19 | height: 667, 20 | isMobile: true, 21 | }, 22 | iPhone8Plus: { 23 | width: 414, 24 | height: 736, 25 | isMobile: true, 26 | }, 27 | iPhoneX: { 28 | width: 375, 29 | height: 812, 30 | isMobile: true, 31 | }, 32 | iPad: { 33 | width: 768, 34 | height: 1024, 35 | isMobile: false, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/greenit-core/analyseFrameCore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The EcoMeter authors (https://gitlab.com/ecoconceptionweb/ecometer) 3 | * Copyright (C) 2019 didierfred@gmail.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | function start_analyse_core() { 20 | const analyseStartingTime = Date.now(); 21 | const dom_size = document.getElementsByTagName('*').length; 22 | let pageAnalysis; 23 | 24 | if (analyseBestPractices) { 25 | // test with http://www.wickham43.net/flashvideo.php 26 | const pluginsNumber = getPluginsNumber(); 27 | const printStyleSheetsNumber = getPrintStyleSheetsNumber(); 28 | const inlineStyleSheetsNumber = getInlineStyleSheetsNumber(); 29 | const emptySrcTagNumber = getEmptySrcTagNumber(); 30 | const inlineJsScript = getInlineJsScript(); 31 | const inlineJsScriptsNumber = getInlineJsScriptsNumber(); 32 | const imagesResizedInBrowser = getImagesResizedInBrowser(); 33 | 34 | pageAnalysis = { 35 | analyseStartingTime: analyseStartingTime, 36 | url: document.URL, 37 | domSize: dom_size, 38 | pluginsNumber: pluginsNumber, 39 | printStyleSheetsNumber: printStyleSheetsNumber, 40 | inlineStyleSheetsNumber: inlineStyleSheetsNumber, 41 | emptySrcTagNumber: emptySrcTagNumber, 42 | inlineJsScript: inlineJsScript, 43 | inlineJsScriptsNumber: inlineJsScriptsNumber, 44 | imagesResizedInBrowser: imagesResizedInBrowser, 45 | }; 46 | } else 47 | pageAnalysis = { 48 | analyseStartingTime: analyseStartingTime, 49 | url: document.URL, 50 | domSize: dom_size, 51 | }; 52 | 53 | return pageAnalysis; 54 | } 55 | 56 | function getPluginsNumber() { 57 | const plugins = document.querySelectorAll('object,embed'); 58 | return plugins === undefined ? 0 : plugins.length; 59 | } 60 | 61 | function getEmptySrcTagNumber() { 62 | return ( 63 | document.querySelectorAll('img[src=""]').length + 64 | document.querySelectorAll('script[src=""]').length + 65 | document.querySelectorAll('link[rel=stylesheet][href=""]').length 66 | ); 67 | } 68 | 69 | function getPrintStyleSheetsNumber() { 70 | return ( 71 | document.querySelectorAll('link[rel=stylesheet][media~=print]').length + 72 | document.querySelectorAll('style[media~=print]').length 73 | ); 74 | } 75 | 76 | function getInlineStyleSheetsNumber() { 77 | let styleSheetsArray = Array.from(document.styleSheets); 78 | let inlineStyleSheetsNumber = 0; 79 | styleSheetsArray.forEach((styleSheet) => { 80 | try { 81 | if (!styleSheet.href) inlineStyleSheetsNumber++; 82 | } catch (err) { 83 | console.log('GREENIT-ANALYSIS ERROR ,' + err.name + ' = ' + err.message); 84 | console.log('GREENIT-ANALYSIS ERROR ' + err.stack); 85 | } 86 | }); 87 | return inlineStyleSheetsNumber; 88 | } 89 | 90 | function getInlineJsScript() { 91 | let scriptArray = Array.from(document.scripts); 92 | let scriptText = ''; 93 | scriptArray.forEach((script) => { 94 | let isJSON = String(script.type) === 'application/ld+json'; // Exclude type="application/ld+json" from parsing js analyse 95 | if (script.text.length > 0 && !isJSON) scriptText += '\n' + script.text; 96 | }); 97 | return scriptText; 98 | } 99 | 100 | function getInlineJsScriptsNumber() { 101 | let scriptArray = Array.from(document.scripts); 102 | let inlineScriptNumber = 0; 103 | scriptArray.forEach((script) => { 104 | let isJSON = String(script.type) === 'application/ld+json'; // Exclude type="application/ld+json" from count 105 | if (script.text.length > 0 && !isJSON) inlineScriptNumber++; 106 | }); 107 | return inlineScriptNumber; 108 | } 109 | 110 | function getImagesResizedInBrowser() { 111 | const imgArray = Array.from(document.querySelectorAll('img')); 112 | let imagesResized = []; 113 | imgArray.forEach((img) => { 114 | if (img.clientWidth < img.naturalWidth || img.clientHeight < img.naturalHeight) { 115 | // Images of one pixel are some times used ... , we exclude them 116 | if (img.naturalWidth > 1) { 117 | const imageMeasures = { 118 | src: img.src, 119 | clientWidth: img.clientWidth, 120 | clientHeight: img.clientHeight, 121 | naturalWidth: img.naturalWidth, 122 | naturalHeight: img.naturalHeight, 123 | }; 124 | imagesResized.push(imageMeasures); 125 | } 126 | } 127 | }); 128 | return imagesResized; 129 | } 130 | -------------------------------------------------------------------------------- /src/greenit-core/ecoIndex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 didierfred@gmail.com 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | let quantiles_dom = [ 19 | 0, 47, 75, 159, 233, 298, 358, 417, 476, 537, 603, 674, 753, 843, 949, 1076, 1237, 1459, 1801, 2479, 594601, 20 | ]; 21 | let quantiles_req = [0, 2, 15, 25, 34, 42, 49, 56, 63, 70, 78, 86, 95, 105, 117, 130, 147, 170, 205, 281, 3920]; 22 | let quantiles_size = [ 23 | 0, 1.37, 144.7, 319.53, 479.46, 631.97, 783.38, 937.91, 1098.62, 1265.47, 1448.32, 1648.27, 1876.08, 2142.06, 24 | 2465.37, 2866.31, 3401.59, 4155.73, 5400.08, 8037.54, 223212.26, 25 | ]; 26 | 27 | /** 28 | Calcul ecoIndex based on formula from web site www.ecoindex.fr 29 | **/ 30 | function computeEcoIndex(dom, req, size) { 31 | const q_dom = computeQuantile(quantiles_dom, dom); 32 | const q_req = computeQuantile(quantiles_req, req); 33 | const q_size = computeQuantile(quantiles_size, size); 34 | 35 | return Math.round(100 - (5 * (3 * q_dom + 2 * q_req + q_size)) / 6); 36 | } 37 | 38 | function computeQuantile(quantiles, value) { 39 | for (let i = 1; i < quantiles.length; i++) { 40 | if (value < quantiles[i]) return i + (value - quantiles[i - 1]) / (quantiles[i] - quantiles[i - 1]); 41 | } 42 | return quantiles.length; 43 | } 44 | 45 | function getEcoIndexGrade(ecoIndex) { 46 | if (ecoIndex > 75) return 'A'; 47 | if (ecoIndex > 65) return 'B'; 48 | if (ecoIndex > 50) return 'C'; 49 | if (ecoIndex > 35) return 'D'; 50 | if (ecoIndex > 20) return 'E'; 51 | if (ecoIndex > 5) return 'F'; 52 | return 'G'; 53 | } 54 | 55 | function computeGreenhouseGasesEmissionfromEcoIndex(ecoIndex) { 56 | return Math.round(100 * (2 + (2 * (50 - ecoIndex)) / 100)) / 100; 57 | } 58 | 59 | function computeWaterConsumptionfromEcoIndex(ecoIndex) { 60 | return Math.round(100 * (3 + (3 * (50 - ecoIndex)) / 100)) / 100; 61 | } 62 | -------------------------------------------------------------------------------- /src/greenit-core/greenpanel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 didierfred@gmail.com 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | let backgroundPageConnection; 19 | let currentRulesChecker; 20 | let lastAnalyseStartingTime = 0; 21 | let measuresAcquisition; 22 | let analyseBestPractices = true; 23 | let har; 24 | let resources; 25 | 26 | function handleResponseFromBackground(frameMeasures) { 27 | if (isOldAnalyse(frameMeasures.analyseStartingTime)) { 28 | debug(() => `Analyse is too old for url ${frameMeasures.url} , time = ${frameMeasures.analyseStartingTime}`); 29 | return; 30 | } 31 | measuresAcquisition.aggregateFrameMeasures(frameMeasures); 32 | } 33 | 34 | function computeEcoIndexMeasures(measures) { 35 | measures.ecoIndex = computeEcoIndex( 36 | measures.domSize, 37 | measures.nbRequest, 38 | Math.round(measures.responsesSize / 1000) 39 | ); 40 | measures.waterConsumption = computeWaterConsumptionfromEcoIndex(measures.ecoIndex); 41 | measures.greenhouseGasesEmission = computeGreenhouseGasesEmissionfromEcoIndex(measures.ecoIndex); 42 | measures.grade = getEcoIndexGrade(measures.ecoIndex); 43 | } 44 | 45 | function launchAnalyse() { 46 | let now = Date.now(); 47 | 48 | // To avoid parallel analyse , force 1 secondes between analysis 49 | if (now - lastAnalyseStartingTime < 1000) { 50 | debug(() => 'Ignore click'); 51 | return; 52 | } 53 | lastAnalyseStartingTime = now; 54 | currentRulesChecker = rulesManager.getNewRulesChecker(); 55 | measuresAcquisition = new MeasuresAcquisition(currentRulesChecker); 56 | measuresAcquisition.initializeMeasures(); 57 | measuresAcquisition.aggregateFrameMeasures(start_analyse_core()); 58 | measuresAcquisition.startMeasuring(); 59 | let returnObj = measuresAcquisition.getMeasures(); 60 | returnObj.bestPractices = measuresAcquisition.getBestPractices(); 61 | return returnObj; 62 | } 63 | 64 | function MeasuresAcquisition(rules) { 65 | let measures; 66 | let localRulesChecker = rules; 67 | let nbGetHarTry = 0; 68 | 69 | this.initializeMeasures = () => { 70 | measures = { 71 | url: '', 72 | domSize: 0, 73 | nbRequest: 0, 74 | responsesSize: 0, 75 | responsesSizeUncompress: 0, 76 | ecoIndex: 100, 77 | grade: 'A', 78 | waterConsumption: 0, 79 | greenhouseGasesEmission: 0, 80 | pluginsNumber: 0, 81 | printStyleSheetsNumber: 0, 82 | inlineStyleSheetsNumber: 0, 83 | emptySrcTagNumber: 0, 84 | inlineJsScriptsNumber: 0, 85 | imagesResizedInBrowser: [], 86 | }; 87 | }; 88 | 89 | this.startMeasuring = function () { 90 | getNetworkMeasure(); 91 | if (analyseBestPractices) getResourcesMeasure(); 92 | }; 93 | 94 | this.getMeasures = () => measures; 95 | 96 | this.getBestPractices = () => Object.fromEntries(localRulesChecker.getAllRules()); 97 | 98 | this.aggregateFrameMeasures = function (frameMeasures) { 99 | measures.domSize += frameMeasures.domSize; 100 | computeEcoIndexMeasures(measures); 101 | 102 | if (analyseBestPractices) { 103 | measures.pluginsNumber += frameMeasures.pluginsNumber; 104 | 105 | measures.printStyleSheetsNumber += frameMeasures.printStyleSheetsNumber; 106 | if (measures.inlineStyleSheetsNumber < frameMeasures.inlineStyleSheetsNumber) 107 | measures.inlineStyleSheetsNumber = frameMeasures.inlineStyleSheetsNumber; 108 | measures.emptySrcTagNumber += frameMeasures.emptySrcTagNumber; 109 | if (frameMeasures.inlineJsScript.length > 0) { 110 | const resourceContent = { 111 | url: 'inline js', 112 | type: 'script', 113 | content: frameMeasures.inlineJsScript, 114 | }; 115 | localRulesChecker.sendEvent('resourceContentReceived', measures, resourceContent); 116 | } 117 | if (measures.inlineJsScriptsNumber < frameMeasures.inlineJsScriptsNumber) 118 | measures.inlineJsScriptsNumber = frameMeasures.inlineJsScriptsNumber; 119 | 120 | measures.imagesResizedInBrowser = frameMeasures.imagesResizedInBrowser; 121 | 122 | localRulesChecker.sendEvent('frameMeasuresReceived', measures); 123 | } 124 | }; 125 | 126 | const getNetworkMeasure = () => { 127 | console.log('Start network measure...'); 128 | // only account for network traffic, filtering resources embedded through data urls 129 | let entries = har.entries.filter((entry) => isNetworkResource(entry)); 130 | 131 | // Get the "mother" url 132 | if (entries.length > 0) measures.url = entries[0].request.url; 133 | else { 134 | // Bug with firefox when we first get har.entries when starting the plugin , we need to ask again to have it 135 | if (nbGetHarTry < 1) { 136 | debug(() => 'No entries, try again to get HAR in 1s'); 137 | nbGetHarTry++; 138 | setTimeout(getNetworkMeasure, 1000); 139 | } 140 | } 141 | 142 | measures.entries = entries; 143 | measures.dataEntries = har.entries.filter((entry) => isDataResource(entry)); // embeded data urls 144 | 145 | if (entries.length) { 146 | measures.nbRequest = entries.length; 147 | entries.forEach((entry) => { 148 | // If chromium : 149 | // _transferSize represent the real data volume transfert 150 | // while content.size represent the size of the page which is uncompress 151 | if (entry.response._transferSize) { 152 | measures.responsesSize += entry.response._transferSize; 153 | measures.responsesSizeUncompress += entry.response.content.size; 154 | } else { 155 | // In firefox , entry.response.content.size can sometimes be undefined 156 | if (entry.response.content.size) measures.responsesSize += entry.response.content.size; 157 | //debug(() => `entry size = ${entry.response.content.size} , responseSize = ${measures.responsesSize}`); 158 | } 159 | }); 160 | if (analyseBestPractices) localRulesChecker.sendEvent('harReceived', measures); 161 | 162 | computeEcoIndexMeasures(measures); 163 | } 164 | }; 165 | 166 | function getResourcesMeasure() { 167 | resources.forEach((resource) => { 168 | if (resource.url.startsWith('file') || resource.url.startsWith('http')) { 169 | if (resource.type === 'script' || resource.type === 'stylesheet' || resource.type === 'image') { 170 | let resourceAnalyser = new ResourceAnalyser(resource); 171 | resourceAnalyser.analyse(); 172 | } 173 | } 174 | }); 175 | } 176 | 177 | function ResourceAnalyser(resource) { 178 | let resourceToAnalyse = resource; 179 | 180 | this.analyse = () => resourceToAnalyse.getContent(this.analyseContent); 181 | 182 | this.analyseContent = (code) => { 183 | // exclude from analyse the injected script 184 | if (resourceToAnalyse.type === 'script' && resourceToAnalyse.url.includes('script/analyseFrame.js')) return; 185 | 186 | let resourceContent = { 187 | url: resourceToAnalyse.url, 188 | type: resourceToAnalyse.type, 189 | content: code, 190 | }; 191 | localRulesChecker.sendEvent('resourceContentReceived', measures, resourceContent); 192 | }; 193 | } 194 | } 195 | 196 | /** 197 | Add to the history the result of an analyse 198 | **/ 199 | function storeAnalysisInHistory() { 200 | let measures = measuresAcquisition.getMeasures(); 201 | if (!measures) return; 202 | 203 | var analyse_history = []; 204 | var string_analyse_history = localStorage.getItem('analyse_history'); 205 | var analyse_to_store = { 206 | resultDate: new Date(), 207 | url: measures.url, 208 | nbRequest: measures.nbRequest, 209 | responsesSize: Math.round(measures.responsesSize / 1000), 210 | domSize: measures.domSize, 211 | greenhouseGasesEmission: measures.greenhouseGasesEmission, 212 | waterConsumption: measures.waterConsumption, 213 | ecoIndex: measures.ecoIndex, 214 | grade: measures.grade, 215 | }; 216 | 217 | if (string_analyse_history) { 218 | analyse_history = JSON.parse(string_analyse_history); 219 | analyse_history.reverse(); 220 | analyse_history.push(analyse_to_store); 221 | analyse_history.reverse(); 222 | } else analyse_history.push(analyse_to_store); 223 | 224 | localStorage.setItem('analyse_history', JSON.stringify(analyse_history)); 225 | } 226 | -------------------------------------------------------------------------------- /src/greenit-core/rules/AddExpiresOrCacheControlHeaders.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "AddExpiresOrCacheControlHeaders", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let staticResourcesSize = 0; 9 | let staticResourcesWithCache = 0; 10 | 11 | if (measures.entries.length) measures.entries.forEach(entry => { 12 | if (isStaticRessource(entry)) { 13 | staticResourcesSize += entry.response.content.size; 14 | if (hasValidCacheHeaders(entry)) { 15 | staticResourcesWithCache += entry.response.content.size; 16 | } 17 | else this.detailComment += chrome.i18n.getMessage("rule_AddExpiresOrCacheControlHeaders_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; 18 | } 19 | }); 20 | 21 | if (staticResourcesSize > 0) { 22 | const cacheHeaderRatio = staticResourcesWithCache / staticResourcesSize * 100; 23 | if (cacheHeaderRatio < 95) { 24 | if (cacheHeaderRatio < 90) this.complianceLevel = 'C' 25 | else this.complianceLevel = 'B'; 26 | } 27 | else this.complianceLevel = 'A'; 28 | this.comment = chrome.i18n.getMessage("rule_AddExpiresOrCacheControlHeaders_Comment", String(Math.round(cacheHeaderRatio * 10) / 10) + "%"); 29 | } 30 | } 31 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/CompressHttp.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "CompressHttp", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let compressibleResourcesSize = 0; 9 | let compressibleResourcesCompressedSize = 0; 10 | if (measures.entries.length) measures.entries.forEach(entry => { 11 | if (isCompressibleResource(entry)) { 12 | compressibleResourcesSize += entry.response.content.size; 13 | if (isResourceCompressed(entry)) { 14 | compressibleResourcesCompressedSize += entry.response.content.size; 15 | } 16 | else this.detailComment += chrome.i18n.getMessage("rule_CompressHttp_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; 17 | } 18 | }); 19 | if (compressibleResourcesSize > 0) { 20 | const compressRatio = compressibleResourcesCompressedSize / compressibleResourcesSize * 100; 21 | if (compressRatio < 95) { 22 | if (compressRatio < 90) this.complianceLevel = 'C' 23 | else this.complianceLevel = 'B'; 24 | } 25 | else this.complianceLevel = 'A'; 26 | this.comment = chrome.i18n.getMessage("rule_CompressHttp_Comment", String(Math.round(compressRatio * 10) / 10) + "%"); 27 | } 28 | } 29 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/DomainsNumber.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "DomainsNumber", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let domains = []; 9 | if (measures.entries.length) measures.entries.forEach(entry => { 10 | let domain = getDomainFromUrl(entry.request.url); 11 | if (domains.indexOf(domain) === -1) { 12 | domains.push(domain); 13 | } 14 | }); 15 | if (domains.length > 2) { 16 | if (domains.length === 3) this.complianceLevel = 'B'; 17 | else this.complianceLevel = 'C'; 18 | } 19 | domains.forEach(domain => { 20 | this.detailComment += domain + "
"; 21 | }); 22 | 23 | this.comment = chrome.i18n.getMessage("rule_DomainsNumber_Comment", String(domains.length)); 24 | } 25 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/DontResizeImageInBrowser.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "DontResizeImageInBrowser", 4 | comment: "", 5 | detailComment: "", 6 | imagesResizedInBrowserNumber: 0, 7 | imgAnalysed: new Map(), 8 | 9 | // need to get a new map , otherwise it's share between instance 10 | initialize: function () { 11 | this.imgAnalysed = new Map(); 12 | }, 13 | 14 | isRevelant: function (entry) { 15 | // exclude svg 16 | if (isSvgUrl(entry.src)) return false; 17 | 18 | // difference of 1 pixel is not relevant 19 | if (entry.naturalWidth - entry.clientWidth < 2) return false; 20 | if (entry.naturalHeight - entry.clientHeight < 2) return false; 21 | 22 | // If picture is 0x0 it meens it's not visible on the ui , see imageDownloadedNotDisplayed 23 | if (entry.clientWidth === 0) return false; 24 | 25 | return true; 26 | }, 27 | 28 | check: function (measures) { 29 | measures.imagesResizedInBrowser.forEach(entry => { 30 | if (!this.imgAnalysed.has(entry.src) && this.isRevelant(entry)) { // Do not count two times the same picture 31 | this.detailComment += chrome.i18n.getMessage("rule_DontResizeImageInBrowser_DetailComment",[entry.src,`${entry.naturalWidth}x${entry.naturalHeight}`,`${entry.clientWidth}x${entry.clientHeight}`]) + '
'; 32 | this.imgAnalysed.set(entry.src); 33 | this.imagesResizedInBrowserNumber += 1; 34 | } 35 | }); 36 | if (this.imagesResizedInBrowserNumber > 0) this.complianceLevel = 'C'; 37 | this.comment = chrome.i18n.getMessage("rule_DontResizeImageInBrowser_Comment", String(this.imagesResizedInBrowserNumber)); 38 | } 39 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/EmptySrcTag.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "EmptySrcTag", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.emptySrcTagNumber > 0) { 9 | this.complianceLevel = 'C'; 10 | this.comment = chrome.i18n.getMessage("rule_EmptySrcTag_Comment", String(measures.emptySrcTagNumber)); 11 | } 12 | } 13 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/ExternalizeCss.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "ExternalizeCss", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.inlineStyleSheetsNumber > 0) { 9 | this.complianceLevel = 'C'; 10 | this.comment = chrome.i18n.getMessage("rule_ExternalizeCss_Comment", String(measures.inlineStyleSheetsNumber)); 11 | } 12 | } 13 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/ExternalizeJs.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "ExternalizeJs", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.inlineJsScriptsNumber > 0) { 9 | if (measures.inlineJsScriptsNumber > 1) this.complianceLevel = 'C'; 10 | this.comment = chrome.i18n.getMessage("rule_ExternalizeJs_Comment", String(measures.inlineJsScriptsNumber)); 11 | 12 | } 13 | } 14 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/HttpError.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "HttpError", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let errorNumber = 0; 9 | if (measures.entries.length) measures.entries.forEach(entry => { 10 | if (entry.response) { 11 | if (entry.response.status >=400 ) { 12 | this.detailComment += entry.response.status + " " + entry.request.url + "
"; 13 | errorNumber++; 14 | } 15 | } 16 | }); 17 | if (errorNumber > 0) this.complianceLevel = 'C'; 18 | this.comment = chrome.i18n.getMessage("rule_HttpError_Comment", String(errorNumber)); 19 | } 20 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/HttpRequests.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "HttpRequests", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.entries.length) measures.entries.forEach(entry => { 9 | this.detailComment += entry.request.url + "
"; 10 | }); 11 | if (measures.nbRequest > 40) this.complianceLevel = 'C'; 12 | else if (measures.nbRequest > 26) this.complianceLevel = 'B'; 13 | this.comment = chrome.i18n.getMessage("rule_HttpRequests_Comment", String(measures.nbRequest)); 14 | } 15 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/ImageDownloadedNotDisplayed.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "ImageDownloadedNotDisplayed", 4 | comment: "", 5 | detailComment: "", 6 | imageDownloadedNotDisplayedNumber: 0, 7 | imgAnalysed: new Map(), 8 | 9 | // need to get a new map , otherwise it's share between instance 10 | initialize: function () { 11 | this.imgAnalysed = new Map(); 12 | }, 13 | 14 | isRevelant: function (entry) { 15 | // Very small images could be download even if not display as it may be icons 16 | if (entry.naturalWidth * entry.naturalHeight < 10000) return false; 17 | if (entry.clientWidth === 0 && entry.clientHeight === 0) return true; 18 | return false; 19 | }, 20 | 21 | check: function (measures) { 22 | measures.imagesResizedInBrowser.forEach(entry => { 23 | if (!this.imgAnalysed.has(entry.src) && this.isRevelant(entry)) { // Do not count two times the same picture 24 | this.detailComment += chrome.i18n.getMessage("rule_ImageDownloadedNotDisplayed_DetailComment",[entry.src,`${entry.naturalWidth}x${entry.naturalHeight}`]) + '
'; 25 | this.imgAnalysed.set(entry.src); 26 | this.imageDownloadedNotDisplayedNumber += 1; 27 | } 28 | }); 29 | if (this.imageDownloadedNotDisplayedNumber > 0) this.complianceLevel = 'C'; 30 | this.comment = chrome.i18n.getMessage("rule_ImageDownloadedNotDisplayed_Comment", String(this.imageDownloadedNotDisplayedNumber)); 31 | } 32 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/JsValidate.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "JsValidate", 4 | comment: "", 5 | detailComment: "", 6 | errors: 0, 7 | totalJsSize: 0, 8 | 9 | check: function (measures, resourceContent) { 10 | if (resourceContent.type === "script") { 11 | this.totalJsSize += resourceContent.content.length; 12 | let errorNumber = computeNumberOfErrorsInJSCode(resourceContent.content, resourceContent.url); 13 | if (errorNumber > 0) { 14 | this.detailComment += (`URL ${resourceContent.url} has ${errorNumber} error(s)
`); 15 | this.errors += errorNumber; 16 | this.complianceLevel = 'C'; 17 | this.comment = chrome.i18n.getMessage("rule_JsValidate_Comment", String(this.errors)); 18 | } 19 | } 20 | } 21 | }, "resourceContentReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/MaxCookiesLength.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "MaxCookiesLength", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let maxCookiesLength = 0; 9 | let domains = new Map(); 10 | if (measures.entries.length) measures.entries.forEach(entry => { 11 | const cookiesLength = getCookiesLength(entry); 12 | if (cookiesLength !== 0) { 13 | let domain = getDomainFromUrl(entry.request.url); 14 | if (domains.has(domain)) { 15 | if (domains.get(domain) < cookiesLength) domains.set(domain, cookiesLength); 16 | } 17 | else domains.set(domain, cookiesLength); 18 | if (cookiesLength > maxCookiesLength) maxCookiesLength = cookiesLength; 19 | } 20 | }); 21 | domains.forEach((value, key) => { 22 | this.detailComment += chrome.i18n.getMessage("rule_MaxCookiesLength_DetailComment",[value,key]) + '
' ; 23 | }); 24 | if (maxCookiesLength !== 0) { 25 | this.comment = chrome.i18n.getMessage("rule_MaxCookiesLength_Comment", String(maxCookiesLength)); 26 | if (maxCookiesLength > 512) this.complianceLevel = 'B'; 27 | if (maxCookiesLength > 1024) this.complianceLevel = 'C'; 28 | } 29 | } 30 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/MinifiedCss.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "MinifiedCss", 4 | comment: "", 5 | detailComment: "", 6 | totalCssSize: 0, 7 | minifiedCssSize: 0, 8 | 9 | check: function (measures, resourceContent) { 10 | if (resourceContent.type === "stylesheet") { 11 | this.totalCssSize += resourceContent.content.length; 12 | if (!isMinified(resourceContent.content)) this.detailComment += chrome.i18n.getMessage("rule_MinifiedCss_DetailComment",resourceContent.url) + '
'; 13 | else this.minifiedCssSize += resourceContent.content.length; 14 | const percentMinifiedCss = this.minifiedCssSize / this.totalCssSize * 100; 15 | this.complianceLevel = 'A'; 16 | if (percentMinifiedCss < 90) this.complianceLevel = 'C'; 17 | else if (percentMinifiedCss < 95) this.complianceLevel = 'B'; 18 | this.comment = chrome.i18n.getMessage("rule_MinifiedCss_Comment", String(Math.round(percentMinifiedCss * 10) / 10)); 19 | } 20 | } 21 | }, "resourceContentReceived"); 22 | -------------------------------------------------------------------------------- /src/greenit-core/rules/MinifiedJs.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "MinifiedJs", 4 | comment: "", 5 | detailComment: "", 6 | totalJsSize: 0, 7 | minifiedJsSize: 0, 8 | 9 | check: function (measures, resourceContent) { 10 | if (resourceContent.type === "script") { 11 | this.totalJsSize += resourceContent.content.length; 12 | if (!isMinified(resourceContent.content)) this.detailComment += chrome.i18n.getMessage("rule_MinifiedJs_DetailComment",resourceContent.url) + '
'; 13 | else this.minifiedJsSize += resourceContent.content.length; 14 | const percentMinifiedJs = this.minifiedJsSize / this.totalJsSize * 100; 15 | this.complianceLevel = 'A'; 16 | if (percentMinifiedJs < 90) this.complianceLevel = 'C'; 17 | else if (percentMinifiedJs < 95) this.complianceLevel = 'B'; 18 | this.comment = chrome.i18n.getMessage("rule_MinifiedJs_Comment", String(Math.round(percentMinifiedJs * 10) / 10)); 19 | } 20 | } 21 | }, "resourceContentReceived"); 22 | -------------------------------------------------------------------------------- /src/greenit-core/rules/NoCookieForStaticRessources.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "NoCookieForStaticRessources", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let nbRessourcesStaticWithCookie = 0; 9 | let totalCookiesSize = 0; 10 | if (measures.entries.length) measures.entries.forEach(entry => { 11 | const cookiesLength = getCookiesLength(entry); 12 | if (isStaticRessource(entry) && (cookiesLength > 0)) { 13 | nbRessourcesStaticWithCookie++; 14 | totalCookiesSize += cookiesLength + 7; // 7 is size for the header name "cookie:" 15 | this.detailComment += chrome.i18n.getMessage("rule_NoCookieForStaticRessources_DetailComment",entry.request.url) + "
"; 16 | } 17 | }); 18 | if (nbRessourcesStaticWithCookie > 0) { 19 | if (totalCookiesSize > 2000) this.complianceLevel = 'C'; 20 | else this.complianceLevel = 'B'; 21 | this.comment = chrome.i18n.getMessage("rule_NoCookieForStaticRessources_Comment", [String(nbRessourcesStaticWithCookie), String(Math.round(totalCookiesSize / 100) / 10)]); 22 | } 23 | } 24 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/NoRedirect.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "NoRedirect", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let redirectNumber = 0; 9 | if (measures.entries.length) measures.entries.forEach(entry => { 10 | if (entry.response) { 11 | if (isHttpRedirectCode(entry.response.status)) { 12 | this.detailComment += entry.response.status + " " + entry.request.url + "
"; 13 | redirectNumber++; 14 | } 15 | } 16 | }); 17 | if (redirectNumber > 0) this.complianceLevel = 'C'; 18 | this.comment = chrome.i18n.getMessage("rule_NoRedirect_Comment", String(redirectNumber)); 19 | } 20 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/OptimizeBitmapImages.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "OptimizeBitmapImages", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let nbImagesToOptimize = 0; 9 | let totalMinGains = 0; 10 | if (measures.entries) measures.entries.forEach(entry => { 11 | if (entry.response) { 12 | const imageType = getImageTypeFromResource(entry); 13 | if (imageType !== "") { 14 | var myImage = new Image(); 15 | myImage.src = entry.request.url; 16 | // needed to access object in the function after 17 | myImage.rule = this; 18 | 19 | myImage.size = entry.response.content.size; 20 | myImage.onload = function () { 21 | 22 | const minGains = getMinOptimisationGainsForImage(this.width * this.height, this.size, imageType); 23 | if (minGains > 500) { // exclude small gain 24 | nbImagesToOptimize++; 25 | totalMinGains += minGains; 26 | this.rule.detailComment += chrome.i18n.getMessage("rule_OptimizeBitmapImages_DetailComment", [this.src + " , " + Math.round(this.size / 1000),this.width + "x" + this.height,String(Math.round(minGains / 1000))]) + "
"; 27 | } 28 | if (nbImagesToOptimize > 0) { 29 | if (totalMinGains < 50000) this.rule.complianceLevel = 'B'; 30 | else this.rule.complianceLevel = 'C'; 31 | this.rule.comment = chrome.i18n.getMessage("rule_OptimizeBitmapImages_Comment", [String(nbImagesToOptimize), String(Math.round(totalMinGains / 1000))]); 32 | showEcoRuleOnUI(this.rule); 33 | } 34 | } 35 | } 36 | } 37 | }); 38 | } 39 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/OptimizeSvg.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "OptimizeSvg", 4 | comment: "", 5 | detailComment: "", 6 | totalSizeToOptimize: 0, 7 | totalResourcesToOptimize: 0, 8 | 9 | check: function (measures, resourceContent) { 10 | if ((resourceContent.type === 'image') && isSvgUrl(resourceContent.url)) { 11 | if (!isSvgOptimized(window.atob(resourceContent.content))) // code is in base64 , decode base64 data with atob 12 | { 13 | this.detailComment += chrome.i18n.getMessage("rule_OptimizeSvg_detailComment",[resourceContent.url,String(Math.round(resourceContent.content.length / 100) / 10)]) + '
'; 14 | this.totalSizeToOptimize += resourceContent.content.length; 15 | this.totalResourcesToOptimize++; 16 | } 17 | if (this.totalSizeToOptimize > 0) { 18 | if (this.totalSizeToOptimize < 20000) this.complianceLevel = 'B'; 19 | else this.complianceLevel = 'C'; 20 | this.comment = chrome.i18n.getMessage("rule_OptimizeSvg_Comment", String(this.totalResourcesToOptimize)); 21 | } 22 | } 23 | } 24 | }, "resourceContentReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/Plugins.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "Plugins", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.pluginsNumber > 0) { 9 | this.complianceLevel = 'C'; 10 | this.comment = chrome.i18n.getMessage("rule_Plugins_Comment", String(measures.pluginsNumber)); 11 | } 12 | } 13 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/PrintStyleSheet.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'C', 3 | id: "PrintStyleSheet", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | if (measures.printStyleSheetsNumber > 0) { 9 | this.complianceLevel = 'A'; 10 | this.comment = chrome.i18n.getMessage("rule_PrintStyleSheet_Comment", String(measures.printStyleSheetsNumber)); 11 | } 12 | } 13 | }, "frameMeasuresReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/SocialNetworkButton.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "SocialNetworkButton", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let nbSocialNetworkButton = 0; 9 | let socialNetworks = []; 10 | if (measures.entries.length) measures.entries.forEach(entry => { 11 | const officalSocialButton = getOfficialSocialButtonFormUrl(entry.request.url); 12 | if (officalSocialButton.length > 0) { 13 | if (socialNetworks.indexOf(officalSocialButton) === -1) { 14 | socialNetworks.push(officalSocialButton); 15 | this.detailComment += chrome.i18n.getMessage("rule_SocialNetworkButton_detailComment", officalSocialButton) + "
"; 16 | nbSocialNetworkButton++; 17 | } 18 | } 19 | }); 20 | if (nbSocialNetworkButton > 0) { 21 | this.complianceLevel = 'C'; 22 | this.comment = chrome.i18n.getMessage("rule_SocialNetworkButton_Comment", String(nbSocialNetworkButton)); 23 | } 24 | } 25 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/StyleSheets.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "StyleSheets", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let styleSheets = []; 9 | if (measures.entries.length) measures.entries.forEach(entry => { 10 | if (getResponseHeaderFromResource(entry, "content-type").toLowerCase().includes('text/css')) { 11 | if (styleSheets.indexOf(entry.request.url) === -1) { 12 | styleSheets.push(entry.request.url); 13 | this.detailComment += entry.request.url + "
"; 14 | } 15 | } 16 | }); 17 | if (styleSheets.length > 2) { 18 | if (styleSheets.length === 3) this.complianceLevel = 'B'; 19 | else this.complianceLevel = 'C'; 20 | this.comment = chrome.i18n.getMessage("rule_StyleSheets_Comment", String(styleSheets.length)); 21 | } 22 | } 23 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/UseETags.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "UseETags", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | 9 | let staticResourcesSize = 0; 10 | let staticResourcesWithETagsSize = 0; 11 | 12 | if (measures.entries.length) measures.entries.forEach(entry => { 13 | if (isStaticRessource(entry)) { 14 | staticResourcesSize += entry.response.content.size; 15 | if (isRessourceUsingETag(entry)) { 16 | staticResourcesWithETagsSize += entry.response.content.size; 17 | } 18 | else this.detailComment +=chrome.i18n.getMessage("rule_UseETags_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; 19 | } 20 | }); 21 | if (staticResourcesSize > 0) { 22 | const eTagsRatio = staticResourcesWithETagsSize / staticResourcesSize * 100; 23 | if (eTagsRatio < 95) { 24 | if (eTagsRatio < 90) this.complianceLevel = 'C' 25 | else this.complianceLevel = 'B'; 26 | } 27 | else this.complianceLevel = 'A'; 28 | this.comment = chrome.i18n.getMessage("rule_UseETags_Comment", 29 | Math.round(eTagsRatio * 10) / 10 + "%"); 30 | } 31 | } 32 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rules/UseStandardTypefaces.js: -------------------------------------------------------------------------------- 1 | rulesManager.registerRule({ 2 | complianceLevel: 'A', 3 | id: "UseStandardTypefaces", 4 | comment: "", 5 | detailComment: "", 6 | 7 | check: function (measures) { 8 | let totalFontsSize = 0; 9 | if (measures.entries.length) measures.entries.forEach(entry => { 10 | if (isFontResource(entry) && (entry.response.content.size > 0)) { 11 | totalFontsSize += entry.response.content.size; 12 | this.detailComment += entry.request.url + " " + Math.round(entry.response.content.size / 1000) + "KB
"; 13 | } 14 | }); 15 | if (measures.dataEntries.length) measures.dataEntries.forEach(entry => { 16 | if (isFontResource(entry) && (entry.response.content.size > 0)) { 17 | totalFontsSize += entry.response.content.size; 18 | url_toshow = entry.request.url; 19 | if (url_toshow.length > 80) url_toshow = url_toshow.substring(0, 80) + "..."; 20 | this.detailComment += url_toshow + " " + Math.round(entry.response.content.size / 1000) + "KB
"; 21 | } 22 | }); 23 | if (totalFontsSize > 10000) this.complianceLevel = 'C'; 24 | else if (totalFontsSize > 0) this.complianceLevel = 'B'; 25 | if (totalFontsSize > 0) this.comment = chrome.i18n.getMessage("rule_UseStandardTypefaces_Comment", String(Math.round(totalFontsSize / 1000))); 26 | } 27 | }, "harReceived"); -------------------------------------------------------------------------------- /src/greenit-core/rulesManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 didierfred@gmail.com 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | rulesManager = new RulesManager(); 19 | 20 | function RulesManager() { 21 | let rulesId = []; 22 | let rulesChecker = new Map(); 23 | let eventListeners = new Map(); 24 | let notCompatibleRules = []; 25 | eventListeners.set('harReceived', []); 26 | eventListeners.set('frameMeasuresReceived', []); 27 | eventListeners.set('resourceContentReceived', []); 28 | 29 | this.registerRule = function (ruleChecker, eventListener) { 30 | rulesId.push(ruleChecker.id); 31 | rulesChecker.set(ruleChecker.id, ruleChecker); 32 | let event = eventListeners.get(eventListener); 33 | if (event) event.push(ruleChecker.id); 34 | }; 35 | 36 | this.getRulesId = function () { 37 | return rulesId; 38 | }; 39 | 40 | this.getRulesNotCompatibleWithCurrentBrowser = function () { 41 | return notCompatibleRules; 42 | }; 43 | 44 | this.getNewRulesChecker = function () { 45 | return new RulesChecker(); 46 | }; 47 | 48 | function RulesChecker() { 49 | let rules = new Map(); 50 | rulesChecker.forEach((ruleChecker, ruleId) => { 51 | let ruleCheckerInstance = Object.create(ruleChecker); 52 | // for certains rules need an initalization , method not implemented in all rules 53 | if (ruleCheckerInstance.initialize) ruleCheckerInstance.initialize(); 54 | rules.set(ruleId, ruleCheckerInstance); 55 | }); 56 | 57 | this.sendEvent = function (event, measures, resource) { 58 | eventListener = eventListeners.get(event); 59 | if (eventListener) { 60 | eventListener.forEach((ruleID) => { 61 | this.checkRule(ruleID, measures, resource); 62 | }); 63 | } 64 | }; 65 | 66 | this.checkRule = function (rule, measures, resource) { 67 | rules.get(rule).check(measures, resource); 68 | }; 69 | 70 | this.getRule = function (rule) { 71 | return rules.get(rule); 72 | }; 73 | 74 | this.getAllRules = function () { 75 | return rules; 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/greenit-core/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The EcoMeter authors (https://gitlab.com/ecoconceptionweb/ecometer) 3 | * Copyright (C) 2019 didierfred@gmail.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | const DEBUG = true; 20 | /* 21 | requirejs.config({ 22 | //By default load any module IDs from script 23 | baseUrl: 'script/externalLibs', 24 | }); 25 | 26 | // Load module require.js 27 | requirejs(['esprima'], 28 | (esprima) => console.log("Load esprima module")); 29 | */ 30 | 31 | const compressibleImage = [ 32 | /^image\/bmp(;|$)/i, 33 | /^image\/svg\+xml(;|$)/i, 34 | /^image\/vnd\.microsoft\.icon(;|$)/i, 35 | /^image\/x-icon(;|$)/i, 36 | ]; 37 | 38 | const image = [/^image\/gif(;|$)/i, /^image\/jpeg(;|$)/i, /^image\/png(;|$)/i, /^image\/tiff(;|$)/i].concat( 39 | compressibleImage 40 | ); 41 | 42 | const css = [/^text\/css(;|$)/i]; 43 | 44 | const javascript = [/^text\/javascript(;|$)/i, /^application\/javascript(;|$)/i, /^application\/x-javascript(;|$)/i]; 45 | 46 | const compressibleFont = [/^font\/eot(;|$)/i, /^font\/opentype(;|$)/i]; 47 | 48 | const font = [ 49 | /^application\/x-font-ttf(;|$)/i, 50 | /^application\/x-font-opentype(;|$)/i, 51 | /^application\/font-woff(;|$)/i, 52 | /^application\/x-font-woff(;|$)/i, 53 | /^application\/font-woff2(;|$)/i, 54 | /^application\/vnd.ms-fontobject(;|$)/i, 55 | /^application\/font-sfnt(;|$)/i, 56 | /^font\/woff2(;|$)/i, 57 | ].concat(compressibleFont); 58 | 59 | const manifest = [ 60 | /^text\/cache-manifest(;|$)/i, 61 | /^application\/x-web-app-manifest\+json(;|$)/i, 62 | /^application\/manifest\+json(;|$)/i, 63 | ]; 64 | 65 | // Mime types from H5B project recommendations 66 | // See https://github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess#L741 67 | const compressible = [ 68 | /^text\/html(;|$)/i, 69 | /^text\/plain(;|$)/i, 70 | /^text\/xml(;|$)/i, 71 | /^application\/json(;|$)/i, 72 | /^application\/atom\+xml(;|$)/i, 73 | /^application\/ld\+json(;|$)/i, 74 | /^application\/rdf\+xml(;|$)/i, 75 | /^application\/rss\+xml(;|$)/i, 76 | /^application\/schema\+json(;|$)/i, 77 | /^application\/vnd\.geo\+json(;|$)/i, 78 | /^application\/vnd\.ms-fontobject(;|$)/i, 79 | /^application\/xhtml\+xml(;|$)/i, 80 | /^application\/xml(;|$)/i, 81 | /^text\/vcard(;|$)/i, 82 | /^text\/vnd\.rim\.location\.xloc(;|$)/i, 83 | /^text\/vtt(;|$)/i, 84 | /^text\/x-component(;|$)/i, 85 | /^text\/x-cross-domain-policy(;|$)/i, 86 | ].concat(javascript, css, compressibleImage, compressibleFont, manifest); 87 | 88 | const audio = [ 89 | /^audio\/mpeg(;|$)/i, 90 | /^audio\/x-ms-wma(;|$)/i, 91 | /^audio\/vnd.rn-realaudio(;|$)/i, 92 | /^audio\/x-wav(;|$)/i, 93 | /^application\/ogg(;|$)/i, 94 | ]; 95 | 96 | const video = [ 97 | /^video\/mpeg(;|$)/i, 98 | /^video\/mp4(;|$)/i, 99 | /^video\/quicktime(;|$)/i, 100 | /^video\/x-ms-wmv(;|$)/i, 101 | /^video\/x-msvideo(;|$)/i, 102 | /^video\/x-flv(;|$)/i, 103 | /^video\/webm(;|$)/i, 104 | ]; 105 | 106 | const others = [ 107 | /^application\/x-shockwave-flash(;|$)/i, 108 | /^application\/octet-stream(;|$)/i, 109 | /^application\/pdf(;|$)/i, 110 | /^application\/zip(;|$)/i, 111 | ]; 112 | 113 | const staticResources = [].concat(image, javascript, font, css, audio, video, manifest, others); 114 | 115 | const httpCompressionTokens = ['br', 'compress', 'deflate', 'gzip', 'pack200-gzip']; 116 | 117 | const httpRedirectCodes = [301, 302, 303, 307]; 118 | 119 | // utils for cache rule 120 | function isStaticRessource(resource) { 121 | const contentType = getResponseHeaderFromResource(resource, 'content-type'); 122 | return staticResources.some((value) => value.test(contentType)); 123 | } 124 | 125 | function isFontResource(resource) { 126 | const contentType = getResponseHeaderFromResource(resource, 'content-type'); 127 | if (font.some((value) => value.test(contentType))) return true; 128 | // if not check url , because sometimes content-type is set to text/plain 129 | if (contentType === 'text/plain' || contentType === '' || contentType == 'application/octet-stream') { 130 | const url = resource.request.url; 131 | if (url.endsWith('.woff')) return true; 132 | if (url.endsWith('.woff2')) return true; 133 | if (url.includes('.woff?')) return true; 134 | if (url.includes('.woff2?')) return true; 135 | if (url.includes('.woff2.json')) return true; 136 | } 137 | return false; 138 | } 139 | 140 | function getHeaderWithName(headers, headerName) { 141 | let headerValue = ''; 142 | headers.forEach((header) => { 143 | if (header.name.toLowerCase() === headerName.toLowerCase()) headerValue = header.value; 144 | }); 145 | return headerValue; 146 | } 147 | 148 | function getResponseHeaderFromResource(resource, headerName) { 149 | return getHeaderWithName(resource.response.headers, headerName); 150 | } 151 | 152 | function getCookiesLength(resource) { 153 | let cookies = getHeaderWithName(resource.request.headers, 'cookie'); 154 | if (cookies) return cookies.length; 155 | else return 0; 156 | } 157 | 158 | function hasValidCacheHeaders(resource) { 159 | const headers = resource.response.headers; 160 | let cache = {}; 161 | let isValid = false; 162 | 163 | headers.forEach((header) => { 164 | if (header.name.toLowerCase() === 'cache-control') cache.CacheControl = header.value; 165 | if (header.name.toLowerCase() === 'expires') cache.Expires = header.value; 166 | if (header.name.toLowerCase() === 'date') cache.Date = header.value; 167 | }); 168 | 169 | // debug(() => `Cache headers gathered: ${JSON.stringify(cache)}`); 170 | 171 | if (cache.CacheControl) { 172 | if (!/(no-cache)|(no-store)|(max-age\s*=\s*0)/i.test(cache.CacheControl)) isValid = true; 173 | } 174 | 175 | if (cache.Expires) { 176 | let now = cache.Date ? new Date(cache.Date) : new Date(); 177 | let expires = new Date(cache.Expires); 178 | // Expires is in the past 179 | if (expires < now) { 180 | //debug(() => `Expires header is in the past ! ${now.toString()} < ${expires.toString()}`); 181 | isValid = false; 182 | } 183 | } 184 | 185 | return isValid; 186 | } 187 | 188 | // utils for compress rule 189 | function isCompressibleResource(resource) { 190 | if (resource.response.content.size <= 150) return false; 191 | const contentType = getResponseHeaderFromResource(resource, 'content-type'); 192 | return compressible.some((value) => value.test(contentType)); 193 | } 194 | 195 | function isResourceCompressed(resource) { 196 | const contentEncoding = getResponseHeaderFromResource(resource, 'content-encoding'); 197 | return contentEncoding.length > 0 && httpCompressionTokens.indexOf(contentEncoding.toLocaleLowerCase()) !== -1; 198 | } 199 | 200 | // utils for ETags rule 201 | function isRessourceUsingETag(resource) { 202 | const eTag = getResponseHeaderFromResource(resource, 'ETag'); 203 | if (eTag === '') return false; 204 | return true; 205 | } 206 | 207 | function getDomainFromUrl(url) { 208 | var elements = url.split('//'); 209 | if (elements[1] === undefined) return ''; 210 | else { 211 | elements = elements[1].split('/'); // get domain with port 212 | elements = elements[0].split(':'); // get domain without port 213 | } 214 | return elements[0]; 215 | } 216 | 217 | /** 218 | * Count character occurences in the given string 219 | */ 220 | function countChar(char, str) { 221 | let total = 0; 222 | str.split('').forEach((curr) => { 223 | if (curr === char) total++; 224 | }); 225 | return total; 226 | } 227 | 228 | /** 229 | * Detect minification for Javascript and CSS files 230 | */ 231 | function isMinified(scriptContent) { 232 | if (!scriptContent) return true; 233 | if (scriptContent.length === 0) return true; 234 | const total = scriptContent.length - 1; 235 | const semicolons = countChar(';', scriptContent); 236 | const linebreaks = countChar('\n', scriptContent); 237 | if (linebreaks < 2) return true; 238 | // Empiric method to detect minified files 239 | // 240 | // javascript code is minified if, on average: 241 | // - there is more than one semicolon by line 242 | // - and there are more than 100 characters by line 243 | return semicolons / linebreaks > 1 && linebreaks / total < 0.01; 244 | } 245 | 246 | /** 247 | * Detect network resources (data urls embedded in page is not network resource) 248 | * Test with request.url as request.httpVersion === "data" does not work with old chrome version (example v55) 249 | */ 250 | function isNetworkResource(harEntry) { 251 | return !harEntry.request.url.startsWith('data'); 252 | } 253 | 254 | /** 255 | * Detect non-network resources (data urls embedded in page) 256 | * Test with request.url as request.httpVersion === "data" does not work with old chrome version (example v55) 257 | */ 258 | function isDataResource(harEntry) { 259 | return harEntry.request.url.startsWith('data'); 260 | } 261 | 262 | function computeNumberOfErrorsInJSCode(code, url) { 263 | let errorNumber = 0; 264 | try { 265 | const syntax = require('esprima').parse(code, { tolerant: true, sourceType: 'script', loc: true }); 266 | if (syntax.errors) { 267 | if (syntax.errors.length > 0) { 268 | errorNumber += syntax.errors.length; 269 | debug(() => `url ${url} : ${Syntax.errors.length} errors`); 270 | } 271 | } 272 | } catch (err) { 273 | errorNumber++; 274 | debug(() => `url ${url} : ${err} `); 275 | } 276 | return errorNumber; 277 | } 278 | 279 | function isHttpRedirectCode(code) { 280 | return httpRedirectCodes.some((value) => value === code); 281 | } 282 | 283 | function getImageTypeFromResource(resource) { 284 | const contentType = getResponseHeaderFromResource(resource, 'content-type'); 285 | if (contentType === 'image/png') return 'png'; 286 | if (contentType === 'image/jpeg') return 'jpeg'; 287 | if (contentType === 'image/gif') return 'gif'; 288 | if (contentType === 'image/bmp') return 'bmp'; 289 | if (contentType === 'image/tiff') return 'tiff'; 290 | return ''; 291 | } 292 | 293 | function getMinOptimisationGainsForImage(pixelsNumber, imageSize, imageType) { 294 | // difficult to get good compression when image is small , images less than 10Kb are considered optimized 295 | if (imageSize < 10000) return 0; 296 | 297 | // image png or gif < 50Kb are considered optimized (used for transparency not supported in jpeg format) 298 | if (imageSize < 50000 && (imageType === 'png' || imageType === 'gif')) return 0; 299 | 300 | let imgMaxSize = Math.max(pixelsNumber / 5, 10000); // difficult to get under 10Kb 301 | 302 | // image > 500Kb are too big for web site , there are considered never optimized 303 | if (imageSize > 500000) return Math.max(imageSize - 500000, imageSize - imgMaxSize); 304 | 305 | return Math.max(0, imageSize - imgMaxSize); 306 | } 307 | 308 | function isSvgUrl(url) { 309 | if (url.endsWith('.svg')) return true; 310 | if (url.includes('.svg?')) return true; 311 | return false; 312 | } 313 | 314 | function isSvgOptimized(svgImage) { 315 | if (svgImage.length < 1000) return true; // do not consider image < 1KB 316 | if (svgImage.search(' <') === -1) return true; 317 | return false; 318 | } 319 | 320 | function getOfficialSocialButtonFormUrl(url) { 321 | if (url.includes('platform.twitter.com/widgets.js')) return 'tweeter'; 322 | if (url.includes('platform.linkedin.com/in.js')) return 'linkedin'; 323 | if (url.includes('assets.pinterest.com/js/pinit.js')) return 'pinterest'; 324 | if (url.includes('connect.facebook.net') && url.includes('sdk.js')) return 'facebook'; 325 | if (url.includes('platform-api.sharethis.com/js/sharethis.js')) return 'sharethis.com (mutliple social network) '; 326 | if (url.includes('s7.addthis.com/js/300/addthis_widget.js')) return 'addthis.com (mutliple social network) '; 327 | if (url.includes('static.addtoany.com/menu/page.js')) return 'addtoany.com (mutliple social network) '; 328 | return ''; 329 | } 330 | 331 | function debug(lazyString) { 332 | if (!DEBUG) return; 333 | const message = typeof lazyString === 'function' ? lazyString() : lazyString; 334 | console.log(`GreenIT-Analysis [DEBUG] ${message}\n`); 335 | } 336 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "rule_AddExpiresOrCacheControlHeaders": "Add expires or cache-control headers (>= 95%)", 3 | "rule_AddExpiresOrCacheControlHeaders_DetailDescription": "The Expires and Cache-Control headers determine how long a browser should keep a resource in its cache. You must therefore use and configure them correctly for CSS style sheets, JavaScript scripts, and images. Ideally, these elements should be retained as long as possible so that the browser no longer requests them from the server. This saves HTTP requests, bandwidth and server power.", 4 | "rule_AddExpiresOrCacheControlHeaders_Comment": "%s hidden resources", 5 | "rule_AddExpiresOrCacheControlHeaders_DetailComment": "%s KB has no Expires or Cache-Control header", 6 | "rule_CompressHttp": "Compress resources (>= 95%)", 7 | "rule_CompressHttp_DetailDescription": "You can compress the content of HTML pages to minimize bandwidth consumption between the client and the server. All modern browsers (for smartphones, tablets, notebook and desktop computers) accept HTML compressed via gzip or Deflate. The easiest way to do so is to configure the web server so that it compresses the HTML data stream, either on-the-fly or automatically, as it leaves the server. This practice (on-the-fly compression) is only beneficial for a HTML data stream as it is constantly evolving. When possible, we recommend that you manually compress static resources (e.g. CSS and JavaScript libraries) all in one go.", 8 | "rule_CompressHttp_Comment": "%s compressed resources", 9 | "description": "Percentage of resources compressed", 10 | "rule_CompressHttp_DetailComment": "%s KB is not compressed", 11 | "rule_DomainsNumber": "Limit the number of domains (< 3)", 12 | "rule_DomainsNumber_DetailDescription": "When a website or online service hosts a web page's components across several domains, the browser has to establish an HTTP connection with every single one. Once the HTML page has been retrieved, the browser calls the sources as it traverses the DOM (Document Object Model). Some resources are essential for the page to work. If they are hosted on another domain which is slow, it may increase the page's render time. You should therefore, when possible, group all resources on a single domain. The only exception to this is for static resources (style sheets, images, etc.), which should be hosted on a separate domain to avoid sending one or multiple cookies for each browser GET HTTP request. This reduces response time and unnecessary bandwidth consumption.", 13 | "rule_DomainsNumber_Comment": "%s domain(s) found", 14 | "rule_DontResizeImageInBrowser": "Do not resize images in the browser", 15 | "rule_DontResizeImageInBrowser_DetailDescription": "Do not resize images using HTML height and width attributes. Doing so sends images in their original size, wasting bandwidth and CPU power.A PNG-24 350 x 300 px image is 41 KB. If you resized the same image file using HTML, and displayed it as a 70 x 60 px thumbnail, it would still be 41 KB, when in reality it should be no more than 3 KB! Meaning 38 KB downloaded for nothing. The best solution is to resize images using software such as Photoshop, without using HTML. When content added by the website's users has no specific added value, it is best to prevent them from being able to insert images using a WYSIWYG editor e.g. CKEditor.", 16 | "rule_DontResizeImageInBrowser_Comment": "%s resized image(s) in the browser", 17 | "rule_DontResizeImageInBrowser_DetailComment": "%s is resized from %s to %s", 18 | "rule_EmptySrcTag": "Avoid empty SRC tags", 19 | "rule_EmptySrcTag_DetailDescription": "If there is an image tag with an empty src attribute, the browser will call the directory in which the page is located, generating unnecessary, additional HTTP requests.", 20 | "rule_EmptySrcTag_DefaultComment": "No empty SRC tag", 21 | "rule_EmptySrcTag_Comment": "%s empty SRC tag(s)", 22 | "rule_ExternalizeCss": "Externalize css", 23 | "rule_ExternalizeCss_DetailDescription": "Ensure that CSS files is separate from the page's HTML code. If you include CSS in the body of the HTML file, and it is used for several pages (or even the whole website), then the code must be sent for each page requested by the user, therefore increasing the volume of data sent. However, if the CSS file is in their own separate files, the browser can avoid requesting them again by storing them in its local cache.", 24 | "rule_ExternalizeCss_DefaultComment": "No inline stylesheet", 25 | "rule_ExternalizeCss_Comment": "%s inline stylesheet(s)", 26 | "rule_ExternalizeJs": "Externalize js", 27 | "rule_ExternalizeJs_DetailDescription": "Ensure that JavaScript code is separate from the page's HTML code, with the exception of any configuration variables for JavaScript objects. If you include JavaScript code in the body of the HTML file, and it is used for several pages (or even the whole website), then the code must be sent for each page requested by the user, therefore increasing the volume of data sent. However, if the JavaScript code are in their own separate files, the browser can avoid requesting them again by storing them in its local cache.", 28 | "rule_ExternalizeJs_DefaultComment": "No JavaScript inline", 29 | "rule_ExternalizeJs_Comment": "%s inline javascript(s)", 30 | "rule_HttpError": "Avoid error requests", 31 | "rule_HttpError_DetailDescription": "Error requests consume resources unnecessarily.", 32 | "rule_HttpError_Comment": "%s HTTP error(s)", 33 | "rule_HttpRequests": "Limit the number of HTTP requests (< 27)", 34 | "rule_HttpRequests_DetailDescription": "A page's download time client-side directly correlates to the number and size of files the browser has to download. For each file, the browser sends a GET HTTP to the server. It waits for the response, and then downloads the resource as soon as it is available. Depending on the type of web server you use, the more requests per page there are, the fewer pages the server can handle. Reducing the number of requests per page is key to reducing the number of HTTP severs needed to run the website, and consequently its environmental impact.", 35 | "rule_HttpRequests_Comment": "%s HTTP request(s)", 36 | "rule_ImageDownloadedNotDisplayed": "Do not download images unnecessarily", 37 | "rule_ImageDownloadedNotDisplayed_DetailDescription": "Downloading images that will not necessarily be visible consumes resources unnecessarily. These include, for example, images that are only displayed following a user action.", 38 | "rule_ImageDownloadedNotDisplayed_Comment": "%s image(s) downloaded but not displayed on the page", 39 | "rule_ImageDownloadedNotDisplayed_DetailComment": "%s of size %s is not displayed", 40 | "rule_JsValidate": "Validate javascript", 41 | "rule_JsValidate_DetailDescription": "JSLint is a code quality control tool that verifies that the JavaScript syntax used will be understood by all browsers. The code produced therefore conforms to coding rules that allow interpreters to execute the code quickly and easily. The processor is therefore used for a shorter time.", 42 | "rule_JsValidate_DefaultComment": "Javascript validated", 43 | "rule_JsValidate_Comment": "%s javascript error(s)", 44 | "rule_MaxCookiesLength": "Maximum size of cookies per domain (< 512 Bytes)", 45 | "rule_MaxCookiesLength_DetailDescription": "The cookie length should be reduced because it is sent on every request.", 46 | "rule_MaxCookiesLength_DefaultComment": "No cookies", 47 | "rule_MaxCookiesLength_Comment": "Maximum size = %s Bytes", 48 | "rule_MaxCookiesLength_DetailComment": "Cookie of length %s for domain %s", 49 | "rule_MinifiedCss": "Minify css (>= 95%)", 50 | "rule_MinifiedCss_DetailDescription": "Use a tool like YUI Compressor to remove unnecessary spaces and line breaks. Google's Apache mod_pagespeed can also automate this operation.", 51 | "rule_MinifiedCss_DefaultComment": "No css", 52 | "rule_MinifiedCss_Comment": "%s% minified css", 53 | "rule_MinifiedCss_DetailComment": "%s should be minified", 54 | "rule_MinifiedJs": "Minify js (>= 95%)", 55 | "rule_MinifiedJs_DetailDescription": "Use a tool like YUI Compressor to remove unnecessary spaces, line breaks, semicolons, and shorten local variable names. This can be automated using Google's Apache mod_pagespeed module .", 56 | "rule_MinifiedJs_DefaultComment": "No js", 57 | "rule_MinifiedJs_Comment": "%s% minified js", 58 | "rule_MinifiedJs_DetailComment": "%s should be minified", 59 | "rule_NoCookieForStaticRessources": "No cookies for static resources", 60 | "rule_NoCookieForStaticRessources_DetailDescription": "For static resources, a cookie is unnecessary, so it consumes bandwidth unnecessarily. To avoid this, we can use a different domain for static resources or restrict the scope of cookies created", 61 | "rule_NoCookieForStaticRessources_DefaultComment": "No cookies", 62 | "rule_NoCookieForStaticRessources_Comment": "%s static resource(s) with a cookie (In total %s KB)", 63 | "rule_NoCookieForStaticRessources_DetailComment": "%s has cookie(s)", 64 | "rule_NoRedirect": "Avoid redirects", 65 | "rule_NoRedirect_DetailDescription": "Redirects should be avoided whenever it is possible as they slow down the response and consume resources unnecessarily.", 66 | "rule_NoRedirect_Comment": "%s redirect(s)", 67 | "rule_OptimizeBitmapImages": "Optimize bitmap images", 68 | "rule_OptimizeBitmapImages_DefaultComment": "No bitmap images to optimize", 69 | "rule_OptimizeBitmapImages_DetailDescription": "Bitmap images often make up the majority of bytes downloaded, just ahead of CSS and JavaScript libraries. So optimizing them has a huge impact on bandwidth consumed.", 70 | "rule_OptimizeBitmapImages_Comment": "%s image(s) probably to be optimized, estimated minimum gain: %s KB", 71 | "rule_OptimizeBitmapImages_DetailComment": "%s KB %s, possibility of winning %s KB", 72 | "rule_OptimizeSvg": "Optimize svg images", 73 | "rule_OptimizeSvg_DefaultComment": "No svg to optimize", 74 | "rule_OptimizeSvg_DetailDescription": "Svg images are less heavy than bitmap images, they can however be optimized and minified by using tools (for example svgo)", 75 | "rule_OptimizeSvg_Comment": "%s image(s) to optimize", 76 | "rule_OptimizeSvg_DetailComment": "%s could be optimized (%s KB)", 77 | "rule_Plugins": "Do not use plugins", 78 | "rule_Plugins_DetailDescription": "Avoid using plugins (Flash Player, Java and Silverlight virtual machines, etc.) because they can be a heavy drain on resources (CPU and RAM). This is especially true with Adobe's Flash Player, to such an extent that Apple decided to not install the technology on its mobile devices to maximize battery life. Favor standard technology such as HTML5 and ECMAScript.", 79 | "rule_Plugins_DefaultComment": "No plugins", 80 | "rule_Plugins_Comment": "%s plugin(s)", 81 | "rule_PrintStyleSheet": "Provide a css print", 82 | "rule_PrintStyleSheet_DetailDescription": "In addition to the benefits for the user, this style sheet reduces the number of pages printed, and therefore indirectly minimizes the website's ecological footprint. It should be as streamlined as possible and employ an ink-light typeface e.g. Century Gothic. Also consider hiding the header, footer, menu and sidebar, as well as deleting all images except those needed for content. This print style sheet makes for a cleaner print by trimming down what is displayed on the screen.", 83 | "rule_PrintStyleSheet_DefaultComment": "No css print", 84 | "rule_PrintStyleSheet_Comment": "%s print css", 85 | "rule_SocialNetworkButton": "Do not use standard social network buttons", 86 | "rule_SocialNetworkButton_DetailDescription": "Social networks such as Facebook, Twitter, Pinterest, etc. provide plugins to install on a WEB page to display a share button and a like counter. These plugins consume resources unnecessarily, it is better to put direct links", 87 | "rule_SocialNetworkButton_DefaultComment": "No standard social network button found", 88 | "rule_SocialNetworkButton_Comment": "%s standard button(s) found", 89 | "rule_SocialNetworkButton_detailComment": "A %s script was found", 90 | "rule_StyleSheets": "Limit the number of css files (<3)", 91 | "rule_StyleSheets_DetailDescription": "Minimize the number of CSS files to reduce the number of HTTP requests. If several style sheets are used on all of the website's pages, concatenate them into one single file. Some CMS and frameworks offer ways to do such optimization automatically.", 92 | "rule_StyleSheets_DefaultComment": "No more than 2 css files", 93 | "rule_StyleSheets_Comment": "%s css files", 94 | "rule_UseETags": "Use ETags (>= 95%)", 95 | "rule_UseETags_DetailDescription": "An ETag is a signature attached a server response. If the client requests a URL (HTML page, image, style sheet, etc.) whose ETag is identical to the one it already has, the web server will reply that it does not need to download the resource and that it should use the one it already possesses. Using ETags saves huge amounts of bandwidth.", 96 | "rule_UseETags_Comment": "%s resources using ETags", 97 | "rule_UseETags_DetailComment": "%s Ko does not use ETags", 98 | "rule_UseStandardTypefaces": "Use standard fonts", 99 | "rule_UseStandardTypefaces_DetailDescription": "Use standard typefaces as they already exist on the user's computer, and therefore do not need to be downloaded. This saves bandwidth and improves the website's render time.", 100 | "rule_UseStandardTypefaces_DefaultComment": "No specific fonts", 101 | "rule_UseStandardTypefaces_Comment": "%s KB of specific character font(s)", 102 | "date": "Date", 103 | "hostname": "Hostname", 104 | "platform": "Platform", 105 | "connection": "Connection", 106 | "grade": "Grade", 107 | "ecoIndex": "EcoIndex", 108 | "nbPages": "Number of pages", 109 | "timeout": "Timeout", 110 | "nbConcAnalysis": "Number of concurrent analysis", 111 | "nbAdditionalAttemps": "Number of additional attempts in case of failure", 112 | "nbErrors": "Number of analysis errors", 113 | "analysisErrors": "Analysis errors:", 114 | "priorityPages": "Priority pages:", 115 | "rulesToApply": "Rules to apply:", 116 | "url": "URL", 117 | "water": "Water (cl)", 118 | "greenhouseGasesEmission": "GHG (gCO2e)", 119 | "domSize": "DOM size", 120 | "pageSize": "Page size (Ko)", 121 | "nbRequests": "Number of requests", 122 | "nbPlugins": "Number of plugins", 123 | "nbCssFiles": "Number of CSS files", 124 | "nbInlineCss": "Number of \"inline\" CSS", 125 | "nbEmptySrc": "Number of empty \"src\" tags", 126 | "nbInlineJs": "Number of \"inline\" JS", 127 | "resizedImage": "Resized image in browser:", 128 | "bestPractices": "Best practices", 129 | "mobile": "Mobile", 130 | "wired": "Wired", 131 | "noData": "No data", 132 | "greenItAnalysisReport": "GreenIT-Analysis report", 133 | "executionDate": "Execution date", 134 | "scenarios": "Scenario(s)", 135 | "errors": "Error(s)", 136 | "error": "Error", 137 | "scenario": "Scenario", 138 | "shareDueToActions": "Share due to actions", 139 | "bestPracticesToImplement": "Best practices to implement", 140 | "priority": "Priority", 141 | "allPriorities": "All priorities", 142 | "bestPractice": "Best practice", 143 | "effort": "Effort", 144 | "impact": "Impact", 145 | "note": "Grade", 146 | "footerEcoIndex": "To understand the EcoIndex calculation:", 147 | "footerBestPractices": "Among the 115 ecodesign best practices, 24 are verified by the tool. To know more:", 148 | "tooltip_ecoIndex": "Environmental performance is represented by a score out of 100 and a grade from A to G (the higher the grade, the better!). The EcoIndex is that of the page, actions included.", 149 | "tooltip_shareDueToActions": "[EcoIndex when opening the page] - [EcoIndex after execution of actions], this part is included in the EcoIndex.", 150 | "tooltip_bestPracticesToImplement": "Number of ecodesign best practices to be implemented per page out of the 24 analyzed by the tool.", 151 | "requests": "Requests", 152 | "steps": "Steps", 153 | "step": "Step", 154 | "result": "Result", 155 | "trend": "Trend", 156 | "bestPracticesToImplementWithNumber": "Best practices (%d to implement)", 157 | "pageLoading": "Page loading" 158 | } 159 | -------------------------------------------------------------------------------- /src/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "rule_AddExpiresOrCacheControlHeaders": "Ajouter des expires ou cache-control headers (>= 95%)", 3 | "rule_AddExpiresOrCacheControlHeaders_DetailDescription": "Les en-têtes Expires et Cache-Control déterminent la durée pendant laquelle un navigateur doit conserver une ressource dans son cache. Vous devez donc les utiliser et les configurer correctement pour les feuilles de style CSS, les scripts JavaScript et les images. Idéalement, ces éléments doivent être conservés le plus longtemps possible pour que le navigateur ne les demande plus au serveur. Cela permet d'économiser les requêtes HTTP, la bande passante et l'alimentation du serveur.", 4 | "rule_AddExpiresOrCacheControlHeaders_Comment": "%s ressources cachées", 5 | "rule_AddExpiresOrCacheControlHeaders_DetailComment": "%s Ko n'a pas d'Expires ou Cache-Control header", 6 | "rule_CompressHttp": "Compresser les ressources (>= 95%) ", 7 | "rule_CompressHttp_DetailDescription": "Vous pouvez compresser le contenu des pages HTML pour réduire la consommation de bande passante entre le client et le serveur. Tous les navigateurs modernes (smartphones, tablettes, ordinateurs portables et ordinateurs de bureau) acceptent le format HTML compressé via gzip ou Deflate. Le moyen le plus simple consiste à configurer le serveur Web de manière à ce qu'il comprime le flux de données HTML, à la volée ou automatiquement, à la sortie du serveur. Cette pratique (compression à la volée) n’est bénéfique que pour un flux de données HTML, car il évolue constamment. Lorsque cela est possible, nous vous recommandons de compresser manuellement les ressources statiques (par exemple, les bibliothèques CSS et JavaScript) en une seule fois.", 8 | "rule_CompressHttp_Comment": "%s ressources compressées", 9 | "description": "Percentage of resources compressed", 10 | "rule_CompressHttp_DetailComment": "%s Ko n'est pas compressé", 11 | "rule_DomainsNumber": "Limiter le nombre de domaines (< 3)", 12 | "rule_DomainsNumber_DetailDescription": "Lorsqu'un site Web ou un service en ligne héberge les composants d'une page Web dans plusieurs domaines, le navigateur doit établir une connexion HTTP avec chacun d'entre eux. Une fois la page HTML récupérée, le navigateur appelle les sources lorsqu'il traverse le DOM (Document Object Model). Certaines ressources sont essentielles au bon fonctionnement de la page. S'ils sont hébergés sur un autre domaine qui est lent, cela peut augmenter le temps de rendu de la page. Vous devez donc, lorsque cela est possible, regrouper toutes les ressources sur un seul domaine. La seule exception à cette règle concerne les ressources statiques (feuilles de style, images, etc.), qui doivent être hébergées sur un domaine distinct afin d'éviter l'envoi d'un ou plusieurs cookies pour chaque requête HTTP du navigateur GET. Cela réduit le temps de réponse et la consommation inutile de bande passante.", 13 | "rule_DomainsNumber_Comment": "%s domaine(s) trouvé(s)", 14 | "rule_DontResizeImageInBrowser": "Ne pas retailler les images dans le navigateur", 15 | "rule_DontResizeImageInBrowser_DetailDescription": "Ne redimensionnez pas les images avec les attributs HTML height et width. Cela envoie des images dans leur taille originale, gaspillant de la bande passante et de la puissance du processeur. Une image PNG-24 de 350 x 300 px est de 41 KB. Si vous redimensionniez le même fichier image en HTML et que vous l’affichez sous forme de vignette de 70 x 60 px, il s’agirait toujours de 41 Ko, alors qu’il devrait en réalité ne pas dépasser 3 Ko! Signification 38 KB téléchargés pour rien. La meilleure solution consiste à redimensionner les images à l'aide d'un logiciel tel que Photoshop, sans utiliser HTML. Lorsque le contenu ajouté par les utilisateurs du site Web n’a pas de valeur ajoutée spécifique, il est préférable de les empêcher d’insérer des images à l’aide d’un éditeur WYSIWYG, par exemple CKEditor.", 16 | "rule_DontResizeImageInBrowser_Comment": "%s image(s) retaillée(s) dans le navigateur", 17 | "rule_DontResizeImageInBrowser_DetailComment": "%s est redimensionnée de %s à %s", 18 | "rule_EmptySrcTag": "Eviter les tags SRC vides", 19 | "rule_EmptySrcTag_DetailDescription": "S'il existe une balise d'image avec un attribut src vide, le navigateur appelle le répertoire dans lequel se trouve la page, générant des requêtes HTTP inutiles et supplémentaires.", 20 | "rule_EmptySrcTag_DefaultComment": "Pas de tag SRC vide", 21 | "rule_EmptySrcTag_Comment": "%s tag(s) SRC vide(s)", 22 | "rule_ExternalizeCss": "Externaliser les css", 23 | "rule_ExternalizeCss_DetailDescription": "Assurez-vous que les fichiers CSS sont séparés du code HTML de la page. Si vous incluez du CSS dans le corps du fichier HTML et qu'il est utilisé pour plusieurs pages (ou même pour l'ensemble du site Web), le code doit être envoyé pour chaque page demandée par l'utilisateur, augmentant ainsi le volume de données envoyées. Toutefois, si les CSS se trouvent dans leurs propres fichiers distincts, le navigateur peut éviter de les redemander en les stockant dans son cache local.", 24 | "rule_ExternalizeCss_DefaultComment": "Pas de inline stylesheet", 25 | "rule_ExternalizeCss_Comment": "%s inline stylesheet(s)", 26 | "rule_ExternalizeJs": "Externaliser les js", 27 | "rule_ExternalizeJs_DetailDescription": "Assurez-vous que le code JavaScript est distinct du code HTML de la page, à l’exception de toute variable de configuration pour les objets JavaScript. Si vous incluez du code JavaScript dans le corps du fichier HTML et qu'il est utilisé pour plusieurs pages (ou même pour l'ensemble du site Web), le code doit être envoyé pour chaque page demandée par l'utilisateur, augmentant ainsi le volume de données envoyées. Toutefois, si le code JavaScript se trouve dans son propre fichier séparé, le navigateur peut éviter de les redemander en les stockant dans son cache local.", 28 | "rule_ExternalizeJs_DefaultComment": "Pas d'inline JavaScript", 29 | "rule_ExternalizeJs_Comment": "%s inline javascript(s)", 30 | "rule_HttpError": "Eviter les requêtes en erreur", 31 | "rule_HttpError_DetailDescription": "Les requêtes en erreurs consomment inutilement des ressources.", 32 | "rule_HttpError_Comment": "%s erreur(s) HTTP", 33 | "rule_HttpRequests": "Limiter le nombre de requêtes HTTP (< 27)", 34 | "rule_HttpRequests_DetailDescription": "Le temps de téléchargement d’une page côté client est directement corrélé au nombre et à la taille des fichiers que le navigateur doit télécharger. Pour chaque fichier, le navigateur envoie un HTTP GET au serveur. Il attend la réponse, puis télécharge la ressource dès qu'elle est disponible. Selon le type de serveur Web que vous utilisez, plus le nombre de demandes par page est élevé, moins le serveur peut gérer de pages. La réduction du nombre de requêtes par page est essentielle pour réduire le nombre de serveurs HTTP nécessaires à l'exécution du site Web et, partant, son impact sur l'environnement.", 35 | "rule_HttpRequests_Comment": "%s requête(s) HTTP ", 36 | "rule_ImageDownloadedNotDisplayed": "Ne pas télécharger des images inutilement", 37 | "rule_ImageDownloadedNotDisplayed_DetailDescription": "Télécharger des images qui ne seront pas nécessairement visibles consomme inutilement des ressources. Il s'agit par exemple d'images qui sont affichées uniquement suite à une action utilisateur.", 38 | "rule_ImageDownloadedNotDisplayed_Comment": "%s image(s) téléchargée(s) mais non affichée(s) dans la page ", 39 | "rule_ImageDownloadedNotDisplayed_DetailComment": "%s de taille %s n'est pas affichée", 40 | "rule_JsValidate": "Valider le javascript", 41 | "rule_JsValidate_DetailDescription": "JSLint est un outil de contrôle de qualité du code qui vérifie que la syntaxe JavaScript utilisée sera comprise par tous les navigateurs. Le code produit est donc conforme aux règles de codage qui permettent aux interpréteurs d’exécuter le code rapidement et facilement. Le processeur est donc utilisé pour un temps plus court.", 42 | "rule_JsValidate_DefaultComment": "Javascript validé", 43 | "rule_JsValidate_Comment": "%s erreur(s) javascript", 44 | "rule_MaxCookiesLength": "Taille maximum des cookies par domaine(< 512 Octets)", 45 | "rule_MaxCookiesLength_DetailDescription": "La longueur du cookie doit être réduite car il est envoyé à chaque requête.", 46 | "rule_MaxCookiesLength_DefaultComment": "Pas de cookies", 47 | "rule_MaxCookiesLength_Comment": "Taille maximum = %s Octets ", 48 | "rule_MaxCookiesLength_DetailComment": "Cookie de longueur %s pour le domaine %s", 49 | "rule_MinifiedCss": "Minifier les css (>= 95%)", 50 | "rule_MinifiedCss_DetailDescription": "Utilisez un outil tel que YUI Compressor pour supprimer les espaces et les sauts de ligne inutiles. Apache mod_pagespeed de Google peut également automatiser cette opération.", 51 | "rule_MinifiedCss_DefaultComment": "Aucun css", 52 | "rule_MinifiedCss_Comment": "%s% css minifiées", 53 | "rule_MinifiedCss_DetailComment": "%s devrait être minifié", 54 | "rule_MinifiedJs": "Minifier les js (>= 95%)", 55 | "rule_MinifiedJs_DetailDescription": "Utilisez un outil tel que YUI Compressor pour supprimer les espaces inutiles, les sauts de ligne, les points-virgules et raccourcir les noms de variables locales. Cette opération peut être automatisée à l’aide du module Apache mod_pagespeed de Google.", 56 | "rule_MinifiedJs_DefaultComment": "Aucun js", 57 | "rule_MinifiedJs_Comment": "%s% js minifié", 58 | "rule_MinifiedJs_DetailComment": "%s devrait être minifié", 59 | "rule_NoCookieForStaticRessources": "Pas de cookie pour les ressources statiques", 60 | "rule_NoCookieForStaticRessources_DetailDescription": "Pour les ressources statiques, un cookie est inutile, cela consomme donc inutilement de la bande passante. Pour éviter cela, on peut utiliser un domaine différent pour les ressources statiques ou restreindre la portée des cookies crées", 61 | "rule_NoCookieForStaticRessources_DefaultComment": "Aucun cookie", 62 | "rule_NoCookieForStaticRessources_Comment": "%s ressource(s) statiques avec un cookie (Au total %s Ko)", 63 | "rule_NoCookieForStaticRessources_DetailComment": "%s a un(des) cookie(s)", 64 | "rule_NoRedirect": "Eviter les redirections", 65 | "rule_NoRedirect_DetailDescription": "Les redirections doivent être évitées autant que possible car elles ralentissent la réponse et consomment inutilement des ressources.", 66 | "rule_NoRedirect_Comment": "%s redirection(s)", 67 | "rule_OptimizeBitmapImages": "Optimiser les images bitmap", 68 | "rule_OptimizeBitmapImages_DefaultComment": "Pas d'images bitmap à optimiser", 69 | "rule_OptimizeBitmapImages_DetailDescription": "Les images bitmap constituent souvent la plupart des octets téléchargés, juste devant les bibliothèques CSS et JavaScript. Leur optimisation a donc un impact considérable sur la bande passante consommée.", 70 | "rule_OptimizeBitmapImages_Comment": "%s image(s) à probablement optimiser, gain minimum estimé: %s Ko", 71 | "rule_OptimizeBitmapImages_DetailComment": "%s Ko %s, possibilité de gagner %s Ko", 72 | "rule_OptimizeSvg": "Optimiser les images svg", 73 | "rule_OptimizeSvg_DefaultComment": "Pas de svg à optimiser", 74 | "rule_OptimizeSvg_DetailDescription": "Les images svg sont moins lourdes que les images bitmap, elles peuvent cependant être optimisées et minifiées via des outils (par exemple svgo)", 75 | "rule_OptimizeSvg_Comment": "%s image(s) à optimiser", 76 | "rule_OptimizeSvg_DetailComment": "%s pourrait être optimisée (%s Ko)", 77 | "rule_Plugins": "Ne pas utiliser de plugins", 78 | "rule_Plugins_DetailDescription": "Évitez d’utiliser des plug-ins (machines virtuelles Flash Player, Java et Silverlight, etc.), car ils peuvent entraîner une lourde charge de ressources (processeur et RAM). C’est particulièrement vrai avec le lecteur Adobe, à tel point qu’Apple a décidé de ne pas installer la technologie sur ses appareils mobiles afin de maximiser la durée de vie de la batterie. Privilégiez les technologies standard telles que HTML5 et ECMAScript", 79 | "rule_Plugins_DefaultComment": "Aucun plugin", 80 | "rule_Plugins_Comment": "%s plugin(s)", 81 | "rule_PrintStyleSheet": "Fournir une print css", 82 | "rule_PrintStyleSheet_DetailDescription": "Outre les avantages pour l’utilisateur, cette feuille de style réduit le nombre de pages imprimées et donc réduit indirectement l’empreinte écologique du site Web. Elle doit être aussi simple que possible et utiliser une police de caractères à l'encre claire, par exemple Siècle gothique. Envisagez également de masquer l'en-tête, le pied de page, le menu et la barre latérale, ainsi que de supprimer toutes les images, à l'exception de celles nécessaires au contenu. Cette feuille de style d'impression permet d'obtenir une impression plus nette en réduisant ce qui est affiché à l'écran.", 83 | "rule_PrintStyleSheet_DefaultComment": "Pas de print css", 84 | "rule_PrintStyleSheet_Comment": "%s print css", 85 | "rule_SocialNetworkButton": "N'utilisez pas les boutons standards des réseaux sociaux", 86 | "rule_SocialNetworkButton_DetailDescription": "Les réseaux sociaux tels que Facebook, Twiter, Pinterest, etc. fournissent des plugins à installer sur une page WEB pour y afficher un bouton partager et un compteur de j'aime. Ces plugins consomme des ressources inutilement, il est mieux de mettre des liens directs", 87 | "rule_SocialNetworkButton_DefaultComment": "Pas de bouton standard de réseau social trouvé", 88 | "rule_SocialNetworkButton_Comment": "%s bouton(s) standart(s) trouvé(s)", 89 | "rule_SocialNetworkButton_detailComment": "Un script %s a été trouvé", 90 | "rule_StyleSheets": "Limiter le nombre de fichiers css (<3) ", 91 | "rule_StyleSheets_DetailDescription": "Réduisez le nombre de fichiers CSS pour réduire le nombre de requêtes HTTP. Si plusieurs feuilles de style sont utilisées sur toutes les pages du site Web, concaténez-les dans un seul fichier. Certains CMS et frameworks offrent des moyens d'effectuer cette optimisation automatiquement.", 92 | "rule_StyleSheets_DefaultComment": "Pas plus de 2 fichiers css", 93 | "rule_StyleSheets_Comment": "%s fichiers css", 94 | "rule_UseETags": "Utiliser des ETags (>= 95%)", 95 | "rule_UseETags_DetailDescription": "Un ETag est une signature associée à une réponse du serveur. Si le client demande une URL (page HTML, image, feuille de style, etc.) dont l'ETag est identique à celle qu'il a déjà, le serveur Web répondra qu'il n'a pas besoin de télécharger la ressource et qu'il doit utiliser celle-ci. il possède déjà. L'utilisation d'ETags permet d'économiser d'énormes quantités de bande passante.", 96 | "rule_UseETags_Comment": "%s ressources utilisant des ETags", 97 | "rule_UseETags_DetailComment": "%s Ko n'utilise pas d'ETags", 98 | "rule_UseStandardTypefaces": "Utiliser des polices de caractères standards", 99 | "rule_UseStandardTypefaces_DetailDescription": "Utilisez des polices standards telles qu’elles existent déjà sur l’ordinateur de l’utilisateur et n’ont donc pas besoin d’être téléchargées. Cela économise de la bande passante et améliore le temps de rendu du site Web.", 100 | "rule_UseStandardTypefaces_DefaultComment": "Pas de polices de caractères spécifiques", 101 | "rule_UseStandardTypefaces_Comment": "%s Ko de police(s) de caractères spécifique(s)", 102 | "date": "Date", 103 | "hostname": "Hostname", 104 | "platform": "Plateforme", 105 | "connection": "Connexion", 106 | "grade": "Grade", 107 | "ecoIndex": "EcoIndex", 108 | "nbPages": "Nombre de pages", 109 | "timeout": "Timeout", 110 | "nbConcAnalysis": "Nombre d'analyses concurrentes", 111 | "nbAdditionalAttemps": "Nombre d'essais supplémentaires en cas d'échec", 112 | "nbErrors": "Nombre d'erreurs d'analyse", 113 | "analysisErrors": "Erreurs d'analyse :", 114 | "priorityPages": "Pages prioritaires :", 115 | "rulesToApply": "Règles à appliquer :", 116 | "url": "URL", 117 | "water": "Eau (cl)", 118 | "greenhouseGasesEmission": "GES (gCO2e)", 119 | "domSize": "Taille du DOM", 120 | "pageSize": "Taille de la page (Ko)", 121 | "nbRequests": "Nombre de requêtes", 122 | "nbPlugins": "Nombre de plugins", 123 | "nbCssFiles": "Nombre de fichiers CSS", 124 | "nbInlineCss": "Nombre de \"inline\" CSS", 125 | "nbEmptySrc": "Nombre de tags \"src\" vides", 126 | "nbInlineJs": "Nombre de \"inline\" JS", 127 | "resizedImage": "Image retaillée dans le navigateur :", 128 | "bestPractices": "Bonnes pratiques", 129 | "mobile": "Mobile", 130 | "wired": "Filaire", 131 | "noData": "Pas de données", 132 | "greenItAnalysisReport": "Rapport GreenIT-Analysis", 133 | "executionDate": "Date d'exécution", 134 | "scenarios": "Scénario(s)", 135 | "errors": "Erreur(s)", 136 | "error": "Erreur", 137 | "scenario": "Scénario", 138 | "shareDueToActions": "Part due aux actions", 139 | "bestPracticesToImplement": "Bonnes pratiques à mettre en oeuvre", 140 | "priority": "Priorité", 141 | "allPriorities": "Toutes les priorités", 142 | "bestPractice": "Bonne pratique", 143 | "effort": "Effort", 144 | "impact": "Impact", 145 | "note": "Grade", 146 | "footerEcoIndex": "Pour comprendre le calcul de l'EcoIndex :", 147 | "footerBestPractices": "Parmi les 115 bonnes pratiques d'écoconception, 24 sont vérifiées par l'outil. Pour en savoir plus :", 148 | "tooltip_ecoIndex": "La performance environnementale est représentée par un score sur 100 et une note de A à G (plus la note est élévée, mieux c'est !). L'EcoIndex est celui de la page, actions comprises.", 149 | "tooltip_shareDueToActions": "[EcoIndex à l'ouverture de la page] - [EcoIndex après l'exécution des actions], cette part est inclue dans l'EcoIndex.", 150 | "tooltip_bestPracticesToImplement": "Nombre de bonnes pratiques d'écoconception à mettre en oeuvre par page sur les 24 analysées par l'outil.", 151 | "requests": "Requêtes", 152 | "steps": "Etapes", 153 | "step": "Etape", 154 | "result": "Résultat", 155 | "trend": "Tendance", 156 | "bestPracticesToImplementWithNumber": "Bonnes pratiques (%d à mettre en oeuvre)", 157 | "pageLoading": "Chargement de la page" 158 | } 159 | -------------------------------------------------------------------------------- /tests/commands/analyse.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const analyse_core = require('../../src/commands/analyse.js').analyse_core; 4 | 5 | describe('Test de la fonction analyse', () => { 6 | const inputFilePath = 'samples/greenit-url.yml'; 7 | const outputFolder = 'tests/commands/output'; 8 | const outputFolderPath = path.join(__dirname, 'output'); 9 | const outputPath = path.join(outputFolder, 'json/globalReport.json'); 10 | const reportFilePath = path.join(outputFolder, 'greenit.html'); 11 | const referencePath = 'tests/commands/reference/globalReport.json'; 12 | const referenceFolderPath = path.join(__dirname, 'reference'); 13 | 14 | const timeoutTest = 1 * 60 * 1000; 15 | 16 | beforeAll(() => { 17 | clearDirectory(outputFolderPath); 18 | }); 19 | 20 | afterAll(() => { 21 | clearDirectory(outputFolderPath); 22 | }); 23 | 24 | it("doit déclencher l'analyse du fichier samples/greenit-url.yml", async() => { 25 | // Arrange 26 | const options = { 27 | url_input_file: inputFilePath, 28 | report_output_file: reportFilePath, 29 | max_tab: 1, 30 | timeout: 10000, 31 | retry: 3 32 | } 33 | 34 | // Act 35 | await analyse_core(options); 36 | 37 | // Assert 38 | const result = JSON.parse(fs.readFileSync(outputPath, 'utf-8')); 39 | const expected = JSON.parse(fs.readFileSync(referencePath, 'utf-8')); 40 | 41 | expect(result.nbScenarios).toStrictEqual(expected.nbScenarios); 42 | expect(result.errors).toStrictEqual(expected.errors); 43 | 44 | for (let indexScenario = 1; indexScenario <= result.nbScenarios; indexScenario++) { 45 | expectScenario(indexScenario, outputFolderPath, referenceFolderPath); 46 | } 47 | 48 | 49 | }, timeoutTest); 50 | }); 51 | 52 | function expectScenario(indexScenario, outputFolderPath, referenceFolderPath) { 53 | const result = JSON.parse(fs.readFileSync(path.join(outputFolderPath, 'json', `${indexScenario}.json`), 'utf-8')); 54 | const expected = JSON.parse(fs.readFileSync(path.join(referenceFolderPath, `${indexScenario}.json`), 'utf-8')); 55 | 56 | expect(result.pages.length, `Scenario ${indexScenario}`).toStrictEqual(expected.pages.length); 57 | 58 | for (let indexPage = 0; indexPage < result.pages.length; indexPage++) { 59 | const pageResult = result.pages[indexPage]; 60 | const pageExpected = expected.pages[indexPage]; 61 | expect(pageResult.actions.length, `Scenario ${indexScenario} / Page ${indexPage}`).toStrictEqual(pageExpected.actions.length); 62 | for (let indexAction = 0; indexAction < pageResult.actions.length; indexAction++) { 63 | const actionResult = pageResult.actions[indexAction]; 64 | const actionExpected = pageExpected.actions[indexAction]; 65 | const prefixMessageError = `Scenario ${indexScenario} / Page ${indexPage} / Action ${indexAction}`; 66 | expectToBeNear(`${prefixMessageError} : nbRequest`, actionResult.nbRequest, actionExpected.nbRequest, 5); 67 | //expectToBeNear(`${prefixMessageError} : responsesSize`, actionResult.responsesSize, actionExpected.responsesSize, 100000); 68 | //expectToBeNear(`${prefixMessageError} : responsesSizeUncompress`, actionResult.responsesSizeUncompress, actionExpected.responsesSizeUncompress, 100000); 69 | expectToBeNear(`${prefixMessageError} : domSize`, actionResult.domSize, actionExpected.domSize, 5); 70 | expectToBeNear(`${prefixMessageError} : ecoIndex`, actionResult.ecoIndex, actionExpected.ecoIndex, 2); 71 | } 72 | } 73 | } 74 | 75 | // OK if result = expected +/- delta 76 | // expected-delta <= result <= expected+delta 77 | function expectToBeNear(prefixMessageError, result, expected, delta) { 78 | expect(result, prefixMessageError).toBeGreaterThanOrEqual(expected - delta); 79 | expect(result, prefixMessageError).toBeLessThanOrEqual(expected + delta); 80 | } 81 | 82 | 83 | function clearDirectory(outputFolder) { 84 | if (fs.existsSync(outputFolder)) { 85 | fs.rmSync(outputFolder, { recursive: true }); 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /tests/commands/reference/globalReport.json: -------------------------------------------------------------------------------- 1 | {"date":"02/06/2025 06:26:13","hostname":"collectif.greenit.fr","device":"desktop","connection":"Filaire","grade":"G","ecoIndex":null,"worstEcoIndexes":[{"ecoIndex":69,"grade":"B"},{"ecoIndex":69,"grade":"B"}],"nbScenarios":8,"timeout":15000,"maxTab":1,"retry":3,"errors":[],"worstPages":[{"nb":1,"url":"https://collectif.greenit.fr/"},{"nb":2,"url":"https://collectif.greenit.fr/outils.html"},{"nb":3,"url":"https://collectif.greenit.fr/index_en.html"},{"nb":4,"url":"https://collectif.greenit.fr/"},{"nb":5,"url":"https://collectif.greenit.fr/"}],"worstRules":["AddExpiresOrCacheControlHeaders","UseStandardTypefaces","UseETags","MinifiedJs","JsValidate"],"nbBestPracticesToCorrect":0} --------------------------------------------------------------------------------