Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TS): auto-generate TypeScript definitions and add typecheck script #176

Merged
merged 3 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"eslint.js",
"husky.js",
"jest.js",
"prettier.js"
"prettier.js",
"shared-tsconfig.json"
],
"keywords": [],
"author": "Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)",
Expand Down
21 changes: 21 additions & 0 deletions shared-tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I welcome feedback on this shared config. I think most folks should be able to simply do:

{
  "extends": "./node_modules/kcd-scripts/shared-tsconfig.json",
  "include": ["src/*"]
}

But I may be mistaken.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding "forceConsistentCasingInFileNames": true which

Disallow inconsistently-cased references to the same file.

And assumeChangesOnlyAffectDirectDependencies: true which

When this option is enabled, TypeScript will avoid rechecking/rebuilding all truly possibly-affected files, and only recheck/rebuild files that have changed as well as files that directly import them.

This can be considered a ‘fast & loose’ implementation of the watching algorithm, which can drastically reduce incremental rebuild times at the expense of having to run the full build occasionally to get all compiler error messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the include can even be left out in project-specific ts-config if we include it in shared-tsconfig, no? 🤔

People can then override include if they want to in their project-specific ts-config if they want.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @eddyw,

at the expense of having to run the full build occasionally to get all compiler error messages.

Seems like it would be confusing if we enable this and someone runs typecheck once and gets some error messages, and then they run it again and they don't 🤔

I'm not sure I understand forceConsistentCasingInFileNames.

Thank you @MichaelDeBoey,

I was worried that include would be relative to the shared config, not the extending config. I'll do a little test to be sure.

Copy link
Owner Author

@kentcdodds kentcdodds Nov 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, my suspicions were confirmed:

image

I suppose I could try ../../src/**/*... That actually should work fine... I think I'll do that.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to enable forceConsistentCasingInFileNames but not assumeChangesOnlyAffectDirectDependencies. We can alter that in the future if I learn to trust it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand forceConsistentCasingInFileNames.

It's like the most valuable option in tsconfig 😆 (joking)
It warns the developer if there is inconsistent casing in the filenames when importing or require

For instance, if you have thisExample.ts but you do import {} from './ThisExample.ts, it'll warn because the casing of the filenames is inconsistent. While this code will likely run in Windows, it'll surely break in Linux / MacOS.

I was worried that include would be relative to the shared config, not the extending config. I'll do a little test to be sure.

The default value for include is **/* if files option isn't specified. Maybe it's a sane default and you could leave it as is?

Also, I'd suggest removing exclude since the default value is actually better:

Default: ["node_modules", "bower_components", "jspm_packages"], plus the value of outDir if one is specified.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for that! I only want to include the src files because those are the ones that I want to generate type defs for. People can override if they need.

I'm pushing a fix to remove exclude. Thanks!

"exclude": ["node_modules"],
"include": ["../../src/**/*"],
"compilerOptions": {
"isolatedModules": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noEmit": true,
"strict": true,
"jsx": "react",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "../../src",
"paths": {
"*": ["*", "../tests/*"]
},
"preserveWatchOutput": true,
"incremental": true,
"tsBuildInfoFile": "../.cache/kcd-scripts/.tsbuildinfo"
}
}
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Available Scripts:
lint
pre-commit
test
typecheck
validate

Options:
Expand Down
10 changes: 9 additions & 1 deletion src/run-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 45 additions & 21 deletions src/scripts/build/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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())
76 changes: 55 additions & 21 deletions src/scripts/build/rollup.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -9,6 +10,8 @@ const {
fromRoot,
getConcurrentlyArgs,
writeExtraEntry,
hasTypescript,
generateTypeDefs,
} = require('../../utils')

const crossEnv = resolveBin('cross-env')
Expand Down Expand Up @@ -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')

Expand All @@ -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) {
Expand Down Expand Up @@ -111,4 +145,4 @@ function getCommands({preact = false} = {}) {
}, {})
}

process.exit(result.status)
process.exit(go())
36 changes: 13 additions & 23 deletions src/scripts/pre-commit.js
Original file line number Diff line number Diff line change
@@ -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(), '.')
Expand All @@ -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())
24 changes: 24 additions & 0 deletions src/scripts/typecheck.js
Original file line number Diff line number Diff line change
@@ -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)
Loading