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==}