Skip to content

Commit

Permalink
Fix a number of Typescript issues (#31877)
Browse files Browse the repository at this point in the history
Typescript error count is reduced from 633 to 540 with this. No runtime
changes except in test code.
  • Loading branch information
silverwind authored Aug 28, 2024
1 parent 39d2fde commit 7207d93
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 78 deletions.
50 changes: 40 additions & 10 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,52 @@ declare module '*.css' {

declare let __webpack_public_path__: string;

declare module 'htmx.org/dist/htmx.esm.js' {
const value = await import('htmx.org');
export default value;
}

declare module 'uint8-to-base64' {
export function encode(arrayBuffer: ArrayBuffer): string;
export function decode(base64str: string): ArrayBuffer;
}

declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
const value = await import('swagger-ui-dist');
export default value.SwaggerUIBundle;
}

interface JQuery {
api: any, // fomantic
areYouSure: any, // jquery.are-you-sure
dimmer: any, // fomantic
dropdown: any; // fomantic
modal: any; // fomantic
tab: any; // fomantic
transition: any, // fomantic
}

interface JQueryStatic {
api: any, // fomantic
}

interface Element {
_tippy: import('tippy.js').Instance;
}

type Writable<T> = { -readonly [K in keyof T]: T[K] };

interface Window {
config: import('./web_src/js/types.ts').Config;
$: typeof import('@types/jquery'),
jQuery: typeof import('@types/jquery'),
htmx: typeof import('htmx.org'),
htmx: Omit<typeof import('htmx.org/dist/htmx.esm.js').default, 'config'> & {
config?: Writable<typeof import('htmx.org').default.config>,
},
ui?: any,
_globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & {
_inited: boolean,
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
},
}

declare module 'htmx.org/dist/htmx.esm.js' {
const value = await import('htmx.org');
export default value;
}

interface Element {
_tippy: import('tippy.js').Instance;
__webpack_public_path__: string;
}
9 changes: 5 additions & 4 deletions web_src/js/htmx.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {showErrorToast} from './modules/toast.ts';
import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx
import type {HtmxResponseInfo} from 'htmx.org';

// https://github.com/bigskysoftware/idiomorph#htmx
import 'idiomorph/dist/idiomorph-ext.js';
type HtmxEvent = Event & {detail: HtmxResponseInfo};

// https://htmx.org/reference/#config
window.htmx.config.requestClass = 'is-loading';
window.htmx.config.scrollIntoViewOnBoost = false;

// https://htmx.org/events/#htmx:sendError
document.body.addEventListener('htmx:sendError', (event) => {
document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => {
// TODO: add translations
showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
});

// https://htmx.org/events/#htmx:responseError
document.body.addEventListener('htmx:responseError', (event) => {
document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => {
// TODO: add translations
showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
});
4 changes: 2 additions & 2 deletions web_src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ initGiteaFomantic();
initDirAuto();
initSubmitEventPolyfill();

function callInitFunctions(functions) {
function callInitFunctions(functions: (() => any)[]) {
// Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1"
// It is a quick check, no side effect so no need to do slow URL parsing.
const initStart = performance.now();
if (window.location.search.includes('_ui_performance_trace=1')) {
let results = [];
let results: {name: string, dur: number}[] = [];
for (const func of functions) {
const start = performance.now();
func();
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/render/ansi.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {AnsiUp} from 'ansi_up';

const replacements = [
const replacements: Array<[RegExp, string]> = [
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
];

// render ANSI to HTML
export function renderAnsi(line) {
export function renderAnsi(line: string): string {
// create a fresh ansi_up instance because otherwise previous renders can influence
// the output of future renders, because ansi_up is stateful and remembers things like
// unclosed opening tags for colors.
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/standalone/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ window.addEventListener('load', async () => {

// Make the page's protocol be at the top of the schemes list
const proto = window.location.protocol.slice(0, -1);
spec.schemes.sort((a, b) => {
spec.schemes.sort((a: string, b: string) => {
if (a === proto) return -1;
if (b === proto) return 1;
return 0;
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/svg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test('svgParseOuterInner', () => {
test('SvgIcon', () => {
const root = document.createElement('div');
createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
const node = root.firstChild;
const node = root.firstChild as Element;
expect(node.nodeName).toEqual('svg');
expect(node.getAttribute('width')).toEqual('24');
expect(node.getAttribute('height')).toEqual('24');
Expand Down
7 changes: 7 additions & 0 deletions web_src/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams;
export type RequestOpts = {
data?: RequestData,
} & RequestInit;

export type IssueData = {
owner: string,
repo: string,
type: string,
index: string,
}
15 changes: 6 additions & 9 deletions web_src/js/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => {
});

test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
// TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not
// structurally comparable, so we convert to array to compare. The conversion can be
// removed once https://github.com/jsdom/jsdom/issues/2524 is resolved.
const encoder = new TextEncoder();
const uint8array = encoder.encode.bind(encoder);

expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"

expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?')));
expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~')));
expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?')));
expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~')));
expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?'));
expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~'));
expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?'));
expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~'));

expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a')));
expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a')));
expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a'));
expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a'));
});

test('file detection', () => {
Expand Down
47 changes: 25 additions & 22 deletions web_src/js/utils.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
import {encode, decode} from 'uint8-to-base64';
import type {IssueData} from './types.ts';

// transform /path/to/file.ext to file.ext
export function basename(path) {
export function basename(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
}

// transform /path/to/file.ext to .ext
export function extname(path) {
export function extname(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
const lastPointIndex = path.lastIndexOf('.');
if (lastSlashIndex > lastPointIndex) return '';
return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
}

// test whether a variable is an object
export function isObject(obj) {
export function isObject(obj: any): boolean {
return Object.prototype.toString.call(obj) === '[object Object]';
}

// returns whether a dark theme is enabled
export function isDarkTheme() {
export function isDarkTheme(): boolean {
const style = window.getComputedStyle(document.documentElement);
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
}

// strip <tags> from a string
export function stripTags(text) {
export function stripTags(text: string): string {
return text.replace(/<[^>]*>?/g, '');
}

export function parseIssueHref(href) {
export function parseIssueHref(href: string): IssueData {
const path = (href || '').replace(/[#?].*$/, '');
const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
return {owner, repo, type, index};
}

// parse a URL, either relative '/path' or absolute 'https://localhost/path'
export function parseUrl(str) {
export function parseUrl(str: string): URL {
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
}

// return current locale chosen by user
export function getCurrentLocale() {
export function getCurrentLocale(): string {
return document.documentElement.lang;
}

// given a month (0-11), returns it in the documents language
export function translateMonth(month) {
export function translateMonth(month: number) {
return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'});
}

// given a weekday (0-6, Sunday to Saturday), returns it in the documents language
export function translateDay(day) {
export function translateDay(day: number) {
return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'});
}

// convert a Blob to a DataURI
export function blobToDataURI(blob) {
export function blobToDataURI(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader();
reader.addEventListener('load', (e) => {
resolve(e.target.result);
resolve(e.target.result as string);
});
reader.addEventListener('error', () => {
reject(new Error('FileReader failed'));
Expand All @@ -75,7 +76,7 @@ export function blobToDataURI(blob) {
}

// convert image Blob to another mime-type format.
export function convertImage(blob, mime) {
export function convertImage(blob: Blob, mime: string): Promise<Blob> {
return new Promise(async (resolve, reject) => {
try {
const img = new Image();
Expand Down Expand Up @@ -104,7 +105,7 @@ export function convertImage(blob, mime) {
});
}

export function toAbsoluteUrl(url) {
export function toAbsoluteUrl(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
Expand All @@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) {
}

// Encode an ArrayBuffer into a URLEncoded base64 string.
export function encodeURLEncodedBase64(arrayBuffer) {
export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string {
return encode(arrayBuffer)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

// Decode a URLEncoded base64 to an ArrayBuffer string.
export function decodeURLEncodedBase64(base64url) {
// Decode a URLEncoded base64 to an ArrayBuffer.
export function decodeURLEncodedBase64(base64url: string): ArrayBuffer {
return decode(base64url
.replace(/_/g, '/')
.replace(/-/g, '+'));
Expand All @@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) {
const domParser = new DOMParser();
const xmlSerializer = new XMLSerializer();

export function parseDom(text, contentType) {
export function parseDom(text: string, contentType: DOMParserSupportedType): Document {
return domParser.parseFromString(text, contentType);
}

export function serializeXml(node) {
export function serializeXml(node: Element | Node): string {
return xmlSerializer.serializeToString(node);
}

export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export function isImageFile({name, type}) {
export function isImageFile({name, type}: {name: string, type?: string}): boolean {
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
}

export function isVideoFile({name, type}) {
export function isVideoFile({name, type}: {name: string, type?: string}): boolean {
return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
}
8 changes: 4 additions & 4 deletions web_src/js/utils/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ import type {ColorInput} from 'tinycolor2';

// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
// Keep this in sync with modules/util/color.go
function getRelativeLuminance(color: ColorInput) {
function getRelativeLuminance(color: ColorInput): number {
const {r, g, b} = tinycolor(color).toRgb();
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
}

function useLightText(backgroundColor: ColorInput) {
function useLightText(backgroundColor: ColorInput): boolean {
return getRelativeLuminance(backgroundColor) < 0.453;
}

// Given a background color, returns a black or white foreground color that the highest
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
export function contrastColor(backgroundColor: ColorInput) {
export function contrastColor(backgroundColor: ColorInput): string {
return useLightText(backgroundColor) ? '#fff' : '#000';
}

function resolveColors(obj: Record<string, string>) {
function resolveColors(obj: Record<string, string>): Record<string, string> {
const styles = window.getComputedStyle(document.documentElement);
const getColor = (name: string) => styles.getPropertyValue(name).trim();
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
Expand Down
4 changes: 1 addition & 3 deletions web_src/js/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() {
/**
* Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
* Note: This function doesn't account for all possible visibility scenarios.
* @param {HTMLElement} element The element to check.
* @returns {boolean} True if the element is visible.
*/
export function isElemVisible(element: HTMLElement) {
export function isElemVisible(element: HTMLElement): boolean {
if (!element) return false;

return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
Expand Down
16 changes: 13 additions & 3 deletions web_src/js/utils/image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export async function pngChunks(blob) {
type PngChunk = {
name: string,
data: Uint8Array,
}

export async function pngChunks(blob: Blob): Promise<PngChunk[]> {
const uint8arr = new Uint8Array(await blob.arrayBuffer());
const chunks = [];
const chunks: PngChunk[] = [];
if (uint8arr.length < 12) return chunks;
const view = new DataView(uint8arr.buffer);
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
Expand All @@ -19,9 +24,14 @@ export async function pngChunks(blob) {
return chunks;
}

type ImageInfo = {
width?: number,
dppx?: number,
}

// decode a image and try to obtain width and dppx. It will never throw but instead
// return default values.
export async function imageInfo(blob) {
export async function imageInfo(blob: Blob): Promise<ImageInfo> {
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens

if (blob.type === 'image/png') { // only png is supported currently
Expand Down
Loading

0 comments on commit 7207d93

Please sign in to comment.