ブログ
@lunariajs/coreを深掘り:LunariaJSの核心アーキテクチャを理解する
LunariaJSコアライブラリのアーキテクチャ設計と実装原理を深く解説。ソースレベルでファイル解析、状態追跡、設定処理などのコアモジュールを理解し、カスタム拡張と高度な使い方の基盤を構築します。
LibDoc Team 2026年3月6日 LunariaJS 連載 135 分で読める
#LunariaJS
#ソースコード解析
#コアアーキテクチャ
#@lunariajs/core
#TypeScript
@lunariajs/coreを深掘り:LunariaJSの核心アーキテクチャを理解する
これまでの7つの入門記事で、LunariaJSのインストール、設定、CLIコマンド、ダッシュボード、Gitワークフロー、Starlight統合、CI/CD統合を包括的に学びました。ここからは、ソースレベルでLunariaJSのコアアーキテクチャと実装原理を深く掘り下げます。
💡 公式ドキュメント:LunariaJS 日本語ドキュメント - APIリファレンス
コアアーキテクチャの概要
全体アーキテクチャ図
LunariaJSのコアアーキテクチャは以下の層で構成されます:
┌─────────────────────────────────────────────────────────────────┐
│ CLI Layer │
│ (lunaria init/build/preview) │
├─────────────────────────────────────────────────────────────────┤
│ API Layer │
│ (Public API for external use) │
├─────────────────────────────────────────────────────────────────┤
│ Core Modules │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Config │ │ File │ │ Status │ │
│ │ System │ │ Parsers │ │ Tracker │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Git │ │ Dashboard │ │ Reporter │ │
│ │ Client │ │ Generator │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Infrastructure │
│ (File System, Git, Logger, Utils) │
└─────────────────────────────────────────────────────────────────┘
コアモジュールの役割
| モジュール | 役割 | コアファイル |
|---|---|---|
| Config System | 設定の読み込み、バリデーション、デフォルト値の処理 | config.ts |
| File Parsers | 各種形式のローカライズファイルの解析 | parsers/ |
| Status Tracker | 翻訳状態の追跡(done/outdated/missing) | status.ts |
| Git Client | Git操作のカプセル化(コミット履歴、ファイル状態の取得) | git.ts |
| Dashboard Generator | 静的HTMLダッシュボードの生成 | dashboard/ |
| Reporter | 状態レポートとログ出力の生成 | reporter.ts |
技術スタックの選択
LunariaJSは以下の技術スタックを採用:
| 技術 | 用途 | 選択理由 |
|---|---|---|
| TypeScript | 主要言語 | 型安全性、IDEサポートの向上 |
| Zod | Schemaバリデーション | 設定ファイルのバリデーション |
| Vite | ビルドツール | 高速なESモジュールサポート |
| simple-git | Git操作 | Node.js用Gitクライアント |
| handlebars | テンプレートエンジン | ダッシュボードHTML生成 |
ディレクトリ構造
@lunariajs/core/
├── src/
│ ├── index.ts # パブリックAPIエントリー
│ ├── config.ts # 設定システム
│ ├── status.ts # 状態追跡
│ ├── git.ts # Git操作
│ ├── reporter.ts # レポート生成
│ ├── parsers/ # ファイルパーサー
│ │ ├── index.ts
│ │ ├── json.ts
│ │ ├── yaml.ts
│ │ └── markdown.ts
│ ├── dashboard/ # ダッシュボード生成
│ │ ├── index.ts
│ │ ├── templates/
│ │ └── assets/
│ ├── types/ # 型定義
│ │ ├── config.ts
│ │ └── status.ts
│ └── utils/ # ユーティリティ関数
│ ├── file.ts
│ └── logger.ts
├── package.json
├── tsconfig.json
└── README.md
設定システム
設定型定義
LunariaJSは厳格なTypeScript型定義を使用:
// src/types/config.ts
/**
* サポートされるファイル形式
*/
export type FileFormat = 'json' | 'yaml' | 'md' | 'csv';
/**
* 言語設定
*/
export interface LocaleConfig {
/** 言語ラベル(例:'en', 'zh-cn') */
label: string;
/** テキスト方向 */
dir?: 'ltr' | 'rtl';
/** カスタムデータ */
[key: string]: unknown;
}
/**
* ファイルパターン設定
*/
export interface FilePattern {
/** ソース言語ファイルのパスパターン */
sourcePath: string;
/** 翻訳ファイルのパスパターン */
localizationPath: string;
/** インクルードするファイルのglobパターン */
include?: string[];
/** 除外するファイルのglobパターン */
exclude?: string[];
}
/**
* ダッシュボード設定
*/
export interface DashboardConfig {
/** 出力ディレクトリ */
outputDir: string;
/** ダッシュボードタイトル */
title?: string;
/** ダッシュボード説明 */
description?: string;
/** インターフェース言語 */
uiLanguage?: string;
/** カスタムCSSパス */
customCss?: string;
/** サイトURL(SEO用) */
site?: string;
}
/**
* LunariaJS完全設定
*/
export interface LunariaConfig {
/** ソース言語 */
sourceLanguage: string;
/** サポートするすべての言語 */
languages: string[];
/** 言語設定マッピング */
locales?: Record<string, LocaleConfig>;
/** ファイルパターンリスト */
files: FilePattern[];
/** ダッシュボード設定 */
dashboard?: DashboardConfig;
/** 無視するキーワード(プレースホルダー翻訳の検出用) */
ignoreKeywords?: string[];
}
設定読み込みメカニズム
設定読み込みプロセスは3つのフェーズで構成されます:
// src/config.ts
import { promises as fs } from 'fs';
import { pathExists } from './utils/file';
import { LunariaConfig } from './types/config';
import { defaultConfig } from './config/defaults';
/**
* 設定読み込み
*/
export async function loadConfig(
configPath: string = 'lunaria.config.json'
): Promise<LunariaConfig> {
// フェーズ1: ユーザー設定の読み込みを試みる
const userConfig = await loadUserConfig(configPath);
// フェーズ2: デフォルト設定とマージ
const mergedConfig = mergeConfig(defaultConfig, userConfig);
// フェーズ3: 設定をバリデーション
const validatedConfig = validateConfig(mergedConfig);
return validatedConfig;
}
/**
* ユーザー設定ファイルの読み込み
*/
async function loadUserConfig(
configPath: string
): Promise<Partial<LunariaConfig>> {
// JSONとJS/TS形式をサポート
if (configPath.endsWith('.json')) {
return loadJsonConfig(configPath);
} else if (configPath.endsWith('.js') || configPath.endsWith('.ts')) {
return loadJsConfig(configPath);
}
// デフォルトでJSONを試みる
return loadJsonConfig(configPath);
}
/**
* JSON設定の読み込み
*/
async function loadJsonConfig(
configPath: string
): Promise<Partial<LunariaConfig>> {
if (!(await pathExists(configPath))) {
return {};
}
const content = await fs.readFile(configPath, 'utf-8');
return JSON.parse(content);
}
/**
* JavaScript/TypeScript設定の読み込み
*/
async function loadJsConfig(
configPath: string
): Promise<Partial<LunariaConfig>> {
// ESモジュールを動的インポート
const module = await import(require.resolve(configPath, { paths: [process.cwd()] }));
return module.default || module;
}
/**
* 設定のマージ(ディープマージ)
*/
function mergeConfig(
defaultCfg: LunariaConfig,
userCfg: Partial<LunariaConfig>
): LunariaConfig {
return {
...defaultCfg,
...userCfg,
dashboard: {
...defaultCfg.dashboard,
...userCfg.dashboard,
},
};
}
設定のバリデーション
Zodを使用して厳格な設定バリデーションを実行:
// src/config/validation.ts
import { z } from 'zod';
/**
* 設定Schema定義
*/
const LunariaConfigSchema = z.object({
sourceLanguage: z.string().min(1, 'sourceLanguage is required'),
languages: z.array(z.string()).min(1, 'At least one language is required'),
files: z.array(
z.object({
sourcePath: z.string(),
localizationPath: z.string(),
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
})
).min(1, 'At least one file pattern is required'),
dashboard: z.object({
outputDir: z.string().default('lunaria-dashboard'),
title: z.string().optional(),
description: z.string().optional(),
}).optional(),
});
/**
* 設定をバリデーション
*/
export function validateConfig(config: unknown): LunariaConfig {
const result = LunariaConfigSchema.safeParse(config);
if (!result.success) {
const errors = result.error.errors.map(
(e) => `${e.path.join('.')}: ${e.message}`
);
throw new Error(`Invalid configuration:\n${errors.join('\n')}`);
}
// 追加の意味的バリデーション
validateSemantics(result.data);
return result.data;
}
/**
* 意味的バリデーション
*/
function validateSemantics(config: LunariaConfig): void {
// ソース言語が言語リストに含まれているかチェック
if (!config.languages.includes(config.sourceLanguage)) {
throw new Error(
`sourceLanguage "${config.sourceLanguage}" must be included in languages array`
);
}
// パスパターンのプレースホルダーをチェック
for (const pattern of config.files) {
validatePathPattern(pattern.sourcePath, 'sourcePath');
validatePathPattern(pattern.localizationPath, 'localizationPath');
}
}
/**
* パスパターンのバリデーション
*/
function validatePathPattern(pattern: string, fieldName: string): void {
// {slug}プレースホルダーを含む必要がある
if (!pattern.includes('{slug}')) {
console.warn(
`Warning: ${fieldName} "${pattern}" does not contain {slug} placeholder. ` +
`This may result in incorrect file matching.`
);
}
// localizationPathは{lang}プレースホルダーを含む必要がある
if (fieldName === 'localizationPath' && !pattern.includes('{lang}')) {
throw new Error(
`localizationPath "${pattern}" must contain {lang} placeholder`
);
}
}
ファイルパーサー
パーサーインターフェース設計
LunariaJSは統一されたパーサーインターフェースを定義:
// src/parsers/types.ts
/**
* ローカライズファイルの内容
*/
export interface LocalizedContent {
/** ファイルパス */
path: string;
/** 言語コード */
lang: string;
/** キーと値のペア */
entries: Record<string, string>;
/** 生の内容 */
raw: string;
/** 最終更新日時 */
lastModified?: Date;
}
/**
* ファイルパーサーインターフェース
*/
export interface FileParser {
/** サポートするファイル拡張子 */
extensions: string[];
/** ファイル内容を解析 */
parse(content: string, path: string): Promise<LocalizedContent>;
/** 内容をシリアライズ(ファイル生成用) */
stringify(data: Record<string, string>): string;
}
JSONパーサー
// src/parsers/json.ts
import { FileParser, LocalizedContent } from './types';
export const JsonParser: FileParser = {
extensions: ['.json'],
async parse(content: string, path: string): Promise<LocalizedContent> {
try {
const data = JSON.parse(content);
// ネストされたオブジェクトをフラット化
const entries = flattenObject(data);
return {
path,
lang: extractLangFromPath(path),
entries,
raw: content,
};
} catch (error) {
throw new Error(`Failed to parse JSON file ${path}: ${error}`);
}
},
stringify(data: Record<string, string>): string {
// フラット構造をネストされたオブジェクトに復元
const nested = unflattenObject(data);
return JSON.stringify(nested, null, 2);
},
};
/**
* ネストされたオブジェクトをフラット化
* 例: { a: { b: 'value' } } => { 'a.b': 'value' }
*/
function flattenObject(obj: Record<string, any>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'string') {
result[newKey] = value;
} else if (typeof value === 'object' && value !== null) {
Object.assign(result, flattenObject(value, newKey));
}
}
return result;
}
/**
* パスから言語コードを抽出
*/
function extractLangFromPath(path: string): string {
const match = path.match(/\/([a-z]{2}(-[a-z]{2})?)\//i);
return match ? match[1] : 'unknown';
}
YAMLパーサー
// src/parsers/yaml.ts
import { FileParser, LocalizedContent } from './types';
import * as yaml from 'js-yaml';
export const YamlParser: FileParser = {
extensions: ['.yaml', '.yml'],
async parse(content: string, path: string): Promise<LocalizedContent> {
try {
const data = yaml.load(content);
if (typeof data !== 'object' || data === null) {
throw new Error('YAML content must be an object');
}
const entries = flattenObject(data as Record<string, any>);
return {
path,
lang: extractLangFromPath(path),
entries,
raw: content,
};
} catch (error) {
throw new Error(`Failed to parse YAML file ${path}: ${error}`);
}
},
stringify(data: Record<string, string>): string {
const nested = unflattenObject(data);
return yaml.dump(nested, { indent: 2, lineWidth: -1 });
},
};
Markdownパーサー
Markdownファイルの処理は少し異なり、単純なキーと値のペアではないため:
// src/parsers/markdown.ts
import { FileParser, LocalizedContent } from './types';
import { matter } from 'gray-matter';
export const MarkdownParser: FileParser = {
extensions: ['.md', '.mdx'],
async parse(content: string, path: string): Promise<LocalizedContent> {
try {
// frontmatterを解析
const { data: frontmatter, content: body } = matter(content);
// frontmatterとbodyを異なるエントリーとして扱う
const entries: Record<string, string> = {
// frontmatterエントリー
...flattenObject(frontmatter || {}),
// bodyを全体として1つのエントリー
'_body': body.trim(),
};
return {
path,
lang: extractLangFromPath(path),
entries,
raw: content,
};
} catch (error) {
throw new Error(`Failed to parse Markdown file ${path}: ${error}`);
}
},
stringify(data: Record<string, string>): string {
const { _body, ...frontmatter } = data;
const nested = unflattenObject(frontmatter);
const frontmatterStr = Object.keys(nested).length > 0
? matter.stringify(_body || '', nested)
: _body || '';
return frontmatterStr;
},
};
パーサーの登録と選択
// src/parsers/index.ts
import { FileParser } from './types';
import { JsonParser } from './json';
import { YamlParser } from './yaml';
import { MarkdownParser } from './markdown';
/**
* パーサーレジストリ
*/
const parsers: FileParser[] = [
JsonParser,
YamlParser,
MarkdownParser,
];
/**
* ファイル拡張子からパーサーを取得
*/
export function getParser(filePath: string): FileParser | null {
const ext = filePath.toLowerCase();
for (const parser of parsers) {
if (parser.extensions.some(e => ext.endsWith(e))) {
return parser;
}
}
return null;
}
/**
* ファイルを解析
*/
export async function parseFile(
filePath: string,
content: string
): Promise<LocalizedContent> {
const parser = getParser(filePath);
if (!parser) {
// デフォルトでテキストパーサーを使用
return {
path: filePath,
lang: extractLangFromPath(filePath),
entries: { '_raw': content },
raw: content,
};
}
return parser.parse(content, filePath);
}
状態追跡エンジン
状態計算の核心ロジック
状態追跡はLunariaJSのコア機能です:
// src/status.ts
import { LunariaConfig } from './types/config';
import { FileStatus, TranslationStatus } from './types/status';
import { getFileLastModified } from './git';
/**
* 単一ファイルの翻訳状態を計算
*/
export async function calculateFileStatus(
sourcePath: string,
translationPath: string,
lang: string,
config: LunariaConfig
): Promise<FileStatus> {
// 翻訳ファイルの存在をチェック
const translationExists = await fileExists(translationPath);
if (!translationExists) {
return {
path: translationPath,
lang,
status: 'missing',
sourcePath,
};
}
// 最終更新時刻を取得
const sourceLastModified = await getFileLastModified(sourcePath);
const translationLastModified = await getFileLastModified(translationPath);
// タイムスタンプを比較
if (translationLastModified >= sourceLastModified) {
return {
path: translationPath,
lang,
status: 'done',
sourcePath,
lastModified: translationLastModified,
};
}
// 翻訳が古い
const daysOutdated = Math.floor(
(sourceLastModified.getTime() - translationLastModified.getTime()) /
(1000 * 60 * 60 * 24)
);
return {
path: translationPath,
lang,
status: 'outdated',
sourcePath,
lastModified: translationLastModified,
sourceLastModified,
daysOutdated,
};
}
/**
* すべてのファイルの翻訳状態を計算
*/
export async function calculateAllStatus(
config: LunariaConfig
): Promise<TranslationStatus> {
const status: TranslationStatus = {
sourceLanguage: config.sourceLanguage,
languages: {},
totalFiles: 0,
generatedAt: new Date(),
};
// 言語状態を初期化
for (const lang of config.languages) {
status.languages[lang] = {
total: 0,
done: 0,
outdated: 0,
missing: 0,
files: [],
};
}
// すべてのファイルパターンを走査
for (const pattern of config.files) {
const sourceFiles = await globFiles(pattern.sourcePath, pattern.include, pattern.exclude);
for (const sourceFile of sourceFiles) {
status.totalFiles++;
const slug = extractSlug(sourceFile, pattern.sourcePath);
// 各ターゲット言語の状態を計算
for (const lang of config.languages) {
if (lang === config.sourceLanguage) continue;
const translationPath = resolveTranslationPath(
pattern.localizationPath,
lang,
slug
);
const fileStatus = await calculateFileStatus(
sourceFile,
translationPath,
lang,
config
);
status.languages[lang].total++;
status.languages[lang].files.push(fileStatus);
switch (fileStatus.status) {
case 'done':
status.languages[lang].done++;
break;
case 'outdated':
status.languages[lang].outdated++;
break;
case 'missing':
status.languages[lang].missing++;
break;
}
}
}
}
return status;
}
/**
* 翻訳完了率を計算
*/
export function calculateProgress(status: TranslationStatus, lang: string): number {
const langStatus = status.languages[lang];
if (!langStatus || langStatus.total === 0) return 0;
return Math.round((langStatus.done / langStatus.total) * 100);
}
Git操作のカプセル化
// src/git.ts
import simpleGit, { SimpleGit } from 'simple-git';
let gitClient: SimpleGit | null = null;
/**
* Gitクライアントインスタンスを取得
*/
function getGitClient(): SimpleGit {
if (!gitClient) {
gitClient = simpleGit();
}
return gitClient;
}
/**
* ファイルの最終更新日時を取得
*/
export async function getFileLastModified(filePath: string): Promise<Date> {
const git = getGitClient();
try {
// ファイルの最終コミットを取得
const log = await git.log(['-1', '--format=%ct', '--', filePath]);
if (log.latest) {
const timestamp = parseInt(log.latest.hash, 10);
return new Date(timestamp * 1000);
}
// Git履歴がない場合、ファイルシステムの時刻を使用
const stats = await fs.stat(filePath);
return stats.mtime;
} catch (error) {
// Git操作失敗、ファイルシステムの時刻を使用
const stats = await fs.stat(filePath);
return stats.mtime;
}
}
/**
* ファイルの完全なコミット履歴を取得
*/
export async function getFileHistory(
filePath: string,
limit: number = 10
): Promise<FileCommit[]> {
const git = getGitClient();
try {
const log = await git.log(['-' + limit, '--format=%H|%ct|%an|%s', '--', filePath]);
return log.all.map(commit => {
const [hash, timestamp, author, message] = commit.hash.split('|');
return {
hash,
date: new Date(parseInt(timestamp, 10) * 1000),
author,
message,
};
});
} catch (error) {
return [];
}
}
/**
* Gitリポジトリかどうかをチェック
*/
export async function isGitRepository(): Promise<boolean> {
const git = getGitClient();
try {
await git.status();
return true;
} catch {
return false;
}
}
ダッシュボード生成
テンプレートシステム
LunariaJSはHandlebarsテンプレートエンジンを使用してダッシュボードを生成:
// src/dashboard/generator.ts
import Handlebars from 'handlebars';
import { TranslationStatus } from '../types/status';
import { readTemplate, copyAssets } from './utils';
// Handlebarsヘルパーを登録
Handlebars.registerHelper('progressBar', (progress: number) => {
const color = progress >= 90 ? 'green' : progress >= 70 ? 'yellow' : 'red';
return new Handlebars.SafeString(
`<div class="progress-bar">
<div class="progress-fill ${color}" style="width: ${progress}%"></div>
</div>`
);
});
Handlebars.registerHelper('statusIcon', (status: string) => {
const icons: Record<string, string> = {
done: '✅',
outdated: '⚠️',
missing: '❌',
};
return icons[status] || '❓';
});
Handlebars.registerHelper('formatDate', (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(new Date(date));
});
/**
* ダッシュボードHTMLを生成
*/
export async function generateDashboard(
status: TranslationStatus,
outputDir: string
): Promise<void> {
// テンプレートを読み込み
const indexTemplate = await readTemplate('index.html');
const compiledTemplate = Handlebars.compile(indexTemplate);
// テンプレートデータを準備
const templateData = {
title: 'Localization Status',
generatedAt: new Date().toISOString(),
sourceLanguage: status.sourceLanguage,
languages: Object.entries(status.languages).map(([lang, data]) => ({
code: lang,
total: data.total,
done: data.done,
outdated: data.outdated,
missing: data.missing,
progress: Math.round((data.done / data.total) * 100),
files: data.files,
})),
totalFiles: status.totalFiles,
};
// HTMLをレンダリング
const html = compiledTemplate(templateData);
// ファイルに書き込み
await fs.writeFile(path.join(outputDir, 'index.html'), html);
// 静的アセットをコピー
await copyAssets(outputDir);
// JSONデータを書き込み(API用)
await fs.writeFile(
path.join(outputDir, 'data', 'status.json'),
JSON.stringify(status, null, 2)
);
}
テンプレート例
<!-- src/dashboard/templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<link rel="stylesheet" href="assets/styles.css">
</head>
<body>
<header>
<h1>🌙 {{title}}</h1>
<p class="subtitle">Generated at {{formatDate generatedAt}}</p>
</header>
<main>
<section class="overview">
<h2>Translation Progress</h2>
<div class="language-grid">
{{#each languages}}
<div class="language-card">
<h3>{{code}}</h3>
{{progressBar progress}}
<p class="progress-text">{{progress}}% Complete</p>
<ul class="stats">
<li>✅ Done: {{done}}</li>
<li>⚠️ Outdated: {{outdated}}</li>
<li>❌ Missing: {{missing}}</li>
</ul>
</div>
{{/each}}
</div>
</section>
<section class="files">
<h2>File Status</h2>
{{#each languages}}
<details>
<summary>{{code}} ({{done}}/{{total}})</summary>
<table>
<thead>
<tr>
<th>File</th>
<th>Status</th>
<th>Last Modified</th>
</tr>
</thead>
<tbody>
{{#each files}}
<tr>
<td>{{path}}</td>
<td>{{statusIcon status}} {{status}}</td>
<td>{{formatDate lastModified}}</td>
</tr>
{{/each}}
</tbody>
</table>
</details>
{{/each}}
</section>
</main>
<footer>
<p>Powered by <a href="https://lunaria.libdoc.top/">LunariaJS</a></p>
</footer>
</body>
</html>
API設計分析
パブリックAPI
LunariaJSは以下のパブリックAPIをエクスポート:
// src/index.ts
export { loadConfig } from './config';
export { calculateAllStatus, calculateProgress } from './status';
export { generateDashboard } from './dashboard/generator';
export { getFileLastModified, getFileHistory } from './git';
export { parseFile, getParser } from './parsers';
// 型のエクスポート
export type { LunariaConfig, FilePattern, DashboardConfig } from './types/config';
export type { TranslationStatus, FileStatus } from './types/status';
export type { LocalizedContent, FileParser } from './parsers/types';
使用例
import {
loadConfig,
calculateAllStatus,
generateDashboard,
calculateProgress,
} from '@lunariajs/core';
async function main() {
// 1. 設定を読み込み
const config = await loadConfig('lunaria.config.json');
// 2. 翻訳状態を計算
const status = await calculateAllStatus(config);
// 3. ダッシュボードを生成
await generateDashboard(status, config.dashboard?.outputDir || 'lunaria-dashboard');
// 4. 進捗レポートを出力
for (const lang of Object.keys(status.languages)) {
const progress = calculateProgress(status, lang);
console.log(`${lang}: ${progress}% complete`);
}
}
main().catch(console.error);
ソースコード読解ガイド
エントリーファイル分析
src/index.tsから開始します。これはパッケージのエントリーポイントです:
- 依存関係のインポート:設定、状態計算、ダッシュボード生成などのモジュールをロード
- APIのエクスポート:外部使用のための関数と型を公開
- バージョン情報:現在のバージョン番号をエクスポート
コアモジュールナビゲーション
推奨読解順序:
src/types/- データ構造を理解src/config.ts- 設定読み込みロジックsrc/git.ts- Git操作のカプセル化src/status.ts- 状態計算の核心src/dashboard/- ダッシュボード生成
デバッグテクニック
// デバッグログを有効化
process.env.LUNARIA_DEBUG = 'true';
// verboseモードを使用
npx lunaria build --verbose
拡張開発の基礎
カスタムパーサーの実装
// カスタムCSVパーサー
import { FileParser, LocalizedContent } from '@lunariajs/core/parsers/types';
export const CsvParser: FileParser = {
extensions: ['.csv'],
async parse(content: string, path: string): Promise<LocalizedContent> {
const lines = content.split('\n');
const entries: Record<string, string> = {};
for (const line of lines.slice(1)) { // ヘッダーをスキップ
const [key, value] = line.split(',');
if (key && value) {
entries[key.trim()] = value.trim();
}
}
return {
path,
lang: extractLangFromPath(path),
entries,
raw: content,
};
},
stringify(data: Record<string, string>): string {
const lines = ['key,value'];
for (const [key, value] of Object.entries(data)) {
lines.push(`${key},${value}`);
}
return lines.join('\n');
},
};
カスタム状態計算戦略
// カスタム状態計算ロジック
export async function customStatusCalculator(
sourcePath: string,
translationPath: string,
options: { threshold: number }
): Promise<FileStatus> {
// カスタムロジックで状態を判断
const sourceContent = await fs.readFile(sourcePath, 'utf-8');
const translationContent = await fs.readFile(translationPath, 'utf-8');
// 内容の類似度を比較
const similarity = calculateSimilarity(sourceContent, translationContent);
if (similarity < options.threshold) {
return { status: 'outdated', similarity };
}
return { status: 'done', similarity };
}
貢献ガイド
- リポジトリをFork:
https://github.com/withastro/lunaria - ブランチを作成:
git checkout -b feature/your-feature - コードを記述:TypeScriptとESLint規範に従う
- テストを実行:
npm test - PRを提出:変更内容を明確に説明
まとめ
この記事では、@lunariajs/coreのコアアーキテクチャを深く掘り下げました:
| モジュール | コア機能 | 主要技術 |
|---|---|---|
| 設定システム | 設定の読み込み、バリデーション、マージ | Zod Schemaバリデーション |
| ファイルパーサー | JSON/YAML/Markdownの解析 | 統一インターフェース設計 |
| 状態追跡 | 翻訳状態の計算 | Gitタイムスタンプ比較 |
| Gitクライアント | Git操作のカプセル化 | simple-git |
| ダッシュボード生成 | 静的HTML生成 | Handlebarsテンプレート |
重要なポイント:
- TypeScriptによる型安全性
- モジュラー設計、責任の明確な分離
- Zodによる設定バリデーション
- Git履歴による翻訳状態追跡
- テンプレートエンジンによるダッシュボード生成
次のステップ
次の記事では、LunariaJSの高度な設定とカスタム戦略について解説し、以下を学びます:
- エンタープライズレベルの複雑設定の処理
- 複数リポジトリのローカライズ管理
- カスタムローカライズ戦略の設計
- 大規模プロジェクトのパフォーマンス最適化
お楽しみに!
💡 おすすめ読書: