mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
Load chrono-node lazily to keep it out of the main bundle
Convert parseNaturalLanguageDate to dynamically import chrono-node on first use so the bundler splits it into a separate chunk fetched only when a date is actually parsed. The mention menu now resolves the parse asynchronously in an effect.
This commit is contained in:
@@ -67,9 +67,27 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
const maxResultsInSection = search ? 25 : 5;
|
||||
|
||||
// Surface a date suggestion when the search query parses as a natural
|
||||
// language date (e.g. "tomorrow", "next friday", "jan 2").
|
||||
const parsedDate = search ? parseNaturalLanguageDate(search) : null;
|
||||
const parsedISODate = parsedDate ? toISODate(parsedDate) : undefined;
|
||||
// language date (e.g. "tomorrow", "next friday", "jan 2"). Parsing is
|
||||
// asynchronous as chrono-node is loaded lazily, so the result is held in
|
||||
// state and applied once resolved.
|
||||
const [parsedISODate, setParsedISODate] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!search) {
|
||||
setParsedISODate(undefined);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void parseNaturalLanguageDate(search).then((date) => {
|
||||
if (!cancelled) {
|
||||
setParsedISODate(date ? toISODate(date) : undefined);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [search]);
|
||||
|
||||
const dateItems: MentionItem[] =
|
||||
actorId && parsedISODate
|
||||
? [
|
||||
|
||||
@@ -3,42 +3,42 @@ import { parseNaturalLanguageDate } from "./parseNaturalLanguageDate";
|
||||
describe("parseNaturalLanguageDate", () => {
|
||||
const reference = new Date(2024, 0, 1); // Mon Jan 1, 2024
|
||||
|
||||
it("returns null for empty input", () => {
|
||||
expect(parseNaturalLanguageDate("", reference)).toBeNull();
|
||||
expect(parseNaturalLanguageDate(" ", reference)).toBeNull();
|
||||
it("returns null for empty input", async () => {
|
||||
expect(await parseNaturalLanguageDate("", reference)).toBeNull();
|
||||
expect(await parseNaturalLanguageDate(" ", reference)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for non-date input", () => {
|
||||
expect(parseNaturalLanguageDate("hello world", reference)).toBeNull();
|
||||
it("returns null for non-date input", async () => {
|
||||
expect(await parseNaturalLanguageDate("hello world", reference)).toBeNull();
|
||||
});
|
||||
|
||||
it("parses 'today'", () => {
|
||||
const result = parseNaturalLanguageDate("today", reference);
|
||||
it("parses 'today'", async () => {
|
||||
const result = await parseNaturalLanguageDate("today", reference);
|
||||
expect(result).toEqual(new Date(2024, 0, 1));
|
||||
});
|
||||
|
||||
it("parses 'tomorrow'", () => {
|
||||
const result = parseNaturalLanguageDate("tomorrow", reference);
|
||||
it("parses 'tomorrow'", async () => {
|
||||
const result = await parseNaturalLanguageDate("tomorrow", reference);
|
||||
expect(result).toEqual(new Date(2024, 0, 2));
|
||||
});
|
||||
|
||||
it("parses 'yesterday'", () => {
|
||||
const result = parseNaturalLanguageDate("yesterday", reference);
|
||||
it("parses 'yesterday'", async () => {
|
||||
const result = await parseNaturalLanguageDate("yesterday", reference);
|
||||
expect(result).toEqual(new Date(2023, 11, 31));
|
||||
});
|
||||
|
||||
it("parses 'in 3 days'", () => {
|
||||
const result = parseNaturalLanguageDate("in 3 days", reference);
|
||||
it("parses 'in 3 days'", async () => {
|
||||
const result = await parseNaturalLanguageDate("in 3 days", reference);
|
||||
expect(result).toEqual(new Date(2024, 0, 4));
|
||||
});
|
||||
|
||||
it("parses an explicit month and day", () => {
|
||||
const result = parseNaturalLanguageDate("February 3", reference);
|
||||
it("parses an explicit month and day", async () => {
|
||||
const result = await parseNaturalLanguageDate("February 3", reference);
|
||||
expect(result).toEqual(new Date(2024, 1, 3));
|
||||
});
|
||||
|
||||
it("normalizes the time component to local midnight", () => {
|
||||
const result = parseNaturalLanguageDate("tomorrow at 5pm", reference);
|
||||
it("normalizes the time component to local midnight", async () => {
|
||||
const result = await parseNaturalLanguageDate("tomorrow at 5pm", reference);
|
||||
expect(result?.getHours()).toBe(0);
|
||||
expect(result?.getMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
import * as chrono from "chrono-node";
|
||||
// Type-only import is fully erased at compile time, so it does not pull
|
||||
// chrono-node into the bundle.
|
||||
import type * as Chrono from "chrono-node";
|
||||
|
||||
/**
|
||||
* chrono-node is a sizeable dependency, so it is loaded lazily on first use
|
||||
* via a dynamic import. The bundler splits it into its own chunk that is only
|
||||
* fetched when a date actually needs to be parsed (i.e. when the user types in
|
||||
* the mention menu), keeping it out of the main bundle.
|
||||
*/
|
||||
let chronoPromise: Promise<typeof Chrono> | undefined;
|
||||
|
||||
function loadChrono(): Promise<typeof Chrono> {
|
||||
if (!chronoPromise) {
|
||||
chronoPromise = import("chrono-node");
|
||||
}
|
||||
return chronoPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a natural language string such as "tomorrow", "next friday",
|
||||
@@ -6,23 +23,25 @@ import * as chrono from "chrono-node";
|
||||
*
|
||||
* The time component is intentionally discarded as date mentions are
|
||||
* day-granular; only the year, month and day of the matched date are
|
||||
* returned.
|
||||
* returned. chrono-node is loaded asynchronously the first time this is
|
||||
* called.
|
||||
*
|
||||
* @param input the natural language string to parse.
|
||||
* @param referenceDate the date relative to which terms like "tomorrow"
|
||||
* are resolved, defaults to now.
|
||||
* @returns the matched date with the time set to local midnight, or null
|
||||
* when no date could be confidently parsed.
|
||||
* @returns a promise resolving to the matched date with the time set to
|
||||
* local midnight, or null when no date could be confidently parsed.
|
||||
*/
|
||||
export function parseNaturalLanguageDate(
|
||||
export async function parseNaturalLanguageDate(
|
||||
input: string,
|
||||
referenceDate: Date = new Date()
|
||||
): Date | null {
|
||||
): Promise<Date | null> {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chrono = await loadChrono();
|
||||
const results = chrono.parse(trimmed, referenceDate, { forwardDate: true });
|
||||
const result = results[0];
|
||||
if (!result) {
|
||||
|
||||
Reference in New Issue
Block a user