Initial commit: Developer Tools MVP with visual editor

- Complete React app with 7 developer tools
- JSON Tool with visual structured editor
- Serialize Tool with visual structured editor
- URL, Base64, CSV/JSON, Beautifier, Diff tools
- Responsive navigation with dropdown menu
- Dark/light mode toggle
- Mobile-responsive design with sticky header
- All tools working with copy/paste functionality
This commit is contained in:
dwindown
2025-08-02 09:31:26 +07:00
commit 7f2dd5260f
45657 changed files with 4730486 additions and 0 deletions

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

@@ -0,0 +1,103 @@
/*
* 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 {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {bindReporter} from './lib/bindReporter.js';
import {getFCP} from './getFCP.js';
import {ReportHandler} from './types.js';
// https://wicg.github.io/layout-instability/#sec-layout-shift
interface LayoutShift extends PerformanceEntry {
value: number;
hadRecentInput: boolean;
}
let isMonitoringFCP = false;
let fcpValue = -1;
export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean) => {
// Start monitoring FCP so we can only report CLS if FCP is also reported.
// Note: this is done to match the current behavior of CrUX.
if (!isMonitoringFCP) {
getFCP((metric) => {
fcpValue = metric.value;
});
isMonitoringFCP = true;
}
const onReportWrapped: ReportHandler = (arg) => {
if (fcpValue > -1) {
onReport(arg);
}
};
let metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const entryHandler = (entry: LayoutShift) => {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue > metric.value) {
metric.value = sessionValue;
metric.entries = sessionEntries;
report();
}
}
};
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
if (po) {
report = bindReporter(onReportWrapped, metric, reportAllChanges);
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
report(true);
});
onBFCacheRestore(() => {
sessionValue = 0;
fcpValue = -1;
metric = initMetric('CLS', 0);
report = bindReporter(onReportWrapped, metric, reportAllChanges);
});
}
};

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

@@ -0,0 +1,74 @@
/*
* 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 {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {ReportHandler} from './types.js';
export const getFCP = (onReport: ReportHandler, reportAllChanges?: boolean) => {
const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('FCP');
let report: ReturnType<typeof bindReporter>;
const entryHandler = (entry: PerformanceEntry) => {
if (entry.name === 'first-contentful-paint') {
if (po) {
po.disconnect();
}
// Only report if the page wasn't hidden prior to the first paint.
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
metric.value = entry.startTime;
metric.entries.push(entry);
report(true);
}
}
};
// TODO(philipwalton): remove the use of `fcpEntry` once this bug is fixed.
// https://bugs.webkit.org/show_bug.cgi?id=225305
// The check for `getEntriesByName` is needed to support Opera:
// https://github.com/GoogleChrome/web-vitals/issues/159
// The check for `window.performance` is needed to support Opera mini:
// https://github.com/GoogleChrome/web-vitals/issues/185
const fcpEntry = window.performance && performance.getEntriesByName &&
performance.getEntriesByName('first-contentful-paint')[0];
const po = fcpEntry ? null : observe('paint', entryHandler);
if (fcpEntry || po) {
report = bindReporter(onReport, metric, reportAllChanges);
if (fcpEntry) {
entryHandler(fcpEntry);
}
onBFCacheRestore((event) => {
metric = initMetric('FCP');
report = bindReporter(onReport, metric, reportAllChanges);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
report(true);
});
});
});
}
};

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

@@ -0,0 +1,73 @@
/*
* 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 {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe, PerformanceEntryHandler} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {onHidden} from './lib/onHidden.js';
import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js';
import {FirstInputPolyfillCallback, PerformanceEventTiming, ReportHandler} from './types.js';
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean) => {
const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('FID');
let report: ReturnType<typeof bindReporter>;
const entryHandler = (entry: PerformanceEventTiming) => {
// Only report if the page wasn't hidden prior to the first input.
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
metric.value = entry.processingStart - entry.startTime;
metric.entries.push(entry);
report(true);
}
};
const po = observe('first-input', entryHandler as PerformanceEntryHandler);
report = bindReporter(onReport, metric, reportAllChanges);
if (po) {
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
po.disconnect();
}, true);
}
if (window.__WEB_VITALS_POLYFILL__) {
// Prefer the native implementation if available,
if (!po) {
window.webVitals.firstInputPolyfill(entryHandler as FirstInputPolyfillCallback)
}
onBFCacheRestore(() => {
metric = initMetric('FID');
report = bindReporter(onReport, metric, reportAllChanges);
window.webVitals.resetFirstInputPolyfill();
window.webVitals.firstInputPolyfill(entryHandler as FirstInputPolyfillCallback);
});
} else {
// Only monitor bfcache restores if the browser supports FID natively.
if (po) {
onBFCacheRestore(() => {
metric = initMetric('FID');
report = bindReporter(onReport, metric, reportAllChanges);
resetFirstInputPolyfill();
firstInputPolyfill(entryHandler as FirstInputPolyfillCallback);
});
}
}
};

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

@@ -0,0 +1,82 @@
/*
* 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 {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe, PerformanceEntryHandler} from './lib/observe.js';
import {onBFCacheRestore} from './lib/onBFCacheRestore.js';
import {onHidden} from './lib/onHidden.js';
import {ReportHandler} from './types.js';
const reportedMetricIDs: Record<string, boolean> = {};
export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean) => {
const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('LCP');
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 < visibilityWatcher.firstHiddenTime) {
metric.value = value;
metric.entries.push(entry);
report();
}
};
const po = observe('largest-contentful-paint', entryHandler);
if (po) {
report = bindReporter(onReport, metric, reportAllChanges);
const stopListening = () => {
if (!reportedMetricIDs[metric.id]) {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
po.disconnect();
reportedMetricIDs[metric.id] = true;
report(true);
}
}
// Stop listening after input. Note: while scrolling is an input that
// stop LCP observation, it's unreliable since it can be programmatically
// generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
['keydown', 'click'].forEach((type) => {
addEventListener(type, stopListening, {once: true, capture: true});
});
onHidden(stopListening, true);
onBFCacheRestore((event) => {
metric = initMetric('LCP');
report = bindReporter(onReport, metric, reportAllChanges);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
reportedMetricIDs[metric.id] = true;
report(true);
});
});
});
}
};

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

@@ -0,0 +1,75 @@
/*
* 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, NavigationTimingPolyfillEntry} from './types.js';
const afterLoad = (callback: () => void) => {
if (document.readyState === 'complete') {
// Queue a task so the callback runs after `loadEventEnd`.
setTimeout(callback, 0);
} else {
// Queue a task so the callback runs after `loadEventEnd`.
addEventListener('load', () => setTimeout(callback, 0));
}
}
const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => {
// Really annoying that TypeScript errors when using `PerformanceTiming`.
const timing = performance.timing;
const navigationEntry: {[key: string]: number | string} = {
entryType: 'navigation',
startTime: 0,
};
for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
navigationEntry[key] = Math.max(
(timing[key as keyof PerformanceTiming] as number) -
timing.navigationStart, 0);
}
}
return navigationEntry as unknown as NavigationTimingPolyfillEntry;
};
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;
// In some cases the value reported is negative or is larger
// than the current page time. Ignore these cases:
// https://github.com/GoogleChrome/web-vitals/issues/137
// https://github.com/GoogleChrome/web-vitals/issues/162
if (metric.value < 0 || metric.value > performance.now()) return;
metric.entries = [navigationEntry];
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';

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

@@ -0,0 +1,42 @@
/*
* 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,
reportAllChanges?: boolean,
) => {
let prevValue: number;
return (forceReport?: boolean) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
metric.delta = metric.value - (prevValue || 0);
// Report the metric if there's a non-zero delta 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 || prevValue === undefined) {
prevValue = metric.value;
callback(metric);
}
}
}
}
}

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, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
export const generateUniqueID = () => {
return `v2-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};

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 {onBFCacheRestore} from './onBFCacheRestore.js';
import {onHidden} from './onHidden.js';
let firstHiddenTime = -1;
const initHiddenTime = () => {
return document.visibilityState === 'hidden' ? 0 : Infinity;
}
const trackChanges = () => {
// Update the time if/when the document becomes hidden.
onHidden(({timeStamp}) => {
firstHiddenTime = timeStamp
}, true);
};
export const getVisibilityWatcher = () => {
if (firstHiddenTime < 0) {
// 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.
if (window.__WEB_VITALS_POLYFILL__) {
firstHiddenTime = window.webVitals.firstHiddenTime;
if (firstHiddenTime === Infinity) {
trackChanges();
}
} else {
firstHiddenTime = initHiddenTime();
trackChanges();
}
// Reset the time on bfcache restores.
onBFCacheRestore(() => {
// Schedule a task in order to track the `visibilityState` once it's
// had an opportunity to change to visible in all browsers.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1133363
setTimeout(() => {
firstHiddenTime = initHiddenTime();
trackChanges();
}, 0);
});
}
return {
get firstHiddenTime() {
return firstHiddenTime;
}
}
};

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

@@ -0,0 +1,29 @@
/*
* 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?: number): Metric => {
return {
name,
value: typeof value === 'undefined' ? -1 : value,
delta: 0,
entries: [],
id: generateUniqueID()
};
};

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

@@ -0,0 +1,51 @@
/*
* 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)) {
// More extensive feature detect needed for Firefox due to:
// https://github.com/GoogleChrome/web-vitals/issues/142
if (type === 'first-input' && !('PerformanceEventTiming' in self)) {
return;
}
const po: PerformanceObserver =
new PerformanceObserver((l) => l.getEntries().map(callback));
po.observe({type, buffered: true});
return po;
}
} catch (e) {
// Do nothing.
}
return;
};

27
node_modules/web-vitals/src/lib/onBFCacheRestore.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/*
* 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.
*/
interface onBFCacheRestoreCallback {
(event: PageTransitionEvent): void;
}
export const onBFCacheRestore = (cb: onBFCacheRestoreCallback) => {
addEventListener('pageshow', (event) => {
if (event.persisted) {
cb(event);
}
}, true);
};

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

@@ -0,0 +1,37 @@
/*
* 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 {
(event: Event): void;
}
export const onHidden = (cb: OnHiddenCallback, once?: boolean) => {
const onHiddenOrPageHide = (event: Event) => {
if (event.type === 'pagehide' || document.visibilityState === 'hidden') {
cb(event);
if (once) {
removeEventListener('visibilitychange', onHiddenOrPageHide, true);
removeEventListener('pagehide', onHiddenOrPageHide, true);
}
}
}
addEventListener('visibilitychange', onHiddenOrPageHide, true);
// Some browsers have buggy implementations of visibilitychange,
// so we use pagehide in addition, just to be safe.
addEventListener('pagehide', onHiddenOrPageHide, true);
};

View File

@@ -0,0 +1,174 @@
/*
* 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 {FirstInputPolyfillEntry, FirstInputPolyfillCallback} from '../../types.js';
type addOrRemoveEventListener =
typeof addEventListener | typeof removeEventListener;
let firstInputEvent: Event|null;
let firstInputDelay: number;
let firstInputTimeStamp: Date;
let callbacks: FirstInputPolyfillCallback[]
const listenerOpts: AddEventListenerOptions = {passive: true, capture: true};
const startTimeStamp: Date = new Date();
/**
* Accepts a callback to be invoked once the first input delay and event
* are known.
*/
export const firstInputPolyfill = (
onFirstInput: FirstInputPolyfillCallback
) => {
callbacks.push(onFirstInput);
reportFirstInputDelayIfRecordedAndValid();
}
export const resetFirstInputPolyfill = () => {
callbacks = [];
firstInputDelay = -1;
firstInputEvent = null;
eachEventType(addEventListener);
}
/**
* Records the first input delay and event, so subsequent events can be
* ignored. All added event listeners are then removed.
*/
const recordFirstInputDelay = (delay: number, event: Event) => {
if (!firstInputEvent) {
firstInputEvent = event;
firstInputDelay = delay;
firstInputTimeStamp = new Date;
eachEventType(removeEventListener);
reportFirstInputDelayIfRecordedAndValid();
}
}
/**
* Reports the first input delay and event (if they're recorded and valid)
* by running the array of callback functions.
*/
const reportFirstInputDelayIfRecordedAndValid = () => {
// In some cases the recorded delay is clearly wrong, e.g. it's negative
// or it's larger than the delta between now and initialization.
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/6
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/7
if (firstInputDelay >= 0 &&
// @ts-ignore (subtracting two dates always returns a number)
firstInputDelay < firstInputTimeStamp - startTimeStamp) {
const entry = {
entryType: 'first-input',
name: firstInputEvent!.type,
target: firstInputEvent!.target,
cancelable: firstInputEvent!.cancelable,
startTime: firstInputEvent!.timeStamp,
processingStart: firstInputEvent!.timeStamp + firstInputDelay,
} as FirstInputPolyfillEntry;
callbacks.forEach(function(callback) {
callback(entry);
});
callbacks = [];
}
}
/**
* Handles pointer down events, which are a special case.
* Pointer events can trigger main or compositor thread behavior.
* We differentiate these cases based on whether or not we see a
* 'pointercancel' event, which are fired when we scroll. If we're scrolling
* we don't need to report input delay since FID excludes scrolling and
* pinch/zooming.
*/
const onPointerDown = (delay: number, event: Event) => {
/**
* Responds to 'pointerup' events and records a delay. If a pointer up event
* is the next event after a pointerdown event, then it's not a scroll or
* a pinch/zoom.
*/
const onPointerUp = () => {
recordFirstInputDelay(delay, event);
removePointerEventListeners();
}
/**
* Responds to 'pointercancel' events and removes pointer listeners.
* If a 'pointercancel' is the next event to fire after a pointerdown event,
* it means this is a scroll or pinch/zoom interaction.
*/
const onPointerCancel = () => {
removePointerEventListeners();
}
/**
* Removes added pointer event listeners.
*/
const removePointerEventListeners = () => {
removeEventListener('pointerup', onPointerUp, listenerOpts);
removeEventListener('pointercancel', onPointerCancel, listenerOpts);
}
addEventListener('pointerup', onPointerUp, listenerOpts);
addEventListener('pointercancel', onPointerCancel, listenerOpts);
}
/**
* Handles all input events and records the time between when the event
* was received by the operating system and when it's JavaScript listeners
* were able to run.
*/
const onInput = (event: Event) => {
// Only count cancelable events, which should trigger behavior
// important to the user.
if (event.cancelable) {
// In some browsers `event.timeStamp` returns a `DOMTimeStamp` value
// (epoch time) instead of the newer `DOMHighResTimeStamp`
// (document-origin time). To check for that we assume any timestamp
// greater than 1 trillion is a `DOMTimeStamp`, and compare it using
// the `Date` object rather than `performance.now()`.
// - https://github.com/GoogleChromeLabs/first-input-delay/issues/4
const isEpochTime = event.timeStamp > 1e12;
const now = isEpochTime ? new Date : performance.now();
// Input delay is the delta between when the system received the event
// (e.g. event.timeStamp) and when it could run the callback (e.g. `now`).
const delay = now as number - event.timeStamp;
if (event.type == 'pointerdown') {
onPointerDown(delay, event);
} else {
recordFirstInputDelay(delay, event);
}
}
}
/**
* Invokes the passed callback const for = each event type with t =>he
* `onInput` const and = `listenerOpts =>`.
*/
const eachEventType = (callback: addOrRemoveEventListener) => {
const eventTypes = [
'mousedown',
'keydown',
'touchstart',
'pointerdown',
];
eventTypes.forEach((type) => callback(type, onInput, listenerOpts));
}

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.
*/
let firstHiddenTime =
document.visibilityState === 'hidden' ? 0 : Infinity;
const onVisibilityChange = (event: Event) => {
if (document.visibilityState === 'hidden') {
firstHiddenTime = event.timeStamp;
removeEventListener('visibilitychange', onVisibilityChange, true);
}
}
// Note: do not add event listeners unconditionally (outside of polyfills).
addEventListener('visibilitychange', onVisibilityChange, true);
export const getFirstHiddenTime = () => firstHiddenTime;

27
node_modules/web-vitals/src/polyfill.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/*
* 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 {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js';
import {getFirstHiddenTime} from './lib/polyfills/getFirstHiddenTimePolyfill.js';
resetFirstInputPolyfill();
self.webVitals = {
firstInputPolyfill: firstInputPolyfill,
resetFirstInputPolyfill: resetFirstInputPolyfill,
get firstHiddenTime() {
return getFirstHiddenTime();
},
};

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

@@ -0,0 +1,79 @@
/*
* 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 instance. This ID can
// be used by an analytics tool to dedupe multiple values sent for the same
// metric instance, or to group multiple deltas together and calculate a
// total. It can also be used to differentiate multiple different metric
// instances sent from the same page, which can happen if the page is
// restored from the back/forward cache (in that case new metrics object
// get created).
id: string;
// Any performance entries used in the metric value calculation.
// Note, entries will be added to the array as the value changes.
entries: (PerformanceEntry | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];
}
export interface ReportHandler {
(metric: Metric): void;
}
// https://wicg.github.io/event-timing/#sec-performance-event-timing
export interface PerformanceEventTiming extends PerformanceEntry {
processingStart: DOMHighResTimeStamp;
processingEnd: DOMHighResTimeStamp;
duration: DOMHighResTimeStamp;
cancelable?: boolean;
target?: Element;
}
export type FirstInputPolyfillEntry =
Omit<PerformanceEventTiming, 'processingEnd' | 'toJSON'>
export interface FirstInputPolyfillCallback {
(entry: FirstInputPolyfillEntry): void;
}
export type NavigationTimingPolyfillEntry = Omit<PerformanceNavigationTiming,
'initiatorType' | 'nextHopProtocol' | 'redirectCount' | 'transferSize' |
'encodedBodySize' | 'decodedBodySize' | 'toJSON'>
export interface WebVitalsGlobal {
firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void;
resetFirstInputPolyfill: () => void;
firstHiddenTime: number;
}
declare global {
interface Window {
webVitals: WebVitalsGlobal;
// Build flags:
__WEB_VITALS_POLYFILL__: boolean;
}
}