Skip to content

Commit

Permalink
Add handling for auto installing TypeScript deps and HMRing tsconfig (#…
Browse files Browse the repository at this point in the history
…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: #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`
  • Loading branch information
ijjk committed Aug 23, 2022
1 parent 8dfab19 commit ec25b47
Show file tree
Hide file tree
Showing 34 changed files with 886 additions and 197 deletions.
9 changes: 8 additions & 1 deletion packages/create-next-app/helpers/install.ts
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions packages/next/build/index.ts
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/next/build/load-jsconfig.ts
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
34 changes: 10 additions & 24 deletions packages/next/build/webpack-config.ts
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
24 changes: 13 additions & 11 deletions packages/next/build/webpack/plugins/jsconfig-paths-plugin.ts
Expand Up @@ -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')
Expand All @@ -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)
Expand Down Expand Up @@ -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,
})
Expand Down
51 changes: 29 additions & 22 deletions packages/next/lib/has-necessary-dependencies.ts
@@ -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
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion packages/next/lib/helpers/install.ts
Expand Up @@ -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) {
Expand Down
55 changes: 55 additions & 0 deletions packages/next/lib/resolve-from.ts
@@ -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()
}
43 changes: 0 additions & 43 deletions packages/next/lib/typescript/missingDependencyError.ts

This file was deleted.

0 comments on commit ec25b47

Please sign in to comment.