├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── package-sync └── package-sync.yml ├── package.json ├── scripts ├── build.js ├── create-standalone-archive.sh └── verify-build.sh ├── src ├── Application.ts ├── Configuration.ts ├── commands │ ├── Analyze.ts │ ├── Command.ts │ ├── Fix.ts │ ├── ListFixers.ts │ ├── PullPackage.ts │ └── PullTemplate.ts ├── comparisons │ ├── Comparison.ts │ ├── ComposerPackagesComparison.ts │ ├── ComposerScriptsComparison.ts │ ├── ExtraFilesComparison.ts │ ├── FileExistsComparison.ts │ ├── FileSizeComparison.ts │ └── StringComparison.ts ├── fixers │ ├── DirectoryNotFoundFixer.ts │ ├── FileDoesNotMatchFixer.ts │ ├── FileNotFoundFixer.ts │ ├── Fixer.ts │ ├── FixerManager.ts │ ├── FixerRepository.ts │ ├── GithubFixer.ts │ ├── MergeFilesFixer.ts │ ├── OptionalPackagesFixer.ts │ ├── OverwriteFileFixer.ts │ ├── PackageNotUsedFixer.ts │ ├── PackageScriptNotFoundFixer.ts │ ├── PackageVersionFixer.ts │ └── PsalmFixer.ts ├── index.ts ├── lib │ ├── File.ts │ ├── FileMerger.ts │ ├── GitBranch.ts │ ├── GitCommandResult.ts │ ├── GitUtilties.ts │ ├── LineMerger.ts │ ├── composer │ │ └── Composer.ts │ └── helpers.ts ├── printers │ └── ConsolePrinter.ts ├── repositories │ ├── Repository.ts │ ├── RepositoryFile.ts │ ├── RepositoryIssue.ts │ └── RepositoryValidator.ts └── types │ ├── ComparisonScoreRequirements.ts │ ├── FileComparisonResult.ts │ ├── FileScoreRequirements.ts │ └── ScoreRequirements.ts └── tsconfig.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | 6 | # npx --no-install lint-staged 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.github 3 | /.husky 4 | /.vscode 5 | /coverage 6 | /dist/temp 7 | /node_modules 8 | /scripts 9 | /src 10 | /tests 11 | .gitattributes 12 | .gitignore 13 | .editorconfig 14 | .eslintrc.js 15 | .eslintrc.test.js 16 | .prettierignore 17 | .prettierrc 18 | jest.config.js 19 | package-lock.json 20 | tsconfig.json 21 | *.sh 22 | *.txt 23 | *.ignore 24 | 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | build/* 4 | *.yml 5 | *.yaml 6 | tests/**/*.json 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `package-sync` will be documented in this file. 4 | 5 | --- 6 | 7 | # 1.3.2 - 2022-10-20 8 | 9 | - update deps 10 | 11 | ## 1.3.1 - unreleased 12 | 13 | - fix config file object references (#14) 14 | - update deps 15 | 16 | ## 1.3.0 - unreleased 17 | 18 | - fix `dist/package-sync` script to work better with `npm` and `npx` 19 | 20 | ## 1.2.0 - 2021-04-08 21 | 22 | - add config options to define the values used when replacing template variables 23 | 24 | ## 1.1.0 - 2021-04-08 25 | 26 | - add config option to define the package vendor name 27 | - use package vendor name when pulling packages 28 | - add `--config ` flag to `analyze` and `fix` commands 29 | - minor fixes 30 | 31 | ## 1.0.1 - 2021-04-08 32 | 33 | - change npm package name 34 | - fix npm bin script 35 | 36 | ## 1.0.0 - 2021-04-08 37 | 38 | - initial release 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2021 Permafrost Development 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [](https://supportukrainenow.org) 3 | 4 |

5 | image 6 |

7 | 8 | # package-sync 9 | 10 | ![version](https://shields.io/npm/v/package-sync-cli) ![license](https://shields.io/github/license/spatie/package-sync) ![downloads](https://shields.io/npm/dt/package-sync-cli?logo=npm) ![Run Tests](https://github.com/spatie/package-sync/actions/workflows/run-tests.yml/badge.svg) 11 | 12 | --- 13 | 14 | `package-sync` helps package repository maintainers keep their package files and settings in sync with a skeleton/template repository. 15 | 16 | compares the contents of a package repo against a package skeleton repo, displaying out-of-sync files and other issues. 17 | 18 | ## Requirements 19 | 20 | `package-sync` requires: 21 | - `node v12+` 22 | - `git` 23 | 24 | ## Installation 25 | 26 | You can install this application using npm: 27 | 28 | ```bash 29 | npm install package-sync-cli 30 | 31 | ./node_modules/.bin/package-sync analyze array-to-xml 32 | ``` 33 | 34 | or run using `npx`: 35 | 36 | ```bash 37 | npx package-sync-cli analyze array-to-xml 38 | 39 | # with a specific config file 40 | npx package-sync-cli --config myconfig.yml analyze array-to-xml 41 | ``` 42 | 43 | ## Standalone releases 44 | 45 | Each release also has a standalone version released as an archive that can be extracted and run without the need for `npm`, `npx`, or `git clone`. See the [latest release](https://github.com/spatie/package-sync/releases/latest) to download the most recent standalone version. 46 | 47 | ## Local Setup 48 | 49 | If you instead prefer to clone the repository, clone with `git clone` and then run: 50 | 51 | ```bash 52 | npm install 53 | 54 | npm run build:prod 55 | 56 | ./dist/package-sync --help 57 | ``` 58 | 59 | ## Configuration 60 | 61 | Make sure you've modified the configuration file `dist/package-sync.yml`, specifically the `paths.packages` and `paths.templates` settings. 62 | If the directories don't exist, they will be created for you when you run `package-sync`. 63 | 64 | You can use the placeholder `{{__dirname}}` in the values of either setting and it will be replaced with the directory that the config file is in. 65 | 66 | > Make sure to quote the yaml value if you use the `{{__dirname}}` placeholder to ensure valid YAML. 67 | 68 | To configure the github user/organization name packages are pulled from, set the `config.packages.vendor` key. 69 | 70 | > Not maintaining Spatie packages? make sure to customize the `config.packages.email` and `config.packages.homepage` properties. 71 | 72 | See the configuration file comments for more information on the various options. 73 | 74 | ## Analyzing packages 75 | 76 | After running an analysis, you'll see a list of issues discovered with the package repository and the fixes available for each issue _(not all issues have automated fixes available)_. Issues include out-of-sync files, missing composer dependencies, required version updates, missing files and more. 77 | 78 | Analyze the `spatie/array-to-xml` package using the `spatie/package-skeleton-php` repository as a template: 79 | 80 | ```bash 81 | ./dist/package-sync analyze array-to-xml 82 | ``` 83 | 84 | ![image](https://user-images.githubusercontent.com/5508707/113942438-c808a900-97ce-11eb-8546-4d160ccd3e58.png) 85 | 86 | Fixers are color-coded: 87 | 88 | - `green`: considered safe to run without major file changes 89 | - `blue`: 'multi' fixers run safe fixers on groups of related files _(such as all psalm-related files, etc.)_ 90 | - `red`: considered risky - these fixers make _(possibly significant)_ modifications to existing files 91 | 92 | #### Issue Scores 93 | 94 | The values in the `score` column indicate how similar the package copy of a file is to the skeleton's copy. 95 | 96 | For decimal values, the closer to `1.0`, the more similar the files are: `0.75` means the files are somewhat similar, and `0.89` means they are very similar. 97 | Percentages indicate how different the files are: a value of `8.5%` would mean the files are fairly similar, differing by only that much. 98 | 99 | If an issue lists a filename but no value, the file only exists in either the skeleton or the package, but not both. 100 | 101 | Dependency version issues list a score of `major` or `minor`, indicating the part of the semver version that needs to be upgraded. 102 | 103 | Other issues not related to files, such as missing dependencies, do not have a score. 104 | 105 | ## Fixing package issues 106 | 107 | Issues are resolved by `fixers`, which are utilities that perform a various action, such as copying a missing file from the skeleton to the package repository. The available fixers for an issue are listed in the output of the `analyze` command. 108 | 109 | > If there are multiple fixers listed for an issue, the first one is used when running `fix`. 110 | 111 | Some fixers are considered "risky" - meaning they modify existing files. By default, these fixers will not run automatically when running the `fix` command. In order to permit risky fixers to run, you must call `fix` with the `--risky` flag. 112 | 113 | You can apply all fixers to the discovered issues with the `fix` command. 114 | 115 | You may specify both the fixer name and the `--file` flag to apply a fixer to the given file. 116 | 117 | ```bash 118 | # fix all issues except for those with "risky" fixes 119 | ./dist/package-sync fix array-to-xml 120 | 121 | # fix all issues 122 | ./dist/package-sync fix array-to-xml --risky 123 | 124 | # only fix a specific file 125 | ./dist/package-sync fix array-to-xml --file psalm.xml.dist 126 | 127 | # apply the 'psalm' fixer to only the specified filename 128 | ./dist/package-sync fix array-to-xml psalm --file psalm.xml.dist 129 | ``` 130 | ![image](https://user-images.githubusercontent.com/5508707/113923782-f37f9980-97b6-11eb-8b29-9c6ae04c6e03.png) 131 | 132 | Fix only certian issue types: 133 | 134 | ```bash 135 | ./dist/package-sync fix array-to-xml missing_pkg 136 | ``` 137 | 138 | Run a specific fixer by name: 139 | 140 | ```bash 141 | ./dist/package-sync fix array-to-xml psalm 142 | ``` 143 | 144 | ![image](https://user-images.githubusercontent.com/5508707/113923468-91bf2f80-97b6-11eb-807d-cfaee1b107af.png) 145 | 146 | Apply a specific fixer to a specific file: 147 | 148 | ```bash 149 | ./dist/package-sync fix array-to-xml psalm --file psalm.xml.dist 150 | ``` 151 | 152 | ![image](https://user-images.githubusercontent.com/5508707/113930020-c1723580-97be-11eb-9c02-be3b94cf033b.png) 153 | 154 | ### Fixers 155 | 156 | | name | note | description | 157 | | --- | --- | --- | 158 | | `add-dep` | | adds a dependency to the package's composer.json file. | 159 | | `bump-version` | | updates the version of a dependency in the package repository. | 160 | | `copy-script` | | adds a missing composer script to the package's composer.json file. | 161 | | `create-dir` | | creates a missing directory | 162 | | `create-file` | | copies a file from the skeleton repository into the package repository. | 163 | | `github` | multi | recreates all missing directories and files under the '.github' directory. | 164 | | `merge-files` | risky | merges the contents of both the skeleton and package versions of a file. | 165 | | `overwrite-file` | risky | overwrite a file with the skeleton version to force an exact match. | 166 | | `psalm` | multi | creates all missing psalm-related files and installs all psalm composer scripts and dependencies. | 167 | | `rewrite-file` | risky | overwrites an existing file with a newer version from the skeleton. | 168 | | `skip-dep` | | skips the installation of a dependency. | 169 | 170 | ## Manually pulling repositories 171 | 172 | You can manually update your local copy of either a skeleton or package git repository. If the repository already exists locally, the `pull-*` commands will run `git pull` instead of `git clone`. 173 | 174 | > It's usually not necessary to run `pull-*` commands manually 175 | 176 | > Repositories are cloned/updated automatically when running `analyze` or `fix`. 177 | 178 | ```bash 179 | # pull an individual skeleton repo by name: 180 | ./dist/package-sync pull-template php 181 | ./dist/package-sync pull-template laravel 182 | 183 | # or pull all skeleton repos: 184 | ./dist/package-sync pull-template 185 | ``` 186 | 187 | ```bash 188 | # pull the spatie/array-to-xml package repo 189 | ./dist/package-sync pull-package array-to-xml 190 | 191 | # pull spatie/laravel-sluggable 192 | ./dist/package-sync pull-package laravel-sluggable 193 | ``` 194 | 195 | ## Commands 196 | 197 | | Command | Aliases | Description | 198 | | --- | --- | --- | 199 | | `analyze ` | `a`, `an` | Compare a package against a template/skeleton repository | 200 | | `fix [type]` | _--_ | Fix a package's issues, optionally only fixing issues of the specified type | 201 | | `fixers` | _--_ | List all fixers and their descriptions | 202 | | `pull-template [name]` | `pt` | Update/retrieve the named skeleton repository, or all if no name specified | 203 | | `pull-package ` | `pp` | Update/retrieve the named package repository | 204 | 205 | ## Testing 206 | 207 | `package-sync` uses Jest for unit tests. To run the test suite: 208 | 209 | ```bash 210 | npm run test 211 | ``` 212 | 213 | --- 214 | 215 | ## Changelog 216 | 217 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 218 | 219 | ## Contributing 220 | 221 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 222 | 223 | ## Security Vulnerabilities 224 | 225 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 226 | 227 | ## Credits 228 | 229 | - [Patrick Organ](https://github.com/patinthehat) 230 | - [All Contributors](../../contributors) 231 | 232 | ## License 233 | 234 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 235 | -------------------------------------------------------------------------------- /dist/package-sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | THISFILE=$(realpath $0) 4 | THISDIR=$(dirname $THISFILE) 5 | NODE=$(which node) 6 | 7 | # support for being run with npx or npm 8 | if [ -f "$THISDIR/../package-sync-cli/dist/package-sync.js" ]; then 9 | $NODE "$THISDIR/../package-sync-cli/dist/package-sync.js" $* 10 | exit $? 11 | fi 12 | 13 | # support for running build locally 14 | if [ -e "$THISDIR/package-sync.js" ]; then 15 | $NODE "$THISDIR/package-sync.js" $* 16 | exit $? 17 | fi 18 | -------------------------------------------------------------------------------- /dist/package-sync.yml: -------------------------------------------------------------------------------- 1 | config: 2 | 3 | paths: 4 | templates: '{{__dirname}}/temp/spatie/templates' 5 | packages: '{{__dirname}}/temp/spatie/packages' 6 | 7 | fixers: 8 | # list of disabled fixer names; items can be either a name of a fixer or a wildcard match. 9 | disabled: 10 | - rewrite-file 11 | 12 | git: 13 | branches: 14 | default: main 15 | format: 'package-sync-{{hash}}' 16 | createOn: [fix] 17 | 18 | issues: 19 | ignored: 20 | # don't install these dependencies: 21 | missing_pkg: 22 | - spatie/ray 23 | - friendsofphp/php-cs-fixer 24 | # don't copy these composer scripts: 25 | pkg_script: 26 | - format 27 | # don't list 'extra_file' issues with these filenames: 28 | extra_file: 29 | - .scrutinizer.yml 30 | 31 | packages: 32 | vendor: spatie 33 | email: freek@spatie.be 34 | homepage: https://spatie.be/open-source/support-us 35 | 36 | templates: 37 | # github organization name 38 | vendor: spatie 39 | # skeleton repositories, must be named *-php and *-laravel. 40 | # the laravel skeleton is used automatically if the package being analyzed or fixed 41 | # starts with 'laravel-', otherwise the php skeleton is used. 42 | names: 43 | - package-skeleton-php 44 | - package-skeleton-laravel 45 | 46 | # similiar and size score requirements for determining if a file is out of sync. 47 | # for similar scores, the closer to 1.0 the value is, the more a file must match with 1.0 being an exact match. 48 | # for size scores, the value is the percent difference in terms of file size, so the closer to 0 the value is, 49 | # the more a file must match. 50 | # 51 | # these scores are used together because a file can be considered fairly similar but still be out of sync. for example, if 52 | # a few lines were added to the skeleton .gitignore, a package's gitignore might be considered similar but would be listed as 53 | # out of sync due to the size difference. 54 | scoreRequirements: 55 | defaults: 56 | similar: 0.75 57 | size: 10 58 | files: 59 | - name: CONTRIBUTING.md 60 | scores: 61 | similar: 0.95 62 | size: 5 63 | - name: .editorconfig 64 | scores: 65 | similar: 1 66 | size: 5 67 | - name: .gitattributes 68 | scores: 69 | similar: 0.8 70 | size: 8 71 | - name: .gitignore 72 | scores: 73 | similar: 0.8 74 | size: 8 75 | - name: .php_cs.dist 76 | scores: 77 | similar: 1 78 | size: 5 79 | 80 | # list of filenames that should not be compared during analysis 81 | skipComparisons: 82 | - composer.json 83 | - README.md 84 | 85 | ignoreNames: 86 | - .git/* 87 | - .idea/* 88 | - .vscode/* 89 | - '*/.gitkeep' 90 | - README.md 91 | - CHANGELOG.md 92 | - art/**/* 93 | - (art) 94 | - build/**/* 95 | - (build) 96 | - composer.lock 97 | - config/**/* 98 | - (config) 99 | - coverage/ 100 | - (coverage) 101 | - database/**/* 102 | - (database) 103 | - docs/**/* 104 | - (docs) 105 | - node_modules/ 106 | - package-lock.json 107 | - resources/**/* 108 | - (resources) 109 | - src/**/* 110 | - (src) 111 | - stubs/* 112 | - (stubs) 113 | - tests/**/* 114 | - (tests) 115 | - vendor/* 116 | - (vendor) 117 | - '*.sh' 118 | - '*.cache' 119 | - '*.lock' 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-sync-cli", 3 | "version": "1.3.2", 4 | "description": "keep package repositories in sync with a master template repository", 5 | "author": "Patrick Organ ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/spatie/package-sync", 8 | "keywords": [ 9 | "spatie", 10 | "packages", 11 | "synchronize" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/spatie/package-sync.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/spatie/package-sync/issues" 19 | }, 20 | "main": "dist/package-sync.js", 21 | "scripts": { 22 | "test": "./node_modules/.bin/jest tests --verbose", 23 | "test:coverage": "./node_modules/.bin/jest tests --coverage", 24 | "fmt": "./node_modules/.bin/prettier --config .prettierrc --write 'src/**/*.{js,ts,json,yml,yaml}' 'tests/**/*.{js,ts,json,yml,yaml}' './*.{js,yml,yaml}' 'dist/*.{json,yml,yaml}'", 25 | "lint": "./node_modules/.bin/eslint --ext ts,js src/", 26 | "lint:fix": "./node_modules/.bin/eslint --ext ts,js --fix src/", 27 | "lint:fix-tests": "./node_modules/.bin/eslint --ext ts,js --config ./.eslintrc.test.js --fix tests/", 28 | "lint:staged": "./node_modules/.bin/lint-staged", 29 | "fix": "npm run fmt && ./node_modules/.bin/concurrently npm:lint:fix npm:lint:fix-tests", 30 | "build:dev": "node scripts/build.js --development", 31 | "build:prod": "node scripts/build.js --production", 32 | "build:verify": "./scripts/verify-build.sh", 33 | "dev": "npm run build:dev && node dist/package-sync.js", 34 | "prepare": "is-ci || husky install", 35 | "check:circular": "npx madge --circular --extensions ts ./src", 36 | "preversion": "npm run test", 37 | "postversion": "npm run build:prod" 38 | }, 39 | "lint-staged": { 40 | "tests/**/*.{js,ts}": [ 41 | "./node_modules/.bin/prettier --config .prettierrc --write", 42 | "./node_modules/.bin/eslint --ext ts,js --config ./.eslintrc.test.js --fix" 43 | ], 44 | "*.{js,ts}": [ 45 | "./node_modules/.bin/prettier --config .prettierrc --write", 46 | "./node_modules/.bin/eslint --ext ts,js --fix" 47 | ], 48 | "*.{json,yaml,yml}": [ 49 | "./node_modules/.bin/prettier --config .prettierrc --write" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@types/jest": "^29.2.0", 54 | "@types/node": "^22.5.5", 55 | "@types/yargs": "^17.0.13", 56 | "@typescript-eslint/eslint-plugin": "^8.16.0", 57 | "@typescript-eslint/parser": "^5.40.1", 58 | "better-strip-color": "^1.0.2", 59 | "concurrently": "^8.0.1", 60 | "esbuild": "^0.23.1", 61 | "eslint": "^8.25.0", 62 | "husky": "^9.0.11", 63 | "is-ci": "^3.0.1", 64 | "jest": "^29.2.1", 65 | "lint-staged": "^15.2.2", 66 | "prettier": "^3.1.0", 67 | "prettier-eslint-cli": "^6.0.1", 68 | "ts-jest": "^29.0.3", 69 | "typescript": "^4.8" 70 | }, 71 | "dependencies": { 72 | "chalk": "^4.1.2", 73 | "cli-table3": "^0.6.3", 74 | "js-yaml": "^4.1.0", 75 | "micromatch": "^4.0.5", 76 | "semver": "^7.3.8", 77 | "string-similarity": "^4.0.4", 78 | "yargs": "^17.6.0" 79 | }, 80 | "bin": { 81 | "package-sync": "dist/package-sync" 82 | }, 83 | "engines": { 84 | "node": ">=14.0.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const { realpathSync, existsSync, statSync } = require('fs'); 2 | const { chdir } = require('process'); 3 | const { sep } = require('path'); 4 | 5 | const args = process.argv.slice(2); 6 | const baseDir = realpathSync(`${__dirname}/..`); 7 | const pkg = require(`${baseDir}/package.json`); 8 | 9 | function fileSize(fn) { 10 | if (!existsSync(fn)) { 11 | return `0.0 kb`; 12 | } 13 | const stat = statSync(fn); 14 | return ((stat.isFile() ? stat.size : 0.0) / 1024).toFixed(1) + ' kb'; 15 | } 16 | 17 | let options = { 18 | minify: process.env.NODE_ENV === 'production', 19 | quiet: false, 20 | }; 21 | 22 | if (args.includes('--prod') || args.includes('--production')) { 23 | options.minify = true; 24 | } 25 | 26 | if (args.includes('--dev') || args.includes('--development')) { 27 | options.minify = false; 28 | } 29 | 30 | if (args.includes('--quiet')) { 31 | options.quiet = true; 32 | } 33 | 34 | chdir(baseDir); 35 | 36 | if (typeof pkg['main'] === 'undefined') { 37 | pkg['main'] = 'dist/package-sync.js'; 38 | } 39 | 40 | const buildConfig = { 41 | entryPoints: [`${baseDir}/src/index.ts`], 42 | bundle: true, 43 | outfile: `${baseDir}/${pkg.main}`, 44 | write: true, 45 | platform: 'node', 46 | format: 'cjs', 47 | target: ['node14'], 48 | define: { 49 | __APP_VERSION__: `"${pkg.version}"`, 50 | }, 51 | logLevel: 'error', 52 | minify: options.minify, 53 | }; 54 | 55 | const start = new Date() 56 | .getTime(); 57 | const result = require('esbuild') 58 | .buildSync(buildConfig); 59 | const elapsed = new Date() 60 | .getTime() - start; 61 | 62 | if (typeof result['errors'] !== 'undefined' && result.errors.length) { 63 | if (!options.quiet) { 64 | console.log('* There were errors while building. Failed.'); 65 | console.log(result['errors']); 66 | } 67 | 68 | process.exit(1); 69 | } 70 | 71 | if (!options.quiet) { 72 | console.log( 73 | `* Build completed in ${elapsed}ms (${buildConfig.outfile.replace(baseDir + sep, '')} - ${fileSize(buildConfig.outfile)})`, 74 | ); 75 | console.log( 76 | `* Version: ${buildConfig.define.__APP_VERSION__.replace(/"/g, '')} (${options.minify ? 'production' : 'development'})`, 77 | ); 78 | console.log(''); 79 | } 80 | -------------------------------------------------------------------------------- /scripts/create-standalone-archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | THISDIR=$(dirname $0) 4 | PROJECTDIR=$(realpath "$THISDIR/..") 5 | 6 | PKGVERSION=$(egrep -o '"version":\s*"[0-9\.]+"' "$PROJECTDIR/package.json" | egrep -o '[0-9\.]+') 7 | 8 | pushd $PROJECTDIR 9 | 10 | npm run test 11 | 12 | if [ $? -ne 0 ]; then 13 | echo 'tests failed, not creating archive' 14 | exit 1 15 | fi 16 | 17 | npm run build:prod 18 | 19 | tar czf package-sync-standalone-$PKGVERSION.tar.gz --exclude=dist/temp dist 20 | 21 | popd 22 | -------------------------------------------------------------------------------- /scripts/verify-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | THISDIR=$(dirname $0) 4 | PROJECTDIR=$(realpath "$THISDIR/..") 5 | 6 | PKGVERSION=$(egrep -o '"version":\s*"[0-9\.]+"' "$PROJECTDIR/package.json" | egrep -o '[0-9\.]+') 7 | 8 | cd $PROJECTDIR 9 | 10 | npm run build:prod 11 | 12 | if [ $? -ne 0 ]; then 13 | echo "build process failed" 14 | exit 1 15 | fi 16 | 17 | VERSIONOUTPUT=$(node "$PROJECTDIR/dist/package-sync.js" --version) 18 | 19 | if [ $? -ne 0 ]; then 20 | echo "failed to run package-sync.js" 21 | exit 1 22 | fi 23 | 24 | if [ "$VERSIONOUTPUT" != "$PKGVERSION" ]; then 25 | echo "version check failed" 26 | exit 1 27 | fi 28 | 29 | echo "* all checks successful." 30 | -------------------------------------------------------------------------------- /src/Application.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import { existsSync, mkdirSync } from 'fs'; 5 | import { ComposerPackagesComparison } from './comparisons/ComposerPackagesComparison'; 6 | import { ComposerScriptsComparison } from './comparisons/ComposerScriptsComparison'; 7 | import { ExtraFilesComparison } from './comparisons/ExtraFilesComparison'; 8 | import { FileExistsComparison } from './comparisons/FileExistsComparison'; 9 | import { FileSizeComparison } from './comparisons/FileSizeComparison'; 10 | import { StringComparison } from './comparisons/StringComparison'; 11 | import { Configuration } from './Configuration'; 12 | import { FixerRepository } from './fixers/FixerRepository'; 13 | import { Repository, RepositoryKind } from './repositories/Repository'; 14 | import { RepositoryValidator } from './repositories/RepositoryValidator'; 15 | 16 | export class Application { 17 | public configuration: Configuration; 18 | 19 | constructor(configuration: Configuration | null = null) { 20 | this.configuration = configuration ?? new Configuration(); 21 | 22 | this.ensureStoragePathsExist(); 23 | } 24 | 25 | get config() { 26 | return this.configuration.conf; 27 | } 28 | 29 | public loadConfigFile(filename: string | null) { 30 | if (this.configuration.filename === filename || filename === null) { 31 | return this; 32 | } 33 | 34 | return this.useConfig(new Configuration(filename ?? this.configuration.filename)); 35 | } 36 | 37 | public useConfig(configuration: Configuration) { 38 | this.configuration = configuration; 39 | 40 | return this; 41 | } 42 | 43 | public ensureStoragePathsExist() { 44 | if (!existsSync(this.configuration.conf.paths.templates)) { 45 | mkdirSync(this.configuration.conf.paths.templates, { recursive: true }); 46 | } 47 | if (!existsSync(this.configuration.conf.paths.packages)) { 48 | mkdirSync(this.configuration.conf.paths.packages, { recursive: true }); 49 | } 50 | } 51 | 52 | compareRepositories(skeleton: Repository, repo: Repository) { 53 | const comparisons = { 54 | // FileSizeComparison should be last to prioritize StringComparison 55 | files: [FileExistsComparison, StringComparison, FileSizeComparison], 56 | other: [ExtraFilesComparison, ComposerScriptsComparison, ComposerPackagesComparison], 57 | }; 58 | 59 | skeleton.files.forEach(file => { 60 | const repoFile = repo.getFile(file.relativeName); 61 | 62 | for (const comparisonClass of comparisons.files) { 63 | const comparison = comparisonClass.create(skeleton, repo, file, repoFile); 64 | 65 | comparison.compare(null); 66 | 67 | if (!comparison.passed()) { 68 | return; 69 | } 70 | } 71 | }); 72 | 73 | comparisons.other.forEach((comparisonClass: any) => { 74 | comparisonClass.create(skeleton, repo, null, null) 75 | .compare(null); 76 | }); 77 | } 78 | 79 | analyzePackage(packageName: string) { 80 | const skeletonType = packageName.startsWith('laravel-') ? 'laravel' : 'php'; 81 | const templateName = this.configuration.getFullTemplateName(skeletonType); 82 | 83 | const validator = new RepositoryValidator(this.config.paths.packages, this.config.paths.templates); 84 | 85 | validator.ensurePackageExists(packageName); 86 | validator.ensureTemplateExists(templateName); 87 | 88 | const skeleton = Repository.create(this.configuration.templatePath(templateName), RepositoryKind.SKELETON); 89 | const repo = Repository.create(this.configuration.packagePath(packageName), RepositoryKind.PACKAGE); 90 | 91 | return this.analyzeRepository(skeleton, repo); 92 | } 93 | 94 | analyzeRepository(skeleton: Repository, repo: Repository) { 95 | this.compareRepositories(skeleton, repo); 96 | 97 | repo.issues.forEach(issue => { 98 | FixerRepository.all() 99 | .forEach(fixer => { 100 | if (fixer.fixes(issue.kind) && fixer.canFix(issue) && !this.configuration.shouldIgnoreIssue(issue)) { 101 | issue.addFixer(new fixer(issue)); 102 | } 103 | }); 104 | }); 105 | 106 | return { skeleton, repo }; 107 | } 108 | } 109 | 110 | export const app = new Application(); 111 | -------------------------------------------------------------------------------- /src/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { ComparisonKind } from './types/FileComparisonResult'; 3 | import { ScoreRequirements } from './types/ScoreRequirements'; 4 | import { sep, basename } from 'path'; 5 | import { ComparisonScoreRequirements } from './types/ComparisonScoreRequirements'; 6 | import { existsSync } from 'fs'; 7 | 8 | const yaml = require('js-yaml'); 9 | const micromatch = require('micromatch'); 10 | 11 | export enum PackageSyncAction { 12 | FIX = 'fix', // eslint-disable-line no-unused-vars 13 | ANALYZE = 'analyze', // eslint-disable-line no-unused-vars 14 | } 15 | 16 | export interface ConfigurationRecord { 17 | paths: { 18 | templates: string; 19 | packages: string; 20 | }; 21 | 22 | fixers: { 23 | disabled?: string[]; 24 | OptionalPackages: string[]; 25 | }; 26 | 27 | git: { 28 | branches: { 29 | default: string; 30 | format: string; 31 | createOn: Set; 32 | }; 33 | }; 34 | 35 | scoreRequirements: ScoreRequirements; 36 | ignoreNames: Array; 37 | skipComparisons: Array; 38 | 39 | templates: { 40 | vendor: string; 41 | names: string[]; 42 | }; 43 | 44 | packages: { 45 | vendor: string; 46 | email: string; 47 | homepage: string; 48 | }; 49 | 50 | issues: { 51 | ignored: { 52 | [ComparisonKind.DIRECTORY_NOT_FOUND]?: string[]; 53 | [ComparisonKind.DIRECTORY_NOT_IN_SKELETON]?: string[]; 54 | [ComparisonKind.FILE_DOES_NOT_MATCH]?: string[]; 55 | [ComparisonKind.FILE_NOT_IN_SKELETON]?: string[]; 56 | [ComparisonKind.FILE_NOT_SIMILAR_ENOUGH]?: string[]; 57 | [ComparisonKind.PACKAGE_NOT_USED]?: string[]; 58 | [ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND]?: string[]; 59 | [ComparisonKind.PACKAGE_VERSION_MISMATCH]?: string[]; 60 | }; 61 | }; 62 | } 63 | 64 | const defaultConfig = { 65 | fixers: { 66 | disabled: [], 67 | }, 68 | scoreRequirements: { 69 | defaults: { 70 | similar: 0.5, 71 | size: 20, 72 | }, 73 | files: [], 74 | }, 75 | ignoreNames: [], 76 | skipComparisons: [], 77 | paths: { 78 | templates: `${__dirname}/templates`, 79 | packages: `${__dirname}/packages`, 80 | }, 81 | templates: { 82 | vendor: 'spatie', 83 | names: ['package-skeleton-php', 'package-skeleton-laravel'], 84 | }, 85 | issues: { 86 | ignored: { 87 | [ComparisonKind.DIRECTORY_NOT_FOUND]: [], 88 | [ComparisonKind.DIRECTORY_NOT_IN_SKELETON]: [], 89 | [ComparisonKind.FILE_DOES_NOT_MATCH]: [], 90 | [ComparisonKind.FILE_NOT_IN_SKELETON]: [], 91 | [ComparisonKind.FILE_NOT_SIMILAR_ENOUGH]: [], 92 | [ComparisonKind.PACKAGE_NOT_USED]: [], 93 | [ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND]: [], 94 | [ComparisonKind.PACKAGE_VERSION_MISMATCH]: [], 95 | }, 96 | }, 97 | }; 98 | 99 | export class Configuration { 100 | public conf: ConfigurationRecord; 101 | public filename: string; 102 | 103 | constructor(filename: string | null = null) { 104 | if (filename === null) { 105 | filename = __filename.replace(/\.[tj]s$/, '.yml'); 106 | 107 | if (process.env.NODE_ENV === 'test') { 108 | filename = process.cwd() + '/tests/data/index.yml'; 109 | } 110 | } 111 | 112 | this.filename = filename; 113 | this.conf = this.loadConfigurationFile(this.filename).config; 114 | } 115 | 116 | public loadConfigurationFile(filename: string) { 117 | if (!existsSync(filename)) { 118 | return { config: defaultConfig }; 119 | } 120 | 121 | const content = readFileSync(filename, { encoding: 'utf-8' }) 122 | .replace(/\{\{__dirname\}\}/g, __dirname); 123 | 124 | return yaml.load(content); 125 | } 126 | 127 | public qualifiedTemplateName(templateName: string): string { 128 | return `${this.conf.templates.vendor}/${templateName}`; 129 | } 130 | 131 | public qualifiedPackageName(name: string): string { 132 | if (name.includes('/') && name.length > 2) { 133 | return name; 134 | } 135 | return `${this.conf.packages.vendor}/${name}`; 136 | } 137 | 138 | public getFullTemplateName(shortName: string): string { 139 | const shortTemplateName = (longName: string) => { 140 | return longName.split('-') 141 | .pop() ?? longName; 142 | }; 143 | 144 | return this.conf.templates.names.find(name => shortTemplateName(name) === shortName) ?? shortName; 145 | } 146 | 147 | // public isIssueIgnored(issue: PackageIssue): boolean { 148 | // return this.conf.issues.ignored[issue.result.kind.toString()]?.includes(issue.result.name) ?? false; 149 | // } 150 | 151 | public templatePath(templateName: string): string { 152 | return `${this.conf.paths.templates}/${templateName}`; 153 | } 154 | 155 | public packagePath(packageName: string): string { 156 | return `${this.conf.paths.packages}/${packageName}`; 157 | } 158 | 159 | public shouldIgnoreFile(fn: string): boolean { 160 | return ( 161 | micromatch.isMatch(fn, config.conf.ignoreNames) || 162 | micromatch.isMatch(fn.replace(process.cwd() + sep, ''), config.conf.ignoreNames) 163 | ); 164 | } 165 | 166 | public shouldIgnoreIssue(issue: any): boolean { 167 | if (typeof config.conf.issues.ignored[issue.kind] !== 'undefined') { 168 | return micromatch.isMatch(issue.name, config.conf.issues.ignored[issue.kind]); 169 | } 170 | 171 | return false; 172 | } 173 | 174 | public shouldCompareFile(fn: string): boolean { 175 | return !config.conf.skipComparisons.includes(basename(fn)); 176 | } 177 | 178 | public getSimilarScoreRequirement(fn: string): number { 179 | const reqs = config.conf.scoreRequirements; 180 | 181 | return reqs.files.find(req => req.name === basename(fn))?.scores?.similar ?? reqs.defaults.similar; 182 | } 183 | 184 | public getMaxAllowedSizeDifferenceScore(fn: string): number { 185 | const reqs = config.conf.scoreRequirements; 186 | 187 | return reqs.files.find(req => req.name === basename(fn))?.scores?.size ?? reqs.defaults.size; 188 | } 189 | 190 | public getFileScoreRequirements(fn: string): ComparisonScoreRequirements { 191 | return { 192 | similar: this.getSimilarScoreRequirement(fn), 193 | size: this.getMaxAllowedSizeDifferenceScore(fn), 194 | }; 195 | } 196 | } 197 | 198 | export const config = new Configuration(); 199 | -------------------------------------------------------------------------------- /src/commands/Analyze.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { app } from '../Application'; 5 | import { Command, createOption } from './Command'; 6 | import { ConsolePrinter } from '../printers/ConsolePrinter'; 7 | 8 | export default class AnalyzeCommand extends Command { 9 | public static command = 'analyze '; 10 | public static aliases = ['a', 'an']; 11 | public static description = 'Analyze a package using its template/skeleton repository'; 12 | public static exports = exports; 13 | 14 | public static options = [createOption('config', null, { alias: 'c', type: 'string' })]; 15 | 16 | static handle(argv: any): void { 17 | const { repo } = app.loadConfigFile(argv.config || null) 18 | .analyzePackage(argv.packageName); 19 | 20 | ConsolePrinter.printTable(ConsolePrinter.printRepositoryIssues(repo)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | export interface OptionDefinition { 5 | name: string; 6 | alias?: string; 7 | default?: any; 8 | describe?: string; 9 | type?: string; 10 | } 11 | 12 | export abstract class Command { 13 | public static command: string; 14 | public static aliases: string[] = []; 15 | public static description: string; 16 | public static options: OptionDefinition[] = []; 17 | public static exports: any; 18 | 19 | static handle(argv: any[]): void { 20 | return; 21 | } 22 | 23 | protected static optionsToBuilder() { 24 | return (yargs: any, helpOrVersionSet: any) => { 25 | this.options.forEach((option: OptionDefinition) => { 26 | yargs.option(option.name, option); 27 | }); 28 | 29 | return yargs; 30 | }; 31 | } 32 | 33 | public static export() { 34 | this.exports.command = this.command; 35 | this.exports.describe = this.description; 36 | this.exports.builder = this.optionsToBuilder(); 37 | this.exports.handler = this.handle; 38 | 39 | //if (typeof this['aliases'] !== 'undefined' && this.aliases.length && this.aliases[0].length) { 40 | const aliases = this.aliases || []; 41 | 42 | if (Array.isArray(aliases) && aliases.length) { 43 | this.exports.aliases = aliases.slice(0); 44 | } else { 45 | this.exports.aliases = []; 46 | } 47 | 48 | return this.exports; 49 | } 50 | } 51 | 52 | export function createOption(name: string, defaultValue: any = undefined, settings: Record = {}): OptionDefinition { 53 | return Object.assign({}, { name: name, default: defaultValue }, settings); 54 | } 55 | 56 | export const loadCommand = (cmd: any) => cmd.default.export(); 57 | -------------------------------------------------------------------------------- /src/commands/Fix.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { app } from '../Application'; 4 | import { Command, createOption } from './Command'; 5 | import { FixerManager } from '../fixers/FixerManager'; 6 | import { ConsolePrinter } from '../printers/ConsolePrinter'; 7 | import { matches } from '../lib/helpers'; 8 | import { config } from '../Configuration'; 9 | 10 | export default class FixCommand extends Command { 11 | public static command = 'fix [issueType]'; 12 | public static aliases: string[] = []; 13 | public static description = "Fix a package's issues"; 14 | public static exports = exports; 15 | 16 | public static options = [ 17 | createOption('config', null, { alias: 'c', type: 'string' }), 18 | createOption('file', null, { alias: 'f', type: 'string' }), 19 | ]; 20 | 21 | static handle(argv: any): void { 22 | let issueType: string = (argv['issueType'] ?? 'all').trim() 23 | .toLowerCase(); 24 | 25 | const allowRisky: boolean = argv['risky'] ?? false; 26 | 27 | if (issueType.trim().length === 0) { 28 | issueType = '*'; 29 | } 30 | 31 | if (issueType === 'all') { 32 | issueType = '*'; 33 | } 34 | 35 | const { repo } = app.loadConfigFile(argv.config || config.filename) 36 | .analyzePackage(argv.packageName); 37 | 38 | const nameMap = fixers => fixers.map(fixer => fixer.getName()); 39 | 40 | FixerManager.create() 41 | .fixIssues( 42 | repo.issues 43 | .filter(issue => (argv['file'] ?? null) === null || issue.name === argv.file) 44 | .filter( 45 | issue => 46 | issueType === '*' || 47 | nameMap(issue.fixers) 48 | .includes(issueType) || 49 | matches(issueType, issue.kind) || 50 | matches(issueType, nameMap(issue.fixers)), 51 | ), 52 | issueType, 53 | allowRisky, 54 | ); 55 | 56 | ConsolePrinter.printTable(ConsolePrinter.printRepositoryFixerResults(repo)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/ListFixers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { Command } from './Command'; 4 | import { ConsolePrinter } from '../printers/ConsolePrinter'; 5 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 6 | import { FixerRepository } from '../fixers/FixerRepository'; 7 | 8 | export default class ListFixersCommand extends Command { 9 | public static command = 'fixers'; 10 | public static aliases: string[] = []; 11 | public static description = 'List all available fixers'; 12 | public static exports = exports; 13 | 14 | public static options = []; 15 | 16 | static handle(): void { 17 | const fixers = FixerRepository.all() 18 | .map(fixer => new fixer((null))); 19 | 20 | ConsolePrinter.printTable(ConsolePrinter.printFixerSummary(fixers)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/PullPackage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import { app } from '../Application'; 5 | import { Command } from './Command'; 6 | import { GitUtilties } from '../lib/GitUtilties'; 7 | 8 | export default class PullPackageCommand extends Command { 9 | public static command = 'pull-package '; 10 | public static aliases: string[] = ['pp']; 11 | public static description = 'Update/retrieve the named package repository'; 12 | public static exports = exports; 13 | public static options = []; 14 | 15 | static handle(argv: any): void { 16 | const name = argv.name; 17 | const config = app.configuration; 18 | 19 | GitUtilties.displayStatusMessages = true; 20 | 21 | GitUtilties.cloneRepo(config.qualifiedPackageName(name), config.conf.paths.packages); 22 | GitUtilties.pullRepo(name, config.packagePath(name)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/PullTemplate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { app } from '../Application'; 4 | import { Command } from './Command'; 5 | import { GitUtilties } from '../lib/GitUtilties'; 6 | 7 | export default class PullTemplateCommand extends Command { 8 | public static command = 'pull-template [name]'; 9 | public static aliases: string[] = ['pt']; 10 | public static description = 'Update/retrieve the named skeleton repository, or both if not specified (can be "php" or "laravel")'; 11 | public static exports = exports; 12 | public static options = []; 13 | 14 | static handle(argv: any): void { 15 | const argvName = argv.name ?? ''; 16 | const config = app.configuration; 17 | 18 | const shortTemplateName = (longName: string) => { 19 | return longName.split('-') 20 | .pop() ?? longName; 21 | }; 22 | 23 | config.conf.templates.names 24 | .filter(name => shortTemplateName(name) === argvName || name === argvName || argvName === '') 25 | .forEach(name => { 26 | GitUtilties.displayStatusMessages = true; 27 | 28 | GitUtilties.cloneRepo(config.qualifiedTemplateName(name), config.conf.paths.templates); 29 | GitUtilties.pullRepo(name, config.templatePath(name)); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/comparisons/Comparison.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { Repository } from '../repositories/Repository'; 4 | import { RepositoryFile } from '../repositories/RepositoryFile'; 5 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 6 | import { ComparisonKind } from '../types/FileComparisonResult'; 7 | 8 | export abstract class Comparison { 9 | protected comparisonPassed = false; 10 | protected _score: number | string | null = null; 11 | 12 | get score() { 13 | return this._score; 14 | } 15 | 16 | set score(value: number | string | null) { 17 | this._score = value; 18 | } 19 | 20 | constructor( 21 | public skeleton: Repository, 22 | public repo: Repository, 23 | public file: RepositoryFile | null, 24 | public repoFile: RepositoryFile | null, 25 | ) { 26 | // 27 | } 28 | 29 | static create( 30 | this: new (...args: any[]) => T, 31 | skeleton: Repository, 32 | repo: Repository, 33 | file: RepositoryFile | null, 34 | repoFile: RepositoryFile | null, 35 | ): T { 36 | return new this(skeleton, repo, file, repoFile); 37 | } 38 | 39 | public abstract compare(requiredScore: number | null): Comparison; 40 | 41 | public abstract getKind(): ComparisonKind; 42 | 43 | public prettyScore(): string { 44 | if (this.score === null || this.score === 0) { 45 | return '-'; 46 | } 47 | 48 | if (typeof this.score === 'string') { 49 | return this.score; 50 | } 51 | 52 | return this.score?.toFixed(3) ?? ''; 53 | } 54 | 55 | protected createIssue(additional: Record | null): void { 56 | const compareResult = { 57 | kind: this.getKind(), 58 | score: this.prettyScore(), 59 | ...(additional ?? {}), 60 | }; 61 | 62 | this.repo.issues.push( 63 | new RepositoryIssue( 64 | compareResult, 65 | this.file?.relativeName ?? '.', 66 | this.file, 67 | this.repoFile, 68 | this.skeleton, 69 | this.repo, 70 | false, 71 | ), 72 | ); 73 | } 74 | 75 | public passed(): boolean { 76 | return this.comparisonPassed; 77 | } 78 | 79 | public failed(): boolean { 80 | return !this.passed(); 81 | } 82 | 83 | public meetsRequirement(minimum: number | null): boolean { 84 | return (this.score ?? 0) >= (minimum ?? 15); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/comparisons/ComposerPackagesComparison.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 4 | 5 | import { ComparisonKind } from '../types/FileComparisonResult'; 6 | import { Comparison } from './Comparison'; 7 | import { Composer } from '../lib/composer/Composer'; 8 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 9 | 10 | const semver = require('semver'); 11 | 12 | export class ComposerPackagesComparison extends Comparison { 13 | protected kind: ComparisonKind = ComparisonKind.PACKAGE_NOT_USED; 14 | 15 | protected getVersionDiff(repoVersion: string, newVersion: string): string | null { 16 | const newPart = newVersion.split('|') 17 | .pop() ?? '0.0'; 18 | const repoPart = repoVersion.split('|') 19 | .pop() ?? '0.0'; 20 | 21 | const diff = semver.diff(semver.coerce(newPart), semver.coerce(repoPart)); 22 | 23 | return ['major', 'minor', 'patch'].includes(diff) ? diff : null; 24 | } 25 | 26 | public compare(requiredScore: number | null): Comparison { 27 | this.score = 0; 28 | 29 | const skeletonComposer = Composer.createFromPath(this.skeleton.path); 30 | const repositoryComposer = Composer.createFromPath(this.repo.path); 31 | 32 | let issueCounter = 0; 33 | 34 | skeletonComposer.packages('all') 35 | .forEach(pkg => { 36 | if (!repositoryComposer.hasPackage(pkg.name, pkg.section)) { 37 | this.kind = ComparisonKind.PACKAGE_NOT_USED; 38 | this.score = 0; 39 | 40 | this.createIssue({ 41 | name: pkg.name, 42 | context: pkg.section, 43 | skeletonPath: this.skeleton.path, 44 | repositoryPath: this.repo.path, 45 | }); 46 | 47 | issueCounter++; 48 | 49 | return; 50 | } 51 | 52 | const repositoryPackage = repositoryComposer.package(pkg.name); 53 | const versionDiff = this.getVersionDiff(repositoryPackage.version, pkg.version); 54 | 55 | // treat version strings like '^8.1|^9.5' and '^9.5' as a non-issue 56 | if (repositoryPackage.version.includes(pkg.version)) { 57 | return; 58 | } 59 | 60 | if (versionDiff !== null) { 61 | this.kind = ComparisonKind.PACKAGE_VERSION_MISMATCH; 62 | this.score = versionDiff; 63 | 64 | this.createIssue({ 65 | name: pkg.name, 66 | context: pkg, 67 | skeletonPath: this.skeleton.path, 68 | repositoryPath: this.repo.path, 69 | }); 70 | 71 | issueCounter++; 72 | 73 | return; 74 | } 75 | }); 76 | 77 | this.comparisonPassed = issueCounter === 0; 78 | 79 | return this; 80 | } 81 | 82 | public getKind(): ComparisonKind { 83 | return this.kind; 84 | } 85 | 86 | public meetsRequirement(percentage): boolean { 87 | return true; 88 | } 89 | 90 | protected createIssue(additional: Record | null): void { 91 | const compareResult = { 92 | kind: this.getKind(), 93 | score: this.prettyScore(), 94 | ...(additional ?? {}), 95 | }; 96 | 97 | this.repo.issues.push( 98 | new RepositoryIssue( 99 | compareResult, 100 | // @ts-ignore 101 | compareResult.name, 102 | this.file, 103 | this.repoFile, 104 | this.skeleton, 105 | this.repo, 106 | false, 107 | ), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/comparisons/ComposerScriptsComparison.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 4 | 5 | import { ComparisonKind } from '../types/FileComparisonResult'; 6 | import { Comparison } from './Comparison'; 7 | import { Composer } from '../lib/composer/Composer'; 8 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 9 | 10 | export class ComposerScriptsComparison extends Comparison { 11 | protected kind: ComparisonKind = ComparisonKind.FILE_NOT_FOUND; 12 | 13 | public compare(requiredScore: number | null): Comparison { 14 | this.score = 0; 15 | 16 | const skeletonComposer = Composer.createFromPath(this.skeleton.path); 17 | const repositoryComposer = Composer.createFromPath(this.repo.path); 18 | 19 | const missingScripts = skeletonComposer.scripts() 20 | .filter(script => !repositoryComposer.hasScript(script.name)); 21 | 22 | this.comparisonPassed = missingScripts.length === 0; 23 | 24 | if (!this.comparisonPassed) { 25 | missingScripts.forEach(script => { 26 | this.createIssue({ 27 | name: script.name, 28 | context: Object.assign({}, script), 29 | skeletonPath: this.skeleton.path, 30 | repositoryPath: this.repo.path, 31 | }); 32 | }); 33 | } 34 | 35 | return this; 36 | } 37 | 38 | public getKind(): ComparisonKind { 39 | return ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND; 40 | } 41 | 42 | public meetsRequirement(percentage): boolean { 43 | return true; 44 | } 45 | 46 | protected createIssue(additional: Record | null): void { 47 | const compareResult = { 48 | kind: this.getKind(), 49 | score: this.prettyScore(), 50 | ...(additional ?? {}), 51 | }; 52 | 53 | this.repo.issues.push( 54 | new RepositoryIssue( 55 | compareResult, 56 | // @ts-ignore 57 | compareResult.name, 58 | this.file, 59 | this.repoFile, 60 | this.skeleton, 61 | this.repo, 62 | false, 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/comparisons/ExtraFilesComparison.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { RepositoryFile } from '../repositories/RepositoryFile'; 5 | import { ComparisonKind } from '../types/FileComparisonResult'; 6 | import { Comparison } from './Comparison'; 7 | import { matches } from '../lib/helpers'; 8 | import { config } from '../Configuration'; 9 | 10 | export class ExtraFilesComparison extends Comparison { 11 | protected kind: ComparisonKind = ComparisonKind.FILE_NOT_FOUND; 12 | protected extraFiles: RepositoryFile[] = []; 13 | 14 | public compare(requiredScore: number | null): Comparison { 15 | this.score = 0; 16 | 17 | this.extraFiles = this.repo.files 18 | .filter(file => !matches(file.relativeName, config.conf.ignoreNames)) 19 | .filter(file => !config.conf.ignoreNames.includes(file.relativeName)) 20 | .filter(file => !file.shouldIgnore) 21 | .filter(file => !this.skeleton.hasFile(file)); 22 | 23 | this.comparisonPassed = this.extraFiles.length === 0; 24 | 25 | if (!this.comparisonPassed) { 26 | this.extraFiles.forEach(file => { 27 | this.file = file; 28 | this.createIssue(null); 29 | }); 30 | } 31 | 32 | return this; 33 | } 34 | 35 | public getKind(): ComparisonKind { 36 | return this.file?.isFile() ?? true ? ComparisonKind.FILE_NOT_IN_SKELETON : ComparisonKind.DIRECTORY_NOT_IN_SKELETON; 37 | } 38 | 39 | public meetsRequirement(percentage): boolean { 40 | return this.extraFiles.length === 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/comparisons/FileExistsComparison.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { RepositoryFile } from '../repositories/RepositoryFile'; 5 | import { ComparisonKind } from '../types/FileComparisonResult'; 6 | import { Comparison } from './Comparison'; 7 | 8 | export class FileExistsComparison extends Comparison { 9 | protected kind: ComparisonKind = ComparisonKind.FILE_NOT_FOUND; 10 | 11 | public compare(requiredScore: number | null): Comparison { 12 | this.score = 0; 13 | this.comparisonPassed = this.meetsRequirement(requiredScore ?? 0); 14 | 15 | if (!this.comparisonPassed) { 16 | this.kind = this.file?.isFile() ?? false ? ComparisonKind.FILE_NOT_FOUND : ComparisonKind.DIRECTORY_NOT_FOUND; 17 | 18 | this.createIssue(null); 19 | } 20 | 21 | return this; 22 | } 23 | 24 | public getKind(): ComparisonKind { 25 | return this.kind; 26 | } 27 | 28 | public meetsRequirement(percentage): boolean { 29 | if (this.file === null) { 30 | return false; 31 | } 32 | 33 | return (this.file?.shouldIgnore ?? false) || this.repo.hasFile((this.file)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/comparisons/FileSizeComparison.ts: -------------------------------------------------------------------------------- 1 | import { ComparisonKind } from '../types/FileComparisonResult'; 2 | import { Comparison } from './Comparison'; 3 | 4 | export class FileSizeComparison extends Comparison { 5 | protected filesize1 = 0; 6 | protected filesize2 = 0; 7 | 8 | public compare(requiredScore: number | null): Comparison { 9 | this.filesize1 = this.file?.sizeOnDisk ?? 0; 10 | this.filesize2 = this.repoFile?.sizeOnDisk ?? 0; 11 | 12 | this.score = this.percentage(); 13 | this.comparisonPassed = !this.meetsRequirement(requiredScore); 14 | 15 | if (!this.passed()) { 16 | this.createIssue(null); 17 | } 18 | 19 | return this; 20 | } 21 | 22 | public getKind(): ComparisonKind { 23 | return ComparisonKind.ALLOWED_SIZE_DIFFERENCE_EXCEEDED; 24 | } 25 | 26 | public meetsRequirement(percentage): boolean { 27 | if (percentage === null) { 28 | percentage = this.file?.requiredScores?.size ?? 15; 29 | } 30 | 31 | const pctDecimal = percentage * 0.01; 32 | 33 | return this.difference() > Math.round(this.filesize1 * pctDecimal); 34 | } 35 | 36 | public prettyScore(): string { 37 | if (this.score === null) { 38 | return ''; 39 | } 40 | 41 | if (typeof this.score === 'string') { 42 | return this.score; 43 | } 44 | 45 | return (this.score?.toFixed(2) ?? '0.0') + '%'; 46 | } 47 | 48 | public difference() { 49 | return this.filesize1 - this.filesize2; 50 | } 51 | 52 | public percentage(): number { 53 | if (this.difference() === 0) { 54 | return 0; 55 | } 56 | 57 | return (this.difference() / this.filesize1) * 100; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/comparisons/StringComparison.ts: -------------------------------------------------------------------------------- 1 | import { ComparisonKind } from '../types/FileComparisonResult'; 2 | import { Comparison } from './Comparison'; 3 | 4 | const { compareTwoStrings } = require('string-similarity'); 5 | 6 | export class StringComparison extends Comparison { 7 | public compare(requiredScore: number | null): Comparison { 8 | this.score = compareTwoStrings(this.file?.contents ?? '', this.repoFile?.contents ?? ''); 9 | this.comparisonPassed = this.meetsRequirement(requiredScore ?? 0); 10 | 11 | if (!this.passed()) { 12 | this.createIssue(null); 13 | } 14 | 15 | return this; 16 | } 17 | 18 | public getKind(): ComparisonKind { 19 | return ComparisonKind.FILE_NOT_SIMILAR_ENOUGH; 20 | } 21 | 22 | public meetsRequirement(percentage) { 23 | if (percentage === null || percentage === 0) { 24 | percentage = this.file?.requiredScores?.similar ?? 15; 25 | } 26 | 27 | return super.meetsRequirement(percentage); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/fixers/DirectoryNotFoundFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { mkdirSync } from 'fs'; 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import Fixer from './Fixer'; 6 | 7 | export class DirectoryNotFoundFixer extends Fixer { 8 | public static handles = [ComparisonKind.DIRECTORY_NOT_FOUND]; 9 | 10 | public description() { 11 | return 'creates a missing directory'; 12 | } 13 | 14 | public fix(): boolean { 15 | if (!this.shouldPerformFix()) { 16 | return false; 17 | } 18 | 19 | mkdirSync(`${this.issue.repository.path}/${this.issue.name}`, { recursive: true }); 20 | 21 | this.issue.resolve(this) 22 | .addResolvedNote(`created directory '${this.issue.name}'`); 23 | 24 | return true; 25 | } 26 | 27 | public static prettyName(): string { 28 | return 'create-dir'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/fixers/FileDoesNotMatchFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { File } from '../lib/File'; 5 | import { Fixer } from './Fixer'; 6 | 7 | export class FileDoesNotMatchFixer extends Fixer { 8 | public static handles = [ComparisonKind.FILE_DOES_NOT_MATCH]; 9 | 10 | public description() { 11 | return 'overwrite a file with the skeleton version to force an exact match.'; 12 | } 13 | 14 | public isRisky(): boolean { 15 | return true; 16 | } 17 | 18 | public fix(): boolean { 19 | if (!this.shouldPerformFix()) { 20 | return false; 21 | } 22 | 23 | File.read(this.issue.srcFile?.filename ?? '') 24 | .saveAs(this.issue.destFile?.filename ?? ''); 25 | 26 | this.issue.resolve(this) 27 | .addResolvedNote(`overwrote existing file '${this.issue.name}'`); 28 | 29 | return true; 30 | } 31 | 32 | public static prettyName(): string { 33 | return 'overwrite-file'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/fixers/FileNotFoundFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { existsSync } from 'fs'; 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import { File } from '../lib/File'; 6 | import { Fixer } from './Fixer'; 7 | 8 | export class FileNotFoundFixer extends Fixer { 9 | public static handles = [ComparisonKind.FILE_NOT_FOUND]; 10 | 11 | public description() { 12 | return 'copies a file from the skeleton repository into the package repository.'; 13 | } 14 | 15 | public fix(): boolean { 16 | if (!this.shouldPerformFix()) { 17 | return false; 18 | } 19 | 20 | const relativeFn: string = this.issue.srcFile?.relativeName ?? this.issue.name; 21 | 22 | if (!existsSync(`${this.issue.repository.path}/${relativeFn}`)) { 23 | const file = File.read(`${this.issue.skeleton.path}/${relativeFn}`); 24 | 25 | file.setContents(file.processTemplate(this.issue.repository.name)) 26 | .saveAs(`${this.issue.repository.path}/${relativeFn}`); 27 | } 28 | 29 | this.issue.resolve(this) 30 | .addResolvedNote(`copied file '${relativeFn}' from skeleton`); 31 | 32 | return true; 33 | } 34 | 35 | public static prettyName(): string { 36 | return 'create-file'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/fixers/Fixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { classOf } from '../lib/helpers'; 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 6 | 7 | export class Fixer { 8 | public enabled = true; 9 | 10 | public static handles: ComparisonKind[] = []; 11 | 12 | public constructor(public issue: RepositoryIssue) { 13 | // 14 | } 15 | 16 | description() { 17 | return ''; 18 | } 19 | 20 | getName() { 21 | return this.getClass() 22 | .prettyName(); 23 | } 24 | 25 | getClass() { 26 | return classOf(this); 27 | } 28 | 29 | public fix(): boolean { 30 | return false; 31 | } 32 | 33 | public isRisky(): boolean { 34 | return false; 35 | } 36 | 37 | public runsFixers(): boolean { 38 | return false; 39 | } 40 | 41 | public static canFix(issue: RepositoryIssue): boolean { 42 | return !issue.resolved; 43 | } 44 | 45 | public static fixes(kind: ComparisonKind): boolean { 46 | return this.handles.includes(kind); 47 | } 48 | 49 | public static prettyName(): string { 50 | return this.name; 51 | } 52 | 53 | public disable() { 54 | this.enabled = false; 55 | } 56 | 57 | public enable() { 58 | this.enabled = true; 59 | } 60 | 61 | public fixesIssue(issue: RepositoryIssue) { 62 | return this.getClass() 63 | .fixes(issue.kind) && this.getClass() 64 | .canFix(issue); 65 | } 66 | 67 | protected shouldPerformFix() { 68 | return !this.issue.resolved && this.enabled; 69 | } 70 | } 71 | 72 | export default Fixer; 73 | -------------------------------------------------------------------------------- /src/fixers/FixerManager.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import { config, ConfigurationRecord } from '../Configuration'; 5 | import { matches, uniqueArray } from '../lib/helpers'; 6 | import { Fixer } from './Fixer'; 7 | import { FixerRepository } from './FixerRepository'; 8 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 9 | 10 | export class FixerManager { 11 | public config: ConfigurationRecord; 12 | 13 | constructor(conf: ConfigurationRecord | null = null) { 14 | this.config = (conf ?? config); 15 | } 16 | 17 | static create(config: ConfigurationRecord | null = null) { 18 | return new FixerManager(config); 19 | } 20 | 21 | public isFixerDisabled(fixer: any): boolean { 22 | // const name = Object.getOwnPropertyDescriptor(fixer, 'name')?.value ?? ''; 23 | // const shortName = name.replace(/Fixer$/, ''); 24 | const disabledFixers = this.config.fixers?.disabled ?? []; 25 | 26 | return matches(fixer.prettyName(), disabledFixers); 27 | } 28 | 29 | public static getFixerClass(name: string): any | null { 30 | const result = FixerRepository.all() 31 | .find(fixer => fixer.prettyName() === name); 32 | 33 | if (result === undefined) { 34 | return null; 35 | } 36 | 37 | return result; 38 | } 39 | 40 | public getFixerForIssue(name: string, issue: RepositoryIssue): Fixer | null { 41 | const fixerClass = FixerRepository.all() 42 | .find(fixer => fixer.prettyName() === name) ?? null; 43 | 44 | if (fixerClass) { 45 | // @ts-ignore 46 | return new fixerClass(issue); 47 | } 48 | 49 | return null; 50 | } 51 | 52 | public fixIssues(issues: RepositoryIssue[], issueTypeOrFixer: string, allowRisky: boolean) { 53 | issues.forEach(issue => this.fixIssue(issue, issueTypeOrFixer, allowRisky)); 54 | } 55 | 56 | public fixIssue(issue: RepositoryIssue, issueTypeOrFixer: string, allowRisky: boolean) { 57 | const fixers = issue.fixers 58 | .filter(fixer => !this.isFixerDisabled(fixer.getClass())) 59 | .filter(fixer => fixer.getClass() 60 | .canFix(issue)) 61 | .slice(0); 62 | 63 | let skippedFixers: string[] = []; 64 | let fixer = fixers.find(fixer => fixer.getName() === issueTypeOrFixer); 65 | const onlyRunOnce = fixer === undefined; 66 | 67 | if (fixer === undefined) { 68 | fixer = fixers.shift() ?? undefined; 69 | } 70 | 71 | let counter = 0; 72 | 73 | while (!issue.resolved && fixer !== undefined) { 74 | counter++; 75 | 76 | if (onlyRunOnce && counter > 1) { 77 | return; 78 | } 79 | 80 | if (fixer === undefined) { 81 | return; 82 | } 83 | 84 | if (issue.resolved) { 85 | return; 86 | } 87 | 88 | if (!allowRisky && fixer.isRisky()) { 89 | skippedFixers.push(fixer.getName()); 90 | fixer = fixers.shift() ?? undefined; 91 | 92 | continue; 93 | } 94 | 95 | if (fixer.fix()) { 96 | return issue.resolve(fixer); 97 | } 98 | 99 | fixer = fixers.shift() ?? undefined; 100 | } 101 | 102 | skippedFixers = uniqueArray(skippedFixers); 103 | 104 | if (skippedFixers.length > 1) { 105 | issue.addResolvedNote(`skipped ${skippedFixers.length} fixers (risky)`); 106 | } else if (skippedFixers.length === 1) { 107 | issue.addResolvedNote(`skipped ${skippedFixers[0]} (risky)`); 108 | } 109 | 110 | if (!issue.resolved) { 111 | issue.addResolvedNote(`unresolved`, true); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/fixers/FixerRepository.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../Configuration'; 2 | import { DirectoryNotFoundFixer } from './DirectoryNotFoundFixer'; 3 | import { FileDoesNotMatchFixer } from './FileDoesNotMatchFixer'; 4 | import { FileNotFoundFixer } from './FileNotFoundFixer'; 5 | import { MergeFilesFixer } from './MergeFilesFixer'; 6 | import { GithubFixer } from './GithubFixer'; 7 | import { OptionalPackagesFixer } from './OptionalPackagesFixer'; 8 | import { OverwriteFileFixer } from './OverwriteFileFixer'; 9 | import { PackageNotUsedFixer } from './PackageNotUsedFixer'; 10 | import { PackageScriptNotFoundFixer } from './PackageScriptNotFoundFixer'; 11 | import { PackageVersionFixer } from './PackageVersionFixer'; 12 | import { PsalmFixer } from './PsalmFixer'; 13 | 14 | export class FixerRepository { 15 | public static all() { 16 | return [ 17 | // specific fixers: 18 | MergeFilesFixer, 19 | GithubFixer, 20 | PsalmFixer, 21 | OptionalPackagesFixer, 22 | // generic fixers: 23 | DirectoryNotFoundFixer, 24 | OverwriteFileFixer, 25 | FileDoesNotMatchFixer, 26 | FileNotFoundFixer, 27 | PackageNotUsedFixer, 28 | PackageScriptNotFoundFixer, 29 | PackageVersionFixer, 30 | ].filter(fixer => !config.conf.fixers.disabled?.includes(fixer.prettyName())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/fixers/GithubFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { Fixer } from './Fixer'; 5 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 6 | import { DirectoryNotFoundFixer } from './DirectoryNotFoundFixer'; 7 | import { FileNotFoundFixer } from './FileNotFoundFixer'; 8 | 9 | export class GithubFixer extends Fixer { 10 | public static handles = [ComparisonKind.DIRECTORY_NOT_FOUND, ComparisonKind.FILE_NOT_FOUND]; 11 | 12 | public description() { 13 | return `recreates all missing directories and files under the '.github' directory.`; 14 | } 15 | 16 | public runsFixers(): boolean { 17 | return true; 18 | } 19 | 20 | public fixesIssue(issue: RepositoryIssue): boolean { 21 | return GithubFixer.canFix(issue); 22 | } 23 | 24 | public static canFix(issue: RepositoryIssue): boolean { 25 | if (!super.canFix(issue)) { 26 | return false; 27 | } 28 | 29 | if (!issue.name.startsWith('.github')) { 30 | return false; 31 | } 32 | 33 | return GithubFixer.handles.includes(issue.kind); 34 | } 35 | 36 | public fix(): boolean { 37 | if (!this.shouldPerformFix()) { 38 | return false; 39 | } 40 | 41 | if (this.issue.is(ComparisonKind.DIRECTORY_NOT_FOUND)) { 42 | new DirectoryNotFoundFixer(this.issue) 43 | .fix(); 44 | } 45 | 46 | if (this.issue.is(ComparisonKind.FILE_NOT_FOUND)) { 47 | new FileNotFoundFixer(this.issue) 48 | .fix(); 49 | } 50 | 51 | this.issue.resolve(this); 52 | 53 | return true; 54 | } 55 | 56 | public static prettyName(): string { 57 | return 'github'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/fixers/MergeFilesFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { FileMerger } from '../lib/FileMerger'; 5 | import { Fixer } from './Fixer'; 6 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 7 | 8 | export class MergeFilesFixer extends Fixer { 9 | public static handles = [ComparisonKind.ALLOWED_SIZE_DIFFERENCE_EXCEEDED, ComparisonKind.FILE_NOT_SIMILAR_ENOUGH]; 10 | 11 | public description() { 12 | return 'merges the contents of both the skeleton and package versions of a file.'; 13 | } 14 | 15 | public isRisky(): boolean { 16 | return true; 17 | } 18 | 19 | public static canFix(issue: RepositoryIssue): boolean { 20 | if (!super.canFix(issue)) { 21 | return false; 22 | } 23 | 24 | return ['.gitattributes', '.gitignore'].includes(issue.name); 25 | } 26 | 27 | public fix(): boolean { 28 | if (!this.shouldPerformFix()) { 29 | return false; 30 | } 31 | 32 | FileMerger.create() 33 | .add(this.issue.sourcefile.filename, this.issue.targetfile.filename) 34 | .mergeAndSave(this.issue.targetfile.filename); 35 | 36 | this.issue.resolve(this) 37 | .addResolvedNote(`merged '${this.issue.sourcefile.relativeName}' from skeleton and package`); 38 | 39 | return true; 40 | } 41 | 42 | public static prettyName(): string { 43 | return 'merge-files'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/fixers/OptionalPackagesFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { Fixer } from './Fixer'; 5 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 6 | import { classOf } from '../lib/helpers'; 7 | 8 | export class OptionalPackagesFixer extends Fixer { 9 | public static handles = [ComparisonKind.PACKAGE_NOT_USED, ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND]; 10 | 11 | public description() { 12 | return 'skips the installation of a dependency.'; 13 | } 14 | 15 | public static canFix(issue: RepositoryIssue): boolean { 16 | if (!super.canFix(issue)) { 17 | return false; 18 | } 19 | 20 | return false; 21 | } 22 | 23 | public fix(): boolean { 24 | if (!this.shouldPerformFix()) { 25 | return false; 26 | } 27 | 28 | this.issue.resolve(classOf(this) 29 | .prettyName()) 30 | .addResolvedNote(`skipped '${this.issue.name}'`); 31 | 32 | return true; 33 | } 34 | 35 | public static prettyName(): string { 36 | return 'skip-dep'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/fixers/OverwriteFileFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { existsSync } from 'fs'; 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import { File } from '../lib/File'; 6 | import { Fixer } from './Fixer'; 7 | 8 | export class OverwriteFileFixer extends Fixer { 9 | public static handles = [ComparisonKind.FILE_NOT_SIMILAR_ENOUGH]; 10 | 11 | public description() { 12 | return 'overwrites an existing file with a newer version from the skeleton.'; 13 | } 14 | 15 | public isRisky(): boolean { 16 | return true; 17 | } 18 | 19 | public fix(): boolean { 20 | if (!this.shouldPerformFix() || this.issue.pending) { 21 | return false; 22 | } 23 | 24 | const relativeFn: string = this.issue.srcFile?.relativeName ?? this.issue.name; 25 | 26 | if (existsSync(`${this.issue.repository.path}/${relativeFn}`)) { 27 | const file = File.read(`${this.issue.skeleton.path}/${relativeFn}`); 28 | 29 | file.setContents(file.processTemplate(this.issue.repository.name)) 30 | .saveAs(`${this.issue.repository.path}/${relativeFn}`); 31 | 32 | this.issue.resolve(this) 33 | .addResolvedNote(`overwrite file '${relativeFn}' with latest`); 34 | } 35 | 36 | return true; 37 | } 38 | 39 | public static prettyName(): string { 40 | return 'rewrite-file'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/fixers/PackageNotUsedFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { Fixer } from './Fixer'; 5 | 6 | export class PackageNotUsedFixer extends Fixer { 7 | public static handles = [ComparisonKind.PACKAGE_NOT_USED]; 8 | 9 | public description() { 10 | return "adds a dependency to the package's composer.json file."; 11 | } 12 | 13 | public fix(): boolean { 14 | if (!this.shouldPerformFix()) { 15 | return false; 16 | } 17 | 18 | const pkg = this.issue.skeleton.composer.package(this.issue.name); 19 | 20 | this.issue.repository.composer.addPackage(pkg) 21 | .save(); 22 | 23 | this.issue.resolve(this) 24 | .addResolvedNote(`added package dependency '${this.issue.name}'`); 25 | 26 | return true; 27 | } 28 | 29 | public static prettyName(): string { 30 | return 'add-dep'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/fixers/PackageScriptNotFoundFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { ComparisonKind } from '../types/FileComparisonResult'; 3 | import { Fixer } from './Fixer'; 4 | 5 | export class PackageScriptNotFoundFixer extends Fixer { 6 | public static handles = [ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND]; 7 | 8 | public description() { 9 | return "adds a missing composer script to the package's composer.json file."; 10 | } 11 | 12 | public fix(): boolean { 13 | if (!this.shouldPerformFix()) { 14 | return false; 15 | } 16 | 17 | const script = this.issue.skeleton.composer.script(this.issue.name); 18 | 19 | this.issue.repository.composer.addScript(script) 20 | .save(); 21 | 22 | this.issue.resolve(this) 23 | .addResolvedNote(`added composer script '${this.issue.name}'`); 24 | 25 | return true; 26 | } 27 | 28 | public static prettyName(): string { 29 | return 'copy-script'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/fixers/PackageVersionFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { uniqueArray } from '../lib/helpers'; 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { Fixer } from './Fixer'; 5 | 6 | const semver = require('semver'); 7 | 8 | export class PackageVersionFixer extends Fixer { 9 | public static handles = [ComparisonKind.PACKAGE_VERSION_MISMATCH]; 10 | 11 | public description() { 12 | return 'updates the version of a dependency in the package repository.'; 13 | } 14 | 15 | /** 16 | * merges two version strings like '^6.0|^7.1` and '^7.2' into '^6.0|^7.2'. 17 | * 18 | * @param repoVersion the 'old' version 19 | * @param newVersion the 'new' version 20 | * @returns {string} 21 | */ 22 | public mergeVersions(repoVersion: string, newVersion: string) { 23 | const repoVersionParts = repoVersion.split('|') 24 | .sort(); 25 | const newVersionParts = newVersion.split('|') 26 | .sort(); 27 | 28 | const versions = Array(repoVersionParts.length); 29 | const newVersions: string[] = []; 30 | let foundMinorDiff = false; 31 | 32 | for (let i = 0; i < versions.length; i++) { 33 | const newPart = newVersionParts[i] ?? newVersionParts[newVersionParts.length - 1]; 34 | const repoPart = repoVersionParts[i] ?? repoVersionParts[repoVersionParts.length - 1]; 35 | 36 | if (!semver.gt(semver.coerce(newPart), semver.coerce(repoPart))) { 37 | versions[i] = repoPart; 38 | continue; 39 | } 40 | 41 | const diff = semver.diff(semver.coerce(newPart), semver.coerce(repoPart)); 42 | 43 | if (diff !== 'major') { 44 | foundMinorDiff = true; 45 | } 46 | 47 | if (diff === 'major') { 48 | versions[i] = repoPart; 49 | newVersions.push(newPart); 50 | } else { 51 | versions[i] = newPart; 52 | } 53 | } 54 | 55 | versions.push(...newVersions); 56 | 57 | if (newVersionParts.length > repoVersionParts.length) { 58 | versions.push(...newVersionParts.slice(repoVersionParts.length - 1)); 59 | } 60 | 61 | //create a mapping of the coerced version numbers to the actual version string to allow for proper sorting 62 | const versionMap = {}; 63 | 64 | uniqueArray(versions) 65 | .forEach(version => { 66 | versionMap[semver.coerce(version)] = version; 67 | }); 68 | 69 | return uniqueArray(versions) 70 | .map(version => semver.coerce(version)) 71 | .sort() 72 | .map(version => versionMap[version]) 73 | .join('|'); 74 | } 75 | 76 | public fix(): boolean { 77 | if (!this.shouldPerformFix()) { 78 | return false; 79 | } 80 | 81 | const newPkg = this.issue.skeleton.composer.package(this.issue.name); 82 | const repoPkg = this.issue.repository.composer.package(this.issue.name); 83 | const mergedVersion = this.mergeVersions(repoPkg.version, newPkg.version); 84 | 85 | this.issue.repository.composer.setPackageVersion(repoPkg, mergedVersion) 86 | .save(); 87 | 88 | this.issue.resolve(this) 89 | .addResolvedNote(`updated version to '${repoPkg.version}'`); 90 | 91 | return true; 92 | } 93 | 94 | public static prettyName(): string { 95 | return 'bump-version'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/fixers/PsalmFixer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import { Fixer } from './Fixer'; 6 | import { RepositoryIssue } from '../repositories/RepositoryIssue'; 7 | import { FileNotFoundFixer } from './FileNotFoundFixer'; 8 | import { PackageScriptNotFoundFixer } from './PackageScriptNotFoundFixer'; 9 | import { PackageNotUsedFixer } from './PackageNotUsedFixer'; 10 | 11 | export class PsalmFixer extends Fixer { 12 | public static handles = [ComparisonKind.PACKAGE_NOT_USED, ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND, ComparisonKind.FILE_NOT_FOUND]; 13 | 14 | public description() { 15 | return 'creates all missing psalm-related files and installs all psalm composer scripts and dependencies.'; 16 | } 17 | 18 | public runsFixers(): boolean { 19 | return true; 20 | } 21 | 22 | public fixesIssue(issue: RepositoryIssue): boolean { 23 | return PsalmFixer.canFix(issue); 24 | } 25 | 26 | public static canFix(issue: RepositoryIssue): boolean { 27 | if (!super.canFix(issue)) { 28 | return false; 29 | } 30 | 31 | if (issue.name.includes('psalm')) { 32 | if (issue.kind === ComparisonKind.FILE_NOT_FOUND) { 33 | return ['psalm.xml.dist', '.github/workflows/psalm.yml'].includes(issue.name); 34 | } 35 | 36 | if (issue.kind === ComparisonKind.PACKAGE_NOT_USED) { 37 | return issue.name === 'vimeo/psalm'; 38 | } 39 | 40 | if (issue.kind === ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND) { 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public fix(): boolean { 49 | if (!this.shouldPerformFix()) { 50 | return false; 51 | } 52 | 53 | if (this.issue.kind === ComparisonKind.PACKAGE_NOT_USED) { 54 | new PackageNotUsedFixer(this.issue) 55 | .fix(); 56 | } 57 | 58 | if (this.issue.kind === ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND) { 59 | new PackageScriptNotFoundFixer(this.issue) 60 | .fix(); 61 | } 62 | 63 | if (this.issue.kind === ComparisonKind.FILE_NOT_FOUND) { 64 | new FileNotFoundFixer(this.issue) 65 | .fix(); 66 | } 67 | 68 | this.issue.resolve(this); 69 | 70 | return true; 71 | } 72 | 73 | public static prettyName(): string { 74 | return 'psalm'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-undef */ 3 | 4 | import { loadCommand } from './commands/Command'; 5 | 6 | const yargs = require('yargs'); 7 | 8 | yargs(process.argv.slice(2)) 9 | .scriptName('package-sync') 10 | // @ts-ignore 11 | .version(__APP_VERSION__) // this const is defined by esbuild at compile time 12 | .command(loadCommand(require('./commands/Analyze'))) 13 | .command(loadCommand(require('./commands/Fix'))) 14 | .command(loadCommand(require('./commands/ListFixers'))) 15 | .command(loadCommand(require('./commands/PullTemplate'))) 16 | .command(loadCommand(require('./commands/PullPackage'))) 17 | .demandCommand() 18 | .help() 19 | .wrap(120).argv; 20 | -------------------------------------------------------------------------------- /src/lib/File.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { rmdirSync, existsSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs'; 4 | import { basename, extname } from 'path'; 5 | import { config, Configuration } from '../Configuration'; 6 | import { isDirectory } from './helpers'; 7 | 8 | export enum FileType { 9 | FILE = 'file', 10 | DIRECTORY = 'directory', 11 | } 12 | 13 | export class File { 14 | protected data: string | null = null; 15 | protected type: FileType; 16 | 17 | constructor(protected fn: string, contents: string | null = null) { 18 | this.type = FileType.FILE; 19 | 20 | if (this.exists()) { 21 | this.type = isDirectory(fn) ? FileType.DIRECTORY : FileType.FILE; 22 | } 23 | 24 | this.data = contents; 25 | } 26 | 27 | static create(fn: string, contents: string | null = null) { 28 | return new File(fn, contents); 29 | } 30 | 31 | static read(fn: string) { 32 | return new File(fn) 33 | .load(); 34 | } 35 | 36 | static contents(fn: string | null): string { 37 | if (!fn) { 38 | return ''; 39 | } 40 | 41 | if (!fn.length) { 42 | return ''; 43 | } 44 | 45 | return File.create(fn).contents; 46 | } 47 | 48 | get filename() { 49 | return this.fn; 50 | } 51 | 52 | set filename(value: string) { 53 | this.fn = value; 54 | } 55 | 56 | get contents() { 57 | if (this.isDirectory()) { 58 | return ''; 59 | } 60 | 61 | if (this.data === null) { 62 | this.load(); 63 | } 64 | 65 | return this.data ?? ''; 66 | } 67 | 68 | get sizeOnDisk() { 69 | if (this.isDirectory()) { 70 | return 0; 71 | } 72 | 73 | if (!this.exists()) { 74 | return 0; 75 | } 76 | 77 | const stat = statSync(this.fn); 78 | 79 | if (!stat.isFile()) { 80 | return 0; 81 | } 82 | 83 | return stat.size; 84 | } 85 | 86 | get basename() { 87 | return basename(this.fn); 88 | } 89 | 90 | get extension() { 91 | return extname(this.fn); 92 | } 93 | 94 | public isDirectory() { 95 | return this.type === FileType.DIRECTORY; 96 | } 97 | 98 | public isFile() { 99 | return this.type === FileType.FILE; 100 | } 101 | 102 | public load() { 103 | if (!this.isFile()) { 104 | return this; 105 | } 106 | 107 | this.data = readFileSync(this.fn, { encoding: 'utf-8' }); 108 | 109 | return this; 110 | } 111 | 112 | public save() { 113 | return this.saveAs(this.fn); 114 | } 115 | 116 | public saveAs(fn: string) { 117 | if (this.isFile()) { 118 | writeFileSync(fn, this.data ?? ''); 119 | } 120 | 121 | return this; 122 | } 123 | 124 | public delete() { 125 | if (this.isFile()) { 126 | unlinkSync(this.fn); 127 | } 128 | 129 | if (this.isDirectory()) { 130 | rmdirSync(this.fn); 131 | } 132 | 133 | return this; 134 | } 135 | 136 | public exists() { 137 | return existsSync(this.fn); 138 | } 139 | 140 | public setContents(data: string) { 141 | this.data = data; 142 | 143 | return this; 144 | } 145 | 146 | public processTemplate(repoName: string, conf: Configuration | null = null): string { 147 | if (conf === null) { 148 | conf = config; 149 | } 150 | 151 | return this.contents 152 | .replace(/:vendor_name/g, conf.conf.packages.vendor ?? 'spatie') 153 | .replace(/:package_name/g, repoName) 154 | .replace(/author@domain\.com/g, conf.conf.packages.email ?? 'freek@spatie.be') 155 | .replace(/:author_homepage/g, conf.conf.packages.homepage ?? 'https://spatie.be/open-source/support-us'); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/lib/FileMerger.ts: -------------------------------------------------------------------------------- 1 | import { File } from './File'; 2 | import { LineMerger } from './LineMerger'; 3 | 4 | export class FileMerger { 5 | public merger: LineMerger; 6 | 7 | constructor() { 8 | this.merger = new LineMerger(); 9 | } 10 | 11 | static create(): FileMerger { 12 | return new FileMerger(); 13 | } 14 | 15 | public add(...filenames: string[]) { 16 | filenames.forEach(fn => { 17 | this.merger.add(File.contents(fn)); 18 | }); 19 | 20 | return this; 21 | } 22 | 23 | public merge(): string { 24 | return this.merger.merge() 25 | .join('\n'); 26 | } 27 | 28 | public mergeAndSave(targetFile: string) { 29 | File.create(targetFile) 30 | .setContents(this.merge()) 31 | .save(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/GitBranch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { runGitCommand } from './helpers'; 4 | 5 | export class GitBranch { 6 | constructor(public name: string) { 7 | // 8 | } 9 | 10 | public create() { 11 | runGitCommand(`branch ${this.name}`); 12 | 13 | return this; 14 | } 15 | 16 | public checkout(create = true) { 17 | runGitCommand(`checkout ${create ? '-b' : ''} ${this.name}`); 18 | 19 | return this; 20 | } 21 | 22 | public exists() { 23 | return !runGitCommand(`show-ref refs/heads/${this.name}`).empty; 24 | } 25 | 26 | public isCurrent() { 27 | return runGitCommand('branch --show-current') 28 | .equals(this.name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/GitCommandResult.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { SpawnSyncReturns } from 'child_process'; 3 | 4 | export class GitCommandResult { 5 | constructor(public result: SpawnSyncReturns) { 6 | //console.log(result.pid); 7 | } 8 | 9 | get empty() { 10 | return this.result.output.length > 0; 11 | } 12 | 13 | get value() { 14 | return this.result.output.join('\n'); 15 | } 16 | 17 | equals(str: string) { 18 | return str === this.value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/GitUtilties.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { basename } from 'path'; 3 | import { runCommand } from './helpers'; 4 | 5 | export class GitUtilties { 6 | public static displayStatusMessages = false; 7 | 8 | public static runCmd: CallableFunction | null = null; 9 | 10 | static runCommand(cmd: string, args: string[], cwd: string) { 11 | if (this.runCmd !== null) { 12 | return this.runCmd(cmd, args, cwd); 13 | } 14 | return runCommand(cmd, args, cwd); 15 | } 16 | 17 | static pullRepo(name: string, path: string) { 18 | if (existsSync(path)) { 19 | if (GitUtilties.displayStatusMessages) { 20 | console.log(`* Updating repository '${basename(path)}'`); 21 | } 22 | 23 | this.runCommand('git', ['pull'], path); 24 | } 25 | } 26 | 27 | static cloneRepo(name: string, parentPath: string, cloneIntoDir: string | null = null) { 28 | cloneIntoDir = cloneIntoDir ?? (name.split('/') 29 | .pop() || ''); 30 | 31 | const gitCloneUrl = `https://github.com/${name}.git`; 32 | 33 | if (!existsSync(parentPath + '/' + cloneIntoDir)) { 34 | if (GitUtilties.displayStatusMessages) { 35 | console.log(`* Cloning repository '${name}'`); 36 | } 37 | 38 | this.runCommand('git', ['clone', gitCloneUrl, cloneIntoDir], parentPath); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/LineMerger.ts: -------------------------------------------------------------------------------- 1 | import { uniqueStrings } from './helpers'; 2 | 3 | export class LineMerger { 4 | public lines: string[]; 5 | 6 | constructor() { 7 | this.lines = []; 8 | } 9 | 10 | public add(data: string | string[]) { 11 | if (!Array.isArray(data)) { 12 | const lines = data.trim() 13 | .split('\n'); 14 | this.lines.push(...lines); 15 | } else { 16 | this.lines.push(...data); 17 | } 18 | 19 | return this; 20 | } 21 | 22 | public merge(): string[] { 23 | const result: string[] = this.lines.slice(0); 24 | 25 | return uniqueStrings( 26 | result.map(line => line.trim()), 27 | // strip comments at the end of the line 28 | //.map(line => line.replace(/(.+)\s*(#.*)$/m, '$1')) 29 | // strip comments at the start of the line 30 | //.filter(line => !line.startsWith('#')) 31 | //.filter(line => line.length > 0) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/composer/Composer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { dirname } from 'path'; 4 | import { File } from '../File'; 5 | 6 | export type ComposerPackageSection = 'require' | 'require-dev'; 7 | export interface ComposerPackage { 8 | name: string; 9 | version: string; 10 | section: ComposerPackageSection; 11 | } 12 | 13 | export interface ComposerScript { 14 | name: string; 15 | command: string; 16 | } 17 | 18 | export class Composer { 19 | public rawData: any = {}; 20 | protected loaded = false; 21 | 22 | constructor(public filename: string) { 23 | this.loaded = false; 24 | } 25 | 26 | static create(filename: string) { 27 | return new Composer(filename); 28 | } 29 | 30 | static createFromPath(directoryPath: string) { 31 | return new Composer(`${directoryPath}/composer.json`); 32 | } 33 | 34 | get dirname() { 35 | return dirname(this.filename); 36 | } 37 | 38 | get data() { 39 | if (!this.loaded) { 40 | this.load(); 41 | } 42 | 43 | return this.rawData; 44 | } 45 | 46 | public toJson() { 47 | return JSON.stringify(this.data, null, 4); 48 | } 49 | 50 | public fromJson(json: string) { 51 | this.rawData = JSON.parse(json); 52 | 53 | return this; 54 | } 55 | 56 | public load() { 57 | this.rawData = require(this.filename); 58 | this.loaded = true; 59 | 60 | return this; 61 | } 62 | 63 | public hasPackage(name: string, section: ComposerPackageSection | 'all') { 64 | if (section === 'all') { 65 | return this.hasPackage(name, 'require') || this.hasPackage(name, 'require-dev'); 66 | } 67 | 68 | this.ensureSectionExists(section); 69 | 70 | return typeof this.data[section][name] !== 'undefined'; 71 | } 72 | 73 | public packageSection(name: string): ComposerPackageSection { 74 | if (this.hasPackage(name, 'require-dev')) { 75 | return 'require-dev'; 76 | } 77 | 78 | return 'require'; 79 | } 80 | 81 | public package(name: string): ComposerPackage { 82 | const version = this.packages('all') 83 | .find(pkg => pkg.name === name)?.version ?? '*'; 84 | const section = this.packageSection(name); 85 | 86 | return { name, version, section }; 87 | } 88 | 89 | public packages(kind: ComposerPackageSection | 'all'): ComposerPackage[] { 90 | const result: ComposerPackage[] = []; 91 | let pkgs: any = {}; 92 | 93 | switch (kind) { 94 | case 'require': 95 | pkgs = this.data.require || {}; 96 | break; 97 | 98 | case 'require-dev': 99 | pkgs = this.data['require-dev'] || {}; 100 | break; 101 | 102 | case 'all': 103 | pkgs = Object.assign({}, this.data.require || {}, this.data['require-dev'] || {}); 104 | break; 105 | } 106 | 107 | for (const name in pkgs) { 108 | const section = kind === 'all' ? this.packageSection(name) : kind; 109 | 110 | result.push({ name, version: pkgs[name], section }); 111 | } 112 | 113 | return result; 114 | } 115 | 116 | public packageNames(kind: ComposerPackageSection | 'all'): string[] { 117 | return this.packages(kind) 118 | .map(pkg => pkg.name); 119 | } 120 | 121 | public scripts(): ComposerScript[] { 122 | const scripts = this.data.scripts || {}; 123 | const result: ComposerScript[] = []; 124 | 125 | for (const name in scripts) { 126 | result.push({ name, command: scripts[name] }); 127 | } 128 | 129 | return result; 130 | } 131 | 132 | public addPackage(pkg: ComposerPackage) { 133 | this.ensureSectionExists(pkg.section); 134 | this.data[pkg.section][pkg.name] = pkg.version; 135 | 136 | return this; 137 | } 138 | 139 | public removePackage(pkg: ComposerPackage) { 140 | this.ensureSectionExists(pkg.section); 141 | 142 | if (typeof this.data[pkg.section][pkg.name] !== 'undefined') { 143 | delete this.data[pkg.section][pkg.name]; 144 | } 145 | 146 | return this; 147 | } 148 | 149 | public setPackageVersion(pkg: ComposerPackage, version: string) { 150 | this.ensureSectionExists(pkg.section); 151 | 152 | this.rawData[pkg.section][pkg.name] = version; 153 | pkg.version = version; 154 | 155 | return this; 156 | } 157 | 158 | public script(name: string): ComposerScript { 159 | return this.scripts() 160 | .find(script => script.name === name) ?? {}; 161 | } 162 | 163 | public scriptNames(): string[] { 164 | return this.scripts() 165 | .map(script => script.name); 166 | } 167 | 168 | public hasScript(name: string): boolean { 169 | return this.scriptNames() 170 | .includes(name); 171 | } 172 | 173 | public addScript(script: ComposerScript) { 174 | this.ensureSectionExists('scripts'); 175 | this.data.scripts[script.name] = script.command; 176 | 177 | return this; 178 | } 179 | 180 | public removeScript(name: string) { 181 | this.ensureSectionExists('scripts'); 182 | 183 | if (typeof this.data.scripts[name] !== 'undefined') { 184 | delete this.data.scripts[name]; 185 | } 186 | 187 | return this; 188 | } 189 | 190 | public save() { 191 | File.create(this.filename) 192 | .setContents(this.toJson()) 193 | .save(); 194 | 195 | return this; 196 | } 197 | 198 | public ensureSectionExists(section: string) { 199 | if (typeof this.data[section] === 'undefined') { 200 | this.data[section] = {}; 201 | } 202 | 203 | return this; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | import { existsSync, lstatSync, readdirSync, statSync } from 'fs'; 3 | import { basename } from 'path'; 4 | import { GitCommandResult } from './GitCommandResult'; 5 | 6 | const micromatch = require('micromatch'); 7 | 8 | export function isDirectory(path: string): boolean { 9 | try { 10 | const stat = lstatSync(path); 11 | return stat.isDirectory(); 12 | } catch (e) { 13 | // lstatSync throws an error if path doesn't exist 14 | return false; 15 | // 16 | } 17 | } 18 | 19 | export function fileSize(fn: string): number { 20 | if (!existsSync(fn)) { 21 | return 0; 22 | } 23 | 24 | const stat = statSync(fn); 25 | 26 | if (!stat.isFile()) { 27 | return 0; 28 | } 29 | 30 | return stat.size; 31 | } 32 | 33 | export function getFileList(directory: string, basePath: string | null = null, recursive = true) { 34 | const result: string[] = []; 35 | 36 | readdirSync(directory) 37 | .filter(f => !['.', '..', '.git', '.idea', '.vscode', 'node_modules', 'vendor'].includes(basename(f))) 38 | .forEach(fn => { 39 | const fqName = `${directory}/${fn}`; 40 | 41 | result.push(fqName); 42 | 43 | if (recursive && isDirectory(fqName)) { 44 | result.push(...getFileList(fqName, basePath || directory, recursive)); 45 | } 46 | }); 47 | 48 | return uniqueArray(result); 49 | } 50 | 51 | export function uniqueArray(a: any[]): any[] { 52 | return [...new Set(a)]; 53 | } 54 | 55 | export function uniqueStrings(arr: string[], allowEmptyLines = true, sortResult = false): string[] { 56 | const result: string[] = []; 57 | 58 | for (let i = 0, l = arr.length; i < l; i++) { 59 | if (allowEmptyLines && arr[i].trim().length === 0) { 60 | result.push(''); 61 | continue; 62 | } 63 | 64 | if (result.indexOf(arr[i]) === -1) { 65 | result.push(arr[i]); 66 | } 67 | } 68 | 69 | if (sortResult) { 70 | return result.sort(); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | export function runCommand(cmd: string, args: string[], cwd: string | undefined = undefined, stdio: any = 'inherit') { 77 | return spawnSync(cmd, args, { cwd: cwd, stdio: stdio, encoding: 'utf8', env: process.env }); 78 | } 79 | 80 | export function runGitCommand(cmd: string, cwd: string | undefined = undefined, stdio: any = 'pipe'): GitCommandResult { 81 | const result = runCommand('git', cmd.replace(/\s\s+/g, ' ') 82 | .split(' '), cwd, stdio); 83 | 84 | return new GitCommandResult(result); 85 | } 86 | 87 | export const last = (arr: any[]) => arr[arr.length - 1] ?? undefined; 88 | 89 | export function classOf(o: T): any { 90 | return (o as any).constructor; 91 | } 92 | 93 | export function matches(str: string | string[], patterns: string | string[]): boolean { 94 | if (!Array.isArray(str)) { 95 | str = [str]; 96 | } 97 | 98 | for (const s of str) { 99 | if (micromatch.isMatch(s, patterns)) { 100 | return true; 101 | } 102 | } 103 | 104 | return false; 105 | } 106 | -------------------------------------------------------------------------------- /src/printers/ConsolePrinter.ts: -------------------------------------------------------------------------------- 1 | import { app } from '../Application'; 2 | import { Repository } from '../repositories/Repository'; 3 | import Table from 'cli-table3'; 4 | import { ComparisonKind } from '../types/FileComparisonResult'; 5 | import Fixer from '../fixers/Fixer'; 6 | 7 | const chalk = require('chalk'); 8 | 9 | const colorText = (text: string, kind: ComparisonKind) => ConsolePrinter.kindColor(kind)(text); 10 | 11 | export class ConsolePrinter { 12 | protected static makeTable(columns: Record): Table.Table { 13 | const headerSep: string[] = []; 14 | 15 | const table = new Table({ 16 | head: Object.keys(columns), 17 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 18 | style: { 19 | head: [], //disable colors in header cells 20 | border: [], //disable colors for the border 21 | }, 22 | colWidths: Object.values(columns), 23 | }); 24 | 25 | Object.values(columns) 26 | .forEach(w => { 27 | headerSep.push('-'.padEnd(w > 3 ? w - 3 : w, '-')); 28 | }); 29 | 30 | table.push(headerSep); 31 | 32 | return table; 33 | } 34 | 35 | public static printTable(table: Table.Table) { 36 | const output = table.toString() + '\n\n'; 37 | 38 | process.stdout.write(output); 39 | } 40 | 41 | public static kindColor(kind: ComparisonKind) { 42 | const colors = { 43 | [ComparisonKind.DIRECTORY_NOT_FOUND]: '#7DD3FC', //#3B82F6', 44 | [ComparisonKind.FILE_NOT_FOUND]: '#7DD3FC', //#3B82F6', 45 | [ComparisonKind.PACKAGE_NOT_USED]: '#818CF8', 46 | [ComparisonKind.PACKAGE_SCRIPT_NOT_FOUND]: '#10B981', 47 | [ComparisonKind.PACKAGE_VERSION_MISMATCH]: '#A78BFA', 48 | [ComparisonKind.FILE_NOT_SIMILAR_ENOUGH]: '#0EA5E9', //#475569', 49 | [ComparisonKind.ALLOWED_SIZE_DIFFERENCE_EXCEEDED]: '#64748B', 50 | [ComparisonKind.FILE_NOT_IN_SKELETON]: '#64748B', 51 | }; 52 | 53 | const color = colors[kind] ?? '#FAFAF9'; 54 | 55 | return chalk.hex(color); 56 | } 57 | 58 | public static printFixerSummary(fixers: Fixer[]) { 59 | const table = this.makeTable({ 60 | 'fixer name': 18, 61 | type: 8, 62 | description: 75, 63 | }); 64 | 65 | fixers 66 | .sort((a, b) => a.getName() 67 | .localeCompare(b.getName())) 68 | .forEach(fixer => { 69 | let type = '', 70 | name = fixer.getName(); 71 | const desc = fixer.description() 72 | .replace(/(?![^\n]{1,73}$)([^\n]{1,73})\s/g, '$1\n'); // wrap at 73 chars 73 | 74 | if (fixer.runsFixers()) { 75 | name = chalk.hex('#60A5FA')(name); 76 | type = chalk.hex('#60A5FA')('multi'); 77 | } 78 | 79 | if (fixer.isRisky()) { 80 | name = chalk.hex('#FCA5A5')(name); 81 | type = chalk.hex('#FCA5A5')('risky'); 82 | } 83 | 84 | table.push([name, type, desc]); 85 | }); 86 | 87 | return table; 88 | } 89 | 90 | public static printRepositoryIssues(repo: Repository) { 91 | const table = this.makeTable({ 92 | issue: 15, 93 | score: 8, 94 | filename: 45, 95 | fixers: 30, 96 | //notes: 30, 97 | }); 98 | 99 | repo.issues 100 | .filter(issue => !issue.resolved) 101 | .filter(issue => !app.config.ignoreNames.includes(issue.name)) 102 | .filter(issue => !app.config.skipComparisons.includes(issue.name)) 103 | .filter(issue => !app.config.issues.ignored[issue.kind]?.includes(issue.name) ?? true) 104 | .sort((a, b) => (a.kind + a.score).localeCompare(b.kind + b.score)) 105 | .forEach(issue => { 106 | const fixers = issue.fixers 107 | .map(fixer => { 108 | // display risky fixers in red 109 | if (fixer.isRisky()) { 110 | return chalk.hex('#FCA5A5')(fixer.getName()); 111 | } 112 | // multi fixers in blue 113 | if (fixer.runsFixers()) { 114 | return chalk.hex('#60A5FA')(fixer.getName()); 115 | } 116 | // safe fixer in green 117 | return chalk.hex('#4ADE80')(fixer.getName()); 118 | }) 119 | .join(', '); 120 | 121 | table.push([ 122 | colorText(issue.kind, issue.kind), 123 | issue.score, 124 | colorText(issue.name, issue.kind), 125 | fixers, 126 | //issue.note?.toString() ?? '', 127 | ]); 128 | }); 129 | 130 | return table; 131 | } 132 | 133 | public static printRepositoryFixerResults(repo: Repository) { 134 | const table = this.makeTable({ filename: 40, fixer: 15, status: 65 }); 135 | 136 | repo.issues 137 | .filter(issue => !app.config.ignoreNames.includes(issue.name)) 138 | .filter(issue => !app.config.skipComparisons.includes(issue.name)) 139 | .filter(issue => !app.config.issues.ignored[issue.kind]?.includes(issue.name) ?? true) 140 | .sort((a, b) => a.kind.localeCompare(b.kind)) 141 | .forEach(issue => { 142 | if (issue.resolvedByFixer === 'none') { 143 | issue.resolvedByFixer = '-'; 144 | // issue.addResolvedNote('issue unresolved'); 145 | } 146 | 147 | table.push([colorText(issue.name, issue.kind), issue.resolvedByFixer, issue.resolvedNotes.join('; ')]); 148 | }); 149 | 150 | return table; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/repositories/Repository.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { basename } from 'path'; 5 | import { config } from '../Configuration'; 6 | import { RepositoryIssue } from './RepositoryIssue'; 7 | import { Composer } from '../lib/composer/Composer'; 8 | import { getFileList } from '../lib/helpers'; 9 | import { RepositoryFile } from './RepositoryFile'; 10 | 11 | export enum RepositoryKind { 12 | SKELETON = 'skeleton', 13 | PACKAGE = 'package', 14 | } 15 | 16 | export class Repository { 17 | public composerData: Composer; 18 | protected loadedFiles = false; 19 | public fileList: RepositoryFile[] = []; 20 | public issues: RepositoryIssue[] = []; 21 | 22 | constructor(public path: string, public kind: RepositoryKind) { 23 | this.composerData = Composer.createFromPath(path); 24 | } 25 | 26 | static create(path: string, kind: RepositoryKind) { 27 | return new Repository(path, kind); 28 | } 29 | 30 | get name() { 31 | return basename(this.path); 32 | } 33 | 34 | get composer() { 35 | return this.composerData; 36 | } 37 | 38 | get packages() { 39 | return this.composer.packages('all'); 40 | } 41 | 42 | get files() { 43 | if (!this.loadedFiles) { 44 | this.loadFiles(); 45 | } 46 | 47 | return this.fileList; 48 | } 49 | 50 | getFile(relativeName: string): RepositoryFile | null { 51 | return this.files.find(f => f.relativeName === relativeName) || null; 52 | } 53 | 54 | hasFile(file: RepositoryFile): boolean { 55 | return this.files.find(f => f.relativeName === file.relativeName) !== undefined; 56 | } 57 | 58 | protected getFileList(directory: string, basePath: string | null = null): RepositoryFile[] { 59 | const result: RepositoryFile[] = []; 60 | 61 | getFileList(directory, basePath, true) 62 | .filter(fn => !config.shouldIgnoreFile(fn.replace(`${basePath}/`, ''))) 63 | .forEach(fqName => { 64 | const rfile = new RepositoryFile(this, fqName); 65 | const relativeName = basePath ? fqName.replace(`${basePath}/`, '') : fqName; 66 | 67 | if (!result.find(item => item.relativeName === relativeName)) { 68 | result.push( 69 | rfile.withOptions( 70 | config.shouldIgnoreFile(relativeName), 71 | rfile.isFile() && config.shouldCompareFile(fqName), 72 | config.getFileScoreRequirements(fqName), 73 | ), 74 | ); 75 | 76 | if (rfile.isDirectory()) { 77 | result.push(...this.getFileList(fqName, basePath ?? directory)); 78 | } 79 | } 80 | }); 81 | 82 | return result; 83 | } 84 | 85 | public loadFiles() { 86 | this.fileList = this.getFileList(this.path, this.path); 87 | this.loadedFiles = true; 88 | 89 | return this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/repositories/RepositoryFile.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonScoreRequirements } from '../types/ComparisonScoreRequirements'; 4 | import { File } from '../lib/File'; 5 | 6 | export class RepositoryFile extends File { 7 | public shouldIgnore = false; 8 | public shouldCompare = false; 9 | public requiredScores: ComparisonScoreRequirements = { similar: 0, size: 0 }; 10 | 11 | /** 12 | * 13 | * @param repository Repository class 14 | * @param fn string 15 | * @param contents string | null 16 | */ 17 | constructor(public repository: any, protected fn: string, contents: string | null = null) { 18 | super(fn, contents); 19 | } 20 | 21 | get relativeName() { 22 | return this.fn.replace(this.repository.path + '/', ''); 23 | } 24 | 25 | public withOptions(ignore: boolean, compare: boolean, requiredScores: ComparisonScoreRequirements) { 26 | this.shouldIgnore = ignore; 27 | this.shouldCompare = compare; 28 | this.requiredScores = requiredScores; 29 | // 30 | return this; 31 | } 32 | 33 | public processTemplate(): string { 34 | return super.processTemplate(this.repository.name); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/repositories/RepositoryIssue.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { ComparisonKind } from '../types/FileComparisonResult'; 4 | import { Repository } from './Repository'; 5 | import { RepositoryFile } from './RepositoryFile'; 6 | //import Fixer from './fixers/Fixer'; 7 | import { classOf } from '../lib/helpers'; 8 | 9 | export class RepositoryIssue { 10 | protected _fixers: any[] = []; 11 | public resolvedByFixer = 'none'; 12 | public resolvedNotes: string[] = []; 13 | public pending = false; 14 | 15 | constructor( 16 | public result: any, 17 | public name: string, 18 | public srcFile: RepositoryFile | null, 19 | public destFile: RepositoryFile | null, 20 | public skeleton: Repository, 21 | public repository: Repository, 22 | public resolved: boolean = false, 23 | public note: string | null = null, 24 | public context: any | null = null, 25 | ) { 26 | // 27 | } 28 | 29 | get fixers() { 30 | const same = (a, b, method) => (a[method]() && b[method]()) || (!a[method]() && !b[method]()); 31 | const isTrue = (a, method) => (a[method]() ? 1 : -1); 32 | const methodCompare = (a, b, method) => (same(a, b, method) ? 0 : isTrue(a, method)); 33 | 34 | return this._fixers.sort((a, b) => methodCompare(a, b, 'runsFixers')) 35 | .sort((a, b) => methodCompare(a, b, 'isRisky')); 36 | } 37 | 38 | get kind(): ComparisonKind { 39 | return this.result.kind; 40 | } 41 | 42 | get score(): number | string { 43 | if (this.result.score === 0) { 44 | return '-'; 45 | } 46 | 47 | if (typeof this.result.score === 'number') { 48 | return this.result.score.toFixed(3); 49 | } 50 | 51 | return this.result.score; 52 | } 53 | 54 | get sourcefile(): RepositoryFile { 55 | return this.srcFile; 56 | } 57 | 58 | get targetfile(): RepositoryFile { 59 | return this.destFile; 60 | } 61 | 62 | public addFixer(fixer: any) { 63 | if (this.fixers.find(f => f.getName() === fixer.getName()) === undefined) { 64 | this.fixers.push(fixer); 65 | } 66 | 67 | return this; 68 | } 69 | 70 | public is(kind: ComparisonKind): boolean { 71 | return this.kind === kind; 72 | } 73 | 74 | public resolve(resolvedByFixer: string | any) { 75 | if (typeof resolvedByFixer !== 'string') { 76 | resolvedByFixer = classOf(resolvedByFixer) 77 | .prettyName(); 78 | } 79 | 80 | this.resolvedByFixer = resolvedByFixer; 81 | this.resolved = true; 82 | 83 | return this; 84 | } 85 | 86 | public addResolvedNote(note: string, prepend = false) { 87 | if (prepend) { 88 | this.resolvedNotes.unshift(note); 89 | return this; 90 | } 91 | 92 | this.resolvedNotes.push(note); 93 | 94 | return this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/repositories/RepositoryValidator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import { existsSync, mkdirSync } from 'fs'; 5 | import { Command } from '../commands/Command'; 6 | import PullPackageCommand from '../commands/PullPackage'; 7 | import PullTemplateCommand from '../commands/PullTemplate'; 8 | import { RepositoryKind } from './Repository'; 9 | 10 | export class RepositoryValidator { 11 | constructor( 12 | public packagesPath: string, 13 | public templatesPath: string, 14 | public pullPackageCmd: Command | null = null, 15 | public pullTemplateCmd: Command | null = null, 16 | ) { 17 | this.pullPackageCmd = pullPackageCmd ?? PullPackageCommand; 18 | this.pullTemplateCmd = pullTemplateCmd ?? PullTemplateCommand; 19 | } 20 | 21 | ensureExists(name: string, kind: RepositoryKind) { 22 | if (kind === RepositoryKind.PACKAGE) { 23 | this.ensurePathExists(this.packagesPath); 24 | // @ts-ignore 25 | this.pullPackageCmd.handle({ name }); 26 | return true; 27 | } 28 | 29 | if (kind === RepositoryKind.SKELETON) { 30 | this.ensurePathExists(this.templatesPath); 31 | // @ts-ignore 32 | this.pullTemplateCmd.handle({ name }); 33 | return true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | ensurePackageExists(name: string) { 40 | return this.ensureExists(name, RepositoryKind.PACKAGE); 41 | } 42 | 43 | ensureTemplateExists(name: string) { 44 | return this.ensureExists(name, RepositoryKind.SKELETON); 45 | } 46 | 47 | protected ensurePathExists(path: string) { 48 | if (path.length && !existsSync(path)) { 49 | mkdirSync(path, { recursive: true }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/types/ComparisonScoreRequirements.ts: -------------------------------------------------------------------------------- 1 | export interface ComparisonScoreRequirements { 2 | similar: number; 3 | size: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/FileComparisonResult.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | export enum ComparisonKind { 4 | PACKAGE_NOT_USED = 'missing_pkg', 5 | PACKAGE_VERSION_MISMATCH = 'pkg_version', 6 | PACKAGE_SCRIPT_NOT_FOUND = 'pkg_script', 7 | 8 | FILE_NOT_FOUND = 'missing_file', 9 | DIRECTORY_NOT_FOUND = 'missing_dir', 10 | 11 | FILE_DOES_NOT_MATCH = 'not_exact', 12 | FILE_NOT_SIMILAR_ENOUGH = 'not_similar', 13 | 14 | FILE_NOT_IN_SKELETON = 'extra_file', 15 | DIRECTORY_NOT_IN_SKELETON = 'extra_dir', 16 | 17 | ALLOWED_SIZE_DIFFERENCE_EXCEEDED = 'size_diff', 18 | } 19 | 20 | export interface FileComparisonResult { 21 | kind: ComparisonKind; 22 | score: string | number; 23 | name: string; 24 | context?: any; 25 | skeletonPath: string; 26 | repositoryPath: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/FileScoreRequirements.ts: -------------------------------------------------------------------------------- 1 | import { ComparisonScoreRequirements } from './ComparisonScoreRequirements'; 2 | 3 | export interface FileScoreRequirements { 4 | name: string; 5 | scores: ComparisonScoreRequirements; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/ScoreRequirements.ts: -------------------------------------------------------------------------------- 1 | import { ComparisonScoreRequirements } from './ComparisonScoreRequirements'; 2 | import { FileScoreRequirements } from './FileScoreRequirements'; 3 | 4 | export interface ScoreRequirements { 5 | defaults: ComparisonScoreRequirements; 6 | files: Array; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | "lib": ["esnext"] /* Specify library files to be included in the compilation. */, 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./dist/temp", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": true /* Do not emit comments to output. */, 22 | // "noEmit": true /* Do not emit outputs. */, 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true /* Skip type checking of declaration files. */, 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 69 | "suppressExcessPropertyErrors": false 70 | } 71 | } 72 | --------------------------------------------------------------------------------