Better dev server integration

This commit is contained in:
MICHAEL JACKSON
2018-02-16 16:00:06 -08:00
parent a22e0fa801
commit d6f2bc089a
42 changed files with 1753 additions and 1154 deletions

View File

@ -0,0 +1,26 @@
/**
* An express middleware that sets req.manifest from the build manifest
* in the given file. Should be used in production together with
* https://github.com/soundcloud/chunk-manifest-webpack-plugin
* to get consistent hashes.
*/
function assetsManifest(webpackManifestFile) {
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(webpackManifestFile, "utf8"));
} catch (error) {
invariant(
false,
'assetsManifest middleware cannot read the manifest file "%s"; ' +
"run `yarn build` before starting the server",
webpackManifestFile
);
}
return (req, res, next) => {
req.manifest = manifest;
next();
};
}
module.exports = assetsManifest;

View File

@ -1,4 +1,4 @@
const BlacklistAPI = require("../BlacklistAPI")
const BlacklistAPI = require("../BlacklistAPI");
function checkBlacklist(req, res, next) {
BlacklistAPI.includesPackage(req.packageName).then(
@ -8,19 +8,19 @@ function checkBlacklist(req, res, next) {
res
.status(403)
.type("text")
.send(`Package "${req.packageName}" is blacklisted`)
.send(`Package "${req.packageName}" is blacklisted`);
} else {
next()
next();
}
},
error => {
console.error(error)
console.error(error);
res.status(500).send({
error: "Unable to fetch the blacklist"
})
});
}
)
);
}
module.exports = checkBlacklist
module.exports = checkBlacklist;

View File

@ -1,55 +0,0 @@
const React = require("react")
const prettyBytes = require("pretty-bytes")
const getFileContentType = require("../utils/getFileContentType")
const e = React.createElement
const formatTime = time => new Date(time).toISOString()
const DirectoryListing = ({ dir, entries }) => {
const rows = entries.map(({ file, stats }, index) => {
const isDir = stats.isDirectory()
const href = file + (isDir ? "/" : "")
return e(
"tr",
{ key: file, className: index % 2 ? "odd" : "even" },
e("td", null, e("a", { title: file, href }, file)),
e("td", null, isDir ? "-" : getFileContentType(file)),
e("td", null, isDir ? "-" : prettyBytes(stats.size)),
e("td", null, isDir ? "-" : formatTime(stats.mtime))
)
})
if (dir !== "/")
rows.unshift(
e(
"tr",
{ key: "..", className: "odd" },
e("td", null, e("a", { title: "Parent directory", href: "../" }, "..")),
e("td", null, "-"),
e("td", null, "-"),
e("td", null, "-")
)
)
return e(
"table",
null,
e(
"thead",
null,
e(
"tr",
null,
e("th", null, "Name"),
e("th", null, "Type"),
e("th", null, "Size"),
e("th", null, "Last Modified")
)
),
e("tbody", null, rows)
)
}
module.exports = DirectoryListing

View File

@ -1,43 +0,0 @@
body {
font-size: 16px;
font-family: -apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif;
line-height: 1.5;
padding: 0px 10px 5px;
}
table {
width: 100%;
border-collapse: collapse;
font: 0.85em Monaco, monospace;
}
tr.even {
background-color: #eee;
}
th {
text-align: left;
}
th, td {
padding: 0.25em 0.5em;
}
.content-wrapper {
max-width: 900px;
margin: 0 auto;
}
.version-wrapper {
line-height: 2.25em;
float: right;
}
#version {
font-size: 1em;
}
address {
text-align: right;
}

View File

@ -1,54 +0,0 @@
const React = require("react")
const semver = require("semver")
const DirectoryListing = require("./DirectoryListing")
const readCSS = require("../utils/readCSS")
const e = React.createElement
const IndexPageStyle = readCSS(__dirname, "IndexPage.css")
const IndexPageScript = `
var s = document.getElementById('version'), v = s.value
s.onchange = function () {
window.location.href = window.location.href.replace('@' + v, '@' + s.value)
}
`
const byVersion = (a, b) => (semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0)
const IndexPage = ({ packageInfo, version, dir, entries }) => {
const versions = Object.keys(packageInfo.versions).sort(byVersion)
const options = versions.map(v => e("option", { key: v, value: v }, `${packageInfo.name}@${v}`))
return e(
"html",
null,
e(
"head",
null,
e("meta", { charSet: "utf-8" }),
e("title", null, `Index of ${dir}`),
e("style", { dangerouslySetInnerHTML: { __html: IndexPageStyle } })
),
e(
"body",
null,
e(
"div",
{ className: "content-wrapper" },
e(
"div",
{ className: "version-wrapper" },
e("select", { id: "version", defaultValue: version }, options)
),
e("h1", null, `Index of ${dir}`),
e("script", { dangerouslySetInnerHTML: { __html: IndexPageScript } }),
e("hr"),
e(DirectoryListing, { dir, entries }),
e("hr"),
e("address", null, `${packageInfo.name}@${version}`)
)
)
)
}
module.exports = IndexPage

View File

@ -0,0 +1,28 @@
const invariant = require("invariant");
const createBundle = require("./utils/createBundle");
/**
* An express middleware that sets req.bundle from the
* latest result from a running webpack compiler (i.e. using
* webpack-dev-middleware). Should only be used in dev.
*/
function devAssets(webpackCompiler) {
let bundle;
webpackCompiler.plugin("done", stats => {
bundle = createBundle(stats.toJson());
});
return (req, res, next) => {
invariant(
bundle != null,
"devAssets middleware needs a running compiler; " +
"use webpack-dev-middleware in front of devAssets"
);
req.bundle = bundle;
next();
};
}
module.exports = devAssets;

View File

@ -1,20 +1,20 @@
const fs = require("fs")
const path = require("path")
const semver = require("semver")
const createPackageURL = require("../utils/createPackageURL")
const createSearch = require("./utils/createSearch")
const getPackageInfo = require("./utils/getPackageInfo")
const getPackage = require("./utils/getPackage")
const incrementCounter = require("./utils/incrementCounter")
const fs = require("fs");
const path = require("path");
const semver = require("semver");
const createPackageURL = require("../utils/createPackageURL");
const createSearch = require("./utils/createSearch");
const getPackageInfo = require("./utils/getPackageInfo");
const getPackage = require("./utils/getPackage");
const incrementCounter = require("./utils/incrementCounter");
function getBasename(file) {
return path.basename(file, path.extname(file))
return path.basename(file, path.extname(file));
}
/**
* File extensions to look for when automatically resolving.
*/
const FindExtensions = ["", ".js", ".json"]
const FindExtensions = ["", ".js", ".json"];
/**
* Resolves a path like "lib/file" into "lib/file.js" or "lib/file.json"
@ -22,32 +22,36 @@ const FindExtensions = ["", ".js", ".json"]
*/
function findFile(base, useIndex, callback) {
FindExtensions.reduceRight((next, ext) => {
const file = base + ext
const file = base + ext;
return () => {
fs.stat(file, (error, stats) => {
if (error) {
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
next()
next();
} else {
callback(error)
callback(error);
}
} else if (useIndex && stats.isDirectory()) {
findFile(path.join(file, "index"), false, (error, indexFile, indexStats) => {
if (error) {
callback(error)
} else if (indexFile) {
callback(null, indexFile, indexStats)
} else {
next()
findFile(
path.join(file, "index"),
false,
(error, indexFile, indexStats) => {
if (error) {
callback(error);
} else if (indexFile) {
callback(null, indexFile, indexStats);
} else {
next();
}
}
})
);
} else {
callback(null, file, stats)
callback(null, file, stats);
}
})
}
}, callback)()
});
};
}, callback)();
}
/**
@ -57,112 +61,134 @@ function findFile(base, useIndex, callback) {
function fetchFile(req, res, next) {
getPackageInfo(req.packageName, (error, packageInfo) => {
if (error) {
console.error(error)
console.error(error);
return res
.status(500)
.type("text")
.send(`Cannot get info for package "${req.packageName}"`)
.send(`Cannot get info for package "${req.packageName}"`);
}
if (packageInfo == null || packageInfo.versions == null)
return res
.status(404)
.type("text")
.send(`Cannot find package "${req.packageName}"`)
.send(`Cannot find package "${req.packageName}"`);
req.packageInfo = packageInfo
req.packageInfo = packageInfo;
if (req.packageVersion in req.packageInfo.versions) {
// A valid request for a package we haven't downloaded yet.
req.packageConfig = req.packageInfo.versions[req.packageVersion]
req.packageConfig = req.packageInfo.versions[req.packageVersion];
getPackage(req.packageConfig, (error, outputDir) => {
if (error) {
console.error(error)
console.error(error);
res
.status(500)
.type("text")
.send(`Cannot fetch package ${req.packageSpec}`)
.send(`Cannot fetch package ${req.packageSpec}`);
} else {
req.packageDir = outputDir
req.packageDir = outputDir;
let filename = req.filename
let useIndex = true
let filename = req.filename;
let useIndex = true;
if (req.query.module != null) {
// They want an ES module. Try "module", "jsnext:main", and "/"
// https://github.com/rollup/rollup/wiki/pkg.module
if (!filename) {
filename = req.packageConfig.module || req.packageConfig["jsnext:main"] || "/"
filename =
req.packageConfig.module ||
req.packageConfig["jsnext:main"] ||
"/";
}
} else if (filename) {
// They are requesting an explicit filename. Only try to find an
// index file if they are NOT requesting an HTML directory listing.
useIndex = filename[filename.length - 1] !== "/"
} else if (req.query.main && typeof req.packageConfig[req.query.main] === "string") {
useIndex = filename[filename.length - 1] !== "/";
} else if (
req.query.main &&
typeof req.packageConfig[req.query.main] === "string"
) {
// They specified a custom ?main field.
filename = req.packageConfig[req.query.main]
filename = req.packageConfig[req.query.main];
incrementCounter(
"package-json-custom-main",
req.packageSpec + "?main=" + req.query.main,
1
)
);
} else if (typeof req.packageConfig.unpkg === "string") {
// The "unpkg" field allows packages to explicitly declare the
// file to serve at the bare URL (see #59).
filename = req.packageConfig.unpkg
filename = req.packageConfig.unpkg;
} else if (typeof req.packageConfig.browser === "string") {
// Fall back to the "browser" field if declared (only support strings).
filename = req.packageConfig.browser
filename = req.packageConfig.browser;
// Count which packages + versions are actually using this fallback
// so we can warn them when we deprecate this functionality.
// See https://github.com/unpkg/unpkg/issues/63
incrementCounter("package-json-browser-fallback", req.packageSpec, 1)
incrementCounter(
"package-json-browser-fallback",
req.packageSpec,
1
);
} else {
// Fall back to "main" or / (same as npm).
filename = req.packageConfig.main || "/"
filename = req.packageConfig.main || "/";
}
findFile(path.join(req.packageDir, filename), useIndex, (error, file, stats) => {
if (error) console.error(error)
findFile(
path.join(req.packageDir, filename),
useIndex,
(error, file, stats) => {
if (error) console.error(error);
if (file == null) {
return res
.status(404)
.type("text")
.send(`Cannot find module "${filename}" in package ${req.packageSpec}`)
if (file == null) {
return res
.status(404)
.type("text")
.send(
`Cannot find module "${filename}" in package ${
req.packageSpec
}`
);
}
filename = file.replace(req.packageDir, "");
if (
req.query.main != null ||
getBasename(req.filename) !== getBasename(filename)
) {
// Need to redirect to the module file so relative imports resolve
// correctly. Cache module redirects for 1 minute.
delete req.query.main;
res
.set({
"Cache-Control": "public, max-age=60",
"Cache-Tag": "redirect,module-redirect"
})
.redirect(
302,
createPackageURL(
req.packageName,
req.packageVersion,
filename,
createSearch(req.query)
)
);
} else {
req.filename = filename;
req.stats = stats;
next();
}
}
filename = file.replace(req.packageDir, "")
if (req.query.main != null || getBasename(req.filename) !== getBasename(filename)) {
// Need to redirect to the module file so relative imports resolve
// correctly. Cache module redirects for 1 minute.
delete req.query.main
res
.set({
"Cache-Control": "public, max-age=60",
"Cache-Tag": "redirect,module-redirect"
})
.redirect(
302,
createPackageURL(
req.packageName,
req.packageVersion,
filename,
createSearch(req.query)
)
)
} else {
req.filename = filename
req.stats = stats
next()
}
})
);
}
})
});
} else if (req.packageVersion in req.packageInfo["dist-tags"]) {
// Cache tag redirects for 1 minute.
res
@ -178,12 +204,12 @@ function fetchFile(req, res, next) {
req.filename,
req.search
)
)
);
} else {
const maxVersion = semver.maxSatisfying(
Object.keys(req.packageInfo.versions),
req.packageVersion
)
);
if (maxVersion) {
// Cache semver redirects for 1 minute.
@ -192,15 +218,23 @@ function fetchFile(req, res, next) {
"Cache-Control": "public, max-age=60",
"Cache-Tag": "redirect,semver-redirect"
})
.redirect(302, createPackageURL(req.packageName, maxVersion, req.filename, req.search))
.redirect(
302,
createPackageURL(
req.packageName,
maxVersion,
req.filename,
req.search
)
);
} else {
res
.status(404)
.type("text")
.send(`Cannot find package ${req.packageSpec}`)
.send(`Cannot find package ${req.packageSpec}`);
}
}
})
});
}
module.exports = fetchFile
module.exports = fetchFile;

View File

@ -1,29 +1,29 @@
const validateNpmPackageName = require("validate-npm-package-name")
const parsePackageURL = require("../utils/parsePackageURL")
const createSearch = require("./utils/createSearch")
const validateNpmPackageName = require("validate-npm-package-name");
const parsePackageURL = require("../utils/parsePackageURL");
const createSearch = require("./utils/createSearch");
const KnownQueryParams = {
main: true,
meta: true,
module: true
}
};
function isKnownQueryParam(param) {
return !!KnownQueryParams[param]
return !!KnownQueryParams[param];
}
function queryIsKnown(query) {
return Object.keys(query).every(isKnownQueryParam)
return Object.keys(query).every(isKnownQueryParam);
}
function sanitizeQuery(query) {
const saneQuery = {}
const saneQuery = {};
Object.keys(query).forEach(param => {
if (isKnownQueryParam(param)) saneQuery[param] = query[param]
})
if (isKnownQueryParam(param)) saneQuery[param] = query[param];
});
return saneQuery
return saneQuery;
}
/**
@ -32,54 +32,54 @@ function sanitizeQuery(query) {
function parseURL(req, res, next) {
// Redirect /_meta/path to /path?meta.
if (req.path.match(/^\/_meta\//)) {
req.query.meta = ""
return res.redirect(302, req.path.substr(6) + createSearch(req.query))
req.query.meta = "";
return res.redirect(302, req.path.substr(6) + createSearch(req.query));
}
// Redirect /path?json => /path?meta
if (req.query.json != null) {
delete req.query.json
req.query.meta = ""
return res.redirect(302, req.path + createSearch(req.query))
delete req.query.json;
req.query.meta = "";
return res.redirect(302, req.path + createSearch(req.query));
}
// Redirect requests with unknown query params to their equivalents
// with only known params so they can be served from the cache. This
// prevents people using random query params designed to bust the cache.
if (!queryIsKnown(req.query)) {
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)))
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)));
}
const url = parsePackageURL(req.url)
const url = parsePackageURL(req.url);
// Disallow invalid URLs.
if (url == null) {
return res
.status(403)
.type("text")
.send(`Invalid URL: ${req.url}`)
.send(`Invalid URL: ${req.url}`);
}
const nameErrors = validateNpmPackageName(url.packageName).errors
const nameErrors = validateNpmPackageName(url.packageName).errors;
// Disallow invalid package names.
if (nameErrors) {
const reason = nameErrors.join(", ")
const reason = nameErrors.join(", ");
return res
.status(403)
.type("text")
.send(`Invalid package name "${url.packageName}" (${reason})`)
.send(`Invalid package name "${url.packageName}" (${reason})`);
}
req.packageName = url.packageName
req.packageVersion = url.packageVersion
req.packageSpec = `${url.packageName}@${url.packageVersion}`
req.pathname = url.pathname
req.filename = url.filename
req.search = url.search
req.query = url.query
req.packageName = url.packageName;
req.packageVersion = url.packageVersion;
req.packageSpec = `${url.packageName}@${url.packageVersion}`;
req.pathname = url.pathname;
req.filename = url.filename;
req.search = url.search;
req.query = url.query;
next()
next();
}
module.exports = parseURL
module.exports = parseURL;

View File

@ -1,154 +0,0 @@
const fs = require("fs")
const path = require("path")
const etag = require("etag")
const babel = require("babel-core")
const getMetadata = require("./utils/getMetadata")
const getFileContentType = require("./utils/getFileContentType")
const getIndexHTML = require("./utils/getIndexHTML")
const unpkgRewrite = require("./utils/unpkgRewriteBabelPlugin")
/**
* Automatically generate HTML pages that show package contents.
*/
const AutoIndex = !process.env.DISABLE_INDEX
/**
* Maximum recursion depth for meta listings.
*/
const MaximumDepth = 128
function rewriteBareModuleIdentifiers(file, packageConfig, callback) {
const dependencies = Object.assign({}, packageConfig.peerDependencies, packageConfig.dependencies)
const options = {
// Ignore .babelrc and package.json babel config
// because we haven't installed dependencies so
// we can't load plugins; see #84
babelrc: false,
plugins: [unpkgRewrite(dependencies)]
}
babel.transformFile(file, options, (error, result) => {
callback(error, result && result.code)
})
}
/**
* Send the file, JSON metadata, or HTML directory listing.
*/
function serveFile(req, res) {
if (req.query.meta != null) {
// Serve JSON metadata.
getMetadata(req.packageDir, req.filename, req.stats, MaximumDepth, (error, metadata) => {
if (error) {
console.error(error)
res
.status(500)
.type("text")
.send(`Cannot generate metadata for ${req.packageSpec}${req.filename}`)
} else {
// Cache metadata for 1 year.
res
.set({
"Cache-Control": "public, max-age=31536000",
"Cache-Tag": "meta"
})
.send(metadata)
}
})
} else if (req.stats.isFile()) {
// Serve a file.
const file = path.join(req.packageDir, req.filename)
let contentType = getFileContentType(file)
if (contentType === "text/html") contentType = "text/plain" // We can't serve HTML because bad people :(
if (contentType === "application/javascript" && req.query.module != null) {
// Serve a JavaScript module.
rewriteBareModuleIdentifiers(file, req.packageConfig, (error, code) => {
if (error) {
console.error(error)
const debugInfo =
error.constructor.name +
": " +
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
"\n\n" +
error.codeFrame
res
.status(500)
.type("text")
.send(`Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}`)
} else {
// Cache modules for 1 year.
res
.set({
"Content-Type": `${contentType}; charset=utf-8`,
"Content-Length": Buffer.byteLength(code),
"Cache-Control": "public, max-age=31536000",
"Cache-Tag": "file,js-file,js-module"
})
.send(code)
}
})
} else {
// Serve some other static file.
const tags = ["file"]
const ext = path.extname(req.filename).substr(1)
if (ext) tags.push(`${ext}-file`)
if (contentType === "application/javascript") contentType += "; charset=utf-8"
// Cache files for 1 year.
res.set({
"Content-Type": contentType,
"Content-Length": req.stats.size,
"Cache-Control": "public, max-age=31536000",
"Last-Modified": req.stats.mtime.toUTCString(),
ETag: etag(req.stats),
"Cache-Tag": tags.join(",")
})
const stream = fs.createReadStream(file)
stream.on("error", error => {
console.error(`Cannot send file ${req.packageSpec}${req.filename}`)
console.error(error)
res.sendStatus(500)
})
stream.pipe(res)
}
} else if (AutoIndex && req.stats.isDirectory()) {
// Serve an HTML directory listing.
getIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.filename).then(
html => {
// Cache HTML directory listings for 1 minute.
res
.set({
"Cache-Control": "public, max-age=60",
"Cache-Tag": "index"
})
.send(html)
},
error => {
console.error(error)
res
.status(500)
.type("text")
.send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
}
)
} else {
res
.status(403)
.type("text")
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
}
}
module.exports = serveFile

View File

@ -0,0 +1,30 @@
const fs = require("fs");
const invariant = require("invariant");
const createBundle = require("./utils/createBundle");
/**
* An express middleware that sets req.bundle from the build
* info in the given stats file. Should be used in production.
*/
function staticAssets(webpackStatsFile) {
let stats;
try {
stats = JSON.parse(fs.readFileSync(webpackStatsFile, "utf8"));
} catch (error) {
invariant(
false,
"staticAssets middleware cannot read the build stats in %s; " +
"run `yarn build` before starting the server",
webpackStatsFile
);
}
const bundle = createBundle(stats);
return (req, res, next) => {
req.bundle = bundle;
next();
};
}
module.exports = staticAssets;

View File

@ -1,5 +0,0 @@
{
"env": {
"jest": true
}
}

View File

@ -1,5 +0,0 @@
{
"env": {
"jest": true
}
}

View File

@ -1,35 +0,0 @@
const getFileContentType = require("../getFileContentType")
it("gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile", () => {
expect(getFileContentType("AUTHORS")).toBe("text/plain")
expect(getFileContentType("CHANGES")).toBe("text/plain")
expect(getFileContentType("LICENSE")).toBe("text/plain")
expect(getFileContentType("Makefile")).toBe("text/plain")
expect(getFileContentType("PATENTS")).toBe("text/plain")
expect(getFileContentType("README")).toBe("text/plain")
})
it("gets a content type of text/plain for .*rc files", () => {
expect(getFileContentType(".eslintrc")).toBe("text/plain")
expect(getFileContentType(".babelrc")).toBe("text/plain")
expect(getFileContentType(".anythingrc")).toBe("text/plain")
})
it("gets a content type of text/plain for .git* files", () => {
expect(getFileContentType(".gitignore")).toBe("text/plain")
expect(getFileContentType(".gitanything")).toBe("text/plain")
})
it("gets a content type of text/plain for .*ignore files", () => {
expect(getFileContentType(".eslintignore")).toBe("text/plain")
expect(getFileContentType(".anythingignore")).toBe("text/plain")
})
it("gets a content type of text/plain for .ts files", () => {
expect(getFileContentType("app.ts")).toBe("text/plain")
expect(getFileContentType("app.d.ts")).toBe("text/plain")
})
it("gets a content type of text/plain for .flow files", () => {
expect(getFileContentType("app.js.flow")).toBe("text/plain")
})

View File

@ -1,76 +0,0 @@
const babel = require("babel-core")
const unpkgRewrite = require("../unpkgRewriteBabelPlugin")
const testCases = [
{
before: "import React from 'react';",
after: "import React from 'https://unpkg.com/react@15.6.1?module';"
},
{
before: "import router from '@angular/router';",
after: "import router from 'https://unpkg.com/@angular/router@4.3.5?module';"
},
{
before: "import map from 'lodash.map';",
after: "import map from 'https://unpkg.com/lodash.map@4.6.0?module';"
},
{
before: "import fs from 'pn/fs';",
after: "import fs from 'https://unpkg.com/pn@1.0.0/fs?module';"
},
{
before: "import cupcakes from './cupcakes';",
after: "import cupcakes from './cupcakes?module';"
},
{
before: "import shoelaces from '/shoelaces';",
after: "import shoelaces from '/shoelaces?module';"
},
{
before: "import something from '//something.com/whatevs';",
after: "import something from '//something.com/whatevs';"
},
{
before: "import something from 'http://something.com/whatevs';",
after: "import something from 'http://something.com/whatevs';"
},
{
before: "let ReactDOM = require('react-dom');",
after: "let ReactDOM = require('react-dom');"
},
{
before: "export React from 'react';",
after: "export React from 'https://unpkg.com/react@15.6.1?module';"
},
{
before: "export { Component } from 'react';",
after: "export { Component } from 'https://unpkg.com/react@15.6.1?module';"
},
{
before: "export * from 'react';",
after: "export * from 'https://unpkg.com/react@15.6.1?module';"
},
{
before: "export var message = 'hello';",
after: "export var message = 'hello';"
}
]
const dependencies = {
react: "15.6.1",
"@angular/router": "4.3.5",
"lodash.map": "4.6.0",
pn: "1.0.0"
}
describe("Rewriting imports/exports", () => {
testCases.forEach(testCase => {
it(`successfully rewrites "${testCase.before}"`, () => {
const result = babel.transform(testCase.before, {
plugins: [unpkgRewrite(dependencies)]
})
expect(result.code).toEqual(testCase.after)
})
})
})

View File

@ -0,0 +1,28 @@
/**
* Creates a bundle object that is stored on req.bundle.
*/
function createBundle(webpackStats) {
const { publicPath, assetsByChunkName } = webpackStats;
const createURL = asset => publicPath + asset;
const getAssets = (chunks = ["main"]) =>
(Array.isArray(chunks) ? chunks : [chunks])
.reduce((memo, chunk) => memo.concat(assetsByChunkName[chunk] || []), [])
.map(createURL);
const getScripts = (...args) =>
getAssets(...args).filter(asset => /\.js$/.test(asset));
const getStyles = (...args) =>
getAssets(...args).filter(asset => /\.css$/.test(asset));
return {
createURL,
getAssets,
getScripts,
getStyles
};
}
module.exports = createBundle;

View File

@ -1,13 +0,0 @@
const mime = require("mime")
mime.define({
"text/plain": ["authors", "changes", "license", "makefile", "patents", "readme", "ts", "flow"]
})
const TextFiles = /\/?(\.[a-z]*rc|\.git[a-z]*|\.[a-z]*ignore)$/i
function getFileContentType(file) {
return TextFiles.test(file) ? "text/plain" : mime.lookup(file)
}
module.exports = getFileContentType

View File

@ -1,15 +0,0 @@
const fs = require("fs")
function getFileStats(file) {
return new Promise((resolve, reject) => {
fs.lstat(file, (error, stats) => {
if (error) {
reject(error)
} else {
resolve(stats)
}
})
})
}
module.exports = getFileStats

View File

@ -1,12 +0,0 @@
function getFileType(stats) {
if (stats.isFile()) return "file"
if (stats.isDirectory()) return "directory"
if (stats.isBlockDevice()) return "blockDevice"
if (stats.isCharacterDevice()) return "characterDevice"
if (stats.isSymbolicLink()) return "symlink"
if (stats.isSocket()) return "socket"
if (stats.isFIFO()) return "fifo"
return "unknown"
}
module.exports = getFileType

View File

@ -1,40 +0,0 @@
const fs = require("fs")
const path = require("path")
const React = require("react")
const ReactDOMServer = require("react-dom/server")
const getFileStats = require("./getFileStats")
const IndexPage = require("../components/IndexPage")
const e = React.createElement
function getEntries(dir) {
return new Promise((resolve, reject) => {
fs.readdir(dir, function(error, files) {
if (error) {
reject(error)
} else {
resolve(
Promise.all(files.map(file => getFileStats(path.join(dir, file)))).then(statsArray => {
return statsArray.map((stats, index) => {
return { file: files[index], stats }
})
})
)
}
})
})
}
const DOCTYPE = "<!DOCTYPE html>"
function createHTML(props) {
return DOCTYPE + ReactDOMServer.renderToStaticMarkup(e(IndexPage, props))
}
function getIndexHTML(packageInfo, version, baseDir, dir) {
return getEntries(path.join(baseDir, dir)).then(entries =>
createHTML({ packageInfo, version, dir, entries })
)
}
module.exports = getIndexHTML

View File

@ -1,74 +0,0 @@
const fs = require("fs")
const path = require("path")
const SRIToolbox = require("sri-toolbox")
const getFileContentType = require("./getFileContentType")
const getFileStats = require("./getFileStats")
const getFileType = require("./getFileType")
function getEntries(dir, file, maximumDepth) {
return new Promise((resolve, reject) => {
fs.readdir(path.join(dir, file), function(error, files) {
if (error) {
reject(error)
} else {
resolve(
Promise.all(files.map(f => getFileStats(path.join(dir, file, f)))).then(statsArray => {
return Promise.all(
statsArray.map((stats, index) =>
getMetadataRecursive(dir, path.join(file, files[index]), stats, maximumDepth - 1)
)
)
})
)
}
})
})
}
function formatTime(time) {
return new Date(time).toISOString()
}
function getIntegrity(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, function(error, data) {
if (error) {
reject(error)
} else {
resolve(SRIToolbox.generate({ algorithms: ["sha384"] }, data))
}
})
})
}
function getMetadataRecursive(dir, file, stats, maximumDepth) {
const metadata = {
lastModified: formatTime(stats.mtime),
contentType: getFileContentType(file),
path: file,
size: stats.size,
type: getFileType(stats)
}
if (stats.isFile()) {
return getIntegrity(path.join(dir, file)).then(integrity => {
metadata.integrity = integrity
return metadata
})
}
if (!stats.isDirectory() || maximumDepth === 0) return Promise.resolve(metadata)
return getEntries(dir, file, maximumDepth).then(files => {
metadata.files = files
return metadata
})
}
function getMetadata(baseDir, path, stats, maximumDepth, callback) {
getMetadataRecursive(baseDir, path, stats, maximumDepth).then(function(metadata) {
callback(null, metadata)
}, callback)
}
module.exports = getMetadata

View File

@ -1,9 +0,0 @@
const fs = require("fs")
const path = require("path")
const csso = require("csso")
function readCSS(...args) {
return csso.minify(fs.readFileSync(path.resolve(...args), "utf8")).css
}
module.exports = readCSS

View File

@ -1,44 +0,0 @@
const URL = require("whatwg-url")
const warning = require("warning")
const BareIdentifierFormat = /^((?:@[^\/]+\/)?[^\/]+)(\/.*)?$/
function unpkgRewriteBabelPlugin(dependencies = {}) {
return {
inherits: require("babel-plugin-syntax-export-extensions"),
visitor: {
"ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration"(path) {
if (!path.node.source) return // probably a variable declaration
if (
URL.parseURL(path.node.source.value) != null ||
path.node.source.value.substr(0, 2) === "//"
)
return // valid URL or URL w/o protocol, leave it alone
if ([".", "/"].indexOf(path.node.source.value.charAt(0)) >= 0) {
// local path
path.node.source.value = `${path.node.source.value}?module`
} else {
// "bare" identifier
const match = BareIdentifierFormat.exec(path.node.source.value)
const packageName = match[1]
const file = match[2] || ""
warning(
dependencies[packageName],
'Missing version info for package "%s" in dependencies; falling back to "latest"',
packageName
)
const version = dependencies[packageName] || "latest"
path.node.source.value = `https://unpkg.com/${packageName}@${version}${file}?module`
}
}
}
}
}
module.exports = unpkgRewriteBabelPlugin