Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scraping expo/react-native version from package.json #838

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 39 additions & 0 deletions debug-github-repos.json
Expand Up @@ -20,5 +20,44 @@
"npmPkg": "@react-native-community/datetimepicker",
"expo": true,
"newArchitecture": true
},
{ "githubUrl": "https://github.com/stripe/stripe-react-native",
"npmPkg": "@stripe/stripe-react-native",
"ios": true,
"android": true,
"expo": true
},
{
"githubUrl": "https://github.com/Purii/react-native-tableview-simple",
"npmPkg": "react-native-tableview-simple",
"ios": true,
"android": true,
"expo": true
},
{
"githubUrl": "https://github.com/vonovak/react-navigation-header-buttons",
"npmPkg": "react-navigation-header-buttons",
"ios": true,
"android": true,
"expo": true
},
{
"githubUrl": "https://github.com/gabrielmoncea/react-native-template",
"npmPkg": "@gabrielmoncea/react-native-template",
"ios": true,
"android": true,
"template": true
},
{
"githubUrl": "https://github.com/BabylonJS/BabylonReactNative/tree/master/Modules/@babylonjs/react-native",
"npmPkg": "@babylonjs/react-native",
"examples": [
"https://github.com/BabylonJS/BabylonReactNative/tree/master/Apps",
"https://github.com/BabylonJS/BabylonReactNativeSample",
"https://github.com/runtothedoor/rotating-cube-demo-babylon-rxn"
],
"ios": true,
"android": true,
"windows": true
}
]
48 changes: 48 additions & 0 deletions expo-react-native.json
@@ -0,0 +1,48 @@
{
"expoSdkVersions": [
"46.0.0",
"45.0.0",
"44.0.0",
"43.0.0"
],
"reactNativeVersions": [
"0.70.1",
"0.70.0",
"0.69.5",
"0.69.4",
"0.69.3",
"0.69.2",
"0.69.1",
"0.69.0",
"0.68.4",
"0.68.3",
"0.68.2",
"0.68.1",
"0.68.0",
"0.67.4",
"0.67.3",
"0.67.2",
"0.67.1",
"0.67.0",
"0.66.4",
"0.66.3",
"0.64.3",
"0.66.2",
"0.66.1",
"0.66.0",
"0.65.1",
"0.65.0",
"0.64.2",
"0.64.1",
"0.62.3",
"0.64.0",
"0.63.4",
"0.63.3",
"0.63.2",
"0.63.1",
"0.63.0",
"0.62.2",
"0.62.1",
"0.62.0"
]
}
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -33,6 +33,7 @@
"lodash": "^4.17.21",
"next": "12.2.0",
"node-emoji": "^1.11.0",
"npm-registry-fetch": "^13.1.1",
"react": "18.2.0",
"react-content-loader": "^6.2.0",
"react-dom": "18.2.0",
Expand All @@ -42,6 +43,7 @@
"react-native-svg": "^12.4.4",
"react-native-web": "^0.18.7",
"react-native-web-hooks": "^3.0.2",
"semver": "^7.3.7",
"use-debounce": "^8.0.4"
},
"devDependencies": {
Expand Down
108 changes: 108 additions & 0 deletions pages/api/versions/index.ts
@@ -0,0 +1,108 @@
import { NextApiRequest, NextApiResponse } from 'next';

import data from '../../../assets/data.json';

type RequestQuery = {
reactNativeVersion: string;
expoSdkVersion: string;
packages: string[];
};

type PackageInfo = {
npmPackage: string;
sdkVersion?: string;
versionRange: string | null;
};
type ExpoVersionsData = {
sdkVersions: {
[keyof: string]: {
facebookReactNativeVersion: string;
};
};
};

const EXPO_API_V2 = 'https://exp.host';

async function fetchExpoVersionsAsync(): Promise<{ data: ExpoVersionsData }> {
const url = new URL('/--/api/v2/versions/latest', EXPO_API_V2);
const req = await fetch(url);
return req.json();
}

async function fetchNativeModulesAsync(sdkVersion: string): Promise<{ data: PackageInfo[] }> {
const url = new URL(`/--/api/v2/sdks/${sdkVersion}/native-modules`, EXPO_API_V2);
const req = await fetch(url);
return req.json();
}

function lookupPackageForReactNative(rnVersion: string, packages: string[]) {
return data.libraries
.filter(lib => packages.includes(lib.npmPkg) && lib.rnVersions?.[rnVersion])
.map(lib => ({
npmPackage: lib.npmPkg,
versionRange: lib.rnVersions[rnVersion],
}));
}

async function processRequestAsync(data: RequestQuery): Promise<PackageInfo[]> {
const { expoSdkVersion, reactNativeVersion, packages } = data;
if (expoSdkVersion) {
const { data } = await fetchNativeModulesAsync(expoSdkVersion);
const resolvedPackage = new Set();
const result = data.filter(({ npmPackage }) => {
if (packages.includes(npmPackage)) {
resolvedPackage.add(npmPackage);
return true;
}
return false;
});

const missingPackages = packages.filter(pkg => !resolvedPackage.has(pkg));
if (missingPackages.length === 0) {
// All packages are resolved by Expo Native Modules.
return result;
}

// Continue looking for missing package based on RN version.
let rnVersion = reactNativeVersion;
if (!rnVersion) {
const { data: versionsData } = await fetchExpoVersionsAsync();
if (!versionsData.sdkVersions[expoSdkVersion]) {
throw new Error(`invalid expoSdkVersion: ${expoSdkVersion}`);
}

({ facebookReactNativeVersion: rnVersion } = versionsData.sdkVersions[expoSdkVersion]);
}
return result.concat(lookupPackageForReactNative(rnVersion, missingPackages));
} else if (reactNativeVersion) {
return lookupPackageForReactNative(reactNativeVersion, packages);
}

return [];
}

export const config = {
api: {},
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const body = req.query as RequestQuery;
try {
const packages = await processRequestAsync(body);

res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.json({
...body,
packages: packages.map(({ npmPackage, sdkVersion, versionRange }) => ({
npmPackage,
sdkVersion,
versionRange,
})),
});
} catch {
res.statusCode = 500;
giautm marked this conversation as resolved.
Show resolved Hide resolved
res.setHeader('Content-Type', 'application/json');
res.json({ error: 'Internal server error' });
}
}
23 changes: 23 additions & 0 deletions scripts/build-and-score-data.js
Expand Up @@ -4,12 +4,14 @@ import chunk from 'lodash/chunk';
import path from 'path';

import debugGithubRepos from '../debug-github-repos.json';
import dataVersions from '../expo-react-native.json';
import githubRepos from '../react-native-libraries.json';
import * as Strings from '../util/strings';
import { calculateDirectoryScore, calculatePopularityScore } from './calculate-score';
import { fetchGithubData, fetchGithubRateLimit, loadGitHubLicenses } from './fetch-github-data';
import { fetchNpmData, fetchNpmDataBulk } from './fetch-npm-data';
import fetchReadmeImages from './fetch-readme-images';
import { fetchVersionsData } from './fetch-versions-data';

// Uses debug-github-repos.json instead, so we have less repositories to crunch
// each time we run the script
Expand Down Expand Up @@ -107,6 +109,27 @@ const buildAndScoreData = async () => {
}
);

console.log('\n** Fetch NPM Package JSON');
data = await Promise.all(
data.map(async project => {
if (project.npm.downloads === undefined) {
console.log(`Skipping ${project.npmPkg} because it doesn't exist on NPM`);
return project;
}

const versionsData = await fetchVersionsData(project.npmPkg, [
{
name: 'react-native',
versions: dataVersions.reactNativeVersions,
},
]);
return {
...project,
rnVersions: versionsData?.[0]?.supports ?? {},
};
})
);

console.log('\n** Calculating Directory Score');
data = data.map(project => {
try {
Expand Down
70 changes: 70 additions & 0 deletions scripts/fetch-versions-data.js
@@ -0,0 +1,70 @@
import npmFetch from 'npm-registry-fetch';
import path from 'path';
import semver from 'semver';

const DEPS_IN_ORDER = ['devDependencies', 'peerDependencies', 'dependencies'];
const REGEXP_VERSION = /^\d+\.\d+\.\d+$/;

function isStableVersion(ver) {
return REGEXP_VERSION.exec(ver) !== null;
}

function fillSupportVersions(packageManifest, dependencies) {
if (!packageManifest) {
return () => false;
}

const versionOfDeps = dependencies.reduce((acc, dep) => {
DEPS_IN_ORDER.forEach(depType => {
const version = packageManifest[depType]?.[dep];
if (version) {
acc[dep] = version;
}
});
return acc;
}, {});

return depData => {
const requiredVersion = versionOfDeps?.[depData.name];
if (requiredVersion && depData.unsupported.length > 0) {
depData.unsupported = depData.unsupported.filter(version => {
if (semver.satisfies(version, requiredVersion)) {
if (!depData.supports) {
depData.supports = {};
}
depData.supports[version] = packageManifest.version;
return false;
}

return true;
});
}

return depData.unsupported.length === 0;
};
}

export const fetchVersionsData = async (npmPackage, packages) => {
const deps = packages.map(({ name }) => name);
const data = packages.map(({ name, versions: unsupported }) => ({
name,
unsupported,
}));

const manifest = await npmFetch.json('/' + path.join(npmPackage, 'latest'));
if (!data.every(fillSupportVersions(manifest, deps))) {
// Fetch manifest for all versions processing.
const metadata = await npmFetch.json(npmPackage);

const versions = Object.keys(metadata.versions).filter(isStableVersion).reverse();
for (const version of versions) {
if (data.every(fillSupportVersions(metadata.versions[version], deps))) {
break;
}
}
}

return data
.filter(({ supports }) => !!supports)
.map(({ name, supports }) => ({ name, supports }));
};