Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[minor] add unbundledJS option #448

Merged
merged 2 commits into from
Jun 30, 2017
Merged
Changes from 1 commit
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
Prev Previous commit
[minor] add unbundledJS option
  • Loading branch information
jchip committed Jun 30, 2017
commit 470346e28ba3f301781d838797a62912b9b79e95
61 changes: 38 additions & 23 deletions packages/electrode-react-webapp/README.md
Original file line number Diff line number Diff line change
@@ -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
@@ -45,6 +45,11 @@ const config = {
"/{args*}": {
view: "index",
content: "<h1>Hello React!</h1>"
},
unbundledJS: {
enterHead: [
{src: "http://cdn.com/js/lib.js"}
]
}
}
}
@@ -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

@@ -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
1 change: 0 additions & 1 deletion packages/electrode-react-webapp/lib/express/index.js
Original file line number Diff line number Diff line change
@@ -28,7 +28,6 @@ const registerRoutes = (app, options, next) => {
method = "ALL";
}
app[method.toLowerCase()](path, (request, response) => {
//eslint-disable-line
const handleStatus = data => {
const status = data.status;
if (status === HTTP_REDIRECT) {
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
@@ -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}}";
@@ -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>/;
@@ -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:
@@ -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",
@@ -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 => {
@@ -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
}

11 changes: 11 additions & 0 deletions packages/electrode-react-webapp/package.json
Original file line number Diff line number Diff line change
@@ -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
@@ -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);
})
@@ -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;
});
});
});
});