@akanjs/devkit 0.0.143 → 0.0.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Logger } from "@akanjs/common";
1
+ import { capitalize, Logger } from "@akanjs/common";
2
2
  import {
3
3
  getAppConfig,
4
4
  getDefaultFileScan,
@@ -11,7 +11,9 @@ import fs from "fs";
11
11
  import fsPromise from "fs/promises";
12
12
  import path from "path";
13
13
  import { TypeScriptDependencyScanner } from "./dependencyScanner";
14
+ import { Linter } from "./linter";
14
15
  import { Spinner } from "./spinner";
16
+ import { TypeChecker } from "./typeChecker";
15
17
  const execEmoji = {
16
18
  workspace: "\u{1F3E0}",
17
19
  app: "\u{1F680}",
@@ -31,6 +33,8 @@ class Executor {
31
33
  logger;
32
34
  cwdPath;
33
35
  emoji = execEmoji.default;
36
+ typeChecker = null;
37
+ linter = null;
34
38
  constructor(name, cwdPath) {
35
39
  this.name = name;
36
40
  this.logger = new Logger(name);
@@ -65,28 +69,32 @@ class Executor {
65
69
  spawn(command, args = [], options = {}) {
66
70
  const proc = spawn(command, args, {
67
71
  cwd: this.cwdPath,
68
- stdio: "inherit",
72
+ // stdio: "inherit",
69
73
  ...options
70
74
  });
75
+ let stdout = "";
76
+ proc.stdout?.on("data", (data) => {
77
+ stdout += data;
78
+ });
71
79
  proc.stdout?.on("data", (data) => {
72
80
  this.#stdout(data);
73
81
  });
74
82
  proc.stderr?.on("data", (data) => {
75
- this.#stderr(data);
83
+ this.#stdout(data);
76
84
  });
77
85
  return new Promise((resolve, reject) => {
78
86
  proc.on("exit", (code, signal) => {
79
87
  if (!!code || signal)
80
- reject({ code, signal });
88
+ reject({ code, signal, stdout });
81
89
  else
82
- resolve({ code, signal });
90
+ resolve(stdout);
83
91
  });
84
92
  });
85
93
  }
86
94
  fork(modulePath, args = [], options = {}) {
87
95
  const proc = fork(modulePath, args, {
88
96
  cwd: this.cwdPath,
89
- stdio: ["ignore", "inherit", "inherit", "ipc"],
97
+ // stdio: ["ignore", "inherit", "inherit", "ipc"],
90
98
  ...options
91
99
  });
92
100
  proc.stdout?.on("data", (data) => {
@@ -104,45 +112,70 @@ class Executor {
104
112
  });
105
113
  });
106
114
  }
107
- #getPath(filePath) {
108
- return path.isAbsolute(filePath) ? filePath : `${this.cwdPath}/${filePath}`;
115
+ getPath(filePath) {
116
+ if (path.isAbsolute(filePath))
117
+ return filePath;
118
+ const baseParts = this.cwdPath.split("/").filter(Boolean);
119
+ const targetParts = filePath.split("/").filter(Boolean);
120
+ let overlapLength = 0;
121
+ for (let i = 1; i <= Math.min(baseParts.length, targetParts.length); i++) {
122
+ let isOverlap = true;
123
+ for (let j = 0; j < i; j++)
124
+ if (baseParts[baseParts.length - i + j] !== targetParts[j]) {
125
+ isOverlap = false;
126
+ break;
127
+ }
128
+ if (isOverlap)
129
+ overlapLength = i;
130
+ }
131
+ const result = overlapLength > 0 ? `/${[...baseParts, ...targetParts.slice(overlapLength)].join("/")}` : `${this.cwdPath}/${filePath}`;
132
+ return result.replace(/\/+/g, "/");
109
133
  }
110
134
  mkdir(dirPath) {
111
- const writePath = this.#getPath(dirPath);
135
+ const writePath = this.getPath(dirPath);
112
136
  if (!fs.existsSync(writePath))
113
137
  fs.mkdirSync(writePath, { recursive: true });
114
138
  this.logger.verbose(`Make directory ${writePath}`);
115
139
  return this;
116
140
  }
141
+ async readdir(dirPath) {
142
+ const readPath = this.getPath(dirPath);
143
+ try {
144
+ return await fsPromise.readdir(readPath);
145
+ } catch (error) {
146
+ return [];
147
+ }
148
+ }
117
149
  exists(filePath) {
118
- const readPath = this.#getPath(filePath);
150
+ const readPath = this.getPath(filePath);
119
151
  return fs.existsSync(readPath);
120
152
  }
121
153
  remove(filePath) {
122
- const readPath = this.#getPath(filePath);
154
+ const readPath = this.getPath(filePath);
123
155
  if (fs.existsSync(readPath))
124
156
  fs.unlinkSync(readPath);
125
157
  this.logger.verbose(`Remove file ${readPath}`);
126
158
  return this;
127
159
  }
128
160
  removeDir(dirPath) {
129
- const readPath = this.#getPath(dirPath);
161
+ const readPath = this.getPath(dirPath);
130
162
  if (fs.existsSync(readPath))
131
163
  fs.rmSync(readPath, { recursive: true });
132
164
  this.logger.verbose(`Remove directory ${readPath}`);
133
165
  return this;
134
166
  }
135
167
  writeFile(filePath, content, { overwrite = true } = {}) {
136
- const writePath = this.#getPath(filePath);
168
+ const writePath = this.getPath(filePath);
137
169
  const dir = path.dirname(writePath);
138
170
  if (!fs.existsSync(dir))
139
171
  fs.mkdirSync(dir, { recursive: true });
140
- const contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2);
172
+ let contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2);
141
173
  if (fs.existsSync(writePath)) {
142
174
  const currentContent = fs.readFileSync(writePath, "utf8");
143
- if (currentContent === contentStr || !overwrite)
175
+ if (currentContent === contentStr || !overwrite) {
144
176
  this.logger.verbose(`File ${writePath} is unchanged`);
145
- else {
177
+ contentStr = fs.readFileSync(writePath, "utf-8");
178
+ } else {
146
179
  fs.writeFileSync(writePath, contentStr, "utf8");
147
180
  this.logger.verbose(`File ${writePath} is changed`);
148
181
  }
@@ -150,28 +183,28 @@ class Executor {
150
183
  fs.writeFileSync(writePath, contentStr, "utf8");
151
184
  this.logger.verbose(`File ${writePath} is created`);
152
185
  }
153
- return this;
186
+ return { filePath: writePath, content: contentStr };
154
187
  }
155
188
  writeJson(filePath, content) {
156
189
  this.writeFile(filePath, JSON.stringify(content, null, 2) + "\n");
157
190
  return this;
158
191
  }
159
- getLocalFile(filePath) {
160
- const filepath = path.isAbsolute(filePath) ? filePath : filePath.replace(this.cwdPath, "");
161
- const content = this.readFile(filepath);
162
- return { filepath, content };
192
+ getLocalFile(targetPath) {
193
+ const filePath = path.isAbsolute(targetPath) ? targetPath : targetPath.replace(this.cwdPath, "");
194
+ const content = this.readFile(filePath);
195
+ return { filePath, content };
163
196
  }
164
197
  readFile(filePath) {
165
- const readPath = this.#getPath(filePath);
198
+ const readPath = this.getPath(filePath);
166
199
  return fs.readFileSync(readPath, "utf8");
167
200
  }
168
201
  readJson(filePath) {
169
- const readPath = this.#getPath(filePath);
202
+ const readPath = this.getPath(filePath);
170
203
  return JSON.parse(fs.readFileSync(readPath, "utf8"));
171
204
  }
172
205
  async cp(srcPath, destPath) {
173
- const src = this.#getPath(srcPath);
174
- const dest = this.#getPath(destPath);
206
+ const src = this.getPath(srcPath);
207
+ const dest = this.getPath(destPath);
175
208
  await fsPromise.cp(src, dest, { recursive: true });
176
209
  }
177
210
  log(msg) {
@@ -207,7 +240,7 @@ class Executor {
207
240
  const getContent = require(templatePath);
208
241
  const result = getContent.default(scanResult ?? null, dict);
209
242
  if (result === null)
210
- return;
243
+ return null;
211
244
  const filename = typeof result === "object" ? result.filename : path.basename(targetPath).replace(".js", ".ts");
212
245
  const content = typeof result === "object" ? result.content : result;
213
246
  const dirname = path.dirname(targetPath);
@@ -216,7 +249,7 @@ class Executor {
216
249
  `${dirname}/${filename}`
217
250
  );
218
251
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
219
- this.writeFile(convertedTargetPath, content, { overwrite });
252
+ return this.writeFile(convertedTargetPath, content, { overwrite });
220
253
  } else if (targetPath.endsWith(".template")) {
221
254
  const content = await fsPromise.readFile(templatePath, "utf8");
222
255
  const convertedTargetPath = Object.entries(dict).reduce(
@@ -228,10 +261,11 @@ class Executor {
228
261
  content
229
262
  );
230
263
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
231
- this.writeFile(convertedTargetPath, convertedContent, { overwrite });
232
- }
264
+ return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
265
+ } else
266
+ return null;
233
267
  }
234
- async applyTemplate({
268
+ async _applyTemplate({
235
269
  basePath,
236
270
  template,
237
271
  scanResult,
@@ -241,22 +275,24 @@ class Executor {
241
275
  const templatePath = `${__dirname}/src/templates${template ? `/${template}` : ""}`.replace(".ts", ".js");
242
276
  if (fs.statSync(templatePath).isFile()) {
243
277
  const filename = path.basename(templatePath);
244
- await this.#applyTemplateFile(
278
+ const fileContent = await this.#applyTemplateFile(
245
279
  { templatePath, targetPath: path.join(basePath, filename), scanResult, overwrite },
246
280
  dict
247
281
  );
282
+ return fileContent ? [fileContent] : [];
248
283
  } else {
249
- const subdirs = await fsPromise.readdir(templatePath);
250
- await Promise.all(
284
+ const subdirs = await this.readdir(templatePath);
285
+ const fileContents = (await Promise.all(
251
286
  subdirs.map(async (subdir) => {
252
287
  const subpath = path.join(templatePath, subdir);
253
- if (fs.statSync(subpath).isFile())
254
- await this.#applyTemplateFile(
288
+ if (fs.statSync(subpath).isFile()) {
289
+ const fileContent = await this.#applyTemplateFile(
255
290
  { templatePath: subpath, targetPath: path.join(basePath, subdir), scanResult, overwrite },
256
291
  dict
257
292
  );
258
- else
259
- await this.applyTemplate({
293
+ return fileContent ? [fileContent] : [];
294
+ } else
295
+ return await this._applyTemplate({
260
296
  basePath: path.join(basePath, subdir),
261
297
  template: path.join(template, subdir),
262
298
  scanResult,
@@ -264,9 +300,41 @@ class Executor {
264
300
  overwrite
265
301
  });
266
302
  })
267
- );
303
+ )).flat();
304
+ return fileContents;
268
305
  }
269
306
  }
307
+ async applyTemplate(options) {
308
+ const dict = {
309
+ ...options.dict ?? {},
310
+ ...Object.fromEntries(
311
+ Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)])
312
+ )
313
+ };
314
+ return this._applyTemplate({ ...options, dict });
315
+ }
316
+ getTypeChecker() {
317
+ this.typeChecker ??= new TypeChecker(this);
318
+ return this.typeChecker;
319
+ }
320
+ typeCheck(filePath) {
321
+ const path2 = this.getPath(filePath);
322
+ const typeChecker = this.getTypeChecker();
323
+ const { diagnostics, errors, warnings } = typeChecker.check(path2);
324
+ const message = typeChecker.formatDiagnostics(diagnostics);
325
+ return { diagnostics, errors, warnings, message };
326
+ }
327
+ getLinter() {
328
+ this.linter ??= new Linter(this.cwdPath);
329
+ return this.linter;
330
+ }
331
+ async lint(filePath, { fix = false, dryRun = false } = {}) {
332
+ const path2 = this.getPath(filePath);
333
+ const linter = this.getLinter();
334
+ const { results, errors, warnings } = await linter.lint(path2, { fix, dryRun });
335
+ const message = linter.formatLintResults(results);
336
+ return { results, message, errors, warnings };
337
+ }
270
338
  }
271
339
  class WorkspaceExecutor extends Executor {
272
340
  workspaceRoot;
@@ -379,7 +447,7 @@ class WorkspaceExecutor extends Executor {
379
447
  async getDirInModule(basePath, name) {
380
448
  const AVOID_DIRS = ["__lib", "__scalar", `_`, `_${name}`];
381
449
  const getDirs = async (dirname, maxDepth = 3, results = [], prefix = "") => {
382
- const dirs = await fsPromise.readdir(dirname);
450
+ const dirs = await this.readdir(dirname);
383
451
  await Promise.all(
384
452
  dirs.map(async (dir) => {
385
453
  if (dir.includes("_") || AVOID_DIRS.includes(dir))
@@ -406,7 +474,7 @@ class WorkspaceExecutor extends Executor {
406
474
  async #getDirHasFile(basePath, targetFilename) {
407
475
  const AVOID_DIRS = ["node_modules", "dist", "public", "./next"];
408
476
  const getDirs = async (dirname, maxDepth = 3, results = [], prefix = "") => {
409
- const dirs = await fsPromise.readdir(dirname);
477
+ const dirs = await this.readdir(dirname);
410
478
  await Promise.all(
411
479
  dirs.map(async (dir) => {
412
480
  if (AVOID_DIRS.includes(dir))
@@ -504,14 +572,14 @@ class SysExecutor extends Executor {
504
572
  if (!fs.existsSync(`${this.cwdPath}/lib/__scalar`))
505
573
  fs.mkdirSync(`${this.cwdPath}/lib/__scalar`, { recursive: true });
506
574
  const files = getDefaultFileScan();
507
- const dirnames = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter(
575
+ const dirnames = (await this.readdir("lib")).filter(
508
576
  (name) => fs.lstatSync(`${this.cwdPath}/lib/${name}`).isDirectory()
509
577
  );
510
578
  const databaseDirs = dirnames.filter((name) => !name.startsWith("_"));
511
579
  const serviceDirs = dirnames.filter((name) => name.startsWith("_") && !name.startsWith("__"));
512
580
  await Promise.all(
513
581
  databaseDirs.map(async (name) => {
514
- const filenames = await fsPromise.readdir(path.join(this.cwdPath, "lib", name));
582
+ const filenames = await this.readdir(path.join("lib", name));
515
583
  filenames.forEach((filename) => {
516
584
  if (filename.endsWith(".constant.ts"))
517
585
  files.constants.databases.push(name);
@@ -533,7 +601,7 @@ class SysExecutor extends Executor {
533
601
  await Promise.all(
534
602
  serviceDirs.map(async (dirname) => {
535
603
  const name = dirname.slice(1);
536
- const filenames = await fsPromise.readdir(path.join(this.cwdPath, "lib", dirname));
604
+ const filenames = await this.readdir(path.join("lib", dirname));
537
605
  filenames.forEach((filename) => {
538
606
  if (filename.endsWith(".dictionary.ts"))
539
607
  files.dictionary.services.push(name);
@@ -548,12 +616,10 @@ class SysExecutor extends Executor {
548
616
  });
549
617
  })
550
618
  );
551
- const scalarDirs = (await fsPromise.readdir(`${this.cwdPath}/lib/__scalar`)).filter(
552
- (name) => !name.startsWith("_")
553
- );
619
+ const scalarDirs = (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_"));
554
620
  await Promise.all(
555
621
  scalarDirs.map(async (name) => {
556
- const filenames = await fsPromise.readdir(path.join(this.cwdPath, "lib/__scalar", name));
622
+ const filenames = await this.readdir(path.join("lib/__scalar", name));
557
623
  filenames.forEach((filename) => {
558
624
  if (filename.endsWith(".constant.ts"))
559
625
  files.constants.scalars.push(name);
@@ -600,11 +666,11 @@ class SysExecutor extends Executor {
600
666
  dependencies: [...npmDepSet].filter((dep) => !dep.startsWith("@akanjs")),
601
667
  libs: Object.fromEntries(akanConfig.libs.map((libName) => [libName, libScanResults[libName]]))
602
668
  };
603
- await this.applyTemplate({ basePath: "lib", template: "lib", scanResult });
604
- await this.applyTemplate({ basePath: ".", template: "server.ts", scanResult });
605
- await this.applyTemplate({ basePath: ".", template: "client.ts", scanResult });
669
+ await this._applyTemplate({ basePath: "lib", template: "lib", scanResult });
670
+ await this._applyTemplate({ basePath: ".", template: "server.ts", scanResult });
671
+ await this._applyTemplate({ basePath: ".", template: "client.ts", scanResult });
606
672
  if (this.type === "lib")
607
- await this.applyTemplate({ basePath: ".", template: "index.ts", scanResult });
673
+ await this._applyTemplate({ basePath: ".", template: "index.ts", scanResult });
608
674
  this.writeJson(`akan.${this.type}.json`, scanResult);
609
675
  if (this.type === "app")
610
676
  return scanResult;
@@ -627,33 +693,33 @@ class SysExecutor extends Executor {
627
693
  this.writeJson("package.json", libPkgJsonWithDeps);
628
694
  return scanResult;
629
695
  }
630
- getLocalFile(filePath) {
631
- const filepath = path.isAbsolute(filePath) ? filePath : `${this.type}s/${this.name}/${filePath}`;
632
- const content = this.workspace.readFile(filepath);
633
- return { filepath, content };
696
+ getLocalFile(targetPath) {
697
+ const filePath = path.isAbsolute(targetPath) ? targetPath : `${this.type}s/${this.name}/${targetPath}`;
698
+ const content = this.workspace.readFile(filePath);
699
+ return { filePath, content };
634
700
  }
635
701
  async getDatabaseModules() {
636
- const databaseModules = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
702
+ const databaseModules = (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
637
703
  return databaseModules;
638
704
  }
639
705
  async getServiceModules() {
640
- const serviceModules = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.service.ts`));
706
+ const serviceModules = (await this.readdir("lib")).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.service.ts`));
641
707
  return serviceModules;
642
708
  }
643
709
  async getScalarModules() {
644
- const scalarModules = (await fsPromise.readdir(`${this.cwdPath}/lib/__scalar`)).filter((name) => !name.startsWith("_")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
710
+ const scalarModules = (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
645
711
  return scalarModules;
646
712
  }
647
713
  async getViewComponents() {
648
- const viewComponents = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.View.tsx`));
714
+ const viewComponents = (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.View.tsx`));
649
715
  return viewComponents;
650
716
  }
651
717
  async getUnitComponents() {
652
- const unitComponents = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.Unit.tsx`));
718
+ const unitComponents = (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.Unit.tsx`));
653
719
  return unitComponents;
654
720
  }
655
721
  async getTemplateComponents() {
656
- const templateComponents = (await fsPromise.readdir(`${this.cwdPath}/lib`)).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.Template.tsx`));
722
+ const templateComponents = (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts")).filter((name) => fs.existsSync(`${this.cwdPath}/lib/${name}/${name}.Template.tsx`));
657
723
  return templateComponents;
658
724
  }
659
725
  async getViewsSourceCode() {
@@ -684,10 +750,35 @@ class SysExecutor extends Executor {
684
750
  const modules = await this.getModules();
685
751
  return modules.map((module) => this.getLocalFile(`lib/${module}/${module}.constant.ts`));
686
752
  }
753
+ async getConstantFilesWithLibs() {
754
+ const config = await this.getConfig();
755
+ const sysContantFiles = await this.getConstantFiles();
756
+ const sysScalarConstantFiles = await this.getScalarConstantFiles();
757
+ const libConstantFiles = await Promise.all(
758
+ config.libs.map(async (lib) => [
759
+ ...await LibExecutor.from(this, lib).getConstantFiles(),
760
+ ...await LibExecutor.from(this, lib).getScalarConstantFiles()
761
+ ])
762
+ );
763
+ return [...sysContantFiles, ...sysScalarConstantFiles, ...libConstantFiles.flat()];
764
+ }
687
765
  async getDictionaryFiles() {
688
766
  const modules = await this.getModules();
689
767
  return modules.map((module) => this.getLocalFile(`lib/${module}/${module}.dictionary.ts`));
690
768
  }
769
+ async applyTemplate(options) {
770
+ const dict = {
771
+ ...options.dict ?? {},
772
+ ...Object.fromEntries(
773
+ Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)])
774
+ )
775
+ };
776
+ const akanConfig = await this.getConfig();
777
+ const scanResult = await this.scan({ akanConfig });
778
+ const fileContents = await this._applyTemplate({ ...options, scanResult, dict });
779
+ await this.scan({ akanConfig });
780
+ return fileContents;
781
+ }
691
782
  setTsPaths() {
692
783
  this.workspace.setTsPaths(this.type, this.name);
693
784
  return this;
File without changes
package/esm/src/index.js CHANGED
@@ -16,3 +16,5 @@ export * from "./commandDecorators";
16
16
  export * from "./aiEditor";
17
17
  export * from "./builder";
18
18
  export * from "./spinner";
19
+ export * from "./prompter";
20
+ export * from "./guideline";
@@ -0,0 +1,205 @@
1
+ import { Logger } from "@akanjs/common";
2
+ import chalk from "chalk";
3
+ import { ESLint } from "eslint";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ class Linter {
7
+ #logger = new Logger("Linter");
8
+ #eslint;
9
+ lintRoot;
10
+ constructor(cwdPath) {
11
+ this.lintRoot = this.#findEslintRootPath(cwdPath);
12
+ this.#eslint = new ESLint({ cwd: this.lintRoot, errorOnUnmatchedPattern: false });
13
+ }
14
+ #findEslintRootPath(dir) {
15
+ const configPath = path.join(dir, "eslint.config.ts");
16
+ if (fs.existsSync(configPath))
17
+ return dir;
18
+ const parentDir = path.dirname(dir);
19
+ return this.#findEslintRootPath(parentDir);
20
+ }
21
+ async lint(filePath, { fix = false, dryRun = false } = {}) {
22
+ if (fix)
23
+ return await this.fixFile(filePath, dryRun);
24
+ return await this.lintFile(filePath);
25
+ }
26
+ /**
27
+ * Lint a single file using ESLint
28
+ * @param filePath - Path to the file to lint
29
+ * @returns Array of ESLint results
30
+ */
31
+ async lintFile(filePath) {
32
+ if (!fs.existsSync(filePath))
33
+ throw new Error(`File not found: ${filePath}`);
34
+ const isIgnored = await this.#eslint.isPathIgnored(filePath);
35
+ if (isIgnored) {
36
+ this.#logger.warn(`File ${filePath} is ignored by ESLint configuration`);
37
+ return { fixed: false, results: [], errors: [], warnings: [] };
38
+ }
39
+ const results = await this.#eslint.lintFiles([filePath]);
40
+ const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
41
+ const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
42
+ return { fixed: false, results, errors, warnings };
43
+ }
44
+ /**
45
+ * Format lint results for console output
46
+ * @param results - Array of ESLint results
47
+ * @returns Formatted string
48
+ */
49
+ formatLintResults(results) {
50
+ if (results.length === 0)
51
+ return "No files to lint";
52
+ const output = [];
53
+ let totalErrors = 0;
54
+ let totalWarnings = 0;
55
+ results.forEach((result) => {
56
+ totalErrors += result.errorCount;
57
+ totalWarnings += result.warningCount;
58
+ if (result.messages.length > 0) {
59
+ output.push(`
60
+ ${chalk.cyan(result.filePath)}`);
61
+ let sourceLines = [];
62
+ if (fs.existsSync(result.filePath)) {
63
+ try {
64
+ const sourceContent = fs.readFileSync(result.filePath, "utf8");
65
+ sourceLines = sourceContent.split("\n");
66
+ } catch (error) {
67
+ }
68
+ }
69
+ result.messages.forEach((message) => {
70
+ const type = message.severity === 2 ? "error" : "warning";
71
+ const typeColor = message.severity === 2 ? chalk.red : chalk.yellow;
72
+ const icon = message.severity === 2 ? "\u274C" : "\u26A0\uFE0F";
73
+ const ruleInfo = message.ruleId ? chalk.dim(` (${message.ruleId})`) : "";
74
+ output.push(`
75
+ ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
76
+ output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
77
+ if (sourceLines.length > 0 && message.line <= sourceLines.length) {
78
+ const sourceLine = sourceLines[message.line - 1];
79
+ const lineNumber = message.line.toString().padStart(5, " ");
80
+ output.push(`
81
+ ${chalk.dim(lineNumber + " |")} ${sourceLine}`);
82
+ const underlinePrefix = " ".repeat(message.column - 1);
83
+ const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
84
+ const underline = "^".repeat(Math.max(1, underlineLength));
85
+ output.push(`${chalk.dim(" ".repeat(lineNumber.length) + " |")} ${underlinePrefix}${typeColor(underline)}`);
86
+ }
87
+ });
88
+ }
89
+ });
90
+ if (totalErrors === 0 && totalWarnings === 0)
91
+ return chalk.bold("\u2705 No ESLint errors or warnings found");
92
+ const errorText = totalErrors > 0 ? chalk.red(`${totalErrors} error(s)`) : "0 errors";
93
+ const warningText = totalWarnings > 0 ? chalk.yellow(`${totalWarnings} warning(s)`) : "0 warnings";
94
+ const summary = [`
95
+ ${errorText}, ${warningText} found`];
96
+ return summary.concat(output).join("\n");
97
+ }
98
+ /**
99
+ * Get detailed lint information
100
+ * @param filePath - Path to the file to lint
101
+ * @returns Object containing detailed lint information
102
+ */
103
+ async getDetailedLintInfo(filePath) {
104
+ const { results } = await this.lintFile(filePath);
105
+ const details = results.flatMap(
106
+ (result) => result.messages.map((message) => ({
107
+ line: message.line,
108
+ column: message.column,
109
+ message: message.message,
110
+ ruleId: message.ruleId,
111
+ severity: message.severity === 2 ? "error" : "warning",
112
+ fix: message.fix,
113
+ suggestions: message.suggestions
114
+ }))
115
+ );
116
+ const stats = results.reduce(
117
+ (acc, result) => ({
118
+ errorCount: acc.errorCount + result.errorCount,
119
+ warningCount: acc.warningCount + result.warningCount,
120
+ fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
121
+ fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount
122
+ }),
123
+ { errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 }
124
+ );
125
+ return { results, details, stats };
126
+ }
127
+ /**
128
+ * Check if a file has lint errors
129
+ * @param filePath - Path to the file to check
130
+ * @returns true if there are no errors, false otherwise
131
+ */
132
+ async hasNoLintErrors(filePath) {
133
+ try {
134
+ const { results } = await this.lintFile(filePath);
135
+ return results.every((result) => result.errorCount === 0);
136
+ } catch (error) {
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Get only error messages (excluding warnings)
142
+ * @param filePath - Path to the file to lint
143
+ * @returns Array of error messages
144
+ */
145
+ async getErrors(filePath) {
146
+ const { results } = await this.lintFile(filePath);
147
+ return results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
148
+ }
149
+ /**
150
+ * Get only warning messages
151
+ * @param filePath - Path to the file to lint
152
+ * @returns Array of warning messages
153
+ */
154
+ async getWarnings(filePath) {
155
+ const { results } = await this.lintFile(filePath);
156
+ return results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
157
+ }
158
+ /**
159
+ * Fix lint errors automatically
160
+ * @param filePath - Path to the file to fix
161
+ * @param dryRun - If true, returns the fixed content without writing to file
162
+ * @returns Fixed content and remaining issues
163
+ */
164
+ async fixFile(filePath, dryRun = false) {
165
+ if (!fs.existsSync(filePath))
166
+ throw new Error(`File not found: ${filePath}`);
167
+ const eslint = new ESLint({ cwd: this.lintRoot, fix: true });
168
+ const results = await eslint.lintFiles([filePath]);
169
+ const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
170
+ const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
171
+ if (!dryRun)
172
+ await ESLint.outputFixes(results);
173
+ const fixedResult = results[0];
174
+ return { fixed: fixedResult.output !== void 0, output: fixedResult.output, results, errors, warnings };
175
+ }
176
+ /**
177
+ * Get ESLint configuration for a file
178
+ * @param filePath - Path to the file
179
+ * @returns ESLint configuration object
180
+ */
181
+ async getConfigForFile(filePath) {
182
+ const eslint = new ESLint();
183
+ const config = await eslint.calculateConfigForFile(filePath);
184
+ return config;
185
+ }
186
+ /**
187
+ * Get rules that are causing errors in a file
188
+ * @param filePath - Path to the file to check
189
+ * @returns Object mapping rule IDs to their error counts
190
+ */
191
+ async getProblematicRules(filePath) {
192
+ const { results } = await this.lintFile(filePath);
193
+ const ruleCounts = {};
194
+ results.forEach((result) => {
195
+ result.messages.forEach((message) => {
196
+ if (message.ruleId)
197
+ ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
198
+ });
199
+ });
200
+ return ruleCounts;
201
+ }
202
+ }
203
+ export {
204
+ Linter
205
+ };