From 5a85c2a52e0f81fd9d8a382f84b86e1e51d5eff3 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 25 Nov 2020 22:48:59 -0700 Subject: [PATCH 1/3] feat(TS): auto-generate TypeScript definitions and add typecheck script --- README.md | 19 +++--- package.json | 3 +- shared-tsconfig.json | 11 ++++ src/__tests__/__snapshots__/index.js.snap | 1 + src/run-script.js | 10 ++- src/scripts/build/babel.js | 66 +++++++++++++------- src/scripts/build/rollup.js | 76 ++++++++++++++++------- src/scripts/pre-commit.js | 36 ++++------- src/scripts/typecheck.js | 24 +++++++ src/utils.js | 17 +++++ 10 files changed, 184 insertions(+), 79 deletions(-) create mode 100644 shared-tsconfig.json create mode 100644 src/scripts/typecheck.js diff --git a/README.md b/README.md index d8556275..fce7b64d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ for linting, testing, building, and more. - [Installation](#installation) - [Usage](#usage) - [Overriding Config](#overriding-config) - - [Flow support](#flow-support) - [TypeScript Support](#typescript-support) - [Inspiration](#inspiration) - [Other Solutions](#other-solutions) @@ -113,27 +112,23 @@ module.exports = Object.assign(jestConfig, { > configuring things to make it less magical and more straightforward. Extending > can take place on your terms. I think this is actually a great way to do this. -### Flow support - -If the `flow-bin` is a dependency on the project the `@babel/preset-flow` will -automatically get loaded when you use the default babel config that comes with -`kcd-scripts`. If you customised your `.babelrc`-file you might need to manually -add `@babel/preset-flow` to the `presets`-section. - ### TypeScript Support If the `tsconfig.json`-file is present in the project root directory and `typescript` is a dependency the `@babel/preset-typescript` will automatically get loaded when you use the default babel config that comes with `kcd-scripts`. -If you customised your `.babelrc`-file you might need to manually add +If you customized your `.babelrc`-file you might need to manually add `@babel/preset-typescript` to the `presets`-section. `kcd-scripts` will automatically load any `.ts` and `.tsx` files, including the default entry point, so you don't have to worry about any rollup configuration. -`tsc --build tsconfig.json` will run during before committing to verify that -files will compile. So make sure to add the `noEmit` flag to the -`tsconfig.json`'s `compilerOptions`. +If you have a `typecheck` script (normally set to `kcd-scripts typecheck`) that +will be run as part of the `validate` script (which is run as part of the +`pre-commit` script as well). + +TypeScript definition files will also automatically be generated during the +`build` script. ## Inspiration diff --git a/package.json b/package.json index acab0a7d..5d475f43 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "eslint.js", "husky.js", "jest.js", - "prettier.js" + "prettier.js", + "shared-tsconfig.json" ], "keywords": [], "author": "Kent C. Dodds (https://kentcdodds.com)", diff --git a/shared-tsconfig.json b/shared-tsconfig.json new file mode 100644 index 00000000..34c9b7ef --- /dev/null +++ b/shared-tsconfig.json @@ -0,0 +1,11 @@ +{ + "exclude": ["node_modules"], + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "moduleResolution": "node", + "noEmit": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/src/__tests__/__snapshots__/index.js.snap b/src/__tests__/__snapshots__/index.js.snap index 7aaeafb0..343a16e8 100644 --- a/src/__tests__/__snapshots__/index.js.snap +++ b/src/__tests__/__snapshots__/index.js.snap @@ -34,6 +34,7 @@ Available Scripts: lint pre-commit test + typecheck validate Options: diff --git a/src/run-script.js b/src/run-script.js index 49bdf9b5..9cf4de1f 100755 --- a/src/run-script.js +++ b/src/run-script.js @@ -56,7 +56,15 @@ function spawnScript() { // get all the arguments of the script and find the position of our script commands const args = process.argv.slice(2) const scriptIndex = args.findIndex(x => - ['build', 'format', 'lint', 'pre-commit', 'test', 'validate'].includes(x), + [ + 'build', + 'format', + 'lint', + 'pre-commit', + 'test', + 'validate', + 'typecheck', + ].includes(x), ) // Extract the node arguments so we can pass them to node later on diff --git a/src/scripts/build/babel.js b/src/scripts/build/babel.js index 87acc588..0a116c0f 100644 --- a/src/scripts/build/babel.js +++ b/src/scripts/build/babel.js @@ -4,7 +4,14 @@ const spawn = require('cross-spawn') const yargsParser = require('yargs-parser') const rimraf = require('rimraf') const glob = require('glob') -const {hasPkgProp, fromRoot, resolveBin, hasFile} = require('../../utils') +const { + hasPkgProp, + fromRoot, + resolveBin, + hasFile, + hasTypescript, + generateTypeDefs, +} = require('../../utils') let args = process.argv.slice(2) const here = p => path.join(__dirname, p) @@ -42,25 +49,42 @@ if (!useSpecifiedOutDir && !args.includes('--no-clean')) { args = args.filter(a => a !== '--no-clean') } -const result = spawn.sync( - resolveBin('@babel/cli', {executable: 'babel'}), - [...outDir, ...copyFiles, ...ignore, ...extensions, ...config, 'src'].concat( - args, - ), - {stdio: 'inherit'}, -) +function go() { + let result = spawn.sync( + resolveBin('@babel/cli', {executable: 'babel'}), + [ + ...outDir, + ...copyFiles, + ...ignore, + ...extensions, + ...config, + 'src', + ].concat(args), + {stdio: 'inherit'}, + ) + if (result.status !== 0) return result.status -// because babel will copy even ignored files, we need to remove the ignored files -const pathToOutDir = fromRoot(parsedArgs.outDir || builtInOutDir) -const ignoredPatterns = (parsedArgs.ignore || builtInIgnore) - .split(',') - .map(pattern => path.join(pathToOutDir, pattern)) -const ignoredFiles = ignoredPatterns.reduce( - (all, pattern) => [...all, ...glob.sync(pattern)], - [], -) -ignoredFiles.forEach(ignoredFile => { - rimraf.sync(ignoredFile) -}) + if (hasTypescript && !args.includes('--no-ts-defs')) { + console.log('Generating TypeScript definitions') + result = generateTypeDefs() + console.log('TypeScript definitions generated') + if (result.status !== 0) return result.status + } -process.exit(result.status) + // because babel will copy even ignored files, we need to remove the ignored files + const pathToOutDir = fromRoot(parsedArgs.outDir || builtInOutDir) + const ignoredPatterns = (parsedArgs.ignore || builtInIgnore) + .split(',') + .map(pattern => path.join(pathToOutDir, pattern)) + const ignoredFiles = ignoredPatterns.reduce( + (all, pattern) => [...all, ...glob.sync(pattern)], + [], + ) + ignoredFiles.forEach(ignoredFile => { + rimraf.sync(ignoredFile) + }) + + return result.status +} + +process.exit(go()) diff --git a/src/scripts/build/rollup.js b/src/scripts/build/rollup.js index 62269b34..04a9ea86 100644 --- a/src/scripts/build/rollup.js +++ b/src/scripts/build/rollup.js @@ -1,4 +1,5 @@ const path = require('path') +const fs = require('fs') const spawn = require('cross-spawn') const glob = require('glob') const rimraf = require('rimraf') @@ -9,6 +10,8 @@ const { fromRoot, getConcurrentlyArgs, writeExtraEntry, + hasTypescript, + generateTypeDefs, } = require('../../utils') const crossEnv = resolveBin('cross-env') @@ -46,9 +49,9 @@ const getCommand = (env, ...flags) => .join(' ') const buildPreact = args.includes('--p-react') -const scripts = buildPreact - ? getPReactScripts() - : getConcurrentlyArgs(getCommands()) +const scripts = getConcurrentlyArgs( + buildPreact ? getPReactCommands() : getCommands(), +) const cleanBuildDirs = !args.includes('--no-clean') @@ -60,25 +63,56 @@ if (cleanBuildDirs) { } } -const result = spawn.sync(resolveBin('concurrently'), scripts, { - stdio: 'inherit', -}) - -if (result.status === 0 && buildPreact && !args.includes('--no-package-json')) { - writeExtraEntry( - 'preact', - { - cjs: glob.sync(fromRoot('preact/**/*.cjs.js'))[0], - esm: glob.sync(fromRoot('preact/**/*.esm.js'))[0], - }, - false, - ) +function go() { + let result = spawn.sync(resolveBin('concurrently'), scripts, { + stdio: 'inherit', + }) + + if (result.status !== 0) return result.status + + if (buildPreact && !args.includes('--no-package-json')) { + writeExtraEntry( + 'preact', + { + cjs: glob.sync(fromRoot('preact/**/*.cjs.js'))[0], + esm: glob.sync(fromRoot('preact/**/*.esm.js'))[0], + }, + false, + ) + } + + if (hasTypescript && !args.includes('--no-ts-defs')) { + console.log('Generating TypeScript definitions') + result = generateTypeDefs() + if (result.status !== 0) return result.status + + for (const format of formats) { + const [formatFile] = glob.sync(fromRoot(`dist/*.${format}.js`)) + const {name} = path.parse(formatFile) + // make a .d.ts file for every generated file that re-exports index.d.ts + fs.writeFileSync(fromRoot('dist', `${name}.d.ts`), 'export * from ".";\n') + } + + // because typescript generates type defs for ignored files, we need to + // remove the ignored files + const ignoredFiles = [ + ...glob.sync(fromRoot('dist', '**/__tests__/**')), + ...glob.sync(fromRoot('dist', '**/__mocks__/**')), + ] + ignoredFiles.forEach(ignoredFile => { + rimraf.sync(ignoredFile) + }) + console.log('TypeScript definitions generated') + } + + return result.status } -function getPReactScripts() { - const reactCommands = prefixKeys('react.', getCommands()) - const preactCommands = prefixKeys('preact.', getCommands({preact: true})) - return getConcurrentlyArgs(Object.assign(reactCommands, preactCommands)) +function getPReactCommands() { + return { + ...prefixKeys('react.', getCommands()), + ...prefixKeys('preact.', getCommands({preact: true})), + } } function prefixKeys(prefix, object) { @@ -111,4 +145,4 @@ function getCommands({preact = false} = {}) { }, {}) } -process.exit(result.status) +process.exit(go()) diff --git a/src/scripts/pre-commit.js b/src/scripts/pre-commit.js index e75ae67a..2c1fdc48 100644 --- a/src/scripts/pre-commit.js +++ b/src/scripts/pre-commit.js @@ -1,6 +1,6 @@ const path = require('path') const spawn = require('cross-spawn') -const {hasPkgProp, hasFile, resolveBin, hasTypescript} = require('../utils') +const {hasPkgProp, hasFile, resolveBin} = require('../utils') const here = p => path.join(__dirname, p) const hereRelative = p => here(p).replace(process.cwd(), '.') @@ -17,30 +17,20 @@ const config = useBuiltInConfig ? ['--config', hereRelative('../config/lintstagedrc.js')] : [] -const lintStagedResult = spawn.sync( - resolveBin('lint-staged'), - [...config, ...args], - {stdio: 'inherit'}, -) +function go() { + let result -if (lintStagedResult.status !== 0) { - process.exit(lintStagedResult.status) -} + result = spawn.sync(resolveBin('lint-staged'), [...config, ...args], { + stdio: 'inherit', + }) -if (hasTypescript) { - const tscResult = spawn.sync( - resolveBin('typescript', {executable: 'tsc'}), - ['--build', 'tsconfig.json'], - {stdio: 'inherit'}, - ) + if (result.status !== 0) return result.status - if (tscResult.status !== 0) { - process.exit(tscResult.status) - } -} + result = spawn.sync('npm', ['run', 'validate'], { + stdio: 'inherit', + }) -const validateResult = spawn.sync('npm', ['run', 'validate'], { - stdio: 'inherit', -}) + return result.status +} -process.exit(validateResult.status) +process.exit(go()) diff --git a/src/scripts/typecheck.js b/src/scripts/typecheck.js new file mode 100644 index 00000000..d31c2781 --- /dev/null +++ b/src/scripts/typecheck.js @@ -0,0 +1,24 @@ +const spawn = require('cross-spawn') +const {hasAnyDep, resolveBin, hasFile} = require('../utils') + +const args = process.argv.slice(2) + +if (!hasAnyDep('typescript')) { + throw new Error( + 'Cannot use the "typecheck" script in a project that does not have typescript listed as a dependency (or devDependency).', + ) +} + +if (!hasFile('tsconfig.json')) { + throw new Error( + 'Cannot use the "typecheck" script in a project that does not have a tsconfig.json file.', + ) +} + +const result = spawn.sync( + resolveBin('typescript', {executable: 'tsc'}), + ['--build', ...args], + {stdio: 'inherit'}, +) + +process.exit(result.status) diff --git a/src/utils.js b/src/utils.js index 0799c0d2..e732db72 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ const fs = require('fs') const path = require('path') +const spawn = require('cross-spawn') const rimraf = require('rimraf') const mkdirp = require('mkdirp') const arrify = require('arrify') @@ -167,6 +168,20 @@ function hasLocalConfig(moduleName, searchOptions = {}) { return result !== null } +function generateTypeDefs() { + return spawn.sync( + resolveBin('typescript', {executable: 'tsc'}), + // prettier-ignore + [ + '--declaration', + '--emitDeclarationOnly', + '--noEmit', 'false', + '--outDir', fromRoot('dist'), + ], + {stdio: 'inherit'}, + ) +} + module.exports = { appDirectory, fromRoot, @@ -175,6 +190,7 @@ module.exports = { hasLocalConfig, hasPkgProp, hasScript, + hasAnyDep, hasDep, ifAnyDep, ifDep, @@ -190,4 +206,5 @@ module.exports = { resolveKcdScripts, uniq, writeExtraEntry, + generateTypeDefs, } From 9616acf30c6a56b74e7c650b3c6939cfb01a898b Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 26 Nov 2020 07:17:58 -0700 Subject: [PATCH 2/3] add include --- shared-tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/shared-tsconfig.json b/shared-tsconfig.json index 34c9b7ef..a9702caf 100644 --- a/shared-tsconfig.json +++ b/shared-tsconfig.json @@ -1,5 +1,6 @@ { "exclude": ["node_modules"], + "include": ["../../src/**/*"], "compilerOptions": { "isolatedModules": true, "esModuleInterop": true, From 22fe9976e2004b8c6ec5335d454463347933164f Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 26 Nov 2020 07:42:02 -0700 Subject: [PATCH 3/3] configure a few more things --- shared-tsconfig.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shared-tsconfig.json b/shared-tsconfig.json index a9702caf..91b5f99a 100644 --- a/shared-tsconfig.json +++ b/shared-tsconfig.json @@ -7,6 +7,15 @@ "moduleResolution": "node", "noEmit": true, "strict": true, - "skipLibCheck": true + "jsx": "react", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "../../src", + "paths": { + "*": ["*", "../tests/*"] + }, + "preserveWatchOutput": true, + "incremental": true, + "tsBuildInfoFile": "../.cache/kcd-scripts/.tsbuildinfo" } }