From d001ba530c15e93fd17a3654e95a81e4c890ef27 Mon Sep 17 00:00:00 2001 From: Jules Delecour* Date: Mon, 15 Mar 2021 08:55:32 +0100 Subject: [PATCH] Adding sources --- .dockerignore | 7 + .gitignore | 8 + Dockerfile | 20 + LICENSE | 661 ++++++++ builder.js | 24 + cli-core/analyis.js | 181 +++ cli-core/report.js | 282 ++++ commands/analyse.js | 65 + commands/sitemapParser.js | 19 + greenit | 78 + greenit-core/analyseFrameCore.js | 134 ++ greenit-core/ecoIndex.js | 67 + greenit-core/greenpanel.js | 236 +++ .../rules/AddExpiresOrCacheControlHeaders.js | 31 + greenit-core/rules/CompressHttp.js | 29 + greenit-core/rules/DomainsNumber.js | 25 + .../rules/DontResizeImageInBrowser.js | 39 + greenit-core/rules/EmptySrcTag.js | 13 + greenit-core/rules/ExternalizeCss.js | 13 + greenit-core/rules/ExternalizeJs.js | 14 + greenit-core/rules/HttpError.js | 20 + greenit-core/rules/HttpRequests.js | 15 + .../rules/ImageDownloadedNotDisplayed.js | 32 + greenit-core/rules/JsValidate.js | 21 + greenit-core/rules/MaxCookiesLength.js | 30 + greenit-core/rules/MinifiedCss.js | 21 + greenit-core/rules/MinifiedJs.js | 21 + .../rules/NoCookieForStaticRessources.js | 24 + greenit-core/rules/NoRedirect.js | 20 + greenit-core/rules/OptimizeBitmapImages.js | 39 + greenit-core/rules/OptimizeSvg.js | 24 + greenit-core/rules/Plugins.js | 13 + greenit-core/rules/PrintStyleSheet.js | 13 + greenit-core/rules/SocialNetworkButton.js | 25 + greenit-core/rules/StyleSheets.js | 23 + greenit-core/rules/UseETags.js | 32 + greenit-core/rules/UseStandardTypefaces.js | 27 + greenit-core/rulesManager.js | 81 + greenit-core/utils.js | 366 +++++ package-lock.json | 1380 +++++++++++++++++ package.json | 30 + sizes.js | 37 + 42 files changed, 4240 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 builder.js create mode 100644 cli-core/analyis.js create mode 100644 cli-core/report.js create mode 100644 commands/analyse.js create mode 100644 commands/sitemapParser.js create mode 100644 greenit create mode 100644 greenit-core/analyseFrameCore.js create mode 100644 greenit-core/ecoIndex.js create mode 100644 greenit-core/greenpanel.js create mode 100644 greenit-core/rules/AddExpiresOrCacheControlHeaders.js create mode 100644 greenit-core/rules/CompressHttp.js create mode 100644 greenit-core/rules/DomainsNumber.js create mode 100644 greenit-core/rules/DontResizeImageInBrowser.js create mode 100644 greenit-core/rules/EmptySrcTag.js create mode 100644 greenit-core/rules/ExternalizeCss.js create mode 100644 greenit-core/rules/ExternalizeJs.js create mode 100644 greenit-core/rules/HttpError.js create mode 100644 greenit-core/rules/HttpRequests.js create mode 100644 greenit-core/rules/ImageDownloadedNotDisplayed.js create mode 100644 greenit-core/rules/JsValidate.js create mode 100644 greenit-core/rules/MaxCookiesLength.js create mode 100644 greenit-core/rules/MinifiedCss.js create mode 100644 greenit-core/rules/MinifiedJs.js create mode 100644 greenit-core/rules/NoCookieForStaticRessources.js create mode 100644 greenit-core/rules/NoRedirect.js create mode 100644 greenit-core/rules/OptimizeBitmapImages.js create mode 100644 greenit-core/rules/OptimizeSvg.js create mode 100644 greenit-core/rules/Plugins.js create mode 100644 greenit-core/rules/PrintStyleSheet.js create mode 100644 greenit-core/rules/SocialNetworkButton.js create mode 100644 greenit-core/rules/StyleSheets.js create mode 100644 greenit-core/rules/UseETags.js create mode 100644 greenit-core/rules/UseStandardTypefaces.js create mode 100644 greenit-core/rulesManager.js create mode 100644 greenit-core/utils.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 sizes.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..956b0c8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +dist/ +script/ +node_modules/ +results/ +results.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7d7086 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.scannerwork +.vscode +node_modules/ +dist/ +results/ +results_test/ +*.xlsx +*.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b5f4e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node +RUN apt-get update \ + && apt-get install -y wget gnupg \ + && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY . . +RUN npm i \ + && npm link \ + && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ + && mkdir -p /home/pptruser/Downloads \ + && chown -R pptruser:pptruser /home/pptruser \ + && chown -R pptruser:pptruser /app/ +USER pptruser + +CMD ["greenit","analyse", "url.yaml", "results/results.xlsx"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ce8758 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/builder.js b/builder.js new file mode 100644 index 0000000..fe7b34d --- /dev/null +++ b/builder.js @@ -0,0 +1,24 @@ +const concat = require('concat-files'); +const glob = require('glob'); +const fs = require('fs') + +const DIR = './dist'; + +if (!fs.existsSync(DIR)){ + fs.mkdirSync(DIR); +} + +const rules = glob.sync('./greenit-core/rules/*.js') + +//One script to analyse them all +concat([ + './greenit-core/analyseFrameCore.js', + './greenit-core/utils.js', + './greenit-core/rulesManager.js', + './greenit-core/ecoIndex.js', + ...rules, + './greenit-core/greenpanel.js' +], './dist/bundle.js', function(err) { + if (err) throw err + console.log('build complete'); +}); \ No newline at end of file diff --git a/cli-core/analyis.js b/cli-core/analyis.js new file mode 100644 index 0000000..579e69c --- /dev/null +++ b/cli-core/analyis.js @@ -0,0 +1,181 @@ +const PuppeteerHar = require('puppeteer-har'); +const fs = require('fs') +const path = require('path'); +const ProgressBar = require('progress'); +const sizes = require('../sizes.js'); + + +//Path to the url file +const SUBRESULTS_DIRECTORY = path.join(__dirname,'../results'); + + +//Analyse a webpage +async function analyseURL(browser, url, options) { + let result = {}; + + const TIMEOUT = options.timeout + const TAB_ID = options.tabId + const TRY_NB = options.tryNb || 1 + const DEVICE = options.device || "desktop" + + try { + const page = await browser.newPage(); + await page.setViewport(sizes[DEVICE]); + //get har file + const pptrHar = new PuppeteerHar(page); + await pptrHar.start(); + //go to ulr + await page.goto(url, {timeout : TIMEOUT}); + let harObj = await pptrHar.stop(); + //get ressources + const client = await page.target().createCDPSession(); + let ressourceTree = await client.send('Page.getResourceTree'); + await client.detach() + + //get rid of chrome.i18n.getMessage not declared + await page.evaluate(x=>(chrome = { "i18n" : {"getMessage" : function () {return undefined}}})); + //add script, get run, then remove it to not interfere with the analysis + let script = await page.addScriptTag({ path: path.join(__dirname,'../dist/bundle.js')}); + await script.evaluate(x=>(x.remove())); + //pass node object to browser + await page.evaluate(x=>(har = x), harObj.log); + await page.evaluate(x=>(resources = x), ressourceTree.frameTree.resources); + + //launch analyse + result = await page.evaluate(()=>(launchAnalyse())); + page.close(); + result.success = true; + } catch (error) { + result.url = url; + result.success = false; + } + result.tryNb = TRY_NB; + result.tabId = TAB_ID; + return result; +} + +//handle login +async function login(browser,loginInformations) { + //use the tab that opens with the browser + const page = (await browser.pages())[0]; + //go to login page + await page.goto(loginInformations.url) + //ensure page is loaded + await page.waitForSelector(loginInformations.loginButtonSelector); + //complete fields + for (let index = 0; index < loginInformations.fields.length; index++) { + let field = loginInformations.fields[index] + await page.type(field.selector, field.value) + } + //click login button + await page.click(loginInformations.loginButtonSelector); + //make sure to not wait for the full authentification procedure + await page.waitForNavigation(); +} + +//Core +async function createJsonReports(browser, urlTable, options) { + //Timeout for an analysis + const TIMEOUT = options.timeout; + //Concurent tab + const MAX_TAB = options.max_tab; + //Nb of retry before dropping analysis + const RETRY = options.retry; + //Device to emulate + const DEVICE = options.device; + + //initialise progress bar + let progressBar; + if (!options.ci){ + progressBar = new ProgressBar(' Analysing [:bar] :percent Remaining: :etas Time: :elapseds', { + complete: '=', + incomplete: ' ', + width: 40, + total: urlTable.length+2 + }); + progressBar.tick(); + } else { + console.log("Analysing ..."); + } + + let asyncFunctions = []; + let results; + let resultId = 1; + let index = 0 + let reports = []; + let writeList = []; + + let convert = []; + + for (let i = 0; i < MAX_TAB; i++) { + convert[i] = i; + } + + //create directory for subresults + if (fs.existsSync(SUBRESULTS_DIRECTORY)){ + fs.rmdirSync(SUBRESULTS_DIRECTORY, { recursive: true }); + } + fs.mkdirSync(SUBRESULTS_DIRECTORY); + //Asynchronous analysis with MAX_TAB open simultaneously to json + for (let i = 0; i < MAX_TAB && index < urlTable.length; i++) { + asyncFunctions.push(analyseURL(browser,urlTable[index],{ + device: DEVICE, + timeout:TIMEOUT, + tabId: i + })); + index++; + //console.log(`Start of analysis #${index}/${urlTable.length}`) + } + + while (asyncFunctions.length != 0) { + results = await Promise.race(asyncFunctions); + if (!results.success && results.tryNb <= RETRY) { + asyncFunctions.splice(convert[results.tabId],1,analyseURL(browser,results.url,{ + device: DEVICE, + timeout:TIMEOUT, + tabId: results.tabId, + tryNb: results.tryNb + 1 + })); // convert is NEEDED, varialbe size array + }else{ + let filePath = path.resolve(SUBRESULTS_DIRECTORY,`${resultId}.json`) + writeList.push(fs.promises.writeFile(filePath, JSON.stringify(results))); + reports.push({name:`${resultId}`, path: filePath}); + //console.log(`End of an analysis (${resultId}/${urlTable.length}). Results will be saved in ${filePath}`); + if (progressBar){ + progressBar.tick() + } else { + console.log(`${resultId}/${urlTable.length}`); + } + resultId++; + if (index == (urlTable.length)){ + asyncFunctions.splice(convert[results.tabId],1); // convert is NEEDED, varialbe size array + for (let i = results.tabId+1; i < convert.length; i++) { + convert[i] = convert[i]-1; + } + } else { + asyncFunctions.splice(results.tabId,1,analyseURL(browser,urlTable[index],{ + device: DEVICE, + timeout:TIMEOUT, + tabId: results.tabId + })); // No need for convert, fixed size array + index++; + //console.log(`Start of analysis #${index}/${urlTable.length}`) + } + } + } + + //wait for all file to be written + await Promise.all(writeList); + //results to xlsx file + if (progressBar){ + progressBar.tick() + } else { + console.log("Analyse done"); + } + return reports +} + +module.exports = { + createJsonReports, + login +} \ No newline at end of file diff --git a/cli-core/report.js b/cli-core/report.js new file mode 100644 index 0000000..14b6e81 --- /dev/null +++ b/cli-core/report.js @@ -0,0 +1,282 @@ +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.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.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.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.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/commands/analyse.js b/commands/analyse.js new file mode 100644 index 0000000..04b569a --- /dev/null +++ b/commands/analyse.js @@ -0,0 +1,65 @@ +const fs = require('fs'); +const YAML = require('yaml'); +const path = require('path'); +const puppeteer = require('puppeteer'); +const createJsonReports = require('../cli-core/analyis.js').createJsonReports; +const login = require('../cli-core/analyis.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; +//launch core +async function analyse_core(options) { + const URL_YAML_FILE = path.resolve(options.yaml_input_file); + //Get list of url + let urlTable; + try { + urlTable = YAML.parse(fs.readFileSync(URL_YAML_FILE).toString()); + } catch (error) { + throw ` yaml_input_file : "${URL_YAML_FILE}" is not a valid YAML file.` + } + + //start browser + const browser = await puppeteer.launch({ + headless:true, + args :[ + "--no-sandbox", // can't run inside docker without + "--disable-setuid-sandbox" // but security issues + ], + // Keep gpu horsepower in headless + ignoreDefaultArgs:[ + '--disable-gpu' + ] + }); + //handle analyse + let reports; + try { + //handle login + if (options.login){ + const LOGIN_YAML_FILE = path.resolve(options.login); + let loginInfos; + try { + loginInfos = YAML.parse(fs.readFileSync(LOGIN_YAML_FILE).toString()); + } catch (error) { + throw ` --login : "${LOGIN_YAML_FILE}" is not a valid YAML file.` + } + console.log(loginInfos) + await login(browser, loginInfos) + } + //analyse + reports = await createJsonReports(browser, urlTable, options); + } finally { + //close browser + let pages = await browser.pages(); + await Promise.all(pages.map(page =>page.close())); + await browser.close() + } + //create report + let reportObj = await create_global_report(reports, options); + await create_XLSX_report(reportObj, options) +} + +//export method that handle error +function analyse(options) { + analyse_core(options).catch(e=>console.error("ERROR : \n" + e)) +} + +module.exports = analyse; \ No newline at end of file diff --git a/commands/sitemapParser.js b/commands/sitemapParser.js new file mode 100644 index 0000000..894c3e4 --- /dev/null +++ b/commands/sitemapParser.js @@ -0,0 +1,19 @@ +const Sitemapper = require('sitemapper'); +const fs = require('fs'); +const YAML = require('yaml'); +const path = require('path') +const sitemap = new Sitemapper(); + +module.exports = (options) => { + //handle inputs + const SITEMAP_URL = options.sitemap_url; + const OUTPUT_FILE = path.resolve(options.yaml_output_file); + //parse sitemap + sitemap.fetch(SITEMAP_URL).then(function(res) { + try { + fs.writeFileSync(OUTPUT_FILE,YAML.stringify(res.sites)) + } catch (error) { + throw ` yaml_output_file : Path "${OUTPUT_FILE}" cannot be reached.` + } + }).catch(e => console.log("ERROR : \n" + e)) +} \ No newline at end of file diff --git a/greenit b/greenit new file mode 100644 index 0000000..b41bb94 --- /dev/null +++ b/greenit @@ -0,0 +1,78 @@ +#!/usr/bin/env node +"use strict"; +const yargs = require('yargs/yargs') +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) => { + yargs + .positional('yaml_input_file', { + describe: 'YAML file path listing all URL', + default: "url.yaml" + }) + .positional('xlsx_output_file', { + describe: 'Output file path', + default: "results.xlsx" + }) + .option('timeout', { + alias: 't', + type: 'number', + description: 'Timout for an analysis of a URL in ms', + default: 180000 + }) + .option('retry', { + alias: 'r', + type: 'number', + description: 'Number of retry when an analysis of a URL fail', + default: 2 + }) + .option('max_tab', { + type: 'number', + description: 'Number of concurrent analysis', + default: 40 + }) + .option('worst_pages', { + type: 'number', + description: 'Number of displayed worst pages', + default: 5 + }) + .option('worst_rules', { + type: 'number', + description: 'Number of displayed worst rules', + default: 5 + }) + .option('login', { + type: 'string', + alias:'l', + description: 'Path to YAML file with login informations' + }) + .option('device', { + alias:'d', + description: 'Hardware to simulate', + choices:Object.keys(sizes), + default: "desktop" + }) + }, (argv) => { + require("./commands/analyse.js")(argv) + }) + .command('parseSitemap [yaml_output_file]', 'Parse sitemap to a YAML file', (yargs) => { + yargs + .positional('sitemap_url', { + describe: 'URL to the sitemap.xml file', + }) + .positional('yaml_output_file', { + describe: 'Output file path', + default: "url.yaml" + }) + }, (argv) => { + require("./commands/sitemapParser.js")(argv) + }) + .option('ci', { + type: 'boolean', + description: 'Disable progress bar to work with GitLab CI' + }) + .strict() + .demandCommand() + .help() + .argv \ No newline at end of file diff --git a/greenit-core/analyseFrameCore.js b/greenit-core/analyseFrameCore.js new file mode 100644 index 0000000..061b5e1 --- /dev/null +++ b/greenit-core/analyseFrameCore.js @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 The EcoMeter authors (https://gitlab.com/ecoconceptionweb/ecometer) + * Copyright (C) 2019 didierfred@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +function start_analyse_core() { + const analyseStartingTime = Date.now(); + const dom_size = document.getElementsByTagName("*").length; + let pageAnalysis; + + if (analyseBestPractices) { + // test with http://www.wickham43.net/flashvideo.php + const pluginsNumber = getPluginsNumber(); + const printStyleSheetsNumber = getPrintStyleSheetsNumber(); + const inlineStyleSheetsNumber = getInlineStyleSheetsNumber(); + const emptySrcTagNumber = getEmptySrcTagNumber(); + const inlineJsScript = getInlineJsScript(); + const inlineJsScriptsNumber = getInlineJsScriptsNumber(); + const imagesResizedInBrowser = getImagesResizedInBrowser(); + + + pageAnalysis = { + "analyseStartingTime": analyseStartingTime, + "url": document.URL, + "domSize": dom_size, + "pluginsNumber": pluginsNumber, + "printStyleSheetsNumber": printStyleSheetsNumber, + "inlineStyleSheetsNumber": inlineStyleSheetsNumber, + "emptySrcTagNumber": emptySrcTagNumber, + "inlineJsScript": inlineJsScript, + "inlineJsScriptsNumber": inlineJsScriptsNumber, + "imagesResizedInBrowser": imagesResizedInBrowser, + } + } + else pageAnalysis = { + "analyseStartingTime": analyseStartingTime, + "url": document.URL, + "domSize": dom_size + } + + return pageAnalysis; + +} + +function getPluginsNumber() { + const plugins = document.querySelectorAll('object,embed'); + return (plugins === undefined) ? 0 : plugins.length; +} + + + +function getEmptySrcTagNumber() { + return document.querySelectorAll('img[src=""]').length + + document.querySelectorAll('script[src=""]').length + + document.querySelectorAll('link[rel=stylesheet][href=""]').length; +} + + +function getPrintStyleSheetsNumber() { + return document.querySelectorAll('link[rel=stylesheet][media~=print]').length + + document.querySelectorAll('style[media~=print]').length; +} + +function getInlineStyleSheetsNumber() { + let styleSheetsArray = Array.from(document.styleSheets); + let inlineStyleSheetsNumber = 0; + styleSheetsArray.forEach(styleSheet => { + try { + if (!styleSheet.href) inlineStyleSheetsNumber++; + } + catch (err) { + console.log("GREENIT-ANALYSIS ERROR ," + err.name + " = " + err.message); + console.log("GREENIT-ANALYSIS ERROR " + err.stack); + } + }); +return inlineStyleSheetsNumber; +} + + +function getInlineJsScript() { + let scriptArray = Array.from(document.scripts); + let scriptText = ""; + scriptArray.forEach(script => { + let isJSON = (String(script.type) === "application/ld+json"); // Exclude type="application/ld+json" from parsing js analyse + if ((script.text.length > 0) && (!isJSON)) scriptText += "\n" + script.text; + }); + return scriptText; +} + +function getInlineJsScriptsNumber() { + let scriptArray = Array.from(document.scripts); + let inlineScriptNumber = 0; + scriptArray.forEach(script => { + let isJSON = (String(script.type) === "application/ld+json"); // Exclude type="application/ld+json" from count + if ((script.text.length > 0) && (!isJSON)) inlineScriptNumber++; + }); + return inlineScriptNumber; +} + + +function getImagesResizedInBrowser() { + const imgArray = Array.from(document.querySelectorAll('img')); + let imagesResized = []; + imgArray.forEach(img => { + if (img.clientWidth < img.naturalWidth || img.clientHeight < img.naturalHeight) { + // Images of one pixel are some times used ... , we exclude them + if (img.naturalWidth > 1) + { + const imageMeasures = { + "src":img.src, + "clientWidth":img.clientWidth, + "clientHeight":img.clientHeight, + "naturalWidth":img.naturalWidth, + "naturalHeight":img.naturalHeight + } + imagesResized.push(imageMeasures); + } + } + }); + return imagesResized; +} diff --git a/greenit-core/ecoIndex.js b/greenit-core/ecoIndex.js new file mode 100644 index 0000000..5ff3ab7 --- /dev/null +++ b/greenit-core/ecoIndex.js @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 didierfred@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +let quantiles_dom = [0, 47, 75, 159, 233, 298, 358, 417, 476, 537, 603, 674, 753, 843, 949, 1076, 1237, 1459, 1801, 2479, 594601]; +let quantiles_req = [0, 2, 15, 25, 34, 42, 49, 56, 63, 70, 78, 86, 95, 105, 117, 130, 147, 170, 205, 281, 3920]; +let quantiles_size = [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, 2465.37, 2866.31, 3401.59, 4155.73, 5400.08, 8037.54, 223212.26]; + + +/** +Calcul ecoIndex based on formula from web site www.ecoindex.fr +**/ +function computeEcoIndex(dom,req,size) +{ + +const q_dom= computeQuantile(quantiles_dom,dom); +const q_req= computeQuantile(quantiles_req,req); +const q_size= computeQuantile(quantiles_size,size); + + +return Math.round(100 - 5 * (3*q_dom + 2*q_req + q_size)/6); +} + +function computeQuantile(quantiles,value) +{ +for (let i=1;i 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"; +} + +function computeGreenhouseGasesEmissionfromEcoIndex(ecoIndex) +{ + return (Math.round(100 * (2 + 2 * (50 - ecoIndex) / 100)) / 100); +} + +function computeWaterConsumptionfromEcoIndex(ecoIndex) +{ + return (Math.round(100 * (3 + 3 * (50 - ecoIndex) / 100)) / 100); +} + diff --git a/greenit-core/greenpanel.js b/greenit-core/greenpanel.js new file mode 100644 index 0000000..28b088a --- /dev/null +++ b/greenit-core/greenpanel.js @@ -0,0 +1,236 @@ + +/* + * Copyright (C) 2019 didierfred@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +let backgroundPageConnection; +let currentRulesChecker; +let lastAnalyseStartingTime = 0; +let measuresAcquisition; +let analyseBestPractices = true; +let har; +let resources; + +function handleResponseFromBackground(frameMeasures) { + if (isOldAnalyse(frameMeasures.analyseStartingTime)) { + debug(() => `Analyse is too old for url ${frameMeasures.url} , time = ${frameMeasures.analyseStartingTime}`); + return; + } + measuresAcquisition.aggregateFrameMeasures(frameMeasures); +} + + + +function computeEcoIndexMeasures(measures) { + measures.ecoIndex = computeEcoIndex(measures.domSize, measures.nbRequest, Math.round(measures.responsesSize / 1000)); + measures.waterConsumption = computeWaterConsumptionfromEcoIndex(measures.ecoIndex); + measures.greenhouseGasesEmission = computeGreenhouseGasesEmissionfromEcoIndex(measures.ecoIndex); + measures.grade = getEcoIndexGrade(measures.ecoIndex); +} + + +function launchAnalyse() { + let now = Date.now(); + + // To avoid parallel analyse , force 1 secondes between analysis + if (now - lastAnalyseStartingTime < 1000) { + debug(() => "Ignore click"); + return; + } + lastAnalyseStartingTime = now; + currentRulesChecker = rulesManager.getNewRulesChecker(); + measuresAcquisition = new MeasuresAcquisition(currentRulesChecker); + measuresAcquisition.initializeMeasures(); + measuresAcquisition.aggregateFrameMeasures(start_analyse_core()) + measuresAcquisition.startMeasuring(); + let returnObj = measuresAcquisition.getMeasures(); + returnObj.bestPractices = measuresAcquisition.getBestPractices() + return returnObj; +} + + +function MeasuresAcquisition(rules) { + + let measures; + let localRulesChecker = rules; + let nbGetHarTry = 0; + + this.initializeMeasures = () => { + measures = { + "url": "", + "domSize": 0, + "nbRequest": 0, + "responsesSize": 0, + "responsesSizeUncompress": 0, + "ecoIndex": 100, + "grade": 'A', + "waterConsumption": 0, + "greenhouseGasesEmission": 0, + "pluginsNumber": 0, + "printStyleSheetsNumber": 0, + "inlineStyleSheetsNumber": 0, + "emptySrcTagNumber": 0, + "inlineJsScriptsNumber": 0, + "imagesResizedInBrowser": [] + }; + } + + this.startMeasuring = function () { + getNetworkMeasure(); + if (analyseBestPractices) getResourcesMeasure(); + } + + this.getMeasures = () => measures; + + this.getBestPractices = () => Object.fromEntries(localRulesChecker.getAllRules()) + + this.aggregateFrameMeasures = function (frameMeasures) { + measures.domSize += frameMeasures.domSize; + computeEcoIndexMeasures(measures); + + if (analyseBestPractices) { + measures.pluginsNumber += frameMeasures.pluginsNumber; + + measures.printStyleSheetsNumber += frameMeasures.printStyleSheetsNumber; + if (measures.inlineStyleSheetsNumber < frameMeasures.inlineStyleSheetsNumber) measures.inlineStyleSheetsNumber = frameMeasures.inlineStyleSheetsNumber; + measures.emptySrcTagNumber += frameMeasures.emptySrcTagNumber; + if (frameMeasures.inlineJsScript.length > 0) { + const resourceContent = { + url:"inline js", + type:"script", + content:frameMeasures.inlineJsScript + } + localRulesChecker.sendEvent('resourceContentReceived',measures,resourceContent); + } + if (measures.inlineJsScriptsNumber < frameMeasures.inlineJsScriptsNumber) measures.inlineJsScriptsNumber = frameMeasures.inlineJsScriptsNumber; + + measures.imagesResizedInBrowser = frameMeasures.imagesResizedInBrowser; + + localRulesChecker.sendEvent('frameMeasuresReceived',measures); + + } + } + + + + const getNetworkMeasure = () => { + + console.log("Start network measure..."); + // only account for network traffic, filtering resources embedded through data urls + let entries = har.entries.filter(entry => isNetworkResource(entry)); + + // Get the "mother" url + if (entries.length > 0) measures.url = entries[0].request.url; + else { + // Bug with firefox when we first get har.entries when starting the plugin , we need to ask again to have it + if (nbGetHarTry < 1) { + debug(() => 'No entries, try again to get HAR in 1s'); + nbGetHarTry++; + setTimeout(getNetworkMeasure, 1000); + } + } + + measures.entries = entries; + measures.dataEntries = har.entries.filter(entry => isDataResource(entry)); // embeded data urls + + if (entries.length) { + measures.nbRequest = entries.length; + entries.forEach(entry => { + + + // If chromium : + // _transferSize represent the real data volume transfert + // while content.size represent the size of the page which is uncompress + if (entry.response._transferSize) { + measures.responsesSize += entry.response._transferSize; + measures.responsesSizeUncompress += entry.response.content.size; + } + else { + // In firefox , entry.response.content.size can sometimes be undefined + if (entry.response.content.size) measures.responsesSize += entry.response.content.size; + //debug(() => `entry size = ${entry.response.content.size} , responseSize = ${measures.responsesSize}`); + } + }); + if (analyseBestPractices) localRulesChecker.sendEvent('harReceived',measures); + + computeEcoIndexMeasures(measures); + } + } + + function getResourcesMeasure() { + resources.forEach(resource => { + if (resource.url.startsWith("file") || resource.url.startsWith("http")) { + if ((resource.type === 'script') || (resource.type === 'stylesheet') || (resource.type === 'image')) { + let resourceAnalyser = new ResourceAnalyser(resource); + resourceAnalyser.analyse(); + } + } + }); + } + + function ResourceAnalyser(resource) { + let resourceToAnalyse = resource; + + this.analyse = () => resourceToAnalyse.getContent(this.analyseContent); + + this.analyseContent = (code) => { + // exclude from analyse the injected script + if ((resourceToAnalyse.type === 'script') && (resourceToAnalyse.url.includes("script/analyseFrame.js"))) return; + + let resourceContent = { + url: resourceToAnalyse.url, + type : resourceToAnalyse.type, + content: code + }; + localRulesChecker.sendEvent('resourceContentReceived',measures,resourceContent); + + } + } + +} + +/** +Add to the history the result of an analyse +**/ +function storeAnalysisInHistory() { + let measures = measuresAcquisition.getMeasures(); + if (!measures) return; + + var analyse_history = []; + var string_analyse_history = localStorage.getItem("analyse_history"); + var analyse_to_store = { + resultDate: new Date(), + url: measures.url, + nbRequest: measures.nbRequest, + responsesSize: Math.round(measures.responsesSize / 1000), + domSize: measures.domSize, + greenhouseGasesEmission: measures.greenhouseGasesEmission, + waterConsumption: measures.waterConsumption, + ecoIndex: measures.ecoIndex, + grade: measures.grade + }; + + if (string_analyse_history) { + analyse_history = JSON.parse(string_analyse_history); + analyse_history.reverse(); + analyse_history.push(analyse_to_store); + analyse_history.reverse(); + } + else analyse_history.push(analyse_to_store); + + + localStorage.setItem("analyse_history", JSON.stringify(analyse_history)); +} diff --git a/greenit-core/rules/AddExpiresOrCacheControlHeaders.js b/greenit-core/rules/AddExpiresOrCacheControlHeaders.js new file mode 100644 index 0000000..4ecef10 --- /dev/null +++ b/greenit-core/rules/AddExpiresOrCacheControlHeaders.js @@ -0,0 +1,31 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "AddExpiresOrCacheControlHeaders", + comment: "", + detailComment: "", + + check: function (measures) { + let staticResourcesSize = 0; + let staticResourcesWithCache = 0; + + if (measures.entries.length) measures.entries.forEach(entry => { + if (isStaticRessource(entry)) { + staticResourcesSize += entry.response.content.size; + if (hasValidCacheHeaders(entry)) { + staticResourcesWithCache += entry.response.content.size; + } + else this.detailComment += chrome.i18n.getMessage("rule_AddExpiresOrCacheControlHeaders_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; + } + }); + + if (staticResourcesSize > 0) { + const cacheHeaderRatio = staticResourcesWithCache / staticResourcesSize * 100; + if (cacheHeaderRatio < 95) { + if (cacheHeaderRatio < 90) this.complianceLevel = 'C' + else this.complianceLevel = 'B'; + } + else this.complianceLevel = 'A'; + this.comment = chrome.i18n.getMessage("rule_AddExpiresOrCacheControlHeaders_Comment", String(Math.round(cacheHeaderRatio * 10) / 10) + "%"); + } + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/CompressHttp.js b/greenit-core/rules/CompressHttp.js new file mode 100644 index 0000000..57964da --- /dev/null +++ b/greenit-core/rules/CompressHttp.js @@ -0,0 +1,29 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "CompressHttp", + comment: "", + detailComment: "", + + check: function (measures) { + let compressibleResourcesSize = 0; + let compressibleResourcesCompressedSize = 0; + if (measures.entries.length) measures.entries.forEach(entry => { + if (isCompressibleResource(entry)) { + compressibleResourcesSize += entry.response.content.size; + if (isResourceCompressed(entry)) { + compressibleResourcesCompressedSize += entry.response.content.size; + } + else this.detailComment += chrome.i18n.getMessage("rule_CompressHttp_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; + } + }); + if (compressibleResourcesSize > 0) { + const compressRatio = compressibleResourcesCompressedSize / compressibleResourcesSize * 100; + if (compressRatio < 95) { + if (compressRatio < 90) this.complianceLevel = 'C' + else this.complianceLevel = 'B'; + } + else this.complianceLevel = 'A'; + this.comment = chrome.i18n.getMessage("rule_CompressHttp_Comment", String(Math.round(compressRatio * 10) / 10) + "%"); + } + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/DomainsNumber.js b/greenit-core/rules/DomainsNumber.js new file mode 100644 index 0000000..494ba6b --- /dev/null +++ b/greenit-core/rules/DomainsNumber.js @@ -0,0 +1,25 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "DomainsNumber", + comment: "", + detailComment: "", + + check: function (measures) { + let domains = []; + if (measures.entries.length) measures.entries.forEach(entry => { + let domain = getDomainFromUrl(entry.request.url); + if (domains.indexOf(domain) === -1) { + domains.push(domain); + } + }); + if (domains.length > 2) { + if (domains.length === 3) this.complianceLevel = 'B'; + else this.complianceLevel = 'C'; + } + domains.forEach(domain => { + this.detailComment += domain + "
"; + }); + + this.comment = chrome.i18n.getMessage("rule_DomainsNumber_Comment", String(domains.length)); + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/DontResizeImageInBrowser.js b/greenit-core/rules/DontResizeImageInBrowser.js new file mode 100644 index 0000000..239c3e3 --- /dev/null +++ b/greenit-core/rules/DontResizeImageInBrowser.js @@ -0,0 +1,39 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "DontResizeImageInBrowser", + comment: "", + detailComment: "", + imagesResizedInBrowserNumber: 0, + imgAnalysed: new Map(), + + // need to get a new map , otherwise it's share between instance + initialize: function () { + this.imgAnalysed = new Map(); + }, + + isRevelant: function (entry) { + // exclude svg + if (isSvgUrl(entry.src)) return false; + + // difference of 1 pixel is not relevant + if (entry.naturalWidth - entry.clientWidth < 2) return false; + if (entry.naturalHeight - entry.clientHeight < 2) return false; + + // If picture is 0x0 it meens it's not visible on the ui , see imageDownloadedNotDisplayed + if (entry.clientWidth === 0) return false; + + return true; + }, + + check: function (measures) { + measures.imagesResizedInBrowser.forEach(entry => { + if (!this.imgAnalysed.has(entry.src) && this.isRevelant(entry)) { // Do not count two times the same picture + this.detailComment += chrome.i18n.getMessage("rule_DontResizeImageInBrowser_DetailComment",[entry.src,`${entry.naturalWidth}x${entry.naturalHeight}`,`${entry.clientWidth}x${entry.clientHeight}`]) + '
'; + this.imgAnalysed.set(entry.src); + this.imagesResizedInBrowserNumber += 1; + } + }); + if (this.imagesResizedInBrowserNumber > 0) this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_DontResizeImageInBrowser_Comment", String(this.imagesResizedInBrowserNumber)); + } +}, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/EmptySrcTag.js b/greenit-core/rules/EmptySrcTag.js new file mode 100644 index 0000000..544caf3 --- /dev/null +++ b/greenit-core/rules/EmptySrcTag.js @@ -0,0 +1,13 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "EmptySrcTag", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.emptySrcTagNumber > 0) { + this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_EmptySrcTag_Comment", String(measures.emptySrcTagNumber)); + } + } +}, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/ExternalizeCss.js b/greenit-core/rules/ExternalizeCss.js new file mode 100644 index 0000000..87bf726 --- /dev/null +++ b/greenit-core/rules/ExternalizeCss.js @@ -0,0 +1,13 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "ExternalizeCss", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.inlineStyleSheetsNumber > 0) { + this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_ExternalizeCss_Comment", String(measures.inlineStyleSheetsNumber)); + } + } +}, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/ExternalizeJs.js b/greenit-core/rules/ExternalizeJs.js new file mode 100644 index 0000000..65b5115 --- /dev/null +++ b/greenit-core/rules/ExternalizeJs.js @@ -0,0 +1,14 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "ExternalizeJs", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.inlineJsScriptsNumber > 0) { + if (measures.inlineJsScriptsNumber > 1) this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_ExternalizeJs_Comment", String(measures.inlineJsScriptsNumber)); + + } + } +}, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/HttpError.js b/greenit-core/rules/HttpError.js new file mode 100644 index 0000000..017eb30 --- /dev/null +++ b/greenit-core/rules/HttpError.js @@ -0,0 +1,20 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "HttpError", + comment: "", + detailComment: "", + + check: function (measures) { + let errorNumber = 0; + if (measures.entries.length) measures.entries.forEach(entry => { + if (entry.response) { + if (entry.response.status >=400 ) { + this.detailComment += entry.response.status + " " + entry.request.url + "
"; + errorNumber++; + } + } + }); + if (errorNumber > 0) this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_HttpError_Comment", String(errorNumber)); + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/HttpRequests.js b/greenit-core/rules/HttpRequests.js new file mode 100644 index 0000000..27d390b --- /dev/null +++ b/greenit-core/rules/HttpRequests.js @@ -0,0 +1,15 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "HttpRequests", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.entries.length) measures.entries.forEach(entry => { + this.detailComment += entry.request.url + "
"; + }); + if (measures.nbRequest > 40) this.complianceLevel = 'C'; + else if (measures.nbRequest > 26) this.complianceLevel = 'B'; + this.comment = chrome.i18n.getMessage("rule_HttpRequests_Comment", String(measures.nbRequest)); + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/ImageDownloadedNotDisplayed.js b/greenit-core/rules/ImageDownloadedNotDisplayed.js new file mode 100644 index 0000000..e5d1529 --- /dev/null +++ b/greenit-core/rules/ImageDownloadedNotDisplayed.js @@ -0,0 +1,32 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "ImageDownloadedNotDisplayed", + comment: "", + detailComment: "", + imageDownloadedNotDisplayedNumber: 0, + imgAnalysed: new Map(), + + // need to get a new map , otherwise it's share between instance + initialize: function () { + this.imgAnalysed = new Map(); + }, + + isRevelant: function (entry) { + // Very small images could be download even if not display as it may be icons + if (entry.naturalWidth * entry.naturalHeight < 10000) return false; + if (entry.clientWidth === 0 && entry.clientHeight === 0) return true; + return false; + }, + + check: function (measures) { + measures.imagesResizedInBrowser.forEach(entry => { + if (!this.imgAnalysed.has(entry.src) && this.isRevelant(entry)) { // Do not count two times the same picture + this.detailComment += chrome.i18n.getMessage("rule_ImageDownloadedNotDisplayed_DetailComment",[entry.src,`${entry.naturalWidth}x${entry.naturalHeight}`]) + '
'; + this.imgAnalysed.set(entry.src); + this.imageDownloadedNotDisplayedNumber += 1; + } + }); + if (this.imageDownloadedNotDisplayedNumber > 0) this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_ImageDownloadedNotDisplayed_Comment", String(this.imageDownloadedNotDisplayedNumber)); + } +}, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/JsValidate.js b/greenit-core/rules/JsValidate.js new file mode 100644 index 0000000..6f869e9 --- /dev/null +++ b/greenit-core/rules/JsValidate.js @@ -0,0 +1,21 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "JsValidate", + comment: "", + detailComment: "", + errors: 0, + totalJsSize: 0, + + check: function (measures, resourceContent) { + if (resourceContent.type === "script") { + this.totalJsSize += resourceContent.content.length; + let errorNumber = computeNumberOfErrorsInJSCode(resourceContent.content, resourceContent.url); + if (errorNumber > 0) { + this.detailComment += (`URL ${resourceContent.url} has ${errorNumber} error(s)
`); + this.errors += errorNumber; + this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_JsValidate_Comment", String(this.errors)); + } + } + } +}, "resourceContentReceived"); \ No newline at end of file diff --git a/greenit-core/rules/MaxCookiesLength.js b/greenit-core/rules/MaxCookiesLength.js new file mode 100644 index 0000000..91215ff --- /dev/null +++ b/greenit-core/rules/MaxCookiesLength.js @@ -0,0 +1,30 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "MaxCookiesLength", + comment: "", + detailComment: "", + + check: function (measures) { + let maxCookiesLength = 0; + let domains = new Map(); + if (measures.entries.length) measures.entries.forEach(entry => { + const cookiesLength = getCookiesLength(entry); + if (cookiesLength !== 0) { + let domain = getDomainFromUrl(entry.request.url); + if (domains.has(domain)) { + if (domains.get(domain) < cookiesLength) domains.set(domain, cookiesLength); + } + else domains.set(domain, cookiesLength); + if (cookiesLength > maxCookiesLength) maxCookiesLength = cookiesLength; + } + }); + domains.forEach((value, key) => { + this.detailComment += chrome.i18n.getMessage("rule_MaxCookiesLength_DetailComment",[value,key]) + '
' ; + }); + if (maxCookiesLength !== 0) { + this.comment = chrome.i18n.getMessage("rule_MaxCookiesLength_Comment", String(maxCookiesLength)); + if (maxCookiesLength > 512) this.complianceLevel = 'B'; + if (maxCookiesLength > 1024) this.complianceLevel = 'C'; + } + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/MinifiedCss.js b/greenit-core/rules/MinifiedCss.js new file mode 100644 index 0000000..5c5a189 --- /dev/null +++ b/greenit-core/rules/MinifiedCss.js @@ -0,0 +1,21 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "MinifiedCss", + comment: "", + detailComment: "", + totalCssSize: 0, + minifiedCssSize: 0, + + check: function (measures, resourceContent) { + if (resourceContent.type === "stylesheet") { + this.totalCssSize += resourceContent.content.length; + if (!isMinified(resourceContent.content)) this.detailComment += chrome.i18n.getMessage("rule_MinifiedCss_DetailComment",resourceContent.url) + '
'; + else this.minifiedCssSize += resourceContent.content.length; + const percentMinifiedCss = this.minifiedCssSize / this.totalCssSize * 100; + this.complianceLevel = 'A'; + if (percentMinifiedCss < 90) this.complianceLevel = 'C'; + else if (percentMinifiedCss < 95) this.complianceLevel = 'B'; + this.comment = chrome.i18n.getMessage("rule_MinifiedCss_Comment", String(Math.round(percentMinifiedCss * 10) / 10)); + } + } +}, "resourceContentReceived"); diff --git a/greenit-core/rules/MinifiedJs.js b/greenit-core/rules/MinifiedJs.js new file mode 100644 index 0000000..47708d4 --- /dev/null +++ b/greenit-core/rules/MinifiedJs.js @@ -0,0 +1,21 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "MinifiedJs", + comment: "", + detailComment: "", + totalJsSize: 0, + minifiedJsSize: 0, + + check: function (measures, resourceContent) { + if (resourceContent.type === "script") { + this.totalJsSize += resourceContent.content.length; + if (!isMinified(resourceContent.content)) this.detailComment += chrome.i18n.getMessage("rule_MinifiedJs_DetailComment",resourceContent.url) + '
'; + else this.minifiedJsSize += resourceContent.content.length; + const percentMinifiedJs = this.minifiedJsSize / this.totalJsSize * 100; + this.complianceLevel = 'A'; + if (percentMinifiedJs < 90) this.complianceLevel = 'C'; + else if (percentMinifiedJs < 95) this.complianceLevel = 'B'; + this.comment = chrome.i18n.getMessage("rule_MinifiedJs_Comment", String(Math.round(percentMinifiedJs * 10) / 10)); + } + } +}, "resourceContentReceived"); diff --git a/greenit-core/rules/NoCookieForStaticRessources.js b/greenit-core/rules/NoCookieForStaticRessources.js new file mode 100644 index 0000000..e7abe45 --- /dev/null +++ b/greenit-core/rules/NoCookieForStaticRessources.js @@ -0,0 +1,24 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "NoCookieForStaticRessources", + comment: "", + detailComment: "", + + check: function (measures) { + let nbRessourcesStaticWithCookie = 0; + let totalCookiesSize = 0; + if (measures.entries.length) measures.entries.forEach(entry => { + const cookiesLength = getCookiesLength(entry); + if (isStaticRessource(entry) && (cookiesLength > 0)) { + nbRessourcesStaticWithCookie++; + totalCookiesSize += cookiesLength + 7; // 7 is size for the header name "cookie:" + this.detailComment += chrome.i18n.getMessage("rule_NoCookieForStaticRessources_DetailComment",entry.request.url) + "
"; + } + }); + if (nbRessourcesStaticWithCookie > 0) { + if (totalCookiesSize > 2000) this.complianceLevel = 'C'; + else this.complianceLevel = 'B'; + this.comment = chrome.i18n.getMessage("rule_NoCookieForStaticRessources_Comment", [String(nbRessourcesStaticWithCookie), String(Math.round(totalCookiesSize / 100) / 10)]); + } + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/NoRedirect.js b/greenit-core/rules/NoRedirect.js new file mode 100644 index 0000000..66c4c96 --- /dev/null +++ b/greenit-core/rules/NoRedirect.js @@ -0,0 +1,20 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "NoRedirect", + comment: "", + detailComment: "", + + check: function (measures) { + let redirectNumber = 0; + if (measures.entries.length) measures.entries.forEach(entry => { + if (entry.response) { + if (isHttpRedirectCode(entry.response.status)) { + this.detailComment += entry.response.status + " " + entry.request.url + "
"; + redirectNumber++; + } + } + }); + if (redirectNumber > 0) this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_NoRedirect_Comment", String(redirectNumber)); + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/OptimizeBitmapImages.js b/greenit-core/rules/OptimizeBitmapImages.js new file mode 100644 index 0000000..87b7c40 --- /dev/null +++ b/greenit-core/rules/OptimizeBitmapImages.js @@ -0,0 +1,39 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "OptimizeBitmapImages", + comment: "", + detailComment: "", + + check: function (measures) { + let nbImagesToOptimize = 0; + let totalMinGains = 0; + if (measures.entries) measures.entries.forEach(entry => { + if (entry.response) { + const imageType = getImageTypeFromResource(entry); + if (imageType !== "") { + var myImage = new Image(); + myImage.src = entry.request.url; + // needed to access object in the function after + myImage.rule = this; + + myImage.size = entry.response.content.size; + myImage.onload = function () { + + const minGains = getMinOptimisationGainsForImage(this.width * this.height, this.size, imageType); + if (minGains > 500) { // exclude small gain + nbImagesToOptimize++; + totalMinGains += minGains; + 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))]) + "
"; + } + if (nbImagesToOptimize > 0) { + if (totalMinGains < 50000) this.rule.complianceLevel = 'B'; + else this.rule.complianceLevel = 'C'; + this.rule.comment = chrome.i18n.getMessage("rule_OptimizeBitmapImages_Comment", [String(nbImagesToOptimize), String(Math.round(totalMinGains / 1000))]); + showEcoRuleOnUI(this.rule); + } + } + } + } + }); + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/OptimizeSvg.js b/greenit-core/rules/OptimizeSvg.js new file mode 100644 index 0000000..3b94feb --- /dev/null +++ b/greenit-core/rules/OptimizeSvg.js @@ -0,0 +1,24 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "OptimizeSvg", + comment: "", + detailComment: "", + totalSizeToOptimize: 0, + totalResourcesToOptimize: 0, + + check: function (measures, resourceContent) { + if ((resourceContent.type === 'image') && isSvgUrl(resourceContent.url)) { + if (!isSvgOptimized(window.atob(resourceContent.content))) // code is in base64 , decode base64 data with atob + { + this.detailComment += chrome.i18n.getMessage("rule_OptimizeSvg_detailComment",[resourceContent.url,String(Math.round(resourceContent.content.length / 100) / 10)]) + '
'; + this.totalSizeToOptimize += resourceContent.content.length; + this.totalResourcesToOptimize++; + } + if (this.totalSizeToOptimize > 0) { + if (this.totalSizeToOptimize < 20000) this.complianceLevel = 'B'; + else this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_OptimizeSvg_Comment", String(this.totalResourcesToOptimize)); + } + } + } + }, "resourceContentReceived"); \ No newline at end of file diff --git a/greenit-core/rules/Plugins.js b/greenit-core/rules/Plugins.js new file mode 100644 index 0000000..d2442dc --- /dev/null +++ b/greenit-core/rules/Plugins.js @@ -0,0 +1,13 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "Plugins", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.pluginsNumber > 0) { + this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_Plugins_Comment", String(measures.pluginsNumber)); + } + } + }, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/PrintStyleSheet.js b/greenit-core/rules/PrintStyleSheet.js new file mode 100644 index 0000000..590c45a --- /dev/null +++ b/greenit-core/rules/PrintStyleSheet.js @@ -0,0 +1,13 @@ +rulesManager.registerRule({ + complianceLevel: 'C', + id: "PrintStyleSheet", + comment: "", + detailComment: "", + + check: function (measures) { + if (measures.printStyleSheetsNumber > 0) { + this.complianceLevel = 'A'; + this.comment = chrome.i18n.getMessage("rule_PrintStyleSheet_Comment", String(measures.printStyleSheetsNumber)); + } + } + }, "frameMeasuresReceived"); \ No newline at end of file diff --git a/greenit-core/rules/SocialNetworkButton.js b/greenit-core/rules/SocialNetworkButton.js new file mode 100644 index 0000000..7fa8b86 --- /dev/null +++ b/greenit-core/rules/SocialNetworkButton.js @@ -0,0 +1,25 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "SocialNetworkButton", + comment: "", + detailComment: "", + + check: function (measures) { + let nbSocialNetworkButton = 0; + let socialNetworks = []; + if (measures.entries.length) measures.entries.forEach(entry => { + const officalSocialButton = getOfficialSocialButtonFormUrl(entry.request.url); + if (officalSocialButton.length > 0) { + if (socialNetworks.indexOf(officalSocialButton) === -1) { + socialNetworks.push(officalSocialButton); + this.detailComment += chrome.i18n.getMessage("rule_SocialNetworkButton_detailComment", officalSocialButton) + "
"; + nbSocialNetworkButton++; + } + } + }); + if (nbSocialNetworkButton > 0) { + this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_SocialNetworkButton_Comment", String(nbSocialNetworkButton)); + } + } +}, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/StyleSheets.js b/greenit-core/rules/StyleSheets.js new file mode 100644 index 0000000..38244e8 --- /dev/null +++ b/greenit-core/rules/StyleSheets.js @@ -0,0 +1,23 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "StyleSheets", + comment: "", + detailComment: "", + + check: function (measures) { + let styleSheets = []; + if (measures.entries.length) measures.entries.forEach(entry => { + if (getResponseHeaderFromResource(entry, "content-type").toLowerCase().includes('text/css')) { + if (styleSheets.indexOf(entry.request.url) === -1) { + styleSheets.push(entry.request.url); + this.detailComment += entry.request.url + "
"; + } + } + }); + if (styleSheets.length > 2) { + if (styleSheets.length === 3) this.complianceLevel = 'B'; + else this.complianceLevel = 'C'; + this.comment = chrome.i18n.getMessage("rule_StyleSheets_Comment", String(styleSheets.length)); + } + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/UseETags.js b/greenit-core/rules/UseETags.js new file mode 100644 index 0000000..a48164d --- /dev/null +++ b/greenit-core/rules/UseETags.js @@ -0,0 +1,32 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "UseETags", + comment: "", + detailComment: "", + + check: function (measures) { + + let staticResourcesSize = 0; + let staticResourcesWithETagsSize = 0; + + if (measures.entries.length) measures.entries.forEach(entry => { + if (isStaticRessource(entry)) { + staticResourcesSize += entry.response.content.size; + if (isRessourceUsingETag(entry)) { + staticResourcesWithETagsSize += entry.response.content.size; + } + else this.detailComment +=chrome.i18n.getMessage("rule_UseETags_DetailComment",`${entry.request.url} ${Math.round(entry.response.content.size / 100) / 10}`) + '
'; + } + }); + if (staticResourcesSize > 0) { + const eTagsRatio = staticResourcesWithETagsSize / staticResourcesSize * 100; + if (eTagsRatio < 95) { + if (eTagsRatio < 90) this.complianceLevel = 'C' + else this.complianceLevel = 'B'; + } + else this.complianceLevel = 'A'; + this.comment = chrome.i18n.getMessage("rule_UseETags_Comment", + Math.round(eTagsRatio * 10) / 10 + "%"); + } + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rules/UseStandardTypefaces.js b/greenit-core/rules/UseStandardTypefaces.js new file mode 100644 index 0000000..b9ffb69 --- /dev/null +++ b/greenit-core/rules/UseStandardTypefaces.js @@ -0,0 +1,27 @@ +rulesManager.registerRule({ + complianceLevel: 'A', + id: "UseStandardTypefaces", + comment: "", + detailComment: "", + + check: function (measures) { + let totalFontsSize = 0; + if (measures.entries.length) measures.entries.forEach(entry => { + if (isFontResource(entry) && (entry.response.content.size > 0)) { + totalFontsSize += entry.response.content.size; + this.detailComment += entry.request.url + " " + Math.round(entry.response.content.size / 1000) + "KB
"; + } + }); + if (measures.dataEntries.length) measures.dataEntries.forEach(entry => { + if (isFontResource(entry) && (entry.response.content.size > 0)) { + totalFontsSize += entry.response.content.size; + url_toshow = entry.request.url; + if (url_toshow.length > 80) url_toshow = url_toshow.substring(0, 80) + "..."; + this.detailComment += url_toshow + " " + Math.round(entry.response.content.size / 1000) + "KB
"; + } + }); + if (totalFontsSize > 10000) this.complianceLevel = 'C'; + else if (totalFontsSize > 0) this.complianceLevel = 'B'; + if (totalFontsSize > 0) this.comment = chrome.i18n.getMessage("rule_UseStandardTypefaces_Comment", String(Math.round(totalFontsSize / 1000))); + } + }, "harReceived"); \ No newline at end of file diff --git a/greenit-core/rulesManager.js b/greenit-core/rulesManager.js new file mode 100644 index 0000000..89538ae --- /dev/null +++ b/greenit-core/rulesManager.js @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 didierfred@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +rulesManager = new RulesManager(); + +function RulesManager() { + + let rulesId = []; + let rulesChecker = new Map(); + let eventListeners = new Map(); + let notCompatibleRules = []; + eventListeners.set("harReceived", []); + eventListeners.set("frameMeasuresReceived", []); + eventListeners.set("resourceContentReceived", []); + + this.registerRule = function (ruleChecker, eventListener) { + rulesId.push(ruleChecker.id); + rulesChecker.set(ruleChecker.id, ruleChecker); + let event = eventListeners.get(eventListener); + if (event) event.push(ruleChecker.id); + } + + this.getRulesId = function () { + return rulesId; + } + + this.getRulesNotCompatibleWithCurrentBrowser = function () { + return notCompatibleRules; + + } + + this.getNewRulesChecker = function () { + return new RulesChecker(); + } + + function RulesChecker() { + let rules = new Map(); + rulesChecker.forEach((ruleChecker, ruleId) => { + let ruleCheckerInstance = Object.create(ruleChecker) + // for certains rules need an initalization , method not implemented in all rules + if (ruleCheckerInstance.initialize) ruleCheckerInstance.initialize(); + rules.set(ruleId, ruleCheckerInstance); + }); + + this.sendEvent = function (event, measures, resource) { + + eventListener = eventListeners.get(event); + if (eventListener) { + eventListener.forEach(ruleID => { + this.checkRule(ruleID, measures, resource); + }); + } + } + + this.checkRule = function (rule, measures, resource) { + rules.get(rule).check(measures, resource); + } + + this.getRule = function (rule) { + return rules.get(rule); + } + + this.getAllRules = function () { + return rules; + } + } +} diff --git a/greenit-core/utils.js b/greenit-core/utils.js new file mode 100644 index 0000000..d63097b --- /dev/null +++ b/greenit-core/utils.js @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2016 The EcoMeter authors (https://gitlab.com/ecoconceptionweb/ecometer) + * Copyright (C) 2019 didierfred@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +const DEBUG = true; +/* +requirejs.config({ + //By default load any module IDs from script + baseUrl: 'script/externalLibs', +}); + +// Load module require.js +requirejs(['esprima'], + (esprima) => console.log("Load esprima module")); +*/ + +const compressibleImage = [ + /^image\/bmp(;|$)/i, + /^image\/svg\+xml(;|$)/i, + /^image\/vnd\.microsoft\.icon(;|$)/i, + /^image\/x-icon(;|$)/i, +]; + +const image = [ + /^image\/gif(;|$)/i, + /^image\/jpeg(;|$)/i, + /^image\/png(;|$)/i, + /^image\/tiff(;|$)/i, +].concat(compressibleImage); + +const css = [ + /^text\/css(;|$)/i, +]; + +const javascript = [ + /^text\/javascript(;|$)/i, + /^application\/javascript(;|$)/i, + /^application\/x-javascript(;|$)/i, +]; + +const compressibleFont = [ + /^font\/eot(;|$)/i, + /^font\/opentype(;|$)/i, +]; + +const font = [ + /^application\/x-font-ttf(;|$)/i, + /^application\/x-font-opentype(;|$)/i, + /^application\/font-woff(;|$)/i, + /^application\/x-font-woff(;|$)/i, + /^application\/font-woff2(;|$)/i, + /^application\/vnd.ms-fontobject(;|$)/i, + /^application\/font-sfnt(;|$)/i, + /^font\/woff2(;|$)/i, +].concat(compressibleFont); + +const manifest = [ + /^text\/cache-manifest(;|$)/i, + /^application\/x-web-app-manifest\+json(;|$)/i, + /^application\/manifest\+json(;|$)/i, +]; + +// Mime types from H5B project recommendations +// See https://github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess#L741 +const compressible = [ + /^text\/html(;|$)/i, + /^text\/plain(;|$)/i, + /^text\/xml(;|$)/i, + /^application\/json(;|$)/i, + /^application\/atom\+xml(;|$)/i, + /^application\/ld\+json(;|$)/i, + /^application\/rdf\+xml(;|$)/i, + /^application\/rss\+xml(;|$)/i, + /^application\/schema\+json(;|$)/i, + /^application\/vnd\.geo\+json(;|$)/i, + /^application\/vnd\.ms-fontobject(;|$)/i, + /^application\/xhtml\+xml(;|$)/i, + /^application\/xml(;|$)/i, + /^text\/vcard(;|$)/i, + /^text\/vnd\.rim\.location\.xloc(;|$)/i, + /^text\/vtt(;|$)/i, + /^text\/x-component(;|$)/i, + /^text\/x-cross-domain-policy(;|$)/i, +].concat(javascript, css, compressibleImage, compressibleFont, manifest); + +const audio = [ + /^audio\/mpeg(;|$)/i, + /^audio\/x-ms-wma(;|$)/i, + /^audio\/vnd.rn-realaudio(;|$)/i, + /^audio\/x-wav(;|$)/i, + /^application\/ogg(;|$)/i, +]; + +const video = [ + /^video\/mpeg(;|$)/i, + /^video\/mp4(;|$)/i, + /^video\/quicktime(;|$)/i, + /^video\/x-ms-wmv(;|$)/i, + /^video\/x-msvideo(;|$)/i, + /^video\/x-flv(;|$)/i, + /^video\/webm(;|$)/i, +]; + +const others = [ + /^application\/x-shockwave-flash(;|$)/i, + /^application\/octet-stream(;|$)/i, + /^application\/pdf(;|$)/i, + /^application\/zip(;|$)/i, +]; + +const staticResources = [].concat(image, javascript, font, css, audio, video, manifest, others); + + +const httpCompressionTokens = [ + 'br', + 'compress', + 'deflate', + 'gzip', + 'pack200-gzip', +]; + +const httpRedirectCodes = [301, 302, 303, 307]; + +// utils for cache rule +function isStaticRessource(resource) { + const contentType = getResponseHeaderFromResource(resource, "content-type"); + return staticResources.some(value => value.test(contentType)); +} + +function isFontResource(resource) { + const contentType = getResponseHeaderFromResource(resource, "content-type"); + if (font.some(value => value.test(contentType))) return true; + // if not check url , because sometimes content-type is set to text/plain + if (contentType === "text/plain" || contentType==="" || contentType =="application/octet-stream") { + const url = resource.request.url; + if (url.endsWith(".woff")) return true; + if (url.endsWith(".woff2")) return true; + if (url.includes(".woff?")) return true; + if (url.includes(".woff2?")) return true; + if (url.includes(".woff2.json")) return true; + } + return false; +} + +function getHeaderWithName(headers, headerName) { + let headerValue = ""; + headers.forEach(header => { + if (header.name.toLowerCase() === headerName.toLowerCase()) headerValue = header.value; + }); + return headerValue; +} + +function getResponseHeaderFromResource(resource, headerName) { + return getHeaderWithName(resource.response.headers, headerName); +} + +function getCookiesLength(resource) { + let cookies = getHeaderWithName(resource.request.headers, "cookie"); + if (cookies) return cookies.length; + else return 0; +} + + +function hasValidCacheHeaders(resource) { + + const headers = resource.response.headers; + let cache = {}; + let isValid = false; + + headers.forEach(header => { + if (header.name.toLowerCase() === 'cache-control') cache.CacheControl = header.value; + if (header.name.toLowerCase() === 'expires') cache.Expires = header.value; + if (header.name.toLowerCase() === 'date') cache.Date = header.value; + }); + + // debug(() => `Cache headers gathered: ${JSON.stringify(cache)}`); + + if (cache.CacheControl) { + if (!(/(no-cache)|(no-store)|(max-age\s*=\s*0)/i).test(cache.CacheControl)) isValid = true; + } + + if (cache.Expires) { + let now = cache.Date ? new Date(cache.Date) : new Date(); + let expires = new Date(cache.Expires); + // Expires is in the past + if (expires < now) { + //debug(() => `Expires header is in the past ! ${now.toString()} < ${expires.toString()}`); + isValid = false; + } + } + + return isValid; +} + + +// utils for compress rule +function isCompressibleResource(resource) { + if (resource.response.content.size <= 150) return false; + const contentType = getResponseHeaderFromResource(resource, "content-type"); + return compressible.some(value => value.test(contentType)); +} + +function isResourceCompressed(resource) { + const contentEncoding = getResponseHeaderFromResource(resource, "content-encoding"); + return ((contentEncoding.length > 0) && (httpCompressionTokens.indexOf(contentEncoding.toLocaleLowerCase()) !== -1)); +} + +// utils for ETags rule +function isRessourceUsingETag(resource) { + const eTag = getResponseHeaderFromResource(resource, "ETag"); + if (eTag === "") return false; + return true; +} + +function getDomainFromUrl(url) { + var elements = url.split("//"); + if (elements[1] === undefined) return ""; + else { + elements = elements[1].split('/'); // get domain with port + elements = elements[0].split(':'); // get domain without port + } + return elements[0]; +} + +/** +* Count character occurences in the given string +*/ +function countChar(char, str) { + let total = 0; + str.split("").forEach(curr => { + if (curr === char) total++; + }); + return total; +} + +/** + * Detect minification for Javascript and CSS files + */ +function isMinified(scriptContent) { + + if (!scriptContent) return true; + if (scriptContent.length === 0) return true; + const total = scriptContent.length - 1; + const semicolons = countChar(';', scriptContent); + const linebreaks = countChar('\n', scriptContent); + if (linebreaks < 2) return true; + // Empiric method to detect minified files + // + // javascript code is minified if, on average: + // - there is more than one semicolon by line + // - and there are more than 100 characters by line + return semicolons / linebreaks > 1 && linebreaks / total < 0.01; + +} + +/** + * Detect network resources (data urls embedded in page is not network resource) + * Test with request.url as request.httpVersion === "data" does not work with old chrome version (example v55) + */ +function isNetworkResource(harEntry) { + return !(harEntry.request.url.startsWith("data")); +} + +/** + * Detect non-network resources (data urls embedded in page) + * Test with request.url as request.httpVersion === "data" does not work with old chrome version (example v55) + */ +function isDataResource(harEntry) { + return (harEntry.request.url.startsWith("data")); +} + +function computeNumberOfErrorsInJSCode(code, url) { + let errorNumber = 0; + try { + const syntax = require("esprima").parse(code, { tolerant: true, sourceType: 'script', loc: true }); + if (syntax.errors) { + if (syntax.errors.length > 0) { + errorNumber += syntax.errors.length; + debug(() => `url ${url} : ${Syntax.errors.length} errors`); + } + } + } catch (err) { + errorNumber++; + debug(() => `url ${url} : ${err} `); + } + return errorNumber; +} + +function isHttpRedirectCode(code) { + return httpRedirectCodes.some(value => value === code); +} + + +function getImageTypeFromResource(resource) { + const contentType = getResponseHeaderFromResource(resource, "content-type"); + if (contentType === "image/png") return "png"; + if (contentType === "image/jpeg") return "jpeg"; + if (contentType === "image/gif") return "gif"; + if (contentType === "image/bmp") return "bmp"; + if (contentType === "image/tiff") return "tiff"; + return ""; +} + + +function getMinOptimisationGainsForImage(pixelsNumber, imageSize, imageType) { + + // difficult to get good compression when image is small , images less than 10Kb are considered optimized + if (imageSize < 10000) return 0; + + // image png or gif < 50Kb are considered optimized (used for transparency not supported in jpeg format) + if ((imageSize < 50000) && ((imageType === 'png') || (imageType === 'gif'))) return 0; + + let imgMaxSize = Math.max(pixelsNumber / 5, 10000); // difficult to get under 10Kb + + // image > 500Kb are too big for web site , there are considered never optimized + if (imageSize > 500000) return Math.max(imageSize - 500000, imageSize - imgMaxSize); + + return Math.max(0, imageSize - imgMaxSize); +} + +function isSvgUrl(url) { + if (url.endsWith(".svg")) return true; + if (url.includes(".svg?")) return true; + return false; +} + +function isSvgOptimized(svgImage) { + if (svgImage.length < 1000) return true; // do not consider image < 1KB + if (svgImage.search(" <") === -1) return true; + return false; +} + + + +function getOfficialSocialButtonFormUrl(url) +{ + if (url.includes("platform.twitter.com/widgets.js")) return "tweeter"; + if (url.includes("platform.linkedin.com/in.js")) return "linkedin"; + if (url.includes("assets.pinterest.com/js/pinit.js")) return "pinterest"; + if (url.includes("connect.facebook.net") && url.includes("sdk.js")) return "facebook"; + if (url.includes("platform-api.sharethis.com/js/sharethis.js")) return "sharethis.com (mutliple social network) "; + if (url.includes("s7.addthis.com/js/300/addthis_widget.js")) return "addthis.com (mutliple social network) "; + if (url.includes("static.addtoany.com/menu/page.js")) return "addtoany.com (mutliple social network) "; + return ""; +} + +function debug(lazyString) { + if (!DEBUG) return; + const message = typeof lazyString === 'function' ? lazyString() : lazyString; + console.log(`GreenIT-Analysis [DEBUG] ${message}\n`); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cb86eec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1380 @@ +{ + "name": "greenit-cli", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==" + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "14.14.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", + "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "archiver": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz", + "integrity": "sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.0", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.1.4", + "zip-stream": "^4.0.4" + }, + "dependencies": { + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + } + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" + }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "chrome-har": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.11.12.tgz", + "integrity": "sha512-Fi/YCoUHjQMQC0sPKCdiuGVbApeEwIUNvISrlwZgbuUcxfHJA6MjD4RsIH/YSOAo/Z3ENiF+xaEpsdqqdETIjg==", + "requires": { + "dayjs": "1.8.31", + "debug": "4.1.1", + "tough-cookie": "4.0.0", + "uuid": "8.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "compress-commons": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz", + "integrity": "sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "concat-files": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/concat-files/-/concat-files-0.1.1.tgz", + "integrity": "sha1-cBauYCeLo/awtWE2j6IRFg5VbbU=", + "requires": { + "async": "~0.2.6" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, + "crc32-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.1.tgz", + "integrity": "sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "dayjs": { + "version": "1.8.31", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.31.tgz", + "integrity": "sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g==" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, + "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==" + }, + "devtools-protocol": { + "version": "0.0.818844", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", + "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==" + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "exceljs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.2.0.tgz", + "integrity": "sha512-r0/ClXRs3cQmMGJOQY6/ymnZSBSzeJL/LjAlKjY75ev1iQgf0LcmeFfTqFTFK0fADLAWieOMXe7abPoGYWI6hA==", + "requires": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.5.0", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "dependencies": { + "dayjs": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", + "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "requires": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "follow-redirects": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "got": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.1.tgz", + "integrity": "sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "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==" + }, + "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", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "requires": { + "agent-base": "5", + "debug": "4" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "jszip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", + "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha1-SeKM1VkBNFjIFMVHnTxmOiG/qmw=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "puppeteer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.5.0.tgz", + "integrity": "sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==", + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.818844", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + } + }, + "puppeteer-har": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/puppeteer-har/-/puppeteer-har-1.1.2.tgz", + "integrity": "sha512-Z5zfoj8RkhUT9UbrrR8JjOHNnCr7sNINoeR346F40sLo/4zn+KX/sw/eoKNrtsISc1s/2YCZaqaSEVx6cZ8NQg==", + "requires": { + "chrome-har": "^0.11.3" + } + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "sitemapper": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/sitemapper/-/sitemapper-3.1.12.tgz", + "integrity": "sha512-0BOXAhIfjQll1rrUkkFkpAhYy7MTs887H7Zpc4eAxLPPJJRFXNDdrryTadFWubGMn62bqGr3KdKBKhVdtt/HWg==", + "requires": { + "got": "^11.8.0", + "xml2js": "^0.4.23" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.5", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.5.tgz", + "integrity": "sha512-jYRGS3zWy20NtDtK2kBgo/TlAoy5YUuhD9/LZ7z7W4j1Fdw2cqD0xEEclf8fxc8xjD6X5Qr+qQQwCEsP8iRiYg==" + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "zip-stream": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz", + "integrity": "sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw==", + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.0.2", + "readable-stream": "^3.6.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4440884 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "greenit-cli", + "version": "1.0.0", + "description": "", + "main": "index.js", + "bin": { + "greenit": "greenit" + }, + "scripts": { + "postinstall": "node builder.js", + "test": "echo \"Error: no test specified\" && exit 1", + "build": "node builder.js", + "start": "node index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1", + "concat-files": "^0.1.1", + "exceljs": "^4.2.0", + "glob": "^7.1.6", + "progress": "^2.0.3", + "puppeteer": "^5.5.0", + "puppeteer-har": "^1.1.2", + "sitemapper": "^3.1.12", + "yaml": "^1.10.0", + "yargs": "^16.2.0" + }, + "devDependencies": {} +} diff --git a/sizes.js b/sizes.js new file mode 100644 index 0000000..4e1bf52 --- /dev/null +++ b/sizes.js @@ -0,0 +1,37 @@ +module.exports = { + desktop:{ + width: 1920, + height: 1080, + isMobile: false + }, + galaxyS9: { + width: 360, + height: 740, + isMobile: true + }, + galaxyS20: { + width: 360, + height: 740, + isMobile: true + }, + iPhone8: { + width: 375, + height: 667, + isMobile: true + }, + iPhone8Plus: { + width: 414, + height: 736, + isMobile: true + }, + iPhoneX: { + width: 375, + height: 812, + isMobile: true + }, + iPad: { + width: 768, + height: 1024, + isMobile: false + } +} \ No newline at end of file