Claude-skill-registry javascript-refactoring
Instructions for refactoring JavaScript code into separate files
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/javascript-refactoring" ~/.claude/skills/majiayu000-claude-skill-registry-javascript-refactoring && rm -rf "$T"
skills/data/javascript-refactoring/SKILL.mdJavaScript Code Refactoring Guide
This guide explains how to refactor JavaScript code into a separate
.cjs file in the gh-aw repository. Follow these steps when extracting shared functionality or creating new JavaScript modules.
Overview
The gh-aw project uses CommonJS modules (
.cjs files) for JavaScript code that runs in GitHub Actions workflows. These files are:
- Embedded in the Go binary using
directives//go:embed - Bundled using a custom JavaScript bundler that inlines local
callsrequire() - Executed in GitHub Actions using
actions/github-script@v8
Top-Level Script Pattern
Top-level
.cjs scripts (those that are executed directly in workflows) follow a specific pattern:
✅ Correct Pattern - Export main, but don't call it:
async function main() { // Script logic here core.info("Running the script"); } module.exports = { main };
❌ Incorrect Pattern - Don't call main in the file:
async function main() { // Script logic here core.info("Running the script"); } await main(); // ❌ Don't do this! module.exports = { main };
Why this pattern?
- The bundler automatically injects
during inline execution in GitHub Actionsawait main() - This allows the script to be both imported (for testing) and executed (in workflows)
- It provides a clean separation between module definition and execution
- It enables better testing by allowing tests to import and call
with mocksmain()
Examples of top-level scripts:
- Creates GitHub issuescreate_issue.cjs
- Adds comments to issues/PRsadd_comment.cjs
- Adds labels to issues/PRsadd_labels.cjs
- Updates GitHub Projectsupdate_project.cjs
All of these files export
main but do not call it directly.
Step 1: Create the New .cjs File
Create your new file in
/home/runner/work/gh-aw/gh-aw/pkg/workflow/js/ with a descriptive name:
File naming convention:
- Use snake_case for filenames (e.g.,
,sanitize_content.cjs
)load_agent_output.cjs - Use
extension (CommonJS module).cjs - Choose names that clearly describe the module's purpose
Example file structure:
// @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, };
Key points:
- Include
for TypeScript checking// @ts-check - Include
for GitHub Actions types/// <reference types="@actions/github-script" /> - Use JSDoc comments for documentation
- Export functions using
module.exports = { ... } - Do NOT import
or@actions/core
- these are available globally in GitHub Actions@actions/github
Step 2: Add Tests
Create a test file with the same base name plus
.test.cjs:
Example: 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(""); }); });
Testing guidelines:
- Use vitest for testing framework
- Mock
andcore
globals as neededgithub - Use dynamic imports (
) to allow mocking before module loadawait import() - Clear mocks in
to ensure test isolationbeforeEach - Test both success cases and error handling
- Follow existing test patterns in
filespkg/workflow/js/*.test.cjs
Run tests:
make test-js
Step 3: Add Embedded Variable in Go
Add an
//go:embed directive and variable in the appropriate Go file:
For shared utility functions (used by multiple scripts):
Add to
:pkg/workflow/js.go
//go:embed js/my_module.cjs var myModuleScript string
Then add to the
GetJavaScriptSources() function:
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 } }
For main scripts (top-level scripts that use bundling):
Add to
:pkg/workflow/scripts.go
//go:embed js/my_script.cjs var myScriptSource string
Then create a getter function with bundling:
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 }
Important:
- Variables in
are for shared utilities that get bundled into other scriptsjs.go - Variables in
are for main scripts that use the bundler to inline dependenciesscripts.go - Use
pattern for lazy bundling insync.Oncescripts.go - The bundler will inline all local
calls at runtimerequire()
Step 4: Register in the Bundler (if creating a shared utility)
If you're creating a shared utility that will be used by other scripts via
require(), it's automatically available through the GetJavaScriptSources() map (Step 3).
The bundler will:
- Detect
in any scriptrequire('./my_module.cjs') - Look up the file in the
mapGetJavaScriptSources() - Inline the required module's content
- Remove the
statementrequire() - Deduplicate if the same module is required multiple times
No additional bundler registration needed - just ensure the file is in the
GetJavaScriptSources() map.
Step 5: Use Local Require in Other JavaScript Files
To use your new module in other JavaScript files, use CommonJS
require():
Example usage in another
file:.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 };
Important: Top-level scripts should export
main but NOT call it directly. The bundler injects await main() during inline execution in GitHub Actions.
Require guidelines:
- Use relative paths starting with
./ - Include the
extension.cjs - Use destructuring to import specific functions
- The bundler will inline the required module at compile time
Multiple requires example:
const { sanitizeContent } = require("./sanitize_content.cjs"); const { loadAgentOutput } = require("./load_agent_output.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs");
Complete Example: Creating a New Utility Module
Let's walk through creating a new
format_timestamp.cjs utility:
1. Create the file: pkg/workflow/js/format_timestamp.cjs
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. Create tests: pkg/workflow/js/format_timestamp.test.cjs
pkg/workflow/js/format_timestamp.test.cjsimport { 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. Add to pkg/workflow/js.go
:
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. Use in another script:
// @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 };
Note: The script exports
main but does not call it. The bundler will inject await main() when the script is executed inline in GitHub Actions.
5. Build and test:
# 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
Verification Checklist
Before committing your refactored code:
- New
file created in.cjspkg/workflow/js/ - Tests created in corresponding
file.test.cjs - Tests pass with
make test-js - Embedded variable added in
orpkg/workflow/js.gopkg/workflow/scripts.go - If utility: Added to
mapGetJavaScriptSources() - If main script: Created bundling getter function with
sync.Once - Local
statements work correctly in other filesrequire() - Code formatted with
make fmt-cjs - Code linted with
make lint-cjs - All Go tests pass with
make test-unit - Build succeeds with
make build
Common Patterns
Pattern 1: Shared Utility Function
Files like
sanitize_content.cjs, load_agent_output.cjs that provide reusable functions:
- Add to
withjs.go//go:embed - Add to
mapGetJavaScriptSources() - Use via
in other scriptsrequire()
Pattern 2: Main Workflow Script
Files like
create_issue.cjs, add_labels.cjs that are top-level scripts:
- Add to
withscripts.go
as//go:embed
variablexxxSource - Create bundling getter function with
patternsync.Once - These scripts can
utilities fromrequire()GetJavaScriptSources() - Must export
function but NOT call it - the bundler injectsmain
during executionawait main()
Pattern 3: Log Parser
Files like
parse_claude_log.cjs that parse AI engine logs:
- Add to
withjs.go//go:embed - Add case in
functionGetLogParserScript() - Used by workflow compilation system
Troubleshooting
Issue: "required file not found in sources"
Cause: File not added to
GetJavaScriptSources() map
Solution: Add the file to the map in
pkg/workflow/js.go
Issue: Tests fail with "core is not defined"
Cause: Missing global mocks
Solution: Add proper mocks before importing the module:
global.core = mockCore; global.github = mockGithub;
Issue: Bundler fails with circular dependency
Cause: File A requires File B which requires File A
Solution: Restructure to break the circular dependency, or combine the modules
Issue: Changes not reflected after rebuild
Cause: Go build cache not recognizing embedded file changes
Solution:
make clean make build
References
- Bundler implementation:
pkg/workflow/bundler.go - JavaScript sources registry:
pkg/workflow/js.go - Script bundling:
pkg/workflow/scripts.go - Existing test examples:
pkg/workflow/js/*.test.cjs - GitHub Actions script documentation: actions/toolkit