├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.md
│ ├── feature_request.md
│ └── support_question.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── issue.yml
│ ├── pull_request.yml
│ └── push.yml
├── .gitignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Procfile
├── README.md
├── codecov.yml
├── config
├── api-config.js
├── copyright.txt
├── fixtures-base.js
├── fixtures-large.js
├── fixtures.js
├── jest.js
├── mariadb.js
├── mysql.js
└── postgres.js
├── demo
├── .eslintrc.js
├── README.md
├── build-tracker-cli.config.js
├── build-tracker.config.js
├── build.sh
└── run.sh
├── docs
├── .gitignore
├── README.md
├── blog
│ └── 2019-03-22-version-1.md
├── docs
│ ├── .eslintrc.js
│ ├── budgets.md
│ ├── guides
│ │ ├── advanced-ci.md
│ │ ├── contributing.md
│ │ ├── github-actions.md
│ │ ├── guides.md
│ │ └── heroku.md
│ ├── installation.md
│ ├── packages
│ │ ├── api-client.md
│ │ ├── api-errors.md
│ │ ├── app.md
│ │ ├── build.md
│ │ ├── cli.md
│ │ ├── comparator.md
│ │ ├── formatting.md
│ │ ├── server.md
│ │ └── types.md
│ └── plugins
│ │ ├── index.md
│ │ ├── with-mariadb.md
│ │ ├── with-mysql.md
│ │ └── with-postgres.md
├── docusaurus.config.js
├── package.json
├── sidebars.js
├── src
│ ├── css
│ │ └── custom.css
│ └── pages
│ │ ├── index.js
│ │ └── styles.module.css
└── static
│ ├── CNAME
│ └── img
│ ├── favicon.png
│ ├── favicon
│ └── favicon.ico
│ ├── github-action.png
│ ├── logo-animated.svg
│ ├── logo.png
│ ├── logo.svg
│ ├── ogImage.png
│ ├── undraw_completed_ngx6.svg
│ ├── undraw_deliveries_131a.svg
│ └── undraw_server_down_s4lk.svg
├── greenkeeper.json
├── husky.config.js
├── jest.config.js
├── lerna.json
├── lint-staged.config.js
├── package.json
├── plugins
├── .eslintrc.js
├── with-mariadb
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ ├── index.test.ts
│ │ │ ├── queries.test.ts
│ │ │ └── setup.test.ts
│ │ ├── index.ts
│ │ ├── queries.ts
│ │ └── setup.ts
│ └── tsconfig.json
├── with-mysql
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ ├── index.test.ts
│ │ │ ├── queries.test.ts
│ │ │ └── setup.test.ts
│ │ ├── index.ts
│ │ ├── queries.ts
│ │ └── setup.ts
│ └── tsconfig.json
└── with-postgres
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── __tests__
│ │ ├── index.test.ts
│ │ ├── queries.test.ts
│ │ └── setup.test.ts
│ ├── index.ts
│ ├── queries.ts
│ └── setup.ts
│ └── tsconfig.json
├── src
├── .eslintrc.js
├── api-client
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── modules
│ │ │ ├── __tests__
│ │ │ ├── config.test.ts
│ │ │ ├── create-build.test.ts
│ │ │ ├── get-build.test.ts
│ │ │ ├── git.test.ts
│ │ │ ├── readfile.test.ts
│ │ │ ├── spawn.test.ts
│ │ │ ├── stat-artifacts.test.ts
│ │ │ └── upload-build.test.ts
│ │ │ ├── config.ts
│ │ │ ├── create-build.ts
│ │ │ ├── get-build.ts
│ │ │ ├── git.ts
│ │ │ ├── readfile.ts
│ │ │ ├── spawn.ts
│ │ │ ├── stat-artifacts.ts
│ │ │ └── upload-build.ts
│ └── tsconfig.json
├── api-errors
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── app
│ ├── README.md
│ ├── config
│ │ ├── .eslintrc.js
│ │ ├── constants.js
│ │ ├── devserver.config.js
│ │ ├── jest
│ │ │ ├── fileMock.js
│ │ │ └── setup.js
│ │ ├── webpack-client.config.js
│ │ ├── webpack-progress.js
│ │ ├── webpack-server.config.js
│ │ └── webpack.config.js
│ ├── index.js
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── app.d.ts
│ │ ├── client
│ │ │ ├── Routes.tsx
│ │ │ ├── history.ts
│ │ │ ├── index.tsx
│ │ │ └── routes
│ │ │ │ ├── Builds.tsx
│ │ │ │ ├── Dates.tsx
│ │ │ │ ├── Latest.tsx
│ │ │ │ └── __tests__
│ │ │ │ ├── Builds.test.tsx
│ │ │ │ ├── Dates.test.tsx
│ │ │ │ └── Latest.test.tsx
│ │ ├── components
│ │ │ ├── AppBar.tsx
│ │ │ ├── BuildInfo.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── ColorScalePicker
│ │ │ │ ├── ColorScale.tsx
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── ColorScale.test.tsx
│ │ │ │ │ └── index.test.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ComparisonTable
│ │ │ │ ├── ArtifactCell.tsx
│ │ │ │ ├── BodyRow.tsx
│ │ │ │ ├── ComparisonTable.tsx
│ │ │ │ ├── DeltaCell.tsx
│ │ │ │ ├── GroupCell.tsx
│ │ │ │ ├── GroupRow.tsx
│ │ │ │ ├── HeaderRow.tsx
│ │ │ │ ├── RevisionCell.tsx
│ │ │ │ ├── RevisionDeltaCell.tsx
│ │ │ │ ├── TextCell.tsx
│ │ │ │ ├── TotalCell.tsx
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── ArtifactCell.test.tsx
│ │ │ │ │ ├── BodyRow.test.tsx
│ │ │ │ │ ├── ComparisonTable.test.tsx
│ │ │ │ │ ├── DeltaCell.test.tsx
│ │ │ │ │ ├── GroupCell.test.tsx
│ │ │ │ │ ├── GroupRow.test.tsx
│ │ │ │ │ ├── HeaderRow.test.tsx
│ │ │ │ │ ├── RevisionCell.test.tsx
│ │ │ │ │ ├── RevisionDeltaCell.test.tsx
│ │ │ │ │ ├── TextCell.test.tsx
│ │ │ │ │ └── TotalCell.test.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── DatePicker.tsx
│ │ │ ├── DateTextField.tsx
│ │ │ ├── Divider.tsx
│ │ │ ├── Drawer.tsx
│ │ │ ├── DrawerLink.tsx
│ │ │ ├── EmptyState.tsx
│ │ │ ├── Graph
│ │ │ │ ├── Area.tsx
│ │ │ │ ├── HoverOverlay.tsx
│ │ │ │ ├── Offset.ts
│ │ │ │ ├── StackedBar.tsx
│ │ │ │ ├── XAxis.tsx
│ │ │ │ ├── YAxis.tsx
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── Area.test.tsx
│ │ │ │ │ ├── HoverOverlay.test.tsx
│ │ │ │ │ ├── StackedBar.test.tsx
│ │ │ │ │ ├── XAxis.test.tsx
│ │ │ │ │ ├── YAxis.test.tsx
│ │ │ │ │ └── index.test.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Hoverable.tsx
│ │ │ ├── Menu.tsx
│ │ │ ├── MenuItem.tsx
│ │ │ ├── RadioSelect.tsx
│ │ │ ├── RelativeModal.tsx
│ │ │ ├── RelativeTooltip.tsx
│ │ │ ├── Ripple.tsx
│ │ │ ├── SizeKeyPicker
│ │ │ │ ├── Key.tsx
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── Key.test.tsx
│ │ │ │ │ └── index.test.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Snackbar.tsx
│ │ │ ├── Subtitle.tsx
│ │ │ ├── Table.tsx
│ │ │ ├── TabularMetadata.tsx
│ │ │ ├── TextField.tsx
│ │ │ ├── TextLink.tsx
│ │ │ ├── Tooltip.tsx
│ │ │ └── __tests__
│ │ │ │ ├── AppBar.test.tsx
│ │ │ │ ├── BuildInfo.test.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ ├── DatePicker.test.tsx
│ │ │ │ ├── DateTextField.test.tsx
│ │ │ │ ├── Divider.test.tsx
│ │ │ │ ├── Drawer.test.tsx
│ │ │ │ ├── DrawerLink.test.tsx
│ │ │ │ ├── EmptyState.test.tsx
│ │ │ │ ├── Hoverable.test.tsx
│ │ │ │ ├── Menu.test.tsx
│ │ │ │ ├── MenuItem.test.tsx
│ │ │ │ ├── RadioSelect.test.tsx
│ │ │ │ ├── RelativeModal.test.tsx
│ │ │ │ ├── RelativeTooltip.test.tsx
│ │ │ │ ├── Ripple.test.tsx
│ │ │ │ ├── Snackbar.test.tsx
│ │ │ │ ├── Subtitle.test.tsx
│ │ │ │ ├── TabularMetadata.test.tsx
│ │ │ │ ├── TextField.test.tsx
│ │ │ │ ├── TextLink.test.tsx
│ │ │ │ └── Tooltip.test.tsx
│ │ ├── icons
│ │ │ ├── .eslintrc.js
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── BarChart.tsx
│ │ │ ├── Clear.tsx
│ │ │ ├── Close.tsx
│ │ │ ├── Collapse.tsx
│ │ │ ├── Document.tsx
│ │ │ ├── Error.tsx
│ │ │ ├── Folder.tsx
│ │ │ ├── Hash.tsx
│ │ │ ├── Heart.tsx
│ │ │ ├── Info.tsx
│ │ │ ├── LineChart.tsx
│ │ │ ├── Link.tsx
│ │ │ ├── ListBulleted.tsx
│ │ │ ├── Logo.tsx
│ │ │ ├── Menu.tsx
│ │ │ ├── More.tsx
│ │ │ ├── OpenInExternal.tsx
│ │ │ ├── Remove.tsx
│ │ │ ├── Table.tsx
│ │ │ ├── Warning.tsx
│ │ │ └── styles.ts
│ │ ├── images
│ │ │ └── favicon.png
│ │ ├── modules
│ │ │ └── ColorScale.ts
│ │ ├── screens
│ │ │ ├── Main.tsx
│ │ │ └── __tests__
│ │ │ │ └── Main.test.tsx
│ │ ├── server
│ │ │ ├── __tests__
│ │ │ │ └── index.test.tsx
│ │ │ └── index.tsx
│ │ ├── store
│ │ │ ├── __tests__
│ │ │ │ ├── reducer.test.ts
│ │ │ │ └── utils.test.ts
│ │ │ ├── actions.ts
│ │ │ ├── index.ts
│ │ │ ├── mock.ts
│ │ │ ├── reducer.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── theme.ts
│ │ └── views
│ │ │ ├── AppBar.tsx
│ │ │ ├── Comparison.tsx
│ │ │ ├── Drawer.tsx
│ │ │ ├── Graph.tsx
│ │ │ ├── Snacks.tsx
│ │ │ └── __tests__
│ │ │ ├── AppBar.test.tsx
│ │ │ └── Graph.test.tsx
│ └── tsconfig.json
├── build
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ └── index.test.ts
│ │ └── index.ts
│ └── tsconfig.json
├── cli
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── bin.ts
│ │ └── commands
│ │ │ ├── __tests__
│ │ │ ├── create-build.test.ts
│ │ │ ├── stat-artifacts.test.ts
│ │ │ ├── upload-build.test.ts
│ │ │ └── version.test.ts
│ │ │ ├── create-build.ts
│ │ │ ├── get-build.ts
│ │ │ ├── stat-artifacts.ts
│ │ │ ├── upload-build.ts
│ │ │ └── version.ts
│ └── tsconfig.json
├── comparator
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── ArtifactDelta.ts
│ │ ├── BuildDelta.ts
│ │ ├── __tests__
│ │ │ ├── ArtifactDelta.test.ts
│ │ │ ├── BuildDelta.test.ts
│ │ │ ├── artifact-math.test.ts
│ │ │ └── index.test.ts
│ │ ├── artifact-math.ts
│ │ └── index.ts
│ └── tsconfig.json
├── fixtures
│ ├── .eslintrc.js
│ ├── README.md
│ ├── builds-large
│ │ ├── 02bdf313fa99040980ed2a83667fa133b8e5c03f.json
│ │ ├── 02f7aaa689a3b50de1c9a9816528ca216d7c62a5.json
│ │ ├── 038af502b8663341d3f39fdb51472efca16d692e.json
│ │ ├── 042e66da793d071a1938737827417f840e1d04be.json
│ │ ├── 05392199a9ab764c9f6733ccc438394ce4960faa.json
│ │ ├── 059e66cbf5487d0228f0f0fb69eba4b69ca0f3de.json
│ │ ├── 0a651288e67cfdf935d410b414b62edbcab31c97.json
│ │ ├── 0d0e6a13380539f0ebd7460bb5ee3ac64dd12704.json
│ │ ├── 0d7dda5ced7c88b30fc61ab6b7aa95e00b44a051.json
│ │ ├── 0dc5dd7ddb4188432785101b14d4d0843d09f77b.json
│ │ ├── 0f66e01e2f03c959bb2c4bb0220a9fe853b1f7ca.json
│ │ ├── 108d099e3073c347dd4f59a24c5ecb18a3534e48.json
│ │ ├── 109ed16292d5507b8a19c57653f7bafd010653be.json
│ │ ├── 12d7b29e44d9876cdf12c57319ddf83f10811c62.json
│ │ ├── 15b436cd9a6c26ccb5a29c42cc72fcd236ab68db.json
│ │ ├── 16851b9b198253cea1fdb4660c698f947b2562b1.json
│ │ ├── 16a80d93b835cd91d66e578bd427bc11e70f077f.json
│ │ ├── 174c202f085a6589dce828b3f81396b8ce37909c.json
│ │ ├── 19110d41fe39332b259b02c673eb49139c45fb89.json
│ │ ├── 1df5a4c4b3dfeb3e3c78ef03cc6b006cd6a9ca5f.json
│ │ ├── 1dff4e067686e52a5052b7420f307528300516c8.json
│ │ ├── 1edc8aae8820bb7f717323873d08c082a1fb2d92.json
│ │ ├── 1f52b1fc8a1a036f63bed0ee21eb8d91f4c43e12.json
│ │ ├── 1f918739ce05149c3ee7603a28102cbe268cd0b3.json
│ │ ├── 20724544d271f98ff71435a6c48f239649e82310.json
│ │ ├── 20d4e8e78265f93d80f1fb52ee4076367ef3fe15.json
│ │ ├── 214fe0cd7545840e133ec645df3057cf96d14390.json
│ │ ├── 242c11b770dc9d2750843ea1e4f19b41af7c65b6.json
│ │ ├── 24a6110b01bd5681d4e4827b09ed924ea9f2e701.json
│ │ ├── 24dd1c12bba08a6bd667541156211b7ab3198c2c.json
│ │ ├── 265c153c5cbeeee681442d0050f41d4b8f92ce56.json
│ │ ├── 26c8cc84ae382aed064e94fdfb10d91b5d2cc560.json
│ │ ├── 27055696a6fe26cf3ae2eaf6faa98ab5192045cb.json
│ │ ├── 2802f95211524614860a60e41021aadfcdbcd2dd.json
│ │ ├── 283a64e194bfaa21fe2cbf05e2dbb95094bd4d2c.json
│ │ ├── 2888ba5e296ca4b64e19393e5a4c16bcab0b4fd3.json
│ │ ├── 2bc5bd7eab37ca9a18f2c05f1aadec8573c81505.json
│ │ ├── 2ca236bcd08a660df1e8143a2f86a2f9a6a3c7c3.json
│ │ ├── 313b3fa1785585c8b81ab268564424a0c2f4d9c9.json
│ │ ├── 32befedefb354547dc8c9d3c94cc2c98075a1d71.json
│ │ ├── 32c27eff6715a60fbd30c7c7aa3d14e3e43aeb33.json
│ │ ├── 336e8182d372f24445dbd997ed795134455b8bfd.json
│ │ ├── 35321ff75d9a7a76a5a458660e10875909364832.json
│ │ ├── 35bcb71a6bec2c88e963fcf63026922cc3696a54.json
│ │ ├── 363f622f8c0575362acf0f1f49fafd9edfd28e6f.json
│ │ ├── 37b03d0035c13e255ccdd8caf52b0e66545df891.json
│ │ ├── 391bb1d4f36a4ecf5472a1a77b6f9a8268d6eac1.json
│ │ ├── 3d89368e2e21f165789d5e0e6446a2ae2ac8c0c3.json
│ │ ├── 3eaa932131748311a8ce7a9725780b638b4b52f4.json
│ │ ├── 40e4f7c278c6348c2b3686dd72666628dea7a999.json
│ │ ├── 4989d24b399d2fd5b87ad907098ed9aabaddd288.json
│ │ ├── 4b937dd21ad3616608cffca7eaa5ab8b0dbf0f7c.json
│ │ ├── 4e4447b40ad1c916eceab77c11d6d14b01906712.json
│ │ ├── 4e6e79ac145b7c99c1b2cef04dd0109443618202.json
│ │ ├── 4fa802dba4c0afbef437083fa45c740368390e67.json
│ │ ├── 533b7a39a6a6a92eff92449a1510ad9d72fef9ed.json
│ │ ├── 55bd5b55e0e47ae1e4a372419d9d334c0f74d030.json
│ │ ├── 56b2b9a027d4fdd31665d770e75b7a833f8eaa7c.json
│ │ ├── 58a972c65b72c2215fb3d1906bc4b5b62d573eb3.json
│ │ ├── 59c922c7d91785e6a5543cf4d14b51330ec0a4ce.json
│ │ ├── 5bc11f734f68d51ecba5f47af433bdce7276cee2.json
│ │ ├── 5bc45a245fb800350fdb7af23a770f0667d5faa7.json
│ │ ├── 5c07a5e47f1370f345473dae38a546b4bc27f199.json
│ │ ├── 5cf390f3beb154d224a7bc1725d8c810e25fc0f2.json
│ │ ├── 5e539a67af6f3e7471ced7ebec262ab484fbe282.json
│ │ ├── 5e98f70b2c315fcfb9328b4c7d9371372a3c80ac.json
│ │ ├── 617931a8ea1859ab363e2c227b444d9a62572d3b.json
│ │ ├── 61efd704f3a2a8a8be3c3c385f62ca113c444a84.json
│ │ ├── 62cc3de51d22037b84f07d4543b3618880328f89.json
│ │ ├── 6314a3142b62d25b23798a9b5de88418cf42abca.json
│ │ ├── 6534f190f68cce2caab8aa14bfd9583984061343.json
│ │ ├── 6b5d85b67180173ebd805851913150a8233b9d6a.json
│ │ ├── 6e5eabea0748ac674a69f367f331ccb07703efaf.json
│ │ ├── 6eb4aed2084f1aca83506741b5901b924d6aa31f.json
│ │ ├── 7189e4f619856d2c9e7e03568ab0f56765a5f77a.json
│ │ ├── 7224fb31d6e3e147478907de24c7fa2078ba2d1b.json
│ │ ├── 7295bc43757e41a742a90a99a9d45dde4f3e09c2.json
│ │ ├── 74ae1f09be1bb45beef7b5b0f6c0092bf6113d42.json
│ │ ├── 7676a13f64c2c4b616aa31fd732fd54119be7f94.json
│ │ ├── 768d64b7ff38e8b9e5368700aaf35d254e7f8d0e.json
│ │ ├── 78a61348337e7a3440734dd6e7554e8a1e6d874a.json
│ │ ├── 79a9122d368214b628344ce260c8ce9f16cb9e0d.json
│ │ ├── 7aadeb749a17a06b6abb0fad49de392645930126.json
│ │ ├── 7b1c5069953363cee3e624ecb19fbf4e5a1496af.json
│ │ ├── 7d4ae9984071aad0d436dc9dca425ece070ceaec.json
│ │ ├── 7f6961eb1fce989788c717d7ed7cadbcde086857.json
│ │ ├── 804201b3fbc7021863992877b5cc0548a35c19ea.json
│ │ ├── 8052a710c2befdf8fe2c78635da546794fbb790a.json
│ │ ├── 827f0e70cea43e076bcbec03a16f1d7c70aa7347.json
│ │ ├── 835f2e50f95baf967798327eec4fc3ab0e0ee83f.json
│ │ ├── 8464307d13c834f643017c440fcf2b38ef894deb.json
│ │ ├── 859b00da16f32007f373faa3f7880a3eee44ca95.json
│ │ ├── 85d5aa0db580042c36031007b1d7fad1d263a1cf.json
│ │ ├── 87c8684055979726c0095fa1c6ccd7253e02ff04.json
│ │ ├── 89147300968ef369a29402ec0b465c367ffceaf0.json
│ │ ├── 899b6868d68898a3f9be17d0e9f9efb210c8c22b.json
│ │ ├── 8af1672267e07074e3623113b895e32902f8b8e9.json
│ │ ├── 8b11a9a770fba7f0e72ecba6b34b263416429e15.json
│ │ ├── 8ba674395f1abc702bdfe1840f6a78e3d506d031.json
│ │ ├── 8cbd4d15efc07a6ba447404745decde216bcbc2b.json
│ │ ├── 8e3a06e1076d54237871418b394c4574b30810b1.json
│ │ ├── 8fbf45e985ed42188dbd686960e378d9c4f13ab5.json
│ │ ├── 916993b92e20bc6530b748176168e86fc2ba3f15.json
│ │ ├── 929fdf0ab2ae9e9b532fb81cf1267f93d9c74ee5.json
│ │ ├── 934772440ab687076c0605e5413213aba25347ed.json
│ │ ├── 935af3902229bbb6f14d05d31f2b01a50816e7ba.json
│ │ ├── 93de650c4242d7ef84cbc69c9d7675ae541338e4.json
│ │ ├── 98755a05c40c55b4c894521cee29b43ae3266d4a.json
│ │ ├── 997a86636f0f73c57b14bb5c235c4f5961202002.json
│ │ ├── 9a0b36550bfe7d5192c6e9f99843ee377c59e045.json
│ │ ├── 9acccc33dc58beeaafb0a99aa8f58d4b9f1b3838.json
│ │ ├── 9b2d6bc10b558d031faa598d22697becc6477395.json
│ │ ├── a46f4355d9b446f44f9f9fd3eb35ed863792657c.json
│ │ ├── a6f47eeb26012161426922c3e060ddbbbe87d96d.json
│ │ ├── a76f84c9eb8cdd0cc3107e092df44321dc79cf9b.json
│ │ ├── a778a7ab5ae6d149b6069c58fcec74aaf3069e2b.json
│ │ ├── a9070407f23a90dd90b28dfd37ff41e6625875ed.json
│ │ ├── a9bb0fbb37f13c431420585c5d25ae835e938e5f.json
│ │ ├── a9d3d21f41295d454f6b93e493ac05ca95e6f55a.json
│ │ ├── abb299d0699a72c0fcf3ed56514de3218b279d25.json
│ │ ├── ac5a2cba0fb7a28b15a7290b43a4630566251e47.json
│ │ ├── acaa24eb071762729b0ef50c5860431a33e6fb6d.json
│ │ ├── acbd72dc7d59e9102c591533620676be2d96f3d1.json
│ │ ├── ad118e2efd7dc8a7106018ce4253a38639fa2ecd.json
│ │ ├── ad6e9725f3e1d4e2067c00632d8b9b6992652545.json
│ │ ├── ad8d0eb665196afe9ad66d7d40c514380fda55c2.json
│ │ ├── af1e4d8b772e51abb953a0e2d9e210ba861670d6.json
│ │ ├── af7aed7c8c0e85456f5c5365412b6066f5499a14.json
│ │ ├── b38bfb7db2c77720bd83ef061d1d3376513484a0.json
│ │ ├── b4bcdcd6f1fd0dae33895b3046d3850bc8bad479.json
│ │ ├── b930efa9b71f80f278a1e8a77b3fb59dcb072781.json
│ │ ├── ba02e05709726c04073dd6b0e3cdaad63f7e4a70.json
│ │ ├── ba6f3f3d53448bb365d0bd9d086aba57d36b32e9.json
│ │ ├── bac06a515b5c4b3421916d103214567438c17aa6.json
│ │ ├── bb979ad75cfb903de9721810585ac785de1cd4b6.json
│ │ ├── be5313924b2d90e89e5dc419a86d44516d5a96b2.json
│ │ ├── c28e7503bae87904c47dad46b2ba71a6099202e9.json
│ │ ├── c30fe207650d6cf0d101c14eefde946ddf04833c.json
│ │ ├── c3c21aa17a38aba6067e0d32e19948c0808d558f.json
│ │ ├── c4dc5e627a47576a62d026a68c731d34f26fd392.json
│ │ ├── c74618cd6065a4204d092babd452b1df5c515d11.json
│ │ ├── c7b5fdcc7cde4fb909f75c6b2c36e38b4763f026.json
│ │ ├── c7b655c42c797d97e14834f222880c01afbaee92.json
│ │ ├── cb2061c144c1db6d9399cbe7451c8a89c94256f2.json
│ │ ├── cd45c648992c33f2c38bd9d91714e9d3dee011bb.json
│ │ ├── ce91bb8414321c4205cf5e959ab21a02483dfc59.json
│ │ ├── cff0db25bc18fcb24600af2681d9f158eadf0172.json
│ │ ├── cff8b1a0fe1b44c00a36961e59f198818fe02af9.json
│ │ ├── d07ede0507e371503e1f5debb72ad7d6771fe943.json
│ │ ├── d11d9cca61c2970ed931acea3c63ccc1b997d78f.json
│ │ ├── d2b39a334c041166b2dec3ee47ab7ef399d39390.json
│ │ ├── d4887ae61b0f1a3cca31f5e182d7911af89a223a.json
│ │ ├── d5c1e481ee5a88cc3c357770d214ed39168ad274.json
│ │ ├── d60995e666a191680af2fbd2d077c369717f47e4.json
│ │ ├── d929ffe845c39f1a0ba67ff410ffe597288cac18.json
│ │ ├── e0f48a8d0433fe2a37bd0d065f1f5b3712a431d2.json
│ │ ├── e4de5ffcb8341d3f3cc563c7c71ebecd195335eb.json
│ │ ├── e60ce95f883ff6256a0b214d3700c7455ae10bd9.json
│ │ ├── e7404028794a630d65fe02474dd9c318cf582078.json
│ │ ├── e92478cb2aa4c0996ffc1aa481acb66dc37eac99.json
│ │ ├── e94e2fb286cd5d8505a745a54ee5b82f1def3a09.json
│ │ ├── e9e01c86de760a03ea713c0f41be6bab99be13e9.json
│ │ ├── ed16ec1c51119cdd13d909f2fa4e7c97b9f7302d.json
│ │ ├── ee9735e8a642d6a02398276178d7a1df9b5f82de.json
│ │ ├── ef69c1bccc295531ef21d75086fa0a28861ec5c6.json
│ │ ├── f030863898f4b3dee0ce8811ce5de9dca0b6bce9.json
│ │ ├── f147ec9d9c6f18ed33cbeb87c9661077eb2bf9da.json
│ │ ├── f2d1d162fc6b42c9e2c86662bcc1d9b644539dcc.json
│ │ ├── f47cdafdc6fb4eec8faaac00afb0f9dbcef8561b.json
│ │ ├── f63d52e046a5ff0f06fd366dfb7e12c026ea6594.json
│ │ ├── f9b21fbd65fe5b2c3418544c7ecd697ae0493394.json
│ │ ├── fa0bf6d9ebfcc5c6dd2d5e542204723289034cc1.json
│ │ ├── fa1024039ac1478c343ae849830ac99089c65098.json
│ │ ├── fa57cd0e94b9472d171953ce5e1fe374ec622a9c.json
│ │ ├── faada294394bf7abf762dd439772f18350e56f7f.json
│ │ ├── fccbaa06e5aa7a34016a275604c52de7c6062505.json
│ │ ├── fd06ace7fe7c4ed6ecd03437c5839f72c3be04da.json
│ │ ├── fe540d67a3b60dd98d681e7b47245c36184c366e.json
│ │ └── fe541551877fc3b8512e96ed8e343cec705c6193.json
│ ├── builds-medium
│ │ ├── 01141f29743fb2bdd7e176cf919fc964025cea5a.json
│ │ ├── 1327ec715a0a2dd204eb7bede5f86bcf9c78f324.json
│ │ ├── 188aa19da077a0db3972b52f0f1b580fafe75f49.json
│ │ ├── 19868a0432f039d45783bca1845cede313fbfbe1.json
│ │ ├── 1bf811ec249c2b13a314e127312b2d760f6658e2.json
│ │ ├── 1d088ea7d0aa3a93fa0d5c0497a3fc96b33ce1c9.json
│ │ ├── 20fd519231b53d27e4ce7c3914f815e0b02684b4.json
│ │ ├── 22abb6f829a07ca96ff56deeadf4d0e8fc2dbb04.json
│ │ ├── 243024909db66ac3c3e48d2ffe4015f049609834.json
│ │ ├── 298dfe16af18f420dfbb192d2f38b1ac6022abb8.json
│ │ ├── 30af629d1d4c9f2f199cec5f572a019d4198004c.json
│ │ ├── 3d7d22d51db1369426c4ae1a300180c325e0b61d.json
│ │ ├── 4a8882483a664401a602f64a882d0ed7fb1763cb.json
│ │ ├── 4c9d2a8fc24d37c967cf0e596f293d3168b7696e.json
│ │ ├── 4d06472b21aef812ffb2ce70dbd41a08529fbfe6.json
│ │ ├── 5113828d6b0e628a78fa383a53933c413b6bdbef.json
│ │ ├── 6666692feabaa56483f2d21eaec994bf75ac2583.json
│ │ ├── 703baac11f1be4b2d02ddb89de6789c7605183b0.json
│ │ ├── 71a383fdf7ae287aa1f18ebe7e270b0520da2536.json
│ │ ├── 7f68d84c59b4604ecd88943eae4a88a41b137ed6.json
│ │ ├── 8de1726f26559b4191f4826817df64baff07711d.json
│ │ ├── 90aedcf89f12d2013f58105e0b1493e85c72e92c.json
│ │ ├── 946f0519038552aac5d36b99dc1e5dbd23bba6bc.json
│ │ ├── 99649d52f3bf158766c90120b7c3a60d34d6f76c.json
│ │ ├── 9d5afc23011f42b7794f137e78af45527cb3b58b.json
│ │ ├── a7f9952a68c69bbdfd73e7f8dee230d281b0c2b8.json
│ │ ├── b4098b3a46c207924e1e9edfa22fc21960524c72.json
│ │ ├── bd691addbd384df852e116f4452c8e0e615d1b6f.json
│ │ ├── bf4c66a3ee2a8c8355f8bd1c207593d32fa29d6b.json
│ │ ├── c00dc16fb85a6de1e51e9eb0f13056443614769e.json
│ │ ├── c0bdda3979f0badcbc3338c66b826ee1d5903e2a.json
│ │ ├── c74d7997da43e8b51b66b09a75cb24f2b49fe645.json
│ │ ├── d167fec4ee6a0bec23a06ce6085a93af3379f287.json
│ │ ├── d48a6bde107f8663818fd5d2460ee059365cc5e1.json
│ │ ├── d691af0754ce7dcc08c76a1ed2d8259962cb9d1d.json
│ │ ├── f7cb67d8312045fe3526cd7c8039c31a3183b03d.json
│ │ ├── f90b4831ad2bf921d5ccccfffeb61f6486567777.json
│ │ ├── f915ee2df9a9d3db4a8fb1a8b8c0e82f30338e13.json
│ │ ├── fa752a9ce64a440f16e2c3670aa97a28be25c71f.json
│ │ └── fc55ce1d311aa85bd0499c51e230255bbde19ef3.json
│ ├── cli-configs
│ │ ├── commonjs
│ │ │ └── build-tracker.config.js
│ │ ├── fakedist
│ │ │ ├── main.1234567.js
│ │ │ ├── test-folder
│ │ │ │ └── test-no-extension
│ │ │ └── vendor.js
│ │ └── rc
│ │ │ ├── .build-tracker-build-url-rc.js
│ │ │ ├── .build-tracker-http-rc.js
│ │ │ ├── .build-trackerrc-invalid.js
│ │ │ └── .build-trackerrc.js
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ └── server-configs
│ │ └── build-tracker.config.js
├── formatting
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ └── index.test.ts
│ │ └── index.ts
│ └── tsconfig.json
├── server
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ ├── csp.test.ts
│ │ │ └── server.test.ts
│ │ ├── api
│ │ │ ├── __tests__
│ │ │ │ ├── insert.test.ts
│ │ │ │ ├── protected.test.ts
│ │ │ │ └── read.test.ts
│ │ │ ├── index.ts
│ │ │ ├── insert.ts
│ │ │ ├── protected.ts
│ │ │ └── read.ts
│ │ ├── commands
│ │ │ ├── .eslintrc.js
│ │ │ ├── __tests__
│ │ │ │ ├── run.test.ts
│ │ │ │ ├── setup.test.ts
│ │ │ │ └── version.test.ts
│ │ │ ├── run.ts
│ │ │ ├── seed.ts
│ │ │ ├── setup.ts
│ │ │ └── version.ts
│ │ ├── csp.ts
│ │ ├── index.ts
│ │ ├── server.ts
│ │ └── types.ts
│ └── tsconfig.json
└── types
│ ├── README.md
│ ├── index.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ └── index.ts
│ └── tsconfig.json
├── tsconfig.json
├── tsconfig.test.json
├── typings
└── react-native
│ ├── LICENSE
│ ├── index.d.ts
│ └── package.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | docs/src/theme
4 | docs/build
5 | typings
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github:
4 | - paularmstrong
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: If something isn't working as expected 🤔.
4 | ---
5 |
6 |
14 |
15 | ## Problem
16 |
17 |
23 |
24 | ## Steps to Reproduce
25 |
26 |
35 |
36 | ## Expected Result
37 |
38 |
44 |
45 | ## Actual Result
46 |
47 |
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Feature Request
3 | about: I have a suggestion (and I might like to implement it myself 😀)!
4 | ---
5 |
6 |
14 |
15 | ## Problem
16 |
17 |
26 |
27 | ## Proposed solution
28 |
29 |
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support_question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤗 Support or Question
3 | about: If you have a question 💬, please check for help on StackOverflow!
4 | ---
5 |
6 | # General support is not provided in GitHub issues!
7 |
8 | Issues on GitHub are intended to be related to problems with Build Tracker itself. It's often seen as rude to ask basic usage questions here. Please make sure you read the documentation thoroughly and try searching the web for your problem. You are not likely to receive support with how to use it here 😁.
9 |
10 | ---
11 |
12 | If you can't find an answer, please submit them to one of this resources:
13 |
14 | - StackOverflow: https://stackoverflow.com/questions/tagged/build-tracker using the tag `build-tracker`
15 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 |
5 | # Problem
6 |
7 |
10 |
11 | # Solution
12 |
13 |
22 |
23 | # TODO
24 |
25 | - [ ] 🤓 Add & update tests (always try to _increase_ test coverage)
26 | - [ ] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
27 | - [ ] 📖 Update relevant documentation
28 |
--------------------------------------------------------------------------------
/.github/workflows/issue.yml:
--------------------------------------------------------------------------------
1 | name: Issue checker
2 |
3 | on:
4 | issues:
5 | types: [opened, edited]
6 |
7 | jobs:
8 | auto_close_issues:
9 | name: Verify well-formed
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v1
14 | - name: Automatically close issues that don't follow the issue template
15 | uses: lucasbento/auto-close-issues@v1.0.2
16 | with:
17 | github-token: ${{ secrets.GITHUB_TOKEN }}
18 | issue-close-message: "Hello @${issue.user.login} :wave: Thanks for helping out with Build Tracker!\n\nThis issue is being automatically closed because it does not follow the issue template. Please follow the template provided, do not delete any of the template contents, and include as much information as possible.\n\nYou can edit this issue and include the template to re-open or start with a [New Issue](https://github.com/paularmstrong/build-tracker/issues/new/choose)."
19 | closed-issues-label: '🙁 Issue template broken'
20 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: On push
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | run_tests:
10 | name: Typecheck, Test, Lint
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@master
14 | - uses: actions/setup-node@master
15 | with:
16 | node-version: 12.x
17 | - run: npm install -g yarn
18 | - run: yarn install
19 | - name: Typecheck
20 | run: yarn tsc
21 | - name: Tests
22 | run: yarn test:ci --coverage
23 | - name: Lint
24 | run: yarn lint:ci
25 | - uses: codecov/codecov-action@v1
26 |
27 | upload-build:
28 | name: Upload build
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: actions/checkout@master
32 | - uses: actions/setup-node@master
33 | with:
34 | node-version: 12.x
35 | - run: npm install -g yarn
36 | - run: yarn install
37 | - name: Build all
38 | run: yarn build
39 | - name: Upload build
40 | run: yarn ts-node src/cli/src/bin.ts upload-build -c demo/build-tracker-cli.config.js -b main
41 | env:
42 | BT_API_AUTH_TOKEN: ${{ secrets.BT_API_AUTH_TOKEN }}
43 |
44 | deploy:
45 | name: Deploy docs
46 | runs-on: ubuntu-latest
47 | steps:
48 | - uses: actions/checkout@master
49 | - uses: actions/setup-node@master
50 | with:
51 | node-version: 12.x
52 | - run: npm install -g yarn
53 | - run: yarn install
54 | - name: Build docs
55 | run: yarn workspace @build-tracker/docs build
56 | - name: Deploy documentation
57 | uses: peaceiris/actions-gh-pages@v3
58 | with:
59 | github_token: ${{ secrets.GITHUB_TOKEN }}
60 | publish_dir: ./docs/build
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules
3 | /**/node_modules
4 | *.log
5 | *.cache
6 | dist
7 | .eslintcache
8 | coverage
9 | .env
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Please refer to the contributing guidelines at https://buildtracker.dev/docs/guides/contributing
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Paul Armstrong
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: demo/build.sh
2 | web: demo/run.sh
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build Tracker [](https://github.com/paularmstrong/build-tracker/actions)
2 |
3 | > **Warning**
4 | > Build Tracker needs some love. While the functionality is all present and works great, it's pretty out of date and would greatly benefit from your help.
5 |
6 |
7 |
8 | ## Documentation
9 |
10 | All documentation can be found at [buildtracker.dev](https://buildtracker.dev).
11 |
12 | ## Contributing
13 |
14 | ### [Code of Conduct](https://github.com/paularmstrong/build-tracker/blob/main/CODE_OF_CONDUCT.md)
15 |
16 | All project participants are expected to adhere to the repository's Code of Conduct. Please read [the full text](https://github.com/paularmstrong/build-tracker/blob/main/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
17 |
18 | ### [Contributing Guide](https://buildtracker.dev/docs/guides/contributing)
19 |
20 | Read the contributing guide to learn about the development process, how to propose fixes and improvements, and how to build and test your changes.
21 |
22 | ## License
23 |
24 | Build Tracker is [MIT licensed](https://github.com/paularmstrong/build-tracker/blob/main/LICENSE).
25 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | branch: main
3 | require_ci_to_pass: no
4 |
5 | coverage:
6 | precision: 2
7 | round: down
8 | range: '90...100'
9 |
10 | comment:
11 | layout: 'diff,files'
12 | behavior: default
13 | require_changes: no
14 |
--------------------------------------------------------------------------------
/config/api-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | applicationUrl: 'http://localhost:3000',
3 | artifacts: ['src/*/dist/**/*', 'plugins/*/dist/**/*'],
4 | };
5 |
--------------------------------------------------------------------------------
/config/copyright.txt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 |
--------------------------------------------------------------------------------
/config/fixtures-large.js:
--------------------------------------------------------------------------------
1 | const makeFixtureConfig = require('./fixtures-base');
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | module.exports = makeFixtureConfig('large', {
5 | artifacts: {
6 | budgets: {
7 | '*': [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.PERCENT_DELTA, maximum: 0.5 }],
8 | },
9 | groups: [
10 | { artifactMatch: /^web\//, name: 'Web' },
11 | { artifactMatch: /^serviceworker\//, name: 'Service Worker' },
12 | ],
13 | filters: [
14 | // Ignore all but english
15 | /web\/i18n-\w+\/(?!en\.).+/,
16 | // Ignore all but english
17 | /web\/ondemand\.emoji\.(?!en\.).+/,
18 | /web\/ondemand\.countries-(?!en\.).+/,
19 | ],
20 | },
21 | budgets: [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.DELTA, maximum: 1000 }],
22 | defaultSizeKey: 'gzip',
23 | name: 'Static Fixtures - Large Set',
24 | });
25 |
--------------------------------------------------------------------------------
/config/fixtures.js:
--------------------------------------------------------------------------------
1 | const makeFixtureConfig = require('./fixtures-base');
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | module.exports = makeFixtureConfig('medium', {
5 | artifacts: {
6 | groups: [
7 | {
8 | name: 'Entries',
9 | artifactMatch: /^\w+$/,
10 | budgets: [{ level: BudgetLevel.ERROR, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 250000 }],
11 | },
12 | {
13 | name: 'Home',
14 | artifactNames: ['main', 'vendor', 'shared', 'runtime', 'bundle.HomeTimeline'],
15 | budgets: [{ level: BudgetLevel.ERROR, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 350000 }],
16 | },
17 | ],
18 | },
19 | defaultSizeKey: 'gzip',
20 | name: 'Static Fixtures',
21 | });
22 |
--------------------------------------------------------------------------------
/config/jest.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverageFrom: ['/src/**/*.{ts,tsx}'],
3 | globals: {
4 | 'ts-jest': {
5 | isolatedModules: true,
6 | },
7 | },
8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
9 | modulePathIgnorePatterns: ['dist', 'lib'],
10 | resetMocks: true,
11 | restoreMocks: true,
12 | testPathIgnorePatterns: ['node_modules'],
13 | testURL: 'https://build-tracker.local',
14 | timers: 'fake',
15 | transform: {
16 | '^.+\\.tsx?$': 'ts-jest',
17 | '^.+\\.jsx?$': 'babel-jest',
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/config/mariadb.js:
--------------------------------------------------------------------------------
1 | const withMaria = require('@build-tracker/plugin-with-mariadb').default;
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | /**
5 | * To run a mariadb docker container:
6 | * docker run -p 3307:3306 --name bt-mariadb -e MYSQL_ROOT_PASSWORD=tacos -e MYSQL_ROOT_HOST=% -e MYSQL_DATABASE=buildtracker -d mariadb --default-authentication-plugin=mysql_native_password
7 | * yarn ts-node src/server/src/index.ts setup -c ./config/mariadb.js
8 | * yarn ts-node src/server/src/index.ts seed -c ./config/mariadb.js
9 | */
10 |
11 | module.exports = withMaria({
12 | defaultBranch: 'main',
13 | dev: true,
14 | artifacts: {
15 | groups: [
16 | {
17 | name: 'Web App',
18 | artifactMatch: /^app\/client/,
19 | budgets: [{ level: BudgetLevel.ERROR, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 150000 }],
20 | },
21 | ],
22 | },
23 | mariadb: {
24 | user: 'root',
25 | password: 'tacos',
26 | database: 'buildtracker',
27 | host: '127.0.0.1',
28 | port: 3307,
29 | },
30 | url: 'http://localhost:3000',
31 | });
32 |
--------------------------------------------------------------------------------
/config/mysql.js:
--------------------------------------------------------------------------------
1 | const withMysql = require('@build-tracker/plugin-with-mysql').default;
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | /**
5 | * To run a mysql docker container:
6 | * docker run -p 3306:3306 --name bt-mysql -e MYSQL_ROOT_PASSWORD=tacos -e MYSQL_ROOT_HOST=% -e MYSQL_DATABASE=buildtracker -d mysql --default-authentication-plugin=mysql_native_password
7 | * yarn ts-node src/server/src/index.ts setup -c ./config/mysql.js
8 | * yarn ts-node src/server/src/index.ts seed -c ./config/mysql.js
9 | */
10 |
11 | module.exports = withMysql({
12 | defaultBranch: 'main',
13 | dev: true,
14 | artifacts: {
15 | groups: [
16 | {
17 | name: 'Web App',
18 | artifactMatch: /^app\/client/,
19 | budgets: [{ level: BudgetLevel.ERROR, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 150000 }],
20 | },
21 | ],
22 | },
23 | mysql: {
24 | user: 'root',
25 | password: 'tacos',
26 | database: 'buildtracker',
27 | host: '127.0.0.1',
28 | port: 3306,
29 | },
30 | url: 'http://localhost:3000',
31 | });
32 |
--------------------------------------------------------------------------------
/config/postgres.js:
--------------------------------------------------------------------------------
1 | const withPostgres = require('@build-tracker/plugin-with-postgres').default;
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | const { config } = require('dotenv');
5 | config();
6 |
7 | /**
8 | * To run a mysql docker container:
9 | * docker run --name pg -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_DB=buildtracker -p 54320:5432 -d postgres
10 | * yarn ts-node src/server/src/index.ts setup -c ./config/postgres.js
11 | * yarn ts-node src/server/src/index.ts seed -c ./config/postgres.js
12 | */
13 |
14 | module.exports = withPostgres({
15 | defaultBranch: 'main',
16 | dev: true,
17 | artifacts: {
18 | groups: [
19 | {
20 | name: 'Web App',
21 | artifactMatch: /^app\/client/,
22 | budgets: [{ level: BudgetLevel.ERROR, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 150000 }],
23 | },
24 | ],
25 | },
26 | pg: {
27 | connectionString: 'postgresql://postgres:mysecretpassword@127.0.0.1:54320/buildtracker',
28 | ssl: false,
29 | },
30 | url: 'http://localhost:3000',
31 | });
32 |
--------------------------------------------------------------------------------
/demo/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-console': 'off'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Not a code demo
2 |
3 | > This is not a demo that you should use to learn how to set up your own Build Tracker instance. This is code that deploys and runs the un-published source code from this repository in our online demo application https://build-tracker-demo.herokuapp.com
4 |
5 | Please read the [Getting Started documentation](https://buildtracker.dev/docs/installation) to learn how to set up your own instance.
6 |
--------------------------------------------------------------------------------
/demo/build-tracker-cli.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const repoRoot = path.join(__dirname, '..');
4 |
5 | const filenameHash = (fileName) => {
6 | const parts = path.basename(fileName, '.js').split('.');
7 | return parts.length > 1 ? parts[parts.length - 1] : null;
8 | };
9 |
10 | module.exports = {
11 | applicationUrl: 'https://build-tracker-demo.herokuapp.com',
12 | artifacts: ['src/*/dist/**/*.js', 'plugins/*/dist/**/*.js'],
13 | baseDir: repoRoot,
14 | cwd: repoRoot,
15 | filenameHash,
16 | buildUrlFormat: 'https://github.com/paularmstrong/build-tracker/commit/:revision',
17 | nameMapper: (fileName) => {
18 | const hash = filenameHash(fileName);
19 | let out = fileName.replace(/\.js$/, '').replace(/(plugins|src|dist)\//g, '');
20 | return hash ? out.replace(`.${hash}`, '') : out;
21 | },
22 | onCompare: ({ markdown }) => {
23 | console.log(markdown);
24 | return Promise.resolve();
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/demo/build-tracker.config.js:
--------------------------------------------------------------------------------
1 | const withPostgres = require('@build-tracker/plugin-with-postgres').default;
2 | const { BudgetLevel, BudgetType } = require('@build-tracker/types');
3 |
4 | module.exports = withPostgres({
5 | artifacts: {
6 | groups: [
7 | {
8 | name: 'Web App',
9 | artifactMatch: /^app\/client/,
10 | budgets: [{ level: BudgetLevel.WARN, sizeKey: 'gzip', type: BudgetType.SIZE, maximum: 153600 }],
11 | },
12 | ],
13 | },
14 | defaultBranch: 'main',
15 | pg: {
16 | connectionString: process.env.DATABASE_URL,
17 | ssl: true,
18 | },
19 | port: process.env.PORT,
20 | url: 'https://build-tracker-demo.herokuapp.com',
21 | });
22 |
--------------------------------------------------------------------------------
/demo/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd `dirname $0`/../
4 |
5 | node src/server/dist/index.js setup -c demo/build-tracker.config.js
6 |
--------------------------------------------------------------------------------
/demo/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd `dirname $0`/../
4 |
5 | node src/server/dist/index.js run -c demo/build-tracker.config.js
6 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Production
2 | /build
3 |
4 | # Generated files
5 | .docusaurus
6 | .cache-loader
7 |
8 | # Misc
9 | .env.local
10 | .env.development.local
11 | .env.test.local
12 | .env.production.local
13 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | ```
30 | $ GIT_USER= USE_SSH=true yarn deploy
31 | ```
32 |
33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
34 |
--------------------------------------------------------------------------------
/docs/docs/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | plugins: ['header'],
4 | overrides: [
5 | {
6 | files: ['*.md'],
7 | rules: {
8 | '@typescript-eslint/explicit-function-return-type': 'off',
9 | '@typescript-eslint/no-unused-vars': 'off'
10 | }
11 | }
12 | ]
13 | };
14 |
--------------------------------------------------------------------------------
/docs/docs/guides/guides.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: guides
3 | title: Guides
4 | sidebar_label: Guides
5 | ---
6 |
7 | > This page is currently just a placeholder.
8 | > It is a jumping point for guides and ideas for doing more with Build Tracker. If you have ideas for more, please help by [contributing](contributing)!
9 |
10 | - [Deploy to Heroku](heroku)
11 | - [GitHub Actions](github-actions)
12 | - [Continuous Integration](advanced-ci)
13 | - [Contributing](contributing)
14 |
--------------------------------------------------------------------------------
/docs/docs/guides/heroku.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: heroku
3 | title: Deploy to Heroku
4 | sidebar_label: Deploy to Heroku
5 | ---
6 |
7 | ## Prerequisites
8 |
9 | This documentation assumes that you have an active Heroku account.
10 |
11 | ## 1. Create a new repository from the template
12 |
13 | On GitHub, [create a new repository using the template](https://github.com/paularmstrong/build-tracker-heroku/generate) from [paularmstrong/build-tracker-heroku](https://github.com/paularmstrong/build-tracker-heroku).
14 |
15 | ## 2. Update the `build-tracker.config.js` file
16 |
17 | This is the `@build-tracker/server` configuration. The most important thing to do here is to set up your [performance budgets](/docs/budgets). There are also [more options](/docs/packages/server#configuration) available that you may want to set as well. It is not recommended to change any of the presets other than the `artifacts` option.
18 |
19 | ## 3. Deploy to Heroku
20 |
21 | If you are set up as a public GitHub repository, simply click the _Deploy to Heroku_ button at the top of the README.md file while viewing it on GitHub.
22 |
23 | If you are using a private repository, update the `README.md` link to use the `?template=` argument, replacing the repository `my-org/my-repository` in this string with the correct organization and repository path:
24 |
25 | ```
26 | https://heroku.com/deploy?template=https://github.com/my-org/my-repository/tree/master
27 | ```
28 |
29 | Once that URL is updated, you may click it any time.
30 |
31 | ## 4. Configure on Heroku
32 |
33 | Follow the instructions provided on Heroku to deploy your Build Tracker application. Take special note that the `BT_URL` option must be provided with the same application name if you plan on using a herokuapp.com domain. If you are going to configure any other domain, enter that instead.
34 |
--------------------------------------------------------------------------------
/docs/docs/packages/api-errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: api-errors
3 | title: '@build-tracker/api-errors'
4 | sidebar_label: '@build-tracker/api-errors'
5 | ---
6 |
7 | This is a shared package for creating and comparing API errors returned from the Build Tracker server's API.
8 |
9 | ## 400 `ValidationError`
10 |
11 | The build that you are trying to insert into the database does not meet requirements. See the specific error message for more information.
12 |
13 | ## 401 `AuthError`
14 |
15 | If your server's API is protected with an API key and you do not provide it with requests requiring authentication, a 401 unauthorized response will be returned.
16 |
17 | ## 404 `NotFoundError`
18 |
19 | When querying one or more builds, you may find that they do not exist. This will result in a 404 not found error.
20 |
21 | ## 501 `UnimplementedError`
22 |
23 | If your API or API plugin does not support a method, a 501 unimplemented error will be returned.
24 |
--------------------------------------------------------------------------------
/docs/docs/packages/app.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: app
3 | title: '@build-tracker/app'
4 | sidebar_label: '@build-tracker/app'
5 | ---
6 |
7 | This package includes a pre-compiled client and server-side application that is intended for use only with the Build Tracker Server.
8 |
9 | To use the Build Tracker Application, please install [`@build-tracker/server`](packages/server.md).
10 |
--------------------------------------------------------------------------------
/docs/docs/packages/formatting.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: formatting
3 | title: '@build-tracker/formatting'
4 | sidebar_label: '@build-tracker/formatting'
5 | ---
6 |
7 | This package includes default formatting options for Build and Comparator data. nder most circumstances, these are internally shared and may not be needed for external integration purposes.
8 |
--------------------------------------------------------------------------------
/docs/docs/packages/types.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: types
3 | title: '@build-tracker/types'
4 | sidebar_label: '@build-tracker/types'
5 | ---
6 |
7 | This package includes types and enums for working with other Build Tracker packages.
8 |
9 | # Exports
10 |
11 | ## `BudgetType`
12 |
13 | ```js
14 | import { BudgetType } from '@build-tracker/types';
15 | ```
16 |
17 | ### `BudgetType.SIZE`
18 |
19 | A size budget is an absolute limite on the total size of an artifact. This is the most simple budget, because passing or failing can be seen with a single build and no math.
20 |
21 | ### `BudgetType.DELTA`
22 |
23 | A budget delta is a limit on the amount of change allowed for one or more artifacts from one build to the next.
24 |
25 | | | First build | Second build | Δ (delta) |
26 | | ------- | ----------- | ------------ | --------- |
27 | | main.js | 40 KiB | 48 KiB | +8 KiB |
28 |
29 | ### `BudgetType.PERCENT_DELTA`
30 |
31 | The percent delta budget is a limit on the artifacts total size percent change from one build to the next. For this, we divide the delta by the size of the first build:
32 |
33 | > Note: this is a simplified version of the formula that does not account for edge cases
34 |
35 | ```js
36 | function percentDelta(firstSize, secondSize) {
37 | return (secondSize - firstSize) / firstSize;
38 | }
39 |
40 | percentDelta(40, 48);
41 | ```
42 |
43 | | | First build | Second build | Δ% (percent delta) |
44 | | ------- | ----------- | ------------ | ------------------ |
45 | | main.js | 40 KiB | 48 KiB | + 20% |
46 |
47 | ## `BudgetType`
48 |
49 | ```js
50 | import { BudgetType } from '@build-tracker/types';
51 | ```
52 |
53 | ### `BudgetLevel.ERROR`
54 |
55 | ### `BudgetLevel.WARN`
56 |
--------------------------------------------------------------------------------
/docs/docs/plugins/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: plugins
3 | title: Plugins
4 | ---
5 |
6 | Plugins are community-developed config composers that make running your server simple. Most plugins remove the need to develop custom database integrations.
7 |
8 | - Database Integrations
9 | - Postgres
10 | - [@build-tracker/plugin-with-postgres](/docs/plugins/withPostgres)
11 | - MariaDB
12 | - [@build-tracker/plugin-with-mariadb](/docs/plugins/withMariadb)
13 | - MySQL
14 | - [@build-tracker/plugin-with-mysql](/docs/plugins/withMysql)
15 |
--------------------------------------------------------------------------------
/docs/docs/plugins/with-mariadb.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: withMariadb
3 | title: MariaDB
4 | ---
5 |
6 | Connecting your Build Tracker application to a Maria database is easy with the help of `@build-tracker/plugin-with-mariadb`
7 |
8 | ## Installation
9 |
10 | ```sh
11 | yarn add @build-tracker/plugin-with-mariadb@latest
12 | # or
13 | npm install --save @build-tracker/plugin-with-mariadb@latest
14 | ```
15 |
16 | ## Configuration
17 |
18 | Edit your `build-tracker.config.js` file and compose your output configuration:
19 |
20 | ```js
21 | const withMariadb = require('@build-tracker/plugin-with-mariadb');
22 |
23 | module.exports = withMariadb({
24 | mariadb: {
25 | user: '', // default: process.env.MARIAUSER
26 | host: '', // default: process.env.MARIAHOST
27 | database: '', // default: process.env.MARIADATABASE
28 | password: '', // default: process.env.MARIAPASSWORD
29 | port: 3306, // default: process.env.MARIAPORT
30 | },
31 | });
32 | ```
33 |
34 | All configuration options that are able to fall back on `process.env` environment variables can be written to your systems `ENV` or to a local `.env` file via [dotenv](https://github.com/motdotla/dotenv#readme).
35 |
36 | ### `host: string = process.env.MARIAHOST`
37 |
38 | Database host.
39 |
40 | ### `database: string = process.env.MARIAPASSWORD`
41 |
42 | Database name.
43 |
44 | ### `user: string = process.env.MARIAUSER`
45 |
46 | Database username with read access.
47 |
48 | ### `password: string = process.env.MARIADATABASE`
49 |
50 | Password for the given database username.
51 |
52 | ### `port: number = process.env.MARIAPORT = 3306`
53 |
54 | Database host port.
55 |
--------------------------------------------------------------------------------
/docs/docs/plugins/with-mysql.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: withMysql
3 | title: MySQL
4 | ---
5 |
6 | Connecting your Build Tracker application to a MySQL database is easy with the help of `@build-tracker/plugin-with-mysql`
7 |
8 | ## Installation
9 |
10 | ```sh
11 | yarn add @build-tracker/plugin-with-mysql@latest
12 | # or
13 | npm install --save @build-tracker/plugin-with-mysql@latest
14 | ```
15 |
16 | ## Configuration
17 |
18 | Edit your `build-tracker.config.js` file and compose your output configuration:
19 |
20 | ```js
21 | const withMysql = require('@build-tracker/plugin-with-mysql');
22 |
23 | module.exports = withMysql({
24 | mysql: {
25 | user: '', // default: process.env.MYSQLUSER
26 | host: '', // default: process.env.MYSQLHOST
27 | database: '', // default: process.env.MYSQLDATABASE
28 | password: '', // default: process.env.MYSQLPASSWORD
29 | port: 3306, // default: process.env.MYSQLPORT
30 | },
31 | });
32 | ```
33 |
34 | All configuration options that are able to fall back on `process.env` environment variables can be written to your systems `ENV` or to a local `.env` file via [dotenv](https://github.com/motdotla/dotenv#readme).
35 |
36 | ### `host: string = process.env.MYSQLHOST`
37 |
38 | Database host.
39 |
40 | ### `database: string = process.env.MYSQLPASSWORD`
41 |
42 | Database name.
43 |
44 | ### `user: string = process.env.MYSQLUSER`
45 |
46 | Database username with read access.
47 |
48 | ### `password: string = process.env.MYSQLDATABASE`
49 |
50 | Password for the given database username.
51 |
52 | ### `port: number = process.env.MYSQLPORT = 3306`
53 |
54 | Database host port.
55 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/docs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "docusaurus start",
7 | "build": "docusaurus build",
8 | "swizzle": "docusaurus swizzle",
9 | "deploy": "docusaurus deploy"
10 | },
11 | "dependencies": {
12 | "@docusaurus/core": "2.0.0-alpha.66",
13 | "@docusaurus/preset-classic": "2.0.0-alpha.66",
14 | "@mdx-js/react": "^1.5.8",
15 | "clsx": "^1.1.1",
16 | "react": "^16.8.4",
17 | "react-dom": "^16.8.4"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | docs: {
3 | 'Getting Started': ['installation', 'budgets'],
4 | Guides: ['guides/guides', 'guides/heroku', 'guides/github-actions', 'guides/advanced-ci', 'guides/contributing'],
5 | Packages: [
6 | 'packages/api-client',
7 | 'packages/api-errors',
8 | 'packages/app',
9 | 'packages/build',
10 | 'packages/cli',
11 | 'packages/comparator',
12 | 'packages/formatting',
13 | 'packages/server',
14 | 'packages/types',
15 | ],
16 | Plugins: ['plugins/plugins', 'plugins/withPostgres', 'plugins/withMariadb', 'plugins/withMysql'],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #12346c;
10 | --ifm-color-primary-dark: #0f2b59;
11 | --ifm-color-primary-darker: #0c2245;
12 | --ifm-color-primary-darkest: #091832;
13 | --ifm-color-primary-light: #3d5886;
14 | --ifm-color-primary-lighter: #687da1;
15 | --ifm-color-primary-lightest: #93a2bc;
16 | --ifm-code-font-size: 95%;
17 |
18 | --ifm-color-secondary: #a50011;
19 | --ifm-color-secondary-dark: #88000e;
20 | --ifm-color-secondary-darker: #6a000b;
21 | --ifm-color-secondary-darkest: #4b0008;
22 | --ifm-color-secondary-light: #b52e3c;
23 | --ifm-color-secondary-lighter: #c55c67;
24 | --ifm-color-secondary-lightest: #d68b92;
25 | }
26 |
27 | [data-theme='dark'] {
28 | --ifm-color-primary: var(--ifm-color-primary-lighter);
29 | }
30 |
31 | [data-theme='dark'] .hero.hero--primary {
32 | background-color: #12346c;
33 | }
34 |
35 | .docusaurus-highlight-code-line {
36 | background-color: rgb(72, 77, 91);
37 | display: block;
38 | margin: 0 calc(-1 * var(--ifm-pre-padding));
39 | padding: 0 var(--ifm-pre-padding);
40 | }
41 |
--------------------------------------------------------------------------------
/docs/src/pages/styles.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 | .heroBanner {
6 | padding: 4rem 0;
7 | text-align: start;
8 | position: relative;
9 | overflow: hidden;
10 | }
11 |
12 | @media screen and (max-width: 966px) {
13 | .heroBanner {
14 | padding: 2rem;
15 | }
16 | .heroContainer {
17 | flex-direction: column;
18 | }
19 | }
20 |
21 | .buttons {
22 | display: flex;
23 | justify-content: space-around;
24 | }
25 |
26 | .features {
27 | display: flex;
28 | align-items: center;
29 | padding: 2rem 0;
30 | width: 100%;
31 | }
32 |
33 | .featureImage {
34 | height: 200px;
35 | width: 200px;
36 | }
37 |
38 | .heroFirstLine {
39 | word-wrap: nowrap;
40 | display: flex;
41 | }
42 |
43 | .heroAmpersand {
44 | color: var(--ifm-color-primary-lightest);
45 | }
46 |
47 | .heroContainer {
48 | display: flex;
49 | }
50 |
51 | .heroBloat {
52 | color: var(--ifm-color-secondary-lighter);
53 | font-weight: 900;
54 | }
55 |
56 | .heroBoxes {
57 | flex-grow: 0.5;
58 | display: flex;
59 | justify-content: center;
60 | }
61 |
--------------------------------------------------------------------------------
/docs/static/CNAME:
--------------------------------------------------------------------------------
1 | buildtracker.dev
2 |
--------------------------------------------------------------------------------
/docs/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/docs/static/img/favicon.png
--------------------------------------------------------------------------------
/docs/static/img/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/docs/static/img/favicon/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/github-action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/docs/static/img/github-action.png
--------------------------------------------------------------------------------
/docs/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/docs/static/img/logo.png
--------------------------------------------------------------------------------
/docs/static/img/ogImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/docs/static/img/ogImage.png
--------------------------------------------------------------------------------
/greenkeeper.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": {
3 | "repo": {
4 | "packages": ["package.json"]
5 | },
6 | "packages": {
7 | "packages": [
8 | "src/app/package.json",
9 | "src/build/package.json",
10 | "src/comparator/package.json",
11 | "src/formatting/package.json",
12 | "src/server/package.json",
13 | "src/types/package.json"
14 | ]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/husky.config.js:
--------------------------------------------------------------------------------
1 | const runYarnLock = 'yarn install --frozen-lockfile';
2 |
3 | module.exports = {
4 | hooks: {
5 | 'post-checkout': `if [[ $HUSKY_GIT_PARAMS =~ 1$ ]]; then ${runYarnLock}; fi`,
6 | 'post-merge': runYarnLock,
7 | 'post-rebase': 'yarn install',
8 | 'pre-commit': 'yarn tsc && yarn lint-staged',
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projects: ['/src/*', '/plugins/*'],
3 | testPathIgnorePatterns: ['src'],
4 | };
5 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmClient": "yarn",
3 | "useWorkspaces": true,
4 | "version": "1.0.0",
5 | "packages": [
6 | "src/*",
7 | "plugins/*"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '*.{ts,tsx,js,jsx,json,md}': ['yarn lint', 'git add'],
3 | '*.{ts,tsx}': ['jest --bail --findRelatedTests'],
4 | };
5 |
--------------------------------------------------------------------------------
/plugins/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | plugins: ['header'],
4 | rules: {
5 | 'header/header': ['error', path.join(__dirname, '../config/copyright.txt')]
6 | },
7 | overrides: [{ files: ['*.md', '*.json'], rules: { 'header/header': 'off' } }]
8 | };
9 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/plugin-with-mariadb
2 |
3 | > A server-configuration plugin for Build Tracker to enable reading build data from a MariaDB database.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/plugins/withMariadb) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMariadb from './dist';
5 | export * from './dist';
6 | export default withMariadb;
7 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMariadb from './src';
5 | export * from './src';
6 | export default withMariadb;
7 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'with-mariadb',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/plugin-with-mariadb",
3 | "version": "1.0.0",
4 | "description": "Build Tracker server plugin for MariaDB",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "dependencies": {
22 | "@build-tracker/api-errors": "^1.0.0",
23 | "@build-tracker/build": "^1.0.0",
24 | "@build-tracker/server": "^1.0.0",
25 | "@types/dotenv": "^6.1.0",
26 | "dotenv": "^7.0.0",
27 | "mariadb": "^2.0.5"
28 | },
29 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
30 | "publishConfig": {
31 | "access": "public"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMariadb from '../';
5 |
6 | const url = 'https://build-tracker.local';
7 |
8 | jest.mock('mariadb');
9 |
10 | describe('withMariadb', () => {
11 | test('preserves user-set config', () => {
12 | expect(withMariadb({ mariadb: {}, port: 1234, url })).toMatchObject({ port: 1234 });
13 | });
14 |
15 | test('adds setup', () => {
16 | expect(withMariadb({ mariadb: {}, url })).toHaveProperty('setup');
17 | });
18 |
19 | test('adds queries', () => {
20 | expect(withMariadb({ mariadb: {}, url })).toMatchObject({
21 | queries: {
22 | build: {
23 | byRevision: expect.any(Function),
24 | insert: expect.any(Function),
25 | },
26 | builds: {
27 | byRevisions: expect.any(Function),
28 | byRevisionRange: expect.any(Function),
29 | byTimeRange: expect.any(Function),
30 | },
31 | },
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/src/__tests__/setup.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Mariadb from 'mariadb';
5 | import setup from '../setup';
6 |
7 | describe('withMariadb setup', () => {
8 | let query, release, setupFn;
9 | beforeEach(() => {
10 | query = jest.fn();
11 | release = jest.fn();
12 | // @ts-ignore
13 | jest.spyOn(Mariadb, 'createPool').mockImplementation(() => ({
14 | // @ts-ignore
15 | getConnection: () => Promise.resolve({ query, release }),
16 | }));
17 |
18 | setupFn = setup(Mariadb.createPool({}));
19 | });
20 |
21 | test('creates the table if not exists', async () => {
22 | await expect(setupFn()).resolves.toBe(true);
23 | expect(query).toHaveBeenCalledWith(expect.stringMatching('CREATE TABLE IF NOT EXISTS builds'));
24 | });
25 |
26 | test('creates a multi-index', async () => {
27 | await expect(setupFn()).resolves.toBe(true);
28 | expect(query).toHaveBeenCalledWith(
29 | 'CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)'
30 | );
31 | });
32 |
33 | test('creates an index on timestamp', async () => {
34 | await expect(setupFn()).resolves.toBe(true);
35 | expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
36 | });
37 |
38 | test('alters the branch column to length 256', async () => {
39 | await expect(setupFn()).resolves.toBe(true);
40 | expect(query).toHaveBeenCalledWith('ALTER TABLE builds MODIFY branch VARCHAR(256)');
41 | });
42 |
43 | test('releases the client on complete', async () => {
44 | await expect(setupFn()).resolves.toBe(true);
45 | expect(release).toHaveBeenCalled();
46 | });
47 |
48 | test('releases the client on error', async () => {
49 | const error = new Error('tacos');
50 | query.mockReturnValueOnce(Promise.reject(error));
51 | await expect(setupFn()).rejects.toThrow(error);
52 | expect(release).toHaveBeenCalled();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { config } from 'dotenv';
5 | import Queries from './queries';
6 | import { ServerConfig } from '@build-tracker/server';
7 | import setup from './setup';
8 | import { createPool, PoolConfig } from 'mariadb';
9 |
10 | config();
11 | type Omit = Pick>;
12 |
13 | export default function withMariadb(config: Omit & { mariadb: PoolConfig }): ServerConfig {
14 | const { mariadb: mariaConfig } = config;
15 | const database = mariaConfig.database || process.env.MARIADATABASE || 'buildtracker';
16 |
17 | const pool = createPool({
18 | user: process.env.MARIAUSER,
19 | host: process.env.MARIAHOST,
20 | password: process.env.MARIAPASSWORD,
21 | port: process.env.MARIAPORT ? parseInt(process.env.MARIAPORT, 10) : 3306,
22 | ...mariaConfig,
23 | database,
24 | });
25 |
26 | const queries = new Queries(pool);
27 |
28 | return {
29 | ...config,
30 | setup: setup(pool),
31 | queries: {
32 | build: {
33 | byRevision: queries.getByRevision,
34 | insert: queries.insert,
35 | },
36 | builds: {
37 | byRevisions: queries.getByRevisions,
38 | byRevisionRange: queries.getByRevisionRange,
39 | byTimeRange: queries.getByTimeRange,
40 | recent: queries.getRecent,
41 | },
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/src/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Pool } from 'mariadb';
5 |
6 | export default function setup(pool: Pool): () => Promise {
7 | return async function setup(): Promise {
8 | const client = await pool.getConnection();
9 | try {
10 | await client.query(`
11 | CREATE TABLE IF NOT EXISTS builds(
12 | revision VARCHAR(64) PRIMARY KEY NOT NULL,
13 | branch VARCHAR(256) NOT NULL,
14 | parentRevision VARCHAR(64),
15 | timestamp INT NOT NULL,
16 | meta VARCHAR(1024),
17 | artifacts BLOB,
18 | CHECK (meta IS NULL OR JSON_VALID(meta))
19 | )`);
20 | await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)');
21 | await client.query('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
22 | await client.query('ALTER TABLE builds MODIFY branch VARCHAR(256)');
23 | } catch (err) {
24 | client.release();
25 | throw err;
26 | }
27 | client.release();
28 | return Promise.resolve(true);
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/plugins/with-mariadb/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/plugins/with-mysql/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/plugin-with-mysql
2 |
3 | > A server-configuration plugin for Build Tracker to enable reading build data from a MySQL database.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/plugins/withMysql) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/plugins/with-mysql/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMariaDB from './dist';
5 | export * from './dist';
6 | export default withMariaDB;
7 |
--------------------------------------------------------------------------------
/plugins/with-mysql/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMysql from './src';
5 | export * from './src';
6 | export default withMysql;
7 |
--------------------------------------------------------------------------------
/plugins/with-mysql/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'with-mysql',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/plugins/with-mysql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/plugin-with-mysql",
3 | "version": "1.0.0",
4 | "description": "Build Tracker server plugin for MariaDB",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "dependencies": {
22 | "@build-tracker/api-errors": "^1.0.0",
23 | "@build-tracker/build": "^1.0.0",
24 | "@build-tracker/server": "^1.0.0",
25 | "@types/dotenv": "^6.1.0",
26 | "@types/mysql": "^2.15.7",
27 | "dotenv": "^7.0.0",
28 | "mysql": "^2.17.1"
29 | },
30 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
31 | "publishConfig": {
32 | "access": "public"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/plugins/with-mysql/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withMysql from '../';
5 |
6 | const url = 'https://build-tracker.local';
7 |
8 | jest.mock('mysql');
9 |
10 | describe('withMysql', () => {
11 | test('preserves user-set config', () => {
12 | expect(withMysql({ mysql: {}, port: 1234, url })).toMatchObject({ port: 1234 });
13 | });
14 |
15 | test('adds setup', () => {
16 | expect(withMysql({ mysql: {}, url })).toHaveProperty('setup');
17 | });
18 |
19 | test('adds queries', () => {
20 | expect(withMysql({ mysql: {}, url })).toMatchObject({
21 | queries: {
22 | build: {
23 | byRevision: expect.any(Function),
24 | insert: expect.any(Function),
25 | },
26 | builds: {
27 | byRevisions: expect.any(Function),
28 | byRevisionRange: expect.any(Function),
29 | byTimeRange: expect.any(Function),
30 | },
31 | },
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/plugins/with-mysql/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { config } from 'dotenv';
5 | import Queries from './queries';
6 | import { ServerConfig } from '@build-tracker/server';
7 | import setup from './setup';
8 | import { createPool, PoolConfig } from 'mysql';
9 |
10 | config();
11 | type Omit = Pick>;
12 |
13 | export default function withMysql(config: Omit & { mysql: PoolConfig }): ServerConfig {
14 | const { mysql: mysqlConfig } = config;
15 | const database = mysqlConfig.database || process.env.MYSQLDATABASE || 'buildtracker';
16 |
17 | const pool = createPool({
18 | user: process.env.MYSQLUSER,
19 | host: process.env.MYSQLHOST,
20 | password: process.env.MYSQLPASSWORD,
21 | port: process.env.MYSQLPORT ? parseInt(process.env.MYSQLPORT, 10) : 3306,
22 | ...mysqlConfig,
23 | database,
24 | });
25 |
26 | const queries = new Queries(pool);
27 |
28 | return {
29 | ...config,
30 | setup: setup(pool),
31 | queries: {
32 | build: {
33 | byRevision: queries.getByRevision,
34 | insert: queries.insert,
35 | },
36 | builds: {
37 | byRevisions: queries.getByRevisions,
38 | byRevisionRange: queries.getByRevisionRange,
39 | byTimeRange: queries.getByTimeRange,
40 | recent: queries.getRecent,
41 | },
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/plugins/with-mysql/src/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Pool } from 'mysql';
5 |
6 | export default function setup(pool: Pool): () => Promise {
7 | return async function setup(): Promise {
8 | return new Promise((resolve) => {
9 | pool.getConnection((err, client) => {
10 | if (err) {
11 | if (client) {
12 | client.release();
13 | }
14 | throw err;
15 | }
16 | client.query(
17 | `
18 | CREATE TABLE IF NOT EXISTS builds(
19 | revision VARCHAR(64) PRIMARY KEY NOT NULL,
20 | branch VARCHAR(256) NOT NULL,
21 | parentRevision VARCHAR(64),
22 | timestamp INT NOT NULL,
23 | meta VARCHAR(1024),
24 | artifacts BLOB,
25 | CHECK (meta IS NULL OR JSON_VALID(meta))
26 | )`,
27 | (err) => {
28 | if (err) {
29 | client.release();
30 | throw err;
31 | }
32 | client.query('ALTER TABLE builds ADD INDEX (revision, parentRevision, branch)', (err) => {
33 | if (err) {
34 | client.release();
35 | throw err;
36 | }
37 | client.query('ALTER TABLE builds ADD INDEX (timestamp)', (err) => {
38 | if (err) {
39 | client.release();
40 | throw err;
41 | }
42 | client.query('ALTER TABLE builds MODIFY branch VARCHAR(256)', (err) => {
43 | if (err) {
44 | client.release();
45 | throw err;
46 | }
47 | client.release();
48 | resolve(true);
49 | });
50 | });
51 | });
52 | }
53 | );
54 | });
55 | });
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/plugins/with-mysql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/plugins/with-postgres/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/plugin-with-postgres
2 |
3 | > A server-configuration plugin for Build Tracker to enable reading build data from a PostgreSQL database.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/plugins/withPostgres) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/plugins/with-postgres/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withPostgres from './dist';
5 | export * from './dist';
6 | export default withPostgres;
7 |
--------------------------------------------------------------------------------
/plugins/with-postgres/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import withPostgres from './src';
5 | export * from './src';
6 | export default withPostgres;
7 |
--------------------------------------------------------------------------------
/plugins/with-postgres/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'with-postgres',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/plugins/with-postgres/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/plugin-with-postgres",
3 | "version": "1.0.0",
4 | "description": "Build Tracker server plugin for PostgreSQL",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "dependencies": {
22 | "@build-tracker/api-errors": "^1.0.0",
23 | "@build-tracker/build": "^1.0.0",
24 | "@build-tracker/server": "^1.0.0",
25 | "@types/dotenv": "^6.1.0",
26 | "@types/pg": "^7.4.13",
27 | "dotenv": "^7.0.0",
28 | "pg": "^7.8.2"
29 | },
30 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
31 | "publishConfig": {
32 | "access": "public"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/plugins/with-postgres/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Pool } from 'pg';
5 | import withPostgres from '../';
6 |
7 | jest.mock('pg');
8 |
9 | const url = 'https://build-tracker.local';
10 |
11 | describe('withPostgres', () => {
12 | beforeAll(() => {
13 | // @ts-ignore
14 | Pool.mockImplementation(() => {
15 | return {};
16 | });
17 | });
18 |
19 | test('preserves user-set config', () => {
20 | expect(withPostgres({ pg: {}, port: 1234, url })).toMatchObject({ port: 1234 });
21 | });
22 |
23 | test('adds setup', () => {
24 | expect(withPostgres({ pg: {}, url })).toHaveProperty('setup');
25 | });
26 |
27 | test('adds queries', () => {
28 | expect(withPostgres({ pg: {}, url })).toMatchObject({
29 | queries: {
30 | build: {
31 | byRevision: expect.any(Function),
32 | insert: expect.any(Function),
33 | },
34 | builds: {
35 | byRevisions: expect.any(Function),
36 | byRevisionRange: expect.any(Function),
37 | byTimeRange: expect.any(Function),
38 | },
39 | },
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/plugins/with-postgres/src/__tests__/setup.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Pool } from 'pg';
5 | import setup from '../setup';
6 |
7 | jest.mock('pg');
8 |
9 | describe('withPostgres', () => {
10 | let query, release, setupFn;
11 | beforeEach(() => {
12 | query = jest.fn();
13 | release = jest.fn();
14 | // @ts-ignore
15 | Pool.mockImplementation(() => ({
16 | connect: () => ({ query, release }),
17 | }));
18 |
19 | setupFn = setup(new Pool());
20 | });
21 |
22 | test('creates the table if not exists', async () => {
23 | await expect(setupFn()).resolves.toBe(true);
24 | expect(query).toHaveBeenCalledWith(expect.stringMatching('CREATE TABLE IF NOT EXISTS builds'));
25 | });
26 |
27 | test('creates a multi-index', async () => {
28 | await expect(setupFn()).resolves.toBe(true);
29 | expect(query).toHaveBeenCalledWith(
30 | 'CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)'
31 | );
32 | });
33 |
34 | test('creates an index on timestamp', async () => {
35 | await expect(setupFn()).resolves.toBe(true);
36 | expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
37 | });
38 |
39 | test('alters the branch column to length 256', async () => {
40 | await expect(setupFn()).resolves.toBe(true);
41 | expect(query).toHaveBeenCalledWith('ALTER TABLE builds ALTER COLUMN branch TYPE VARCHAR(256)');
42 | });
43 |
44 | test('releases the client on complete', async () => {
45 | await expect(setupFn()).resolves.toBe(true);
46 | expect(release).toHaveBeenCalled();
47 | });
48 |
49 | test('releases the client on error', async () => {
50 | const error = new Error('tacos');
51 | query.mockReturnValueOnce(Promise.reject(error));
52 | await expect(setupFn()).rejects.toEqual(error);
53 | expect(release).toHaveBeenCalled();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/plugins/with-postgres/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { config } from 'dotenv';
5 | import Queries from './queries';
6 | import { ServerConfig } from '@build-tracker/server';
7 | import setup from './setup';
8 | import { Pool, PoolConfig } from 'pg';
9 |
10 | config();
11 |
12 | type Omit = Pick>;
13 |
14 | export default function withPostgres(config: Omit & { pg: PoolConfig }): ServerConfig {
15 | const { pg: pgConfig } = config;
16 |
17 | const pool = new Pool(pgConfig);
18 |
19 | const queries = new Queries(pool);
20 |
21 | return {
22 | ...config,
23 | setup: setup(pool),
24 | queries: {
25 | build: {
26 | byRevision: queries.getByRevision,
27 | insert: queries.insert,
28 | },
29 | builds: {
30 | byRevisions: queries.getByRevisions,
31 | byRevisionRange: queries.getByRevisionRange,
32 | byTimeRange: queries.getByTimeRange,
33 | recent: queries.getRecent,
34 | },
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/plugins/with-postgres/src/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Pool } from 'pg';
5 |
6 | export default function setup(pool: Pool): () => Promise {
7 | return async function setup(): Promise {
8 | const client = await pool.connect();
9 | try {
10 | await client.query(`CREATE TABLE IF NOT EXISTS builds(
11 | revision char(64) PRIMARY KEY NOT NULL,
12 | branch char(256) NOT NULL,
13 | parentRevision char(64),
14 | timestamp int NOT NULL,
15 | meta jsonb NOT NULL,
16 | artifacts jsonb NOT NULL
17 | )`);
18 | await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)');
19 | await client.query('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
20 | await client.query('ALTER TABLE builds ALTER COLUMN branch TYPE VARCHAR(256)');
21 | } catch (err) {
22 | client.release();
23 | throw err;
24 | }
25 | client.release();
26 | return Promise.resolve(true);
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/with-postgres/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | plugins: ['header'],
4 | rules: {
5 | 'header/header': ['error', path.join(__dirname, '../config/copyright.txt')]
6 | },
7 | overrides: [{ files: ['*.md', '*.json'], rules: { 'header/header': 'off' } }]
8 | };
9 |
--------------------------------------------------------------------------------
/src/api-client/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/api-client
2 |
3 | > A suite of tools for interfacing with a [Build Tracker](https://buildtracker.dev) instance's API.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/api-client) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/api-client/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './dist';
5 |
--------------------------------------------------------------------------------
/src/api-client/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './src';
5 |
--------------------------------------------------------------------------------
/src/api-client/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'api-client',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/api-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/api-client",
3 | "version": "1.0.0",
4 | "description": "Build Tracker tool for reading build artifact sizes and uploading to your server",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "dependencies": {
22 | "@types/glob": "^7.1.1",
23 | "@types/node": "^11.10.4",
24 | "@types/yargs": "^15.0.0",
25 | "brotli-size": "^4.0.0",
26 | "cosmiconfig": "^7.0.0",
27 | "glob": "^7.1.3",
28 | "gzip-size": "^5.0.0",
29 | "path-to-regexp": "3.0.0",
30 | "yargs": "^15.0.0"
31 | },
32 | "devDependencies": {
33 | "mock-spawn": "^0.2.6",
34 | "nock": "^10.0.6"
35 | },
36 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
37 | "publishConfig": {
38 | "access": "public"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/api-client/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 |
5 | export { default as createBuild } from './modules/create-build';
6 | export { default as getBuild } from './modules/get-build';
7 | export { Config, default as getConfig } from './modules/config';
8 | export { default as statArtifacts } from './modules/stat-artifacts';
9 | export { default as uploadBuild } from './modules/upload-build';
10 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/__tests__/config.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as path from 'path';
5 | import getConfig from '../config';
6 |
7 | const commonjsConfig = require(path.join(
8 | path.dirname(require.resolve('@build-tracker/fixtures')),
9 | 'cli-configs/commonjs/build-tracker.config.js'
10 | ));
11 | const rcConfig = require(path.join(
12 | path.dirname(require.resolve('@build-tracker/fixtures')),
13 | 'cli-configs/rc/.build-trackerrc.js'
14 | ));
15 |
16 | describe('getConfig', () => {
17 | test('found via cosmiconfig when not provided', () => {
18 | jest
19 | .spyOn(process, 'cwd')
20 | .mockReturnValue(path.join(path.dirname(require.resolve('@build-tracker/fixtures')), 'cli-configs/commonjs'));
21 | return getConfig().then((result) => {
22 | expect(result).toEqual(commonjsConfig);
23 | });
24 | });
25 |
26 | test('loaded via cosmiconfig when provided', () => {
27 | return getConfig(
28 | path.join(path.dirname(require.resolve('@build-tracker/fixtures')), 'cli-configs/rc/.build-trackerrc.js')
29 | ).then((result) => {
30 | expect(result).toMatchObject(rcConfig);
31 | });
32 | });
33 |
34 | test('propagate errors', () => {
35 | return getConfig(
36 | path.join(path.dirname(require.resolve('@build-tracker/fixtures')), 'cli-configs/rc/.build-trackerrc-invalid.js')
37 | ).catch((e) => {
38 | expect(e.message).toMatch('test');
39 | });
40 | });
41 |
42 | test('throws if no configuration found', () => {
43 | return getConfig('tacos').catch((e) => {
44 | expect(e.message).toMatch('Could not find configuration file');
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/__tests__/get-build.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import config from '@build-tracker/fixtures/cli-configs/rc/.build-trackerrc.js';
5 | const fakeBuild = require('@build-tracker/fixtures/builds-medium/1bf811ec249c2b13a314e127312b2d760f6658e2.json');
6 | import getBuild from '../get-build';
7 | import nock from 'nock';
8 | import { NotFoundError } from '@build-tracker/api-errors';
9 |
10 | describe('getBuild', () => {
11 | afterEach(() => {
12 | nock.cleanAll();
13 | });
14 |
15 | test('exist', () => {
16 | expect(getBuild).toBeDefined();
17 | });
18 |
19 | test('when found returns build', async () => {
20 | nock(`${config.applicationUrl}/`).get('/api/build/1bf811ec249c2b13a314e127312b2d760f6658e2').reply(200, fakeBuild);
21 |
22 | await expect(getBuild(config, '1bf811ec249c2b13a314e127312b2d760f6658e2')).resolves.toMatchObject(fakeBuild);
23 | });
24 |
25 | test('when missing', async () => {
26 | nock(`${config.applicationUrl}/`).get('/api/build/blah').reply(404, new NotFoundError());
27 |
28 | await expect(getBuild(config, 'blah')).rejects.toThrow(Error);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/__tests__/readfile.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as brotliSize from 'brotli-size';
5 | import fs from 'fs';
6 | import gzipSize from 'gzip-size';
7 | import path from 'path';
8 | import readfile from '../readfile';
9 |
10 | const fixturePath = require.resolve('@build-tracker/fixtures');
11 | const main = path.join(path.dirname(fixturePath), 'cli-configs/fakedist/main.1234567.js');
12 |
13 | describe('readfile', () => {
14 | let brotliSizeMock;
15 | beforeEach(() => {
16 | // @ts-ignore
17 | jest.spyOn(fs, 'statSync').mockImplementationOnce(() => ({ size: 64 }));
18 | brotliSizeMock = jest.spyOn(brotliSize, 'sync').mockImplementationOnce(() => 49);
19 | jest.spyOn(gzipSize, 'sync').mockImplementationOnce(() => 73);
20 | });
21 |
22 | describe('hash', () => {
23 | test('defaults to an MD5 of the file', () => {
24 | expect(readfile(main).hash).toMatchInlineSnapshot(`"764196c430cf8a94c698b74b6dfdad71"`);
25 | });
26 |
27 | test('can get the hash from filename function', () => {
28 | expect(
29 | readfile(main, (fileName: string): string => {
30 | return fileName.split('.')[1];
31 | })
32 | ).toMatchObject({
33 | hash: '1234567',
34 | });
35 | });
36 | });
37 |
38 | describe('sizes', () => {
39 | test('returns a stat size', () => {
40 | expect(readfile(main).stat).toEqual(64);
41 | });
42 |
43 | test('returns a gzip size', () => {
44 | expect(readfile(main).gzip).toEqual(73);
45 | });
46 |
47 | test('returns a brotli size', () => {
48 | expect(readfile(main).brotli).toEqual(49);
49 | });
50 |
51 | test('if brotli throws, does not fail', () => {
52 | brotliSizeMock.mockReset().mockImplementationOnce(() => {
53 | throw new Error('not implemented');
54 | });
55 | expect(readfile(main)).not.toMatchObject({ brotli: expect.any(Number) });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/__tests__/stat-artifacts.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as brotliSize from 'brotli-size';
5 | import config from '@build-tracker/fixtures/cli-configs/rc/.build-trackerrc.js';
6 | import statArtifacts from '../stat-artifacts';
7 |
8 | describe('statArtifacts', () => {
9 | test('gets artifact sizes', () => {
10 | jest.spyOn(brotliSize, 'sync').mockImplementation(() => {
11 | throw new Error('disabled in test');
12 | });
13 | const artifacts = statArtifacts(config);
14 | expect(artifacts).toBeInstanceOf(Map);
15 | expect(artifacts.size).toEqual(3);
16 | expect(artifacts.get('../../fakedist/main.1234567.js')).toMatchInlineSnapshot(`
17 | Object {
18 | "gzip": 74,
19 | "hash": "764196c430cf8a94c698b74b6dfdad71",
20 | "stat": 65,
21 | }
22 | `);
23 | expect(artifacts.get('../../fakedist/test-folder/test-no-extension')).toMatchInlineSnapshot(`
24 | Object {
25 | "gzip": 54,
26 | "hash": "415dec15fc798bb79f499aeff00258fb",
27 | "stat": 34,
28 | }
29 | `);
30 | expect(artifacts.get('../../fakedist/vendor.js')).toMatchInlineSnapshot(`
31 | Object {
32 | "gzip": 83,
33 | "hash": "0b3bb1892728da2a8a5af73335a51f35",
34 | "stat": 83,
35 | }
36 | `);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { cosmiconfig } from 'cosmiconfig';
5 |
6 | export interface ApiReturn {
7 | comparatorData: string;
8 | summary: Array;
9 | }
10 |
11 | export interface Config {
12 | applicationUrl: string;
13 | artifacts: Array;
14 | baseDir?: string;
15 | cwd?: string;
16 | buildUrlFormat?: string;
17 | getFilenameHash?: (filename: string) => string | null;
18 | nameMapper?: (filename: string) => string;
19 | onCompare?: (data: ApiReturn) => Promise;
20 | }
21 |
22 | export default async function getConfig(path?: string): Promise {
23 | const explorer = cosmiconfig('build-tracker');
24 | let result;
25 | try {
26 | result = !path ? await explorer.search() : await explorer.load(path);
27 | } catch (e) {
28 | if (e.code === 'ENOENT') {
29 | throw new Error('Could not find configuration file');
30 | } else {
31 | throw e;
32 | }
33 | }
34 |
35 | return {
36 | baseDir: process.cwd(),
37 | cwd: process.cwd(),
38 | ...result.config,
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/readfile.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as brotliSize from 'brotli-size';
5 | import crypto from 'crypto';
6 | import fs from 'fs';
7 | import gzipSize from 'gzip-size';
8 |
9 | interface SizeDef {
10 | brotli?: number;
11 | gzip: number;
12 | hash: string;
13 | stat: number;
14 | }
15 |
16 | type GetFileNameHash = (fileName: string) => string | void;
17 | const defaultFilenameHash: GetFileNameHash = (): string | void => null;
18 |
19 | export default function readfile(filePath: string, getFilenameHash: GetFileNameHash = defaultFilenameHash): SizeDef {
20 | const stat = fs.statSync(filePath).size;
21 | const contents = fs.readFileSync(filePath);
22 | const gzip = gzipSize.sync(contents);
23 |
24 | let hash = getFilenameHash(filePath);
25 | if (!hash) {
26 | const md5sum = crypto.createHash('md5');
27 | md5sum.update(contents);
28 | hash = md5sum.digest('hex');
29 | }
30 |
31 | const output: SizeDef = { stat, gzip, hash };
32 |
33 | try {
34 | output.brotli = brotliSize.sync(contents);
35 | } catch (e) {}
36 |
37 | return output;
38 | }
39 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/spawn.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { spawn as nodeSpawn, SpawnOptions } from 'child_process';
5 |
6 | export default function spawn(command: string, args?: ReadonlyArray, options?: SpawnOptions): Promise {
7 | return new Promise((resolve, reject) => {
8 | const proc = nodeSpawn(command, args, options);
9 | const errors = {
10 | spawn: null,
11 | stdout: null,
12 | stderr: null,
13 | };
14 |
15 | const stderr: Array = [];
16 | const stdout: Array = [];
17 |
18 | proc.on('error', (error) => {
19 | errors.spawn = error;
20 | });
21 |
22 | proc.stdout.on('error', (error) => {
23 | errors.stdout = error;
24 | });
25 |
26 | proc.stderr.on('error', (error) => {
27 | errors.stderr = error;
28 | });
29 |
30 | proc.stderr.on('data', (data) => {
31 | stderr.push(data);
32 | });
33 |
34 | proc.stdout.on('data', (data) => {
35 | stdout.push(data);
36 | });
37 |
38 | proc.on('close', (code) => {
39 | if (code !== 0) {
40 | const invocation = `${command} ${(args || []).join(' ')}`;
41 | return reject({ code, stderr: Buffer.concat(stderr).toString(), errors, invocation });
42 | }
43 | resolve(Buffer.concat(stdout));
44 | });
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/api-client/src/modules/stat-artifacts.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Config } from './config';
5 | import glob from 'glob';
6 | import path from 'path';
7 | import readfile from './readfile';
8 |
9 | export interface Stat {
10 | hash: string;
11 | stat: number;
12 | gzip: number;
13 | brotli: number;
14 | }
15 |
16 | const defaultNameMapper = (fileName: string): string => fileName;
17 |
18 | export default function statArtifacts(config: Config): Map {
19 | const { artifacts: artifactGlobs, baseDir, cwd, getFilenameHash, nameMapper = defaultNameMapper } = config;
20 |
21 | const artifacts = new Map();
22 |
23 | artifactGlobs.forEach((fileGlob) => {
24 | glob.sync(path.resolve(cwd, fileGlob), { nodir: true }).forEach((filePath) => {
25 | const sizes = readfile(filePath, getFilenameHash);
26 | artifacts.set(nameMapper(path.relative(baseDir, filePath).replace(`.${sizes.hash}`, '')), sizes);
27 | });
28 | }, []);
29 |
30 | return artifacts;
31 | }
32 |
--------------------------------------------------------------------------------
/src/api-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/api-errors/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/api-errors
2 |
3 | > A comprehensive list of error types that may be returned from a [Build Tracker](https://buildtracker.dev) instance's API.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/api-errors) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/api-errors/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './dist';
5 |
--------------------------------------------------------------------------------
/src/api-errors/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './src';
5 |
--------------------------------------------------------------------------------
/src/api-errors/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | displayName: 'api-errors',
6 | testEnvironment: 'node',
7 | resetMocks: true,
8 | rootDir: './',
9 | roots: ['/src'],
10 | transform: {
11 | '^.+\\.ts$': 'ts-jest',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/api-errors/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/api-errors",
3 | "version": "1.0.0",
4 | "description": "Build Tracker API errors",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
22 | "publishConfig": {
23 | "access": "public"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/api-errors/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 |
5 | export class AuthError extends Error {
6 | public readonly status = 401;
7 |
8 | public constructor(message?: string) {
9 | super(`Unauthorized access${message ? `: ${message}` : ''}`);
10 | Object.setPrototypeOf(this, AuthError.prototype);
11 | }
12 | }
13 |
14 | export class NotFoundError extends Error {
15 | public readonly status = 404;
16 |
17 | public constructor(message?: string) {
18 | super(`No builds found${message ? `: ${message}` : ''}`);
19 | Object.setPrototypeOf(this, NotFoundError.prototype);
20 | }
21 | }
22 |
23 | export class UnimplementedError extends Error {
24 | public readonly status = 501;
25 |
26 | public constructor(message?: string) {
27 | super(`Method not implemented${message ? `: ${message}` : ''}`);
28 | Object.setPrototypeOf(this, UnimplementedError.prototype);
29 | }
30 | }
31 |
32 | export class ValidationError extends Error {
33 | public readonly status = 400;
34 |
35 | public constructor(field: string, expected: string, received: string | number | void) {
36 | super(`"${field}" expected to receive "${expected}", but value was "${received}"`);
37 | Object.setPrototypeOf(this, ValidationError.prototype);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/api-errors/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/app
2 |
3 | > A React.js client-side application for [Build Tracker](https://buildtracker.dev).
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/app) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/app/config/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-console': 'off'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/config/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const path = require('path');
5 |
6 | module.exports = {
7 | SRC_ROOT: path.join(__dirname, '..', 'src'),
8 | DIST_ROOT: path.join(__dirname, '..', 'dist'),
9 | IS_PROD: process.env.NODE_ENV === 'production',
10 | };
11 |
--------------------------------------------------------------------------------
/src/app/config/devserver.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const fakeBuild = require('@build-tracker/fixtures/builds-medium/01141f29743fb2bdd7e176cf919fc964025cea5a.json');
5 |
6 | module.exports = {
7 | dev: true,
8 | port: 3000,
9 | queries: {
10 | build: {
11 | byRevision: () => Promise.resolve(fakeBuild),
12 | },
13 | builds: {
14 | byRevisions: () => Promise.resolve([fakeBuild]),
15 | byRevisionRange: () => Promise.resolve([fakeBuild]),
16 | byTimeRange: () => Promise.resolve([fakeBuild]),
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/app/config/jest/fileMock.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = 'test-file-stub';
5 |
--------------------------------------------------------------------------------
/src/app/config/jest/setup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | process.on('unhandledRejection', (error) => {
5 | console.error(error);
6 | });
7 |
8 | const ignoredErrors = [
9 | /An update to [^ ]+ inside a test was not wrapped in act/,
10 | /Warning: useLayoutEffect does nothing on the server/,
11 | ];
12 |
13 | console.error = (...args) => {
14 | if (ignoredErrors.some((err) => err.test(args[0]))) {
15 | return;
16 | }
17 | throw new Error(`Console error, ${args.join(' ')}`);
18 | };
19 |
20 | const ignoredWarnings = [/Warning: \w+ has been renamed, and is not recommended for use. See https:\/\/fb\.me/];
21 | const consoleWarn = console.warn.bind(console);
22 |
23 | console.warn = (...args) => {
24 | if (ignoredWarnings.some((warning) => warning.test(args[0]))) {
25 | return;
26 | }
27 | consoleWarn(...args);
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/config/webpack-progress.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const chalk = require('chalk');
5 |
6 | const clearOutput = () => {
7 | process.stdout.write('\x1B[2J\x1B[0f');
8 | };
9 |
10 | module.exports = (port) => ({
11 | change: (context, { shortPath }) => {
12 | clearOutput();
13 | console.log(`⏱ ${shortPath} changed. Rebuilding…`);
14 | },
15 |
16 | done: (context, { stats }) => {
17 | const info = stats.toJson();
18 |
19 | if (stats.hasErrors()) {
20 | info.errors.forEach((error) => {
21 | console.error(error);
22 | });
23 | }
24 | },
25 |
26 | afterAllDone: (context) => {
27 | const { hasErrors, message, name } = context.state;
28 |
29 | if (hasErrors) {
30 | console.log(`\n 🚨 Server is ready on https://localhost:${port}, but may not work correctly\n`);
31 | console.error(`${chalk.red(name)}: ${message}`);
32 | } else {
33 | clearOutput();
34 | console.log(`\n 🚀 Server is ready on https://localhost:${port}\n`);
35 | }
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/src/app/config/webpack-server.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const path = require('path');
5 | const webpack = require('webpack');
6 | const WebpackBar = require('webpackbar');
7 |
8 | const { DIST_ROOT, IS_PROD, SRC_ROOT } = require('./constants');
9 |
10 | module.exports = (env, reporter) => ({
11 | name: 'server',
12 | target: 'node',
13 | entry: path.join(SRC_ROOT, 'server/index.tsx'),
14 | mode: IS_PROD ? 'production' : 'development',
15 | devServer: {
16 | contentBase: SRC_ROOT,
17 | hot: true,
18 | noInfo: true,
19 | },
20 | devtool: IS_PROD ? false : 'cheap-module-eval-source-map',
21 | module: {
22 | rules: [
23 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
24 | {
25 | test: /\.(png)$/i,
26 | loader: require.resolve('file-loader'),
27 | options: { outputPath: '../client/static', publicPath: '/client/static', name: '[name].[hash:8].[ext]' },
28 | },
29 | ],
30 | },
31 | resolve: {
32 | alias: {
33 | 'react-native$': 'react-native-web',
34 | },
35 | extensions: ['.tsx', '.ts', '.js'],
36 | },
37 | node: {
38 | global: false,
39 | __dirname: true,
40 | },
41 | optimization: {
42 | minimize: false,
43 | sideEffects: true,
44 | splitChunks: {},
45 | },
46 | output: {
47 | filename: '[name].js',
48 | libraryTarget: 'commonjs2',
49 | path: path.join(DIST_ROOT, 'server'),
50 | },
51 | plugins: [
52 | !IS_PROD &&
53 | reporter &&
54 | new WebpackBar({
55 | name: 'Server',
56 | color: 'blue',
57 | reporters: ['fancy', reporter],
58 | }),
59 | !IS_PROD && new webpack.HotModuleReplacementPlugin(),
60 | ].filter(Boolean),
61 | });
62 |
--------------------------------------------------------------------------------
/src/app/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const clientConfig = require('./webpack-client.config');
5 | const serverConfig = require('./webpack-server.config');
6 | const createReporter = require('./webpack-progress');
7 |
8 | module.exports = (env = {}) => {
9 | const reporter = env.port && createReporter(env.port);
10 | return [clientConfig(env, reporter), serverConfig(env, reporter)];
11 | };
12 |
--------------------------------------------------------------------------------
/src/app/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 |
--------------------------------------------------------------------------------
/src/app/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'app',
7 | moduleNameMapper: {
8 | '\\.png$': '/config/jest/fileMock.js',
9 | '^react-native$': require.resolve('react-native-web'),
10 | },
11 | rootDir: './',
12 | roots: ['/src'],
13 | setupFiles: ['react-native-web/jest/setup.js', '/config/jest/setup.js'],
14 | testEnvironment: 'jsdom',
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/src/app.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 |
5 | /* eslint-disable @typescript-eslint/no-explicit-any */
6 |
7 | declare module '*.png' {
8 | const value: any;
9 | export = value;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/src/client/Routes.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Builds from './routes/Builds';
5 | import Dates from './routes/Dates';
6 | import Latest from './routes/Latest';
7 | import React, { FunctionComponent } from 'react';
8 | import { Route, Switch } from 'react-router';
9 |
10 | const Routes: FunctionComponent<{}> = (): React.ReactElement => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Routes;
22 |
--------------------------------------------------------------------------------
/src/app/src/client/history.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
5 | import { createBrowserHistory, createMemoryHistory } from 'history';
6 |
7 | export default canUseDOM ? createBrowserHistory() : createMemoryHistory();
8 |
--------------------------------------------------------------------------------
/src/app/src/client/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { AppRegistry } from 'react-native';
5 | import history from './history';
6 | import Main from '../screens/Main';
7 | import makeStore from '../store';
8 | import { Provider } from 'react-redux';
9 | import React from 'react';
10 | import { Router } from 'react-router';
11 | import Routes from './Routes';
12 | import { searchParamsToStore } from '../store/utils';
13 |
14 | // @ts-ignore
15 | const initialProps = window.__PROPS__ || {};
16 |
17 | const store = makeStore({ ...initialProps, ...searchParamsToStore(window.location.search) });
18 |
19 | const App = (): React.ReactElement => (
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 |
28 | AppRegistry.registerComponent('App', () => App);
29 | AppRegistry.runApplication('App', {
30 | hydrate: true,
31 | initialProps: {},
32 | rootTag: document.getElementById('root'),
33 | });
34 |
--------------------------------------------------------------------------------
/src/app/src/client/routes/Builds.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from '@build-tracker/build';
5 | import { fetch } from 'cross-fetch';
6 | import { RouteComponentProps } from 'react-router';
7 | import { FetchState, State } from '../../store/types';
8 | import React, { FunctionComponent } from 'react';
9 | import { setBuilds, setFetchState } from '../../store/actions';
10 | import { useDispatch, useSelector } from 'react-redux';
11 |
12 | const Builds: FunctionComponent> = (props): React.ReactElement => {
13 | const { revisions } = props.match.params;
14 | const url = useSelector((state: State) => state.url);
15 | const dispatch = useDispatch();
16 |
17 | React.useEffect(() => {
18 | dispatch(setFetchState(FetchState.FETCHING));
19 | fetch(`${url}/api/builds/list/${revisions}`)
20 | .then((response) => response.json())
21 | .then((builds) => {
22 | if (!Array.isArray(builds)) {
23 | throw new Error('Bad response');
24 | }
25 | dispatch(setBuilds(builds.map((buildStruct) => new Build(buildStruct.meta, buildStruct.artifacts))));
26 | dispatch(setFetchState(FetchState.FETCHED));
27 | })
28 | .catch(() => {
29 | dispatch(setFetchState(FetchState.ERROR));
30 | });
31 | }, [dispatch, revisions, url]);
32 | return null;
33 | };
34 |
35 | export default Builds;
36 |
--------------------------------------------------------------------------------
/src/app/src/client/routes/Dates.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from '@build-tracker/build';
5 | import { fetch } from 'cross-fetch';
6 | import { RouteComponentProps } from 'react-router';
7 | import { FetchState, State } from '../../store/types';
8 | import React, { FunctionComponent } from 'react';
9 | import { setBuilds, setFetchState } from '../../store/actions';
10 | import { useDispatch, useSelector } from 'react-redux';
11 |
12 | const Dates: FunctionComponent> = (
13 | props
14 | ): React.ReactElement => {
15 | const { startTimestamp, endTimestamp } = props.match.params;
16 | const url = useSelector((state: State) => state.url);
17 | const dispatch = useDispatch();
18 |
19 | React.useEffect(() => {
20 | dispatch(setFetchState(FetchState.FETCHING));
21 | fetch(`${url}/api/builds/time/${startTimestamp}...${endTimestamp}`)
22 | .then((response) => response.json())
23 | .then((builds) => {
24 | if (!Array.isArray(builds)) {
25 | throw new Error('Bad response');
26 | }
27 | dispatch(setBuilds(builds.map((buildStruct) => new Build(buildStruct.meta, buildStruct.artifacts))));
28 | dispatch(setFetchState(FetchState.FETCHED));
29 | })
30 | .catch(() => {
31 | dispatch(setFetchState(FetchState.ERROR));
32 | });
33 | }, [dispatch, endTimestamp, startTimestamp, url]);
34 | return null;
35 | };
36 |
37 | export default Dates;
38 |
--------------------------------------------------------------------------------
/src/app/src/client/routes/Latest.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from '@build-tracker/build';
5 | import { fetch } from 'cross-fetch';
6 | import { RouteComponentProps } from 'react-router';
7 | import { FetchState, State } from '../../store/types';
8 | import React, { FunctionComponent } from 'react';
9 | import { setBuilds, setFetchState } from '../../store/actions';
10 | import { useDispatch, useSelector } from 'react-redux';
11 |
12 | const DEFAULT_LIMIT = '30';
13 |
14 | const Latest: FunctionComponent> = (props): React.ReactElement => {
15 | const { limit = DEFAULT_LIMIT } = props.match.params;
16 | const url = useSelector((state: State) => state.url);
17 | const dispatch = useDispatch();
18 |
19 | React.useEffect(() => {
20 | dispatch(setFetchState(FetchState.FETCHING));
21 | fetch(`${url}/api/builds/${limit}`)
22 | .then((response) => response.json())
23 | .then((builds) => {
24 | if (!Array.isArray(builds)) {
25 | throw new Error('Bad response');
26 | }
27 | dispatch(setBuilds(builds.map((buildStruct) => new Build(buildStruct.meta, buildStruct.artifacts))));
28 | dispatch(setFetchState(FetchState.FETCHED));
29 | })
30 | .catch(() => {
31 | dispatch(setFetchState(FetchState.ERROR));
32 | });
33 | }, [dispatch, limit, url]);
34 | return null;
35 | };
36 |
37 | export default Latest;
38 |
--------------------------------------------------------------------------------
/src/app/src/components/ColorScalePicker/ColorScale.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../../theme';
5 | import ColorScales from '../../modules/ColorScale';
6 | import RadioSelect from '../RadioSelect';
7 | import React from 'react';
8 | import { setColorScale } from '../../store/actions';
9 | import { useDispatch } from 'react-redux';
10 | import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
11 |
12 | interface Props {
13 | isSelected?: boolean;
14 | name: keyof typeof ColorScales;
15 | style?: StyleProp;
16 | }
17 |
18 | export const ColorScale = (props: Props): React.ReactElement => {
19 | const { isSelected, name, style } = props;
20 |
21 | const dispatch = useDispatch();
22 | const handleSelect = React.useCallback(
23 | (checked: boolean) => {
24 | if (checked) {
25 | dispatch(setColorScale(name));
26 | }
27 | },
28 | [dispatch, name]
29 | );
30 |
31 | const nativeID = `radio${`${name}`.replace(/^\w/g, '')}`;
32 |
33 | return (
34 |
35 |
36 | {name}
37 |
38 | );
39 | };
40 |
41 | const styles = StyleSheet.create({
42 | root: {
43 | cursor: 'pointer',
44 | flexDirection: 'row',
45 | alignItems: 'center',
46 | },
47 | radio: {
48 | marginEnd: Theme.Spacing.Small,
49 | },
50 | });
51 |
52 | export default ColorScale;
53 |
--------------------------------------------------------------------------------
/src/app/src/components/ColorScalePicker/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { ColorScalePicker } from '../';
5 | import mockStore from '../../../store/mock';
6 | import { Provider } from 'react-redux';
7 | import React from 'react';
8 | import { render } from 'react-native-testing-library';
9 |
10 | describe('ColorScalePicker', () => {
11 | test('sets the active scale to selected', () => {
12 | const { queryAllByProps } = render(
13 |
14 |
15 |
16 | );
17 | const selected = queryAllByProps({ isSelected: true });
18 | expect(selected).toHaveLength(1);
19 | expect(selected[0].props.name).toBe('Magma');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/src/components/ColorScalePicker/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../../theme';
5 | import ColorScale from './ColorScale';
6 | import ColorScales from '../../modules/ColorScale';
7 | import React from 'react';
8 | import { State } from '../../store/types';
9 | import { useSelector } from 'react-redux';
10 | import { StyleSheet, View } from 'react-native';
11 |
12 | export const ColorScalePicker = (): React.ReactElement => {
13 | const activeColorScale = useSelector((state: State) => ColorScales[state.colorScale]);
14 | return (
15 |
16 | {Object.entries(ColorScales).map(([name, scale]) => {
17 | return ;
18 | })}
19 |
20 | );
21 | };
22 |
23 | const styles = StyleSheet.create({
24 | root: {
25 | flexDirection: 'column',
26 | },
27 | scale: {
28 | marginBottom: Theme.Spacing.Small,
29 | },
30 | });
31 |
32 | export default React.memo(ColorScalePicker);
33 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/HeaderRow.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import RevisionCell from './RevisionCell';
6 | import RevisionDeltaCell from './RevisionDeltaCell';
7 | import { StyleSheet } from 'react-native';
8 | import TextCell from './TextCell';
9 | import { Tr } from './../Table';
10 | import { CellType, HeaderRow as HRow } from '@build-tracker/comparator';
11 |
12 | interface Props {
13 | onFocusRevision: (artifactName: string) => void;
14 | onRemoveRevision: (artifactName: string) => void;
15 | row: HRow;
16 | }
17 |
18 | export const HeaderRow = (props: Props): React.ReactElement => {
19 | const { onFocusRevision, onRemoveRevision, row } = props;
20 |
21 | const mapHeaderCell = (cell, i): React.ReactElement | void => {
22 | switch (cell.type) {
23 | case CellType.TEXT:
24 | return ;
25 | case CellType.REVISION:
26 | return (
27 |
34 | );
35 | case CellType.REVISION_DELTA:
36 | return (
37 |
42 | );
43 | }
44 | };
45 |
46 | return {row.map(mapHeaderCell)}
;
47 | };
48 |
49 | const styles = StyleSheet.create({
50 | headerCell: {
51 | backgroundColor: 'white',
52 | position: 'sticky',
53 | top: 0,
54 | zIndex: 4,
55 | height: 'calc(4rem - 1px)',
56 | },
57 | });
58 |
59 | export default React.memo(HeaderRow);
60 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/RevisionDeltaCell.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { RevisionDeltaCell as Cell } from '@build-tracker/comparator';
5 | import { formatSha } from '@build-tracker/formatting';
6 | import Hoverable from '../Hoverable';
7 | import React from 'react';
8 | import RelativeTooltip from '../RelativeTooltip';
9 | import { Th } from '../Table';
10 | import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
11 |
12 | interface Props {
13 | cell: Cell;
14 | style?: StyleProp;
15 | }
16 |
17 | export const RevisionDeltaCell = (props: Props): React.ReactElement => {
18 | const { againstRevision, deltaIndex, revision } = props.cell;
19 | const viewRef = React.useRef(null);
20 |
21 | return (
22 |
23 |
24 | {(isHovered) => (
25 |
26 | {`𝚫${deltaIndex}`}
27 | {isHovered ? (
28 |
32 | ) : null}
33 |
34 | )}
35 |
36 | |
37 | );
38 | };
39 |
40 | const styles = StyleSheet.create({
41 | delta: {
42 | fontWeight: 'bold',
43 | },
44 | });
45 |
46 | export default React.memo(RevisionDeltaCell);
47 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/TextCell.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { TextCell as Cell } from '@build-tracker/comparator';
5 | import React from 'react';
6 | import { StyleProp, Text, ViewStyle } from 'react-native';
7 | import { Td, Th } from './../Table';
8 |
9 | interface Props {
10 | cell: Cell;
11 | header?: boolean;
12 | style?: StyleProp;
13 | }
14 |
15 | export const TextCell = (props: Props): React.ReactElement => {
16 | const El = props.header ? Th : Td;
17 | return (
18 |
19 | {props.cell.text}
20 |
21 | );
22 | };
23 |
24 | export default React.memo(TextCell);
25 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/__tests__/RevisionDeltaCell.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { CellType } from '@build-tracker/comparator';
5 | import React from 'react';
6 | import { RevisionDeltaCell } from '../RevisionDeltaCell';
7 | import { fireEvent, render } from 'react-native-testing-library';
8 |
9 | describe('RevisionDeltaCell', () => {
10 | beforeEach(() => {
11 | // Enable the hover monitor
12 | document.dispatchEvent(new Event('mousemove'));
13 | });
14 |
15 | describe('tooltip', () => {
16 | test('mouse enter shows a tooltip', () => {
17 | const { getByTestId, queryAllByProps } = render(
18 |
21 | );
22 | fireEvent(getByTestId('delta'), 'mouseEnter');
23 | expect(queryAllByProps({ accessibilityRole: 'tooltip' })).toHaveLength(1);
24 | });
25 |
26 | test('mouse leave removes the tooltip', () => {
27 | const { getByTestId, queryAllByProps } = render(
28 |
31 | );
32 | fireEvent(getByTestId('delta'), 'mouseEnter');
33 | fireEvent(getByTestId('delta'), 'mouseLeave');
34 | expect(queryAllByProps({ accessibilityRole: 'tooltip' })).toHaveLength(0);
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/__tests__/TextCell.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { CellType } from '@build-tracker/comparator';
5 | import React from 'react';
6 | import { render } from 'react-native-testing-library';
7 | import { TextCell } from '../TextCell';
8 | import { Td, Th } from '../../Table';
9 |
10 | describe('TextCell', () => {
11 | describe('header', () => {
12 | test('renders as Th when true', () => {
13 | const { queryAllByType } = render();
14 | expect(queryAllByType(Th)).toHaveLength(1);
15 | expect(queryAllByType(Td)).toHaveLength(0);
16 | });
17 |
18 | test('renders as Td when false', () => {
19 | const { queryAllByType } = render();
20 | expect(queryAllByType(Th)).toHaveLength(0);
21 | expect(queryAllByType(Td)).toHaveLength(1);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/app/src/components/ComparisonTable/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './ComparisonTable';
5 | export { default } from './ComparisonTable';
6 |
--------------------------------------------------------------------------------
/src/app/src/components/Divider.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../theme';
5 | import React from 'react';
6 | import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
7 |
8 | interface Props {
9 | color?: string;
10 | style?: StyleProp;
11 | }
12 |
13 | const Divider = (props: Props): React.ReactElement => {
14 | const { color, style } = props;
15 | return ;
16 | };
17 |
18 | const styles = StyleSheet.create({
19 | root: {
20 | height: StyleSheet.hairlineWidth,
21 | overflow: 'hidden',
22 | backgroundColor: Theme.Color.Gray20,
23 | },
24 | });
25 |
26 | export default Divider;
27 |
--------------------------------------------------------------------------------
/src/app/src/components/DrawerLink.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../theme';
5 | import Hoverable from './Hoverable';
6 | import React from 'react';
7 | import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
8 |
9 | interface Props {
10 | href: string;
11 | icon?: React.ComponentType<{ style?: StyleProp }>;
12 | style?: StyleProp;
13 | text: string;
14 | }
15 |
16 | const DrawerLink = (props: Props): React.ReactElement => {
17 | const { href, icon: Icon = null, style, text } = props;
18 |
19 | return (
20 |
21 | {(isHovered) => {
22 | return (
23 |
29 |
30 | <>
31 | {Icon ? : null}
32 | {text}
33 | >
34 |
35 |
36 | );
37 | }}
38 |
39 | );
40 | };
41 |
42 | const styles = StyleSheet.create({
43 | root: {
44 | marginBottom: Theme.Spacing.Small,
45 | marginHorizontal: `calc(-1 * ${Theme.Spacing.Xsmall})`,
46 | borderRadius: Theme.BorderRadius.Normal,
47 | padding: Theme.Spacing.Xsmall,
48 | },
49 | rootHovered: {
50 | backgroundColor: Theme.Color.Primary00,
51 | },
52 | text: {
53 | fontSize: Theme.FontSize.Normal,
54 | },
55 | textHovered: {
56 | color: Theme.Color.Primary40,
57 | },
58 | icon: {
59 | marginEnd: Theme.Spacing.Small,
60 | },
61 | });
62 |
63 | export default DrawerLink;
64 |
--------------------------------------------------------------------------------
/src/app/src/components/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import { StyleSheet, Text, View } from 'react-native';
6 |
7 | interface Props {
8 | message: string;
9 | }
10 |
11 | const EmptyState = (props: Props): React.ReactElement => {
12 | const { message } = props;
13 | return (
14 |
15 | {message}
16 |
17 | );
18 | };
19 |
20 | const styles = StyleSheet.create({
21 | root: {
22 | flexGrow: 1,
23 | width: '100%',
24 | height: '100%',
25 | justifyContent: 'center',
26 | alignItems: 'center',
27 | },
28 | });
29 |
30 | export default EmptyState;
31 |
--------------------------------------------------------------------------------
/src/app/src/components/Graph/Offset.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export enum Offset {
5 | TOP = 20,
6 | RIGHT = 20,
7 | BOTTOM = 100,
8 | LEFT = 80,
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/src/components/Graph/XAxis.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { axisBottom } from 'd3-axis';
5 | import { formatSha } from '@build-tracker/formatting';
6 | import React from 'react';
7 | import { ScalePoint } from 'd3-scale';
8 | import { select } from 'd3-selection';
9 |
10 | interface Props {
11 | height: number;
12 | scale: ScalePoint;
13 | }
14 |
15 | const MAX_TICK_LABELS = 50;
16 |
17 | const XAxis = (props: Props): React.ReactElement => {
18 | const { height, scale } = props;
19 | const ref = React.useRef(null);
20 | const domainLength = scale.domain().length;
21 |
22 | React.useEffect(() => {
23 | const axis = axisBottom(scale).tickFormat((d, i) => {
24 | if (domainLength <= MAX_TICK_LABELS || i % Math.floor(domainLength / (MAX_TICK_LABELS / 2)) === 0) {
25 | return formatSha(d);
26 | }
27 | return '';
28 | });
29 |
30 | select(ref.current)
31 | .call(axis)
32 | .selectAll('text')
33 | .attr('y', 3)
34 | .attr('x', 6)
35 | .attr('transform', 'rotate(24)')
36 | .style('text-anchor', 'start');
37 | }, [domainLength, scale]);
38 |
39 | return ;
40 | };
41 |
42 | export default XAxis;
43 |
--------------------------------------------------------------------------------
/src/app/src/components/Graph/YAxis.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { axisLeft } from 'd3-axis';
5 | import { formatBytes } from '@build-tracker/formatting';
6 | import React from 'react';
7 | import { ScaleLinear } from 'd3-scale';
8 | import { select } from 'd3-selection';
9 |
10 | interface Props {
11 | scale: ScaleLinear;
12 | }
13 |
14 | const tickFormat = (d): string =>
15 | formatBytes(d.valueOf(), { formatter: (bytes: number, units: number): number => Math.round(bytes / units) });
16 |
17 | const YAxis = (props: Props): React.ReactElement => {
18 | const { scale } = props;
19 | const ref = React.useRef(null);
20 |
21 | React.useEffect(() => {
22 | const axis = axisLeft(scale).tickFormat(tickFormat);
23 | select(ref.current).call(axis);
24 | }, [scale]);
25 |
26 | return ;
27 | };
28 |
29 | export default YAxis;
30 |
--------------------------------------------------------------------------------
/src/app/src/components/Graph/__tests__/YAxis.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Axes from 'd3-axis';
5 | import * as Selection from 'd3-selection';
6 | import React from 'react';
7 | import { render } from '@testing-library/react';
8 | import { scaleLinear } from 'd3-scale';
9 | import YAxis from '../YAxis';
10 |
11 | const scale = scaleLinear().range([100, 0]).domain([0, 100]);
12 |
13 | describe('YAxis', () => {
14 | let mockCall, selectSpy;
15 | beforeEach(() => {
16 | mockCall = jest.fn();
17 | // @ts-ignore Just want to make sure call is called
18 | selectSpy = jest.spyOn(Selection, 'select').mockReturnValue({ call: mockCall });
19 | });
20 |
21 | test('creates a left axis', () => {
22 | render(
23 |
26 | );
27 |
28 | expect(selectSpy).toHaveBeenCalled();
29 | expect(mockCall).toHaveBeenCalled();
30 | });
31 |
32 | test('rounds ticks', () => {
33 | const mockTickFormat = jest.fn(() => '1');
34 | // @ts-ignore
35 | jest.spyOn(Axes, 'axisLeft').mockReturnValue({ tickFormat: mockTickFormat });
36 |
37 | render(
38 |
41 | );
42 |
43 | // @ts-ignore
44 | const formatter = mockTickFormat.mock.calls[0][0];
45 | // @ts-ignore
46 | expect(formatter(1234)).toEqual('1 KiB');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/app/src/components/Menu.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import MenuItem from './MenuItem';
5 | import React from 'react';
6 | import RelativeModal from './RelativeModal';
7 |
8 | interface Props extends React.ComponentProps {
9 | children: React.ReactElement | Array>;
10 | }
11 |
12 | const Menu = (props: Props): React.ReactElement => {
13 | return ;
14 | };
15 |
16 | export default Menu;
17 |
--------------------------------------------------------------------------------
/src/app/src/components/RelativeTooltip.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import Tooltip from './Tooltip';
6 | import { View } from 'react-native';
7 |
8 | interface Props {
9 | relativeTo: React.RefObject;
10 | text: string;
11 | }
12 |
13 | const RelativeTooltip = (props: Props): React.ReactElement => {
14 | const { relativeTo, text } = props;
15 | const [position, setPosition] = React.useState({ top: -999, left: 0 });
16 |
17 | React.useEffect(() => {
18 | let mounted = true;
19 | if (relativeTo.current) {
20 | relativeTo.current.measureInWindow((x: number, y: number, width: number, height: number): void => {
21 | if (!mounted) {
22 | return;
23 | }
24 |
25 | setPosition({ left: x + Math.round(width / 2), top: y + Math.round(height / 2) });
26 | });
27 | }
28 | return () => {
29 | mounted = false;
30 | };
31 | /* eslint-disable react-hooks/exhaustive-deps */
32 | // Tests break if you pass the ref/relativeTo higher object in and not the actual value we're using
33 | }, [relativeTo.current]);
34 | /* eslint-enable react-hooks/exhaustive-deps */
35 |
36 | return ;
37 | };
38 |
39 | export default RelativeTooltip;
40 |
--------------------------------------------------------------------------------
/src/app/src/components/SizeKeyPicker/Key.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../../theme';
5 | import RadioSelect from '../RadioSelect';
6 | import React from 'react';
7 | import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
8 |
9 | interface Props {
10 | isSelected?: boolean;
11 | onSelect: (value: string) => void;
12 | style?: StyleProp;
13 | value: string;
14 | }
15 |
16 | const SizeKeyPicker = (props: Props): React.ReactElement => {
17 | const { isSelected, onSelect, style, value } = props;
18 |
19 | const handleSelect = React.useCallback(
20 | (checked: boolean): void => {
21 | if (checked) {
22 | onSelect(value);
23 | }
24 | },
25 | [onSelect, value]
26 | );
27 |
28 | const nativeID = `radio${value.replace(/^\w/g, '')}`;
29 |
30 | return (
31 |
32 |
33 |
34 | {value}
35 |
36 |
37 | );
38 | };
39 |
40 | const styles = StyleSheet.create({
41 | root: {
42 | flexDirection: 'row',
43 | alignItems: 'center',
44 | cursor: 'pointer',
45 | },
46 | radio: {
47 | marginEnd: Theme.Spacing.Small,
48 | },
49 | text: {},
50 | });
51 |
52 | export default SizeKeyPicker;
53 |
--------------------------------------------------------------------------------
/src/app/src/components/SizeKeyPicker/__tests__/Key.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import RadioSelect from '../../RadioSelect';
5 | import React from 'react';
6 | import SizeKey from '../Key';
7 | import { fireEvent, render } from 'react-native-testing-library';
8 |
9 | describe('SizeKey', () => {
10 | describe('rendering', () => {
11 | test('wraps the radio in a label', () => {
12 | const { queryAllByProps } = render();
13 | expect(
14 | queryAllByProps({
15 | accessibilityRole: 'label',
16 | })
17 | ).toHaveLength(1);
18 | });
19 | });
20 |
21 | describe('isSelected', () => {
22 | test('sets checkbox value when true', () => {
23 | const { getByType } = render();
24 | expect(getByType(RadioSelect).props.value).toBe(true);
25 | });
26 |
27 | test('does not set checkbox value when false', () => {
28 | const { getByType } = render();
29 | expect(getByType(RadioSelect).props.value).toBeUndefined();
30 | });
31 | });
32 |
33 | describe('onSelect', () => {
34 | test('fires onSelect with the value', () => {
35 | const handleOnSelect = jest.fn();
36 | const { getByType } = render();
37 | fireEvent(getByType(RadioSelect), 'valueChange', true);
38 | expect(handleOnSelect).toHaveBeenCalledWith('tacos');
39 | });
40 |
41 | test('does not fire onSelect when value changes to false', () => {
42 | const handleOnSelect = jest.fn();
43 | const { getByType } = render();
44 | fireEvent(getByType(RadioSelect), 'valueChange', false);
45 | expect(handleOnSelect).not.toHaveBeenCalled();
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/app/src/components/SizeKeyPicker/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Actions from '../../../store/actions';
5 | import mockStore from '../../../store/mock';
6 | import { Provider } from 'react-redux';
7 | import React from 'react';
8 | import SizeKeyPicker from '../';
9 | import { fireEvent, render } from 'react-native-testing-library';
10 |
11 | describe('SizeKeyPicker', () => {
12 | test('renders a button per key', () => {
13 | const { queryAllByProps } = render(
14 |
15 |
16 |
17 | );
18 | expect(queryAllByProps({ isSelected: true, value: 'foo' })).toHaveLength(1);
19 | expect(queryAllByProps({ isSelected: false, value: 'bar' })).toHaveLength(1);
20 | });
21 |
22 | test('passes the onSelect handler', () => {
23 | const handleSelect = jest.spyOn(Actions, 'setSizeKey');
24 | const { queryAllByProps } = render(
25 |
26 |
27 |
28 | );
29 | expect(queryAllByProps({ value: 'foo' })).toHaveLength(1);
30 | expect(queryAllByProps({ value: 'bar' })).toHaveLength(1);
31 | expect(queryAllByProps({ value: 'tacos' })).toHaveLength(1);
32 | fireEvent(queryAllByProps({ value: 'bar' })[0], 'select', 'foo');
33 | expect(handleSelect).toHaveBeenCalledWith('foo');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/src/components/SizeKeyPicker/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import { setSizeKey } from '../../store/actions';
6 | import SizeKey from './Key';
7 | import { State } from '../../store/types';
8 | import { StyleSheet, View } from 'react-native';
9 | import { useDispatch, useSelector } from 'react-redux';
10 |
11 | interface Props {
12 | keys: Array;
13 | }
14 |
15 | const SizeKeyPicker = (props: Props): React.ReactElement => {
16 | const { keys } = props;
17 | const selected = useSelector((state: State) => state.sizeKey);
18 | const dispatch = useDispatch();
19 | const handleSelect = React.useCallback(
20 | (key: string): void => {
21 | dispatch(setSizeKey(key));
22 | },
23 | [dispatch]
24 | );
25 | return (
26 |
27 | {keys.map((key) => (
28 |
29 | ))}
30 |
31 | );
32 | };
33 |
34 | const styles = StyleSheet.create({
35 | root: {},
36 | button: {},
37 | });
38 |
39 | export default SizeKeyPicker;
40 |
--------------------------------------------------------------------------------
/src/app/src/components/Snackbar.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../theme';
5 | import React from 'react';
6 | import { StyleSheet, Text, View } from 'react-native';
7 |
8 | interface Props {
9 | text: string;
10 | }
11 |
12 | const Snackbar = (props: Props): React.ReactElement => {
13 | const { text } = props;
14 | return (
15 |
16 | {text}
17 |
18 | );
19 | };
20 |
21 | const styles = StyleSheet.create({
22 | root: {
23 | position: 'absolute',
24 | bottom: Theme.Spacing.Normal,
25 | left: Theme.Spacing.Normal,
26 | width: 'auto',
27 | paddingVertical: Theme.Spacing.Small,
28 | paddingHorizontal: Theme.Spacing.Normal,
29 | backgroundColor: Theme.Color.Black,
30 | borderRadius: Theme.BorderRadius.Normal,
31 | shadowOffset: { width: 0, height: 2 },
32 | shadowColor: Theme.Color.Black,
33 | shadowOpacity: 0.3,
34 | shadowRadius: 5,
35 | elevation: 10,
36 | animationKeyframes: [
37 | {
38 | from: { transform: [{ scale: 0.85 }], opacity: 0 },
39 | to: { transform: [{ scale: 1 }], opacity: 1 },
40 | },
41 | ],
42 | animationDuration: '0.1s',
43 | animationTimingFunction: Theme.MotionTiming.Accelerate,
44 | animationIterationCount: 1,
45 | },
46 | text: {
47 | color: Theme.TextColor.Black,
48 | },
49 | });
50 |
51 | export default Snackbar;
52 |
--------------------------------------------------------------------------------
/src/app/src/components/Subtitle.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as React from 'react';
5 | import * as Theme from '../theme';
6 | import { StyleSheet, Text } from 'react-native';
7 |
8 | interface Props {
9 | title: string;
10 | }
11 |
12 | const Subtitle = (props: Props): React.ReactElement => {
13 | const { title } = props;
14 | return {title};
15 | };
16 |
17 | const styles = StyleSheet.create({
18 | subtitle: {
19 | color: Theme.Color.Gray50,
20 | fontSize: Theme.FontSize.Normal,
21 | marginBottom: Theme.Spacing.Xsmall,
22 | },
23 | });
24 |
25 | export default Subtitle;
26 |
--------------------------------------------------------------------------------
/src/app/src/components/TextLink.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../theme';
5 | import Hoverable from './Hoverable';
6 | import React from 'react';
7 | import { StyleProp, StyleSheet, Text, ViewStyle } from 'react-native';
8 |
9 | interface Props {
10 | href: string;
11 | style?: StyleProp;
12 | text: string;
13 | }
14 |
15 | const DrawerLink = (props: Props): React.ReactElement => {
16 | const { href, style, text } = props;
17 |
18 | return (
19 |
20 | {(isHovered) => {
21 | return (
22 |
28 | {text}
29 |
30 | );
31 | }}
32 |
33 | );
34 | };
35 |
36 | const styles = StyleSheet.create({
37 | root: {
38 | borderRadius: Theme.BorderRadius.Normal,
39 | color: Theme.Color.Primary40,
40 | },
41 | rootHovered: {
42 | backgroundColor: Theme.Color.Primary00,
43 | },
44 | });
45 |
46 | export default DrawerLink;
47 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/BuildInfo.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Actions from '../../store/actions';
5 | import Build from '@build-tracker/build';
6 | import BuildInfo from '../BuildInfo';
7 | import Comparator from '@build-tracker/comparator';
8 | import mockStore from '../../store/mock';
9 | import { Provider } from 'react-redux';
10 | import React from 'react';
11 | import { fireEvent, render } from 'react-native-testing-library';
12 |
13 | const build = new Build({ branch: 'main', revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
14 |
15 | describe('BuildInfo', () => {
16 | test('can be closed', () => {
17 | const focusRevisionSpy = jest.spyOn(Actions, 'setFocusedRevision');
18 | const { getByProps } = render(
19 |
20 |
21 |
22 | );
23 | fireEvent.press(getByProps({ title: 'Collapse details' }));
24 | expect(focusRevisionSpy).toHaveBeenCalledWith(undefined);
25 | });
26 |
27 | test('removes the build focus on button press', () => {
28 | const removeComparedRevisionSpy = jest.spyOn(Actions, 'removeComparedRevision');
29 | const { getByProps } = render(
30 |
31 |
32 |
33 | );
34 | fireEvent.press(getByProps({ title: 'Remove build' }));
35 | expect(removeComparedRevisionSpy).toHaveBeenCalledWith('1234565');
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/Divider.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../../theme';
5 | import Divider from '../Divider';
6 | import React from 'react';
7 | import { render } from 'react-native-testing-library';
8 | import { StyleSheet, View } from 'react-native';
9 |
10 | describe('Divider', () => {
11 | test('renders a simple divider', () => {
12 | const { getByType } = render();
13 | expect(StyleSheet.flatten(getByType(View).props.style)).toMatchObject({ backgroundColor: Theme.Color.Gray20 });
14 | });
15 |
16 | test('renders a divider with the given color', () => {
17 | const { getByType } = render();
18 | expect(StyleSheet.flatten(getByType(View).props.style)).toMatchObject({ backgroundColor: 'red' });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/Drawer.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import Drawer, { Handles } from '../Drawer';
6 | import { fireEvent, render } from 'react-native-testing-library';
7 | import { ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
8 |
9 | describe('Drawer', () => {
10 | describe('hidden', () => {
11 | test('is hidden initially', () => {
12 | const { getByType } = render(
13 |
14 |
15 |
16 | );
17 | expect(getByType(ScrollView).props['aria-hidden']).toBe(true);
18 | const styles = StyleSheet.flatten(getByType(ScrollView).props.style);
19 | expect(styles).toMatchObject({ maxWidth: 300, left: -300, position: 'absolute' });
20 | });
21 | });
22 |
23 | describe('show', () => {
24 | test('makes the drawer visible', () => {
25 | const ref = React.createRef();
26 | const { getByType } = render(
27 |
28 |
29 |
30 | );
31 | ref.current.show();
32 | expect(getByType(ScrollView).props['aria-hidden']).toBe(false);
33 | const styles = StyleSheet.flatten(getByType(ScrollView).props.style);
34 | expect(styles).toMatchObject({ maxWidth: 300, left: 0 });
35 | });
36 | });
37 |
38 | describe('scrim', () => {
39 | test('hides the drawer when presse3d', () => {
40 | const ref = React.createRef();
41 | const { getByType } = render(
42 |
43 |
44 |
45 | );
46 | ref.current.show();
47 | fireEvent.press(getByType(TouchableOpacity));
48 | expect(getByType(ScrollView).props['aria-hidden']).toBe(true);
49 | const styles = StyleSheet.flatten(getByType(ScrollView).props.style);
50 | expect(styles).toMatchObject({ maxWidth: 300, left: -300, position: 'absolute' });
51 | });
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/EmptyState.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import EmptyState from '../EmptyState';
5 | import React from 'react';
6 | import { render } from 'react-native-testing-library';
7 |
8 | describe('EmptyState', () => {
9 | test('renders a message', () => {
10 | const { queryAllByText } = render();
11 | expect(queryAllByText('tacos rule')).toHaveLength(1);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/Menu.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Menu from '../Menu';
5 | import MenuItem from '../MenuItem';
6 | import React from 'react';
7 | import { render } from '@testing-library/react';
8 | import { View } from 'react-native';
9 |
10 | describe('Menu', () => {
11 | let viewRef;
12 | beforeEach(() => {
13 | viewRef = React.createRef();
14 | render();
15 | });
16 |
17 | test('renders with menu accessibilityRole', () => {
18 | const { queryAllByRole } = render(
19 |
22 | );
23 | expect(queryAllByRole('menu')).toHaveLength(1);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/Snackbar.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import { render } from '@testing-library/react';
6 | import Snackbar from '../Snackbar';
7 |
8 | describe('Snackbar', () => {
9 | test('renders to a portal', () => {
10 | const portal = document.createElement('div');
11 | portal.setAttribute('id', 'snackbarPortal');
12 | document.body.appendChild(portal);
13 |
14 | const { queryAllByRole, queryAllByText } = render(, {
15 | container: portal,
16 | });
17 |
18 | expect(queryAllByRole('alert')).toHaveLength(1);
19 | expect(queryAllByText('foobar')).toHaveLength(1);
20 | });
21 |
22 | test('renders directly without a portal available', () => {
23 | const { queryAllByRole, queryAllByText } = render();
24 |
25 | expect(queryAllByRole('alert')).toHaveLength(1);
26 | expect(queryAllByText('foobar')).toHaveLength(1);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/Subtitle.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import React from 'react';
5 | import { render } from 'react-native-testing-library';
6 | import Subtitle from '../Subtitle';
7 |
8 | describe('Subtitle', () => {
9 | test('renders a subtitle', () => {
10 | const { getByText } = render();
11 | expect(getByText('Tacos')).not.toBeUndefined();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/src/components/__tests__/TextLink.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Theme from '../../theme';
5 | import React from 'react';
6 | import { StyleSheet } from 'react-native';
7 | import TextLink from '../TextLink';
8 | import { fireEvent, render } from 'react-native-testing-library';
9 |
10 | describe('TextLink', () => {
11 | describe('hover', () => {
12 | beforeEach(() => {
13 | document.dispatchEvent(new Event('mousemove'));
14 | });
15 |
16 | test('changes the bg color', () => {
17 | const { getByProps } = render();
18 | const root = getByProps({ accessibilityRole: 'link' });
19 | expect(StyleSheet.flatten(root.props.style)).not.toMatchObject({
20 | backgroundColor: expect.any(String),
21 | });
22 | fireEvent(root, 'mouseEnter');
23 | expect(StyleSheet.flatten(root.props.style)).toMatchObject({
24 | backgroundColor: Theme.Color.Primary00,
25 | });
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/src/icons/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'header/header': 'off'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/src/icons/ArrowLeft.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const ArrowLeft = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | ArrowLeft.metadata = { height: 24, width: 24 };
31 |
32 | export default ArrowLeft;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/ArrowRight.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const ArrowRight = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | ArrowRight.metadata = { height: 24, width: 24 };
31 |
32 | export default ArrowRight;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/BarChart.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const BarChart = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | BarChart.metadata = { height: 24, width: 24 };
31 |
32 | export default BarChart;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Clear.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Clear = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Clear.metadata = { height: 24, width: 24 };
31 |
32 | export default Clear;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Close.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Close = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Close.metadata = { height: 24, width: 24 };
31 |
32 | export default Close;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Collapse.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Collapse = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Collapse.metadata = { height: 24, width: 24 };
31 |
32 | export default Collapse;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Document.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Document = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Document.metadata = { height: 24, width: 24 };
31 |
32 | export default Document;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Error.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Error = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Error.metadata = { height: 24, width: 24 };
31 |
32 | export default Error;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Folder.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Folder = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Folder.metadata = { height: 24, width: 24 };
31 |
32 | export default Folder;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Hash.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/Templarian/MaterialDesign-JS
3 | *
4 | * SVG contents redistributed under MIT License
5 | * https://github.com/Templarian/MaterialDesign-JS/blob/master/LICENSE
6 | * Copyright 2018 Austin Andrews
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Error = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 | );
26 |
27 | Error.metadata = { height: 24, width: 24 };
28 |
29 | export default Error;
30 |
--------------------------------------------------------------------------------
/src/app/src/icons/Heart.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Heart = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Heart.metadata = { height: 24, width: 24 };
31 |
32 | export default Heart;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Info.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Info = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Info.metadata = { height: 24, width: 24 };
31 |
32 | export default Info;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/LineChart.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const LineChart = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | LineChart.metadata = { height: 24, width: 24 };
31 |
32 | export default LineChart;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Link.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Link = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Link.metadata = { height: 24, width: 24 };
31 |
32 | export default Link;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/ListBulleted.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const ListBulleted = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | ListBulleted.metadata = { height: 24, width: 24 };
31 |
32 | export default ListBulleted;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Menu.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Menu = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Menu.metadata = { height: 24, width: 24 };
31 |
32 | export default Menu;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/More.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const More = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | More.metadata = { height: 24, width: 24 };
31 |
32 | export default More;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/OpenInExternal.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const OpenInExternal = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | OpenInExternal.metadata = { height: 24, width: 24 };
31 |
32 | export default OpenInExternal;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Remove.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Remove = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Remove.metadata = { height: 24, width: 24 };
31 |
32 | export default Remove;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Table.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Table = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Table.metadata = { height: 24, width: 24 };
31 |
32 | export default Table;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/Warning.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Material design redistributed from https://github.com/google/material-design-icons
3 | *
4 | * SVG contents redistributed under Apache License 2.0:
5 | * https://github.com/google/material-design-icons/blob/master/LICENSE
6 | * Copyright 2015 Google, Inc. All Rights Reserved.
7 | */
8 | import React from 'react';
9 | import styles from './styles';
10 | import { StyleProp, TextStyle, unstable_createElement, ViewProps } from 'react-native';
11 |
12 | interface Props extends ViewProps {
13 | style?: StyleProp;
14 | }
15 |
16 | const Warning = (props: Props): React.ReactElement =>
17 | unstable_createElement(
18 | 'svg',
19 | {
20 | ...props,
21 | style: [styles.root, props.style],
22 | viewBox: '0 0 24 24',
23 | },
24 |
25 |
26 |
27 |
28 | );
29 |
30 | Warning.metadata = { height: 24, width: 24 };
31 |
32 | export default Warning;
33 |
--------------------------------------------------------------------------------
/src/app/src/icons/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | root: {
5 | display: 'inline-block',
6 | fill: 'currentcolor',
7 | height: '1.25em',
8 | maxWidth: '100%',
9 | position: 'relative',
10 | userSelect: 'none',
11 | textAlignVertical: 'text-bottom',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/src/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paularmstrong/build-tracker/863d80755ea9188c5c8b9fc6f1e6ab1bfe062313/src/app/src/images/favicon.png
--------------------------------------------------------------------------------
/src/app/src/modules/ColorScale.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { interpolateCool, interpolateMagma, interpolateRainbow, interpolateRdYlBu } from 'd3-scale-chromatic';
5 | import { scaleSequential, ScaleSequential } from 'd3-scale';
6 |
7 | interface Scales {
8 | [key: string]: ScaleSequential;
9 | }
10 |
11 | const scales: Scales = Object.freeze({
12 | Standard: scaleSequential(interpolateRdYlBu),
13 | Cool: scaleSequential(interpolateCool),
14 | Magma: scaleSequential(interpolateMagma),
15 | Rainbow: scaleSequential(interpolateRainbow),
16 | });
17 |
18 | export default scales;
19 |
--------------------------------------------------------------------------------
/src/app/src/store/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { GraphType } from '../types';
5 | import { searchParamsToStore } from '../utils';
6 |
7 | describe('searchParamsToStore', () => {
8 | test('works with or without leading ?', () => {
9 | expect(searchParamsToStore('?sizeKey=bar')).toEqual({ sizeKey: 'bar' });
10 | expect(searchParamsToStore('sizeKey=foo')).toEqual({ sizeKey: 'foo' });
11 | });
12 |
13 | test('returns empty object for empty string', () => {
14 | expect(searchParamsToStore('')).toEqual({});
15 | });
16 |
17 | test('reduces activeArtifacts to an object', () => {
18 | expect(searchParamsToStore('?activeArtifacts=main&activeArtifacts=vendor&activeArtifacts=shared')).toEqual({
19 | activeArtifacts: { main: true, vendor: true, shared: true },
20 | });
21 | });
22 |
23 | test('converts graphType to enum', () => {
24 | expect(searchParamsToStore('graphType=STACKED_BAR')).toEqual({ graphType: GraphType.STACKED_BAR });
25 | expect(searchParamsToStore('graphType=AREA')).toEqual({ graphType: GraphType.AREA });
26 | });
27 |
28 | test('does not set invalid graphType', () => {
29 | expect(searchParamsToStore('graphType=tacos')).toEqual({});
30 | });
31 |
32 | test('converts disabledArtifactsVisible to boolean', () => {
33 | expect(searchParamsToStore('disabledArtifactsVisible=true')).toEqual({ disabledArtifactsVisible: true });
34 | expect(searchParamsToStore('disabledArtifactsVisible=false')).toEqual({ disabledArtifactsVisible: false });
35 | });
36 |
37 | test('sets comparedRevisions to an array', () => {
38 | expect(searchParamsToStore('comparedRevisions=123&comparedRevisions=abc')).toEqual({
39 | comparedRevisions: ['123', 'abc'],
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/app/src/store/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
5 | import ColorScale from '../modules/ColorScale';
6 | import Comparator from '@build-tracker/comparator';
7 | import reducer from './reducer';
8 | import { Actions, FetchState, GraphType, State } from './types';
9 | import { createStore, Store } from 'redux';
10 |
11 | export default function makeStore(initialState: Partial = {}): Store {
12 | const store = createStore(reducer, {
13 | activeArtifacts: {},
14 | activeComparator: null,
15 | artifactConfig: {},
16 | budgets: [],
17 | builds: [],
18 | colorScale: Object.keys(ColorScale)[0],
19 | comparator: new Comparator({ builds: [] }),
20 | comparedRevisions: [],
21 | disabledArtifactsVisible: true,
22 | fetchState: FetchState.NONE,
23 | graphType: GraphType.AREA,
24 | hideAttribution: false,
25 | hoveredArtifacts: [],
26 | name: 'Build Tracker',
27 | snacks: [],
28 | sizeKey: '',
29 | ...initialState,
30 | });
31 | if (process.env.NODE_ENV !== 'production' && canUseDOM && !window.hasOwnProperty('redux')) {
32 | Object.defineProperty(window, 'redux', {
33 | writable: false,
34 | value: store,
35 | });
36 | }
37 | return store;
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/src/store/mock.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import configureStore from 'redux-mock-store';
5 |
6 | const mockStore = configureStore();
7 | export default mockStore;
8 |
--------------------------------------------------------------------------------
/src/app/src/store/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { GraphType, State } from './types';
5 |
6 | export const searchParamsToStore = (params: string): Partial => {
7 | const searchParams = new URLSearchParams(params.replace(/^\?/, ''));
8 | const state: Partial = {};
9 |
10 | const activeArtifacts = searchParams.getAll('activeArtifacts');
11 |
12 | if (activeArtifacts.length) {
13 | state.activeArtifacts = activeArtifacts.reduce((memo, artifactName) => ({ ...memo, [artifactName]: true }), {});
14 | }
15 |
16 | const graphType: GraphType = GraphType[searchParams.get('graphType')];
17 | if (graphType in GraphType) {
18 | state.graphType = graphType;
19 | }
20 |
21 | const sizeKey = searchParams.get('sizeKey');
22 | if (sizeKey) {
23 | state.sizeKey = sizeKey;
24 | }
25 |
26 | const disabledArtifactsVisible =
27 | searchParams.get('disabledArtifactsVisible') === 'false'
28 | ? false
29 | : searchParams.get('disabledArtifactsVisible') === 'true'
30 | ? true
31 | : undefined;
32 | if (typeof disabledArtifactsVisible === 'boolean') {
33 | state.disabledArtifactsVisible = disabledArtifactsVisible;
34 | }
35 |
36 | const comparedRevisions = searchParams.getAll('comparedRevisions');
37 | if (comparedRevisions.length) {
38 | state.comparedRevisions = comparedRevisions;
39 | }
40 |
41 | return state;
42 | };
43 |
--------------------------------------------------------------------------------
/src/app/src/views/Graph.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import EmptyState from '../components/EmptyState';
5 | import Graph from '../components/Graph';
6 | import React from 'react';
7 | import { State } from '../store/types';
8 | import { useSelector } from 'react-redux';
9 |
10 | const GraphView = (): React.ReactElement => {
11 | const comparator = useSelector((state: State) => state.comparator);
12 |
13 | if (comparator.builds.length) {
14 | return ;
15 | }
16 |
17 | return ;
18 | };
19 |
20 | export default GraphView;
21 |
--------------------------------------------------------------------------------
/src/app/src/views/Snacks.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
5 | import React from 'react';
6 | import ReactDOM from 'react-dom';
7 | import { removeSnack } from '../store/actions';
8 | import Snackbar from '../components/Snackbar';
9 | import { State } from '../store/types';
10 | import { useDispatch, useSelector } from 'react-redux';
11 |
12 | const SnacksView = (): React.ReactElement => {
13 | const dispatch = useDispatch();
14 | const message = useSelector((state: State) => state.snacks[0]);
15 |
16 | const portalRoot = canUseDOM && document.getElementById('snackbarPortal');
17 |
18 | React.useEffect(() => {
19 | if (message) {
20 | setTimeout(() => {
21 | dispatch(removeSnack(message));
22 | }, 4000);
23 | }
24 | }, [dispatch, message]);
25 |
26 | const bar = message ? : null;
27 |
28 | return portalRoot ? ReactDOM.createPortal(bar, portalRoot) : bar;
29 | };
30 |
31 | export default SnacksView;
32 |
--------------------------------------------------------------------------------
/src/app/src/views/__tests__/Graph.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from '@build-tracker/build';
5 | import Comparator from '@build-tracker/comparator';
6 | import EmptyState from '../../components/EmptyState';
7 | import Graph from '../Graph';
8 | import mockStore from '../../store/mock';
9 | import { Provider } from 'react-redux';
10 | import React from 'react';
11 | import { render } from 'react-native-testing-library';
12 |
13 | const build = new Build({ branch: 'main', revision: '1234565', parentRevision: 'abcdef', timestamp: 123 }, []);
14 |
15 | describe('Graph', () => {
16 | test('renders an empty message if no builds in comparator', () => {
17 | const { queryAllByType } = render(
18 |
19 |
20 |
21 | );
22 | expect(queryAllByType(EmptyState)).toHaveLength(1);
23 | });
24 |
25 | test('does not render an empty message if there are builds in comparator', () => {
26 | const { queryAllByType } = render(
27 |
30 |
31 |
32 | );
33 | expect(queryAllByType(EmptyState)).toHaveLength(0);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "outDir": "./dist"
6 | },
7 | "include": ["src", "../build", "../comparator", "../fixtures", "../formatting", "../types"]
8 | }
9 |
--------------------------------------------------------------------------------
/src/build/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/build
2 |
3 | > A shareable representation of a single `build` used in [Build Tracker](https://buildtracker.dev).
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/build) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/build/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from './dist';
5 | export * from './dist';
6 | export default Build;
7 |
--------------------------------------------------------------------------------
/src/build/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Build from './src';
5 | export * from './src';
6 | export default Build;
7 |
--------------------------------------------------------------------------------
/src/build/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'build',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/build/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/build",
3 | "version": "1.0.0",
4 | "description": "Build Tracker tools for interacting with individual Builds",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "@build-tracker/types": "^1.0.0-beta.12",
18 | "build": "tsc",
19 | "clean": "rimraf dist",
20 | "tsc": "tsc --noEmit"
21 | },
22 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
23 | "publishConfig": {
24 | "access": "public"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/build/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/cli/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/cli
2 |
3 | > A command-line interface for interacting with a [Build Tracker](https://buildtracker.dev) instance's API.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/cli) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/cli/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'cli',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/cli",
3 | "version": "1.0.0",
4 | "description": "Build Tracker tool for reading build artifact sizes and uploading to your server",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "bin": {
9 | "bt-cli": "./dist/bin.js"
10 | },
11 | "main": "dist",
12 | "module": "./",
13 | "types": "dist/index.d.ts",
14 | "sideEffects": false,
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "build": "tsc",
20 | "clean": "rimraf dist",
21 | "tsc": "tsc --noEmit"
22 | },
23 | "dependencies": {
24 | "@build-tracker/api-client": "^1.0.0",
25 | "@types/node": "^11.10.4",
26 | "@types/yargs": "^15.0.0",
27 | "yargs": "^15.0.0"
28 | },
29 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
30 | "publishConfig": {
31 | "access": "public"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/cli/src/bin.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Copyright (c) 2019 Paul Armstrong
4 | */
5 | import * as path from 'path';
6 | import yargs from 'yargs';
7 |
8 | const argv = yargs
9 | .usage('$0 [args]')
10 | .help('help', 'Show this help screen')
11 | .alias('help', 'h')
12 | .wrap(Math.min(process.stdout.columns, 120))
13 | .strict()
14 | .recommendCommands()
15 | .option('v', {
16 | default: 0,
17 | describe: 'Set the logging verbosity. Use -vv for more verbose',
18 | global: true,
19 | type: 'count',
20 | })
21 | .group(['help', 'verbose'], 'Global:')
22 | .commandDir(path.join(__dirname, 'commands'), {
23 | extensions: ['ts', 'js'],
24 | exclude: /\.d\.ts$/,
25 | })
26 | .demandCommand(1, 1, 'Please provide a command to run').argv;
27 |
28 | /* eslint-disable no-console */
29 | argv.v >= 2 && console.debug(argv);
30 |
--------------------------------------------------------------------------------
/src/cli/src/commands/__tests__/upload-build.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as Command from '../upload-build';
5 | import yargs from 'yargs';
6 |
7 | describe('upload-build', () => {
8 | describe('builder', () => {
9 | test('defaults config', () => {
10 | const args = Command.builder(yargs([]));
11 | expect(args.argv).toEqual({
12 | $0: expect.any(String),
13 | _: [],
14 | 'skip-dirty-check': false,
15 | skipDirtyCheck: false,
16 | });
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/cli/src/commands/__tests__/version.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as VersionCommand from '../version';
5 | import packageJson from '../../../package.json';
6 | import yargs from 'yargs';
7 |
8 | describe('version command', () => {
9 | describe('builder', () => {
10 | test('returns the config', () => {
11 | const args = VersionCommand.builder(yargs([]));
12 | expect(args.argv).toMatchObject({ _: [] });
13 | });
14 | });
15 |
16 | describe('handler', () => {
17 | test('runs the server with the given config', () => {
18 | const mockWrite = jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
19 | VersionCommand.handler();
20 | expect(mockWrite).toHaveBeenCalledWith(`${packageJson.version}\n`);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/cli/src/commands/get-build.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Argv } from 'yargs';
5 | import { getBuild, getConfig } from '@build-tracker/api-client';
6 |
7 | export const command = 'get-build';
8 |
9 | export const description = 'Retrieve a build by revision';
10 |
11 | export interface Args {
12 | revision: string;
13 | config?: string;
14 | out: boolean;
15 | }
16 |
17 | const group = 'Retrieve a build';
18 |
19 | export const getBuildOptions = (yargs): Argv =>
20 | yargs
21 | .option('revision', {
22 | alias: 'r',
23 | description: 'Get the build using revision',
24 | group,
25 | type: 'string',
26 | })
27 | .option('config', {
28 | alias: 'c',
29 | description: 'Override path to the build-tracker CLI config file',
30 | group,
31 | normalize: true,
32 | });
33 |
34 | export const builder = (yargs): Argv =>
35 | getBuildOptions(yargs).usage(`Usage: $0 ${command}`).option('out', {
36 | alias: 'o',
37 | default: true,
38 | description: 'Write the build to stdout',
39 | group,
40 | type: 'boolean',
41 | });
42 |
43 | export const handler = async (args: Args): Promise => {
44 | const config = await getConfig(args.config);
45 | const { revision } = args;
46 | const build = await getBuild(config, revision);
47 |
48 | if (args.out) {
49 | process.stdout.write(JSON.stringify(build, null, 2));
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/src/cli/src/commands/stat-artifacts.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Argv } from 'yargs';
5 | import { getConfig, statArtifacts } from '@build-tracker/api-client';
6 |
7 | export const command = 'stat-artifacts';
8 |
9 | export const description = 'Compute your artifact stats';
10 |
11 | interface Args {
12 | config?: string;
13 | out: boolean;
14 | }
15 |
16 | const group = 'Stat artifacts';
17 |
18 | export const builder = (yargs): Argv =>
19 | yargs
20 | .usage(`Usage: $0 ${command}`)
21 | .option('config', {
22 | alias: 'c',
23 | description: 'Override path to the build-tracker CLI config file',
24 | group,
25 | normalize: true,
26 | })
27 | .option('out', {
28 | alias: 'o',
29 | default: true,
30 | description: 'Write the stats to stdout',
31 | group,
32 | type: 'boolean',
33 | });
34 |
35 | export const handler = async (args: Args): Promise => {
36 | const config = await getConfig(args.config);
37 |
38 | const artifacts = statArtifacts(config);
39 |
40 | if (args.out) {
41 | const fileOut = Array.from(artifacts).reduce((memo, [artifactName, stat]) => {
42 | memo[artifactName] = stat;
43 | return memo;
44 | }, {});
45 | // @ts-ignore
46 | process.stdout.write(JSON.stringify(fileOut, null, 2));
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/cli/src/commands/upload-build.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Argv } from 'yargs';
5 | import { Args as BuildArgs, getBuildOptions } from './create-build';
6 | import { createBuild, getConfig, uploadBuild } from '@build-tracker/api-client';
7 |
8 | export const command = 'upload-build';
9 |
10 | export const description = 'Upload a build for the current commit';
11 |
12 | type Args = BuildArgs;
13 |
14 | export const builder = (yargs): Argv => getBuildOptions(yargs).usage(`Usage: $0 ${command}`);
15 |
16 | export const handler = async (args: Args): Promise => {
17 | const config = await getConfig(args.config);
18 | const build = await createBuild(config, {
19 | branch: args.branch,
20 | meta: args.meta,
21 | parentRevision: args['parent-revision'],
22 | skipDirtyCheck: args['skip-dirty-check'],
23 | });
24 |
25 | try {
26 | await uploadBuild(config, build, process.env.BT_API_AUTH_TOKEN, {
27 | log: (input) => {
28 | process.stdout.write(`${input}\n`);
29 | },
30 | error: (input) => {
31 | process.stderr.write(`${input}\n`);
32 | },
33 | });
34 | } catch (e) {
35 | process.exit(1);
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/cli/src/commands/version.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Argv } from 'yargs';
5 |
6 | export const command = 'version';
7 |
8 | export const description = 'Print the Build Tracker version';
9 |
10 | export const builder = (yargs): Argv<{}> => yargs.usage(`Usage: $0 ${command}`);
11 |
12 | export const handler = (): void => {
13 | process.stdout.write(`${require('@build-tracker/api-client/package.json').version}\n`);
14 | };
15 |
--------------------------------------------------------------------------------
/src/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/comparator/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/comparator
2 |
3 | > The [Build Tracker](https://buildtracker.dev) Comparator contains the core functionality for calculating how two or more Builds differ. This package is only needed individually if you would like to do some custom reporting on various Builds.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/comparator) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/comparator/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './dist';
5 |
--------------------------------------------------------------------------------
/src/comparator/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Comparator from './src';
5 | export * from './src';
6 |
7 | export default Comparator;
8 |
--------------------------------------------------------------------------------
/src/comparator/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'comparator',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/comparator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/comparator",
3 | "version": "1.0.0",
4 | "description": "Build Tracker tool for comparing two or more builds",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist",
19 | "tsc": "tsc --noEmit"
20 | },
21 | "dependencies": {
22 | "@build-tracker/build": "^1.0.0",
23 | "@build-tracker/formatting": "^1.0.0",
24 | "@build-tracker/types": "^1.0.0",
25 | "markdown-table": "^1.1.2"
26 | },
27 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
28 | "publishConfig": {
29 | "access": "public"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/comparator/src/artifact-math.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { ArtifactSizes } from '@build-tracker/build';
5 |
6 | export const delta = (key: string, baseSizes?: ArtifactSizes, prevSizes?: ArtifactSizes): number => {
7 | if (!baseSizes) {
8 | if (!prevSizes) {
9 | return 0;
10 | }
11 | return -prevSizes[key];
12 | } else if (!prevSizes) {
13 | return baseSizes[key];
14 | }
15 |
16 | return baseSizes[key] - prevSizes[key];
17 | };
18 |
19 | export const percentDelta = (key: string, baseSizes?: ArtifactSizes, prevSizes?: ArtifactSizes): number => {
20 | if (!baseSizes) {
21 | if (!prevSizes) {
22 | return 0;
23 | }
24 | return -1;
25 | }
26 |
27 | if (!prevSizes) {
28 | return 1;
29 | }
30 |
31 | const base = prevSizes[key];
32 | const changed = baseSizes[key];
33 | if (base === 0) {
34 | return changed === 0 ? 0 : 1;
35 | } else if (changed > base) {
36 | const delta = changed - base;
37 | return delta / base;
38 | } else if (changed < base) {
39 | const delta = base - changed;
40 | return -(delta / base);
41 | }
42 | return 0;
43 | };
44 |
--------------------------------------------------------------------------------
/src/comparator/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/fixtures/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | plugins: ['header'],
4 | rules: {
5 | 'header/header': 'off'
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/fixtures/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/fixtures
2 |
3 | > Static data for testing and developing [Build Tracker](https://buildtracker.dev).
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/fixtures) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/commonjs/build-tracker.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | applicationUrl: '',
5 | artifacts: ['../fakedist/*.js'],
6 | baseDir: path.resolve(__dirname, 'fakedist'),
7 | cwd: __dirname,
8 | filenameHash: (fileName) => {
9 | const parts = path.basename(fileName, '.js').split('.');
10 | return parts.length > 1 ? parts[parts.length - 1] : null;
11 | },
12 | nameMapper: (fileName) => {
13 | return path.basename(fileName, '.js');
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/fakedist/main.1234567.js:
--------------------------------------------------------------------------------
1 | (function (window) {
2 | alert(window.document.title);
3 | })(window);
4 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/fakedist/test-folder/test-no-extension:
--------------------------------------------------------------------------------
1 | this file intentionally left blank
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/fakedist/vendor.js:
--------------------------------------------------------------------------------
1 | function vendor() {}
2 | function stuff() {}
3 |
4 | module.exports = {
5 | vendor,
6 | stuff,
7 | };
8 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/rc/.build-tracker-build-url-rc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | applicationUrl: 'https://build-tracker.local',
5 | artifacts: ['../fakedist/*.js'],
6 | baseDir: path.resolve(__dirname, 'fakedist'),
7 | buildUrlFormat: 'https://github.com/paularmstrong/build-tracker/commit/:revision',
8 | cwd: __dirname
9 | };
10 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/rc/.build-tracker-http-rc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | applicationUrl: 'http://build-tracker.local',
5 | artifacts: ['../fakedist/*.js'],
6 | baseDir: path.resolve(__dirname, 'fakedist'),
7 | cwd: __dirname
8 | };
9 |
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/rc/.build-trackerrc-invalid.js:
--------------------------------------------------------------------------------
1 | throw new Error('test');
--------------------------------------------------------------------------------
/src/fixtures/cli-configs/rc/.build-trackerrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | applicationUrl: 'https://build-tracker.local',
5 | artifacts: ['../fakedist/**/*'],
6 | baseDir: path.resolve(__dirname, 'fakedist'),
7 | cwd: __dirname,
8 | onCompare: data => Promise.resolve()
9 | };
10 |
--------------------------------------------------------------------------------
/src/fixtures/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/src/fixtures/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export default {};
5 |
--------------------------------------------------------------------------------
/src/fixtures/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/fixtures",
3 | "version": "1.0.0",
4 | "description": "Build Tracker build fixtures",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "./",
9 | "module": "./",
10 | "dependencies": {
11 | "glob": "^7.1.3"
12 | },
13 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
14 | "publishConfig": {
15 | "access": "public"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/fixtures/server-configs/build-tracker.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | const fakeBuild = require('../builds-medium/01141f29743fb2bdd7e176cf919fc964025cea5a.json');
5 |
6 | module.exports = {
7 | getParentBuild: () => Promise.resolve(fakeBuild),
8 | port: 3000,
9 | setup: () => Promise.resolve(),
10 | };
11 |
--------------------------------------------------------------------------------
/src/formatting/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/formatting
2 |
3 | > String formatters for making [Build Tracker](https://buildtracker.dev) data human-readable.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/formatting) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/formatting/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './dist';
5 |
--------------------------------------------------------------------------------
/src/formatting/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './src';
5 |
--------------------------------------------------------------------------------
/src/formatting/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'formatting',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/formatting/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/formatting",
3 | "version": "1.0.0",
4 | "description": "Build Tracker number and string formatting tools",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "scripts": {
13 | "build": "tsc",
14 | "clean": "rimraf dist",
15 | "tsc": "tsc --noEmit"
16 | },
17 | "dependencies": {
18 | "@build-tracker/types": "^1.0.0"
19 | },
20 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
21 | "publishConfig": {
22 | "access": "public"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/formatting/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/server/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/server
2 |
3 | > Runs a [Build Tracker](https://buildtracker.dev) web application (`@build-tracker/app`) using a Node.js server.
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/server) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Server from './dist/server';
5 | export * from './dist/server';
6 | export default Server;
7 |
--------------------------------------------------------------------------------
/src/server/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Server from './src/server';
5 | export * from './src/server';
6 | export default Server;
7 |
--------------------------------------------------------------------------------
/src/server/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | module.exports = {
5 | preset: '../../config/jest.js',
6 | displayName: 'server',
7 | testEnvironment: 'node',
8 | rootDir: './',
9 | roots: ['/src'],
10 | };
11 |
--------------------------------------------------------------------------------
/src/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/server",
3 | "version": "1.0.0",
4 | "description": "Build Tracker node.js web server",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "bin": {
9 | "bt-server": "dist/index.js"
10 | },
11 | "main": "dist/server.js",
12 | "module": "./",
13 | "types": "dist/server.d.ts",
14 | "sideEffects": false,
15 | "files": [
16 | "index.js",
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "tsc",
21 | "clean": "rimraf dist",
22 | "tsc": "tsc --noEmit"
23 | },
24 | "dependencies": {
25 | "@build-tracker/api-errors": "^1.0.0",
26 | "@build-tracker/app": "^1.0.0",
27 | "@build-tracker/build": "^1.0.0",
28 | "@build-tracker/comparator": "^1.0.0",
29 | "@build-tracker/formatting": "^1.0.0",
30 | "@build-tracker/types": "^1.0.0",
31 | "@types/body-parser": "^1.17.0",
32 | "@types/express": "^4.16.1",
33 | "@types/helmet": "^0.0.43",
34 | "@types/node": "^12.0.0",
35 | "@types/yargs": "^15.0.0",
36 | "body-parser": "^1.18.3",
37 | "express": "^4.16.4",
38 | "express-pino-logger": "^4.0.0",
39 | "glob": "^7.1.3",
40 | "helmet": "^3.15.1",
41 | "pino": "^5.11.1",
42 | "yargs": "^15.0.0"
43 | },
44 | "devDependencies": {
45 | "@build-tracker/fixtures": "^1.0.0",
46 | "node-mocks-http": "^1.7.3",
47 | "supertest": "^4.0.0",
48 | "ts-node": "^8.0.2",
49 | "webpack": "^4.29.6",
50 | "webpack-dev-middleware": "^3.6.1",
51 | "webpack-hot-middleware": "^2.24.3",
52 | "webpack-hot-server-middleware": "^0.6.0"
53 | },
54 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
55 | "publishConfig": {
56 | "access": "public"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/server/src/__tests__/csp.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import getCSP from '../csp';
5 |
6 | describe('CSP', () => {
7 | test('gets the nonce from a function', () => {
8 | expect(getCSP()).toMatchObject({
9 | scriptSrc: ["'self'", "'strict-dynamic'", expect.any(Function)],
10 | });
11 |
12 | // @ts-ignore
13 | expect(getCSP().scriptSrc[2]({}, { locals: { nonce: '12345' } })).toEqual("'nonce-12345'");
14 | });
15 |
16 | test('sets unsafe-eval only if explicitly told', () => {
17 | expect(getCSP(true)).toMatchObject({
18 | scriptSrc: ["'self'", "'unsafe-eval'", "'strict-dynamic'", expect.any(Function)],
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/server/src/api/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import express from 'express';
5 | import { Handlers } from '../types';
6 | import { insertBuild } from './insert';
7 | import protectedMiddleware from './protected';
8 | import { ServerConfig } from '../server';
9 | import { queryByRecent, queryByRevision, queryByRevisionRange, queryByRevisions, queryByTimeRange } from './read';
10 |
11 | const defaultBuildInsert = (): Promise => Promise.resolve();
12 |
13 | const middleware = (router: express.Router, config: ServerConfig, handlers?: Handlers): express.Router => {
14 | const { onBuildInsert = defaultBuildInsert } = handlers || {};
15 | router.use(protectedMiddleware);
16 | router.post('/api/builds', insertBuild(config.queries.build, config, onBuildInsert));
17 | router.get('/api/builds/:limit?', queryByRecent(config.queries.builds, config));
18 | router.get('/api/builds/range/:startRevision..:endRevision', queryByRevisionRange(config.queries.builds));
19 | router.get('/api/builds/time/:startTimestamp..:endTimestamp', queryByTimeRange(config.queries.builds, config));
20 | router.get('/api/builds/list/*', queryByRevisions(config.queries.builds));
21 | router.get('/api/build/:revision', queryByRevision(config.queries.build));
22 | return router;
23 | };
24 |
25 | export default middleware;
26 |
--------------------------------------------------------------------------------
/src/server/src/api/protected.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { AuthError } from '@build-tracker/api-errors';
5 | import { NextFunction, Request, Response } from 'express';
6 |
7 | export default function (req: Request, res: Response, next: NextFunction): void {
8 | const authHeaderValue = req.headers['x-bt-auth'];
9 | const authToken = process.env.BT_API_AUTH_TOKEN;
10 |
11 | if (req.method === 'GET' || !authToken) {
12 | next();
13 | return;
14 | }
15 |
16 | if (!authHeaderValue) {
17 | const error = new AuthError(
18 | `${req.path} requires the x-bt-auth header to be set. This API is secured with a token, ensure you set the same BT_API_AUTH_TOKEN variable before requesting again`
19 | );
20 | res.status(error.status).send({ error: error.message });
21 | return;
22 | }
23 |
24 | if (authHeaderValue !== authToken) {
25 | const error = new AuthError('invalid x-bt-auth token header');
26 | res.status(error.status).send({ error: error.message });
27 | return;
28 | }
29 |
30 | next();
31 | }
32 |
--------------------------------------------------------------------------------
/src/server/src/commands/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-console': 'off'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/src/commands/__tests__/run.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as path from 'path';
5 | import * as RunCommand from '../run';
6 | import * as Server from '../../server';
7 | import express from 'express';
8 | import yargs from 'yargs';
9 |
10 | describe('run command', () => {
11 | describe('builder', () => {
12 | test('looks for the config in the process working directory', () => {
13 | jest
14 | .spyOn(process, 'cwd')
15 | .mockReturnValue(path.join(path.dirname(require.resolve('@build-tracker/fixtures')), 'server-configs'));
16 |
17 | const args = RunCommand.builder(yargs([]));
18 | expect(args.argv.config).toEqual(
19 | require.resolve('@build-tracker/fixtures/server-configs/build-tracker.config.js')
20 | );
21 | });
22 |
23 | test('resolves the config path for requires', () => {
24 | jest.spyOn(process, 'cwd').mockReturnValue(path.join(__dirname, '../../..'));
25 |
26 | const args = RunCommand.builder(yargs(['--config', './fixtures/server-configs/build-tracker.config.js']));
27 | expect(args.argv.config).toEqual(
28 | path.join(__dirname, '../../../fixtures/server-configs/build-tracker.config.js')
29 | );
30 | });
31 | });
32 |
33 | describe('handler', () => {
34 | test('runs the server with the given config', () => {
35 | jest.spyOn(Server, 'default').mockImplementation(() => express());
36 | const configPath = require.resolve('@build-tracker/fixtures/server-configs/build-tracker.config.js');
37 | RunCommand.handler({ config: configPath });
38 | expect(Server.default).toHaveBeenCalledWith(require(configPath));
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/server/src/commands/__tests__/version.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as VersionCommand from '../version';
5 | import packageJson from '../../../package.json';
6 | import yargs from 'yargs';
7 |
8 | describe('version command', () => {
9 | describe('builder', () => {
10 | test('returns the config', () => {
11 | const args = VersionCommand.builder(yargs([]));
12 | expect(args.argv).toMatchObject({ _: [] });
13 | });
14 | });
15 |
16 | describe('handler', () => {
17 | test('runs the server with the given config', () => {
18 | const mockWrite = jest.spyOn(process.stdout, 'write').mockImplementationOnce(() => true);
19 | VersionCommand.handler();
20 | expect(mockWrite).toHaveBeenCalledWith(`${packageJson.version}\n`);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/server/src/commands/run.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as path from 'path';
5 | import { Argv } from 'yargs';
6 | import run, { ServerConfig } from '../server';
7 |
8 | export const command = 'run';
9 |
10 | export const description = 'Run the Build Tracker server';
11 |
12 | interface Args {
13 | config: string;
14 | }
15 |
16 | const group = 'Run';
17 |
18 | export const builder = (yargs): Argv =>
19 | yargs.usage(`Usage: $0 ${command}`).option('config', {
20 | alias: 'c',
21 | coerce: (v) => path.join(process.cwd(), v),
22 | default: 'build-tracker.config.js',
23 | description: 'path to the build-tracker config file',
24 | group,
25 | normalize: true,
26 | });
27 |
28 | export const handler = (args: Args): void => {
29 | const config = require(args.config) as ServerConfig;
30 | run(config);
31 | };
32 |
--------------------------------------------------------------------------------
/src/server/src/commands/seed.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as path from 'path';
5 | import { Argv } from 'yargs';
6 | import Build from '@build-tracker/build';
7 | import glob from 'glob';
8 | import { ServerConfig } from '../server';
9 |
10 | export const command = 'seed';
11 |
12 | interface Args {
13 | config: string;
14 | }
15 |
16 | const group = 'Seed';
17 |
18 | export const builder = (yargs): Argv =>
19 | yargs.usage(`Usage: $0 ${command}`).option('config', {
20 | alias: 'c',
21 | coerce: (v) => path.join(process.cwd(), v),
22 | default: 'build-tracker.config.js',
23 | description: 'path to the build-tracker config file',
24 | group,
25 | normalize: true,
26 | });
27 |
28 | export const handler = async (args: Args): Promise => {
29 | const config = require(args.config) as ServerConfig;
30 | const fixturePath = path.dirname(require.resolve('@build-tracker/fixtures'));
31 |
32 | await config.setup();
33 |
34 | const builds = glob.sync(`${path.join(fixturePath, 'builds-medium')}/*.json`);
35 |
36 | for (const build of builds) {
37 | const buildData = require(build);
38 | await config.queries.build.insert(new Build(buildData.meta, buildData.artifacts));
39 | }
40 |
41 | return Promise.resolve();
42 | };
43 |
--------------------------------------------------------------------------------
/src/server/src/commands/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import * as path from 'path';
5 | import { Argv } from 'yargs';
6 | import { ServerConfig } from '../server';
7 |
8 | export const command = 'setup';
9 |
10 | export const description = 'Run the Build Tracker setup';
11 |
12 | interface Args {
13 | config: string;
14 | }
15 |
16 | const group = 'Setup';
17 |
18 | export const builder = (yargs): Argv =>
19 | yargs.usage(`Usage: $0 ${command}`).option('config', {
20 | alias: 'c',
21 | coerce: (v) => path.join(process.cwd(), v),
22 | default: 'build-tracker.config.js',
23 | description: 'path to the build-tracker config file',
24 | group,
25 | normalize: true,
26 | });
27 |
28 | export const handler = async (args: Args): Promise => {
29 | const config = require(args.config) as ServerConfig;
30 | await config.setup();
31 | console.log('Successfully set up your database');
32 | };
33 |
--------------------------------------------------------------------------------
/src/server/src/commands/version.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { Argv } from 'yargs';
5 |
6 | export const command = 'version';
7 |
8 | export const description = 'Print the Build Tracker version';
9 |
10 | export const builder = (yargs): Argv<{}> => yargs.usage(`Usage: $0 ${command}`);
11 |
12 | export const handler = (): void => {
13 | process.stdout.write(`${require('@build-tracker/server/package.json').version}\n`);
14 | };
15 |
--------------------------------------------------------------------------------
/src/server/src/csp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import { IHelmetContentSecurityPolicyDirectives } from 'helmet';
5 | import { Request, Response } from 'express';
6 |
7 | const getNonce = (_req: Request, res: Response): string => `'nonce-${res.locals.nonce}'`;
8 |
9 | const getCSP = (allowUnsafeEval = false): IHelmetContentSecurityPolicyDirectives => ({
10 | blockAllMixedContent: true,
11 | connectSrc: ["'self'"],
12 | childSrc: ["'self'"],
13 | defaultSrc: ["'self'"],
14 | fontSrc: ["'self'"],
15 | formAction: ["'self'"],
16 | frameAncestors: ["'none'"],
17 | frameSrc: ["'none'"],
18 | imgSrc: ["'self'"],
19 | manifestSrc: ["'self'"],
20 | mediaSrc: ["'self'"],
21 | objectSrc: ["'self'"],
22 | scriptSrc: ["'self'", allowUnsafeEval && "'unsafe-eval'", "'strict-dynamic'", getNonce].filter(Boolean),
23 | styleSrc: ["'self'", "'unsafe-inline'"],
24 | upgradeInsecureRequests: false,
25 | workerSrc: ["'self'"],
26 | });
27 |
28 | export default getCSP;
29 |
--------------------------------------------------------------------------------
/src/server/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Copyright (c) 2019 Paul Armstrong
4 | */
5 | import * as path from 'path';
6 | import yargs from 'yargs';
7 |
8 | const argv = yargs
9 | .usage('$0 [args]')
10 | .help('help', 'Show this help screen')
11 | .alias('help', 'h')
12 | .wrap(Math.min(process.stdout.columns, 120))
13 | .recommendCommands()
14 | .option('v', {
15 | default: 0,
16 | describe: 'Set the logging verbosity. Use -vv for more verbose',
17 | global: true,
18 | type: 'count',
19 | })
20 | .group(['help', 'verbose'], 'Global:')
21 | .commandDir(path.join(__dirname, 'commands'), {
22 | extensions: ['ts', 'js'],
23 | exclude: /\.d\.ts$/,
24 | })
25 | .demandCommand(1, 1, 'Please provide a command to run').argv;
26 |
27 | /* eslint-disable no-console */
28 | argv.v >= 2 && console.debug(argv);
29 |
--------------------------------------------------------------------------------
/src/server/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | import Comparator from '@build-tracker/comparator';
5 | import { Artifact, BuildMeta } from '@build-tracker/build';
6 |
7 | export interface Build {
8 | meta: BuildMeta;
9 | artifacts: Array;
10 | }
11 |
12 | export interface Queries {
13 | build: {
14 | byRevision: (revision: string) => Promise;
15 | insert: (build: Build) => Promise;
16 | };
17 | builds: {
18 | byRevisions: (revisions: Array) => Promise>;
19 | byRevisionRange: (startRevision: string, endRevision: string) => Promise>;
20 | byTimeRange: (startTimestamp: number, endTimestamp: number, branch: string) => Promise>;
21 | recent: (limit: number, branch: string) => Promise>;
22 | };
23 | }
24 |
25 | export interface Handlers {
26 | onBuildInsert?: (comparator: Comparator) => Promise;
27 | }
28 |
--------------------------------------------------------------------------------
/src/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/README.md:
--------------------------------------------------------------------------------
1 | # @build-tracker/types
2 |
3 | > Shared types and enums used throughout [Build Tracker](https://buildtracker.dev).
4 |
5 | Read the [documentation online](https://buildtracker.dev/docs/packages/types) for more information on getting started with Build Tracker.
6 |
--------------------------------------------------------------------------------
/src/types/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './dist';
5 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export * from './src';
5 |
--------------------------------------------------------------------------------
/src/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@build-tracker/types",
3 | "version": "1.0.0",
4 | "description": "Build Tracker typescript types",
5 | "author": "Paul Armstrong ",
6 | "repository": "git@github.com:paularmstrong/build-tracker.git",
7 | "license": "MIT",
8 | "main": "dist",
9 | "module": "./",
10 | "types": "dist/index.d.ts",
11 | "sideEffects": false,
12 | "files": [
13 | "index.js",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "build": "tsc",
18 | "clean": "rimraf dist"
19 | },
20 | "gitHead": "156788a6a199fc25e21adf354c106defd002afbf",
21 | "publishConfig": {
22 | "access": "public"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/types/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2019 Paul Armstrong
3 | */
4 | export type ArtifactFilters = Array;
5 |
6 | export enum BudgetLevel {
7 | WARN = 'warn',
8 | ERROR = 'error',
9 | }
10 |
11 | export enum BudgetType {
12 | DELTA = 'delta',
13 | PERCENT_DELTA = 'percentDelta',
14 | SIZE = 'size',
15 | }
16 |
17 | export interface Budget {
18 | /**
19 | * Budget callout level
20 | * warn - inform that this change could be problematic
21 | * error - this change violates the budget and should not be accepted
22 | */
23 | level: BudgetLevel;
24 | /**
25 | * Type of file size to operate on. Usually 'gzip' or 'stat1'
26 | * @type {string}
27 | */
28 | sizeKey: string;
29 | /**
30 | * Type of change to moniro
31 | * delta - maximum allowed for new value minus previous value
32 | * percentDelta - maximum allowed for the percent change from the previous value to the new value
33 | * size - absolute maximum value
34 | */
35 | type: BudgetType;
36 | /**
37 | * Maximum value for the type defined above
38 | * @type {number}
39 | */
40 | maximum: number;
41 | }
42 |
43 | export interface BudgetResult {
44 | sizeKey: string;
45 | passing: boolean;
46 | expected: number;
47 | actual: number;
48 | type: Budget['type'];
49 | level: Budget['level'];
50 | }
51 |
52 | export interface Group {
53 | artifactNames: Array;
54 | artifactMatch?: RegExp;
55 | budgets?: Array;
56 | name: string;
57 | }
58 |
59 | export interface ArtifactBudgets {
60 | [artifactName: string]: Array;
61 | }
62 |
63 | export interface AppConfig {
64 | artifacts?: {
65 | budgets?: ArtifactBudgets;
66 | filters?: ArtifactFilters;
67 | groups?: Array;
68 | };
69 | /**
70 | * Budgets for the sum of all artifacts
71 | * @type {Array}
72 | */
73 | budgets?: Array;
74 | hideAttribution?: boolean;
75 | name?: string;
76 | defaultSizeKey?: string;
77 | }
78 |
--------------------------------------------------------------------------------
/src/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "allowJs": true,
5 | "allowSyntheticDefaultImports": true,
6 | "checkJs": false,
7 | "declaration": true,
8 | "declarationMap": true,
9 | "esModuleInterop": true,
10 | "jsx": "react",
11 | "lib": ["esnext", "dom"],
12 | "module": "commonjs",
13 | "moduleResolution": "node",
14 | "noEmit": false,
15 | "noImplicitReturns": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "outDir": "./dist",
19 | "preserveConstEnums": true,
20 | "removeComments": false,
21 | "resolveJsonModule": true,
22 | "skipLibCheck": true,
23 | "sourceMap": true,
24 | "target": "es5",
25 | "paths": {
26 | "react-native": ["./typings/react-native"]
27 | }
28 | },
29 | "exclude": ["**/*.test.ts", "**/*.test.tsx", "src/*/dist/**", "plugins/**/dist/**"]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true
5 | },
6 | "exclude": ["src/*/dist/**", "plugins/**/dist/**"]
7 | }
8 |
--------------------------------------------------------------------------------
/typings/react-native/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------