Update app and tooling

This commit is contained in:
Lawrence Chen 2026-01-29 17:36:26 -08:00
parent 3046531bdd
commit e620ec7349
4950 changed files with 2975120 additions and 10 deletions

57
node_modules/web-vitals/src/getCLS.ts generated vendored Normal file
View file

@ -0,0 +1,57 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {initMetric} from './lib/initMetric.js';
import {observe, PerformanceEntryHandler} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js';
import {bindReporter} from './lib/bindReporter.js';
import {ReportHandler} from './types.js';
// https://wicg.github.io/layout-instability/#sec-layout-shift
interface LayoutShift extends PerformanceEntry {
value: number;
hadRecentInput: boolean;
}
export const getCLS = (onReport: ReportHandler, reportAllChanges = false) => {
const metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;
const entryHandler = (entry: LayoutShift) => {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
(metric.value as number) += entry.value;
metric.entries.push(entry);
report();
}
};
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
if (po) {
report = bindReporter(onReport, metric, po, reportAllChanges);
onHidden(({isUnloading}) => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
if (isUnloading) {
metric.isFinal = true;
}
report();
});
}
};

46
node_modules/web-vitals/src/getFCP.ts generated vendored Normal file
View file

@ -0,0 +1,46 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {bindReporter} from './lib/bindReporter.js';
import {getFirstHidden} from './lib/getFirstHidden.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {ReportHandler} from './types.js';
export const getFCP = (onReport: ReportHandler) => {
const metric = initMetric('FCP');
const firstHidden = getFirstHidden();
let report: ReturnType<typeof bindReporter>;
const entryHandler = (entry: PerformanceEntry) => {
if (entry.name === 'first-contentful-paint') {
// Only report if the page wasn't hidden prior to the first paint.
if (entry.startTime < firstHidden.timeStamp) {
metric.value = entry.startTime;
metric.isFinal = true;
metric.entries.push(entry);
report();
}
}
};
const po = observe('paint', entryHandler);
if (po) {
report = bindReporter(onReport, metric, po);
}
};

88
node_modules/web-vitals/src/getFID.ts generated vendored Normal file
View file

@ -0,0 +1,88 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {bindReporter} from './lib/bindReporter.js';
import {getFirstHidden} from './lib/getFirstHidden.js';
import {initMetric} from './lib/initMetric.js';
import {observe, PerformanceEntryHandler} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js';
import {ReportHandler} from './types.js';
interface FIDPolyfillCallback {
(value: number, event: Event): void;
}
interface FIDPolyfill {
onFirstInputDelay: (onReport: FIDPolyfillCallback) => void;
}
declare global {
interface Window {
perfMetrics: FIDPolyfill;
}
}
// https://wicg.github.io/event-timing/#sec-performance-event-timing
interface PerformanceEventTiming extends PerformanceEntry {
processingStart: DOMHighResTimeStamp;
cancelable?: boolean;
target?: Element;
}
export const getFID = (onReport: ReportHandler) => {
const metric = initMetric('FID');
const firstHidden = getFirstHidden();
const entryHandler = (entry: PerformanceEventTiming) => {
// Only report if the page wasn't hidden prior to the first input.
if (entry.startTime < firstHidden.timeStamp) {
metric.value = entry.processingStart - entry.startTime;
metric.entries.push(entry);
metric.isFinal = true;
report();
}
};
const po = observe('first-input', entryHandler as PerformanceEntryHandler);
const report = bindReporter(onReport, metric, po);
if (po) {
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
po.disconnect();
}, true);
} else {
if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) {
window.perfMetrics.onFirstInputDelay((value: number, event: Event) => {
// Only report if the page wasn't hidden prior to the first input.
if (event.timeStamp < firstHidden.timeStamp) {
metric.value = value;
metric.isFinal = true;
metric.entries = [{
entryType: 'first-input',
name: event.type,
target: event.target,
cancelable: event.cancelable,
startTime: event.timeStamp,
processingStart: event.timeStamp + value,
} as PerformanceEventTiming];
report();
}
});
}
}
};

65
node_modules/web-vitals/src/getLCP.ts generated vendored Normal file
View file

@ -0,0 +1,65 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {bindReporter} from './lib/bindReporter.js';
import {getFirstHidden} from './lib/getFirstHidden.js';
import {initMetric} from './lib/initMetric.js';
import {observe, PerformanceEntryHandler} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js';
import {whenInput} from './lib/whenInput.js';
import {ReportHandler} from './types.js';
export const getLCP = (onReport: ReportHandler, reportAllChanges = false) => {
const metric = initMetric('LCP');
const firstHidden = getFirstHidden();
let report: ReturnType<typeof bindReporter>;
const entryHandler = (entry: PerformanceEntry) => {
// The startTime attribute returns the value of the renderTime if it is not 0,
// and the value of the loadTime otherwise.
const value = entry.startTime;
// If the page was hidden prior to paint time of the entry,
// ignore it and mark the metric as final, otherwise add the entry.
if (value < firstHidden.timeStamp) {
metric.value = value;
metric.entries.push(entry);
} else {
metric.isFinal = true;
}
report();
};
const po = observe('largest-contentful-paint', entryHandler);
if (po) {
report = bindReporter(onReport, metric, po, reportAllChanges);
const onFinal = () => {
if (!metric.isFinal) {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
metric.isFinal = true;
report();
}
}
whenInput().then(onFinal);
onHidden(onFinal, true);
}
};

117
node_modules/web-vitals/src/getTTFB.ts generated vendored Normal file
View file

@ -0,0 +1,117 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {initMetric} from './lib/initMetric.js';
import {ReportHandler} from './types.js';
interface NavigationEntryShim {
// From `PerformanceNavigationTimingEntry`.
entryType: string;
startTime: number;
// From `performance.timing`.
connectEnd?: number;
connectStart?: number;
domComplete?: number;
domContentLoadedEventEnd?: number;
domContentLoadedEventStart?: number;
domInteractive?: number;
domainLookupEnd?: number;
domainLookupStart?: number;
fetchStart?: number;
loadEventEnd?: number;
loadEventStart?: number;
redirectEnd?: number;
redirectStart?: number;
requestStart?: number;
responseEnd?: number;
responseStart?: number;
secureConnectionStart?: number;
unloadEventEnd?: number;
unloadEventStart?: number;
}
type PerformanceTimingKeys =
'connectEnd' |
'connectStart' |
'domComplete' |
'domContentLoadedEventEnd' |
'domContentLoadedEventStart' |
'domInteractive' |
'domainLookupEnd' |
'domainLookupStart' |
'fetchStart' |
'loadEventEnd' |
'loadEventStart' |
'redirectEnd' |
'redirectStart' |
'requestStart' |
'responseEnd' |
'responseStart' |
'secureConnectionStart' |
'unloadEventEnd' |
'unloadEventStart';
const afterLoad = (callback: () => void) => {
if (document.readyState === 'complete') {
// Queue a task so the callback runs after `loadEventEnd`.
setTimeout(callback, 0);
} else {
// Use `pageshow` so the callback runs after `loadEventEnd`.
addEventListener('pageshow', callback);
}
}
const getNavigationEntryFromPerformanceTiming = () => {
// Really annoying that TypeScript errors when using `PerformanceTiming`.
const timing = performance.timing;
const navigationEntry: NavigationEntryShim = {
entryType: 'navigation',
startTime: 0,
};
for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
navigationEntry[key as PerformanceTimingKeys] = Math.max(
timing[key as PerformanceTimingKeys] - timing.navigationStart, 0);
}
}
return navigationEntry as PerformanceNavigationTiming;
};
export const getTTFB = (onReport: ReportHandler) => {
const metric = initMetric('TTFB');
afterLoad(() => {
try {
// Use the NavigationTiming L2 entry if available.
const navigationEntry = performance.getEntriesByType('navigation')[0] ||
getNavigationEntryFromPerformanceTiming();
metric.value = metric.delta =
(navigationEntry as PerformanceNavigationTiming).responseStart;
metric.entries = [navigationEntry];
metric.isFinal = true;
onReport(metric);
} catch (error) {
// Do nothing.
}
});
};

23
node_modules/web-vitals/src/index.ts generated vendored Normal file
View file

@ -0,0 +1,23 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {getCLS} from './getCLS.js';
export {getFCP} from './getFCP.js';
export {getFID} from './getFID.js';
export {getLCP} from './getLCP.js';
export {getTTFB} from './getTTFB.js';
export * from './types.js';

48
node_modules/web-vitals/src/lib/bindReporter.ts generated vendored Normal file
View file

@ -0,0 +1,48 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Metric, ReportHandler} from '../types.js';
export const bindReporter = (
callback: ReportHandler,
metric: Metric,
po: PerformanceObserver | undefined,
observeAllUpdates?: boolean,
) => {
let prevValue: number;
return () => {
if (po && metric.isFinal) {
po.disconnect();
}
if (metric.value >= 0) {
if (observeAllUpdates ||
metric.isFinal ||
document.visibilityState === 'hidden') {
metric.delta = metric.value - (prevValue || 0);
// Report the metric if there's a non-zero delta, if the metric is
// final, or if no previous value exists (which can happen in the case
// of the document becoming hidden when the metric value is 0).
// See: https://github.com/GoogleChrome/web-vitals/issues/14
if (metric.delta || metric.isFinal || prevValue === undefined) {
callback(metric);
prevValue = metric.value;
}
}
}
}
}

24
node_modules/web-vitals/src/lib/generateUniqueID.ts generated vendored Normal file
View file

@ -0,0 +1,24 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Performantly generate a unique, 27-char string by combining the current
* timestamp with a 13-digit random number.
* @return {string}
*/
export const generateUniqueID = () => {
return `${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};

39
node_modules/web-vitals/src/lib/getFirstHidden.ts generated vendored Normal file
View file

@ -0,0 +1,39 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {onHidden} from './onHidden.js';
let firstHiddenTime: number
export const getFirstHidden = () => {
if (firstHiddenTime === undefined) {
// If the document is hidden when this code runs, assume it was hidden
// since navigation start. This isn't a perfect heuristic, but it's the
// best we can do until an API is available to support querying past
// visibilityState.
firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
// Update the time if/when the document becomes hidden.
onHidden(({timeStamp}) => firstHiddenTime = timeStamp, true);
}
return {
get timeStamp() {
return firstHiddenTime;
}
}
};

30
node_modules/web-vitals/src/lib/initMetric.ts generated vendored Normal file
View file

@ -0,0 +1,30 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Metric} from '../types.js';
import {generateUniqueID} from './generateUniqueID.js';
export const initMetric = (name: Metric['name'], value = -1): Metric => {
return {
name,
value,
delta: 0,
entries: [],
id: generateUniqueID(),
isFinal: false
};
};

45
node_modules/web-vitals/src/lib/observe.ts generated vendored Normal file
View file

@ -0,0 +1,45 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface PerformanceEntryHandler {
(entry: PerformanceEntry): void;
}
/**
* Takes a performance entry type and a callback function, and creates a
* `PerformanceObserver` instance that will observe the specified entry type
* with buffering enabled and call the callback _for each entry_.
*
* This function also feature-detects entry support and wraps the logic in a
* try/catch to avoid errors in unsupporting browsers.
*/
export const observe = (
type: string,
callback: PerformanceEntryHandler,
): PerformanceObserver | undefined => {
try {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const po: PerformanceObserver =
new PerformanceObserver((l) => l.getEntries().map(callback));
po.observe({type, buffered: true});
return po;
}
} catch (e) {
// Do nothing.
}
return;
};

50
node_modules/web-vitals/src/lib/onHidden.ts generated vendored Normal file
View file

@ -0,0 +1,50 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface OnHiddenCallback {
// TODO(philipwalton): add `isPersisted` if needed for bfcache.
({timeStamp, isUnloading}: {timeStamp: number; isUnloading: boolean}): void;
}
let isUnloading = false;
let listenersAdded = false;
const onPageHide = (event: PageTransitionEvent) => {
isUnloading = !event.persisted;
};
const addListeners = () => {
addEventListener('pagehide', onPageHide);
// `beforeunload` is needed to fix this bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=987409
// eslint-disable-next-line @typescript-eslint/no-empty-function
addEventListener('beforeunload', () => {});
}
export const onHidden = (cb: OnHiddenCallback, once = false) => {
if (!listenersAdded) {
addListeners();
listenersAdded = true;
}
addEventListener('visibilitychange', ({timeStamp}) => {
if (document.visibilityState === 'hidden') {
cb({timeStamp, isUnloading});
}
}, {capture: true, once});
};

32
node_modules/web-vitals/src/lib/whenInput.ts generated vendored Normal file
View file

@ -0,0 +1,32 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
let inputPromise: Promise<Event>;
export const whenInput = () => {
if (!inputPromise) {
inputPromise = new Promise((r) => {
return ['scroll', 'keydown', 'pointerdown'].map((type) => {
addEventListener(type, r, {
once: true,
passive: true,
capture: true,
});
});
});
}
return inputPromise;
};

45
node_modules/web-vitals/src/types.ts generated vendored Normal file
View file

@ -0,0 +1,45 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Metric {
// The name of the metric (in acronym form).
name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB';
// The current value of the metric.
value: number;
// The delta between the current value and the last-reported value.
// On the first report, `delta` and `value` will always be the same.
delta: number;
// A unique ID representing this particular metric that's specific to the
// current page. This ID can be used by an analytics tool to dedupe
// multiple values sent for the same metric, or to group multiple deltas
// together and calculate a total.
id: string;
// `false` if the value of the metric may change in the future,
// for the current page.
isFinal: boolean;
// Any performance entries used in the metric value calculation.
// Note, entries will be added to the array as the value changes.
entries: PerformanceEntry[];
}
export interface ReportHandler {
(metric: Metric): void;
}