@mccann-hub/create-typescript-template
Version:
CLI tool to initialize a TypeScript project with dual CommonJS and ESM support, path aliases, unit testing, and linting for NPM publishing.
481 lines (435 loc) • 12 kB
JavaScript
#!/usr/bin/env node
const fs = require('fs');
const { join } = require('path');
const { promisify } = require('util');
const childProcess = require('child_process');
const askQuestion = require('./ask-question.js');
const mkdir = promisify(fs.mkdir);
const exec = promisify(childProcess.exec);
const read = promisify(fs.readFile);
const write = promisify(fs.writeFile);
const cp = promisify(fs.cp);
/*
* calculate project directory name
*/
const defaultFolderName = 'new-typescript-package';
const initWorkingDirectory = process.cwd();
let folderName = defaultFolderName;
if (process.argv.slice(2).length > 0) {
folderName = process.argv.slice(2)[0];
}
const projectWorkingDirectory = join(initWorkingDirectory, folderName);
/* END */
async function main() {
/*
* make new directory and move into it
*/
console.log(`creating directory ${folderName}`);
await mkdir(projectWorkingDirectory);
process.chdir(projectWorkingDirectory);
/* END */
/*
* initialize npm in new project directory
*/
console.log('npm init');
await exec('npm init --yes');
await exec('npm pkg set version=0.0.0');
/* END */
/*
* install TypeScript
*/
console.log('installing TypeScript (this may take a while)');
await exec('npm install --save-dev typescript @types/node');
console.log('initializing typescript');
await exec('npx tsc --init');
console.log('updating tsconfig');
let tsconfig = await read(
join(projectWorkingDirectory, 'tsconfig.json'),
'utf8'
);
configUpdates = [
{
key: 'declaration',
value: true,
},
{
key: 'declarationMap',
value: true,
},
{
key: 'sourceMap',
value: true,
},
{
key: 'module',
value: '"NodeNext"',
},
{
key: 'target',
value: '"ES2017"',
},
{
key: 'moduleResolution',
value: '"Node16"',
},
{
key: 'esModuleInterop',
value: true,
},
{
key: 'skipLibCheck',
value: true,
},
{
key: 'resolveJsonModule',
value: true,
},
{
key: 'outDir',
value: '"./dist"',
},
{
key: 'rootDir',
value: '"./src"',
},
{
key: 'baseUrl',
value: '"."',
},
{
key: 'paths',
value: JSON.stringify({
'@/*': ['src/*'],
'@utils/*': ['src/utils/*'],
}),
},
{
key: 'removeComments',
value: true,
},
];
const valueReg = '(("[^"]+")|(true|false)|(\\[[^\\]]*\\])|({[^}]*}))';
configUpdates.forEach((update) => {
const reg = new RegExp(
`(\/\/)?\\s*"(${update.key})"\\s*:\\s*${valueReg}(,?\\s*\/\\*)`,
'gm'
);
tsconfig = tsconfig.replace(
reg,
(match, p1, p2, p3, p4, p5, p6, p7, p8) => {
return `${p1 ? '' : '\n '}"${p2}": ${update.value}${p8 || ''}`;
}
);
});
tsconfig = tsconfig.replace(
/}\r?\n}/,
`},
"include": ["src/**/*"],
"exclude": [
"node_module",
"dist",
"tests"
]
}`
);
await write(join(projectWorkingDirectory, 'tsconfig.json'), tsconfig);
console.log('writing tsconfig for CommonJS');
await write(
join(projectWorkingDirectory, 'tsconfig.commonjs.json'),
JSON.stringify(
{
extends: './tsconfig.json',
compilerOptions: {
module: 'CommonJS',
moduleResolution: 'Node10',
outDir: './dist/cjs',
declarationDir: './dist/cjs',
target: 'ES2015',
},
include: ['src/**/*'],
},
null,
2
)
);
console.log('writing tsconfig for ESM');
await write(
join(projectWorkingDirectory, 'tsconfig.esm.json'),
JSON.stringify(
{
extends: './tsconfig.json',
compilerOptions: {
outDir: './dist/esm',
declarationDir: './dist/esm',
target: 'ES2020',
},
include: ['src/**/*'],
},
null,
2
)
);
if (fs.existsSync(join(__dirname, '..', 'src'))) {
console.log('copying src directory');
await cp(
join(__dirname, '..', 'src'),
join(projectWorkingDirectory, 'src'),
{
recursive: true,
}
);
}
console.log('updating main in package.json');
await exec('npm pkg set main=./dist/cjs/index.js');
console.log('adding types to package.json');
await exec('npm pkg set types=./dist/cjs/index.d.ts');
console.log('adding module to package.json');
await exec('npm pkg set module=./dist/esm/index.js');
console.log('adding exports in package.json');
await exec('npm pkg set exports["."].import=./dist/esm/index.js');
await exec('npm pkg set exports["."].require=./dist/cjs/index.js');
console.log('adding files in package.json');
await exec('npm pkg set files[0]=dist/**/*');
await exec('npm pkg set files[1]=README.md');
console.log('adding build script(s)');
await exec(
'npm pkg set scripts.build:cjs="tsc --project tsconfig.commonjs.json"'
);
await exec('npm pkg set scripts.build:esm="tsc --project tsconfig.esm.json"');
await exec(
'npm pkg set scripts.build="npm run build:cjs && npm run build:esm"'
);
console.log('adding prepublishOnly script');
await exec('npm pkg set scripts.prepublishOnly="npm run build"');
console.log('adding clean script');
await exec(
`npm pkg set scripts.clean="node -e \\"require('fs').rmSync('./dist', { recursive: true, force: true })\\""`
);
console.log('adding prebuild script');
await exec('npm pkg set scripts.prebuild="npm run clean"');
/* END */
/*
* install tsc-alias
*/
console.log('installing tsc-alias (this may take a while)');
await exec('npm install --save-dev tsc-alias');
console.log('updating build script(s)');
await exec(
'npm pkg set scripts.build:cjs="tsc --project tsconfig.commonjs.json && tsc-alias -p tsconfig.commonjs.json"'
);
await exec(
'npm pkg set scripts.build:esm="tsc --project tsconfig.esm.json && tsc-alias -p tsconfig.esm.json"'
);
/* END */
/*
* install ESLint
* TSLint has been deprecated in favor of ESLint
*/
console.log('installing ESLint (this may take a while)');
await exec('npm install --save-dev eslint@^8.56.0');
console.log('installing typescript-eslint (this may take a while)');
await exec(
'npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin'
);
console.log('writing eslintrc');
await write(
join(projectWorkingDirectory, '.eslintrc'),
JSON.stringify(
{
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'no-console': 0, // off
'no-shadow': 1, // warn
},
},
null,
2
)
);
console.log('writing eslintignore');
await write(
join(projectWorkingDirectory, '.eslintignore'),
['node_modules', 'dist', ''].join('\n')
);
console.log('adding lint script');
await exec('npm pkg set scripts.lint="eslint . --ext .ts,.js --fix"');
/* END */
/*
* install Mocha
*/
console.log('installing Mocha (this may take a while)');
await exec('npm install --save-dev mocha @types/mocha');
console.log('installing tsx (this may take a while)');
await exec('npm install --save-dev tsx tsconfig-paths');
console.log('installing cross-env (this may take a while)');
await exec('npm install --save-dev cross-env');
console.log('writing mocharc');
await write(
join(projectWorkingDirectory, '.mocharc.json'),
JSON.stringify(
{
extension: ['ts'],
spec: 'tests/**/*.spec.ts',
require: ['tsx', 'tsconfig-paths/register'],
recursive: true,
},
null,
2
)
);
console.log('writing test tsconfig');
await write(
join(projectWorkingDirectory, 'tsconfig.test.json'),
JSON.stringify(
{
extends: './tsconfig.json',
compilerOptions: {
module: 'CommonJS',
target: 'ES2020',
outDir: './dist/test',
rootDir: './',
noEmit: false,
types: ['node', 'mocha'],
sourceMap: true,
baseUrl: '.',
paths: {
'@/*': ['src/*'],
'@utils/*': ['src/utils/*'],
},
esModuleInterop: true,
},
include: ['src/**/*.ts', 'tests/**/*.spec.ts'],
exclude: ['node_modules', 'dist'],
},
null,
2
)
);
if (fs.existsSync(join(__dirname, '..', 'tests'))) {
console.log('copying tests directory');
await cp(
join(__dirname, '..', 'tests'),
join(projectWorkingDirectory, 'tests'),
{
recursive: true,
}
);
}
console.log('adding test script');
await exec(
`npm pkg set scripts.test="cross-env TSX_TSCONFIG_PATH='./tsconfig.test.json' mocha"`
);
/* END */
/*
* deno
*/
try {
await exec('deno --version');
await write(
join(projectWorkingDirectory, 'deno.json'),
JSON.stringify(
{
name: folderName,
version: '0.0.0',
exports: './src/index.ts',
tasks: {
dev: 'deno test --watch ./src/index.ts',
},
license: 'MIT',
imports: {
'@/': './src/',
'@utils/': './src/utils/',
'@std/assert': 'jsr:@std/assert@1',
},
lint: {
include: ['./src/'],
exclude: ['./tests/**/*.spec.ts'],
rules: {
tags: ['recommended'],
},
},
fmt: {
useTabs: false,
lineWidth: 80,
indentWidth: 2,
semiColons: true,
singleQuote: true,
proseWrap: 'preserve',
include: ['./src/'],
exclude: ['./tests/**/*.spec.ts'],
},
nodeModulesDir: 'auto',
exclude: ['./dist/'],
publish: {
include: ['./src/', 'README.md', 'deno.json'],
},
},
null,
2
)
);
} catch (error) {
if (error.message.includes('not found')) {
console.log('error finding deno, skipping deno setup');
} else {
console.log(`error: ${error.message}`);
}
}
/* END */
/*
* initialize and configure git
* ALWAYS goes last
*/
const usingGit = !!(
await askQuestion('Are you using git (Y/n)? ', 'y', (a) =>
a.trim().match(/^(y|n|yes|no)$/i) ? true : 'Please enter y or n'
)
)
.trim()
.match(/^(y|yes)$/i);
if (usingGit) {
const gitUrl = await askQuestion('What is the URL for your Git repo? ');
console.log('setting package repository');
await exec('npm pkg set repository.type=git');
await exec(`npm pkg set repository.url=git+${gitUrl}.git`);
console.log('setting package bugs');
await exec(`npm pkg set bugs.url=${gitUrl}/issues`);
console.log('setting package homepage');
await exec(`npm pkg set homepage=${gitUrl}#readme`);
console.log('adding node gitignore');
await exec('npx gitignore node');
if (fs.existsSync(join(__dirname, 'github'))) {
console.log('copying github directory');
await cp(
join(__dirname, 'github'),
join(projectWorkingDirectory, '.github'),
{
recursive: true,
}
);
}
console.log('git init');
await exec('git init');
console.log('initial commit');
await exec('git add --all');
await exec('git commit -m "initial commit"');
console.log('adding git remote origin');
await exec(`git remote add origin ${gitUrl}.git`);
}
/* END */
}
main()
.catch((err) => {
console.error('Error: ', err);
process.exit(1);
})
.then(() => {
console.log('Done');
});