Skip to content
Open
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
5 changes: 2 additions & 3 deletions packages/vinext/src/entries/pages-server-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,10 @@ async function _renderIsrPassToStringAsync(element) {
);
}

${docImportCode}
${appImportCode}
${pageImports.join("\n")}
${apiImports.join("\n")}

${appImportCode}
${docImportCode}
${errorImportCode}

export const pageRoutes = [
Expand Down
106 changes: 50 additions & 56 deletions packages/vinext/src/server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,40 @@ async function collectDevInitialStylesheetHeadHTML(
return html;
}

async function loadPagesRootComponents(
runner: ModuleImporter,
pagesDir: string,
matcher: ValidFileMatcher,
options: { includeDocument?: boolean } = {},
): Promise<{
// oxlint-disable-next-line typescript/no-explicit-any
DocumentComponent: any;
// oxlint-disable-next-line typescript/no-explicit-any
AppComponent: any;
appAssetPath: string | null;
}> {
const { includeDocument = true } = options;
// oxlint-disable-next-line typescript/no-explicit-any
let DocumentComponent: any = null;
if (includeDocument) {
const documentAssetPath = findFileWithExts(pagesDir, "_document", matcher);
if (documentAssetPath) {
const documentModule = await importModule(runner, documentAssetPath);
DocumentComponent = documentModule.default ?? null;
}
}

// oxlint-disable-next-line typescript/no-explicit-any
let AppComponent: any = null;
const appAssetPath = findFileWithExts(pagesDir, "_app", matcher);
if (appAssetPath) {
const appModule = await importModule(runner, appAssetPath);
AppComponent = appModule.default ?? null;
}

return { DocumentComponent, AppComponent, appAssetPath };
}

/**
* Emit a `getServerSideProps` / `getStaticProps` `{ redirect }` result.
*
Expand Down Expand Up @@ -778,23 +812,18 @@ export function createSSRHandler(
}
}

// Load the page module through Vite's SSR pipeline
// This gives us HMR and transform support for free
// Next.js evaluates _app before the matched page on every dev path,
// while _document is only evaluated for HTML rendering.
const { DocumentComponent, AppComponent } = await loadPagesRootComponents(
runner,
pagesDir,
matcher,
{ includeDocument: !isDataReq },
);
// Load the page module through Vite's SSR pipeline after the root
// components. This gives us HMR and transform support for free while
// preserving Next.js module evaluation order.
const pageModule = await importModule(runner, route.filePath);
// Try to load _app.tsx if it exists. This happens before the readiness
// predicate so app-level getInitialProps participates in the same
// initial Pages Router state as the client __NEXT_DATA__ payload.
// oxlint-disable-next-line typescript/no-explicit-any
let AppComponent: any = null;
const appPath = path.join(pagesDir, "_app");
if (findFileWithExtensions(appPath, matcher)) {
try {
const appModule = await importModule(runner, appPath);
AppComponent = appModule.default ?? null;
} catch {
// _app exists but failed to load
}
}
const pagesNextData = buildPagesReadinessNextData({
pageModule,
appComponent: AppComponent,
Expand Down Expand Up @@ -1860,19 +1889,6 @@ hydrate();
},
)}</script>`;

// Try to load custom _document.tsx
const docPath = path.join(pagesDir, "_document");
// oxlint-disable-next-line typescript/no-explicit-any
let DocumentComponent: any = null;
if (findFileWithExtensions(docPath, matcher)) {
try {
const docModule = (await runner.import(docPath)) as Record<string, unknown>;
DocumentComponent = docModule.default ?? null;
} catch {
// _document exists but failed to load
}
}

// Expose page route patterns on window before hydration so the
// next/navigation compat hooks can resolve a dynamic pattern from a
// resolved path, matching the production client entry. Kept in its own
Expand Down Expand Up @@ -2090,6 +2106,11 @@ async function renderErrorPage(
// Try specific status page first, then _error, then fallback
const candidates =
statusCode === 404 ? ["404", "_error"] : statusCode === 500 ? ["500", "_error"] : ["_error"];
const { DocumentComponent, AppComponent, appAssetPath } = await loadPagesRootComponents(
runner,
pagesDir,
matcher,
);

for (const candidate of candidates) {
try {
Expand All @@ -2100,20 +2121,6 @@ async function renderErrorPage(
const ErrorComponent = errorModule.default;
if (!ErrorComponent) continue;

// Try to load _app.tsx to wrap the error page
// oxlint-disable-next-line typescript/no-explicit-any
let AppComponent: any = null;
const appPathErr = path.join(pagesDir, "_app");
const appAssetPath = findFileWithExts(pagesDir, "_app", matcher);
if (findFileWithExtensions(appPathErr, matcher)) {
try {
const appModule = await importModule(runner, appAssetPath ?? appPathErr);
AppComponent = appModule.default ?? null;
} catch {
// _app exists but failed to load
}
}

const createElement = React.createElement;
const initialErrorProps = await loadPagesGetInitialProps(ErrorComponent, {
req,
Expand All @@ -2138,19 +2145,6 @@ async function renderErrorPage(
}
}

// Try custom _document
// oxlint-disable-next-line typescript/no-explicit-any
let DocumentComponent: any = null;
const docPathErr = path.join(pagesDir, "_document");
if (findFileWithExtensions(docPathErr, matcher)) {
try {
const docModule = await importModule(runner, docPathErr);
DocumentComponent = docModule.default ?? null;
} catch {
// _document exists but failed to load
}
}

const createErrorElement = (
// oxlint-disable-next-line typescript/no-explicit-any
FinalApp: any,
Expand Down
47 changes: 47 additions & 0 deletions tests/entry-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,53 @@ describe("Pages Router entry template", () => {
}
});

it("imports Pages root components in _document > _app > page order", async () => {
// Ported from Next.js: test/e2e/app-document-import-order/app-document-import-order.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-document-import-order/app-document-import-order.test.ts
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-pages-import-order-entry-"));
const pagesDir = path.join(tmpDir, "pages");

try {
fs.mkdirSync(pagesDir, { recursive: true });
fs.writeFileSync(
path.join(pagesDir, "_document.tsx"),
"export default function Document() { return null; }",
);
fs.writeFileSync(
path.join(pagesDir, "_app.tsx"),
"export default function App() { return null; }",
);
fs.writeFileSync(
path.join(pagesDir, "index.tsx"),
"export default function Page() { return null; }",
);

const code = await generateServerEntry(
pagesDir,
await resolveNextConfig({}),
createValidFileMatcher(),
null,
null,
);

const documentImportIndex = code.indexOf(
`import { default as DocumentComponent } from ${JSON.stringify(path.join(pagesDir, "_document.tsx"))}`,
);
const appImportIndex = code.indexOf(
`import { default as AppComponent } from ${JSON.stringify(path.join(pagesDir, "_app.tsx"))}`,
);
const pageImportIndex = code.indexOf(
`import * as page_0 from ${JSON.stringify(path.join(pagesDir, "index.tsx"))}`,
);

expect(documentImportIndex).toBeGreaterThanOrEqual(0);
expect(appImportIndex).toBeGreaterThan(documentImportIndex);
expect(pageImportIndex).toBeGreaterThan(appImportIndex);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("precomputes Pages route dataKind in the server entry", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-pages-data-kind-entry-"));
const pagesDir = path.join(tmpDir, "pages");
Expand Down
Loading
Loading