diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..a2d6240 --- /dev/null +++ b/index.js @@ -0,0 +1,111 @@ +const minimist = require('minimist'); +const prompts = require('prompts'); +const path = require('path'); +const fs = require('fs'); +const { + canSafelyOverwrite, + isValidPackageName, + toValidPackageName, + emptyDir, +} = require('./utils/helpers'); +const { templateChoices } = require('./utils/templateOptions'); + +const defaultProjectName = 'ou-app'; + +const init = async () => { + console.log('init'); + + const cwd = process.cwd(); + + const argv = minimist(process.argv.slice(2), { boolean: true }); + + let targetDir; + let result = {}; + try { + // Prompts: + // - Project name: + // - Package name: + // - Should create new directory: + // - Choose a template: + result = await prompts([ + { + name: 'projectName', + type: 'text', + message: 'Project name:', + initial: defaultProjectName, + onState: (state) => + (targetDir = String(state.value).trim() || defaultProjectName), + }, + { + name: 'packageName', + type: () => (isValidPackageName(targetDir) ? null : 'text'), + message: 'Package name:', + initial: () => toValidPackageName(targetDir), + validate: (dir) => + isValidPackageName(dir) || 'Invalid package.json name', + }, + { + name: 'shouldCreateNewDir', + type: 'toggle', + message: 'Should create new directory:', + initial: false, + active: 'Yes', + inactive: 'No', + }, + { + name: 'shouldOverwrite', + type: (shouldCreateNewDir) => + !shouldCreateNewDir || canSafelyOverwrite(targetDir) + ? null + : 'confirm', + message: () => { + const dirForPrompt = + targetDir === '.' + ? 'Current directory' + : `Target directory "${targetDir}"`; + + return `${dirForPrompt} is not empty. Remove existing files and continue?`; + }, + }, + { + name: 'template', + type: 'select', + choices: templateChoices, + message: 'Choose a template:', + }, + ]); + } catch (cancelled) { + console.log(cancelled.message); + process.exit(1); + } + + const { + projectName, + packageName, + shouldCreateNewDir, + shouldOverwrite = false, + template, + } = result; + + const root = shouldCreateNewDir ? path.join(cwd, projectName) : cwd; + + if (shouldCreateNewDir) { + if (fs.existsSync(root) && shouldOverwrite) { + emptyDir(root); + } else if (!fs.existsSync(root)) { + fs.mkdirSync(root); + } + } + + console.log(`\nScaffolding project in ${root}...`); + + const pkg = { name: packageName, version: '0.0.0' }; + fs.writeFileSync( + path.resolve(root, 'package.json'), + JSON.stringify(pkg, null, 2) + ); +}; + +init().catch((e) => { + console.error(e); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9f8463 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "ou", + "version": "1.0.0", + "description": "a cli for doing something.", + "main": "index.js", + "scripts": { + "dev": "node ./index" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ouduidui/ou.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/ouduidui/ou/issues" + }, + "homepage": "https://github.com/ouduidui/ou#readme", + "dependencies": { + "fs-extra": "^10.0.1", + "minimist": "^1.2.5", + "prompts": "^2.4.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3b183db --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,60 @@ +lockfileVersion: 5.3 + +specifiers: + fs-extra: ^10.0.1 + minimist: ^1.2.5 + prompts: ^2.4.2 + +dependencies: + fs-extra: 10.0.1 + minimist: 1.2.5 + prompts: 2.4.2 + +packages: + + /fs-extra/10.0.1: + resolution: {integrity: sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.9 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + + /graceful-fs/4.2.9: + resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} + dev: false + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.9 + dev: false + + /kleur/3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /minimist/1.2.5: + resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} + dev: false + + /prompts/2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + + /sisteransi/1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: false diff --git a/utils/helpers.js b/utils/helpers.js new file mode 100644 index 0000000..c373208 --- /dev/null +++ b/utils/helpers.js @@ -0,0 +1,64 @@ +const fs = require('fs'); +const path = require('path'); + +const canSafelyOverwrite = (dir) => + !fs.existsSync(dir) || fs.readdirSync(dir).length === 0; + +const isValidPackageName = (projectName) => + /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test( + projectName + ); + +const toValidPackageName = (projectName) => + projectName + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/^[._]/, '') + .replace(/[^a-z0-9-~]+/g, '-'); + +const emptyDir = (dir) => { + if (!fs.existsSync(dir)) { + return; + } + + postOrderDirectoryTraverse( + dir, + (dir) => fs.rmdirSync(dir), + (file) => fs.unlinkSync(file) + ); +}; + +function preOrderDirectoryTraverse(dir, dirCallback, fileCallback) { + for (const filename of fs.readdirSync(dir)) { + const fullpath = path.resolve(dir, filename); + if (fs.lstatSync(fullpath).isDirectory()) { + dirCallback(fullpath); + // in case the dirCallback removes the directory entirely + if (fs.existsSync(fullpath)) { + preOrderDirectoryTraverse(fullpath, dirCallback, fileCallback); + } + continue; + } + fileCallback(fullpath); + } +} + +function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) { + for (const filename of fs.readdirSync(dir)) { + const fullpath = path.resolve(dir, filename); + if (fs.lstatSync(fullpath).isDirectory()) { + postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback); + dirCallback(fullpath); + continue; + } + fileCallback(fullpath); + } +} + +module.exports = { + canSafelyOverwrite, + isValidPackageName, + toValidPackageName, + emptyDir, +}; diff --git a/utils/templateOptions.js b/utils/templateOptions.js new file mode 100644 index 0000000..8d92747 --- /dev/null +++ b/utils/templateOptions.js @@ -0,0 +1,15 @@ +const templateOptions = [ + { id: 'NODE', label: 'node-ts-template' }, + { id: 'VUE3', label: 'vue3-template' }, + { id: 'VUE3_LITE', label: 'vue3-lite-template (without router and store)' }, + { id: 'REACT', label: 'react-template' }, + { id: 'REACT_LITE', label: 'react-lite-template' }, +]; + +const optionsToChoices = () => + templateOptions.map((o) => ({ title: o.label, value: o.id })); + +module.exports = { + templateOptions, + templateChoices: optionsToChoices(), +};