UNPKG

create-pro-ts-lib

Version:

A command-line-interface for building Typescript libraries

231 lines (199 loc) 5.85 kB
const _ = require('lodash'); const path = require('path'); const { readdir, readFile, writeFile, mkdir, lstat, chmod, } = require('fs/promises'); const { existsSync } = require('fs'); const ErrorWithCode = require('./ErrorWithCode'); const parseFile = (filePath, strContent) => { let content = strContent; if (path.extname(filePath) === '.json') { content = JSON.parse(content); } return content; }; const stringifyFile = (filePath, content) => { let strContent = content; if (path.extname(filePath) === '.json') { strContent = JSON.stringify(content, null, 4); } return strContent; }; const readdirFiles = async dirPath => { if (!existsSync(dirPath)) return []; const filesNames = await readdir(dirPath); const statuses = await Promise.all( filesNames.map(name => lstat(path.join(dirPath, name))) ); return filesNames.filter((name, i) => statuses[i].isFile()); }; const recursiveReadDir = async dirPath => { if (!existsSync(dirPath)) return []; const entities = await readdir(dirPath); const stats = await Promise.all( entities.map(entity => lstat(path.join(dirPath, entity))) ); const files = []; await Promise.all( _.zip(entities, stats).map(async ([entity, stat]) => { if (stat.isDirectory()) { const newPath = path.join(dirPath, entity); const dirFiles = await recursiveReadDir(newPath); const relativeDirFiles = dirFiles.map(file => path.join(entity, file) ); files.push(...relativeDirFiles); return; } files.push(entity); }) ); return files; }; const readTemplateFiles = async (templatePath, config = {}) => { const filesNames = await recursiveReadDir(templatePath); const filesPaths = filesNames.map(name => path.join(templatePath, name)); const files = await Promise.all( _.zip(filesNames, filesPaths).map(async ([name, filePath]) => { const contentBuffer = await readFile(filePath); const bufferStr = contentBuffer.toString('utf8'); const content = parseFile(name, bufferStr); return { name, path: filePath, content, }; }) ); return files.map(item => ({ ...item, name: config?.files?.rename?.[item.name] ?? item.name, })); }; const sortJson = (deepObject, enableSortArrays = true) => { if (!deepObject) return {}; return Object.entries(deepObject) .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) .reduce((acc, [key, value]) => { let newValue = value; const isObject = typeof value === 'object'; const isArray = Array.isArray(value); if (isObject && !isArray) { newValue = sortJson(value, enableSortArrays); } else if (isArray && enableSortArrays) { newValue = value.sort((objA, objB) => JSON.stringify(objA).localeCompare(JSON.stringify(objB)) ); } return { ...acc, [key]: newValue }; }, {}); }; const sortPackageJsonObj = packageJson => { const sortedPackageJson = [ '$schema', 'name', 'version', 'type', 'description', 'author', 'licence', 'private', 'main', 'exports', 'types', 'scripts', 'repository', 'keywords', 'bugs', 'homepage', 'engines', 'packageManager', 'dependencies', 'peerDependencies', 'devDependencies', ].reduce((obj, key) => Object.assign(obj, { [key]: packageJson[key] }), {}); //sort json of specific keys ['devDependencies', 'dependencies', 'scripts'].forEach(key => { sortedPackageJson[key] = sortJson(packageJson[key]); }); return sortedPackageJson; }; const DIR_EXISTS_ERROR = 409; const DIR_NOT_EMPTY_ERROR = '0X80070091'; const DEFAULT_OPTIONS = { forceWrite: false }; const createFiles = async (filesManager, { forceWrite } = DEFAULT_OPTIONS) => { const filesEntries = Object.entries(filesManager.files); const abortion = new AbortController(); const signal = abortion.signal; process.once('SIGINT', () => { abortion.abort(); }); const isRootDir = !path.relative(process.cwd(), filesManager.path); const isDirExists = !isRootDir && existsSync(filesManager.path); const dirFiles = await readdirFiles(filesManager.path); const isDirEmptyOfFiles = dirFiles.length === 0; if (isDirExists && !forceWrite) { throw new ErrorWithCode( `EEXIST: dir '${path.dirname(filesManager.path)}' already exists`, DIR_EXISTS_ERROR ); } if (!isDirEmptyOfFiles && !forceWrite) { throw new ErrorWithCode( `ENOTEMPTY: dir '${path.dirname(filesManager.path)}' is not empty`, DIR_NOT_EMPTY_ERROR ); } try { if (!isDirExists) { await mkdir(filesManager.path, { recursive: true }); } await Promise.all( filesEntries.map(async ([filePath, content]) => { const strContent = stringifyFile(filePath, content); const dirPath = path.dirname(filePath); if (!existsSync(dirPath)) { await mkdir(dirPath, { recursive: true }); } await writeFile(filePath, strContent, { signal }); if (strContent.includes('#!/usr/bin/env sh')) { await chmod(filePath, 0o755); } }) ); } catch (e) { abortion.abort(); if (e instanceof ErrorWithCode) { throw e; } } }; const postProcessFiles = filesManager => { const packageJson = sortPackageJsonObj(filesManager.get('package.json')); const tsconfig = filesManager.get('tsconfig.json'); filesManager.add('package.json', packageJson, true); filesManager.add('tsconfig.json', sortJson(tsconfig), true); }; const createTemplateFilesDownloader = dirPath => async (filesManager, config) => { const fullPath = path.join(__dirname, '../', dirPath); const files = await readTemplateFiles(fullPath, config); files.forEach(({ name, content }) => { const sanitizedName = name.endsWith('.txt') ? name.substr(0, name.lastIndexOf('.')) : name; filesManager.add(sanitizedName, content); }); }; module.exports = { readTemplateFiles, postProcessFiles, createFiles, createTemplateFilesDownloader, ERRORS: { DIR_EXISTS_ERROR, DIR_NOT_EMPTY_ERROR }, };