diff --git a/Makefile b/Makefile index 3f37ac5aaea..ee1c06d68c3 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ lint_yaml: normalize_yaml ## Lints YAML files (! git diff --name-only | grep "^config/.*\.yml$$") || (echo "Error: Run 'make normalize_yaml' to normalize YAML"; exit 1) lint_yarn_workspaces: ## Lints Yarn workspace packages - scripts/validate-workspaces.js + scripts/validate-workspaces.mjs lint_asset_bundle_size: ## Lints JavaScript and CSS compiled bundle size @# This enforces an asset size budget to ensure that download sizes are reasonable and to protect diff --git a/app/javascript/packages/README.md b/app/javascript/packages/README.md index e2d205b0586..a649bd7c8a1 100644 --- a/app/javascript/packages/README.md +++ b/app/javascript/packages/README.md @@ -2,25 +2,4 @@ Packages are independent JavaScript libraries. As much as possible, their behaviors should be reusable and make no assumptions about particular application pages or configuration. Instead, they should be initialized by a [`pack`](../packs) which provides relevant configuration and page element references. -A package should behave much like any other third-party [NPM package](https://www.npmjs.com/), where each folder in this directory represents a single package. These packages are managed using [Yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). - -Each package should include a `package.json` with at least `name`, `version`, and `private` fields: - -- Name should start with `@18f/identity-` and end with the folder name -- Version should be fixed to `1.0.0` -- Packages should be private, since they are not published -- Any `devDependencies` should be defined in the root project directory, not in a package -- Define `dependencies` for any third-party libraries used in a package - - It is not necessary to define `dependencies` for other sibling packages - -**Example:** - -_packages/analytics/package.json_ - -```json -{ - "name": "@18f/identity-analytics", - "version": "1.0.0", - "private": true -} -``` +A package should behave much like any other third-party [NPM package](https://www.npmjs.com/), where each folder in this directory represents a single package. These packages are managed using [Yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). Refer to [Front-end Architecture documentation on Yarn Workspaces](https://github.com/18F/identity-idp/blob/main/docs/frontend.md#yarn-workspaces) for more information. diff --git a/app/javascript/packages/address-search/package.json b/app/javascript/packages/address-search/package.json index 33541701e7d..4ab55f98a1b 100644 --- a/app/javascript/packages/address-search/package.json +++ b/app/javascript/packages/address-search/package.json @@ -32,5 +32,6 @@ "type": "git", "url": "https://github.com/18f/identity-idp.git", "directory": "app/javascript/packages/address-search" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/analytics/package.json b/app/javascript/packages/analytics/package.json index 6ade5aa72c0..e13018b2865 100644 --- a/app/javascript/packages/analytics/package.json +++ b/app/javascript/packages/analytics/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-analytics", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": [ + "./click-observer-element.ts" + ] } diff --git a/app/javascript/packages/assets/package.json b/app/javascript/packages/assets/package.json index e39802f92f9..23ac44a1bbe 100644 --- a/app/javascript/packages/assets/package.json +++ b/app/javascript/packages/assets/package.json @@ -4,5 +4,6 @@ "version": "1.0.0", "peerDependencies": { "webpack": ">=5" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/build-sass/package.json b/app/javascript/packages/build-sass/package.json index 27fb9961242..2941625c096 100644 --- a/app/javascript/packages/build-sass/package.json +++ b/app/javascript/packages/build-sass/package.json @@ -32,5 +32,6 @@ "chokidar": "^3.5.3", "lightningcss": "^1.23.0", "sass-embedded": "^1.70.0" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/captcha-submit-button/package.json b/app/javascript/packages/captcha-submit-button/package.json index dd41558f931..d4f99869900 100644 --- a/app/javascript/packages/captcha-submit-button/package.json +++ b/app/javascript/packages/captcha-submit-button/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-captcha-submit-button", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": [ + "./captcha-submit-button-element.ts" + ] } diff --git a/app/javascript/packages/clipboard-button/package.json b/app/javascript/packages/clipboard-button/package.json index 8b468540391..31a34e15293 100644 --- a/app/javascript/packages/clipboard-button/package.json +++ b/app/javascript/packages/clipboard-button/package.json @@ -4,5 +4,8 @@ "private": true, "dependencies": { "@18f/identity-design-system": "^8.1.2" - } + }, + "sideEffects": [ + "./clipboard-button-element.ts" + ] } diff --git a/app/javascript/packages/components/package.json b/app/javascript/packages/components/package.json index 64ca0dfd35d..b1f4e42250b 100644 --- a/app/javascript/packages/components/package.json +++ b/app/javascript/packages/components/package.json @@ -32,5 +32,6 @@ "type": "git", "url": "https://github.com/18f/identity-idp.git", "directory": "app/javascript/packages/components" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/compose-components/package.json b/app/javascript/packages/compose-components/package.json index 8ec82d11652..29fcd9d2b79 100644 --- a/app/javascript/packages/compose-components/package.json +++ b/app/javascript/packages/compose-components/package.json @@ -4,5 +4,6 @@ "version": "1.0.0", "dependencies": { "react": "^17.0.2" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/config/package.json b/app/javascript/packages/config/package.json index c9af12df0b3..3afb193f3c8 100644 --- a/app/javascript/packages/config/package.json +++ b/app/javascript/packages/config/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-config", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": false } diff --git a/app/javascript/packages/countdown/package.json b/app/javascript/packages/countdown/package.json index 1bc751a6fc5..82526ba1386 100644 --- a/app/javascript/packages/countdown/package.json +++ b/app/javascript/packages/countdown/package.json @@ -1,5 +1,9 @@ { "name": "@18f/identity-countdown", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": [ + "./countdown-alert-element.ts", + "./countdown-element.ts" + ] } diff --git a/app/javascript/packages/device/package.json b/app/javascript/packages/device/package.json index c7d769e402f..0bb8990314e 100644 --- a/app/javascript/packages/device/package.json +++ b/app/javascript/packages/device/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-device", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": false } diff --git a/app/javascript/packages/document-capture-polling/package.json b/app/javascript/packages/document-capture-polling/package.json index 22cd45db4b9..477951beab3 100644 --- a/app/javascript/packages/document-capture-polling/package.json +++ b/app/javascript/packages/document-capture-polling/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-document-capture-polling", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": false } diff --git a/app/javascript/packages/document-capture/package.json b/app/javascript/packages/document-capture/package.json index 76877772573..0c1ca180991 100644 --- a/app/javascript/packages/document-capture/package.json +++ b/app/javascript/packages/document-capture/package.json @@ -6,5 +6,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "swr": "^2.0.0" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/eslint-plugin/package.json b/app/javascript/packages/eslint-plugin/package.json index c9f3cdc5f20..1cb9c0e98b9 100644 --- a/app/javascript/packages/eslint-plugin/package.json +++ b/app/javascript/packages/eslint-plugin/package.json @@ -56,5 +56,6 @@ "eslint-plugin-react-hooks": { "optional": true } - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/form-steps/package.json b/app/javascript/packages/form-steps/package.json index 74a60e8e1b2..57dee9a85b8 100644 --- a/app/javascript/packages/form-steps/package.json +++ b/app/javascript/packages/form-steps/package.json @@ -4,5 +4,6 @@ "private": true, "peerDependencies": { "react": "*" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/i18n/package.json b/app/javascript/packages/i18n/package.json index 0b07c95f6f9..2af4ad53947 100644 --- a/app/javascript/packages/i18n/package.json +++ b/app/javascript/packages/i18n/package.json @@ -24,5 +24,6 @@ "type": "git", "url": "https://github.com/18f/identity-idp.git", "directory": "app/javascript/packages/i18n" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/manageable-authenticator/package.json b/app/javascript/packages/manageable-authenticator/package.json index 9d02f272dbe..1c50a094e50 100644 --- a/app/javascript/packages/manageable-authenticator/package.json +++ b/app/javascript/packages/manageable-authenticator/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-manageable-authenticator", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": [ + "./manageable-authenticator-element.ts" + ] } diff --git a/app/javascript/packages/masked-text-toggle/package.json b/app/javascript/packages/masked-text-toggle/package.json index 6c94e97790c..03a7125541a 100644 --- a/app/javascript/packages/masked-text-toggle/package.json +++ b/app/javascript/packages/masked-text-toggle/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-masked-text-toggle", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": false } diff --git a/app/javascript/packages/memorable-date/package.json b/app/javascript/packages/memorable-date/package.json index 9a579ea5612..8f2bf2015a5 100644 --- a/app/javascript/packages/memorable-date/package.json +++ b/app/javascript/packages/memorable-date/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-memorable-date", "private": true, - "version": "1.0.0" -} \ No newline at end of file + "version": "1.0.0", + "sideEffects": [ + "./index.ts" + ] +} diff --git a/app/javascript/packages/modal/package.json b/app/javascript/packages/modal/package.json index e8c917ab013..abef112c26b 100644 --- a/app/javascript/packages/modal/package.json +++ b/app/javascript/packages/modal/package.json @@ -4,5 +4,8 @@ "private": true, "dependencies": { "focus-trap": "^6.7.1" - } + }, + "sideEffects": [ + "./modal-element.ts" + ] } diff --git a/app/javascript/packages/normalize-yaml/package.json b/app/javascript/packages/normalize-yaml/package.json index e9f55bb3c47..c6d186d3aab 100644 --- a/app/javascript/packages/normalize-yaml/package.json +++ b/app/javascript/packages/normalize-yaml/package.json @@ -39,5 +39,6 @@ }, "peerDependencies": { "prettier": ">=3" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/one-time-code-input/package.json b/app/javascript/packages/one-time-code-input/package.json index f35d827a064..dc95624fb4a 100644 --- a/app/javascript/packages/one-time-code-input/package.json +++ b/app/javascript/packages/one-time-code-input/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-one-time-code-input", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": [ + "./one-time-code-input-element.ts" + ] } diff --git a/app/javascript/packages/password-confirmation/package.json b/app/javascript/packages/password-confirmation/package.json index 3e52bad9500..fe13b63efae 100644 --- a/app/javascript/packages/password-confirmation/package.json +++ b/app/javascript/packages/password-confirmation/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-password-confirmation", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": [ + "./password-confirmation-element.ts" + ] } diff --git a/app/javascript/packages/password-strength/package.json b/app/javascript/packages/password-strength/package.json index 0b364ce0299..8e38faea13f 100644 --- a/app/javascript/packages/password-strength/package.json +++ b/app/javascript/packages/password-strength/package.json @@ -4,5 +4,8 @@ "version": "1.0.0", "dependencies": { "zxcvbn": "^4.4.2" - } + }, + "sideEffects": [ + "./password-strength-element.ts" + ] } diff --git a/app/javascript/packages/password-toggle/package.json b/app/javascript/packages/password-toggle/package.json index a3daa28083e..555f2f92b4f 100644 --- a/app/javascript/packages/password-toggle/package.json +++ b/app/javascript/packages/password-toggle/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-password-toggle", "private": true, - "version": "1.0.0" + "version": "1.0.0", + "sideEffects": [ + "./password-toggle-element.ts" + ] } diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index 56504a875cd..0ee0ba81331 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -5,5 +5,8 @@ "dependencies": { "intl-tel-input": "^17.0.19", "libphonenumber-js": "^1.10.55" - } + }, + "sideEffects": [ + "./index.ts" + ] } diff --git a/app/javascript/packages/print-button/package.json b/app/javascript/packages/print-button/package.json index 72eebb02604..b42b3597fb9 100644 --- a/app/javascript/packages/print-button/package.json +++ b/app/javascript/packages/print-button/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-print-button", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": [ + "./print-button-element.ts" + ] } diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/package.json b/app/javascript/packages/rails-i18n-webpack-plugin/package.json index 948ecbe20af..dbb6f305731 100644 --- a/app/javascript/packages/rails-i18n-webpack-plugin/package.json +++ b/app/javascript/packages/rails-i18n-webpack-plugin/package.json @@ -10,5 +10,8 @@ "peerDependencies": { "sinon": "*", "webpack": ">=5" - } + }, + "sideEffects": [ + "./spec/**" + ] } diff --git a/app/javascript/packages/react-hooks/package.json b/app/javascript/packages/react-hooks/package.json index 0c7f4ca36a9..f014e531868 100644 --- a/app/javascript/packages/react-hooks/package.json +++ b/app/javascript/packages/react-hooks/package.json @@ -4,5 +4,6 @@ "version": "1.0.0", "peerDependencies": { "react": ">=16.8.0" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/react-i18n/package.json b/app/javascript/packages/react-i18n/package.json index 75371b668c6..5622f922eb6 100644 --- a/app/javascript/packages/react-i18n/package.json +++ b/app/javascript/packages/react-i18n/package.json @@ -4,5 +4,6 @@ "version": "1.0.0", "dependencies": { "react": "^17.0.2" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/request/package.json b/app/javascript/packages/request/package.json index 1a949d73073..dbc8dec4c97 100644 --- a/app/javascript/packages/request/package.json +++ b/app/javascript/packages/request/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-request", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": false } diff --git a/app/javascript/packages/session/package.json b/app/javascript/packages/session/package.json index 0005aa6a9b0..8790dfea903 100644 --- a/app/javascript/packages/session/package.json +++ b/app/javascript/packages/session/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-session", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": false } diff --git a/app/javascript/packages/spinner-button/package.json b/app/javascript/packages/spinner-button/package.json index 8d32bb8b494..b608938c6cc 100644 --- a/app/javascript/packages/spinner-button/package.json +++ b/app/javascript/packages/spinner-button/package.json @@ -9,5 +9,8 @@ "react": { "optional": true } - } + }, + "sideEffects": [ + "./spinner-button-element.ts" + ] } diff --git a/app/javascript/packages/step-indicator/package.json b/app/javascript/packages/step-indicator/package.json index c4462f305d6..bb2c3f36406 100644 --- a/app/javascript/packages/step-indicator/package.json +++ b/app/javascript/packages/step-indicator/package.json @@ -9,5 +9,8 @@ "react": { "optional": true } - } + }, + "sideEffects": [ + "./step-indicator-element.ts" + ] } diff --git a/app/javascript/packages/stylelint-config/package.json b/app/javascript/packages/stylelint-config/package.json index c8a634b13c4..62cefac4729 100644 --- a/app/javascript/packages/stylelint-config/package.json +++ b/app/javascript/packages/stylelint-config/package.json @@ -23,5 +23,6 @@ "peerDependencies": { "prettier": ">=3.0.0", "stylelint": ">=15.10.0" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/submit-button/package.json b/app/javascript/packages/submit-button/package.json index a8ce01b62ae..a99425a4a23 100644 --- a/app/javascript/packages/submit-button/package.json +++ b/app/javascript/packages/submit-button/package.json @@ -1,5 +1,8 @@ { "name": "@18f/identity-submit-button", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": [ + "./submit-button-element.ts" + ] } diff --git a/app/javascript/packages/test-helpers/package.json b/app/javascript/packages/test-helpers/package.json index f461143fe62..3c3784aa636 100644 --- a/app/javascript/packages/test-helpers/package.json +++ b/app/javascript/packages/test-helpers/package.json @@ -4,5 +4,6 @@ "version": "1.0.0", "peerDependencies": { "sinon": "*" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/time-element/package.json b/app/javascript/packages/time-element/package.json index 1e3a3231601..830da5124f2 100644 --- a/app/javascript/packages/time-element/package.json +++ b/app/javascript/packages/time-element/package.json @@ -2,5 +2,8 @@ "name": "@18f/identity-time-element", "private": true, "version": "1.0.0", - "type": "module" + "type": "module", + "sideEffects": [ + "./time-element.ts" + ] } diff --git a/app/javascript/packages/unpolyfill-webpack-plugin/package.json b/app/javascript/packages/unpolyfill-webpack-plugin/package.json index 919749936e2..3c6086c75f6 100644 --- a/app/javascript/packages/unpolyfill-webpack-plugin/package.json +++ b/app/javascript/packages/unpolyfill-webpack-plugin/package.json @@ -11,5 +11,6 @@ }, "peerDependencies": { "webpack": ">=5" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/url/package.json b/app/javascript/packages/url/package.json index 38deefef5de..4fbadaf76e8 100644 --- a/app/javascript/packages/url/package.json +++ b/app/javascript/packages/url/package.json @@ -1,5 +1,6 @@ { "name": "@18f/identity-url", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": false } diff --git a/app/javascript/packages/validated-field/package.json b/app/javascript/packages/validated-field/package.json index b7f3f634e5d..56888c3cc41 100644 --- a/app/javascript/packages/validated-field/package.json +++ b/app/javascript/packages/validated-field/package.json @@ -9,5 +9,8 @@ "react": { "optional": true } - } + }, + "sideEffects": [ + "./validated-field-element.ts" + ] } diff --git a/app/javascript/packages/verify-flow/package.json b/app/javascript/packages/verify-flow/package.json index f059daef1dc..2692e3a10ed 100644 --- a/app/javascript/packages/verify-flow/package.json +++ b/app/javascript/packages/verify-flow/package.json @@ -4,5 +4,6 @@ "private": true, "dependencies": { "react": "^17.0.2" - } + }, + "sideEffects": false } diff --git a/app/javascript/packages/webauthn/package.json b/app/javascript/packages/webauthn/package.json index 329b14b4603..ca713cade83 100644 --- a/app/javascript/packages/webauthn/package.json +++ b/app/javascript/packages/webauthn/package.json @@ -1,5 +1,9 @@ { "name": "@18f/identity-webauthn", "version": "1.0.0", - "private": true + "private": true, + "sideEffects": [ + "./webauthn-input-element.ts", + "./webauthn-verify-button-element.ts" + ] } diff --git a/docs/frontend.md b/docs/frontend.md index a1fc13ebe57..29d6b16630a 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -79,6 +79,7 @@ In practice: package is intended to be published to NPM. - ...a value for the `version` field, since it is required. The value value can be anything, and `"1.0.0"` is a good default. + - ...a `sideEffects` value listing files containing any side effects, used for [Webpack's Tree Shaking optimization](https://webpack.js.org/guides/tree-shaking/). - The package should be importable by its bare name, either with an `index.ts` or equivalent [package entrypoints](https://nodejs.org/api/packages.html#package-entry-points) diff --git a/scripts/validate-workspaces.js b/scripts/validate-workspaces.mjs similarity index 70% rename from scripts/validate-workspaces.js rename to scripts/validate-workspaces.mjs index a9a75bc42d1..7774f39efa3 100755 --- a/scripts/validate-workspaces.js +++ b/scripts/validate-workspaces.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node -const { readFile, stat } = require('fs/promises'); -const { dirname, basename, join } = require('path'); -const { sync: glob } = require('fast-glob'); +import { readFile, stat } from 'node:fs/promises'; +import { dirname, basename, join, resolve, relative } from 'node:path'; +import glob from 'fast-glob'; /** @typedef {[path: string, manifest: Record]} ManifestPair */ /** @typedef {ManifestPair[]} ManifestPairs */ @@ -43,9 +43,16 @@ function checkHaveCommonDependencyVersions(manifests) { */ function checkHaveRequiredFields(manifests) { for (const [path, manifest] of manifests) { - ['name', 'version', 'private'].forEach((field) => { + Object.entries({ + name: `Expected "@18f/identity-${basename(dirname(path))}".`, + version: '"1.0.0" is recommended for private packages.', + private: '`true` is recommended unless the package is intended to be published to NPM.', + sideEffects: + 'This is usually `false`, unless there are files which include side effects, ' + + 'such as custom elements registering to the custom element registry.', + }).forEach(([field, guidance]) => { if (!(field in manifest)) { - throw new Error(`Missing required field ${field} in ${path}`); + throw new Error([`Missing required field ${field} in ${path}`, guidance].join(' ')); } }); } @@ -115,6 +122,31 @@ async function checkHaveDocumentation(manifests) { ); } +/** + * @param {ManifestPairs} manifests + */ +function checkPackageSideEffectsIncludesCustomElements(manifests) { + return Promise.all( + manifests.map(async ([manifestPath, manifest]) => { + const manifestDirectory = dirname(manifestPath); + const customElementPaths = await glob(join(manifestDirectory, '*-element.ts')); + const expectedPaths = customElementPaths.map((path) => resolve(path)); + const actualPaths = Array.from(manifest.sideEffects).map((path) => + resolve(join(manifestDirectory, path)), + ); + const hasExpected = expectedPaths.every((path) => actualPaths.includes(path)); + if (!hasExpected) { + throw new Error( + [ + `Missing expected custom elements in ${manifestPath} sideEffects:`, + customElementPaths.map((path) => `./${relative(manifestDirectory, path)}`).join(', '), + ].join(' '), + ); + } + }), + ); +} + /** * @type {Record void>} */ @@ -126,6 +158,7 @@ const CHECKS = { checkHaveCorrectVersion, checkHaveNoSiblingDependencies, checkHaveDocumentation, + checkPackageSideEffectsIncludesCustomElements, }; /** @@ -137,7 +170,7 @@ const EXCEPTIONS = { checkHaveCorrectPackageName: ['app/javascript/packages/eslint-plugin/package.json'], }; -const manifestPaths = glob('app/javascript/packages/*/package.json'); +const manifestPaths = await glob('app/javascript/packages/*/package.json'); Promise.all(manifestPaths.map(async (path) => [path, await readFile(path, 'utf-8')])) .then((contents) => contents.map(([path, content]) => /** @type {ManifestPair} */ ([path, JSON.parse(content)])),