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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,16 @@ jobs:
with:
node-version-file: .tool-versions
cache: npm
- name: Build and link local repomix
run: |
npm ci
npm run build
npm link
- name: Install website server dependencies
working-directory: website/server
run: npm ci
run: |
npm ci
npm link repomix
- name: Lint website server
working-directory: website/server
run: npm run lint
Expand Down
2 changes: 1 addition & 1 deletion src/cli/actions/defaultAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const runDefaultAction = async (
// Create worker task runner
const taskRunner = initTaskRunner<DefaultActionTask | PingTask, DefaultActionWorkerResult | PingResult>({
numOfTasks: 1,
workerPath: new URL('./workers/defaultActionWorker.js', import.meta.url).href,
workerType: 'defaultAction',
runtime: 'child_process',
});

Expand Down
19 changes: 15 additions & 4 deletions src/cli/actions/workers/defaultActionWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,30 @@ async function defaultActionWorker(
};
}

// Validate task structure
if (!task || typeof task !== 'object') {
throw new Error(`Invalid task: expected object, got ${typeof task}`);
}

// At this point, task is guaranteed to be DefaultActionTask
const { directories, cwd, config, cliOptions, stdinFilePaths } = task;

if (!directories || !Array.isArray(directories)) {
throw new Error('Invalid task: directories must be an array');
}

// Provide defaults for bundled environments where cliOptions might be undefined
const safeCliOptions: CliOptions = cliOptions ?? {};

logger.trace('Worker: Using pre-loaded config:', config);

// Initialize spinner in worker
const spinner = new Spinner('Initializing...', cliOptions);
const spinner = new Spinner('Initializing...', safeCliOptions);
spinner.start();

let packResult: PackResult;

try {
const { skillName, skillDir, skillProjectName, skillSourceUrl } = cliOptions;
const { skillName, skillDir, skillProjectName, skillSourceUrl } = safeCliOptions;
const packOptions = { skillName, skillDir, skillProjectName, skillSourceUrl };

if (stdinFilePaths) {
Expand Down Expand Up @@ -105,7 +116,7 @@ async function defaultActionWorker(
export default defaultActionWorker;

// Export cleanup function for Tinypool teardown
export const onWorkerTermination = async () => {
export const onWorkerTermination = async (): Promise<void> => {
// Any cleanup needed when worker terminates
// Currently no specific cleanup required for defaultAction worker
};
5 changes: 3 additions & 2 deletions src/cli/cliSpinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export class Spinner {
private interval: ReturnType<typeof setInterval> | null = null;
private readonly isQuiet: boolean;

constructor(message: string, cliOptions: CliOptions) {
constructor(message: string, cliOptions?: CliOptions) {
this.message = message;
// If the user has specified the verbose flag, don't show the spinner
this.isQuiet = cliOptions.quiet || cliOptions.verbose || cliOptions.stdout || false;
// Use optional chaining to handle undefined cliOptions (e.g., in bundled worker environments)
this.isQuiet = cliOptions?.quiet || cliOptions?.verbose || cliOptions?.stdout || false;
}

start(): void {
Expand Down
2 changes: 1 addition & 1 deletion src/core/file/fileCollect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const collectFiles = async (
): Promise<FileCollectResults> => {
const taskRunner = deps.initTaskRunner<FileCollectTask, FileCollectResult>({
numOfTasks: filePaths.length,
workerPath: new URL('./workers/fileCollectWorker.js', import.meta.url).href,
workerType: 'fileCollect',
runtime: 'worker_threads',
});
const tasks = filePaths.map(
Expand Down
2 changes: 1 addition & 1 deletion src/core/file/fileProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const processFiles = async (
): Promise<ProcessedFile[]> => {
const taskRunner = deps.initTaskRunner<FileProcessTask, ProcessedFile>({
numOfTasks: rawFiles.length,
workerPath: new URL('./workers/fileProcessWorker.js', import.meta.url).href,
workerType: 'fileProcess',
// High memory usage and leak risk
runtime: 'worker_threads',
});
Expand Down
2 changes: 1 addition & 1 deletion src/core/file/workers/fileCollectWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ export default async ({ filePath, rootDir, maxFileSize }: FileCollectTask): Prom
};

// Export cleanup function for Tinypool teardown (no cleanup needed for this worker)
export const onWorkerTermination = () => {
export const onWorkerTermination = async (): Promise<void> => {
// No cleanup needed for file collection worker
};
2 changes: 1 addition & 1 deletion src/core/file/workers/fileProcessWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export default async ({ rawFile, config }: FileProcessTask): Promise<ProcessedFi
};

// Export cleanup function for Tinypool teardown
export const onWorkerTermination = async () => {
export const onWorkerTermination = async (): Promise<void> => {
await cleanupLanguageParser();
};
2 changes: 1 addition & 1 deletion src/core/metrics/calculateMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const calculateMetrics = async (
deps.taskRunner ??
initTaskRunner<TokenCountTask, number>({
numOfTasks: processedFiles.length,
workerPath: new URL('./workers/calculateMetricsWorker.js', import.meta.url).href,
workerType: 'calculateMetrics',
runtime: 'worker_threads',
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/metrics/workers/calculateMetricsWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ export default async (task: TokenCountTask): Promise<number> => {
};

// Export cleanup function for Tinypool teardown
export const onWorkerTermination = () => {
export const onWorkerTermination = async (): Promise<void> => {
freeTokenCounters();
};
2 changes: 1 addition & 1 deletion src/core/security/securityCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const runSecurityCheck = async (

const taskRunner = deps.initTaskRunner<SecurityCheckTask, SuspiciousFileResult | null>({
numOfTasks: rawFiles.length + gitDiffTasks.length + gitLogTasks.length,
workerPath: new URL('./workers/securityCheckWorker.js', import.meta.url).href,
workerType: 'securityCheck',
runtime: 'worker_threads',
});
const fileTasks = rawFiles.map(
Expand Down
2 changes: 1 addition & 1 deletion src/core/security/workers/securityCheckWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,6 @@ export const createSecretLintConfig = (): SecretLintCoreConfig => ({
});

// Export cleanup function for Tinypool teardown (no cleanup needed for this worker)
export const onWorkerTermination = () => {
export const onWorkerTermination = async (): Promise<void> => {
// No cleanup needed for security check worker
};
35 changes: 34 additions & 1 deletion src/core/treeSitter/loadLanguage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { Language } from 'web-tree-sitter';

const require = createRequire(import.meta.url);

/**
* Custom WASM base path for bundled environments.
* Set via REPOMIX_WASM_DIR environment variable or setWasmBasePath().
* When set, WASM files are loaded from this directory instead of node_modules.
*/
let customWasmBasePath: string | null = null;

/**
* Set a custom base path for WASM files.
* Used in bundled environments where WASM files are copied to a custom location.
*/
export function setWasmBasePath(basePath: string): void {
customWasmBasePath = basePath;
}

/**
* Get the WASM base path from environment variable or custom setting.
*/
function getWasmBasePath(): string | null {
return customWasmBasePath ?? process.env.REPOMIX_WASM_DIR ?? null;
}

export async function loadLanguage(langName: string): Promise<Language> {
if (!langName) {
throw new Error('Invalid language name');
Expand All @@ -19,7 +42,17 @@ export async function loadLanguage(langName: string): Promise<Language> {
}

async function getWasmPath(langName: string): Promise<string> {
const wasmPath = require.resolve(`@repomix/tree-sitter-wasms/out/tree-sitter-${langName}.wasm`);
const wasmBasePath = getWasmBasePath();

let wasmPath: string;
if (wasmBasePath) {
// Use custom WASM path for bundled environments
wasmPath = path.join(wasmBasePath, `tree-sitter-${langName}.wasm`);
} else {
// Use require.resolve for standard node_modules environments
wasmPath = require.resolve(`@repomix/tree-sitter-wasms/out/tree-sitter-${langName}.wasm`);
}

try {
await fs.access(wasmPath);
return wasmPath;
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { TokenCounter } from './core/metrics/TokenCounter.js';

// Tree-sitter
export { parseFile } from './core/treeSitter/parseFile.js';
export { setWasmBasePath } from './core/treeSitter/loadLanguage.js';

// ---------------------------------------------------------------------------------------------------------------------
// Config
Expand Down Expand Up @@ -56,3 +57,12 @@ export { runDefaultAction, buildCliConfig } from './cli/actions/defaultAction.js

// Remote action
export { runRemoteAction } from './cli/actions/remoteAction.js';

// ---------------------------------------------------------------------------------------------------------------------
// Worker (for bundled environments)
// ---------------------------------------------------------------------------------------------------------------------
export {
default as unifiedWorkerHandler,
onWorkerTermination as unifiedWorkerTermination,
type WorkerType,
} from './shared/unifiedWorker.js';
Comment thread
yamadashy marked this conversation as resolved.
45 changes: 42 additions & 3 deletions src/shared/processConcurrency.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import os from 'node:os';
import { type Options, Tinypool } from 'tinypool';
import { logger } from './logger.js';
import type { WorkerType } from './unifiedWorker.js';

export type WorkerRuntime = NonNullable<Options['runtime']>;

// Re-export WorkerType for external consumers
export type { WorkerType } from './unifiedWorker.js';

export interface WorkerOptions {
numOfTasks: number;
workerPath: string;
workerType: WorkerType;
runtime: WorkerRuntime;
}

/**
* Get the worker file path for a given worker type.
* In bundled environments (REPOMIX_WORKER_PATH set), uses the unified worker.
* Otherwise, uses individual worker files.
*/
const getWorkerPath = (workerType: WorkerType): string => {
// Bundled environment: use unified worker path
if (process.env.REPOMIX_WORKER_PATH) {
return process.env.REPOMIX_WORKER_PATH;
}

// Non-bundled environment: use individual worker files
switch (workerType) {
case 'fileCollect':
return new URL('../core/file/workers/fileCollectWorker.js', import.meta.url).href;
case 'fileProcess':
return new URL('../core/file/workers/fileProcessWorker.js', import.meta.url).href;
case 'securityCheck':
return new URL('../core/security/workers/securityCheckWorker.js', import.meta.url).href;
case 'calculateMetrics':
return new URL('../core/metrics/workers/calculateMetricsWorker.js', import.meta.url).href;
case 'defaultAction':
return new URL('../cli/actions/workers/defaultActionWorker.js', import.meta.url).href;
default:
throw new Error(`Unknown worker type: ${workerType}`);
}
};

// Worker initialization is expensive, so we prefer fewer threads unless there are many files
const TASKS_PER_THREAD = 100;

Expand All @@ -32,11 +64,14 @@ export const getWorkerThreadCount = (numOfTasks: number): { minThreads: number;
};

export const createWorkerPool = (options: WorkerOptions): Tinypool => {
const { numOfTasks, workerPath, runtime = 'child_process' } = options;
const { numOfTasks, workerType, runtime = 'child_process' } = options;
const { minThreads, maxThreads } = getWorkerThreadCount(numOfTasks);

// Get worker path - uses unified worker in bundled env, individual files otherwise
const workerPath = getWorkerPath(workerType);

logger.trace(
`Initializing worker pool with min=${minThreads}, max=${maxThreads} threads, runtime=${runtime}. Worker path: ${workerPath}`,
`Initializing worker pool with min=${minThreads}, max=${maxThreads} threads, runtime=${runtime}. Worker type: ${workerType}`,
);

const startTime = process.hrtime.bigint();
Expand All @@ -49,12 +84,16 @@ export const createWorkerPool = (options: WorkerOptions): Tinypool => {
idleTimeout: 5000,
teardown: 'onWorkerTermination',
workerData: {
workerType,
logLevel: logger.getLogLevel(),
},
// Only add env for child_process workers
...(runtime === 'child_process' && {
env: {
...process.env,
// Pass worker type as environment variable for child_process workers
// This is needed because workerData is not directly accessible in child_process runtime
REPOMIX_WORKER_TYPE: workerType,
// Pass log level as environment variable for child_process workers
REPOMIX_LOG_LEVEL: logger.getLogLevel().toString(),
// Ensure color support in child_process workers
Expand Down
Loading
Loading