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
: `${localhost}:3001/hypothesis/${version}/`;
export default {
input: 'src/boot/index.js',
input: 'src/boot/index.ts',
output: {
file: 'build/boot.js',
......
/**
* @typedef SidebarAppConfig
* @prop {string} assetRoot - The root URL to which URLs in `manifest` are relative
* @prop {Record<string,string>} manifest -
* A mapping from canonical asset path to cache-busted asset path
* @prop {string} apiUrl
*/
/**
* @typedef AnnotatorConfig
* @prop {string} assetRoot - The root URL to which URLs in `manifest` are relative
* @prop {string} notebookAppUrl - The URL of the sidebar's notebook
* @prop {string} profileAppUrl - The URL of the sidebar's user profile view
* @prop {string} sidebarAppUrl - The URL of the sidebar's HTML page
* @prop {Record<string,string>} manifest -
* A mapping from canonical asset path to cache-busted asset path
*/
/**
* @typedef {Window & { PDFViewerApplication?: object }} MaybePDFWindow
*/
export type SidebarAppConfig = {
/** The root URL to which URLs in `manifest` are relative. */
assetRoot: string;
/** A mapping from canonical asset path to cache-busted asset path. */
manifest: Record<string, string>;
apiUrl: string;
};
export type AnnotatorConfig = {
/** The root URL to which URLs in `manifest` are relative. */
assetRoot: string;
/** The URL of the sidebar's notebook. */
notebookAppUrl: string;
/** The URL of the sidebar's user profile view. */
profileAppUrl: string;
/** The URL of the sidebar's HTML page. */
sidebarAppUrl: string;
/** A mapping from canonical asset path to cache-busted asset path. */
manifest: Record<string, string>;
};
type MaybePDFWindow = Window & { PDFViewerApplication?: object };
/**
* Mark an element as having been added by the boot script.
*
* This marker is later used to know which elements to remove when unloading
* the client.
*
* @param {HTMLElement} el
*/
function tagElement(el) {
function tagElement(el: HTMLElement) {
el.setAttribute('data-hypothesis-asset', '');
}
/**
* @param {Document} doc
* @param {string} href
*/
function injectStylesheet(doc, href) {
function injectStylesheet(doc: Document, href: string) {
const link = doc.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
......@@ -46,14 +42,19 @@ function injectStylesheet(doc, href) {
doc.head.appendChild(link);
}
/**
* @param {Document} doc
* @param {string} src - The script URL
* @param {object} options
* @param {boolean} [options.esModule] - Whether to load the script as an ES module
* @param {boolean} [options.forceReload] - Whether to force re-evaluation of an ES module script
*/
function injectScript(doc, src, { esModule = true, forceReload = false } = {}) {
function injectScript(
doc: Document,
src: string,
{
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');
if (esModule) {
......@@ -79,13 +80,12 @@ function injectScript(doc, src, { esModule = true, forceReload = false } = {}) {
doc.head.appendChild(script);
}
/**
* @param {Document} doc
* @param {string} rel
* @param {'html'|'javascript'} type
* @param {string} url
*/
function injectLink(doc, rel, type, url) {
function injectLink(
doc: Document,
rel: string,
type: 'html' | 'javascript',
url: string,
) {
const link = doc.createElement('link');
link.rel = rel;
link.href = 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
* 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');
link.rel = 'preload';
link.as = type;
......@@ -122,11 +118,7 @@ function preloadURL(doc, type, url) {
doc.head.appendChild(link);
}
/**
* @param {SidebarAppConfig|AnnotatorConfig} config
* @param {string} path
*/
function assetURL(config, path) {
function assetURL(config: SidebarAppConfig | AnnotatorConfig, path: string) {
return config.assetRoot + 'build/' + config.manifest[path];
}
......@@ -136,11 +128,8 @@ function assetURL(config, path) {
* 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
* 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
const appLinkEl = doc.querySelector(
'link[type="application/annotator+html"]',
......@@ -173,19 +162,17 @@ export function bootHypothesisClient(doc, config) {
);
const scripts = ['scripts/annotator.bundle.js'];
for (let path of scripts) {
for (const path of scripts) {
const url = assetURL(config, path);
injectScript(doc, url, { esModule: false });
}
const styles = [];
if (
/** @type {MaybePDFWindow} */ (window).PDFViewerApplication !== undefined
) {
if ((window as MaybePDFWindow).PDFViewerApplication !== undefined) {
styles.push('styles/pdfjs-overrides.css');
}
styles.push('styles/highlights.css');
for (let path of styles) {
for (const path of styles) {
const url = assetURL(config, path);
injectStylesheet(doc, url);
}
......@@ -193,23 +180,20 @@ export function bootHypothesisClient(doc, config) {
/**
* 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.
preloadURL(doc, 'fetch', config.apiUrl);
preloadURL(doc, 'fetch', config.apiUrl + 'links');
const scripts = ['scripts/sidebar.bundle.js'];
for (let path of scripts) {
for (const path of scripts) {
const url = assetURL(config, path);
injectScript(doc, url, { esModule: true });
}
const styles = ['styles/katex.min.css', 'styles/sidebar.css'];
for (let path of styles) {
for (const path of styles) {
const url = assetURL(config, path);
injectStylesheet(doc, url);
}
......
......@@ -5,10 +5,8 @@
* 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
* 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
// a falsey value if they fail.
const checks = [
......
......@@ -8,26 +8,22 @@
// @ts-ignore - This file is generated before the boot bundle is built.
import manifest from '../../build/manifest.json';
import { bootHypothesisClient, bootSidebarApp } from './boot';
import type { AnnotatorConfig, SidebarAppConfig } from './boot';
import { isBrowserSupported } from './browser-check';
import { getExtensionId, hasExtensionConfig } from './browser-extension-utils';
import { parseJsonConfig } from './parse-json-config';
import { processUrlTemplate } from './url-template';
/**
* @typedef {import('./boot').AnnotatorConfig} AnnotatorConfig
* @typedef {import('./boot').SidebarAppConfig} SidebarAppConfig
*/
if (isBrowserSupported()) {
const config = /** @type {AnnotatorConfig|SidebarAppConfig} */ (
parseJsonConfig(document)
);
const config = parseJsonConfig(document) as
| AnnotatorConfig
| SidebarAppConfig;
const assetRoot = processUrlTemplate(config.assetRoot || '__ASSET_ROOT__');
// Check whether this is a mini-app (indicated by the presence of a
// `<hypothesis-app>` element) and load the appropriate part of the client.
if (document.querySelector('hypothesis-app')) {
const sidebarConfig = /** @type {SidebarAppConfig} */ (config);
const sidebarConfig = config as SidebarAppConfig;
bootSidebarApp(document, {
assetRoot,
manifest,
......@@ -45,7 +41,7 @@ if (isBrowserSupported()) {
// nb. If new asset URLs are added here, the browser extension and
// `hypothesis-injector.ts` need to be updated.
const annotatorConfig = /** @type {AnnotatorConfig} */ (config);
const annotatorConfig = config as AnnotatorConfig;
const notebookAppUrl = processUrlTemplate(
annotatorConfig.notebookAppUrl || '__NOTEBOOK_APP_URL__',
);
......
......@@ -12,11 +12,10 @@
* setting names, scripts further down in the document override those further
* up).
*
* @param {Document|Element} document - The root element to search.
* @param document - The root element to search.
*/
export function parseJsonConfig(document) {
/** @type {Record<string, unknown>} */
const config = {};
export function parseJsonConfig(document: Document | Element) {
const config: Record<string, unknown> = {};
const settingsElements = document.querySelectorAll(
'script.js-hypothesis-config',
);
......
......@@ -4,10 +4,8 @@
* 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
* polyfills can be loaded.
*
* @param {string} url
*/
function extractOrigin(url) {
function extractOrigin(url: string) {
const match = url.match(/(https?):\/\/([^:/]+)/);
if (!match) {
return null;
......@@ -16,9 +14,7 @@ function extractOrigin(url) {
}
function currentScriptOrigin(document_ = document) {
const scriptEl = /** @type {HTMLScriptElement|null} */ (
document_.currentScript
);
const scriptEl = document_.currentScript as HTMLScriptElement | null;
if (!scriptEl) {
// Function was called outside of initial script execution.
return null;
......@@ -34,11 +30,8 @@ function currentScriptOrigin(document_ = document) {
* 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
* 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) {
// Not a template. This should always be the case in production.
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