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