diff --git a/package-lock.json b/package-lock.json index d995eea..adf929e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "4.33.0", + "version": "4.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "4.33.0", + "version": "4.35.0", "license": "MIT", "dependencies": { "@fastify/formbody": "^7.4.0", @@ -17,7 +17,7 @@ "collect.js": "^4.36.1", "csv-parser": "^3.0.0", "execa": "^8.0.1", - "fastify": "^4.26.0", + "fastify": "^4.26.1", "got": "^12.6.1", "http-status-codes": "^2.2.0", "is-wsl": "^2.2.0", @@ -37,7 +37,7 @@ "youch-terminal": "^2.2.2" }, "devDependencies": { - "@athenna/test": "^4.18.0", + "@athenna/test": "^4.22.0", "@athenna/tsconfig": "^4.12.0", "@types/bytes": "^3.1.1", "@types/callsite": "^1.0.31", @@ -105,15 +105,15 @@ "dev": true }, "node_modules/@athenna/test": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@athenna/test/-/test-4.18.0.tgz", - "integrity": "sha512-HpW6XFZ5GqAd31mj2FB9H7eiBe+Wx/3DAEbiFcaf9JhGoi20GstbS2Bno4YAKv84NxsaLJgJxMZExjonu/uSuw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@athenna/test/-/test-4.22.0.tgz", + "integrity": "sha512-cOK7x007pE3E7lxW917zaXQOAUAIUXO3MK+ygXpOD/3+ZMaOGUYy1bupY5xGS0LzeM7OEpIBtb8HJ73jyIVOKA==", "dev": true, "dependencies": { "@japa/assert": "^2.1.0", "@japa/runner": "^3.1.1", "@types/sinon": "^10.0.20", - "c8": "^9.0.0", + "c8": "^9.1.0", "sinon": "^15.1.0" }, "engines": { @@ -2355,13 +2355,14 @@ } }, "node_modules/avvio": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", - "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", "dependencies": { + "@fastify/error": "^3.3.0", "archy": "^1.0.0", "debug": "^4.0.0", - "fastq": "^1.6.1" + "fastq": "^1.17.1" } }, "node_modules/balanced-match": { @@ -2484,9 +2485,9 @@ } }, "node_modules/c8": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.0.0.tgz", - "integrity": "sha512-nFJhU2Cz6Frh2awk3IW7wwk3wx27/U2v8ojQCHGc1GWTCHS6aMu4lal327/ZnnYj7oSThGF1X3qUP1yzAJBcOQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -4444,9 +4445,9 @@ "integrity": "sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==" }, "node_modules/fastify": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.0.tgz", - "integrity": "sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", + "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", "funding": [ { "type": "github", @@ -4462,7 +4463,7 @@ "@fastify/error": "^3.4.0", "@fastify/fast-json-stringify-compiler": "^4.3.0", "abstract-logging": "^2.0.1", - "avvio": "^8.2.1", + "avvio": "^8.3.0", "fast-content-type-parse": "^1.1.0", "fast-json-stringify": "^5.8.0", "find-my-way": "^8.0.0", @@ -4487,9 +4488,9 @@ "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -6246,9 +6247,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -9738,9 +9739,9 @@ } }, "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", diff --git a/package.json b/package.json index 6ce96c0..702d465 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "4.34.0", + "version": "4.35.0", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", @@ -62,7 +62,7 @@ "collect.js": "^4.36.1", "csv-parser": "^3.0.0", "execa": "^8.0.1", - "fastify": "^4.26.0", + "fastify": "^4.26.1", "got": "^12.6.1", "http-status-codes": "^2.2.0", "is-wsl": "^2.2.0", @@ -82,7 +82,7 @@ "youch-terminal": "^2.2.2" }, "devDependencies": { - "@athenna/test": "^4.18.0", + "@athenna/test": "^4.22.0", "@athenna/tsconfig": "^4.12.0", "@types/bytes": "^3.1.1", "@types/callsite": "^1.0.31", diff --git a/src/globals/Path.ts b/src/globals/Path.ts deleted file mode 100644 index ae8ebc7..0000000 --- a/src/globals/Path.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @athenna/common - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Path as PathImpl } from '#src/helpers/Path' - -export {} - -declare global { - export class Path extends PathImpl {} -} - -const __global: any = global - -if (!__global.Path) { - __global.Path = PathImpl -} diff --git a/src/helpers/Exec.ts b/src/helpers/Exec.ts index 9573ff7..eb6deb4 100644 --- a/src/helpers/Exec.ts +++ b/src/helpers/Exec.ts @@ -7,20 +7,25 @@ * file that was distributed with this source code. */ -import { Transform } from 'node:stream' -import { File } from '#src/helpers/File' -import { Options } from '#src/helpers/Options' -import { request as requestHttp } from 'node:http' -import { request as requestHttps } from 'node:https' -import { execa, execaNode, execaCommand, type ExecaChildProcess } from 'execa' import type { CommandInput, CommandOutput, NodeCommandInput, PaginationOptions, - PaginatedResponse + PaginatedResponse, + InstallPackageOptions, + LinkPackageOptions } from '#src/types' +import { Is } from '#src/helpers/Is' +import { Transform } from 'node:stream' +import { File } from '#src/helpers/File' +import { Path } from '#src/helpers/Path' +import { Options } from '#src/helpers/Options' +import { request as requestHttp } from 'node:http' +import { request as requestHttps } from 'node:https' +import { execa, execaNode, execaCommand, type ExecaChildProcess } from 'execa' + export class Exec { /** * Sleep the code in the line that this function @@ -61,6 +66,82 @@ export class Exec { return execa('sh', ['-c', command], options) } + /** + * Install libraries into a path using a registry as a child process. + */ + public static async install( + libraries: string | string[], + options: InstallPackageOptions = {} + ): Promise { + options = Options.create(options, { + args: [], + dev: false, + reject: true, + silent: true, + cached: false, + registry: 'npm', + cwd: Path.pwd() + }) + + if (Is.String(libraries)) { + libraries = [libraries] + } + + const args = ['install'] + + if (options.registry === 'yarn') { + args[0] = 'add' + } + + if (options.dev) { + args.push('-D') + } + + if (options.cached) { + args.push('--prefer-offline') + } + + args.push(...options.args) + args.push(...libraries) + + return execa(options.registry, args, { + reject: options.reject, + stdio: options.silent ? 'ignore' : 'inherit', + cwd: options.cwd + }) + } + + /** + * Link libraries into a path using a registry as a child process. + */ + public static async link( + libraries: string | string[], + options: LinkPackageOptions = {} + ): Promise { + options = Options.create(options, { + args: [], + reject: true, + silent: true, + registry: 'npm', + cwd: Path.pwd() + }) + + if (Is.String(libraries)) { + libraries = [libraries] + } + + const args = ['link'] + + args.push(...options.args) + args.push(...libraries) + + return execa(options.registry, args, { + reject: options.reject, + stdio: options.silent ? 'ignore' : 'inherit', + cwd: options.cwd + }) + } + public static command( command: string, options?: CommandInput diff --git a/src/index.ts b/src/index.ts index f5d0efb..fd76a16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ export * from '#src/types' -export * from '#src/globals/Path' export * from '#src/globals/Error' export * from '#src/globals/Array' diff --git a/src/types/InstallPackageOptions.ts b/src/types/InstallPackageOptions.ts new file mode 100644 index 0000000..9e5dbb1 --- /dev/null +++ b/src/types/InstallPackageOptions.ts @@ -0,0 +1,66 @@ +/** + * @athenna/common + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type InstallPackageOptions = { + /** + * Define the path where the install command should ran. + * + * @default Path.pwd() + */ + cwd?: string + + /** + * Define if the libraries should be installed as + * devDependencies or not. + * + * @default false + */ + dev?: boolean + + /** + * Add additional arguments for the registry CLI you + * are using to install the libraries. + * + * @default [] + */ + args?: string[] + + /** + * Define if the registry should use cached libraries when + * installing it. + * + * @default false + */ + cached?: boolean + + /** + * Define if the libraries installation should display the + * logs from the registry or not. + * + * @default true + */ + silent?: boolean + + /** + * Throw errors if something goes wrong when trying to + * install the libraries. + * + * @default true + */ + reject?: boolean + + /** + * Define the registry that will be used to install the + * dependencies. Tested only with `npm`, but + * you can try to use whatever you like. + * + * @default "npm" + */ + registry?: string +} diff --git a/src/types/LinkPackageOptions.ts b/src/types/LinkPackageOptions.ts new file mode 100644 index 0000000..1a80a7f --- /dev/null +++ b/src/types/LinkPackageOptions.ts @@ -0,0 +1,50 @@ +/** + * @athenna/common + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type LinkPackageOptions = { + /** + * Define the path where the install command should ran. + * + * @default Path.pwd() + */ + cwd?: string + + /** + * Add additional arguments for the registry CLI you + * are using to install the libraries. + * + * @default [] + */ + args?: string[] + + /** + * Define if the libraries installation should display the + * logs from the registry or not. + * + * @default true + */ + silent?: boolean + + /** + * Throw errors if something goes wrong when trying to + * install the libraries. + * + * @default true + */ + reject?: boolean + + /** + * Define the registry that will be used to install the + * dependencies. Tested only with `npm`, but + * you can try to use whatever you like. + * + * @default "npm" + */ + registry?: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 69ed56e..ae76d17 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,6 +33,9 @@ export * from '#src/types/CommandOutput' export * from '#src/types/NodeCommandInput' export * from '#src/types/ObjectBuilderOptions' +export * from '#src/types/LinkPackageOptions' +export * from '#src/types/InstallPackageOptions' + export * from '#src/types/json/FileJson' export * from '#src/types/json/FolderJson' export * from '#src/types/json/ExceptionJson' diff --git a/tests/unit/ExecTest.ts b/tests/unit/ExecTest.ts index dcc441b..23bc514 100644 --- a/tests/unit/ExecTest.ts +++ b/tests/unit/ExecTest.ts @@ -7,8 +7,8 @@ * file that was distributed with this source code. */ -import { Test, BeforeEach, type Context } from '@athenna/test' import { Clean, Exec, File, Folder, Is, Module, Path } from '#src' +import { Test, Cleanup, BeforeEach, type Context, Timeout } from '@athenna/test' export default class ExecTest { @BeforeEach() @@ -52,7 +52,7 @@ export default class ExecTest { await Exec.command('exit 255', { reject: false }) } - await assert.doesNotRejects(useCase) + await assert.doesNotReject(useCase) } @Test() @@ -91,7 +91,7 @@ export default class ExecTest { await Exec.shell('exit 255', { reject: false }) } - await assert.doesNotRejects(useCase) + await assert.doesNotReject(useCase) } @Test() @@ -107,6 +107,90 @@ export default class ExecTest { assert.isTrue(stdout.includes('error thrown')) } + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpm({ assert }: Context) { + const { stdout, stderr, exitCode } = await Exec.install('execa') + + assert.isUndefined(stderr) + assert.isUndefined(stdout) + assert.equal(exitCode, 0) + } + + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpmInCacheMode({ assert }: Context) { + const { stdout, stderr, exitCode } = await Exec.install('execa', { cached: true }) + + assert.isUndefined(stderr) + assert.isUndefined(stdout) + assert.equal(exitCode, 0) + } + + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpmForDevMode({ assert }: Context) { + const { stdout, stderr, exitCode } = await Exec.install('@athenna/test', { dev: true }) + + assert.isUndefined(stderr) + assert.isUndefined(stdout) + assert.equal(exitCode, 0) + } + + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpmAndAddAdditionalArgs({ assert }: Context) { + const { stdout, stderr, exitCode } = await Exec.install('@athenna/test', { args: ['-D'] }) + + assert.isUndefined(stderr) + assert.isUndefined(stdout) + assert.equal(exitCode, 0) + } + + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpmAndRejectIfCommandFails({ assert }: Context) { + await assert.rejects(() => Exec.install('@athenna/not-found-pkg', { reject: true })) + } + + @Test() + @Timeout(30000) + @Cleanup(async () => Exec.link('@athenna/common')) + public async shouldBeAbleToExecuteAInstallCommandWithNpmAndDontRejectIfCommandFails({ assert }: Context) { + await assert.doesNotReject(() => Exec.install('@athenna/not-found-pkg', { reject: false })) + } + + @Test() + public async shouldThrowAnExceptionWhenInstallCommandWithNpmFails({ assert }: Context) { + if (Is.Windows()) { + return + } + + const useCase = async () => { + await Exec.shell('exit 255') + } + + await assert.rejects(useCase) + } + + @Test() + public async shouldBeAbleToIgnoreExceptionWhenRejectOptionIsSetToFalseInNpmInstallCommand({ assert }: Context) { + if (Is.Windows()) { + return + } + + const useCase = async () => { + await Exec.shell('exit 255', { reject: false }) + } + + await assert.doesNotReject(useCase) + } + @Test() public async shouldBeAbleToExecuteANodeScriptInTheVMAndGetTheStdout({ assert }: Context) { const { stdout, stderr, exitCode } = await Exec.node(Path.fixtures('node-script.ts')) @@ -131,7 +215,7 @@ export default class ExecTest { await Exec.node(Path.fixtures('node-script-throw.ts'), [], { reject: false }) } - await assert.doesNotRejects(useCase) + await assert.doesNotReject(useCase) } @Test()