Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ Test projects for various scenarios. Contains examples with different structures
- Export management for React, Vue.js component libraries
- Preparation for webpack, rollup.js bundling

## Coding Guide line

- Don't use one alphabet variable name
- Not use: `const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p);`
- use singular name
- `const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePath) ?? rawFilePath);`

### Commit Log

- Use Conventional Commit format
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ctix",
"version": "2.7.6-beta1",
"version": "2.8.0",
"description": "Automatic create index.ts file",
"scripts": {
"clean": "rimraf ./dist",
Expand Down Expand Up @@ -153,7 +153,7 @@
"peerDependencies": {
"prettier": ">=3",
"prettier-plugin-organize-imports": ">=3",
"typescript": ">=5"
"typescript": ">=5 <6"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand Down
170 changes: 85 additions & 85 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/cli/builders/setProjectOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export function setProjectOptions<T = Argv<IProjectOptions>>(args: Argv<IProject
type: 'string',
choices: ['stdout', 'stderr'],
default: 'stderr',
})
.option('verbose', {
alias: 'v',
describe:
'Enable verbose debug logging to diagnose path resolution and include/exclude issues',
type: 'boolean',
default: false,
});

return args as T;
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/buildCommand.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Debugger } from '#/cli/ux/Debugger';
import { ProgressBar } from '#/cli/ux/ProgressBar';
import { Reasoner } from '#/cli/ux/Reasoner';
import { Spinner } from '#/cli/ux/Spinner';
Expand All @@ -11,6 +12,7 @@ export async function buildCommand(argv: yargs.ArgumentsCamelCase<TCommandBuildA
ProgressBar.it.enable = true;
Spinner.it.enable = true;
Reasoner.it.enable = true;
Debugger.it.enable = argv.verbose ?? false;

try {
const options = await createBuildOptions(argv);
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/removeCommand.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Debugger } from '#/cli/ux/Debugger';
import { ProgressBar } from '#/cli/ux/ProgressBar';
import { Reasoner } from '#/cli/ux/Reasoner';
import { Spinner } from '#/cli/ux/Spinner';
Expand All @@ -15,6 +16,7 @@ export async function removeCommand(
ProgressBar.it.enable = true;
Spinner.it.enable = true;
Reasoner.it.enable = true;
Debugger.it.enable = argv.verbose ?? false;

try {
const options = await createBuildOptions(argv);
Expand Down
5 changes: 3 additions & 2 deletions src/cli/questions/askInitOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CE_CTIX_BUILD_MODE } from '#/configs/const-enum/CE_CTIX_BUILD_MODE';
import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE';
import { getTsconfigComparer } from '#/configs/modules/getTsconfigComparer';
import { getGlobFiles } from '#/modules/file/getGlobFiles';
import { getCwd } from '#/modules/path/getCwd';
import { defaultExclude } from '#/modules/scope/defaultExclude';
import chalk from 'chalk';
import { exists } from 'find-up';
Expand All @@ -11,7 +12,7 @@ import inquirer from 'inquirer';
import pathe from 'pathe';

export async function askInitOptions(): Promise<IInitQuestionAnswer> {
const cwd = process.cwd();
const cwd = getCwd();

const cwdAnswer = await inquirer.prompt<Pick<IInitQuestionAnswer, 'cwd'>>([
{
Expand Down Expand Up @@ -117,7 +118,7 @@ export async function askInitOptions(): Promise<IInitQuestionAnswer> {
overwirte: overwriteAnswer.overwirte,
tsconfig: [],
addEveryOptions: false,
packageJson: pathe.join(process.cwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME),
packageJson: pathe.join(getCwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME),
mode: CE_CTIX_BUILD_MODE.BUNDLE_MODE,
configPosition: '.ctirc',
configComment: true,
Expand Down
3 changes: 2 additions & 1 deletion src/cli/questions/askRemoveFiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IChoiceTypeItem } from '#/cli/interfaces/IChoiceTypeItem';
import { getRatioNumber } from '#/cli/modules/getRatioNumber';
import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE';
import { getCwd } from '#/modules/path/getCwd';
import { posixRelative } from '#/modules/path/modules/posixRelative';
import Fuse from 'fuse.js';
import inquirer from 'inquirer';
Expand All @@ -12,7 +13,7 @@ export async function askRemoveFiles(filePaths: string[]) {
const choiceAbleTypes = filePaths.map((filePath) => {
return {
filePath,
name: posixRelative(process.cwd(), filePath),
name: posixRelative(getCwd(), filePath),
value: filePath,
} satisfies IChoiceTypeItem;
});
Expand Down
123 changes: 123 additions & 0 deletions src/cli/ux/Debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from 'node:fs';
import path from 'node:path';

export class Debugger {
static #it: Debugger;

static #isBootstrap: boolean = false;

static get it() {
return Debugger.#it;
}

static get isBootstrap() {
return Debugger.#isBootstrap;
}

static bootstrap() {
if (Debugger.#isBootstrap) {
return;
}

Debugger.#it = new Debugger(false, undefined);
Debugger.#isBootstrap = true;
}

#enable: boolean;

#logFile: string | undefined;

#stream: fs.WriteStream | undefined;

constructor(enable: boolean, logFile: string | undefined) {
this.#enable = enable;
this.#logFile = logFile;
}

get enable() {
return this.#enable;
}

set enable(value: boolean) {
this.#enable = value;

if (this.#logFile != null && value) {
this.#openStream();
}
}

set logFile(filePath: string | undefined) {
this.#logFile = filePath;

if (filePath != null && this.#enable) {
this.#openStream();
}
}

#openStream() {
// Both callers guard logFile != null before calling; this is a defensive check
/* v8 ignore start */
if (this.#logFile == null) {
return;
}
/* v8 ignore stop */

const logDir = path.dirname(this.#logFile);

if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}

this.#stream = fs.createWriteStream(this.#logFile, { flags: 'a' });
}

log(message: string) {
if (!this.#enable) {
return;
}

const line = `[DEBUG] ${message}`;
process.stderr.write(`${line}\n`);

if (this.#stream != null) {
this.#stream.write(`${line}\n`);
}
}

logList(label: string, items: string[]) {
if (!this.#enable) {
return;
}

this.log(`${label} (${items.length}):`);

if (items.length === 0) {
this.log(' (empty)');
} else {
for (const item of items) {
this.log(` - ${item}`);
}
}
}

table(label: string, entries: [string, unknown][]) {
if (!this.#enable) {
return;
}

this.log(`${label}:`);

for (const [key, value] of entries) {
this.log(` ${key} => ${String(value)}`);
}
}

close() {
if (this.#stream != null) {
this.#stream.end();
this.#stream = undefined;
}
}
}

Debugger.bootstrap();
156 changes: 156 additions & 0 deletions src/cli/ux/__tests__/debugger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Debugger } from '#/cli/ux/Debugger';
import fs from 'node:fs';
import { afterEach, describe, expect, it, vi } from 'vitest';

describe('Debugger', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('table - does nothing when disabled', () => {
const debuggerInstance = new Debugger(false, undefined);
const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

debuggerInstance.table('label', [['key', 'value']]);

expect(writeSpy).not.toHaveBeenCalled();
});

it('table - logs each entry when enabled', () => {
const debuggerInstance = new Debugger(true, undefined);
const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

debuggerInstance.table('label', [
['key1', 'value1'],
['key2', 42],
]);

expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('label'));
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('key1 => value1'));
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('key2 => 42'));
});

it('logList - does nothing when disabled', () => {
const debuggerInstance = new Debugger(false, undefined);
const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

debuggerInstance.logList('label', ['item1']);

expect(writeSpy).not.toHaveBeenCalled();
});

it('logList - logs each item when enabled', () => {
const debuggerInstance = new Debugger(true, undefined);
const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

debuggerInstance.logList('label', ['item1', 'item2']);

expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('label (2)'));
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('- item1'));
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('- item2'));
});

it('logList - logs (empty) when items array is empty', () => {
const debuggerInstance = new Debugger(true, undefined);
const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

debuggerInstance.logList('label', []);

expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('(empty)'));
});

it('log - writes to stream when stream is open', () => {
const mockStream = { end: vi.fn(), write: vi.fn() };
vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);

const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log');
debuggerInstance.enable = true;
debuggerInstance.log('hello');

expect(mockStream.write).toHaveBeenCalledWith(expect.stringContaining('hello'));
});

it('openStream - creates log directory when it does not exist', () => {
const mockStream = { end: vi.fn(), write: vi.fn() };
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream);

const debuggerInstance = new Debugger(false, '/tmp/nonexistent/ctix-test.log');
debuggerInstance.enable = true;

expect(mkdirSpy).toHaveBeenCalledWith('/tmp/nonexistent', { recursive: true });
});

it('isBootstrap - returns true after module load auto-bootstrap', () => {
expect(Debugger.isBootstrap).toBe(true);
});

it('bootstrap - is idempotent and does not reset the singleton', () => {
const before = Debugger.it;
Debugger.bootstrap();
expect(Debugger.it).toBe(before);
});

it('enable getter - returns current enable state', () => {
const debuggerInstance = new Debugger(false, undefined);
expect(debuggerInstance.enable).toBe(false);

debuggerInstance.enable = true;
expect(debuggerInstance.enable).toBe(true);
});

it('logFile setter - opens stream when enable is true and logFile is set', () => {
const mockStream = { end: vi.fn(), write: vi.fn() };
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
const createWriteStreamSpy = vi
.spyOn(fs, 'createWriteStream')
.mockReturnValue(mockStream as unknown as fs.WriteStream);

const debuggerInstance = new Debugger(true, undefined);
debuggerInstance.logFile = '/tmp/ctix-test.log';

expect(createWriteStreamSpy).toHaveBeenCalled();
});

it('logFile setter - does not open stream when enable is false', () => {
const createWriteStreamSpy = vi.spyOn(fs, 'createWriteStream');

const debuggerInstance = new Debugger(false, undefined);
debuggerInstance.logFile = '/tmp/ctix-test.log';

expect(createWriteStreamSpy).not.toHaveBeenCalled();
});

it('close - does nothing when stream is not open', () => {
const debuggerInstance = new Debugger(false, undefined);
expect(() => debuggerInstance.close()).not.toThrow();
});

it('close - ends and clears the write stream', () => {
const mockStream = { end: vi.fn(), write: vi.fn() };
vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);

const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log');
debuggerInstance.enable = true;
debuggerInstance.close();

expect(mockStream.end).toHaveBeenCalledOnce();
});

it('close - second call does nothing after stream is cleared', () => {
const mockStream = { end: vi.fn(), write: vi.fn() };
vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);

const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log');
debuggerInstance.enable = true;
debuggerInstance.close();
debuggerInstance.close();

expect(mockStream.end).toHaveBeenCalledOnce();
});
});
Loading
Loading