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] =?UTF-8?q?feat(cli):=20=E2=9C=A8=20refactoring=20and=20de?= =?UTF-8?q?tection=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 }