Add handling for auto installing TypeScript deps and HMRing tsconfig (#39838)
This adds handling for auto-detecting TypeScript being added to a project and installing the necessary dependencies instead of printing the command and requiring the user run the command. We have been testing the auto install handling for a while now with the `next lint` command and it has worked out pretty well. This also adds HMR handling for `jsconfig.json`/`tsconfig.json` in development so if the `baseURL` or `paths` configs are modified it doesn't require a dev server restart for the updates to be picked up. This also corrects our required dependencies detection as previously an incorrect `paths: []` value was being passed to `require.resolve` causing it to fail in specific situations. Closes: https://github.com/vercel/next.js/issues/36201 ### `next build` before https://user-images.githubusercontent.com/22380829/186039578-75f8c128-a13d-4e07-b5da-13bf186ee011.mp4 ### `next build` after https://user-images.githubusercontent.com/22380829/186039662-57af22a4-da5c-4ede-94ea-96541a032cca.mp4 ### `next dev` automatic setup and HMR handling https://user-images.githubusercontent.com/22380829/186039678-d78469ef-d00b-4ee6-8163-a4706394a7b4.mp4 ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md`
This commit is contained in:
parent
8dfab19d6e
commit
ec25b4742b
34 changed files with 885 additions and 196 deletions
|
@ -95,7 +95,14 @@ export function install(
|
|||
*/
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' },
|
||||
env: {
|
||||
...process.env,
|
||||
ADBLOCK: '1',
|
||||
// we set NODE_ENV to development as pnpm skips dev
|
||||
// dependencies when production
|
||||
NODE_ENV: 'development',
|
||||
DISABLE_OPENCOLLECTIVE: '1',
|
||||
},
|
||||
})
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
|
|
|
@ -187,14 +187,14 @@ function verifyTypeScriptSetup(
|
|||
typeCheckWorker.getStderr().pipe(process.stderr)
|
||||
|
||||
return typeCheckWorker
|
||||
.verifyTypeScriptSetup(
|
||||
.verifyTypeScriptSetup({
|
||||
dir,
|
||||
intentDirs,
|
||||
typeCheckPreflight,
|
||||
tsconfigPath,
|
||||
disableStaticImages,
|
||||
cacheDir
|
||||
)
|
||||
cacheDir,
|
||||
})
|
||||
.then((result) => {
|
||||
typeCheckWorker.end()
|
||||
return result
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as Log from './output/log'
|
|||
import { getTypeScriptConfiguration } from '../lib/typescript/getTypeScriptConfiguration'
|
||||
import { readFileSync } from 'fs'
|
||||
import isError from '../lib/is-error'
|
||||
import { hasNecessaryDependencies } from '../lib/has-necessary-dependencies'
|
||||
|
||||
let TSCONFIG_WARNED = false
|
||||
|
||||
|
@ -42,7 +43,14 @@ export default async function loadJsConfig(
|
|||
) {
|
||||
let typeScriptPath: string | undefined
|
||||
try {
|
||||
typeScriptPath = require.resolve('typescript', { paths: [dir] })
|
||||
const deps = await hasNecessaryDependencies(dir, [
|
||||
{
|
||||
pkg: 'typescript',
|
||||
file: 'typescript/lib/typescript.js',
|
||||
exportsRestrict: true,
|
||||
},
|
||||
])
|
||||
typeScriptPath = deps.resolved.get('typescript')
|
||||
} catch (_) {}
|
||||
const tsConfigPath = path.join(dir, config.typescript.tsconfigPath)
|
||||
const useTypeScript = Boolean(
|
||||
|
|
|
@ -531,10 +531,7 @@ export default async function getBaseWebpackConfig(
|
|||
const isClient = compilerType === COMPILER_NAMES.client
|
||||
const isEdgeServer = compilerType === COMPILER_NAMES.edgeServer
|
||||
const isNodeServer = compilerType === COMPILER_NAMES.server
|
||||
const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig(
|
||||
dir,
|
||||
config
|
||||
)
|
||||
const { jsConfig, resolvedBaseUrl } = await loadJsConfig(dir, config)
|
||||
|
||||
const supportedBrowsers = await getSupportedBrowsers(dir, dev, config)
|
||||
|
||||
|
@ -832,22 +829,8 @@ export default async function getBaseWebpackConfig(
|
|||
const resolveConfig = {
|
||||
// Disable .mjs for node_modules bundling
|
||||
extensions: isNodeServer
|
||||
? [
|
||||
'.js',
|
||||
'.mjs',
|
||||
...(useTypeScript ? ['.tsx', '.ts'] : []),
|
||||
'.jsx',
|
||||
'.json',
|
||||
'.wasm',
|
||||
]
|
||||
: [
|
||||
'.mjs',
|
||||
'.js',
|
||||
...(useTypeScript ? ['.tsx', '.ts'] : []),
|
||||
'.jsx',
|
||||
'.json',
|
||||
'.wasm',
|
||||
],
|
||||
? ['.js', '.mjs', '.tsx', '.ts', '.jsx', '.json', '.wasm']
|
||||
: ['.mjs', '.js', '.tsx', '.ts', '.jsx', '.json', '.wasm'],
|
||||
modules: [
|
||||
'node_modules',
|
||||
...nodePathList, // Support for NODE_PATH environment variable
|
||||
|
@ -1831,11 +1814,14 @@ export default async function getBaseWebpackConfig(
|
|||
webpackConfig.resolve?.modules?.push(resolvedBaseUrl)
|
||||
}
|
||||
|
||||
if (jsConfig?.compilerOptions?.paths && resolvedBaseUrl) {
|
||||
webpackConfig.resolve?.plugins?.unshift(
|
||||
new JsConfigPathsPlugin(jsConfig.compilerOptions.paths, resolvedBaseUrl)
|
||||
// allows add JsConfigPathsPlugin to allow hot-reloading
|
||||
// if the config is added/removed
|
||||
webpackConfig.resolve?.plugins?.unshift(
|
||||
new JsConfigPathsPlugin(
|
||||
jsConfig?.compilerOptions?.paths || {},
|
||||
resolvedBaseUrl || dir
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const webpack5Config = webpackConfig as webpack.Configuration
|
||||
|
||||
|
|
|
@ -169,23 +169,16 @@ type Paths = { [match: string]: string[] }
|
|||
export class JsConfigPathsPlugin implements webpack.ResolvePluginInstance {
|
||||
paths: Paths
|
||||
resolvedBaseUrl: string
|
||||
jsConfigPlugin: true
|
||||
|
||||
constructor(paths: Paths, resolvedBaseUrl: string) {
|
||||
this.paths = paths
|
||||
this.resolvedBaseUrl = resolvedBaseUrl
|
||||
this.jsConfigPlugin = true
|
||||
log('tsconfig.json or jsconfig.json paths: %O', paths)
|
||||
log('resolved baseUrl: %s', resolvedBaseUrl)
|
||||
}
|
||||
apply(resolver: any) {
|
||||
const paths = this.paths
|
||||
const pathsKeys = Object.keys(paths)
|
||||
|
||||
// If no aliases are added bail out
|
||||
if (pathsKeys.length === 0) {
|
||||
log('paths are empty, bailing out')
|
||||
return
|
||||
}
|
||||
|
||||
const baseDirectory = this.resolvedBaseUrl
|
||||
const target = resolver.ensureHook('resolve')
|
||||
resolver
|
||||
.getHook('described-resolve')
|
||||
|
@ -196,6 +189,15 @@ export class JsConfigPathsPlugin implements webpack.ResolvePluginInstance {
|
|||
resolveContext: any,
|
||||
callback: (err?: any, result?: any) => void
|
||||
) => {
|
||||
const paths = this.paths
|
||||
const pathsKeys = Object.keys(paths)
|
||||
|
||||
// If no aliases are added bail out
|
||||
if (pathsKeys.length === 0) {
|
||||
log('paths are empty, bailing out')
|
||||
return callback()
|
||||
}
|
||||
|
||||
const moduleName = request.request
|
||||
|
||||
// Exclude node_modules from paths support (speeds up resolving)
|
||||
|
@ -246,7 +248,7 @@ export class JsConfigPathsPlugin implements webpack.ResolvePluginInstance {
|
|||
// try next path candidate
|
||||
return pathCallback()
|
||||
}
|
||||
const candidate = path.join(baseDirectory, curPath)
|
||||
const candidate = path.join(this.resolvedBaseUrl, curPath)
|
||||
const obj = Object.assign({}, request, {
|
||||
request: candidate,
|
||||
})
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { existsSync } from 'fs'
|
||||
import { join, relative } from 'path'
|
||||
import { promises as fs } from 'fs'
|
||||
import { fileExists } from './file-exists'
|
||||
import { resolveFrom } from './resolve-from'
|
||||
import { dirname, join, relative } from 'path'
|
||||
|
||||
export interface MissingDependency {
|
||||
file: string
|
||||
|
@ -17,31 +19,36 @@ export async function hasNecessaryDependencies(
|
|||
requiredPackages: MissingDependency[]
|
||||
): Promise<NecessaryDependencies> {
|
||||
let resolutions = new Map<string, string>()
|
||||
const missingPackages = requiredPackages.filter((p) => {
|
||||
try {
|
||||
if (p.exportsRestrict) {
|
||||
const pkgPath = require.resolve(`${p.pkg}/package.json`, {
|
||||
paths: [baseDir],
|
||||
})
|
||||
const fileNameToVerify = relative(p.pkg, p.file)
|
||||
if (fileNameToVerify) {
|
||||
const fileToVerify = join(pkgPath, '..', fileNameToVerify)
|
||||
if (existsSync(fileToVerify)) {
|
||||
resolutions.set(p.pkg, join(pkgPath, '..'))
|
||||
const missingPackages: MissingDependency[] = []
|
||||
|
||||
await Promise.all(
|
||||
requiredPackages.map(async (p) => {
|
||||
try {
|
||||
const pkgPath = await fs.realpath(
|
||||
resolveFrom(baseDir, `${p.pkg}/package.json`)
|
||||
)
|
||||
const pkgDir = dirname(pkgPath)
|
||||
|
||||
if (p.exportsRestrict) {
|
||||
const fileNameToVerify = relative(p.pkg, p.file)
|
||||
if (fileNameToVerify) {
|
||||
const fileToVerify = join(pkgDir, fileNameToVerify)
|
||||
if (await fileExists(fileToVerify)) {
|
||||
resolutions.set(p.pkg, fileToVerify)
|
||||
} else {
|
||||
return missingPackages.push(p)
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
resolutions.set(p.pkg, pkgPath)
|
||||
}
|
||||
} else {
|
||||
resolutions.set(p.pkg, pkgPath)
|
||||
resolutions.set(p.pkg, resolveFrom(baseDir, p.file))
|
||||
}
|
||||
} else {
|
||||
resolutions.set(p.pkg, require.resolve(p.file, { paths: [baseDir] }))
|
||||
} catch (_) {
|
||||
return missingPackages.push(p)
|
||||
}
|
||||
return false
|
||||
} catch (_) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
resolved: resolutions,
|
||||
|
|
|
@ -95,7 +95,14 @@ export function install(
|
|||
*/
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' },
|
||||
env: {
|
||||
...process.env,
|
||||
ADBLOCK: '1',
|
||||
// we set NODE_ENV to development as pnpm skips dev
|
||||
// dependencies when production
|
||||
NODE_ENV: 'development',
|
||||
DISABLE_OPENCOLLECTIVE: '1',
|
||||
},
|
||||
})
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
|
|
55
packages/next/lib/resolve-from.ts
Normal file
55
packages/next/lib/resolve-from.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
// source: https://github.com/sindresorhus/resolve-from
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import isError from './is-error'
|
||||
|
||||
const Module = require('module')
|
||||
|
||||
export const resolveFrom = (
|
||||
fromDirectory: string,
|
||||
moduleId: string,
|
||||
silent?: boolean
|
||||
) => {
|
||||
if (typeof fromDirectory !== 'string') {
|
||||
throw new TypeError(
|
||||
`Expected \`fromDir\` to be of type \`string\`, got \`${typeof fromDirectory}\``
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof moduleId !== 'string') {
|
||||
throw new TypeError(
|
||||
`Expected \`moduleId\` to be of type \`string\`, got \`${typeof moduleId}\``
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
fromDirectory = fs.realpathSync(fromDirectory)
|
||||
} catch (error: unknown) {
|
||||
if (isError(error) && error.code === 'ENOENT') {
|
||||
fromDirectory = path.resolve(fromDirectory)
|
||||
} else if (silent) {
|
||||
return
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fromFile = path.join(fromDirectory, 'noop.js')
|
||||
|
||||
const resolveFileName = () =>
|
||||
Module._resolveFilename(moduleId, {
|
||||
id: fromFile,
|
||||
filename: fromFile,
|
||||
paths: Module._nodeModulePaths(fromDirectory),
|
||||
})
|
||||
|
||||
if (silent) {
|
||||
try {
|
||||
return resolveFileName()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return resolveFileName()
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import chalk from 'next/dist/compiled/chalk'
|
||||
|
||||
import { getOxfordCommaList } from '../oxford-comma-list'
|
||||
import { MissingDependency } from '../has-necessary-dependencies'
|
||||
import { FatalError } from '../fatal-error'
|
||||
import { getPkgManager } from '../helpers/get-pkg-manager'
|
||||
|
||||
export async function missingDepsError(
|
||||
dir: string,
|
||||
missingPackages: MissingDependency[]
|
||||
) {
|
||||
const packagesHuman = getOxfordCommaList(missingPackages.map((p) => p.pkg))
|
||||
const packagesCli = missingPackages.map((p) => p.pkg).join(' ')
|
||||
const packageManager = getPkgManager(dir)
|
||||
|
||||
const removalMsg =
|
||||
'\n\n' +
|
||||
chalk.bold(
|
||||
'If you are not trying to use TypeScript, please remove the ' +
|
||||
chalk.cyan('tsconfig.json') +
|
||||
' file from your package root (and any TypeScript files in your pages directory).'
|
||||
)
|
||||
|
||||
throw new FatalError(
|
||||
chalk.bold.red(
|
||||
`It looks like you're trying to use TypeScript but do not have the required package(s) installed.`
|
||||
) +
|
||||
'\n\n' +
|
||||
chalk.bold(`Please install ${chalk.bold(packagesHuman)} by running:`) +
|
||||
'\n\n' +
|
||||
`\t${chalk.bold.cyan(
|
||||
(packageManager === 'yarn'
|
||||
? 'yarn add --dev'
|
||||
: packageManager === 'pnpm'
|
||||
? 'pnpm install --save-dev'
|
||||
: 'npm install --save-dev') +
|
||||
' ' +
|
||||
packagesCli
|
||||
)}` +
|
||||
removalMsg +
|
||||
'\n'
|
||||
)
|
||||
}
|
|
@ -13,10 +13,14 @@ import { getTypeScriptIntent } from './typescript/getTypeScriptIntent'
|
|||
import { TypeCheckResult } from './typescript/runTypeCheck'
|
||||
import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations'
|
||||
import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults'
|
||||
import { missingDepsError } from './typescript/missingDependencyError'
|
||||
import { installDependencies } from './install-dependencies'
|
||||
|
||||
const requiredPackages = [
|
||||
{ file: 'typescript', pkg: 'typescript', exportsRestrict: false },
|
||||
{
|
||||
file: 'typescript/lib/typescript.js',
|
||||
pkg: 'typescript',
|
||||
exportsRestrict: true,
|
||||
},
|
||||
{
|
||||
file: '@types/react/index.d.ts',
|
||||
pkg: '@types/react',
|
||||
|
@ -25,18 +29,25 @@ const requiredPackages = [
|
|||
{
|
||||
file: '@types/node/index.d.ts',
|
||||
pkg: '@types/node',
|
||||
exportsRestrict: false,
|
||||
exportsRestrict: true,
|
||||
},
|
||||
]
|
||||
|
||||
export async function verifyTypeScriptSetup(
|
||||
dir: string,
|
||||
intentDirs: string[],
|
||||
typeCheckPreflight: boolean,
|
||||
tsconfigPath: string,
|
||||
disableStaticImages: boolean,
|
||||
export async function verifyTypeScriptSetup({
|
||||
dir,
|
||||
cacheDir,
|
||||
intentDirs,
|
||||
tsconfigPath,
|
||||
typeCheckPreflight,
|
||||
disableStaticImages,
|
||||
}: {
|
||||
dir: string
|
||||
cacheDir?: string
|
||||
): Promise<{ result?: TypeCheckResult; version: string | null }> {
|
||||
tsconfigPath: string
|
||||
intentDirs: string[]
|
||||
typeCheckPreflight: boolean
|
||||
disableStaticImages: boolean
|
||||
}): Promise<{ result?: TypeCheckResult; version: string | null }> {
|
||||
const resolvedTsConfigPath = path.join(dir, tsconfigPath)
|
||||
|
||||
try {
|
||||
|
@ -47,13 +58,36 @@ export async function verifyTypeScriptSetup(
|
|||
}
|
||||
|
||||
// Ensure TypeScript and necessary `@types/*` are installed:
|
||||
const deps: NecessaryDependencies = await hasNecessaryDependencies(
|
||||
let deps: NecessaryDependencies = await hasNecessaryDependencies(
|
||||
dir,
|
||||
requiredPackages
|
||||
)
|
||||
|
||||
if (deps.missing?.length > 0) {
|
||||
await missingDepsError(dir, deps.missing)
|
||||
console.log(
|
||||
chalk.bold.yellow(
|
||||
`It looks like you're trying to use TypeScript but do not have the required package(s) installed.`
|
||||
) +
|
||||
'\n' +
|
||||
'Installing dependencies' +
|
||||
'\n\n' +
|
||||
chalk.bold(
|
||||
'If you are not trying to use TypeScript, please remove the ' +
|
||||
chalk.cyan('tsconfig.json') +
|
||||
' file from your package root (and any TypeScript files in your pages directory).'
|
||||
) +
|
||||
'\n'
|
||||
)
|
||||
await installDependencies(dir, deps.missing, true).catch((err) => {
|
||||
if (err && typeof err === 'object' && 'command' in err) {
|
||||
console.error(
|
||||
`Failed to install required TypeScript dependencies, please install them manually to continue:\n` +
|
||||
(err as any).command
|
||||
)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
deps = await hasNecessaryDependencies(dir, requiredPackages)
|
||||
}
|
||||
|
||||
// Load TypeScript after we're sure it exists:
|
||||
|
|
|
@ -46,7 +46,10 @@ import { setGlobal } from '../../trace'
|
|||
import HotReloader from './hot-reloader'
|
||||
import { findPageFile } from '../lib/find-page-file'
|
||||
import { getNodeOptionsWithoutInspect } from '../lib/utils'
|
||||
import { withCoalescedInvoke } from '../../lib/coalesced-function'
|
||||
import {
|
||||
UnwrapPromise,
|
||||
withCoalescedInvoke,
|
||||
} from '../../lib/coalesced-function'
|
||||
import { loadDefaultErrorComponents } from '../load-components'
|
||||
import { DecodeError, MiddlewareNotFoundError } from '../../shared/lib/utils'
|
||||
import {
|
||||
|
@ -73,6 +76,7 @@ import {
|
|||
NestedMiddlewareError,
|
||||
} from '../../build/utils'
|
||||
import { getDefineEnv } from '../../build/webpack-config'
|
||||
import loadJsConfig from '../../build/load-jsconfig'
|
||||
|
||||
// Load ReactDevOverlay only when needed
|
||||
let ReactDevOverlayImpl: React.FunctionComponent
|
||||
|
@ -104,6 +108,8 @@ export default class DevServer extends Server {
|
|||
private actualMiddlewareFile?: string
|
||||
private middleware?: RoutingItem
|
||||
private edgeFunctions?: RoutingItem[]
|
||||
private verifyingTypeScript?: boolean
|
||||
private usingTypeScript?: boolean
|
||||
|
||||
protected staticPathsWorker?: { [key: string]: any } & {
|
||||
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
||||
|
@ -287,8 +293,17 @@ export default class DevServer extends Server {
|
|||
].map((file) => pathJoin(this.dir, file))
|
||||
|
||||
files.push(...envFiles)
|
||||
|
||||
// tsconfig/jsonfig paths hot-reloading
|
||||
const tsconfigPaths = [
|
||||
pathJoin(this.dir, 'tsconfig.json'),
|
||||
pathJoin(this.dir, 'jsconfig.json'),
|
||||
]
|
||||
files.push(...tsconfigPaths)
|
||||
|
||||
wp.watch({ directories: [this.dir], startTime: 0 })
|
||||
const envFileTimes = new Map()
|
||||
const fileWatchTimes = new Map()
|
||||
let enabledTypeScript = this.usingTypeScript
|
||||
|
||||
wp.on('aggregated', async () => {
|
||||
let middlewareMatcher: RegExp | undefined
|
||||
|
@ -297,6 +312,7 @@ export default class DevServer extends Server {
|
|||
const appPaths: Record<string, string> = {}
|
||||
const edgeRoutesSet = new Set<string>()
|
||||
let envChange = false
|
||||
let tsconfigChange = false
|
||||
|
||||
for (const [fileName, meta] of knownFiles) {
|
||||
if (
|
||||
|
@ -306,14 +322,24 @@ export default class DevServer extends Server {
|
|||
continue
|
||||
}
|
||||
|
||||
const watchTime = fileWatchTimes.get(fileName)
|
||||
const watchTimeChange = watchTime && watchTime !== meta?.timestamp
|
||||
fileWatchTimes.set(fileName, meta.timestamp)
|
||||
|
||||
if (envFiles.includes(fileName)) {
|
||||
if (
|
||||
envFileTimes.get(fileName) &&
|
||||
envFileTimes.get(fileName) !== meta.timestamp
|
||||
) {
|
||||
if (watchTimeChange) {
|
||||
envChange = true
|
||||
}
|
||||
envFileTimes.set(fileName, meta.timestamp)
|
||||
continue
|
||||
}
|
||||
|
||||
if (tsconfigPaths.includes(fileName)) {
|
||||
if (fileName.endsWith('tsconfig.json')) {
|
||||
enabledTypeScript = true
|
||||
}
|
||||
if (watchTimeChange) {
|
||||
tsconfigChange = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -350,6 +376,10 @@ export default class DevServer extends Server {
|
|||
continue
|
||||
}
|
||||
|
||||
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
|
||||
enabledTypeScript = true
|
||||
}
|
||||
|
||||
let pageName = absolutePathToPage(fileName, {
|
||||
pagesDir: isAppPath ? this.appDir! : this.pagesDir,
|
||||
extensions: this.nextConfig.pageExtensions,
|
||||
|
@ -392,8 +422,31 @@ export default class DevServer extends Server {
|
|||
})
|
||||
}
|
||||
|
||||
if (envChange) {
|
||||
this.loadEnvConfig({ dev: true, forceReload: true })
|
||||
if (!this.usingTypeScript && enabledTypeScript) {
|
||||
// we tolerate the error here as this is best effort
|
||||
// and the manual install command will be shown
|
||||
await this.verifyTypeScript()
|
||||
.then(() => {
|
||||
tsconfigChange = true
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (envChange || tsconfigChange) {
|
||||
if (envChange) {
|
||||
this.loadEnvConfig({ dev: true, forceReload: true })
|
||||
}
|
||||
let tsconfigResult:
|
||||
| UnwrapPromise<ReturnType<typeof loadJsConfig>>
|
||||
| undefined
|
||||
|
||||
if (tsconfigChange) {
|
||||
try {
|
||||
tsconfigResult = await loadJsConfig(this.dir, this.nextConfig)
|
||||
} catch (_) {
|
||||
/* do we want to log if there are syntax errors in tsconfig while editing? */
|
||||
}
|
||||
}
|
||||
|
||||
this.hotReloader?.activeConfigs?.forEach((config, idx) => {
|
||||
const isClient = idx === 0
|
||||
|
@ -404,34 +457,69 @@ export default class DevServer extends Server {
|
|||
this.customRoutes.rewrites.beforeFiles.length > 0 ||
|
||||
this.customRoutes.rewrites.fallback.length > 0
|
||||
|
||||
config.plugins?.forEach((plugin: any) => {
|
||||
// we look for the DefinePlugin definitions so we can
|
||||
// update them on the active compilers
|
||||
if (
|
||||
plugin &&
|
||||
typeof plugin.definitions === 'object' &&
|
||||
plugin.definitions.__NEXT_DEFINE_ENV
|
||||
) {
|
||||
const newDefine = getDefineEnv({
|
||||
dev: true,
|
||||
config: this.nextConfig,
|
||||
distDir: this.distDir,
|
||||
isClient,
|
||||
hasRewrites,
|
||||
hasReactRoot: this.hotReloader?.hasReactRoot,
|
||||
isNodeServer,
|
||||
isEdgeServer,
|
||||
hasServerComponents: this.hotReloader?.hasServerComponents,
|
||||
})
|
||||
if (tsconfigChange) {
|
||||
config.resolve?.plugins?.forEach((plugin: any) => {
|
||||
// look for the JsConfigPathsPlugin and update with
|
||||
// the latest paths/baseUrl config
|
||||
if (plugin && plugin.jsConfigPlugin && tsconfigResult) {
|
||||
const { resolvedBaseUrl, jsConfig } = tsconfigResult
|
||||
const currentResolvedBaseUrl = plugin.resolvedBaseUrl
|
||||
const resolvedUrlIndex = config.resolve?.modules?.findIndex(
|
||||
(item) => item === currentResolvedBaseUrl
|
||||
)
|
||||
|
||||
Object.keys(plugin.definitions).forEach((key) => {
|
||||
if (!(key in newDefine)) {
|
||||
delete plugin.definitions[key]
|
||||
if (
|
||||
resolvedBaseUrl &&
|
||||
resolvedBaseUrl !== currentResolvedBaseUrl
|
||||
) {
|
||||
// remove old baseUrl and add new one
|
||||
if (resolvedUrlIndex && resolvedUrlIndex > -1) {
|
||||
config.resolve?.modules?.splice(resolvedUrlIndex, 1)
|
||||
}
|
||||
config.resolve?.modules?.push(resolvedBaseUrl)
|
||||
}
|
||||
})
|
||||
Object.assign(plugin.definitions, newDefine)
|
||||
}
|
||||
})
|
||||
|
||||
if (jsConfig?.compilerOptions?.paths && resolvedBaseUrl) {
|
||||
Object.keys(plugin.paths).forEach((key) => {
|
||||
delete plugin.paths[key]
|
||||
})
|
||||
Object.assign(plugin.paths, jsConfig.compilerOptions.paths)
|
||||
plugin.resolvedBaseUrl = resolvedBaseUrl
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (envChange) {
|
||||
config.plugins?.forEach((plugin: any) => {
|
||||
// we look for the DefinePlugin definitions so we can
|
||||
// update them on the active compilers
|
||||
if (
|
||||
plugin &&
|
||||
typeof plugin.definitions === 'object' &&
|
||||
plugin.definitions.__NEXT_DEFINE_ENV
|
||||
) {
|
||||
const newDefine = getDefineEnv({
|
||||
dev: true,
|
||||
config: this.nextConfig,
|
||||
distDir: this.distDir,
|
||||
isClient,
|
||||
hasRewrites,
|
||||
hasReactRoot: this.hotReloader?.hasReactRoot,
|
||||
isNodeServer,
|
||||
isEdgeServer,
|
||||
hasServerComponents: this.hotReloader?.hasServerComponents,
|
||||
})
|
||||
|
||||
Object.keys(plugin.definitions).forEach((key) => {
|
||||
if (!(key in newDefine)) {
|
||||
delete plugin.definitions[key]
|
||||
}
|
||||
})
|
||||
Object.assign(plugin.definitions, newDefine)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.hotReloader?.invalidate()
|
||||
}
|
||||
|
@ -516,17 +604,33 @@ export default class DevServer extends Server {
|
|||
this.webpackWatcher = null
|
||||
}
|
||||
|
||||
private async verifyTypeScript() {
|
||||
if (this.verifyingTypeScript) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.verifyingTypeScript = true
|
||||
const verifyResult = await verifyTypeScriptSetup({
|
||||
dir: this.dir,
|
||||
intentDirs: [this.pagesDir!, this.appDir].filter(Boolean) as string[],
|
||||
typeCheckPreflight: false,
|
||||
tsconfigPath: this.nextConfig.typescript.tsconfigPath,
|
||||
disableStaticImages: this.nextConfig.images.disableStaticImages,
|
||||
})
|
||||
|
||||
if (verifyResult.version) {
|
||||
this.usingTypeScript = true
|
||||
}
|
||||
} finally {
|
||||
this.verifyingTypeScript = false
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(): Promise<void> {
|
||||
setGlobal('distDir', this.distDir)
|
||||
setGlobal('phase', PHASE_DEVELOPMENT_SERVER)
|
||||
await verifyTypeScriptSetup(
|
||||
this.dir,
|
||||
[this.pagesDir!, this.appDir].filter(Boolean) as string[],
|
||||
false,
|
||||
this.nextConfig.typescript.tsconfigPath,
|
||||
this.nextConfig.images.disableStaticImages
|
||||
)
|
||||
|
||||
await this.verifyTypeScript()
|
||||
this.customRoutes = await loadCustomRoutes(this.nextConfig)
|
||||
|
||||
// reload router
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function Button1(props) {
|
||||
return <button {...props}>first button</button>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function Button2(props) {
|
||||
return <button {...props}>second button</button>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function Button2(props) {
|
||||
return <button {...props}>third button</button>
|
||||
}
|
26
test/development/jsconfig-path-reloading/app/jsconfig.json
Normal file
26
test/development/jsconfig-path-reloading/app/jsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@c/*": ["components/*"],
|
||||
"@lib/*": ["lib/first-lib/*"],
|
||||
"@mybutton": ["components/button-2.js"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const firstData = {
|
||||
hello: 'world',
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const secondData = {
|
||||
hello: 'again',
|
||||
}
|
13
test/development/jsconfig-path-reloading/app/pages/index.js
Normal file
13
test/development/jsconfig-path-reloading/app/pages/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Button1 } from '@c/button-1'
|
||||
import { Button2 } from '@mybutton'
|
||||
import { firstData } from '@lib/first-data'
|
||||
|
||||
export default function Page(props) {
|
||||
return (
|
||||
<>
|
||||
<Button1 />
|
||||
<Button2 />
|
||||
<p id="first-data">{JSON.stringify(firstData)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
190
test/development/jsconfig-path-reloading/index.test.ts
Normal file
190
test/development/jsconfig-path-reloading/index.test.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import {
|
||||
check,
|
||||
hasRedbox,
|
||||
renderViaHTTP,
|
||||
getRedboxSource,
|
||||
} from 'next-test-utils'
|
||||
import cheerio from 'cheerio'
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
describe('jsconfig-path-reloading', () => {
|
||||
let next: NextInstance
|
||||
const tsConfigFile = 'jsconfig.json'
|
||||
const indexPage = 'pages/index.js'
|
||||
|
||||
function runTests({ addAfterStart }: { addAfterStart?: boolean }) {
|
||||
beforeAll(async () => {
|
||||
let tsConfigContent = await fs.readFile(
|
||||
join(__dirname, 'app/jsconfig.json'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
next = await createNext({
|
||||
files: {
|
||||
components: new FileRef(join(__dirname, 'app/components')),
|
||||
pages: new FileRef(join(__dirname, 'app/pages')),
|
||||
lib: new FileRef(join(__dirname, 'app/lib')),
|
||||
...(addAfterStart
|
||||
? {}
|
||||
: {
|
||||
[tsConfigFile]: tsConfigContent,
|
||||
}),
|
||||
},
|
||||
dependencies: {
|
||||
typescript: 'latest',
|
||||
'@types/react': 'latest',
|
||||
'@types/node': 'latest',
|
||||
},
|
||||
})
|
||||
|
||||
if (addAfterStart) {
|
||||
await next.patchFile(tsConfigFile, tsConfigContent)
|
||||
}
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should load with initial paths config correctly', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/')
|
||||
const $ = cheerio.load(html)
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect($('#first-data').text()).toContain(
|
||||
JSON.stringify({
|
||||
hello: 'world',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should recover from module not found when paths is updated', async () => {
|
||||
const indexContent = await next.readFile(indexPage)
|
||||
const tsconfigContent = await next.readFile(tsConfigFile)
|
||||
const parsedTsConfig = JSON.parse(tsconfigContent)
|
||||
|
||||
const browser = await webdriver(next.url, '/')
|
||||
|
||||
try {
|
||||
const html = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect(html).toContain('first-data')
|
||||
expect(html).not.toContain('second-data')
|
||||
|
||||
await next.patchFile(
|
||||
indexPage,
|
||||
`import {secondData} from "@lib/second-data"\n${indexContent.replace(
|
||||
'</p>',
|
||||
`</p><p id="second-data">{JSON.stringify(secondData)}</p>`
|
||||
)}`
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, true)).toBe(true)
|
||||
expect(await getRedboxSource(browser)).toContain('"@lib/second-data"')
|
||||
|
||||
await next.patchFile(
|
||||
tsConfigFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
...parsedTsConfig,
|
||||
compilerOptions: {
|
||||
...parsedTsConfig.compilerOptions,
|
||||
paths: {
|
||||
...parsedTsConfig.compilerOptions.paths,
|
||||
'@lib/*': ['lib/first-lib/*', 'lib/second-lib/*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, false)).toBe(false)
|
||||
|
||||
const html2 = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html2).toContain('first button')
|
||||
expect(html2).toContain('second button')
|
||||
expect(html2).toContain('first-data')
|
||||
expect(html2).toContain('second-data')
|
||||
} finally {
|
||||
await next.patchFile(indexPage, indexContent)
|
||||
await next.patchFile(tsConfigFile, tsconfigContent)
|
||||
await check(async () => {
|
||||
const html3 = await browser.eval('document.documentElement.innerHTML')
|
||||
return html3.includes('first-data') && !html3.includes('second-data')
|
||||
? 'success'
|
||||
: html3
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
|
||||
it('should automatically fast refresh content when path is added without error', async () => {
|
||||
const indexContent = await next.readFile(indexPage)
|
||||
const tsconfigContent = await next.readFile(tsConfigFile)
|
||||
const parsedTsConfig = JSON.parse(tsconfigContent)
|
||||
|
||||
const browser = await webdriver(next.url, '/')
|
||||
|
||||
try {
|
||||
const html = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect(html).toContain('first-data')
|
||||
|
||||
await next.patchFile(
|
||||
tsConfigFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
...parsedTsConfig,
|
||||
compilerOptions: {
|
||||
...parsedTsConfig.compilerOptions,
|
||||
paths: {
|
||||
...parsedTsConfig.compilerOptions.paths,
|
||||
'@myotherbutton': ['components/button-3.js'],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
await next.patchFile(
|
||||
indexPage,
|
||||
indexContent.replace('@mybutton', '@myotherbutton')
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, false)).toBe(false)
|
||||
|
||||
await check(async () => {
|
||||
const html2 = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html2).toContain('first button')
|
||||
expect(html2).not.toContain('second button')
|
||||
expect(html2).toContain('third button')
|
||||
expect(html2).toContain('first-data')
|
||||
return 'success'
|
||||
}, 'success')
|
||||
} finally {
|
||||
await next.patchFile(indexPage, indexContent)
|
||||
await next.patchFile(tsConfigFile, tsconfigContent)
|
||||
await check(async () => {
|
||||
const html3 = await browser.eval('document.documentElement.innerHTML')
|
||||
return html3.includes('first button') &&
|
||||
!html3.includes('third button')
|
||||
? 'success'
|
||||
: html3
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('jsconfig', () => {
|
||||
runTests({})
|
||||
})
|
||||
|
||||
describe('jsconfig added after starting dev', () => {
|
||||
runTests({ addAfterStart: true })
|
||||
})
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
export function Button1(props) {
|
||||
return <button {...props}>first button</button>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function Button2(props) {
|
||||
return <button {...props}>second button</button>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function Button2(props) {
|
||||
return <button {...props}>third button</button>
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const firstData = {
|
||||
hello: 'world',
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const secondData = {
|
||||
hello: 'again',
|
||||
}
|
13
test/development/tsconfig-path-reloading/app/pages/index.tsx
Normal file
13
test/development/tsconfig-path-reloading/app/pages/index.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Button1 } from '@c/button-1'
|
||||
import { Button2 } from '@mybutton'
|
||||
import { firstData } from '@lib/first-data'
|
||||
|
||||
export default function Page(props) {
|
||||
return (
|
||||
<>
|
||||
<Button1 />
|
||||
<Button2 />
|
||||
<p id="first-data">{JSON.stringify(firstData)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
26
test/development/tsconfig-path-reloading/app/tsconfig.json
Normal file
26
test/development/tsconfig-path-reloading/app/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@c/*": ["components/*"],
|
||||
"@lib/*": ["lib/first-lib/*"],
|
||||
"@mybutton": ["components/button-2.tsx"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
190
test/development/tsconfig-path-reloading/index.test.ts
Normal file
190
test/development/tsconfig-path-reloading/index.test.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import {
|
||||
check,
|
||||
hasRedbox,
|
||||
renderViaHTTP,
|
||||
getRedboxSource,
|
||||
} from 'next-test-utils'
|
||||
import cheerio from 'cheerio'
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
describe('tsconfig-path-reloading', () => {
|
||||
let next: NextInstance
|
||||
const tsConfigFile = 'tsconfig.json'
|
||||
const indexPage = 'pages/index.tsx'
|
||||
|
||||
function runTests({ addAfterStart }: { addAfterStart?: boolean }) {
|
||||
beforeAll(async () => {
|
||||
let tsConfigContent = await fs.readFile(
|
||||
join(__dirname, 'app/tsconfig.json'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
next = await createNext({
|
||||
files: {
|
||||
components: new FileRef(join(__dirname, 'app/components')),
|
||||
pages: new FileRef(join(__dirname, 'app/pages')),
|
||||
lib: new FileRef(join(__dirname, 'app/lib')),
|
||||
...(addAfterStart
|
||||
? {}
|
||||
: {
|
||||
[tsConfigFile]: tsConfigContent,
|
||||
}),
|
||||
},
|
||||
dependencies: {
|
||||
typescript: 'latest',
|
||||
'@types/react': 'latest',
|
||||
'@types/node': 'latest',
|
||||
},
|
||||
})
|
||||
|
||||
if (addAfterStart) {
|
||||
await next.patchFile(tsConfigFile, tsConfigContent)
|
||||
}
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should load with initial paths config correctly', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/')
|
||||
const $ = cheerio.load(html)
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect($('#first-data').text()).toContain(
|
||||
JSON.stringify({
|
||||
hello: 'world',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should recover from module not found when paths is updated', async () => {
|
||||
const indexContent = await next.readFile(indexPage)
|
||||
const tsconfigContent = await next.readFile(tsConfigFile)
|
||||
const parsedTsConfig = JSON.parse(tsconfigContent)
|
||||
|
||||
const browser = await webdriver(next.url, '/')
|
||||
|
||||
try {
|
||||
const html = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect(html).toContain('first-data')
|
||||
expect(html).not.toContain('second-data')
|
||||
|
||||
await next.patchFile(
|
||||
indexPage,
|
||||
`import {secondData} from "@lib/second-data"\n${indexContent.replace(
|
||||
'</p>',
|
||||
`</p><p id="second-data">{JSON.stringify(secondData)}</p>`
|
||||
)}`
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, true)).toBe(true)
|
||||
expect(await getRedboxSource(browser)).toContain('"@lib/second-data"')
|
||||
|
||||
await next.patchFile(
|
||||
tsConfigFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
...parsedTsConfig,
|
||||
compilerOptions: {
|
||||
...parsedTsConfig.compilerOptions,
|
||||
paths: {
|
||||
...parsedTsConfig.compilerOptions.paths,
|
||||
'@lib/*': ['lib/first-lib/*', 'lib/second-lib/*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, false)).toBe(false)
|
||||
|
||||
const html2 = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html2).toContain('first button')
|
||||
expect(html2).toContain('second button')
|
||||
expect(html2).toContain('first-data')
|
||||
expect(html2).toContain('second-data')
|
||||
} finally {
|
||||
await next.patchFile(indexPage, indexContent)
|
||||
await next.patchFile(tsConfigFile, tsconfigContent)
|
||||
await check(async () => {
|
||||
const html3 = await browser.eval('document.documentElement.innerHTML')
|
||||
return html3.includes('first-data') && !html3.includes('second-data')
|
||||
? 'success'
|
||||
: html3
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
|
||||
it('should automatically fast refresh content when path is added without error', async () => {
|
||||
const indexContent = await next.readFile(indexPage)
|
||||
const tsconfigContent = await next.readFile(tsConfigFile)
|
||||
const parsedTsConfig = JSON.parse(tsconfigContent)
|
||||
|
||||
const browser = await webdriver(next.url, '/')
|
||||
|
||||
try {
|
||||
const html = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html).toContain('first button')
|
||||
expect(html).toContain('second button')
|
||||
expect(html).toContain('first-data')
|
||||
|
||||
await next.patchFile(
|
||||
tsConfigFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
...parsedTsConfig,
|
||||
compilerOptions: {
|
||||
...parsedTsConfig.compilerOptions,
|
||||
paths: {
|
||||
...parsedTsConfig.compilerOptions.paths,
|
||||
'@myotherbutton': ['components/button-3.tsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
await next.patchFile(
|
||||
indexPage,
|
||||
indexContent.replace('@mybutton', '@myotherbutton')
|
||||
)
|
||||
|
||||
expect(await hasRedbox(browser, false)).toBe(false)
|
||||
|
||||
await check(async () => {
|
||||
const html2 = await browser.eval('document.documentElement.innerHTML')
|
||||
expect(html2).toContain('first button')
|
||||
expect(html2).not.toContain('second button')
|
||||
expect(html2).toContain('third button')
|
||||
expect(html2).toContain('first-data')
|
||||
return 'success'
|
||||
}, 'success')
|
||||
} finally {
|
||||
await next.patchFile(indexPage, indexContent)
|
||||
await next.patchFile(tsConfigFile, tsconfigContent)
|
||||
await check(async () => {
|
||||
const html3 = await browser.eval('document.documentElement.innerHTML')
|
||||
return html3.includes('first button') &&
|
||||
!html3.includes('third button')
|
||||
? 'success'
|
||||
: html3
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('tsconfig', () => {
|
||||
runTests({})
|
||||
})
|
||||
|
||||
describe('tsconfig added after starting dev', () => {
|
||||
runTests({ addAfterStart: true })
|
||||
})
|
||||
})
|
61
test/development/typescript-auto-install/index.test.ts
Normal file
61
test/development/typescript-auto-install/index.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { createNext } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { check, renderViaHTTP } from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
// @ts-expect-error missing types
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
describe('typescript-auto-install', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
export default function Page() {
|
||||
return <p>hello world</p>
|
||||
}
|
||||
`,
|
||||
},
|
||||
startCommand: 'yarn next dev',
|
||||
installCommand: 'yarn',
|
||||
dependencies: {},
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should work', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/')
|
||||
expect(html).toContain('hello world')
|
||||
})
|
||||
|
||||
it('should detect TypeScript being added and auto setup', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
const pageContent = await next.readFile('pages/index.js')
|
||||
|
||||
await check(
|
||||
() => browser.eval('document.documentElement.innerHTML'),
|
||||
/hello world/
|
||||
)
|
||||
await next.renameFile('pages/index.js', 'pages/index.tsx')
|
||||
|
||||
await check(
|
||||
() => stripAnsi(next.cliOutput),
|
||||
/We detected TypeScript in your project and created a tsconfig\.json file for you/i
|
||||
)
|
||||
|
||||
await check(
|
||||
() => browser.eval('document.documentElement.innerHTML'),
|
||||
/hello world/
|
||||
)
|
||||
await next.patchFile(
|
||||
'pages/index.tsx',
|
||||
pageContent.replace('hello world', 'hello again')
|
||||
)
|
||||
|
||||
await check(
|
||||
() => browser.eval('document.documentElement.innerHTML'),
|
||||
/hello again/
|
||||
)
|
||||
})
|
||||
})
|
5
test/integration/typescript-version-warning/app/node_modules/typescript/index.js
generated
vendored
5
test/integration/typescript-version-warning/app/node_modules/typescript/index.js
generated
vendored
|
@ -1,5 +0,0 @@
|
|||
const mod = require('../../../../../../node_modules/typescript')
|
||||
|
||||
mod.version = '3.8.3'
|
||||
|
||||
module.exports = mod
|
5
test/integration/typescript-version-warning/app/node_modules/typescript/lib/typescript.js
generated
vendored
Normal file
5
test/integration/typescript-version-warning/app/node_modules/typescript/lib/typescript.js
generated
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
const mod = require('../../../../../../../node_modules/typescript')
|
||||
|
||||
mod.version = '3.8.3'
|
||||
|
||||
module.exports = mod
|
2
test/integration/typescript-version-warning/app/node_modules/typescript/package.json
generated
vendored
2
test/integration/typescript-version-warning/app/node_modules/typescript/package.json
generated
vendored
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "typescript",
|
||||
"version": "3.8.3",
|
||||
"main": "./index.js"
|
||||
"main": "./lib/typescript.js"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
@ -4,7 +4,7 @@ import { join } from 'path'
|
|||
import { nextBuild, findPort, launchApp, killApp } from 'next-test-utils'
|
||||
|
||||
const appDir = join(__dirname, '../app')
|
||||
const tsFile = join(appDir, 'node_modules/typescript/index.js')
|
||||
const tsFile = join(appDir, 'node_modules/typescript/lib/typescript.js')
|
||||
|
||||
describe('Minimum TypeScript Warning', () => {
|
||||
it('should show warning during next build with old version', async () => {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { createNext } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
|
||||
describe('missing-dep-error', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.tsx': `
|
||||
export default function Page() {
|
||||
return <p>hello world</p>
|
||||
}
|
||||
`,
|
||||
},
|
||||
skipStart: true,
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should only show error once', async () => {
|
||||
await next.start().catch(() => {})
|
||||
expect(
|
||||
next.cliOutput.match(/It looks like you're trying to use TypeScript/g)
|
||||
?.length
|
||||
).toBe(1)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue