@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 を行わない。ContentStorePort(readJson のみ)を呼び出し側で実装する。
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等を組み合わせる
