@spira-labs/scorm-lms-sdk
v1.1.6
Published
SDK completo para cargar, gestionar y reproducir contenido SCORM 1.2 y 2004 con Express + Firebase
Downloads
114
Readme
🎓 SCORM LMS SDK
SDK completo para cargar, gestionar y reproducir contenido SCORM 1.2 y 2004 en cualquier aplicación web. Incluye servidor Express listo para producción y cliente JavaScript framework-agnostic.
✨ Características
- 🚀 Instalación simple - Un solo comando npm
- 🖥️ Servidor incluido - Express server con API REST completa
- ☁️ Firebase integrado - Firestore para metadata y progreso de estudiantes
- 📁 Filesystem preservado - Archivos SCORM en servidor (referencias relativas funcionan)
- 🎨 Framework agnostic - React, Vue, Angular, Vanilla JS
- 📊 SCORM compliant - Soporta SCORM 1.2 y 2004
- 💾 Progreso persistente - Guardado automático en Firebase
- 🔧 Templates incluidos - Configuraciones listas para producción
- 📦 PM2 ready - Config incluida para despliegue con PM2
📦 Instalación npm:
npm install @spira-labs/scorm-lms-sdkYarn:
yarn add @spira-labs/scorm-lms-sdk🚀 Quick Start
1. Instalar dependencias
npm install firebase2. Copiar archivos de configuración
El SDK incluye templates listos para usar. Cópialos a tu proyecto:
# Desde tu proyecto, copiar templates
cp node_modules/@spira-labs/scorm-lms-sdk/templates/server.js ./server.js
# Si tu app esta con Vite tambien copia vite.config.js
cp node_modules/@spira-labs/scorm-lms-sdk/templates/vite.config.js ./vite.config.jsO crea manualmente:
server.js (en la raíz de tu proyecto):
import express from 'express';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(express.json({ limit: '100mb' }));
const publicDir = path.join(__dirname, 'public');
const uploadsDir = path.join(publicDir, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
app.use(express.static(publicDir));
// Rutas API (ver templates/server.js completo)
// ...
app.listen(3001, () => {
console.log('File storage server running on port 3001');
});vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
host: '127.0.0.1', // IMPORTANTE: debe ser 127.0.0.1
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true,
},
"/uploads": {
target: "http://localhost:3001",
changeOrigin: true,
},
},
},
});3. Iniciar servidor
# Terminal 1: Servidor Express
node server.js
# Terminal 2: React CRA
yarn start
# Terminal 2 alterno: Vite dev server
npm run dev4. Configurar Firebase
// src/config/firebase.js
export const firebaseConfig = {
apiKey: "YOUR-API-KEY",
projectId: "your-project",
authDomain: "your-project.firebaseapp.com",
storageBucket: "your-project.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
};
// Configuración del servidor
export const serverUrl = ''; // Vacío para desarrollo (usa proxy de Vite)
export const basePath = '';⚠️ Si tu app ya tiene Firebase inicializado
Si tu aplicación ya usa Firebase y recibes el error:
FirebaseError: Firebase App named '[DEFAULT]' already exists with different options or configEl SDK tiene 3 formas de uso para evitar conflictos:
Opción 1: Pasar la app Firebase existente (RECOMENDADO)
import { getApp } from 'firebase/app';
import { ScormManager } from '@spira-labs/scorm-lms-sdk';
// Obtener la app Firebase que tu aplicación ya inicializó
const firebaseApp = getApp();
// Pasar la app al ScormManager
const manager = new ScormManager(
{ app: firebaseApp }, // ← Pasas la app directamente
'http://localhost:3001'
);Opción 2: Usar firebaseConfig (el SDK detecta apps existentes)
import { ScormManager } from '@spira-labs/scorm-lms-sdk';
// El SDK verifica si ya existe una app antes de inicializar
const manager = new ScormManager(
{ firebaseConfig: firebaseConfig }, // ← Wrapper con firebaseConfig
'http://localhost:3001'
);Opción 3: Backward compatibility (config directo)
// Forma anterior - el SDK ahora detecta automáticamente apps existentes
const manager = new ScormManager(
firebaseConfig, // ← Config directo (sigue funcionando)
'http://localhost:3001'
);5. Usar en tu aplicación
import { ScormManager, ScormPlayer } from '@spira-labs/scorm-lms-sdk';
import { firebaseConfig } from './config/firebase';
// Crear manager (elige una de las 3 opciones de arriba)
const manager = new ScormManager(
firebaseConfig,
'http://localhost:3001'
);
// Subir curso SCORM
const handleUpload = async (zipFile) => {
const course = await manager.uploadCourse(zipFile, {
userId: 'user123',
title: 'Mi Curso'
});
console.log('Curso subido:', course.id);
};
// Listar cursos
const courses = await manager.listCourses();
// Reproducir curso
const player = new ScormPlayer(
courseId,
document.getElementById('player-container'),
manager
);
await player.launch('user123');
// Escuchar eventos
player.onProgress((data) => {
console.log('Progreso:', data.completionStatus, data.scoreRaw);
});
player.onComplete(() => {
alert('¡Curso completado!');
});🏗️ Arquitectura
Tu Aplicación
├── Frontend (React/Vue/Angular/etc)
│ └── @spira-labs/scorm-lms-sdk/client
│ ├── ScormManager → Gestión de cursos
│ ├── ScormPlayer → Reproductor SCORM
│ └── ScormAPI → Comunicación con SCORM
├── Backend (Express Server)
│ └── @spira-labs/scorm-lms-sdk/server
│ ├── /api/upload → Subir cursos
│ ├── /api/courses → Listar cursos
│ └── /uploads → Servir archivos SCORM
└── Firebase Firestore
├── /courses → Metadata de cursos
└── /progress → Progreso de estudiantes¿Dónde va cada cosa?
| Componente | Ubicación | Propósito |
|------------|-----------|-----------|
| Archivos SCORM (HTML, JS, CSS, assets) | Express Server (public/uploads/) | Preservar referencias relativas del contenido |
| Metadata de cursos (título, versión, ruta) | Firebase Firestore /courses | Información del curso |
| Progreso estudiantes (CMI data, scores) | Firebase Firestore /progress | Estado de avance |
| UI/UX | Tu proyecto | Diseño personalizado |
📚 API Reference
ScormManager
Gestiona la carga, listado y eliminación de cursos SCORM.
const manager = new ScormManager(firebaseConfig, serverUrl);
// Subir curso
const course = await manager.uploadCourse(
file, // File object (zip)
{
userId: 'user123',
title: 'Nombre del curso',
description: 'Descripción opcional'
}
);
// Listar todos los cursos
const allCourses = await manager.listCourses();
// Listar cursos de un usuario
const userCourses = await manager.listCourses('user123');
// Obtener curso específico
const course = await manager.getCourse(courseId);
// Eliminar curso
await manager.deleteCourse(courseId);
// Guardar progreso manualmente (normalmente automático)
await manager.saveCourseProgress(courseId, userId, {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "85"
});
// Obtener progreso
const progress = await manager.getCourseProgress(courseId, userId);ScormPlayer
Reproduce contenido SCORM en un iframe y maneja la comunicación API.
const player = new ScormPlayer(courseId, containerElement, manager);
// Lanzar curso
await player.launch(userId, existingProgress?);
// Eventos
player.onProgress((data) => {
// data.completionStatus, data.successStatus, data.scoreRaw, etc.
});
player.onComplete(() => {
// Curso completado
});
player.onExit(() => {
// Usuario salió del curso
});
// Limpiar recursos
player.destroy();ScormAPI
API SCORM que implementa los estándares 1.2 y 2004 (uso interno del player).
import { ScormAPI } from '@spira-labs/scorm-lms-sdk';
const api = new ScormAPI('1.2'); // o '2004'
api.LMSInitialize('');
api.LMSSetValue('cmi.core.lesson_status', 'completed');
const status = api.LMSGetValue('cmi.core.lesson_status');
api.LMSCommit('');
api.LMSFinish('');🔧 Configuración
Servidor de Desarrollo
Opción 1: CLI (más rápido)
npx scorm-server --port 3001 --cors-origin "*"Opción 2: Script en package.json
{
"scripts": {
"scorm": "scorm-server --port 3001",
"dev": "vite",
"dev:all": "concurrently \"npm run scorm\" \"npm run dev\""
}
}Servidor de Producción
El SDK incluye templates listos para producción:
# En tu proyecto, copiar templates
cp node_modules/@spira-labs/scorm-lms-sdk/templates/server.production.js ./server.js
cp node_modules/@spira-labs/scorm-lms-sdk/templates/ecosystem.config.cjs ./server.js - Servidor optimizado para producción:
const express = require('express');
const path = require('path');
const scormRoutes = require('@spira-labs/scorm-lms-sdk/server');
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(express.json({ limit: '200mb' }));
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
// API Routes
app.use('/api', scormRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// SPA fallback
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
app.listen(PORT, () => {
console.log(`✅ Server running on port ${PORT}`);
});ecosystem.config.cjs - Configuración PM2:
module.exports = {
apps: [{
name: 'scorm-lms',
script: './server.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: 3001
},
error_file: './logs/error.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
watch: false,
max_memory_restart: '500M'
}]
};Firebase Firestore
Solo necesitas Firestore (NO Storage). Reglas de seguridad:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Cursos: lectura pública, escritura autenticada
match /courses/{courseId} {
allow read: if true;
allow write: if request.auth != null;
}
// Progreso: solo el usuario dueño
match /progress/{userId}/courses/{courseId} {
allow read, write: if request.auth != null &&
request.auth.uid == userId;
}
}
}🚀 Despliegue en Producción
Preparar tu aplicación (Local)
# 1. Navegar a tu proyecto
cd tu-proyecto
# 2. Copiar templates del SDK
cp node_modules/@spira-labs/scorm-lms-sdk/templates/server.production.js ./server.js
cp node_modules/@spira-labs/scorm-lms-sdk/templates/ecosystem.config.cjs ./
# 3. Build del frontend
npm run build
# 4. Crear paquete para desplegar
zip -r deploy.zip \
dist/ \
server.js \
ecosystem.config.cjs \
package.json \
package-lock.jsonDesplegar en VPS
# 1. Subir archivo (ejemplo con scp)
scp deploy.zip usuario@tu-servidor:~/
# 2. En el servidor
ssh usuario@tu-servidor
mkdir -p ~/apps/scorm-lms
cd ~/apps/scorm-lms
unzip ~/deploy.zip
# 3. Instalar dependencias
npm install --omit=dev
# 4. Crear directorios necesarios
mkdir -p public/uploads logs
# 5. Iniciar con PM2
pm2 start ecosystem.config.cjs
pm2 save
pm2 startupConfigurar Nginx
server {
listen 80;
server_name tu-dominio.com;
client_max_body_size 100M;
# API del SDK
location /api/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Timeouts para uploads grandes
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
# Archivos SCORM subidos
location ^~ /uploads/ {
alias /home/usuario/apps/scorm-lms/public/uploads/;
autoindex off;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check
location /health {
proxy_pass http://localhost:3001;
access_log off;
}
# Frontend estático
location / {
root /home/usuario/apps/scorm-lms/dist;
try_files $uri $uri/ /index.html;
index index.html;
}
}Importante: Dar permisos a Nginx para acceder a los archivos:
sudo chmod o+rx /home/usuario
sudo chmod o+rx /home/usuario/apps
sudo chmod -R o+rx /home/usuario/apps/scorm-lms/dist
sudo chmod -R o+rx /home/usuario/apps/scorm-lms/public
sudo systemctl restart nginx📁 Estructura de Datos
Firestore
// Collection: courses
{
id: "course_1234567890",
title: "Introducción a JavaScript",
description: "Curso básico de JS",
scormVersion: "SCORM 1.2",
launchPath: "index.html",
uploadedBy: "user123",
serverUrl: "http://localhost:3001",
coursePath: "/uploads/course_1234567890",
createdAt: Timestamp
}
// Collection: progress/{userId}/courses
{
courseId: "course_1234567890",
userId: "user123",
scormData: {
"cmi.core.lesson_status": "incomplete",
"cmi.core.score.raw": "75",
"cmi.core.score.min": "0",
"cmi.core.score.max": "100",
"cmi.suspend_data": "bookmark=lesson3",
"cmi.core.lesson_location": "page5"
},
completionStatus: "incomplete",
successStatus: "unknown",
scoreRaw: 75,
lastAccess: Timestamp,
updatedAt: Timestamp
}Filesystem (Servidor)
tu-proyecto/
├── dist/ # Frontend compilado
├── public/
│ └── uploads/ # Cursos SCORM
│ └── course_1234567890/
│ ├── index.html # Entry point del curso
│ ├── imsmanifest.xml # Manifest SCORM
│ └── scormcontent/ # Contenido del curso
│ ├── assets/
│ ├── js/
│ └── css/
├── server.js # Servidor producción
├── ecosystem.config.cjs # Config PM2
└── package.json📖 Ejemplos Completos
React + Vite
import { useState, useEffect, useRef } from 'react';
import { ScormManager, ScormPlayer } from '@spira-labs/scorm-lms-sdk';
import { firebaseConfig } from './config/firebase';
function App() {
const [manager] = useState(() =>
new ScormManager(firebaseConfig, 'http://localhost:3001')
);
const [courses, setCourses] = useState([]);
const [selectedCourse, setSelectedCourse] = useState(null);
useEffect(() => {
loadCourses();
}, []);
const loadCourses = async () => {
const data = await manager.listCourses();
setCourses(data);
};
const handleUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
await manager.uploadCourse(file, {
userId: 'user123',
title: file.name.replace('.zip', '')
});
await loadCourses();
alert('¡Curso subido exitosamente!');
} catch (error) {
alert('Error al subir curso: ' + error.message);
}
};
const launchCourse = (courseId) => {
setSelectedCourse(courseId);
};
return (
<div>
<h1>Mi LMS SCORM</h1>
{!selectedCourse ? (
<>
<input
type="file"
accept=".zip"
onChange={handleUpload}
/>
<ul>
{courses.map(course => (
<li key={course.id}>
{course.title}
<button onClick={() => launchCourse(course.id)}>
Lanzar
</button>
</li>
))}
</ul>
</>
) : (
<ScormPlayerComponent
courseId={selectedCourse}
manager={manager}
onClose={() => setSelectedCourse(null)}
/>
)}
</div>
);
}
function ScormPlayerComponent({ courseId, manager, onClose }) {
const containerRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
const player = new ScormPlayer(courseId, containerRef.current, manager);
playerRef.current = player;
player.launch('user123');
player.onProgress((data) => {
console.log('Progreso:', data);
});
player.onComplete(() => {
alert('¡Curso completado!');
});
return () => player.destroy();
}, [courseId]);
return (
<div>
<button onClick={onClose}>Cerrar</button>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}
export default App;Vue 3
<script setup>
import { ref, onMounted } from 'vue';
import { ScormManager } from '@spira-labs/scorm-lms-sdk';
import { firebaseConfig } from './config/firebase';
const manager = new ScormManager(firebaseConfig, 'http://localhost:3001');
const courses = ref([]);
onMounted(async () => {
courses.value = await manager.listCourses();
});
const handleUpload = async (event) => {
const file = event.target.files[0];
await manager.uploadCourse(file, { userId: 'user123' });
courses.value = await manager.listCourses();
};
</script>
<template>
<div>
<input type="file" @change="handleUpload" accept=".zip" />
<ul>
<li v-for="course in courses" :key="course.id">
{{ course.title }}
</li>
</ul>
</div>
</template>Vanilla JavaScript
<!DOCTYPE html>
<html>
<head>
<title>SCORM LMS</title>
</head>
<body>
<input type="file" id="upload" accept=".zip" />
<div id="courses"></div>
<div id="player"></div>
<script type="module">
import { ScormManager, ScormPlayer } from '@spira-labs/scorm-lms-sdk';
const manager = new ScormManager(firebaseConfig, 'http://localhost:3001');
// Upload
document.getElementById('upload').addEventListener('change', async (e) => {
const file = e.target.files[0];
await manager.uploadCourse(file, { userId: 'user123' });
loadCourses();
});
// List courses
async function loadCourses() {
const courses = await manager.listCourses();
const html = courses.map(c => `
<div>
<h3>${c.title}</h3>
<button onclick="launchCourse('${c.id}')">Lanzar</button>
</div>
`).join('');
document.getElementById('courses').innerHTML = html;
}
// Launch course
window.launchCourse = async (courseId) => {
const container = document.getElementById('player');
const player = new ScormPlayer(courseId, container, manager);
await player.launch('user123');
player.onComplete(() => {
alert('¡Completado!');
});
};
loadCourses();
</script>
</body>
</html>🛠️ Comandos Útiles
Desarrollo
# Iniciar servidor SCORM
npx scorm-server
# Con opciones
npx scorm-server --port 4000 --cors-origin "http://localhost:5173"
# Ver ayuda
npx scorm-server --helpProducción (PM2)
# Iniciar
pm2 start ecosystem.config.cjs
# Estado
pm2 status
# Logs
pm2 logs scorm-lms
pm2 logs scorm-lms --lines 100
# Reiniciar
pm2 restart scorm-lms
# Detener
pm2 stop scorm-lms
# Auto-arranque
pm2 save
pm2 startup
# Monitoreo
pm2 monitNginx
# Probar configuración
sudo nginx -t
# Reiniciar
sudo systemctl restart nginx
# Ver logs
sudo tail -f /var/log/nginx/error.log
# Estado
sudo systemctl status nginx🐛 Troubleshooting
Error 502 Bad Gateway
Causa: Nginx no puede conectarse al backend.
# Verificar que PM2 está corriendo
pm2 status
# Ver logs
pm2 logs scorm-lms
# Verificar puerto
sudo lsof -i :3001
# Reiniciar
pm2 restart scorm-lmsNo se pueden subir cursos
Causa: Permisos o límite de tamaño.
# Verificar directorio
ls -la public/uploads/
chmod -R 755 public/uploads/
# Verificar límite en Nginx (debe tener client_max_body_size 100M)
sudo nano /etc/nginx/sites-available/scorm-lms
# Ver logs
pm2 logs scorm-lmsArchivos SCORM dan 403/404
Causa: Nginx no tiene permisos.
# Dar permisos
sudo chmod o+rx /home/$(whoami)
sudo chmod o+rx /home/$(whoami)/apps
sudo chmod -R o+rx /home/$(whoami)/apps/scorm-lms/public
# Reiniciar nginx
sudo systemctl restart nginxPlayer no carga el curso
Causas comunes:
- CORS mal configurado
- Ruta del servidor incorrecta
- Firebase no configurado
// Verificar config
console.log('Server URL:', manager.serverUrl);
console.log('Firebase Config:', firebaseConfig);
// Ver errores en consola del navegador (F12)📊 Estructura Final en Producción
VPS/Servidor
└── ~/apps/scorm-lms/
├── dist/ # Frontend (React/Vue build)
│ ├── index.html
│ └── assets/
│ ├── index-[hash].js
│ └── index-[hash].css
├── node_modules/
│ └── @spira-labs/
│ └── scorm-lms-sdk/ # El SDK
│ ├── client/ # Cliente JS
│ └── server/ # Express server
├── public/
│ └── uploads/ # Cursos SCORM subidos
│ └── course_*/
├── logs/ # Logs de PM2
├── server.js # Servidor producción
├── ecosystem.config.cjs # Config PM2
├── package.json
└── package-lock.json✅ Checklist de Despliegue
- [ ] SDK instalado (
npm install @spira-labs/scorm-lms-sdk) - [ ] Firebase configurado (Firestore + reglas)
- [ ] Frontend compilado (
npm run build) - [ ] Templates copiados (
server.js,ecosystem.config.cjs) - [ ] Dependencias instaladas en servidor (
npm install --omit=dev) - [ ] Directorios creados (
public/uploads,logs) - [ ] PM2 corriendo y configurado para auto-arranque
- [ ] Nginx configurado como proxy reverso
- [ ] Permisos correctos para Nginx (
chmod o+rx) - [ ] Firewall configurado (puertos 80, 443)
- [ ] Health check funciona (
curl http://localhost:3001/health) - [ ] Aplicación accesible desde navegador
🤝 Soporte y Documentación
- 📚 Guía Completa: Ver
/docs/GUIA-COMPLETA.md - 🏗️ Arquitectura: Ver
/docs/ARQUITECTURA.md - 🚀 Deploy Nginx: Ver
/docs/DEPLOY-NGINX.md - 💻 Demo App: Ver
/demopara ejemplo completo - 🐛 Issues: GitHub Issues
📝 Licencia
MIT © Spira Labs
🎉 ¡Comienza Ahora!
# Instalar
npm install @spira-labs/scorm-lms-sdk firebase
# Iniciar servidor
npx scorm-server
# Ver ejemplo completo
cd node_modules/@spira-labs/scorm-lms-sdk/demo
npm install
npm run dev¡Listo para desplegar tu LMS SCORM! 🚀
