Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/components/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,26 @@ export default function Stats(props: Props) {
);
}

type StatProp = SizeWithUnit & { label: string };
type StatProp = SizeWithUnit & { label: string, scale?: number };

export function Stat(props: StatProp) {
const scale = props.scale || 1;

function Stat(props: StatProp) {
const styleValue: React.CSSProperties = {
fontSize: '3rem',
fontSize: `${3 * scale}rem`,
fontWeight: 'bold',
color: '#212121',
};

const styleUnit: React.CSSProperties = {
fontSize: '2.4rem',
fontSize: `${2.4 * scale}rem`,
color: '#666E78',
fontWeight: 'bold',
marginLeft: '4px',
};

const styleLabel: React.CSSProperties = {
fontSize: '1rem',
fontSize: `${1 * scale}rem`,
color: '#666E78',
textTransform: 'uppercase',
letterSpacing: '2px',
Expand Down
16 changes: 16 additions & 0 deletions src/interfaces/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ interface ResultProps {
isLatest: boolean;
}

interface ComparePackage {
pkgSize: PkgSize;
cacheResult: boolean;
isLatest: boolean;
}

interface CompareProps {
results: ComparePackage[];
}

interface ParsedUrlQuery {
[key: string]: string | string[] | undefined;
}
Expand All @@ -52,3 +62,9 @@ interface NpmManifest {
time: { [version: string]: string };
'dist-tags': { [tag: string]: string };
}

interface PackageJson {
dependencies: {
[key: string]: string;
}
}
87 changes: 87 additions & 0 deletions src/page-props/compare.ts
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;
}
15 changes: 14 additions & 1 deletion src/pages/_document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import { renderToNodeStream } from 'react-dom/server';

import IndexPage from '../pages/index';
import ResultPage from '../pages/result';
import ComparePage from '../pages/compare';
import NotFoundPage from '../pages/404';
import ServerErrorPage from '../pages/500';
import ParseFailurePage from '../pages/parse-failure';

const IndexFactory = createFactory(IndexPage);
const ResultFactory = createFactory(ResultPage);
const CompareFactory = createFactory(ComparePage);
const NotFoundFactory = createFactory(NotFoundPage);
const ServerErrorFactory = createFactory(ServerErrorPage);
const ParseFailureFactory = createFactory(ParseFailurePage);

import { getResultProps } from '../page-props/results';
import { getCompareProps } from '../page-props/compare';

import { containerId, pages, hostname } from '../util/constants';
import OctocatCorner from '../components/OctocatCorner';
Expand Down Expand Up @@ -121,6 +126,10 @@ export async function renderPage(
);
stream.on('end', () => {
res.end(`</div>
<script>
const input = document.querySelector('input[type=file]');
input.onchange = () => input.form.submit();
</script>
<script>document.getElementById('spinner').style.display='none'</script>
<script type="text/javascript">
if (window.location.hostname === '${hostname}') {
Expand All @@ -143,8 +152,12 @@ async function routePage(pathname: string, query: ParsedUrlQuery, tmpDir: string
switch (pathname) {
case pages.index:
return IndexFactory();
case pages.parseFailure:
return ParseFailureFactory();
case pages.result:
Comment thread
styfle marked this conversation as resolved.
return ResultFactory(await getResultProps(query, tmpDir));
return (query.p || '').includes(',')
? CompareFactory(await getCompareProps(query, tmpDir))
: ResultFactory(await getResultProps(query, tmpDir));
default:
return NotFoundFactory();
}
Expand Down
53 changes: 53 additions & 0 deletions src/pages/compare.tsx
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 />
</>
);
}
}
8 changes: 7 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export default () => (
</p>

<SearchBar autoFocus={true} />
</PageContainer>

<form style={{ marginTop: '60px' }} method="post" action={pages.compare} encType="multipart/form-data">
<label><span style={{ marginRight: '20px' }}>Or upload a package.json</span>
<input name="package.json" type="file" />
Comment thread
styfle marked this conversation as resolved.
</label>
<noscript><button type="submit" value="submit">Submit</button></noscript>
</form>
</PageContainer>
<Footer />
</>
);
26 changes: 26 additions & 0 deletions src/pages/parse-failure.tsx
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',
};
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use import PageContainer from '../components/PageContainer'; here from PR #450

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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 class PageError extends Error that we can then detect in the router to determine if the error should be rendered to the user.


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>
Comment thread
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 />
</>
);
25 changes: 25 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]+}/) || [];
Copy link
Copy Markdown
Owner

@styfle styfle Nov 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this regex? Can you instead use JSON.parse(data.toString())?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the data comes through as multipart/form-data, which includes the content boundaries (https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2). This seems like one lightweight way to parse the contents out, but definitely open to better options if you have any thoughts.

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;
Expand Down
2 changes: 2 additions & 0 deletions src/util/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const pages = {
index: '/index',
result: '/result',
compare: '/scan-results',
parseFailure: '/parse-failure',
badge: '/badge',
apiv1: '/api.json',
apiv2: '/v2/api.json',
Expand Down