feat(cli): project structure detection

This commit is contained in:
Guz013
2023-07-31 19:01:59 -03:00
parent 6f1fca2513
commit 41fd41bef6
5 changed files with 356 additions and 6 deletions

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./node_modules/**", "./dist/**"],
"include": ["./index.d.ts", "./src/**/*.ts", "./src/**/*.js"],
}

41
packages/cli/package.json Normal file
View File

@@ -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"
}
}

225
packages/cli/src/index.js Executable file
View File

@@ -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<T>} promise - The async function to try running
* @returns {Promise<T | null>} - 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<string[]>} - 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<string>} - 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<import('./types').Package>} - 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<string[]>} - 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<import('./types').Workspace>}
* 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();

17
packages/cli/src/types.d.ts vendored Normal file
View File

@@ -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[]
}

74
pnpm-lock.yaml generated
View File

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