diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 9aff27259e628..f5bee9cdc7f73 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -170,7 +170,8 @@ function verifyTypeScriptSetup( disableStaticImages: boolean, cacheDir: string | undefined, numWorkers: number | undefined, - enableWorkerThreads: boolean | undefined + enableWorkerThreads: boolean | undefined, + isAppDirEnabled: boolean ) { const typeCheckWorker = new JestWorker( require.resolve('../lib/verifyTypeScriptSetup'), @@ -194,6 +195,7 @@ function verifyTypeScriptSetup( tsconfigPath, disableStaticImages, cacheDir, + isAppDirEnabled, }) .then((result) => { typeCheckWorker.end() @@ -327,100 +329,111 @@ export default async function build( telemetry.record(events) ) - const ignoreTypeScriptErrors = Boolean( - config.typescript.ignoreBuildErrors - ) - const ignoreESLint = Boolean(config.eslint.ignoreDuringBuilds) - const eslintCacheDir = path.join(cacheDir, 'eslint/') const shouldLint = !ignoreESLint && runLint - if (ignoreTypeScriptErrors) { - Log.info('Skipping validation of types') - } - if (runLint && ignoreESLint) { - // only print log when build requre lint while ignoreESLint is enabled - Log.info('Skipping linting') - } + const startTypeChecking = async () => { + const ignoreTypeScriptErrors = Boolean( + config.typescript.ignoreBuildErrors + ) - let typeCheckingAndLintingSpinnerPrefixText: string | undefined - let typeCheckingAndLintingSpinner: - | ReturnType - | undefined - - if (!ignoreTypeScriptErrors && shouldLint) { - typeCheckingAndLintingSpinnerPrefixText = - 'Linting and checking validity of types' - } else if (!ignoreTypeScriptErrors) { - typeCheckingAndLintingSpinnerPrefixText = 'Checking validity of types' - } else if (shouldLint) { - typeCheckingAndLintingSpinnerPrefixText = 'Linting' - } + const eslintCacheDir = path.join(cacheDir, 'eslint/') - // we will not create a spinner if both ignoreTypeScriptErrors and ignoreESLint are - // enabled, but we will still verifying project's tsconfig and dependencies. - if (typeCheckingAndLintingSpinnerPrefixText) { - typeCheckingAndLintingSpinner = createSpinner({ - prefixText: `${Log.prefixes.info} ${typeCheckingAndLintingSpinnerPrefixText}`, - }) - } + if (ignoreTypeScriptErrors) { + Log.info('Skipping validation of types') + } + if (runLint && ignoreESLint) { + // only print log when build requre lint while ignoreESLint is enabled + Log.info('Skipping linting') + } - const typeCheckStart = process.hrtime() + let typeCheckingAndLintingSpinnerPrefixText: string | undefined + let typeCheckingAndLintingSpinner: + | ReturnType + | undefined + + if (!ignoreTypeScriptErrors && shouldLint) { + typeCheckingAndLintingSpinnerPrefixText = + 'Linting and checking validity of types' + } else if (!ignoreTypeScriptErrors) { + typeCheckingAndLintingSpinnerPrefixText = 'Checking validity of types' + } else if (shouldLint) { + typeCheckingAndLintingSpinnerPrefixText = 'Linting' + } - try { - const [[verifyResult, typeCheckEnd]] = await Promise.all([ - nextBuildSpan.traceChild('verify-typescript-setup').traceAsyncFn(() => - verifyTypeScriptSetup( - dir, - [pagesDir, appDir].filter(Boolean) as string[], - !ignoreTypeScriptErrors, - config.typescript.tsconfigPath, - config.images.disableStaticImages, - cacheDir, - config.experimental.cpus, - config.experimental.workerThreads - ).then((resolved) => { - const checkEnd = process.hrtime(typeCheckStart) - return [resolved, checkEnd] as const - }) - ), - shouldLint && + // we will not create a spinner if both ignoreTypeScriptErrors and ignoreESLint are + // enabled, but we will still verifying project's tsconfig and dependencies. + if (typeCheckingAndLintingSpinnerPrefixText) { + typeCheckingAndLintingSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} ${typeCheckingAndLintingSpinnerPrefixText}`, + }) + } + + const typeCheckStart = process.hrtime() + + try { + const [[verifyResult, typeCheckEnd]] = await Promise.all([ nextBuildSpan - .traceChild('verify-and-lint') - .traceAsyncFn(async () => { - await verifyAndLint( + .traceChild('verify-typescript-setup') + .traceAsyncFn(() => + verifyTypeScriptSetup( dir, - eslintCacheDir, - config.eslint?.dirs, + [pagesDir, appDir].filter(Boolean) as string[], + !ignoreTypeScriptErrors, + config.typescript.tsconfigPath, + config.images.disableStaticImages, + cacheDir, config.experimental.cpus, config.experimental.workerThreads, - telemetry, - isAppDirEnabled && !!appDir - ) - }), - ]) - typeCheckingAndLintingSpinner?.stopAndPersist() - - if (!ignoreTypeScriptErrors && verifyResult) { - telemetry.record( - eventTypeCheckCompleted({ - durationInSeconds: typeCheckEnd[0], - typescriptVersion: verifyResult.version, - inputFilesCount: verifyResult.result?.inputFilesCount, - totalFilesCount: verifyResult.result?.totalFilesCount, - incremental: verifyResult.result?.incremental, - }) - ) - } - } catch (err) { - // prevent showing jest-worker internal error as it - // isn't helpful for users and clutters output - if (isError(err) && err.message === 'Call retries were exceeded') { - process.exit(1) + isAppDirEnabled + ).then((resolved) => { + const checkEnd = process.hrtime(typeCheckStart) + return [resolved, checkEnd] as const + }) + ), + shouldLint && + nextBuildSpan + .traceChild('verify-and-lint') + .traceAsyncFn(async () => { + await verifyAndLint( + dir, + eslintCacheDir, + config.eslint?.dirs, + config.experimental.cpus, + config.experimental.workerThreads, + telemetry, + isAppDirEnabled && !!appDir + ) + }), + ]) + typeCheckingAndLintingSpinner?.stopAndPersist() + + if (!ignoreTypeScriptErrors && verifyResult) { + telemetry.record( + eventTypeCheckCompleted({ + durationInSeconds: typeCheckEnd[0], + typescriptVersion: verifyResult.version, + inputFilesCount: verifyResult.result?.inputFilesCount, + totalFilesCount: verifyResult.result?.totalFilesCount, + incremental: verifyResult.result?.incremental, + }) + ) + } + } catch (err) { + // prevent showing jest-worker internal error as it + // isn't helpful for users and clutters output + if (isError(err) && err.message === 'Call retries were exceeded') { + process.exit(1) + } + throw err } - throw err } + // For app directory, we run type checking after build. That's because + // we dynamically generate types for each layout and page in the app + // directory. + if (!appDir) await startTypeChecking() + const buildLintEvent: EventBuildFeatureUsage = { featureName: 'build-lint', invocationCount: shouldLint ? 1 : 0, @@ -1038,6 +1051,11 @@ export default async function build( } } + // For app directory, we run type checking after build. + if (appDir) { + await startTypeChecking() + } + const postCompileSpinner = createSpinner({ prefixText: `${Log.prefixes.info} Collecting page data`, }) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 65a861f8af11f..474c86bcae3ad 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -47,6 +47,7 @@ import { regexLikeCss } from './webpack/config/blocks/css' import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin' import { FlightManifestPlugin } from './webpack/plugins/flight-manifest-plugin' import { FlightClientEntryPlugin } from './webpack/plugins/flight-client-entry-plugin' +import { FlightTypesPlugin } from './webpack/plugins/flight-types-plugin' import type { Feature, SWC_TARGET_TRIPLE, @@ -1976,6 +1977,10 @@ export default async function getBaseWebpackConfig( dev, isEdgeServer, })), + hasAppDir && + !isClient && + !dev && + new FlightTypesPlugin({ dir, appDir, dev, isEdgeServer }), !dev && isClient && !!config.experimental.sri?.algorithm && diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index 31c8eb5dbcfb5..4427f89bd3d7e 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -17,9 +17,10 @@ import { EDGE_RUNTIME_WEBPACK, FLIGHT_SERVER_CSS_MANIFEST, } from '../../../shared/lib/constants' -import { FlightCSSManifest, traverseModules } from './flight-manifest-plugin' +import { FlightCSSManifest } from './flight-manifest-plugin' import { ASYNC_CLIENT_MODULES } from './flight-manifest-plugin' import { isClientComponentModule, regexCSS } from '../loaders/utils' +import { traverseModules } from '../utils' interface Options { dev: boolean @@ -106,27 +107,8 @@ export class FlightClientEntryPlugin { } } - compilation.chunkGroups.forEach((chunkGroup) => { - chunkGroup.chunks.forEach((chunk: webpack.Chunk) => { - const chunkModules = compilation.chunkGraph.getChunkModulesIterable( - chunk - ) as Iterable - - for (const mod of chunkModules) { - const modId = compilation.chunkGraph.getModuleId(mod) - - recordModule(modId, mod) - - // If this is a concatenation, register each child to the parent ID. - // TODO: remove any - const anyModule = mod as any - if (anyModule.modules) { - anyModule.modules.forEach((concatenatedMod: any) => { - recordModule(modId, concatenatedMod) - }) - } - } - }) + traverseModules(compilation, (mod, _chunk, _chunkGroup, modId) => { + recordModule(modId, mod) }) }) } diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 39d254dc49b7b..89ec15471bcd2 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -15,6 +15,8 @@ import { serverModuleIds, } from './flight-client-entry-plugin' +import { traverseModules } from '../utils' + // This is the module that will be used to anchor all client references to. // I.e. it will have all the client files as async deps from this point on. // We use the Flight client implementation because you can't get to these @@ -82,32 +84,6 @@ const PLUGIN_NAME = 'FlightManifestPlugin' // So that react could unwrap the async module from promise and render module itself. export const ASYNC_CLIENT_MODULES = new Set() -export function traverseModules( - compilation: webpack.Compilation, - callback: ( - mod: any, - chunk: webpack.Chunk, - chunkGroup: typeof compilation.chunkGroups[0] - ) => any -) { - compilation.chunkGroups.forEach((chunkGroup) => { - chunkGroup.chunks.forEach((chunk: webpack.Chunk) => { - const chunkModules = compilation.chunkGraph.getChunkModulesIterable( - chunk - // TODO: Update type so that it doesn't have to be cast. - ) as Iterable - for (const mod of chunkModules) { - callback(mod, chunk, chunkGroup) - const anyModule = mod as any - if (anyModule.modules) { - for (const subMod of anyModule.modules) - callback(subMod, chunk, chunkGroup) - } - } - }) - }) -} - export class FlightManifestPlugin { dev: Options['dev'] = false diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts new file mode 100644 index 0000000000000..bf73137589ff3 --- /dev/null +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -0,0 +1,136 @@ +import path from 'path' + +import { webpack, sources } from 'next/dist/compiled/webpack/webpack' +import { WEBPACK_LAYERS } from '../../../lib/constants' + +const PLUGIN_NAME = 'FlightTypesPlugin' + +interface Options { + dir: string + appDir: string + dev: boolean + isEdgeServer: boolean +} + +function createTypeGuardFile( + fullPath: string, + relativePath: string, + options: { + type: 'layout' | 'page' + } +) { + return `// File: ${fullPath} +import * as entry from '${relativePath}' +type TEntry = typeof entry + +check(entry) + +interface IEntry { + ${ + options.type === 'layout' + ? `default: (props: { children: React.ReactNode; params?: any }) => React.ReactNode | null` + : `default: (props: { params?: any }) => React.ReactNode | null` + } + generateStaticParams?: (params?:any) => Promise + config?: { + // TODO: remove revalidate here + revalidate?: number | boolean + ${options.type === 'page' ? 'runtime?: string' : ''} + } + revalidate?: RevalidateRange | false + dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static' + dynamicParams?: boolean + fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache' + preferredRegion?: 'auto' | 'home' | 'edge' +} + +// ============= +// Utility types +type RevalidateRange = T extends { revalidate: any } ? NonNegative : never +type Impossible = { [P in K]: never } +function check(_mod: T & Impossible>): void {} + +// https://github.com/sindresorhus/type-fest +type Numeric = number | bigint +type Zero = 0 | 0n +type Negative = T extends Zero ? never : \`\${T}\` extends \`-\${string}\` ? T : never +type NonNegative = T extends Zero ? T : Negative extends never ? T : '__invalid_negative_number__' +` +} + +export class FlightTypesPlugin { + dir: string + appDir: string + dev: boolean + isEdgeServer: boolean + + constructor(options: Options) { + this.dir = options.dir + this.appDir = options.appDir + this.dev = options.dev + this.isEdgeServer = options.isEdgeServer + } + + apply(compiler: webpack.Compiler) { + const assetPrefix = this.dev ? '..' : this.isEdgeServer ? '..' : '../..' + + const handleModule = (_mod: webpack.Module, assets: any) => { + if (_mod.layer !== WEBPACK_LAYERS.server) return + const mod: webpack.NormalModule = _mod as any + + if (!mod.resource) return + if (!mod.resource.startsWith(this.appDir + path.sep)) return + if (!/\.(js|jsx|ts|tsx|mjs)$/.test(mod.resource)) return + + const IS_LAYOUT = /[/\\]layout\.[^./\\]+$/.test(mod.resource) + const IS_PAGE = !IS_LAYOUT && /[/\\]page\.[^.]+$/.test(mod.resource) + const relativePathToApp = path.relative(this.appDir, mod.resource) + const relativePathToRoot = path.relative(this.dir, mod.resource) + + const typePath = path.join( + 'types', + 'app', + relativePathToApp.replace(/\.(js|jsx|ts|tsx|mjs)$/, '.ts') + ) + const relativeImportPath = path + .join( + path.relative(typePath, ''), + relativePathToRoot.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') + ) + .replace(/\\/g, '/') + const assetPath = assetPrefix + '/' + typePath.replace(/\\/g, '/') + + if (IS_LAYOUT) { + assets[assetPath] = new sources.RawSource( + createTypeGuardFile(mod.resource, relativeImportPath, { + type: 'layout', + }) + ) as unknown as webpack.sources.RawSource + } else if (IS_PAGE) { + assets[assetPath] = new sources.RawSource( + createTypeGuardFile(mod.resource, relativeImportPath, { + type: 'page', + }) + ) as unknown as webpack.sources.RawSource + } + } + + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, + }, + (assets) => { + for (const entrypoint of compilation.entrypoints.values()) { + for (const chunk of entrypoint.chunks) { + compilation.chunkGraph.getChunkModules(chunk).forEach((mod) => { + handleModule(mod, assets) + }) + } + } + } + ) + }) + } +} diff --git a/packages/next/build/webpack/utils.ts b/packages/next/build/webpack/utils.ts new file mode 100644 index 0000000000000..e84b86ed5dd14 --- /dev/null +++ b/packages/next/build/webpack/utils.ts @@ -0,0 +1,29 @@ +import { webpack } from 'next/dist/compiled/webpack/webpack' + +export function traverseModules( + compilation: webpack.Compilation, + callback: ( + mod: any, + chunk: webpack.Chunk, + chunkGroup: typeof compilation.chunkGroups[0], + modId: string | number + ) => any +) { + compilation.chunkGroups.forEach((chunkGroup) => { + chunkGroup.chunks.forEach((chunk: webpack.Chunk) => { + const chunkModules = compilation.chunkGraph.getChunkModulesIterable( + chunk + // TODO: Update type so that it doesn't have to be cast. + ) as Iterable + for (const mod of chunkModules) { + const modId = compilation.chunkGraph.getModuleId(mod) + callback(mod, chunk, chunkGroup, modId) + const anyModule = mod as any + if (anyModule.modules) { + for (const subMod of anyModule.modules) + callback(subMod, chunk, chunkGroup, modId) + } + } + }) + }) +} diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index 29d8d29d50d62..174965e5b9eee 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -11,14 +11,123 @@ export enum DiagnosticCategory { Message = 3, } +function getFormattedLayoutAndPageDiagnosticMessageText( + baseDir: string, + diagnostic: import('typescript').Diagnostic +) { + const message = diagnostic.messageText + const sourceFilepath = + diagnostic.file?.text.trim().match(/^\/\/ File: (.+)\n/)?.[1] || '' + + if (sourceFilepath && typeof message !== 'string') { + const relativeSourceFile = path.relative(baseDir, sourceFilepath) + const type = /'typeof import\(".+page"\)'/.test(message.messageText) + ? 'Page' + : 'Layout' + + // Reference of error codes: + // https://github.com/Microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json + switch (message.code) { + case 2344: + const filepathAndType = message.messageText.match( + /'typeof import\("(.+)"\)'.+'(.+)'/ + ) + if (filepathAndType) { + let main = `${type} "${chalk.bold( + relativeSourceFile + )}" does not match the required types of a Next.js ${type}.` + + function processNext( + indent: number, + next?: import('typescript').DiagnosticMessageChain[] + ) { + if (!next) return + + for (const item of next) { + switch (item.code) { + case 2200: + const mismatchedField = + item.messageText.match(/The types of '(.+)'/) + if (mismatchedField) { + main += '\n' + ' '.repeat(indent * 2) + main += `"${chalk.bold( + mismatchedField[1] + )}" has the wrong type:` + } + break + case 2322: + const types = item.messageText.match( + /Type '(.+)' is not assignable to type '(.+)'./ + ) + if (types) { + main += '\n' + ' '.repeat(indent * 2) + main += `Expected "${chalk.bold( + types[2].replace( + '"__invalid_negative_number__"', + 'number (>= 0)' + ) + )}", got "${chalk.bold(types[1])}".` + } + break + case 2326: + main += '\n' + ' '.repeat(indent * 2) + main += `Invalid configuration:` + break + case 2559: + const invalid = item.messageText.match(/Type '(.+)' has/) + if (invalid) { + main += '\n' + ' '.repeat(indent * 2) + main += `Type "${chalk.bold(invalid[1])}" isn't allowed.` + } + break + default: + } + + processNext(indent + 1, item.next) + } + } + + processNext(1, message.next) + return main + } + break + case 2345: + const filepathAndInvalidExport = message.messageText.match( + /'typeof import\("(.+)"\)'.+Impossible<"(.+)">/ + ) + if (filepathAndInvalidExport) { + const main = `${type} "${chalk.bold( + relativeSourceFile + )}" exports invalid field "${chalk.bold( + filepathAndInvalidExport[2] + )}". Only "default" and other configuration exports are allowed.` + return main + } + break + default: + } + } +} + export async function getFormattedDiagnostic( ts: typeof import('typescript'), baseDir: string, - diagnostic: import('typescript').Diagnostic + diagnostic: import('typescript').Diagnostic, + isAppDirEnabled?: boolean ): Promise { + // If the error comes from .next/types/, we handle it specially. + const isLayoutOrPageError = + isAppDirEnabled && + diagnostic.file?.fileName.includes(path.join(baseDir, '.next', 'types')) + let message = '' - const reason = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') + const layoutReason = isLayoutOrPageError + ? getFormattedLayoutAndPageDiagnosticMessageText(baseDir, diagnostic) + : null + const reason = + layoutReason || + ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') const category = diagnostic.category switch (category) { // Warning @@ -39,9 +148,10 @@ export async function getFormattedDiagnostic( break } } + message += reason + '\n' - if (diagnostic.file) { + if (!isLayoutOrPageError && diagnostic.file) { const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!) const line = pos.line + 1 const character = pos.character + 1 diff --git a/packages/next/lib/typescript/runTypeCheck.ts b/packages/next/lib/typescript/runTypeCheck.ts index c6165759939de..561ff160f1a0e 100644 --- a/packages/next/lib/typescript/runTypeCheck.ts +++ b/packages/next/lib/typescript/runTypeCheck.ts @@ -21,7 +21,8 @@ export async function runTypeCheck( ts: typeof import('typescript'), baseDir: string, tsConfigPath: string, - cacheDir?: string + cacheDir?: string, + isAppDirEnabled?: boolean ): Promise { const effectiveConfiguration = await getTypeScriptConfiguration( ts, @@ -69,6 +70,7 @@ export async function runTypeCheck( } else { program = ts.createProgram(effectiveConfiguration.fileNames, options) } + const result = program.emit() // Intended to match: @@ -95,14 +97,14 @@ export async function runTypeCheck( if (firstError) { throw new CompileError( - await getFormattedDiagnostic(ts, baseDir, firstError) + await getFormattedDiagnostic(ts, baseDir, firstError, isAppDirEnabled) ) } const warnings = await Promise.all( allDiagnostics .filter((d) => d.category === DiagnosticCategory.Warning) - .map((d) => getFormattedDiagnostic(ts, baseDir, d)) + .map((d) => getFormattedDiagnostic(ts, baseDir, d, isAppDirEnabled)) ) return { hasWarnings: true, diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index bd34c6e76b445..a203b6dd3d1b5 100644 --- a/packages/next/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/lib/typescript/writeConfigurationDefaults.ts @@ -102,7 +102,8 @@ export function getRequiredConfiguration( export async function writeConfigurationDefaults( ts: typeof import('typescript'), tsConfigPath: string, - isFirstTimeSetup: boolean + isFirstTimeSetup: boolean, + isAppDirEnabled: boolean ): Promise { if (isFirstTimeSetup) { await fs.writeFile(tsConfigPath, '{}' + os.EOL) @@ -162,11 +163,27 @@ export async function writeConfigurationDefaults( } if (!('include' in rawConfig)) { - userTsConfig.include = ['next-env.d.ts', '**/*.ts', '**/*.tsx'] + userTsConfig.include = isAppDirEnabled + ? ['next-env.d.ts', '.next/types/**/*.ts', '**/*.ts', '**/*.tsx'] + : ['next-env.d.ts', '**/*.ts', '**/*.tsx'] suggestedActions.push( chalk.cyan('include') + ' was set to ' + - chalk.bold(`['next-env.d.ts', '**/*.ts', '**/*.tsx']`) + chalk.bold( + isAppDirEnabled + ? `['next-env.d.ts', '.next/types/**/*.ts', '**/*.ts', '**/*.tsx']` + : `['next-env.d.ts', '**/*.ts', '**/*.tsx']` + ) + ) + } else if ( + isAppDirEnabled && + !rawConfig.include.includes('.next/types/**/*.ts') + ) { + userTsConfig.include.push('.next/types/**/*.ts') + suggestedActions.push( + chalk.cyan('include') + + ' was updated to add ' + + chalk.bold(`'.next/types/**/*.ts'`) ) } diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index dd632d27b3dbb..38497e872115f 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -42,6 +42,7 @@ export async function verifyTypeScriptSetup({ tsconfigPath, typeCheckPreflight, disableStaticImages, + isAppDirEnabled, }: { dir: string cacheDir?: string @@ -49,6 +50,7 @@ export async function verifyTypeScriptSetup({ intentDirs: string[] typeCheckPreflight: boolean disableStaticImages: boolean + isAppDirEnabled: boolean }): Promise<{ result?: TypeCheckResult; version: string | null }> { const resolvedTsConfigPath = path.join(dir, tsconfigPath) @@ -113,7 +115,8 @@ export async function verifyTypeScriptSetup({ await writeConfigurationDefaults( ts, resolvedTsConfigPath, - intent.firstTimeSetup + intent.firstTimeSetup, + isAppDirEnabled ) // Write out the necessary `next-env.d.ts` file to correctly register // Next.js' types: @@ -124,7 +127,13 @@ export async function verifyTypeScriptSetup({ const { runTypeCheck } = require('./typescript/runTypeCheck') // Verify the project passes type-checking before we go to webpack phase: - result = await runTypeCheck(ts, dir, resolvedTsConfigPath, cacheDir) + result = await runTypeCheck( + ts, + dir, + resolvedTsConfigPath, + cacheDir, + isAppDirEnabled + ) } return { result, version: ts.version } } catch (err) { diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index b35a381deecfa..46f623b8886c7 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -643,6 +643,7 @@ export default class DevServer extends Server { typeCheckPreflight: false, tsconfigPath: this.nextConfig.typescript.tsconfigPath, disableStaticImages: this.nextConfig.images.disableStaticImages, + isAppDirEnabled: !!this.appDir, }) if (verifyResult.version) { diff --git a/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx b/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx index 219e7502744eb..5bbdfe8c63cc9 100644 --- a/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx +++ b/test/e2e/app-dir/app-edge/app/app-edge/layout.tsx @@ -9,7 +9,3 @@ export default function Layout({ children }: { children: React.ReactNode }) { useSelectedLayoutSegment() return children } - -export const config = { - runtime: 'experimental-edge', -} diff --git a/test/e2e/app-dir/app-edge/tsconfig.json b/test/e2e/app-dir/app-edge/tsconfig.json index da141694cc0c8..870352da94f86 100644 --- a/test/e2e/app-dir/app-edge/tsconfig.json +++ b/test/e2e/app-dir/app-edge/tsconfig.json @@ -19,6 +19,6 @@ "@/ui/*": ["ui/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/test/e2e/app-dir/app-typescript/app/inner/page.tsx b/test/e2e/app-dir/app-typescript/app/inner/page.tsx new file mode 100644 index 0000000000000..d6d806e3f5ac4 --- /dev/null +++ b/test/e2e/app-dir/app-typescript/app/inner/page.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return
hello
+} + +export const config = { + runtime: 'nodejs', +} diff --git a/test/e2e/app-dir/app-typescript/app/layout.js b/test/e2e/app-dir/app-typescript/app/layout.js new file mode 100644 index 0000000000000..37eb3db97a06f --- /dev/null +++ b/test/e2e/app-dir/app-typescript/app/layout.js @@ -0,0 +1,16 @@ +export const config = { + revalidate: 0, +} + +// export const revalidate = -1 + +export default function Root({ children }) { + return ( + + + Hello + + {children} + + ) +} diff --git a/test/e2e/app-dir/app-typescript/next.config.js b/test/e2e/app-dir/app-typescript/next.config.js new file mode 100644 index 0000000000000..cfa3ac3d7aa94 --- /dev/null +++ b/test/e2e/app-dir/app-typescript/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + appDir: true, + }, +} diff --git a/test/e2e/app-dir/app-typescript/package.json b/test/e2e/app-dir/app-typescript/package.json new file mode 100644 index 0000000000000..cc77b7b1aa341 --- /dev/null +++ b/test/e2e/app-dir/app-typescript/package.json @@ -0,0 +1,12 @@ +{ + "name": "app-typescript", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/test/e2e/app-dir/app-typescript/tsconfig.json b/test/e2e/app-dir/app-typescript/tsconfig.json new file mode 100644 index 0000000000000..b767889f4be7d --- /dev/null +++ b/test/e2e/app-dir/app-typescript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}