npm package: @patch-adams/core
Description: Core patching engine for e-learning course packages (Rise, Storyline, Captivate, Xyleme) - inject CSS/JS into SCORM, cmi5, and xAPI courses
Latest version: 1.6.3

# @patch-adams/core

Patch Rise course packages (SCORM 1.2, SCORM 2004, cmi5, xAPI) to inject remote CSS/JS with local fallback. Enables centralized control over deployed e-learning courses.

## Features

- **4 injection points**: CSS before/after, JS before/after
- **Remote-first with local fallback**: Try CDN first, fallback to bundled files
- **FOUC prevention**: Blocking "before" scripts hide content until ready
- **Format detection**: Auto-detects SCORM 1.2, SCORM 2004, cmi5, xAPI
- **Manifest updates**: Updates `imsmanifest.xml` for SCORM compliance

## Installation

```bash
npm install @patch-adams/core
```

## Quick Start

### CLI Usage

```bash
# Patch a single file
npx patch-adams patch course.zip --remote-domain https://cdn.example.com

# Patch with custom output
npx patch-adams patch course.zip -o patched-course.zip --remote-domain https://cdn.example.com

# Initialize a config file
npx patch-adams init
```

### Programmatic Usage

```typescript
import { Patcher } from '@patch-adams/core';

const patcher = new Patcher({
  remoteDomain: 'https://cdn.example.com/rise-overrides',

  // HTML classes added to <html> tag
  htmlClass: 'pa-patched',      // Always present
  loadingClass: 'pa-loading',   // Removed when ready

  // CSS injection
  cssBefore: { filename: 'before.css', enabled: true },
  cssAfter: { filename: 'after.css', enabled: true, timeout: 5000 },

  // JS injection
  jsBefore: { filename: 'before.js', enabled: true },
  jsAfter: { filename: 'after.js', enabled: true, timeout: 5000 },

  // Local fallback folders (inside scormcontent/)
  localFolders: { css: 'css', js: 'js' },

  // Update manifests for SCORM compliance
  updateManifests: true,
});

// Patch a file
const patchedBuffer = await patcher.patchFile('course.zip');

// Or patch a buffer directly
const inputBuffer = fs.readFileSync('course.zip');
const outputBuffer = await patcher.patchBuffer(inputBuffer);
```

## Configuration

Create a `patch-adams.config.json` file:

```json
{
  "remoteDomain": "https://cdn.example.com/rise-overrides",
  "htmlClass": "pa-patched",
  "loadingClass": "pa-loading",
  "cssBefore": {
    "filename": "before.css",
    "enabled": true
  },
  "cssAfter": {
    "filename": "after.css",
    "enabled": true,
    "timeout": 5000
  },
  "jsBefore": {
    "filename": "before.js",
    "enabled": true
  },
  "jsAfter": {
    "filename": "after.js",
    "enabled": true,
    "timeout": 5000
  },
  "localFolders": {
    "css": "css",
    "js": "js"
  },
  "updateManifests": true
}
```

## How It Works

### Injection Points

| Slot | Location | Blocking? | Purpose |
|------|----------|-----------|---------|
| CSS Before | Start of `<head>` | Yes | Hide content, prevent FOUC |
| JS Before | After CSS Before | Yes | Setup, intercept APIs |
| CSS After | End of `<head>` | No (async) | Override Rise styles |
| JS After | End of `<body>` | No (async) | Modify after Rise loads |

### Loading Sequence

1. Browser loads `scormcontent/index.html`
2. `<html class="pa-patched pa-loading">` applied immediately
3. CSS Before loads (blocking) - hides content with `.pa-loading`
4. JS Before loads (blocking) - setup code runs
5. Rise CSS/JS loads and initializes
6. CSS After loads (async with timeout fallback)
7. JS After loads (async with timeout fallback)
8. JS After removes `.pa-loading` class - content reveals

### Remote Override Files

Host these files on your CDN at `remoteDomain`:

```
https://cdn.example.com/rise-overrides/
├── before.css    # Blocking CSS (hide content)
├── after.css     # Style overrides
├── before.js     # Setup scripts
└── after.js      # Post-initialization scripts
```

### Example CSS Override

```css
/* before.css - Hide content until ready */
html.pa-patched.pa-loading body {
  visibility: hidden !important;
}

/* after.css - Style overrides */
html.pa-patched .blocks-text {
  font-family: 'Your Font', sans-serif !important;
}

html.pa-patched .nav__logo {
  display: none !important;
}
```

### Example JS Override

```javascript
// before.js - Setup
window.PatchAdams = {
  version: '1.0.0',
  config: { /* custom config */ }
};

// after.js - Post-initialization
document.querySelectorAll('.blocks-text').forEach(el => {
  // Modify elements after Rise renders
});
```

## Patched Package Structure

```
course.zip
└── scormcontent/
    ├── index.html          # Patched with injection points
    ├── css/
    │   ├── before.css      # Local fallback
    │   └── after.css       # Local fallback
    ├── js/
    │   ├── before.js       # Local fallback
    │   └── after.js        # Local fallback
    └── lib/                # Original Rise files (unchanged)
```

## CLI Commands

```bash
# Patch a course
patch-adams patch <input.zip> [options]

Options:
  -o, --output <file>       Output file path
  -r, --remote-domain <url> Remote domain for CSS/JS
  -c, --config <file>       Config file path
  --no-css-before          Disable CSS before injection
  --no-css-after           Disable CSS after injection
  --no-js-before           Disable JS before injection
  --no-js-after            Disable JS after injection

# Initialize config file
patch-adams init

# Show version
patch-adams --version
```

## API Reference

### `Patcher`

```typescript
class Patcher {
  constructor(config: PatchAdamsConfig)

  // Patch a file on disk
  patchFile(inputPath: string, outputPath?: string): Promise<Buffer>

  // Patch a buffer in memory
  patchBuffer(buffer: Buffer): Promise<Buffer>
}
```

### `PatchAdamsConfig`

```typescript
interface PatchAdamsConfig {
  remoteDomain: string;
  htmlClass?: string;           // default: 'pa-patched'
  loadingClass?: string;        // default: 'pa-loading'
  cssBefore?: {
    filename: string;
    enabled: boolean;
  };
  cssAfter?: {
    filename: string;
    enabled: boolean;
    timeout?: number;           // default: 5000
  };
  jsBefore?: {
    filename: string;
    enabled: boolean;
  };
  jsAfter?: {
    filename: string;
    enabled: boolean;
    timeout?: number;           // default: 5000
  };
  localFolders?: {
    css: string;                // default: 'css'
    js: string;                 // default: 'js'
  };
  updateManifests?: boolean;    // default: true
}
```

## License

MIT