feat(blog): add file-based blog with dynamic slugs, MDX content and layout shell

- Introduced blog routing using Next.js App Router
- Implemented dynamic [slug] pages for blog posts
- Added MDX-based content loading via lib/posts
- Integrated shared TopBar layout with navigation
- Established clear content, lib and component separation
This commit is contained in:
PascalSchattenburg
2026-01-22 14:14:15 +01:00
parent b717952234
commit d147843c76
10412 changed files with 2475583 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { PureComponent } from 'react';
import { dispatcher } from 'next/dist/compiled/next-devtools';
import { RuntimeErrorHandler } from '../../../client/dev/runtime-error-handler';
import { ErrorBoundary } from '../../../client/components/error-boundary';
import DefaultGlobalError from '../../../client/components/builtin/global-error';
import { SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE } from './segment-explorer-node';
function ErroredHtml({ globalError: [GlobalError, globalErrorStyles], error }) {
if (!error) {
return /*#__PURE__*/ _jsxs("html", {
children: [
/*#__PURE__*/ _jsx("head", {}),
/*#__PURE__*/ _jsx("body", {})
]
});
}
return /*#__PURE__*/ _jsxs(ErrorBoundary, {
errorComponent: DefaultGlobalError,
children: [
globalErrorStyles,
/*#__PURE__*/ _jsx(GlobalError, {
error: error
})
]
});
}
export class AppDevOverlayErrorBoundary extends PureComponent {
static getDerivedStateFromError(error) {
RuntimeErrorHandler.hadRuntimeError = true;
return {
reactError: error
};
}
componentDidCatch(err) {
if (process.env.NODE_ENV === 'development' && err.message === SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE) {
return;
}
dispatcher.openErrorOverlay();
}
render() {
const { children, globalError } = this.props;
const { reactError } = this.state;
const fallback = /*#__PURE__*/ _jsx(ErroredHtml, {
globalError: globalError,
error: reactError
});
return reactError !== null ? fallback : children;
}
constructor(...args){
super(...args), this.state = {
reactError: null
};
}
}
//# sourceMappingURL=app-dev-overlay-error-boundary.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/app/app-dev-overlay-error-boundary.tsx"],"sourcesContent":["import { PureComponent } from 'react'\nimport { dispatcher } from 'next/dist/compiled/next-devtools'\nimport { RuntimeErrorHandler } from '../../../client/dev/runtime-error-handler'\nimport { ErrorBoundary } from '../../../client/components/error-boundary'\nimport DefaultGlobalError from '../../../client/components/builtin/global-error'\nimport type { GlobalErrorState } from '../../../client/components/app-router-instance'\nimport { SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE } from './segment-explorer-node'\n\ntype AppDevOverlayErrorBoundaryProps = {\n children: React.ReactNode\n globalError: GlobalErrorState\n}\n\ntype AppDevOverlayErrorBoundaryState = {\n reactError: unknown\n}\n\nfunction ErroredHtml({\n globalError: [GlobalError, globalErrorStyles],\n error,\n}: {\n globalError: GlobalErrorState\n error: unknown\n}) {\n if (!error) {\n return (\n <html>\n <head />\n <body />\n </html>\n )\n }\n return (\n <ErrorBoundary errorComponent={DefaultGlobalError}>\n {globalErrorStyles}\n <GlobalError error={error} />\n </ErrorBoundary>\n )\n}\n\nexport class AppDevOverlayErrorBoundary extends PureComponent<\n AppDevOverlayErrorBoundaryProps,\n AppDevOverlayErrorBoundaryState\n> {\n state = { reactError: null }\n\n static getDerivedStateFromError(error: Error) {\n RuntimeErrorHandler.hadRuntimeError = true\n\n return {\n reactError: error,\n }\n }\n\n componentDidCatch(err: Error) {\n if (\n process.env.NODE_ENV === 'development' &&\n err.message === SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE\n ) {\n return\n }\n dispatcher.openErrorOverlay()\n }\n\n render() {\n const { children, globalError } = this.props\n const { reactError } = this.state\n\n const fallback = (\n <ErroredHtml globalError={globalError} error={reactError} />\n )\n\n return reactError !== null ? fallback : children\n }\n}\n"],"names":["PureComponent","dispatcher","RuntimeErrorHandler","ErrorBoundary","DefaultGlobalError","SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE","ErroredHtml","globalError","GlobalError","globalErrorStyles","error","html","head","body","errorComponent","AppDevOverlayErrorBoundary","getDerivedStateFromError","hadRuntimeError","reactError","componentDidCatch","err","process","env","NODE_ENV","message","openErrorOverlay","render","children","props","state","fallback"],"mappings":";AAAA,SAASA,aAAa,QAAQ,QAAO;AACrC,SAASC,UAAU,QAAQ,mCAAkC;AAC7D,SAASC,mBAAmB,QAAQ,4CAA2C;AAC/E,SAASC,aAAa,QAAQ,4CAA2C;AACzE,OAAOC,wBAAwB,kDAAiD;AAEhF,SAASC,wCAAwC,QAAQ,0BAAyB;AAWlF,SAASC,YAAY,EACnBC,aAAa,CAACC,aAAaC,kBAAkB,EAC7CC,KAAK,EAIN;IACC,IAAI,CAACA,OAAO;QACV,qBACE,MAACC;;8BACC,KAACC;8BACD,KAACC;;;IAGP;IACA,qBACE,MAACV;QAAcW,gBAAgBV;;YAC5BK;0BACD,KAACD;gBAAYE,OAAOA;;;;AAG1B;AAEA,OAAO,MAAMK,mCAAmCf;IAM9C,OAAOgB,yBAAyBN,KAAY,EAAE;QAC5CR,oBAAoBe,eAAe,GAAG;QAEtC,OAAO;YACLC,YAAYR;QACd;IACF;IAEAS,kBAAkBC,GAAU,EAAE;QAC5B,IACEC,QAAQC,GAAG,CAACC,QAAQ,KAAK,iBACzBH,IAAII,OAAO,KAAKnB,0CAChB;YACA;QACF;QACAJ,WAAWwB,gBAAgB;IAC7B;IAEAC,SAAS;QACP,MAAM,EAAEC,QAAQ,EAAEpB,WAAW,EAAE,GAAG,IAAI,CAACqB,KAAK;QAC5C,MAAM,EAAEV,UAAU,EAAE,GAAG,IAAI,CAACW,KAAK;QAEjC,MAAMC,yBACJ,KAACxB;YAAYC,aAAaA;YAAaG,OAAOQ;;QAGhD,OAAOA,eAAe,OAAOY,WAAWH;IAC1C;;QAjCK,qBAILE,QAAQ;YAAEX,YAAY;QAAK;;AA8B7B","ignoreList":[0]}

View File

@@ -0,0 +1,8 @@
import { patchConsoleError } from './errors/intercept-console-error';
import { handleGlobalErrors } from './errors/use-error-handler';
import { initializeDebugLogForwarding } from './forward-logs';
handleGlobalErrors();
patchConsoleError();
initializeDebugLogForwarding('app');
//# sourceMappingURL=app-dev-overlay-setup.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/app/app-dev-overlay-setup.ts"],"sourcesContent":["import { patchConsoleError } from './errors/intercept-console-error'\nimport { handleGlobalErrors } from './errors/use-error-handler'\nimport { initializeDebugLogForwarding } from './forward-logs'\n\nhandleGlobalErrors()\npatchConsoleError()\n\ninitializeDebugLogForwarding('app')\n"],"names":["patchConsoleError","handleGlobalErrors","initializeDebugLogForwarding"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,mCAAkC;AACpE,SAASC,kBAAkB,QAAQ,6BAA4B;AAC/D,SAASC,4BAA4B,QAAQ,iBAAgB;AAE7DD;AACAD;AAEAE,6BAA6B","ignoreList":[0]}

View File

@@ -0,0 +1,17 @@
import { jsx as _jsx } from "react/jsx-runtime";
import React from 'react';
import DefaultGlobalError from '../../../client/components/builtin/global-error';
import { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary';
// If an error is thrown while rendering an RSC stream, this will catch it in
// dev and show the error overlay.
export function RootLevelDevOverlayElement({ children }) {
return /*#__PURE__*/ _jsx(AppDevOverlayErrorBoundary, {
globalError: [
DefaultGlobalError,
null
],
children: children
});
}
//# sourceMappingURL=client-entry.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/app/client-entry.tsx"],"sourcesContent":["import React from 'react'\nimport DefaultGlobalError from '../../../client/components/builtin/global-error'\nimport { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary'\n\n// If an error is thrown while rendering an RSC stream, this will catch it in\n// dev and show the error overlay.\nexport function RootLevelDevOverlayElement({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <AppDevOverlayErrorBoundary globalError={[DefaultGlobalError, null]}>\n {children}\n </AppDevOverlayErrorBoundary>\n )\n}\n"],"names":["React","DefaultGlobalError","AppDevOverlayErrorBoundary","RootLevelDevOverlayElement","children","globalError"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AACzB,OAAOC,wBAAwB,kDAAiD;AAChF,SAASC,0BAA0B,QAAQ,mCAAkC;AAE7E,6EAA6E;AAC7E,kCAAkC;AAClC,OAAO,SAASC,2BAA2B,EACzCC,QAAQ,EAGT;IACC,qBACE,KAACF;QAA2BG,aAAa;YAACJ;YAAoB;SAAK;kBAChEG;;AAGP","ignoreList":[0]}

View File

@@ -0,0 +1,5 @@
export { originConsoleError } from './intercept-console-error';
export { handleClientError } from './use-error-handler';
export { decorateDevError } from './stitched-error';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/next-devtools/userspace/app/errors/index.ts"],"sourcesContent":["export { originConsoleError } from './intercept-console-error'\nexport { handleClientError } from './use-error-handler'\nexport { decorateDevError } from './stitched-error'\n"],"names":["originConsoleError","handleClientError","decorateDevError"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,4BAA2B;AAC9D,SAASC,iBAAiB,QAAQ,sBAAqB;AACvD,SAASC,gBAAgB,QAAQ,mBAAkB","ignoreList":[0]}

View File

@@ -0,0 +1,40 @@
import isError from '../../../../lib/is-error';
import { isNextRouterError } from '../../../../client/components/is-next-router-error';
import { handleConsoleError } from './use-error-handler';
import { parseConsoleArgs } from '../../../../client/lib/console';
import { forwardErrorLog } from '../forward-logs';
export const originConsoleError = globalThis.console.error;
// Patch console.error to collect information about hydration errors
export function patchConsoleError() {
// Ensure it's only patched once
if (typeof window === 'undefined') {
return;
}
window.console.error = function error(...args) {
let maybeError;
if (process.env.NODE_ENV !== 'production') {
const { error: replayedError } = parseConsoleArgs(args);
if (replayedError) {
maybeError = replayedError;
} else if (isError(args[0])) {
maybeError = args[0];
} else {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
maybeError = args[1];
}
} else {
maybeError = args[0];
}
if (!isNextRouterError(maybeError)) {
if (process.env.NODE_ENV !== 'production') {
handleConsoleError(// replayed errors have their own complex format string that should be used,
// but if we pass the error directly, `handleClientError` will ignore it
maybeError, args);
}
forwardErrorLog(args);
originConsoleError.apply(window.console, args);
}
};
}
//# sourceMappingURL=intercept-console-error.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/next-devtools/userspace/app/errors/intercept-console-error.ts"],"sourcesContent":["import isError from '../../../../lib/is-error'\nimport { isNextRouterError } from '../../../../client/components/is-next-router-error'\nimport { handleConsoleError } from './use-error-handler'\nimport { parseConsoleArgs } from '../../../../client/lib/console'\nimport { forwardErrorLog } from '../forward-logs'\n\nexport const originConsoleError = globalThis.console.error\n\n// Patch console.error to collect information about hydration errors\nexport function patchConsoleError() {\n // Ensure it's only patched once\n if (typeof window === 'undefined') {\n return\n }\n window.console.error = function error(...args: any[]) {\n let maybeError: unknown\n if (process.env.NODE_ENV !== 'production') {\n const { error: replayedError } = parseConsoleArgs(args)\n if (replayedError) {\n maybeError = replayedError\n } else if (isError(args[0])) {\n maybeError = args[0]\n } else {\n // See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78\n maybeError = args[1]\n }\n } else {\n maybeError = args[0]\n }\n\n if (!isNextRouterError(maybeError)) {\n if (process.env.NODE_ENV !== 'production') {\n handleConsoleError(\n // replayed errors have their own complex format string that should be used,\n // but if we pass the error directly, `handleClientError` will ignore it\n maybeError,\n args\n )\n }\n forwardErrorLog(args)\n\n originConsoleError.apply(window.console, args)\n }\n }\n}\n"],"names":["isError","isNextRouterError","handleConsoleError","parseConsoleArgs","forwardErrorLog","originConsoleError","globalThis","console","error","patchConsoleError","window","args","maybeError","process","env","NODE_ENV","replayedError","apply"],"mappings":"AAAA,OAAOA,aAAa,2BAA0B;AAC9C,SAASC,iBAAiB,QAAQ,qDAAoD;AACtF,SAASC,kBAAkB,QAAQ,sBAAqB;AACxD,SAASC,gBAAgB,QAAQ,iCAAgC;AACjE,SAASC,eAAe,QAAQ,kBAAiB;AAEjD,OAAO,MAAMC,qBAAqBC,WAAWC,OAAO,CAACC,KAAK,CAAA;AAE1D,oEAAoE;AACpE,OAAO,SAASC;IACd,gCAAgC;IAChC,IAAI,OAAOC,WAAW,aAAa;QACjC;IACF;IACAA,OAAOH,OAAO,CAACC,KAAK,GAAG,SAASA,MAAM,GAAGG,IAAW;QAClD,IAAIC;QACJ,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;YACzC,MAAM,EAAEP,OAAOQ,aAAa,EAAE,GAAGb,iBAAiBQ;YAClD,IAAIK,eAAe;gBACjBJ,aAAaI;YACf,OAAO,IAAIhB,QAAQW,IAAI,CAAC,EAAE,GAAG;gBAC3BC,aAAaD,IAAI,CAAC,EAAE;YACtB,OAAO;gBACL,iJAAiJ;gBACjJC,aAAaD,IAAI,CAAC,EAAE;YACtB;QACF,OAAO;YACLC,aAAaD,IAAI,CAAC,EAAE;QACtB;QAEA,IAAI,CAACV,kBAAkBW,aAAa;YAClC,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;gBACzCb,mBACE,4EAA4E;gBAC5E,wEAAwE;gBACxEU,YACAD;YAEJ;YACAP,gBAAgBO;YAEhBN,mBAAmBY,KAAK,CAACP,OAAOH,OAAO,EAAEI;QAC3C;IACF;AACF","ignoreList":[0]}

View File

@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { handleClientError } from './use-error-handler';
import { isNextRouterError } from '../../../../client/components/is-next-router-error';
import { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants';
function readSsrError() {
if (typeof document === 'undefined') {
return null;
}
const ssrErrorTemplateTag = document.querySelector('template[data-next-error-message]');
if (ssrErrorTemplateTag) {
const message = ssrErrorTemplateTag.getAttribute('data-next-error-message');
const stack = ssrErrorTemplateTag.getAttribute('data-next-error-stack');
const digest = ssrErrorTemplateTag.getAttribute('data-next-error-digest');
const error = Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", {
value: "E394",
enumerable: false,
configurable: true
});
if (digest) {
;
error.digest = digest;
}
// Skip Next.js SSR'd internal errors that which will be handled by the error boundaries.
if (isNextRouterError(error)) {
return null;
}
error.stack = stack || '';
return error;
}
return null;
}
/**
* Needs to be in the same error boundary as the shell.
* If it commits, we know we recovered from an SSR error.
* If it doesn't commit, we errored again and React will take care of error reporting.
*/ export function ReplaySsrOnlyErrors({ onBlockingError }) {
if (process.env.NODE_ENV !== 'production') {
// Need to read during render. The attributes will be gone after commit.
const ssrError = readSsrError();
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(()=>{
if (ssrError !== null) {
// TODO(veil): Include original Owner Stack (NDX-905)
// TODO(veil): Mark as recoverable error
// TODO(veil): console.error
handleClientError(ssrError);
// If it's missing root tags, we can't recover, make it blocking.
if (ssrError.digest === MISSING_ROOT_TAGS_ERROR) {
onBlockingError();
}
}
}, [
ssrError,
onBlockingError
]);
}
return null;
}
//# sourceMappingURL=replay-ssr-only-errors.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/next-devtools/userspace/app/errors/replay-ssr-only-errors.tsx"],"sourcesContent":["import { useEffect } from 'react'\nimport { handleClientError } from './use-error-handler'\nimport { isNextRouterError } from '../../../../client/components/is-next-router-error'\nimport { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants'\n\nfunction readSsrError(): (Error & { digest?: string }) | null {\n if (typeof document === 'undefined') {\n return null\n }\n\n const ssrErrorTemplateTag = document.querySelector(\n 'template[data-next-error-message]'\n )\n if (ssrErrorTemplateTag) {\n const message: string = ssrErrorTemplateTag.getAttribute(\n 'data-next-error-message'\n )!\n const stack = ssrErrorTemplateTag.getAttribute('data-next-error-stack')\n const digest = ssrErrorTemplateTag.getAttribute('data-next-error-digest')\n const error = new Error(message)\n if (digest) {\n ;(error as any).digest = digest\n }\n // Skip Next.js SSR'd internal errors that which will be handled by the error boundaries.\n if (isNextRouterError(error)) {\n return null\n }\n error.stack = stack || ''\n return error\n }\n\n return null\n}\n\n/**\n * Needs to be in the same error boundary as the shell.\n * If it commits, we know we recovered from an SSR error.\n * If it doesn't commit, we errored again and React will take care of error reporting.\n */\nexport function ReplaySsrOnlyErrors({\n onBlockingError,\n}: {\n onBlockingError: () => void\n}) {\n if (process.env.NODE_ENV !== 'production') {\n // Need to read during render. The attributes will be gone after commit.\n const ssrError = readSsrError()\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useEffect(() => {\n if (ssrError !== null) {\n // TODO(veil): Include original Owner Stack (NDX-905)\n // TODO(veil): Mark as recoverable error\n // TODO(veil): console.error\n handleClientError(ssrError)\n\n // If it's missing root tags, we can't recover, make it blocking.\n if (ssrError.digest === MISSING_ROOT_TAGS_ERROR) {\n onBlockingError()\n }\n }\n }, [ssrError, onBlockingError])\n }\n\n return null\n}\n"],"names":["useEffect","handleClientError","isNextRouterError","MISSING_ROOT_TAGS_ERROR","readSsrError","document","ssrErrorTemplateTag","querySelector","message","getAttribute","stack","digest","error","Error","ReplaySsrOnlyErrors","onBlockingError","process","env","NODE_ENV","ssrError"],"mappings":"AAAA,SAASA,SAAS,QAAQ,QAAO;AACjC,SAASC,iBAAiB,QAAQ,sBAAqB;AACvD,SAASC,iBAAiB,QAAQ,qDAAoD;AACtF,SAASC,uBAAuB,QAAQ,0CAAyC;AAEjF,SAASC;IACP,IAAI,OAAOC,aAAa,aAAa;QACnC,OAAO;IACT;IAEA,MAAMC,sBAAsBD,SAASE,aAAa,CAChD;IAEF,IAAID,qBAAqB;QACvB,MAAME,UAAkBF,oBAAoBG,YAAY,CACtD;QAEF,MAAMC,QAAQJ,oBAAoBG,YAAY,CAAC;QAC/C,MAAME,SAASL,oBAAoBG,YAAY,CAAC;QAChD,MAAMG,QAAQ,qBAAkB,CAAlB,IAAIC,MAAML,UAAV,qBAAA;mBAAA;wBAAA;0BAAA;QAAiB;QAC/B,IAAIG,QAAQ;;YACRC,MAAcD,MAAM,GAAGA;QAC3B;QACA,yFAAyF;QACzF,IAAIT,kBAAkBU,QAAQ;YAC5B,OAAO;QACT;QACAA,MAAMF,KAAK,GAAGA,SAAS;QACvB,OAAOE;IACT;IAEA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,oBAAoB,EAClCC,eAAe,EAGhB;IACC,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,cAAc;QACzC,wEAAwE;QACxE,MAAMC,WAAWf;QACjB,sDAAsD;QACtDJ,UAAU;YACR,IAAImB,aAAa,MAAM;gBACrB,qDAAqD;gBACrD,wCAAwC;gBACxC,4BAA4B;gBAC5BlB,kBAAkBkB;gBAElB,iEAAiE;gBACjE,IAAIA,SAASR,MAAM,KAAKR,yBAAyB;oBAC/CY;gBACF;YACF;QACF,GAAG;YAACI;YAAUJ;SAAgB;IAChC;IAEA,OAAO;AACT","ignoreList":[0]}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import isError from '../../../../lib/is-error';
const ownerStacks = new WeakMap();
export function getOwnerStack(error) {
return ownerStacks.get(error);
}
export function setOwnerStack(error, stack) {
ownerStacks.set(error, stack);
}
export function coerceError(value) {
return isError(value) ? value : Object.defineProperty(new Error('' + value), "__NEXT_ERROR_CODE", {
value: "E394",
enumerable: false,
configurable: true
});
}
export function setOwnerStackIfAvailable(error) {
// React 18 and prod does not have `captureOwnerStack`
if ('captureOwnerStack' in React) {
setOwnerStack(error, React.captureOwnerStack());
}
}
export function decorateDevError(thrownValue) {
const error = coerceError(thrownValue);
setOwnerStackIfAvailable(error);
return error;
}
//# sourceMappingURL=stitched-error.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../../src/next-devtools/userspace/app/errors/stitched-error.ts"],"sourcesContent":["import React from 'react'\nimport isError from '../../../../lib/is-error'\n\nconst ownerStacks = new WeakMap<Error, string | null>()\n\nexport function getOwnerStack(error: Error): string | null | undefined {\n return ownerStacks.get(error)\n}\nexport function setOwnerStack(error: Error, stack: string | null) {\n ownerStacks.set(error, stack)\n}\n\nexport function coerceError(value: unknown): Error {\n return isError(value) ? value : new Error('' + value)\n}\n\nexport function setOwnerStackIfAvailable(error: Error): void {\n // React 18 and prod does not have `captureOwnerStack`\n if ('captureOwnerStack' in React) {\n setOwnerStack(error, React.captureOwnerStack())\n }\n}\n\nexport function decorateDevError(thrownValue: unknown) {\n const error = coerceError(thrownValue)\n setOwnerStackIfAvailable(error)\n return error\n}\n"],"names":["React","isError","ownerStacks","WeakMap","getOwnerStack","error","get","setOwnerStack","stack","set","coerceError","value","Error","setOwnerStackIfAvailable","captureOwnerStack","decorateDevError","thrownValue"],"mappings":"AAAA,OAAOA,WAAW,QAAO;AACzB,OAAOC,aAAa,2BAA0B;AAE9C,MAAMC,cAAc,IAAIC;AAExB,OAAO,SAASC,cAAcC,KAAY;IACxC,OAAOH,YAAYI,GAAG,CAACD;AACzB;AACA,OAAO,SAASE,cAAcF,KAAY,EAAEG,KAAoB;IAC9DN,YAAYO,GAAG,CAACJ,OAAOG;AACzB;AAEA,OAAO,SAASE,YAAYC,KAAc;IACxC,OAAOV,QAAQU,SAASA,QAAQ,qBAAqB,CAArB,IAAIC,MAAM,KAAKD,QAAf,qBAAA;eAAA;oBAAA;sBAAA;IAAoB;AACtD;AAEA,OAAO,SAASE,yBAAyBR,KAAY;IACnD,sDAAsD;IACtD,IAAI,uBAAuBL,OAAO;QAChCO,cAAcF,OAAOL,MAAMc,iBAAiB;IAC9C;AACF;AAEA,OAAO,SAASC,iBAAiBC,WAAoB;IACnD,MAAMX,QAAQK,YAAYM;IAC1BH,yBAAyBR;IACzB,OAAOA;AACT","ignoreList":[0]}

View File

@@ -0,0 +1,102 @@
import { useEffect } from 'react';
import { isNextRouterError } from '../../../../client/components/is-next-router-error';
import { formatConsoleArgs, parseConsoleArgs } from '../../../../client/lib/console';
import isError from '../../../../lib/is-error';
import { createConsoleError } from '../../../shared/console-error';
import { coerceError, setOwnerStackIfAvailable } from './stitched-error';
import { forwardUnhandledError, logUnhandledRejection } from '../forward-logs';
const queueMicroTask = globalThis.queueMicrotask || ((cb)=>Promise.resolve().then(cb));
const errorQueue = [];
const errorHandlers = [];
const rejectionQueue = [];
const rejectionHandlers = [];
export function handleConsoleError(originError, consoleErrorArgs) {
let error;
const { environmentName } = parseConsoleArgs(consoleErrorArgs);
if (isError(originError)) {
error = createConsoleError(originError, environmentName);
} else {
error = createConsoleError(formatConsoleArgs(consoleErrorArgs), environmentName);
}
setOwnerStackIfAvailable(error);
errorQueue.push(error);
for (const handler of errorHandlers){
// Delayed the error being passed to React Dev Overlay,
// avoid the state being synchronously updated in the component.
queueMicroTask(()=>{
handler(error);
});
}
}
export function handleClientError(error) {
errorQueue.push(error);
for (const handler of errorHandlers){
// Delayed the error being passed to React Dev Overlay,
// avoid the state being synchronously updated in the component.
queueMicroTask(()=>{
handler(error);
});
}
}
export function useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection) {
useEffect(()=>{
// Handle queued errors.
errorQueue.forEach(handleOnUnhandledError);
rejectionQueue.forEach(handleOnUnhandledRejection);
// Listen to new errors.
errorHandlers.push(handleOnUnhandledError);
rejectionHandlers.push(handleOnUnhandledRejection);
return ()=>{
// Remove listeners.
errorHandlers.splice(errorHandlers.indexOf(handleOnUnhandledError), 1);
rejectionHandlers.splice(rejectionHandlers.indexOf(handleOnUnhandledRejection), 1);
// Reset error queues.
errorQueue.splice(0, errorQueue.length);
rejectionQueue.splice(0, rejectionQueue.length);
};
}, [
handleOnUnhandledError,
handleOnUnhandledRejection
]);
}
function onUnhandledError(event) {
const thrownValue = event.error;
if (isNextRouterError(thrownValue)) {
event.preventDefault();
return false;
}
// When there's an error property present, we log the error to error overlay.
// Otherwise we don't do anything as it's not logging in the console either.
if (thrownValue) {
const error = coerceError(thrownValue);
setOwnerStackIfAvailable(error);
handleClientError(error);
forwardUnhandledError(error);
}
}
function onUnhandledRejection(ev) {
const reason = ev?.reason;
if (isNextRouterError(reason)) {
ev.preventDefault();
return;
}
const error = coerceError(reason);
setOwnerStackIfAvailable(error);
rejectionQueue.push(error);
for (const handler of rejectionHandlers){
handler(error);
}
logUnhandledRejection(reason);
}
export function handleGlobalErrors() {
if (typeof window !== 'undefined') {
try {
// Increase the number of stack frames on the client
Error.stackTraceLimit = 50;
} catch {}
window.addEventListener('error', onUnhandledError);
window.addEventListener('unhandledrejection', onUnhandledRejection);
}
}
//# sourceMappingURL=use-error-handler.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,69 @@
import { configure } from 'next/dist/compiled/safe-stable-stringify';
import { getTerminalLoggingConfig } from './terminal-logging-config';
import { UNDEFINED_MARKER } from '../../shared/forward-logs-shared';
const terminalLoggingConfig = getTerminalLoggingConfig();
const PROMISE_MARKER = 'Promise {}';
const UNAVAILABLE_MARKER = '[Unable to view]';
const maximumDepth = typeof terminalLoggingConfig === 'object' && terminalLoggingConfig.depthLimit ? terminalLoggingConfig.depthLimit : 5;
const maximumBreadth = typeof terminalLoggingConfig === 'object' && terminalLoggingConfig.edgeLimit ? terminalLoggingConfig.edgeLimit : 100;
export const safeStringifyWithDepth = configure({
maximumDepth,
maximumBreadth
});
/**
* allows us to:
* - revive the undefined log in the server as it would look in the browser
* - not read/attempt to serialize promises (next will console error if you do that, and will cause this program to infinitely recurse)
* - if we read a proxy that throws (no way to detect if something is a proxy), explain to the user we can't read this data
*/ export function preLogSerializationClone(value, seen = new WeakMap()) {
if (value === undefined) return UNDEFINED_MARKER;
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value);
try {
Object.keys(value);
} catch {
return UNAVAILABLE_MARKER;
}
try {
if (typeof value.then === 'function') return PROMISE_MARKER;
} catch {
return UNAVAILABLE_MARKER;
}
if (Array.isArray(value)) {
const out = [];
seen.set(value, out);
for (const item of value){
try {
out.push(preLogSerializationClone(item, seen));
} catch {
out.push(UNAVAILABLE_MARKER);
}
}
return out;
}
const proto = Object.getPrototypeOf(value);
if (proto === Object.prototype || proto === null) {
const out = {};
seen.set(value, out);
for (const key of Object.keys(value)){
try {
out[key] = preLogSerializationClone(value[key], seen);
} catch {
out[key] = UNAVAILABLE_MARKER;
}
}
return out;
}
return Object.prototype.toString.call(value);
}
// only safe if passed safeClone data
export const logStringify = (data)=>{
try {
const result = safeStringifyWithDepth(data);
return result ?? `"${UNAVAILABLE_MARKER}"`;
} catch {
return `"${UNAVAILABLE_MARKER}"`;
}
};
//# sourceMappingURL=forward-logs-utils.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/app/forward-logs-utils.ts"],"sourcesContent":["import { configure } from 'next/dist/compiled/safe-stable-stringify'\nimport { getTerminalLoggingConfig } from './terminal-logging-config'\nimport { UNDEFINED_MARKER } from '../../shared/forward-logs-shared'\n\nconst terminalLoggingConfig = getTerminalLoggingConfig()\n\nconst PROMISE_MARKER = 'Promise {}'\nconst UNAVAILABLE_MARKER = '[Unable to view]'\n\nconst maximumDepth =\n typeof terminalLoggingConfig === 'object' && terminalLoggingConfig.depthLimit\n ? terminalLoggingConfig.depthLimit\n : 5\nconst maximumBreadth =\n typeof terminalLoggingConfig === 'object' && terminalLoggingConfig.edgeLimit\n ? terminalLoggingConfig.edgeLimit\n : 100\n\nexport const safeStringifyWithDepth = configure({\n maximumDepth,\n maximumBreadth,\n})\n\n/**\n * allows us to:\n * - revive the undefined log in the server as it would look in the browser\n * - not read/attempt to serialize promises (next will console error if you do that, and will cause this program to infinitely recurse)\n * - if we read a proxy that throws (no way to detect if something is a proxy), explain to the user we can't read this data\n */\nexport function preLogSerializationClone<T>(\n value: T,\n seen = new WeakMap()\n): any {\n if (value === undefined) return UNDEFINED_MARKER\n if (value === null || typeof value !== 'object') return value\n if (seen.has(value as object)) return seen.get(value as object)\n\n try {\n Object.keys(value as object)\n } catch {\n return UNAVAILABLE_MARKER\n }\n\n try {\n if (typeof (value as any).then === 'function') return PROMISE_MARKER\n } catch {\n return UNAVAILABLE_MARKER\n }\n\n if (Array.isArray(value)) {\n const out: any[] = []\n seen.set(value, out)\n for (const item of value) {\n try {\n out.push(preLogSerializationClone(item, seen))\n } catch {\n out.push(UNAVAILABLE_MARKER)\n }\n }\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto === Object.prototype || proto === null) {\n const out: Record<string, unknown> = {}\n seen.set(value as object, out)\n for (const key of Object.keys(value as object)) {\n try {\n out[key] = preLogSerializationClone((value as any)[key], seen)\n } catch {\n out[key] = UNAVAILABLE_MARKER\n }\n }\n return out\n }\n\n return Object.prototype.toString.call(value)\n}\n\n// only safe if passed safeClone data\nexport const logStringify = (data: unknown): string => {\n try {\n const result = safeStringifyWithDepth(data)\n return result ?? `\"${UNAVAILABLE_MARKER}\"`\n } catch {\n return `\"${UNAVAILABLE_MARKER}\"`\n }\n}\n"],"names":["configure","getTerminalLoggingConfig","UNDEFINED_MARKER","terminalLoggingConfig","PROMISE_MARKER","UNAVAILABLE_MARKER","maximumDepth","depthLimit","maximumBreadth","edgeLimit","safeStringifyWithDepth","preLogSerializationClone","value","seen","WeakMap","undefined","has","get","Object","keys","then","Array","isArray","out","set","item","push","proto","getPrototypeOf","prototype","key","toString","call","logStringify","data","result"],"mappings":"AAAA,SAASA,SAAS,QAAQ,2CAA0C;AACpE,SAASC,wBAAwB,QAAQ,4BAA2B;AACpE,SAASC,gBAAgB,QAAQ,mCAAkC;AAEnE,MAAMC,wBAAwBF;AAE9B,MAAMG,iBAAiB;AACvB,MAAMC,qBAAqB;AAE3B,MAAMC,eACJ,OAAOH,0BAA0B,YAAYA,sBAAsBI,UAAU,GACzEJ,sBAAsBI,UAAU,GAChC;AACN,MAAMC,iBACJ,OAAOL,0BAA0B,YAAYA,sBAAsBM,SAAS,GACxEN,sBAAsBM,SAAS,GAC/B;AAEN,OAAO,MAAMC,yBAAyBV,UAAU;IAC9CM;IACAE;AACF,GAAE;AAEF;;;;;CAKC,GACD,OAAO,SAASG,yBACdC,KAAQ,EACRC,OAAO,IAAIC,SAAS;IAEpB,IAAIF,UAAUG,WAAW,OAAOb;IAChC,IAAIU,UAAU,QAAQ,OAAOA,UAAU,UAAU,OAAOA;IACxD,IAAIC,KAAKG,GAAG,CAACJ,QAAkB,OAAOC,KAAKI,GAAG,CAACL;IAE/C,IAAI;QACFM,OAAOC,IAAI,CAACP;IACd,EAAE,OAAM;QACN,OAAOP;IACT;IAEA,IAAI;QACF,IAAI,OAAO,AAACO,MAAcQ,IAAI,KAAK,YAAY,OAAOhB;IACxD,EAAE,OAAM;QACN,OAAOC;IACT;IAEA,IAAIgB,MAAMC,OAAO,CAACV,QAAQ;QACxB,MAAMW,MAAa,EAAE;QACrBV,KAAKW,GAAG,CAACZ,OAAOW;QAChB,KAAK,MAAME,QAAQb,MAAO;YACxB,IAAI;gBACFW,IAAIG,IAAI,CAACf,yBAAyBc,MAAMZ;YAC1C,EAAE,OAAM;gBACNU,IAAIG,IAAI,CAACrB;YACX;QACF;QACA,OAAOkB;IACT;IAEA,MAAMI,QAAQT,OAAOU,cAAc,CAAChB;IACpC,IAAIe,UAAUT,OAAOW,SAAS,IAAIF,UAAU,MAAM;QAChD,MAAMJ,MAA+B,CAAC;QACtCV,KAAKW,GAAG,CAACZ,OAAiBW;QAC1B,KAAK,MAAMO,OAAOZ,OAAOC,IAAI,CAACP,OAAkB;YAC9C,IAAI;gBACFW,GAAG,CAACO,IAAI,GAAGnB,yBAAyB,AAACC,KAAa,CAACkB,IAAI,EAAEjB;YAC3D,EAAE,OAAM;gBACNU,GAAG,CAACO,IAAI,GAAGzB;YACb;QACF;QACA,OAAOkB;IACT;IAEA,OAAOL,OAAOW,SAAS,CAACE,QAAQ,CAACC,IAAI,CAACpB;AACxC;AAEA,qCAAqC;AACrC,OAAO,MAAMqB,eAAe,CAACC;IAC3B,IAAI;QACF,MAAMC,SAASzB,uBAAuBwB;QACtC,OAAOC,UAAU,CAAC,CAAC,EAAE9B,mBAAmB,CAAC,CAAC;IAC5C,EAAE,OAAM;QACN,OAAO,CAAC,CAAC,EAAEA,mBAAmB,CAAC,CAAC;IAClC;AACF,EAAC","ignoreList":[0]}

View File

@@ -0,0 +1,467 @@
import { getOwnerStack, setOwnerStackIfAvailable } from './errors/stitched-error';
import { getErrorSource } from '../../../shared/lib/error-source';
import { getIsTerminalLoggingEnabled } from './terminal-logging-config';
import { patchConsoleMethod } from '../../shared/forward-logs-shared';
import { preLogSerializationClone, logStringify, safeStringifyWithDepth } from './forward-logs-utils';
// Client-side file logger for browser logs
class ClientFileLogger {
formatTimestamp() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
log(level, args) {
if (isReactServerReplayedLog(args)) {
return;
}
// Format the args into a message string
const message = args.map((arg)=>{
if (typeof arg === 'string') return arg;
if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
// Handle DOM nodes - only log the tag name to avoid React proxied elements
if (arg instanceof Element) {
return `<${arg.tagName.toLowerCase()}>`;
}
return safeStringifyWithDepth(arg);
}).join(' ');
const logEntry = {
timestamp: this.formatTimestamp(),
level: level.toUpperCase(),
message
};
this.logEntries.push(logEntry);
// Schedule flush when new log is added
scheduleLogFlush();
}
getLogs() {
return [
...this.logEntries
];
}
clear() {
this.logEntries = [];
}
constructor(){
this.logEntries = [];
}
}
const clientFileLogger = new ClientFileLogger();
// Set up flush-based sending of client file logs
let logFlushTimeout = null;
let heartbeatInterval = null;
const scheduleLogFlush = ()=>{
if (logFlushTimeout) {
clearTimeout(logFlushTimeout);
}
logFlushTimeout = setTimeout(()=>{
sendClientFileLogs();
logFlushTimeout = null;
}, 100) // Send after 100ms (much faster with debouncing)
;
};
const cancelLogFlush = ()=>{
if (logFlushTimeout) {
clearTimeout(logFlushTimeout);
logFlushTimeout = null;
}
};
const startHeartbeat = ()=>{
if (heartbeatInterval) return;
heartbeatInterval = setInterval(()=>{
if (logQueue.socket && logQueue.socket.readyState === WebSocket.OPEN) {
try {
// Send a ping to keep the connection alive
logQueue.socket.send(JSON.stringify({
event: 'ping'
}));
} catch (error) {
// Connection might be closed, stop heartbeat
stopHeartbeat();
}
} else {
stopHeartbeat();
}
}, 5000) // Send ping every 5 seconds
;
};
const stopHeartbeat = ()=>{
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
};
const isTerminalLoggingEnabled = getIsTerminalLoggingEnabled();
const methods = [
'log',
'info',
'warn',
'debug',
'table',
'assert',
'dir',
'dirxml',
'group',
'groupCollapsed',
'groupEnd',
'trace'
];
const afterThisFrame = (cb)=>{
let timeout;
const rafId = requestAnimationFrame(()=>{
timeout = setTimeout(()=>{
cb();
});
});
return ()=>{
cancelAnimationFrame(rafId);
clearTimeout(timeout);
};
};
let isPatched = false;
const serializeEntries = (entries)=>entries.map((clientEntry)=>{
switch(clientEntry.kind){
case 'any-logged-error':
case 'console':
{
return {
...clientEntry,
args: clientEntry.args.map(stringifyUserArg)
};
}
case 'formatted-error':
{
return clientEntry;
}
default:
{
return null;
}
}
});
// Function to send client file logs to server
const sendClientFileLogs = ()=>{
if (!logQueue.socket || logQueue.socket.readyState !== WebSocket.OPEN) {
return;
}
const logs = clientFileLogger.getLogs();
if (logs.length === 0) {
return;
}
try {
const payload = JSON.stringify({
event: 'client-file-logs',
logs: logs
});
logQueue.socket.send(payload);
} catch (error) {
console.error(error);
} finally{
// Clear logs regardless of send success to prevent memory leaks
clientFileLogger.clear();
}
};
// Combined state and public API
export const logQueue = {
entries: [],
flushScheduled: false,
cancelFlush: null,
socket: null,
sourceType: undefined,
router: null,
scheduleLogSend: (entry)=>{
logQueue.entries.push(entry);
if (logQueue.flushScheduled) {
return;
}
// safe to deref and use in setTimeout closure since we cancel on new socket
const socket = logQueue.socket;
if (!socket) {
return;
}
// we probably dont need this
logQueue.flushScheduled = true;
// non blocking log flush, runs at most once per frame
logQueue.cancelFlush = afterThisFrame(()=>{
logQueue.flushScheduled = false;
// just incase
try {
const payload = JSON.stringify({
event: 'browser-logs',
entries: serializeEntries(logQueue.entries),
router: logQueue.router,
// needed for source mapping, we just assign the sourceType from the last error for the whole batch
sourceType: logQueue.sourceType
});
socket.send(payload);
logQueue.entries = [];
logQueue.sourceType = undefined;
// Also send client file logs
sendClientFileLogs();
} catch {
// error (make sure u don't infinite loop)
/* noop */ }
});
},
onSocketReady: (socket)=>{
// When MCP or terminal logging is enabled, we enable the socket connection,
// otherwise it will not proceed.
if (!isTerminalLoggingEnabled && !process.env.__NEXT_MCP_SERVER) {
return;
}
if (socket.readyState !== WebSocket.OPEN) {
// invariant
return;
}
// incase an existing timeout was going to run with a stale socket
logQueue.cancelFlush?.();
logQueue.socket = socket;
// Add socket event listeners to track connection state
socket.addEventListener('close', ()=>{
cancelLogFlush();
stopHeartbeat();
});
// Only send terminal logs if enabled
if (isTerminalLoggingEnabled) {
try {
const payload = JSON.stringify({
event: 'browser-logs',
entries: serializeEntries(logQueue.entries),
router: logQueue.router,
sourceType: logQueue.sourceType
});
socket.send(payload);
logQueue.entries = [];
logQueue.sourceType = undefined;
} catch {
/** noop just incase */ }
}
// Always send client file logs when socket is ready
sendClientFileLogs();
// Start heartbeat to keep connection alive
startHeartbeat();
}
};
const stringifyUserArg = (arg)=>{
if (arg.kind !== 'arg') {
return arg;
}
return {
...arg,
data: logStringify(arg.data)
};
};
const createErrorArg = (error)=>{
const stack = stackWithOwners(error);
return {
kind: 'formatted-error-arg',
prefix: error.message ? `${error.name}: ${error.message}` : `${error.name}`,
stack
};
};
const createLogEntry = (level, args)=>{
// Always log to client file logger with args (formatting done inside log method)
clientFileLogger.log(level, args);
// Only forward to terminal if enabled
if (!isTerminalLoggingEnabled) {
return;
}
// do not abstract this, it implicitly relies on which functions call it. forcing the inlined implementation makes you think about callers
// error capture stack trace maybe
const stack = stackWithOwners(new Error());
const stackLines = stack?.split('\n');
const cleanStack = stackLines?.slice(3).join('\n') // this is probably ignored anyways
;
const entry = {
kind: 'console',
consoleMethodStack: cleanStack ?? null,
method: level,
args: args.map((arg)=>{
if (arg instanceof Error) {
return createErrorArg(arg);
}
return {
kind: 'arg',
data: preLogSerializationClone(arg)
};
})
};
logQueue.scheduleLogSend(entry);
};
export const forwardErrorLog = (args)=>{
// Always log to client file logger with args (formatting done inside log method)
clientFileLogger.log('error', args);
// Only forward to terminal if enabled
if (!isTerminalLoggingEnabled) {
return;
}
const errorObjects = args.filter((arg)=>arg instanceof Error);
const first = errorObjects.at(0);
if (first) {
const source = getErrorSource(first);
if (source) {
logQueue.sourceType = source;
}
}
/**
* browser shows stack regardless of type of data passed to console.error, so we should do the same
*
* do not abstract this, it implicitly relies on which functions call it. forcing the inlined implementation makes you think about callers
*/ const stack = stackWithOwners(new Error());
const stackLines = stack?.split('\n');
const cleanStack = stackLines?.slice(3).join('\n');
const entry = {
kind: 'any-logged-error',
method: 'error',
consoleErrorStack: cleanStack ?? '',
args: args.map((arg)=>{
if (arg instanceof Error) {
return createErrorArg(arg);
}
return {
kind: 'arg',
data: preLogSerializationClone(arg)
};
})
};
logQueue.scheduleLogSend(entry);
};
const createUncaughtErrorEntry = (errorName, errorMessage, fullStack)=>{
const entry = {
kind: 'formatted-error',
prefix: `Uncaught ${errorName}: ${errorMessage}`,
stack: fullStack,
method: 'error'
};
logQueue.scheduleLogSend(entry);
};
const stackWithOwners = (error)=>{
let ownerStack = '';
setOwnerStackIfAvailable(error);
ownerStack = getOwnerStack(error) || '';
const stack = (error.stack || '') + ownerStack;
return stack;
};
export function logUnhandledRejection(reason) {
// Always log to client file logger
const message = reason instanceof Error ? `${reason.name}: ${reason.message}` : JSON.stringify(reason);
clientFileLogger.log('error', [
`unhandledRejection: ${message}`
]);
// Only forward to terminal if enabled
if (!isTerminalLoggingEnabled) {
return;
}
if (reason instanceof Error) {
createUnhandledRejectionErrorEntry(reason, stackWithOwners(reason));
return;
}
createUnhandledRejectionNonErrorEntry(reason);
}
const createUnhandledRejectionErrorEntry = (error, fullStack)=>{
const source = getErrorSource(error);
if (source) {
logQueue.sourceType = source;
}
const entry = {
kind: 'formatted-error',
prefix: ` unhandledRejection: ${error.name}: ${error.message}`,
stack: fullStack,
method: 'error'
};
logQueue.scheduleLogSend(entry);
};
const createUnhandledRejectionNonErrorEntry = (reason)=>{
const entry = {
kind: 'any-logged-error',
// we can't access the stack since the event is dispatched async and creating an inline error would be meaningless
consoleErrorStack: '',
method: 'error',
args: [
{
kind: 'arg',
data: ` unhandledRejection:`,
isRejectionMessage: true
},
{
kind: 'arg',
data: preLogSerializationClone(reason)
}
]
};
logQueue.scheduleLogSend(entry);
};
const isHMR = (args)=>{
const firstArg = args[0];
if (typeof firstArg !== 'string') {
return false;
}
if (firstArg.startsWith('[Fast Refresh]')) {
return true;
}
if (firstArg.startsWith('[HMR]')) {
return true;
}
return false;
};
/**
* Matches the format of logs arguments React replayed from the RSC.
*/ const isReactServerReplayedLog = (args)=>{
if (args.length < 3) {
return false;
}
const [format, styles, label] = args;
if (typeof format !== 'string' || typeof styles !== 'string' || typeof label !== 'string') {
return false;
}
return format.startsWith('%c%s%c') && styles.includes('background:');
};
export function forwardUnhandledError(error) {
// Always log to client file logger
clientFileLogger.log('error', [
`uncaughtError: ${error.name}: ${error.message}`
]);
// Only forward to terminal if enabled
if (!isTerminalLoggingEnabled) {
return;
}
createUncaughtErrorEntry(error.name, error.message, stackWithOwners(error));
}
// TODO: this router check is brittle, we need to update based on the current router the user is using
export const initializeDebugLogForwarding = (router)=>{
// probably don't need this
if (isPatched) {
return;
}
// TODO(rob): why does this break rendering on server, important to know incase the same bug appears in browser
if (typeof window === 'undefined') {
return;
}
// better to be safe than sorry
try {
methods.forEach((method)=>patchConsoleMethod(method, (_, ...args)=>{
if (isHMR(args)) {
return;
}
if (isReactServerReplayedLog(args)) {
return;
}
createLogEntry(method, args);
}));
} catch {}
logQueue.router = router;
isPatched = true;
// Cleanup on page unload
window.addEventListener('beforeunload', ()=>{
cancelLogFlush();
stopHeartbeat();
// Send any remaining logs before page unloads
sendClientFileLogs();
});
};
//# sourceMappingURL=forward-logs.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,113 @@
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useState, createContext, useContext, use, useMemo, useCallback } from 'react';
import { useLayoutEffect } from 'react';
import { dispatcher } from 'next/dist/compiled/next-devtools';
import { notFound } from '../../../client/components/not-found';
export const SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE = 'NEXT_DEVTOOLS_SIMULATED_ERROR';
function SegmentTrieNode({ type, pagePath }) {
const { boundaryType, setBoundaryType } = useSegmentState();
const nodeState = useMemo(()=>{
return {
type,
pagePath,
boundaryType,
setBoundaryType
};
}, [
type,
pagePath,
boundaryType,
setBoundaryType
]);
// Use `useLayoutEffect` to ensure the state is updated during suspense.
// `useEffect` won't work as the state is preserved during suspense.
useLayoutEffect(()=>{
dispatcher.segmentExplorerNodeAdd(nodeState);
return ()=>{
dispatcher.segmentExplorerNodeRemove(nodeState);
};
}, [
nodeState
]);
return null;
}
function NotFoundSegmentNode() {
notFound();
}
function ErrorSegmentNode() {
throw Object.defineProperty(new Error(SEGMENT_EXPLORER_SIMULATED_ERROR_MESSAGE), "__NEXT_ERROR_CODE", {
value: "E394",
enumerable: false,
configurable: true
});
}
const forever = new Promise(()=>{});
function LoadingSegmentNode() {
use(forever);
return null;
}
export function SegmentViewStateNode({ page }) {
useLayoutEffect(()=>{
dispatcher.segmentExplorerUpdateRouteState(page);
return ()=>{
dispatcher.segmentExplorerUpdateRouteState('');
};
}, [
page
]);
return null;
}
export function SegmentBoundaryTriggerNode() {
const { boundaryType } = useSegmentState();
let segmentNode = null;
if (boundaryType === 'loading') {
segmentNode = /*#__PURE__*/ _jsx(LoadingSegmentNode, {});
} else if (boundaryType === 'not-found') {
segmentNode = /*#__PURE__*/ _jsx(NotFoundSegmentNode, {});
} else if (boundaryType === 'error') {
segmentNode = /*#__PURE__*/ _jsx(ErrorSegmentNode, {});
}
return segmentNode;
}
export function SegmentViewNode({ type, pagePath, children }) {
const segmentNode = /*#__PURE__*/ _jsx(SegmentTrieNode, {
type: type,
pagePath: pagePath
}, type);
return /*#__PURE__*/ _jsxs(_Fragment, {
children: [
segmentNode,
children
]
});
}
const SegmentStateContext = /*#__PURE__*/ createContext({
boundaryType: null,
setBoundaryType: ()=>{}
});
export function SegmentStateProvider({ children }) {
const [boundaryType, setBoundaryType] = useState(null);
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0);
const reloadBoundary = useCallback(()=>setErrorBoundaryKey((prev)=>prev + 1), []);
const setBoundaryTypeAndReload = useCallback((type)=>{
if (type === null) {
reloadBoundary();
}
setBoundaryType(type);
}, [
reloadBoundary
]);
return /*#__PURE__*/ _jsx(SegmentStateContext.Provider, {
value: {
boundaryType,
setBoundaryType: setBoundaryTypeAndReload
},
children: children
}, errorBoundaryKey);
}
export function useSegmentState() {
return useContext(SegmentStateContext);
}
//# sourceMappingURL=segment-explorer-node.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
export function getTerminalLoggingConfig() {
try {
return JSON.parse(process.env.__NEXT_BROWSER_DEBUG_INFO_IN_TERMINAL || 'false');
} catch {
return false;
}
}
export function getIsTerminalLoggingEnabled() {
const config = getTerminalLoggingConfig();
return Boolean(config);
}
//# sourceMappingURL=terminal-logging-config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/app/terminal-logging-config.ts"],"sourcesContent":["export function getTerminalLoggingConfig():\n | false\n | boolean\n | {\n depthLimit?: number\n edgeLimit?: number\n showSourceLocation?: boolean\n } {\n try {\n return JSON.parse(\n process.env.__NEXT_BROWSER_DEBUG_INFO_IN_TERMINAL || 'false'\n )\n } catch {\n return false\n }\n}\n\nexport function getIsTerminalLoggingEnabled(): boolean {\n const config = getTerminalLoggingConfig()\n return Boolean(config)\n}\n"],"names":["getTerminalLoggingConfig","JSON","parse","process","env","__NEXT_BROWSER_DEBUG_INFO_IN_TERMINAL","getIsTerminalLoggingEnabled","config","Boolean"],"mappings":"AAAA,OAAO,SAASA;IAQd,IAAI;QACF,OAAOC,KAAKC,KAAK,CACfC,QAAQC,GAAG,CAACC,qCAAqC,IAAI;IAEzD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,OAAO,SAASC;IACd,MAAMC,SAASP;IACf,OAAOQ,QAAQD;AACjB","ignoreList":[0]}

View File

@@ -0,0 +1,131 @@
import { getHydrationWarningType, isHydrationError as isReact18HydrationError, isHydrationWarning as isReact18HydrationWarning } from '../../shared/react-18-hydration-error';
import { isHydrationError as isReact19HydrationError, isErrorMessageWithComponentStackDiff as isReact19HydrationWarning } from '../../shared/react-19-hydration-error';
// We only need this for React 18 or hydration console errors in React 19.
// Once we surface console.error in the dev overlay in pages router, we should only
// use this for React 18.
let hydrationErrorState = {};
const squashedHydrationErrorDetails = new WeakMap();
export function getSquashedHydrationErrorDetails(error) {
return squashedHydrationErrorDetails.has(error) ? squashedHydrationErrorDetails.get(error) : null;
}
export function attachHydrationErrorState(error) {
if (!isReact18HydrationError(error) && !isReact19HydrationError(error)) {
return;
}
let parsedHydrationErrorState = {};
// If there's any extra information in the error message to display,
// append it to the error message details property
if (hydrationErrorState.warning) {
// The patched console.error found hydration errors logged by React
// Append the logged warning to the error message
parsedHydrationErrorState = {
// It contains the warning, component stack, server and client tag names
...hydrationErrorState
};
// Consume the cached hydration diff.
// This is only required for now when we still squashed the hydration diff log into hydration error.
// Once the all error is logged to dev overlay in order, this will go away.
if (hydrationErrorState.reactOutputComponentDiff) {
parsedHydrationErrorState.reactOutputComponentDiff = hydrationErrorState.reactOutputComponentDiff;
}
squashedHydrationErrorDetails.set(error, parsedHydrationErrorState);
}
}
// TODO: Only handle React 18. Once we surface console.error in the dev overlay in pages router,
// we can use the same behavior as App Router.
export function storeHydrationErrorStateFromConsoleArgs(...args) {
let [message, firstContent, secondContent, ...rest] = args;
if (isReact18HydrationWarning(message)) {
// Some hydration warnings has 4 arguments, some has 3, fallback to the last argument
// when the 3rd argument is not the component stack but an empty string
// For some warnings, there's only 1 argument for template.
// The second argument is the diff or component stack.
if (args.length === 3) {
secondContent = '';
}
const warning = message.replace(/Warning: /, '').replace('%s', firstContent).replace('%s', secondContent)// remove the last %s from the message
.replace(/%s/g, '');
const lastArg = (rest[rest.length - 1] || '').trim();
hydrationErrorState.reactOutputComponentDiff = generateHydrationDiffReact18(message, firstContent, secondContent, lastArg);
hydrationErrorState.warning = warning;
} else if (isReact19HydrationWarning(message)) {
// Some hydration warnings has 4 arguments, some has 3, fallback to the last argument
// when the 3rd argument is not the component stack but an empty string
// For some warnings, there's only 1 argument for template.
// The second argument is the diff or component stack.
if (args.length === 3) {
secondContent = '';
}
const warning = message.replace('%s', firstContent).replace('%s', secondContent)// remove the last %s from the message
.replace(/%s/g, '');
const lastArg = (args[args.length - 1] || '').trim();
hydrationErrorState.reactOutputComponentDiff = lastArg;
hydrationErrorState.warning = warning;
}
}
/*
* Some hydration errors in React 18 does not have the diff in the error message.
* Instead it has the error stack trace which is component stack that we can leverage.
* Will parse the diff from the error stack trace
* e.g.
* Warning: Expected server HTML to contain a matching <div> in <p>.
* at div
* at p
* at div
* at div
* at Page
* output:
* <Page>
* <div>
* <p>
* > <div>
*
*/ function generateHydrationDiffReact18(message, firstContent, secondContent, lastArg) {
const componentStack = lastArg;
let firstIndex = -1;
let secondIndex = -1;
const hydrationWarningType = getHydrationWarningType(message);
// at div\n at Foo\n at Bar (....)\n -> [div, Foo]
const components = componentStack.split('\n')// .reverse()
.map((line, index)=>{
// `<space>at <component> (<location>)` -> `at <component> (<location>)`
line = line.trim();
// extract `<space>at <component>` to `<<component>>`
// e.g. ` at Foo` -> `<Foo>`
const [, component, location] = /at (\w+)( \((.*)\))?/.exec(line) || [];
// If there's no location then it's user-land stack frame
if (!location) {
if (component === firstContent && firstIndex === -1) {
firstIndex = index;
} else if (component === secondContent && secondIndex === -1) {
secondIndex = index;
}
}
return location ? '' : component;
}).filter(Boolean).reverse();
let diff = '';
for(let i = 0; i < components.length; i++){
const component = components[i];
const matchFirstContent = hydrationWarningType === 'tag' && i === components.length - firstIndex - 1;
const matchSecondContent = hydrationWarningType === 'tag' && i === components.length - secondIndex - 1;
if (matchFirstContent || matchSecondContent) {
const spaces = ' '.repeat(Math.max(i * 2 - 2, 0) + 2);
diff += `> ${spaces}<${component}>\n`;
} else {
const spaces = ' '.repeat(i * 2 + 2);
diff += `${spaces}<${component}>\n`;
}
}
if (hydrationWarningType === 'text') {
const spaces = ' '.repeat(components.length * 2);
diff += `+ ${spaces}"${firstContent}"\n`;
diff += `- ${spaces}"${secondContent}"\n`;
} else if (hydrationWarningType === 'text-in-tag') {
const spaces = ' '.repeat(components.length * 2);
diff += `> ${spaces}<${secondContent}>\n`;
diff += `> ${spaces}"${firstContent}"\n`;
}
return diff;
}
//# sourceMappingURL=hydration-error-state.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
import React from 'react';
export class PagesDevOverlayErrorBoundary extends React.PureComponent {
static getDerivedStateFromError(error) {
return {
error
};
}
// Explicit type is needed to avoid the generated `.d.ts` having a wide return type that could be specific to the `@types/react` version.
render() {
// The component has to be unmounted or else it would continue to error
return this.state.error ? null : this.props.children;
}
constructor(...args){
super(...args), this.state = {
error: null
};
}
}
//# sourceMappingURL=pages-dev-overlay-error-boundary.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/next-devtools/userspace/pages/pages-dev-overlay-error-boundary.tsx"],"sourcesContent":["import React from 'react'\n\ntype PagesDevOverlayErrorBoundaryProps = {\n children?: React.ReactNode\n}\ntype PagesDevOverlayErrorBoundaryState = { error: Error | null }\n\nexport class PagesDevOverlayErrorBoundary extends React.PureComponent<\n PagesDevOverlayErrorBoundaryProps,\n PagesDevOverlayErrorBoundaryState\n> {\n state = { error: null }\n\n static getDerivedStateFromError(error: Error) {\n return { error }\n }\n\n // Explicit type is needed to avoid the generated `.d.ts` having a wide return type that could be specific to the `@types/react` version.\n render(): React.ReactNode {\n // The component has to be unmounted or else it would continue to error\n return this.state.error ? null : this.props.children\n }\n}\n"],"names":["React","PagesDevOverlayErrorBoundary","PureComponent","getDerivedStateFromError","error","render","state","props","children"],"mappings":"AAAA,OAAOA,WAAW,QAAO;AAOzB,OAAO,MAAMC,qCAAqCD,MAAME,aAAa;IAMnE,OAAOC,yBAAyBC,KAAY,EAAE;QAC5C,OAAO;YAAEA;QAAM;IACjB;IAEA,yIAAyI;IACzIC,SAA0B;QACxB,uEAAuE;QACvE,OAAO,IAAI,CAACC,KAAK,CAACF,KAAK,GAAG,OAAO,IAAI,CAACG,KAAK,CAACC,QAAQ;IACtD;;QAdK,qBAILF,QAAQ;YAAEF,OAAO;QAAK;;AAWxB","ignoreList":[0]}

View File

@@ -0,0 +1,85 @@
import { jsx as _jsx } from "react/jsx-runtime";
import React from 'react';
import { renderPagesDevOverlay } from 'next/dist/compiled/next-devtools';
import { dispatcher } from 'next/dist/compiled/next-devtools';
import { attachHydrationErrorState, storeHydrationErrorStateFromConsoleArgs } from './hydration-error-state';
import { Router } from '../../../client/router';
import { getOwnerStack } from '../app/errors/stitched-error';
import { isRecoverableError } from '../../../client/react-client-callbacks/on-recoverable-error';
import { getSquashedHydrationErrorDetails } from './hydration-error-state';
import { PagesDevOverlayErrorBoundary } from './pages-dev-overlay-error-boundary';
import { initializeDebugLogForwarding, forwardUnhandledError, logUnhandledRejection, forwardErrorLog } from '../app/forward-logs';
const usePagesDevOverlayBridge = ()=>{
React.useInsertionEffect(()=>{
// NDT uses a different React instance so it's not technically a state update
// scheduled from useInsertionEffect.
renderPagesDevOverlay(getOwnerStack, getSquashedHydrationErrorDetails, isRecoverableError);
}, []);
React.useEffect(()=>{
const { handleStaticIndicator } = require('../../../client/dev/hot-reloader/pages/hot-reloader-pages');
Router.events.on('routeChangeComplete', handleStaticIndicator);
return function() {
Router.events.off('routeChangeComplete', handleStaticIndicator);
};
}, []);
};
export function PagesDevOverlayBridge({ children }) {
usePagesDevOverlayBridge();
return /*#__PURE__*/ _jsx(PagesDevOverlayErrorBoundary, {
children: children
});
}
let isRegistered = false;
function handleError(error) {
if (!error || !(error instanceof Error) || typeof error.stack !== 'string') {
// A non-error was thrown, we don't have anything to show. :-(
return;
}
attachHydrationErrorState(error);
// Skip ModuleBuildError and ModuleNotFoundError, as it will be sent through onBuildError callback.
// This is to avoid same error as different type showing up on client to cause flashing.
if (error.name !== 'ModuleBuildError' && error.name !== 'ModuleNotFoundError') {
dispatcher.onUnhandledError(error);
}
}
let origConsoleError = console.error;
function nextJsHandleConsoleError(...args) {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
const maybeError = process.env.NODE_ENV !== 'production' ? args[1] : args[0];
storeHydrationErrorStateFromConsoleArgs(...args);
// TODO: Surfaces non-errors logged via `console.error`.
handleError(maybeError);
forwardErrorLog(args);
origConsoleError.apply(window.console, args);
}
function onUnhandledError(event) {
const error = event?.error;
handleError(error);
if (error) {
forwardUnhandledError(error);
}
}
function onUnhandledRejection(ev) {
const reason = ev?.reason;
if (!reason || !(reason instanceof Error) || typeof reason.stack !== 'string') {
// A non-error was thrown, we don't have anything to show. :-(
return;
}
dispatcher.onUnhandledRejection(reason);
logUnhandledRejection(reason);
}
export function register() {
if (isRegistered) {
return;
}
isRegistered = true;
try {
Error.stackTraceLimit = 50;
} catch {}
initializeDebugLogForwarding('pages');
window.addEventListener('error', onUnhandledError);
window.addEventListener('unhandledrejection', onUnhandledRejection);
window.console.error = nextJsHandleConsoleError;
}
//# sourceMappingURL=pages-dev-overlay-setup.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
'use client';
import { useEffect, useTransition } from 'react';
import { dispatcher } from 'next/dist/compiled/next-devtools';
export const useAppDevRenderingIndicator = ()=>{
const [isPending, startTransition] = useTransition();
useEffect(()=>{
if (isPending) {
dispatcher.renderingIndicatorShow();
} else {
dispatcher.renderingIndicatorHide();
}
}, [
isPending
]);
return startTransition;
};
//# sourceMappingURL=use-app-dev-rendering-indicator.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/next-devtools/userspace/use-app-dev-rendering-indicator.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useTransition } from 'react'\nimport { dispatcher } from 'next/dist/compiled/next-devtools'\n\nexport const useAppDevRenderingIndicator = () => {\n const [isPending, startTransition] = useTransition()\n\n useEffect(() => {\n if (isPending) {\n dispatcher.renderingIndicatorShow()\n } else {\n dispatcher.renderingIndicatorHide()\n }\n }, [isPending])\n\n return startTransition\n}\n"],"names":["useEffect","useTransition","dispatcher","useAppDevRenderingIndicator","isPending","startTransition","renderingIndicatorShow","renderingIndicatorHide"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,aAAa,QAAQ,QAAO;AAChD,SAASC,UAAU,QAAQ,mCAAkC;AAE7D,OAAO,MAAMC,8BAA8B;IACzC,MAAM,CAACC,WAAWC,gBAAgB,GAAGJ;IAErCD,UAAU;QACR,IAAII,WAAW;YACbF,WAAWI,sBAAsB;QACnC,OAAO;YACLJ,WAAWK,sBAAsB;QACnC;IACF,GAAG;QAACH;KAAU;IAEd,OAAOC;AACT,EAAC","ignoreList":[0]}