npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

naite-like

v0.1.1

Published

A test framework that embeds test traces in production code using AsyncLocalStorage

Readme

Naite (나이테)

License: MIT

나이테(Naite)는 프로덕션 코드 내에 주석과 동일한 형태로 테스트 코드를 삽입하고, 함수를 실행하는 과정에서 이 값들을 관찰하는 방식으로 코드를 테스트하는 테스트 프레임워크입니다.

나이테는 AsyncLocalStorage를 사용해 이를 달성하며, Vite 혹은 esbuild의 dead code elimination으로 테스트 코드가 삭제되므로 라이브 코드에는 영향을 주지 않습니다.

크레딧 및 면책 조항

이 테스트 프레임워크는 Minsang Kim의 글 테스트를 위해 코드를 쪼개지 마세요: Naite와 함께하는 관찰 기반 테스팅를 기반으로, AI를 활용하여 만든 프레임워크입니다.

면책 조항: 이 구현체는 원작자의 공식 릴리스가 아닌 독립적인 프로젝트입니다. 추후 공식 Naite가 공개될 경우 이 프로젝트는 deprecated될 예정입니다.

주요 특징

  1. 함수가 완성되지 않아도 테스트 가능 - 구현 중인 함수도 중간 단계까지 검증
  2. 중간 과정을 바로 검증 - 함수 내부 실행 흐름을 트레이스로 확인
  3. 함수 쪼개기 강박 사라짐 - 테스트를 위해 함수를 무리하게 분리할 필요 없음
  4. 컨텍스트 유지 - AsyncLocalStorage로 테스트 간 격리 보장
  5. 의존성 제로 - Node.js 내장 모듈만 사용, 런타임 의존성 없음

설치

# npm
npm install naite-like

# pnpm
pnpm add naite-like

# yarn
yarn add naite-like

요구사항: Node.js >= 12.17.0

빠른 시작

1. 프로덕션 코드에 트레이스 삽입

import { Naite } from 'naite-like';

export async function queryComplexResult(params: QueryParams): Promise<QueryResult> {
  Naite.t('step', '첫번째 쿼리 생성');
  const query1 = buildFirstQuery(params);
  Naite.t('query1', query1);

  Naite.t('step', '두번째 쿼리 생성');
  const query2 = buildSecondQuery(params);
  Naite.t('query2', query2);

  Naite.t('step', '최종 결과 계산');
  const result = await calculateResult(query1, query2);
  Naite.t('result', result);

  return result;
}

2. 테스트 작성

import { describe, it, expect } from 'vitest';
import { Naite, runWithContext } from 'naite-like';
import { queryComplexResult } from './query.js';

describe('QueryTest', () => {
  it('should generate queries in correct order', async () => {
    await runWithContext(async () => {
      try {
        await queryComplexResult({ userId: 123 });
      } catch (error) {
        // 에러가 나도 상관없음!
      } finally {
        // 실행 흐름을 검증
        expect(Naite.get('step').last()).toBe('최종 결과 계산');
        expect(Naite.get('query1').first()).toBeDefined();
        expect(Naite.get('query2').first()).toBeDefined();
      }
    });
  });
});

API 문서

Naite

메인 클래스로, 트레이스 기록 및 조회를 담당합니다.

Naite.t(key: string, value: unknown): void

트레이스를 기록합니다. NODE_ENV=test일 때만 작동합니다.

Naite.t('userId', 123);
Naite.t('step', 'processing data');
Naite.t('result', { success: true });

Naite.get(keyPattern: string): NaiteQuery

키 또는 와일드카드 패턴으로 트레이스를 조회합니다.

// 정확한 키 매칭
Naite.get('userId').first();  // 123

// 와일드카드 패턴
Naite.get('syncer:*').result();  // 모든 syncer:* 트레이스
Naite.get('syncer:*:user').result();  // syncer:{any}:user 형태

Naite.getAll(): Record<string, unknown[]>

모든 트레이스를 객체 형태로 반환합니다.

const all = Naite.getAll();
// { userId: [123], step: ['processing data'], ... }

Naite.del(key: string): void

특정 키의 트레이스를 삭제합니다.

Naite.del('userId');

Naite.createStore(): NaiteStore

새로운 빈 스토어를 생성합니다 (고급 사용).

NaiteQuery

체이닝 방식으로 트레이스를 필터링하고 조회합니다.

.result(): unknown[]

모든 데이터를 배열로 반환합니다.

Naite.get('key').result();  // [value1, value2, value3]

.first(): unknown | undefined

첫 번째 데이터를 반환합니다.

Naite.get('key').first();  // value1

.last(): unknown | undefined

마지막 데이터를 반환합니다.

Naite.get('key').last();  // value3

.at(index: number): unknown | undefined

n번째 데이터를 반환합니다 (0-based).

Naite.get('key').at(1);  // value2

.fromFile(fileName: string): NaiteQuery

특정 파일에서 기록된 트레이스만 필터링합니다.

Naite.get('trace')
  .fromFile('syncer.test.ts')
  .result();

.fromFunction(funcName: string, options?): NaiteQuery

특정 함수에서 기록된 트레이스만 필터링합니다.

// 모든 호출 (직접 + 간접)
Naite.get('trace').fromFunction('renderTemplate').result();

// 직접 호출만
Naite.get('trace').fromFunction('renderTemplate', { from: 'direct' }).result();

// 간접 호출만
Naite.get('trace').fromFunction('renderTemplate', { from: 'indirect' }).result();

.where(path: string, operator: ComparisonOperator, value: unknown): NaiteQuery

데이터 필드 조건으로 필터링합니다.

// 같음
Naite.get('user').where('data.userId', '=', 123).result();

// 비교 연산자
Naite.get('count').where('data.value', '>', 10).result();

// 문자열 포함
Naite.get('log').where('data.message', 'includes', 'error').result();

지원 연산자: >, <, >=, <=, =, !=, includes

.getTraces(): NaiteTrace[]

원본 트레이스 객체 배열을 반환합니다 (디버깅용).

const traces = Naite.get('key').getTraces();
// [{ key, data, stack, at }, ...]

컨텍스트 관리

runWithContext<T>(fn: () => T | Promise<T>): Promise<T>

새로운 Naite 컨텍스트를 생성하고 함수를 실행합니다.

await runWithContext(async () => {
  Naite.t('key', 'value');
  expect(Naite.get('key').first()).toBe('value');
});

runWithExistingContext<T>(store: NaiteStore, fn: () => T | Promise<T>): Promise<T>

기존 스토어를 사용하여 함수를 실행합니다.

const store = Naite.createStore();

await runWithExistingContext(store, () => {
  Naite.t('key1', 'value1');
});

await runWithExistingContext(store, () => {
  Naite.t('key2', 'value2');
});

// store에는 key1, key2가 모두 존재

getContext(): NaiteStore | undefined

현재 컨텍스트의 스토어를 반환합니다.

const store = getContext();
if (store) {
  // 컨텍스트 내부
}

고급 사용법

메서드 체이닝

여러 필터를 조합하여 정확한 트레이스를 찾을 수 있습니다.

const result = Naite.get('trace')
  .fromFile('syncer.test.ts')
  .fromFunction('renderTemplate')
  .where('data.userId', '=', 123)
  .first();

와일드카드 패턴

계층적 키 구조에서 강력한 쿼리가 가능합니다.

// syncer:로 시작하는 모든 키
Naite.get('syncer:*').result();

// 중간에 와일드카드
Naite.get('syncer:*:user').result();

// 여러 와일드카드
Naite.get('*:*:user').result();

스택 정보 활용

각 트레이스는 호출 스택 정보를 포함합니다.

const traces = Naite.get('key').getTraces();
traces.forEach(trace => {
  console.log('Key:', trace.key);
  console.log('Data:', trace.data);
  console.log('Called from:', trace.stack[0].functionName);
  console.log('File:', trace.stack[0].filePath);
  console.log('Line:', trace.stack[0].lineNumber);
  console.log('Timestamp:', trace.at);
});

프로덕션 빌드에서 제거

Vite나 esbuild 사용 시, 자동으로 dead code elimination이 적용됩니다.

// vite.config.ts
export default defineConfig({
  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
  },
});

프로덕션 빌드 시 Naite.t() 호출이 모두 제거됩니다.

사용 예제

예제 1: 복잡한 쿼리 함수 테스트

// src/query.ts
export async function buildComplexQuery(userId: number) {
  Naite.t('step', 'Fetch user');
  const user = await fetchUser(userId);
  Naite.t('user', user);

  if (!user) {
    Naite.t('step', 'User not found');
    throw new Error('User not found');
  }

  Naite.t('step', 'Build query');
  const query = { ...user, timestamp: Date.now() };
  Naite.t('query', query);

  return query;
}

// tests/query.test.ts
it('should handle missing user', async () => {
  await runWithContext(async () => {
    try {
      await buildComplexQuery(999);
    } catch (error) {
      expect(Naite.get('step').last()).toBe('User not found');
      expect(Naite.get('user').first()).toBeNull();
    }
  });
});

예제 2: 다중 트레이스 검증

// src/processor.ts
export function processItems(items: Item[]) {
  items.forEach((item, index) => {
    Naite.t('process:item', { index, id: item.id });

    if (item.valid) {
      Naite.t('process:valid', item.id);
    } else {
      Naite.t('process:invalid', item.id);
    }
  });
}

// tests/processor.test.ts
it('should process all items', async () => {
  await runWithContext(() => {
    processItems([
      { id: 1, valid: true },
      { id: 2, valid: false },
      { id: 3, valid: true },
    ]);

    // 모든 아이템 처리 확인
    expect(Naite.get('process:*').result()).toHaveLength(6);

    // 유효한 아이템만
    expect(Naite.get('process:valid').result()).toEqual([1, 3]);

    // 무효한 아이템만
    expect(Naite.get('process:invalid').result()).toEqual([2]);
  });
});

예제 3: 조건부 필터링

it('should filter by conditions', async () => {
  await runWithContext(() => {
    Naite.t('metric', { cpu: 45, memory: 512 });
    Naite.t('metric', { cpu: 78, memory: 1024 });
    Naite.t('metric', { cpu: 92, memory: 2048 });

    // CPU 사용률이 80 이상인 메트릭
    const highCpu = Naite.get('metric')
      .where('data.cpu', '>=', 80)
      .result();

    expect(highCpu).toHaveLength(1);
    expect(highCpu[0].cpu).toBe(92);

    // 메모리가 1GB 이상인 메트릭
    const highMemory = Naite.get('metric')
      .where('data.memory', '>=', 1024)
      .result();

    expect(highMemory).toHaveLength(2);
  });
});

라이센스

MIT (https://github.com/cartanova-ai/sonamu/blob/ac2e3883629b9c12f10c945654aa5682a50fb529/modules/sonamu/package.json#L29 에 기반함)