Skip to content

Commit

Permalink
[minor] add unbundledJS option
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Jun 30, 2017
1 parent 62573ed commit 96cc052
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 38 deletions.
61 changes: 38 additions & 23 deletions packages/electrode-react-webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ a bootstrapping React application. With support for webpack dev server integrat

## Installing

```
npm install electrode-react-webapp --save
```bash
$ npm install electrode-react-webapp --save
```

## Usage
Expand Down Expand Up @@ -45,6 +45,11 @@ const config = {
"/{args*}": {
view: "index",
content: "<h1>Hello React!</h1>"
},
unbundledJS: {
enterHead: [
{src: "http://cdn.com/js/lib.js"}
]
}
}
}
Expand Down Expand Up @@ -80,23 +85,26 @@ The current defaults are:
What you can do with the options:
* `pageTitle` `(String)` The value to be shown in the browser's title bar
* `webpackDev` `(Boolean)` whether to use webpack-dev-server's URLs for retrieving CSS and JS bundles.
* `serverSideRendering` `(Boolean)` Toggle server-side rendering.
* `htmlFile` `(String)` Absolute or relative path to the application root html file.
It must contains the following placeholders:
- `{{PAGE_TITLE}}` page title.
- `{{WEBAPP_BUNDLES}}` injected `<script>` and `<link>` tags to load bundled JavaScript and Css
- `{{PREFETCH_BUNDLES}}` `<script>` tag containing code that will contains prefetched JavaScript code
- `{{SSR_CONTENT}}` injected content rendered on server side
* `paths` `(Object)` An object of key/value pairs specifying paths within your application with their view and (optionally) initial content for server-side render
- _path_ `(Object)`
- `view` `(String)` Name of the view to be used for this path **required**
- `content` Content to be rendered by the server when server-side rendering is used _optional_ [see details](#content-details)
* `devServer` `(Object)` Options for webpack's DevServer
- `host` `(String)` The host that webpack-dev-server runs on
- `port` `(String)` The port that webpack-dev-server runs on
* `prodBundleBase` `(String)` Base path to locate the JavaScript, CSS and manifest bundles. Defaults to "/js/". Should end with "/".
- `pageTitle` `(String)` The value to be shown in the browser's title bar
- `webpackDev` `(Boolean)` whether to use webpack-dev-server's URLs for retrieving CSS and JS bundles.
- `serverSideRendering` `(Boolean)` Toggle server-side rendering.
- `htmlFile` `(String)` Absolute or relative path to the application root html file.
It must contains the following placeholders:
- `{{PAGE_TITLE}}` page title.
- `{{WEBAPP_BUNDLES}}` injected `<script>` and `<link>` tags to load bundled JavaScript and Css
- `{{PREFETCH_BUNDLES}}` `<script>` tag containing code that will contains prefetched JavaScript code
- `{{SSR_CONTENT}}` injected content rendered on server side
- `paths` `(Object)` An object of key/value pairs specifying paths within your application with their view and (optionally) initial content for server-side render
- _path_ `(Object)`
- `view` `(String)` Name of the view to be used for this path **required**
- `content` Content to be rendered by the server when server-side rendering is used _optional_ [see details](#content-details)
- `unbundledJS` (Object) Specify JavaScript files to be loaded at an available extension point in the index template
- `enterHead` (Array) Array of script objects (`{ src: "path to file" }`) to be inserted as `<script>` tags in the document `head` before anything else. To load scripts asynchronously use `{ src: "...", async: true }` or `{ src: "...", defer: true }`
- `preBundle` (Array) Array of script objects (`{ src: "path to file" }`) to be inserted as `<script>` tags in the document `body` before the application's bundled JavaScript
- `devServer` `(Object)` Options for webpack's DevServer
- `host` `(String)` The host that webpack-dev-server runs on
- `port` `(String)` The port that webpack-dev-server runs on
- `prodBundleBase` `(String)` Base path to locate the JavaScript, CSS and manifest bundles. Defaults to "/js/". Should end with "/".
### Content details
Expand All @@ -117,10 +125,17 @@ function myContent() {
```
[npm-image]: https://badge.fury.io/js/electrode-react-webapp.svg
[npm-url]: https://npmjs.org/package/electrode-react-webapp
[daviddm-image]: https://david-dm.org/electrode-io/electrode/status.svg?path=packages/electrode-react-webapp
[daviddm-url]: https://david-dm.org/electrode-io/electrode?path=packages/electrode-react-webapp
[daviddm-dev-image]:https://david-dm.org/electrode-io/electrode/dev-status.svg?path=packages/electrode-react-webapp
[daviddm-dev-url]:https://david-dm.org/electrode-io/electrode?path=packages/electrode-react-webapp?type-dev
[npm-downloads-image]:https://img.shields.io/npm/dm/electrode-react-webapp.svg
[npm-downloads-url]:https://www.npmjs.com/package/electrode-react-webapp
[daviddm-dev-image]: https://david-dm.org/electrode-io/electrode/dev-status.svg?path=packages/electrode-react-webapp
[daviddm-dev-url]: https://david-dm.org/electrode-io/electrode?path=packages/electrode-react-webapp?type-dev
[npm-downloads-image]: https://img.shields.io/npm/dm/electrode-react-webapp.svg
[npm-downloads-url]: https://www.npmjs.com/package/electrode-react-webapp
41 changes: 41 additions & 0 deletions packages/electrode-react-webapp/lib/group-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use strict";

const _ = require("lodash");

function joinScripts(acc) {
if (acc.current) {
acc.scripts.push(
acc.src
? acc.current
: acc.current
.map(x => {
x = _.trim(x);
return x.endsWith(";") ? x : `${x};`;
})
.join("\n\n")
);
acc.current = undefined;
}
}

module.exports = function groupScripts(data) {
const output = data.filter(x => x).reduce((acc, x) => {
const update = src => {
if (acc.src !== src || !acc.current) {
joinScripts(acc);
acc.current = [x];
acc.src = src;
} else {
acc.current.push(x);
}
};

update(!!x.src);

return acc;
}, { src: false, scripts: [] });

joinScripts(output);

return output;
};
46 changes: 33 additions & 13 deletions packages/electrode-react-webapp/lib/react-webapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Promise = require("bluebird");
const fs = require("fs");
const Path = require("path");
const Helmet = require("react-helmet").Helmet;
const groupScripts = require("./group-scripts");

const CONTENT_MARKER = "{{SSR_CONTENT}}";
const HEADER_BUNDLE_MARKER = "{{WEBAPP_HEADER_BUNDLES}}";
Expand Down Expand Up @@ -178,26 +179,40 @@ function makeRouteHandler(routeOptions, userContent) {
});
};

const htmlifyScripts = scripts => {
return scripts
.map(
x =>
typeof x === "string"
? `<script>${x}</script>\n`
: x.map(n => `<script src="${n.src}"></script>`).join("\n")
)
.join("\n");
};

const makeHeaderBundles = helmet => {
const manifest = bundleManifest();
const manifestLink = manifest ? `<link rel="manifest" href="${manifest}" />` : "";
const manifestLink = manifest ? `<link rel="manifest" href="${manifest}" />\n` : "";
const css = bundleCss();
const cssLink = css && !criticalCSS
? `<link rel="stylesheet" href="${css}" />`
: "";
const cssLink = css && !criticalCSS ? `<link rel="stylesheet" href="${css}" />` : "";
const scriptsFromHelmet = ["link", "style", "script", "noscript"]
.map((tagName) => helmet[tagName].toString()).join("");
.map(tagName => helmet[tagName].toString())
.join("");

return `${manifestLink}${cssLink}${scriptsFromHelmet}`;
const htmlScripts = htmlifyScripts(groupScripts(routeOptions.unbundledJS.enterHead).scripts);
return `${manifestLink}${cssLink}${htmlScripts}\n${scriptsFromHelmet}`;
};

const makeBodyBundles = () => {
const js = bundleJs();
const css = bundleCss();
const cssLink = css && criticalCSS ? `<link rel="stylesheet" href="${css}" />` : "";
const jsLink = js ? `<script src="${js}"></script>` : "";
const jsLink = js ? { src: js } : "";

return `${cssLink}${jsLink}`;
const ins = routeOptions.unbundledJS.preBundle
.concat([jsLink])
.concat(routeOptions.unbundledJS.postBundle);
const htmlScripts = htmlifyScripts(groupScripts(ins).scripts);

return `${htmlScripts}`;
};

const emptyTitleRegex = /<title[^>]*><\/title>/;
Expand All @@ -221,7 +236,7 @@ function makeRouteHandler(routeOptions, userContent) {
case HEADER_BUNDLE_MARKER:
return makeHeaderBundles(helmet);
case BODY_BUNDLE_MARKER:
return makeBodyBundles(helmet);
return makeBodyBundles();
case PREFETCH_MARKER:
return `<script>${content.prefetch}</script>`;
case META_TAGS_MARKER:
Expand Down Expand Up @@ -257,6 +272,11 @@ const setupOptions = options => {
port: process.env.WEBPACK_DEV_PORT || "2992",
https: Boolean(process.env.WEBPACK_DEV_HTTPS)
},
unbundledJS: {
enterHead: [],
preBundle: [],
postBundle: []
},
paths: {},
stats: "dist/server/stats.json",
iconStats: "dist/server/iconstats.json",
Expand All @@ -269,7 +289,7 @@ const setupOptions = options => {
const chunkSelector = resolveChunkSelector(pluginOptions);
const devProtocol = process.env.WEBPACK_DEV_HTTPS ? "https://" : "http://";
const devBundleBase = `${devProtocol}${pluginOptions.devServer.host}:${pluginOptions.devServer
.port}/js/`; // eslint-disable-line max-len
.port}/js/`;
const statsPath = getStatsPath(pluginOptions.stats, pluginOptions.buildArtifacts);

return Promise.try(() => loadAssetsFromStats(statsPath)).then(assets => {
Expand All @@ -287,7 +307,7 @@ const resolveContent = content => {
if (!_.isString(content) && !_.isFunction(content) && content.module) {
const module = content.module.startsWith(".")
? Path.join(process.cwd(), content.module)
: content.module; // eslint-disable-line
: content.module;
return require(module); // eslint-disable-line
}

Expand Down
11 changes: 11 additions & 0 deletions packages/electrode-react-webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,16 @@
"superagent": "^1.6.1",
"uglify-js": "^2.6.2",
"xclap": "^0.2.0"
},
"nyc": {
"all": true,
"check-coverage": true,
"statements": 94.02,
"branches": 76.11,
"functions": 97.96,
"lines": 93.82,
"cache": true,
"reporter": ["lcov", "text", "text-summary"],
"exclude": ["coverage", "*clap.js", "gulpfile.js", "dist", "test", "lib/express", "lib/koa"]
}
}
68 changes: 68 additions & 0 deletions packages/electrode-react-webapp/test/spec/group-scripts.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use strict";

const data = [
"a",
"b",
"c",
{ src: 1 },
{ src: 5, defer: 1 },
{ src: 2 },
"d",
"e",
"f",
"g",
{ src: 3 },
"h",
"i",
"j",
{
src: 4,
async: 1
},
{ src: 10 },
"k",
"l",
"m"
];

const groupScripts = require("../../lib/group-scripts");
const chai = require("chai");

describe("group-scripts", function() {
it("should group scripts", () => {
const expected = [
"a;\n\nb;\n\nc;",
[
{
src: 1
},
{
src: 5,
defer: 1
},
{
src: 2
}
],
"d;\n\ne;\n\nf;\n\ng;",
[
{
src: 3
}
],
"h;\n\ni;\n\nj;",
[
{
src: 4,
async: 1
},
{
src: 10
}
],
"k;\n\nl;\n\nm;"
];
const result = groupScripts(data);
chai.expect(result.scripts).to.deep.equal(expected);
});
});
32 changes: 30 additions & 2 deletions packages/electrode-react-webapp/test/spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ describe("Test electrode-react-webapp", () => {
.then(res => {
expect(res.statusCode).to.equal(200);
expect(res.result).to.contain(
'<script src="http://awesome-cdn.com/multi/bar.bundle.f07a873ce87fc904a6a5.js"'
`<script src="http://awesome-cdn.com/multi/bar.bundle.f07a873ce87fc904a6a5.js"`
);
expect(res.result).to.contain(
'<link rel="stylesheet" href="http://awesome-cdn.com/multi/bar.style.f07a873ce87fc904a6a5.css"'
`<link rel="stylesheet" href="http://awesome-cdn.com/multi/bar.style.f07a873ce87fc904a6a5.css"`
);
stopServer(server);
})
Expand Down Expand Up @@ -357,4 +357,32 @@ describe("Test electrode-react-webapp", () => {
});
});
});

it("should inject from unbundledJS enterHead", () => {
configOptions.prodBundleBase = "http://awesome-cdn.com/myapp/";
configOptions.stats = "test/data/stats-test-one-bundle.json";

config.plugins["./lib/hapi/index"].options.unbundledJS = {
enterHead: [`console.log("test-unbundledJS-enterHead")`, { src: "test-enter-head.js" }]
};
return electrodeServer(config).then(server => {
return server
.inject({
method: "GET",
url: "/"
})
.then(res => {
expect(res.statusCode).to.equal(200);
expect(res.result).to.contain(
'<script src="http://awesome-cdn.com/myapp/bundle.f07a873ce87fc904a6a5.js">'
);
expect(res.result).to.contain("test-unbundledJS-enterHead");
stopServer(server);
})
.catch(err => {
stopServer(server);
throw err;
});
});
});
});

0 comments on commit 96cc052

Please sign in to comment.