-
-
Notifications
You must be signed in to change notification settings - Fork 53
Compare the sizes of multiple packages and analyze package.json #441
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
Changes from all commits
ec95032
b5da52a
6965ce8
3a20c4f
4146fc8
dd051be
20fae9c
285d957
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { parsePackageString } from '../util/npm-parser'; | ||
| import { findOne, insert } from '../util/backend/db'; | ||
| import { | ||
| fetchManifest, | ||
| getAllDistTags, | ||
| getAllVersions, | ||
| } from '../util/npm-api'; | ||
| import { calculatePackageSize } from '../util/backend/npm-stats'; | ||
| import { versionUnknown } from '../util/constants'; | ||
|
|
||
| async function getSize(name: string, version: string | null, force: boolean, tmpDir: string) { | ||
| let manifest: NpmManifest; | ||
| let cacheResult = true; | ||
| let isLatest = false; | ||
|
|
||
| try { | ||
| manifest = await fetchManifest(name); | ||
| } catch (e) { | ||
| console.error(`Package ${name} does not exist in npm`); | ||
| return packageNotFound(name); | ||
| } | ||
|
|
||
| const tagToVersion = getAllDistTags(manifest); | ||
| if (!version) { | ||
| version = tagToVersion['latest']; | ||
| isLatest = true; | ||
| cacheResult = false; | ||
| } else if (typeof tagToVersion[version] !== 'undefined') { | ||
| version = tagToVersion[version]; | ||
| cacheResult = false; | ||
| } | ||
|
|
||
| const allVersions = getAllVersions(manifest); | ||
| if (!allVersions.includes(version)) { | ||
| console.error(`Version ${name}@${version} does not exist in npm`); | ||
| return packageNotFound(name); | ||
| } | ||
|
|
||
| let pkgSize = await findOne(name, version); | ||
| if (!pkgSize || force) { | ||
| console.log(`Cache miss for ${name}@${version} - running npm install in ${tmpDir}...`); | ||
| const start = new Date(); | ||
| pkgSize = await calculatePackageSize(name, version, tmpDir); | ||
| const end = new Date(); | ||
| const sec = (end.getTime() - start.getTime()) / 1000; | ||
| console.log(`Calculated size of ${name}@${version} in ${sec}s`); | ||
| insert(pkgSize); | ||
| } | ||
|
|
||
|
|
||
| const result: ComparePackage = { | ||
| pkgSize, | ||
| cacheResult, | ||
| isLatest, | ||
| }; | ||
| return result; | ||
| } | ||
|
|
||
| export async function getCompareProps(query: ParsedUrlQuery, tmpDir: string) { | ||
| if (!query || typeof query.p !== 'string') { | ||
| throw new Error(`Unknown query string ${query}`); | ||
| } | ||
| const packages = query.p.split(',').map(parsePackageString); | ||
| const force = query.force === '1'; | ||
|
|
||
| const resultPromises = packages.map(async ({ name, version }) => await getSize(name, version, force, tmpDir)); | ||
| const results = await Promise.all(resultPromises); | ||
|
|
||
| return { results }; | ||
| } | ||
|
|
||
| function packageNotFound(name: string) { | ||
| const pkgSize: PkgSize = { | ||
| name, | ||
| version: versionUnknown, | ||
| publishSize: 0, | ||
| installSize: 0, | ||
| disabled: true, | ||
| }; | ||
| const result: ResultProps = { | ||
| pkgSize, | ||
| readings: [], | ||
| cacheResult: false, | ||
| isLatest: false, | ||
| }; | ||
| return result; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import React from 'react'; | ||
| import { versionUnknown } from '../util/constants'; | ||
| import { getReadableFileSize } from '../util/npm-parser'; | ||
| import PageContainer from '../components/PageContainer'; | ||
| import Footer from '../components/Footer'; | ||
| import { getBadgeUrl } from '../util/badge'; | ||
| import { Stat } from '../components/Stats'; | ||
| import SearchBar from '../components/SearchBar'; | ||
|
|
||
| export default class ComparePage extends React.Component<CompareProps, {}> { | ||
| render() { | ||
| const { results } = this.props; | ||
|
|
||
| const resultsToPrint = results.map(({ pkgSize, isLatest }) => { | ||
| const { name, version, installSize, publishSize } = pkgSize; | ||
| const exists = version !== versionUnknown; | ||
| const install = getReadableFileSize(installSize); | ||
| const publish = getReadableFileSize(publishSize); | ||
| const pkgNameAndVersion = isLatest ? name : `${name}@${version}`; | ||
| const badgeUrl = getBadgeUrl(pkgSize, isLatest); | ||
| return { | ||
| exists, | ||
| install, | ||
| publish, | ||
| installSize, | ||
| pkgNameAndVersion, | ||
| badgeUrl, | ||
| } | ||
| }); | ||
|
|
||
| return ( | ||
| <> | ||
| <PageContainer> | ||
| <SearchBar autoFocus={false} defaultValue={resultsToPrint.map(result => result.pkgNameAndVersion).join(',')} /> | ||
| <div style={{ maxWidth: '100%', 'overflow': 'scroll' }}> | ||
| <table style={{ marginTop: '60px' }}> | ||
| <tbody> | ||
| {resultsToPrint.sort((a, b) => b.installSize - a.installSize).filter(result => result.exists).map((result, i) => ( | ||
| <tr key={i}> | ||
| <td style={{ fontSize: '1.2em', paddingRight: '2em' }}>{result.pkgNameAndVersion}</td> | ||
| <td style={{ padding: '0 2em', textAlign: 'center' }}><Stat {...result.install} label="Install" scale={0.75} /></td> | ||
| <td style={{ padding: '0 2em', textAlign: 'center' }}><Stat {...result.publish} label="Publish" scale={0.75} /></td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </PageContainer> | ||
| <Footer /> | ||
| </> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import React from 'react'; | ||
| import Image from '../components/Image'; | ||
| import Footer from '../components/Footer'; | ||
| import { pages } from '../util/constants'; | ||
|
|
||
| const style: React.CSSProperties = { | ||
| height: '100vh', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can use
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, now that I think about it. We don't need a separate page for parse failures. We need to modify the 500 error page to print a user friendly error. Perhaps create something like |
||
|
|
||
| export default () => ( | ||
| <> | ||
| <div style={style}> | ||
| <h1>Failed to parse package.json</h1> | ||
| <p>Oops, the package.json you uploaded could not be parsed. Be sure you uploaded the right file.</p> | ||
| <p> | ||
|
styfle marked this conversation as resolved.
|
||
| <a href={pages.index}>Go Home</a> | ||
| </p> | ||
| <Image width={370} height={299} file="tumblebeasts/tbstand1.png" /> | ||
| </div> | ||
| <Footer /> | ||
| </> | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { pages, versionUnknown } from './util/constants'; | |
| import { getResultProps } from './page-props/results'; | ||
| import { getBadgeUrl, getApiResponseSize } from './util/badge'; | ||
| import { parsePackageString } from './util/npm-parser'; | ||
| import semver from 'semver'; | ||
|
|
||
| const { TMPDIR = '/tmp', GA_ID = '', NODE_ENV } = process.env; | ||
| process.env.HOME = TMPDIR; | ||
|
|
@@ -44,6 +45,30 @@ export async function handler(req: IncomingMessage, res: ServerResponse) { | |
| res.setHeader('Content-Type', mimeType(pathname)); | ||
| res.setHeader('Cache-Control', cacheControl(isProd, cacheResult ? 7 : 0)); | ||
| res.end(JSON.stringify(result)); | ||
| } else if (pathname === pages.compare) { | ||
| let data: any[] = []; | ||
| req.on('data', chunk => data.push(chunk)); | ||
| req.on('end', () => { | ||
| const [packageString] = data.toString().match(/{[\s\S]+}/) || []; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of this regex? Can you instead use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the data comes through as |
||
| if (!packageString) { | ||
| return renderPage(res, pages.parseFailure, query, TMPDIR, GA_ID); | ||
| } | ||
| let packageData: PackageJson; | ||
| try { | ||
| packageData = JSON.parse(packageString); | ||
| } catch (e) { | ||
| return renderPage(res, pages.parseFailure, query, TMPDIR, GA_ID); | ||
| } | ||
| if (!packageData.dependencies) { | ||
| return renderPage(res, pages.parseFailure, query, TMPDIR, GA_ID); | ||
| } | ||
| const queryString = Object.entries(packageData.dependencies).map(([pkg, version]) => { | ||
| const versionPart = version === '*' ? 'latest' : semver.coerce(String(version)); | ||
| return `${pkg}@${versionPart}`; | ||
| }); | ||
| res.writeHead(302, { Location: `/result?p=${queryString}` }); | ||
| return res.end(); | ||
| }); | ||
| } else { | ||
| const isIndex = pathname === pages.index; | ||
| const hasVersion = typeof query.p === 'string' && parsePackageString(query.p).version !== null; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.