From 0ae364c9796ec0c032aac1d60aa5195ba68760fb Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 9 Oct 2022 01:26:14 +0200 Subject: [PATCH 01/18] type plugin --- packages/next/build/webpack-config.ts | 2 + .../webpack/plugins/flight-types-plugin.ts | 120 ++++++++++++++++++ .../app-dir/app-typescript/app/inner/page.js | 5 + .../e2e/app-dir/app-typescript/app/layout.tsx | 24 ++++ .../e2e/app-dir/app-typescript/next.config.js | 5 + test/e2e/app-dir/app-typescript/package.json | 12 ++ test/e2e/app-dir/app-typescript/tsconfig.json | 20 +++ 7 files changed, 188 insertions(+) create mode 100644 packages/next/build/webpack/plugins/flight-types-plugin.ts create mode 100644 test/e2e/app-dir/app-typescript/app/inner/page.js create mode 100644 test/e2e/app-dir/app-typescript/app/layout.tsx create mode 100644 test/e2e/app-dir/app-typescript/next.config.js create mode 100644 test/e2e/app-dir/app-typescript/package.json create mode 100644 test/e2e/app-dir/app-typescript/tsconfig.json diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 20c4adfaf93ea..63959f66c0f7e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -49,6 +49,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, @@ -1959,6 +1960,7 @@ export default async function getBaseWebpackConfig( isEdgeServer, fontLoaderTargets, })), + hasAppDir && !isClient && new FlightTypesPlugin({ appDir }), !dev && isClient && !!config.experimental.sri?.algorithm && 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..b08a8ce0c2e57 --- /dev/null +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -0,0 +1,120 @@ +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 { + appDir: string +} + +enum FileType { + serverOnly, + clientOnly, + both, +} + +export class FlightTypesPlugin { + appDir: string + + constructor(options: Options) { + this.appDir = options.appDir + } + + apply(compiler: webpack.Compiler) { + const handleModule = (mod: webpack.NormalModule, assets: any) => { + if (mod.layer !== WEBPACK_LAYERS.server) return + 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 relativePath = path.relative(this.appDir, mod.resource) + + // const RSC = mod.buildInfo.rsc + + const typePath = path.join( + 'types', + 'app', + relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '.ts') + ) + const relativeImportPath = path.join( + path.relative(typePath, ''), + 'app', + relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') + ) + + if (IS_LAYOUT) { + assets[path.join('..', typePath)] = new sources.RawSource(` +import * as Self from '${relativeImportPath}' + +type Impossible = { + [P in K]: never; +}; + +function check(_mod: T & Impossible>): void {} + +check(Self) + +interface Layout { + default:({ children }: { children: any; }) => JSX.Element + config?: { + revalidate?: number; + } +}`) as unknown as webpack.sources.RawSource + } else if (IS_PAGE) { + assets[path.join('..', typePath)] = new sources.RawSource(` +import * as Self from '${relativeImportPath}' + +type Impossible = { + [P in K]: never; +}; + +function check(_mod: T & Impossible>): void {} + +check(Self) + +interface Page { + default:() => JSX.Element + config?: { + revalidate?: number; + runtime?: 'nodejs' | 'experimental-edge'; + } +} +`) as unknown as webpack.sources.RawSource + } + } + + compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, + }, + (assets) => { + 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) { + handleModule(mod, assets) + + const anyModule = mod as any + if (anyModule.modules) { + anyModule.modules.forEach((concatenatedMod: any) => { + handleModule(concatenatedMod, assets) + }) + } + } + }) + }) + } + ) + }) + } +} diff --git a/test/e2e/app-dir/app-typescript/app/inner/page.js b/test/e2e/app-dir/app-typescript/app/inner/page.js new file mode 100644 index 0000000000000..a57410941935c --- /dev/null +++ b/test/e2e/app-dir/app-typescript/app/inner/page.js @@ -0,0 +1,5 @@ +export default function Page() { + return

hello

+} + +export const now_used = 1 diff --git a/test/e2e/app-dir/app-typescript/app/layout.tsx b/test/e2e/app-dir/app-typescript/app/layout.tsx new file mode 100644 index 0000000000000..f6dd9238742b4 --- /dev/null +++ b/test/e2e/app-dir/app-typescript/app/layout.tsx @@ -0,0 +1,24 @@ +import React, { experimental_use as use } from 'react' + +export const config = { + revalidate: 1, +} + +async function getData() { + return { + world: 'world', + } +} + +export default function Root({ children }) { + const { world } = use(getData()) + + return ( + + + {`hello ${world}`} + + {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..bfb4844d004d7 --- /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", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 539f57590dc23e1508a7cbe7eeca2ae8ea80031a Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 11 Oct 2022 16:18:53 +0200 Subject: [PATCH 02/18] improve types path --- packages/next/build/webpack-config.ts | 2 +- .../webpack/plugins/flight-types-plugin.ts | 21 ++++++++++++------- .../app-dir/app-typescript/app/inner/page.js | 7 ++++++- .../e2e/app-dir/app-typescript/app/layout.tsx | 4 ++-- test/e2e/app-dir/app-typescript/tsconfig.json | 3 ++- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 63959f66c0f7e..452e2f78977b0 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1960,7 +1960,7 @@ export default async function getBaseWebpackConfig( isEdgeServer, fontLoaderTargets, })), - hasAppDir && !isClient && new FlightTypesPlugin({ appDir }), + hasAppDir && !isClient && new FlightTypesPlugin({ appDir, dev }), !dev && isClient && !!config.experimental.sri?.algorithm && diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index b08a8ce0c2e57..2a68e1c51061f 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -7,19 +7,22 @@ const PLUGIN_NAME = 'FlightTypesPlugin' interface Options { appDir: string + dev: boolean } -enum FileType { - serverOnly, - clientOnly, - both, -} +// enum FileType { +// serverOnly, +// clientOnly, +// both, +// } export class FlightTypesPlugin { appDir: string + dev: boolean constructor(options: Options) { this.appDir = options.appDir + this.dev = options.dev } apply(compiler: webpack.Compiler) { @@ -45,9 +48,13 @@ export class FlightTypesPlugin { 'app', relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') ) + const assetPath = path.join( + this.dev ? '..' : path.join('..', '..'), + typePath + ) if (IS_LAYOUT) { - assets[path.join('..', typePath)] = new sources.RawSource(` + assets[assetPath] = new sources.RawSource(` import * as Self from '${relativeImportPath}' type Impossible = { @@ -65,7 +72,7 @@ interface Layout { } }`) as unknown as webpack.sources.RawSource } else if (IS_PAGE) { - assets[path.join('..', typePath)] = new sources.RawSource(` + assets[assetPath] = new sources.RawSource(` import * as Self from '${relativeImportPath}' type Impossible = { diff --git a/test/e2e/app-dir/app-typescript/app/inner/page.js b/test/e2e/app-dir/app-typescript/app/inner/page.js index a57410941935c..4fefbd35ba565 100644 --- a/test/e2e/app-dir/app-typescript/app/inner/page.js +++ b/test/e2e/app-dir/app-typescript/app/inner/page.js @@ -2,4 +2,9 @@ export default function Page() { return

hello

} -export const now_used = 1 +// export const not_used = 1 + +// export const dynamic = 'auto' +// export const config = { +// dynamic: 'auto', +// } diff --git a/test/e2e/app-dir/app-typescript/app/layout.tsx b/test/e2e/app-dir/app-typescript/app/layout.tsx index f6dd9238742b4..d522615e147d2 100644 --- a/test/e2e/app-dir/app-typescript/app/layout.tsx +++ b/test/e2e/app-dir/app-typescript/app/layout.tsx @@ -1,7 +1,7 @@ -import React, { experimental_use as use } from 'react' +import { experimental_use as use } from 'react' export const config = { - revalidate: 1, + revalidate: 'foo', } async function getData() { diff --git a/test/e2e/app-dir/app-typescript/tsconfig.json b/test/e2e/app-dir/app-typescript/tsconfig.json index bfb4844d004d7..aed0515140644 100644 --- a/test/e2e/app-dir/app-typescript/tsconfig.json +++ b/test/e2e/app-dir/app-typescript/tsconfig.json @@ -16,5 +16,6 @@ "jsx": "preserve" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "plugins": ["next/typescript"] } From b78faa72e0803027f3250083096a288c90cb521e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 12 Oct 2022 21:53:48 +0200 Subject: [PATCH 03/18] change ts check order --- packages/next/build/index.ts | 177 ++++++++++-------- .../webpack/plugins/flight-types-plugin.ts | 6 - .../typescript/writeConfigurationDefaults.ts | 18 +- test/e2e/app-dir/app-typescript/tsconfig.json | 3 +- 4 files changed, 113 insertions(+), 91 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index fa66f489f6cff..b614f7def5cc4 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -326,100 +326,110 @@ 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) + config.experimental.workerThreads + ).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, @@ -1071,6 +1081,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/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 2a68e1c51061f..0594e47298ca1 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -10,12 +10,6 @@ interface Options { dev: boolean } -// enum FileType { -// serverOnly, -// clientOnly, -// both, -// } - export class FlightTypesPlugin { appDir: string dev: boolean diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index bd34c6e76b445..d762be7c4e6be 100644 --- a/packages/next/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/lib/typescript/writeConfigurationDefaults.ts @@ -162,11 +162,25 @@ export async function writeConfigurationDefaults( } if (!('include' in rawConfig)) { - userTsConfig.include = ['next-env.d.ts', '**/*.ts', '**/*.tsx'] + userTsConfig.include = [ + 'next-env.d.ts', + '**/*.ts', + '**/*.tsx', + '.next/types/**/*.ts', + ] suggestedActions.push( chalk.cyan('include') + ' was set to ' + - chalk.bold(`['next-env.d.ts', '**/*.ts', '**/*.tsx']`) + chalk.bold( + `['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts']` + ) + ) + } else if (!rawConfig.include.includes('.next/types/**/*.ts')) { + userTsConfig.include.push('.next/types/**/*.ts') + suggestedActions.push( + chalk.cyan('include') + + ' was updated to have ' + + chalk.bold(`'.next/types/**/*.ts'`) ) } diff --git a/test/e2e/app-dir/app-typescript/tsconfig.json b/test/e2e/app-dir/app-typescript/tsconfig.json index aed0515140644..bfb4844d004d7 100644 --- a/test/e2e/app-dir/app-typescript/tsconfig.json +++ b/test/e2e/app-dir/app-typescript/tsconfig.json @@ -16,6 +16,5 @@ "jsx": "preserve" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"], - "plugins": ["next/typescript"] + "exclude": ["node_modules"] } From 1120c7271051e2365f3202d798a4319aa48f7390 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 14 Oct 2022 00:06:36 +0200 Subject: [PATCH 04/18] typechecker --- packages/next/build/index.ts | 7 +- .../webpack/plugins/flight-types-plugin.ts | 26 +++- .../lib/typescript/diagnosticFormatter.ts | 121 +++++++++++++++++- packages/next/lib/typescript/runTypeCheck.ts | 22 +++- packages/next/lib/verifyTypeScriptSetup.ts | 10 +- .../app-dir/app-typescript/app/inner/page.js | 7 +- .../e2e/app-dir/app-typescript/app/layout.tsx | 14 +- 7 files changed, 176 insertions(+), 31 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index b614f7def5cc4..845f7735aacc2 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -172,7 +172,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'), @@ -196,6 +197,7 @@ function verifyTypeScriptSetup( tsconfigPath, disableStaticImages, cacheDir, + isAppDirEnabled, }) .then((result) => { typeCheckWorker.end() @@ -381,7 +383,8 @@ export default async function build( config.images.disableStaticImages, cacheDir, config.experimental.cpus, - config.experimental.workerThreads + config.experimental.workerThreads, + isAppDirEnabled ).then((resolved) => { const checkEnd = process.hrtime(typeCheckStart) return [resolved, checkEnd] as const diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 0594e47298ca1..5878663e7cdac 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -48,7 +48,8 @@ export class FlightTypesPlugin { ) if (IS_LAYOUT) { - assets[assetPath] = new sources.RawSource(` + assets[assetPath] = new sources.RawSource( + `// File: ${mod.resource} import * as Self from '${relativeImportPath}' type Impossible = { @@ -62,11 +63,17 @@ check(Self) interface Layout { default:({ children }: { children: any; }) => JSX.Element config?: { - revalidate?: number; + revalidate?: number | boolean; + dynamic?: string; + dynamicParams?: boolean; + fetchCache?: string; + preferredRegion?: string; } -}`) as unknown as webpack.sources.RawSource +}` + ) as unknown as webpack.sources.RawSource } else if (IS_PAGE) { - assets[assetPath] = new sources.RawSource(` + assets[assetPath] = new sources.RawSource( + `// File: ${mod.resource} import * as Self from '${relativeImportPath}' type Impossible = { @@ -80,11 +87,16 @@ check(Self) interface Page { default:() => JSX.Element config?: { - revalidate?: number; - runtime?: 'nodejs' | 'experimental-edge'; + revalidate?: number | boolean; + dynamic?: string; + dynamicParams?: boolean; + fetchCache?: string; + preferredRegion?: string; + runtime?: string; } } -`) as unknown as webpack.sources.RawSource +` + ) as unknown as webpack.sources.RawSource } } diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index 29d8d29d50d62..a557bcc8a9555 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -11,14 +11,128 @@ export enum DiagnosticCategory { Message = 3, } +function getFormattedLayoutAndPageDiagnosticMessageText( + baseDir: string, + diagnostic: import('typescript').Diagnostic, + program: import('typescript').Program +) { + const ts = require('typescript') as typeof import('typescript') + + const message = diagnostic.messageText + const sourceFilepath = + diagnostic.file?.text.trim().match(/^\/\/ File: (.+)\n/)?.[1] || '' + + if (sourceFilepath && typeof message !== 'string') { + const sourceFile = program.getSourceFile(sourceFilepath) + + 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] + )}", 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 + } + + 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 + } + } +} + export async function getFormattedDiagnostic( ts: typeof import('typescript'), baseDir: string, - diagnostic: import('typescript').Diagnostic + diagnostic: import('typescript').Diagnostic, + program: import('typescript').Program, + 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, + program + ) + : null + const reason = + layoutReason || + ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') const category = diagnostic.category switch (category) { // Warning @@ -39,9 +153,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..8c95d9f7df849 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,28 @@ export async function runTypeCheck( if (firstError) { throw new CompileError( - await getFormattedDiagnostic(ts, baseDir, firstError) + await getFormattedDiagnostic( + ts, + baseDir, + firstError, + program as import('typescript').Program, + 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, + program as import('typescript').Program, + isAppDirEnabled + ) + ) ) return { hasWarnings: true, diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index dd632d27b3dbb..92d67263a07d5 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) @@ -124,7 +126,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/test/e2e/app-dir/app-typescript/app/inner/page.js b/test/e2e/app-dir/app-typescript/app/inner/page.js index 4fefbd35ba565..c2c7ee40b60f8 100644 --- a/test/e2e/app-dir/app-typescript/app/inner/page.js +++ b/test/e2e/app-dir/app-typescript/app/inner/page.js @@ -5,6 +5,7 @@ export default function Page() { // export const not_used = 1 // export const dynamic = 'auto' -// export const config = { -// dynamic: 'auto', -// } + +export const config = { + runtime: 'nodejs', +} diff --git a/test/e2e/app-dir/app-typescript/app/layout.tsx b/test/e2e/app-dir/app-typescript/app/layout.tsx index d522615e147d2..d0dfc7afc5b98 100644 --- a/test/e2e/app-dir/app-typescript/app/layout.tsx +++ b/test/e2e/app-dir/app-typescript/app/layout.tsx @@ -1,22 +1,12 @@ -import { experimental_use as use } from 'react' - export const config = { - revalidate: 'foo', -} - -async function getData() { - return { - world: 'world', - } + foo: 1, } export default function Root({ children }) { - const { world } = use(getData()) - return ( - {`hello ${world}`} + Hello {children} From f557c6d19aa053a3c6112b962e5a18c5bb8ffaeb Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Fri, 14 Oct 2022 19:50:50 +0200 Subject: [PATCH 05/18] update test app --- test/e2e/app-dir/app-typescript/app/inner/page.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/e2e/app-dir/app-typescript/app/inner/page.tsx 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..2667962041372 --- /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', +} From 763f60cc6dd917f3985f9ba77031fcea40ee5c2b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 15 Oct 2022 00:33:22 +0200 Subject: [PATCH 06/18] reorganize --- .../webpack/plugins/flight-types-plugin.ts | 90 ++++++++++--------- .../lib/typescript/diagnosticFormatter.ts | 22 ++--- packages/next/lib/typescript/runTypeCheck.ts | 18 +--- .../app/{layout.tsx => layout.js} | 6 +- 4 files changed, 59 insertions(+), 77 deletions(-) rename test/e2e/app-dir/app-typescript/app/{layout.tsx => layout.js} (81%) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 5878663e7cdac..1acb0dc2afafb 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -10,6 +10,46 @@ interface Options { dev: 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 { + default: ({ children }: { children: any }) => JSX.Element + config?: { + revalidate?: number | boolean + dynamic?: string + dynamicParams?: boolean + fetchCache?: string + preferredRegion?: string + ${options.type === 'page' ? 'runtime?: string' : ''} + } + revalidate?: RevalidateRange | false +} + +// ============= +// 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 { appDir: string dev: boolean @@ -49,53 +89,15 @@ export class FlightTypesPlugin { if (IS_LAYOUT) { assets[assetPath] = new sources.RawSource( - `// File: ${mod.resource} -import * as Self from '${relativeImportPath}' - -type Impossible = { - [P in K]: never; -}; - -function check(_mod: T & Impossible>): void {} - -check(Self) - -interface Layout { - default:({ children }: { children: any; }) => JSX.Element - config?: { - revalidate?: number | boolean; - dynamic?: string; - dynamicParams?: boolean; - fetchCache?: string; - preferredRegion?: string; - } -}` + createTypeGuardFile(mod.resource, relativeImportPath, { + type: 'layout', + }) ) as unknown as webpack.sources.RawSource } else if (IS_PAGE) { assets[assetPath] = new sources.RawSource( - `// File: ${mod.resource} -import * as Self from '${relativeImportPath}' - -type Impossible = { - [P in K]: never; -}; - -function check(_mod: T & Impossible>): void {} - -check(Self) - -interface Page { - default:() => JSX.Element - config?: { - revalidate?: number | boolean; - dynamic?: string; - dynamicParams?: boolean; - fetchCache?: string; - preferredRegion?: string; - runtime?: string; - } -} -` + createTypeGuardFile(mod.resource, relativeImportPath, { + type: 'page', + }) ) as unknown as webpack.sources.RawSource } } diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index a557bcc8a9555..059c0785061ec 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -13,18 +13,13 @@ export enum DiagnosticCategory { function getFormattedLayoutAndPageDiagnosticMessageText( baseDir: string, - diagnostic: import('typescript').Diagnostic, - program: import('typescript').Program + diagnostic: import('typescript').Diagnostic ) { - const ts = require('typescript') as typeof import('typescript') - const message = diagnostic.messageText const sourceFilepath = diagnostic.file?.text.trim().match(/^\/\/ File: (.+)\n/)?.[1] || '' if (sourceFilepath && typeof message !== 'string') { - const sourceFile = program.getSourceFile(sourceFilepath) - const relativeSourceFile = path.relative(baseDir, sourceFilepath) const type = /'typeof import\(".+page"\)'/.test(message.messageText) ? 'Page' @@ -44,7 +39,8 @@ function getFormattedLayoutAndPageDiagnosticMessageText( function processNext( indent: number, - next?: import('typescript').DiagnosticMessageChain[] + next?: import('typescript').DiagnosticMessageChain[], + parent?: null ) { if (!next) return @@ -67,7 +63,10 @@ function getFormattedLayoutAndPageDiagnosticMessageText( if (types) { main += '\n' + ' '.repeat(indent * 2) main += `Expected "${chalk.bold( - types[2] + types[2].replace( + '"__invalid_negative_number__"', + 'number (>= 0)' + ) )}", got "${chalk.bold(types[1])}".` } break @@ -113,7 +112,6 @@ export async function getFormattedDiagnostic( ts: typeof import('typescript'), baseDir: string, diagnostic: import('typescript').Diagnostic, - program: import('typescript').Program, isAppDirEnabled?: boolean ): Promise { // If the error comes from .next/types/, we handle it specially. @@ -124,11 +122,7 @@ export async function getFormattedDiagnostic( let message = '' const layoutReason = isLayoutOrPageError - ? getFormattedLayoutAndPageDiagnosticMessageText( - baseDir, - diagnostic, - program - ) + ? getFormattedLayoutAndPageDiagnosticMessageText(baseDir, diagnostic) : null const reason = layoutReason || diff --git a/packages/next/lib/typescript/runTypeCheck.ts b/packages/next/lib/typescript/runTypeCheck.ts index 8c95d9f7df849..561ff160f1a0e 100644 --- a/packages/next/lib/typescript/runTypeCheck.ts +++ b/packages/next/lib/typescript/runTypeCheck.ts @@ -97,28 +97,14 @@ export async function runTypeCheck( if (firstError) { throw new CompileError( - await getFormattedDiagnostic( - ts, - baseDir, - firstError, - program as import('typescript').Program, - isAppDirEnabled - ) + await getFormattedDiagnostic(ts, baseDir, firstError, isAppDirEnabled) ) } const warnings = await Promise.all( allDiagnostics .filter((d) => d.category === DiagnosticCategory.Warning) - .map((d) => - getFormattedDiagnostic( - ts, - baseDir, - d, - program as import('typescript').Program, - isAppDirEnabled - ) - ) + .map((d) => getFormattedDiagnostic(ts, baseDir, d, isAppDirEnabled)) ) return { hasWarnings: true, diff --git a/test/e2e/app-dir/app-typescript/app/layout.tsx b/test/e2e/app-dir/app-typescript/app/layout.js similarity index 81% rename from test/e2e/app-dir/app-typescript/app/layout.tsx rename to test/e2e/app-dir/app-typescript/app/layout.js index d0dfc7afc5b98..aa762ab2cb1be 100644 --- a/test/e2e/app-dir/app-typescript/app/layout.tsx +++ b/test/e2e/app-dir/app-typescript/app/layout.js @@ -1,6 +1,6 @@ -export const config = { - foo: 1, -} +export const config = {} + +export const revalidate = -1 export default function Root({ children }) { return ( From a204e7d11b82fd395660b4d09a55848c6a8a60f9 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 15 Oct 2022 00:51:15 +0200 Subject: [PATCH 07/18] check more stuff --- .../build/webpack/plugins/flight-types-plugin.ts | 12 +++++++----- test/e2e/app-dir/app-typescript/app/inner/page.tsx | 2 +- test/e2e/app-dir/app-typescript/app/layout.js | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 1acb0dc2afafb..1726421994786 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -24,16 +24,18 @@ type TEntry = typeof entry check(entry) interface IEntry { - default: ({ children }: { children: any }) => JSX.Element + default: (props: { children: any; params?: any }) => React.ReactElement | null + generateStaticParams?: (params?:any) => Promise config?: { + // TODO: remove revalidate here revalidate?: number | boolean - dynamic?: string - dynamicParams?: boolean - fetchCache?: string - preferredRegion?: string ${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' } // ============= diff --git a/test/e2e/app-dir/app-typescript/app/inner/page.tsx b/test/e2e/app-dir/app-typescript/app/inner/page.tsx index 2667962041372..d6d806e3f5ac4 100644 --- a/test/e2e/app-dir/app-typescript/app/inner/page.tsx +++ b/test/e2e/app-dir/app-typescript/app/inner/page.tsx @@ -1,5 +1,5 @@ export default function Page() { - return

hello

+ return
hello
} export const config = { diff --git a/test/e2e/app-dir/app-typescript/app/layout.js b/test/e2e/app-dir/app-typescript/app/layout.js index aa762ab2cb1be..37eb3db97a06f 100644 --- a/test/e2e/app-dir/app-typescript/app/layout.js +++ b/test/e2e/app-dir/app-typescript/app/layout.js @@ -1,6 +1,8 @@ -export const config = {} +export const config = { + revalidate: 0, +} -export const revalidate = -1 +// export const revalidate = -1 export default function Root({ children }) { return ( From 07e36595f0754d01aba5e3cc855f9f3e8d673495 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 15 Oct 2022 01:02:09 +0200 Subject: [PATCH 08/18] fix type error --- packages/next/server/dev/next-dev-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index cc34cfb70d123..268bdaa5110f5 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -641,6 +641,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) { From d14cae7b43711f459dc9016026270c026213db66 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 16:20:09 +0200 Subject: [PATCH 09/18] only extend tsconfig if app dir is enabled --- .../typescript/writeConfigurationDefaults.ts | 23 +++++++++++-------- packages/next/lib/verifyTypeScriptSetup.ts | 3 ++- test/e2e/app-dir/app-typescript/tsconfig.json | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index d762be7c4e6be..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,24 +163,26 @@ export async function writeConfigurationDefaults( } if (!('include' in rawConfig)) { - userTsConfig.include = [ - 'next-env.d.ts', - '**/*.ts', - '**/*.tsx', - '.next/types/**/*.ts', - ] + 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', '.next/types/**/*.ts']` + isAppDirEnabled + ? `['next-env.d.ts', '.next/types/**/*.ts', '**/*.ts', '**/*.tsx']` + : `['next-env.d.ts', '**/*.ts', '**/*.tsx']` ) ) - } else if (!rawConfig.include.includes('.next/types/**/*.ts')) { + } else if ( + isAppDirEnabled && + !rawConfig.include.includes('.next/types/**/*.ts') + ) { userTsConfig.include.push('.next/types/**/*.ts') suggestedActions.push( chalk.cyan('include') + - ' was updated to have ' + + ' was updated to add ' + chalk.bold(`'.next/types/**/*.ts'`) ) } diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index 92d67263a07d5..38497e872115f 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -115,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: diff --git a/test/e2e/app-dir/app-typescript/tsconfig.json b/test/e2e/app-dir/app-typescript/tsconfig.json index bfb4844d004d7..b767889f4be7d 100644 --- a/test/e2e/app-dir/app-typescript/tsconfig.json +++ b/test/e2e/app-dir/app-typescript/tsconfig.json @@ -15,6 +15,6 @@ "isolatedModules": true, "jsx": "preserve" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } From dc01828a681e888ce7af1ece9e29143026f35345 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 16:35:16 +0200 Subject: [PATCH 10/18] resolve linter errors --- packages/next/build/webpack/plugins/flight-types-plugin.ts | 4 ++-- packages/next/lib/typescript/diagnosticFormatter.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 1726421994786..7c37dd6ed59e9 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -68,8 +68,8 @@ export class FlightTypesPlugin { 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 IS_LAYOUT = /[/\\]layout\.[^.]+$/.test(mod.resource) + const IS_PAGE = !IS_LAYOUT && /[/\\]page\.[^.]+$/.test(mod.resource) const relativePath = path.relative(this.appDir, mod.resource) // const RSC = mod.buildInfo.rsc diff --git a/packages/next/lib/typescript/diagnosticFormatter.ts b/packages/next/lib/typescript/diagnosticFormatter.ts index 059c0785061ec..174965e5b9eee 100644 --- a/packages/next/lib/typescript/diagnosticFormatter.ts +++ b/packages/next/lib/typescript/diagnosticFormatter.ts @@ -39,8 +39,7 @@ function getFormattedLayoutAndPageDiagnosticMessageText( function processNext( indent: number, - next?: import('typescript').DiagnosticMessageChain[], - parent?: null + next?: import('typescript').DiagnosticMessageChain[] ) { if (!next) return @@ -81,6 +80,7 @@ function getFormattedLayoutAndPageDiagnosticMessageText( main += `Type "${chalk.bold(invalid[1])}" isn't allowed.` } break + default: } processNext(indent + 1, item.next) @@ -104,6 +104,7 @@ function getFormattedLayoutAndPageDiagnosticMessageText( return main } break + default: } } } From 09dc218cd4a98f7682d43e1eeb5b042fe14dd5dc Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 16:47:01 +0200 Subject: [PATCH 11/18] fix relative paths --- packages/next/build/webpack-config.ts | 4 +++- .../webpack/plugins/flight-types-plugin.ts | 20 ++++++++++++++----- .../app-dir/app-edge/app/app-edge/layout.tsx | 4 ---- test/e2e/app-dir/app-edge/tsconfig.json | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c6cd54114cd9c..339dbf5d7173f 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1946,7 +1946,9 @@ export default async function getBaseWebpackConfig( dev, isEdgeServer, })), - hasAppDir && !isClient && new FlightTypesPlugin({ appDir, dev }), + hasAppDir && + !isClient && + new FlightTypesPlugin({ appDir, dev, isEdgeServer }), !dev && isClient && !!config.experimental.sri?.algorithm && diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 7c37dd6ed59e9..4b62d8fc4a4ad 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -8,6 +8,7 @@ const PLUGIN_NAME = 'FlightTypesPlugin' interface Options { appDir: string dev: boolean + isEdgeServer: boolean } function createTypeGuardFile( @@ -24,7 +25,11 @@ type TEntry = typeof entry check(entry) interface IEntry { - default: (props: { children: any; params?: any }) => React.ReactElement | null + ${ + 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 @@ -55,13 +60,21 @@ type NonNegative = T extends Zero ? T : Negative extends n export class FlightTypesPlugin { appDir: string dev: boolean + isEdgeServer: boolean constructor(options: Options) { this.appDir = options.appDir this.dev = options.dev + this.isEdgeServer = options.isEdgeServer } apply(compiler: webpack.Compiler) { + const assetPrefix = this.dev + ? '..' + : this.isEdgeServer + ? '..' + : path.join('..', '..') + const handleModule = (mod: webpack.NormalModule, assets: any) => { if (mod.layer !== WEBPACK_LAYERS.server) return if (!mod.resource) return @@ -84,10 +97,7 @@ export class FlightTypesPlugin { 'app', relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') ) - const assetPath = path.join( - this.dev ? '..' : path.join('..', '..'), - typePath - ) + const assetPath = path.join(assetPrefix, typePath) if (IS_LAYOUT) { assets[assetPath] = new sources.RawSource( 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"] } From 586ba8b3e3cfdcefe5e4b8ae34e839ee4b4ea1ec Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 16:56:23 +0200 Subject: [PATCH 12/18] disable the plugin at dev --- packages/next/build/webpack-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 339dbf5d7173f..4952a5913586d 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1948,6 +1948,7 @@ export default async function getBaseWebpackConfig( })), hasAppDir && !isClient && + !dev && new FlightTypesPlugin({ appDir, dev, isEdgeServer }), !dev && isClient && From 200f17dee8631e344a12e6a454ab6666dd186d9b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 17:06:29 +0200 Subject: [PATCH 13/18] reorganize code --- .../plugins/flight-client-entry-plugin.ts | 26 +++-------------- .../webpack/plugins/flight-manifest-plugin.ts | 28 ++---------------- .../webpack/plugins/flight-types-plugin.ts | 22 +++----------- packages/next/build/webpack/utils.ts | 29 +++++++++++++++++++ 4 files changed, 39 insertions(+), 66 deletions(-) create mode 100644 packages/next/build/webpack/utils.ts 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 index 4b62d8fc4a4ad..05ecab44010d9 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -3,6 +3,8 @@ import path from 'path' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_LAYERS } from '../../../lib/constants' +import { traverseModules } from '../utils' + const PLUGIN_NAME = 'FlightTypesPlugin' interface Options { @@ -121,24 +123,8 @@ export class FlightTypesPlugin { stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, }, (assets) => { - 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) { - handleModule(mod, assets) - - const anyModule = mod as any - if (anyModule.modules) { - anyModule.modules.forEach((concatenatedMod: any) => { - handleModule(concatenatedMod, assets) - }) - } - } - }) + traverseModules(compilation, (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) + } + } + }) + }) +} From 8e175bc1445f28683883e6c5c24f559dc98102b9 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 17:20:26 +0200 Subject: [PATCH 14/18] change to use .entries --- .../build/webpack/plugins/flight-types-plugin.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 05ecab44010d9..7da76c2b29c82 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -3,8 +3,6 @@ import path from 'path' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_LAYERS } from '../../../lib/constants' -import { traverseModules } from '../utils' - const PLUGIN_NAME = 'FlightTypesPlugin' interface Options { @@ -77,8 +75,10 @@ export class FlightTypesPlugin { ? '..' : path.join('..', '..') - const handleModule = (mod: webpack.NormalModule, assets: any) => { - if (mod.layer !== WEBPACK_LAYERS.server) return + 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 @@ -123,9 +123,9 @@ export class FlightTypesPlugin { stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, }, (assets) => { - traverseModules(compilation, (mod) => { + for (const mod of compilation.modules) { handleModule(mod, assets) - }) + } } ) }) From b0dfa25d6f185099640d741c82a23cf8c40d90d4 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 22:15:11 +0200 Subject: [PATCH 15/18] Apply suggestions from code review Co-authored-by: Tobias Koppers --- packages/next/build/webpack/plugins/flight-types-plugin.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 7da76c2b29c82..05ee2d4baa45f 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -83,11 +83,10 @@ export class FlightTypesPlugin { 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_LAYOUT = /[/\\]layout\.[^./\\]+$/.test(mod.resource) const IS_PAGE = !IS_LAYOUT && /[/\\]page\.[^.]+$/.test(mod.resource) const relativePath = path.relative(this.appDir, mod.resource) - // const RSC = mod.buildInfo.rsc const typePath = path.join( 'types', @@ -116,7 +115,7 @@ export class FlightTypesPlugin { } } - compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { compilation.hooks.processAssets.tap( { name: PLUGIN_NAME, From b869ce1345ff400203c8a77c08ea65bd4a61a9b3 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sun, 16 Oct 2022 23:32:21 +0200 Subject: [PATCH 16/18] fix --- .../webpack/plugins/flight-types-plugin.ts | 28 ++++++++++--------- .../app-dir/app-typescript/app/inner/page.js | 11 -------- 2 files changed, 15 insertions(+), 24 deletions(-) delete mode 100644 test/e2e/app-dir/app-typescript/app/inner/page.js diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 05ee2d4baa45f..d08583a51244b 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -69,11 +69,7 @@ export class FlightTypesPlugin { } apply(compiler: webpack.Compiler) { - const assetPrefix = this.dev - ? '..' - : this.isEdgeServer - ? '..' - : path.join('..', '..') + const assetPrefix = this.dev ? '..' : this.isEdgeServer ? '..' : '../..' const handleModule = (_mod: webpack.Module, assets: any) => { if (_mod.layer !== WEBPACK_LAYERS.server) return @@ -93,12 +89,14 @@ export class FlightTypesPlugin { 'app', relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '.ts') ) - const relativeImportPath = path.join( - path.relative(typePath, ''), - 'app', - relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') - ) - const assetPath = path.join(assetPrefix, typePath) + const relativeImportPath = path + .join( + path.relative(typePath, ''), + 'app', + relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') + ) + .replace(/\\/g, '/') + const assetPath = assetPrefix + '/' + typePath.replace(/\\/g, '/') if (IS_LAYOUT) { assets[assetPath] = new sources.RawSource( @@ -122,8 +120,12 @@ export class FlightTypesPlugin { stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, }, (assets) => { - for (const mod of compilation.modules) { - handleModule(mod, 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/test/e2e/app-dir/app-typescript/app/inner/page.js b/test/e2e/app-dir/app-typescript/app/inner/page.js deleted file mode 100644 index c2c7ee40b60f8..0000000000000 --- a/test/e2e/app-dir/app-typescript/app/inner/page.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function Page() { - return

hello

-} - -// export const not_used = 1 - -// export const dynamic = 'auto' - -export const config = { - runtime: 'nodejs', -} From 207fd917997f5c503ea808bb07ca5937f1d225ed Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 18 Oct 2022 15:25:15 +0200 Subject: [PATCH 17/18] fix lint error --- packages/next/build/webpack/plugins/flight-types-plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index d08583a51244b..0243f90cabcde 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -83,7 +83,6 @@ export class FlightTypesPlugin { const IS_PAGE = !IS_LAYOUT && /[/\\]page\.[^.]+$/.test(mod.resource) const relativePath = path.relative(this.appDir, mod.resource) - const typePath = path.join( 'types', 'app', From 0eba38e6170ceb734b9d517c460def0b12475d68 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 19 Oct 2022 15:33:55 +0200 Subject: [PATCH 18/18] support src folder --- packages/next/build/webpack-config.ts | 2 +- .../next/build/webpack/plugins/flight-types-plugin.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 80aa7c5908014..2b3acdc221a0e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1939,7 +1939,7 @@ export default async function getBaseWebpackConfig( hasAppDir && !isClient && !dev && - new FlightTypesPlugin({ appDir, dev, isEdgeServer }), + new FlightTypesPlugin({ dir, appDir, dev, isEdgeServer }), !dev && isClient && !!config.experimental.sri?.algorithm && diff --git a/packages/next/build/webpack/plugins/flight-types-plugin.ts b/packages/next/build/webpack/plugins/flight-types-plugin.ts index 0243f90cabcde..bf73137589ff3 100644 --- a/packages/next/build/webpack/plugins/flight-types-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-types-plugin.ts @@ -6,6 +6,7 @@ import { WEBPACK_LAYERS } from '../../../lib/constants' const PLUGIN_NAME = 'FlightTypesPlugin' interface Options { + dir: string appDir: string dev: boolean isEdgeServer: boolean @@ -58,11 +59,13 @@ type NonNegative = T extends Zero ? T : Negative extends n } 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 @@ -81,18 +84,18 @@ export class FlightTypesPlugin { const IS_LAYOUT = /[/\\]layout\.[^./\\]+$/.test(mod.resource) const IS_PAGE = !IS_LAYOUT && /[/\\]page\.[^.]+$/.test(mod.resource) - const relativePath = path.relative(this.appDir, mod.resource) + const relativePathToApp = path.relative(this.appDir, mod.resource) + const relativePathToRoot = path.relative(this.dir, mod.resource) const typePath = path.join( 'types', 'app', - relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '.ts') + relativePathToApp.replace(/\.(js|jsx|ts|tsx|mjs)$/, '.ts') ) const relativeImportPath = path .join( path.relative(typePath, ''), - 'app', - relativePath.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') + relativePathToRoot.replace(/\.(js|jsx|ts|tsx|mjs)$/, '') ) .replace(/\\/g, '/') const assetPath = assetPrefix + '/' + typePath.replace(/\\/g, '/')