New "browse" UI
Also, separated out browse, ?meta, and ?module request handlers. Fixes #82
This commit is contained in:
@ -1,39 +1,47 @@
|
||||
import getContentType from '../getContentType.js';
|
||||
|
||||
it('gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => {
|
||||
expect(getContentType('AUTHORS')).toBe('text/plain');
|
||||
expect(getContentType('CHANGES')).toBe('text/plain');
|
||||
expect(getContentType('LICENSE')).toBe('text/plain');
|
||||
expect(getContentType('Makefile')).toBe('text/plain');
|
||||
expect(getContentType('PATENTS')).toBe('text/plain');
|
||||
expect(getContentType('README')).toBe('text/plain');
|
||||
});
|
||||
describe('getContentType', () => {
|
||||
it('returns text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => {
|
||||
expect(getContentType('AUTHORS')).toBe('text/plain');
|
||||
expect(getContentType('CHANGES')).toBe('text/plain');
|
||||
expect(getContentType('LICENSE')).toBe('text/plain');
|
||||
expect(getContentType('Makefile')).toBe('text/plain');
|
||||
expect(getContentType('PATENTS')).toBe('text/plain');
|
||||
expect(getContentType('README')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .*rc files', () => {
|
||||
expect(getContentType('.eslintrc')).toBe('text/plain');
|
||||
expect(getContentType('.babelrc')).toBe('text/plain');
|
||||
expect(getContentType('.anythingrc')).toBe('text/plain');
|
||||
});
|
||||
it('returns text/plain for .*rc files', () => {
|
||||
expect(getContentType('.eslintrc')).toBe('text/plain');
|
||||
expect(getContentType('.babelrc')).toBe('text/plain');
|
||||
expect(getContentType('.anythingrc')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .git* files', () => {
|
||||
expect(getContentType('.gitignore')).toBe('text/plain');
|
||||
expect(getContentType('.gitanything')).toBe('text/plain');
|
||||
});
|
||||
it('returns text/plain for .git* files', () => {
|
||||
expect(getContentType('.gitignore')).toBe('text/plain');
|
||||
expect(getContentType('.gitanything')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .*ignore files', () => {
|
||||
expect(getContentType('.eslintignore')).toBe('text/plain');
|
||||
expect(getContentType('.anythingignore')).toBe('text/plain');
|
||||
});
|
||||
it('returns text/plain for .*ignore files', () => {
|
||||
expect(getContentType('.eslintignore')).toBe('text/plain');
|
||||
expect(getContentType('.anythingignore')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .ts files', () => {
|
||||
expect(getContentType('app.ts')).toBe('text/plain');
|
||||
expect(getContentType('app.d.ts')).toBe('text/plain');
|
||||
});
|
||||
it('returns text/plain for .ts(x) files', () => {
|
||||
expect(getContentType('app.ts')).toBe('text/plain');
|
||||
expect(getContentType('app.d.ts')).toBe('text/plain');
|
||||
expect(getContentType('app.tsx')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .flow files', () => {
|
||||
expect(getContentType('app.js.flow')).toBe('text/plain');
|
||||
});
|
||||
it('returns text/plain for .flow files', () => {
|
||||
expect(getContentType('app.js.flow')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('gets a content type of text/plain for .lock files', () => {
|
||||
expect(getContentType('yarn.lock')).toBe('text/plain');
|
||||
it('returns text/plain for .lock files', () => {
|
||||
expect(getContentType('yarn.lock')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('returns application/json for .map files', () => {
|
||||
expect(getContentType('react.js.map')).toBe('application/json');
|
||||
expect(getContentType('react.json.map')).toBe('application/json');
|
||||
});
|
||||
});
|
||||
|
84
modules/utils/__tests__/getLanguageName-test.js
Normal file
84
modules/utils/__tests__/getLanguageName-test.js
Normal file
@ -0,0 +1,84 @@
|
||||
import getLanguageName from '../getLanguageName.js';
|
||||
|
||||
describe('getLanguageName', () => {
|
||||
// Hard-coded overrides
|
||||
|
||||
it('detects Flow files', () => {
|
||||
expect(getLanguageName('react.flow')).toBe('Flow');
|
||||
});
|
||||
|
||||
it('detects source maps', () => {
|
||||
expect(getLanguageName('react.map')).toBe('Source Map (JSON)');
|
||||
expect(getLanguageName('react.js.map')).toBe('Source Map (JSON)');
|
||||
expect(getLanguageName('react.json.map')).toBe('Source Map (JSON)');
|
||||
});
|
||||
|
||||
it('detects TypeScript files', () => {
|
||||
expect(getLanguageName('react.d.ts')).toBe('TypeScript');
|
||||
expect(getLanguageName('react.tsx')).toBe('TypeScript');
|
||||
});
|
||||
|
||||
// Content-Type lookups
|
||||
|
||||
it('detects JavaScript files', () => {
|
||||
expect(getLanguageName('react.js')).toBe('JavaScript');
|
||||
});
|
||||
|
||||
it('detects JSON files', () => {
|
||||
expect(getLanguageName('react.json')).toBe('JSON');
|
||||
});
|
||||
|
||||
it('detects binary files', () => {
|
||||
expect(getLanguageName('ionicons.bin')).toBe('Binary');
|
||||
});
|
||||
|
||||
it('detects EOT files', () => {
|
||||
expect(getLanguageName('ionicons.eot')).toBe('Embedded OpenType');
|
||||
});
|
||||
|
||||
it('detects SVG files', () => {
|
||||
expect(getLanguageName('react.svg')).toBe('SVG');
|
||||
});
|
||||
|
||||
it('detects TTF files', () => {
|
||||
expect(getLanguageName('ionicons.ttf')).toBe('TrueType Font');
|
||||
});
|
||||
|
||||
it('detects WOFF files', () => {
|
||||
expect(getLanguageName('ionicons.woff')).toBe('WOFF');
|
||||
});
|
||||
|
||||
it('detects WOFF2 files', () => {
|
||||
expect(getLanguageName('ionicons.woff2')).toBe('WOFF2');
|
||||
});
|
||||
|
||||
it('detects CSS files', () => {
|
||||
expect(getLanguageName('react.css')).toBe('CSS');
|
||||
});
|
||||
|
||||
it('detects HTML files', () => {
|
||||
expect(getLanguageName('react.html')).toBe('HTML');
|
||||
});
|
||||
|
||||
it('detects JSX files', () => {
|
||||
expect(getLanguageName('react.jsx')).toBe('JSX');
|
||||
});
|
||||
|
||||
it('detects Markdown files', () => {
|
||||
expect(getLanguageName('README.md')).toBe('Markdown');
|
||||
});
|
||||
|
||||
it('detects plain text files', () => {
|
||||
expect(getLanguageName('README')).toBe('Plain Text');
|
||||
expect(getLanguageName('LICENSE')).toBe('Plain Text');
|
||||
});
|
||||
|
||||
it('detects SCSS files', () => {
|
||||
expect(getLanguageName('some.scss')).toBe('SCSS');
|
||||
});
|
||||
|
||||
it('detects YAML files', () => {
|
||||
expect(getLanguageName('config.yml')).toBe('YAML');
|
||||
expect(getLanguageName('config.yaml')).toBe('YAML');
|
||||
});
|
||||
});
|
74
modules/utils/cloudflare.js
Normal file
74
modules/utils/cloudflare.js
Normal file
@ -0,0 +1,74 @@
|
||||
import fetch from 'isomorphic-fetch';
|
||||
|
||||
const cloudflareURL = 'https://api.cloudflare.com/client/v4';
|
||||
const cloudflareEmail = process.env.CLOUDFLARE_EMAIL;
|
||||
const cloudflareKey = process.env.CLOUDFLARE_KEY;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!cloudflareEmail) {
|
||||
throw new Error('Missing the $CLOUDFLARE_EMAIL environment variable');
|
||||
}
|
||||
|
||||
if (!cloudflareKey) {
|
||||
throw new Error('Missing the $CLOUDFLARE_KEY environment variable');
|
||||
}
|
||||
}
|
||||
|
||||
function get(path, headers) {
|
||||
return fetch(`${cloudflareURL}${path}`, {
|
||||
headers: Object.assign({}, headers, {
|
||||
'X-Auth-Email': cloudflareEmail,
|
||||
'X-Auth-Key': cloudflareKey
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getJSON(path, headers) {
|
||||
return get(path, headers)
|
||||
.then(res => {
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error(`cloudflare.getJSON failed at ${path}`);
|
||||
console.error(data);
|
||||
throw new Error('Failed to getJSON from Cloudflare');
|
||||
}
|
||||
|
||||
return data.result;
|
||||
});
|
||||
}
|
||||
|
||||
export function getZones(domains) {
|
||||
return Promise.all(
|
||||
(Array.isArray(domains) ? domains : [domains]).map(domain =>
|
||||
getJSON(`/zones?name=${domain}`)
|
||||
)
|
||||
).then(results => results.reduce((memo, zones) => memo.concat(zones)));
|
||||
}
|
||||
|
||||
function reduceResults(target, values) {
|
||||
Object.keys(values).forEach(key => {
|
||||
const value = values[key];
|
||||
|
||||
if (typeof value === 'object' && value) {
|
||||
target[key] = reduceResults(target[key] || {}, value);
|
||||
} else if (typeof value === 'number') {
|
||||
target[key] = (target[key] || 0) + values[key];
|
||||
}
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export function getZoneAnalyticsDashboard(zones, since, until) {
|
||||
return Promise.all(
|
||||
(Array.isArray(zones) ? zones : [zones]).map(zone => {
|
||||
return getJSON(
|
||||
`/zones/${
|
||||
zone.id
|
||||
}/analytics/dashboard?since=${since.toISOString()}&until=${until.toISOString()}`
|
||||
);
|
||||
})
|
||||
).then(results => results.reduce(reduceResults));
|
||||
}
|
3
modules/utils/createDataURI.js
Normal file
3
modules/utils/createDataURI.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function createDataURI(contentType, content) {
|
||||
return `data:${contentType};base64,${content.toString('base64')}`;
|
||||
}
|
8
modules/utils/encodeJSONForScript.js
Normal file
8
modules/utils/encodeJSONForScript.js
Normal file
@ -0,0 +1,8 @@
|
||||
import jsesc from 'jsesc';
|
||||
|
||||
/**
|
||||
* Encodes some data as JSON that may safely be included in HTML.
|
||||
*/
|
||||
export default function encodeJSONForScript(data) {
|
||||
return jsesc(data, { json: true, isScriptContext: true });
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
mime.define(
|
||||
@ -19,5 +20,9 @@ mime.define(
|
||||
const textFiles = /\/?(\.[a-z]*rc|\.git[a-z]*|\.[a-z]*ignore|\.lock)$/i;
|
||||
|
||||
export default function getContentType(file) {
|
||||
return textFiles.test(file) ? 'text/plain' : mime.getType(file);
|
||||
const name = path.basename(file);
|
||||
|
||||
return textFiles.test(name)
|
||||
? 'text/plain'
|
||||
: mime.getType(name) || 'text/plain';
|
||||
}
|
||||
|
82
modules/utils/getHighlights.js
Normal file
82
modules/utils/getHighlights.js
Normal file
@ -0,0 +1,82 @@
|
||||
import path from 'path';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
import getContentType from './getContentType.js';
|
||||
|
||||
function escapeHTML(code) {
|
||||
return code
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// These should probably be added to highlight.js auto-detection.
|
||||
const extLanguages = {
|
||||
map: 'json',
|
||||
mjs: 'javascript',
|
||||
tsbuildinfo: 'json',
|
||||
tsx: 'typescript',
|
||||
vue: 'html'
|
||||
};
|
||||
|
||||
function getLanguage(file) {
|
||||
// Try to guess the language based on the file extension.
|
||||
const ext = path.extname(file).substr(1);
|
||||
|
||||
if (ext) {
|
||||
return extLanguages[ext] || ext;
|
||||
}
|
||||
|
||||
const contentType = getContentType(file);
|
||||
|
||||
if (contentType === 'text/plain') {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLines(code) {
|
||||
return code
|
||||
.split('\n')
|
||||
.map((line, index, array) =>
|
||||
index === array.length - 1 ? line : line + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of HTML strings that highlight the given source code.
|
||||
*/
|
||||
export default function getHighlights(code, file) {
|
||||
const language = getLanguage(file);
|
||||
|
||||
if (!language) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (language === 'text') {
|
||||
return getLines(code).map(escapeHTML);
|
||||
}
|
||||
|
||||
try {
|
||||
let continuation = false;
|
||||
const hi = getLines(code).map(line => {
|
||||
const result = hljs.highlight(language, line, false, continuation);
|
||||
continuation = result.top;
|
||||
return result;
|
||||
});
|
||||
|
||||
return hi.map(result =>
|
||||
result.value.replace(
|
||||
/<span class="hljs-(\w+)">/g,
|
||||
'<span class="code-$1">'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
// Probably an "unknown language" error.
|
||||
// console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
36
modules/utils/getLanguageName.js
Normal file
36
modules/utils/getLanguageName.js
Normal file
@ -0,0 +1,36 @@
|
||||
import getContentType from './getContentType.js';
|
||||
|
||||
const contentTypeNames = {
|
||||
'application/javascript': 'JavaScript',
|
||||
'application/json': 'JSON',
|
||||
'application/octet-stream': 'Binary',
|
||||
'application/vnd.ms-fontobject': 'Embedded OpenType',
|
||||
'application/xml': 'XML',
|
||||
'image/svg+xml': 'SVG',
|
||||
'font/ttf': 'TrueType Font',
|
||||
'font/woff': 'WOFF',
|
||||
'font/woff2': 'WOFF2',
|
||||
'text/css': 'CSS',
|
||||
'text/html': 'HTML',
|
||||
'text/jsx': 'JSX',
|
||||
'text/markdown': 'Markdown',
|
||||
'text/plain': 'Plain Text',
|
||||
'text/x-scss': 'SCSS',
|
||||
'text/yaml': 'YAML'
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a human-friendly name for whatever is in the given file.
|
||||
*/
|
||||
export default function getLanguageName(file) {
|
||||
// Content-Type is text/plain, but we can be more descriptive.
|
||||
if (/\.flow$/.test(file)) return 'Flow';
|
||||
if (/\.(d\.ts|tsx)$/.test(file)) return 'TypeScript';
|
||||
|
||||
// Content-Type is application/json, but we can be more descriptive.
|
||||
if (/\.map$/.test(file)) return 'Source Map (JSON)';
|
||||
|
||||
const contentType = getContentType(file);
|
||||
|
||||
return contentTypeNames[contentType] || contentType;
|
||||
}
|
42
modules/utils/getScripts.js
Normal file
42
modules/utils/getScripts.js
Normal file
@ -0,0 +1,42 @@
|
||||
// Virtual module id; see rollup.config.js
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import entryManifest from 'entry-manifest';
|
||||
|
||||
import { createElement, createScript } from './markup.js';
|
||||
|
||||
function getEntryPoint(name, format) {
|
||||
let entryPoints;
|
||||
entryManifest.forEach(manifest => {
|
||||
if (name in manifest) {
|
||||
entryPoints = manifest[name];
|
||||
}
|
||||
});
|
||||
|
||||
if (entryPoints) {
|
||||
return entryPoints.find(e => e.format === format);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGlobalScripts(entryPoint, globalURLs) {
|
||||
return entryPoint.globalImports.map(id => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!globalURLs[id]) {
|
||||
throw new Error('Missing global URL for id "%s"', id);
|
||||
}
|
||||
}
|
||||
|
||||
return createElement('script', { src: globalURLs[id] });
|
||||
});
|
||||
}
|
||||
|
||||
export default function getScripts(entryName, format, globalURLs) {
|
||||
const entryPoint = getEntryPoint(entryName, format);
|
||||
|
||||
if (!entryPoint) return [];
|
||||
|
||||
return getGlobalScripts(entryPoint, globalURLs).concat(
|
||||
createScript(entryPoint.code)
|
||||
);
|
||||
}
|
38
modules/utils/getStats.js
Normal file
38
modules/utils/getStats.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { getZones, getZoneAnalyticsDashboard } from './cloudflare.js';
|
||||
|
||||
function extractPublicInfo(data) {
|
||||
return {
|
||||
since: data.since,
|
||||
until: data.until,
|
||||
requests: {
|
||||
all: data.requests.all,
|
||||
cached: data.requests.cached,
|
||||
country: data.requests.country,
|
||||
status: data.requests.http_status
|
||||
},
|
||||
bandwidth: {
|
||||
all: data.bandwidth.all,
|
||||
cached: data.bandwidth.cached,
|
||||
country: data.bandwidth.country
|
||||
},
|
||||
threats: {
|
||||
all: data.threats.all,
|
||||
country: data.threats.country
|
||||
},
|
||||
uniques: {
|
||||
all: data.uniques.all
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const DomainNames = ['unpkg.com', 'npmcdn.com'];
|
||||
|
||||
export default async function getStats(since, until) {
|
||||
const zones = await getZones(DomainNames);
|
||||
const dashboard = await getZoneAnalyticsDashboard(zones, since, until);
|
||||
|
||||
return {
|
||||
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
||||
totals: extractPublicInfo(dashboard.totals)
|
||||
};
|
||||
}
|
13
modules/utils/markup.js
Normal file
13
modules/utils/markup.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
export { createElement };
|
||||
|
||||
export function createHTML(code) {
|
||||
return { __html: code };
|
||||
}
|
||||
|
||||
export function createScript(script) {
|
||||
return createElement('script', {
|
||||
dangerouslySetInnerHTML: createHTML(script)
|
||||
});
|
||||
}
|
@ -19,7 +19,7 @@ export default function parsePackageURL(originalURL) {
|
||||
|
||||
const packageName = match[1];
|
||||
const packageVersion = match[2] || 'latest';
|
||||
const filename = match[3] || '';
|
||||
const filename = (match[3] || '').replace(/\/\/+/g, '/');
|
||||
|
||||
return {
|
||||
// If the URL is /@scope/name@version/file.js?main=browser:
|
||||
|
Reference in New Issue
Block a user