diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c9717ca5..c38095564 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ jobs: - name: 'Build web app artifacts' run: | npm run build - npm run doc - uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/run-release.yml b/.github/workflows/run-release.yml index 7a7c021ad..943ad52ae 100644 --- a/.github/workflows/run-release.yml +++ b/.github/workflows/run-release.yml @@ -95,7 +95,6 @@ jobs: - run: | PUBLIC_URL='/farmhand' npm run build - npm run doc # https://github.com/marketplace/actions/deploy-to-github-pages - name: Deploy to jeremyckahn.github.io/farmhand diff --git a/.jsdoc b/.jsdoc deleted file mode 100644 index b5f762377..000000000 --- a/.jsdoc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "plugins": [ - "plugins/markdown", - "./jsdoc-ts.js" - ], - "opts": { - "destination": "build/docs", - "template": "node_modules/@jeremyckahn/minami", - "readme": "README.md" - }, - "templates": { - "cleverLinks": true, - "useLongnameInNav": false, - "showInheritedInNav": true - } -} diff --git a/README.md b/README.md index 941963a6b..48e0a3a5d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ - Includes native app downloads for Linux, MacOS and Windows. - All versioned releases available at [unpkg](https://unpkg.com/browse/@jeremyckahn/farmhand/build/) - [Wiki](https://github.com/jeremyckahn/farmhand/wiki) -- [Data model documentation](https://jeremyckahn.github.io/farmhand/docs/index.html) - [API deployment logs](https://farmhand.vercel.app/_logs) Community links: diff --git a/doc/adr/0005-type-system.md b/doc/adr/0005-type-system.md new file mode 100644 index 000000000..b45d132e3 --- /dev/null +++ b/doc/adr/0005-type-system.md @@ -0,0 +1,26 @@ +# 5. Type system + +Date: 2023-07-23 + +## Status + +Accepted + +## Context + +Farmhand is implemented with JavaScript. Given the size of the codebase, the dynamic nature of JavaScript creates some ambiguity and confusion during development. A type system helps to mitigate these challenges. The ideal type system solution would be a migration to TypeScript, but that is a significant undertaking that would take an outsize amount of effort to complete and get value from. A more approachable solution is to incorporate [Typed JavaScript](https://depth-first.com/articles/2021/11/03/typed-javascript/). Typed JavaScript can be adopted incrementally and does not introduce new syntaxes or tooling needs. + +## Decision + +Farmhand will transition to using Typed JavaScript. This transition will take a long time and may never be fully completed, but effort should be made to update preexisting code as it modified. Type violations will not be used to break builds until all type violations have been fixed. + +## Consequences + +- Code will be more clearly and completely documented +- Data type errors will be more obvious +- Improved support for automated refactoring for editors that support such features +- More verbose code +- Many features of TypeScript are unavailable in Typed JavaScript, such as proper enums and interface extension +- Errors will be shown during development in editors that support Typed JavaScript until the code has been updated to be type safe +- The transition may never be complete +- At will be much easier to transition to TypeScript if the transition to Typed JavaScript is ever completed diff --git a/jsdoc-ts.js b/jsdoc-ts.js deleted file mode 100644 index 5427bfc4a..000000000 --- a/jsdoc-ts.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * A small JSDoc plugin that replaces TSDoc (typescript flavored JSDoc) - * with regular JSDoc for not crashing documentation generation, while - * still having accurate types. - * @author arthuro555 - */ - -const TYPESCRIPT_IMPORT_TYPE = /typedef {import\(.*\)/ -const TYPESCRIPT_RECORD_TYPE = /Record<.*,(.*)>/ - -exports.astNodeVisitor = { - visitNode: function(node) { - if (node.type === 'File') { - if (node.comments) { - node.comments.forEach((/** @type {{ value: string }} */ comment) => { - // Remove imports as they are only necessary for typescript and crash jsdoc - if (TYPESCRIPT_IMPORT_TYPE.test(comment.value)) comment.value = '' - - // Replace typescript Record syntax with JSDoc Object. - comment.value = comment.value.replace( - TYPESCRIPT_RECORD_TYPE, - `Object.<$1>` - ) - }) - } - } - }, -} diff --git a/package-lock.json b/package-lock.json index ad166a642..88004ddec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,16 +77,19 @@ }, "devDependencies": { "@babel/node": "^7.20.7", - "@jeremyckahn/minami": "^1.3.1", "@testing-library/dom": "^8.3.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", + "@types/dinero.js": "^1.9.0", + "@types/lodash.sortby": "^4.7.7", "@types/markdown-table": "^2.0.0", "@types/react": "^17.0.2", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/parser": "^2.23.0", "bittorrent-tracker": "^9.19.0", + "browserslist": "^4.21.9", "cross-env": "^7.0.2", "electron": "^22.1.0", "electron-builder": "^23.1.0", @@ -104,7 +107,6 @@ "jest-extended": "^0.11.5", "jest-junit": "^14.0.1", "jimp": "^0.22.8", - "jsdoc": "^3.5.5", "jsdom": "^16.2.1", "markdown-table": "^2.0.0", "mprocs": "^0.6.4", @@ -4106,13 +4108,6 @@ "node": ">=8" } }, - "node_modules/@jeremyckahn/minami": { - "version": "1.3.1", - "dev": true, - "peerDependencies": { - "jsdoc": "^3.5.5" - } - }, "node_modules/@jest/console": { "version": "27.5.1", "dev": true, @@ -6669,6 +6664,12 @@ "@types/ms": "*" } }, + "node_modules/@types/dinero.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/dinero.js/-/dinero.js-1.9.0.tgz", + "integrity": "sha512-H2XdE6N/A2wJ/TJhGqeHDMUhCaey2R/Lcq9ichGBncKsFGvqrroXZWPNdDkCcgQOBPoCD4n9QuSBUC/35wuJiw==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.4.10", "dev": true, @@ -6847,35 +6848,31 @@ "@types/node": "*" } }, - "node_modules/@types/linkify-it": { - "version": "3.0.2", + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "dev": true + }, + "node_modules/@types/lodash.sortby": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.7.tgz", + "integrity": "sha512-J/4IS+jQopGBrrRetBXDCX0KnSeXJZ0rOTmGAxR9MWGV24YdHxX8IRi9LCGAU9GKWlBov9KRSfQpuup9PReqrw==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/lodash": "*" + } }, "node_modules/@types/long": { "version": "4.0.2", "license": "MIT" }, - "node_modules/@types/markdown-it": { - "version": "12.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" - } - }, "node_modules/@types/markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/markdown-table/-/markdown-table-2.0.0.tgz", "integrity": "sha512-fVZN/DRjZvjuk+lo7ovlI/ZycS51gpYU5vw5EcFeqkcX6lucQ+UWgEOH2O4KJHkSck4DHAY7D7CkVLD0wzc5qw==", "dev": true }, - "node_modules/@types/mdurl": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mime": { "version": "3.0.1", "dev": true, @@ -7064,6 +7061,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "dev": true + }, "node_modules/@types/verror": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", @@ -9624,7 +9627,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.21.4", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "funding": [ { @@ -9634,14 +9639,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -10059,7 +10067,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001441", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true, "funding": [ { @@ -10069,9 +10079,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -10093,17 +10106,6 @@ "node": ">=6" } }, - "node_modules/catharsis": { - "version": "0.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/cborg": { "version": "1.9.6", "license": "Apache-2.0", @@ -12602,9 +12604,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.284", - "dev": true, - "license": "ISC" + "version": "1.4.467", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz", + "integrity": "sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg==", + "dev": true }, "node_modules/electron-updater": { "version": "5.3.0", @@ -21261,66 +21264,11 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, "node_modules/jsbn": { "version": "0.1.1", "license": "MIT", "optional": true }, - "node_modules/jsdoc": { - "version": "3.6.11", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsdoc/node_modules/escape-string-regexp": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jsdoc/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jsdom": { "version": "16.7.0", "dev": true, @@ -21695,14 +21643,6 @@ "node": ">=0.10.0" } }, - "node_modules/klaw": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, "node_modules/kleur": { "version": "3.0.3", "dev": true, @@ -22619,14 +22559,6 @@ "dev": true, "license": "MIT" }, - "node_modules/linkify-it": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, "node_modules/load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -22985,38 +22917,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/markdown-it": { - "version": "12.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.6", - "dev": true, - "license": "Unlicense", - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", @@ -23030,17 +22930,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "4.2.5", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -23065,11 +22954,6 @@ "version": "2.0.14", "license": "CC0-1.0" }, - "node_modules/mdurl": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, "node_modules/media-typer": { "version": "0.3.0", "dev": true, @@ -24074,9 +23958,10 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.8", - "dev": true, - "license": "MIT" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true }, "node_modules/node-sass": { "version": "8.0.0", @@ -29138,14 +29023,6 @@ "dev": true, "license": "MIT" }, - "node_modules/requizzle": { - "version": "0.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - } - }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "license": "MIT" @@ -31728,10 +31605,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/taffydb": { - "version": "2.6.2", - "dev": true - }, "node_modules/tailwindcss": { "version": "3.2.4", "dev": true, @@ -32631,11 +32504,6 @@ "node": ">=4.2.0" } }, - "node_modules/uc.micro": { - "version": "1.0.6", - "dev": true, - "license": "MIT" - }, "node_modules/uint8arrays": { "version": "2.1.10", "license": "MIT", @@ -32662,11 +32530,6 @@ "dev": true, "license": "MIT" }, - "node_modules/underscore": { - "version": "1.13.6", - "dev": true, - "license": "MIT" - }, "node_modules/unherit": { "version": "1.1.3", "license": "MIT", @@ -32904,7 +32767,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "funding": [ { @@ -32914,15 +32779,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -34365,11 +34233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/xmlhttprequest-ssl": { "version": "2.0.0", "engines": { @@ -36991,10 +36854,6 @@ "version": "0.1.3", "dev": true }, - "@jeremyckahn/minami": { - "version": "1.3.1", - "dev": true - }, "@jest/console": { "version": "27.5.1", "dev": true, @@ -38775,6 +38634,12 @@ "@types/ms": "*" } }, + "@types/dinero.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/dinero.js/-/dinero.js-1.9.0.tgz", + "integrity": "sha512-H2XdE6N/A2wJ/TJhGqeHDMUhCaey2R/Lcq9ichGBncKsFGvqrroXZWPNdDkCcgQOBPoCD4n9QuSBUC/35wuJiw==", + "dev": true + }, "@types/eslint": { "version": "8.4.10", "dev": true, @@ -38927,31 +38792,30 @@ "@types/node": "*" } }, - "@types/linkify-it": { - "version": "3.0.2", + "@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, - "@types/long": { - "version": "4.0.2" - }, - "@types/markdown-it": { - "version": "12.2.3", + "@types/lodash.sortby": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.7.tgz", + "integrity": "sha512-J/4IS+jQopGBrrRetBXDCX0KnSeXJZ0rOTmGAxR9MWGV24YdHxX8IRi9LCGAU9GKWlBov9KRSfQpuup9PReqrw==", "dev": true, "requires": { - "@types/linkify-it": "*", - "@types/mdurl": "*" + "@types/lodash": "*" } }, + "@types/long": { + "version": "4.0.2" + }, "@types/markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/markdown-table/-/markdown-table-2.0.0.tgz", "integrity": "sha512-fVZN/DRjZvjuk+lo7ovlI/ZycS51gpYU5vw5EcFeqkcX6lucQ+UWgEOH2O4KJHkSck4DHAY7D7CkVLD0wzc5qw==", "dev": true }, - "@types/mdurl": { - "version": "1.0.2", - "dev": true - }, "@types/mime": { "version": "3.0.1", "dev": true @@ -39120,6 +38984,12 @@ "version": "2.0.2", "dev": true }, + "@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "dev": true + }, "@types/verror": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", @@ -40903,13 +40773,15 @@ "version": "1.0.3" }, "browserslist": { - "version": "4.21.4", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" } }, "bser": { @@ -41198,7 +41070,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001441", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true }, "case-sensitive-paths-webpack-plugin": { @@ -41212,13 +41086,6 @@ "catering": { "version": "2.1.1" }, - "catharsis": { - "version": "0.9.0", - "dev": true, - "requires": { - "lodash": "^4.17.15" - } - }, "cborg": { "version": "1.9.6" }, @@ -42882,7 +42749,9 @@ } }, "electron-to-chromium": { - "version": "1.4.284", + "version": "1.4.467", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz", + "integrity": "sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg==", "dev": true }, "electron-updater": { @@ -48828,48 +48697,10 @@ "argparse": "^2.0.1" } }, - "js2xmlparser": { - "version": "4.0.2", - "dev": true, - "requires": { - "xmlcreate": "^2.0.4" - } - }, "jsbn": { "version": "0.1.1", "optional": true }, - "jsdoc": { - "version": "3.6.11", - "dev": true, - "requires": { - "@babel/parser": "^7.9.4", - "@types/markdown-it": "^12.2.3", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^12.3.2", - "markdown-it-anchor": "^8.4.1", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.2" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "dev": true - } - } - }, "jsdom": { "version": "16.7.0", "dev": true, @@ -49155,13 +48986,6 @@ "version": "6.0.3", "dev": true }, - "klaw": { - "version": "3.0.0", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, "kleur": { "version": "3.0.3", "dev": true @@ -49909,13 +49733,6 @@ "version": "1.2.4", "dev": true }, - "linkify-it": { - "version": "3.0.3", - "dev": true, - "requires": { - "uc.micro": "^1.0.1" - } - }, "load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -50169,27 +49986,6 @@ "markdown-escapes": { "version": "1.0.4" }, - "markdown-it": { - "version": "12.3.2", - "dev": true, - "requires": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "dependencies": { - "entities": { - "version": "2.1.0", - "dev": true - } - } - }, - "markdown-it-anchor": { - "version": "8.6.6", - "dev": true - }, "markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", @@ -50199,10 +49995,6 @@ "repeat-string": "^1.0.0" } }, - "marked": { - "version": "4.2.5", - "dev": true - }, "matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -50222,10 +50014,6 @@ "mdn-data": { "version": "2.0.14" }, - "mdurl": { - "version": "1.0.1", - "dev": true - }, "media-typer": { "version": "0.3.0", "dev": true @@ -50910,7 +50698,9 @@ "dev": true }, "node-releases": { - "version": "2.0.8", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node-sass": { @@ -54104,13 +53894,6 @@ "version": "1.0.0", "dev": true }, - "requizzle": { - "version": "0.2.4", - "dev": true, - "requires": { - "lodash": "^4.17.21" - } - }, "resize-observer-polyfill": { "version": "1.5.1" }, @@ -55834,10 +55617,6 @@ } } }, - "taffydb": { - "version": "2.6.2", - "dev": true - }, "tailwindcss": { "version": "3.2.4", "dev": true, @@ -56439,10 +56218,6 @@ "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", "dev": true }, - "uc.micro": { - "version": "1.0.6", - "dev": true - }, "uint8arrays": { "version": "2.1.10", "requires": { @@ -56463,10 +56238,6 @@ "version": "2.0.5", "dev": true }, - "underscore": { - "version": "1.13.6", - "dev": true - }, "unherit": { "version": "1.1.3", "requires": { @@ -56629,7 +56400,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -57665,10 +57438,6 @@ "version": "2.2.0", "dev": true }, - "xmlcreate": { - "version": "2.0.4", - "dev": true - }, "xmlhttprequest-ssl": { "version": "2.0.0" }, diff --git a/package.json b/package.json index 2f674a33c..7eb763c4b 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,6 @@ "dev": "mprocs \"cross-env REACT_APP_TRACKER_URL='ws://localhost:8000' npm run start\" \"npm run start:api\" \"npm run start:backend\" \"npm run start:tracker\"", "dev:native": "mprocs \"BROWSER=none npm run start\" \"npm run start:api\" \"npm run start:backend\" \"npm run electron\"", "electron": "wait-on tcp:3000 && electron .", - "doc": "jsdoc -c .jsdoc -r src", - "doc:view": "serve -S dist/doc", - "doc:watch": "nodemon --exec \"npm run doc\" --watch src --watch ./ --ext js,md --ignore dist", "print:crops": "npx -p @babel/core -p @babel/node babel-node --presets @babel/preset-env src/scripts/generate-crop-table.js", "lint": "eslint src --max-warnings=0", "prettier": "prettier 'src/**/*.js' --write", @@ -42,16 +39,19 @@ }, "devDependencies": { "@babel/node": "^7.20.7", - "@jeremyckahn/minami": "^1.3.1", "@testing-library/dom": "^8.3.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", + "@types/dinero.js": "^1.9.0", + "@types/lodash.sortby": "^4.7.7", "@types/markdown-table": "^2.0.0", "@types/react": "^17.0.2", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/parser": "^2.23.0", "bittorrent-tracker": "^9.19.0", + "browserslist": "^4.21.9", "cross-env": "^7.0.2", "electron": "^22.1.0", "electron-builder": "^23.1.0", @@ -69,7 +69,6 @@ "jest-extended": "^0.11.5", "jest-junit": "^14.0.1", "jimp": "^0.22.8", - "jsdoc": "^3.5.5", "jsdom": "^16.2.1", "markdown-table": "^2.0.0", "mprocs": "^0.6.4", diff --git a/src/components/Farmhand/Farmhand.js b/src/components/Farmhand/Farmhand.js index 916f71be8..67a129853 100644 --- a/src/components/Farmhand/Farmhand.js +++ b/src/components/Farmhand/Farmhand.js @@ -270,8 +270,8 @@ const applyPriceEvents = (valueAdjustments, priceCrashes, priceSurges) => { * @property {Object} todaysPurchases Keys are item names, values are their * respective quantities. * @property {number} todaysRevenue Should always be a positive number. - * @property {Object} todaysStartingInventory Keys are item names, values are - * their respective quantities. + * @property {Record} todaysStartingInventory Keys + * are item names, values are their respective quantities. * @property {boolean} useAlternateEndDayButtonPosition Option to display the * Bed button on the left side of the screen. * @property {Object.} valueAdjustments diff --git a/src/enums.js b/src/enums.js index e550c220a..9ab5257c3 100644 --- a/src/enums.js +++ b/src/enums.js @@ -5,7 +5,7 @@ /** * @param {Array.} keys - * @returns {Object.} + * @returns {Record} */ export const enumify = keys => keys.reduce((acc, key) => ({ [key]: key, ...acc }), {}) diff --git a/src/index.js b/src/index.js index 0e02ad048..bf4cf27cb 100644 --- a/src/index.js +++ b/src/index.js @@ -2,24 +2,32 @@ * @namespace farmhand */ +/** + * @typedef {import("./enums").cropType} cropType + * @typedef {import("./enums").cowColors} cowColors + * @typedef {import("./enums").recipeType} recipeType + * @typedef {import("./enums").toolLevel} toolLevel + * @typedef {import("./enums").toolType} toolType + * @typedef {import("./enums").fertilizerType} fertilizerType + */ + /** * Lookup table for the lifecycle durations of a crop (in days). * @typedef farmhand.cropTimetable - * @readonly - * @type {Object} * @property {number} seed * @property {number} growing + * @readonly */ /** * Reference object for an item. * @typedef farmhand.item - * @type {Object} * @property {string} id * @property {string} name * @property {string} type + * @property {number} value * @property {farmhand.cropTimetable} [cropTimetable] - * @property {farmhand.module:enums.cropType} [cropType] + * @property {cropType} [cropType] * @property {string} [description] A user-friendly description of the item. * @property {string} [enablesFieldMode] The fieldMode that this item enables. * @property {string|Array.} [growsInto] The id of farmhand.item or list of ids of other farmhand.items that this farmhand.item (likely a crop seed) will grow into. @@ -36,28 +44,28 @@ /** * @typedef farmhand.cropVariety - * @type {farmhand.item} * @property {string} imageId + * @extends farmhand.item */ /** * This is a minimalist base type to be inherited and expanded on by types like * farmhand.crop. This also represents non-crop plot content like scarecrows * and sprinklers. - * @typedef farmhand.plotContent - * @type {Object} + * @typedef farmhand.plotContentType * @property {string} itemId + * @property {boolean=} isFertilized Deprecated by fertilizerType. + * @property {fertilizerType} fertilizerType + * @typedef {farmhand.plotContentType} farmhand.plotContent */ /** * Represents a crop as it proceeds through the lifecycle. - * @typedef farmhand.crop - * @type {farmhand.plotContent} + * @typedef farmhand.cropType * @property {number} daysOld * @property {number} daysWatered - * @property {boolean?} isFertilized Deprecated by fertilizerType. - * @property {farmhand.module:enums.fertilizerType} fertilizerType * @property {boolean} wasWateredToday + * @typedef {farmhand.plotContent & farmhand.cropType} farmhand.crop */ /** @@ -76,7 +84,7 @@ * @type {Object} * @property {number} baseWeight * @property {string} color - * @property {Object.} colorsInBloodline + * @property {Object.} colorsInBloodline * @property {number} daysOld * @property {number} daysSinceMilking Only applies to female cows. * @property {number} daysSinceProducingFertilizer Only applies to male cows. @@ -109,14 +117,14 @@ /** * @typedef farmhand.recipe - * @readonly * @type {farmhand.item} - * @property {farmhand.module:enums.recipeType} recipeType The type of recipe + * @property {recipeType} recipeType The type of recipe * this is. * @property {{string: number}} ingredients An object where each * key is the id of a farmhand.item and the value is the quantity of that item. * @property {farmhand.recipeCondition} condition This must return `true` for * the recipe to be made available to the player. + * @readonly */ /** @@ -151,7 +159,6 @@ /** * @typedef farmhand.achievement - * @readonly * @type {Object} * @property {string} id * @property {string} name @@ -159,15 +166,16 @@ * @property {string} rewardDescription * @property {farmhand.achievementCondition} condition * @property {farmhand.achievementReward} reward + * @readonly */ /** * @typedef farmhand.level - * @readonly * @type {Object} * @property {number} id * @property {boolean} [increasesSprinklerRange] * @property {string} [unlocksShopItem] Must reference a farmhand.item id. + * @readonly */ /** @@ -192,14 +200,14 @@ * @property {string} id * @property {string?} description * @property {string} name - * @property {Object.?} ingredients - * @property {farmhand.module:enums.toolLevel?} nextLevel + * @property {Record?} ingredients + * @property {toolLevel?} nextLevel * @property {boolean?} isMaxLevel */ /** * @typedef farmhand.upgradesMetadata - * @type {Object.} + * @type {Object.} */ /** diff --git a/src/utils/index.js b/src/utils/index.js index cc2c358c1..72b6359cc 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,6 +1,16 @@ /** @typedef {import("../index").farmhand.crop} farmhand.crop */ /** @typedef {import("../index").farmhand.item} farmhand.item */ /** @typedef {import("../index").farmhand.plotContent} farmhand.plotContent */ +/** @typedef {import("../index").farmhand.shoveledPlot} farmhand.shoveledPlot */ +/** @typedef {import("../index").farmhand.cropTimetable} farmhand.cropTimetable */ +/** @typedef {import("../index").farmhand.cow} farmhand.cow */ +/** @typedef {import("../index").farmhand.recipe} farmhand.recipe */ +/** @typedef {import("../index").farmhand.priceEvent} farmhand.priceEvent */ +/** @typedef {import("../index").farmhand.cowBreedingPen} farmhand.cowBreedingPen */ +/** @typedef {import("../enums").cropLifeStage} farmhand.cropLifeStage */ +/** @typedef {import("../enums").toolLevel} farmhand.toolLevel */ +/** @typedef {import("../enums").toolType} farmhand.toolType */ +/** @typedef {import("../components/Farmhand/Farmhand").farmhand.state} farmhand.state */ /** * @module farmhand.utils @@ -37,7 +47,6 @@ import { fertilizerType, genders, itemType, - stageFocusType, standardCowColors, toolLevel, } from '../enums' @@ -86,6 +95,20 @@ const Jimp = configureJimp({ const { SEED, GROWING, GROWN } = cropLifeStage +/** + * @param {unknown} obj + * @returns {obj is farmhand.plotContent} + */ +const isPlotContent = (obj = {}) => + Boolean(obj && obj['itemId'] && obj['fertilizerType']) + +/** + * @param {unknown} obj + * @returns {obj is farmhand.shoveledPlot} + */ +const isShoveledPlot = (obj = {}) => + Boolean(obj && obj['isShoveled'] && obj['daysUntilClear']) + const purchasableItemMap = [...cowShopInventory, ...shopInventory].reduce( (acc, item) => { acc[item.id] = item @@ -107,11 +130,14 @@ export const chooseRandomIndex = list => */ export const chooseRandom = list => list[chooseRandomIndex(list)] -// Ensures that the condition argument to memoize() is not ignored, per -// https://github.com/caiogondim/fast-memoize.js#function-arguments -// -// Pass this is the `serializer` option to any memoize()-ed functions that -// accept function arguments. +/** + * Ensures that the condition argument to memoize() is not ignored, per + * https://github.com/caiogondim/fast-memoize.js#function-arguments + * + * Pass this is the `serializer` option to any memoize()-ed functions that + * accept function arguments. + * @param {any[]} args + */ const memoizationSerializer = args => JSON.stringify( [...args].map(arg => (typeof arg === 'function' ? arg.toString() : arg)) @@ -125,11 +151,14 @@ const memoizationSerializer = args => export const clampNumber = (num, min, max) => num <= min ? min : num >= max ? max : num +/** + * @param {number} num + */ export const castToMoney = num => Math.round(num * 100) / 100 /** * Safely adds dollar figures to avoid IEEE 754 rounding errors. - * @param {...number} num Numbers that represent money values. + * @param {...number} args Numbers that represent money values. * @returns {number} * @see http://adripofjavascript.com/blog/drips/avoiding-problems-with-decimal-math-in-javascript.html */ @@ -192,7 +221,7 @@ export const percentageString = number => `${Math.round(number * 100)}%` /** * @param {farmhand.item} item - * @param {Object.} valueAdjustments + * @param {Record} valueAdjustments * @returns {number} */ export const getItemCurrentValue = ({ id }, valueAdjustments) => @@ -207,7 +236,7 @@ export const getItemCurrentValue = ({ id }, valueAdjustments) => }).toUnit() /** - * @param {Object} valueAdjustments + * @param {Record} valueAdjustments * @param {string} itemId * @returns {number} Rounded to a money value. */ @@ -239,11 +268,10 @@ export const getPlotContentFromItemId = itemId => ({ * @param {string} itemId * @returns {farmhand.crop} */ -export const getCropFromItemId = itemId => ({ +export const getCropFromItemId = itemId => /** @type farmhand.crop */ ({ ...getPlotContentFromItemId(itemId), daysOld: 0, daysWatered: 0, - fertilizerType: fertilizerType.NONE, wasWateredToday: false, }) @@ -259,7 +287,7 @@ export const getPlotContentType = ({ itemId }) => * @returns {boolean} */ export const doesPlotContainCrop = plot => - plot && getPlotContentType(plot) === itemType.CROP + plot !== null && getPlotContentType(plot) === itemType.CROP /** * @param {farmhand.item} item @@ -279,12 +307,13 @@ export const isItemAFarmProduct = item => item.type === itemType.CRAFTED_ITEM ) -/** - * @param {farmhand.cropTimetable} cropTimetable - * @returns {Array.} - */ -export const getLifeStageRange = memoize(cropTimetable => +export const getLifeStageRange = memoize(( + /** @type {farmhand.cropTimetable} */ cropTimetable +) => [SEED, GROWING].reduce( + /** + * @param {farmhand.cropLifeStage[]} acc + */ (acc, stage) => acc.concat(Array(cropTimetable[stage]).fill(stage)), [] ) @@ -292,51 +321,60 @@ export const getLifeStageRange = memoize(cropTimetable => /** * @param {farmhand.crop} crop - * @returns {enums.cropLifeStage} + * @returns {farmhand.cropLifeStage} */ -export const getCropLifeStage = ({ itemId, daysWatered }) => - getLifeStageRange(itemsMap[itemId].cropTimetable)[Math.floor(daysWatered)] || - GROWN +export const getCropLifeStage = crop => { + const { itemId, daysWatered } = crop + const { cropTimetable } = itemsMap[itemId] + + if (!cropTimetable) { + throw new Error(`${itemId} has no cropTimetable`) + } + + return getLifeStageRange(cropTimetable)[Math.floor(daysWatered)] || GROWN +} /** - * @param {farmhand.plotContent} plotContent + * @param {farmhand.plotContent | farmhand.shoveledPlot | null} plotContents * @param {number} x * @param {number} y * @returns {?string} */ -export const getPlotImage = (plotContent, x, y) => { - if (plotContent) { - if (getPlotContentType(plotContent) === itemType.CROP) { +export const getPlotImage = (plotContents, x, y) => { + if (isPlotContent(plotContents)) { + if (isPlotContentACrop(plotContents)) { let itemImageId - switch (getCropLifeStage(plotContent)) { + switch (getCropLifeStage(plotContents)) { case GROWN: - itemImageId = plotContent.itemId + itemImageId = plotContents.itemId break case GROWING: - itemImageId = `${plotContent.itemId}-growing` + itemImageId = `${plotContents.itemId}-growing` break default: - const seedItem = cropItemIdToSeedItemMap[plotContent.itemId] + const seedItem = cropItemIdToSeedItemMap[plotContents.itemId] itemImageId = seedItem.id } return itemImages[itemImageId] } - if (getPlotContentType(plotContent) === itemType.WEED) { + if (getPlotContentType(plotContents) === itemType.WEED) { const weedColors = ['yellow', 'orange', 'pink'] const color = weedColors[(x * y) % weedColors.length] return itemImages[`weed-${color}`] - } else if (plotContent?.oreId) { - return itemImages[plotContent.oreId] - } else { - return itemImages[plotContent.itemId] } } + if (isShoveledPlot(plotContents) && plotContents?.oreId) { + return itemImages[plotContents.oreId] + } else if (isPlotContent(plotContents)) { + return itemImages[plotContents.itemId] + } + return null } @@ -344,7 +382,7 @@ export const getPlotImage = (plotContent, x, y) => { * @param {number} rangeSize * @param {number} centerX * @param {number} centerY - * @returns {Array.>} + * @returns {{x: number, y: number}[][]} */ export const getRangeCoords = (rangeSize, centerX, centerY) => { const squareSize = 2 * rangeSize + 1 @@ -352,26 +390,28 @@ export const getRangeCoords = (rangeSize, centerX, centerY) => { const rangeStartY = centerY - rangeSize return new Array(squareSize) - .fill() + .fill(null) .map((_, y) => new Array(squareSize) - .fill() + .fill(null) .map((_, x) => ({ x: rangeStartX + x, y: rangeStartY + y })) ) } /** * @param {farmhand.item} item - * @param {number} [variationIdx=0] - * @returns {farmhand.item} + * @param {number} [variantIdx] */ -export const getFinalCropItemFromSeedItem = ({ id }, variantIdx = 0) => - itemsMap[getFinalCropItemIdFromSeedItemId(id, variantIdx)] +export const getFinalCropItemFromSeedItem = ({ id }, variantIdx = 0) => { + const itemId = getFinalCropItemIdFromSeedItemId(id, variantIdx) + + if (itemId) return itemsMap[itemId] +} /** * @param {string} seedItemId * @param {number} [variationIdx] - * @returns {string} + * @returns {string=} */ export const getFinalCropItemIdFromSeedItemId = ( seedItemId, @@ -386,19 +426,22 @@ export const getFinalCropItemIdFromSeedItemId = ( } } -/** - * @param {string} cropItemId - * @returns {string} - */ export const getSeedItemIdFromFinalStageCropItemId = memoize( - cropItemId => { - return Object.values(itemsMap).find(({ growsInto }) => { + (/** @type {string} */ cropItemId) => { + const seedItemId = Object.values(itemsMap).find(({ growsInto }) => { if (Array.isArray(growsInto)) { return growsInto.includes(cropItemId) } else { return growsInto === cropItemId } - }).id + })?.id + + if (!seedItemId) + throw new Error( + `Crop item ID ${cropItemId} does not have a corresponding seed` + ) + + return seedItemId }, { cacheSize: Object.keys(itemsMap).length, @@ -599,6 +642,9 @@ export const getCowValue = (cow, computeSaleValue = false) => ) : getCowWeight(cow) * 1.5 +/** + * @param {farmhand.cow} cow + */ export const getCowSellValue = cow => getCowValue(cow, true) /** @@ -620,7 +666,7 @@ export const maxYieldOfRecipe = memoize(({ ingredients }, inventory) => { /** * @param {farmhand.recipe} recipe - * @param {Array.} inventory + * @param {farmhand.item[]} inventory * @param {number} howMany * @returns {boolean} */ @@ -628,8 +674,8 @@ export const canMakeRecipe = (recipe, inventory, howMany) => maxYieldOfRecipe(recipe, inventory) >= howMany /** - * @param {Array.} itemIds - * @returns {Array.} + * @param {string[]} itemsIds + * @returns {string[]} */ export const filterItemIdsToSeeds = itemsIds => itemsIds.filter(id => itemsMap[id].type === itemType.CROP) @@ -645,10 +691,17 @@ export const getRandomUnlockedCrop = unlockedSeedItemIds => { ? chooseRandomIndex(seedItem.growsInto) : 0 - const cropItem = - itemsMap[getFinalCropItemIdFromSeedItemId(seedItemId, variationIdx)] + const finalCropItemId = getFinalCropItemIdFromSeedItemId( + seedItemId, + variationIdx + ) - return cropItem + if (!finalCropItemId) + throw new Error( + `Seed item ID ${seedItemId} has no corresponding final crop ID` + ) + + return itemsMap[finalCropItemId] } /** @@ -661,39 +714,25 @@ export const getPriceEventForCrop = cropItem => ({ getCropLifecycleDuration(cropItem) - PRICE_EVENT_STANDARD_DURATION_DECREASE, }) -/** - * @param {Array.>} field - * @param {function(?farmhand.plotContent)} condition - * @returns {?farmhand.plotContent} - */ export const findInField = memoize( - (field, condition) => field.find(row => row.find(condition)) || null, - { - serializer: memoizationSerializer, - } -) - -// This is currently unused, but it could be useful later. -/** - * @param {Array.>} field - * @param {function(?farmhand.plotContent)} filterCondition - * @returns {Array.>} - */ -export const getCrops = memoize( - (field, filterCondition) => - field.reduce((acc, row) => { - acc.push(...row.filter(filterCondition)) + /** + * @param {(?farmhand.plotContent)[][]} field + * @param {function(?farmhand.plotContent): boolean} condition + * @returns {?farmhand.plotContent} + */ + (field, condition) => { + const [plot = null] = + field.find(row => { + return row.find(condition) + }) ?? [] - return acc - }, []), + return plot + }, { serializer: memoizationSerializer, } ) -/** - * @returns {boolean} - */ export const doesMenuObstructStage = () => window.innerWidth < BREAKPOINTS.MD const itemTypesToShowInReverse = new Set([itemType.MILK]) @@ -719,12 +758,12 @@ export const sortItems = items => { return sortItemIdsByTypeAndValue(items.map(({ id }) => id)).map(id => map[id]) } -/** - * @param {Array.} inventory - * @returns {number} - */ -export const inventorySpaceConsumed = memoize(inventory => - inventory.reduce((sum, { quantity }) => sum + quantity, 0) +export const inventorySpaceConsumed = memoize( + /** + * @param {farmhand.item[]} inventory + * @returns {number} + */ + inventory => inventory.reduce((sum, { quantity = 0 }) => sum + quantity, 0) ) /** @@ -762,25 +801,26 @@ export const nullArray = memoize( } ) -/** - * @param {Array.} cowInventory - * @param {string} id - * @returns {farmhand.cow|undefined} - */ -export const findCowById = memoize((cowInventory, id) => - cowInventory.find(cow => id === cow.id) +export const findCowById = memoize( + /** + * @param {Array.} cowInventory + * @param {string} id + * @returns {farmhand.cow|undefined} + */ + (cowInventory, id) => cowInventory.find(cow => id === cow.id) ) -/** - * @param {Object.} itemsSold - * @returns {number} - */ -export const farmProductsSold = memoize(itemsSold => - Object.entries(itemsSold).reduce( - (sum, [itemId, numberSold]) => - sum + (isItemAFarmProduct(itemsMap[itemId]) ? numberSold : 0), - 0 - ) +export const farmProductsSold = memoize( + /** + * @param {Record} itemsSold + * @returns {number} + */ + itemsSold => + Object.entries(itemsSold).reduce( + (sum, [itemId, numberSold]) => + sum + (isItemAFarmProduct(itemsMap[itemId]) ? numberSold : 0), + 0 + ) ) /** @@ -905,9 +945,9 @@ export const getProfitRecord = ( ) => Math.max(recordSingleDayProfit, getProfit(todaysRevenue, todaysLosses)) /** - * @param {Object} todaysStartingInventory - * @param {Object} todaysPurchases - * @param {Array.<{ id: farmhand.item, quantity: number }>} inventory + * @param {farmhand.state['todaysStartingInventory']} todaysStartingInventory + * @param {farmhand.state['todaysPurchases']} todaysPurchases + * @param {{ id: farmhand.item['id'], quantity: number }[]} inventory * @return {Object} Keys are item IDs, values are either 1 or -1. */ export const computeMarketPositions = ( @@ -941,8 +981,8 @@ export const computeMarketPositions = ( }, {}) /** - * @param {Object.} currentToolLevels - * @param {farmhand.module:enums.toolType} toolType + * @param {Object.} currentToolLevels + * @param {farmhand.toolType} toolType * @returns {farmhand.state} */ export const unlockTool = (currentToolLevels, toolType) => { @@ -956,71 +996,15 @@ export const unlockTool = (currentToolLevels, toolType) => { } /** - * @param {Farmhand.state} state - * @return {Object} + * @param {farmhand.state} state + * @return {farmhand.state} */ export const transformStateDataForImport = state => { const sanitizedState = { ...state } - const { id } = sanitizedState const rejectedKeys = ['version'] rejectedKeys.forEach(rejectedKey => delete sanitizedState[rejectedKey]) - // Update old data models - - if (sanitizedState.field) { - // Update plot data - sanitizedState.field = sanitizedState.field.map(row => - row.map(plot => { - if (plot === null) { - return null - } - - const { isFertilized, ...rest } = plot - - return { - ...rest, - - // Convert from isFertilized (boolean) to fertilizerType (enum) - fertilizerType: - rest.fertilizerType || - (isFertilized ? fertilizerType.STANDARD : fertilizerType.NONE), - } - }) - ) - } - - const { tools: unlockedTools } = getLevelEntitlements( - levelAchieved(farmProductsSold(sanitizedState.itemsSold)) - ) - - for (const tool of Object.keys(unlockedTools)) { - sanitizedState.toolLevels = unlockTool(sanitizedState.toolLevels, tool) - } - - if ( - !sanitizedState.showHomeScreen && - sanitizedState.stageFocus === stageFocusType.HOME - ) { - sanitizedState.stageFocus = stageFocusType.SHOP - } - - // TODO: Remove these cowInventory and cowForSale transformations after - // 3/15/2023 - sanitizedState.cowInventory = sanitizedState.cowInventory.map(cow => ({ - ownerId: id, - originalOwnerId: id, - timesTraded: 0, - ...cow, - })) - - sanitizedState.cowForSale = { - ownerId: '', - originalOwnerId: '', - timesTraded: 0, - ...sanitizedState.cowForSale, - } - return sanitizedState } @@ -1048,7 +1032,8 @@ export const getCostOfNextStorageExpansion = currentInventoryLimit => { /** * Create a no-op Promise that resolves in a specified amount of time. - * @returns {Promise} + * @param {number} ms + * @returns {Promise} */ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) @@ -1070,6 +1055,13 @@ export const getSalePriceMultiplier = (completedAchievements = {}) => { return salePriceMultiplier } +/** + * @param {farmhand.plotContent} plotContents + * @returns {plotContents is farmhand.crop} + */ +const isPlotContentACrop = plotContents => + getPlotContentType(plotContents) === itemType.CROP + /** * @param {Array} weightedOptions an array of objects each containing a `weight` property * @returns {Object} one of the items from weightedOptions @@ -1104,14 +1096,16 @@ const colorizeCowTemplate = (() => { const cowImageWidth = 48 const cowImageHeight = 48 const cowImageFactoryCanvas = document.createElement('canvas') - cowImageFactoryCanvas.setAttribute('height', cowImageHeight) - cowImageFactoryCanvas.setAttribute('width', cowImageWidth) + cowImageFactoryCanvas.setAttribute('height', String(cowImageHeight)) + cowImageFactoryCanvas.setAttribute('width', String(cowImageWidth)) const cachedCowImages = {} // https://stackoverflow.com/a/5624139 const hexToRgb = memoize(hex => { - const [, r, g, b] = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + const [, r, g, b] = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec( + hex + ) ?? ['', '0', '0', '0'] return { r: parseInt(r, 16), @@ -1177,7 +1171,7 @@ const colorizeCowTemplate = (() => { /** * @param {farmhand.cow} cow - * @returns {string} Base64 representation of an image + * @returns {Promise} Base64 representation of an image */ export const getCowImage = async cow => { const cowIdNumber = convertStringToInteger(cow.id) diff --git a/src/utils/memoize.js b/src/utils/memoize.js index 8ae7f73d1..d748da507 100644 --- a/src/utils/memoize.js +++ b/src/utils/memoize.js @@ -1,3 +1,5 @@ +/** @typedef {import("fast-memoize").Options} MemoizeOptions */ + import fastMemoize from 'fast-memoize' import { MEMOIZE_CACHE_CLEAR_THRESHOLD } from '../constants' @@ -40,9 +42,10 @@ export class MemoizeCache { } /** - * @param {function} fn - * @param {Object} [config] - * @param {number} [config.cacheSize] + * @template {(...args: any[]) => any} T Copied from https://github.com/caiogondim/fast-memoize.js/blob/5cdfc8dde23d86b16e0104bae1b04cd447b98c63/typings/fast-memoize.d.ts#L1 + * @param {T} fn + * @param {MemoizeOptions & Partial<{ cacheSize: number }>} [config] + * @returns T * @see https://github.com/caiogondim/fast-memoize.js */ export const memoize = (fn, config) => diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..6a464d57b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true /* Report errors in .js files. */, + "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */, + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./build/types" /* Redirect output structure to the directory. */, + // "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + //"include": ["src/utils/index.js"], + "exclude": ["build"] +}