diff --git a/packages/vinext/src/shims/app.ts b/packages/vinext/src/shims/app.ts
deleted file mode 100644
index 53ec6849d..000000000
--- a/packages/vinext/src/shims/app.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * next/app shim
- *
- * Provides the AppProps type and default App component for _app.tsx.
- */
-import type { ComponentType } from "react";
-
-export type AppProps
> = {
- Component: ComponentType
;
- pageProps: P;
-};
-
-// Re-export for named import compatibility
-export type { AppProps as default };
diff --git a/packages/vinext/src/shims/app.tsx b/packages/vinext/src/shims/app.tsx
new file mode 100644
index 000000000..d357c2d45
--- /dev/null
+++ b/packages/vinext/src/shims/app.tsx
@@ -0,0 +1,107 @@
+/**
+ * next/app shim
+ *
+ * Provides the AppProps type and a runtime default `App` class component
+ * for Pages Router fixtures that follow the canonical `_app.js` pattern:
+ *
+ * import App from "next/app";
+ * export default class MyApp extends App { ... }
+ *
+ * or call `App.getInitialProps(appContext)` from a custom getInitialProps.
+ *
+ * Ported from Next.js:
+ * https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx
+ *
+ * Behavioural parity notes:
+ * - `App.getInitialProps(appContext)` returns `{ pageProps }`, where
+ * `pageProps` comes from the wrapped page's own `getInitialProps` (if
+ * any). This matches Next.js's behaviour via `loadGetInitialProps`.
+ * - `render()` returns `` — the default
+ * behaviour Next.js documents for the built-in App.
+ * - `origGetInitialProps` is preserved alongside `getInitialProps` for
+ * userland code that introspects the original implementation.
+ *
+ * Type signatures mirror Next.js's intentionally permissive `
`
+ * generics so that userland subclasses like `class MyApp extends App`
+ * type-check without forcing the caller to supply generic parameters.
+ */
+// oxlint-disable typescript/no-explicit-any -- match Next.js's permissive _app.tsx generics
+import React, { type ComponentType } from "react";
+
+export type AppProps
= {
+ Component: ComponentType
& {
+ getInitialProps?: (ctx: any) => any;
+ };
+ pageProps: P;
+ router?: any;
+ __N_SSG?: boolean;
+ __N_SSP?: boolean;
+};
+
+/**
+ * The context passed to `App.getInitialProps`. Mirrors Next.js's
+ * `AppContextType` from `packages/next/src/shared/lib/utils.ts`.
+ */
+export type AppContext = {
+ Component: ComponentType & {
+ getInitialProps?: (ctx: any) => any;
+ };
+ AppTree: ComponentType;
+ ctx: any;
+ router: any;
+};
+
+/**
+ * The initial props shape returned by `App.getInitialProps`. Mirrors
+ * Next.js's `AppInitialProps` from `packages/next/src/shared/lib/utils.ts`.
+ */
+export type AppInitialProps = {
+ pageProps: PageProps;
+};
+
+async function appGetInitialProps({ Component, ctx }: AppContext): Promise {
+ // Next.js delegates this to `loadGetInitialProps(Component, ctx)`. For the
+ // canonical _app pattern the relevant behaviour is: invoke the wrapped
+ // page's `getInitialProps` if defined, otherwise return `{}` for
+ // pageProps. We replicate that minimal shape without pulling in the
+ // full development-only validation logic from utils.ts.
+ let pageProps: any = {};
+ if (typeof Component.getInitialProps === "function") {
+ pageProps = await Component.getInitialProps(ctx);
+ // Divergence from Next.js (intentional, current scope):
+ //
+ // Next.js's `loadGetInitialProps` throws when a page's getInitialProps
+ // resolves to null/undefined:
+ //
+ // ".getInitialProps() should resolve to an object.
+ // But found "null"/"undefined" instead."
+ //
+ // See: https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils.ts
+ //
+ // vinext currently coerces the missing value to `{}` so that fixtures
+ // and userland code that accidentally return nothing still render
+ // (just with empty pageProps) instead of crashing the page. If you're
+ // debugging a Pages Router page that renders with mysteriously empty
+ // props, suspect a `getInitialProps` that returns undefined — Next.js
+ // would have surfaced that as a thrown error at this point.
+ if (pageProps == null) {
+ pageProps = {};
+ }
+ }
+ return { pageProps };
+}
+
+export default class App extends React.Component
, S> {
+ static origGetInitialProps = appGetInitialProps;
+ static getInitialProps = appGetInitialProps;
+
+ render(): React.ReactNode {
+ const { Component, pageProps } = this.props as AppProps;
+ // Cast to ComponentType so the JSX spread type-checks regardless
+ // of the user-supplied `CP` generic. Mirrors how Next.js's _app.tsx
+ // works in practice: callers extending `App` rarely supply explicit
+ // page-prop generics, so the spread has to be permissive here.
+ const PageComponent = Component as ComponentType;
+ return ;
+ }
+}
diff --git a/packages/vinext/src/shims/next-shims.d.ts b/packages/vinext/src/shims/next-shims.d.ts
index d5c7c0f13..c254aab14 100644
--- a/packages/vinext/src/shims/next-shims.d.ts
+++ b/packages/vinext/src/shims/next-shims.d.ts
@@ -504,11 +504,46 @@ declare module "next/font/local" {
}
declare module "next/app" {
- import { ComponentType } from "react";
- export type AppProps = {
- Component: ComponentType;
- pageProps: Record;
+ import * as React from "react";
+ import type { ComponentType } from "react";
+
+ export type AppProps = {
+ Component: ComponentType
& {
+ getInitialProps?: (ctx: any) => any;
+ };
+ pageProps: P;
+ router?: any;
+ __N_SSG?: boolean;
+ __N_SSP?: boolean;
+ };
+
+ export type AppContext = {
+ Component: ComponentType & {
+ getInitialProps?: (ctx: any) => any;
+ };
+ AppTree: ComponentType;
+ ctx: any;
+ router: any;
};
+
+ export type AppInitialProps = {
+ pageProps: PageProps;
+ };
+
+ /**
+ * Default `App` class component used by Pages Router `_app.js`. Mirrors
+ * Next.js's `packages/next/src/pages/_app.tsx` so userland code can
+ * `import App from "next/app"` and either subclass or call
+ * `App.getInitialProps(appContext)` directly.
+ */
+ export default class App extends React.Component<
+ P & AppProps,
+ S
+ > {
+ static origGetInitialProps: (ctx: AppContext) => Promise;
+ static getInitialProps: (ctx: AppContext) => Promise;
+ render(): React.ReactNode;
+ }
}
declare module "next/cache" {
diff --git a/tests/shims.test.ts b/tests/shims.test.ts
index 8867b0332..a35426e5e 100644
--- a/tests/shims.test.ts
+++ b/tests/shims.test.ts
@@ -12120,6 +12120,84 @@ describe("next/error shim", () => {
});
});
+// next/app default export
+//
+// Ported from Next.js: packages/next/src/pages/_app.tsx
+// https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx
+//
+// Pages Router fixtures very commonly do:
+// import App from "next/app";
+// export default class MyApp extends App { ... }
+// or call App.getInitialProps(appContext) from a custom getInitialProps.
+// Without a runtime default export, every such fixture fails to build with
+// "[MISSING_EXPORT] 'default' is not exported by ...shims/app.js".
+describe("next/app shim", () => {
+ it("exports a React class component as default", async () => {
+ const React = await import("react");
+ const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
+ expect(typeof AppDefault).toBe("function");
+ // Class component: prototype must have a render method, instances must
+ // be instances of React.Component.
+ expect(typeof AppDefault.prototype.render).toBe("function");
+ const instance = new AppDefault({ Component: () => null, pageProps: {} });
+ expect(instance).toBeInstanceOf(React.Component);
+ });
+
+ it("default App.render() returns ", async () => {
+ const React = await import("react");
+ const { renderToStaticMarkup } = await import("react-dom/server");
+ const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
+
+ function Page(props: { greeting: string }) {
+ return React.createElement("p", null, props.greeting);
+ }
+
+ // Use explicit generics so React.createElement's prop type inference
+ // lines up with `AppProps<{ greeting: string }>`.
+ const html = renderToStaticMarkup(
+ React.createElement(AppDefault, {
+ Component: Page,
+ pageProps: { greeting: "hello world" },
+ }),
+ );
+ expect(html).toBe("hello world
");
+ });
+
+ it("App.getInitialProps is a function and forwards Component.getInitialProps result as pageProps", async () => {
+ const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
+ expect(typeof AppDefault.getInitialProps).toBe("function");
+ // origGetInitialProps is preserved for userland code that introspects it.
+ expect(typeof AppDefault.origGetInitialProps).toBe("function");
+
+ const pageCtx = { req: { url: "/test" } };
+ const Component = Object.assign(() => null, {
+ getInitialProps: async (ctx: unknown) => {
+ expect(ctx).toBe(pageCtx);
+ return { foo: "bar" };
+ },
+ });
+
+ const result = await AppDefault.getInitialProps({
+ Component,
+ AppTree: () => null,
+ ctx: pageCtx,
+ router: {},
+ });
+ expect(result).toEqual({ pageProps: { foo: "bar" } });
+ });
+
+ it("App.getInitialProps returns { pageProps: {} } when Component has no getInitialProps", async () => {
+ const AppDefault = (await import("../packages/vinext/src/shims/app.js")).default;
+ const result = await AppDefault.getInitialProps({
+ Component: () => null,
+ AppTree: () => null,
+ ctx: {},
+ router: {},
+ });
+ expect(result).toEqual({ pageProps: {} });
+ });
+});
+
describe("next/constants shim", () => {
it("exports all phase constants", async () => {
const constants = await import("../packages/vinext/src/shims/constants.js");