Commit da9c1d78 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Update devserver to use express, Mustache and serve PDFJS-enabled PDFs

- Rewrite dev server as `express` app
- Add `mustache-express` as template engine
- Restructure document directories; add PDFs
- Add `pdfjs-init.js` script for embedding PDFJS with H client
- Update linting config to ignore static scripts
parent bbd365ab
......@@ -2,3 +2,4 @@ build/**
**/vendor/**/*.js
**/coverage/**
docs/_build/*
dev-server/static/**/*.js
build/
/build/
node_modules/
coverage/
docs/_build/
......
......@@ -3,3 +3,4 @@
build/
coverage/
docs/
dev-server/static/*
\ No newline at end of file
......@@ -2,9 +2,14 @@ This directory contains the Hypothesis client's development server that is
started by `make dev`. It hosts the assets that make up the Hypothesis client
from the `build/` directory as well as content for testing the Hypothesis client.
Test documents in the `documents/` directory are available at
Test documents in the `documents/html` directory are available at
`localhost:<port>/document/<filename-without-extension>`,
e.g. `documents/foo.mustache` would be available at `localhost:<port>/document/foo`.
PDFs in the `documents/pdf` directory are available at
`localhost:port/pdf/<filename-without-extension>` and will be served with the
PDF JS viewer as well as the embedded client.
Mustache-templated HTML documents may use `{{{ hypothesisScript }}}` to inject
the client application as configured by `templates/client-config.js.mustache`.
PDFs may be dropped in as-is.
/* global __dirname */
'use strict';
const fs = require('fs');
const log = require('fancy-log');
const urlParser = require('url');
const Mustache = require('mustache');
const { createServer, useSsl } = require('./create-server');
const DOCUMENT_PATH = `${__dirname}/documents/`;
const TEMPLATE_PATH = `${__dirname}/templates/`;
const DOCUMENT_PATTERN = /\.mustache/;
/**
* Generate `<script>` content for client configuration and injection
*
* @param {string} clientUrl
* @return {string}
*/
function renderConfig(clientUrl) {
const scriptTemplate = fs.readFileSync(
`${TEMPLATE_PATH}client-config.js.mustache`,
'utf-8'
);
return Mustache.render(scriptTemplate, { clientUrl });
}
/**
* Read in the file at `filename` and render it as a template, injecting
* script source for the embedded client.
*
* @param {string} filename
* @param {string} clientUrl
* @param [{Object}] context - optional extra view context
* @return {string} Rendered HTML template with injected script
*/
function injectClientScript(filename, clientUrl, context = {}) {
const documentTemplate = fs.readFileSync(filename, 'utf-8');
const scriptContent = renderConfig(clientUrl);
context = { ...context, hypothesisScript: scriptContent };
return Mustache.render(documentTemplate, context);
}
/**
* Provide a "route" for all HTML documents in the test-documents directory
*
* @return {Object<string, string>} - Routes, mapping route (path) to
* filepath of HTML document
*/
function buildDocumentRoutes() {
const documentRoutes = {};
const documentPaths = fs
.readdirSync(DOCUMENT_PATH)
.filter(filename => filename.match(DOCUMENT_PATTERN));
documentPaths.forEach(filename => {
const shortName = filename.replace(DOCUMENT_PATTERN, '');
const routePath = shortName === 'index' ? '/' : `/document/${shortName}`;
documentRoutes[routePath] = `${DOCUMENT_PATH}${filename}`;
});
return documentRoutes;
}
/**
* @typedef Config
* @property {string} clientUrl - The URL of the client's boot script
*/
/**
* An HTTP server which serves a test page with the development client embedded.
*
* @param {number} port - The port that the test server should listen on.
* @param {Config} config - Config for the server
*
* @constructor
*/
function DevServer(port, config) {
const documentRoutes = buildDocumentRoutes();
function listen() {
const app = function (req, response) {
const url = urlParser.parse(req.url);
let content;
if (documentRoutes[url.pathname]) {
content = injectClientScript(
documentRoutes[url.pathname],
config.clientUrl
);
} else {
content = injectClientScript(
`${TEMPLATE_PATH}404.mustache`,
config.clientUrl
);
}
response.end(content);
};
const server = createServer(app);
server.listen(port, function (err) {
if (err) {
log('Setting up dev server failed', err);
}
const scheme = useSsl ? 'https' : 'http';
log(`Dev server listening at ${scheme}://localhost:${port}/`);
});
}
listen();
}
module.exports = DevServer;
'use strict';
/* eslint-env node */
const fs = require('fs');
const path = require('path');
const express = require('express');
const log = require('fancy-log');
const mustacheExpress = require('mustache-express');
const Mustache = require('mustache');
const { createServer, useSsl } = require('./create-server');
const HTML_PATH = `${__dirname}/documents/html/`;
const PDF_PATH = `${__dirname}/documents/pdf/`;
const TEMPLATE_PATH = `${__dirname}/templates/`;
/**
* @typedef Config
* @property {string} clientUrl - The URL of the client's boot script
*/
/**
* Generate `<script>` content for client configuration and injection
*
* @param {string} clientUrl
* @return {string}
*/
function renderConfig(clientUrl) {
const scriptTemplate = fs.readFileSync(
`${TEMPLATE_PATH}client-config.js.mustache`,
'utf-8'
);
return Mustache.render(scriptTemplate, { clientUrl });
}
/**
* Build context for rendering templates in the defined views directory. This
* is dead simple at present but could be extended as needs grow.
*
* @param {Config} config
*/
function templateContext(config) {
return {
hypothesisScript: renderConfig(config.clientUrl),
};
}
/**
* An HTTP server which serves test documents with the development client embedded.
*
* @param {number} port - The port that the test server should listen on.
* @param {Config} config - Config for the server
*/
function serveDev(port, config) {
const app = express();
app.engine('mustache', mustacheExpress());
app.set('view engine', 'mustache');
app.set('views', [HTML_PATH, path.join(__dirname, '/templates')]);
app.use(express.static(path.join(__dirname, 'static')));
// Serve static PDF files out of the PDF directory, but serve under
// `/pdf-source/` — these are needed by PDF JS viewer
app.use('/pdf-source', express.static(PDF_PATH));
// Enable CORS for assets so that cross-origin font loading works.
app.use(function (req, res, next) {
res.append('Access-Control-Allow-Origin', '*');
res.append('Access-Control-Allow-Methods', 'GET');
next();
});
// Landing page
app.get('/', (req, res) => {
res.render('index', templateContext(config));
});
// Serve HTML documents with injected client script
app.get('/document/:document', (req, res, next) => {
if (fs.existsSync(`${HTML_PATH}${req.params.document}.mustache`)) {
res.render(req.params.document, templateContext(config));
} else {
next();
}
});
// Serve PDF documents with PDFJS viewer and client script
app.get('/pdf/:pdf', (req, res, next) => {
if (fs.existsSync(`${PDF_PATH}${req.params.pdf}.pdf`)) {
const fullUrl = `${req.protocol}://${req.hostname}:${port}${req.originalUrl}`;
res.render('pdfjs-viewer', {
documentUrl: fullUrl, // The URL that annotations are associated with
url: `/pdf-source/${req.params.pdf}.pdf`, // The URL for the PDF source file
clientUrl: config.clientUrl,
});
} else {
next();
}
});
// Nothing else matches: this is a 404
app.use((req, res) => {
res.render('404', templateContext(config));
});
createServer(app).listen(port, () => {
const scheme = useSsl ? 'https' : 'http';
log(`Dev web server started at ${scheme}://localhost:${port}/`);
});
}
module.exports = serveDev;
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8 9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
\ No newline at end of file
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8 9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
\ No newline at end of file
This diff is collapsed.
'use strict';
// Note: This file is not transpiled.
// Listen for `webviewerloaded` event to configure the viewer after its files
// have been loaded but before it is initialized.
document.addEventListener('webviewerloaded', () => {
const appOptions = window.PDFViewerApplicationOptions;
const app = window.PDFViewerApplication;
// Ensure that PDF.js viewer events such as "documentloaded" are dispatched
// to the DOM. The client relies on this.
appOptions.set('eventBusDispatchToDOM', true);
// Disable preferences support, as otherwise this will result in `eventBusDispatchToDOM`
// being overridden with the default value of `false`.
appOptions.set('disablePreferences', true);
// Prevent loading of default viewer PDF.
appOptions.set('defaultUrl', '');
// Read configuration rendered into template as global vars.
const documentUrl = window.DOCUMENT_URL;
const url = window.PDF_URL;
const clientEmbedUrl = window.CLIENT_URL;
// Wait for the PDF viewer to be fully initialized and then load the Hypothesis client.
//
// This is required because the client currently assumes that `PDFViewerApplication`
// is fully initialized when it loads. Note that "fully initialized" only means
// that the PDF viewer application's components have been initialized. The
// PDF itself will still be loading, and the client will wait for that to
// complete before fetching annotations.
//
const pdfjsInitialized = new Promise(resolve => {
// Poll `app.initialized` as there doesn't appear to be an event that
// we can listen to.
const timer = setInterval(function () {
if (app.initialized) {
clearTimeout(timer);
resolve();
}
}, 20);
});
pdfjsInitialized.then(() => {
// Prevent PDF.js' `Promise` polyfill, if it was used, from being
// overwritten by the one that ships with Hypothesis (both from core-js).
//
// See https://github.com/hypothesis/via/issues/81#issuecomment-531121534
if (
typeof Promise === 'function' &&
typeof PromiseRejectionEvent === 'undefined'
) {
window.PromiseRejectionEvent = function FakePromiseRejectionEvent() {
// core-js doesn't actually use this, it just tests for `typeof PromiseRejectionEvent`
console.warn('Tried to construct fake `PromiseRejectionEvent`');
};
}
// Load the Hypothesis client.
const embedScript = document.createElement('script');
embedScript.src = clientEmbedUrl;
document.body.appendChild(embedScript);
// Load the PDF specified in the URL.
//
// This is done after the viewer components are initialized to avoid some
// race conditions in `PDFViewerApplication` if the PDF finishes loading
// (eg. from the HTTP cache) before the viewer is fully initialized.
//
// See https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions#can-i-specify-a-different-pdf-in-the-default-viewer
// and https://github.com/mozilla/pdf.js/issues/10435#issuecomment-452706770
app.open({
// Load PDF through Via to work around CORS restrictions.
url: url,
// Make sure `PDFViewerApplication.url` returns the original URL, as this
// is the URL associated with annotations.
originalUrl: documentUrl,
});
});
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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