diff --git a/CITATION.cff b/CITATION.cff index 6ec27b4e..436fbad3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,8 +6,8 @@ authors: orcid: https://orcid.org/0009-0001-1641-9397 identifiers: - type: url - value: https://github.com/caffeine-addictt/template/releases/tag/v1.12.3 - description: The GitHub release URL of tag v1.12.3. + value: https://github.com/caffeine-addictt/template/releases/tag/v1.13.2 + description: The GitHub release URL of tag v1.13.2. cff-version: 1.2.0 date-released: 2024-05-12 keywords: @@ -19,4 +19,4 @@ message: If you use this software, please cite it using these metadata. repository-code: https://github.com/caffeine-addictt/template title: template type: software -version: 1.12.3 +version: 1.13.2 diff --git a/README.md b/README.md index 251918ff..360eee7d 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,6 @@ Alex - [contact@ngjx.org](mailto:contact@ngjx.org) [stars-shield]: https://img.shields.io/github/stars/caffeine-addictt/template.svg?style=for-the-badge&color=yellow [stars-url]: https://github.com/caffeine-addictt/template/stargazers [license-shield]: https://img.shields.io/github/license/caffeine-addictt/template.svg?style=for-the-badge -[license-url]: https://github.com/caffeine-addictt/template/blob/master/LICENSE.txt +[license-url]: https://github.com/caffeine-addictt/template/blob/main/LICENSE [use-shield]: https://img.shields.io/badge/Use%20template-FFFFFF?style=for-the-badge [use-url]: https://github.com/new?template_name=template&template_owner=caffeine-addictt diff --git a/dist/io-util.js b/dist/io-util.js index e049a53d..0e3b6af0 100644 --- a/dist/io-util.js +++ b/dist/io-util.js @@ -25,7 +25,7 @@ const replaceInFile = (filePath, _tempDir, data) => new Promise((resolve) => { // Revert to legacy let fileContent = fs_1.default.readFileSync(filePath, 'utf8'); fileContent = fileContent - .replace(/{{REPOSITORY}}/g, data.repository) + .replace(/{{REPOSITORY}}/g, `${data.username}/${data.repository}`) .replace(/{{PROJECT_NAME}}/g, data.proj_name) .replace(/{{PROJECT_SHORT_DESCRIPTION}}/g, data.proj_short_desc) .replace(/{{PROJECT_LONG_DESCRIPTION}}/g, data.proj_long_desc) diff --git a/dist/parser.js b/dist/parser.js new file mode 100644 index 00000000..3d7e7d8e --- /dev/null +++ b/dist/parser.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extractRepoInfo = void 0; +/** Extract username/org and repo from url */ +const extractRepoInfo = (url) => { + const originUrl = url.trim(); + const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(originUrl); + if (!match || !match[1] || !match[2]) + throw new AggregateError(`Could not extract user and repo from git remote: ${originUrl}`); + return { + url: originUrl, + org: match[1], + repo: match[2], + }; +}; +exports.extractRepoInfo = extractRepoInfo; diff --git a/dist/resolver.js b/dist/resolver.js new file mode 100644 index 00000000..80db13b4 --- /dev/null +++ b/dist/resolver.js @@ -0,0 +1,35 @@ +"use strict"; +/** + * This file will hold the configuration options to make the setup script not so annoying + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveUserInfo = exports.resolveRepoInfo = void 0; +const parser_1 = require("./parser"); +const term_1 = require("./term"); +const resolveRepoInfo = () => (0, term_1.execute)('git', ['remote', 'get-url', 'origin']) + .then((_originUrl) => (0, parser_1.extractRepoInfo)(_originUrl.toString())) + .catch(() => ({})); +exports.resolveRepoInfo = resolveRepoInfo; +const resolveUserInfo = async () => { + const promises = await Promise.all([ + (0, term_1.execute)('git', ['config', '--global', 'user.name']) + .then((r) => r.toString().trim()) + .catch(() => undefined), + (0, term_1.execute)('git', ['config', '--global', 'user.email']) + .then((r) => r.toString().trim()) + .catch(() => undefined), + ]); + return { + name: promises[0], + email: promises[1], + }; +}; +exports.resolveUserInfo = resolveUserInfo; +const resolveGitInfo = async () => { + const promises = await Promise.all([(0, exports.resolveRepoInfo)(), (0, exports.resolveUserInfo)()]); + return { + ...promises[0], + ...promises[1], + }; +}; +exports.default = resolveGitInfo; diff --git a/dist/setup.js b/dist/setup.js index 045aba85..2f7c4394 100644 --- a/dist/setup.js +++ b/dist/setup.js @@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const readline_1 = __importDefault(require("readline")); const io_util_1 = require("./io-util"); +const resolver_1 = __importDefault(require("./resolver")); /** * For interacting with stdin/stdout */ @@ -28,15 +29,25 @@ const question = (query, validator = [], trimWhitespace = true) => new Promise(( })); /** Ask for project information */ const fetchInfo = async (cleanup) => { - const name = await question('Name? (This will go on the LICENSE)\n=> '); - const email = await question('Email?\n=> ', [ + const resolved = await (0, resolver_1.default)(); + // Ask user + const name = await question(`Name? (This will go on the LICENSE)${resolved.name ? ` [Resolved: ${resolved.name}]` : ''}\n=> `) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.name ?? '')); + const email = await question(`Email?${resolved.email ? ` [Resolved: ${resolved.email}]` : ''}\n=> `, [ { - validate: (s) => /.+@.+\..+/.test(s), + validate: (s) => !!resolved.email || /.+@.+\..+/.test(s), onError: () => console.log('Invalid email!'), }, - ]); - const username = await question('Username? (https://github.com/)\n=> '); - const repository = await question('Repository? ((https://github.com/$username/\n=> '); + ]) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.email ?? '')); + const username = await question(`Username? (https://github.com/)${resolved.org ? ` [Resolved: ${resolved.org}]` : ''}\n=> `) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.org ?? '')); + const repository = await question(`Repository? (https://github.com/${username}/)${resolved.repo ? ` [Resolved: ${resolved.repo}]` : ''}\n=> `) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.repo ?? '')); const proj_name = await question('Project name?\n=> '); const proj_short_desc = await question('Short description?\n=> '); const proj_long_desc = await question('Long description?\n=> '); @@ -90,14 +101,14 @@ const { func: main } = (0, io_util_1.withTempDir)('caffeine-addictt-template-', recursive: true, }); // Use async - await Promise.all(filesToUpdate.map((filename) => async () => { + await Promise.all(filesToUpdate.map((filename) => (async () => { const filePath = path_1.default.join('./template', filename); const fileInfo = fs_1.default.statSync(filePath); if (fileInfo.isDirectory()) { return; } await (0, io_util_1.replaceInFile)(filePath, tempDir, data); - })); + })())); // Write CODEOWNERS fs_1.default.appendFileSync('./template/.github/CODEOWNERS', `* @${data.username}`); // ########################################## // @@ -122,6 +133,9 @@ const { func: main } = (0, io_util_1.withTempDir)('caffeine-addictt-template-', // ################# // // Stage 3: Clean up // // ################# // + // Only add `force: true` for files or directories that + // will only exist if some development task was carried out + // like eslintcache console.log('Cleaning up...'); // Js fs_1.default.unlinkSync('package.json'); @@ -133,16 +147,16 @@ const { func: main } = (0, io_util_1.withTempDir)('caffeine-addictt-template-', fs_1.default.unlinkSync('babel.config.cjs'); fs_1.default.rmSync('tests', { recursive: true }); // Linting - fs_1.default.unlinkSync('.eslintcache'); fs_1.default.unlinkSync('.eslintignore'); fs_1.default.unlinkSync('.prettierignore'); fs_1.default.unlinkSync('eslint.config.mjs'); + fs_1.default.rmSync('.eslintcache', { force: true }); // Syncing fs_1.default.unlinkSync('.templatesyncignore'); // Git fs_1.default.unlinkSync('.gitignore'); // Node - fs_1.default.rmSync('node_modules', { recursive: true }); + fs_1.default.rmSync('node_modules', { recursive: true, force: true }); // Clean up dist fs_1.default.unlinkSync(__filename); fs_1.default.rmSync('dist', { recursive: true }); diff --git a/dist/term.js b/dist/term.js new file mode 100644 index 00000000..29ff8f2a --- /dev/null +++ b/dist/term.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = void 0; +const child_process_1 = require("child_process"); +const execute = (command, args, spawnOptions) => new Promise((resolve, reject) => { + const cmd = (0, child_process_1.spawn)(command, args, spawnOptions); + cmd.stdout.on('data', resolve); + cmd.stderr.on('data', reject); +}); +exports.execute = execute; diff --git a/package-lock.json b/package-lock.json index 2afe8a63..a5b5caaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3255,12 +3255,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4015,9 +4015,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/src/io-util.ts b/src/io-util.ts index da384456..70580dbe 100644 --- a/src/io-util.ts +++ b/src/io-util.ts @@ -42,7 +42,7 @@ export const replaceInFile = ( let fileContent = fs.readFileSync(filePath, 'utf8'); fileContent = fileContent - .replace(/{{REPOSITORY}}/g, data.repository) + .replace(/{{REPOSITORY}}/g, `${data.username}/${data.repository}`) .replace(/{{PROJECT_NAME}}/g, data.proj_name) .replace(/{{PROJECT_SHORT_DESCRIPTION}}/g, data.proj_short_desc) .replace(/{{PROJECT_LONG_DESCRIPTION}}/g, data.proj_long_desc) diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 00000000..97ae7e7b --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,20 @@ +import type { RepoInfo } from './types'; + +/** Extract username/org and repo from url */ +export const extractRepoInfo = (url: string): RepoInfo => { + const originUrl = url.trim(); + const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec( + originUrl, + ); + + if (!match || !match[1] || !match[2]) + throw new AggregateError( + `Could not extract user and repo from git remote: ${originUrl}`, + ); + + return { + url: originUrl, + org: match[1], + repo: match[2], + } satisfies RepoInfo; +}; diff --git a/src/resolver.ts b/src/resolver.ts new file mode 100644 index 00000000..e99a82b3 --- /dev/null +++ b/src/resolver.ts @@ -0,0 +1,37 @@ +/** + * This file will hold the configuration options to make the setup script not so annoying + */ + +import { extractRepoInfo } from './parser'; +import { execute } from './term'; +import type { GitInfo, RepoInfo, UserInfo } from './types'; + +export const resolveRepoInfo = (): Promise => + execute('git', ['remote', 'get-url', 'origin']) + .then((_originUrl) => extractRepoInfo(_originUrl.toString())) + .catch(() => ({})); + +export const resolveUserInfo = async (): Promise => { + const promises = await Promise.all([ + execute('git', ['config', '--global', 'user.name']) + .then((r) => r.toString().trim()) + .catch(() => undefined), + execute('git', ['config', '--global', 'user.email']) + .then((r) => r.toString().trim()) + .catch(() => undefined), + ]); + + return { + name: promises[0], + email: promises[1], + } satisfies UserInfo; +}; + +const resolveGitInfo = async (): Promise => { + const promises = await Promise.all([resolveRepoInfo(), resolveUserInfo()]); + return { + ...promises[0], + ...promises[1], + } as GitInfo; +}; +export default resolveGitInfo; diff --git a/src/setup.ts b/src/setup.ts index 71050a88..ef1c6979 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -4,6 +4,7 @@ import readline from 'readline'; import type { ProjectInfo } from './types'; import { replaceInFile, withTempDir } from './io-util'; +import resolveGitInfo from './resolver'; /** * For interacting with stdin/stdout @@ -41,19 +42,39 @@ const question = ( const fetchInfo = async ( cleanup: () => void | unknown, ): Promise => { - const name = await question('Name? (This will go on the LICENSE)\n=> '); - const email = await question('Email?\n=> ', [ - { - validate: (s: string) => /.+@.+\..+/.test(s), - onError: () => console.log('Invalid email!'), - }, - ]); + const resolved = await resolveGitInfo(); + + // Ask user + const name = await question( + `Name? (This will go on the LICENSE)${resolved.name ? ` [Resolved: ${resolved.name}]` : ''}\n=> `, + ) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.name ?? '')); + + const email = await question( + `Email?${resolved.email ? ` [Resolved: ${resolved.email}]` : ''}\n=> `, + [ + { + validate: (s: string) => !!resolved.email || /.+@.+\..+/.test(s), + onError: () => console.log('Invalid email!'), + }, + ], + ) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.email ?? '')); + const username = await question( - 'Username? (https://github.com/)\n=> ', - ); + `Username? (https://github.com/)${resolved.org ? ` [Resolved: ${resolved.org}]` : ''}\n=> `, + ) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.org ?? '')); + const repository = await question( - 'Repository? ((https://github.com/$username/\n=> ', - ); + `Repository? (https://github.com/${username}/)${resolved.repo ? ` [Resolved: ${resolved.repo}]` : ''}\n=> `, + ) + .then((val) => val.trim()) + .then((val) => (val.length ? val : resolved.repo ?? '')); + const proj_name = await question('Project name?\n=> '); const proj_short_desc = await question('Short description?\n=> '); const proj_long_desc = await question('Long description?\n=> '); @@ -126,16 +147,18 @@ const { func: main } = withTempDir( // Use async await Promise.all( - filesToUpdate.map((filename) => async () => { - const filePath = path.join('./template', filename); - - const fileInfo = fs.statSync(filePath); - if (fileInfo.isDirectory()) { - return; - } - - await replaceInFile(filePath, tempDir, data); - }), + filesToUpdate.map((filename) => + (async () => { + const filePath = path.join('./template', filename); + + const fileInfo = fs.statSync(filePath); + if (fileInfo.isDirectory()) { + return; + } + + await replaceInFile(filePath, tempDir, data); + })(), + ), ); // Write CODEOWNERS @@ -167,6 +190,9 @@ const { func: main } = withTempDir( // ################# // // Stage 3: Clean up // // ################# // + // Only add `force: true` for files or directories that + // will only exist if some development task was carried out + // like eslintcache console.log('Cleaning up...'); // Js @@ -182,10 +208,10 @@ const { func: main } = withTempDir( fs.rmSync('tests', { recursive: true }); // Linting - fs.unlinkSync('.eslintcache'); fs.unlinkSync('.eslintignore'); fs.unlinkSync('.prettierignore'); fs.unlinkSync('eslint.config.mjs'); + fs.rmSync('.eslintcache', { force: true }); // Syncing fs.unlinkSync('.templatesyncignore'); @@ -194,7 +220,7 @@ const { func: main } = withTempDir( fs.unlinkSync('.gitignore'); // Node - fs.rmSync('node_modules', { recursive: true }); + fs.rmSync('node_modules', { recursive: true, force: true }); // Clean up dist fs.unlinkSync(__filename); diff --git a/src/term.ts b/src/term.ts new file mode 100644 index 00000000..53478fb1 --- /dev/null +++ b/src/term.ts @@ -0,0 +1,13 @@ +import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; + +export const execute = ( + command: string, + args?: string[], + spawnOptions?: SpawnOptionsWithoutStdio, +): Promise => + new Promise((resolve, reject): void => { + const cmd = spawn(command, args, spawnOptions); + + cmd.stdout.on('data', resolve); + cmd.stderr.on('data', reject); + }); diff --git a/src/types.ts b/src/types.ts index 47037077..51150e4e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,3 +11,16 @@ export interface ProjectInfo { docs_url: string; assignees: string; } + +export interface RepoInfo { + url?: string; + org?: string; + repo?: string; +} + +export interface UserInfo { + name?: string; + email?: string; +} + +export type GitInfo = RepoInfo & UserInfo; diff --git a/template/README.md b/template/README.md index 76b4c5da..ef0c77d5 100644 --- a/template/README.md +++ b/template/README.md @@ -137,4 +137,4 @@ Distributed under the MIT License. See [LICENSE.txt](./LICENSE.txt) for more inf [stars-shield]: https://img.shields.io/github/stars/{{REPOSITORY}}.svg?style=for-the-badge&color=yellow [stars-url]: https://github.com/{{REPOSITORY}}/stargazers [license-shield]: https://img.shields.io/github/license/{{REPOSITORY}}.svg?style=for-the-badge -[license-url]: https://github.com/{{REPOSITORY}}/blob/master/LICENSE.txt +[license-url]: https://github.com/{{REPOSITORY}}/blob/main/LICENSE diff --git a/tests/io-util.test.ts b/tests/io-util.test.ts index deed0b9e..17462490 100644 --- a/tests/io-util.test.ts +++ b/tests/io-util.test.ts @@ -23,7 +23,7 @@ assignees: ['{{ASSIGNEES}}'] const replaced = ` My name is: John -The repo is https://github.com/username/repo +The repo is johnny/repo HbVYf@example.com https://example.com/docs Project with some description @@ -65,7 +65,7 @@ describe('replace the correct content', () => { name: 'John', email: 'HbVYf@example.com', username: 'johnny', - repository: 'https://github.com/username/repo', + repository: 'repo', proj_name: 'My Project', proj_short_desc: 'Project with some description', proj_long_desc: 'Project with some description that is really long',