diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index bf1be02f0..4c09eddbb 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,4 +1,4 @@
-
# 📝 Why & how
@@ -14,4 +14,4 @@ Please follow the template so that the reviewers can easily understand what the
- [ ] Documented in this PR how to use the feature or replicate the bug.
-- [ ] Documented in this PR how you fixed or created the feature.
+- [ ] Documented in this PR how you fixed an issue or created the feature.
diff --git a/bun.lock b/bun.lock
index ea7e3521e..0d28b37b5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,10 +10,9 @@
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.2",
"@sentry/react": "^10.12.0",
- "@vercel/blob": "^0.27.3",
+ "es-toolkit": "^1.39.10",
"expo": "54.0.9",
"expo-font": "^14.0.8",
- "lodash": "^4.17.21",
"next": "^15.5.3",
"node-emoji": "^2.2.0",
"react": "19.1.1",
@@ -30,8 +29,8 @@
"@expo/next-adapter": "^6.0.0",
"@next/bundle-analyzer": "^15.5.3",
"@types/bun": "^1.2.22",
- "@types/lodash": "^4.17.20",
"@types/react": "^19.1.13",
+ "@vercel/blob": "^0.27.3",
"ajv-cli": "^5.0.0",
"browserslist": "^4.26.2",
"cheerio": "^1.1.2",
@@ -587,8 +586,6 @@
"@types/linkifyjs": ["@types/linkifyjs@2.1.7", "", { "dependencies": { "@types/react": "*" } }, "sha512-+SIYXs1lajyD7t/2+V9GLfdFlc/6Nr2tr65kjA2F5oOzBlPH+NiPqySJDHzREoGcL91Au9Qef8M5JdZiRXsaJw=="],
- "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
-
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
@@ -1019,6 +1016,8 @@
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
+ "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
@@ -1445,8 +1444,6 @@
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
- "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
@@ -1963,7 +1960,7 @@
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
- "undici": ["undici@7.15.0", "", {}, "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ=="],
+ "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
@@ -2167,14 +2164,14 @@
"@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
- "@vercel/blob/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
-
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="],
+ "cheerio/undici": ["undici@7.15.0", "", {}, "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ=="],
+
"chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
diff --git a/components/Library/MetaData.tsx b/components/Library/MetaData.tsx
index 814f2d5b6..be36e321e 100644
--- a/components/Library/MetaData.tsx
+++ b/components/Library/MetaData.tsx
@@ -211,18 +211,20 @@ export function MetaData({ library, secondary }: Props) {
const data = generateSecondaryData(library, isDark).filter(Boolean);
return (
<>
- {data.map(({ id, icon, content }, i) => (
-
- {icon}
- {content}
-
- ))}
+ {data
+ .filter(entry => !!entry)
+ .map(({ id, icon, content }, i) => (
+
+ {icon}
+ {content}
+
+ ))}
>
);
} else {
diff --git a/next.config.js b/next.config.ts
similarity index 63%
rename from next.config.js
rename to next.config.ts
index 47643159b..1e8772081 100644
--- a/next.config.js
+++ b/next.config.ts
@@ -1,9 +1,21 @@
import { withExpo } from '@expo/next-adapter';
import BundleAnalyzer from '@next/bundle-analyzer';
+import type { NextConfig } from 'next';
import withPlugins from 'next-compose-plugins';
import withFonts from 'next-fonts';
import withImages from 'next-images';
+const PACKAGES_TO_OPTIMIZE = [
+ '@expo/html-elements',
+ '@react-native-picker/picker',
+ '@sentry/*',
+ 'node-emoji',
+ 'react-native',
+ 'react-native-safe-area-context',
+ 'react-native-svg',
+ 'react-native-web',
+];
+
const withBundleAnalyzer = BundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
@@ -12,40 +24,21 @@ export default withPlugins([withExpo, withImages, withFonts, withBundleAnalyzer]
productionBrowserSourceMaps: true,
reactStrictMode: true,
poweredByHeader: false,
- devIndicators: {
- enabled: false,
- },
+ devIndicators: false,
eslint: {
ignoreDuringBuilds: true,
},
images: {
disableStaticImages: true,
},
- transpilePackages: [
- '@expo/html-elements',
- '@react-native-picker/picker',
- '@sentry/react',
- 'react-native-safe-area-context',
- 'react-native-svg',
- 'react-native-web',
- 'react-native-web-hooks',
- 'react-native',
- ],
+ transpilePackages: PACKAGES_TO_OPTIMIZE,
experimental: {
forceSwcTransforms: true,
webpackBuildWorker: true,
browserDebugInfoInTerminal: true,
clientSegmentCache: true,
- optimizePackageImports: [
- '@expo/html-elements',
- '@react-native-picker/picker',
- '@sentry/react',
- 'react-native-safe-area-context',
- 'react-native-svg',
- 'react-native-web',
- 'react-native-web-hooks',
- 'react-native',
- ],
+ useLightningcss: true,
+ optimizePackageImports: PACKAGES_TO_OPTIMIZE,
},
async headers() {
return [
@@ -58,4 +51,4 @@ export default withPlugins([withExpo, withImages, withFonts, withBundleAnalyzer]
},
];
},
-});
+} satisfies NextConfig);
diff --git a/package.json b/package.json
index 422955e66..b75ad8222 100644
--- a/package.json
+++ b/package.json
@@ -24,10 +24,9 @@
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.2",
"@sentry/react": "^10.12.0",
- "@vercel/blob": "^0.27.3",
"expo": "54.0.9",
"expo-font": "^14.0.8",
- "lodash": "^4.17.21",
+ "es-toolkit": "^1.39.10",
"next": "^15.5.3",
"node-emoji": "^2.2.0",
"react": "19.1.1",
@@ -44,8 +43,8 @@
"@expo/next-adapter": "^6.0.0",
"@next/bundle-analyzer": "^15.5.3",
"@types/bun": "^1.2.22",
- "@types/lodash": "^4.17.20",
"@types/react": "^19.1.13",
+ "@vercel/blob": "^0.27.3",
"ajv-cli": "^5.0.0",
"browserslist": "^4.26.2",
"cheerio": "^1.1.2",
diff --git a/pages/api/libraries/check.ts b/pages/api/libraries/check.ts
index 74884147b..855ec1973 100644
--- a/pages/api/libraries/check.ts
+++ b/pages/api/libraries/check.ts
@@ -2,10 +2,19 @@ import { type NextApiRequest, type NextApiResponse } from 'next';
import data from '~/assets/data.json';
import { type DataAssetType } from '~/types';
-import { getNewArchSupportStatus } from '~/util/newArchStatus';
+import { getNewArchSupportStatus, NewArchSupportStatus } from '~/util/newArchStatus';
+
+type CheckResultsType = Record<
+ string,
+ {
+ unmaintained?: boolean;
+ newArchitecture: NewArchSupportStatus;
+ }
+>;
// Copy data into an object that is keyed by npm package name for faster lookup
-const dataByNpmPackage = {};
+const dataByNpmPackage: CheckResultsType = {};
+
(data as DataAssetType).libraries.forEach(library => {
dataByNpmPackage[library.npmPkg] = {
unmaintained: library.unmaintained,
@@ -30,7 +39,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
}
res.statusCode = 200;
- const result = {};
+ const result: CheckResultsType = {};
packages.forEach(pkgName => {
result[pkgName] = dataByNpmPackage[pkgName];
});
diff --git a/pages/api/libraries/index.ts b/pages/api/libraries/index.ts
index 255c8889e..f983311f2 100644
--- a/pages/api/libraries/index.ts
+++ b/pages/api/libraries/index.ts
@@ -1,15 +1,14 @@
-import drop from 'lodash/drop';
-import take from 'lodash/take';
+import { drop, take } from 'es-toolkit';
import { NextApiRequest, NextApiResponse } from 'next';
import data from '~/assets/data.json';
-import { type LibraryType, QueryOrder } from '~/types';
+import { type DataAssetType, QueryOrder } from '~/types';
import { NUM_PER_PAGE } from '~/util/Constants';
import { parseQueryParams } from '~/util/parseQueryParams';
import { handleFilterLibraries } from '~/util/search';
import * as Sorting from '~/util/sorting';
-const originalData = [...data.libraries] as LibraryType[];
+const originalData = [...(data as DataAssetType).libraries];
const getData = () => ({
updated: Sorting.updated([...originalData]),
added: [...originalData.reverse()],
diff --git a/scripts/build-and-score-data.ts b/scripts/build-and-score-data.ts
index 36436d3d6..28beb667e 100644
--- a/scripts/build-and-score-data.ts
+++ b/scripts/build-and-score-data.ts
@@ -1,6 +1,6 @@
import { BlobAccessError, list, put } from '@vercel/blob';
import { fetch } from 'bun';
-import chunk from 'lodash/chunk';
+import { chunk } from 'es-toolkit';
import fs from 'node:fs';
import path from 'node:path';
diff --git a/scripts/calculate-score.ts b/scripts/calculate-score.ts
index 17417f922..052c3fe53 100644
--- a/scripts/calculate-score.ts
+++ b/scripts/calculate-score.ts
@@ -95,10 +95,9 @@ export function calculateDirectoryScore(data: LibraryType) {
};
}
-function getCombinedPopularity(data: LibraryType) {
- const { subscribers, forks, stars } = data.github.stats;
- const { downloads } = data.npm;
- return subscribers * 50 + forks * 25 + stars * 10 + downloads / 100;
+function getCombinedPopularity({ github, npm }: LibraryType) {
+ const { subscribers, forks, stars } = github.stats;
+ return subscribers * 50 + forks * 25 + stars * 10 + (npm?.downloads ?? 0) / 100;
}
function getUpdatedDaysAgo(data: LibraryType) {
diff --git a/scripts/cleanup-libraries-json.ts b/scripts/cleanup-libraries-json.ts
index 509678f11..73426d56c 100644
--- a/scripts/cleanup-libraries-json.ts
+++ b/scripts/cleanup-libraries-json.ts
@@ -1,6 +1,4 @@
-import identity from 'lodash/identity';
-import omit from 'lodash/omit';
-import pickBy from 'lodash/pickBy';
+import { identity, omit, pickBy } from 'es-toolkit';
import fs from 'node:fs';
import path from 'node:path';
@@ -11,15 +9,15 @@ const LIBRARIES_JSON_PATH = path.join('react-native-libraries.json');
const emptyPropertiesToKeep = ['newArchitecture'];
-function removeEmptyArray(lib: LibraryDataEntryType, key: string) {
- return lib[key] && !lib[key].length ? (omit(lib, key) as LibraryDataEntryType) : lib;
+function removeEmptyArray(lib: LibraryDataEntryType, key: 'examples' | 'images') {
+ return lib[key] && !lib[key].length ? omit(lib, [key]) : lib;
}
-const processedLibraries = (libraries as LibraryDataEntryType[])
+const processedLibraries = libraries
// Remove redundant `npmPkg` for libraries with the correct GitHub repository name
- .map(lib =>
+ .map((lib: LibraryDataEntryType) =>
lib.npmPkg && !lib.npmPkg.includes('/') && lib.githubUrl.endsWith(`/${lib.npmPkg}`)
- ? omit(lib, 'npmPkg')
+ ? omit(lib, ['npmPkg'])
: lib
)
// Remove empty arrays
@@ -31,7 +29,7 @@ const processedLibraries = (libraries as LibraryDataEntryType[])
if (emptyPropertiesToKeep.includes(key)) {
return true;
} else {
- return identity(value);
+ return !!identity(value);
}
})
);
diff --git a/scripts/helpers.ts b/scripts/helpers.ts
index 2a6645a3b..e215c13e2 100644
--- a/scripts/helpers.ts
+++ b/scripts/helpers.ts
@@ -58,7 +58,7 @@ export function processTopics(topics?: string[]) {
}
function splitAndGetLastChunk(value: string, delimiter = '/') {
- return value.split(delimiter).at(-1).toLowerCase();
+ return value.split(delimiter).at(-1)?.toLowerCase();
}
export function hasMismatchedPackageData(library: LibraryType) {
@@ -66,7 +66,7 @@ export function hasMismatchedPackageData(library: LibraryType) {
if (library.github.registry && library.github.registry !== 'https://registry.npmjs.org/') {
const registryScope = splitAndGetLastChunk(library.github.registry);
- if (registryScope.startsWith('@')) {
+ if (registryScope?.startsWith('@')) {
return library.github?.name.replace(`${registryScope}/`, '') !== desiredName;
}
}
diff --git a/scripts/validate-new-entries.ts b/scripts/validate-new-entries.ts
index 64e73b51b..cfbd30e0c 100644
--- a/scripts/validate-new-entries.ts
+++ b/scripts/validate-new-entries.ts
@@ -1,6 +1,5 @@
import { fetch } from 'bun';
-import differenceWith from 'lodash/differenceWith';
-import isEqual from 'lodash/isEqual';
+import { differenceWith, isEqual } from 'es-toolkit';
import { fetchGithubData } from './fetch-github-data';
import { fetchNpmDownloadData } from './fetch-npm-download-data';
diff --git a/types/app.d.ts b/types/app.d.ts
new file mode 100644
index 000000000..352d73fda
--- /dev/null
+++ b/types/app.d.ts
@@ -0,0 +1,3 @@
+declare module 'next-compose-plugins';
+declare module 'next-fonts';
+declare module 'next-images';
diff --git a/types/index.ts b/types/index.ts
index 1a87454f3..ed77171e1 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -123,7 +123,7 @@ export type LibraryDataEntryType = {
fireos?: boolean;
tvos?: boolean;
visionos?: boolean;
- unmaintained?: boolean | string;
+ unmaintained?: boolean;
dev?: boolean;
template?: boolean;
newArchitecture?: boolean | 'new-arch-only';