diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 9d92207b222ae5b..0213ea62b1fd350 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -1,4 +1,5 @@ import path from 'path' +import { promises as fs } from 'fs' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_LAYERS } from '../../../lib/constants' @@ -17,6 +18,7 @@ function createTypeGuardFile( relativePath: string, options: { type: 'layout' | 'page' + slots?: string[] } ) { return `// File: ${fullPath} @@ -32,6 +34,11 @@ interface PageProps { } interface LayoutProps { children: React.ReactNode +${ + options.slots + ? options.slots.map((slot) => ` ${slot}: React.ReactNode`).join('\n') + : '' +} params: any } @@ -68,6 +75,18 @@ type NonNegative = T extends Zero ? T : Negative extends n ` } +async function collectNamedSlots(layoutPath: string) { + const layoutDir = path.dirname(layoutPath) + const items = await fs.readdir(layoutDir, { withFileTypes: true }) + const slots = [] + for (const item of items) { + if (item.isDirectory() && item.name.startsWith('@')) { + slots.push(item.name.slice(1)) + } + } + return slots +} + export class FlightTypesPlugin { dir: string appDir: string @@ -84,7 +103,7 @@ export class FlightTypesPlugin { apply(compiler: webpack.Compiler) { const assetPrefix = this.dev ? '..' : this.isEdgeServer ? '..' : '../..' - const handleModule = (_mod: webpack.Module, assets: any) => { + const handleModule = async (_mod: webpack.Module, assets: any) => { if (_mod.layer !== WEBPACK_LAYERS.server) return const mod: webpack.NormalModule = _mod as any @@ -111,9 +130,11 @@ export class FlightTypesPlugin { const assetPath = assetPrefix + '/' + typePath.replace(/\\/g, '/') if (IS_LAYOUT) { + const slots = await collectNamedSlots(mod.resource) assets[assetPath] = new sources.RawSource( createTypeGuardFile(mod.resource, relativeImportPath, { type: 'layout', + slots, }) ) as unknown as webpack.sources.RawSource } else if (IS_PAGE) { @@ -126,19 +147,22 @@ export class FlightTypesPlugin { } compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - compilation.hooks.processAssets.tap( + compilation.hooks.processAssets.tapAsync( { name: PLUGIN_NAME, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, }, - (assets) => { + async (assets, callback) => { + const promises: Promise[] = [] for (const entrypoint of compilation.entrypoints.values()) { for (const chunk of entrypoint.chunks) { compilation.chunkGraph.getChunkModules(chunk).forEach((mod) => { - handleModule(mod, assets) + promises.push(handleModule(mod, assets)) }) } } + await Promise.all(promises) + callback() } ) }) diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index 26aeaf7bd395986..d5f8ef5c9e976dd 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -103,14 +103,24 @@ function getFormattedLayoutAndPageDiagnosticMessageText( } break case 2741: - const incompatProp = item.messageText.match( + const incompatPageProp = item.messageText.match( /Property '(.+)' is missing in type 'PageProps'/ ) - if (incompatProp) { + if (incompatPageProp) { main += '\n' + ' '.repeat(indent * 2) main += `Prop "${chalk.bold( - incompatProp[1] + incompatPageProp[1] )}" will never be passed. Remove it from the component's props.` + } else { + const extraLayoutProp = item.messageText.match( + /Property '(.+)' is missing in type 'LayoutProps' but required in type '(.+)'/ + ) + if (extraLayoutProp) { + main += '\n' + ' '.repeat(indent * 2) + main += `Prop "${chalk.bold( + extraLayoutProp[1] + )}" is not valid for this Layout, remove it to fix.` + } } break default: