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 🙏

© 2024 – Pkg Stats / Ryan Hefner

update-readme-table-of-contents

v1.0.5

Published

This project automatically updates a specified README file with changes from markdown files on commit, generating a table of contents for the modified markdown files.

Downloads

7

Readme

Table of Contents

Usage

example

프로젝트 설정 및 스크립트 설명

설치환경

  • Node.js 버전: 20.13.1

폴더 구조

scripts/
├── hooks/
│   ├── pre-commit
│   └── setup-hooks.sh
├── updateReadme.test.ts
└── updateReadme.ts
  • scripts/: 스크립트 파일들을 저장하는 디렉토리입니다.
    • hooks/: Git 훅 스크립트 파일들을 저장하는 디렉토리입니다.
      • pre-commit: 커밋 전에 실행되는 Git 훅 스크립트입니다.
      • setup-hooks.sh: Git 훅을 설정하는 스크립트입니다.
    • updateReadme.test.ts: updateReadme.ts 스크립트의 테스트 파일입니다.
    • updateReadme.ts: README.md 파일을 업데이트하는 TypeScript 스크립트입니다.

설치 및 초기 설정

package.json 설치 스크립트

프로젝트의 package.json 파일에는 다음과 같은 스크립트가 포함되어 있습니다:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "update-readme": "node --no-warnings=ExperimentalWarning --loader ts-node/esm scripts/updateReadme.ts",
    "setup-hooks": "sh scripts/setup-hooks.sh"
  }
}
  • update-readme: README.md 파일을 업데이트합니다.
  • setup-hooks: setup-hooks.sh 스크립트를 실행하여 Git 훅을 설정합니다.

설치 방법

프로젝트를 처음 설정할 때, 다음 명령을 실행하여 필요한 패키지를 설치합니다:

yarn install

그 후, Git 훅을 설정하기 위해 다음 명령을 실행합니다:

yarn run setup-hooks

추가 설정 파일

프로젝트의 루트 경로에 다음 파일들을 추가해야 합니다:

templateReadme.md

이 파일은 README.md 파일의 템플릿으로 사용됩니다. {updateReadme} 플레이스홀더는 updateReadme.ts 스크립트를 통해 동적 콘텐츠로 대체됩니다.

# Project Title

Some initial project information.

# Table of Contents
{updateReadme}

## Footer

Some footer information.

readmeConfig.json

이 파일은 updateReadme.ts 스크립트의 설정을 정의합니다. baseUrl은 README 파일 생성을 시작할 기본 경로를 지정하고, exclude는 제외할 폴더들을, order는 콘텐츠의 폴더 순서를 정의합니다. 또한 readmePathtemplatePath를 지정할 수 있습니다.

{
  "baseUrl": "./src",
  "exclude": ["scripts"],
  "order": ["troubleshooting", "dev_notes"],
  "readmePath": "./README.md",
  "templatePath": "./templateReadme.md"
}

각 파일 설명

pre-commit

이 스크립트는 Git의 pre-commit 훅으로, 커밋 전에 실행됩니다. 변경된 파일 중 .md 확장자를 가진 파일이 있는지 검사하여, 있으면 updateReadme 스크립트를 실행하여 README.md 파일을 업데이트합니다.

#!/bin/sh

echo "Running pre-commit hook..."

# 변경된 파일 중 .md 확장자를 가진 파일이 있는지 검사
md_files=$(git diff --cached --name-only | grep '\.md$')

if [ -z "$md_files" ]; then
    echo "No .md files detected, skipping update-readme script"
else
    echo ".md files detected:"
    echo "$md_files"
    echo "Running update-readme script..."

    # .md 파일이 있을 경우 README.md 업데이트 스크립트 실행
    yarn run update-readme

    if [ $? -eq 0 ]; then
        echo "update-readme script executed successfully"

        # readmeConfig.json 파일에서 readmePath 읽기
        readme_path=$(node -e "console.log(require('./readmeConfig.json').readmePath || './README.md')")

        # README.md 파일을 스테이징
        git add "$readme_path"

        if [ $? -eq 0 ]; then
            echo "$readme_path successfully added to the staging area"
        else
            echo "Failed to add $readme_path to the staging area"
            exit 1
        fi
    else
        echo "update-readme script failed"
        exit 1
    fi
fi

setup-hooks.sh

이 스크립트는 Git 훅을 설정하는 스크립트로, pre-commit 훅을 .git/hooks 디렉토리에 복사하고 운영 체제에 따라 적절한 실행 권한을 설정합니다.

#!/bin/sh

# Git 훅 디렉토리가 존재하지 않으면 생성
mkdir -p .git/hooks

# pre-commit 훅을 복사
cp src/scripts/hooks/pre-commit .git/hooks/pre-commit

# 운영 체제에 따라 권한 설정
OS="$(uname -s)"
case "${OS}" in
    CYGWIN*|MINGW*|MSYS*)
        echo "Detected Windows environment"
        # Git Bash에서 실행 권한을 설정하는 방법 (Windows)
        chmod +x .git/hooks/pre-commit
        ;;
    *)
        echo "Detected Unix-like environment"
        # Unix-like 시스템 (Linux, MacOS 등)에서 실행 권한 설정
        chmod +x .git/hooks/pre-commit
        ;;
esac

updateReadme.test.ts

이 파일은 updateReadme.ts 스크립트의 테스트를 위한 파일입니다.

import mock from 'mock-fs';
import { promises as fs } from 'fs';
import path from 'path';
import { generateMarkdownEntry, updateReadme } from './updateReadme';

const mockConfig = {
  baseUrl: './src',
  exclude: ['scripts'],
  order: ['troubleshooting', 'dev_notes'],
  readmePath: './README.md',
  templatePath: './templateReadme.md'
};

const mockTemplate = `
# Project Title

Some initial project information.

# Table of Contents
{updateReadme}

## Footer

Some footer information.
`;

describe('updateReadme functions', () => {
  const configFilePath = 'readmeConfig.json';

  beforeEach(() => {
    mock({
      'src/troubleshooting': {
        'example1.md': 'Content of example1',
      },
      'src/dev_notes': {
        'example2.md': 'Content of example2',
      },
      'readmeConfig.json': JSON.stringify(mockConfig),
      'templateReadme.md': mockTemplate,
      'README.md': '',
    });
  });

  afterEach(() => {
    mock.restore();
  });

  it('should generate markdown entries correctly', async () => {
    const markdown = await generateMarkdownEntry(
      'src',
      '',
      './src',
      1,
      mockConfig.exclude,
      mockConfig.order,
    );
    expect(markdown).toContain('## troubleshooting');
    expect(markdown).toContain('## dev_notes');
    expect(markdown).toContain(
      '- [example1](./src/troubleshooting/example1.md)',
    );
    expect(markdown).toContain('- [example2](./src/dev_notes/example2.md)');
  });

  it('should update README.md correctly', async () => {
    await updateReadme(configFilePath);
    const readmeContent = await fs.readFile(mockConfig.readmePath, 'utf-8');
    expect(readmeContent).toContain('# Project Title');
    expect(readmeContent).toContain('## troubleshooting');
    expect(readmeContent).toContain(
      '- [example1](./src/troubleshooting/example1.md)',
    );
    expect(readmeContent).toContain('## dev_notes');
    expect(readmeContent).toContain(
      '- [example2](./src/dev_notes/example2.md)',
    );
    expect(readmeContent).toContain('## Footer');
  });
});

updateReadme.ts

이 파일은 README.md 디렉토리 구조를 탐색하고, 특정 조건에 따라 README.md 파일을 갱신합니다.

import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import * as path from 'path';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

// ES 모듈에서 __dirname 및 __filename 설정
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// CLI 옵션 설정
const argv = yargs(hideBin(process.argv))
  .option('config', {
    alias: 'c',
    type: 'string',
    description: 'Path to the config file',
  })
  .help()
  .argv as { config?: string };

// 설정 파일 경로 결정
const configPath = argv.config || path.resolve('./readmeConfig.json');

async function loadConfig(configPath: string) {
  const configContent = await fs.readFile(configPath, 'utf-8');
  return JSON.parse(configContent);
}

export async function generateMarkdownEntry(
  dirPath: string,
  basePath: string = '',


  srcBasePath: string = '',
  level: number = 1,
  exclude: string[] = [],
  order: string[] = [],
): Promise<string> {
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  const folders = entries
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name);
  const files = entries
    .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
    .map((entry) => entry.name);

  const sortedFolders = [...folders].sort((a, b) => {
    const orderA = order.indexOf(a);
    const orderB = order.indexOf(b);
    if (orderA === -1 && orderB === -1) return a.localeCompare(b);
    if (orderA === -1) return 1;
    if (orderB === -1) return -1;
    return orderA - orderB;
  });

  let markdown = '';

  for (const folder of sortedFolders) {
    if (exclude.includes(folder)) {
      continue;
    }
    const filePath = path.join(dirPath, folder);
    const nestedMarkdown = await generateMarkdownEntry(
      filePath,
      path.join(basePath, folder),
      srcBasePath,
      level + 1,
      exclude,
      order,
    );
    if (nestedMarkdown) {
      if (level === 1) {
        markdown += `## ${folder}\n${nestedMarkdown}`;
      } else if (level === 2) {
        markdown += `### ${folder}\n${nestedMarkdown}`;
      } else {
        markdown += `\n#### ${folder}\n${nestedMarkdown}`;
      }
    }
  }

  for (const file of files) {
    const relativePath = path
      .join(srcBasePath, basePath, file)
      .replace(/\\/g, '/')
      .replace(/ /g, '%20');
    const markdownLink = `[${file.replace('.md', '')}](./${relativePath})`;
    markdown += `  - ${markdownLink}\n`;
  }

  return markdown;
}

export async function updateReadme(configPath: string) {
  const config = await loadConfig(configPath);
  const { baseUrl, exclude, order, readmePath: configReadmePath, templatePath: configTemplatePath } = config;

  const rootDir = path.resolve(); // 현재 작업 디렉토리
  const srcDir = path.join(rootDir, baseUrl);

  const readmePath = configReadmePath ? path.resolve(rootDir, configReadmePath) : path.join(rootDir, 'README.md');
  const templatePath = configTemplatePath ? path.resolve(rootDir, configTemplatePath) : path.join(rootDir, 'templateReadme.md');

  const markdownContent = await generateMarkdownEntry(
    srcDir,
    '',
    baseUrl,
    1,
    exclude,
    order,
  );

  const templateContent = await fs.readFile(templatePath, 'utf-8');

  const readmeContent = templateContent.replace(
    '{updateReadme}',
    markdownContent,
  );

  await fs.writeFile(readmePath, readmeContent);
}

// 설정 파일 경로를 인자로 전달하여 updateReadme 호출
updateReadme(configPath).catch(console.error);

이 문서에서는 프로젝트 설정 및 각 스크립트 파일에 대한 설명과 함께, --config 옵션을 사용하여 설정 파일 경로를 동적으로 처리하는 방법을 설명합니다. 이러한 구조는 프로젝트의 유연성을 높이며, 다양한 사용 사례에 맞게 동작합니다.

Footer