chore: Move to Typescript (#2783)

This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously.

closes #1282
This commit is contained in:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -1,4 +1,2 @@
// @flow
export const validateColorHex = (color: string) =>
/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);

View File

@@ -1,19 +1,20 @@
// @flow
import { subDays, subMonths, subWeeks, subYears } from "date-fns";
import { DateFilter } from "@shared/types";
export function subtractDate(
date: Date,
period: "day" | "week" | "month" | "year"
) {
export function subtractDate(date: Date, period: DateFilter) {
switch (period) {
case "day":
return subDays(date, 1);
case "week":
return subWeeks(date, 1);
case "month":
return subMonths(date, 1);
case "year":
return subYears(date, 1);
default:
return date;
}

View File

@@ -1,6 +1,4 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { stripSubdomain, parseDomain, isCustomSubdomain } from "./domains";
// test suite is based on subset of parse-domain module we want to support
// https://github.com/peerigon/parse-domain/blob/master/test/parseDomain.test.js
describe("#parseDomain", () => {
@@ -100,7 +98,9 @@ describe("#parseDomain", () => {
});
it("should return null if the given value is not a string", () => {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'undefined' is not assignable to ... Remove this comment to see the full error message
expect(parseDomain(undefined)).toBe(null);
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{}' is not assignable to paramet... Remove this comment to see the full error message
expect(parseDomain({})).toBe(null);
expect(parseDomain("")).toBe(null);
});
@@ -113,35 +113,38 @@ describe("#parseDomain", () => {
});
});
});
describe("#stripSubdomain", () => {
test("to work with localhost", () => {
expect(stripSubdomain("localhost")).toBe("localhost");
});
test("to return domains without a subdomain", () => {
expect(stripSubdomain("example")).toBe("example");
expect(stripSubdomain("example.com")).toBe("example.com");
expect(stripSubdomain("example.org:3000")).toBe("example.org");
});
test("to remove subdomains", () => {
expect(stripSubdomain("test.example.com")).toBe("example.com");
expect(stripSubdomain("test.example.com:3000")).toBe("example.com");
});
});
describe("#isCustomSubdomain", () => {
test("to work with localhost", () => {
expect(isCustomSubdomain("localhost")).toBe(false);
});
test("to return false for domains without a subdomain", () => {
expect(isCustomSubdomain("example")).toBe(false);
expect(isCustomSubdomain("example.com")).toBe(false);
expect(isCustomSubdomain("example.org:3000")).toBe(false);
});
test("to return false for www", () => {
expect(isCustomSubdomain("www.example.com")).toBe(false);
expect(isCustomSubdomain("www.example.com:3000")).toBe(false);
});
test("to return true for subdomains", () => {
expect(isCustomSubdomain("test.example.com")).toBe(true);
expect(isCustomSubdomain("test.example.com:3000")).toBe(true);

View File

@@ -1,19 +1,17 @@
// @flow
import { trim } from "lodash";
type Domain = {
tld: string,
subdomain: string,
domain: string,
tld: string;
subdomain: string;
domain: string;
};
// we originally used the parse-domain npm module however this includes
// a large list of possible TLD's which increase the size of the bundle
// unnecessarily for our usecase of trusted input.
export function parseDomain(url: string): ?Domain {
export function parseDomain(url: string): Domain | null | undefined {
if (typeof url !== "string") return null;
if (url === "") return null;
// strip extermeties and whitespace from input
const normalizedDomain = trim(url.replace(/(https?:)?\/\//, ""));
const parts = normalizedDomain.split(".");
@@ -32,6 +30,7 @@ export function parseDomain(url: string): ?Domain {
tld: cleanTLD(parts.slice(2).join(".")),
};
}
if (parts.length === 2) {
return {
subdomain: "",
@@ -55,13 +54,13 @@ export function parseDomain(url: string): ?Domain {
export function stripSubdomain(hostname: string) {
const parsed = parseDomain(hostname);
if (!parsed) return hostname;
if (parsed.tld) return `${parsed.domain}.${parsed.tld}`;
return parsed.domain;
}
export function isCustomSubdomain(hostname: string) {
const parsed = parseDomain(hostname);
if (
!parsed ||
!parsed.subdomain ||
@@ -70,6 +69,7 @@ export function isCustomSubdomain(hostname: string) {
) {
return false;
}
return true;
}

View File

@@ -1,10 +1,9 @@
// @flow
const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/gi;
export default function getTasks(text: string) {
const matches = [...text.matchAll(CHECKBOX_REGEX)];
let total = matches.length;
const total = matches.length;
if (!total) {
return {
completed: 0,
@@ -16,6 +15,9 @@ export default function getTasks(text: string) {
match[1] === " " ? accumulator + 1 : accumulator,
0
);
return { completed: total - notCompleted, total };
return {
completed: total - notCompleted,
total,
};
}
}

View File

@@ -1,4 +1,2 @@
// @flow
export const validateIndexCharacters = (index: string) =>
new RegExp("^[\x20-\x7E]+$").test(index);

View File

@@ -1,61 +0,0 @@
// @flow
import naturalSort from "./naturalSort";
describe("#naturalSort", () => {
it("should sort a list of objects by the given key", () => {
const items = [{ name: "Joan" }, { name: "Pedro" }, { name: "Mark" }];
expect(naturalSort(items, "name")).toEqual([
{ name: "Joan" },
{ name: "Mark" },
{ name: "Pedro" },
]);
});
it("should accept a function as the object key", () => {
const items = [{ name: "Joan" }, { name: "Pedro" }, { name: "Mark" }];
expect(naturalSort(items, (item) => item.name)).toEqual([
{ name: "Joan" },
{ name: "Mark" },
{ name: "Pedro" },
]);
});
it("should accept natural-sort options", () => {
const items = [
{ name: "Joan" },
{ name: "joan" },
{ name: "Pedro" },
{ name: "Mark" },
];
expect(
naturalSort(items, "name", { direction: "desc", caseSensitive: true })
).toEqual([
{ name: "joan" },
{ name: "Pedro" },
{ name: "Mark" },
{ name: "Joan" },
]);
});
it("should ignore non basic latin letters", () => {
const items = [{ name: "Abel" }, { name: "Martín" }, { name: "Ávila" }];
expect(naturalSort(items, "name")).toEqual([
{ name: "Abel" },
{ name: "Ávila" },
{ name: "Martín" },
]);
});
it("should ignore emojis", () => {
const items = [
{ title: "🍔 Document 2" },
{ title: "🐻 Document 3" },
{ title: "🙂 Document 1" },
];
expect(naturalSort(items, "title")).toEqual([
{ title: "🙂 Document 1" },
{ title: "🍔 Document 2" },
{ title: "🐻 Document 3" },
]);
});
});

View File

@@ -0,0 +1,140 @@
import naturalSort from "./naturalSort";
describe("#naturalSort", () => {
it("should sort a list of objects by the given key", () => {
const items = [
{
name: "Joan",
},
{
name: "Pedro",
},
{
name: "Mark",
},
];
expect(naturalSort(items, "name")).toEqual([
{
name: "Joan",
},
{
name: "Mark",
},
{
name: "Pedro",
},
]);
});
it("should accept a function as the object key", () => {
const items = [
{
name: "Joan",
},
{
name: "Pedro",
},
{
name: "Mark",
},
];
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(item: any) => any' is not assig... Remove this comment to see the full error message
expect(naturalSort(items, (item) => item.name)).toEqual([
{
name: "Joan",
},
{
name: "Mark",
},
{
name: "Pedro",
},
]);
});
it("should accept natural-sort options", () => {
const items = [
{
name: "Joan",
},
{
name: "joan",
},
{
name: "Pedro",
},
{
name: "Mark",
},
];
expect(
naturalSort(items, "name", {
direction: "desc",
caseSensitive: true,
})
).toEqual([
{
name: "joan",
},
{
name: "Pedro",
},
{
name: "Mark",
},
{
name: "Joan",
},
]);
});
it("should ignore non basic latin letters", () => {
const items = [
{
name: "Abel",
},
{
name: "Martín",
},
{
name: "Ávila",
},
];
expect(naturalSort(items, "name")).toEqual([
{
name: "Abel",
},
{
name: "Ávila",
},
{
name: "Martín",
},
]);
});
it("should ignore emojis", () => {
const items = [
{
title: "🍔 Document 2",
},
{
title: "🐻 Document 3",
},
{
title: "🙂 Document 1",
},
];
expect(naturalSort(items, "title")).toEqual([
{
title: "🙂 Document 1",
},
{
title: "🍔 Document 2",
},
{
title: "🐻 Document 3",
},
]);
});
});

View File

@@ -1,40 +1,40 @@
// @flow
import emojiRegex from "emoji-regex";
import { deburr } from "lodash";
import naturalSort from "natural-sort";
type NaturalSortOptions = {
caseSensitive?: boolean,
direction?: "asc" | "desc",
caseSensitive?: boolean;
direction?: "asc" | "desc";
};
const sorter = naturalSort();
const regex = emojiRegex();
const stripEmojis = (value: string) => value.replace(regex, "");
const cleanValue = (value: string) => stripEmojis(deburr(value));
function getSortByField<T: Object>(
function getSortByField<T extends Record<string, any>>(
item: T,
keyOrCallback: string | ((obj: T) => string)
keyOrCallback: string | (() => string)
) {
const field =
typeof keyOrCallback === "string"
? item[keyOrCallback]
: keyOrCallback(item);
: // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
keyOrCallback(item);
return cleanValue(field);
}
function naturalSortBy<T>(
items: T[],
key: string | ((obj: T) => string),
key: string | (() => string),
sortOptions?: NaturalSortOptions
): T[] {
if (!items) return [];
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'NaturalSortOptions' is not assig... Remove this comment to see the full error message
const sort = sortOptions ? naturalSort(sortOptions) : sorter;
return items.sort((a: any, b: any): -1 | 0 | 1 =>
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type '0 | 1 | -... Remove this comment to see the full error message
sort(getSortByField(a, key), getSortByField(b, key))
);
}

View File

@@ -1,4 +1,3 @@
// @flow
import parseDocumentSlug from "./parseDocumentSlug";
describe("#parseDocumentSlug", () => {

View File

@@ -1,7 +1,6 @@
// @flow
export default function parseDocumentSlug(url: string) {
let parsed;
if (url[0] === "/") {
parsed = url;
} else {

View File

@@ -1,26 +1,21 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import parseTitle from "./parseTitle";
it("should trim the title", () => {
expect(parseTitle(`# Lots of space `).title).toBe("Lots of space");
});
it("should extract first title", () => {
expect(parseTitle(`# Title one\n# Title two`).title).toBe("Title one");
});
it("should remove escape characters", () => {
expect(parseTitle(`# Thing \\- one`).title).toBe("Thing - one");
expect(parseTitle(`# \\[wip\\] Title`).title).toBe("[wip] Title");
expect(parseTitle(`# \\> Title`).title).toBe("> Title");
});
it("should parse emoji if first character", () => {
const parsed = parseTitle(`# 😀 Title`);
expect(parsed.title).toBe("😀 Title");
expect(parsed.emoji).toBe("😀");
});
it("should not parse emoji if not first character", () => {
const parsed = parseTitle(`# Title 🌈`);
expect(parsed.title).toBe("Title 🌈");

View File

@@ -1,8 +1,7 @@
// @flow
import emojiRegex from "emoji-regex";
import unescape from "./unescape";
export default function parseTitle(text: string = "") {
export default function parseTitle(text = "") {
const regex = emojiRegex();
// find and extract title
@@ -18,5 +17,8 @@ export default function parseTitle(text: string = "") {
const startsWithEmoji = firstEmoji && title.startsWith(firstEmoji);
const emoji = startsWithEmoji ? firstEmoji : undefined;
return { title, emoji };
return {
title,
emoji,
};
}

View File

@@ -1,5 +1,3 @@
// @flow
export function slackAuth(
state: string,
scopes: string[] = [
@@ -8,8 +6,9 @@ export function slackAuth(
"identity.avatar",
"identity.team",
],
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
clientId: string = process.env.SLACK_KEY,
redirectUri: string = `${process.env.URL}/auth/slack.callback`
redirectUri = `${process.env.URL}/auth/slack.callback`
): string {
const baseUrl = "https://slack.com/oauth/authorize";
const params = {
@@ -18,11 +17,9 @@ export function slackAuth(
redirect_uri: redirectUri,
state,
};
const urlParams = Object.keys(params)
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
.join("&");
return `${baseUrl}?${urlParams}`;
}
@@ -50,7 +47,7 @@ export function changelogUrl(): string {
return `https://www.getoutline.com/changelog`;
}
export function signin(service: string = "slack"): string {
export function signin(service = "slack"): string {
return `${process.env.URL}/auth/${service}`;
}

View File

@@ -1,8 +0,0 @@
// @flow
import slugify from "slugify";
// Slugify, escape, and remove periods from headings so that they are
// compatible with url hashes AND dom selectors
export default function safeSlugify(text: string) {
return `h-${escape(slugify(text, { lower: true }).replace(".", "-"))}`;
}

10
shared/utils/slugify.ts Normal file
View File

@@ -0,0 +1,10 @@
import slugify from "slugify"; // Slugify, escape, and remove periods from headings so that they are
// compatible with url hashes AND dom selectors
export default function safeSlugify(text: string) {
return `h-${escape(
slugify(text, {
lower: true,
}).replace(".", "-")
)}`;
}

View File

@@ -1,5 +1,3 @@
// @flow
const unescape = (text: string) => {
return text
.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, "$1")

View File

@@ -1,4 +1,3 @@
// @flow
const env = typeof window !== "undefined" ? window.env : process.env;
export function cdnPath(path: string): string {

View File

@@ -1,24 +1,24 @@
// @flow
import path from "path";
// @ts-expect-error ts-migrate(2724) FIXME: '"jszip"' has no exported member named 'ZipObject'... Remove this comment to see the full error message
import JSZip, { ZipObject } from "jszip";
export type Item = {|
path: string,
dir: string,
name: string,
depth: number,
metadata: Object,
type: "collection" | "document" | "attachment",
item: ZipObject,
|};
export type Item = {
path: string;
dir: string;
name: string;
depth: number;
metadata: Record<string, any>;
type: "collection" | "document" | "attachment";
item: ZipObject;
};
export async function parseOutlineExport(
input: File | Buffer
): Promise<Item[]> {
const zip = await JSZip.loadAsync(input);
// this is so we can use async / await a little easier
let items: Item[] = [];
const items: Item[] = [];
zip.forEach(async function (rawPath, item) {
const itemPath = rawPath.replace(/\/$/, "");
const dir = path.dirname(itemPath);
@@ -32,6 +32,7 @@ export async function parseOutlineExport(
// attempt to parse extra metadata from zip comment
let metadata = {};
try {
metadata = item.comment ? JSON.parse(item.comment) : {};
} catch (err) {
@@ -47,12 +48,15 @@ export async function parseOutlineExport(
}
let type;
if (depth === 0 && item.dir && name) {
type = "collection";
}
if (depth > 0 && !item.dir && item.name.endsWith(".md")) {
type = "document";
}
if (depth > 0 && !item.dir && itemPath.includes("uploads")) {
type = "attachment";
}
@@ -66,11 +70,11 @@ export async function parseOutlineExport(
dir,
name,
depth,
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type '"collecti... Remove this comment to see the full error message
type,
metadata,
item,
});
});
return items;
}