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

@c-time/frelio-content-ops

v1.2.1

Published

Content lifecycle operations for Frelio CMS — view index, dashboard metadata, content type list

Readme

@c-time/frelio-content-ops

コンテンツライフサイクルに伴う派生ファイルの自動更新ロジック。

ビューインデックス (_index.json)、ダッシュボードメタデータ (_dashboard.json)、コンテンツタイプ一覧 (content_types.json) の更新を純粋関数として提供する。

Install

npm install @c-time/frelio-content-ops

使い方

ContentStorePort を実装する

パッケージは I/O を行わない。ContentStorePortreadJson のみ)を呼び出し側で実装する。

import type { ContentStorePort } from '@c-time/frelio-content-ops'

// ローカルファイルシステム用
const store: ContentStorePort = {
  readJson: async <T>(filePath: string): Promise<T | null> => {
    try {
      const raw = await fs.readFile(path.join(repoRoot, filePath), 'utf-8')
      return JSON.parse(raw) as T
    } catch {
      return null
    }
  },
}

// ブラウザ(GitHub API)用
const store: ContentStorePort = {
  readJson: async <T>(filePath: string): Promise<T | null> => {
    try {
      return await githubClient.fetchJson<T>(filePath, branch)
    } catch {
      return null
    }
  },
}

操作関数

import {
  computeViewIndexUpsert,
  computeViewIndexRemoval,
  computeDashboardMetadataOnSave,
  computeDashboardMetadataOnDelete,
  computeContentTypeListAdd,
  computeContentTypeListRemove,
  rebuildAllIndexes,
} from '@c-time/frelio-content-ops'

すべての関数は (store, params) => Promise<FileChange[]> または Promise<FileChange> を返す。書き込みは呼び出し側で行う。


ファイルウォッチャーの実装例

contents-repo をローカルでクローンし、コンテンツ JSON を編集したら自動でインデックスを再生成する仕組み。

前提

  • chokidar でファイル変更を監視
  • 変更されたコンテンツファイルのパスから contentTypeId, basePath, contentId を抽出
  • computeViewIndexUpsert / computeViewIndexRemoval で派生ファイルを再計算
  • 結果をローカルファイルに書き戻す

依存パッケージ

npm install chokidar @c-time/frelio-content-ops

スクリプト例 (scripts/watch-content.ts)

import chokidar from 'chokidar'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import {
  computeViewIndexUpsert,
  computeViewIndexRemoval,
  computeDashboardMetadataOnSave,
  computeDashboardMetadataOnDelete,
  type ContentStorePort,
  type ContentData,
  type BasePath,
  type FileChange,
} from '@c-time/frelio-content-ops'

const REPO_ROOT = path.resolve(import.meta.dirname, '..')

// --- ContentStorePort: ローカル FS 実装 ---

const store: ContentStorePort = {
  readJson: async <T>(filePath: string): Promise<T | null> => {
    try {
      const raw = await fs.readFile(path.join(REPO_ROOT, filePath), 'utf-8')
      return JSON.parse(raw) as T
    } catch {
      return null
    }
  },
}

// --- パスからコンテンツ情報を抽出 ---

type ContentFileInfo = {
  basePath: BasePath
  contentTypeId: string
  contentId: string
}

function parseContentPath(filePath: string): ContentFileInfo | null {
  // frelio-data/site/contents/{published|private}/{contentTypeId}/{contentId}.json
  const rel = path.relative(REPO_ROOT, filePath).replace(/\\/g, '/')
  const match = rel.match(
    /^frelio-data\/site\/contents\/(published|private)\/([^/]+)\/([^/_][^/]*)\.json$/
  )
  if (!match) return null
  return {
    basePath: match[1] as BasePath,
    contentTypeId: match[2],
    contentId: match[3],
  }
}

// --- FileChange をディスクに書き込む ---

async function applyChanges(changes: FileChange[]): Promise<void> {
  for (const change of changes) {
    const fullPath = path.join(REPO_ROOT, change.path)
    if (change.delete) {
      await fs.unlink(fullPath).catch(() => {})
    } else {
      await fs.mkdir(path.dirname(fullPath), { recursive: true })
      await fs.writeFile(fullPath, change.content)
    }
  }
}

// --- メイン ---

const watchTarget = path.join(REPO_ROOT, 'frelio-data/site/contents')

console.log(`Watching: ${watchTarget}`)

const watcher = chokidar.watch(watchTarget, {
  ignoreInitial: true,
  // _index*.json の変更は無視(自分が書いた派生ファイルに反応しないように)
  ignored: /_index.*\.json$/,
})

watcher.on('change', async (filePath) => {
  const info = parseContentPath(filePath)
  if (!info) return

  console.log(`[change] ${info.basePath}/${info.contentTypeId}/${info.contentId}`)

  const content = await store.readJson<ContentData>(
    `frelio-data/site/contents/${info.basePath}/${info.contentTypeId}/${info.contentId}.json`
  )
  if (!content) return

  const changes = await computeViewIndexUpsert(store, {
    basePath: info.basePath,
    contentTypeId: info.contentTypeId,
    content,
  })
  await applyChanges(changes)
  console.log(`  -> Updated ${changes.length} index file(s)`)
})

watcher.on('unlink', async (filePath) => {
  const info = parseContentPath(filePath)
  if (!info) return

  console.log(`[delete] ${info.basePath}/${info.contentTypeId}/${info.contentId}`)

  const changes = await computeViewIndexRemoval(store, {
    basePath: info.basePath,
    contentTypeId: info.contentTypeId,
    contentId: info.contentId,
  })
  await applyChanges(changes)
  console.log(`  -> Updated ${changes.length} index file(s)`)
})

console.log('Ready. Edit content JSON files to trigger index regeneration.')

package.json に追加

{
  "scripts": {
    "watch:content": "tsx scripts/watch-content.ts"
  }
}

実行

npm run watch:content

コンテンツ JSON を編集・保存すると、対応する _index.json / _index.{viewId}.json が自動更新される。

注意事項

  • _index*.json の変更は ignored で除外している(無限ループ防止)
  • ダッシュボードメタデータの自動更新が必要なら computeDashboardMetadataOnSave / OnDelete も呼ぶ
  • Git commit は手動。自動 commit したい場合は simple-git 等を組み合わせる