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:
Claude
2026-06-07 19:46:37 +00:00
parent 31e111e4d8
commit c46a5a7d1e
3 changed files with 63 additions and 26 deletions
+21 -3
View File
@@ -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
? [
+17 -17
View File
@@ -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);
});
+25 -6
View File
@@ -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) {