scorm-player 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # SCORM Player 2004
2
+
3
+ scorm-player is a universal module for working with SCORM 2004 packages. It works with Next.js, React, and any Node.js project.
4
+
5
+ ## Features
6
+
7
+ - Upload and unpack SCORM 2004 packages on the server
8
+ - Save files to Supabase Storage with a customizable folder
9
+ - Generate launch URLs for lessons or tests
10
+ - React component ScormPlayer to embed SCORM content
11
+ - Track user progress and save it to a database
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install scorm-player
17
+ ```
18
+
19
+ React and ReactDOM must be installed in your project:
20
+
21
+ ```bash
22
+ npm install react react-dom
23
+ npm install --save-dev @types/react @types/react-dom
24
+ ```
25
+
26
+ ## Backend: SCORM unpacking and uploading
27
+
28
+ ```typescript
29
+ import { unpackSCORM, parseManifest } from "scorm-player/backend";
30
+ import { SupabaseAdapter } from "scorm-player/backend/storageAdapters/supabaseAdapter";
31
+
32
+ export async function uploadSCORMCourse(file: File, folderName: string) {
33
+ const { files, manifestXml } = await unpackSCORM(file);
34
+ const launchUrl = parseManifest(manifestXml);
35
+
36
+ const adapter = new SupabaseAdapter(
37
+ process.env.SUPABASE_URL!,
38
+ process.env.SUPABASE_KEY!,
39
+ folderName
40
+ );
41
+
42
+ await adapter.uploadFolder(folderName, files);
43
+
44
+ return launchUrl;
45
+ }
46
+ ```
47
+
48
+ ## Frontend: React ScormPlayer component
49
+
50
+ ```typescript
51
+ import { ScormPlayer } from "scorm-player/frontend";
52
+
53
+ <ScormPlayer
54
+ launchUrl='https://xyz.supabase.co/storage/v1/object/public/my_custom_folder/index.html'
55
+ userId={user.id}
56
+ courseId={course.id}
57
+ lessonId={lesson.id}
58
+ saveProgress={async (data) => {
59
+ await fetch("/api/scorm/saveProgress", {
60
+ method: "POST",
61
+ body: JSON.stringify({
62
+ userId: user.id,
63
+ courseId: course.id,
64
+ lessonId: lesson.id,
65
+ data,
66
+ }),
67
+ });
68
+ }}
69
+ />;
70
+ ```
71
+
72
+ - `launchUrl` — public URL to the main SCORM lesson HTML file
73
+ - `saveProgress` — callback to save user progress
74
+
75
+ ## Supabase Storage
76
+
77
+ Create a bucket, e.g., scorm-courses
78
+
79
+ You can set a custom folder name:
80
+
81
+ ```typescript
82
+ const adapter = new SupabaseAdapter(
83
+ process.env.SUPABASE_URL!,
84
+ process.env.SUPABASE_KEY!,
85
+ "my_custom_folder"
86
+ );
87
+ ```
88
+
89
+ - All SCORM files will be uploaded to the specified folder
90
+ - `getFileUrl(path)` returns a public link to the file
91
+
92
+ ## TypeScript typings
93
+
94
+ ```typescript
95
+ import { SCORMState } from "scorm-player/types/scorm";
96
+
97
+ const progress: SCORMState = {
98
+ "cmi.location": "page_3",
99
+ "cmi.completion_status": "incomplete",
100
+ "cmi.score.raw": 80,
101
+ };
102
+ ```
103
+
104
+ ## Features
105
+
106
+ - Supports SCORM 2004 API (API_1484_11)
107
+ - Can be used as an NPM package in any React/Next.js project
108
+ - Frontend component and backend functions are fully decoupled
109
+ - Custom folders in Supabase allow organizing courses flexibly
110
+
111
+ ## Project structure example
112
+
113
+ ```
114
+ src/
115
+ ├─ backend/
116
+ │ ├─ unpackSCORM.ts
117
+ │ ├─ parseManifest.ts
118
+ │ └─ storageAdapters/supabaseAdapter.ts
119
+ ├─ frontend/
120
+ │ └─ ScormPlayer.tsx
121
+ ├─ types/
122
+ │ └─ scorm.ts
123
+ └─ index.ts
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,38 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ interface ParsedSCORM {
4
+ files: Record<string, string>;
5
+ manifestXml: string;
6
+ }
7
+ declare function unpackSCORM(zipFile: File | Buffer): Promise<ParsedSCORM>;
8
+
9
+ declare function parseManifest(manifestXml: string): string;
10
+
11
+ declare class SupabaseAdapter {
12
+ client: SupabaseClient;
13
+ rootFolder: string;
14
+ constructor(supabaseUrl: string, supabaseKey: string, rootFolder?: string);
15
+ uploadFile(path: string, content: string | Buffer): Promise<string>;
16
+ uploadFolder(folderPath: string, files: Record<string, string | Buffer>): Promise<void>;
17
+ getFileUrl(path: string): string;
18
+ }
19
+
20
+ interface SCORMState {
21
+ "cmi.location"?: string;
22
+ "cmi.completion_status"?: string;
23
+ "cmi.success_status"?: string;
24
+ "cmi.score.raw"?: number;
25
+ "cmi.progress_measure"?: number;
26
+ "cmi.suspend_data"?: string;
27
+ }
28
+
29
+ interface ScormPlayerProps {
30
+ launchUrl: string;
31
+ userId: string;
32
+ courseId: string;
33
+ lessonId: string;
34
+ saveProgress: (data: SCORMState) => void;
35
+ }
36
+ declare function ScormPlayer({ launchUrl, userId, courseId, lessonId, saveProgress, }: ScormPlayerProps): JSX.Element;
37
+
38
+ export { type ParsedSCORM, type SCORMState, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
@@ -0,0 +1,38 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ interface ParsedSCORM {
4
+ files: Record<string, string>;
5
+ manifestXml: string;
6
+ }
7
+ declare function unpackSCORM(zipFile: File | Buffer): Promise<ParsedSCORM>;
8
+
9
+ declare function parseManifest(manifestXml: string): string;
10
+
11
+ declare class SupabaseAdapter {
12
+ client: SupabaseClient;
13
+ rootFolder: string;
14
+ constructor(supabaseUrl: string, supabaseKey: string, rootFolder?: string);
15
+ uploadFile(path: string, content: string | Buffer): Promise<string>;
16
+ uploadFolder(folderPath: string, files: Record<string, string | Buffer>): Promise<void>;
17
+ getFileUrl(path: string): string;
18
+ }
19
+
20
+ interface SCORMState {
21
+ "cmi.location"?: string;
22
+ "cmi.completion_status"?: string;
23
+ "cmi.success_status"?: string;
24
+ "cmi.score.raw"?: number;
25
+ "cmi.progress_measure"?: number;
26
+ "cmi.suspend_data"?: string;
27
+ }
28
+
29
+ interface ScormPlayerProps {
30
+ launchUrl: string;
31
+ userId: string;
32
+ courseId: string;
33
+ lessonId: string;
34
+ saveProgress: (data: SCORMState) => void;
35
+ }
36
+ declare function ScormPlayer({ launchUrl, userId, courseId, lessonId, saveProgress, }: ScormPlayerProps): JSX.Element;
37
+
38
+ export { type ParsedSCORM, type SCORMState, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ ScormPlayer: () => ScormPlayer,
34
+ SupabaseAdapter: () => SupabaseAdapter,
35
+ parseManifest: () => parseManifest,
36
+ unpackSCORM: () => unpackSCORM
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+
40
+ // src/backend/unpackSCORM.ts
41
+ var import_jszip = __toESM(require("jszip"));
42
+ async function unpackSCORM(zipFile) {
43
+ const zip = await import_jszip.default.loadAsync(zipFile);
44
+ const files = {};
45
+ for (const filename of Object.keys(zip.files)) {
46
+ const fileData = await zip.files[filename].async("string");
47
+ files[filename] = fileData;
48
+ }
49
+ const manifestXml = files["imsmanifest.xml"];
50
+ if (!manifestXml)
51
+ throw new Error("imsmanifest.xml not found in SCORM package");
52
+ return { files, manifestXml };
53
+ }
54
+
55
+ // src/backend/parseManifest.ts
56
+ function parseManifest(manifestXml) {
57
+ const parser = new DOMParser();
58
+ const xml = parser.parseFromString(manifestXml, "text/xml");
59
+ const resource = xml.querySelector("resource");
60
+ if (!resource)
61
+ throw new Error("No <resource> found in manifest");
62
+ return resource.getAttribute("href") || "";
63
+ }
64
+
65
+ // src/backend/storageAdapters/supabaseAdapter.ts
66
+ var import_supabase_js = require("@supabase/supabase-js");
67
+ var SupabaseAdapter = class {
68
+ constructor(supabaseUrl, supabaseKey, rootFolder = "scorm-courses") {
69
+ this.client = (0, import_supabase_js.createClient)(supabaseUrl, supabaseKey);
70
+ this.rootFolder = rootFolder;
71
+ }
72
+ async uploadFile(path, content) {
73
+ const fullPath = `${this.rootFolder}/${path}`;
74
+ await this.client.storage.from(this.rootFolder).upload(path, content, { upsert: true });
75
+ return fullPath;
76
+ }
77
+ async uploadFolder(folderPath, files) {
78
+ for (const [filePath, content] of Object.entries(files)) {
79
+ const fullPath = `${folderPath}/${filePath}`;
80
+ await this.uploadFile(fullPath, content);
81
+ }
82
+ }
83
+ getFileUrl(path) {
84
+ return this.client.storage.from(this.rootFolder).getPublicUrl(path).data.publicUrl;
85
+ }
86
+ };
87
+
88
+ // src/frontend/ScormPlayer.tsx
89
+ var import_react = require("react");
90
+ var import_jsx_runtime = require("react/jsx-runtime");
91
+ function ScormPlayer({
92
+ launchUrl,
93
+ userId,
94
+ courseId,
95
+ lessonId,
96
+ saveProgress
97
+ }) {
98
+ const iframeRef = (0, import_react.useRef)(null);
99
+ const [scormState, setScormState] = (0, import_react.useState)({});
100
+ (0, import_react.useEffect)(() => {
101
+ window.API_1484_11 = {
102
+ Initialize: () => "true",
103
+ Terminate: () => "true",
104
+ GetValue: (key) => scormState[key] || "",
105
+ SetValue: (key, value) => {
106
+ const updatedState = { ...scormState, [key]: value };
107
+ setScormState(updatedState);
108
+ saveProgress(updatedState);
109
+ return "true";
110
+ },
111
+ Commit: () => {
112
+ saveProgress(scormState);
113
+ return "true";
114
+ },
115
+ GetLastError: () => 0,
116
+ GetErrorString: () => "",
117
+ GetDiagnostic: () => ""
118
+ };
119
+ }, [scormState, saveProgress]);
120
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("iframe", { ref: iframeRef, src: launchUrl, width: "100%", height: "600px" });
121
+ }
122
+ // Annotate the CommonJS export names for ESM import in node:
123
+ 0 && (module.exports = {
124
+ ScormPlayer,
125
+ SupabaseAdapter,
126
+ parseManifest,
127
+ unpackSCORM
128
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,88 @@
1
+ // src/backend/unpackSCORM.ts
2
+ import JSZip from "jszip";
3
+ async function unpackSCORM(zipFile) {
4
+ const zip = await JSZip.loadAsync(zipFile);
5
+ const files = {};
6
+ for (const filename of Object.keys(zip.files)) {
7
+ const fileData = await zip.files[filename].async("string");
8
+ files[filename] = fileData;
9
+ }
10
+ const manifestXml = files["imsmanifest.xml"];
11
+ if (!manifestXml)
12
+ throw new Error("imsmanifest.xml not found in SCORM package");
13
+ return { files, manifestXml };
14
+ }
15
+
16
+ // src/backend/parseManifest.ts
17
+ function parseManifest(manifestXml) {
18
+ const parser = new DOMParser();
19
+ const xml = parser.parseFromString(manifestXml, "text/xml");
20
+ const resource = xml.querySelector("resource");
21
+ if (!resource)
22
+ throw new Error("No <resource> found in manifest");
23
+ return resource.getAttribute("href") || "";
24
+ }
25
+
26
+ // src/backend/storageAdapters/supabaseAdapter.ts
27
+ import { createClient } from "@supabase/supabase-js";
28
+ var SupabaseAdapter = class {
29
+ constructor(supabaseUrl, supabaseKey, rootFolder = "scorm-courses") {
30
+ this.client = createClient(supabaseUrl, supabaseKey);
31
+ this.rootFolder = rootFolder;
32
+ }
33
+ async uploadFile(path, content) {
34
+ const fullPath = `${this.rootFolder}/${path}`;
35
+ await this.client.storage.from(this.rootFolder).upload(path, content, { upsert: true });
36
+ return fullPath;
37
+ }
38
+ async uploadFolder(folderPath, files) {
39
+ for (const [filePath, content] of Object.entries(files)) {
40
+ const fullPath = `${folderPath}/${filePath}`;
41
+ await this.uploadFile(fullPath, content);
42
+ }
43
+ }
44
+ getFileUrl(path) {
45
+ return this.client.storage.from(this.rootFolder).getPublicUrl(path).data.publicUrl;
46
+ }
47
+ };
48
+
49
+ // src/frontend/ScormPlayer.tsx
50
+ import { useEffect, useRef, useState } from "react";
51
+ import { jsx } from "react/jsx-runtime";
52
+ function ScormPlayer({
53
+ launchUrl,
54
+ userId,
55
+ courseId,
56
+ lessonId,
57
+ saveProgress
58
+ }) {
59
+ const iframeRef = useRef(null);
60
+ const [scormState, setScormState] = useState({});
61
+ useEffect(() => {
62
+ window.API_1484_11 = {
63
+ Initialize: () => "true",
64
+ Terminate: () => "true",
65
+ GetValue: (key) => scormState[key] || "",
66
+ SetValue: (key, value) => {
67
+ const updatedState = { ...scormState, [key]: value };
68
+ setScormState(updatedState);
69
+ saveProgress(updatedState);
70
+ return "true";
71
+ },
72
+ Commit: () => {
73
+ saveProgress(scormState);
74
+ return "true";
75
+ },
76
+ GetLastError: () => 0,
77
+ GetErrorString: () => "",
78
+ GetDiagnostic: () => ""
79
+ };
80
+ }, [scormState, saveProgress]);
81
+ return /* @__PURE__ */ jsx("iframe", { ref: iframeRef, src: launchUrl, width: "100%", height: "600px" });
82
+ }
83
+ export {
84
+ ScormPlayer,
85
+ SupabaseAdapter,
86
+ parseManifest,
87
+ unpackSCORM
88
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "scorm-player",
3
+ "version": "1.0.0",
4
+ "description": "SCORM 2004 player module for Next.js and other frameworks, with backend unpacking and SCORM API support.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist",
12
+ "clean": "rm -rf dist"
13
+ },
14
+ "keywords": [
15
+ "scorm",
16
+ "scorm2004",
17
+ "nextjs",
18
+ "react",
19
+ "player",
20
+ "elearning"
21
+ ],
22
+ "author": "Shmelev Sergei",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@supabase/supabase-js": "^2.9.0",
26
+ "jszip": "^3.10.1"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^18.3.1",
30
+ "react-dom": "^18.3.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.3.27",
34
+ "@types/react-dom": "^18.3.7",
35
+ "tsup": "^7.3.0",
36
+ "typescript": "^5.2.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/shmelevsergei/scorm-player-2004.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/shmelevsergei/scorm-player-2004/issues"
44
+ },
45
+ "homepage": "https://github.com/shmelevsergei/scorm-player-2004#readme"
46
+ }