Skip to content

fix(shims): add runtime default export to next/app#1204

Merged
james-elicx merged 2 commits into
mainfrom
fix/shim-app-default-export
May 14, 2026
Merged

fix(shims): add runtime default export to next/app#1204
james-elicx merged 2 commits into
mainfrom
fix/shim-app-default-export

Conversation

@james-elicx
Copy link
Copy Markdown
Collaborator

Summary

Adds a runtime default export to packages/vinext/src/shims/app.ts (now .tsx) so Pages Router fixtures that follow the canonical pages/_app.js pattern can build under vinext.

The problem

Pages Router code that does:

import App from "next/app";
export default class MyApp extends App {
  static async getInitialProps(appContext) {
    const appProps = await App.getInitialProps(appContext);
    return { ...appProps, extraProp: "Hi There" };
  }
  // ...
}

currently fails to build under vinext with:

[MISSING_EXPORT] "default" is not exported by "node_modules/.../vinext/dist/shims/app.js"

The existing shim only re-exported a type as default (export type { AppProps as default }), with no runtime value.

This is the single highest-leverage shim fix in the Next.js deploy suite: 51 build failures all trace back to this missing default export.

What I ported

From packages/next/src/pages/_app.tsx (Next.js canary):

  • App extends React.Component<P & AppProps<CP>, S> and renders <Component {...pageProps} />.
  • App.getInitialProps(appContext) returns { pageProps } — invokes the wrapped page's getInitialProps if defined, otherwise returns { pageProps: {} }.
  • App.origGetInitialProps is preserved alongside getInitialProps (Next.js userland code occasionally introspects this).
  • AppProps, AppContext, and AppInitialProps types are exported.

The original Next.js implementation delegates to loadGetInitialProps in shared/lib/utils.ts. For the canonical _app pattern, the only relevant behaviour is the pageProps extraction, which is replicated inline; the dev-only validation (getInitialProps as an instance method, response-sent check, empty-object warning) is intentionally omitted — see "Intentional omissions" below.

Tests

tests/shims.test.ts gains a next/app shim describe with four cases:

  1. The default export is a React class component (has prototype.render, instances are React.Component instances).
  2. App.render() produces <Component {...pageProps} /> (asserted via renderToStaticMarkup).
  3. App.getInitialProps is a function, returns { pageProps: <result> } when Component.getInitialProps is defined, and origGetInitialProps is preserved.
  4. App.getInitialProps returns { pageProps: {} } when Component has no getInitialProps.

Run with: vp test run tests/shims.test.ts -t "next/app shim" — all 4 pass, full file (870 tests) green.

Intentional omissions

These bits of Next.js's _app.tsx were intentionally left out because they are not exercised by Pages Router test fixtures and would expand the shim surface beyond what the failing deploy-suite builds need:

  • loadGetInitialProps dev-mode validation — Next.js throws if getInitialProps is defined as an instance method, warns on empty objects, and checks isResSent(res). Userland code calling App.getInitialProps(appContext) doesn't depend on these. If the deploy suite later catches a fixture that relies on them, they're easy to port over.
  • AppType, AppPropsType, AppTreeType as re-exports — the typescript test suite imports AppType from next/app, but no JS fixture does. Adding these is a follow-up if the typing parity gap matters.
  • Detailed NextPageContext / Router typingctx and router are typed as any here, matching Next.js's permissive _app.tsx generics (<P = any>).

Verification

  • vp test run tests/shims.test.ts — 870/870 pass
  • vp check — formatting, lint, type checks clean

Related

This is the first of a series of focused shim fixes against the Next.js deploy suite. Other shim-level parity gaps are separate PRs.

Pages Router fixtures using the canonical `pages/_app.js` pattern
(`import App from 'next/app'; export default class MyApp extends App`)
currently fail to build under vinext with:

  [MISSING_EXPORT] "default" is not exported by
  "node_modules/.../vinext/dist/shims/app.js"

The existing shim only re-exported a type as default. This adds a real
runtime class component matching Next.js's behaviour:

- `App` extends React.Component, renders `<Component {...pageProps} />`.
- `App.getInitialProps(appContext)` returns `{ pageProps }`, invoking the
  wrapped page's `getInitialProps` if defined.
- `App.origGetInitialProps` is preserved alongside `getInitialProps`.
- AppProps, AppContext, AppInitialProps types are exported.

Ported from:
  https://github.com/vercel/next.js/blob/canary/packages/next/src/pages/_app.tsx

This unblocks 51 build failures in the Next.js deploy suite that all
trace back to the missing default export.
@james-elicx
Copy link
Copy Markdown
Collaborator Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1204

commit: 5773166

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Looks good. The shim faithfully ports the essential behavior from Next.js _app.tsx — class shape, origGetInitialProps/getInitialProps statics, and the render() delegation all match upstream.

Tests are thorough: class component shape assertion, renderToStaticMarkup verification of render output, getInitialProps delegation with and without Component.getInitialProps.

The intentional omissions (dev-mode validation from loadGetInitialProps, AppType/AppPropsType re-exports) are well-documented and reasonable for the current scope.

Minor gap (not blocking): next-shims.d.ts lines 506-512 still declare the old minimal AppProps-only shape without the default class export, AppContext, or AppInitialProps. Since the shim map in index.ts resolves to the source .tsx file directly, this does not affect builds — but external .d.ts consumers would not see the new types. Fine as a follow-up.

One nit inline on null-coercion behavior.

pageProps = await Component.getInitialProps(ctx);
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.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 14, 2026

Review posted successfully. Here's a summary of what I reviewed and the outcome:

Verdict: APPROVED

The PR adds a runtime default export to the next/app shim, replacing a type-only default export that caused 51 build failures across the deploy suite. The implementation faithfully ports the core behavior from Next.js's _app.tsx:

  • App class extends React.Component with render() returning <Component {...pageProps} />
  • Static getInitialProps and origGetInitialProps delegate to the wrapped page's getInitialProps
  • Tests cover class shape, render output, getInitialProps delegation, and the no-getInitialProps fallback

One nit flagged (inline comment on app.tsx:73): Next.js's loadGetInitialProps throws when getInitialProps returns null/undefined, while this shim silently coerces to {}. Documented as intentional, fine for current scope.

One minor gap noted (not blocking): next-shims.d.ts still has the old minimal type declarations and doesn't reflect the new default class export or AppContext/AppInitialProps types. Since the shim map resolves to the source .tsx directly, this doesn't affect builds — but worth a follow-up for external .d.ts consumers.

github run

…l coercion

Address two nits from the PR #1204 bonk review:

1. `next-shims.d.ts` still declared the old minimal `AppProps`-only shape.
   External `.d.ts` consumers (editor, type checks against the published
   package) couldn't see the new runtime class, `AppContext`, or
   `AppInitialProps`. Update the `declare module 'next/app'` block to
   declare the full surface added in the shim:
     - default `class App<P, CP, S>` extending `React.Component` with
       `static origGetInitialProps` / `static getInitialProps` and a
       `render(): React.ReactNode` method
     - `AppContext` and `AppInitialProps<P>` type aliases
     - Expanded `AppProps<P>` shape (router, __N_SSG/__N_SSP flags)

2. `appGetInitialProps` silently coerces a null/undefined return from
   `Component.getInitialProps` to `{}`. Next.js's `loadGetInitialProps`
   throws in that case. Add an inline comment at the coercion site
   noting the divergence, linking to the upstream source, and giving
   future maintainers a bread crumb when they're debugging Pages Router
   pages that render with mysteriously empty props.
@james-elicx
Copy link
Copy Markdown
Collaborator Author

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Clean, well-scoped fix. The shim faithfully replicates the essential runtime behavior from Next.js's _app.tsx — class shape, getInitialProps/origGetInitialProps statics, and render() delegation all match upstream. The type declarations in next-shims.d.ts were updated to reflect the new default class export, AppContext, and AppInitialProps. Tests are solid.

Ship it.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 14, 2026

Review posted: APPROVED

The PR is clean and well-scoped. Here's my assessment:

What it does: Replaces the type-only default export in packages/vinext/src/shims/app.ts (now .tsx) with a real runtime App class component, unblocking 51 deploy-suite build failures that all trace to [MISSING_EXPORT] "default" is not exported by .../shims/app.js.

What I verified:

  • The App class correctly extends React.Component with render() returning <Component {...pageProps} />
  • Static getInitialProps and origGetInitialProps faithfully port the core behavior from Next.js's _app.tsx
  • The null-coercion divergence (line 87-89) is well-documented and reasonable for current scope
  • next-shims.d.ts was updated to reflect the new default class export plus AppContext/AppInitialProps types
  • Four focused test cases cover class shape, render output, getInitialProps delegation, and the no-getInitialProps fallback
  • CI Check job passed; deploys and E2E tests all green

github run

@james-elicx james-elicx marked this pull request as ready for review May 14, 2026 15:14
@james-elicx james-elicx merged commit a6d9826 into main May 14, 2026
35 checks passed
@james-elicx james-elicx deleted the fix/shim-app-default-export branch May 14, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant