diff --git a/README.md b/README.md index 971a6aa..be35411 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ This will install local packages referenced inside `fireDependencies` It means that `node_modules` folder will be already populated with appropriate packages after `npm install` ```bash -firelink --bootstrap --no-runner +firelink --bootstrap --skip-runner ``` Deploying as usual diff --git a/package-lock.json b/package-lock.json index 4a1670a..591cfa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1442,6 +1442,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, "compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", diff --git a/package.json b/package.json index e0cc989..1651acb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ }, "main": "./dist/index.js", "scripts": { - "build": "npx parcel@1.12.3 build ./src/main.ts --experimental-scope-hoisting --target node", + "start": "npx gapi start", + "build": "npx parcel build ./src/main.ts --target node", "build-binary": "npx gapi build --single-executable && mkdir binaries && npm run copy-binaries", "build-all": "npm run build-binary && rm -rf dist && npm run build", "test": "npx jest", @@ -40,6 +41,7 @@ "jest-cli": "29.2.2", "prettier": "^2.7.1", "ts-jest": "29.0.3", + "commander": "^9.0.0", "typescript": "^4.8.4" } } \ No newline at end of file diff --git a/src/create-virtual-symlink.spec.ts b/src/create-virtual-symlink.spec.ts deleted file mode 100644 index a1cf8b9..0000000 --- a/src/create-virtual-symlink.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { createVirtualSymlink } from './create-virtual-symlink'; -import { PackageJson } from './injection-tokens'; - -const mockBuildPackages = jest.fn(); -// const mockCopyPackages = jest.fn(); -const mockExitHandler = jest.fn(); -const mockModifyJson = jest.fn(); -const mockRevertJson = jest.fn(); -const mockRunCommand = jest.fn(); - -jest.mock('./helpers/build-packages', () => ({ - buildPackages: async (...args: unknown[]) => mockBuildPackages(...args), -})); -// jest.mock('./helpers/copy-packages', () => ({ -// copyPackages: -// (...args: unknown[]) => -// () => -// () => -// () => -// () => -// mockCopyPackages(...args), -// })); -jest.mock('./helpers/exit-handler', () => ({ - exitHandler: (...args: unknown[]) => mockExitHandler(...args), -})); -jest.mock('./helpers/modify-json', () => ({ - modifyJson: async (...args: unknown[]) => mockModifyJson(...args), -})); -jest.mock('./helpers/revert-json', () => ({ - revertJson: async () => mockRevertJson(), -})); -jest.mock('./helpers/run-command', () => ({ - runCommand: async () => mockRunCommand(), -})); - -const fakePackageJson = { - dependencies: { - foo: 'bar', - }, - fireDependencies: { - baz: 'qux', - }, -} as PackageJson; -const outFolder = '.'; -const outFolderName = '.packages'; - -describe('createVirtualSymlink', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should exit successfully if final command succeeds', async () => { - mockRunCommand.mockImplementationOnce(() => Promise.resolve(true)); - await createVirtualSymlink(fakePackageJson, outFolder, outFolderName); - expect(mockRunCommand).toHaveBeenCalledTimes(1); - expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, true); - }); - - it('should exit with error if final command fails', async () => { - mockRunCommand.mockImplementationOnce(() => Promise.reject(false)); - await createVirtualSymlink(fakePackageJson, outFolder, outFolderName); - expect(mockRunCommand).toHaveBeenCalledTimes(1); - expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, false); - }); - - it('should only revert json and exit if --revert-changes flag is present', async () => { - process.argv.push('--revert-changes'); - await createVirtualSymlink(fakePackageJson, outFolder, outFolderName); - expect(mockRevertJson).toHaveBeenCalledTimes(1); - expect(mockRunCommand).toHaveBeenCalledTimes(0); - expect(mockExitHandler).toHaveBeenCalledTimes(0); - process.argv.pop(); - }); - - it('should update dependencies if fireDependencies are present in original package.json', async () => { - mockRunCommand.mockImplementationOnce(() => Promise.resolve(true)); - await createVirtualSymlink(fakePackageJson, outFolder, outFolderName); - // expect(mockCopyPackages).toHaveBeenCalledTimes(1); - expect(mockModifyJson).toHaveBeenCalledTimes(1); - expect(mockRunCommand).toHaveBeenCalledTimes(1); - expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, true); - }); - - it('should build fireDependencies if --buildCommand flag is present', async () => { - process.argv.push('--buildCommand'); - mockRunCommand.mockImplementationOnce(() => Promise.resolve(true)); - await createVirtualSymlink(fakePackageJson, outFolder, outFolderName); - // expect(mockCopyPackages).toHaveBeenCalledTimes(1); - expect(mockBuildPackages).toHaveBeenCalledTimes(1); - expect(mockModifyJson).toHaveBeenCalledTimes(1); - expect(mockRunCommand).toHaveBeenCalledTimes(1); - expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, true); - }); -}); diff --git a/src/create-virtual-symlink.ts b/src/create-virtual-symlink.ts deleted file mode 100644 index 69b6eca..0000000 --- a/src/create-virtual-symlink.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { includes, nextOrDefault } from './helpers/args-extractors'; -import { buildPackages } from './helpers/build-packages'; -import { exitHandler } from './helpers/exit-handler'; -import { modifyJson } from './helpers/modify-json'; -import { revertJson } from './helpers/revert-json'; -import { runCommand } from './helpers/run-command'; -import { Worker } from './helpers/worker'; -import { - DEFAULT_RUNNER, - FireLinkConfig, - PackageJson, - Signals, - Tasks, - WorkingFiles, -} from './injection-tokens'; - -export async function createVirtualSymlink( - packageJson: PackageJson = {} as PackageJson, - outFolder: string, - outFolderName: string, -) { - packageJson.fireConfig = packageJson.fireConfig || ({} as FireLinkConfig); - let successStatus = false; - const runner = - nextOrDefault(Tasks.RUNNER) || - packageJson.fireConfig.runner || - DEFAULT_RUNNER; - - if (includes(Tasks.REVERT)) { - return await revertJson( - WorkingFiles.PACKAGE_JSON, - WorkingFiles.PACKAGE_TEMP_JSON, - ); - } - - const originalPackageJson = JSON.parse(JSON.stringify(packageJson)); - - if (packageJson.fireDependencies) { - const linkedDepndencies = packageJson.fireDependencies; - const dependencies = Object.keys(linkedDepndencies).map((dep) => ({ - dep, - folder: linkedDepndencies[dep], - })); - - if (includes(Tasks.BUILD)) { - try { - await buildPackages(outFolder, outFolderName); - } catch (e) {} - } - process.stdin.resume(); - const signals: Signals[] = [ - 'exit', - 'SIGINT', - 'SIGUSR1', - 'SIGUSR2', - 'uncaughtException', - ]; - signals.map((event) => - process.on(event as never, () => - exitHandler(originalPackageJson, successStatus), - ), - ); - - await modifyJson(packageJson, dependencies); - } - - try { - if (includes(Tasks.BOOTSTRAP)) { - await Worker({ command: 'npm', args: ['install'] }); - } - if (!includes(Tasks.NO_RUNNER)) { - await runCommand(runner, process.argv); - } - successStatus = true; - } catch (e) { - } finally { - await exitHandler(originalPackageJson, successStatus); - } - process.stdin.pause(); -} diff --git a/src/helpers/args-extractors.ts b/src/helpers/args-extractors.ts deleted file mode 100644 index 9503043..0000000 --- a/src/helpers/args-extractors.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const includes = (i: string) => process.argv.toString().includes(i); -export const nextOrDefault = ( - i: string, - fb?: string, - type = (p: string) => p, -) => { - if (process.argv.toString().includes(i)) { - const isNextArgumentPresent = process.argv[process.argv.indexOf(i) + 1]; - if (!isNextArgumentPresent) { - return fb; - } - if (isNextArgumentPresent.includes('--')) { - return fb; - } - return type(isNextArgumentPresent); - } - return fb; -}; diff --git a/src/helpers/build-packages.ts b/src/helpers/build-packages.ts deleted file mode 100644 index 3d990cc..0000000 --- a/src/helpers/build-packages.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { readdir } from 'fs'; -import { join } from 'path'; -import { promisify } from 'util'; - -import { Tasks } from '../injection-tokens'; -import { nextOrDefault } from './args-extractors'; -import { Worker } from './worker'; - -export async function buildPackages(outFolder: string, outFolderName: string) { - return await Promise.all( - ( - await promisify(readdir)(join(outFolder, outFolderName)) - ).map(async (dir) => { - await Worker({ - command: 'npx', - args: (nextOrDefault(Tasks.BUILD, 'tsc') as string).split(' '), - cwd: join(outFolder, outFolderName, dir), - }); - }), - ); -} diff --git a/src/helpers/custom-error.ts b/src/helpers/custom-error.ts new file mode 100644 index 0000000..a12b084 --- /dev/null +++ b/src/helpers/custom-error.ts @@ -0,0 +1,18 @@ +export class CustomError extends Error { + get name(): string { + return this.constructor.name; + } +} + +export class ExitCodeError extends CustomError { + readonly code: number; + + constructor(code: number, command?: string) { + if (command) { + super(`Command '${command}' exited with code ${code}`); + } else { + super(`Child exited with code ${code}`); + } + this.code = code; + } +} diff --git a/src/helpers/exit-handler.ts b/src/helpers/exit-handler.ts deleted file mode 100644 index 37c1ba7..0000000 --- a/src/helpers/exit-handler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PackageJson, Tasks, WorkingFiles } from '../injection-tokens'; -import { includes } from './args-extractors'; -import { fileExists } from './file-exists'; -import { writeFileJson } from './write-file-json'; - -export async function exitHandler( - originalPackageJson: PackageJson, - success: boolean, -) { - if (!includes(Tasks.LEAVE_CHANGES)) { - writeFileJson(WorkingFiles.PACKAGE_JSON, originalPackageJson); - } else if (!(await fileExists(WorkingFiles.PACKAGE_TEMP_JSON))) { - writeFileJson(WorkingFiles.PACKAGE_TEMP_JSON, originalPackageJson); - } - process.exit(success ? 0 : 1); -} diff --git a/src/helpers/file-exists.ts b/src/helpers/file-exists.ts deleted file mode 100644 index 8d02d49..0000000 --- a/src/helpers/file-exists.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { stat } from 'fs'; -import { promisify } from 'util'; - -export async function fileExists(filename: string) { - try { - await promisify(stat)(filename); - return true; - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } else { - throw err; - } - } -} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 4a3cded..77b76b1 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,14 +1,2 @@ -export * from './args-extractors'; -export * from './build-packages'; -export * from './copy-packages'; -export * from './copy-recursive'; -export * from './exit-handler'; -export * from './file-exists'; -export * from './modify-json'; -export * from './parse-ignore'; -export * from './read-excludes'; -export * from './read-json'; -export * from './revert-json'; -export * from './run-command'; -export * from './worker'; -export * from './write-file-json'; +export * from './custom-error'; +export * from './utils.service'; diff --git a/src/helpers/modify-json.ts b/src/helpers/modify-json.ts deleted file mode 100644 index 93bd8ab..0000000 --- a/src/helpers/modify-json.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { writeFile } from 'fs'; -import { promisify } from 'util'; - -import { - DependenciesLink, - PackageJson, - WorkingFiles, -} from '../injection-tokens'; - -export async function modifyJson( - packageJson: PackageJson, - dependencies: DependenciesLink[], -) { - for (const { dep, folder } of dependencies) { - packageJson.dependencies[dep] = `file:${folder}`; - } - await promisify(writeFile)( - WorkingFiles.PACKAGE_JSON, - JSON.stringify(packageJson, null, 2), - { encoding: 'utf-8' }, - ); -} diff --git a/src/helpers/parse-ignore.ts b/src/helpers/parse-ignore.ts deleted file mode 100644 index 59f9476..0000000 --- a/src/helpers/parse-ignore.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const parseIgnoredFiles = (input: string): string[] => - input - .toString() - .split(/\r?\n/) - .filter((l) => l.trim() !== '' && l.charAt(0) !== '#'); diff --git a/src/helpers/read-json.ts b/src/helpers/read-json.ts deleted file mode 100644 index 318369f..0000000 --- a/src/helpers/read-json.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { readFile } from 'fs'; -import { join } from 'path'; -import { promisify } from 'util'; - -import { PackageJson } from '../injection-tokens'; - -export async function readJson(name: string, cwd: string = process.cwd()) { - return JSON.parse( - await promisify(readFile)(join(cwd, name), { - encoding: 'utf-8', - }), - ) as PackageJson; -} diff --git a/src/helpers/revert-json.ts b/src/helpers/revert-json.ts deleted file mode 100644 index c23c686..0000000 --- a/src/helpers/revert-json.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { unlink } from 'fs'; -import { promisify } from 'util'; - -import { readJson } from './read-json'; -import { writeFileJson } from './write-file-json'; - -export async function revertJson(originalJson: string, tempJson: string) { - const json = await readJson(tempJson); - writeFileJson(originalJson, json); - await promisify(unlink)(tempJson); -} diff --git a/src/helpers/run-command.ts b/src/helpers/run-command.ts deleted file mode 100644 index 57ba16b..0000000 --- a/src/helpers/run-command.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isWin, Tasks } from '../injection-tokens'; -import { Worker } from './worker'; - -export async function runCommand( - runner: string, - args: string[], -): Promise { - return await Worker({ - command: isWin ? 'cmd' : 'npx', - args: [ - ...(isWin ? ['/c', 'npx'] : []), - runner, - ...args - .slice(2) - .filter((a) => !Object.values(Tasks).includes(a as never)), - ], - }); -} diff --git a/src/helpers/utils.service.create-virtual-symlink.spec.ts b/src/helpers/utils.service.create-virtual-symlink.spec.ts new file mode 100644 index 0000000..7ec6d49 --- /dev/null +++ b/src/helpers/utils.service.create-virtual-symlink.spec.ts @@ -0,0 +1,59 @@ +import { PackageJson } from '../types'; +import { UtilsService } from './utils.service'; + +describe('createVirtualSymlink', () => { + const fakePackageJson = { + dependencies: { + foo: 'bar', + }, + fireDependencies: { + baz: 'qux', + }, + } as PackageJson; + + const mockExitHandler = jest.fn().mockImplementation(() => () => null); + const mockModifyJson = jest.fn(); + const mockRevertJson = jest.fn(); + const mockRunCommand = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + jest.spyOn(UtilsService, 'runCommand').mockImplementation(mockRunCommand); + jest.spyOn(UtilsService, 'revertJson').mockImplementation(mockRevertJson); + jest.spyOn(UtilsService, 'modifyJson').mockImplementation(mockModifyJson); + jest.spyOn(UtilsService, 'exitHandler').mockImplementation(mockExitHandler); + }); + + it('should exit successfully if final command succeeds', async () => { + mockRunCommand.mockImplementationOnce(() => Promise.resolve(true)); + await UtilsService.createVirtualSymlink(fakePackageJson)({}); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, true); + }); + + it('should exit with error if final command fails', async () => { + mockRunCommand.mockImplementationOnce(() => Promise.reject(false)); + await UtilsService.createVirtualSymlink(fakePackageJson)({}); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, false); + }); + + it('should only revert json and exit if --revert-changes flag is present', async () => { + await UtilsService.createVirtualSymlink(fakePackageJson)({ + revertChanges: true, + }); + expect(mockRevertJson).toHaveBeenCalledTimes(1); + expect(mockRunCommand).toHaveBeenCalledTimes(0); + expect(mockExitHandler).toHaveBeenCalledTimes(0); + process.argv.pop(); + }); + + it('should update dependencies if fireDependencies are present in original package.json', async () => { + mockRunCommand.mockImplementationOnce(() => Promise.resolve(true)); + await UtilsService.createVirtualSymlink(fakePackageJson)({}); + expect(mockModifyJson).toHaveBeenCalledTimes(1); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith(fakePackageJson, true); + }); +}); diff --git a/src/helpers/exit-handler.spec.ts b/src/helpers/utils.service.exit-handler.spec.ts similarity index 56% rename from src/helpers/exit-handler.spec.ts rename to src/helpers/utils.service.exit-handler.spec.ts index 98ae5ac..c897cc5 100644 --- a/src/helpers/exit-handler.spec.ts +++ b/src/helpers/utils.service.exit-handler.spec.ts @@ -1,55 +1,48 @@ -import { PackageJson, WorkingFiles } from '../injection-tokens'; -import { exitHandler } from './exit-handler'; +import { PackageJson, WorkingFiles } from '../types'; +import { UtilsService } from './utils.service'; -const mockIncludes = jest.fn(); -const mockFileExists = jest.fn(); -const mockWriteFileJson = jest.fn(); - -jest.mock('./args-extractors', () => ({ - includes: () => mockIncludes(), -})); -jest.mock('./file-exists', () => ({ - fileExists: () => mockFileExists(), -})); -jest.mock('./write-file-json', () => ({ - writeFileJson: (...args: unknown[]) => mockWriteFileJson(...args), -})); - -jest - .spyOn(process, 'exit') - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .mockImplementation((_code: number) => undefined as never); +describe('exitHandler', () => { + const fakePackageJson = { + dependencies: { + foo: 'bar', + }, + fireDependencies: { + baz: 'qux', + }, + } as PackageJson; -const fakePackageJson = { - dependencies: { - foo: 'bar', - }, - fireDependencies: { - baz: 'qux', - }, -} as PackageJson; + const mockIncludes = jest.fn(); + const mockFileExists = jest.fn(); + const mockWriteFileJson = jest.fn(); -describe('exitHandler', () => { beforeEach(() => { jest.clearAllMocks(); + + jest + .spyOn(UtilsService, 'writeFileJson') + .mockImplementation(mockWriteFileJson); + jest.spyOn(UtilsService, 'fileExists').mockImplementation(mockFileExists); + + jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); }); it('should exit with correct status on success', async () => { - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler(fakePackageJson, true)({}); expect(process.exit).toHaveBeenCalledTimes(1); expect(process.exit).toHaveBeenCalledWith(0); }); it('should exit with correct status on failure', async () => { - await exitHandler(fakePackageJson, false); + await UtilsService.exitHandler(fakePackageJson, false)({}); expect(process.exit).toHaveBeenCalledTimes(1); expect(process.exit).toHaveBeenCalledWith(1); }); it('should write original package.json contents back to package.json if not otherwise specified', async () => { - mockIncludes.mockReturnValueOnce(false); - - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler( + fakePackageJson, + true, + )({ leaveChanges: false }); expect(mockWriteFileJson).toHaveBeenCalledTimes(1); expect(mockWriteFileJson).toHaveBeenCalledWith( WorkingFiles.PACKAGE_JSON, @@ -61,7 +54,10 @@ describe('exitHandler', () => { mockIncludes.mockReturnValueOnce(true); mockFileExists.mockReturnValueOnce(false); - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler( + fakePackageJson, + true, + )({ leaveChanges: true }); expect(mockWriteFileJson).toHaveBeenCalledTimes(1); expect(mockWriteFileJson).toHaveBeenCalledWith( WorkingFiles.PACKAGE_TEMP_JSON, @@ -73,7 +69,10 @@ describe('exitHandler', () => { mockIncludes.mockReturnValueOnce(true); mockFileExists.mockReturnValueOnce(true); - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler( + fakePackageJson, + true, + )({ leaveChanges: true }); expect(mockWriteFileJson).toHaveBeenCalledTimes(0); }); }); diff --git a/src/helpers/revert-json.spec.ts b/src/helpers/utils.service.spec.ts similarity index 52% rename from src/helpers/revert-json.spec.ts rename to src/helpers/utils.service.spec.ts index 20cab86..0ee5eed 100644 --- a/src/helpers/revert-json.spec.ts +++ b/src/helpers/utils.service.spec.ts @@ -3,21 +3,32 @@ import 'jest'; import { readFile, unlink } from 'fs'; import { promisify } from 'util'; -import { PackageJson } from '../injection-tokens'; -import { fileExists } from './file-exists'; -import { revertJson } from './revert-json'; -import { writeFileJson } from './write-file-json'; +import { PackageJson } from '../types'; +import { UtilsService } from './utils.service'; + +describe('[Util]: tests', () => { + it('Should write json file', async () => { + UtilsService.writeFileJson('package-test.json', { + dependencies: {}, + fireDependencies: {}, + fireConfig: {}, + }); + let isExists = await UtilsService.fileExists('package-test.json'); + expect(isExists).toBeTruthy(); + await promisify(unlink)('package-test.json'); + isExists = await UtilsService.fileExists('package-test.json'); + expect(isExists).toBeFalsy(); + }); -describe('[RevertJson]: tests', () => { it('Should revert package-temp.json', async () => { const testJsonFileName = 'package-temp2.json'; const testJsonToSave = 'package-temp3.json'; - writeFileJson(testJsonToSave, { + UtilsService.writeFileJson(testJsonToSave, { dependencies: { '@pesho/test': '0.0.1' }, fireDependencies: {}, fireConfig: {}, }); - writeFileJson(testJsonFileName, { + UtilsService.writeFileJson(testJsonFileName, { dependencies: { '@pesho/test': '0.0.1' }, fireDependencies: {}, fireConfig: {}, @@ -26,12 +37,12 @@ describe('[RevertJson]: tests', () => { await promisify(readFile)(testJsonFileName, { encoding: 'utf-8' }), ); expect(file.dependencies['@pesho/test']).toBe('0.0.1'); - revertJson(testJsonToSave, testJsonFileName); + UtilsService.revertJson(testJsonToSave, testJsonFileName); const modifiedJson: PackageJson = JSON.parse( await promisify(readFile)(testJsonToSave, { encoding: 'utf-8' }), ); expect(modifiedJson.dependencies['@pesho/test']).toBe('0.0.1'); await promisify(unlink)(testJsonToSave); - expect(await fileExists(testJsonToSave)).toBeFalsy(); + expect(await UtilsService.fileExists(testJsonToSave)).toBeFalsy(); }); }); diff --git a/src/helpers/utils.service.ts b/src/helpers/utils.service.ts new file mode 100644 index 0000000..2142203 --- /dev/null +++ b/src/helpers/utils.service.ts @@ -0,0 +1,207 @@ +import { spawn } from 'child_process'; +import { readFile, stat, unlink, writeFile, writeFileSync } from 'fs'; +import { join } from 'path'; +import { promisify } from 'util'; + +import { + Arguments, + DEFAULT_RUNNER, + DependenciesLink, + FireLinkConfig, + isWin, + PackageJson, + Signals, + Tasks, + WorkingFiles, +} from '../types'; +import { WorkerOptions } from '../types'; +import { ExitCodeError } from './custom-error'; + +export class UtilsService { + static writeFileJson(name: string, json: PackageJson) { + writeFileSync(join(process.cwd(), name), JSON.stringify(json, null, 2), { + encoding: 'utf-8', + }); + } + + static Worker = ( + { command, args, cwd }: WorkerOptions = { + command: 'npx', + + args: [], + }, + ): Promise => { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd: cwd || process.cwd() }); + child.stderr.pipe(process.stderr); + child.stdout.pipe(process.stdout); + child.on('close', (code: number) => { + if (code !== 0) { + return reject(!code); + } + return resolve(!code); + }); + }); + }; + + static runCommand(runner: string, args: string[]): Promise { + return UtilsService.Worker({ + command: isWin ? 'cmd' : 'npx', + args: [ + ...(isWin ? ['/c', 'npx'] : []), + runner, + ...args + .slice(2) + .filter((arg) => !Object.values(Tasks).includes(arg as Tasks)) + .filter((v) => v !== runner), + ], + }); + } + + static async revertJson(originalJson: string, tempJson: string) { + const json = await UtilsService.readJson(tempJson); + UtilsService.writeFileJson(originalJson, json); + await promisify(unlink)(tempJson); + } + + static async readJson(name: string, cwd: string = process.cwd()) { + return JSON.parse( + await promisify(readFile)(join(cwd, name), { + encoding: 'utf-8', + }), + ) as PackageJson; + } + + static async modifyJson( + packageJson: PackageJson, + dependencies: DependenciesLink[], + ) { + for (const { dep, folder } of dependencies) { + packageJson.dependencies[dep] = `file:${folder}`; + } + await promisify(writeFile)( + WorkingFiles.PACKAGE_JSON, + JSON.stringify(packageJson, null, 2), + { encoding: 'utf-8' }, + ); + } + + static async fileExists(filename: string) { + try { + await promisify(stat)(filename); + return true; + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } else { + throw err; + } + } + } + + static exitHandler(originalPackageJson: PackageJson, success: boolean) { + return async (args: Arguments) => { + if (!args.leaveChanges) { + UtilsService.writeFileJson( + WorkingFiles.PACKAGE_JSON, + originalPackageJson, + ); + } else if ( + !(await UtilsService.fileExists(WorkingFiles.PACKAGE_TEMP_JSON)) + ) { + UtilsService.writeFileJson( + WorkingFiles.PACKAGE_TEMP_JSON, + originalPackageJson, + ); + } + process.exit(success ? 0 : 1); + }; + } + + static createVirtualSymlink(packageJson: PackageJson = {} as PackageJson) { + return async (args: Arguments) => { + console.log(args); + packageJson.fireConfig = packageJson.fireConfig || ({} as FireLinkConfig); + let successStatus = false; + const runner = + args.runner || packageJson.fireConfig.runner || DEFAULT_RUNNER; + + if (args.revertChanges) { + return await UtilsService.revertJson( + WorkingFiles.PACKAGE_JSON, + WorkingFiles.PACKAGE_TEMP_JSON, + ); + } + + const originalPackageJson = JSON.parse(JSON.stringify(packageJson)); + + if (packageJson.fireDependencies) { + const linkedDepndencies = packageJson.fireDependencies; + const dependencies: DependenciesLink[] = Object.keys( + linkedDepndencies, + ).map((dep) => ({ + dep, + folder: linkedDepndencies[dep], + })); + + process.stdin.resume(); + const signals: Signals[] = [ + 'exit', + 'SIGINT', + 'SIGUSR1', + 'SIGUSR2', + 'uncaughtException', + ]; + signals.map((event) => + process.on(event as never, () => + UtilsService.exitHandler(originalPackageJson, successStatus)(args), + ), + ); + + await UtilsService.modifyJson(packageJson, dependencies); + } + + try { + if (args.bootstrap) { + await UtilsService.Worker({ command: 'npm', args: ['install'] }); + } + if (!args.skipRunner) { + await UtilsService.runCommand(runner, process.argv); + } + successStatus = true; + } catch (e) { + } finally { + await UtilsService.exitHandler( + originalPackageJson, + successStatus, + )(args); + } + process.stdin.pause(); + }; + } + + static lazy( + getActionFunc: () => Promise<(...args: never[]) => Promise>, + ): (...args: never[]) => Promise { + return async (...args: never[]) => { + try { + const actionFunc = await getActionFunc(); + await actionFunc(...args); + + process.exit(0); + } catch (error) { + UtilsService.exitWithError(error); + } + }; + } + + static exitWithError(error: Error): never { + if (error instanceof ExitCodeError) { + process.stderr.write(`\n${error.message}\n\n`); + process.exit(error.code); + } else { + process.stderr.write(`\n${error}\n\n`); + process.exit(1); + } + } +} diff --git a/src/helpers/worker.ts b/src/helpers/worker.ts deleted file mode 100644 index 934c270..0000000 --- a/src/helpers/worker.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { spawn } from 'child_process'; - -import { WorkerOptions } from '../injection-tokens'; - -export const Worker = ( - { command, args, cwd }: WorkerOptions = { - command: 'npx', - - args: [], - }, -): Promise => { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd: cwd || process.cwd() }); - child.stderr.pipe(process.stderr); - child.stdout.pipe(process.stdout); - child.on('close', (code: number) => { - if (code !== 0) { - return reject(!code); - } - return resolve(!code); - }); - }); -}; diff --git a/src/helpers/write-file-json.spec.ts b/src/helpers/write-file-json.spec.ts deleted file mode 100644 index 4f7e901..0000000 --- a/src/helpers/write-file-json.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import 'jest'; - -import { unlink } from 'fs'; -import { promisify } from 'util'; - -import { fileExists } from './file-exists'; -import { writeFileJson } from './write-file-json'; - -describe('[WriteFileJson]: tests', () => { - it('Should write json file', async () => { - writeFileJson('package-test.json', { - dependencies: {}, - fireDependencies: {}, - fireConfig: {}, - }); - let isExists = await fileExists('package-test.json'); - expect(isExists).toBeTruthy(); - await promisify(unlink)('package-test.json'); - isExists = await fileExists('package-test.json'); - expect(isExists).toBeFalsy(); - }); -}); diff --git a/src/helpers/write-file-json.ts b/src/helpers/write-file-json.ts deleted file mode 100644 index d5c93aa..0000000 --- a/src/helpers/write-file-json.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { writeFileSync } from 'fs'; -import { join } from 'path'; - -import { PackageJson } from '../injection-tokens'; - -export function writeFileJson(name: string, json: PackageJson) { - writeFileSync(join(process.cwd(), name), JSON.stringify(json, null, 2), { - encoding: 'utf-8', - }); -} diff --git a/src/index.ts b/src/index.ts index fa221d8..8b7c7fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export * from './create-virtual-symlink'; export * from './helpers'; -export * from './injection-tokens'; +export * from './types'; diff --git a/src/main.ts b/src/main.ts index 847908a..1e5728a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,11 @@ -import { createVirtualSymlink } from './create-virtual-symlink'; -import { readJson } from './helpers/read-json'; -import { - getOutFolder, - getPackagesFolderName, - WorkingFiles, -} from './injection-tokens'; +#! /usr/bin/env node +import { program } from 'commander'; -(async () => { - const packageJson = await readJson(WorkingFiles.PACKAGE_JSON); +import packageJson from '../package.json'; +import { commands } from './tasks'; - await createVirtualSymlink( - packageJson, - getOutFolder(packageJson), - getPackagesFolderName(packageJson), - ); -})(); +program.name(packageJson.name).version(packageJson.version); + +commands.map((command) => command(program)); + +program.parse(process.argv); diff --git a/src/tasks/default.ts b/src/tasks/default.ts new file mode 100644 index 0000000..1b20fab --- /dev/null +++ b/src/tasks/default.ts @@ -0,0 +1,7 @@ +import { UtilsService } from '../helpers'; +import { Arguments, Options } from '../types'; + +export default (options: Options) => async (args: Arguments) => + UtilsService.readJson(options.packageJsonName).then((packageJson) => + UtilsService.createVirtualSymlink(packageJson)(args), + ); diff --git a/src/tasks/index.ts b/src/tasks/index.ts new file mode 100644 index 0000000..7f9cfc1 --- /dev/null +++ b/src/tasks/index.ts @@ -0,0 +1,33 @@ +import { Command } from 'commander'; + +import { UtilsService } from '../helpers'; +import { Tasks, WorkingFiles } from '../types'; + +function mainTasks(program: Command) { + program + .allowUnknownOption() + .option( + Tasks.LEAVE_CHANGES, + 'modify package.json and create package-temp.json', + ) + .option(Tasks.REVERT, 'revert package.json from package-temp.json') + .option(Tasks.SKIP_RUNNER, 'do not run the script after finish') + .option( + `-r, ${Tasks.RUNNER} `, + 'do not run the script after finish', + ) + .option( + Tasks.BOOTSTRAP, + 'Change reference inside package.json dependencies to local and execute npm install after finish reverts all of the changes made to package.json', + ) + .description('https://github.com/rxdi/firelink') + .action( + UtilsService.lazy(() => + import('./default').then((module) => + module.default({ packageJsonName: WorkingFiles.PACKAGE_JSON }), + ), + ), + ); +} + +export const commands = [mainTasks]; diff --git a/src/injection-tokens.ts b/src/types.ts similarity index 51% rename from src/injection-tokens.ts rename to src/types.ts index 32727e1..c17ca1b 100644 --- a/src/injection-tokens.ts +++ b/src/types.ts @@ -1,25 +1,13 @@ export interface FireLinkConfig { runner?: string; - outFolderLocation?: string; - outFolderName?: string; - excludes?: string[]; - excludesFileName?: string; - useNativeCopy?: boolean; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface PackageJson extends Record { - dependencies: { [key: string]: string }; +export interface PackageJson { + dependencies: Record; fireConfig?: FireLinkConfig; - fireDependencies: { [key: string]: string }; + fireDependencies: Record; } -export const getPackagesFolderName = (packageJson: PackageJson) => - packageJson?.fireConfig?.outFolderName || '.packages'; - -export const getOutFolder = (packageJson: PackageJson) => - packageJson?.fireConfig?.outFolderLocation || '.'; - export const DEFAULT_RUNNER = 'firebase'; export enum WorkingFiles { @@ -35,9 +23,8 @@ export interface WorkerOptions { export enum Tasks { REVERT = '--revert-changes', - BUILD = '--buildCommand', LEAVE_CHANGES = '--leave-changes', - NO_RUNNER = '--no-runner', + SKIP_RUNNER = '--skip-runner', RUNNER = '--runner', BOOTSTRAP = '--bootstrap', } @@ -50,3 +37,15 @@ export interface DependenciesLink { export const isWin = process.platform === 'win32'; export type Signals = NodeJS.Signals | 'exit' | 'uncaughtException'; + +export interface Arguments { + runner?: string; + bootstrap?: boolean; + revertChanges?: boolean; + leaveChanges?: boolean; + skipRunner?: boolean; +} + +export interface Options { + packageJsonName: string; +} diff --git a/tsconfig.json b/tsconfig.json index 8fa51c6..a4e2c79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,13 +15,30 @@ "noFallthroughCasesInSwitch": true, "noImplicitAny": false, "noImplicitReturns": true, + "esModuleInterop": true, + "resolveJsonModule": true, "noImplicitThis": false, "noUnusedLocals": true, "noUnusedParameters": false, "outDir": "dist", - "lib": ["es2017", "es2016", "es2015", "es6", "dom", "esnext.asynciterable"], - "typeRoots": ["node_modules/@types"] + "lib": [ + "es2017", + "es2016", + "es2015", + "es6", + "dom", + "esnext.asynciterable" + ], + "typeRoots": [ + "node_modules/@types", + "typings.d.ts" + ] }, - "include": ["./src/**/*"], - "exclude": ["./node_modules", "./src/**/*.spec.ts"] -} + "include": [ + "./src/**/*" + ], + "exclude": [ + "./node_modules", + "./src/**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 0000000..f09ff14 --- /dev/null +++ b/typings.d.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare module '*.json' { + const value: any; + export default value; +}