mcpField
J
javascript-refactoring

javascript-refactoring

github.com/github開発5.5k68 回インストール今日更新
リポジトリ

javascript-refactoring について

gh-aw ワークフローの JavaScript をリファクタリングする開発者向けガイドです。CommonJS の `.cjs` モジュール構成、Go への埋め込みとバンドル、テスト方法を説明します。

主な機能

  1. 01CommonJS モジュールの規約
  2. 02Go への埋め込みとバンドル
  3. 03ユーティリティとスクリプトのパターン
  4. 04Vitest のテスト指針
  5. 05フォーマットと検証のチェックリスト

活用シーン

  1. 01ワークフローの JavaScript を役割別のモジュールに分割する
  2. 02共有ユーティリティやトップレベルのワークフロースクリプトを追加する
  3. 03`.cjs` の変更をテストして検証する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/github/gh-aw --skill javascript-refactoring

インストール後、Claude Code で/javascript-refactoringを入力して読み込まれたか確認します。

詳細

JavaScript コードのリファクタリングガイド

このガイドでは、gh-aw の JavaScript を個別の .cjs ファイルにリファクタリングする方法を説明します。

概要

gh-aw は、GitHub Actions のワークフロー内で JavaScript に CommonJS モジュール(.cjs)を使用します。これらのファイルは次のように扱われます。

  • //go:embed ディレクティブを使用して Go バイナリに埋め込まれます
  • ローカルの require() 呼び出しをインライン化するカスタム JavaScript バンドラーを使用してバンドルされます
  • actions/github-script@v8 を使用して GitHub Actions で実行されます
トップレベルスクリプトのパターン

ワークフローで直接実行するトップレベルの .cjs スクリプトは、次のパターンに従います。

✅ 正しいパターン - main をエクスポートするが、呼び出さない:

async function main() {
  // Script logic here
  core.info("Running the script");
}

module.exports = { main };

❌ 誤ったパターン - ファイル内で main を呼び出さない:

async function main() {
  // Script logic here
  core.info("Running the script");
}

await main(); // ❌ Don't do this!

module.exports = { main };

このパターンを使う理由

  • バンドラーは、GitHub Actions でのインライン実行時に await main() を自動的に挿入します
  • これにより、スクリプトをインポート(テスト用)と実行(ワークフロー用)の両方に使用できます
  • モジュールの定義と実行を明確に分離できます
  • テストでモックを使って main() をインポートして呼び出せるため、テストがしやすくなります

トップレベルスクリプトの例:

  • create_issue.cjs - GitHub のイシューを作成します
  • add_comment.cjs - イシューや PR にコメントを追加します
  • add_labels.cjs - イシューや PR にラベルを追加します
  • update_project.cjs - GitHub Projects を更新します

これらのファイルはすべて main をエクスポートしますが、直接呼び出しません。

ステップ 1:新しい .cjs ファイルを作成する

新しいファイルを /home/runner/work/gh-aw/gh-aw/pkg/workflow/js/ に、内容がわかる名前を付けて作成します。

ファイル名の規則:

  • ファイル名には snake_case を使用します(例:sanitize_content.cjs、load_agent_output.cjs)
  • .cjs 拡張子を使用します(CommonJS モジュール)
  • モジュールの目的が明確に伝わる名前を選びます

ファイル構成の例:

// @ts-check
/// <reference types="@actions/github-script" />

/**
 * Brief description of what this module does
 */

/**
 * Function documentation
 * @param {string} input - Description of parameter
 * @returns {string} Description of return value
 */
function myFunction(input) {
  // Implementation
  return input;
}

// Export the function(s)
module.exports = {
  myFunction,
};

重要なポイント:

  • TypeScript のチェックに // @ts-check を含めます
  • GitHub Actions の型に /// <reference types="@actions/github-script" /> を含めます
  • ドキュメントには JSDoc コメントを使用します
  • module.exports = { ... } を使用して関数をエクスポートします
  • @actions/core や @actions/github はインポートしないでください。これらは GitHub Actions でグローバルに利用できます

ステップ 2:テストを追加する

同じベース名に .test.cjs を付けたテストファイルを作成します。

例:pkg/workflow/js/my_module.test.cjs

import { describe, it, expect, beforeEach, vi } from "vitest";

// Mock the global objects that GitHub Actions provides
const mockCore = {
  debug: vi.fn(),
  info: vi.fn(),
  warning: vi.fn(),
  error: vi.fn(),
  setFailed: vi.fn(),
  setOutput: vi.fn(),
  summary: {
    addRaw: vi.fn().mockReturnThis(),
    write: vi.fn().mockResolvedValue(),
  },
};

// Set up global mocks before importing the module
global.core = mockCore;

describe("myFunction", () => {
  beforeEach(() => {
    // Reset mocks before each test
    vi.clearAllMocks();
  });

  it("should handle basic input", async () => {
    // Import the module to test
    const { myFunction } = await import("./my_module.cjs");
    
    const result = myFunction("test input");
    
    expect(result).toBe("expected output");
  });

  it("should handle edge cases", async () => {
    const { myFunction } = await import("./my_module.cjs");
    
    const result = myFunction("");
    
    expect(result).toBe("");
  });
});

テストのガイドライン:

  • テストフレームワークには vitest を使用します
  • 必要に応じて core と github のグローバルをモックします
  • モジュールの読み込み前にモックを設定できるよう、動的インポート(await import())を使用します
  • テストを分離するため、beforeEach でモックをクリアします
  • 成功するケースとエラー処理の両方をテストします
  • pkg/workflow/js/*.test.cjs ファイルにある既存のテストパターンに従います

テストを実行する:

make test-js

ステップ 3:Go に埋め込み変数を追加する

適切な Go ファイルに //go:embed ディレクティブと変数を追加します。

共有ユーティリティ関数の場合(複数のスクリプトで使用する場合):

pkg/workflow/js.go に追加します。

//go:embed js/my_module.cjs
var myModuleScript string

次に、GetJavaScriptSources() 関数に追加します。

func GetJavaScriptSources() map[string]string {
	return map[string]string{
		"sanitize_content.cjs":       sanitizeContentScript,
		"sanitize_label_content.cjs": sanitizeLabelContentScript,
		"sanitize_workflow_name.cjs": sanitizeWorkflowNameScript,
		"load_agent_output.cjs":      loadAgentOutputScript,
		"staged_preview.cjs":         stagedPreviewScript,
		"is_truthy.cjs":              isTruthyScript,
		"my_module.cjs":              myModuleScript,  // Add this line
	}
}
メインスクリプトの場合(バンドリングを使用するトップレベルスクリプト):

pkg/workflow/scripts.go に追加します。

//go:embed js/my_script.cjs
var myScriptSource string

次に、バンドリングを行う getter 関数を作成します。

var (
	myScript     string
	myScriptOnce sync.Once
)

// getMyScript returns the bundled my_script script
// Bundling is performed on first access and cached for subsequent calls
func getMyScript() string {
	myScriptOnce.Do(func() {
		sources := GetJavaScriptSources()
		bundled, err := BundleJavaScriptFromSources(myScriptSource, sources, "")
		if err != nil {
			scriptsLog.Printf("Bundling failed for my_script, using source as-is: %v", err)
			// If bundling fails, use the source as-is
			myScript = myScriptSource
		} else {
			myScript = bundled
		}
	})
	return myScript
}

重要:

  • js.go の変数は、他のスクリプトにバンドルされる共有ユーティリティ用です
  • scripts.go の変数は、バンドラーで依存関係をインライン化するメインスクリプト用です
  • scripts.go での遅延バンドリングには sync.Once パターンを使用します
  • バンドラーは実行時に、ローカルの require() 呼び出しをすべてインライン化します

ステップ 4:バンドラーに登録する(共有ユーティリティを作成する場合)

require() を介して他のスクリプトで使用する共有ユーティリティを作成する場合は、ステップ 3 で GetJavaScriptSources() のマップに追加することで、自動的に利用可能になります。

バンドラーの処理:

  1. 任意のスクリプト内にある require('./my_module.cjs') を検出します
  2. GetJavaScriptSources() のマップでファイルを検索します
  3. 必要なモジュールの内容をインライン化します
  4. require() 文を削除します
  5. 同じモジュールが複数回必要な場合は重複を排除します

バンドラーへの追加登録は不要です。ファイルが GetJavaScriptSources() のマップに含まれていることを確認してください。

ステップ 5:ほかの JavaScript ファイルでローカルの require を使用する

ほかの JavaScript ファイルで新しいモジュールを使用するには、CommonJS の require() を使用します。

別の .cjs ファイルでの使用例:

// @ts-check
/// <reference types="@actions/github-script" />

const { myFunction } = require("./my_module.cjs");

async function main() {
  const result = myFunction("some input");
  core.info(`Result: ${result}`);
}

module.exports = { main };

**重要:**トップレベルスクリプトでは main をエクスポートしますが、直接呼び出してはいけません。GitHub Actions でインライン実行する際に、バンドラーが await main() を挿入します。

require のガイドライン:

  • ./ で始まる相対パスを使用します
  • .cjs 拡張子を含めます
  • 特定の関数をインポートするには分割代入を使用します
  • バンドラーはコンパイル時に、require したモジュールをインライン化します

複数の require の例:

const { sanitizeContent } = require("./sanitize_content.cjs");
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { generateStagedPreview } = require("./staged_preview.cjs");

完全な例:新しいユーティリティモジュールの作成

新しい format_timestamp.cjs ユーティリティを作成する手順を見ていきましょう。

1. ファイルを作成します:pkg/workflow/js/format_timestamp.cjs
// @ts-check
/// <reference types="@actions/github-script" />

/**
 * Formats a timestamp to ISO 8601 format
 * @param {Date|string|number} timestamp - Timestamp to format
 * @returns {string} ISO 8601 formatted timestamp
 */
function formatTimestamp(timestamp) {
  const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
  return date.toISOString();
}

/**
 * Formats a timestamp to a human-readable string
 * @param {Date|string|number} timestamp - Timestamp to format
 * @returns {string} Human-readable timestamp
 */
function formatTimestampHuman(timestamp) {
  const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
  return date.toLocaleString('en-US', { 
    dateStyle: 'medium', 
    timeStyle: 'short' 
  });
}

module.exports = {
  formatTimestamp,
  formatTimestampHuman,
};
2. テストを作成します:pkg/workflow/js/format_timestamp.test.cjs
import { describe, it, expect } from "vitest";

describe("formatTimestamp", () => {
  it("should format Date object to ISO 8601", async () => {
    const { formatTimestamp } = await import("./format_timestamp.cjs");
    const date = new Date('2024-01-15T12:30:00Z');
    
    const result = formatTimestamp(date);
    
    expect(result).toBe('2024-01-15T12:30:00.000Z');
  });

  it("should format timestamp number to ISO 8601", async () => {
    const { formatTimestamp } = await import("./format_timestamp.cjs");
    const timestamp = 1705323000000; // Jan 15, 2024 12:30:00 UTC
    
    const result = formatTimestamp(timestamp);
    
    expect(result).toBe('2024-01-15T12:30:00.000Z');
  });
});

describe("formatTimestampHuman", () => {
  it("should format Date object to human-readable string", async () => {
    const { formatTimestampHuman } = await import("./format_timestamp.cjs");
    const date = new Date('2024-01-15T12:30:00Z');
    
    const result = formatTimestampHuman(date);
    
    expect(result).toContain('Jan');
    expect(result).toContain('15');
    expect(result).toContain('2024');
  });
});
3. pkg/workflow/js.go に追加します:
//go:embed js/format_timestamp.cjs
var formatTimestampScript string

func GetJavaScriptSources() map[string]string {
	return map[string]string{
		// ... existing entries ...
		"format_timestamp.cjs": formatTimestampScript,
	}
}
4. 別のスクリプトで使用します:
// @ts-check
/// <reference types="@actions/github-script" />

const { formatTimestamp } = require("./format_timestamp.cjs");

async function main() {
  const now = new Date();
  core.info(`Current time: ${formatTimestamp(now)}`);
}

module.exports = { main };

**注:**スクリプトは main をエクスポートしますが、呼び出しません。GitHub Actions でスクリプトをインライン実行する際に、バンドラーが await main() を挿入します。

5. ビルドしてテストします:
# Format the code
make fmt-cjs

# Run JavaScript tests
make test-js

# Run Go tests (includes bundler tests)
make test-unit

# Build the binary (embeds JavaScript files)
make build

検証チェックリスト

リファクタリングしたコードをコミットする前に、次の項目を確認してください。

  • 新しい .cjs ファイルを pkg/workflow/js/ に作成した
  • 対応する .test.cjs ファイルにテストを作成した
  • make test-js でテストが成功する
  • pkg/workflow/js.go または pkg/workflow/scripts.go に埋め込み変数を追加した
  • ユーティリティの場合:GetJavaScriptSources() マップに追加した
  • メインスクリプトの場合:sync.Once を使ったバンドリング用ゲッター関数を作成した
  • ほかのファイルでローカルの require() 文が正しく動作する
  • make fmt-cjs でコードをフォーマットした
  • make lint-cjs でコードをリントした
  • make test-unit ですべての Go テストが成功する
  • make build でビルドが成功する

よくあるパターン

パターン 1:共有ユーティリティ関数

再利用可能な関数を提供する sanitize_content.cjs や load_agent_output.cjs などのファイル:

  • //go:embed を使って js.go に追加します
  • GetJavaScriptSources() マップに追加します
  • ほかのスクリプトで require() を使って呼び出します
パターン 2:メインワークフロースクリプト

トップレベルスクリプトである create_issue.cjs や add_labels.cjs などのファイル:

  • //go:embed を使い、xxxSource 変数として scripts.go に追加します
  • sync.Once パターンを使ったバンドリング用ゲッター関数を作成します
  • これらのスクリプトでは、GetJavaScriptSources() にあるユーティリティを require() できます
  • main 関数をエクスポートする必要がありますが、呼び出してはいけません。実行時にバンドラーが await main() を挿入します
パターン 3:ログパーサー

AI エンジンのログを解析する parse_claude_log.cjs などのファイル:

  • //go:embed を使って js.go に追加します
  • GetLogParserScript() 関数にケースを追加します
  • ワークフローのコンパイルシステムで使用されます

トラブルシューティング

問題:「required file not found in sources」

**原因:**ファイルが GetJavaScriptSources() マップに追加されていません

**解決策:**ファイルを pkg/workflow/js.go のマップに追加します

問題:「core is not defined」というエラーでテストが失敗する

**原因:**グローバルモックが不足しています

**解決策:**モジュールをインポートする前に、適切なモックを追加します。

global.core = mockCore;
global.github = mockGithub;
問題:循環依存関係が原因でバンドラーが失敗する

**原因:**ファイル A がファイル B を要求し、ファイル B がファイル A を要求しています

**解決策:**循環依存関係を解消するように構成を変更するか、モジュールを統合します

問題:再ビルド後も変更が反映されない

**原因:**Go のビルドキャッシュが埋め込みファイルの変更を認識していません

解決策:

make clean
make build

参考資料

  • バンドラーの実装:pkg/workflow/bundler.go
  • JavaScript ソースのレジストリ:pkg/workflow/js.go
  • スクリプトのバンドリング:pkg/workflow/scripts.go
  • 既存のテスト例:pkg/workflow/js/*.test.cjs
  • GitHub Actions スクリプトのドキュメント:actions/toolkit

コメント