scorm-player 1.0.1 → 1.2.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 CHANGED
@@ -1,128 +1,159 @@
1
- # SCORM Player 2004
1
+ # SCORM Player 2004 — Node/Browser Compatible Loader & Parser
2
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.
3
+ A lightweight SCORM 2004 unpacker + manifest parser + storage adapter system.
4
4
 
5
- ## Features
5
+ This package allows you to:
6
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
7
+ - Upload SCORM 2004 ZIP packages
8
+ - Unpack them server-side (Node.js, Next.js server actions)
9
+ - Parse imsmanifest.xml (using a Node-compatible DOM parser)
10
+ - Save unpacked files to any storage (Supabase included)
11
+ - Render SCORM content inside a React <ScormPlayer /> component
12
12
 
13
- ## Installation
13
+ ## 🚀 Features
14
+
15
+ ✔ Supports SCORM 2004 ZIP packages
16
+ ✔ Works in Node.js (backend) and Browser (frontend)
17
+ ✔ SCORM ZIP unpacking using JSZip
18
+ ✔ Manifest parsing using xmldom (no browser APIs)
19
+ ✔ Supabase storage adapter included
20
+ ✔ Ready for Next.js — Server Actions + Client Components
21
+ ✔ Works with React 18 and React 19
22
+ ✔ Published as a reusable NPM module
23
+
24
+ ## 📦 Installation
14
25
 
15
26
  ```bash
16
27
  npm install scorm-player
17
28
  ```
18
29
 
19
- React and ReactDOM must be installed in your project:
30
+ The package requires:
20
31
 
21
- ```bash
22
- npm install react react-dom
23
- npm install --save-dev @types/react @types/react-dom
32
+ - react >= 18
33
+
34
+ React is declared as a peer dependency, so it will use whatever version the project already has.
35
+
36
+ ## 📁 Package Structure
37
+
38
+ ```
39
+ src/
40
+ backend/
41
+ unpackSCORM.ts
42
+ parseManifest.ts
43
+ storageAdapters/
44
+ supabaseAdapter.ts
45
+ frontend/
46
+ ScormPlayer.tsx
47
+ ```
48
+
49
+ You can import backend utilities or frontend components separately:
50
+
51
+ ### Backend
52
+
53
+ ```typescript
54
+ import { unpackSCORM, parseManifest, SupabaseAdapter } from "scorm-player";
55
+ ```
56
+
57
+ ### Frontend
58
+
59
+ ```typescript
60
+ import { ScormPlayer } from "scorm-player";
61
+ ```
62
+
63
+ ## 🧭 Usage — Upload SCORM Package (Next.js Server Action)
64
+
65
+ ### Client Component (upload button)
66
+
67
+ ```typescript
68
+ "use client";
69
+
70
+ import { useState } from "react";
71
+ import { uploadSCORMCourse } from "./actions/uploadSCORMCourse";
72
+
73
+ export default function ScormPage() {
74
+ const [file, setFile] = useState<File | null>(null);
75
+
76
+ return (
77
+ <div>
78
+ <input
79
+ type='file'
80
+ onChange={(e) => setFile(e.target.files?.[0] || null)}
81
+ />
82
+ <button onClick={() => file && uploadSCORMCourse(file)}>
83
+ Upload
84
+ </button>
85
+ </div>
86
+ );
87
+ }
24
88
  ```
25
89
 
26
- ## Backend: SCORM unpacking and uploading
90
+ ### Server Action (unpack + save SCORM)
27
91
 
28
92
  ```typescript
29
- import { unpackSCORM, parseManifest } from "scorm-player/backend";
30
- import { SupabaseAdapter } from "scorm-player/backend/storageAdapters/supabaseAdapter";
93
+ "use server";
94
+
95
+ import { unpackSCORM, parseManifest, SupabaseAdapter } from "scorm-player";
31
96
 
32
- export async function uploadSCORMCourse(file: File, folderName: string) {
33
- const { files, manifestXml } = await unpackSCORM(file);
97
+ export async function uploadSCORMCourse(file: File) {
98
+ const arrayBuffer = await file.arrayBuffer();
99
+ const buffer = Buffer.from(arrayBuffer);
100
+
101
+ const { files, manifestXml } = await unpackSCORM(buffer);
34
102
  const launchUrl = parseManifest(manifestXml);
35
103
 
36
104
  const adapter = new SupabaseAdapter(
37
105
  process.env.SUPABASE_URL!,
38
106
  process.env.SUPABASE_KEY!,
39
- folderName
107
+ "courses"
40
108
  );
41
109
 
42
- await adapter.uploadFolder(folderName, files);
110
+ await adapter.uploadFolder("courses", files);
43
111
 
44
112
  return launchUrl;
45
113
  }
46
114
  ```
47
115
 
48
- ## Frontend: React ScormPlayer component
116
+ ## 🖥️ Displaying SCORM Content
49
117
 
50
118
  ```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
- ```
119
+ import { ScormPlayer } from "scorm-player";
71
120
 
72
- - `launchUrl` public URL to the main SCORM lesson HTML file
73
- - `saveProgress` callback to save user progress
121
+ export default function PlayerPage() {
122
+ return <ScormPlayer launchUrl='courses/index.html' />;
123
+ }
124
+ ```
74
125
 
75
- ## Supabase Storage
126
+ The component uses an <iframe /> to show SCORM content.
76
127
 
77
- Create a bucket, e.g., scorm-courses
128
+ ## 🗃️ Supabase Storage Adapter
78
129
 
79
- You can set a custom folder name:
130
+ Uploads all extracted SCORM files:
80
131
 
81
132
  ```typescript
82
- const adapter = new SupabaseAdapter(
83
- process.env.SUPABASE_URL!,
84
- process.env.SUPABASE_KEY!,
85
- "my_custom_folder"
86
- );
133
+ const adapter = new SupabaseAdapter(SUPABASE_URL, SUPABASE_KEY, "folderName");
134
+
135
+ await adapter.uploadFolder("folderName", files);
87
136
  ```
88
137
 
89
- - All SCORM files will be uploaded to the specified folder
90
- - `getFileUrl(path)` returns a public link to the file
138
+ You can also write your own adapters (S3, GCP, local FS, etc.).
91
139
 
92
- ## TypeScript typings
140
+ ## 🧩 API Reference
93
141
 
94
- ```typescript
95
- import { SCORMState } from "scorm-player/types/scorm";
142
+ - `unpackSCORM(zipData: Buffer | Uint8Array)` - Unpacks a SCORM ZIP file into a dictionary of files.
143
+ - `parseManifest(manifestXml: string)` - Locates the SCORM launch URL from imsmanifest.xml.
144
+ - `SupabaseAdapter` - Uploads unpacked files into Supabase Storage.
145
+ - `<ScormPlayer launchUrl="..." />` - Renders SCORM content inside an iframe.
96
146
 
97
- const progress: SCORMState = {
98
- "cmi.location": "page_3",
99
- "cmi.completion_status": "incomplete",
100
- "cmi.score.raw": 80,
101
- };
102
- ```
147
+ ## 🛠️ Requirements
103
148
 
104
- ## Features
149
+ - Node.js 18+
150
+ - React 18 or newer
151
+ - Next.js (optional but supported)
105
152
 
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
153
+ ## 📄 License
110
154
 
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
- ```
155
+ MIT License.
125
156
 
126
- ## License
157
+ ## 🎉 Done
127
158
 
128
- MIT
159
+ Your SCORM Player is ready to use.
package/dist/index.d.mts CHANGED
@@ -4,7 +4,7 @@ interface ParsedSCORM {
4
4
  files: Record<string, string>;
5
5
  manifestXml: string;
6
6
  }
7
- declare function unpackSCORM(zipFile: File | Buffer): Promise<ParsedSCORM>;
7
+ declare function unpackSCORM(zipData: Buffer | Uint8Array): Promise<ParsedSCORM>;
8
8
 
9
9
  declare function parseManifest(manifestXml: string): string;
10
10
 
@@ -12,9 +12,11 @@ declare class SupabaseAdapter {
12
12
  client: SupabaseClient;
13
13
  rootFolder: string;
14
14
  constructor(supabaseUrl: string, supabaseKey: string, rootFolder?: string);
15
+ generateUniqueFolder(baseName: string): string;
15
16
  uploadFile(path: string, content: string | Buffer): Promise<string>;
16
- uploadFolder(folderPath: string, files: Record<string, string | Buffer>): Promise<void>;
17
+ uploadFolder(files: Record<string, string | Buffer>, baseFolderName?: string): Promise<string>;
17
18
  getFileUrl(path: string): string;
19
+ getLaunchUrl(folderName: string, launchPath: string): string;
18
20
  }
19
21
 
20
22
  interface SCORMState {
@@ -35,4 +37,4 @@ interface ScormPlayerProps {
35
37
  }
36
38
  declare function ScormPlayer({ launchUrl, userId, courseId, lessonId, saveProgress, }: ScormPlayerProps): JSX.Element;
37
39
 
38
- export { type ParsedSCORM, type SCORMState, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
40
+ export { type ParsedSCORM, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ interface ParsedSCORM {
4
4
  files: Record<string, string>;
5
5
  manifestXml: string;
6
6
  }
7
- declare function unpackSCORM(zipFile: File | Buffer): Promise<ParsedSCORM>;
7
+ declare function unpackSCORM(zipData: Buffer | Uint8Array): Promise<ParsedSCORM>;
8
8
 
9
9
  declare function parseManifest(manifestXml: string): string;
10
10
 
@@ -12,9 +12,11 @@ declare class SupabaseAdapter {
12
12
  client: SupabaseClient;
13
13
  rootFolder: string;
14
14
  constructor(supabaseUrl: string, supabaseKey: string, rootFolder?: string);
15
+ generateUniqueFolder(baseName: string): string;
15
16
  uploadFile(path: string, content: string | Buffer): Promise<string>;
16
- uploadFolder(folderPath: string, files: Record<string, string | Buffer>): Promise<void>;
17
+ uploadFolder(files: Record<string, string | Buffer>, baseFolderName?: string): Promise<string>;
17
18
  getFileUrl(path: string): string;
19
+ getLaunchUrl(folderName: string, launchPath: string): string;
18
20
  }
19
21
 
20
22
  interface SCORMState {
@@ -35,4 +37,4 @@ interface ScormPlayerProps {
35
37
  }
36
38
  declare function ScormPlayer({ launchUrl, userId, courseId, lessonId, saveProgress, }: ScormPlayerProps): JSX.Element;
37
39
 
38
- export { type ParsedSCORM, type SCORMState, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
40
+ export { type ParsedSCORM, ScormPlayer, SupabaseAdapter, parseManifest, unpackSCORM };
package/dist/index.js CHANGED
@@ -39,50 +39,75 @@ module.exports = __toCommonJS(src_exports);
39
39
 
40
40
  // src/backend/unpackSCORM.ts
41
41
  var import_jszip = __toESM(require("jszip"));
42
- async function unpackSCORM(zipFile) {
43
- const zip = await import_jszip.default.loadAsync(zipFile);
42
+ async function unpackSCORM(zipData) {
43
+ const zip = await import_jszip.default.loadAsync(zipData);
44
44
  const files = {};
45
45
  for (const filename of Object.keys(zip.files)) {
46
- const fileData = await zip.files[filename].async("string");
47
- files[filename] = fileData;
46
+ const file = zip.files[filename];
47
+ if (!file)
48
+ continue;
49
+ const content = await file.async("string");
50
+ files[filename] = content;
48
51
  }
49
52
  const manifestXml = files["imsmanifest.xml"];
50
- if (!manifestXml)
53
+ if (!manifestXml) {
51
54
  throw new Error("imsmanifest.xml not found in SCORM package");
55
+ }
52
56
  return { files, manifestXml };
53
57
  }
54
58
 
55
59
  // src/backend/parseManifest.ts
60
+ var import_xmldom = require("xmldom");
56
61
  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") || "";
62
+ const xmlDoc = new import_xmldom.DOMParser().parseFromString(manifestXml, "text/xml");
63
+ const resources = xmlDoc.getElementsByTagName("resource");
64
+ const firstResource = resources[0];
65
+ if (!firstResource) {
66
+ throw new Error("SCORM manifest has no <resource> element");
67
+ }
68
+ const launch = firstResource.getAttribute("href");
69
+ if (!launch) {
70
+ throw new Error("SCORM launch URL (href) not found in manifest");
71
+ }
72
+ return launch;
63
73
  }
64
74
 
65
75
  // src/backend/storageAdapters/supabaseAdapter.ts
66
76
  var import_supabase_js = require("@supabase/supabase-js");
77
+ var import_crypto = require("crypto");
67
78
  var SupabaseAdapter = class {
68
79
  constructor(supabaseUrl, supabaseKey, rootFolder = "scorm-courses") {
69
80
  this.client = (0, import_supabase_js.createClient)(supabaseUrl, supabaseKey);
70
81
  this.rootFolder = rootFolder;
71
82
  }
83
+ // Генерация уникального имени папки
84
+ generateUniqueFolder(baseName) {
85
+ const id = (0, import_crypto.randomUUID)();
86
+ return `${baseName}-${id}`;
87
+ }
72
88
  async uploadFile(path, content) {
73
89
  const fullPath = `${this.rootFolder}/${path}`;
74
90
  await this.client.storage.from(this.rootFolder).upload(path, content, { upsert: true });
75
91
  return fullPath;
76
92
  }
77
- async uploadFolder(folderPath, files) {
93
+ // Принимаем files и optional baseName для папки
94
+ async uploadFolder(files, baseFolderName = "course") {
95
+ const uniqueFolder = this.generateUniqueFolder(baseFolderName);
78
96
  for (const [filePath, content] of Object.entries(files)) {
79
- const fullPath = `${folderPath}/${filePath}`;
97
+ const fullPath = `${uniqueFolder}/${filePath}`;
80
98
  await this.uploadFile(fullPath, content);
81
99
  }
100
+ return uniqueFolder;
82
101
  }
102
+ // Получаем публичный URL полного пути к файлу
83
103
  getFileUrl(path) {
84
104
  return this.client.storage.from(this.rootFolder).getPublicUrl(path).data.publicUrl;
85
105
  }
106
+ // Удобная функция для получения URL к launch файлу
107
+ getLaunchUrl(folderName, launchPath) {
108
+ const fullPath = `${folderName}/${launchPath}`;
109
+ return this.getFileUrl(fullPath);
110
+ }
86
111
  };
87
112
 
88
113
  // src/frontend/ScormPlayer.tsx
package/dist/index.mjs CHANGED
@@ -1,49 +1,74 @@
1
1
  // src/backend/unpackSCORM.ts
2
2
  import JSZip from "jszip";
3
- async function unpackSCORM(zipFile) {
4
- const zip = await JSZip.loadAsync(zipFile);
3
+ async function unpackSCORM(zipData) {
4
+ const zip = await JSZip.loadAsync(zipData);
5
5
  const files = {};
6
6
  for (const filename of Object.keys(zip.files)) {
7
- const fileData = await zip.files[filename].async("string");
8
- files[filename] = fileData;
7
+ const file = zip.files[filename];
8
+ if (!file)
9
+ continue;
10
+ const content = await file.async("string");
11
+ files[filename] = content;
9
12
  }
10
13
  const manifestXml = files["imsmanifest.xml"];
11
- if (!manifestXml)
14
+ if (!manifestXml) {
12
15
  throw new Error("imsmanifest.xml not found in SCORM package");
16
+ }
13
17
  return { files, manifestXml };
14
18
  }
15
19
 
16
20
  // src/backend/parseManifest.ts
21
+ import { DOMParser } from "xmldom";
17
22
  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") || "";
23
+ const xmlDoc = new DOMParser().parseFromString(manifestXml, "text/xml");
24
+ const resources = xmlDoc.getElementsByTagName("resource");
25
+ const firstResource = resources[0];
26
+ if (!firstResource) {
27
+ throw new Error("SCORM manifest has no <resource> element");
28
+ }
29
+ const launch = firstResource.getAttribute("href");
30
+ if (!launch) {
31
+ throw new Error("SCORM launch URL (href) not found in manifest");
32
+ }
33
+ return launch;
24
34
  }
25
35
 
26
36
  // src/backend/storageAdapters/supabaseAdapter.ts
27
37
  import { createClient } from "@supabase/supabase-js";
38
+ import { randomUUID } from "crypto";
28
39
  var SupabaseAdapter = class {
29
40
  constructor(supabaseUrl, supabaseKey, rootFolder = "scorm-courses") {
30
41
  this.client = createClient(supabaseUrl, supabaseKey);
31
42
  this.rootFolder = rootFolder;
32
43
  }
44
+ // Генерация уникального имени папки
45
+ generateUniqueFolder(baseName) {
46
+ const id = randomUUID();
47
+ return `${baseName}-${id}`;
48
+ }
33
49
  async uploadFile(path, content) {
34
50
  const fullPath = `${this.rootFolder}/${path}`;
35
51
  await this.client.storage.from(this.rootFolder).upload(path, content, { upsert: true });
36
52
  return fullPath;
37
53
  }
38
- async uploadFolder(folderPath, files) {
54
+ // Принимаем files и optional baseName для папки
55
+ async uploadFolder(files, baseFolderName = "course") {
56
+ const uniqueFolder = this.generateUniqueFolder(baseFolderName);
39
57
  for (const [filePath, content] of Object.entries(files)) {
40
- const fullPath = `${folderPath}/${filePath}`;
58
+ const fullPath = `${uniqueFolder}/${filePath}`;
41
59
  await this.uploadFile(fullPath, content);
42
60
  }
61
+ return uniqueFolder;
43
62
  }
63
+ // Получаем публичный URL полного пути к файлу
44
64
  getFileUrl(path) {
45
65
  return this.client.storage.from(this.rootFolder).getPublicUrl(path).data.publicUrl;
46
66
  }
67
+ // Удобная функция для получения URL к launch файлу
68
+ getLaunchUrl(folderName, launchPath) {
69
+ const fullPath = `${folderName}/${launchPath}`;
70
+ return this.getFileUrl(fullPath);
71
+ }
47
72
  };
48
73
 
49
74
  // src/frontend/ScormPlayer.tsx
package/package.json CHANGED
@@ -1,39 +1,45 @@
1
1
  {
2
2
  "name": "scorm-player",
3
- "version": "1.0.1",
4
- "description": "SCORM 2004 player module for Next.js and other frameworks, with backend unpacking and SCORM API support.",
3
+ "version": "1.2.0",
4
+ "description": "SCORM 2004 player, unpacker and manifest parser for Node.js / Next.js with storage adapters and React iframe renderer.",
5
5
  "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
6
7
  "types": "dist/index.d.ts",
7
8
  "files": [
8
9
  "dist"
9
10
  ],
10
11
  "scripts": {
11
12
  "build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist",
12
- "clean": "rm -rf dist"
13
+ "clean": "rm -rf dist",
14
+ "prepublishOnly": "npm run build"
13
15
  },
14
16
  "keywords": [
15
17
  "scorm",
16
18
  "scorm2004",
19
+ "scorm player",
17
20
  "nextjs",
18
21
  "react",
19
- "player",
20
- "elearning"
22
+ "elearning",
23
+ "lms",
24
+ "imsmanifest"
21
25
  ],
22
26
  "author": "Shmelev Sergei",
23
27
  "license": "MIT",
24
28
  "dependencies": {
25
29
  "@supabase/supabase-js": "^2.9.0",
26
- "jszip": "^3.10.1"
30
+ "jszip": "^3.10.1",
31
+ "xmldom": "^0.6.0"
27
32
  },
28
33
  "peerDependencies": {
29
- "react": ">=18 <20",
30
- "react-dom": ">=18 <20"
34
+ "react": ">=18 <21",
35
+ "react-dom": ">=18 <21"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/react": "^18.3.27",
34
39
  "@types/react-dom": "^18.3.7",
40
+ "@types/xmldom": "^0.1.34",
35
41
  "tsup": "^7.3.0",
36
- "typescript": "^5.2.0"
42
+ "typescript": "^5.4.5"
37
43
  },
38
44
  "repository": {
39
45
  "type": "git",