diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f02983..88fa9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [1.0.0-beta-0](https://github.com/rxdi/firelink/compare/v0.9.2...v1.0.0-beta-0) (2023-03-07) + + +### Bug Fixes + +* removed namespacing and instead we use provided folder from fireDependencies the tool resolves now any package not only monorepo packages ([2184330](https://github.com/rxdi/firelink/commit/2184330fdc94c9ffe982940d98f24c7fa52ff161)) + + +### Features + +* **refactor:** whole logic is refactored to new one using commander ([06ec191](https://github.com/rxdi/firelink/commit/06ec1910b5785686303eed784acc2e9a40673980)) + + + ## [0.9.2](https://github.com/rxdi/firelink/compare/v0.9.1...v0.9.2) (2023-03-05) diff --git a/README.md b/README.md index 3d98cb5..be35411 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,26 @@ If the binaries don't work in `mac` or `windows` please install it via `npm` glo #### Usage +Bootstrapping the packages +It will run `npm install` inside the current working directory +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 --skip-runner +``` + +Deploying as usual + ```bash firelink deploy ``` -The same as `firebase deploy` the only differance is that it will COPY monorepos replace package.json > dependencies with appropriate local file structure and then will revert changes after `firebase` script exit +You can also use one liner replacement for `npm install` and `firebase deploy` + +```bash +firelink deploy --bootstrap +``` #### Leave changes to package.json after command execution exited @@ -133,19 +148,6 @@ Revert the changes made inside `package.json` firelink --no-runner --revert-changes ``` -#### Native Nodejs Copy instead of `rsync` - -This argument is introduced due to recent issue that has being made https://github.com/rxdi/firelink/issues/55 -It appears that in the newest nodejs 16 docker image rsync package is missing - -There is a way to specify which runner firelink will use -when specify `--use-native-copy` it will default to nodejs implementation of recursive copy the files -By default in windows environment this is the main method used to copy files since `rsync` is missing in windows - -``` -firelink --use-native-copy -``` - # Configuration Default runner is command `firebase` but you can change it for example to `gcloud` or `serverless` by defining `fireConfig` inside `package.json` @@ -153,30 +155,13 @@ Default runner is command `firebase` but you can change it for example to `gclou ```json { "fireConfig": { - "runner": "firebase", - "outFolderName": ".packages", - "outFolderLocation": ".", - "excludes": ["node_modules"], - "useNativeCopy": true + "runner": "firebase" } } ``` -`excludes` property can exclude some folders or files from being copied it accepts `Array` from `string` - -For example we may want to install dependencies of the packages when deploying using local npm path, -so we want to not waste time duplicating node modules - -Equivalent of excludes inside package.json `.fireignore` can be specified as a file inside a directory where the command `firelink` will be executed. `.fireignore` behaviour is the same as `.gitignore` - -If you do not wish to use `.fireignore` file name the name can be specified from `fireConfig.excludesFileName` - You can pass `--runner dir` argument to the command which will override the default runner `firebase` -By default packages will be saved in `.packages` folder and `current` directory will be used -U can change that by specifiyng properties `outFolderName` and `outFolderLocation` inside `fireConfig` -Check `example` folder inside this repository - You can put even `dir` command ```json diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..f5c229f --- /dev/null +++ b/build.mjs @@ -0,0 +1,16 @@ +import esbuild from 'esbuild'; + +esbuild + .build({ + entryPoints: ['./src/main.ts'], + bundle: true, + treeShaking: true, + platform: 'node', + target: 'node14.4', + outfile: './dist/main.js', + }) + .then((data) => console.log('SUCCESS', data)) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/example/package-lock.json b/example/package-lock.json index cdf3899..983e8c3 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -4,7 +4,7 @@ "lockfileVersion": 1, "dependencies": { "@test/dependency": { - "version": "file:.packages/dependency" + "version": "file:packages/dependency" } } -} +} \ No newline at end of file diff --git a/example/package.json b/example/package.json index 9509d1f..27fb36f 100644 --- a/example/package.json +++ b/example/package.json @@ -7,12 +7,6 @@ "@test/dependency": "./packages/dependency" }, "fireConfig": { - "runner": "npm", - "outFolderName": ".packages", - "outFolderLocation": ".", - "excludes": [ - "node_modules" - ], - "useNativeCopy": true + "runner": "firebase" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4a1670a..8d9e7ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@rxdi/firelink", - "version": "0.9.2", + "version": "1.0.0-beta-0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -462,6 +462,160 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@esbuild/android-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", + "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz", + "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz", + "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz", + "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz", + "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz", + "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz", + "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz", + "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz", + "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz", + "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz", + "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz", + "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz", + "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz", + "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz", + "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.11.tgz", + "integrity": "sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz", + "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz", + "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz", + "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz", + "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz", + "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz", + "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -1442,6 +1596,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", @@ -1804,6 +1964,36 @@ "is-arrayish": "^0.2.1" } }, + "esbuild": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz", + "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.11", + "@esbuild/android-arm64": "0.17.11", + "@esbuild/android-x64": "0.17.11", + "@esbuild/darwin-arm64": "0.17.11", + "@esbuild/darwin-x64": "0.17.11", + "@esbuild/freebsd-arm64": "0.17.11", + "@esbuild/freebsd-x64": "0.17.11", + "@esbuild/linux-arm": "0.17.11", + "@esbuild/linux-arm64": "0.17.11", + "@esbuild/linux-ia32": "0.17.11", + "@esbuild/linux-loong64": "0.17.11", + "@esbuild/linux-mips64el": "0.17.11", + "@esbuild/linux-ppc64": "0.17.11", + "@esbuild/linux-riscv64": "0.17.11", + "@esbuild/linux-s390x": "0.17.11", + "@esbuild/linux-x64": "0.17.11", + "@esbuild/netbsd-x64": "0.17.11", + "@esbuild/openbsd-x64": "0.17.11", + "@esbuild/sunos-x64": "0.17.11", + "@esbuild/win32-arm64": "0.17.11", + "@esbuild/win32-ia32": "0.17.11", + "@esbuild/win32-x64": "0.17.11" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", diff --git a/package.json b/package.json index 6d5c312..78649fb 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "@rxdi/firelink", - "version": "0.9.2", + "version": "1.0.0-beta-0", "bin": { "firelink": "./dist/main.js" }, "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 node build.mjs", "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", + "build-all": "npm run clean && npm run build-binary && rm -rf dist && npm run build", "test": "npx jest", "lint": "npx eslint . --ext .ts", "prepare": "husky install", @@ -16,6 +17,7 @@ "publish-package": "npm publish --update-readme --access public", "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -r 0", "ammend-changelog": "git commit --amend --no-edit", + "clean": "rm -rf binaries dist", "copy-binaries": "cp -r ./dist/firelink-linux ./binaries && cp -r ./dist/firelink-macos ./binaries && cp -r ./dist/firelink-win.exe ./binaries" }, "files": [ @@ -30,7 +32,9 @@ "@types/node": "^12.0.10", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "^5.41.0", + "commander": "^9.0.0", "conventional-changelog-cli": "^2.2.2", + "esbuild": "^0.17.11", "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/create-virtual-symlink.spec.ts b/src/create-virtual-symlink.spec.ts deleted file mode 100644 index d95e1a2..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 e605067..0000000 --- a/src/create-virtual-symlink.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { includes, nextOrDefault } from './helpers/args-extractors'; -import { buildPackages } from './helpers/build-packages'; -import { copyPackages } from './helpers/copy-packages'; -import { exitHandler } from './helpers/exit-handler'; -import { modifyJson } from './helpers/modify-json'; -import { readExcludes } from './helpers/read-excludes'; -import { revertJson } from './helpers/revert-json'; -import { runCommand } from './helpers/run-command'; -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; - - const excludes = [ - ...(packageJson.fireConfig.excludes || []), - ...(await readExcludes( - packageJson.fireConfig.excludesFileName || '.fireignore', - )), - ]; - - 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], - })); - await copyPackages(dependencies)(outFolder)(outFolderName)(excludes)( - includes(Tasks.USE_NATIVE_COPY) || packageJson.fireConfig.useNativeCopy, - ); - 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.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 68e5500..0000000 --- a/src/helpers/build-packages.ts +++ /dev/null @@ -1,24 +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), - }, - false, - ); - }), - ); -} diff --git a/src/helpers/copy-packages.ts b/src/helpers/copy-packages.ts deleted file mode 100644 index 370424d..0000000 --- a/src/helpers/copy-packages.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { join } from 'path'; - -import { DependenciesLink, isWin } from '../injection-tokens'; -import { FolderSync } from './copy-recursive'; -import { Worker } from './worker'; - -function copyOther( - folder: string, - outFolder: string, - outFolderName: string, - excludes: string[], -) { - return Worker({ - command: 'rsync', - args: [ - '-r', - '--force', - ...(excludes - ? excludes.reduce( - (prev, curr) => [...prev, '--exclude', curr], - [] as string[], - ) - : []), - folder, - `${outFolder}/${outFolderName}`, - ], - }); -} - -export function copyPackages(dependencies: DependenciesLink[]) { - return (outFolder: string) => - (outFolderName: string) => - (excludes: string[]) => - (nativeCopyEnabled?: boolean) => - Promise.all( - dependencies.map(({ folder }) => - nativeCopyEnabled || isWin - ? FolderSync.copyFolderRecursive( - folder, - join(outFolder, outFolderName), - ) - : copyOther(folder, outFolder, outFolderName, excludes), - ), - ); -} - -/* Cannot make it work to behave the same as rsync in windows for now */ - -// function copyWindows( -// folder: string, -// outFolder: string, -// outFolderName: string, -// excludes: string[], -// ) { -// console.log(folder); -// console.log([outFolder, outFolderName].join('\\')); -// return Worker({ -// command: 'cmd', -// args: [ -// '/c', -// 'robocopy', -// folder, -// [outFolder, outFolderName].join('\\'), -// '/s', -// '/e', -// '*.*', -// ...(excludes -// ? excludes.reduce( -// (prev, curr) => [...prev, '/xd', curr], -// [] as string[], -// ) -// : []), -// ], -// }); -// } diff --git a/src/helpers/copy-recursive.ts b/src/helpers/copy-recursive.ts deleted file mode 100644 index 6aad562..0000000 --- a/src/helpers/copy-recursive.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { lstat, mkdir, readdir, readFile, writeFile } from 'fs'; -import { basename, join } from 'path'; -import { promisify } from 'util'; - -import { fileExists } from './file-exists'; - -export class FolderSync { - public static async copyFile(source: string, target: string) { - let targetFile = target; - - if (await fileExists(target)) { - if ((await promisify(lstat)(target)).isDirectory()) { - targetFile = join(target, basename(source)); - } - } - - await promisify(writeFile)(targetFile, await promisify(readFile)(source)); - } - - public static async copyFolderRecursive(source: string, target: string) { - const targetFolder = join(target, basename(source)); - if (!(await fileExists(targetFolder))) { - await promisify(mkdir)(targetFolder, { recursive: true }); - } - - const sourceExists = await promisify(lstat)(source); - - if (sourceExists.isDirectory()) { - const files = await promisify(readdir)(source); - await Promise.all( - files.map(async (file) => { - const currrentSource = join(source, file); - const isCurrentExists = await this.isExists(currrentSource); - - if (isCurrentExists.isDirectory()) { - await this.copyFolderRecursive(currrentSource, targetFolder); - } else { - await this.copyFile(currrentSource, targetFolder); - } - }), - ); - } - return new Promise((resolve) => { - /** - * There is a time between notifiyng the OS for saving files and saving them actually. - * Lets wait 1 second for everything to finish and to be actually saved in the storage - */ - setTimeout(() => { - resolve(true); - }, 1000); - }); - } - - private static isExists(source: string) { - return promisify(lstat)(source); - } -} diff --git a/src/helpers/custom-error.ts b/src/helpers/custom-error.ts new file mode 100644 index 0000000..e92fa71 --- /dev/null +++ b/src/helpers/custom-error.ts @@ -0,0 +1,20 @@ +export class CustomError extends Error { + get name(): string { + return this.constructor.name; + } +} + +export class ExitCodeError extends CustomError { + readonly code: number; + + constructor(code: number, message?: string, command?: string) { + if (command) { + super( + `Command '${command}' exited with code ${code} and message "${message}"`, + ); + } else { + super(`Child exited with code ${code} and message "${message}"`); + } + 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-excludes.ts b/src/helpers/read-excludes.ts deleted file mode 100644 index 7f27c50..0000000 --- a/src/helpers/read-excludes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { readFile } from 'fs'; -import { promisify } from 'util'; - -import { parseIgnoredFiles } from './parse-ignore'; - -export async function readExcludes(file: string) { - try { - const ignore = await promisify(readFile)(file, { - encoding: 'utf-8', - }); - return parseIgnoredFiles(ignore); - } catch (e) { - return []; - } -} 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 e8deedf..0000000 --- a/src/helpers/run-command.ts +++ /dev/null @@ -1,23 +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) => - a !== Tasks.LEAVE_CHANGES && - a !== Tasks.REVERT && - a !== Tasks.BUILD, - ), - ], - }); -} 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..f0a83ff --- /dev/null +++ b/src/helpers/utils.service.create-virtual-symlink.spec.ts @@ -0,0 +1,76 @@ +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, fakePackageJson])( + {}, + ); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith( + [fakePackageJson, fakePackageJson], + true, + ); + }); + + it('should exit with error if final command fails', async () => { + mockRunCommand.mockImplementationOnce(() => Promise.reject(false)); + await UtilsService.createVirtualSymlink([fakePackageJson, fakePackageJson])( + {}, + ); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith( + [fakePackageJson, fakePackageJson], + false, + ); + }); + + it('should only revert json and exit if --revert-changes flag is present', async () => { + await UtilsService.createVirtualSymlink([fakePackageJson, 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, fakePackageJson])( + {}, + ); + expect(mockModifyJson).toHaveBeenCalledTimes(1); + expect(mockRunCommand).toHaveBeenCalledTimes(1); + expect(mockExitHandler).toHaveBeenCalledWith( + [fakePackageJson, fakePackageJson], + true, + ); + }); +}); diff --git a/src/helpers/exit-handler.spec.ts b/src/helpers/utils.service.exit-handler.spec.ts similarity index 51% rename from src/helpers/exit-handler.spec.ts rename to src/helpers/utils.service.exit-handler.spec.ts index 98ae5ac..93f0d49 100644 --- a/src/helpers/exit-handler.spec.ts +++ b/src/helpers/utils.service.exit-handler.spec.ts @@ -1,56 +1,55 @@ -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, 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, 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); - expect(mockWriteFileJson).toHaveBeenCalledTimes(1); + await UtilsService.exitHandler( + [fakePackageJson, fakePackageJson], + true, + )({ leaveChanges: false }); + expect(mockWriteFileJson).toHaveBeenCalledTimes(2); expect(mockWriteFileJson).toHaveBeenCalledWith( WorkingFiles.PACKAGE_JSON, fakePackageJson, @@ -61,7 +60,10 @@ describe('exitHandler', () => { mockIncludes.mockReturnValueOnce(true); mockFileExists.mockReturnValueOnce(false); - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler( + [fakePackageJson, fakePackageJson], + true, + )({ leaveChanges: true }); expect(mockWriteFileJson).toHaveBeenCalledTimes(1); expect(mockWriteFileJson).toHaveBeenCalledWith( WorkingFiles.PACKAGE_TEMP_JSON, @@ -73,7 +75,10 @@ describe('exitHandler', () => { mockIncludes.mockReturnValueOnce(true); mockFileExists.mockReturnValueOnce(true); - await exitHandler(fakePackageJson, true); + await UtilsService.exitHandler( + [fakePackageJson, 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..ccc0cc1 --- /dev/null +++ b/src/helpers/utils.service.ts @@ -0,0 +1,232 @@ +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 this.readJsonString(name, cwd)) as T; + } + + private static async readJsonString( + name: string, + cwd: string = process.cwd(), + ) { + try { + return await promisify(readFile)(join(cwd, name), { + encoding: 'utf-8', + }); + } catch (e) {} + return null; + } + + 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, originalPackageLockJson]: [PackageJson, PackageJson], + success: boolean, + ) { + return async (args: Arguments) => { + if (!args.leaveChanges) { + UtilsService.writeFileJson( + WorkingFiles.PACKAGE_JSON, + originalPackageJson, + ); + if (originalPackageLockJson) { + UtilsService.writeFileJson( + WorkingFiles.PACKAGE_LOCK_JSON, + originalPackageLockJson, + ); + } + } else if ( + !(await UtilsService.fileExists(WorkingFiles.PACKAGE_TEMP_JSON)) + ) { + UtilsService.writeFileJson( + WorkingFiles.PACKAGE_TEMP_JSON, + originalPackageJson, + ); + } + process.exit(success ? 0 : 1); + }; + } + + static createVirtualSymlink([packageJson, packageLockJson]: [ + PackageJson, + PackageJson, + ]) { + return async (args: Arguments) => { + 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, packageLockJson], + 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, packageLockJson], + 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 d57a2d2..0000000 --- a/src/helpers/worker.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { spawn } from 'child_process'; - -import { WorkerOptions } from '../injection-tokens'; - -export const Worker = ( - { command, args, cwd }: WorkerOptions = { - command: 'npx', - - args: [], - }, - log = true, -): Promise => { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd }); - if (log) { - 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/injection-tokens.ts b/src/injection-tokens.ts deleted file mode 100644 index 5c81b44..0000000 --- a/src/injection-tokens.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 }; - fireConfig?: FireLinkConfig; - fireDependencies: { [key: string]: string }; -} - -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 { - PACKAGE_JSON = 'package.json', - PACKAGE_TEMP_JSON = 'package.temp.json', -} - -export interface WorkerOptions { - command: string; - args?: string[]; - cwd?: string; -} - -export enum Tasks { - REVERT = '--revert-changes', - BUILD = '--buildCommand', - LEAVE_CHANGES = '--leave-changes', - NO_RUNNER = '--no-runner', - USE_NATIVE_COPY = '--use-native-copy', - RUNNER = '--runner', -} - -export interface DependenciesLink { - dep: string; - folder: string; -} - -export const isWin = process.platform === 'win32'; - -export type Signals = NodeJS.Signals | 'exit' | 'uncaughtException'; 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..0b046c0 --- /dev/null +++ b/src/tasks/default.ts @@ -0,0 +1,14 @@ +import { ExitCodeError, UtilsService } from '../helpers'; +import { Arguments, Options } from '../types'; + +export default (options: Options) => async (args: Arguments) => + Promise.all([ + UtilsService.readJson(options.packageJsonName), + UtilsService.readJson(options.packageLockJsonName), + ]).then((tuple) => { + const [packageJson] = tuple; + if (!packageJson) { + throw new ExitCodeError(1, `Missing package.json`); + } + return UtilsService.createVirtualSymlink(tuple)(args); + }); diff --git a/src/tasks/index.ts b/src/tasks/index.ts new file mode 100644 index 0000000..c4d4c44 --- /dev/null +++ b/src/tasks/index.ts @@ -0,0 +1,36 @@ +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, + packageLockJsonName: WorkingFiles.PACKAGE_LOCK_JSON, + }), + ), + ), + ); +} + +export const commands = [mainTasks]; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a9f9d00 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,53 @@ +export interface FireLinkConfig { + runner?: string; +} + +export interface PackageJson { + dependencies: Record; + fireConfig?: FireLinkConfig; + fireDependencies: Record; +} + +export const DEFAULT_RUNNER = 'firebase'; + +export enum WorkingFiles { + PACKAGE_JSON = 'package.json', + PACKAGE_LOCK_JSON = 'package-lock.json', + PACKAGE_TEMP_JSON = 'package.temp.json', +} + +export interface WorkerOptions { + command: string; + args?: string[]; + cwd?: string; +} + +export enum Tasks { + REVERT = '--revert-changes', + LEAVE_CHANGES = '--leave-changes', + SKIP_RUNNER = '--skip-runner', + RUNNER = '--runner', + BOOTSTRAP = '--bootstrap', +} + +export interface DependenciesLink { + dep: string; + folder: string; +} + +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; + packageLockJsonName: 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; +}