未验证 提交 0eab07b7 编写于 作者: J JJ Kasper 提交者: GitHub

Add automatic TypeScript setup (#7125)

* replace fkill with tree-kill
上级 3641f79a
......@@ -9,6 +9,7 @@ import path from 'path'
import formatWebpackMessages from '../client/dev-error-overlay/format-webpack-messages'
import { recursiveDelete } from '../lib/recursive-delete'
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
import { CompilerResult, runCompiler } from './compiler'
import { createEntrypoints, createPagesMapping } from './entries'
import { FlyingShuttle } from './flying-shuttle'
......@@ -18,15 +19,16 @@ import {
collectPages,
getCacheIdentifier,
getFileForPage,
getPageInfo,
getSpecifiedPages,
printTreeView,
getPageInfo,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { exportManifest, getPageChunks } from './webpack/plugins/chunk-graph-plugin'
import {
exportManifest,
getPageChunks,
} from './webpack/plugins/chunk-graph-plugin'
import { writeBuildId } from './write-build-id'
import { recursiveReadDir } from '../lib/recursive-readdir'
export default async function build(dir: string, conf = null): Promise<void> {
if (!(await isWriteable(dir))) {
......@@ -35,6 +37,8 @@ export default async function build(dir: string, conf = null): Promise<void> {
)
}
await verifyTypeScriptSetup(dir)
const debug =
process.env.__NEXT_BUILDER_EXPERIMENTAL_DEBUG === 'true' ||
process.env.__NEXT_BUILDER_EXPERIMENTAL_DEBUG === '1'
......@@ -251,7 +255,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
pageInfos.set(page, {
...(info || {}),
chunks
chunks,
})
if (!(typeof info.serverSize === 'number')) {
......
......@@ -100,8 +100,14 @@ export default async function getBaseWebpackConfig(
}
: undefined
let typeScriptPath
try {
typeScriptPath = resolve.sync('typescript', { basedir: dir })
} catch (_) {}
const tsConfigPath = path.join(dir, 'tsconfig.json')
const useTypeScript = await fileExists(tsConfigPath)
const useTypeScript = Boolean(
typeScriptPath && (await fileExists(tsConfigPath))
)
const resolveConfig = {
// Disable .mjs for node_modules bundling
......@@ -507,11 +513,10 @@ export default async function getBaseWebpackConfig(
`profile-events-${isServer ? 'server' : 'client'}.json`
),
}),
!isServer && useTypeScript &&
!isServer &&
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: dir,
}),
typescript: typeScriptPath,
async: false,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
......
......@@ -13,15 +13,17 @@ const stat = promisify(fs.stat)
* @param {string=dir`} rootDir Used to replace the initial path, only the relative path is left, it's faster than path.relative.
* @returns Promise array holding all relative paths
*/
export async function recursiveReadDir(dir: string, filter: RegExp, arr: string[] = [], rootDir: string = dir): Promise<string[]> {
export async function recursiveReadDir(dir: string, filter: RegExp, ignore?: RegExp, arr: string[] = [], rootDir: string = dir): Promise<string[]> {
const result = await readdir(dir)
await Promise.all(result.map(async (part: string) => {
const absolutePath = join(dir, part)
if (ignore && ignore.test(part)) return
const pathStat = await stat(absolutePath)
if (pathStat.isDirectory()) {
await recursiveReadDir(absolutePath, filter, arr, rootDir)
await recursiveReadDir(absolutePath, filter, ignore, arr, rootDir)
return
}
......
import fs from 'fs'
import os from 'os'
import path from 'path'
import chalk from 'chalk'
import { promisify } from 'util'
import { recursiveReadDir } from './recursive-readdir'
import resolve from 'next/dist/compiled/resolve/index.js'
const exists = promisify(fs.exists)
const writeFile = promisify(fs.writeFile)
const resolveP = (req: string, opts: any): Promise<string> => {
return new Promise((_resolve, reject) => {
resolve(req, opts, (err, res) => {
if (err) return reject(err)
_resolve(res)
})
})
}
function writeJson(fileName: string, object: object): Promise<void> {
return writeFile(
fileName,
JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL,
)
}
async function verifyNoTypeScript(dir: string) {
const typescriptFiles = await recursiveReadDir(
dir,
/.*\.(ts|tsx)/,
/(node_modules|.*\.d\.ts)/,
)
if (typescriptFiles.length > 0) {
console.warn(
chalk.yellow(
`We detected TypeScript in your project (${chalk.bold(
`${typescriptFiles[0]}`,
)}) and created a ${chalk.bold('tsconfig.json')} file for you.`,
),
)
console.warn()
return false
}
return true
}
export async function verifyTypeScriptSetup(dir: string): Promise<void> {
let firstTimeSetup = false
const yarnLockFile = path.join(dir, 'yarn.lock')
const tsConfigPath = path.join(dir, 'tsconfig.json')
if (!(await exists(tsConfigPath))) {
if (await verifyNoTypeScript(dir)) {
return
}
await writeJson(tsConfigPath, {})
firstTimeSetup = true
}
const isYarn = await exists(yarnLockFile)
// Ensure TypeScript is installed
let typescriptPath = ''
let ts: typeof import('typescript')
try {
typescriptPath = await resolveP('typescript', { basedir: dir })
ts = require(typescriptPath)
} catch (_) {
console.error(
chalk.bold.red(
`It looks like you're trying to use TypeScript but do not have ${chalk.bold(
'typescript',
)} installed.`,
),
)
console.error(
chalk.bold(
'Please install',
chalk.cyan.bold('typescript'),
'by running',
chalk.cyan.bold(
isYarn ? 'yarn add typescript' : 'npm install typescript',
) + '.',
),
)
console.error(
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).',
),
)
console.error()
process.exit(1)
return
}
const compilerOptions: any = {
// These are suggested values and will be set when not present in the
// tsconfig.json
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
target: {
parsedValue: ts.ScriptTarget.ES5,
suggested: 'es5',
},
lib: { suggested: ['dom', 'dom.iterable', 'esnext'] },
allowJs: { suggested: true },
skipLibCheck: { suggested: true },
allowSyntheticDefaultImports: { suggested: true },
strict: { suggested: true },
forceConsistentCasingInFileNames: { suggested: true },
// These values are required and cannot be changed by the user
// Keep this in sync with the webpack config
esModuleInterop: {
value: true,
reason: 'requirement for babel',
},
module: {
parsedValue: ts.ModuleKind.ESNext,
value: 'esnext',
reason: 'for dynamic import() support',
},
moduleResolution: {
parsedValue: ts.ModuleResolutionKind.NodeJs,
value: 'node',
reason: 'to match webpack resolution',
},
resolveJsonModule: { value: true },
isolatedModules: {
value: true,
reason: 'requirement for babel',
},
noEmit: { value: true },
jsx: { parsedValue: ts.JsxEmit.Preserve, value: 'preserve' },
}
const formatDiagnosticHost = {
getCanonicalFileName: (fileName: string) => fileName,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => os.EOL,
}
const messages = []
let appTsConfig
let parsedTsConfig
let parsedCompilerOptions
try {
const { config: readTsConfig, error } = ts.readConfigFile(
tsConfigPath,
ts.sys.readFile,
)
if (error) {
throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost))
}
appTsConfig = readTsConfig
// Get TS to parse and resolve any "extends"
// Calling this function also mutates the tsconfig, adding in "include" and
// "exclude", but the compilerOptions remain untouched
parsedTsConfig = JSON.parse(JSON.stringify(readTsConfig))
const result = ts.parseJsonConfigFileContent(
parsedTsConfig,
ts.sys,
path.dirname(tsConfigPath),
)
if (result.errors && result.errors.length) {
throw new Error(
ts.formatDiagnostic(result.errors[0], formatDiagnosticHost),
)
}
parsedCompilerOptions = result.options
} catch (e) {
if (e && e.name === 'SyntaxError') {
console.error(
chalk.red.bold(
'Could not parse',
chalk.cyan('tsconfig.json') + '.',
'Please make sure it contains syntactically correct JSON.',
),
)
}
console.info(e && e.message ? `${e.message}` : '')
process.exit(1)
return
}
if (appTsConfig.compilerOptions == null) {
appTsConfig.compilerOptions = {}
firstTimeSetup = true
}
for (const option of Object.keys(compilerOptions)) {
const { parsedValue, value, suggested, reason } = compilerOptions[option]
const valueToCheck = parsedValue === undefined ? value : parsedValue
const coloredOption = chalk.cyan('compilerOptions.' + option)
if (suggested != null) {
if (parsedCompilerOptions[option] === undefined) {
appTsConfig.compilerOptions[option] = suggested
messages.push(
`${coloredOption} to be ${chalk.bold(
'suggested',
)} value: ${chalk.cyan.bold(suggested)} (this can be changed)`,
)
}
} else if (parsedCompilerOptions[option] !== valueToCheck) {
appTsConfig.compilerOptions[option] = value
messages.push(
`${coloredOption} ${chalk.bold(
valueToCheck == null ? 'must not' : 'must',
)} be ${valueToCheck == null ? 'set' : chalk.cyan.bold(value)}` +
(reason != null ? ` (${reason})` : ''),
)
}
}
// tsconfig will have the merged "include" and "exclude" by this point
if (parsedTsConfig.exclude == null) {
appTsConfig.exclude = ['node_modules']
}
if (parsedTsConfig.include == null) {
appTsConfig.include = ['**/*.ts', '**/*.tsx']
}
if (messages.length > 0) {
if (firstTimeSetup) {
console.info(
chalk.bold(
'Your',
chalk.cyan('tsconfig.json'),
'has been populated with default values.',
),
)
console.info()
} else {
console.warn(
chalk.bold(
'The following changes are being made to your',
chalk.cyan('tsconfig.json'),
'file:',
),
)
messages.forEach((message) => {
console.warn(' - ' + message)
})
console.warn()
}
await writeJson(tsConfigPath, appTsConfig)
}
return
}
......@@ -7,6 +7,7 @@ import ErrorDebug from './error-debug'
import AmpHtmlValidator from 'amphtml-validator'
import { ampValidation } from '../build/output/index'
import * as Log from '../build/output/log'
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
const React = require('react')
......@@ -73,6 +74,8 @@ export default class DevServer extends Server {
}
async prepare () {
await verifyTypeScriptSetup(this.dir)
this.hotReloader = new HotReloader(this.dir, { config: this.nextConfig, buildId: this.buildId })
await super.prepare()
await this.addExportPathMapRoutes()
......
import React from 'react'
if(typeof window !== 'undefined' && !window['HMR_RANDOM_NUMBER']) {
window['HMR_RANDOM_NUMBER'] = Math.random()
if (typeof window !== 'undefined' && !(window as any).HMR_RANDOM_NUMBER) {
(window as any).HMR_RANDOM_NUMBER = Math.random()
}
export default class Counter extends React.Component {
state = { count: 0 }
incr () {
incr() {
const { count } = this.state
this.setState({ count: count + 1 })
}
render () {
render() {
return (
<div>
<p>COUNT: {this.state.count}</p>
......
const blah: boolean = false
export default () => (
<h3>Hello TypeScript</h3>
)
/* eslint-env jest */
/* global jasmine */
import path from 'path'
import {
exists,
remove,
readFile,
readJSON,
writeJSON
} from 'fs-extra'
import {
launchApp,
findPort,
killApp,
renderViaHTTP
} from 'next-test-utils'
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
describe('Fork ts checker plugin', () => {
const appDir = path.join(__dirname, '../')
const tsConfig = path.join(appDir, 'tsconfig.json')
let appPort
let app
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
await remove(tsConfig)
})
it('Creates a default tsconfig.json when one is missing', async () => {
expect(await exists(tsConfig)).toBe(true)
const tsConfigContent = await readFile(tsConfig)
let parsedTsConfig
expect(() => {
parsedTsConfig = JSON.parse(tsConfigContent)
}).not.toThrow()
expect(parsedTsConfig.exclude[0]).toBe('node_modules')
})
it('Updates an existing tsconfig.json with required value', async () => {
await killApp(app)
let parsedTsConfig = await readJSON(tsConfig)
parsedTsConfig.compilerOptions.esModuleInterop = false
await writeJSON(tsConfig, parsedTsConfig)
appPort = await findPort()
app = await launchApp(appDir, appPort)
parsedTsConfig = await readJSON(tsConfig)
expect(parsedTsConfig.compilerOptions.esModuleInterop).toBe(true)
})
it('Renders a TypeScript page correctly', async () => {
const html = await renderViaHTTP(appPort, '/')
expect(html).toMatch(/Hello TypeScript/)
})
})
......@@ -6,7 +6,7 @@ import path from 'path'
import getPort from 'get-port'
import spawn from 'cross-spawn'
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import fkill from 'fkill'
import treeKill from 'tree-kill'
// `next` here is the symlink in `test/node_modules/next` which points to the root directory.
// This is done so that requiring from `next` works.
......@@ -162,7 +162,12 @@ export function nextExport (dir, { outdir }) {
// Kill a launched app
export async function killApp (instance) {
await fkill(instance.pid, { force: true })
await new Promise((resolve, reject) => {
treeKill(instance.pid, (err) => {
if (err) return reject(err)
resolve()
})
})
}
export async function startApp (app) {
......
......@@ -2035,14 +2035,6 @@ agentkeepalive@^3.4.1:
dependencies:
humanize-ms "^1.2.1"
aggregate-error@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-1.0.0.tgz#888344dad0220a72e3af50906117f48771925fac"
integrity sha1-iINE2tAiCnLjr1CQYRf0h3GSX6w=
dependencies:
clean-stack "^1.0.0"
indent-string "^3.0.0"
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
......@@ -3414,11 +3406,6 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
clean-stack@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31"
integrity sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=
cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
......@@ -3926,14 +3913,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-spawn-async@^2.1.1:
version "2.2.5"
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc"
integrity sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=
dependencies:
lru-cache "^4.0.0"
which "^1.2.8"
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
......@@ -5049,15 +5028,6 @@ exec-sh@^0.2.0:
dependencies:
merge "^1.2.0"
execa@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.1.1.tgz#b09c2a9309bc0ef0501479472db3180f8d4c3edd"
integrity sha1-sJwqkwm8DvBQFHlHLbMYD41MPt0=
dependencies:
cross-spawn-async "^2.1.1"
object-assign "^4.0.1"
strip-eof "^1.0.0"
execa@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
......@@ -5529,16 +5499,6 @@ find-up@^1.0.0:
path-exists "^2.0.0"
pinkie-promise "^2.0.0"
fkill@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/fkill/-/fkill-5.1.0.tgz#d07c20a6bee698b02ae727fdcad61fbcdec198b8"
integrity sha512-Zl4rPQPwG89E9Xd9nV2Mc7RXyVe8RbJGMcFBvLoYvjEC0pXdrY6tgLshD+vJq6oMtB65d81ZTHxj5m/K76mxlw==
dependencies:
aggregate-error "^1.0.0"
arrify "^1.0.0"
execa "^0.8.0"
taskkill "^2.0.0"
flat-cache@^1.2.1:
version "1.3.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f"
......@@ -8185,7 +8145,7 @@ lowercase-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
......@@ -12125,14 +12085,6 @@ tar@^4, tar@^4.4.8:
safe-buffer "^5.1.2"
yallist "^3.0.2"
taskkill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/taskkill/-/taskkill-2.0.0.tgz#a354305702a964357033027aa949eaed5331b784"
integrity sha1-o1QwVwKpZDVwMwJ6qUnq7VMxt4Q=
dependencies:
arrify "^1.0.0"
execa "^0.1.1"
taskr@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/taskr/-/taskr-1.1.0.tgz#4f29d0ace26f4deae9a478eabf9aa0432e884438"
......@@ -12385,6 +12337,11 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
tree-kill@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a"
integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==
trim-lines@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.1.tgz#da738ff58fa74817588455e30b11b85289f2a396"
......@@ -13115,7 +13072,7 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@1, which@^1.2.10, which@^1.2.12, which@^1.2.8, which@^1.2.9, which@^1.3.0, which@^1.3.1:
which@1, which@^1.2.10, which@^1.2.12, which@^1.2.9, which@^1.3.0, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册