Skip to content

Commit

Permalink
Merge pull request #2244 from hypothesis/typecheck-utils
Browse files Browse the repository at this point in the history
Add command to typecheck code and enable it for src/sidebar/util/
  • Loading branch information
robertknight authored Jun 24, 2020
2 parents d5fe3ad + 1b78084 commit 487a1b4
Show file tree
Hide file tree
Showing 35 changed files with 258 additions and 57 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ endif
.PHONY: lint
lint: node_modules/.uptodate
yarn run lint
yarn run typecheck

.PHONY: docs
docs: python
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"terser": "^4.4.0",
"through2": "^3.0.0",
"tiny-emitter": "^2.0.2",
"typescript": "^3.9.5",
"unorm": "^1.3.3",
"vinyl": "^2.2.0",
"watchify": "^3.7.0",
Expand Down Expand Up @@ -127,6 +128,7 @@
"checkformatting": "prettier --check '**/*.{js,scss}'",
"format": "prettier --list-different --write '**/*.{js,scss}'",
"test": "gulp test",
"typecheck": "tsc --build src/tsconfig.json",
"report-coverage": "codecov -f coverage/coverage-final.json",
"version": "make clean build/manifest.json"
}
Expand Down
41 changes: 38 additions & 3 deletions src/sidebar/build-thread.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
/** Default state for new threads, before applying filters etc. */
/**
* @typedef {import('../types/api').Annotation} Annotation
*
* @typedef Thread
* @prop {string} id
* @prop {Annotation|undefined} annotation
* @prop {Thread|undefined} parent
* @prop {boolean} visible
* @prop {boolean} collapsed
* @prop {Thread[]} children
* @prop {number} totalChildren
* @prop {'dim'|'highlight'|undefined} highlightState
*/

/**
* Default state for new threads, before applying filters etc.
*
* @type {Thread}
*/
const DEFAULT_THREAD_STATE = {
/**
* The ID of this thread. This will be the same as the annotation ID for
* created annotations or the `$tag` property for new annotations.
*/
id: undefined,
id: '__default__',
/**
* The Annotation which is displayed by this thread.
*
Expand Down Expand Up @@ -128,11 +146,14 @@ function threadAnnotations(annotations) {
});

const root = {
id: 'root',
annotation: undefined,
children: roots,
visible: true,
collapsed: false,
totalChildren: roots.length,
parent: undefined,
highlightState: undefined,
};

return root;
Expand All @@ -157,7 +178,7 @@ function mapThread(thread, mapFn) {
* Return a sorted copy of an array of threads.
*
* @param {Array<Thread>} threads - The list of threads to sort
* @param {(Annotation,Annotation) => boolean} compareFn
* @param {(a: Annotation, b: Annotation) => boolean} compareFn
* @return {Array<Thread>} Sorted list of threads
*/
function sort(threads, compareFn) {
Expand Down Expand Up @@ -219,8 +240,22 @@ function hasVisibleChildren(thread) {
});
}

/**
* @typedef Options
* @prop {string[]} [selected]
* @prop {string[]} [forceVisible]
* @prop {(a: Annotation) => boolean} [filterFn]
* @prop {(t: Thread) => boolean} [threadFilterFn]
* @prop {Object} [expanded]
* @prop {Object} [highlighted]
* @prop {(a: Annotation, b: Annotation) => boolean} [sortCompareFn]
* @prop {(a: Annotation, b: Annotation) => boolean} [replySortCompareFn]
*/

/**
* Default options for buildThread()
*
* @type {Partial<Options>}
*/
const defaultOpts = {
/** List of currently selected annotation IDs */
Expand Down
35 changes: 35 additions & 0 deletions src/sidebar/host-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,44 @@ import {
toString,
} from '../shared/type-coercions';

/**
* @typedef RequestConfigFromFrameOptions
* @prop {number} ancestorLevel
* @prop {string} origin
*/

/**
* Configuration for the client provided by the frame embedding it.
*
* User-facing documentation exists at
* https://h.readthedocs.io/projects/client/en/latest/publishers/config/
*
* @typedef Config
* @prop {string} [annotations] - Direct-linked annotation ID
* @prop {string} [group] - Direct-linked group ID
* @prop {string} [query] - Initial filter query
* @prop {string} [appType] - Method used to load the client
* @prop {boolean} [openSidebar] - Whether to open the sidebar on the initial load
* @prop {boolean} [showHighlights] - Whether to show highlights
* @prop {Object[]} [services] -
* Configuration for the annotation services that the client connects to
* @prop {Object} [branding] -
* Theme properties (fonts, colors etc.)
* @prop {boolean} [enableExperimentalNewNoteButton] -
* Whether to show the "New note" button on the "Page Notes" tab
* @prop {RequestConfigFromFrameOptions|string} [requestConfigFromFrame]
* Origin of the ancestor frame to request configuration from
* @prop {string} [theme]
* Name of the base theme to use.
* @prop {string} [usernameUrl]
* URL template for username links
*/

/**
* Return the app configuration specified by the frame embedding the Hypothesis
* client.
*
* @return {Config}
*/
export default function hostPageConfig(window) {
const configStr = window.location.hash.slice(1);
Expand Down
5 changes: 5 additions & 0 deletions src/sidebar/util/account-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function parseAccountID(user) {

/**
* Returns the username part of an account ID or an empty string.
*
* @param {string} user
*/
export function username(user) {
const account = parseAccountID(user);
Expand All @@ -30,6 +32,9 @@ export function username(user) {

/**
* Returns true if the authority is of a 3rd party user.
*
* @param {string} user
* @param {string} authDomain
*/
export function isThirdPartyUser(user, authDomain) {
const account = parseAccountID(user);
Expand Down
2 changes: 1 addition & 1 deletion src/sidebar/util/copy-to-clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function copyText(text) {

try {
const range = document.createRange();
const selection = document.getSelection();
const selection = /** @type {Selection} */ (document.getSelection());

selection.removeAllRanges();
range.selectNodeContents(temp);
Expand Down
2 changes: 2 additions & 0 deletions src/sidebar/util/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ let formatter;
/**
* Returns a standard human-readable representation
* of a date and time.
*
* @param {Date} date
*/
export function format(date) {
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
Expand Down
6 changes: 4 additions & 2 deletions src/sidebar/util/disable-opener-for-external-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
*/
export default function disableOpenerForExternalLinks(root) {
root.addEventListener('click', event => {
if (event.target.tagName === 'A') {
const linkEl = event.target;
const target = /** @type {HTMLElement} */ (event.target);

if (target.tagName === 'A') {
const linkEl = /** @type {HTMLAnchorElement} */ (target);
if (linkEl.target === '_blank') {
linkEl.rel = 'noopener';
}
Expand Down
5 changes: 3 additions & 2 deletions src/sidebar/util/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export function getElementHeightWithMargins(element) {
* Attach listeners for one or multiple events to an element and return a
* function that removes the listeners.
*
* @param {Element}
* @param {HTMLElement} element
* @param {string[]} events
* @param {(event: Event) => any} listener
* @param {EventListener} listener
* @param {Object} options
* @param {boolean} [options.useCapture]
* @return {function} Function which removes the event listeners.
*/
Expand Down
11 changes: 7 additions & 4 deletions src/sidebar/util/fetch-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function ancestors(window_) {
* Returns the global embedder ancestor frame.
*
* @param {number} levels - Number of ancestors levels to ascend.
* @param {Window=} window
* @param {Window=} window_
* @return {Window}
*/
function getAncestorFrame(levels, window_ = window) {
Expand Down Expand Up @@ -57,6 +57,7 @@ function fetchConfigFromAncestorFrame(origin, window_ = window) {
ancestor,
origin,
'requestConfig',
[],
timeout
);
configResponses.push(result);
Expand Down Expand Up @@ -86,7 +87,7 @@ function fetchConfigLegacy(appConfig, window_ = window) {
const hostPageConfig = hostConfig(window_);

let embedderConfig;
const origin = hostPageConfig.requestConfigFromFrame;
const origin = /** @type string */ (hostPageConfig.requestConfigFromFrame);
embedderConfig = fetchConfigFromAncestorFrame(origin, window_);

return embedderConfig.then(embedderConfig => {
Expand Down Expand Up @@ -154,7 +155,7 @@ async function fetchConfigRpc(appConfig, parentFrame, origin) {
* already have the `services` value
* @param {function} rpcCall - RPC method
* (method, args, timeout) => Promise
* @return {Object} - The mutated settings
* @return {Promise<Object>} - The mutated settings
*/
async function fetchGroupsAsync(config, rpcCall) {
if (Array.isArray(config.services)) {
Expand All @@ -170,6 +171,7 @@ async function fetchGroupsAsync(config, rpcCall) {
}
return config;
}

/**
* Fetch the host configuration and merge it with the app configuration from h.
*
Expand All @@ -180,10 +182,11 @@ async function fetchGroupsAsync(config, rpcCall) {
*
* @param {Object} appConfig - Settings rendered into `app.html` by the h service.
* @param {Window} window_ - Test seam.
* @return {Object} - The merged settings.
* @return {Promise<Object>} - The merged settings.
*/
export async function fetchConfig(appConfig, window_ = window) {
const hostPageConfig = hostConfig(window);

const requestConfigFromFrame = hostPageConfig.requestConfigFromFrame;

if (!requestConfigFromFrame) {
Expand Down
8 changes: 8 additions & 0 deletions src/sidebar/util/group-list-item-common.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @typedef {import('../../types/api').Group} Group
*/

/**
* @param {Group} group
* @return {string}
*/
export function orgName(group) {
return group.organization && group.organization.name;
}
Expand Down
4 changes: 4 additions & 0 deletions src/sidebar/util/group-organizations.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import immutable from './immutable';

/**
* @typedef {import('../../types/api').Group} Group
*/

// TODO: Update when this is a property available on the API response
const DEFAULT_ORG_ID = '__default__';

Expand Down
2 changes: 2 additions & 0 deletions src/sidebar/util/groups.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @typedef {import('../../types/api').Group} Group */

import escapeStringRegexp from 'escape-string-regexp';
import serviceConfig from '../service-config';

Expand Down
2 changes: 1 addition & 1 deletion src/sidebar/util/is-third-party-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import serviceConfig from '../service-config';
* If no custom annotation services are configured then return `false`.
*
* @param {Object} settings - the sidebar settings object
*
* @return {boolean}
*/
export default function isThirdPartyService(settings) {
const service = serviceConfig(settings);
Expand Down
5 changes: 5 additions & 0 deletions src/sidebar/util/memoize.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
*
* The argument to the input function may be of any type and is compared
* using reference equality.
*
* @template Arg
* @template Result
* @param {(arg: Arg) => Result} fn
* @return {(arg: Arg) => Result}
*/
export default function memoize(fn) {
if (fn.length !== 1) {
Expand Down
6 changes: 4 additions & 2 deletions src/sidebar/util/oauth-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export default class OAuthClient {
response_type: 'code',
state: state,
});

// @ts-ignore - TS doesn't know about `location = <string>`.
authWindow.location = authUrl;

return authResponse.then(rsp => rsp.code);
Expand Down Expand Up @@ -253,10 +255,10 @@ export default class OAuthClient {
})
.replace(/&/g, ',');

return $window.open(
return /** @type {Window} */ ($window.open(
'about:blank',
'Log in to Hypothesis',
authWindowSettings
);
));
}
}
2 changes: 2 additions & 0 deletions src/sidebar/util/observe-element-size.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
* @return {() => void}
*/
export default function observeElementSize(element, onSizeChanged) {
// @ts-ignore - TS is missing `ResizeObserver` type definition
if (typeof ResizeObserver !== 'undefined') {
// @ts-ignore
const observer = new ResizeObserver(() =>
onSizeChanged(element.clientWidth, element.clientHeight)
);
Expand Down
2 changes: 1 addition & 1 deletion src/sidebar/util/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function sharedPermissions(userid, groupid) {
* Return the default permissions for an annotation in a given group.
*
* @param {string} userid - User ID of the author
* @param {string} groupId - ID of the group the annotation is being shared
* @param {string} groupid - ID of the group the annotation is being shared
* with
* @return {Permissions}
*/
Expand Down
7 changes: 3 additions & 4 deletions src/sidebar/util/postmessage-json-rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ function createTimeout(delay, message) {
* @param {string} origin - Origin filter for `window.postMessage` call
* @param {string} method - Name of the JSON-RPC method
* @param {any[]} params - Parameters of the JSON-RPC method
* @param {number|null} [number=2000] timeout - Maximum time to wait in
* ms or null or 0 to ignore timeout.
* @param [Window] window_ - Test seam.
* @param [id] id - Test seam.
* @param {number} [timeout] - Maximum time to wait in ms
* @param {Window} [window_] - Test seam.
* @param {string} [id] - Test seam.
* @return {Promise<any>} - A Promise for the response to the call
*/
export function call(
Expand Down
3 changes: 2 additions & 1 deletion src/sidebar/util/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ function byteToHex(val) {
/**
* Generate a random hex string of `len` chars.
*
* @param {number} - An even-numbered length string to generate.
* @param {number} len - An even-numbered length string to generate.
* @return {string}
*/
export function generateHexString(len) {
// @ts-ignore - TS doesn't know about `msCrypto`.
const crypto = window.crypto || window.msCrypto; /* IE 11 */
const bytes = new Uint8Array(len / 2);
crypto.getRandomValues(bytes);
Expand Down
Loading

0 comments on commit 487a1b4

Please sign in to comment.