diff --git a/README.md b/README.md index f8ec306..6f9ba67 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ npm link ### Installation -1. Créer le dossier `//input` qui vous permettra de mettre à disposition le fichier `` au conteneur : +1. Créer le dossier `//input` qui vous permettra de mettre à disposition le fichier `` au conteneur : ``` mkdir -p //input ``` @@ -122,7 +122,7 @@ docker build -t imageName \ ### Prérequis -Construire le fichier `` qui liste les URL à analyser. Le fichier est au format YAML. +Construire le fichier `` qui liste les URL à analyser. Le fichier est au format YAML. Sa structure est la suivante : @@ -152,12 +152,12 @@ Exemple de fichier `url.yaml` : ### Commande ``` -greenit analyse +greenit analyse ``` Paramètres obligatoires : -- `yaml_input_file` : Chemin vers le fichier YAML listant toutes les URL à analyser. (Valeur par défaut : "url.yaml") -- `xlsx_output_file` : Chemin pour le fichier de sortie. (Valeur par défaut : "results.xlsx") +- `url_input_file` : Chemin vers le fichier YAML listant toutes les URL à analyser. (Valeur par défaut : "url.yaml") +- `report_output_file` : Chemin pour le fichier de sortie. (Valeur par défaut : "results.xlsx") Paramètres optionnels : - `--timeout , -t` : Nombre de millisecondes maximal pour charger une url. (Valeur par défaut : 180000) @@ -200,7 +200,7 @@ Paramètres optionnels : ``` ### Usage avec Docker -1. Déposer le fichier `` dans le dossier `//input`. +1. Déposer le fichier `` dans le dossier `//input`. 2. Lancer l'analyse : ``` docker run -it --init --rm --cap-add=SYS_ADMIN \ diff --git a/cli-core/analysis.js b/cli-core/analysis.js index c374300..cc216cc 100644 --- a/cli-core/analysis.js +++ b/cli-core/analysis.js @@ -71,10 +71,20 @@ async function analyseURL(browser, pageInformations, options) { page.close(); result.success = true; + result.nbBestPracticesToCorrect = 0; + + // Compute number of times where best practices are not respected + for (let key in result.bestPractices) { + if((result.bestPractices[key].complianceLevel || "A") !== "A") { + result.nbBestPracticesToCorrect++; + } + } } catch (error) { result.success = false; - console.error(error); + console.error(`Error while analyzing URL ${pageInformations.url} : `, error); } + const date = new Date(); + result.date = `${date.toLocaleDateString('fr')} ${date.toLocaleTimeString('fr')}`; result.pageInformations = pageInformations; result.tryNb = TRY_NB; result.tabId = TAB_ID; @@ -138,7 +148,7 @@ async function createJsonReports(browser, pagesInformations, options, proxy) { //initialise progress bar let progressBar; if (!options.ci){ - progressBar = new ProgressBar(' Analysing [:bar] :percent Remaining: :etas Time: :elapseds', { + progressBar = new ProgressBar(' Analysing [:bar] :percent Remaining: :etas Time: :elapseds', { complete: '=', incomplete: ' ', width: 40, diff --git a/cli-core/report.js b/cli-core/report.js deleted file mode 100644 index 7e192f8..0000000 --- a/cli-core/report.js +++ /dev/null @@ -1,282 +0,0 @@ -const ExcelJS = require('exceljs'); -const fs = require('fs'); -const path = require('path'); -const ProgressBar = require('progress'); -const axios = require('axios') - -//Path to the url file -const SUBRESULTS_DIRECTORY = path.join(__dirname,'../results'); - -// keep track of worst pages based on ecoIndex -function worstPagesHandler(number){ - return (obj,table) => { - let index; - for (index = 0; index < table.length; index++) { - if (obj.ecoIndex < table[index].ecoIndex) break; - } - let addObj = { - nb : obj.nb, - url : obj.pageInformations.url, - grade : obj.grade, - ecoIndex : obj.ecoIndex - } - table.splice(index,0,addObj); - if (table.length > number) table.pop(); - return table; - } -} - -//keep track of the least followed rule based on grade -function handleWorstRule(bestPracticesTotal,number){ - let table = []; - for (let key in bestPracticesTotal) { - table.push({"name" : key, "total" : bestPracticesTotal[key]}) - } - return table.sort((a,b)=> (a.total - b.total)).slice(0,number).map((obj)=>obj.name); -} - -async function create_global_report(reports,options){ - //Timeout for an analysis - const TIMEOUT = options.timeout || "No data"; - //Concurent tab - const MAX_TAB = options.max_tab || "No data"; - //Nb of retry before dropping analysis - const RETRY = options.retry || "No data"; - //Nb of displayed worst pages - const WORST_PAGES = options.worst_pages; - //Nb of displayed worst rules - const WORST_RULES = options.worst_rules; - - const DEVICE = options.device; - - let handleWorstPages = worstPagesHandler(WORST_PAGES); - - //initialise progress bar - let progressBar; - if (!options.ci){ - progressBar = new ProgressBar(' Create JSON report [:bar] :percent Remaining: :etas Time: :elapseds', { - complete: '=', - incomplete: ' ', - width: 40, - total: reports.length+2 - }); - progressBar.tick() - } else { - console.log('Creating report ...'); - } - - let eco = 0; //future average - let err = []; - let hostname; - let worstPages = []; - let bestPracticesTotal= {}; - //Creating one report sheet per file - reports.forEach((file)=>{ - let obj = JSON.parse(fs.readFileSync(file.path).toString()); - if (!hostname) hostname = obj.pageInformations.url.split('/')[2] - obj.nb = parseInt(file.name); - //handle potential failed analyse - if (obj.success) { - eco += obj.ecoIndex; - handleWorstPages(obj,worstPages); - for (let key in obj.bestPractices) { - bestPracticesTotal[key] = bestPracticesTotal[key] || 0 - bestPracticesTotal[key] += getGradeEcoIndex(obj.bestPractices[key].complianceLevel || "A") - } - } else{ - err.push({ - nb : obj.nb, - url : obj.pageInformations.url, - grade : obj.grade, - ecoIndex : obj.ecoIndex - }); - } - if (progressBar) progressBar.tick() - }) - //Add info the the recap sheet - //Prepare data - const isMobile = (await axios.get('http://ip-api.com/json/?fields=mobile')).data.mobile //get connection type - const date = new Date(); - eco = (reports.length-err.length != 0)? Math.round(eco / (reports.length-err.length)) : "No data"; //Average EcoIndex - let grade = getEcoIndexGrade(eco) - let globalSheet_data = { - date : `${("0" + date.getDate()).slice(-2)}/${("0" + (date.getMonth()+ 1)).slice(-2)}/${date.getFullYear()}`, - hostname : hostname, - device : DEVICE, - connection : (isMobile)? "Mobile":"Filaire", - grade : grade, - ecoIndex : eco, - nbPages : reports.length, - timeout : parseInt(TIMEOUT), - maxTab : parseInt(MAX_TAB), - retry : parseInt(RETRY), - errors : err, - worstPages : worstPages, - worstRules : handleWorstRule(bestPracticesTotal,WORST_RULES) - }; - - if (progressBar) progressBar.tick() - //save report - let filePath = path.join(SUBRESULTS_DIRECTORY,"globalReport.json"); - try { - fs.writeFileSync(filePath, JSON.stringify(globalSheet_data)) - } catch (error) { - throw ` xlsx_output_file : Path "${filePath}" cannot be reached.` - } - return { - globalReport : { - name: "Global Report", - path: filePath - }, - reports - } -} - -//create xlsx report for all the analysed pages and recap on the first sheet -async function create_XLSX_report(reportObject,options){ - //Path of the output file - const OUTPUT_FILE = path.resolve(options.xlsx_output_file); - - const fileList = reportObject.reports; - const globalReport = reportObject.globalReport; - - //initialise progress bar - let progressBar; - if (!options.ci){ - progressBar = new ProgressBar(' Create report [:bar] :percent Remaining: :etas Time: :elapseds', { - complete: '=', - incomplete: ' ', - width: 40, - total: fileList.length+2 - }); - progressBar.tick() - } else { - console.log('Creating report ...'); - } - - - let wb = new ExcelJS.Workbook(); - //Creating the recap page - let globalSheet = wb.addWorksheet(globalReport.name); - let globalReport_data = JSON.parse(fs.readFileSync(globalReport.path).toString()); - let globalSheet_data = [ - [ "Date", globalReport_data.date], - [ "Hostname", globalReport_data.hostname], - [ "Plateforme", globalReport_data.device], - [ "Connexion", globalReport_data.connection], - [ "Grade", globalReport_data.grade], - [ "EcoIndex", globalReport_data.ecoIndex], - [ "Nombre de pages", globalReport_data.nbPages], - [ "Timeout", globalReport_data.timeout], - [ "Nombre d'analyses concurrentes", globalReport_data.maxTab], - [ "Nombre d'essais supplémentaires en cas d'échec", globalReport_data.retry], - [ "Nombre d'erreurs d'analyse", globalReport_data.errors.length], - [ "Erreurs d'analyse :"], - ]; - globalReport_data.errors.forEach(element => { - globalSheet_data.push([element.nb,element.url]) - }); - globalSheet_data.push([],["Pages prioritaires:"]) - globalReport_data.worstPages.forEach((element)=>{ - globalSheet_data.push([element.nb,element.url,"Grade",element.grade,"EcoIndex",element.ecoIndex]) - }) - globalSheet_data.push([],["Règles à appliquer :"]) - globalReport_data.worstRules.forEach( (elem) => { - globalSheet_data.push([elem]) - }); - //add data to the recap sheet - globalSheet.addRows(globalSheet_data); - globalSheet.getCell("B5").fill = { - type: 'pattern', - pattern:'solid', - fgColor:{argb: getGradeColor(globalReport_data.grade) } - } - - if (progressBar) progressBar.tick() - - //Creating one report sheet per file - fileList.forEach((file)=>{ - const sheet_name = file.name; - let obj = JSON.parse(fs.readFileSync(file.path).toString()); - - // Prepare data - let sheet_data = [ - [ "URL", obj.pageInformations.url], - [ "Grade", obj.grade], - [ "EcoIndex", obj.ecoIndex], - [ "Eau (cl)", obj.waterConsumption], - [ "GES (gCO2e)", obj.greenhouseGasesEmission], - [ "Taille du DOM", obj.domSize], - [ "Taille de la page (Ko)", `${Math.round(obj.responsesSize/1000)} (${Math.round(obj.responsesSizeUncompress/1000)})`], - [ "Nombre de requêtes", obj.nbRequest], - [ "Nombre de plugins", obj.pluginsNumber], - [ "Nombre de fichier CSS", obj.printStyleSheetsNumber], - [ "Nombre de \"inline\" CSS", obj.inlineStyleSheetsNumber], - [ "Nombre de tag src vide", obj.emptySrcTagNumber], - [ "Nombre de \"inline\" JS", obj.inlineJsScriptsNumber], - [ "Nombre de requêtes", obj.nbRequest], - - ]; - sheet_data.push([],["Image retaillée dans le navigateur :"]) - for (let elem in obj.imagesResizedInBrowser) { - sheet_data.push([obj.imagesResizedInBrowser[elem].src]) - } - sheet_data.push([],["Best practices :"]) - for (let key in obj.bestPractices) { - sheet_data.push([key,obj.bestPractices[key].complianceLevel || 'A' ]) - } - //Create sheet - let sheet = wb.addWorksheet(sheet_name); - sheet.addRows(sheet_data) - sheet.getCell("B2").fill = { - type: 'pattern', - pattern:'solid', - fgColor:{argb: getGradeColor(obj.grade) } - } - if (progressBar) progressBar.tick() - }) - //save report - try { - await wb.xlsx.writeFile(OUTPUT_FILE); - } catch (error) { - throw ` xlsx_output_file : Path "${OUTPUT_FILE}" cannot be reached.` - } -} - -//EcoIndex -> Grade -function getEcoIndexGrade(ecoIndex){ - if (ecoIndex > 75) return "A"; - if (ecoIndex > 65) return "B"; - if (ecoIndex > 50) return "C"; - if (ecoIndex > 35) return "D"; - if (ecoIndex > 20) return "E"; - if (ecoIndex > 5) return "F"; - return "G"; -} - -//Grade -> EcoIndex -function getGradeEcoIndex(grade){ - if (grade == "A") return 75; - if (grade == "B") return 65; - if (grade == "C") return 50; - if (grade == "D") return 35; - if (grade == "E") return 20; - if (grade == "F") return 5; - return 0; -} - -// Get color code by grade -function getGradeColor(grade){ - if (grade == "A") return "ff009b4f"; - if (grade == "B") return "ff30b857"; - if (grade == "C") return "ffcbda4b"; - if (grade == "D") return "fffbe949"; - if (grade == "E") return "ffffca3e"; - if (grade == "F") return "ffff9349"; - return "fffe002c"; -} - -module.exports = { - create_global_report, - create_XLSX_report -} \ No newline at end of file diff --git a/cli-core/reportExcel.js b/cli-core/reportExcel.js new file mode 100644 index 0000000..58b4551 --- /dev/null +++ b/cli-core/reportExcel.js @@ -0,0 +1,133 @@ +const ExcelJS = require('exceljs'); +const fs = require('fs'); +const path = require('path'); +const ProgressBar = require('progress'); + +//create xlsx report for all the analysed pages and recap on the first sheet +async function create_XLSX_report(reportObject, options){ + //Path of the output file + const OUTPUT_FILE = path.resolve(options.report_output_file); + if (!OUTPUT_FILE.toLowerCase().endsWith('.xlsx')) { + throw ` report_output_file : File "${OUTPUT_FILE}" does not end with the ".xlsx" extension.` + } + + const fileList = reportObject.reports; + const globalReport = reportObject.globalReport; + + //initialise progress bar + let progressBar; + if (!options.ci){ + progressBar = new ProgressBar(' Create Excel report [:bar] :percent Remaining: :etas Time: :elapseds', { + complete: '=', + incomplete: ' ', + width: 40, + total: fileList.length+2 + }); + progressBar.tick() + } else { + console.log('Creating XLSX report ...'); + } + + + let wb = new ExcelJS.Workbook(); + //Creating the recap page + let globalSheet = wb.addWorksheet(globalReport.name); + let globalReport_data = JSON.parse(fs.readFileSync(globalReport.path).toString()); + let globalSheet_data = [ + [ "Date", globalReport_data.date], + [ "Hostname", globalReport_data.hostname], + [ "Plateforme", globalReport_data.device], + [ "Connexion", globalReport_data.connection], + [ "Grade", globalReport_data.grade], + [ "EcoIndex", globalReport_data.ecoIndex], + [ "Nombre de pages", globalReport_data.nbPages], + [ "Timeout", globalReport_data.timeout], + [ "Nombre d'analyses concurrentes", globalReport_data.maxTab], + [ "Nombre d'essais supplémentaires en cas d'échec", globalReport_data.retry], + [ "Nombre d'erreurs d'analyse", globalReport_data.errors.length], + [ "Erreurs d'analyse :"], + ]; + globalReport_data.errors.forEach(element => { + globalSheet_data.push([element.nb,element.url]) + }); + globalSheet_data.push([],["Pages prioritaires:"]) + globalReport_data.worstPages.forEach((element)=>{ + globalSheet_data.push([element.nb,element.url,"Grade",element.grade,"EcoIndex",element.ecoIndex]) + }) + globalSheet_data.push([],["Règles à appliquer :"]) + globalReport_data.worstRules.forEach( (elem) => { + globalSheet_data.push([elem]) + }); + //add data to the recap sheet + globalSheet.addRows(globalSheet_data); + globalSheet.getCell("B5").fill = { + type: 'pattern', + pattern:'solid', + fgColor:{argb: getGradeColor(globalReport_data.grade) } + } + + if (progressBar) progressBar.tick() + + //Creating one report sheet per file + fileList.forEach((file)=>{ + const sheet_name = file.name; + let obj = JSON.parse(fs.readFileSync(file.path).toString()); + + // Prepare data + let sheet_data = [ + [ "URL", obj.pageInformations.url], + [ "Grade", obj.grade], + [ "EcoIndex", obj.ecoIndex], + [ "Eau (cl)", obj.waterConsumption], + [ "GES (gCO2e)", obj.greenhouseGasesEmission], + [ "Taille du DOM", obj.domSize], + [ "Taille de la page (Ko)", `${Math.round(obj.responsesSize/1000)} (${Math.round(obj.responsesSizeUncompress/1000)})`], + [ "Nombre de requêtes", obj.nbRequest], + [ "Nombre de plugins", obj.pluginsNumber], + [ "Nombre de fichier CSS", obj.printStyleSheetsNumber], + [ "Nombre de \"inline\" CSS", obj.inlineStyleSheetsNumber], + [ "Nombre de tag src vide", obj.emptySrcTagNumber], + [ "Nombre de \"inline\" JS", obj.inlineJsScriptsNumber], + [ "Nombre de requêtes", obj.nbRequest], + + ]; + sheet_data.push([],["Image retaillée dans le navigateur :"]) + for (let elem in obj.imagesResizedInBrowser) { + sheet_data.push([obj.imagesResizedInBrowser[elem].src]) + } + sheet_data.push([],["Best practices :"]) + for (let key in obj.bestPractices) { + sheet_data.push([key,obj.bestPractices[key].complianceLevel || 'A' ]) + } + //Create sheet + let sheet = wb.addWorksheet(sheet_name); + sheet.addRows(sheet_data) + sheet.getCell("B2").fill = { + type: 'pattern', + pattern:'solid', + fgColor:{argb: getGradeColor(obj.grade) } + } + if (progressBar) progressBar.tick() + }) + //save report + try { + await wb.xlsx.writeFile(OUTPUT_FILE); + } catch (error) { + throw ` report_output_file : Path "${OUTPUT_FILE}" cannot be reached.` + } +} + +// Get color code by grade +function getGradeColor(grade){ + if (grade == "A") return "ff009b4f"; + if (grade == "B") return "ff30b857"; + if (grade == "C") return "ffcbda4b"; + if (grade == "D") return "fffbe949"; + if (grade == "E") return "ffffca3e"; + if (grade == "F") return "ffff9349"; + return "fffe002c"; +} + +module.exports = { + create_XLSX_report +} \ No newline at end of file diff --git a/cli-core/reportGlobal.js b/cli-core/reportGlobal.js new file mode 100644 index 0000000..e5e2ff9 --- /dev/null +++ b/cli-core/reportGlobal.js @@ -0,0 +1,162 @@ +const ExcelJS = require('exceljs'); +const fs = require('fs'); +const path = require('path'); +const ProgressBar = require('progress'); +const axios = require('axios') + +//Path to the url file +const SUBRESULTS_DIRECTORY = path.join(__dirname,'../results'); + +// keep track of worst pages based on ecoIndex +function worstPagesHandler(number){ + return (obj,table) => { + let index; + for (index = 0; index < table.length; index++) { + if (obj.ecoIndex < table[index].ecoIndex) break; + } + let addObj = { + nb : obj.nb, + url : obj.pageInformations.url, + grade : obj.grade, + ecoIndex : obj.ecoIndex + } + table.splice(index,0,addObj); + if (table.length > number) table.pop(); + return table; + } +} + +//keep track of the least followed rule based on grade +function handleWorstRule(bestPracticesTotal,number){ + let table = []; + for (let key in bestPracticesTotal) { + table.push({"name" : key, "total" : bestPracticesTotal[key]}) + } + return table.sort((a,b)=> (a.total - b.total)).slice(0,number).map((obj)=>obj.name); +} + +async function create_global_report(reports,options){ + //Timeout for an analysis + const TIMEOUT = options.timeout || "No data"; + //Concurent tab + const MAX_TAB = options.max_tab || "No data"; + //Nb of retry before dropping analysis + const RETRY = options.retry || "No data"; + //Nb of displayed worst pages + const WORST_PAGES = options.worst_pages; + //Nb of displayed worst rules + const WORST_RULES = options.worst_rules; + + const DEVICE = options.device; + + let handleWorstPages = worstPagesHandler(WORST_PAGES); + + //initialise progress bar + let progressBar; + if (!options.ci){ + progressBar = new ProgressBar(' Create Global report [:bar] :percent Remaining: :etas Time: :elapseds', { + complete: '=', + incomplete: ' ', + width: 40, + total: reports.length+2 + }); + progressBar.tick() + } else { + console.log('Creating global report ...'); + } + + let eco = 0; //future average + let err = []; + let hostname; + let worstPages = []; + let bestPracticesTotal= {}; + let nbBestPracticesToCorrect = 0; + //Creating one report sheet per file + reports.forEach((file)=>{ + let obj = JSON.parse(fs.readFileSync(file.path).toString()); + if (!hostname) hostname = obj.pageInformations.url.split('/')[2] + obj.nb = parseInt(file.name); + //handle potential failed analyse + if (obj.success) { + eco += obj.ecoIndex; + nbBestPracticesToCorrect += obj.nbBestPracticesToCorrect; + handleWorstPages(obj,worstPages); + for (let key in obj.bestPractices) { + bestPracticesTotal[key] = bestPracticesTotal[key] || 0 + bestPracticesTotal[key] += getGradeEcoIndex(obj.bestPractices[key].complianceLevel || "A") + } + } else{ + err.push({ + nb : obj.nb, + url : obj.pageInformations.url, + grade : obj.grade, + ecoIndex : obj.ecoIndex + }); + } + if (progressBar) progressBar.tick() + }) + //Add info the the recap sheet + //Prepare data + const isMobile = (await axios.get('http://ip-api.com/json/?fields=mobile')).data.mobile //get connection type + const date = new Date(); + eco = (reports.length-err.length != 0)? Math.round(eco / (reports.length-err.length)) : "No data"; //Average EcoIndex + let grade = getEcoIndexGrade(eco) + let globalSheet_data = { + date : `${date.toLocaleDateString('fr')} ${date.toLocaleTimeString('fr')}`, + hostname : hostname, + device : DEVICE, + connection : (isMobile)? "Mobile":"Filaire", + grade : grade, + ecoIndex : eco, + nbPages : reports.length, + timeout : parseInt(TIMEOUT), + maxTab : parseInt(MAX_TAB), + retry : parseInt(RETRY), + errors : err, + worstPages : worstPages, + worstRules : handleWorstRule(bestPracticesTotal,WORST_RULES), + nbBestPracticesToCorrect : nbBestPracticesToCorrect + }; + + if (progressBar) progressBar.tick() + //save report + let filePath = path.join(SUBRESULTS_DIRECTORY,"globalReport.json"); + try { + fs.writeFileSync(filePath, JSON.stringify(globalSheet_data)) + } catch (error) { + throw ` Global report : Path "${filePath}" cannot be reached.` + } + return { + globalReport : { + name: "Global Report", + path: filePath + }, + reports + } +} + +//EcoIndex -> Grade +function getEcoIndexGrade(ecoIndex){ + if (ecoIndex > 75) return "A"; + if (ecoIndex > 65) return "B"; + if (ecoIndex > 50) return "C"; + if (ecoIndex > 35) return "D"; + if (ecoIndex > 20) return "E"; + if (ecoIndex > 5) return "F"; + return "G"; +} + +//Grade -> EcoIndex +function getGradeEcoIndex(grade){ + if (grade == "A") return 75; + if (grade == "B") return 65; + if (grade == "C") return 50; + if (grade == "D") return 35; + if (grade == "E") return 20; + if (grade == "F") return 5; + return 0; +} + +module.exports = { + create_global_report +} \ No newline at end of file diff --git a/cli-core/reportHtml.js b/cli-core/reportHtml.js new file mode 100644 index 0000000..7dfc195 --- /dev/null +++ b/cli-core/reportHtml.js @@ -0,0 +1,210 @@ +const fs = require('fs'); +const path = require('path'); +const ProgressBar = require('progress'); +const TemplateEngine = require('thymeleaf'); +const translator = require('./translator.js').translator; + +//create html report for all the analysed pages and recap on the first sheet +async function create_html_report(reportObject,options){ + //Path of the output file + const OUTPUT_FILE = path.resolve(options.report_output_file); + if (!OUTPUT_FILE.toLowerCase().endsWith('.html')) { + throw ` report_output_file : File "${OUTPUT_FILE}" does not end with the ".html" extension.` + } + + const fileList = reportObject.reports; + const globalReport = reportObject.globalReport; + + //initialise progress bar + let progressBar; + if (!options.ci){ + progressBar = new ProgressBar(' Create HTML report [:bar] :percent Remaining: :etas Time: :elapseds', { + complete: '=', + incomplete: ' ', + width: 40, + total: fileList.length+2 + }); + progressBar.tick() + } else { + console.log('Creating HTML report ...'); + } + + // Read all reports + const allReportsVariables = readAllReports(fileList); + + // Read global report + const globalReportVariables = readGlobalReport(globalReport.path, allReportsVariables); + + // write global report + const templateEngine = new TemplateEngine.TemplateEngine(); + writeGlobalReport(templateEngine, globalReportVariables, OUTPUT_FILE); + + // write all reports + const outputFolder = path.dirname(OUTPUT_FILE); + writeAllReports(templateEngine, allReportsVariables, outputFolder) + + // copy icons in output folder + copyIcons(outputFolder); +} +function readAllReports(fileList) { + let allReportsVariables = []; + let reportVariables = {}; + fileList.forEach((file)=>{ + let report_data = JSON.parse(fs.readFileSync(file.path).toString()); + const pageName = report_data.pageInformations.name || report_data.pageInformations.url; + const pageFilename = report_data.pageInformations.name ? `${removeForbiddenCharacters(report_data.pageInformations.name)}.html` : `${report_data.tabId}.html`; + + if (report_data.success) { + let bestPractices = extractBestPractices(report_data.bestPractices); + reportVariables = { + date: report_data.date, + success: report_data.success, + cssRowError: '', + name: pageName, + link: `${pageName}`, + filename: pageFilename, + header: `GreenIT-Analysis report > ${pageName}`, + bigEcoIndex: `${report_data.ecoIndex} ${report_data.grade}`, + smallEcoIndex: `${report_data.ecoIndex} ${report_data.grade}`, + grade: report_data.grade, + waterConsumption: report_data.waterConsumption, + greenhouseGasesEmission: report_data.greenhouseGasesEmission, + nbRequest: report_data.nbRequest, + pageSize: `${Math.round(report_data.responsesSizeUncompress / 1000)} (${Math.round(report_data.responsesSize / 1000)})`, + domSize: report_data.domSize, + nbBestPracticesToCorrect: report_data.nbBestPracticesToCorrect, + bestPractices + }; + } else { + reportVariables = { + date: report_data.date, + success: report_data.success, + cssRowError: 'bg-danger', + name: pageName, + link: `${pageName}`, + filename: pageFilename, + bestPractices: [] + } + } + allReportsVariables.push(reportVariables); + }); + return allReportsVariables; +} + +function readGlobalReport(path, allReportsVariables) { + let globalReport_data = JSON.parse(fs.readFileSync(path).toString()); + const hasWorstRules = globalReport_data.worstRules?.length > 0 ? true : false; + const globalReportVariables = { + date: globalReport_data.date, + hostname: globalReport_data.hostname, + device: globalReport_data.device, + connection: globalReport_data.connection, + ecoIndex: `${globalReport_data.ecoIndex} ${globalReport_data.grade}`, + grade: globalReport_data.grade, + nbBestPracticesToCorrect: globalReport_data.nbBestPracticesToCorrect, + nbPages: globalReport_data.nbPages, + nbErrors: globalReport_data.errors.length, + allReportsVariables, + worstRulesHeader: hasWorstRules ? `Top ${globalReport_data.worstRules.length} des règles à corriger` : '', + worstRules: hasWorstRules ? globalReport_data.worstRules.map((worstRule, index) => `#${index+1} ${translator.translateRule(worstRule)}`) : '', + cssTablePagesSize: hasWorstRules ? 'col-md-9' : 'col-md-12' + }; + return globalReportVariables; +} + +function extractBestPractices(bestPracticesFromReport) { + const bestPracticesKey = [ + 'AddExpiresOrCacheControlHeaders', + 'CompressHttp', + 'DomainsNumber', + 'DontResizeImageInBrowser', + 'EmptySrcTag', + 'ExternalizeCss', + 'ExternalizeJs', + 'HttpError', + 'HttpRequests', + 'ImageDownloadedNotDisplayed', + 'JsValidate', + 'MaxCookiesLength', + 'MinifiedCss', + 'MinifiedJs', + 'NoCookieForStaticRessources', + 'NoRedirect', + 'OptimizeBitmapImages', + 'OptimizeSvg', + 'Plugins', + 'PrintStyleSheet', + 'SocialNetworkButton', + 'StyleSheets', + 'UseETags', + 'UseStandardTypefaces' + ]; + + let bestPractices = []; + + bestPracticesKey.forEach(key => { + const bestPractice = { + name: translator.translateRule(key), + comment: bestPracticesFromReport[key].comment || '', + iconGrade: `icons/${bestPracticesFromReport[key].complianceLevel || 'A'}.png` + }; + bestPractices.push(bestPractice); + }) + + return bestPractices; +} + +function writeGlobalReport(templateEngine, globalReportVariables, outputFile) { + templateEngine.processFile('cli-core/template/global.html', globalReportVariables) + .then(globalReportHtml => { + fs.writeFileSync(outputFile, globalReportHtml); + }) + .catch(error => { + console.log("Error while reading HTML global template : ", error) + }); +} + +function writeAllReports(templateEngine, allReportsVariables, outputFolder) { + allReportsVariables.forEach(reportVariables => { + templateEngine.processFile('cli-core/template/page.html', reportVariables) + .then(singleReportHtml => { + fs.writeFileSync(`${outputFolder}/${reportVariables.filename}`, singleReportHtml); + }) + .catch(error => { + console.log(`Error while reading HTML template ${reportVariables.filename} : `, error) + }); + }); +} + +function copyIcons(outputFolder) { + const iconsPathFolderDest = path.join(outputFolder, 'icons'); + if (!fs.existsSync(iconsPathFolderDest)){ + fs.mkdirSync(iconsPathFolderDest, { recursive: true }); + } + + const icons = ['A.png', 'B.png', 'C.png']; + icons.forEach(icon => { + const iconPathFileDest = path.join(iconsPathFolderDest, icon); + if(!fs.existsSync(iconPathFileDest)) { + fs.copyFileSync(`cli-core/template/icons/${icon}`, iconPathFileDest); + } + }); +} + +function removeForbiddenCharacters(str) { + str = removeForbiddenCharactersInFile(str); + str = removeAccents(str); + return str; +} + +function removeForbiddenCharactersInFile(str) { + return str.replace(/[/\\?%*:|"<>° ]/g, ''); +} + +function removeAccents(str) { + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); +} + +module.exports = { + create_html_report +} \ No newline at end of file diff --git a/cli-core/template/global.html b/cli-core/template/global.html new file mode 100644 index 0000000..9576fb8 --- /dev/null +++ b/cli-core/template/global.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + GreenIT-Analysis report + + + + +
+
+ + + + + + + + + + + + + + + +
Date d'exécutionHostnamePlateformeType de connexion
2021-05-06 09:12:41hostname:portDesktopFilaire
+
+
+
+
+
+
Ecoindex
+
+

51 C

+
+
+
+ +
+
+
Règles à corriger
+
+

25

+
+
+
+ +
+
+
Pages
+
+

17

+
+
+
+ +
+
+
Erreur(s)
+
+

2

+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
PageEcoindexEau (cl)GES (gCO2e)Règles à corriger
Nom d'une URL analysée Erreur31 A2.32324
+
+
+ + + + + + + + + + + +
Top 5 des règles à corriger
#1 Ajouter des expires ou caches-control headers (>= 95%)
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/cli-core/template/icons/A.png b/cli-core/template/icons/A.png new file mode 100644 index 0000000..639160d Binary files /dev/null and b/cli-core/template/icons/A.png differ diff --git a/cli-core/template/icons/B.png b/cli-core/template/icons/B.png new file mode 100644 index 0000000..a6962c5 Binary files /dev/null and b/cli-core/template/icons/B.png differ diff --git a/cli-core/template/icons/C.png b/cli-core/template/icons/C.png new file mode 100644 index 0000000..4ad3fdf Binary files /dev/null and b/cli-core/template/icons/C.png differ diff --git a/cli-core/template/page.html b/cli-core/template/page.html new file mode 100644 index 0000000..ba04397 --- /dev/null +++ b/cli-core/template/page.html @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + GreenIT-Analysis report + + + + +
+
+
+
+
+
Ecoindex
+
+

51 C

+
+
+
+ +
+
+
Eau (cl)
+
+

3.24

+
+
+
+ +
+
+
GES (gCO2e)
+
+

2.16

+
+
+
+
+
+
+
+
Requêtes
+
+

18

+
+
+
+ +
+
+
Taille de la page (Ko)
+
+

3791 (3787)

+
+
+
+ +
+
+
Taille du DOM
+
+

951

+
+
+
+
+
+ +
+
Bonnes pratiques
+
+
+ + + + + + + + + + + + + +
RègleRésultatNote
Ajouter des expires ou cache-control headers (>= 95%)0% ressources cachées
+
+
+
+
+ + + \ No newline at end of file diff --git a/commands/analyse.js b/commands/analyse.js index 86eef65..3dff39a 100644 --- a/commands/analyse.js +++ b/commands/analyse.js @@ -4,17 +4,19 @@ const path = require('path'); const puppeteer = require('puppeteer'); const createJsonReports = require('../cli-core/analysis.js').createJsonReports; const login = require('../cli-core/analysis.js').login; -const create_global_report = require('../cli-core/report.js').create_global_report; -const create_XLSX_report = require('../cli-core/report.js').create_XLSX_report; +const create_global_report = require('../cli-core/reportGlobal.js').create_global_report; +const create_XLSX_report = require('../cli-core/reportExcel.js').create_XLSX_report; +const create_html_report = require('../cli-core/reportHtml.js').create_html_report; + //launch core async function analyse_core(options) { - const URL_YAML_FILE = path.resolve(options.yaml_input_file); + const URL_YAML_FILE = path.resolve(options.url_input_file); //Get list of pages to analyze and its informations let pagesInformations; try { pagesInformations = YAML.parse(fs.readFileSync(URL_YAML_FILE).toString()); } catch (error) { - throw ` yaml_input_file : "${URL_YAML_FILE}" is not a valid YAML file.` + throw ` url_input_file : "${URL_YAML_FILE}" is not a valid YAML file.` } let browserArgs = [ @@ -71,12 +73,16 @@ async function analyse_core(options) { } //create report let reportObj = await create_global_report(reports, options); - await create_XLSX_report(reportObj, options) + if(options.format === 'html') { + await create_html_report(reportObj, options); + } else { + await create_XLSX_report(reportObj, options); + } } //export method that handle error function analyse(options) { - analyse_core(options).catch(e=>console.error("ERROR : \n" + e)) + analyse_core(options).catch(e=>console.error("ERROR : \n", e)) } module.exports = analyse; \ No newline at end of file diff --git a/greenit b/greenit index a749394..fd46056 100644 --- a/greenit +++ b/greenit @@ -5,14 +5,14 @@ const { hideBin } = require('yargs/helpers') const sizes = require('./sizes.js'); yargs(hideBin(process.argv)) - .command('analyse [yaml_input_file] [xlsx_output_file]', 'Run the analysis', (yargs) => { + .command('analyse [url_input_file] [report_output_file]', 'Run the analysis', (yargs) => { yargs - .positional('yaml_input_file', { + .positional('url_input_file', { describe: 'YAML file path listing all URL', default: "url.yaml" }) - .positional('xlsx_output_file', { - describe: 'Output file path', + .positional('report_output_file', { + describe: 'Report output file path', default: "results.xlsx" }) .option('timeout', { @@ -58,6 +58,12 @@ yargs(hideBin(process.argv)) type: 'string', description: 'Path to YAML file with proxy configuration to apply in Chromium' }) + .option('format', { + alias: 'f', + type: 'string', + description: 'Report format : Possible choices : excel (default), html', + default: 'excel' + }) }, (argv) => { require("./commands/analyse.js")(argv) }) diff --git a/locales/fr.json b/locales/fr.json index 664c2db..1d7aed9 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -33,7 +33,7 @@ "rule_HttpRequests": "Limiter le nombre de requêtes HTTP (< 27)", "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.", "rule_HttpRequests_Comment": "%s requête(s) HTTP ", - "rule_ImageDownloadedNotDisplayed": "Ne télécharger pas des images inutilement", + "rule_ImageDownloadedNotDisplayed": "Ne pas télécharger des images inutilement", "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.", "rule_ImageDownloadedNotDisplayed_Comment": "%s image(s) téléchargée(s) mais non affichée(s) dans la page ", "rule_ImageDownloadedNotDisplayed_DetailComment": "%s de taille %s n'est pas affichée", @@ -41,7 +41,7 @@ "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.", "rule_JsValidate_DefaultComment": "Javascript validé", "rule_JsValidate_Comment": "%s erreur(s) javascript", - "rule_MaxCookiesLength": "Taille maximum des cookies par domaine(<512 Octets)", + "rule_MaxCookiesLength": "Taille maximum des cookies par domaine(< 512 Octets)", "rule_MaxCookiesLength_DetailDescription": "La longueur du cookie doit être réduite car il est envoyé à chaque requête.", "rule_MaxCookiesLength_DefaultComment": "Pas de cookies", "rule_MaxCookiesLength_Comment": "Taille maximum = %s Octets ", diff --git a/package-lock.json b/package-lock.json index cb86eec..50d7e3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,14 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/runtime": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", + "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -44,6 +52,11 @@ "defer-to-connect": "^2.0.0" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@types/cacheable-request": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", @@ -90,6 +103,52 @@ "@types/node": "*" } }, + "@ultraq/array-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ultraq/array-utils/-/array-utils-2.1.0.tgz", + "integrity": "sha512-TKO1zE6foqs5HG3+QH32yKwJ0zhZrm6J3UmltscveQmxCdbgIPXhNf3A8C9HakjyZDHVRK5pYZOU0tTl28YGFg==" + }, + "@ultraq/dom-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@ultraq/dom-utils/-/dom-utils-0.4.0.tgz", + "integrity": "sha512-SZmHIvp7C8UXAPtgO6e0lKfkVzEJfWGdmLoHGwhk5+v0rxVo4Z3TQT4svRpFq42Pj3sXGhXU7jyWCoeWrrkdLA==" + }, + "@ultraq/string-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ultraq/string-utils/-/string-utils-2.1.0.tgz", + "integrity": "sha512-936LzGXW2R5/rG0vVhstr7l5v05TEjjm9SyJdQsvCfKuPF0UzWVfp77ViZIm32fH/zzzmEFEWzcZEcy7DE5+Yw==" + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, + "acorn": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.2.4.tgz", + "integrity": "sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==" + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, "agent-base": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", @@ -180,6 +239,11 @@ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "axios": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", @@ -236,6 +300,11 @@ "concat-map": "0.0.1" } }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -344,6 +413,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "compress-commons": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz", @@ -391,6 +468,36 @@ "readable-stream": "^3.4.0" } }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, "dayjs": { "version": "1.8.31", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.31.tgz", @@ -404,6 +511,11 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==" + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -419,16 +531,46 @@ } } }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, "defer-to-connect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "devtools-protocol": { "version": "0.0.818844", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==" }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" + } + } + }, + "dumb-query-selector": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dumb-query-selector/-/dumb-query-selector-3.3.0.tgz", + "integrity": "sha512-hPRKbOOZUn9UBgO74fPSidcMqrYOJSMs27CEaDLZ+RjuY20ze0zgc4JH4ESXYyDXyCmGe3kirYHp6sFcH78Zsw==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -484,6 +626,33 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "exceljs": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.2.0.tgz", @@ -537,6 +706,11 @@ "@fast-csv/parse": "4.3.6" } }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -559,6 +733,16 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -639,11 +823,39 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + } + } + }, "http2-wrapper": { "version": "1.0.0-beta.5.2", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", @@ -662,6 +874,14 @@ "debug": "4" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -691,11 +911,74 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "jsdom": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.6.0.tgz", + "integrity": "sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg==", + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.5", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + } + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -786,6 +1069,15 @@ } } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -807,6 +1099,11 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -877,6 +1174,19 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, + "mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + }, + "mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "requires": { + "mime-db": "1.47.0" + } + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -928,6 +1238,11 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -936,6 +1251,19 @@ "wrappy": "1" } }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "p-cancelable": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", @@ -967,6 +1295,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -990,6 +1323,11 @@ "find-up": "^4.0.0" } }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, "printj": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", @@ -1079,6 +1417,11 @@ "minimatch": "^3.0.4" } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1110,6 +1453,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -1142,6 +1490,12 @@ "xml2js": "^0.4.23" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -1168,6 +1522,11 @@ "ansi-regex": "^5.0.0" } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -1196,6 +1555,19 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "thymeleaf": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/thymeleaf/-/thymeleaf-0.20.5.tgz", + "integrity": "sha512-3LGXOiKWoJthENI+5oSUy6Y4XhYrQEq8WgoIEiJQixatycTVWPZY1UAiq8gMazcknXGJkVRSMD9OJv42v7uktw==", + "requires": { + "@babel/runtime": "^7.10.4", + "@ultraq/array-utils": "^2.1.0", + "@ultraq/dom-utils": "^0.4.0", + "@ultraq/string-utils": "^2.1.0", + "dumb-query-selector": "^3.3.0", + "jsdom": "^16.3.0" + } + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -1214,11 +1586,27 @@ "universalify": "^0.1.2" } }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "requires": { + "punycode": "^2.1.1" + } + }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, "unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -1289,6 +1677,55 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz", + "integrity": "sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==", + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -1309,6 +1746,11 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, "xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", @@ -1377,4 +1819,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 4440884..b4b0810 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "puppeteer": "^5.5.0", "puppeteer-har": "^1.1.2", "sitemapper": "^3.1.12", + "thymeleaf": "^0.20.5", "yaml": "^1.10.0", "yargs": "^16.2.0" },