Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions packages/vinext/src/shims/app.ts

This file was deleted.

107 changes: 107 additions & 0 deletions packages/vinext/src/shims/app.tsx
Original file line number Diff line number Diff line change
@@ -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 `<Component {...pageProps} />` — 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 `<P = any>`
* 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<P = any> = {
Component: ComponentType<P> & {
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<any> & {
getInitialProps?: (ctx: any) => any;
};
AppTree: ComponentType<any>;
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 = any> = {
pageProps: PageProps;
};

async function appGetInitialProps({ Component, ctx }: AppContext): Promise<AppInitialProps> {
// 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:
//
// "<DisplayName>.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 = {};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Next.js's loadGetInitialProps throws here when the result is null/undefined ("should resolve to an object. But found \"${props}\" instead."), while this silently coerces to {}. The PR documents this as intentional and it's fine for the current scope — well-behaved getInitialProps implementations won't return nullish. Just flagging for awareness: if a future deploy-suite failure shows a page rendering with empty props when it should have errored, this is the divergence point.

}
return { pageProps };
}

export default class App<P = any, CP = any, S = any> extends React.Component<P & AppProps<CP>, S> {
static origGetInitialProps = appGetInitialProps;
static getInitialProps = appGetInitialProps;

render(): React.ReactNode {
const { Component, pageProps } = this.props as AppProps<CP>;
// Cast to ComponentType<any> 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<any>;
return <PageComponent {...pageProps} />;
}
}
43 changes: 39 additions & 4 deletions packages/vinext/src/shims/next-shims.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,11 +504,46 @@ declare module "next/font/local" {
}

declare module "next/app" {
import { ComponentType } from "react";
export type AppProps = {
Component: ComponentType<any>;
pageProps: Record<string, unknown>;
import * as React from "react";
import type { ComponentType } from "react";

export type AppProps<P = any> = {
Component: ComponentType<P> & {
getInitialProps?: (ctx: any) => any;
};
pageProps: P;
router?: any;
__N_SSG?: boolean;
__N_SSP?: boolean;
};

export type AppContext = {
Component: ComponentType<any> & {
getInitialProps?: (ctx: any) => any;
};
AppTree: ComponentType<any>;
ctx: any;
router: any;
};

export type AppInitialProps<PageProps = any> = {
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<P = any, CP = any, S = any> extends React.Component<
P & AppProps<CP>,
S
> {
static origGetInitialProps: (ctx: AppContext) => Promise<AppInitialProps>;
static getInitialProps: (ctx: AppContext) => Promise<AppInitialProps>;
render(): React.ReactNode;
}
}

declare module "next/cache" {
Expand Down
78 changes: 78 additions & 0 deletions tests/shims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Component {...pageProps} />", 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<unknown, { greeting: string }>, {
Component: Page,
pageProps: { greeting: "hello world" },
}),
);
expect(html).toBe("<p>hello world</p>");
});

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");
Expand Down
Loading