├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── RELEASING.md ├── preview.gif └── workflows │ ├── format.yml │ └── unit-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── rollup.config.js ├── src ├── api.ts ├── compress.ts ├── compressor.ts ├── helpers │ ├── fs.ts │ ├── object.ts │ └── process.ts ├── index.ts ├── log │ ├── cli-report.ts │ ├── helpers │ │ ├── bytes.ts │ │ ├── error.ts │ │ ├── format.ts │ │ ├── icons.ts │ │ ├── output.ts │ │ └── path.ts │ ├── no-tty-report.ts │ ├── report.ts │ └── tty-report.ts ├── tsconfig.json └── validation │ ├── Condition.ts │ ├── Config.ts │ ├── File.ts │ ├── Project.ts │ └── Track.ts └── test ├── config-validation ├── config-validation.test.ts ├── fixtures │ ├── compression-missing │ │ ├── index.js │ │ └── package.json │ ├── item-path-missing │ │ └── package.json │ ├── max-size-missing │ │ ├── index.js │ │ └── package.json │ ├── missing-configuration │ │ └── package.json │ ├── missing-package-json │ │ └── index.js │ ├── standalone-config │ │ ├── filesize.json │ │ └── index.js │ ├── track-standalone-format │ │ ├── index.js │ │ └── package.json │ ├── track-standalone │ │ ├── index.js │ │ └── package.json │ ├── track │ │ ├── index.js │ │ └── package.json │ └── unparseable-package-json │ │ └── package.json └── track.test.ts ├── end-to-end ├── fixtures │ ├── api-report │ │ ├── filesize.json │ │ ├── inferno.js │ │ ├── package.json │ │ ├── preact.js │ │ └── react-dom.js │ ├── item-too-large │ │ ├── dist │ │ │ ├── bar.js │ │ │ ├── baz.js │ │ │ ├── foo.js │ │ │ └── index.js │ │ ├── filesize.json │ │ └── package.json │ ├── missing-item │ │ ├── filesize.json │ │ └── package.json │ └── successful │ │ ├── filesize.json │ │ ├── index.js │ │ ├── package.json │ │ └── replaced.js ├── large-config.test.ts ├── large-project.test.ts ├── successful-config.test.ts ├── successful-project.test.ts └── throw-error.test.ts ├── project-validation ├── fixtures │ ├── contains-package-json │ │ ├── index.js │ │ └── package.json │ └── missing-package-json │ │ └── index.js └── project-validation.test.ts └── tsconfig.json /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | See https://github.com/ampproject/meta/blob/master/CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | The AMP Project strives for a positive and growing project community that provides a safe environment for everyone. All members, committers and volunteers in the community are required to act according to the [code of conduct](CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /.github/RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Most of the manual steps for releasing this library are (hopefully) abstracted away by 4 | leveraging `np` to automatically update `package.json` and generate an updated changelist 5 | based on commits since the last active tag. 6 | 7 | ### Release Pre-Requisites: 8 | 9 | 1. Enable [two-factor authentication for your NPM account](https://docs.npmjs.com/configuring-two-factor-authentication). 10 | 2. Ensure you are part of the 'Administration' Group for the NPM `@ampproject` organization. 11 | 12 | ### Release an Update: 13 | 14 | 1. Ensure all tests are passing `yarn test`. The release scripts will not proceed 15 | if any test fails. 16 | 2. Execute in your terminal: `yarn release`. 17 | -------------------------------------------------------------------------------- /.github/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/.github/preview.gif -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | on: pull_request 3 | jobs: 4 | format: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [14.x] 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - name: Format 17 | run: yarn install; yarn format 18 | - name: Push changes 19 | uses: kristoferbaxter/github-push-action@master 20 | with: 21 | force: true 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | branch: ${{ github.head_ref }} -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: yarn install; yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | test/output/ 3 | node_modules/ 4 | .vscode/ 5 | .DS_Store 6 | .nyc_output 7 | .rollup.cache 8 | coverage 9 | yarn-error.log 10 | yarn.lock 11 | package-lock.json 12 | dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019, Google Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filesize 2 | 3 | **Purpose**: Monitor the size of files in your project specified within `package.json`. 4 | 5 | Uses native compression from Node Core, attempts to parallelize compression work across available CPUs, and requires Node >= `12`. 6 | 7 | ![Interactive UI Processing Large Files](https://raw.githubusercontent.com/ampproject/filesize/main/.github/preview.gif) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | yarn add @ampproject/filesize --dev 13 | ``` 14 | 15 | ## Usage 16 | 17 | Specify an object of files you'd like to check the size for inside the `filesize` key of `package.json`. 18 | 19 | ```json 20 | { 21 | "filesize": { 22 | "./dist/index.js": { 23 | "brotli": "11.4 kB" 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | Each file (key in the filesize object) must include an object with key/value pairs: 30 | 1. The key is the `compression` type you would like to use on the file. 31 | 2. The value is the string representation of the files maximum allowed size. 32 | 33 | **After completing configuration**, invoke `filesize` via: `yarn filesize`. 34 | 35 | Optionally one can target a different project directory via the `p` parameter `yarn filesize -p={PATH}`, or a different configuration file via the `c` parameter `yarn filesize -c=${PATH/filesize.json}`. 36 | 37 | ### Track Resource Size 38 | 39 | This utility now also supports tracking filesizes without enforcing a max limit. To use this feature add a `track` key to the `filesize` entry. 40 | 41 | ```json 42 | { 43 | "filesize": { 44 | "track": ["./dist/**/*.mjs"], 45 | } 46 | } 47 | ``` 48 | 49 | These values will be added to the output report for all comression types. 50 | 51 | ## Security disclosures 52 | 53 | The AMP Project accepts responsible security disclosures through the [Google Application Security program](https://www.google.com/about/appsecurity/). 54 | 55 | ## Code of conduct 56 | 57 | The AMP Project strives for a positive and growing project community that provides a safe environment for everyone. All members, committers and volunteers in the community are required to act according to the [code of conduct](.github/CODE_OF_CONDUCT.md). 58 | 59 | ## License 60 | 61 | filesize is licensed under the [Apache License, Version 2.0](LICENSE). 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ampproject/filesize", 3 | "version": "4.2.1", 4 | "description": "Audit the filesize of items specified in package.json.", 5 | "author": "Kristofer Baxter", 6 | "license": "Apache-2.0", 7 | "main": "dist/api.js", 8 | "module": "dist/api.mjs", 9 | "files": [ 10 | "dist" 11 | ], 12 | "engines": { 13 | "node": ">=12" 14 | }, 15 | "bin": { 16 | "filesize": "./dist/filesize" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/ampproject/filesize.git" 21 | }, 22 | "scripts": { 23 | "clean": "rimraf dist output test/output", 24 | "format": "prettier --write '**/*.ts'", 25 | "build-tests": "tsc -p test/tsconfig.json", 26 | "postbuild-tests": "tsc-esm -p test/tsconfig.json", 27 | "pretest": "yarn npm-run-all build build-tests", 28 | "test": "NODE_DISABLE_COLORS=1 tap --no-coverage --no-esm test/output/**/*.test.mjs", 29 | "build": "rollup -c; chmod a+x dist/filesize", 30 | "try": "./dist/filesize", 31 | "release": "np --no-2fa --any-branch", 32 | "prepublishOnly": "yarn npm-run-all clean build" 33 | }, 34 | "dependencies": { 35 | "@kristoferbaxter/async": "1.0.0", 36 | "@kristoferbaxter/kleur": "4.0.2", 37 | "@kristoferbaxter/bytes": "0.1.2", 38 | "fast-glob": "3.2.5", 39 | "mri": "1.1.6" 40 | }, 41 | "devDependencies": { 42 | "@ampproject/rollup-plugin-closure-compiler": "0.26.0", 43 | "@rollup/plugin-commonjs": "17.1.0", 44 | "@rollup/plugin-node-resolve": "11.1.1", 45 | "@rollup/plugin-typescript": "8.1.1", 46 | "@types/mri": "1.1.0", 47 | "@types/node": "14.14.35", 48 | "@types/tap": "14.10.2", 49 | "np": "https://github.com/pixelastic/np/tarball/c3ab2e3b053c7da0ce40a572ca1616273ac080f8", 50 | "npm-run-all": "4.1.5", 51 | "prettier": "2.2.1", 52 | "rimraf": "3.0.2", 53 | "rollup": "2.38.5", 54 | "tap": "14.11.0", 55 | "tslib": "2.1.0", 56 | "typescript": "4.1.3", 57 | "typescript-esm": "2.0.0" 58 | }, 59 | "volta": { 60 | "node": "14.16.0", 61 | "yarn": "1.22.10" 62 | }, 63 | "filesize": { 64 | "track": [ 65 | "./dist/**/*.mjs", 66 | "./dist/filesize" 67 | ], 68 | "./dist/filesize": { 69 | "brotli": "3 kB", 70 | "gzip": "3.8 kB", 71 | "none": "20 kB" 72 | }, 73 | "./dist/api.js": { 74 | "brotli": "5 kB", 75 | "gzip": "6.5 kB" 76 | }, 77 | "./dist/api.mjs": { 78 | "gzip": "10 kB" 79 | } 80 | }, 81 | "publishConfig": { 82 | "access": "public" 83 | }, 84 | "prettier": { 85 | "printWidth": 120, 86 | "trailingComma": "all", 87 | "parser": "typescript", 88 | "singleQuote": true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import resolve from '@rollup/plugin-node-resolve'; 18 | import typescript from '@rollup/plugin-typescript'; 19 | import commonjs from '@rollup/plugin-commonjs'; 20 | import compiler from '@ampproject/rollup-plugin-closure-compiler'; 21 | 22 | const external = ['os', 'zlib', 'path', 'fs', 'stream', 'util', 'events', 'fast-glob', 'process']; 23 | const plugins = executable => [ 24 | resolve({ preferBuiltins: true }), 25 | commonjs({ include: 'node_modules/**' }), 26 | typescript({ tsconfig: 'src/tsconfig.json', include: '**/*.ts', exclude: 'dist/**/*.ts' }), 27 | executable ? compiler() : null, 28 | ]; 29 | 30 | export default [ 31 | { 32 | input: 'src/index.ts', 33 | output: { 34 | file: 'dist/filesize', 35 | format: 'cjs', 36 | sourcemap: true, 37 | banner: '#!/usr/bin/env node', 38 | }, 39 | external, 40 | plugins: plugins(true), 41 | }, 42 | { 43 | input: 'src/api.ts', 44 | output: { 45 | file: 'dist/api.mjs', 46 | format: 'esm', 47 | sourcemap: true, 48 | }, 49 | external, 50 | plugins: plugins(false), 51 | }, 52 | { 53 | input: 'src/api.ts', 54 | output: { 55 | file: 'dist/api.js', 56 | format: 'cjs', 57 | sourcemap: true, 58 | }, 59 | external, 60 | plugins: plugins(false), 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import Project from './validation/Project'; 19 | import Config from './validation/Config'; 20 | import { Context, FileModifier, SizeMap } from './validation/Condition'; 21 | import compress, { CompressionItem, findItemsToCompress } from './compress'; 22 | import { Report } from './log/report'; 23 | export { Report } from './log/report'; 24 | export { NoTTYReport } from './log/no-tty-report'; 25 | 26 | export async function report( 27 | configOrProjectPath: string, 28 | fileModifier: FileModifier, 29 | report?: typeof Report, 30 | silent?: boolean, 31 | ): Promise { 32 | let projectPath = ''; 33 | let packagePath = ''; 34 | if (path.extname(configOrProjectPath) === '.json') { 35 | // The requested config or project path is a config. 36 | packagePath = configOrProjectPath; 37 | } else { 38 | projectPath = configOrProjectPath; 39 | } 40 | 41 | const conditions = [Project, Config]; 42 | let context: Context = { 43 | projectPath, 44 | packagePath, 45 | packageContent: '', 46 | silent: silent ?? true, 47 | originalPaths: new Map(), 48 | // Stores the result of compression 49 | compressed: new Map(), 50 | // Stores the basis of comparison. 51 | comparison: new Map(), 52 | fileModifier, 53 | fileContents: new Map(), 54 | }; 55 | 56 | for (const condition of conditions) { 57 | const message = await condition(context)(); 58 | if (message !== null) { 59 | throw message; 60 | } 61 | } 62 | 63 | const toCompress: Array = await findItemsToCompress(context, true); 64 | await compress(context, toCompress, report || null); 65 | return context.compressed; 66 | } 67 | -------------------------------------------------------------------------------- /src/compress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Context, Compression, OrderedCompressionValues, maxSize } from './validation/Condition'; 18 | import { readFile } from './helpers/fs'; 19 | import { Report } from './log/report'; 20 | import { compressor } from './compressor'; 21 | import { pool } from '@kristoferbaxter/async'; 22 | 23 | export interface CompressionItem { 24 | path: string; 25 | compression: Compression; 26 | maxSize: maxSize; 27 | } 28 | 29 | /** 30 | * Store the original content so it isn't retrieved from FileSystem for each compression. 31 | * @param context 32 | * @param path 33 | */ 34 | async function storeOriginalFileContents(context: Context, path: string): Promise { 35 | if (!context.fileContents.has(path)) { 36 | let content = await readFile(path); 37 | if (context.fileModifier !== null && content !== null) { 38 | content = context.fileModifier(content); 39 | } 40 | context.fileContents.set(path, content || ''); 41 | } 42 | } 43 | 44 | /** 45 | * Find all items to compress, and store them for future compression. 46 | * @param context 47 | * @param findDefaultSize 48 | */ 49 | export async function findItemsToCompress(context: Context, findDefaultSize: boolean): Promise> { 50 | const toCompress: Array = []; 51 | await pool(Array.from(context.compressed), async ([path, sizeMapValue]) => { 52 | for (let iterator: number = 0; iterator < OrderedCompressionValues.length; iterator++) { 53 | const compression: Compression = OrderedCompressionValues[iterator] as Compression; 54 | const [size, maxSize] = sizeMapValue[iterator]; 55 | await storeOriginalFileContents(context, path); 56 | if (findDefaultSize && compression === 'none') { 57 | await compressor(context, null, { path, compression, maxSize }); 58 | } 59 | if (size !== undefined) { 60 | toCompress.push({ 61 | path, 62 | compression, 63 | maxSize, 64 | }); 65 | } 66 | } 67 | }); 68 | 69 | return toCompress; 70 | } 71 | 72 | /** 73 | * Given a context, compress all Items within splitting work eagly per cpu core to achieve some concurrency. 74 | * @param context Finalized Valid Context from Configuration 75 | */ 76 | export default async function compress( 77 | context: Context, 78 | toCompress: Array, 79 | report: typeof Report | null, 80 | ): Promise { 81 | if (toCompress.length === 0) { 82 | return true; 83 | } 84 | 85 | const reportInstance: Report | null = report ? new report(context) : null; 86 | const successful = await pool(toCompress, (item: CompressionItem) => compressor(context, reportInstance, item)); 87 | reportInstance?.end(); 88 | return successful.every((success) => success); 89 | } 90 | -------------------------------------------------------------------------------- /src/compressor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { constants as brotliConstants, brotliCompress, gzip } from 'zlib'; 18 | import { Report } from './log/report'; 19 | import { Context, Compression, maxSize, OrderedCompressionValues } from './validation/Condition'; 20 | import { LogError } from './log/helpers/error'; 21 | 22 | type CompressionMethod = (buffer: Buffer, options: {}, callback: (error: Error | null, result: Buffer) => void) => void; 23 | 24 | interface CompressionItem { 25 | path: string; 26 | compression: Compression; 27 | maxSize: maxSize; 28 | } 29 | 30 | const SUPPORTED_COMPRESSION: Map = new Map([ 31 | [ 32 | 'brotli', 33 | [ 34 | brotliCompress, 35 | { 36 | params: { 37 | [brotliConstants.BROTLI_PARAM_MODE]: brotliConstants.BROTLI_DEFAULT_MODE, 38 | [brotliConstants.BROTLI_PARAM_QUALITY]: brotliConstants.BROTLI_MAX_QUALITY, 39 | [brotliConstants.BROTLI_PARAM_SIZE_HINT]: 0, 40 | }, 41 | }, 42 | ], 43 | ], 44 | [ 45 | 'gzip', 46 | [ 47 | gzip, 48 | { 49 | level: 9, 50 | }, 51 | ], 52 | ], 53 | ]); 54 | 55 | /** 56 | * Use the given configuration and actual size to report item filesize. 57 | * @param report Optional reporter to update with this value 58 | * @param item Configuration for an Item 59 | * @param error Error from compressing an Item 60 | * @param size actual size for this comparison 61 | */ 62 | function store( 63 | report: Report | null, 64 | context: Context, 65 | item: CompressionItem, 66 | error: Error | null, 67 | size: number, 68 | ): boolean { 69 | if (error !== null) { 70 | LogError(`Could not compress '${item.path}' with '${item.compression}'.`); 71 | return false; 72 | } 73 | 74 | // Store the size of the item in the compression map. 75 | const sizeMap = context.compressed.get(item.path); 76 | if (sizeMap === undefined) { 77 | LogError(`Could not find item '${item.path}' with '${item.compression}' in compression map.`); 78 | return false; 79 | } 80 | sizeMap[OrderedCompressionValues.indexOf(item.compression)][0] = size; 81 | 82 | report?.update(context); 83 | if (item.maxSize === undefined) { 84 | return true; 85 | } 86 | return size < item.maxSize; 87 | } 88 | 89 | export function compressor(context: Context, report: Report | null, item: CompressionItem): Promise { 90 | const contents = context.fileContents.get(item.path); 91 | if (contents) { 92 | return new Promise((resolve) => { 93 | const buffer = Buffer.from(contents, 'utf8'); 94 | const compression = SUPPORTED_COMPRESSION.get(item.compression); 95 | if (compression) { 96 | compression[0](buffer, compression[1], (error: Error | null, result: Buffer) => 97 | resolve(store(report, context, item, error, result.byteLength)), 98 | ); 99 | } else { 100 | resolve(store(report, context, item, null, buffer.byteLength)); 101 | } 102 | }); 103 | } 104 | 105 | return Promise.resolve(false); 106 | } 107 | -------------------------------------------------------------------------------- /src/helpers/fs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { promises as fs } from 'fs'; 18 | 19 | /** 20 | * Returns if a path is a directory. 21 | * @param path 22 | */ 23 | export async function isDirectory(path: string): Promise { 24 | try { 25 | return (await fs.lstat(path)).isDirectory(); 26 | } catch (e) {} 27 | return false; 28 | } 29 | 30 | /** 31 | * Returns if a path is a file. 32 | * @param path 33 | */ 34 | export async function isFile(path: string): Promise { 35 | try { 36 | return (await fs.lstat(path)).isFile(); 37 | } catch (e) {} 38 | return false; 39 | } 40 | 41 | /** 42 | * Returns contents of a path as a string 43 | * @param path 44 | */ 45 | export async function readFile(path: string): Promise { 46 | try { 47 | return await fs.readFile(path, 'utf-8'); 48 | } catch (e) {} 49 | return null; 50 | } 51 | -------------------------------------------------------------------------------- /src/helpers/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Helper Method, quick check if an any from JSON is an Object. 19 | * @param item 20 | * @returns {boolean} 21 | */ 22 | export function isObject(item: any) { 23 | return item === Object(item); 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/process.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { exit } from 'process'; 18 | import { exhaust } from '../log/helpers/output'; 19 | 20 | export function shutdown(code: number) { 21 | exhaust(() => { 22 | exit(code); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import mri from 'mri'; 18 | import { stdout } from 'process'; 19 | import Project from './validation/Project'; 20 | import Config from './validation/Config'; 21 | import { Context } from './validation/Condition'; 22 | import compress, { CompressionItem, findItemsToCompress } from './compress'; 23 | import { LogError } from './log/helpers/error'; 24 | import { shutdown } from './helpers/process'; 25 | import { Report } from './log/report'; 26 | import { TTYReport } from './log/tty-report'; 27 | import { NoTTYReport } from './log/no-tty-report'; 28 | 29 | const args = mri(process.argv.slice(2), { 30 | alias: { p: 'project', c: 'config' }, 31 | default: { project: process.cwd(), config: '', silent: false }, 32 | }); 33 | 34 | /** 35 | * Read the configuration from the specified project, validate it, perform requested compression, and report the results. 36 | */ 37 | (async function () { 38 | const { project: projectPath, silent, config: requestedConfig } = args; 39 | const conditions = [Project, Config]; 40 | const context: Context = { 41 | projectPath, 42 | packagePath: requestedConfig, 43 | packageContent: '', 44 | silent, 45 | originalPaths: new Map(), 46 | // Stores the result of compression 47 | compressed: new Map(), 48 | // Stores the basis of comparison. 49 | comparison: new Map(), 50 | fileModifier: null, 51 | fileContents: new Map(), 52 | }; 53 | 54 | for (const condition of conditions) { 55 | const message = await condition(context)(); 56 | if (message !== null) { 57 | LogError(message); 58 | shutdown(5); 59 | return; 60 | } 61 | } 62 | 63 | const toCompress: Array = await findItemsToCompress(context, true); 64 | const report: typeof Report = stdout.isTTY && toCompress.length < 30 ? TTYReport : NoTTYReport; 65 | const successful = await compress(context, toCompress, report); 66 | if (!successful) { 67 | shutdown(6); 68 | } 69 | })(); 70 | -------------------------------------------------------------------------------- /src/log/cli-report.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import kleur from '@kristoferbaxter/kleur'; 18 | import { Report } from './report'; 19 | import { Context, brotliSize, gzipSize, noneSize } from '../validation/Condition'; 20 | import { maxFormatDisplay, formats } from './helpers/format'; 21 | import { maxPathDisplay } from './helpers/path'; 22 | import { write } from './helpers/output'; 23 | import { prettyBytes } from './helpers/bytes'; 24 | import { ICONS } from './helpers/icons'; 25 | 26 | export class CLIReport extends Report { 27 | protected maxPathDisplay: number; 28 | protected maxFormatDisplay: number; 29 | protected currentLine: string; 30 | 31 | constructor(context: Context) { 32 | super(context); 33 | 34 | this.silent = context.silent; 35 | this.maxPathDisplay = maxPathDisplay(context); 36 | this.maxFormatDisplay = maxFormatDisplay(context); 37 | 38 | this.start(); 39 | } 40 | 41 | private start(): void { 42 | write(kleur.bold('\n Filesizes\n')); 43 | write(`${''.padEnd(this.maxPathDisplay + 5)} ${formats(this.maxFormatDisplay)}\n`); 44 | } 45 | 46 | protected displaySize([size, maxSize]: brotliSize | gzipSize | noneSize): { 47 | success: number; 48 | warning: number; 49 | failure: number; 50 | processing: number; 51 | } { 52 | const status = { 53 | success: 0, 54 | warning: 0, 55 | failure: 0, 56 | processing: 0, 57 | }; 58 | 59 | if (size === null) { 60 | // Item is still processing. 61 | status.processing++; 62 | } 63 | 64 | if (size === undefined || size === null) { 65 | // Will not be calculated. 66 | this.currentLine += kleur.dim().grey('–'.padEnd(this.maxFormatDisplay)); 67 | return status; 68 | } 69 | 70 | const outputBytes = prettyBytes(size); 71 | if (maxSize === undefined) { 72 | this.currentLine += kleur.dim().grey(outputBytes.padEnd(this.maxFormatDisplay)); 73 | return status; 74 | } 75 | if (size < maxSize) { 76 | if (1 - size / maxSize < 0.05) { 77 | this.warning++; 78 | status.warning++; 79 | this.currentLine += kleur.yellow(outputBytes.padEnd(this.maxFormatDisplay)); 80 | return status; 81 | } 82 | 83 | this.success++; 84 | status.success++; 85 | this.currentLine += kleur.dim().green(outputBytes.padEnd(this.maxFormatDisplay)); 86 | return status; 87 | } 88 | 89 | this.failure++; 90 | status.failure++; 91 | this.currentLine += kleur.red(outputBytes.padEnd(this.maxFormatDisplay)); 92 | return status; 93 | } 94 | 95 | public end(): void { 96 | super.end(); 97 | 98 | const { success, failure, warning } = this; 99 | if (success > 0 || failure > 0 || warning > 0) { 100 | write( 101 | '\n ' + 102 | kleur.green(success + ` ${success === 1 ? 'check' : 'checks'} passed`) + 103 | (failure === 0 ? ` ${ICONS['tada']}` : ''), 104 | ); 105 | if (warning > 0) { 106 | write( 107 | '\n ' + 108 | kleur.yellow(warning + ` ${warning === 1 ? 'check' : 'checks'} warned`) + 109 | kleur.grey(' (within 5% of allowed size)'), 110 | ); 111 | } 112 | if (failure > 0) { 113 | write('\n ' + kleur.red(failure + ` ${failure === 1 ? 'check' : 'checks'} failed`)); 114 | } 115 | write('\n\n'); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/log/helpers/bytes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import bytes from '@kristoferbaxter/bytes'; 18 | 19 | /** 20 | * Format size into more human readable string. 21 | * @param size 22 | */ 23 | export function prettyBytes(size: number): string { 24 | return bytes(size, { unit: 'kb', fixedDecimals: true, unitSeparator: ' ' }); 25 | } 26 | -------------------------------------------------------------------------------- /src/log/helpers/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { write } from './output'; 18 | import kleur from '@kristoferbaxter/kleur'; 19 | 20 | /** 21 | * Format output as an error message. 22 | * @param output 23 | */ 24 | export function MakeError(output: string): string { 25 | return `${kleur.red('error')} ${output}`; 26 | } 27 | 28 | /** 29 | * Display output as an error message on the console. 30 | * @param output 31 | */ 32 | export function LogError(output: string): void { 33 | write(MakeError(output) + '\n'); 34 | } 35 | -------------------------------------------------------------------------------- /src/log/helpers/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { OrderedCompressionValues, Context } from '../../validation/Condition'; 18 | import { prettyBytes } from './bytes'; 19 | 20 | const COMPRESSED_NAMES_LENGTH = OrderedCompressionValues.map((compression) => compression.length); 21 | const SPACE_AFTER_COMPRESSION = 2; 22 | 23 | /** 24 | * Use the 'none' size of items to find the max width of the format. 25 | * @param context 26 | */ 27 | export function maxFormatDisplay(context: Context): number { 28 | const itemsToConsider: Array = [...COMPRESSED_NAMES_LENGTH]; 29 | const paths = Array.from(context.compressed.keys()); 30 | for (const path of paths) { 31 | const sizeMapValue = context.compressed.get(path); 32 | if (!sizeMapValue) { 33 | continue; 34 | } 35 | 36 | const [size] = sizeMapValue[OrderedCompressionValues.indexOf('none')]; 37 | if (size) { 38 | itemsToConsider.push(prettyBytes(size).length); 39 | } 40 | } 41 | 42 | return Math.max.apply(null, itemsToConsider) + SPACE_AFTER_COMPRESSION; 43 | } 44 | 45 | /** 46 | * Output all formats 47 | * @param maxFormatDisplay 48 | */ 49 | export function formats(maxFormatDisplay: number): string { 50 | return OrderedCompressionValues.map((compression) => compression.padEnd(maxFormatDisplay)).join(''); 51 | } 52 | -------------------------------------------------------------------------------- /src/log/helpers/icons.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Plan to use this spinner while a file is being processed. 18 | const isWin32 = process.platform === 'win32'; 19 | export const SPINNER = isWin32 20 | ? { 21 | interval: 130, 22 | frames: ['-', '\\', '|', '/'], 23 | } 24 | : { 25 | interval: 80, 26 | frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], 27 | }; 28 | 29 | export const ICONS = { 30 | tick: isWin32 ? '√' : '✔', 31 | cross: isWin32 ? '×' : '✖', 32 | tada: '🎉', 33 | }; 34 | -------------------------------------------------------------------------------- /src/log/helpers/output.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { stdout } from 'process'; 18 | 19 | const ESC_SEQUENCE = '\u001B['; 20 | const ERASE_LINE = ESC_SEQUENCE + '2K'; 21 | const CURSOR_LEFT = ESC_SEQUENCE + 'G'; 22 | const CURSOR_UP = ESC_SEQUENCE + '1A'; 23 | 24 | const outputQueue: Array = []; 25 | 26 | /** 27 | * Erase the number of lines from a TTY terminal 28 | * @param count 29 | */ 30 | export function erase(count: number): void { 31 | if (count <= 0) { 32 | return; 33 | } 34 | 35 | let sequence = ''; 36 | for (let i = 0; i < count; i++) { 37 | sequence += ERASE_LINE + (i < count - 1 ? CURSOR_UP : ''); 38 | } 39 | if (count) { 40 | sequence += CURSOR_LEFT; 41 | } 42 | 43 | write(sequence); 44 | } 45 | 46 | /** 47 | * Exhause the outputQueue and call the callback method when finished. 48 | * @param callback 49 | */ 50 | export function exhaust(callback: () => any = () => void 0) { 51 | const text: string | undefined = outputQueue.shift(); 52 | 53 | if (text) { 54 | stdout.write(text, () => { 55 | if (outputQueue.length > 0) { 56 | exhaust(callback); 57 | } else { 58 | callback(); 59 | } 60 | }); 61 | } else { 62 | callback(); 63 | } 64 | } 65 | 66 | export function write(content: string): void { 67 | outputQueue.push(content); 68 | exhaust(); 69 | } 70 | -------------------------------------------------------------------------------- /src/log/helpers/path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Context } from '../../validation/Condition'; 18 | 19 | const SPACE_AFTER_PATH = 2; 20 | const MAX_ALLOWED_PATH_LENGTH = 50; 21 | 22 | function allowedPathLength(path: string): number { 23 | return Math.min(path.length, MAX_ALLOWED_PATH_LENGTH) + SPACE_AFTER_PATH; 24 | } 25 | 26 | export function maxPathDisplay(context: Context): number { 27 | return Math.max.apply(null, Array.from(context.originalPaths.values()).map(allowedPathLength)); 28 | } 29 | -------------------------------------------------------------------------------- /src/log/no-tty-report.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import kleur from '@kristoferbaxter/kleur'; 18 | import { Context, OrderedCompressionValues, SizeMap } from '../validation/Condition'; 19 | import { write } from './helpers/output'; 20 | import { ICONS } from './helpers/icons'; 21 | import { CLIReport } from './cli-report'; 22 | 23 | export class NoTTYReport extends CLIReport { 24 | protected maxPathDisplay: number; 25 | protected maxFormatDisplay: number; 26 | protected currentLine: string; 27 | 28 | public update(context: Context): void { 29 | const completed: SizeMap = super.getUpdated(context); 30 | if (this.silent) { 31 | return; 32 | } 33 | 34 | for (const complete of completed) { 35 | const [path, sizeMapValue] = complete; 36 | const displayPath = context.originalPaths.get(path) || path; 37 | let failure = 0; 38 | 39 | this.currentLine = ` ${displayPath 40 | .substring(displayPath.length - this.maxPathDisplay) 41 | .padEnd(this.maxPathDisplay)} `; 42 | for (let i = 0; i < OrderedCompressionValues.length; i++) { 43 | failure += this.displaySize(sizeMapValue[i]).failure; 44 | } 45 | 46 | if (failure > 0) { 47 | this.currentLine = ` ${kleur.red(ICONS['cross'])}${this.currentLine}`; 48 | } else { 49 | this.currentLine = ` ${kleur.dim().green(ICONS['tick'])}${this.currentLine}`; 50 | } 51 | 52 | write(this.currentLine + '\n'); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/log/report.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Context, SizeMap } from '../validation/Condition'; 18 | 19 | export class Report { 20 | protected silent: boolean = false; 21 | protected paths: Set; 22 | protected warning: number = 0; 23 | protected success: number = 0; 24 | protected failure: number = 0; 25 | 26 | constructor(context: Context) { 27 | this.paths = new Set(context.compressed.keys()); 28 | } 29 | 30 | public end(): void {} 31 | public update(context: Context): void {} 32 | 33 | public getUpdated(context: Context): SizeMap { 34 | const completed: SizeMap = new Map(); 35 | 36 | iterate_paths: for (const path of this.paths) { 37 | const sizeMapValue = context.compressed.get(path); 38 | if (!sizeMapValue) { 39 | continue iterate_paths; 40 | } 41 | 42 | for (const value of sizeMapValue) { 43 | if (value[0] === null) { 44 | // A null value for the size indicates the resource is still processing. 45 | continue iterate_paths; 46 | } 47 | } 48 | 49 | completed.set(path, sizeMapValue); 50 | this.paths.delete(path); 51 | } 52 | 53 | return completed; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/log/tty-report.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import kleur from '@kristoferbaxter/kleur'; 18 | import { Context, OrderedCompressionValues } from '../validation/Condition'; 19 | import { write, erase } from './helpers/output'; 20 | import { ICONS } from './helpers/icons'; 21 | import { CLIReport } from './cli-report'; 22 | 23 | export class TTYReport extends CLIReport { 24 | private outputLength: number = 0; 25 | 26 | private reset = (): number => { 27 | const previousOutputLength = this.outputLength; 28 | this.currentLine = ''; 29 | this.success = 0; 30 | this.failure = 0; 31 | this.warning = 0; 32 | this.outputLength = 0; 33 | return previousOutputLength; 34 | }; 35 | 36 | private status = () => { 37 | write( 38 | '\n ' + 39 | kleur.green(this.success + ` ${this.success === 1 ? 'check' : 'checks'} passed`) + 40 | (this.failure === 0 ? ` ${ICONS['tada']}` : ''), 41 | ); 42 | this.outputLength++; 43 | if (this.warning > 0) { 44 | write( 45 | '\n ' + 46 | kleur.yellow(this.warning + ` ${this.warning === 1 ? 'check' : 'checks'} warned`) + 47 | kleur.grey(' (within 5% of allowed size)'), 48 | ); 49 | this.outputLength++; 50 | } 51 | if (this.failure > 0) { 52 | write('\n ' + kleur.red(this.failure + ` ${this.failure === 1 ? 'check' : 'checks'} failed`)); 53 | this.outputLength++; 54 | } 55 | write('\n\n'); 56 | this.outputLength = this.outputLength + 3; 57 | }; 58 | 59 | public end(): void {} 60 | public update(context: Context): void { 61 | if (this.silent) { 62 | return; 63 | } 64 | const previousOutputLength = this.reset(); 65 | 66 | let output: string = ''; 67 | for (const path of this.paths) { 68 | const sizeMapValue = context.compressed.get(path); 69 | if (!sizeMapValue) { 70 | continue; 71 | } 72 | 73 | const displayPath = context.originalPaths.get(path) || path; 74 | let failure = 0; 75 | let processing = 0; 76 | 77 | this.currentLine = ` ${displayPath 78 | .substring(displayPath.length - this.maxPathDisplay) 79 | .padEnd(this.maxPathDisplay)} `; 80 | for (let i = 0; i < OrderedCompressionValues.length; i++) { 81 | const status = super.displaySize(sizeMapValue[i]); 82 | failure += status.failure; 83 | processing += status.processing; 84 | } 85 | 86 | const icon = 87 | failure > 0 88 | ? kleur.red(ICONS['cross']) 89 | : processing > 0 90 | ? kleur.dim().grey('-') 91 | : kleur.dim().green(ICONS['tick']); 92 | output += ` ${icon}${this.currentLine}\n`; 93 | this.outputLength++; 94 | } 95 | 96 | erase(previousOutputLength); 97 | write(output); 98 | this.status(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "outDir": "output", 8 | "sourceMap": true, 9 | "moduleResolution": "node", 10 | "target": "ES2019", 11 | "module": "esnext", 12 | "allowJs": false, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "esModuleInterop": true, 19 | "declaration": true, 20 | }, 21 | "include": [ 22 | "*.ts", 23 | "**/*.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/validation/Condition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // string indicates an error of the formatted return type. 18 | // null indicates success. 19 | export type ValidationResponse = string | null; 20 | export type ConditionFunction = (context: Context) => () => Promise; 21 | 22 | export type Compression = 'brotli' | 'gzip' | 'none'; 23 | export const OrderedCompressionValues = ['brotli', 'gzip', 'none']; 24 | 25 | type path = string; 26 | export type OriginalPath = Map; 27 | 28 | /* 29 | - number = calculated 30 | - null = awaiting calculation 31 | - undefined = ignored 32 | */ 33 | type size = number | null | undefined; 34 | /* 35 | - number = max allowed size 36 | - undefined = unrestricted size 37 | */ 38 | export type maxSize = number | undefined; 39 | export type brotliSize = [size, maxSize]; 40 | export type gzipSize = [size, maxSize]; 41 | export type noneSize = [size, maxSize]; 42 | export type SizeMapValue = [brotliSize, gzipSize, noneSize]; 43 | export const SizeMapValueIndex = { 44 | brotli: 0, 45 | gzip: 1, 46 | none: 2, 47 | }; 48 | export type SizeMap = Map; 49 | 50 | export type FileContentsMap = Map; 51 | export type FileModifier = ((contents: string) => string) | null; 52 | 53 | export interface Context { 54 | projectPath: string; 55 | packagePath: string; 56 | packageContent: string | null; 57 | silent: boolean; 58 | originalPaths: OriginalPath; 59 | // Stores the result of compression 60 | compressed: SizeMap; 61 | // Stores the basis of comparison. 62 | comparison: SizeMap; 63 | // Allows the API to specify a method that alters content before analysis. 64 | fileModifier: FileModifier; 65 | // Stores the contents of files, to avoid reading from disk per compression type. 66 | fileContents: FileContentsMap; 67 | } 68 | -------------------------------------------------------------------------------- /src/validation/Config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { readFile } from '../helpers/fs'; 18 | import { Context, ConditionFunction, ValidationResponse } from './Condition'; 19 | import { Track } from './Track'; 20 | import ValidateFileConfig from './File'; 21 | import { isObject } from '../helpers/object'; 22 | 23 | /** 24 | * Parse the content of the 'filesize' items. 25 | * @param context 26 | * @param overallContext 27 | */ 28 | async function parseEnforcedKeys(context: Context, enforcedKeys: any): Promise { 29 | for (const [path, item] of Object.entries(enforcedKeys)) { 30 | if (!isObject(item)) { 31 | continue; 32 | } 33 | 34 | const error = await ValidateFileConfig(path, item as any, context); 35 | if (error) { 36 | return error; 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | 43 | const CONDITIONS = [ 44 | /** 45 | * Read the contents of package.json. 46 | * @param context 47 | */ 48 | async function readPackage(context: Context): Promise { 49 | const packageContent = await readFile(context.packagePath); 50 | if (packageContent === null) { 51 | return `Could not read the configuration in '${context.packagePath}'`; 52 | } 53 | 54 | context.packageContent = packageContent; 55 | return null; 56 | }, 57 | /** 58 | * Parse the content of package.json, ensure data is usable. 59 | * @param context 60 | */ 61 | async function parsePackage(context: Context): Promise { 62 | try { 63 | const { filesize: json } = JSON.parse(context.packageContent as string); 64 | 65 | if (typeof json === 'undefined') { 66 | return `There is no 'filesize' configuration in '${context.packagePath}'`; 67 | } 68 | 69 | const { track, trackFormat, ...keys } = json; 70 | if (Object.entries(keys).length === 0 && track === undefined) { 71 | return `There is no data inside the 'filesize' configuration in '${context.packagePath}'`; 72 | } 73 | 74 | if (!isObject(keys) && track === undefined) { 75 | return ( 76 | `'filesize' configuration is not an object in '${context.packagePath}'` + 77 | '(See https://github.com/ampproject/filesize/#usage for details on the structure of the filesize object).' 78 | ); 79 | } 80 | 81 | // Since we have tracked entries, we need to store them 82 | const parseTrackingResult = await Track(context, json); 83 | if (parseTrackingResult !== null) { 84 | return parseTrackingResult; 85 | } 86 | 87 | const parseKeysResults = await parseEnforcedKeys(context, keys); 88 | if (parseKeysResults !== null) { 89 | return parseKeysResults; 90 | } 91 | } catch (e) { 92 | return `Could not parse '${context.packagePath}'`; 93 | } 94 | return null; 95 | }, 96 | ]; 97 | 98 | const Config: ConditionFunction = (context: Context) => 99 | async function () { 100 | for await (const condition of CONDITIONS) { 101 | const conditionResult = await condition(context); 102 | if (conditionResult !== null) { 103 | return conditionResult; 104 | } 105 | } 106 | 107 | return null; 108 | }; 109 | 110 | export default Config; 111 | -------------------------------------------------------------------------------- /src/validation/File.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Compression, Context, SizeMapValue, SizeMapValueIndex } from './Condition'; 18 | import { resolve, isAbsolute } from 'path'; 19 | import { isFile } from '../helpers/fs'; 20 | import bytes from '@kristoferbaxter/bytes'; 21 | 22 | /** 23 | * Format input string to a known Compression Enum Value. 24 | * @param fsValue 25 | */ 26 | export function validateCompressionName(fsValue: string): Compression | null { 27 | const lowerCaseValue = fsValue.toLowerCase(); 28 | switch (fsValue.toLowerCase()) { 29 | case 'brotli': 30 | case 'gzip': 31 | case 'none': 32 | return lowerCaseValue as Compression; 33 | case '': 34 | return 'none'; 35 | default: 36 | return null; 37 | } 38 | } 39 | 40 | export default async function validateFileConfig( 41 | originalPath: string, 42 | compressionConfig: { [key: string]: string }, 43 | context: Context, 44 | ): Promise { 45 | const entries = Object.entries(compressionConfig); 46 | if (entries.length === 0) { 47 | return `Configuration for '${originalPath}' is invalid. (compression values unspecified)`; 48 | } 49 | 50 | let path: string; 51 | if (isAbsolute(originalPath)) { 52 | path = resolve(originalPath); 53 | } else { 54 | path = resolve(context.projectPath, originalPath); 55 | } 56 | if (!(await isFile(path))) { 57 | return `Configuration for '${originalPath}' is invalid. (path is not a valid file)`; 58 | } 59 | 60 | for (const [configKey, configValue] of entries) { 61 | const compression: Compression | null = validateCompressionName(configKey); 62 | if (compression === null) { 63 | return `Configuration for '${originalPath}' is invalid. (Invalid compression value '${configKey}')`; 64 | } 65 | 66 | const maxSize = bytes(configValue); 67 | if (maxSize === null || maxSize < 0) { 68 | return `Configuration for '${originalPath}' is invalid. (size unspecified)`; 69 | } 70 | 71 | let compressedItem: SizeMapValue | undefined = context.compressed.get(path); 72 | if (!compressedItem) { 73 | compressedItem = [ 74 | [undefined, undefined], 75 | [undefined, undefined], 76 | [undefined, undefined], 77 | ]; 78 | } 79 | compressedItem[SizeMapValueIndex[compression]] = [null, maxSize]; 80 | context.compressed.set(path, compressedItem); 81 | context.originalPaths.set(path, originalPath); 82 | } 83 | return null; 84 | } 85 | -------------------------------------------------------------------------------- /src/validation/Project.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ConditionFunction, Context } from './Condition'; 18 | import { isDirectory, isFile } from '../helpers/fs'; 19 | import { resolve, dirname } from 'path'; 20 | import { MakeError } from '../log/helpers/error'; 21 | 22 | /** 23 | * Ensure context contains a valid project directory and `package.json` inside. 24 | * @param context 25 | */ 26 | const Project: ConditionFunction = (context: Context) => { 27 | return async function () { 28 | if (context.packagePath !== '') { 29 | // The package path was specified up front, its likely not a package.json 30 | if (!(await isFile(context.packagePath))) { 31 | return MakeError(`config specified '${context.packagePath}' doesn't exist, is this a valid project?`); 32 | } 33 | context.projectPath = dirname(context.packagePath); 34 | return null; 35 | } 36 | 37 | const projectPath: string = resolve(context.projectPath); 38 | if (!(await isDirectory(projectPath))) { 39 | return MakeError(`project specified '${context.projectPath}' doesn't exist, is this a valid project?`); 40 | } 41 | 42 | const packagePath = resolve(context.projectPath, 'package.json'); 43 | if (!(await isFile(packagePath))) { 44 | return MakeError(`Missing '${packagePath}', is this a valid project?`); 45 | } 46 | 47 | context.projectPath = projectPath; 48 | context.packagePath = packagePath; 49 | return null; 50 | }; 51 | }; 52 | 53 | export default Project; 54 | -------------------------------------------------------------------------------- /src/validation/Track.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { resolve } from 'path'; 18 | import glob from 'fast-glob'; 19 | import { Context, ValidationResponse, SizeMapValue } from './Condition'; 20 | import { validateCompressionName } from './File'; 21 | 22 | /** 23 | * Given formats, format a SizeMapValue. 24 | * @param {Array} formats 25 | * @return {SizeMapValue} 26 | */ 27 | function getTrackedFormats(formats: Array): SizeMapValue { 28 | return [ 29 | [formats.includes('brotli') ? null : undefined, undefined], 30 | [formats.includes('gzip') ? null : undefined, undefined], 31 | [formats.includes('none') ? null : undefined, undefined], 32 | ]; 33 | } 34 | 35 | /** 36 | * Use 'fast-glob' to find files requested to track from configuration. 37 | * @param {Context} context 38 | */ 39 | export async function Track(context: Context, json: any): Promise { 40 | if ('track' in json && Array.isArray(json.track)) { 41 | const entries: Array = await glob(json.track); 42 | let formats = ['brotli', 'gzip', 'none']; 43 | 44 | if ('trackFormat' in json && Array.isArray(json.trackFormat)) { 45 | // `trackFormats` allows you to limit the compression types for tracking 46 | formats = json.trackFormat.map((format: any) => validateCompressionName(String(format))).filter(Boolean); 47 | } 48 | 49 | // glob ensures the results are valid files. 50 | for (const entry of entries) { 51 | const path = resolve(entry); 52 | 53 | context.compressed.set(path, getTrackedFormats(formats)); 54 | context.originalPaths.set(path, entry); 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | -------------------------------------------------------------------------------- /test/config-validation/config-validation.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import Config from '../../src/validation/Config'; 19 | import { Context } from '../../src/validation/Condition'; 20 | 21 | tap.test('missing package.json should fail', async (t) => { 22 | const context: Context = { 23 | packagePath: 'test/config-validation/fixtures/missing-package-json/package.json', 24 | projectPath: 'test/config-validation/fixtures/missing-package-json', 25 | packageContent: '', 26 | originalPaths: new Map(), 27 | compressed: new Map(), 28 | comparison: new Map(), 29 | silent: false, 30 | fileModifier: null, 31 | fileContents: new Map(), 32 | }; 33 | const message = await Config(context)(); 34 | 35 | t.is(message, `Could not read the configuration in '${context.packagePath}'`); 36 | }); 37 | 38 | tap.test('unparseable package.json should fail', async (t) => { 39 | const context: Context = { 40 | packagePath: 'test/config-validation/fixtures/unparseable-package-json/package.json', 41 | projectPath: 'test/config-validation/fixtures/unparseable-package-json', 42 | packageContent: '', 43 | originalPaths: new Map(), 44 | compressed: new Map(), 45 | comparison: new Map(), 46 | silent: false, 47 | fileModifier: null, 48 | fileContents: new Map(), 49 | }; 50 | const message = await Config(context)(); 51 | 52 | t.is(message, `Could not parse '${context.packagePath}'`); 53 | }); 54 | 55 | tap.test("missing 'filesize' key from package.json should fail", async (t) => { 56 | const context: Context = { 57 | packagePath: 'test/config-validation/fixtures/missing-configuration/package.json', 58 | projectPath: 'test/config-validation/fixtures/missing-configuration', 59 | packageContent: '', 60 | originalPaths: new Map(), 61 | compressed: new Map(), 62 | comparison: new Map(), 63 | silent: false, 64 | fileModifier: null, 65 | fileContents: new Map(), 66 | }; 67 | const message = await Config(context)(); 68 | 69 | t.is(message, `There is no 'filesize' configuration in '${context.packagePath}'`); 70 | }); 71 | 72 | tap.test("missing path from item in 'filesize' should fail", async (t) => { 73 | const context: Context = { 74 | packagePath: 'test/config-validation/fixtures/item-path-missing/package.json', 75 | projectPath: 'test/config-validation/fixtures/item-path-missing', 76 | packageContent: '', 77 | originalPaths: new Map(), 78 | compressed: new Map(), 79 | comparison: new Map(), 80 | silent: false, 81 | fileModifier: null, 82 | fileContents: new Map(), 83 | }; 84 | const message = await Config(context)(); 85 | 86 | t.is(message, `There is no data inside the 'filesize' configuration in '${context.packagePath}'`); 87 | }); 88 | 89 | tap.test("missing maxSize from item in 'filesize' should fail", async (t) => { 90 | const context: Context = { 91 | packagePath: 'test/config-validation/fixtures/max-size-missing/package.json', 92 | projectPath: 'test/config-validation/fixtures/max-size-missing', 93 | packageContent: '', 94 | originalPaths: new Map(), 95 | compressed: new Map(), 96 | comparison: new Map(), 97 | silent: false, 98 | fileModifier: null, 99 | fileContents: new Map(), 100 | }; 101 | const message = await Config(context)(); 102 | 103 | t.is(message, "Configuration for 'index.js' is invalid. (size unspecified)"); 104 | }); 105 | 106 | tap.test("missing compression from item in 'filesize' should fail", async (t) => { 107 | const context: Context = { 108 | packagePath: 'test/config-validation/fixtures/compression-missing/package.json', 109 | projectPath: 'test/config-validation/fixtures/compression-missing', 110 | packageContent: '', 111 | originalPaths: new Map(), 112 | compressed: new Map(), 113 | comparison: new Map(), 114 | silent: false, 115 | fileModifier: null, 116 | fileContents: new Map(), 117 | }; 118 | const message = await Config(context)(); 119 | 120 | t.is(message, "Configuration for 'index.js' is invalid. (compression values unspecified)"); 121 | }); 122 | 123 | tap.test('standalone configuration file when valid should pass', async (t) => { 124 | const context: Context = { 125 | packagePath: 'test/config-validation/fixtures/standalone-config/filesize.json', 126 | projectPath: '', 127 | packageContent: '', 128 | originalPaths: new Map(), 129 | compressed: new Map(), 130 | comparison: new Map(), 131 | silent: false, 132 | fileModifier: null, 133 | fileContents: new Map(), 134 | }; 135 | const message = await Config(context)(); 136 | 137 | t.is(message, null); 138 | }); 139 | 140 | tap.test('standalone configuration file when path is invalid should fail', async (t) => { 141 | const context: Context = { 142 | packagePath: 'test/config-validation/fixtures/standalone-config/invalid.json', 143 | projectPath: '', 144 | packageContent: '', 145 | originalPaths: new Map(), 146 | compressed: new Map(), 147 | comparison: new Map(), 148 | silent: false, 149 | fileModifier: null, 150 | fileContents: new Map(), 151 | }; 152 | const message = await Config(context)(); 153 | 154 | t.is(message, `Could not read the configuration in '${context.packagePath}'`); 155 | }); 156 | -------------------------------------------------------------------------------- /test/config-validation/fixtures/compression-missing/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/test/config-validation/fixtures/compression-missing/index.js -------------------------------------------------------------------------------- /test/config-validation/fixtures/compression-missing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": {} 4 | } 5 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/item-path-missing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": {} 3 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/max-size-missing/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/test/config-validation/fixtures/max-size-missing/index.js -------------------------------------------------------------------------------- /test/config-validation/fixtures/max-size-missing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": { 4 | "brotli": null 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/missing-configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nofilesizekey" 3 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/missing-package-json/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /test/config-validation/fixtures/standalone-config/filesize.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "track": ["test/config-validation/fixtures/standalone-config/**/*.js"] 4 | } 5 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/standalone-config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /test/config-validation/fixtures/track-standalone-format/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/test/config-validation/fixtures/track-standalone-format/index.js -------------------------------------------------------------------------------- /test/config-validation/fixtures/track-standalone-format/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "track": ["test/config-validation/fixtures/track-standalone-format/**/*.js"], 4 | "trackFormat": ["brotli"] 5 | } 6 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/track-standalone/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/test/config-validation/fixtures/track-standalone/index.js -------------------------------------------------------------------------------- /test/config-validation/fixtures/track-standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "track": ["test/config-validation/fixtures/track-standalone/**/*.js"] 4 | } 5 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/track/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ampproject/filesize/868a74e43ce058caac0a08595664d4db99e14c48/test/config-validation/fixtures/track/index.js -------------------------------------------------------------------------------- /test/config-validation/fixtures/track/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "track": ["test/config-validation/fixtures/track/*"], 4 | "index.js": { 5 | "brotli": "5 kB", 6 | "gzip": "8 kB", 7 | "none": "20 kB" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/config-validation/fixtures/unparseable-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | // Intentionally broken package.json to test error case. -------------------------------------------------------------------------------- /test/config-validation/track.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { resolve } from 'path'; 19 | import { report } from '../../src/api'; 20 | import Config from '../../src/validation/Config'; 21 | import { Context, SizeMapValue, SizeMap } from '../../src/validation/Condition'; 22 | 23 | tap.test('including trackable items should succeed', async (t) => { 24 | const context: Context = { 25 | packagePath: 'test/config-validation/fixtures/track/package.json', 26 | projectPath: 'test/config-validation/fixtures/track', 27 | packageContent: '', 28 | originalPaths: new Map(), 29 | compressed: new Map(), 30 | comparison: new Map(), 31 | silent: false, 32 | fileModifier: null, 33 | fileContents: new Map(), 34 | }; 35 | const message = await Config(context)(); 36 | 37 | t.is(message, null); 38 | }); 39 | 40 | tap.test('trackable items uses glob to find files', async (t) => { 41 | const sizes: SizeMapValue = [ 42 | [null, undefined], // brotli 43 | [null, undefined], // gzip 44 | [null, undefined], // none 45 | ]; 46 | const expected: SizeMap = new Map(); 47 | expected.set(resolve('test/config-validation/fixtures/track-standalone/index.js'), sizes); 48 | 49 | const results = await report('test/config-validation/fixtures/track-standalone', null); 50 | t.deepEqual(results, expected); 51 | }); 52 | 53 | tap.test('trackable items uses trackFormats to restrict compression types', async (t) => { 54 | const sizes: SizeMapValue = [ 55 | [null, undefined], // brotli 56 | [undefined, undefined], // gzip 57 | [undefined, undefined], // none 58 | ]; 59 | const expected: SizeMap = new Map(); 60 | expected.set(resolve('test/config-validation/fixtures/track-standalone-format/index.js'), sizes); 61 | 62 | const results = await report('test/config-validation/fixtures/track-standalone-format', null); 63 | t.deepEqual(results, expected); 64 | }); 65 | -------------------------------------------------------------------------------- /test/end-to-end/fixtures/api-report/filesize.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "track": ["test/end-to-end/fixtures/api-report/**/*.js"], 4 | "trackFormat": ["brotli"] 5 | } 6 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/api-report/inferno.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"===typeof exports&&"undefined"!==typeof module?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):n((e=e||self).Inferno=e.Inferno||{})}(this,(function(e){"use strict";var n=Array.isArray;function t(e){var n=typeof e;return"string"===n||"number"===n}function r(e){return void 0===e||null===e}function o(e){return null===e||!1===e||!0===e||void 0===e}function i(e){return"function"===typeof e}function l(e){return"string"===typeof e}function a(e){return null===e}function u(e,n){var t={};if(e)for(var r in e)t[r]=e[r];if(n)for(var o in n)t[o]=n[o];return t}function c(e){return!a(e)&&"object"===typeof e}var f={};function s(e){return e.substr(2).toLowerCase()}function d(e,n){e.appendChild(n)}function p(e,n,t){a(t)?d(e,n):e.insertBefore(n,t)}function v(e,n){e.removeChild(n)}function h(e){for(var n=0;n0,v=a(d),h=l(d)&&"$"===d[0];p||v||h?(i=i||r.slice(0,f),(p||h)&&(s=I(s)),(v||h)&&(s.key="$"+f),i.push(s)):i&&i.push(s),s.flags|=65536}}u=0===(i=i||r).length?1:8}else(i=r).flags|=65536,81920&r.flags&&(i=I(r)),u=2;return e.children=i,e.childFlags=u,e}function D(e){if(o(e)||t(e))return S(e,null);if(n(e))return U(e,0,null);return 16384&e.flags?I(e):e}var E="http://www.w3.org/1999/xlink",_="http://www.w3.org/XML/1998/namespace",A={"xlink:actuate":E,"xlink:arcrole":E,"xlink:href":E,"xlink:role":E,"xlink:show":E,"xlink:title":E,"xlink:type":E,"xml:base":_,"xml:lang":_,"xml:space":_};function T(e){return{onClick:e,onDblClick:e,onFocusIn:e,onFocusOut:e,onKeyDown:e,onKeyPress:e,onKeyUp:e,onMouseDown:e,onMouseMove:e,onMouseUp:e,onTouchEnd:e,onTouchMove:e,onTouchStart:e}}var R=T(0),W=T(null),O=T(!0);function j(e,n){var t=n.$EV;return t||(t=n.$EV=T(null)),t[e]||1===++R[e]&&(W[e]=function(e){var n="onClick"===e||"onDblClick"===e?function(e){return function(n){if(0!==n.button)return void n.stopPropagation();Q(n,!0,e,q(n))}}(e):function(e){return function(n){Q(n,!1,e,q(n))}}(e);return document.addEventListener(s(e),n),n}(e)),t}function H(e,n){var t=n.$EV;t&&t[e]&&(0===--R[e]&&(document.removeEventListener(s(e),W[e]),W[e]=null),t[e]=null)}function Q(e,n,t,r){var o=function(e){return i(e.composedPath)?e.composedPath()[0]:e.target}(e);do{if(n&&o.disabled)return;var l=o.$EV;if(l){var u=l[t];if(u&&(r.dom=o,u.event?u.event(u.data,e):u(e),e.cancelBubble))return}o=o.parentNode}while(!a(o))}function X(){this.cancelBubble=!0,this.immediatePropagationStopped||this.stopImmediatePropagation()}function G(){return this.defaultPrevented}function K(){return this.cancelBubble}function q(e){var n={dom:document};return e.isDefaultPrevented=G,e.isPropagationStopped=K,e.stopPropagation=X,Object.defineProperty(e,"currentTarget",{configurable:!0,get:function(){return n.dom}}),n}function z(e,n,t){if(e[n]){var r=e[n];r.event?r.event(r.data,t):r(t)}else{var o=n.toLowerCase();e[o]&&e[o](t)}}function J(e,n){var t=function(t){var r=this.$V;if(!r)return;var o=r.props||f,a=r.dom;if(l(e))z(o,e,t);else for(var u=0;u-1&&t.options[a]&&(u=t.options[a].value),o&&r(u)&&(u=e.defaultValue),function e(t,o){if("option"===t.type)!function(e,t){var o=e.props||f,i=e.dom;i.value=o.value,o.value===t||n(t)&&-1!==t.indexOf(o.value)?i.selected=!0:r(t)&&r(o.selected)||(i.selected=o.selected||!1)}(t,o);else{var i=t.children,l=t.flags;if(4&l)e(i.$LI,o);else if(8&l)e(i,o);else if(2===t.childFlags)e(i,o);else if(12&t.childFlags)for(var a=0,u=i.length;a0;for(var a in l&&(i=de(t))&&function(e,n,t){64&e?function(e,n){Z(n.type)?(Y(e,"change",ne),Y(e,"click",te)):Y(e,"input",ee)}(n,t):256&e?function(e){Y(e,"change",oe)}(n):128&e&&function(e,n){Y(e,"input",ue),n.onChange&&Y(e,"change",ce)}(n,t)}(n,r,t),t)ke(a,null,t[a],r,o,i,null);l&&se(n,e,r,t,!0,i)}function Ce(e,n,t){var r=D(e.render(n,e.state,t)),o=t;return i(e.getChildContext)&&(o=u(t,e.getChildContext())),e.$CX=o,r}function we(e,n,t,r,o,l){var u=new n(t,r),c=u.$N=Boolean(n.getDerivedStateFromProps||u.getSnapshotBeforeUpdate);if(u.$SVG=o,u.$L=l,e.children=u,u.$BS=!1,u.context=r,u.props===f&&(u.props=t),c)u.state=k(u,t,u.state);else if(i(u.componentWillMount)){u.$BR=!0,u.componentWillMount();var s=u.$PS;if(!a(s)){var d=u.state;if(a(d))u.state=s;else for(var p in s)d[p]=s[p];u.$PS=null}u.$BR=!1}return u.$LI=Ce(u,t,r),u}function Pe(e,n,t,r,o,i){var l=e.flags|=16384;481&l?Ne(e,n,t,r,o,i):4&l?function(e,n,t,r,o,i){var l=we(e,e.type,e.props||f,t,r,i);Pe(l.$LI,n,l.$CX,r,o,i),Ve(e.ref,l,i)}(e,n,t,r,o,i):8&l?(function(e,n,t,r,o,i){Pe(e.children=D(function(e,n){return 32768&e.flags?e.type.render(e.props||f,e.ref,n):e.type(e.props||f,n)}(e,t)),n,t,r,o,i)}(e,n,t,r,o,i),Se(e,i)):512&l||16&l?Fe(e,n,o):8192&l?function(e,n,t,r,o,i){var l=e.children,a=e.childFlags;12&a&&0===l.length&&(a=e.childFlags=2,l=e.children=L()),2===a?Pe(l,t,o,r,o,i):xe(l,t,n,r,o,i)}(e,t,n,r,o,i):1024&l&&function(e,n,t,r,o){Pe(e.children,e.ref,n,!1,null,o);var i=L();Fe(i,t,r),e.dom=i.dom}(e,t,n,o,i)}function Fe(e,n,t){var r=e.dom=document.createTextNode(e.children);a(n)||p(n,r,t)}function Ne(e,n,t,o,i,l){var u=e.flags,c=e.props,f=e.className,s=e.children,d=e.childFlags,v=e.dom=function(e,n){if(n)return document.createElementNS("http://www.w3.org/2000/svg",e);return document.createElement(e)}(e.type,o=o||(32&u)>0);if(r(f)||""===f||(o?v.setAttribute("class",f):v.className=f),16===d)w(v,s);else if(1!==d){var h=o&&"foreignObject"!==e.type;2===d?(16384&s.flags&&(e.children=s=I(s)),Pe(s,v,t,h,null,l)):8!==d&&4!==d||xe(s,v,t,h,null,l)}a(n)||p(n,v,i),a(c)||be(e,u,c,v,o),ve(e.ref,v,l)}function xe(e,n,t,r,o,i){for(var l=0;l0,c!==s){var v=c||f;if((a=s||f)!==f)for(var h in(d=(448&i)>0)&&(p=de(a)),a){var g=v[h],m=a[h];g!==m&&ke(h,g,m,u,o,p,e)}if(v!==f)for(var y in v)r(a[y])&&!r(v[y])&&ke(y,v[y],null,u,o,p,e)}var $=n.children,k=n.className;e.className!==k&&(r(k)?u.removeAttribute("class"):o?u.setAttribute("class",k):u.className=k),4096&i?function(e,n){e.textContent!==n&&(e.textContent=n)}(u,$):Ie(e.childFlags,n.childFlags,e.children,$,u,t,o&&"foreignObject"!==n.type,null,e,l),d&&se(i,n,u,a,!1,p);var b=n.ref,C=e.ref;C!==b&&(pe(C),ve(b,u,l))}(e,n,l,c,h,p):4&h?function(e,n,t,r,o,l,c){var s=n.children=e.children;if(a(s))return;s.$L=c;var d=n.props||f,p=n.ref,v=e.ref,h=s.state;if(!s.$N){if(i(s.componentWillReceiveProps)){if(s.$BR=!0,s.componentWillReceiveProps(d,r),s.$UN)return;s.$BR=!1}a(s.$PS)||(h=u(h,s.$PS),s.$PS=null)}Le(s,h,d,t,r,o,!1,l,c),v!==p&&(pe(v),ve(p,s,c))}(e,n,t,l,c,s,p):8&h?function(e,n,t,o,l,a,u){var c=!0,s=n.props||f,d=n.ref,p=e.props,v=!r(d),h=e.children;if(v&&i(d.onComponentShouldUpdate)&&(c=d.onComponentShouldUpdate(p,s)),!1!==c){v&&i(d.onComponentWillUpdate)&&d.onComponentWillUpdate(p,s);var g=n.type,m=D(32768&n.flags?g.render(s,d,o):g(s,o));Ue(h,m,t,o,l,a,u),n.children=m,v&&i(d.onComponentDidUpdate)&&d.onComponentDidUpdate(p,s)}else n.children=h}(e,n,t,l,c,s,p):16&h?function(e,n){var t=n.children,r=n.dom=e.dom;t!==e.children&&(r.nodeValue=t)}(e,n):512&h?n.dom=e.dom:8192&h?function(e,n,t,r,o,i){var l=e.children,a=n.children,u=e.childFlags,c=n.childFlags,f=null;12&c&&0===a.length&&(c=n.childFlags=2,a=n.children=L());var s=0!==(2&c);if(12&u){var d=l.length;(8&u&&8&c||s||!s&&a.length>d)&&(f=m(l[d-1],!1).nextSibling)}Ie(u,c,l,a,t,r,o,f,e,i)}(e,n,t,l,c,p):function(e,n,t,r){var i=e.ref,l=n.ref,a=n.children;if(Ie(e.childFlags,n.childFlags,e.children,a,i,t,!1,null,e,r),n.dom=e.dom,i!==l&&!o(a)){var u=a.dom;v(i,u),d(l,u)}}(e,n,l,p)}function Ie(e,n,t,r,o,i,l,a,u,c){switch(e){case 2:switch(n){case 2:Ue(t,r,o,i,l,a,c);break;case 1:he(t,o);break;case 16:ge(t),w(o,r);break;default:!function(e,n,t,r,o,i){ge(e),xe(n,t,r,o,m(e,!0),i),y(e,t)}(t,r,o,i,l,c)}break;case 1:switch(n){case 2:Pe(r,o,i,l,a,c);break;case 1:break;case 16:w(o,r);break;default:xe(r,o,i,l,a,c)}break;case 16:switch(n){case 16:!function(e,n,t){e!==n&&(""!==e?t.firstChild.nodeValue=n:w(t,n))}(t,r,o);break;case 2:ye(o),Pe(r,o,i,l,a,c);break;case 1:ye(o);break;default:ye(o),xe(r,o,i,l,a,c)}break;default:switch(n){case 16:me(t),w(o,r);break;case 2:$e(o,u,t),Pe(r,o,i,l,a,c);break;case 1:$e(o,u,t);break;default:var f=0|t.length,s=0|r.length;0===f?s>0&&xe(r,o,i,l,a,c):0===s?$e(o,u,t):8===n&&8===e?function(e,n,t,r,o,i,l,a,u,c){var f,s,d=i-1,p=l-1,v=0,h=e[v],g=n[v];e:{for(;h.key===g.key;){if(16384&g.flags&&(n[v]=g=I(g)),Ue(h,g,t,r,o,a,c),e[v]=g,++v>d||v>p)break e;h=e[v],g=n[v]}for(h=e[d],g=n[p];h.key===g.key;){if(16384&g.flags&&(n[p]=g=I(g)),Ue(h,g,t,r,o,a,c),e[d]=g,p--,v>--d||v>p)break e;h=e[d],g=n[p]}}if(v>d){if(v<=p)for(s=(f=p+1)p)for(;v<=d;)he(e[v++],t);else!function(e,n,t,r,o,i,l,a,u,c,f,s,d){var p,v,h,g=0,y=a,k=a,b=i-a+1,C=l-a+1,w=new Int32Array(C+1),P=b===r,F=!1,N=0,x=0;if(o<4||(b|C)<32)for(g=y;g<=i;++g)if(p=e[g],xa?F=!0:N=a,16384&v.flags&&(n[a]=v=I(v)),Ue(p,v,u,t,c,f,d),++x;break}!P&&a>l&&he(p,u)}else P||he(p,u);else{var V={};for(g=k;g<=l;++g)V[n[g].key]=g;for(g=y;g<=i;++g)if(p=e[g],xy;)he(e[y++],u);w[a-k]=g+1,N>a?F=!0:N=a,16384&(v=n[a]).flags&&(n[a]=v=I(v)),Ue(p,v,u,t,c,f,d),++x}else P||he(p,u);else P||he(p,u)}if(P)$e(u,s,e),xe(n,u,t,c,f,d);else if(F){var S=function(e){var n=0,t=0,r=0,o=0,i=0,l=0,a=0,u=e.length;for(u>Me&&(Me=u,le=new Int32Array(u),ae=new Int32Array(u));t>1]]0&&(ae[t]=le[i-1]),le[i]=t)}i=o+1;var c=new Int32Array(i);for(l=le[i-1];i-- >0;)c[i]=l,l=ae[l],le[i]=0;return c}(w);for(a=S.length-1,g=C-1;g>=0;g--)0===w[g]?(16384&(v=n[N=g+k]).flags&&(n[N]=v=I(v)),Pe(v,u,t,c,(h=N+1)=0;g--)0===w[g]&&(16384&(v=n[N=g+k]).flags&&(n[N]=v=I(v)),Pe(v,u,t,c,(h=N+1)l?l:i,d=0;dl)for(d=s;d3)for(u=[u],t=3;t2&&(l.children=c.slice.call(arguments,2)),h(n.type,l,l.key||n.key,l.ref||n.ref,null)},exports.createContext=function(n){var l={},u={__c:"__cC"+f++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var t,i=this;return this.getChildContext||(t=[],this.getChildContext=function(){return l[u.__c]=i,l},this.shouldComponentUpdate=function(n){i.props.value!==n.value&&t.some(function(l){l.context=n.value,w(l)})},this.sub=function(n){t.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){t.splice(t.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Consumer.contextType=u,u},exports.toChildArray=_,exports._e=j,exports.options=n; 2 | //# sourceMappingURL=preact.js.map -------------------------------------------------------------------------------- /test/end-to-end/fixtures/item-too-large/filesize.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "dist/index.js": { 4 | "brotli": "15 kB", 5 | "gzip": "20 kB", 6 | "none": "500 kB" 7 | }, 8 | "dist/foo.js": { 9 | "brotli": "15 kB", 10 | "gzip": "20 kB", 11 | "none": "500 kB" 12 | }, 13 | "dist/bar.js": { 14 | "brotli": "15 kB", 15 | "gzip": "20 kB", 16 | "none": "500 kB" 17 | }, 18 | "dist/baz.js": { 19 | "brotli": "15 kB", 20 | "gzip": "20 kB", 21 | "none": "500 kB" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/item-too-large/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "dist/index.js": { 4 | "brotli": "15 kB", 5 | "gzip": "20 kB", 6 | "none": "500 kB" 7 | }, 8 | "dist/foo.js": { 9 | "brotli": "15 kB", 10 | "gzip": "20 kB", 11 | "none": "500 kB" 12 | }, 13 | "dist/bar.js": { 14 | "brotli": "15 kB", 15 | "gzip": "20 kB", 16 | "none": "500 kB" 17 | }, 18 | "dist/baz.js": { 19 | "brotli": "15 kB", 20 | "gzip": "20 kB", 21 | "none": "500 kB" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/missing-item/filesize.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": { 4 | "none": "500 kB" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/missing-item/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": { 4 | "none": "500 kB" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/successful/filesize.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": { 4 | "brotli": "3.5 kB", 5 | "gzip": "4 kB", 6 | "none": "10 kB" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/successful/index.js: -------------------------------------------------------------------------------- 1 | !function(n,l){"object"==typeof exports&&"undefined"!=typeof module?l(exports):"function"==typeof define&&define.amd?define(["exports"],l):l(n.preact={})}(this,function(n){var l,u,t,i,o,f,r,e={},c=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord/i;function a(n,l){for(var u in l)n[u]=l[u];return n}function h(n){var l=n.parentNode;l&&l.removeChild(n)}function p(n,l,u){var t,i=arguments,o={};for(t in l)"key"!==t&&"ref"!==t&&(o[t]=l[t]);if(arguments.length>3)for(u=[u],t=3;t2&&(l.children=c.slice.call(arguments,2)),v(n.type,l,l.key||n.key,l.ref||n.ref)},n.createContext=function(n){var l={},u={__c:"__cC"+r++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var t,i=this;return this.getChildContext||(t=[],this.getChildContext=function(){return l[u.__c]=i,l},this.shouldComponentUpdate=function(l){n.value!==l.value&&t.some(function(n){n.context=l.value,g(n)})},this.sub=function(n){t.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){t.splice(t.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Consumer.contextType=u,u},n.toChildArray=b,n._e=A,n.options=l}); 2 | //# sourceMappingURL=preact.umd.js.map -------------------------------------------------------------------------------- /test/end-to-end/fixtures/successful/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": { 3 | "index.js": { 4 | "brotli": "3.5 kB", 5 | "gzip": "4 kB", 6 | "none": "10 kB" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test/end-to-end/fixtures/successful/replaced.js: -------------------------------------------------------------------------------- 1 | !function(n,l){"object"==typeof exports&&"undefined"!=typeof module?l(exports):"function"==typeof define&&define.amd?define(["exports"],l):l(n.preact={})}(this,function(n){var l,u,t,i,o,f,r,e={},c=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord/i;function a(n,l){for(var u in l)n[u]=l[u];return n}function h(n){var l=n.parentNode;l&&l.removeChild(n)}function p(n,l,u){var t,i=arguments,o={};for(t in l)"key"!==t&&"ref"!==t&&(o[t]=l[t]);if(arguments.length>3)for(u=[u],t=3;t2&&(l.children=c.slice.call(arguments,2)),v(n.type,l,l.key||n.key,l.ref||n.ref)},n.createContext=function(n){var l={},u={__c:"__cC"+r++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var t,i=this;return this.getChildContext||(t=[],this.getChildContext=function(){return l[u.__c]=i,l},this.shouldComponentUpdate=function(l){n.value!==l.value&&t.some(function(n){n.context=l.value,g(n)})},this.sub=function(n){t.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){t.splice(t.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Consumer.contextType=u,u},n.toChildArray=b,n._e=A,n.options=l}); 2 | //# sourceMappingURL=FOO.map -------------------------------------------------------------------------------- /test/end-to-end/large-config.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { exec } from 'child_process'; 19 | 20 | tap.test('too large valid item fails with exit code 6, using configuration file', (t) => { 21 | const executeFailure = exec('./dist/filesize -c=test/end-to-end/fixtures/item-too-large/filesize.json'); 22 | 23 | executeFailure.on('exit', (code) => { 24 | t.is(code, 6); 25 | t.end(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/end-to-end/large-project.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { exec } from 'child_process'; 19 | 20 | tap.test('too large valid item fails with exit code 6', (t) => { 21 | const executeFailure = exec('./dist/filesize -p=test/end-to-end/fixtures/item-too-large'); 22 | 23 | executeFailure.on('exit', (code) => { 24 | t.is(code, 6); 25 | t.end(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/end-to-end/successful-config.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { exec } from 'child_process'; 19 | import { report, Report } from '../../src/api'; 20 | import { SizeMapValue, SizeMap } from '../../src/validation/Condition'; 21 | import { resolve, relative } from 'path'; 22 | 23 | tap.test('standalone configuration file when valid should pass', (t) => { 24 | const executeSuccess = exec('./dist/filesize -c=test/end-to-end/fixtures/successful/filesize.json'); 25 | 26 | executeSuccess.on('exit', (code) => { 27 | t.is(code, 0); 28 | t.end(); 29 | }); 30 | }); 31 | 32 | tap.test('item under requested filesize limit passes from API, using configuration file', async (t) => { 33 | const sizes: SizeMapValue = [ 34 | [3410, 3584], // brotli 35 | [3737, 4096], // gzip 36 | [9327, 10240], // none 37 | ]; 38 | const expected: SizeMap = new Map(); 39 | expected.set(resolve('test/end-to-end/fixtures/successful/index.js'), sizes); 40 | 41 | const results = await report('test/end-to-end/fixtures/successful/filesize.json', null); 42 | t.deepEqual(results, expected); 43 | }); 44 | 45 | tap.test( 46 | 'item under requested filesize limit passes from API, using configuration file, with replacement', 47 | async (t) => { 48 | const sizes: SizeMapValue = [ 49 | [3401, 3584], // brotli 50 | [3731, 4096], // gzip 51 | [9317, 10240], // none 52 | ]; 53 | const expected: SizeMap = new Map(); 54 | expected.set(resolve('test/end-to-end/fixtures/successful/index.js'), sizes); 55 | 56 | const results = await report('test/end-to-end/fixtures/successful/filesize.json', (content) => 57 | content.replace(new RegExp('preact.umd.js.map', 'g'), 'FOO.map'), 58 | ); 59 | t.deepEqual(results, expected); 60 | }, 61 | ); 62 | 63 | tap.test('api is interactive with custom reporter, using configuration file', async (t) => { 64 | const mapping = new Map([ 65 | ['preact.js', 3477], 66 | ['inferno.js', 7297], 67 | ['react-dom.js', 28721], 68 | ]); 69 | 70 | await report( 71 | 'test/end-to-end/fixtures/api-report/filesize.json', 72 | (content) => content, 73 | class extends Report { 74 | update(context: any) { 75 | const completed = super.getUpdated(context); 76 | for (const complete of completed) { 77 | const [filePath, sizeMap] = complete; 78 | const relativePath = relative('test/end-to-end/fixtures/api-report', filePath); 79 | t.is(mapping.has(relativePath), true); 80 | t.is(mapping.get(relativePath), sizeMap[0][0]); 81 | } 82 | } 83 | }, 84 | ); 85 | }); 86 | -------------------------------------------------------------------------------- /test/end-to-end/successful-project.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { exec } from 'child_process'; 19 | import { report, Report } from '../../src/api'; 20 | import { SizeMapValue, SizeMap } from '../../src/validation/Condition'; 21 | import { resolve, relative } from 'path'; 22 | 23 | tap.test('item under requested filesize limit passes', (t) => { 24 | const executeSuccess = exec('./dist/filesize -p=test/end-to-end/fixtures/successful'); 25 | 26 | executeSuccess.on('exit', (code) => { 27 | t.is(code, 0); 28 | t.end(); 29 | }); 30 | }); 31 | 32 | tap.test('item under requested filesize limit passes from API', async (t) => { 33 | const sizes: SizeMapValue = [ 34 | [3410, 3584], // brotli 35 | [3737, 4096], // gzip 36 | [9327, 10240], // none 37 | ]; 38 | const expected: SizeMap = new Map(); 39 | expected.set(resolve('test/end-to-end/fixtures/successful/index.js'), sizes); 40 | 41 | const results = await report('test/end-to-end/fixtures/successful', null); 42 | t.deepEqual(results, expected); 43 | }); 44 | 45 | tap.test('item under requested filesize limit passes from API, with replacement', async (t) => { 46 | const sizes: SizeMapValue = [ 47 | [3401, 3584], // brotli 48 | [3731, 4096], // gzip 49 | [9317, 10240], // none 50 | ]; 51 | const expected: SizeMap = new Map(); 52 | expected.set(resolve('test/end-to-end/fixtures/successful/index.js'), sizes); 53 | 54 | const results = await report('test/end-to-end/fixtures/successful', (content) => 55 | content.replace(new RegExp('preact.umd.js.map', 'g'), 'FOO.map'), 56 | ); 57 | t.deepEqual(results, expected); 58 | }); 59 | 60 | tap.test('api is interactive with custom reporter', async (t) => { 61 | const mapping = new Map([ 62 | ['preact.js', 3477], 63 | ['inferno.js', 7297], 64 | ['react-dom.js', 28721], 65 | ]); 66 | 67 | await report( 68 | 'test/end-to-end/fixtures/api-report', 69 | (content) => content, 70 | class extends Report { 71 | update(context: any) { 72 | const completed = super.getUpdated(context); 73 | for (const complete of completed) { 74 | const [filePath, sizeMap] = complete; 75 | const relativePath = relative('test/end-to-end/fixtures/api-report', filePath); 76 | t.is(mapping.has(relativePath), true); 77 | t.is(mapping.get(relativePath), sizeMap[0][0]); 78 | } 79 | } 80 | }, 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /test/end-to-end/throw-error.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import { report } from '../../src/api'; 19 | import { exec } from 'child_process'; 20 | 21 | tap.test('item with missing file fails with exit code 5', (t) => { 22 | const executeFailure = exec('./dist/filesize -p=test/end-to-end/fixtures/missing-item'); 23 | 24 | executeFailure.on('exit', (code) => { 25 | t.is(code, 5); 26 | t.end(); 27 | }); 28 | }); 29 | 30 | tap.test('item with missing file fails with exit code 5, using configuration file', (t) => { 31 | const executeFailure = exec('./dist/filesize -c=test/end-to-end/fixtures/missing-item/filesize.json'); 32 | 33 | executeFailure.on('exit', (code) => { 34 | t.is(code, 5); 35 | t.end(); 36 | }); 37 | }); 38 | 39 | tap.test('item with missing file throws exception from API', async (t) => { 40 | try { 41 | await report('test/end-to-end/fixtures/missing-item', null); 42 | } catch (e) { 43 | t.is(e, `Configuration for 'index.js' is invalid. (path is not a valid file)`); 44 | } 45 | }); 46 | 47 | tap.test('item with missing file throws exception from API, using configuration file', async (t) => { 48 | try { 49 | await report('test/end-to-end/fixtures/missing-item/filesize.json', null); 50 | } catch (e) { 51 | t.is(e, `Configuration for 'index.js' is invalid. (path is not a valid file)`); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /test/project-validation/fixtures/contains-package-json/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /test/project-validation/fixtures/contains-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesize": {} 3 | } -------------------------------------------------------------------------------- /test/project-validation/fixtures/missing-package-json/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /test/project-validation/project-validation.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import tap from 'tap'; 18 | import Project from '../../src/validation/Project'; 19 | import { resolve } from 'path'; 20 | import { Context } from '../../src/validation/Condition'; 21 | 22 | tap.test('valid directory should pass', async (t) => { 23 | const context: Context = { 24 | packagePath: '', 25 | projectPath: 'test/project-validation/fixtures/contains-package-json', 26 | packageContent: '', 27 | originalPaths: new Map(), 28 | compressed: new Map(), 29 | comparison: new Map(), 30 | silent: false, 31 | fileModifier: null, 32 | fileContents: new Map(), 33 | }; 34 | const message = await Project(context)(); 35 | 36 | t.is(message, null); 37 | }); 38 | 39 | tap.test('invalid directory should fail', async (t) => { 40 | const context = { 41 | packagePath: '', 42 | projectPath: 'test/project-validation/fixtures-invalid', 43 | packageContent: '', 44 | originalPaths: new Map(), 45 | compressed: new Map(), 46 | comparison: new Map(), 47 | silent: false, 48 | fileModifier: null, 49 | fileContents: new Map(), 50 | }; 51 | const message = await Project(context)(); 52 | 53 | t.is(message, `error project specified '${context.projectPath}' doesn't exist, is this a valid project?`); 54 | }); 55 | 56 | tap.test('directory missing package.json should fail', async (t) => { 57 | const context = { 58 | packagePath: '', 59 | projectPath: 'test/project-validation/fixtures/missing-package-json', 60 | packageContent: '', 61 | originalPaths: new Map(), 62 | compressed: new Map(), 63 | comparison: new Map(), 64 | silent: false, 65 | fileModifier: null, 66 | fileContents: new Map(), 67 | }; 68 | const message = await Project(context)(); 69 | 70 | t.is(message, `error Missing '${resolve(context.projectPath, 'package.json')}', is this a valid project?`); 71 | }); 72 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "outDir": "output", 8 | "sourceMap": false, 9 | "moduleResolution": "node", 10 | "target": "ES2019", 11 | "module": "esnext", 12 | "allowJs": false, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "incremental": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true 21 | }, 22 | "include": [ 23 | "*.ts", 24 | "**/*.ts" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------