Commit 6abe62d6 authored by Robert Knight's avatar Robert Knight

Add a new entry point script for the client application

Add a new bundle (build/boot.js) which serves as the main entry point
for the client. This entry point will be used in two places:

 1. When embedded in a host web page it will add the bundle for the
    annotation layer (injector.bundle.js) to the page plus supporting
    assets.

 2. When included as the sole asset in the sidebar's HTML page
    it will add the bundle for the sidebar app (app.bundle.js) to the
    page plus supporting assets.

Having both entry points be a single script means that when it is
referenced in app.html, the browser will already have it cached locally,
saving a roundtrip.

In this commit the script just adds all <script> and <link> tags for the
annotation layer or sidebar to the document. In future it can be made
smarter - eg. by preloading sidebar assets when loaded in the host page
and not loading polyfills in newer browsers.
parent 29af928e
......@@ -14,6 +14,8 @@ var gulpIf = require('gulp-if');
var gulpUtil = require('gulp-util');
var postcss = require('gulp-postcss');
var postcssURL = require('postcss-url');
var replace = require('gulp-replace');
var rename = require('gulp-rename');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var through = require('through2');
......@@ -94,6 +96,12 @@ var appBundleBaseConfig = {
};
var appBundles = [{
// The entry point for both the Hypothesis client and the sidebar
// application. This is responsible for loading the rest of the assets needed
// by the client.
name: 'boot',
entry: './src/boot/index',
},{
// The sidebar application for displaying and editing annotations.
name: 'app',
transforms: ['coffee'],
......@@ -230,6 +238,19 @@ function triggerLiveReload(changedFiles) {
debouncedLiveReload();
}
/**
* Generates the `build/boot.js` script which serves as the entry point for
* the Hypothesis client.
*
* @param {Object} manifest - Manifest mapping asset paths to cache-busted URLs
*/
function generateBootScript(manifest) {
gulp.src('build/scripts/boot.bundle.js')
.pipe(replace('__MANIFEST__', JSON.stringify(manifest)))
.pipe(rename('boot.js'))
.pipe(gulp.dest('build/'));
}
/**
* Generate a JSON manifest mapping file paths to
* URLs containing cache-busting query string parameters.
......@@ -240,11 +261,15 @@ function generateManifest() {
.pipe(through.obj(function (file, enc, callback) {
gulpUtil.log('Updated asset manifest');
// Trigger a reload of the client in the dev server at localhost:3000
var newManifest = JSON.parse(file.contents.toString());
var changed = changedAssets(prevManifest, newManifest);
prevManifest = newManifest;
triggerLiveReload(changed);
// Expand template vars in boot script bundle
generateBootScript(newManifest);
this.push(file);
callback();
}))
......
......@@ -728,6 +728,12 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.4.0.tgz",
"dev": true
},
"binaryextensions": {
"version": "1.0.1",
"from": "binaryextensions@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz",
"dev": true
},
"blob": {
"version": "0.0.4",
"from": "blob@0.0.4",
......@@ -2558,6 +2564,18 @@
"resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-6.1.0.tgz",
"dev": true
},
"gulp-rename": {
"version": "1.2.2",
"from": "gulp-rename@latest",
"resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.2.2.tgz",
"dev": true
},
"gulp-replace": {
"version": "0.5.4",
"from": "gulp-replace@latest",
"resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz",
"dev": true
},
"gulp-sass": {
"version": "2.2.0",
"from": "gulp-sass@>=2.2.0 <3.0.0",
......@@ -3169,6 +3187,12 @@
}
}
},
"istextorbinary": {
"version": "1.0.2",
"from": "istextorbinary@1.0.2",
"resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz",
"dev": true
},
"jade": {
"version": "0.26.3",
"from": "jade@0.26.3",
......@@ -4966,6 +4990,12 @@
}
}
},
"replacestream": {
"version": "4.0.2",
"from": "replacestream@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.2.tgz",
"dev": true
},
"request": {
"version": "2.76.0",
"from": "request@latest",
......@@ -5605,6 +5635,12 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"dev": true
},
"textextensions": {
"version": "1.0.2",
"from": "textextensions@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz",
"dev": true
},
"throttleit": {
"version": "1.0.0",
"from": "throttleit@>=1.0.0 <2.0.0",
......
......@@ -49,6 +49,8 @@
"gulp-changed": "^1.3.0",
"gulp-if": "^2.0.0",
"gulp-postcss": "^6.1.0",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.5.4",
"gulp-sass": "^2.2.0",
"gulp-sourcemaps": "^1.6.0",
"gulp-util": "^3.0.7",
......
'use strict';
function injectStylesheet(doc, href) {
var link = doc.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
doc.head.appendChild(link);
}
function injectScript(doc, src) {
var script = doc.createElement('script');
script.type = 'text/javascript';
script.src = src;
// Set 'async' to false to maintain execution order of scripts.
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
script.async = false;
doc.head.appendChild(script);
}
function injectAssets(doc, config, assets) {
assets.forEach(function (path) {
var url = config.assetRoot + config.manifest[path];
if (url.match(/\.css/)) {
injectStylesheet(doc, url);
} else {
injectScript(doc, url);
}
});
}
/**
* Bootstrap the Hypothesis client.
*
* This triggers loading of the necessary resources for the client
*/
function bootHypothesisClient(doc, config) {
// Detect presence of Hypothesis in the page
var appLinkEl = doc.querySelector('link[type="application/annotator+html"]');
if (appLinkEl) {
return;
}
// Register the URL of the sidebar app which the Hypothesis client should load.
// The <link> tag is also used by browser extensions etc. to detect the
// presence of the Hypothesis client on the page.
var baseUrl = doc.createElement('link');
baseUrl.rel = 'sidebar';
baseUrl.href = config.appHtmlUrl;
baseUrl.type = 'application/annotator+html';
doc.head.appendChild(baseUrl);
injectAssets(doc, config, [
// Vendor code and polyfills
'scripts/polyfills.bundle.js',
'scripts/jquery.bundle.js',
// Main entry point for the client
'scripts/injector.bundle.js',
'styles/icomoon.css',
'styles/inject.css',
'styles/pdfjs-overrides.css',
]);
}
/**
* Bootstrap the sidebar application which displays annotations.
*/
function bootSidebarApp(doc, config) {
injectAssets(doc, config, [
// Vendor code and polyfills required by app.bundle.js
'scripts/raven.bundle.js',
'scripts/angular.bundle.js',
'scripts/katex.bundle.js',
'scripts/showdown.bundle.js',
'scripts/polyfills.bundle.js',
'scripts/unorm.bundle.js',
// The sidebar app
'scripts/app.bundle.js',
'styles/angular-csp.css',
'styles/angular-toastr.css',
'styles/icomoon.css',
'styles/katex.min.css',
'styles/app.css',
]);
}
function boot(document_, config) {
if (document_.querySelector('hypothesis-app')) {
bootSidebarApp(document_, config);
} else {
bootHypothesisClient(document_, config);
}
}
module.exports = boot;
'use strict';
// This is the main entry point for the Hypothesis client in the host page
// and the sidebar application.
/* global __MANIFEST__ */
var boot = require('./boot');
// The HYP* properties are set (if at all) by the service which is serving the
// Hypothesis client to tell it where to load the sidebar and assets from.
var appHtmlUrl = window.__HYP_APP_HTML_URL__ || 'https://hypothes.is/app.html';
var assetRoot = window.__HYP_CLIENT_ASSET_ROOT__ || 'https://hypothes.is/assets/client/';
boot(document, {
appHtmlUrl: appHtmlUrl,
assetRoot: assetRoot,
manifest: __MANIFEST__, // Replaced by build script
});
'use strict';
var boot = require('../boot');
describe('bootstrap', function () {
var iframe;
beforeEach(function () {
iframe = document.createElement('iframe');
document.body.appendChild(iframe);
});
afterEach(function () {
iframe.remove();
});
function runBoot() {
var assetNames = [
// Annotation layer
'scripts/polyfills.bundle.js',
'scripts/jquery.bundle.js',
'scripts/injector.bundle.js',
'styles/icomoon.css',
'styles/inject.css',
'styles/pdfjs-overrides.css',
// Sidebar app
'scripts/raven.bundle.js',
'scripts/angular.bundle.js',
'scripts/katex.bundle.js',
'scripts/showdown.bundle.js',
'scripts/polyfills.bundle.js',
'scripts/unorm.bundle.js',
'scripts/app.bundle.js',
'styles/angular-csp.css',
'styles/angular-toastr.css',
'styles/icomoon.css',
'styles/katex.min.css',
'styles/app.css',
];
var manifest = assetNames.reduce(function (manifest, path) {
var url = path.replace(/\.([a-z]+)$/, '.1234.$1');
manifest[path] = url;
return manifest;
}, {});
boot(iframe.contentDocument, {
appHtmlUrl: 'https://marginal.ly/app.html',
assetRoot: 'https://marginal.ly/client/',
manifest: manifest,
});
}
function findAssets(doc_) {
var scripts = Array.from(doc_.querySelectorAll('script')).map(function (el) {
return el.src;
});
var styles = Array.from(doc_.querySelectorAll('link[rel="stylesheet"]'))
.map(function (el) {
return el.href;
});
return scripts.concat(styles).sort();
}
context('in the host page', function () {
it('loads assets for the annotation layer', function () {
runBoot();
var expectedAssets = [
'scripts/injector.bundle.1234.js',
'scripts/jquery.bundle.1234.js',
'scripts/polyfills.bundle.1234.js',
'styles/icomoon.1234.css',
'styles/inject.1234.css',
'styles/pdfjs-overrides.1234.css',
].map(function (url) {
return 'https://marginal.ly/client/' + url;
});
assert.deepEqual(findAssets(iframe.contentDocument), expectedAssets);
});
it('creates the link to the sidebar iframe', function () {
runBoot();
var sidebarAppLink = iframe.contentDocument
.querySelector('link[type="application/annotator+html"]');
assert.ok(sidebarAppLink);
assert.equal(sidebarAppLink.href, 'https://marginal.ly/app.html');
});
it('does nothing if Hypothesis is already loaded in the document', function () {
var link = iframe.contentDocument.createElement('link');
link.type = 'application/annotator+html';
iframe.contentDocument.head.appendChild(link);
runBoot();
assert.deepEqual(findAssets(iframe.contentDocument), []);
});
});
context('in the sidebar application', function () {
var appRootElement;
beforeEach(function () {
appRootElement = iframe.contentDocument.createElement('hypothesis-app');
iframe.contentDocument.body.appendChild(appRootElement);
});
afterEach(function () {
appRootElement.remove();
});
it('loads assets for the sidebar application', function () {
runBoot();
var expectedAssets = [
'scripts/angular.bundle.1234.js',
'scripts/app.bundle.1234.js',
'scripts/katex.bundle.1234.js',
'scripts/polyfills.bundle.1234.js',
'scripts/raven.bundle.1234.js',
'scripts/showdown.bundle.1234.js',
'scripts/unorm.bundle.1234.js',
'styles/angular-csp.1234.css',
'styles/angular-toastr.1234.css',
'styles/app.1234.css',
'styles/icomoon.1234.css',
'styles/katex.min.1234.css',
].map(function (url) {
return 'https://marginal.ly/client/' + url;
});
assert.deepEqual(findAssets(iframe.contentDocument), expectedAssets);
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment