From 41fd41bef665ef42be2dfdd521c6c17b56c86b1a Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:01:59 -0300 Subject: [PATCH 01/29] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20project=20struct?= =?UTF-8?q?ure=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/jsconfig.json | 5 + packages/cli/package.json | 41 +++++++ packages/cli/src/index.js | 225 ++++++++++++++++++++++++++++++++++++ packages/cli/src/types.d.ts | 17 +++ pnpm-lock.yaml | 74 +++++++++++- 5 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 packages/cli/jsconfig.json create mode 100644 packages/cli/package.json create mode 100755 packages/cli/src/index.js create mode 100644 packages/cli/src/types.d.ts diff --git a/packages/cli/jsconfig.json b/packages/cli/jsconfig.json new file mode 100644 index 0000000..d4e37f4 --- /dev/null +++ b/packages/cli/jsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["./node_modules/**", "./dist/**"], + "include": ["./index.d.ts", "./src/**/*.ts", "./src/**/*.js"], +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..c3df4da --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,41 @@ +{ + "name": "@eslit/cli", + "version": "0.1.0", + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint ." + }, + "keywords": [], + "author": { + "email": "contact.guz013@gmail.com", + "name": "Gustavo \"Guz\" L. de Mello", + "url": "https://guz.one" + }, + "module": "./src/index.js", + "source": "./src/index.js", + "files": [ + "src", + "index.d.ts" + ], + "homepage": "https://github.com/LoredDev/ESLit", + "type": "module", + "repository": { + "directory": "packages/config", + "type": "git", + "url": "https://github.com/LoredDev/ESLit" + }, + "bin": "./src/index.js", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.2", + "nanospinner": "^1.1.0", + "picocolors": "^1.0.0", + "picomatch": "^2.3.1", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/node": "^20.4.2", + "yaml": "^2.3.1" + } +} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js new file mode 100755 index 0000000..fc7457a --- /dev/null +++ b/packages/cli/src/index.js @@ -0,0 +1,225 @@ +#!node +import fs from 'node:fs/promises'; +import path, { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import YAML from 'yaml'; +import glob from 'picomatch'; + + +/** + * @template T + * + * @param {Promise} promise - The async function to try running + * @returns {Promise} - Returns the result of the async function, or null if it errors + */ +async function tryRun(promise) { + try { + return await promise; + } + catch (err) { + return null; + } +} + +/** + * @param {string} directory - The directory to find .gitignore and .eslintignore + * @returns {Promise} - List of ignore glob patterns + */ +async function getIgnoredFiles(directory) { + const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + return [...eslintIgnore, ...gitIgnore]; +} + +/** + * @param {string} directory - The directory to work in. + * @returns {Promise} - The package name founded. + */ +async function getPackageName(directory) { + if (existsSync(join(directory, 'package.json'))) { + const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); + /** @type {{name?: string}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const obj = JSON.parse(file); + + if (obj.name) return obj.name; + } + return path.normalize(directory).split('/').at(-1) ?? directory; +} + +/** + * @param {string} directory - The directory path to work on + * @param {string[]} files - The file list to be filtered + * @param {string[]} [packages] - The packages to be filtered + * @returns {Promise} - The package object + */ +async function getRootPackage(directory, files, packages = []) { + + const ignorePatterns = [ + ...packages.map(p => + `${join(directory, p, '**/*')}`, + )]; + + console.log(ignorePatterns); + + return { + name: `${await getPackageName(directory)} [ROOT]`, + files: files.filter(f => + // glob.isMatch(f, join(directory, '*/**')) && + !glob.isMatch(f, ignorePatterns), + ) ?? [], + }; +} + +class Cli { + /** @type {string} */ + dir = process.cwd(); + + /** + * @param {string} [directory] - The directory to the cli work on + * @param {string[]} [packages] - List of packages paths in the workspace + */ + constructor( + directory, + packages, + ) { + this.dir ||= path.normalize(directory ?? this.dir); + this.packages ||= packages; + } + + /** @type {{files: string[], directories: string[]} | undefined} */ + #paths; + + /** + * @param {string} [directory] - The directory to work on + * @param {string[]} [ignores] - Glob patterns to ignore + * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory + */ + async getPaths(directory = this.dir, ignores = []) { + + ignores.push( + ...[ + '.git', + '.dist', + '.DS_Store', + 'node_modules', + ].map((f) => join(directory, f)), + ...await getIgnoredFiles(directory), + ); + + const paths = (await fs.readdir(directory)) + .map((f) => path.normalize(join(directory, f))) + .filter((p) => !glob.isMatch(p, ignores)); + + /** @type {string[]} */ + const files = []; + /** @type {string[]} */ + const directories = []; + + for (const path of paths) { + if ((await fs.lstat(path)).isDirectory()) { + const subPaths = await this.getPaths(path, ignores); + directories.push(path, ...subPaths.directories); + files.push(...subPaths.files); + } + else { + files.push(path); + } + } + return { files, directories }; + } + + /** @type {string[] | undefined} */ + packages; + + /** + * @returns {Promise} - List of packages on a directory; + */ + async getPackages() { + + /** @type {string[]} */ + let packages = []; + + const pnpmWorkspace = + existsSync(join(this.dir, 'pnpm-workspace.yaml')) + ? 'pnpm-workspace.yaml' + : existsSync(join(this.dir, 'pnpm-workspace.yml')) + ? 'pnpm-workspace.yml' + : null; + + if (pnpmWorkspace) { + const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); + + /** @type {{packages?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const fileObj = YAML.parse(fileYaml); + + packages.push(...(fileObj?.packages ?? [])); + } + else if (existsSync(join(this.dir, 'package.json'))) { + const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); + + /** @type {{workspaces?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const packageObj = JSON.parse(packageJson); + + packages.push(...(packageObj?.workspaces ?? [])); + } + return packages; + } + + /** @type {import('./types').Workspace | undefined} */ + #workspace; + + /** + * @returns {Promise} + * The workspace structure and packages founded + */ + async getWorkspace() { + console.log(this.packages); + const rootPackage = await getRootPackage(this.dir, this.#paths?.files ?? [], this.packages); + + /** @type {string[]} */ + const packagesPaths = this.#paths?.directories.filter(d => + glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), + ) ?? []; + + /** @type {import('./types').Package[]} */ + const packages = []; + + for (const pkgPath of packagesPaths) { + packages.push({ + name: await getPackageName(pkgPath), + files: this.#paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + }); + } + + return { + packages: [ + rootPackage, + ...packages, + ], + }; + } + + async run() { + this.packages ||= await this.getPackages(); + this.#paths = await this.getPaths(); + this.#workspace = await this.getWorkspace(); + + + + console.log(this.dir); + console.log(this.#workspace.packages); + } +} + +await new Cli().run(); diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts new file mode 100644 index 0000000..7dd4775 --- /dev/null +++ b/packages/cli/src/types.d.ts @@ -0,0 +1,17 @@ + +interface Options { + environment: { + node: boolean + deno: boolean + browser: boolean + } +} + +export interface Workspace { + packages: Package[] +} + +export interface Package { + name: string + files: string[] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60c9ef7..ea44a29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@commitlint/types': specifier: ^17.4.4 version: 17.4.4 + '@eslit/cli': + specifier: workspace:* + version: link:packages/cli '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -34,8 +37,17 @@ importers: specifier: ^1.10.9 version: 1.10.9 + fixtures/library: + dependencies: + '@eslit/cli': + specifier: workspace:* + version: link:../../packages/cli + fixtures/svelte: devDependencies: + '@eslit/cli': + specifier: workspace:* + version: link:../../packages/cli '@fontsource/fira-mono': specifier: ^4.5.10 version: 4.5.10 @@ -79,6 +91,31 @@ importers: specifier: ^4.4.2 version: 4.4.2 + packages/cli: + dependencies: + magic-string: + specifier: ^0.30.2 + version: 0.30.2 + nanospinner: + specifier: ^1.1.0 + version: 1.1.0 + picocolors: + specifier: ^1.0.0 + version: 1.0.0 + picomatch: + specifier: ^2.3.1 + version: 2.3.1 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + devDependencies: + '@types/node': + specifier: ^20.4.2 + version: 20.4.2 + yaml: + specifier: ^2.3.1 + version: 2.3.1 + packages/config: dependencies: '@eslint/eslintrc': @@ -99,9 +136,6 @@ importers: globals: specifier: ^13.20.0 version: 13.20.0 - yaml: - specifier: ^2.3.1 - version: 2.3.1 devDependencies: '@types/eslint__js': specifier: ^8.42.0 @@ -666,7 +700,6 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -2457,6 +2490,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -2547,6 +2585,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magic-string@0.30.2: + resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2649,6 +2694,12 @@ packages: hasBin: true dev: true + /nanospinner@1.1.0: + resolution: {integrity: sha512-yFvNYMig4AthKYfHFl1sLj7B2nkHL4lzdig4osvl9/LdGbXwrdFRoqBS98gsEsOakr0yH+r5NZ/1Y9gdVB8trA==} + dependencies: + picocolors: 1.0.0 + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -2814,7 +2865,6 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -2887,6 +2937,14 @@ packages: hasBin: true dev: true + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true @@ -3118,6 +3176,10 @@ packages: totalist: 3.0.1 dev: true + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3753,7 +3815,7 @@ packages: /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} - dev: false + dev: true /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} From 86c178419c6e423c3e9140ccdf7fc7a51b2e9b55 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:18:30 -0300 Subject: [PATCH 02/29] =?UTF-8?q?fix(deps):=20=F0=9F=90=9B=20commit=20miss?= =?UTF-8?q?ing=20package.json=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fixtures/library/package.json | 14 +++++++++++++ fixtures/svelte/package.json | 4 +++- package.json | 1 + packages/config/package.json | 37 +++++++++++++++++------------------ 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 fixtures/library/package.json diff --git a/fixtures/library/package.json b/fixtures/library/package.json new file mode 100644 index 0000000..57168b6 --- /dev/null +++ b/fixtures/library/package.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "pnpm cli" + }, + "dependencies": { + "@eslit/cli": "workspace:*" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/fixtures/svelte/package.json b/fixtures/svelte/package.json index 15c07a2..1b96188 100644 --- a/fixtures/svelte/package.json +++ b/fixtures/svelte/package.json @@ -7,9 +7,11 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test:lint": "eslint ." + "test:lint": "eslint .", + "test:cli": "pnpm cli" }, "devDependencies": { + "@eslit/cli": "workspace:*", "@fontsource/fira-mono": "^4.5.10", "@neoconfetti/svelte": "^1.0.0", "@sveltejs/adapter-auto": "^2.0.0", diff --git a/package.json b/package.json index bbd5ed8..5970f08 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@eslit/config": "workspace:*" }, "devDependencies": { + "@eslit/cli": "workspace:*", "@changesets/cli": "^2.26.2", "@commitlint/config-conventional": "^17.6.6", "@commitlint/types": "^17.4.4", diff --git a/packages/config/package.json b/packages/config/package.json index 9684538..a7bee8e 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -5,11 +5,11 @@ "main": "index.js", "module": "./src/index.js", "source": "./src/index.js", - "files": [ - "src", - "index.d.ts" - ], - "homepage": "https://github.com/LoredDev/ESLit", + "files": [ + "src", + "index.d.ts" + ], + "homepage": "https://github.com/LoredDev/ESLit", "exports": { "default": "./src/index.js", "import": "./src/index.js", @@ -21,16 +21,16 @@ "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint ." }, - "repository": { - "directory": "packages/config", - "type": "git", - "url": "https://github.com/LoredDev/ESLit" - }, + "repository": { + "directory": "packages/config", + "type": "git", + "url": "https://github.com/LoredDev/ESLit" + }, "author": { - "email": "contact.guz013@gmail.com", - "name": "Gustavo \"Guz\" L. de Mello", - "url": "https://guz.one" - }, + "email": "contact.guz013@gmail.com", + "name": "Gustavo \"Guz\" L. de Mello", + "url": "https://guz.one" + }, "license": "MIT", "devDependencies": { "@types/eslint__js": "^8.42.0", @@ -44,14 +44,13 @@ "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint-plugin-jsdoc": "^46.4.4", - "globals": "^13.20.0", - "yaml": "^2.3.1" + "globals": "^13.20.0" }, "peerDependencies": { "eslint": "^8.45.0", "typescript": "^5.1.6" }, - "publishConfig": { - "access": "public" - } + "publishConfig": { + "access": "public" + } } From 5752e76197e6d413eec9048a133fd1cd67ad45e4 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:19:26 -0300 Subject: [PATCH 03/29] =?UTF-8?q?chore:=20=F0=9F=94=A7=20make=20`fixture/l?= =?UTF-8?q?ibrary`=20private?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fixtures/library/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/fixtures/library/package.json b/fixtures/library/package.json index 57168b6..d50b4d5 100644 --- a/fixtures/library/package.json +++ b/fixtures/library/package.json @@ -2,6 +2,7 @@ "version": "1.0.0", "description": "", "main": "index.js", + "private": true, "scripts": { "test": "pnpm cli" }, From 03a9ce3de5fc4db03957f4e3f9e496cd7a838060 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:20:12 -0300 Subject: [PATCH 04/29] =?UTF-8?q?chore(fixtures):=20=F0=9F=94=A7=20add=20p?= =?UTF-8?q?ackage=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fixtures/library/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/fixtures/library/package.json b/fixtures/library/package.json index d50b4d5..5f3a745 100644 --- a/fixtures/library/package.json +++ b/fixtures/library/package.json @@ -1,4 +1,5 @@ { + "name": "@eslit-fixtures/library", "version": "1.0.0", "description": "", "main": "index.js", From 1296891431117c9a386d36d84c8d402013c3a094 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:23:14 -0300 Subject: [PATCH 05/29] =?UTF-8?q?chore:=20=F0=9F=94=A7=20add=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/curly-tomatoes-enjoy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curly-tomatoes-enjoy.md diff --git a/.changeset/curly-tomatoes-enjoy.md b/.changeset/curly-tomatoes-enjoy.md new file mode 100644 index 0000000..1504887 --- /dev/null +++ b/.changeset/curly-tomatoes-enjoy.md @@ -0,0 +1,5 @@ +--- +"@eslit/cli": minor +--- + +Now the cli can automatically detect the workspace structure on monorepos and single repositories From fdad36331317721e081e307b78b74d1365697e66 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:57:33 -0300 Subject: [PATCH 06/29] =?UTF-8?q?chore(deps):=20=F0=9F=94=97=20make=20yaml?= =?UTF-8?q?=20a=20dependency=20and=20not=20devdep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 8 ++++---- pnpm-lock.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index c3df4da..6f053b4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@eslit/cli", - "version": "0.1.0", + "version": "0.0.0", "description": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", @@ -32,10 +32,10 @@ "nanospinner": "^1.1.0", "picocolors": "^1.0.0", "picomatch": "^2.3.1", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "yaml": "^2.3.1" }, "devDependencies": { - "@types/node": "^20.4.2", - "yaml": "^2.3.1" + "@types/node": "^20.4.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea44a29..727fcda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,13 +108,13 @@ importers: prompts: specifier: ^2.4.2 version: 2.4.2 + yaml: + specifier: ^2.3.1 + version: 2.3.1 devDependencies: '@types/node': specifier: ^20.4.2 version: 20.4.2 - yaml: - specifier: ^2.3.1 - version: 2.3.1 packages/config: dependencies: @@ -3815,7 +3815,7 @@ packages: /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} - dev: true + dev: false /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} From 48b70de8d91ac4ab102960b4f7f9ea8f5eeed5dc Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:19:05 -0300 Subject: [PATCH 07/29] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20list=20directori?= =?UTF-8?q?es=20of=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/index.js | 13 ++++++++----- packages/cli/src/types.d.ts | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index fc7457a..a2c9327 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -57,11 +57,11 @@ async function getPackageName(directory) { /** * @param {string} directory - The directory path to work on - * @param {string[]} files - The file list to be filtered + * @param {{files: string[], directories: string[]}} paths - The file list to be filtered * @param {string[]} [packages] - The packages to be filtered * @returns {Promise} - The package object */ -async function getRootPackage(directory, files, packages = []) { +async function getRootPackage(directory, paths, packages = []) { const ignorePatterns = [ ...packages.map(p => @@ -72,10 +72,12 @@ async function getRootPackage(directory, files, packages = []) { return { name: `${await getPackageName(directory)} [ROOT]`, - files: files.filter(f => - // glob.isMatch(f, join(directory, '*/**')) && + files: paths.files.filter(f => !glob.isMatch(f, ignorePatterns), ) ?? [], + directories: paths.directories.filter(d => + !glob.isMatch(d, ignorePatterns), + ) ?? [], }; } @@ -185,7 +187,7 @@ class Cli { */ async getWorkspace() { console.log(this.packages); - const rootPackage = await getRootPackage(this.dir, this.#paths?.files ?? [], this.packages); + const rootPackage = await getRootPackage(this.dir, this.#paths ?? { files: [], directories: [] }, this.packages); /** @type {string[]} */ const packagesPaths = this.#paths?.directories.filter(d => @@ -199,6 +201,7 @@ class Cli { packages.push({ name: await getPackageName(pkgPath), files: this.#paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + directories: this.#paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], }); } diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 7dd4775..3d34174 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -14,4 +14,5 @@ export interface Workspace { export interface Package { name: string files: string[] + directories: string[] } From f7b6faff09d90875843f3f6c6dd8037cc6182d01 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:20:17 -0300 Subject: [PATCH 08/29] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20refactoring=20an?= =?UTF-8?q?d=20detection=20of=20configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 324 ++++++++++++++++++++++++++++++++++++ packages/cli/src/configs.js | 22 +++ packages/cli/src/index.js | 231 +------------------------ packages/cli/src/types.d.ts | 18 +- 4 files changed, 362 insertions(+), 233 deletions(-) create mode 100755 packages/cli/src/cli.js create mode 100644 packages/cli/src/configs.js mode change 100755 => 100644 packages/cli/src/index.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js new file mode 100755 index 0000000..91cb3d8 --- /dev/null +++ b/packages/cli/src/cli.js @@ -0,0 +1,324 @@ +#!node +import fs from 'node:fs/promises'; +import path, { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import { createSpinner } from 'nanospinner'; +import glob from 'picomatch'; +import YAML from 'yaml'; +import Debugger from './debugger.js'; +import c from 'picocolors'; + +/** + * @template T + * + * @param {Promise} promise - The async function to try running + * @returns {Promise} - Returns the result of the async function, or null if it errors + */ +async function tryRun(promise) { + try { + return await promise; + } + catch (err) { + return null; + } +} + +/** + * @param {string} directory - The directory to find .gitignore and .eslintignore + * @returns {Promise} - List of ignore glob patterns + */ +async function getIgnoredFiles(directory) { + const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + return [...eslintIgnore, ...gitIgnore]; +} + +/** + * @param {string} directory - The directory to work in. + * @returns {Promise} - The package name founded. + */ +async function getPackageName(directory) { + if (existsSync(join(directory, 'package.json'))) { + const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); + /** @type {{name?: string}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const obj = JSON.parse(file); + + if (obj.name) return obj.name; + } + return path.normalize(directory).split('/').at(-1) ?? directory; +} + +/** + * @param {string} directory - The directory path to work on + * @param {{files: string[], directories: string[]}} paths - The file list to be filtered + * @param {string[]} [packages] - The packages to be filtered + * @returns {Promise} - The package object + */ +async function getRootPackage(directory, paths, packages = []) { + + const ignorePatterns = [ + ...packages.map(p => + `${join(directory, p, '**/*')}`, + )]; + + console.log(ignorePatterns); + + return { + name: `${await getPackageName(directory)} [ROOT]`, + files: paths.files.filter(f => + !glob.isMatch(f, ignorePatterns), + ) ?? [], + directories: paths.directories.filter(d => + !glob.isMatch(d, ignorePatterns), + ) ?? [], + path: directory, + }; +} + +export default class Cli { + /** @type {string} */ + dir = process.cwd(); + + /** @type {boolean} */ + debug = false; + + /** @type {import('./types').Config[]} */ + configs; + + /** + * @param {{ + * configs: import('./types').Config[] + * packages?: string[], + * directory?: string, + * debug?: boolean, + * }} options - Cli options + */ + constructor(options) { + this.configs = options?.configs; + this.packages = options.packages; + this.dir = path.normalize(options.directory ?? this.dir); + this.debug = options.debug ?? this.debug; + this.#debugger = new Debugger(this.debug); + } + + #debugger = new Debugger(this.debug); + + /** @type {{files: string[], directories: string[]} | undefined} */ + #paths; + + /** + * @param {string} [directory] - The directory to work on + * @param {string[]} [ignores] - Glob patterns to ignore + * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory + */ + async getPaths(directory = this.dir, ignores = []) { + + ignores.push( + ...[ + '.git', + '.dist', + '.DS_Store', + 'node_modules', + ].map((f) => join(directory, f)), + ...await getIgnoredFiles(directory), + ); + + const paths = (await fs.readdir(directory)) + .map((f) => path.normalize(join(directory, f))) + .filter((p) => !glob.isMatch(p, ignores)); + + /** @type {string[]} */ + const files = []; + /** @type {string[]} */ + const directories = []; + + for (const path of paths) { + if ((await fs.lstat(path)).isDirectory()) { + const subPaths = await this.getPaths(path, ignores); + directories.push(path, ...subPaths.directories); + files.push(...subPaths.files); + } + else { + files.push(path); + } + } + + return { files, directories }; + } + + /** @type {string[] | undefined} */ + packages; + + /** + * @returns {Promise} - List of packages on a directory; + */ + async getPackages() { + + /** @type {string[]} */ + let packages = []; + + const pnpmWorkspace = + existsSync(join(this.dir, 'pnpm-workspace.yaml')) + ? 'pnpm-workspace.yaml' + : existsSync(join(this.dir, 'pnpm-workspace.yml')) + ? 'pnpm-workspace.yml' + : null; + + if (pnpmWorkspace) { + const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); + + /** @type {{packages?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const fileObj = YAML.parse(fileYaml); + + packages.push(...(fileObj?.packages ?? [])); + } + else if (existsSync(join(this.dir, 'package.json'))) { + const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); + + /** @type {{workspaces?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const packageObj = JSON.parse(packageJson); + + packages.push(...(packageObj?.workspaces ?? [])); + } + return packages; + } + + /** @type {import('./types').Workspace | undefined} */ + #workspace; + + /** + * @returns {Promise} + * The workspace structure and packages founded + */ + async getWorkspace() { + console.log(this.packages); + const rootPackage = await getRootPackage(this.dir, this.#paths ?? { files: [], directories: [] }, this.packages); + + /** @type {string[]} */ + const packagesPaths = this.#paths?.directories.filter(d => + glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), + ) ?? []; + + /** @type {import('./types').Package[]} */ + const packages = []; + + for (const pkgPath of packagesPaths) { + packages.push({ + name: await getPackageName(pkgPath), + files: this.#paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + directories: this.#paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + path: pkgPath, + }); + } + + return { + packages: [ + rootPackage, + ...packages, + ], + }; + } + + /** + * @param {import('./types').Package} pkg - The package to detect configs + * @returns {import('./types').Package['config']} - Detected configs record + */ + detectConfig(pkg) { + + const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); + spinner.start(); + + /** @type {import('./types').Package['config']} */ + const pkgConfig = {}; + + for (const config of this.configs) { + pkgConfig[config.name] = this.detectOptions( + pkg, + config.options, + config.type === 'single', + spinner, + ); + spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); + } + + spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}` }); + return pkgConfig; + } + + /** + * @param {import('./types').Package} pkg - Package to detect from + * @param {import('./types').Config['options']} options - Options to be passed + * @param {boolean} single - Whether to only detect one option + * @param {import('nanospinner').Spinner} spinner - Spinner to update + * @returns {string[]} - The detected options + */ + detectOptions(pkg, options, single, spinner) { + + /** @type {string[]} */ + const detectedOptions = []; + + for (const option of options) { + + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)}`)}`, + }); + + if (option.detect === true) { + detectedOptions.push(option.name); + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✓')}`)}`, + }); + continue; + } + else if (!option.detect) continue; + + const match = glob(option.detect.map(p => join(pkg.path, p))); + + const files = pkg.files.filter(f => match ? match(f) : false); + const directories = pkg.directories.filter(f => match ? match(f) : false); + + if (files.length > 0 || directories.length > 0) { + detectedOptions.push(option.name); + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✔')}`)}`, + }); + if (single) break; + } + else { + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.red('✖')}`)}`, + }); + } + } + + return detectedOptions; + } + + async run() { + + this.packages ||= await this.getPackages(); + this.#paths = await this.getPaths(); + this.#workspace = await this.getWorkspace(); + + this.#workspace.packages = this.#workspace.packages.map( + pkg => { + pkg.config = this.detectConfig(pkg); return pkg; + }, + ); + + console.log(JSON.stringify(this.#workspace.packages, null, 2)); + + } +} + diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js new file mode 100644 index 0000000..ad6aea4 --- /dev/null +++ b/packages/cli/src/configs.js @@ -0,0 +1,22 @@ + +/** @type {import('./types').Config[]} */ +export default [ + { + name: 'framework', + type: 'single', + options: [ + { + name: 'svelte', + packages: { '@eslit/svelte': 'svelte' }, + rules: ['svelte.default'], + detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'], + }, + { + name: 'vue', + packages: { '@eslint/vue': 'vue' }, + rules: ['vue.default'], + detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'], + }, + ], + }, +]; diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js old mode 100755 new mode 100644 index a2c9327..71d2979 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -1,228 +1,5 @@ -#!node -import fs from 'node:fs/promises'; -import path, { join } from 'node:path'; -import { existsSync } from 'node:fs'; -import YAML from 'yaml'; -import glob from 'picomatch'; +import Cli from './cli.js'; +import configs from './configs.js'; - -/** - * @template T - * - * @param {Promise} promise - The async function to try running - * @returns {Promise} - Returns the result of the async function, or null if it errors - */ -async function tryRun(promise) { - try { - return await promise; - } - catch (err) { - return null; - } -} - -/** - * @param {string} directory - The directory to find .gitignore and .eslintignore - * @returns {Promise} - List of ignore glob patterns - */ -async function getIgnoredFiles(directory) { - const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') - .split('\n') - .filter(p => p && !p.startsWith('#')) - .map(p => join(directory, '**', p)); - - const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') - .split('\n') - .filter(p => p && !p.startsWith('#')) - .map(p => join(directory, '**', p)); - - return [...eslintIgnore, ...gitIgnore]; -} - -/** - * @param {string} directory - The directory to work in. - * @returns {Promise} - The package name founded. - */ -async function getPackageName(directory) { - if (existsSync(join(directory, 'package.json'))) { - const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); - /** @type {{name?: string}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const obj = JSON.parse(file); - - if (obj.name) return obj.name; - } - return path.normalize(directory).split('/').at(-1) ?? directory; -} - -/** - * @param {string} directory - The directory path to work on - * @param {{files: string[], directories: string[]}} paths - The file list to be filtered - * @param {string[]} [packages] - The packages to be filtered - * @returns {Promise} - The package object - */ -async function getRootPackage(directory, paths, packages = []) { - - const ignorePatterns = [ - ...packages.map(p => - `${join(directory, p, '**/*')}`, - )]; - - console.log(ignorePatterns); - - return { - name: `${await getPackageName(directory)} [ROOT]`, - files: paths.files.filter(f => - !glob.isMatch(f, ignorePatterns), - ) ?? [], - directories: paths.directories.filter(d => - !glob.isMatch(d, ignorePatterns), - ) ?? [], - }; -} - -class Cli { - /** @type {string} */ - dir = process.cwd(); - - /** - * @param {string} [directory] - The directory to the cli work on - * @param {string[]} [packages] - List of packages paths in the workspace - */ - constructor( - directory, - packages, - ) { - this.dir ||= path.normalize(directory ?? this.dir); - this.packages ||= packages; - } - - /** @type {{files: string[], directories: string[]} | undefined} */ - #paths; - - /** - * @param {string} [directory] - The directory to work on - * @param {string[]} [ignores] - Glob patterns to ignore - * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory - */ - async getPaths(directory = this.dir, ignores = []) { - - ignores.push( - ...[ - '.git', - '.dist', - '.DS_Store', - 'node_modules', - ].map((f) => join(directory, f)), - ...await getIgnoredFiles(directory), - ); - - const paths = (await fs.readdir(directory)) - .map((f) => path.normalize(join(directory, f))) - .filter((p) => !glob.isMatch(p, ignores)); - - /** @type {string[]} */ - const files = []; - /** @type {string[]} */ - const directories = []; - - for (const path of paths) { - if ((await fs.lstat(path)).isDirectory()) { - const subPaths = await this.getPaths(path, ignores); - directories.push(path, ...subPaths.directories); - files.push(...subPaths.files); - } - else { - files.push(path); - } - } - return { files, directories }; - } - - /** @type {string[] | undefined} */ - packages; - - /** - * @returns {Promise} - List of packages on a directory; - */ - async getPackages() { - - /** @type {string[]} */ - let packages = []; - - const pnpmWorkspace = - existsSync(join(this.dir, 'pnpm-workspace.yaml')) - ? 'pnpm-workspace.yaml' - : existsSync(join(this.dir, 'pnpm-workspace.yml')) - ? 'pnpm-workspace.yml' - : null; - - if (pnpmWorkspace) { - const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); - - /** @type {{packages?: string[]}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const fileObj = YAML.parse(fileYaml); - - packages.push(...(fileObj?.packages ?? [])); - } - else if (existsSync(join(this.dir, 'package.json'))) { - const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); - - /** @type {{workspaces?: string[]}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const packageObj = JSON.parse(packageJson); - - packages.push(...(packageObj?.workspaces ?? [])); - } - return packages; - } - - /** @type {import('./types').Workspace | undefined} */ - #workspace; - - /** - * @returns {Promise} - * The workspace structure and packages founded - */ - async getWorkspace() { - console.log(this.packages); - const rootPackage = await getRootPackage(this.dir, this.#paths ?? { files: [], directories: [] }, this.packages); - - /** @type {string[]} */ - const packagesPaths = this.#paths?.directories.filter(d => - glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), - ) ?? []; - - /** @type {import('./types').Package[]} */ - const packages = []; - - for (const pkgPath of packagesPaths) { - packages.push({ - name: await getPackageName(pkgPath), - files: this.#paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - directories: this.#paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - }); - } - - return { - packages: [ - rootPackage, - ...packages, - ], - }; - } - - async run() { - this.packages ||= await this.getPackages(); - this.#paths = await this.getPaths(); - this.#workspace = await this.getWorkspace(); - - - - console.log(this.dir); - console.log(this.#workspace.packages); - } -} - -await new Cli().run(); +const cli = new Cli({ configs }); +await cli.run(); diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 3d34174..579ddc1 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -1,10 +1,14 @@ -interface Options { - environment: { - node: boolean - deno: boolean - browser: boolean - } +export interface Config { + name: string + type: 'single' | 'multiple' + manual?: boolean + options: { + name: string + packages: Record + rules: string[] + detect?: string[] | true + }[] } export interface Workspace { @@ -13,6 +17,8 @@ export interface Workspace { export interface Package { name: string + path: string files: string[] directories: string[] + config?: Record } From c1f4c262dc07e6c92f9016ef6a4c439d8127fb61 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:23:14 -0300 Subject: [PATCH 09/29] =?UTF-8?q?refactor(cli):=20=E2=99=BB=EF=B8=8F=20rem?= =?UTF-8?q?ove=20unused=20debugger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 91cb3d8..743a145 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -5,7 +5,6 @@ import { existsSync } from 'node:fs'; import { createSpinner } from 'nanospinner'; import glob from 'picomatch'; import YAML from 'yaml'; -import Debugger from './debugger.js'; import c from 'picocolors'; /** @@ -107,11 +106,8 @@ export default class Cli { this.packages = options.packages; this.dir = path.normalize(options.directory ?? this.dir); this.debug = options.debug ?? this.debug; - this.#debugger = new Debugger(this.debug); } - #debugger = new Debugger(this.debug); - /** @type {{files: string[], directories: string[]} | undefined} */ #paths; From 8c1e7213463c7b7ff83d2afd39ed86581e46f0c8 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:55:49 -0300 Subject: [PATCH 10/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20move=20w?= =?UTF-8?q?orkspace=20on=20its=20own=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 212 ++------------------------------- packages/cli/src/workspace.js | 214 ++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 203 deletions(-) create mode 100644 packages/cli/src/workspace.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 743a145..181a121 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -1,87 +1,9 @@ #!node -import fs from 'node:fs/promises'; import path, { join } from 'node:path'; -import { existsSync } from 'node:fs'; import { createSpinner } from 'nanospinner'; import glob from 'picomatch'; -import YAML from 'yaml'; import c from 'picocolors'; - -/** - * @template T - * - * @param {Promise} promise - The async function to try running - * @returns {Promise} - Returns the result of the async function, or null if it errors - */ -async function tryRun(promise) { - try { - return await promise; - } - catch (err) { - return null; - } -} - -/** - * @param {string} directory - The directory to find .gitignore and .eslintignore - * @returns {Promise} - List of ignore glob patterns - */ -async function getIgnoredFiles(directory) { - const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') - .split('\n') - .filter(p => p && !p.startsWith('#')) - .map(p => join(directory, '**', p)); - - const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') - .split('\n') - .filter(p => p && !p.startsWith('#')) - .map(p => join(directory, '**', p)); - - return [...eslintIgnore, ...gitIgnore]; -} - -/** - * @param {string} directory - The directory to work in. - * @returns {Promise} - The package name founded. - */ -async function getPackageName(directory) { - if (existsSync(join(directory, 'package.json'))) { - const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); - /** @type {{name?: string}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const obj = JSON.parse(file); - - if (obj.name) return obj.name; - } - return path.normalize(directory).split('/').at(-1) ?? directory; -} - -/** - * @param {string} directory - The directory path to work on - * @param {{files: string[], directories: string[]}} paths - The file list to be filtered - * @param {string[]} [packages] - The packages to be filtered - * @returns {Promise} - The package object - */ -async function getRootPackage(directory, paths, packages = []) { - - const ignorePatterns = [ - ...packages.map(p => - `${join(directory, p, '**/*')}`, - )]; - - console.log(ignorePatterns); - - return { - name: `${await getPackageName(directory)} [ROOT]`, - files: paths.files.filter(f => - !glob.isMatch(f, ignorePatterns), - ) ?? [], - directories: paths.directories.filter(d => - !glob.isMatch(d, ignorePatterns), - ) ?? [], - path: directory, - }; -} +import Workspace from './workspace.js'; export default class Cli { /** @type {string} */ @@ -93,10 +15,14 @@ export default class Cli { /** @type {import('./types').Config[]} */ configs; + /** @type {import('./types').Workspace | undefined} */ + #workspace; + /** * @param {{ * configs: import('./types').Config[] * packages?: string[], + * workspace?: import('./types').Workspace, * directory?: string, * debug?: boolean, * }} options - Cli options @@ -104,128 +30,11 @@ export default class Cli { constructor(options) { this.configs = options?.configs; this.packages = options.packages; + this.#workspace = options.workspace; this.dir = path.normalize(options.directory ?? this.dir); this.debug = options.debug ?? this.debug; } - /** @type {{files: string[], directories: string[]} | undefined} */ - #paths; - - /** - * @param {string} [directory] - The directory to work on - * @param {string[]} [ignores] - Glob patterns to ignore - * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory - */ - async getPaths(directory = this.dir, ignores = []) { - - ignores.push( - ...[ - '.git', - '.dist', - '.DS_Store', - 'node_modules', - ].map((f) => join(directory, f)), - ...await getIgnoredFiles(directory), - ); - - const paths = (await fs.readdir(directory)) - .map((f) => path.normalize(join(directory, f))) - .filter((p) => !glob.isMatch(p, ignores)); - - /** @type {string[]} */ - const files = []; - /** @type {string[]} */ - const directories = []; - - for (const path of paths) { - if ((await fs.lstat(path)).isDirectory()) { - const subPaths = await this.getPaths(path, ignores); - directories.push(path, ...subPaths.directories); - files.push(...subPaths.files); - } - else { - files.push(path); - } - } - - return { files, directories }; - } - - /** @type {string[] | undefined} */ - packages; - - /** - * @returns {Promise} - List of packages on a directory; - */ - async getPackages() { - - /** @type {string[]} */ - let packages = []; - - const pnpmWorkspace = - existsSync(join(this.dir, 'pnpm-workspace.yaml')) - ? 'pnpm-workspace.yaml' - : existsSync(join(this.dir, 'pnpm-workspace.yml')) - ? 'pnpm-workspace.yml' - : null; - - if (pnpmWorkspace) { - const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); - - /** @type {{packages?: string[]}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const fileObj = YAML.parse(fileYaml); - - packages.push(...(fileObj?.packages ?? [])); - } - else if (existsSync(join(this.dir, 'package.json'))) { - const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); - - /** @type {{workspaces?: string[]}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const packageObj = JSON.parse(packageJson); - - packages.push(...(packageObj?.workspaces ?? [])); - } - return packages; - } - - /** @type {import('./types').Workspace | undefined} */ - #workspace; - - /** - * @returns {Promise} - * The workspace structure and packages founded - */ - async getWorkspace() { - console.log(this.packages); - const rootPackage = await getRootPackage(this.dir, this.#paths ?? { files: [], directories: [] }, this.packages); - - /** @type {string[]} */ - const packagesPaths = this.#paths?.directories.filter(d => - glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), - ) ?? []; - - /** @type {import('./types').Package[]} */ - const packages = []; - - for (const pkgPath of packagesPaths) { - packages.push({ - name: await getPackageName(pkgPath), - files: this.#paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - directories: this.#paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - path: pkgPath, - }); - } - - return { - packages: [ - rootPackage, - ...packages, - ], - }; - } - /** * @param {import('./types').Package} pkg - The package to detect configs * @returns {import('./types').Package['config']} - Detected configs record @@ -302,18 +111,15 @@ export default class Cli { } async run() { + const workspace = this.#workspace ?? await new Workspace(this.dir).get(); - this.packages ||= await this.getPackages(); - this.#paths = await this.getPaths(); - this.#workspace = await this.getWorkspace(); - - this.#workspace.packages = this.#workspace.packages.map( + workspace.packages = workspace.packages.map( pkg => { pkg.config = this.detectConfig(pkg); return pkg; }, ); - console.log(JSON.stringify(this.#workspace.packages, null, 2)); + console.log(JSON.stringify(workspace.packages, null, 2)); } } diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js new file mode 100644 index 0000000..9c18341 --- /dev/null +++ b/packages/cli/src/workspace.js @@ -0,0 +1,214 @@ +import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import YAML from 'yaml'; +import path, { join } from 'node:path'; +import glob from 'picomatch'; + + +/** + * @template T + * + * @param {Promise} promise - The async function to try running + * @returns {Promise} - Returns the result of the async function, or null if it errors + */ +async function tryRun(promise) { + try { + return await promise; + } + catch (err) { + return null; + } +} + +/** + * @param {string} directory - The directory to find .gitignore and .eslintignore + * @returns {Promise} - List of ignore glob patterns + */ +async function getIgnoredFiles(directory) { + const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') + .split('\n') + .filter(p => p && !p.startsWith('#')) + .map(p => join(directory, '**', p)); + + return [...eslintIgnore, ...gitIgnore]; +} + +/** + * @param {string} directory - The directory to work in. + * @returns {Promise} - The package name founded. + */ +async function getPackageName(directory) { + if (existsSync(join(directory, 'package.json'))) { + const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); + /** @type {{name?: string}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const obj = JSON.parse(file); + + if (obj.name) return obj.name; + } + return path.normalize(directory).split('/').at(-1) ?? directory; +} + +/** + * @param {string} directory - The directory path to work on + * @param {{files: string[], directories: string[]}} paths - The file list to be filtered + * @param {string[]} [packages] - The packages to be filtered + * @returns {Promise} - The package object + */ +async function getRootPackage(directory, paths, packages = []) { + + const ignorePatterns = [ + ...packages.map(p => + `${join(directory, p, '**/*')}`, + )]; + + return { + name: `${await getPackageName(directory)} [ROOT]`, + files: paths.files.filter(f => + !glob.isMatch(f, ignorePatterns), + ) ?? [], + directories: paths.directories.filter(d => + !glob.isMatch(d, ignorePatterns), + ) ?? [], + path: directory, + }; +} +export default class Workspace { + /** @type {{files: string[], directories: string[]} | undefined} */ + paths; + + /** + * @param {string} directory - The directory to get the workspace from + */ + constructor(directory) { + this.dir = directory; + } + + /** + * @param {string} [directory] - The directory to work on + * @param {string[]} [ignores] - Glob patterns to ignore + * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory + */ + async getPaths(directory = this.dir, ignores = []) { + + ignores.push( + ...[ + '.git', + '.dist', + '.DS_Store', + 'node_modules', + ].map((f) => join(directory, f)), + ...await getIgnoredFiles(directory), + ); + + const paths = (await fs.readdir(directory)) + .map((f) => path.normalize(join(directory, f))) + .filter((p) => !glob.isMatch(p, ignores)); + + /** @type {string[]} */ + const files = []; + /** @type {string[]} */ + const directories = []; + + for (const path of paths) { + if ((await fs.lstat(path)).isDirectory()) { + const subPaths = await this.getPaths(path, ignores); + directories.push(path, ...subPaths.directories); + files.push(...subPaths.files); + } + else { + files.push(path); + } + } + + return { files, directories }; + } + + /** @type {string[] | undefined} */ + packages; + + /** + * @returns {Promise} - List of packages on a directory; + */ + async getPackages() { + + /** @type {string[]} */ + let packages = []; + + const pnpmWorkspace = + existsSync(join(this.dir, 'pnpm-workspace.yaml')) + ? 'pnpm-workspace.yaml' + : existsSync(join(this.dir, 'pnpm-workspace.yml')) + ? 'pnpm-workspace.yml' + : null; + + if (pnpmWorkspace) { + const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); + + /** @type {{packages?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const fileObj = YAML.parse(fileYaml); + + packages.push(...(fileObj?.packages ?? [])); + } + else if (existsSync(join(this.dir, 'package.json'))) { + const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); + + /** @type {{workspaces?: string[]}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const packageObj = JSON.parse(packageJson); + + packages.push(...(packageObj?.workspaces ?? [])); + } + return packages; + } + + /** @type {import('./types').Workspace | undefined} */ + workspace; + + /** + * @returns {Promise} + * The workspace structure and packages founded + */ + async getWorkspace() { + console.log(this.packages); + const rootPackage = await getRootPackage(this.dir, this.paths ?? { files: [], directories: [] }, this.packages); + + /** @type {string[]} */ + const packagesPaths = this.paths?.directories.filter(d => + glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), + ) ?? []; + + /** @type {import('./types').Package[]} */ + const packages = []; + + for (const pkgPath of packagesPaths) { + packages.push({ + name: await getPackageName(pkgPath), + files: this.paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + directories: this.paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], + path: pkgPath, + }); + } + + return { + packages: [ + rootPackage, + ...packages, + ], + }; + } + + async get() { + this.packages ||= await this.getPackages(); + this.paths = await this.getPaths(); + this.workspace = await this.getWorkspace(); + + return this.workspace; + } +} From f24aba4f8e9bb559b7e6bd130d188c75c9e89832 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:55:56 -0300 Subject: [PATCH 11/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20move=20f?= =?UTF-8?q?unction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 181a121..6dba65a 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -35,32 +35,6 @@ export default class Cli { this.debug = options.debug ?? this.debug; } - /** - * @param {import('./types').Package} pkg - The package to detect configs - * @returns {import('./types').Package['config']} - Detected configs record - */ - detectConfig(pkg) { - - const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); - spinner.start(); - - /** @type {import('./types').Package['config']} */ - const pkgConfig = {}; - - for (const config of this.configs) { - pkgConfig[config.name] = this.detectOptions( - pkg, - config.options, - config.type === 'single', - spinner, - ); - spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); - } - - spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}` }); - return pkgConfig; - } - /** * @param {import('./types').Package} pkg - Package to detect from * @param {import('./types').Config['options']} options - Options to be passed @@ -110,6 +84,32 @@ export default class Cli { return detectedOptions; } + /** + * @param {import('./types').Package} pkg - The package to detect configs + * @returns {import('./types').Package['config']} - Detected configs record + */ + detectConfig(pkg) { + + const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); + spinner.start(); + + /** @type {import('./types').Package['config']} */ + const pkgConfig = {}; + + for (const config of this.configs) { + pkgConfig[config.name] = this.detectOptions( + pkg, + config.options, + config.type === 'single', + spinner, + ); + spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); + } + + spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}` }); + return pkgConfig; + } + async run() { const workspace = this.#workspace ?? await new Workspace(this.dir).get(); From 73b71033b3dfd82d03627df9ff9e3b0d934cea71 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:19:00 -0300 Subject: [PATCH 12/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20simplify?= =?UTF-8?q?/"normalize"=20packages=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored how the cli gets the workspace structure, making it more "agnostic" for single- and multi- packages workspaces. Also the paths listed on the package object were simplified to be relative to the package's path. --- packages/cli/src/cli.js | 22 ++----- packages/cli/src/index.js | 0 packages/cli/src/types.d.ts | 5 +- packages/cli/src/workspace.js | 116 ++++++++++++++++------------------ 4 files changed, 61 insertions(+), 82 deletions(-) mode change 100644 => 100755 packages/cli/src/index.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 6dba65a..051996b 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -1,5 +1,5 @@ #!node -import path, { join } from 'node:path'; +import path from 'node:path'; import { createSpinner } from 'nanospinner'; import glob from 'picomatch'; import c from 'picocolors'; @@ -9,28 +9,20 @@ export default class Cli { /** @type {string} */ dir = process.cwd(); - /** @type {boolean} */ - debug = false; - /** @type {import('./types').Config[]} */ configs; - /** @type {import('./types').Workspace | undefined} */ - #workspace; - /** * @param {{ * configs: import('./types').Config[] * packages?: string[], - * workspace?: import('./types').Workspace, + * workspace?: import('./types').Package[], * directory?: string, * debug?: boolean, * }} options - Cli options */ constructor(options) { this.configs = options?.configs; - this.packages = options.packages; - this.#workspace = options.workspace; this.dir = path.normalize(options.directory ?? this.dir); this.debug = options.debug ?? this.debug; } @@ -62,7 +54,7 @@ export default class Cli { } else if (!option.detect) continue; - const match = glob(option.detect.map(p => join(pkg.path, p))); + const match = glob(option.detect); const files = pkg.files.filter(f => match ? match(f) : false); const directories = pkg.directories.filter(f => match ? match(f) : false); @@ -106,20 +98,20 @@ export default class Cli { spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); } - spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}` }); + spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}\n${c.dim(JSON.stringify(pkgConfig))}\n` }); return pkgConfig; } async run() { - const workspace = this.#workspace ?? await new Workspace(this.dir).get(); + let packages = await new Workspace(this.dir).getPackages(); - workspace.packages = workspace.packages.map( + packages = packages.map( pkg => { pkg.config = this.detectConfig(pkg); return pkg; }, ); - console.log(JSON.stringify(workspace.packages, null, 2)); + console.log(packages); } } diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js old mode 100644 new mode 100755 diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 579ddc1..ed5821b 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -11,11 +11,8 @@ export interface Config { }[] } -export interface Workspace { - packages: Package[] -} - export interface Package { + root?: boolean name: string path: string files: string[] diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js index 9c18341..a3b1f08 100644 --- a/packages/cli/src/workspace.js +++ b/packages/cli/src/workspace.js @@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'; import YAML from 'yaml'; import path, { join } from 'node:path'; import glob from 'picomatch'; +import picomatch from 'picomatch'; /** @@ -54,30 +55,6 @@ async function getPackageName(directory) { return path.normalize(directory).split('/').at(-1) ?? directory; } -/** - * @param {string} directory - The directory path to work on - * @param {{files: string[], directories: string[]}} paths - The file list to be filtered - * @param {string[]} [packages] - The packages to be filtered - * @returns {Promise} - The package object - */ -async function getRootPackage(directory, paths, packages = []) { - - const ignorePatterns = [ - ...packages.map(p => - `${join(directory, p, '**/*')}`, - )]; - - return { - name: `${await getPackageName(directory)} [ROOT]`, - files: paths.files.filter(f => - !glob.isMatch(f, ignorePatterns), - ) ?? [], - directories: paths.directories.filter(d => - !glob.isMatch(d, ignorePatterns), - ) ?? [], - path: directory, - }; -} export default class Workspace { /** @type {{files: string[], directories: string[]} | undefined} */ paths; @@ -126,7 +103,10 @@ export default class Workspace { } } - return { files, directories }; + return { + files: files.map(p => path.normalize(p.replace(this.dir, './'))), + directories: directories.map(p => path.normalize(p.replace(this.dir, './'))), + }; } /** @type {string[] | undefined} */ @@ -135,10 +115,10 @@ export default class Workspace { /** * @returns {Promise} - List of packages on a directory; */ - async getPackages() { + async getPackagePatterns() { /** @type {string[]} */ - let packages = []; + let packagePatterns = []; const pnpmWorkspace = existsSync(join(this.dir, 'pnpm-workspace.yaml')) @@ -148,67 +128,77 @@ export default class Workspace { : null; if (pnpmWorkspace) { - const fileYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); + const pnpmWorkspaceYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); /** @type {{packages?: string[]}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const fileObj = YAML.parse(fileYaml); + const pnpmWorkspaceObj = YAML.parse(pnpmWorkspaceYaml); - packages.push(...(fileObj?.packages ?? [])); + packagePatterns.push(...(pnpmWorkspaceObj?.packages ?? [])); } else if (existsSync(join(this.dir, 'package.json'))) { const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); /** @type {{workspaces?: string[]}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const packageObj = JSON.parse(packageJson); + const packageJsonObj = JSON.parse(packageJson); - packages.push(...(packageObj?.workspaces ?? [])); + packagePatterns.push(...(packageJsonObj?.workspaces ?? [])); } - return packages; + + return packagePatterns.map(p => { + p = path.normalize(p); + p = p.startsWith('/') ? p.replace('/', '') : p; + p = p.endsWith('/') ? p.slice(0, p.length - 1) : p; + return p; + }); } - /** @type {import('./types').Workspace | undefined} */ - workspace; - /** - * @returns {Promise} - * The workspace structure and packages founded + * @returns {Promise} - The list of packages that exist in the workspace */ - async getWorkspace() { - console.log(this.packages); - const rootPackage = await getRootPackage(this.dir, this.paths ?? { files: [], directories: [] }, this.packages); + async getPackages() { - /** @type {string[]} */ - const packagesPaths = this.paths?.directories.filter(d => - glob.isMatch(d, this.packages?.map(p => join(this.dir, p)) ?? ''), - ) ?? []; + const paths = await this.getPaths(); + const packagePatterns = await this.getPackagePatterns(); + const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); + + console.log(packagePatterns); + + /** @type {import('./types').Package} */ + const rootPackage = { + root: true, + name: await getPackageName(this.dir), + path: this.dir, + files: paths.files, + directories: paths.directories, + }; /** @type {import('./types').Package[]} */ const packages = []; - for (const pkgPath of packagesPaths) { + for (const packagePath of packagePaths) { packages.push({ - name: await getPackageName(pkgPath), - files: this.paths?.files.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - directories: this.paths?.directories.filter(f => glob.isMatch(f, join(pkgPath, '**/*'))) ?? [], - path: pkgPath, + root: false, + path: join(this.dir, packagePath), + name: await getPackageName(join(this.dir, packagePath)), + files: paths.files + .filter(f => picomatch.isMatch(f, `${packagePath}/**/*`)) + .map(f => f.replace(`${packagePath}/`, '')), + directories: paths.directories + .filter(d => picomatch.isMatch(d, `${packagePath}/**/*`)) + .map(d => d.replace(`${packagePath}/`, '')), }); + + rootPackage.files = rootPackage.files + .filter(f => picomatch.isMatch(f, `!${packagePath}/**/*`)); + + rootPackage.directories = rootPackage.directories + .filter(d => picomatch.isMatch(d, `!${packagePath}/**/*`)); } - return { - packages: [ - rootPackage, - ...packages, - ], - }; + return [rootPackage, ...packages]; + } - async get() { - this.packages ||= await this.getPackages(); - this.paths = await this.getPaths(); - this.workspace = await this.getWorkspace(); - - return this.workspace; - } } From b0e00d6e5cae124c6e7b52b737180c8d1b4a4b03 Mon Sep 17 00:00:00 2001 From: Turbobot Date: Thu, 3 Aug 2023 16:53:26 -0300 Subject: [PATCH 13/29] =?UTF-8?q?feat(create-turbo):=20=E2=9C=A8=20apply?= =?UTF-8?q?=20official-starter=20transform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 36 +++++++++++++++++++++++++++++++++++- packages/cli/src/types.d.ts | 5 ++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 051996b..24e22c1 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -102,6 +102,39 @@ export default class Cli { return pkgConfig; } + /** + * @param {import('./types').Package[]} packages Packages to generate the map from + * @returns {import('./types').PackagesConfigsMap} A map of what packages has some configuration + */ + generateConfigMap(packages) { + + /** @type {import('./types').PackagesConfigsMap} */ + const configMap = new Map(); + + for (const pkg of packages) { + + Object.entries(pkg.config ?? {}).forEach(([key, options]) => { + /** @type {Map} */ + const optionsMap = configMap.get(key) ?? new Map(); + + options.forEach(option => { + const paths = optionsMap.get(option) ?? []; + optionsMap.set(option, [pkg.path, ...paths]); + + if (paths.length >= packages.length - 2 || paths.includes(this.dir)) { + console.log('a', packages.length, paths.length); + optionsMap.set(option, [this.dir]); + } + }); + + configMap.set(key, optionsMap); + }); + } + + return configMap; + + } + async run() { let packages = await new Workspace(this.dir).getPackages(); @@ -111,7 +144,8 @@ export default class Cli { }, ); - console.log(packages); + console.log(packages.length); + console.log(this.generateConfigMap(packages)); } } diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index ed5821b..e9c9b21 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -6,11 +6,14 @@ export interface Config { options: { name: string packages: Record - rules: string[] + configs?: string[] + rules?: string[] detect?: string[] | true }[] } +export type PackagesConfigsMap = Map>; + export interface Package { root?: boolean name: string From c201a25e6e4fca1f904b0cf69861f5a00e747ade Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:38:03 -0300 Subject: [PATCH 14/29] =?UTF-8?q?feat:=20=E2=9C=A8=20pass=20packages=20fro?= =?UTF-8?q?m=20cli=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fixtures/monorepo/.gitignore | 36 ++++++++++++++++++++++++++++++++++ fixtures/monorepo/package.json | 15 ++++++++++++++ packages/cli/package.json | 1 + packages/cli/src/cli.js | 14 ++++++------- packages/cli/src/index.js | 9 ++++++++- packages/cli/src/workspace.js | 13 ++++-------- pnpm-lock.yaml | 14 +++++++++++++ 7 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 fixtures/monorepo/.gitignore create mode 100644 fixtures/monorepo/package.json diff --git a/fixtures/monorepo/.gitignore b/fixtures/monorepo/.gitignore new file mode 100644 index 0000000..d1595af --- /dev/null +++ b/fixtures/monorepo/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# vercel +.vercel diff --git a/fixtures/monorepo/package.json b/fixtures/monorepo/package.json new file mode 100644 index 0000000..aa0b92a --- /dev/null +++ b/fixtures/monorepo/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "scripts": { + "test:cli": "pnpm cli" + }, + "devDependencies": { + "@eslit/cli": "workspace:*" + }, + "packageManager": "pnpm@8.6.10", + "name": "monorepo", + "workspaces": [ + "apps/*", + "packages/*" + ] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 6f053b4..39e2683 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,6 +28,7 @@ "bin": "./src/index.js", "license": "MIT", "dependencies": { + "commander": "^11.0.0", "magic-string": "^0.30.2", "nanospinner": "^1.1.0", "picocolors": "^1.0.0", diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 24e22c1..2da197d 100755 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -12,19 +12,20 @@ export default class Cli { /** @type {import('./types').Config[]} */ configs; + /** @type {string[] | undefined} */ + #packagesPatterns; + /** * @param {{ - * configs: import('./types').Config[] + * configs: import('./types').Config[], * packages?: string[], - * workspace?: import('./types').Package[], * directory?: string, - * debug?: boolean, * }} options - Cli options */ constructor(options) { + this.#packagesPatterns = options.packages; this.configs = options?.configs; this.dir = path.normalize(options.directory ?? this.dir); - this.debug = options.debug ?? this.debug; } /** @@ -136,7 +137,7 @@ export default class Cli { } async run() { - let packages = await new Workspace(this.dir).getPackages(); + let packages = await new Workspace(this.dir, this.#packagesPatterns).getPackages(); packages = packages.map( pkg => { @@ -144,8 +145,7 @@ export default class Cli { }, ); - console.log(packages.length); - console.log(this.generateConfigMap(packages)); + console.log(packages); } } diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 71d2979..454f4a8 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -1,5 +1,12 @@ import Cli from './cli.js'; import configs from './configs.js'; +import { program } from 'commander'; -const cli = new Cli({ configs }); +program.option('--packages '); + +program.parse(process.argv); +/** @type {{ packages?: string[] } & import('commander').OptionValues} */ +const options = program.opts(); + +const cli = new Cli({ configs, packages: options.packages }); await cli.run(); diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js index a3b1f08..1bbaad1 100644 --- a/packages/cli/src/workspace.js +++ b/packages/cli/src/workspace.js @@ -56,14 +56,14 @@ async function getPackageName(directory) { } export default class Workspace { - /** @type {{files: string[], directories: string[]} | undefined} */ - paths; /** * @param {string} directory - The directory to get the workspace from + * @param {string[]} [packagePatterns] - List of package patterns */ - constructor(directory) { + constructor(directory, packagePatterns) { this.dir = directory; + this.packagePatterns = packagePatterns; } /** @@ -109,9 +109,6 @@ export default class Workspace { }; } - /** @type {string[] | undefined} */ - packages; - /** * @returns {Promise} - List of packages on a directory; */ @@ -160,11 +157,9 @@ export default class Workspace { async getPackages() { const paths = await this.getPaths(); - const packagePatterns = await this.getPackagePatterns(); + const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns(); const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); - console.log(packagePatterns); - /** @type {import('./types').Package} */ const rootPackage = { root: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727fcda..5edc700 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,12 @@ importers: specifier: workspace:* version: link:../../packages/cli + fixtures/monorepo: + devDependencies: + '@eslit/cli': + specifier: workspace:* + version: link:../../packages/cli + fixtures/svelte: devDependencies: '@eslit/cli': @@ -93,6 +99,9 @@ importers: packages/cli: dependencies: + commander: + specifier: ^11.0.0 + version: 11.0.0 magic-string: specifier: ^0.30.2 version: 0.30.2 @@ -1426,6 +1435,11 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /commander@11.0.0: + resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + engines: {node: '>=16'} + dev: false + /comment-parser@1.3.1: resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==} engines: {node: '>= 12.0.0'} From e775d83ccfab2693c9f7b5e86da58c2a79db4e37 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:33:58 -0300 Subject: [PATCH 15/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20repurpos?= =?UTF-8?q?e=20Cli=20class=20to=20Configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed Cli to ConfigsProcessor, now the Cli class is purposed to just parsing the cli arguments and orchestrate other classes --- packages/cli/src/cli.js | 169 ++++++--------------------- packages/cli/src/configs.js | 2 +- packages/cli/src/configsProcessor.js | 138 ++++++++++++++++++++++ packages/cli/src/index.js | 10 +- packages/cli/src/types.d.ts | 7 ++ pnpm-lock.yaml | 11 +- 6 files changed, 193 insertions(+), 144 deletions(-) mode change 100755 => 100644 packages/cli/src/cli.js create mode 100755 packages/cli/src/configsProcessor.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js old mode 100755 new mode 100644 index 2da197d..ef1b2f7 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -1,152 +1,55 @@ -#!node -import path from 'node:path'; -import { createSpinner } from 'nanospinner'; -import glob from 'picomatch'; -import c from 'picocolors'; +import { Command } from 'commander'; +import ConfigsProcessor from './configsProcessor.js'; +import configs from './configs.js'; import Workspace from './workspace.js'; +import path from 'node:path'; export default class Cli { - /** @type {string} */ - dir = process.cwd(); - /** @type {import('./types').Config[]} */ - configs; + #program = new Command(); - /** @type {string[] | undefined} */ - #packagesPatterns; + /** @type {import('./types').CliArgs} */ + args = { + dir: process.cwd(), + }; - /** - * @param {{ - * configs: import('./types').Config[], - * packages?: string[], - * directory?: string, - * }} options - Cli options - */ - constructor(options) { - this.#packagesPatterns = options.packages; - this.configs = options?.configs; - this.dir = path.normalize(options.directory ?? this.dir); - } + setArgs() { + this.#program + .option('--packages ') + .option('--merge-to-root') + .option('--dir ', undefined); - /** - * @param {import('./types').Package} pkg - Package to detect from - * @param {import('./types').Config['options']} options - Options to be passed - * @param {boolean} single - Whether to only detect one option - * @param {import('nanospinner').Spinner} spinner - Spinner to update - * @returns {string[]} - The detected options - */ - detectOptions(pkg, options, single, spinner) { + this.#program.parse(); - /** @type {string[]} */ - const detectedOptions = []; + this.args = { + ...this.args, + ...this.#program.opts(), + }; - for (const option of options) { - - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)}`)}`, - }); - - if (option.detect === true) { - detectedOptions.push(option.name); - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✓')}`)}`, - }); - continue; - } - else if (!option.detect) continue; - - const match = glob(option.detect); - - const files = pkg.files.filter(f => match ? match(f) : false); - const directories = pkg.directories.filter(f => match ? match(f) : false); - - if (files.length > 0 || directories.length > 0) { - detectedOptions.push(option.name); - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✔')}`)}`, - }); - if (single) break; - } - else { - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.red('✖')}`)}`, - }); - } - } - - return detectedOptions; - } - - /** - * @param {import('./types').Package} pkg - The package to detect configs - * @returns {import('./types').Package['config']} - Detected configs record - */ - detectConfig(pkg) { - - const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); - spinner.start(); - - /** @type {import('./types').Package['config']} */ - const pkgConfig = {}; - - for (const config of this.configs) { - pkgConfig[config.name] = this.detectOptions( - pkg, - config.options, - config.type === 'single', - spinner, - ); - spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); - } - - spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}\n${c.dim(JSON.stringify(pkgConfig))}\n` }); - return pkgConfig; - } - - /** - * @param {import('./types').Package[]} packages Packages to generate the map from - * @returns {import('./types').PackagesConfigsMap} A map of what packages has some configuration - */ - generateConfigMap(packages) { - - /** @type {import('./types').PackagesConfigsMap} */ - const configMap = new Map(); - - for (const pkg of packages) { - - Object.entries(pkg.config ?? {}).forEach(([key, options]) => { - /** @type {Map} */ - const optionsMap = configMap.get(key) ?? new Map(); - - options.forEach(option => { - const paths = optionsMap.get(option) ?? []; - optionsMap.set(option, [pkg.path, ...paths]); - - if (paths.length >= packages.length - 2 || paths.includes(this.dir)) { - console.log('a', packages.length, paths.length); - optionsMap.set(option, [this.dir]); - } - }); - - configMap.set(key, optionsMap); - }); - } - - return configMap; + this.args.dir = !this.args.dir.startsWith('/') + ? path.join(process.cwd(), this.args.dir) + : this.args.dir; } async run() { - let packages = await new Workspace(this.dir, this.#packagesPatterns).getPackages(); + this.setArgs(); - packages = packages.map( - pkg => { - pkg.config = this.detectConfig(pkg); return pkg; - }, - ); + console.log(this.args.dir); - console.log(packages); + const processor = new ConfigsProcessor({ configs }); + const packages = (await new Workspace(this.args.dir, this.args?.packages ) + .getPackages()) + .map(pkg => { + pkg.config = processor.detectConfig(pkg); + return pkg; + }); + + const configsMaps = processor.generateConfigMap(packages); + + console.log(configsMaps); } + } diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js index ad6aea4..896b094 100644 --- a/packages/cli/src/configs.js +++ b/packages/cli/src/configs.js @@ -3,7 +3,7 @@ export default [ { name: 'framework', - type: 'single', + type: 'multiple', options: [ { name: 'svelte', diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js new file mode 100755 index 0000000..065ef08 --- /dev/null +++ b/packages/cli/src/configsProcessor.js @@ -0,0 +1,138 @@ +#!node +import path from 'node:path'; +import { createSpinner } from 'nanospinner'; +import glob from 'picomatch'; +import c from 'picocolors'; + +export default class ConfigsProcessor { + /** @type {string} */ + dir = process.cwd(); + + /** @type {import('./types.js').Config[]} */ + configs; + + /** @type {string[] | undefined} */ + #packagesPatterns; + + /** + * @param {{ + * configs: import('./types.js').Config[], + * packages?: string[], + * directory?: string, + * }} options - Cli options + */ + constructor(options) { + this.#packagesPatterns = options.packages; + this.configs = options?.configs; + this.dir = path.normalize(options.directory ?? this.dir); + } + + /** + * @param {import('./types.js').Package} pkg - Package to detect from + * @param {import('./types.js').Config['options']} options - Options to be passed + * @param {boolean} single - Whether to only detect one option + * @param {import('nanospinner').Spinner} spinner - Spinner to update + * @returns {string[]} - The detected options + */ + detectOptions(pkg, options, single, spinner) { + + /** @type {string[]} */ + const detectedOptions = []; + + for (const option of options) { + + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)}`)}`, + }); + + if (option.detect === true) { + detectedOptions.push(option.name); + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✓')}`)}`, + }); + continue; + } + else if (!option.detect) continue; + + const match = glob(option.detect); + + const files = pkg.files.filter(f => match ? match(f) : false); + const directories = pkg.directories.filter(f => match ? match(f) : false); + + if (files.length > 0 || directories.length > 0) { + detectedOptions.push(option.name); + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✔')}`)}`, + }); + if (single) break; + } + else { + spinner.update({ + text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.red('✖')}`)}`, + }); + } + } + + return detectedOptions; + } + + /** + * @param {import('./types.js').Package} pkg - The package to detect configs + * @returns {import('./types.js').Package['config']} - Detected configs record + */ + detectConfig(pkg) { + + const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); + spinner.start(); + + /** @type {import('./types.js').Package['config']} */ + const pkgConfig = {}; + + for (const config of this.configs) { + pkgConfig[config.name] = this.detectOptions( + pkg, + config.options, + config.type === 'single', + spinner, + ); + spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); + } + + spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}\n${c.dim(JSON.stringify(pkgConfig))}\n` }); + return pkgConfig; + } + + /** + * @param {import('./types.js').Package[]} packages Packages to generate the map from + * @returns {import('./types.js').PackagesConfigsMap} A map of what packages has some configuration + */ + generateConfigMap(packages) { + + /** @type {import('./types.js').PackagesConfigsMap} */ + const configMap = new Map(); + + for (const pkg of packages) { + + Object.entries(pkg.config ?? {}).forEach(([key, options]) => { + /** @type {Map} */ + const optionsMap = configMap.get(key) ?? new Map(); + + options.forEach(option => { + const paths = optionsMap.get(option) ?? []; + optionsMap.set(option, [pkg.path, ...paths]); + + if (paths.length >= packages.length - 2 || paths.includes(this.dir)) { + console.log('a', packages.length, paths.length); + optionsMap.set(option, [this.dir]); + } + }); + + configMap.set(key, optionsMap); + }); + } + + return configMap; + + } +} + diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 454f4a8..20e2906 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -1,12 +1,4 @@ import Cli from './cli.js'; -import configs from './configs.js'; -import { program } from 'commander'; -program.option('--packages '); - -program.parse(process.argv); -/** @type {{ packages?: string[] } & import('commander').OptionValues} */ -const options = program.opts(); - -const cli = new Cli({ configs, packages: options.packages }); +const cli = new Cli(); await cli.run(); diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index e9c9b21..91c2d7f 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -1,3 +1,10 @@ +import type { OptionValues } from 'commander'; + +export type CliArgs = { + packages?: string[] + mergeToRoot?: boolean + dir: string +} & OptionValues; export interface Config { name: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5edc700..0a81224 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@types/node': specifier: ^20.4.2 version: 20.4.2 + '@types/prompts': + specifier: ^2.4.4 + version: 2.4.4 packages/config: dependencies: @@ -891,6 +894,13 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/prompts@2.4.4: + resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==} + dependencies: + '@types/node': 20.4.2 + kleur: 3.0.3 + dev: true + /@types/pug@2.0.6: resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} dev: true @@ -2507,7 +2517,6 @@ packages: /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - dev: false /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} From d2adda8aeb7a7bc5b56579032fa3f1af8c3049b9 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:07:59 -0300 Subject: [PATCH 16/29] =?UTF-8?q?feat:=20=E2=9C=A8=20manual=20options/conf?= =?UTF-8?q?igs=20selection=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 3 +- packages/cli/src/cli.js | 19 +++--- packages/cli/src/configs.js | 2 + packages/cli/src/configsProcessor.js | 96 +++++++++++++++++++--------- packages/cli/src/lib/str.js | 10 +++ packages/cli/src/types.d.ts | 1 + 6 files changed, 90 insertions(+), 41 deletions(-) create mode 100644 packages/cli/src/lib/str.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 39e2683..0823e1c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "yaml": "^2.3.1" }, "devDependencies": { - "@types/node": "^20.4.2" + "@types/node": "^20.4.2", + "@types/prompts": "^2.4.4" } } diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index ef1b2f7..59f8f4d 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -13,17 +13,20 @@ export default class Cli { dir: process.cwd(), }; - setArgs() { + /** + * @param {import('./types').CliArgs} [args] Cli arguments object + */ + constructor(args) { this.#program .option('--packages ') .option('--merge-to-root') - .option('--dir ', undefined); - - this.#program.parse(); + .option('--dir ', undefined) + .parse(); this.args = { ...this.args, ...this.#program.opts(), + ...args, }; this.args.dir = !this.args.dir.startsWith('/') @@ -33,17 +36,15 @@ export default class Cli { } async run() { - this.setArgs(); - - console.log(this.args.dir); const processor = new ConfigsProcessor({ configs }); - const packages = (await new Workspace(this.args.dir, this.args?.packages ) - .getPackages()) + const workspace = new Workspace(this.args.dir, this.args?.packages); + let packages = (await workspace.getPackages()) .map(pkg => { pkg.config = processor.detectConfig(pkg); return pkg; }); + packages = await processor.questionConfigs(packages); const configsMaps = processor.generateConfigMap(packages); diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js index 896b094..7f7b71e 100644 --- a/packages/cli/src/configs.js +++ b/packages/cli/src/configs.js @@ -4,6 +4,8 @@ export default [ { name: 'framework', type: 'multiple', + description: 'The UI frameworks being used in the project', + manual: true, options: [ { name: 'svelte', diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js index 065ef08..81bcd51 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configsProcessor.js @@ -1,8 +1,9 @@ #!node import path from 'node:path'; -import { createSpinner } from 'nanospinner'; import glob from 'picomatch'; +import prompts from 'prompts'; import c from 'picocolors'; +import str from './lib/str.js'; export default class ConfigsProcessor { /** @type {string} */ @@ -31,25 +32,17 @@ export default class ConfigsProcessor { * @param {import('./types.js').Package} pkg - Package to detect from * @param {import('./types.js').Config['options']} options - Options to be passed * @param {boolean} single - Whether to only detect one option - * @param {import('nanospinner').Spinner} spinner - Spinner to update * @returns {string[]} - The detected options */ - detectOptions(pkg, options, single, spinner) { + detectOptions(pkg, options, single) { /** @type {string[]} */ const detectedOptions = []; for (const option of options) { - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)}`)}`, - }); - if (option.detect === true) { detectedOptions.push(option.name); - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✓')}`)}`, - }); continue; } else if (!option.detect) continue; @@ -61,54 +54,96 @@ export default class ConfigsProcessor { if (files.length > 0 || directories.length > 0) { detectedOptions.push(option.name); - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✔')}`)}`, - }); if (single) break; } - else { - spinner.update({ - text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.red('✖')}`)}`, - }); - } } return detectedOptions; } /** - * @param {import('./types.js').Package} pkg - The package to detect configs - * @returns {import('./types.js').Package['config']} - Detected configs record + * @param {import('./types.js').Package[]} packages - The package to detect configs + * @returns {Promise} - The selected options by the user + */ + async questionConfigs(packages) { + + const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`); + + for (const config of this.configs.filter(c => c.manual)) { + + /** @type {import('prompts').Choice[]} */ + const configChoices = config.options.map(option => {return { title: `${str.capitalize(option.name)}`, value: option.name };}); + + /** @type {Record} */ + const selectedOptions = await prompts({ + name: config.name, + type: config.type === 'single' ? 'select' : 'multiselect', + message: str.capitalize(config.name), + choices: configChoices, + hint: config.description, + instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')), + }); + + if (selectedOptions[config.name].length === 0) continue; + + /** @type {{title: string, value: import('./types').Package}[]} */ + const packagesOptions = packages + .map(pkg => { + return !pkg.root + ? { + title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`, + value: pkg, + } + : { title: 'root', value: pkg }; + }) + .filter(p => p.title !== 'root'); + + /** @type {Record<'packages', import('./types').Package[]>} */ + const selected = await prompts({ + name: 'packages', + type: 'multiselect', + message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`, + choices: packagesOptions, + min: 1, + instructions: instructions + c.dim(c.italic('\nToggle all to use in the root configuration\n')), + }); + selected.packages = selected.packages ?? []; + + selected.packages.map(pkg => { pkg.config = { ...pkg.config, ...selectedOptions }; return pkg; }); + packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg); + } + + return packages; + + } + + /** + * @param {import('./types').Package} pkg - The package to detect configs + * @returns {import('./types').Package['config']} - Detected configs record */ detectConfig(pkg) { - const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`); - spinner.start(); - /** @type {import('./types.js').Package['config']} */ const pkgConfig = {}; - for (const config of this.configs) { + for (const config of this.configs.filter(c => !c.manual)) { pkgConfig[config.name] = this.detectOptions( pkg, config.options, config.type === 'single', - spinner, ); - spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` }); } - spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}\n${c.dim(JSON.stringify(pkgConfig))}\n` }); return pkgConfig; } /** - * @param {import('./types.js').Package[]} packages Packages to generate the map from - * @returns {import('./types.js').PackagesConfigsMap} A map of what packages has some configuration + * @param {import('./types').Package[]} packages Packages to generate the map from + * @returns {import('./types').PackagesConfigsMap} A map of what packages has some configuration */ generateConfigMap(packages) { - /** @type {import('./types.js').PackagesConfigsMap} */ + /** @type {import('./types').PackagesConfigsMap} */ const configMap = new Map(); for (const pkg of packages) { @@ -122,7 +157,6 @@ export default class ConfigsProcessor { optionsMap.set(option, [pkg.path, ...paths]); if (paths.length >= packages.length - 2 || paths.includes(this.dir)) { - console.log('a', packages.length, paths.length); optionsMap.set(option, [this.dir]); } }); diff --git a/packages/cli/src/lib/str.js b/packages/cli/src/lib/str.js new file mode 100644 index 0000000..257b0d4 --- /dev/null +++ b/packages/cli/src/lib/str.js @@ -0,0 +1,10 @@ + +/** + * @param {string} str - The string to capitalize + * @returns {string} The capitalized string + */ +function capitalize(str) { + return str[0].toUpperCase() + str.slice(1); +} + +export default { capitalize }; diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 91c2d7f..15b0e48 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -10,6 +10,7 @@ export interface Config { name: string type: 'single' | 'multiple' manual?: boolean + description?: string options: { name: string packages: Record From 95ad4abf9f32902fd3119d06afe10ae156df3eba Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:12:32 -0300 Subject: [PATCH 17/29] =?UTF-8?q?feat:=20=E2=9C=A8=20"confirm"=20config=20?= =?UTF-8?q?type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added "confirm" config type for yes/no questions/configs --- packages/cli/src/cli.js | 17 +++++++++++++- packages/cli/src/configs.js | 11 +++++++++- packages/cli/src/configsProcessor.js | 33 +++++++++++++++++++++++----- packages/cli/src/lib/count.js | 13 +++++++++++ packages/cli/src/types.d.ts | 20 +++++++++++++---- 5 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/lib/count.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 59f8f4d..84e81cb 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -2,7 +2,10 @@ import { Command } from 'commander'; import ConfigsProcessor from './configsProcessor.js'; import configs from './configs.js'; import Workspace from './workspace.js'; +import c from 'picocolors'; import path from 'node:path'; +import { createSpinner } from 'nanospinner'; +import count from './lib/count.js'; export default class Cli { @@ -37,14 +40,26 @@ export default class Cli { async run() { + const spinner = createSpinner('Detecting workspace configuration'); + const processor = new ConfigsProcessor({ configs }); const workspace = new Workspace(this.args.dir, this.args?.packages); + let packages = (await workspace.getPackages()) .map(pkg => { + spinner.update({ text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}` }); + pkg.config = processor.detectConfig(pkg); + return pkg; }); - packages = await processor.questionConfigs(packages); + + spinner.success({ text: + 'Detecting workspace configuration ' + + c.dim(`${count.packagesWithConfigs(packages)} configs founded`), + }); + + packages = await processor.questionConfig(packages, configs.filter(c => c.manual)); const configsMaps = processor.generateConfigMap(packages); diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js index 7f7b71e..05d4ab2 100644 --- a/packages/cli/src/configs.js +++ b/packages/cli/src/configs.js @@ -5,7 +5,6 @@ export default [ name: 'framework', type: 'multiple', description: 'The UI frameworks being used in the project', - manual: true, options: [ { name: 'svelte', @@ -21,4 +20,14 @@ export default [ }, ], }, + { + name: 'strict', + type: 'confirm', + manual: true, + options: [{ + name: 'yes', + packages: { '@eslint/config': 'config' }, + configs: ['config.strict'], + }], + }, ]; diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js index 81bcd51..7fb598e 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configsProcessor.js @@ -62,14 +62,17 @@ export default class ConfigsProcessor { } /** - * @param {import('./types.js').Package[]} packages - The package to detect configs + * @param {import('./types.js').Package[] | import('./types.js').Package} pkg - The packages to questions the configs + * @param {import('./types').Config[]} configs - The configs to be used * @returns {Promise} - The selected options by the user */ - async questionConfigs(packages) { + async questionConfig(pkg, configs) { + + const packages = Array.isArray(pkg) ? [...pkg] : [pkg]; const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`); - for (const config of this.configs.filter(c => c.manual)) { + for (const config of configs) { /** @type {import('prompts').Choice[]} */ const configChoices = config.options.map(option => {return { title: `${str.capitalize(option.name)}`, value: option.name };}); @@ -77,15 +80,31 @@ export default class ConfigsProcessor { /** @type {Record} */ const selectedOptions = await prompts({ name: config.name, - type: config.type === 'single' ? 'select' : 'multiselect', + type: config.type === 'multiple' ? 'multiselect' : 'select', message: str.capitalize(config.name), - choices: configChoices, + choices: config.type === 'confirm' ? [ + { + title: 'Yes', + value: ['yes'], + }, + { + title: 'No', + value: null, + }, + ] : configChoices, hint: config.description, instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')), }); + if (selectedOptions[config.name] === null) continue; + if (selectedOptions[config.name].length === 0) continue; + if (packages.length <= 1) { + packages[0].config = selectedOptions; + continue; + } + /** @type {{title: string, value: import('./types').Package}[]} */ const packagesOptions = packages .map(pkg => { @@ -130,7 +149,7 @@ export default class ConfigsProcessor { pkgConfig[config.name] = this.detectOptions( pkg, config.options, - config.type === 'single', + config.type !== 'multiple', ); } @@ -143,6 +162,8 @@ export default class ConfigsProcessor { */ generateConfigMap(packages) { + console.log(packages); + /** @type {import('./types').PackagesConfigsMap} */ const configMap = new Map(); diff --git a/packages/cli/src/lib/count.js b/packages/cli/src/lib/count.js new file mode 100644 index 0000000..6a17350 --- /dev/null +++ b/packages/cli/src/lib/count.js @@ -0,0 +1,13 @@ + +/** + * @param {import('../types').Package[]} packages - Package list + * @returns {number} Number of packages' configs + */ +function packagesWithConfigs(packages) { + return packages.map(p => + Object.entries(p.config ?? {}) + .filter(([, options]) => options.length > 0).length, + ).reduce((partial, sum) => partial + sum, 0); +} + +export default { packagesWithConfigs }; diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 15b0e48..90f6f95 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -6,21 +6,33 @@ export type CliArgs = { dir: string } & OptionValues; -export interface Config { +export type Config = { name: string type: 'single' | 'multiple' manual?: boolean description?: string options: { name: string - packages: Record + packages?: Record configs?: string[] rules?: string[] detect?: string[] | true }[] -} +} | { + name: string + type: 'confirm' + manual: true + description?: string + options: [{ + name: 'yes' + packages?: Record + configs?: string[] + rules?: string[] + detect?: undefined + }] +}; -export type PackagesConfigsMap = Map>; +export type PackagesConfigsMap = Map>; export interface Package { root?: boolean From 107be3d4aba655434de342c70e778c03d714d323 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:25:42 -0300 Subject: [PATCH 18/29] =?UTF-8?q?fix:=20=F0=9F=90=9B=20overridden=20config?= =?UTF-8?q?s=20in=20single-package=20arrays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/configsProcessor.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js index 7fb598e..fd274d7 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configsProcessor.js @@ -101,7 +101,7 @@ export default class ConfigsProcessor { if (selectedOptions[config.name].length === 0) continue; if (packages.length <= 1) { - packages[0].config = selectedOptions; + packages[0].config = { ...packages[0].config, ...selectedOptions }; continue; } @@ -162,8 +162,6 @@ export default class ConfigsProcessor { */ generateConfigMap(packages) { - console.log(packages); - /** @type {import('./types').PackagesConfigsMap} */ const configMap = new Map(); From 4384f6143a37fc25cd8dc3c3c6bbc399a3624d12 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:34:31 -0300 Subject: [PATCH 19/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20use=20Ma?= =?UTF-8?q?p=20on=20packages'=20configs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/configsProcessor.js | 21 +++++++++++++++------ packages/cli/src/lib/notNull.js | 21 +++++++++++++++++++++ packages/cli/src/types.d.ts | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/lib/notNull.js diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js index fd274d7..9cd3ab6 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configsProcessor.js @@ -4,6 +4,7 @@ import glob from 'picomatch'; import prompts from 'prompts'; import c from 'picocolors'; import str from './lib/str.js'; +import notNull from './lib/notNull.js'; export default class ConfigsProcessor { /** @type {string} */ @@ -101,7 +102,10 @@ export default class ConfigsProcessor { if (selectedOptions[config.name].length === 0) continue; if (packages.length <= 1) { - packages[0].config = { ...packages[0].config, ...selectedOptions }; + packages[0].config = new Map([ + ...(packages[0].config ?? []), + ...Object.entries(selectedOptions), + ]); continue; } @@ -128,7 +132,12 @@ export default class ConfigsProcessor { }); selected.packages = selected.packages ?? []; - selected.packages.map(pkg => { pkg.config = { ...pkg.config, ...selectedOptions }; return pkg; }); + selected.packages.map(pkg => { + pkg.config = new Map([ + ...(pkg.config ?? []), + ...Object.entries(selectedOptions), + ]); return pkg; + }); packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg); } @@ -143,14 +152,14 @@ export default class ConfigsProcessor { detectConfig(pkg) { /** @type {import('./types.js').Package['config']} */ - const pkgConfig = {}; + const pkgConfig = new Map(); for (const config of this.configs.filter(c => !c.manual)) { - pkgConfig[config.name] = this.detectOptions( + pkgConfig.set(config.name, this.detectOptions( pkg, config.options, config.type !== 'multiple', - ); + )); } return pkgConfig; @@ -167,7 +176,7 @@ export default class ConfigsProcessor { for (const pkg of packages) { - Object.entries(pkg.config ?? {}).forEach(([key, options]) => { + notNull(pkg.config).forEach((options, key) => { /** @type {Map} */ const optionsMap = configMap.get(key) ?? new Map(); diff --git a/packages/cli/src/lib/notNull.js b/packages/cli/src/lib/notNull.js new file mode 100644 index 0000000..cca1b49 --- /dev/null +++ b/packages/cli/src/lib/notNull.js @@ -0,0 +1,21 @@ +/** + * JSDoc types lack a non-null assertion. + * + * @template T + * + * @param {T} value The value which to assert against null or undefined + * @returns {NonNullable} The said value + * + * @throws {TypeError} If the value is unexpectedly null or undefined + * + * @author Jimmy Wärting - https://github.com/jimmywarting + * + * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031 + * + * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966 + */ +export default function notNull(value) { + // Use `==` to check for both null and undefined + if (value == null) throw new Error('did not expect value to be null or undefined'); + return value; +} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 90f6f95..a1e2210 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -40,5 +40,5 @@ export interface Package { path: string files: string[] directories: string[] - config?: Record + config?: Map } From 1b2891b7eef1b1f6b16557333021827f32474fe9 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:10:24 -0300 Subject: [PATCH 20/29] =?UTF-8?q?feat:=20=E2=9C=A8=20merge=20configuration?= =?UTF-8?q?s=20to=20root=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 26 ++++++++++++++--- packages/cli/src/lib/count.js | 3 +- packages/cli/src/workspace.js | 54 +++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 84e81cb..6ec8d02 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -6,6 +6,7 @@ import c from 'picocolors'; import path from 'node:path'; import { createSpinner } from 'nanospinner'; import count from './lib/count.js'; +import prompts from 'prompts'; export default class Cli { @@ -54,12 +55,29 @@ export default class Cli { return pkg; }); - spinner.success({ text: - 'Detecting workspace configuration ' + - c.dim(`${count.packagesWithConfigs(packages)} configs founded`), + spinner.success({ + text: + 'Detecting workspace configuration ' + + c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`), }); - packages = await processor.questionConfig(packages, configs.filter(c => c.manual)); + const merge = this.args.mergeToRoot ?? packages.length > 1 ? + /** @type {{merge: boolean}} */ + (await prompts({ + name: 'merge', + message: + `Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` + + c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')), + initial: true, + type: 'confirm', + })).merge : true; + + console.log(c.dim('\nPlease select which options you prefer\n')); + + packages = await processor.questionConfig( + merge ? workspace.mergePackages(packages) : packages, + configs.filter(c => c.manual), + ); const configsMaps = processor.generateConfigMap(packages); diff --git a/packages/cli/src/lib/count.js b/packages/cli/src/lib/count.js index 6a17350..d772dcd 100644 --- a/packages/cli/src/lib/count.js +++ b/packages/cli/src/lib/count.js @@ -5,8 +5,7 @@ */ function packagesWithConfigs(packages) { return packages.map(p => - Object.entries(p.config ?? {}) - .filter(([, options]) => options.length > 0).length, + [...p.config?.values() ?? []].filter((options) => options.length > 0).length, ).reduce((partial, sum) => partial + sum, 0); } diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js index 1bbaad1..3a1e1e9 100644 --- a/packages/cli/src/workspace.js +++ b/packages/cli/src/workspace.js @@ -59,7 +59,8 @@ export default class Workspace { /** * @param {string} directory - The directory to get the workspace from - * @param {string[]} [packagePatterns] - List of package patterns + * @param {string[] | false} [packagePatterns] + * List of package patterns (`false` to explicitly tell that this workspace is not a monorepo) */ constructor(directory, packagePatterns) { this.dir = directory; @@ -157,8 +158,6 @@ export default class Workspace { async getPackages() { const paths = await this.getPaths(); - const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns(); - const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); /** @type {import('./types').Package} */ const rootPackage = { @@ -169,6 +168,11 @@ export default class Workspace { directories: paths.directories, }; + if (this.packagePatterns === false) return [rootPackage]; + + const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns(); + const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); + /** @type {import('./types').Package[]} */ const packages = []; @@ -196,4 +200,48 @@ export default class Workspace { } + /** + * @param {import('./types').Package[]} packages - Packages to be merged into root + * @returns {[import('./types').Package]} A array containing only the root package + */ + mergePackages(packages) { + + const rootPackage = packages.find(p => p.root) ?? packages[0]; + + const merged = packages.reduce((accumulated, pkg) => { + + const files = [...new Set([ + ...accumulated.files, + ...pkg.files.map(f => join(pkg.path, f)), + ] + .map(p => p.replace(`${rootPackage.path}/`, '')), + )]; + + const directories = [...new Set([ + ...accumulated.directories, + ...pkg.directories.map(d => join(pkg.path, d)), + ] + .map(p => p.replace(`${rootPackage.path}/`, ''))), + ]; + + const mergedConfig = new Map(); + for (const [config, options] of pkg.config ?? []) { + const accumulatedOptions = accumulated.config?.get(config) ?? []; + mergedConfig.set(config, [...new Set([...options, ...accumulatedOptions])]); + } + + return { + root: true, + path: rootPackage.path, + name: rootPackage.name, + files, + directories, + config: mergedConfig, + }; + }, rootPackage); + + return [merged]; + + } + } From 978f06605ec747ec424243219a8ed93a0cc7bdeb Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 7 Aug 2023 16:25:04 -0300 Subject: [PATCH 21/29] =?UTF-8?q?feat:=20=E2=9C=A8=20config=20file=20objec?= =?UTF-8?q?t=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 2 +- packages/cli/src/cli.js | 7 +- packages/cli/src/configs.js | 8 +- packages/cli/src/configsWriter.js | 126 ++++++++++++++++++++++++++++++ packages/cli/src/types.d.ts | 17 +++- pnpm-lock.yaml | 109 +++++++++++++++++++------- 6 files changed, 230 insertions(+), 39 deletions(-) create mode 100644 packages/cli/src/configsWriter.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 0823e1c..ba0196b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,11 +29,11 @@ "license": "MIT", "dependencies": { "commander": "^11.0.0", - "magic-string": "^0.30.2", "nanospinner": "^1.1.0", "picocolors": "^1.0.0", "picomatch": "^2.3.1", "prompts": "^2.4.2", + "recast": "^0.23.3", "yaml": "^2.3.1" }, "devDependencies": { diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 6ec8d02..bcd4167 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -7,6 +7,7 @@ import path from 'node:path'; import { createSpinner } from 'nanospinner'; import count from './lib/count.js'; import prompts from 'prompts'; +import ConfigsWriter from './configsWriter.js'; export default class Cli { @@ -79,9 +80,11 @@ export default class Cli { configs.filter(c => c.manual), ); - const configsMaps = processor.generateConfigMap(packages); + const writer = new ConfigsWriter(configs, packages.find(c => c.root)?.path); - console.log(configsMaps); + console.log(packages[0].config); + + packages.map(pkg => {pkg.configFile = writer.generateFileObj(pkg); return pkg;}); } diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js index 05d4ab2..44d948a 100644 --- a/packages/cli/src/configs.js +++ b/packages/cli/src/configs.js @@ -9,13 +9,13 @@ export default [ { name: 'svelte', packages: { '@eslit/svelte': 'svelte' }, - rules: ['svelte.default'], + configs: ['svelte.recommended'], detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'], }, { name: 'vue', - packages: { '@eslint/vue': 'vue' }, - rules: ['vue.default'], + packages: { '@eslit/vue': ['vue', ['hello', 'world']], '@eslit/svelte': ['hello'] }, + configs: ['vue.recommended'], detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'], }, ], @@ -26,7 +26,7 @@ export default [ manual: true, options: [{ name: 'yes', - packages: { '@eslint/config': 'config' }, + packages: { '@eslit/config': 'config', '@eslit/vue': ['test1'] }, configs: ['config.strict'], }], }, diff --git a/packages/cli/src/configsWriter.js b/packages/cli/src/configsWriter.js new file mode 100644 index 0000000..625cc05 --- /dev/null +++ b/packages/cli/src/configsWriter.js @@ -0,0 +1,126 @@ +import path from 'node:path'; +import {} from 'recast'; +import notNull from './lib/notNull.js'; + +/** + * @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2 + * @param {import('./types').ConfigFile['imports']} map2 - The map to has it values merged to map1 + * @returns {import('./types').ConfigFile['imports']} The resulting map + */ +function mergeImportsMaps(map1, map2) { + for (const [key, value] of map2) { + if (!map1.has(key)) { + map1.set(key, value); + continue; + } + const imports1 = notNull(map1.get(key)); + const imports2 = notNull(map2.get(key)); + + /** + * Because arrays and objects are always different independently from having equal values + * ([] === [] -> false). It is converted to a string so the comparison can be made. + */ + switch ([typeof imports1 === 'string', typeof imports2 === 'string'].join(',')) { + case 'true,true': + map1.set(key, imports1); + break; + case 'true,false': + map1.set(key, [['default', imports1.toString()], ...imports2]); + break; + case 'false,true': + map1.set(key, [['default', imports2.toString()], ...imports1]); + break; + case 'false,false': + map1.set(key, [...imports1, ...imports2]); + break; + } + map1.set(key, [...new Set(map1.get(key))]); + } + return map1; +} + +/** + * @param {string} path1 The path to traverse from + * @param {string} root The root path + * @returns {string} The path to traverse + */ +function getPathDepth(path1, root) { + const pathDepth = path1.replace(root, '').split('/').slice(1); + if (pathDepth.length <= 1) return pathDepth.map(() => '.').join('/'); + return pathDepth.map(() => '..').join('/'); +} + +export default class ConfigsWriter { + + /** @type {string} */ + root = process.cwd(); + + /** + * @param {import('./types').Config[]} configs The array of configs to construct from + * @param {string} [root] The root directory path + */ + constructor(configs, root) { + this.configs = configs; + this.root = root ?? this.root; + } + + /** + * @param {import('./types').Package} pkg The package to generate the config string from + * @returns {import('./types').ConfigFile} The config file object + */ + generateFileObj(pkg) { + /** @type {import('./types').ConfigFile} */ + const configObj = { + path: path.join(pkg.path, 'eslint.config.js'), + imports: new Map(), + configs: [], + presets: [], + rules: [], + }; + + if (!pkg.root) { + const rootConfig = path.join(getPathDepth(pkg.path, this.root), 'eslint.config.js'); + configObj.imports.set(!rootConfig.startsWith('.') ? `./${rootConfig}` : rootConfig, 'root'); + configObj.presets.push('root'); + } + + for (const [configName, optionsNames] of notNull(pkg.config)) { + + const config = this.configs.find(c => c.name === configName); + if (!config) continue; + + const options = config.options.filter(o => optionsNames.includes(o.name)); + if (!options || options.length === 0) continue; + + const imports = options.reduce((acc, opt) => { + const map1 = new Map(Object.entries(acc.packages ?? {})); + const map2 = new Map(Object.entries(opt.packages ?? {})); + acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2)); + return acc; + }); + + configObj.imports = mergeImportsMaps(configObj.imports, new Map(Object.entries(imports.packages ?? {}))); + + configObj.configs = [...new Set([ + ...configObj.configs, + ...options.map(o => o.configs ?? []).flat(), + ])]; + + configObj.rules = [...new Set([ + ...configObj.rules, + ...options.map(o => o.rules ?? []).flat(), + ])]; + + configObj.presets = [...new Set([ + ...configObj.presets, + ...options.map(o => o.presets ?? []).flat(), + ])]; + + } + + console.log(configObj); + + return configObj; + } + +} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index a1e2210..b72a1fd 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -13,9 +13,10 @@ export type Config = { description?: string options: { name: string - packages?: Record + packages?: Record configs?: string[] rules?: string[] + presets?: string[] detect?: string[] | true }[] } | { @@ -25,15 +26,14 @@ export type Config = { description?: string options: [{ name: 'yes' - packages?: Record + packages?: Record configs?: string[] rules?: string[] + presets?: string[] detect?: undefined }] }; -export type PackagesConfigsMap = Map>; - export interface Package { root?: boolean name: string @@ -41,4 +41,13 @@ export interface Package { files: string[] directories: string[] config?: Map + configFile?: ConfigFile +} + +export interface ConfigFile { + path: string + imports: Map + configs: string[] + presets: string[] + rules: string[] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a81224..8314e7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,9 +102,6 @@ importers: commander: specifier: ^11.0.0 version: 11.0.0 - magic-string: - specifier: ^0.30.2 - version: 0.30.2 nanospinner: specifier: ^1.1.0 version: 1.1.0 @@ -117,6 +114,9 @@ importers: prompts: specifier: ^2.4.2 version: 2.4.2 + recast: + specifier: ^0.23.3 + version: 0.23.3 yaml: specifier: ^2.3.1 version: 2.3.1 @@ -712,6 +712,7 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -1269,10 +1270,25 @@ packages: engines: {node: '>=0.10.0'} dev: true + /assert@2.0.0: + resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==} + dependencies: + es6-object-assign: 1.1.0 + is-nan: 1.3.2 + object-is: 1.1.5 + util: 0.12.5 + dev: false + + /ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + dependencies: + tslib: 2.4.1 + dev: false + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: true /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} @@ -1334,7 +1350,6 @@ packages: dependencies: function-bind: 1.1.1 get-intrinsic: 1.2.1 - dev: true /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1573,7 +1588,6 @@ packages: dependencies: has-property-descriptors: 1.0.0 object-keys: 1.1.1 - dev: true /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} @@ -1694,6 +1708,10 @@ packages: is-symbol: 1.0.4 dev: true + /es6-object-assign@1.1.0: + resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==} + dev: false + /es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: true @@ -1931,7 +1949,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -2049,7 +2066,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: true /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -2082,7 +2098,6 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -2110,7 +2125,6 @@ packages: has: 1.0.3 has-proto: 1.0.1 has-symbols: 1.0.3 - dev: true /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} @@ -2170,7 +2184,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: true /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2205,31 +2218,26 @@ packages: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: get-intrinsic: 1.2.1 - dev: true /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} - dev: true /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - dev: true /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 - dev: true /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2294,6 +2302,14 @@ packages: side-channel: 1.0.4 dev: true + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -2337,7 +2353,6 @@ packages: /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: true /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} @@ -2368,12 +2383,27 @@ packages: engines: {node: '>=8'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + dev: false + /is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -2454,7 +2484,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: true /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} @@ -2608,13 +2637,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /magic-string@0.30.2: - resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: false - /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2759,10 +2781,17 @@ packages: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + dev: false + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - dev: true /object.assign@4.1.4: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} @@ -3025,6 +3054,17 @@ packages: picomatch: 2.3.1 dev: true + /recast@0.23.3: + resolution: {integrity: sha512-HbCVFh2ANP6a09nzD4lx7XthsxMOJWKX5pIcUwtLrmeEIl3I0DwjCoVXDE0Aobk+7k/mS3H50FK4iuYArpcT6Q==} + engines: {node: '>= 4'} + dependencies: + assert: 2.0.0 + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tslib: 2.4.1 + dev: false + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3235,6 +3275,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + /spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -3518,7 +3563,6 @@ packages: /tslib@2.4.1: resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} - dev: true /tsutils@3.21.0(typescript@5.0.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -3675,6 +3719,16 @@ packages: dependencies: punycode: 2.3.0 + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.10 + which-typed-array: 1.1.10 + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -3777,7 +3831,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: true /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} From 983d4958f2a1f4bde57fc188445999218cb0dc18 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:08:23 -0300 Subject: [PATCH 22/29] =?UTF-8?q?feat:=20=E2=9C=A8=20write=20and=20manipul?= =?UTF-8?q?ate=20eslint.config.js=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 1 + packages/cli/src/cli.js | 6 +- packages/cli/src/configsWriter.js | 176 +++++++++++++++++++++++++++++- pnpm-lock.yaml | 3 + 4 files changed, 181 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ba0196b..eee25ee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "yaml": "^2.3.1" }, "devDependencies": { + "@types/estree": "^1.0.1", "@types/node": "^20.4.2", "@types/prompts": "^2.4.4" } diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index bcd4167..999cf35 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -84,7 +84,11 @@ export default class Cli { console.log(packages[0].config); - packages.map(pkg => {pkg.configFile = writer.generateFileObj(pkg); return pkg;}); + packages.map(async pkg => { + pkg.configFile = writer.generateObj(pkg); + console.log(await writer.write(pkg.configFile)); + return pkg; + }); } diff --git a/packages/cli/src/configsWriter.js b/packages/cli/src/configsWriter.js index 625cc05..701473d 100644 --- a/packages/cli/src/configsWriter.js +++ b/packages/cli/src/configsWriter.js @@ -1,6 +1,8 @@ import path from 'node:path'; -import {} from 'recast'; import notNull from './lib/notNull.js'; +import * as recast from 'recast'; +import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; /** * @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2 @@ -68,7 +70,7 @@ export default class ConfigsWriter { * @param {import('./types').Package} pkg The package to generate the config string from * @returns {import('./types').ConfigFile} The config file object */ - generateFileObj(pkg) { + generateObj(pkg) { /** @type {import('./types').ConfigFile} */ const configObj = { path: path.join(pkg.path, 'eslint.config.js'), @@ -118,9 +120,175 @@ export default class ConfigsWriter { } - console.log(configObj); - return configObj; } + /** + * ! NOTE: + * These functions declared bellow are notably hard to read and have lots of exceptions and + * disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to + * prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work + * with them. + * + * But for somewhat help developing and prevent unwanted errors in the future, the types and eslint + * errors are explicitly disabled and types are explicitly overridden. This is why there are so + * many JSDoc type annotations and comments in general. + * + * Any help to make this code more readable and robust is appreciated + */ + + /** + * @param {import('./types').ConfigFile['configs']} configs The configs objects defined in the config file + * @returns {(import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression)[]} + * The ast expressions nodes to be printed + */ + createConfigExpressions(configs) { + return configs + .map(c => { + /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const e = recast.parse(c).program.body[0].expression; return e; + }) + .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)); + } + + /** + * @param {import('./types').ConfigFile['presets']} presets The presets objects defined in the config file + * @returns {import('estree').SpreadElement[]} + * The ast expressions nodes to be printed + */ + createPresetExpressions(presets) { + return presets + .map(p => { + /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const e = recast.parse(p).program.body[0].expression; return e; + }) + .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)) + .map(e => { + /** @type {import('estree').SpreadElement} */ + const spreadElement = { + type: 'SpreadElement', + argument: e, + }; + return spreadElement; + }); + } + + /** + * @param {import('./types').ConfigFile['rules']} rules The rules objects defined in the config file + * @returns {import('estree').ObjectExpression} + * The ast object expression nodes to be printed + */ + createRulesExpression(rules) { + /** @type {import('estree').ObjectExpression} */ + const rulesObjectExpression = rules + .map(r => { + /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const e = recast.parse(r).program.body[0].expression; return e; + }) + .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)) + .map(e => { + /** @type {import('estree').SpreadElement} */ + const spreadElement = { + type: 'SpreadElement', + argument: e, + }; + return spreadElement; + }).reduce((acc, spreadElement) => { + acc.properties.push(spreadElement); + return acc; + }, { + /** @type {import('estree').ObjectExpression['type']} */ + type: 'ObjectExpression', + /** @type {import('estree').ObjectExpression['properties']} */ + properties: [], + }); + /** @type {import('estree').ObjectExpression} */ + const rulesExpression = { + type: 'ObjectExpression', + properties: [{ + // @ts-expect-error because ObjectProperty doesn't exist in estree types + type: 'ObjectProperty', + key: { type: 'Identifier', name: 'rules' }, + value: rulesObjectExpression, + }], + }; + return rulesExpression; + } + + /** + * @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file + * @returns {Promise} + */ + async write(config) { + + const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : ''; + + /** @type {import('estree').ExportDefaultDeclaration}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const defaultExportTemplate = recast.parse([ + '/** @type {import(\'eslint\').Linter.FlatConfig[]} */', + 'export default [', + '', + '];', + ].join('\n'), { + parser: (await import('recast/parsers/babel.js')), + }).program.body.find((/** @type {{ type: string; }} */ n) => n.type === 'ExportDefaultDeclaration'); + + + /** @type {{program: import('estree').Program}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) }); + + /** @type {import('estree').ExportDefaultDeclaration | undefined} */ + // @ts-expect-error because the types don't match, but are expected to be correct here + // as the type needs to be ExportDefaultDeclaration + let defaultExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); + if (!defaultExport) { + defaultExport = defaultExportTemplate; + } + else if (defaultExport.declaration.type !== 'ArrayExpression') { + ast.body.push({ + type: 'VariableDeclaration', + kind: 'const', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: 'oldConfig' }, + // @ts-expect-error because defaultExport's declaration is a ArrayExpression + init: defaultExport.declaration, + }], + }); + defaultExport = defaultExportTemplate; + // @ts-expect-error because defaultExport's declaration is a ArrayExpression + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + defaultExport.declaration.elements.push({ + type: 'SpreadElement', + argument: { type: 'Identifier', name: 'oldConfig' }, + }); + } + + const elementsExpressions = []; + + if (config.presets.length > 0) + elementsExpressions.push(...this.createPresetExpressions(config.presets)); + if (config.configs.length > 0) + elementsExpressions.push(...this.createConfigExpressions(config.configs)); + if (config.rules.length > 0) + elementsExpressions.push(this.createRulesExpression(config.rules)); + + // @ts-expect-error because defaultExport's declaration is a ArrayExpression + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + defaultExport.declaration.elements.push(...elementsExpressions); + + const idx = ast.body.findIndex(n => n.type === 'ExportDefaultDeclaration'); + if (idx > -1) ast.body.splice(idx, 1); + ast.body.push(defaultExport); + + const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code; + console.log(finalCode); + + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8314e7f..c874401 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: specifier: ^2.3.1 version: 2.3.1 devDependencies: + '@types/estree': + specifier: ^1.0.1 + version: 1.0.1 '@types/node': specifier: ^20.4.2 version: 20.4.2 From 5eb7eac8ab7b1528610d02983eefa57955d88500 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:04:29 -0300 Subject: [PATCH 23/29] =?UTF-8?q?feat:=20=E2=9C=A8=20generate=20config=20i?= =?UTF-8?q?mports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/configsWriter.js | 55 +++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/configsWriter.js b/packages/cli/src/configsWriter.js index 701473d..609843a 100644 --- a/packages/cli/src/configsWriter.js +++ b/packages/cli/src/configsWriter.js @@ -218,6 +218,53 @@ export default class ConfigsWriter { return rulesExpression; } + /** + * @param {import('./types').ConfigFile['imports']} imports The import map to be used to create the nodes + * @returns {import('estree').ImportDeclaration[]} The ImportDeclaration nodes + */ + createImportDeclarations(imports) { + /** @type {import('estree').ImportDeclaration[]} */ + const importsDeclarations = []; + for (const [pkgName, values] of imports) { + /** @type {import('estree').ImportDeclaration} */ + const declaration = { + type: 'ImportDeclaration', + specifiers: [], + source: { + type: 'Literal', + value: pkgName, + }, + }; + + if (typeof values === 'string') { + declaration.specifiers.push({ + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: values }, + }); + importsDeclarations.push(declaration); + continue; + } + + declaration.specifiers = values.map(v => { + /** @type {import('estree').ImportSpecifier} */ + const specifier = { + type: 'ImportSpecifier', + imported: { + type: 'Identifier', + name: typeof v === 'string' ? v : v[0], + }, + local: { + type: 'Identifier', + name: typeof v === 'string' ? v : v[1], + }, + }; + return specifier; + }); + importsDeclarations.push(declaration); + } + return importsDeclarations; + } + /** * @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file * @returns {Promise} @@ -243,8 +290,7 @@ export default class ConfigsWriter { const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) }); /** @type {import('estree').ExportDefaultDeclaration | undefined} */ - // @ts-expect-error because the types don't match, but are expected to be correct here - // as the type needs to be ExportDefaultDeclaration + // @ts-expect-error because the type to find has to be ExportDefaultDeclaration let defaultExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); if (!defaultExport) { defaultExport = defaultExportTemplate; @@ -269,6 +315,9 @@ export default class ConfigsWriter { }); } + if (config.imports.size > 0) + ast.body.unshift(...this.createImportDeclarations(config.imports)); + const elementsExpressions = []; if (config.presets.length > 0) @@ -287,7 +336,7 @@ export default class ConfigsWriter { ast.body.push(defaultExport); const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code; - console.log(finalCode); + console.log(finalCode, config.imports); } From 9a9ffc1a0434c60db281e62a908b397ffec1fc37 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:33:44 -0300 Subject: [PATCH 24/29] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20write=20config?= =?UTF-8?q?=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 2 + packages/cli/src/cli.js | 39 +++++++++++++------ .../src/{configsWriter.js => configsFile.js} | 30 ++++++++++---- packages/cli/src/types.d.ts | 1 + 4 files changed, 54 insertions(+), 18 deletions(-) rename packages/cli/src/{configsWriter.js => configsFile.js} (95%) diff --git a/packages/cli/package.json b/packages/cli/package.json index eee25ee..b2127fe 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,12 +28,14 @@ "bin": "./src/index.js", "license": "MIT", "dependencies": { + "cardinal": "^2.1.1", "commander": "^11.0.0", "nanospinner": "^1.1.0", "picocolors": "^1.0.0", "picomatch": "^2.3.1", "prompts": "^2.4.2", "recast": "^0.23.3", + "sisteransi": "^1.0.5", "yaml": "^2.3.1" }, "devDependencies": { diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 999cf35..c4bc5bf 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -7,7 +7,11 @@ import path from 'node:path'; import { createSpinner } from 'nanospinner'; import count from './lib/count.js'; import prompts from 'prompts'; -import ConfigsWriter from './configsWriter.js'; +import ConfigsFile from './configsFile.js'; +import * as cardinal from 'cardinal'; +import ansi from 'sisteransi'; + +const stdout = process.stdout; export default class Cli { @@ -63,12 +67,12 @@ export default class Cli { }); const merge = this.args.mergeToRoot ?? packages.length > 1 ? - /** @type {{merge: boolean}} */ + /** @type {{merge: boolean}} */ (await prompts({ name: 'merge', message: - `Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` + - c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')), + `Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` + + c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')), initial: true, type: 'confirm', })).merge : true; @@ -80,15 +84,28 @@ export default class Cli { configs.filter(c => c.manual), ); - const writer = new ConfigsWriter(configs, packages.find(c => c.root)?.path); + const fileHandler = new ConfigsFile(configs, packages.find(c => c.root)?.path); - console.log(packages[0].config); + for (const pkg of packages) { - packages.map(async pkg => { - pkg.configFile = writer.generateObj(pkg); - console.log(await writer.write(pkg.configFile)); - return pkg; - }); + pkg.configFile = fileHandler.generateObj(pkg); + pkg.configFile.content = await fileHandler.generate(pkg.configFile), + + await prompts({ + type: 'confirm', + name: 'write', + message: `Do you want to write this config file for ${pkg.root + ? c.blue('the root directory') + : c.blue(pkg.name) + }?\n\n${cardinal.highlight(pkg.configFile.content)}`, + initial: true, + }); + + stdout.write(ansi.erase.lines(pkg.configFile.content.split('\n').length + 2)); + + await fileHandler.write(pkg.configFile.path, pkg.configFile.content); + + } } diff --git a/packages/cli/src/configsWriter.js b/packages/cli/src/configsFile.js similarity index 95% rename from packages/cli/src/configsWriter.js rename to packages/cli/src/configsFile.js index 609843a..47515d7 100644 --- a/packages/cli/src/configsWriter.js +++ b/packages/cli/src/configsFile.js @@ -141,6 +141,8 @@ export default class ConfigsWriter { * @param {import('./types').ConfigFile['configs']} configs The configs objects defined in the config file * @returns {(import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression)[]} * The ast expressions nodes to be printed + * + * @private */ createConfigExpressions(configs) { return configs @@ -154,8 +156,9 @@ export default class ConfigsWriter { /** * @param {import('./types').ConfigFile['presets']} presets The presets objects defined in the config file - * @returns {import('estree').SpreadElement[]} - * The ast expressions nodes to be printed + * @returns {import('estree').SpreadElement[]} The ast expressions nodes to be printed + * + * @private */ createPresetExpressions(presets) { return presets @@ -177,8 +180,9 @@ export default class ConfigsWriter { /** * @param {import('./types').ConfigFile['rules']} rules The rules objects defined in the config file - * @returns {import('estree').ObjectExpression} - * The ast object expression nodes to be printed + * @returns {import('estree').ObjectExpression} The ast object expression nodes to be printed + * + * @private */ createRulesExpression(rules) { /** @type {import('estree').ObjectExpression} */ @@ -221,6 +225,8 @@ export default class ConfigsWriter { /** * @param {import('./types').ConfigFile['imports']} imports The import map to be used to create the nodes * @returns {import('estree').ImportDeclaration[]} The ImportDeclaration nodes + * + * @private */ createImportDeclarations(imports) { /** @type {import('estree').ImportDeclaration[]} */ @@ -267,9 +273,9 @@ export default class ConfigsWriter { /** * @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file - * @returns {Promise} + * @returns {Promise} The generated config file contents */ - async write(config) { + async generate(config) { const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : ''; @@ -336,8 +342,18 @@ export default class ConfigsWriter { ast.body.push(defaultExport); const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code; - console.log(finalCode, config.imports); + + return finalCode; } + /** + * @param {string} path The path to the file to be written + * @param {string} content The content of the file + * @returns {Promise} + */ + async write(path, content) { + await fs.writeFile(path, content, 'utf-8'); + } + } diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index b72a1fd..eced0d0 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -50,4 +50,5 @@ export interface ConfigFile { configs: string[] presets: string[] rules: string[] + content?: string } From 568bdb5d97f2ce87e17dabe18138ea1ea9f4c405 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:58:41 -0300 Subject: [PATCH 25/29] =?UTF-8?q?fix(cli):=20=F0=9F=90=9B=20confirm=20file?= =?UTF-8?q?=20write=20operation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index c4bc5bf..63ca5ff 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -89,21 +89,24 @@ export default class Cli { for (const pkg of packages) { pkg.configFile = fileHandler.generateObj(pkg); - pkg.configFile.content = await fileHandler.generate(pkg.configFile), + pkg.configFile.content = await fileHandler.generate(pkg.configFile); - await prompts({ - type: 'confirm', - name: 'write', - message: `Do you want to write this config file for ${pkg.root - ? c.blue('the root directory') - : c.blue(pkg.name) + /** @type {boolean} */ + const shouldWrite = + /** @type {{write: boolean}} */ + (await prompts({ + type: 'confirm', + name: 'write', + message: `Do you want to write this config file for ${pkg.root + ? c.blue('the root directory') + : c.blue(pkg.name) }?\n\n${cardinal.highlight(pkg.configFile.content)}`, - initial: true, - }); + initial: true, + })).write; stdout.write(ansi.erase.lines(pkg.configFile.content.split('\n').length + 2)); - await fileHandler.write(pkg.configFile.path, pkg.configFile.content); + if (shouldWrite) await fileHandler.write(pkg.configFile.path, pkg.configFile.content); } From 4792b485d6eb10cb955d6a91d32e1f8c80ddbb40 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:02:47 -0300 Subject: [PATCH 26/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20remove?= =?UTF-8?q?=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/configsProcessor.js | 34 +--------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configsProcessor.js index 9cd3ab6..e9d1767 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configsProcessor.js @@ -4,7 +4,6 @@ import glob from 'picomatch'; import prompts from 'prompts'; import c from 'picocolors'; import str from './lib/str.js'; -import notNull from './lib/notNull.js'; export default class ConfigsProcessor { /** @type {string} */ @@ -124,7 +123,7 @@ export default class ConfigsProcessor { /** @type {Record<'packages', import('./types').Package[]>} */ const selected = await prompts({ name: 'packages', - type: 'multiselect', + type: 'autocompleteMultiselect', message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`, choices: packagesOptions, min: 1, @@ -165,36 +164,5 @@ export default class ConfigsProcessor { return pkgConfig; } - /** - * @param {import('./types').Package[]} packages Packages to generate the map from - * @returns {import('./types').PackagesConfigsMap} A map of what packages has some configuration - */ - generateConfigMap(packages) { - - /** @type {import('./types').PackagesConfigsMap} */ - const configMap = new Map(); - - for (const pkg of packages) { - - notNull(pkg.config).forEach((options, key) => { - /** @type {Map} */ - const optionsMap = configMap.get(key) ?? new Map(); - - options.forEach(option => { - const paths = optionsMap.get(option) ?? []; - optionsMap.set(option, [pkg.path, ...paths]); - - if (paths.length >= packages.length - 2 || paths.includes(this.dir)) { - optionsMap.set(option, [this.dir]); - } - }); - - configMap.set(key, optionsMap); - }); - } - - return configMap; - - } } From 13e517964cc6f25af9894f2effaaef1a2fb1e1dc Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:03:22 -0300 Subject: [PATCH 27/29] =?UTF-8?q?fix(deps):=20=F0=9F=90=9B=20update=20lock?= =?UTF-8?q?=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c874401..c1969bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: packages/cli: dependencies: + cardinal: + specifier: ^2.1.1 + version: 2.1.1 commander: specifier: ^11.0.0 version: 11.0.0 @@ -117,6 +120,9 @@ importers: recast: specifier: ^0.23.3 version: 0.23.3 + sisteransi: + specifier: ^1.0.5 + version: 1.0.5 yaml: specifier: ^2.3.1 version: 2.3.1 @@ -1215,6 +1221,10 @@ packages: dependencies: color-convert: 2.0.1 + /ansicolors@0.3.2: + resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} + dev: false + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1372,6 +1382,14 @@ packages: engines: {node: '>=6'} dev: true + /cardinal@2.1.1: + resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} + hasBin: true + dependencies: + ansicolors: 0.3.2 + redeyed: 2.1.1 + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3076,6 +3094,12 @@ packages: strip-indent: 3.0.0 dev: true + /redeyed@2.1.1: + resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} + dependencies: + esprima: 4.0.1 + dev: false + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: true From a830ec71bdf6127fd0a2fdc21015524c1c09c9a5 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:53:47 -0300 Subject: [PATCH 28/29] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20complete?= =?UTF-8?q?ly=20refactor=20the=20ast=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/configsFile.js | 317 +++++++++++++++---------------- packages/cli/src/lib/astUtils.js | 180 ++++++++++++++++++ 2 files changed, 335 insertions(+), 162 deletions(-) create mode 100644 packages/cli/src/lib/astUtils.js diff --git a/packages/cli/src/configsFile.js b/packages/cli/src/configsFile.js index 47515d7..e800c26 100644 --- a/packages/cli/src/configsFile.js +++ b/packages/cli/src/configsFile.js @@ -3,6 +3,7 @@ import notNull from './lib/notNull.js'; import * as recast from 'recast'; import fs from 'node:fs/promises'; import { existsSync } from 'node:fs'; +import astUtils from './lib/astUtils.js'; /** * @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2 @@ -24,7 +25,10 @@ function mergeImportsMaps(map1, map2) { */ switch ([typeof imports1 === 'string', typeof imports2 === 'string'].join(',')) { case 'true,true': - map1.set(key, imports1); + if (imports1.toString() === imports2.toString()) + map1.set(key, value); + else + map1.set(key, [['default', imports1.toString()], ['default', imports2.toString()]]); break; case 'true,false': map1.set(key, [['default', imports1.toString()], ...imports2]); @@ -36,7 +40,8 @@ function mergeImportsMaps(map1, map2) { map1.set(key, [...imports1, ...imports2]); break; } - map1.set(key, [...new Set(map1.get(key))]); + if (typeof map1.get(key) !== 'string') + map1.set(key, [...new Set(map1.get(key))]); } return map1; } @@ -138,139 +143,165 @@ export default class ConfigsWriter { */ /** - * @param {import('./types').ConfigFile['configs']} configs The configs objects defined in the config file - * @returns {(import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression)[]} - * The ast expressions nodes to be printed + * @typedef {import('estree').Program} Program + * + * @typedef {( + * import('./lib/astUtils.js').ExpressionOrIdentifier | + * import('estree').ObjectExpression | + * import('estree').SpreadElement + * )} ConfigArrayElement + */ + + /** + * @param {Program} ast The program ast to be manipulated + * @returns {Promise} The final ast with the recreated default export * * @private */ - createConfigExpressions(configs) { - return configs - .map(c => { - /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const e = recast.parse(c).program.body[0].expression; return e; - }) - .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)); + async addDefaultExport(ast) { + + /** @type {{program: Program}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const { program: exportTemplateAst } = recast.parse([ + '/** @type {import(\'eslint\').Linter.FlatConfig[]} */', + 'export default [', + '', + '];', + ].join('\n'), { parser: (await import('recast/parsers/babel.js')) }); + /** @type {import('estree').ExportDefaultDeclaration} */ + // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded + const exportTemplateNode = exportTemplateAst.body.find(n => n.type === 'ExportDefaultDeclaration'); + + /** @type {import('estree').ExportDefaultDeclaration | undefined} */ + // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded + let astExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); + if (!astExport) { ast.body.push(exportTemplateNode); return ast; } + + /** @type {import('estree').VariableDeclaration | undefined} */ + const oldExportValue = astExport.declaration.type !== 'ArrayExpression' + // @ts-expect-error astExport.declaration is a expression + ? astUtils.createVariable('oldConfig', 'const', astExport.declaration) + : undefined; + + if (!oldExportValue) return ast; + + // @ts-expect-error declaration is a ArrayExpression + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + exportTemplateNode.declaration.elements.push({ + type: 'SpreadElement', + argument: { type: 'Identifier', name: 'oldConfig' }, + }); + + const astExportIdx = ast.body.indexOf(astExport); + ast.body[astExportIdx] = exportTemplateNode; + ast.body.splice(astExportIdx - 1, 0, oldExportValue); + + return ast; + } /** - * @param {import('./types').ConfigFile['presets']} presets The presets objects defined in the config file - * @returns {import('estree').SpreadElement[]} The ast expressions nodes to be printed + * @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object + * @returns {import('estree').ObjectExpression} The object containing the spread rules * * @private */ - createPresetExpressions(presets) { - return presets - .map(p => { - /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const e = recast.parse(p).program.body[0].expression; return e; - }) - .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)) - .map(e => { - /** @type {import('estree').SpreadElement} */ - const spreadElement = { - type: 'SpreadElement', - argument: e, - }; - return spreadElement; - }); - } - - /** - * @param {import('./types').ConfigFile['rules']} rules The rules objects defined in the config file - * @returns {import('estree').ObjectExpression} The ast object expression nodes to be printed - * - * @private - */ - createRulesExpression(rules) { - /** @type {import('estree').ObjectExpression} */ - const rulesObjectExpression = rules + createRulesObject(rules) { + /** @type {import('estree').SpreadElement[]} */ + // @ts-expect-error The array is filtered to remove undefined's + const expressions = rules .map(r => { - /** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const e = recast.parse(r).program.body[0].expression; return e; - }) - .filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type)) - .map(e => { - /** @type {import('estree').SpreadElement} */ - const spreadElement = { - type: 'SpreadElement', - argument: e, - }; - return spreadElement; - }).reduce((acc, spreadElement) => { - acc.properties.push(spreadElement); - return acc; - }, { - /** @type {import('estree').ObjectExpression['type']} */ - type: 'ObjectExpression', - /** @type {import('estree').ObjectExpression['properties']} */ - properties: [], - }); - /** @type {import('estree').ObjectExpression} */ - const rulesExpression = { + const e = astUtils.stringToExpression(r); + if (e) return astUtils.toSpreadElement(e); + else undefined; + }).filter(e => e); + + return { type: 'ObjectExpression', properties: [{ // @ts-expect-error because ObjectProperty doesn't exist in estree types type: 'ObjectProperty', key: { type: 'Identifier', name: 'rules' }, - value: rulesObjectExpression, + value: { + type: 'ObjectExpression', + properties: expressions, + }, }], }; - return rulesExpression; + } /** - * @param {import('./types').ConfigFile['imports']} imports The import map to be used to create the nodes - * @returns {import('estree').ImportDeclaration[]} The ImportDeclaration nodes + * Adds elements to the default export node, without adding duplicates + * + * @typedef {import('estree').ArrayExpression} ArrayExpression + * + * @param {Program} ast The program ast to be manipulated + * @param {ConfigArrayElement[]} elements The elements to be added to the array + * @returns {Program} The final ast with the recreated default export * * @private */ - createImportDeclarations(imports) { - /** @type {import('estree').ImportDeclaration[]} */ - const importsDeclarations = []; - for (const [pkgName, values] of imports) { - /** @type {import('estree').ImportDeclaration} */ - const declaration = { - type: 'ImportDeclaration', - specifiers: [], - source: { - type: 'Literal', - value: pkgName, - }, - }; + addElementsToExport(ast, elements) { + /** @type {import('estree').ExportDefaultDeclaration} */ + // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded + const exportNode = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); + const exportNodeIdx = ast.body.indexOf(exportNode); - if (typeof values === 'string') { - declaration.specifiers.push({ - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: values }, + /** @type {ArrayExpression} */ + // @ts-expect-error declaration is a ArrayExpression + const array = exportNode.declaration; + + for (const e of elements) { + if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue; + array.elements.push(e); + } + + exportNode.declaration = array; + ast.body[exportNodeIdx] = exportNode; + + return ast; + } + + /** + * @param {Program} ast The program ast to be manipulated + * @param {import('./types').ConfigFile['imports']} imports The imports map to be used + * @returns {Program} The final ast with the recreated default export + */ + addPackageImports(ast, imports) { + + /** @type {import('estree').ImportDeclaration[]} */ + const importDeclarations = []; + + console.log(imports); + + for (const [pkgName, specifiers] of imports) { + /** @type {import('estree').ImportDeclaration | undefined} */ + // @ts-expect-error type error, the specifier has to be ImportDeclaration to be founded + const existingDeclaration = ast.body.find(s => s.type === 'ImportDeclaration' && s.source.value === pkgName); + + const importDeclaration = astUtils.createImportDeclaration( + pkgName, typeof specifiers === 'string' ? specifiers : undefined, existingDeclaration, + ); + + if (typeof specifiers !== 'string') { + specifiers.forEach(s => { + if (typeof s === 'string') return importDeclaration.addSpecifier(s); + else return importDeclaration.addSpecifier(s[0], s[1]); }); - importsDeclarations.push(declaration); - continue; } - declaration.specifiers = values.map(v => { - /** @type {import('estree').ImportSpecifier} */ - const specifier = { - type: 'ImportSpecifier', - imported: { - type: 'Identifier', - name: typeof v === 'string' ? v : v[0], - }, - local: { - type: 'Identifier', - name: typeof v === 'string' ? v : v[1], - }, - }; - return specifier; - }); - importsDeclarations.push(declaration); + if (existingDeclaration) ast.body[ast.body.indexOf(existingDeclaration)] = importDeclaration.body; + else importDeclarations.push(importDeclaration.body); + } - return importsDeclarations; + + ast.body.unshift(...importDeclarations); + + return ast; } + /** * @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file * @returns {Promise} The generated config file contents @@ -279,70 +310,32 @@ export default class ConfigsWriter { const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : ''; - /** @type {import('estree').ExportDefaultDeclaration}} */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - const defaultExportTemplate = recast.parse([ - '/** @type {import(\'eslint\').Linter.FlatConfig[]} */', - 'export default [', - '', - '];', - ].join('\n'), { - parser: (await import('recast/parsers/babel.js')), - }).program.body.find((/** @type {{ type: string; }} */ n) => n.type === 'ExportDefaultDeclaration'); - - - /** @type {{program: import('estree').Program}} */ + /** @type {{program: Program}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) }); - /** @type {import('estree').ExportDefaultDeclaration | undefined} */ - // @ts-expect-error because the type to find has to be ExportDefaultDeclaration - let defaultExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); - if (!defaultExport) { - defaultExport = defaultExportTemplate; - } - else if (defaultExport.declaration.type !== 'ArrayExpression') { - ast.body.push({ - type: 'VariableDeclaration', - kind: 'const', - declarations: [{ - type: 'VariableDeclarator', - id: { type: 'Identifier', name: 'oldConfig' }, - // @ts-expect-error because defaultExport's declaration is a ArrayExpression - init: defaultExport.declaration, - }], - }); - defaultExport = defaultExportTemplate; - // @ts-expect-error because defaultExport's declaration is a ArrayExpression - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - defaultExport.declaration.elements.push({ - type: 'SpreadElement', - argument: { type: 'Identifier', name: 'oldConfig' }, - }); - } + await this.addDefaultExport(ast); - if (config.imports.size > 0) - ast.body.unshift(...this.createImportDeclarations(config.imports)); + /** + * @type {ConfigArrayElement[]} + */ + // @ts-expect-error The array is filtered to remove undefined's + const elements = [ + ...config.configs.map(c => astUtils.stringToExpression(c)), + ...config.presets.map(p => { + const e = astUtils.stringToExpression(p); + if (e) return astUtils.toSpreadElement(e); + else undefined; + }), + config.rules.length > 0 + ? this.createRulesObject(config.rules) + : undefined, + ].filter(e => e); - const elementsExpressions = []; - - if (config.presets.length > 0) - elementsExpressions.push(...this.createPresetExpressions(config.presets)); - if (config.configs.length > 0) - elementsExpressions.push(...this.createConfigExpressions(config.configs)); - if (config.rules.length > 0) - elementsExpressions.push(this.createRulesExpression(config.rules)); - - // @ts-expect-error because defaultExport's declaration is a ArrayExpression - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - defaultExport.declaration.elements.push(...elementsExpressions); - - const idx = ast.body.findIndex(n => n.type === 'ExportDefaultDeclaration'); - if (idx > -1) ast.body.splice(idx, 1); - ast.body.push(defaultExport); + this.addElementsToExport(ast, elements); + this.addPackageImports(ast, config.imports); const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code; - return finalCode; } diff --git a/packages/cli/src/lib/astUtils.js b/packages/cli/src/lib/astUtils.js new file mode 100644 index 0000000..a008f43 --- /dev/null +++ b/packages/cli/src/lib/astUtils.js @@ -0,0 +1,180 @@ +import * as recast from 'recast'; + +/** + * @typedef {( + * import('estree').MemberExpression | + * import('estree').Identifier | + * import('estree').CallExpression | + * import('estree').NewExpression + * )} ExpressionOrIdentifier + * This type only includes the expressions used in the cli's config type + * + * @typedef {import('estree').VariableDeclaration} VariableDeclaration + * + * @typedef {import('estree').Identifier['name']} IdentifierName + * + * @typedef {VariableDeclaration['kind']} VariableKind + * + * @typedef {import('estree').VariableDeclarator['init']} VariableInit + * + * @typedef {import('estree').SpreadElement} SpreadElement + * + * @typedef {import('estree').Expression} Expression + * + * @typedef {import('estree').ArrayExpression} ArrayExpression + */ + +/** + * @param {IdentifierName} identifier Nave of the variable identifier + * @param {VariableKind} [kind] Type of variable declaration + * @param {VariableInit} [init] Initial value of the variable + * @returns {VariableDeclaration} The variable declaration ast node object + */ +export function createVariable(identifier, kind = 'const', init) { + return { + type: 'VariableDeclaration', + kind, + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: identifier }, + init, + }], + }; +} + +/** + * @param {string} string The expression in string + * @returns {ExpressionOrIdentifier | undefined} The expression or identifier node of that string (undefined if string is not a expression) + */ +export function stringToExpression(string) { + /** @type {ExpressionOrIdentifier} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const e = recast.parse(string).program.body[0].expression; + if (['MemberExpression', 'Identifier', 'CallExpression', 'NewExpression'].includes(e.type)) return e; + else return undefined; +} + +/** + * @param {ArrayExpression} array The array node to search trough + * @param {ExpressionOrIdentifier | SpreadElement} element The element to be search + * @returns {ExpressionOrIdentifier | undefined} The element of the array founded, undefined if it isn't found + */ +export function findInArray(array, element) { + + /** @type {ExpressionOrIdentifier[]} */ + // @ts-expect-error The array should have just tge type above + element = element.type === 'SpreadElement' ? element.argument : element; + + /** @type {ExpressionOrIdentifier[]} */ + // @ts-expect-error The array is filtered to have the type above + const filteredElements = array.elements + .map(n => { + if (n?.type === 'SpreadElement') return n.argument; + return n; + }).filter(n => n && n.type === element.type); + + const toStringElements = filteredElements.map(n => recast.print(n).code); + const toStringElement = recast.print(element).code; + + const idx = toStringElements.findIndex(e => e === toStringElement); + return filteredElements[idx]; +} + +/** + * @param {ExpressionOrIdentifier} expression The expression to be spread + * @returns {SpreadElement} The spread element node + */ +export function toSpreadElement(expression) { + return { + type: 'SpreadElement', + argument: expression, + }; +} + +/** + * @typedef {{ + * body: import('estree').ImportDeclaration + * addSpecifier: (specifier: string, alias?: string) => ThisType + * convertDefaultSpecifier: () => ThisType + * }} ImportDeclarationHelper + * + * @param {string} source The package name or source path to be imported + * @param {string} [defaultImported] The default specifier imported + * @param {import('estree').ImportDeclaration} [body] The body of the import declaration to start with + * @returns {ImportDeclarationHelper} A helper object for manipulating the import declaration + */ +export function createImportDeclaration(source, defaultImported, body) { + const helper = { + /** @type {import('estree').ImportDeclaration} */ + body: body ?? { + type: 'ImportDeclaration', + specifiers: defaultImported ? [{ + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: defaultImported }, + }] : [], + source: { + type: 'Literal', + value: source, + }, + }, + /** + * Converts a default specifier to a specifier with a alias. + * + * @example + * import eslit from 'eslit'; + * // Is converted to + * import { default as eslit } from 'eslit'; + * + * @returns {ThisType} This helper with the converted default specifier + */ + convertDefaultSpecifier() { + const specifier = this.body.specifiers.find(s => s.type === 'ImportDefaultSpecifier'); + if (!specifier) + return this; + + this.body.specifiers.splice( + this.body.specifiers.indexOf(specifier), + 1, + ); + return this.addSpecifier('default', specifier.local.name); + }, + /** + * @param {string} specifier The value to be imported from the package + * @param {string} [alias] The local alias of the value + * @returns {ThisType} This helper with the added specifiers + */ + addSpecifier(specifier, alias) { + this.convertDefaultSpecifier(); + if (this.body.specifiers.find(s => s.local.name === alias || s.local.name === specifier)) + return this; + + this.body.specifiers.push({ + type: 'ImportSpecifier', + imported: { + type: 'Identifier', + name: specifier, + }, + local: { + type: 'Identifier', + name: alias ?? specifier, + }, + }); + + return this; + }, + }; + + if (defaultImported && body && !body.specifiers.find(s => s.type === 'ImportDefaultSpecifier' && s.local.name === defaultImported)) { + helper.addSpecifier('default', defaultImported); + } + return helper; + +} + +export default { + createVariable, + stringToExpression, + toSpreadElement, + findInArray, + createImportDeclaration, +}; From 64dc504e2aded8f03d377719173143329102fe13 Mon Sep 17 00:00:00 2001 From: Guz013 <43732358+Guz013@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:54:15 -0300 Subject: [PATCH 29/29] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20install=20packag?= =?UTF-8?q?es=20after=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/cli.js | 41 +++++- packages/cli/src/configs.js | 6 +- packages/cli/src/configsFile.js | 8 - packages/cli/src/lib/astUtils.js | 10 -- packages/cli/src/lib/notNull.js | 6 - packages/cli/src/packageInstaller.js | 210 +++++++++++++++++++++++++++ packages/cli/src/types.d.ts | 7 + packages/cli/src/workspace.js | 1 - packages/config/src/configs/jsdoc.js | 11 -- 9 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 packages/cli/src/packageInstaller.js diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 63ca5ff..990923b 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -10,6 +10,8 @@ import prompts from 'prompts'; import ConfigsFile from './configsFile.js'; import * as cardinal from 'cardinal'; import ansi from 'sisteransi'; +import PackageInstaller from './packageInstaller.js'; +import notNull from './lib/notNull.js'; const stdout = process.stdout; @@ -28,8 +30,9 @@ export default class Cli { constructor(args) { this.#program .option('--packages ') - .option('--merge-to-root') .option('--dir ', undefined) + .option('--merge-to-root') + .option('--install-pkgs') .parse(); this.args = { @@ -46,6 +49,8 @@ export default class Cli { async run() { + process.chdir(this.args.dir); + const spinner = createSpinner('Detecting workspace configuration'); const processor = new ConfigsProcessor({ configs }); @@ -110,6 +115,40 @@ export default class Cli { } + const packagesMap = new Map(packages.map(p => [p.path, [...notNull(p.configFile).imports.keys()]])); + const installer = new PackageInstaller(packagesMap, packages.find(p => p.root === true)?.path ?? this.args.dir); + + /** @type {boolean | 'changePackage'} */ + let installPkgs = this.args.installPkgs !== undefined ? true : + /** @type {{install: boolean | 'changePackage'}} */ + (await prompts({ + name: 'install', + message: + `Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?`, + choices: [ + { title: 'Yes, install all packages', value: true, description: installer.packageManager.description }, + { title: 'No, I will install them manually', value: false }, + { title: 'Change package manager', value: 'changePackage' }, + ], + type: 'select', + })).install; + + if (installPkgs === 'changePackage') { + /** @type {{manager: import('./types').PackageManagerName}} */ + const prompt = await prompts({ + name: 'manager', + message: 'What package manager do you want ESLit to use?', + choices: Object.values(installer.packageManagers).map(m => { + return { title: m.name, description: m.description, value: m.name }; + }), + type: 'select', + }); + installer.packageManager = installer.packageManagers[prompt.manager]; + installPkgs = true; + } + + if (installPkgs) await installer.install(); + } } diff --git a/packages/cli/src/configs.js b/packages/cli/src/configs.js index 44d948a..7f40b1b 100644 --- a/packages/cli/src/configs.js +++ b/packages/cli/src/configs.js @@ -8,13 +8,13 @@ export default [ options: [ { name: 'svelte', - packages: { '@eslit/svelte': 'svelte' }, + packages: { 'svelte': 'svelte' }, configs: ['svelte.recommended'], detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'], }, { name: 'vue', - packages: { '@eslit/vue': ['vue', ['hello', 'world']], '@eslit/svelte': ['hello'] }, + packages: { 'vue': ['vue', ['hello', 'world']], 'svelte': ['hello'] }, configs: ['vue.recommended'], detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'], }, @@ -26,7 +26,7 @@ export default [ manual: true, options: [{ name: 'yes', - packages: { '@eslit/config': 'config', '@eslit/vue': ['test1'] }, + packages: { 'eslint': 'config', 'svelte': ['test1'] }, configs: ['config.strict'], }], }, diff --git a/packages/cli/src/configsFile.js b/packages/cli/src/configsFile.js index e800c26..152a05f 100644 --- a/packages/cli/src/configsFile.js +++ b/packages/cli/src/configsFile.js @@ -144,7 +144,6 @@ export default class ConfigsWriter { /** * @typedef {import('estree').Program} Program - * * @typedef {( * import('./lib/astUtils.js').ExpressionOrIdentifier | * import('estree').ObjectExpression | @@ -155,7 +154,6 @@ export default class ConfigsWriter { /** * @param {Program} ast The program ast to be manipulated * @returns {Promise} The final ast with the recreated default export - * * @private */ async addDefaultExport(ast) { @@ -203,7 +201,6 @@ export default class ConfigsWriter { /** * @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object * @returns {import('estree').ObjectExpression} The object containing the spread rules - * * @private */ createRulesObject(rules) { @@ -233,13 +230,10 @@ export default class ConfigsWriter { /** * Adds elements to the default export node, without adding duplicates - * * @typedef {import('estree').ArrayExpression} ArrayExpression - * * @param {Program} ast The program ast to be manipulated * @param {ConfigArrayElement[]} elements The elements to be added to the array * @returns {Program} The final ast with the recreated default export - * * @private */ addElementsToExport(ast, elements) { @@ -273,8 +267,6 @@ export default class ConfigsWriter { /** @type {import('estree').ImportDeclaration[]} */ const importDeclarations = []; - console.log(imports); - for (const [pkgName, specifiers] of imports) { /** @type {import('estree').ImportDeclaration | undefined} */ // @ts-expect-error type error, the specifier has to be ImportDeclaration to be founded diff --git a/packages/cli/src/lib/astUtils.js b/packages/cli/src/lib/astUtils.js index a008f43..824d48d 100644 --- a/packages/cli/src/lib/astUtils.js +++ b/packages/cli/src/lib/astUtils.js @@ -8,19 +8,12 @@ import * as recast from 'recast'; * import('estree').NewExpression * )} ExpressionOrIdentifier * This type only includes the expressions used in the cli's config type - * * @typedef {import('estree').VariableDeclaration} VariableDeclaration - * * @typedef {import('estree').Identifier['name']} IdentifierName - * * @typedef {VariableDeclaration['kind']} VariableKind - * * @typedef {import('estree').VariableDeclarator['init']} VariableInit - * * @typedef {import('estree').SpreadElement} SpreadElement - * * @typedef {import('estree').Expression} Expression - * * @typedef {import('estree').ArrayExpression} ArrayExpression */ @@ -97,7 +90,6 @@ export function toSpreadElement(expression) { * addSpecifier: (specifier: string, alias?: string) => ThisType * convertDefaultSpecifier: () => ThisType * }} ImportDeclarationHelper - * * @param {string} source The package name or source path to be imported * @param {string} [defaultImported] The default specifier imported * @param {import('estree').ImportDeclaration} [body] The body of the import declaration to start with @@ -119,12 +111,10 @@ export function createImportDeclaration(source, defaultImported, body) { }, /** * Converts a default specifier to a specifier with a alias. - * * @example * import eslit from 'eslit'; * // Is converted to * import { default as eslit } from 'eslit'; - * * @returns {ThisType} This helper with the converted default specifier */ convertDefaultSpecifier() { diff --git a/packages/cli/src/lib/notNull.js b/packages/cli/src/lib/notNull.js index cca1b49..b58aa98 100644 --- a/packages/cli/src/lib/notNull.js +++ b/packages/cli/src/lib/notNull.js @@ -1,17 +1,11 @@ /** * JSDoc types lack a non-null assertion. - * * @template T - * * @param {T} value The value which to assert against null or undefined * @returns {NonNullable} The said value - * * @throws {TypeError} If the value is unexpectedly null or undefined - * * @author Jimmy Wärting - https://github.com/jimmywarting - * * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031 - * * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966 */ export default function notNull(value) { diff --git a/packages/cli/src/packageInstaller.js b/packages/cli/src/packageInstaller.js new file mode 100644 index 0000000..4d9ca9b --- /dev/null +++ b/packages/cli/src/packageInstaller.js @@ -0,0 +1,210 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { exec } from 'node:child_process'; +import { createSpinner } from 'nanospinner'; +import c from 'picocolors'; +import * as recast from 'recast'; +import { readFile, writeFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; + + +/** + * @type {import('./types').PackageManagerHandler} + */ +class CommandHandler { + + /** @type {string} */ + command; + + /** @type {((path: string, packages: string[]) => string | Promise) | undefined} */ + checker; + + /** + * @param {string} command What command to use to install + * @param {(path: string, packages: string[]) => string | Promise} [checker] Checks if a argument should be passed + */ + constructor(command, checker) { + this.command = command; + this.checker = checker; + } + + /** + * @param {string} path The path to run the command + * @param {string[]} packages The packages to be added on the command + * @returns {Promise} + */ + async install(path, packages) { + + if (this.checker) + this.command += await this.checker(path, packages); + + return new Promise((res) => { + const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start(); + try { + const child = exec(`${this.command} ${packages.join(' ')}`, { cwd: path }); + child.stdout?.on('data', (chunk) => spinner.update({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + text: `Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}\n ${c.dim(chunk)}`, + })); + child.stdout?.on('close', () => { + spinner.success({ + text: `Installed packages with ${c.green(this.command)}`, + }); res(); + }); + } + catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`)); + } + }); + } +} + +/** + * @type {import('./types').PackageManagerHandler} + */ +class DenoHandler { + + /** + * @param {string} path The path to run the command + * @param {string[]} packages The packages to be added on the command + * @returns {Promise} + */ + async install(path, packages) { + const configPath = join(path, 'eslint.config.js'); + + if (!existsSync(configPath)) return; + + const configFile = await readFile(configPath, 'utf8'); + /** @type {{program: import('estree').Program}}*/ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { program: ast } = recast.parse(configFile, { parser: (await import('recast/parsers/babel.js')) }); + + ast.body.map((node) => { + if (node.type !== 'ImportDeclaration') return node; + + if (packages.includes(node.source.value?.toString() ?? '')) { + node.source.value = `npm:${node.source.value}`; + } + return node; + }); + + await writeFile(configPath, recast.prettyPrint(ast).code, 'utf-8'); + + console.log(c.green('Added npm: specifier to dependencies')); + + } + +} + +export default class PackageInstaller { + + /** + * @typedef {Map} PackagesMap + * @type {PackagesMap} + */ + packagesMap; + + /** + * @typedef {{ + * name: import('./types').PackageManagerName + * description: string + * handler: import('./types').PackageManagerHandler + * }} PackageManager + * @type {PackageManager} + */ + packageManager; + + /** + * @type {Record} + */ + packageManagers = { + deno: { + name: 'deno', + description: 'Adds npm: specifiers to the eslint.config.js file', + handler: new DenoHandler(), + }, + bun: { + name: 'bun', + description: 'Uses bun install', + handler: new CommandHandler('bun install'), + }, + pnpm: { + name: 'pnpm', + description: 'Uses pnpm install', + handler: new CommandHandler('pnpm install --save-dev', (path) => { + if (existsSync(join(path, 'pnpm-workspace.yaml')) && existsSync(join(path, 'package.json'))) + return ' -w'; + else return ''; + }), + }, + yarn: { + name: 'yarn', + description: 'Uses yarn add', + handler: new CommandHandler('yarn add --dev'), + }, + npm: { + name: 'npm', + description: 'Uses npm install', + handler: new CommandHandler('npm install --save-dev'), + }, + }; + + /** + * @param {PackagesMap} packagesMap The map of directories and packages to be installed + * @param {string} root Root directory path + */ + constructor(packagesMap, root) { + this.packagesMap = packagesMap; + this.packageManager = this.detectPackageManager(root); + } + + /** + * @param {string} root Root directory path + * @returns {PackageManager} The package manager detected; + * @private + */ + detectPackageManager(root) { + /** @type {(...path: string[]) => boolean} */ + const exists = (...path) => existsSync(join(root, ...path)); + + switch (true) { + case exists('deno.json'): + case exists('deno.jsonc'): + return this.packageManagers.deno; + + case exists('bun.lockb'): + return this.packageManagers.bun; + + case exists('pnpm-lock.yaml'): + return this.packageManagers.pnpm; + + case exists('yarn.lock'): + return this.packageManagers.yarn; + + case exists('package-lock.json'): + return this.packageManagers.npm; + + case exists('package.json'): + /** @type {{packageManager?: string}} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-case-declarations + const { packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); + if (!packageManager) return this.packageManagers.npm; + + if (packageManager.includes('pnpm')) return this.packageManagers.pnpm; + if (packageManager.includes('yarn')) return this.packageManagers.yarn; + if (packageManager.includes('npm')) return this.packageManagers.npm; + + else return this.packageManagers.npm; + + default: return this.packageManagers.npm; + } + } + + async install() { + for (const [path, packages] of this.packagesMap) { + await this.packageManager.handler.install(path, packages); + } + } + +} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index eced0d0..e784a55 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -1,8 +1,11 @@ import type { OptionValues } from 'commander'; +export type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'; + export type CliArgs = { packages?: string[] mergeToRoot?: boolean + installPkgs?: boolean | PackageManagerName dir: string } & OptionValues; @@ -52,3 +55,7 @@ export interface ConfigFile { rules: string[] content?: string } + +export interface PackageManagerHandler { + install(path: string, packages: string[]): Promise | void +} diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js index 3a1e1e9..f4fd0e0 100644 --- a/packages/cli/src/workspace.js +++ b/packages/cli/src/workspace.js @@ -8,7 +8,6 @@ import picomatch from 'picomatch'; /** * @template T - * * @param {Promise} promise - The async function to try running * @returns {Promise} - Returns the result of the async function, or null if it errors */ diff --git a/packages/config/src/configs/jsdoc.js b/packages/config/src/configs/jsdoc.js index 9ffe6e2..f3169f6 100644 --- a/packages/config/src/configs/jsdoc.js +++ b/packages/config/src/configs/jsdoc.js @@ -2,7 +2,6 @@ import jsdoc from 'eslint-plugin-jsdoc'; /** * JSDoc rules overrides - * * @type {Readonly} */ const config = { @@ -11,16 +10,6 @@ const config = { rules: { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...jsdoc.configs['recommended-typescript-flavor-error'].rules, - - 'jsdoc/tag-lines': ['error', 'always', { - count: 1, - applyToEndTag: false, - startLines: 1, - endLines: 0, - tags: { - param: { lines: 'never' }, - }, - }], }, }; export default config;