Add dev runtime from web-starter

This commit is contained in:
Michael Jackson 2016-05-16 15:55:24 -07:00
parent 5f2615f2ed
commit 66ef9a780c
10 changed files with 382 additions and 106 deletions

View File

@ -0,0 +1,86 @@
import React from 'react'
import './Home.css'
const Home = React.createClass({
render() {
return (
<div>
<header>
<h1>npmcdn</h1>
</header>
<section id="wrapper">
<p>npmcdn is a CDN for packages that are published via <a href="https://www.npmjs.com/">npm</a>. Use it to quickly and easily load files using a simple URL like <code>https://npmcdn.com/package@version/path/to/file</code>.</p>
<p>A few examples:</p>
<ul>
<li><a href="/react@15.0.1/dist/react.min.js">https://npmcdn.com/react@15.0.1/dist/react.min.js</a></li>
<li><a href="/react-dom@15.0.1/dist/react-dom.min.js">https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js</a></li>
<li><a href="/history@1.12.5/umd/History.min.js">https://npmcdn.com/history@1.12.5/umd/History.min.js</a></li>
<li><a href="/react-router@1.0.0/umd/ReactRouter.min.js">https://npmcdn.com/react-router@1.0.0/umd/ReactRouter.min.js</a></li>
</ul>
<p>You may also use a <a href="https://docs.npmjs.com/cli/dist-tag">tag</a> or <a href="https://docs.npmjs.com/misc/semver">version range</a> instead of a fixed version number, or omit the version/tag entirely to use the <code>latest</code> tag.</p>
<ul>
<li><a href="/react@^0.14/dist/react.min.js">https://npmcdn.com/react@^0.14/dist/react.min.js</a></li>
<li><a href="/react/dist/react.min.js">https://npmcdn.com/react/dist/react.min.js</a></li>
</ul>
<p>If you omit the file path, the <a href="https://docs.npmjs.com/files/package.json#main">main module</a> will be served. This is especially useful for loading libaries that publish a UMD build as their main module.</p>
<ul>
<li><a href="/three">https://npmcdn.com/three</a></li>
<li><a href="/jquery">https://npmcdn.com/jquery</a></li>
<li><a href="/angular-formly">https://npmcdn.com/angular-formly</a></li>
</ul>
<p>Append a <code>/</code> at the end of a URL to view a listing of all the files in a package.</p>
<ul>
<li><a href="/lodash/">https://npmcdn.com/lodash/</a></li>
<li><a href="/modernizr/">https://npmcdn.com/modernizr/</a></li>
<li><a href="/react/">https://npmcdn.com/react/</a></li>
</ul>
<p>You may use the special <code>/bower.zip</code> file path in packages that contain a <code>bower.json</code> file to dynamically generate a zip file that Bower can use to install the package.</p>
<ul>
<li><a href="/react-swap/bower.zip">https://npmcdn.com/react-swap/bower.zip</a></li>
<li><a href="/react-collapse@1.6.3/bower.zip">https://npmcdn.com/react-collapse@1.6.3/bower.zip</a></li>
</ul>
<p><strong>Please note: <em>We do NOT recommend JavaScript libraries use Bower.</em></strong> Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017. Please move to npm for installing packages and stop using Bower before that time. See <a href="https://github.com/mjackson/npm-http-server#bower-support">here</a> for our rationale.</p>
<h3>Query Parameters</h3>
<table cellPadding="0" cellSpacing="0">
<thead>
<tr>
<th width="80px">Name</th>
<th width="120px">Default Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>main</code></td>
<td><code>main</code></td>
<td>The name of the field in <a href="https://docs.npmjs.com/files/package.json">package.json</a> to use as the main entry point when there is no file path in the URL (e.g. <code>?main=browser</code>).</td>
</tr>
</tbody>
</table>
<h3>Suggested Workflow</h3>
<p>For npm package authors, npmcdn relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your <a href="https://github.com/umdjs/umd">UMD</a> build in your npm package (not your repo, that's different!).</p>
<p>You can do this easily using the following setup:</p>
<ul>
<li>Add the <code>umd</code> (or <code>dist</code>) directory to your <code>.gitignore</code> file</li>
<li>Add the <code>umd</code> directory to your <a href="https://docs.npmjs.com/files/package.json#files">files array</a> in <code>package.json</code></li>
<li>Use a build script to generate your UMD build in the <code>umd</code> directory when you publish</li>
</ul>
<p>That's it! Now when you <code>npm publish</code> you'll have a version available on npmcdn as well.</p>
<h3>Feedback</h3>
<p>If you think this is useful, I'd love to hear from you. Please reach out to <a href="https://twitter.com/mjackson">@mjackson</a> with any questions/concerns.</p>
<p>Also, please feel free to examine the source on <a href="https://github.com/mjackson/npmcdn.com">GitHub</a>.</p>
</section>
</div>
)
}
})
export default Home

8
modules/client/home.js Normal file
View File

@ -0,0 +1,8 @@
import React from 'react'
import { render } from 'react-dom'
import Home from './components/Home'
render(
<Home/>,
document.getElementById('app')
)

View File

@ -0,0 +1,115 @@
import fs from 'fs'
import invariant from 'invariant'
import webpack from 'webpack'
const createAssets = (webpackStats) => {
const createURL = (asset) =>
webpackStats.publicPath + asset
const getAssets = (chunkName = 'main') => {
const assets = webpackStats.assetsByChunkName[chunkName] || []
return Array.isArray(assets) ? assets : [ assets ]
}
const getScriptURLs = (chunkName = 'main') =>
getAssets(chunkName)
.filter(asset => (/\.js$/).test(asset))
.map(createURL)
const getStyleURLs = (chunkName = 'main') =>
getAssets(chunkName)
.filter(asset => (/\.css$/).test(asset))
.map(createURL)
return {
createURL,
getAssets,
getScriptURLs,
getStyleURLs
}
}
/**
* An express middleware that sets req.assets from the build
* info in the given stats file. Should be used in production.
*/
export const 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; ' +
'do `npm run build` before starting the server',
webpackStatsFile
)
}
const assets = createAssets(stats)
return (req, res, next) => {
req.assets = assets
next()
}
}
/**
* An express middleware that sets req.assets from the
* latest result from a running webpack compiler (i.e. using
* webpack-dev-middleware). Should only be used in dev.
*/
export const assetsCompiler = (webpackCompiler) => {
let assets
webpackCompiler.plugin('done', (stats) => {
assets = createAssets(stats.toJson())
})
return (req, res, next) => {
invariant(
assets != null,
'assetsCompiler middleware needs a running compiler; ' +
'use webpack-dev-middleware in front of assetsCompiler'
)
req.assets = assets
next()
}
}
/**
* Creates a webpack compiler that automatically inlines the
* webpack dev runtime in all entry points.
*/
export const createDevCompiler = (webpackConfig, webpackRuntimeModuleID) =>
webpack({
...webpackConfig,
entry: prependModuleID(
webpackConfig.entry,
webpackRuntimeModuleID
)
})
/**
* Returns a modified copy of the given webpackEntry object with
* the moduleID in front of all other assets.
*/
const prependModuleID = (webpackEntry, moduleID) => {
if (typeof webpackEntry === 'string')
return [ moduleID, webpackEntry ]
if (Array.isArray(webpackEntry))
return [ moduleID, ...webpackEntry ]
if (webpackEntry && typeof webpackEntry === 'object') {
const entry = { ...webpackEntry }
for (const chunkName in entry)
if (entry.hasOwnProperty(chunkName))
entry[chunkName] = prependModuleID(entry[chunkName], moduleID)
return entry
}
throw new Error('Invalid webpack entry object')
}

View File

@ -0,0 +1,16 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import HomePage from './components/HomePage'
const DOCTYPE = '<!DOCTYPE html>'
export const sendHomePage = (req, res) => {
const props = {
styles: req.assets.getStyleURLs('home'),
scripts: req.assets.getScriptURLs('home')
}
res.send(
DOCTYPE + renderToStaticMarkup(<HomePage {...props}/>)
)
}

View File

@ -1,94 +1,35 @@
import React from 'react'
import { readCSS } from '../StyleUtils'
import React, { PropTypes } from 'react'
const assetType = PropTypes.string
const HomePage = React.createClass({
statics: {
css: readCSS(__dirname, './HomePage.css')
propTypes: {
styles: PropTypes.arrayOf(assetType),
scripts: PropTypes.arrayOf(assetType)
},
getDefaultProps() {
return {
styles: [],
scripts: []
}
},
render() {
const { styles, scripts } = this.props
return (
<html>
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="user-scalable=no,initial-scale=1.0,maximum-scale=1.0,width=device-width"/>
<meta name="timestamp" content={(new Date).toISOString()}/>
<title>npmcdn</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="rendered-at" content={(new Date).toISOString()}/>
<style dangerouslySetInnerHTML={{ __html: HomePage.css }}/>
{styles.map(style => <link key={style} rel="stylesheet" href={style}/>)}
</head>
<body>
<header>
<h1>npmcdn</h1>
</header>
<section id="wrapper">
<p>npmcdn is a CDN for packages that are published via <a href="https://www.npmjs.com/">npm</a>. Use it to quickly and easily load files using a simple URL like <code>https://npmcdn.com/package@version/path/to/file</code>.</p>
<p>A few examples:</p>
<ul>
<li><a href="/react@15.0.1/dist/react.min.js">https://npmcdn.com/react@15.0.1/dist/react.min.js</a></li>
<li><a href="/react-dom@15.0.1/dist/react-dom.min.js">https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js</a></li>
<li><a href="/history@1.12.5/umd/History.min.js">https://npmcdn.com/history@1.12.5/umd/History.min.js</a></li>
<li><a href="/react-router@1.0.0/umd/ReactRouter.min.js">https://npmcdn.com/react-router@1.0.0/umd/ReactRouter.min.js</a></li>
</ul>
<p>You may also use a <a href="https://docs.npmjs.com/cli/dist-tag">tag</a> or <a href="https://docs.npmjs.com/misc/semver">version range</a> instead of a fixed version number, or omit the version/tag entirely to use the <code>latest</code> tag.</p>
<ul>
<li><a href="/react@^0.14/dist/react.min.js">https://npmcdn.com/react@^0.14/dist/react.min.js</a></li>
<li><a href="/react/dist/react.min.js">https://npmcdn.com/react/dist/react.min.js</a></li>
</ul>
<p>If you omit the file path, the <a href="https://docs.npmjs.com/files/package.json#main">main module</a> will be served. This is especially useful for loading libaries that publish a UMD build as their main module.</p>
<ul>
<li><a href="/three">https://npmcdn.com/three</a></li>
<li><a href="/jquery">https://npmcdn.com/jquery</a></li>
<li><a href="/angular-formly">https://npmcdn.com/angular-formly</a></li>
</ul>
<p>Append a <code>/</code> at the end of a URL to view a listing of all the files in a package.</p>
<ul>
<li><a href="/lodash/">https://npmcdn.com/lodash/</a></li>
<li><a href="/modernizr/">https://npmcdn.com/modernizr/</a></li>
<li><a href="/react/">https://npmcdn.com/react/</a></li>
</ul>
<p>You may use the special <code>/bower.zip</code> file path in packages that contain a <code>bower.json</code> file to dynamically generate a zip file that Bower can use to install the package.</p>
<ul>
<li><a href="/react-swap/bower.zip">https://npmcdn.com/react-swap/bower.zip</a></li>
<li><a href="/react-collapse@1.6.3/bower.zip">https://npmcdn.com/react-collapse@1.6.3/bower.zip</a></li>
</ul>
<p><strong>Please note: <em>We do NOT recommend JavaScript libraries use Bower.</em></strong> Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017. Please move to npm for installing packages and stop using Bower before that time. See <a href="https://github.com/mjackson/npm-http-server#bower-support">here</a> for our rationale.</p>
<h3>Query Parameters</h3>
<table cellPadding="0" cellSpacing="0">
<thead>
<tr>
<th width="80px">Name</th>
<th width="120px">Default Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>main</code></td>
<td><code>main</code></td>
<td>The name of the field in <a href="https://docs.npmjs.com/files/package.json">package.json</a> to use as the main entry point when there is no file path in the URL (e.g. <code>?main=browser</code>).</td>
</tr>
</tbody>
</table>
<h3>Suggested Workflow</h3>
<p>For npm package authors, npmcdn relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your <a href="https://github.com/umdjs/umd">UMD</a> build in your npm package (not your repo, that's different!).</p>
<p>You can do this easily using the following setup:</p>
<ul>
<li>Add the <code>umd</code> (or <code>dist</code>) directory to your <code>.gitignore</code> file</li>
<li>Add the <code>umd</code> directory to your <a href="https://docs.npmjs.com/files/package.json#files">files array</a> in <code>package.json</code></li>
<li>Use a build script to generate your UMD build in the <code>umd</code> directory when you publish</li>
</ul>
<p>That's it! Now when you <code>npm publish</code> you'll have a version available on npmcdn as well.</p>
<h3>Feedback</h3>
<p>If you think this is useful, I'd love to hear from you. Please reach out to <a href="https://twitter.com/mjackson">@mjackson</a> with any questions/concerns.</p>
<p>Also, please feel free to examine the source on <a href="https://github.com/mjackson/npmcdn.com">GitHub</a>.</p>
</section>
<div id="app"/>
{scripts.map(script => <script key={script} src={script}/>)}
</body>
</html>
)

View File

@ -1,31 +1,81 @@
import path from 'path'
import cors from 'cors'
import morgan from 'morgan'
import express from 'express'
import ExpressReactViews from 'express-react-views'
import devErrorHandler from 'errorhandler'
import WebpackDevServer from 'webpack-dev-server'
import { createRequestHandler } from 'npm-http-server'
import { staticAssets, assetsCompiler, createDevCompiler } from './AssetsUtils'
import { sendHomePage } from './MainController'
import { logStats } from './StatsUtils'
const serveHomePage = (req, res) =>
res.render('HomePage')
export const createRouter = (config = {}) => {
const router = express.Router()
export const createServer = (options = {}) => {
router.get('/', sendHomePage)
if (config.redisURL)
router.use(logStats(config.redisURL))
router.use(createRequestHandler(config))
return router
}
const errorHandler = (err, req, res, next) => {
res.status(500).send('<p>Internal Server Error</p>')
console.error(error.stack)
next(err)
}
export const createServer = (config) => {
const app = express()
app.disable('x-powered-by')
app.set('view engine', 'js')
app.set('views', path.resolve(__dirname, 'components'))
app.engine('js', ExpressReactViews.createEngine({
transformViews: false
}))
app.use(errorHandler)
app.use(cors())
app.use(express.static('public', { maxAge: 60000 }))
if (options.redisURL)
app.use(logStats(options.redisURL))
app.get('/', serveHomePage)
app.use(createRequestHandler(options))
app.use(express.static(config.publicDir, { maxAge: 60000 }))
app.use(staticAssets(config.statsFile))
app.use(createRouter(config))
return app
}
export const createDevServer = (config) => {
const webpackConfig = config.webpackConfig
const compiler = createDevCompiler(
webpackConfig,
`webpack-dev-server/client?http://localhost:${config.port}`
)
const server = new WebpackDevServer(compiler, {
// webpack-dev-middleware options.
publicPath: webpackConfig.output.publicPath,
quiet: false,
noInfo: false,
stats: {
// https://webpack.github.io/docs/node.js-api.html#stats-tojson
assets: true,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false
},
// webpack-dev-server options.
contentBase: false,
setup(app) {
// This runs before webpack-dev-middleware.
app.disable('x-powered-by')
app.use(morgan('dev'))
}
})
// This runs after webpack-dev-middleware.
server.use(devErrorHandler())
server.use(express.static(config.publicDir))
server.use(assetsCompiler(compiler))
server.use(createRouter(config))
return server
}

View File

@ -1,19 +1,31 @@
import { createServer } from './index'
import path from 'path'
import { createServer, createDevServer } from './index'
const port = process.env.PORT || 5000
const webpackConfig = require('../../webpack.config')
const statsFile = path.resolve(__dirname, '../../stats.json')
const publicDir = path.resolve(__dirname, '../../public')
const registryURL = process.env.REGISTRY_URL || 'https://registry.npmjs.org'
const bowerBundle = process.env.BOWER_BUNDLE || '/bower.zip'
const redirectTTL = process.env.REDIRECT_TTL || 500
const autoIndex = !process.env.DISABLE_INDEX
const redisURL = process.env.REDIS_URL
const server = createServer({
const serverConfig = {
port,
webpackConfig,
statsFile,
publicDir,
registryURL,
bowerBundle,
redirectTTL,
autoIndex,
redisURL
})
}
const server = process.env.NODE_ENV === 'production'
? createServer(serverConfig)
: createDevServer(serverConfig)
server.listen(port, () => {
console.log('Server started on port %s, Ctrl+C to quit', port)

View File

@ -7,20 +7,32 @@
"heroku-postbuild": "npm run build"
},
"dependencies": {
"autoprefixer": "^6.3.6",
"babel-cli": "^6.8.0",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-register": "^6.7.2",
"babel-preset-stage-1": "^6.5.0",
"cors": "^2.7.1",
"csso": "^2.0.0",
"css-loader": "^0.23.1",
"errorhandler": "^1.4.3",
"express": "^4.13.4",
"express-react-views": "^0.10.1",
"extract-text-webpack-plugin": "^1.0.1",
"invariant": "^2.2.1",
"morgan": "^1.7.0",
"npm-http-server": "^2.10.0",
"on-finished": "^2.3.0",
"postcss-loader": "^0.9.1",
"react": "^15.0.2",
"react-dom": "^15.0.2",
"redis": "^2.6.0-1",
"rimraf": "^2.5.2"
"rimraf": "^2.5.2",
"style-loader": "^0.13.1",
"webpack": "^1.13.0",
"webpack-dev-server": "^1.14.1"
},
"devDependencies": {
"babel-register": "^6.8.0"
},
"engines": {
"node": "5.8.x"
@ -28,7 +40,8 @@
"babel": {
"presets": [
"es2015",
"react"
"react",
"stage-1"
]
}
}

35
webpack.config.js Normal file
View File

@ -0,0 +1,35 @@
const path = require('path')
const webpack = require('webpack')
const autoprefixer = require('autoprefixer')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const AssetURLPrefix = '[hash:8]/'
module.exports = {
entry: {
home: path.resolve(__dirname, 'modules/client/home.js')
},
output: {
filename: `${AssetURLPrefix}[name].js`,
path: path.resolve(__dirname, 'public/__assets__'),
publicPath: '/__assets__/'
},
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css!postcss') }
]
},
plugins: [
new webpack.optimize.OccurrenceOrderPlugin(),
new ExtractTextPlugin(`${AssetURLPrefix}styles.css`),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
})
],
postcss: () => [ autoprefixer ]
}