Commit be0d5b0c authored by Robert Knight's avatar Robert Knight

Convert boot script modules to TypeScript

parent 45fe9d7f
...@@ -36,7 +36,7 @@ const assetRoot = isProd ...@@ -36,7 +36,7 @@ const assetRoot = isProd
: `${localhost}:3001/hypothesis/${version}/`; : `${localhost}:3001/hypothesis/${version}/`;
export default { export default {
input: 'src/boot/index.js', input: 'src/boot/index.ts',
output: { output: {
file: 'build/boot.js', file: 'build/boot.js',
......
/** export type SidebarAppConfig = {
* @typedef SidebarAppConfig /** The root URL to which URLs in `manifest` are relative. */
* @prop {string} assetRoot - The root URL to which URLs in `manifest` are relative assetRoot: string;
* @prop {Record<string,string>} manifest -
* A mapping from canonical asset path to cache-busted asset path /** A mapping from canonical asset path to cache-busted asset path. */
* @prop {string} apiUrl manifest: Record<string, string>;
*/ apiUrl: string;
};
/**
* @typedef AnnotatorConfig export type AnnotatorConfig = {
* @prop {string} assetRoot - The root URL to which URLs in `manifest` are relative /** The root URL to which URLs in `manifest` are relative. */
* @prop {string} notebookAppUrl - The URL of the sidebar's notebook assetRoot: string;
* @prop {string} profileAppUrl - The URL of the sidebar's user profile view /** The URL of the sidebar's notebook. */
* @prop {string} sidebarAppUrl - The URL of the sidebar's HTML page notebookAppUrl: string;
* @prop {Record<string,string>} manifest - /** The URL of the sidebar's user profile view. */
* A mapping from canonical asset path to cache-busted asset path profileAppUrl: string;
*/ /** The URL of the sidebar's HTML page. */
sidebarAppUrl: string;
/** /** A mapping from canonical asset path to cache-busted asset path. */
* @typedef {Window & { PDFViewerApplication?: object }} MaybePDFWindow manifest: Record<string, string>;
*/ };
type MaybePDFWindow = Window & { PDFViewerApplication?: object };
/** /**
* Mark an element as having been added by the boot script. * Mark an element as having been added by the boot script.
* *
* This marker is later used to know which elements to remove when unloading * This marker is later used to know which elements to remove when unloading
* the client. * the client.
*
* @param {HTMLElement} el
*/ */
function tagElement(el) { function tagElement(el: HTMLElement) {
el.setAttribute('data-hypothesis-asset', ''); el.setAttribute('data-hypothesis-asset', '');
} }
/** function injectStylesheet(doc: Document, href: string) {
* @param {Document} doc
* @param {string} href
*/
function injectStylesheet(doc, href) {
const link = doc.createElement('link'); const link = doc.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.type = 'text/css'; link.type = 'text/css';
...@@ -46,14 +42,19 @@ function injectStylesheet(doc, href) { ...@@ -46,14 +42,19 @@ function injectStylesheet(doc, href) {
doc.head.appendChild(link); doc.head.appendChild(link);
} }
/** function injectScript(
* @param {Document} doc doc: Document,
* @param {string} src - The script URL src: string,
* @param {object} options {
* @param {boolean} [options.esModule] - Whether to load the script as an ES module esModule = true,
* @param {boolean} [options.forceReload] - Whether to force re-evaluation of an ES module script forceReload = false,
*/ }: {
function injectScript(doc, src, { esModule = true, forceReload = false } = {}) { /** Whether to load the script as an ES module. */
esModule?: boolean;
/** Whether to force re-evaluation of an ES module script. */
forceReload?: boolean;
} = {},
) {
const script = doc.createElement('script'); const script = doc.createElement('script');
if (esModule) { if (esModule) {
...@@ -79,13 +80,12 @@ function injectScript(doc, src, { esModule = true, forceReload = false } = {}) { ...@@ -79,13 +80,12 @@ function injectScript(doc, src, { esModule = true, forceReload = false } = {}) {
doc.head.appendChild(script); doc.head.appendChild(script);
} }
/** function injectLink(
* @param {Document} doc doc: Document,
* @param {string} rel rel: string,
* @param {'html'|'javascript'} type type: 'html' | 'javascript',
* @param {string} url url: string,
*/ ) {
function injectLink(doc, rel, type, url) {
const link = doc.createElement('link'); const link = doc.createElement('link');
link.rel = rel; link.rel = rel;
link.href = url; link.href = url;
...@@ -100,12 +100,8 @@ function injectLink(doc, rel, type, url) { ...@@ -100,12 +100,8 @@ function injectLink(doc, rel, type, url) {
* *
* This can be used to preload an API request or other resource which we know * This can be used to preload an API request or other resource which we know
* that the client will load. * that the client will load.
*
* @param {Document} doc
* @param {string} type - Type of resource
* @param {string} url
*/ */
function preloadURL(doc, type, url) { function preloadURL(doc: Document, type: string, url: string) {
const link = doc.createElement('link'); const link = doc.createElement('link');
link.rel = 'preload'; link.rel = 'preload';
link.as = type; link.as = type;
...@@ -122,11 +118,7 @@ function preloadURL(doc, type, url) { ...@@ -122,11 +118,7 @@ function preloadURL(doc, type, url) {
doc.head.appendChild(link); doc.head.appendChild(link);
} }
/** function assetURL(config: SidebarAppConfig | AnnotatorConfig, path: string) {
* @param {SidebarAppConfig|AnnotatorConfig} config
* @param {string} path
*/
function assetURL(config, path) {
return config.assetRoot + 'build/' + config.manifest[path]; return config.assetRoot + 'build/' + config.manifest[path];
} }
...@@ -136,11 +128,8 @@ function assetURL(config, path) { ...@@ -136,11 +128,8 @@ function assetURL(config, path) {
* This triggers loading of the necessary resources for the client in a host * This triggers loading of the necessary resources for the client in a host
* or guest frame. We could in future simplify booting in guest-only frames * or guest frame. We could in future simplify booting in guest-only frames
* by omitting resources that are only needed in the host frame. * by omitting resources that are only needed in the host frame.
*
* @param {Document} doc
* @param {AnnotatorConfig} config
*/ */
export function bootHypothesisClient(doc, config) { export function bootHypothesisClient(doc: Document, config: AnnotatorConfig) {
// Detect presence of Hypothesis in the page // Detect presence of Hypothesis in the page
const appLinkEl = doc.querySelector( const appLinkEl = doc.querySelector(
'link[type="application/annotator+html"]', 'link[type="application/annotator+html"]',
...@@ -173,19 +162,17 @@ export function bootHypothesisClient(doc, config) { ...@@ -173,19 +162,17 @@ export function bootHypothesisClient(doc, config) {
); );
const scripts = ['scripts/annotator.bundle.js']; const scripts = ['scripts/annotator.bundle.js'];
for (let path of scripts) { for (const path of scripts) {
const url = assetURL(config, path); const url = assetURL(config, path);
injectScript(doc, url, { esModule: false }); injectScript(doc, url, { esModule: false });
} }
const styles = []; const styles = [];
if ( if ((window as MaybePDFWindow).PDFViewerApplication !== undefined) {
/** @type {MaybePDFWindow} */ (window).PDFViewerApplication !== undefined
) {
styles.push('styles/pdfjs-overrides.css'); styles.push('styles/pdfjs-overrides.css');
} }
styles.push('styles/highlights.css'); styles.push('styles/highlights.css');
for (let path of styles) { for (const path of styles) {
const url = assetURL(config, path); const url = assetURL(config, path);
injectStylesheet(doc, url); injectStylesheet(doc, url);
} }
...@@ -193,23 +180,20 @@ export function bootHypothesisClient(doc, config) { ...@@ -193,23 +180,20 @@ export function bootHypothesisClient(doc, config) {
/** /**
* Bootstrap the sidebar application which displays annotations. * Bootstrap the sidebar application which displays annotations.
*
* @param {Document} doc
* @param {SidebarAppConfig} config
*/ */
export function bootSidebarApp(doc, config) { export function bootSidebarApp(doc: Document, config: SidebarAppConfig) {
// Preload `/api/` and `/api/links` API responses. // Preload `/api/` and `/api/links` API responses.
preloadURL(doc, 'fetch', config.apiUrl); preloadURL(doc, 'fetch', config.apiUrl);
preloadURL(doc, 'fetch', config.apiUrl + 'links'); preloadURL(doc, 'fetch', config.apiUrl + 'links');
const scripts = ['scripts/sidebar.bundle.js']; const scripts = ['scripts/sidebar.bundle.js'];
for (let path of scripts) { for (const path of scripts) {
const url = assetURL(config, path); const url = assetURL(config, path);
injectScript(doc, url, { esModule: true }); injectScript(doc, url, { esModule: true });
} }
const styles = ['styles/katex.min.css', 'styles/sidebar.css']; const styles = ['styles/katex.min.css', 'styles/sidebar.css'];
for (let path of styles) { for (const path of styles) {
const url = assetURL(config, path); const url = assetURL(config, path);
injectStylesheet(doc, url); injectStylesheet(doc, url);
} }
......
...@@ -5,10 +5,8 @@ ...@@ -5,10 +5,8 @@
* We use feature tests to try to avoid false negatives, accepting some risk of * We use feature tests to try to avoid false negatives, accepting some risk of
* false positives due to the host page having loaded polyfills for APIs in order * false positives due to the host page having loaded polyfills for APIs in order
* to support older browsers. * to support older browsers.
*
* @return {boolean}
*/ */
export function isBrowserSupported() { export function isBrowserSupported(): boolean {
// Checks that return a truthy value if they succeed and throw or return // Checks that return a truthy value if they succeed and throw or return
// a falsey value if they fail. // a falsey value if they fail.
const checks = [ const checks = [
......
...@@ -8,26 +8,22 @@ ...@@ -8,26 +8,22 @@
// @ts-ignore - This file is generated before the boot bundle is built. // @ts-ignore - This file is generated before the boot bundle is built.
import manifest from '../../build/manifest.json'; import manifest from '../../build/manifest.json';
import { bootHypothesisClient, bootSidebarApp } from './boot'; import { bootHypothesisClient, bootSidebarApp } from './boot';
import type { AnnotatorConfig, SidebarAppConfig } from './boot';
import { isBrowserSupported } from './browser-check'; import { isBrowserSupported } from './browser-check';
import { getExtensionId, hasExtensionConfig } from './browser-extension-utils'; import { getExtensionId, hasExtensionConfig } from './browser-extension-utils';
import { parseJsonConfig } from './parse-json-config'; import { parseJsonConfig } from './parse-json-config';
import { processUrlTemplate } from './url-template'; import { processUrlTemplate } from './url-template';
/**
* @typedef {import('./boot').AnnotatorConfig} AnnotatorConfig
* @typedef {import('./boot').SidebarAppConfig} SidebarAppConfig
*/
if (isBrowserSupported()) { if (isBrowserSupported()) {
const config = /** @type {AnnotatorConfig|SidebarAppConfig} */ ( const config = parseJsonConfig(document) as
parseJsonConfig(document) | AnnotatorConfig
); | SidebarAppConfig;
const assetRoot = processUrlTemplate(config.assetRoot || '__ASSET_ROOT__'); const assetRoot = processUrlTemplate(config.assetRoot || '__ASSET_ROOT__');
// Check whether this is a mini-app (indicated by the presence of a // Check whether this is a mini-app (indicated by the presence of a
// `<hypothesis-app>` element) and load the appropriate part of the client. // `<hypothesis-app>` element) and load the appropriate part of the client.
if (document.querySelector('hypothesis-app')) { if (document.querySelector('hypothesis-app')) {
const sidebarConfig = /** @type {SidebarAppConfig} */ (config); const sidebarConfig = config as SidebarAppConfig;
bootSidebarApp(document, { bootSidebarApp(document, {
assetRoot, assetRoot,
manifest, manifest,
...@@ -45,7 +41,7 @@ if (isBrowserSupported()) { ...@@ -45,7 +41,7 @@ if (isBrowserSupported()) {
// nb. If new asset URLs are added here, the browser extension and // nb. If new asset URLs are added here, the browser extension and
// `hypothesis-injector.ts` need to be updated. // `hypothesis-injector.ts` need to be updated.
const annotatorConfig = /** @type {AnnotatorConfig} */ (config); const annotatorConfig = config as AnnotatorConfig;
const notebookAppUrl = processUrlTemplate( const notebookAppUrl = processUrlTemplate(
annotatorConfig.notebookAppUrl || '__NOTEBOOK_APP_URL__', annotatorConfig.notebookAppUrl || '__NOTEBOOK_APP_URL__',
); );
......
...@@ -12,11 +12,10 @@ ...@@ -12,11 +12,10 @@
* setting names, scripts further down in the document override those further * setting names, scripts further down in the document override those further
* up). * up).
* *
* @param {Document|Element} document - The root element to search. * @param document - The root element to search.
*/ */
export function parseJsonConfig(document) { export function parseJsonConfig(document: Document | Element) {
/** @type {Record<string, unknown>} */ const config: Record<string, unknown> = {};
const config = {};
const settingsElements = document.querySelectorAll( const settingsElements = document.querySelectorAll(
'script.js-hypothesis-config', 'script.js-hypothesis-config',
); );
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
* We don't use the URL constructor here because IE and early versions of Edge * We don't use the URL constructor here because IE and early versions of Edge
* do not support it and this code runs early in the life of the app before any * do not support it and this code runs early in the life of the app before any
* polyfills can be loaded. * polyfills can be loaded.
*
* @param {string} url
*/ */
function extractOrigin(url) { function extractOrigin(url: string) {
const match = url.match(/(https?):\/\/([^:/]+)/); const match = url.match(/(https?):\/\/([^:/]+)/);
if (!match) { if (!match) {
return null; return null;
...@@ -16,9 +14,7 @@ function extractOrigin(url) { ...@@ -16,9 +14,7 @@ function extractOrigin(url) {
} }
function currentScriptOrigin(document_ = document) { function currentScriptOrigin(document_ = document) {
const scriptEl = /** @type {HTMLScriptElement|null} */ ( const scriptEl = document_.currentScript as HTMLScriptElement | null;
document_.currentScript
);
if (!scriptEl) { if (!scriptEl) {
// Function was called outside of initial script execution. // Function was called outside of initial script execution.
return null; return null;
...@@ -34,11 +30,8 @@ function currentScriptOrigin(document_ = document) { ...@@ -34,11 +30,8 @@ function currentScriptOrigin(document_ = document) {
* from a device or VM that is not the system where the development server is * from a device or VM that is not the system where the development server is
* running. In that case, all references to `localhost` need to be replaced * running. In that case, all references to `localhost` need to be replaced
* with the IP/hostname of the dev server. * with the IP/hostname of the dev server.
*
* @param {string} url
* @param {Document} document_
*/ */
export function processUrlTemplate(url, document_ = document) { export function processUrlTemplate(url: string, document_ = document) {
if (url.indexOf('{') === -1) { if (url.indexOf('{') === -1) {
// Not a template. This should always be the case in production. // Not a template. This should always be the case in production.
return url; return url;
......
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