@lxpack/scorm 0.1.0 → 0.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 ADDED
@@ -0,0 +1,172 @@
1
+ # @lxpack/scorm
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@lxpack/scorm)](https://www.npmjs.com/package/@lxpack/scorm)
4
+ [![CI](https://github.com/eddiethedean/lxpack/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lxpack/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/github/license/eddiethedean/lxpack)](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
7
+
8
+ SCORM 1.2, SCORM 2004 (multi-SCO), and standalone HTML export for LXPack courses.
9
+
10
+ Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime (**v0.2.0**).
11
+
12
+ | Related | Package |
13
+ |---------|---------|
14
+ | CLI | [`@lxpack/cli`](../cli/README.md) |
15
+ | Manifest validation | [`@lxpack/validators`](../validators/README.md) |
16
+ | Embedded client | [`@lxpack/runtime`](../runtime/README.md) |
17
+ | Components bundle | [`@lxpack/components`](../components/README.md) |
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @lxpack/scorm
23
+ ```
24
+
25
+ Requires Node.js 20+.
26
+
27
+ ## Usage
28
+
29
+ ### SCORM 1.2 or standalone ZIP
30
+
31
+ ```ts
32
+ import { readFile } from "node:fs/promises";
33
+ import type { CourseManifest } from "@lxpack/validators";
34
+ import { buildRuntimeAssessmentBundle } from "@lxpack/validators";
35
+ import {
36
+ packageCourse,
37
+ packageStandaloneDir,
38
+ buildIndexHtml,
39
+ collectFiles,
40
+ safeJsonForHtml,
41
+ courseSlug,
42
+ } from "@lxpack/scorm";
43
+
44
+ const courseDir = "/path/to/my-course";
45
+ const manifest: CourseManifest = /* from validateCourse */;
46
+ const assessmentBundle = await buildRuntimeAssessmentBundle(courseDir, manifest);
47
+
48
+ const runtimeClientJs = await readFile("path/to/client.js", "utf8");
49
+ const runtimeCss = await readFile("path/to/styles.css", "utf8");
50
+ const componentsJs = await readFile("path/to/bundle.js", "utf8"); // optional
51
+
52
+ await packageCourse({
53
+ courseDir,
54
+ manifest,
55
+ target: "scorm12",
56
+ outputPath: "/path/to/output/course-scorm12.zip",
57
+ runtimeClientJs,
58
+ runtimeCss,
59
+ componentsJs,
60
+ assessmentBundle,
61
+ });
62
+ ```
63
+
64
+ ### SCORM 2004 multi-SCO
65
+
66
+ ```ts
67
+ import { packageScorm2004, buildScoIndexHtml, listCourseActivities } from "@lxpack/scorm";
68
+
69
+ const activities = listCourseActivities(manifest);
70
+
71
+ await packageScorm2004({
72
+ courseDir,
73
+ manifest,
74
+ outputPath: "/path/to/output/course-scorm2004.zip",
75
+ runtimeClientJs,
76
+ runtimeCss,
77
+ componentsJs,
78
+ assessmentBundle,
79
+ });
80
+ ```
81
+
82
+ Each activity gets `sco/<activityId>/index.html` (via `buildScoIndexHtml`). Shared assets include `lxpack-runtime.js` and `lxpack-components.js`. `generateScorm2004Manifest` emits `imsmanifest.xml` with IMS Simple Sequencing metadata.
83
+
84
+ ### Standalone directory
85
+
86
+ ```ts
87
+ await packageStandaloneDir({
88
+ courseDir,
89
+ manifest,
90
+ target: "standalone",
91
+ outputDir: "/path/to/output/standalone",
92
+ runtimeClientJs,
93
+ runtimeCss,
94
+ componentsJs,
95
+ assessmentBundle,
96
+ });
97
+ ```
98
+
99
+ Most users should use `lxpack build` from [`@lxpack/cli`](../cli/README.md) instead of calling these APIs directly.
100
+
101
+ ## Exports
102
+
103
+ | Export | Description |
104
+ |--------|-------------|
105
+ | `packageCourse(options)` | Build a SCORM 1.2 or standalone ZIP |
106
+ | `packageScorm2004(options)` | Build a SCORM 2004 multi-SCO ZIP |
107
+ | `packageStandaloneDir(options)` | Write an unpacked directory |
108
+ | `listCourseActivities(manifest)` | Ordered activities for multi-SCO layout |
109
+ | `generateImsManifest(manifest, files)` | SCORM 1.2 `imsmanifest.xml` |
110
+ | `generateScorm2004Manifest(...)` | SCORM 2004 manifest with sequencing |
111
+ | `buildIndexHtml(options)` | Single-SCO course shell HTML |
112
+ | `buildScoIndexHtml(options)` | Per-activity launch HTML for SCORM 2004 |
113
+ | `scoLaunchPath(activityId)` | Relative launch path (`sco/<id>/index.html`) |
114
+ | `buildRuntimeConfig(options)` | Config object passed to `safeJsonForHtml` |
115
+ | `collectFiles(courseDir, baseDir)` | Course assets for packaging (respects skip rules) |
116
+ | `shouldSkipCourseFile(rel)` | Whether a relative path is omitted from exports |
117
+ | `buildManifestFileList(courseFiles)` | File list for `imsmanifest.xml` |
118
+ | `safeJsonForHtml(value)` | JSON safe for `<script type="application/json">` blocks |
119
+ | `courseSlug(manifest)` | Stable slug for ZIP names and manifest identifiers |
120
+ | `ExportTarget` | `"scorm12"` \| `"scorm2004"` \| `"standalone"` |
121
+
122
+ ### `PackageOptions`
123
+
124
+ | Field | Description |
125
+ |-------|-------------|
126
+ | `courseDir` | Course root directory |
127
+ | `manifest` | Parsed `course.yaml` |
128
+ | `outputPath` | Destination ZIP path |
129
+ | `target` | `scorm12`, `scorm2004`, or `standalone` |
130
+ | `runtimeClientJs` | Contents of `@lxpack/runtime/client` bundle |
131
+ | `runtimeCss` | Runtime stylesheet |
132
+ | `componentsJs` | Optional `@lxpack/components/bundle` for component lessons |
133
+ | `assessmentBundle` | From `buildRuntimeAssessmentBundle()` (assessments, keys, configs, feedback) |
134
+
135
+ ## What gets packaged
136
+
137
+ **Included**
138
+
139
+ - Lessons, interactions, assets, and component overrides referenced by the manifest
140
+ - `lxpack-runtime.js` — browser client bundle
141
+ - `lxpack-components.js` — when component lessons are used
142
+ - **SCORM 1.2 / standalone:** root `index.html` with embedded config
143
+ - **SCORM 2004:** `sco/<activityId>/index.html` per activity + `imsmanifest.xml`
144
+
145
+ **Excluded** (`shouldSkipCourseFile`)
146
+
147
+ - `course.yaml`, `lxpack.config.json`, `.lxpack/`
148
+ - `assessments/**` — author YAML; assessments are embedded in config instead
149
+ - Root `index.html` if present (packager generates entry pages)
150
+
151
+ Answer keys and feedback text are only present inside the JSON config block (with `<` escaped via `safeJsonForHtml`), not as downloadable files.
152
+
153
+ ## Development
154
+
155
+ From the monorepo root:
156
+
157
+ ```bash
158
+ pnpm --filter @lxpack/scorm build
159
+ pnpm --filter @lxpack/scorm test
160
+ pnpm --filter @lxpack/scorm typecheck
161
+ ```
162
+
163
+ ## Links
164
+
165
+ - [LXPack repository](https://github.com/eddiethedean/lxpack)
166
+ - [Documentation index](https://github.com/eddiethedean/lxpack/blob/main/docs/README.md)
167
+ - [Roadmap & phases](https://github.com/eddiethedean/lxpack/blob/main/docs/ROADMAP.md)
168
+ - [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
169
+
170
+ ## License
171
+
172
+ Apache-2.0
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CourseManifest } from '@lxpack/validators';
1
+ import { CourseManifest, RuntimeAssessmentBundle } from '@lxpack/validators';
2
2
 
3
3
  declare function manifestIdentifier(manifest: CourseManifest): string;
4
4
  declare function generateImsManifest(manifest: CourseManifest, files: string[], launchUrl?: string): string;
@@ -6,11 +6,18 @@ declare function generateImsManifest(manifest: CourseManifest, files: string[],
6
6
  interface BuildHtmlOptions {
7
7
  manifest: CourseManifest;
8
8
  runtimeCss: string;
9
- mode: "standalone" | "scorm12";
9
+ mode: "standalone" | "scorm12" | "scorm2004";
10
+ activityId?: string;
11
+ assessmentBundle?: RuntimeAssessmentBundle;
12
+ componentsScript?: string;
10
13
  }
14
+ declare function buildRuntimeConfig(options: BuildHtmlOptions): Record<string, unknown>;
15
+ declare function buildScoIndexHtml(options: BuildHtmlOptions & {
16
+ activityId: string;
17
+ }): string;
11
18
  declare function buildIndexHtml(options: BuildHtmlOptions): string;
12
19
 
13
- type ExportTarget = "scorm12" | "standalone";
20
+ type ExportTarget = "scorm12" | "scorm2004" | "standalone";
14
21
  interface PackageOptions {
15
22
  courseDir: string;
16
23
  manifest: CourseManifest;
@@ -18,7 +25,10 @@ interface PackageOptions {
18
25
  target: ExportTarget;
19
26
  runtimeClientJs: string;
20
27
  runtimeCss: string;
28
+ componentsBundleJs?: string;
29
+ assessmentBundle?: RuntimeAssessmentBundle;
21
30
  }
31
+ declare function shouldSkipCourseFile(rel: string): boolean;
22
32
  declare function collectFiles(dir: string, baseDir: string): Promise<Array<{
23
33
  path: string;
24
34
  fullPath: string;
@@ -26,6 +36,10 @@ declare function collectFiles(dir: string, baseDir: string): Promise<Array<{
26
36
  declare function buildManifestFileList(courseFiles: Array<{
27
37
  path: string;
28
38
  }>): string[];
39
+ declare function packageScorm2004(options: PackageOptions): Promise<{
40
+ outputPath: string;
41
+ fileCount: number;
42
+ }>;
29
43
  declare function packageCourse(options: PackageOptions): Promise<{
30
44
  outputPath: string;
31
45
  fileCount: number;
@@ -37,4 +51,21 @@ declare function packageStandaloneDir(options: Omit<PackageOptions, "outputPath"
37
51
  fileCount: number;
38
52
  }>;
39
53
 
40
- export { type BuildHtmlOptions, type ExportTarget, type PackageOptions, buildIndexHtml, buildManifestFileList, collectFiles, generateImsManifest, manifestIdentifier, packageCourse, packageStandaloneDir };
54
+ /** JSON safe to embed in HTML script blocks (prevents `</script>` breakout). */
55
+ declare function safeJsonForHtml(value: unknown): string;
56
+
57
+ /** Stable slug for ZIP filenames and SCORM identifiers. */
58
+ declare function courseSlug(manifest: CourseManifest): string;
59
+
60
+ interface CourseActivity {
61
+ id: string;
62
+ title: string;
63
+ kind: "lesson" | "assessment";
64
+ }
65
+ declare function listCourseActivities(manifest: CourseManifest): CourseActivity[];
66
+
67
+ declare function scoLaunchPath(activityId: string): string;
68
+ declare function buildScorm2004ManifestFiles(manifest: CourseManifest, courseFiles: string[]): string[];
69
+ declare function generateScorm2004Manifest(manifest: CourseManifest, courseFiles: string[]): string;
70
+
71
+ export { type BuildHtmlOptions, type CourseActivity, type ExportTarget, type PackageOptions, buildIndexHtml, buildManifestFileList, buildRuntimeConfig, buildScoIndexHtml, buildScorm2004ManifestFiles, collectFiles, courseSlug, generateImsManifest, generateScorm2004Manifest, listCourseActivities, manifestIdentifier, packageCourse, packageScorm2004, packageStandaloneDir, safeJsonForHtml, scoLaunchPath, shouldSkipCourseFile };
package/dist/index.js CHANGED
@@ -1,13 +1,19 @@
1
- // src/manifest.ts
2
- function manifestIdentifier(manifest) {
1
+ // src/slug.ts
2
+ function courseSlug(manifest) {
3
3
  const slug = manifest.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4
4
  if (slug) return slug;
5
5
  let hash = 0;
6
- for (let i = 0; i < manifest.title.length; i++) {
7
- hash = (hash << 5) - hash + manifest.title.charCodeAt(i);
6
+ const key = `${manifest.title}::${manifest.version}`;
7
+ for (let i = 0; i < key.length; i++) {
8
+ hash = (hash << 5) - hash + key.charCodeAt(i);
8
9
  hash |= 0;
9
10
  }
10
- return `course-${Math.abs(hash)}`;
11
+ return `course-${Math.abs(hash).toString(36)}`;
12
+ }
13
+
14
+ // src/manifest.ts
15
+ function manifestIdentifier(manifest) {
16
+ return courseSlug(manifest);
11
17
  }
12
18
  function generateImsManifest(manifest, files, launchUrl = "index.html") {
13
19
  const identifier = manifestIdentifier(manifest);
@@ -46,14 +52,54 @@ function escapeXml(text) {
46
52
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
47
53
  }
48
54
 
55
+ // src/safe-json.ts
56
+ function safeJsonForHtml(value) {
57
+ return JSON.stringify(value).replace(/</g, "\\u003c");
58
+ }
59
+
49
60
  // src/build-html.ts
50
- function buildIndexHtml(options) {
51
- const { manifest, runtimeCss, mode } = options;
52
- const config = JSON.stringify({
61
+ function buildRuntimeConfig(options) {
62
+ const { manifest, mode, assessmentBundle, activityId } = options;
63
+ return {
53
64
  manifest,
54
65
  baseUrl: ".",
55
- mode
56
- });
66
+ mode,
67
+ ...activityId ? { activityId } : {},
68
+ ...assessmentBundle ? {
69
+ assessments: assessmentBundle.assessments,
70
+ answerKeys: assessmentBundle.answerKeys,
71
+ assessmentConfigs: assessmentBundle.configs,
72
+ assessmentFeedback: assessmentBundle.feedback
73
+ } : {}
74
+ };
75
+ }
76
+ function buildScoIndexHtml(options) {
77
+ const { manifest, runtimeCss, activityId, componentsScript } = options;
78
+ const config = safeJsonForHtml(buildRuntimeConfig({ ...options, activityId }));
79
+ const componentsTag = componentsScript ? `<script type="module" src="${escapeHtml(componentsScript)}"></script>` : "";
80
+ return `<!DOCTYPE html>
81
+ <html lang="en">
82
+ <head>
83
+ <meta charset="UTF-8">
84
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
85
+ <title>${escapeHtml(manifest.title)} \u2014 ${escapeHtml(activityId)}</title>
86
+ <style>${runtimeCss}</style>
87
+ </head>
88
+ <body>
89
+ <div id="lxpack-app"></div>
90
+ <script type="application/json" id="lxpack-config">${config}</script>
91
+ <script>
92
+ window.__LXPACK_CONFIG__ = JSON.parse(document.getElementById('lxpack-config').textContent);
93
+ </script>
94
+ ${componentsTag}
95
+ <script type="module" src="../../lxpack-runtime.js"></script>
96
+ </body>
97
+ </html>`;
98
+ }
99
+ function buildIndexHtml(options) {
100
+ const { manifest, runtimeCss, componentsScript } = options;
101
+ const config = safeJsonForHtml(buildRuntimeConfig(options));
102
+ const componentsTag = componentsScript ? `<script type="module" src="${escapeHtml(componentsScript)}"></script>` : "";
57
103
  return `<!DOCTYPE html>
58
104
  <html lang="en">
59
105
  <head>
@@ -68,6 +114,7 @@ function buildIndexHtml(options) {
68
114
  <script>
69
115
  window.__LXPACK_CONFIG__ = JSON.parse(document.getElementById('lxpack-config').textContent);
70
116
  </script>
117
+ ${componentsTag}
71
118
  <script type="module" src="./lxpack-runtime.js"></script>
72
119
  </body>
73
120
  </html>`;
@@ -77,14 +124,124 @@ function escapeHtml(text) {
77
124
  }
78
125
 
79
126
  // src/package.ts
80
- import { readFile, readdir } from "fs/promises";
127
+ import { readFile, readdir, writeFile, mkdir, cp } from "fs/promises";
128
+ import { existsSync } from "fs";
81
129
  import { dirname, join, relative } from "path";
82
130
  import JSZip from "jszip";
131
+
132
+ // src/activities.ts
133
+ function listCourseActivities(manifest) {
134
+ const activities = manifest.lessons.map((lesson) => ({
135
+ id: lesson.id,
136
+ title: lesson.title ?? lesson.id,
137
+ kind: "lesson"
138
+ }));
139
+ for (const ref of manifest.assessments ?? []) {
140
+ activities.push({
141
+ id: ref.id,
142
+ title: ref.id.replace(/_/g, " "),
143
+ kind: "assessment"
144
+ });
145
+ }
146
+ return activities;
147
+ }
148
+
149
+ // src/scorm2004-manifest.ts
150
+ function escapeXml2(text) {
151
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
152
+ }
153
+ function scoLaunchPath(activityId) {
154
+ return `sco/${activityId}/index.html`;
155
+ }
156
+ function buildScorm2004ManifestFiles(manifest, courseFiles) {
157
+ const activities = listCourseActivities(manifest);
158
+ const scoFiles = activities.map((a) => scoLaunchPath(a.id));
159
+ return [
160
+ "lxpack-runtime.js",
161
+ "lxpack-components.js",
162
+ ...scoFiles,
163
+ ...courseFiles
164
+ ];
165
+ }
166
+ function generateScorm2004Manifest(manifest, courseFiles) {
167
+ const identifier = manifestIdentifier(manifest);
168
+ const orgId = `${identifier}-org`;
169
+ const activities = listCourseActivities(manifest);
170
+ const itemEntries = activities.map((activity) => {
171
+ const itemId = `item_${activity.id}`;
172
+ const resourceId = `res_${activity.id}`;
173
+ const sequencing = activity.kind === "assessment" ? `
174
+ <imsss:sequencing>
175
+ <imsss:deliveryControls tracked="true" completionSetByContent="true" objectiveSetByContent="true"/>
176
+ </imsss:sequencing>` : `
177
+ <imsss:sequencing>
178
+ <imsss:deliveryControls tracked="true" completionSetByContent="true"/>
179
+ </imsss:sequencing>`;
180
+ return ` <item identifier="${escapeXml2(itemId)}" identifierref="${escapeXml2(resourceId)}">
181
+ <title>${escapeXml2(activity.title)}</title>${sequencing}
182
+ </item>`;
183
+ }).join("\n");
184
+ const resources = activities.map((activity) => {
185
+ const resourceId = `res_${activity.id}`;
186
+ const href = scoLaunchPath(activity.id);
187
+ return ` <resource identifier="${escapeXml2(resourceId)}" type="webcontent" adlcp:scormType="sco" href="${escapeXml2(href)}">
188
+ <file href="${escapeXml2(href)}"/>
189
+ <file href="lxpack-runtime.js"/>
190
+ <file href="lxpack-components.js"/>
191
+ </resource>`;
192
+ }).join("\n");
193
+ const orgSequencing = `
194
+ <imsss:sequencing>
195
+ <imsss:controlMode choice="true" flow="true"/>
196
+ </imsss:sequencing>`;
197
+ const uniqueCourseFiles = [...new Set(courseFiles)].filter((f) => !f.startsWith("sco/")).map(
198
+ (href) => ` <file href="${escapeXml2(href)}"/>`
199
+ ).join("\n");
200
+ return `<?xml version="1.0" encoding="UTF-8"?>
201
+ <manifest identifier="${escapeXml2(identifier)}" version="${escapeXml2(manifest.version)}"
202
+ xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
203
+ xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_v1p3"
204
+ xmlns:adlseq="http://www.adlnet.org/xsd/adlseq_v1p3"
205
+ xmlns:imsss="http://www.imsglobal.org/xsd/imsss"
206
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
207
+ <metadata>
208
+ <schema>ADL SCORM</schema>
209
+ <schemaversion>2004 4th Edition</schemaversion>
210
+ </metadata>
211
+ <organizations default="${escapeXml2(orgId)}">
212
+ <organization identifier="${escapeXml2(orgId)}">${orgSequencing}
213
+ ${itemEntries}
214
+ </organization>
215
+ </organizations>
216
+ <resources>
217
+ ${resources}
218
+ <resource identifier="shared_assets" type="webcontent" adlcp:scormType="asset">
219
+ ${uniqueCourseFiles}
220
+ <file href="lxpack-runtime.js"/>
221
+ <file href="lxpack-components.js"/>
222
+ </resource>
223
+ </resources>
224
+ </manifest>`;
225
+ }
226
+
227
+ // src/package.ts
83
228
  var SKIP_FILES = /* @__PURE__ */ new Set([
84
229
  "course.yaml",
85
230
  "lxpack.config.ts",
86
231
  "lxpack.config.json"
87
232
  ]);
233
+ function shouldSkipCourseFile(rel) {
234
+ if (SKIP_FILES.has(rel) || rel.startsWith(".lxpack")) {
235
+ return true;
236
+ }
237
+ if (rel === "index.html") {
238
+ return true;
239
+ }
240
+ if (rel.startsWith("assessments/")) {
241
+ return true;
242
+ }
243
+ return false;
244
+ }
88
245
  async function collectFiles(dir, baseDir) {
89
246
  const entries = await readdir(dir, { withFileTypes: true });
90
247
  const files = [];
@@ -97,10 +254,7 @@ async function collectFiles(dir, baseDir) {
97
254
  files.push(...await collectFiles(fullPath, baseDir));
98
255
  } else if (entry.isFile()) {
99
256
  const rel = relative(baseDir, fullPath).replace(/\\/g, "/");
100
- if (SKIP_FILES.has(rel) || rel.startsWith(".lxpack")) {
101
- continue;
102
- }
103
- if (rel === "index.html") {
257
+ if (shouldSkipCourseFile(rel)) {
104
258
  continue;
105
259
  }
106
260
  files.push({ path: rel, fullPath });
@@ -111,8 +265,67 @@ async function collectFiles(dir, baseDir) {
111
265
  function buildManifestFileList(courseFiles) {
112
266
  return ["index.html", "lxpack-runtime.js", ...courseFiles.map((f) => f.path)];
113
267
  }
268
+ async function packageScorm2004(options) {
269
+ const {
270
+ courseDir,
271
+ manifest,
272
+ outputPath,
273
+ runtimeClientJs,
274
+ runtimeCss,
275
+ componentsBundleJs,
276
+ assessmentBundle
277
+ } = options;
278
+ const zip = new JSZip();
279
+ const courseFiles = await collectFiles(courseDir, courseDir);
280
+ for (const file of courseFiles) {
281
+ const content = await readFile(file.fullPath);
282
+ zip.file(file.path, content);
283
+ }
284
+ zip.file("lxpack-runtime.js", runtimeClientJs);
285
+ if (componentsBundleJs) {
286
+ zip.file("lxpack-components.js", componentsBundleJs);
287
+ }
288
+ const activities = listCourseActivities(manifest);
289
+ for (const activity of activities) {
290
+ const html = buildScoIndexHtml({
291
+ manifest,
292
+ runtimeCss,
293
+ mode: "scorm2004",
294
+ activityId: activity.id,
295
+ assessmentBundle,
296
+ componentsScript: componentsBundleJs ? "../../lxpack-components.js" : void 0
297
+ });
298
+ zip.file(`sco/${activity.id}/index.html`, html);
299
+ }
300
+ const manifestFiles = buildScorm2004ManifestFiles(
301
+ manifest,
302
+ courseFiles.map((f) => f.path)
303
+ );
304
+ zip.file("imsmanifest.xml", generateScorm2004Manifest(manifest, manifestFiles));
305
+ const buffer = await zip.generateAsync({
306
+ type: "nodebuffer",
307
+ compression: "DEFLATE"
308
+ });
309
+ await writeFile(outputPath, buffer);
310
+ return {
311
+ outputPath,
312
+ fileCount: Object.keys(zip.files).length
313
+ };
314
+ }
114
315
  async function packageCourse(options) {
115
- const { courseDir, manifest, outputPath, target, runtimeClientJs, runtimeCss } = options;
316
+ if (options.target === "scorm2004") {
317
+ return packageScorm2004(options);
318
+ }
319
+ const {
320
+ courseDir,
321
+ manifest,
322
+ outputPath,
323
+ target,
324
+ runtimeClientJs,
325
+ runtimeCss,
326
+ assessmentBundle,
327
+ componentsBundleJs
328
+ } = options;
116
329
  const zip = new JSZip();
117
330
  const mode = target === "scorm12" ? "scorm12" : "standalone";
118
331
  const courseFiles = await collectFiles(courseDir, courseDir);
@@ -120,9 +333,18 @@ async function packageCourse(options) {
120
333
  const content = await readFile(file.fullPath);
121
334
  zip.file(file.path, content);
122
335
  }
123
- const indexHtml = buildIndexHtml({ manifest, runtimeCss, mode });
336
+ const indexHtml = buildIndexHtml({
337
+ manifest,
338
+ runtimeCss,
339
+ mode,
340
+ assessmentBundle,
341
+ componentsScript: componentsBundleJs ? "./lxpack-components.js" : void 0
342
+ });
124
343
  zip.file("index.html", indexHtml);
125
344
  zip.file("lxpack-runtime.js", runtimeClientJs);
345
+ if (componentsBundleJs) {
346
+ zip.file("lxpack-components.js", componentsBundleJs);
347
+ }
126
348
  if (target === "scorm12") {
127
349
  const manifestFiles = buildManifestFileList(courseFiles);
128
350
  zip.file(
@@ -134,7 +356,6 @@ async function packageCourse(options) {
134
356
  type: "nodebuffer",
135
357
  compression: "DEFLATE"
136
358
  });
137
- const { writeFile } = await import("fs/promises");
138
359
  await writeFile(outputPath, buffer);
139
360
  return {
140
361
  outputPath,
@@ -142,9 +363,15 @@ async function packageCourse(options) {
142
363
  };
143
364
  }
144
365
  async function packageStandaloneDir(options) {
145
- const { courseDir, manifest, outputDir, target, runtimeClientJs, runtimeCss } = options;
146
- const { mkdir, writeFile, cp } = await import("fs/promises");
147
- const { existsSync } = await import("fs");
366
+ const {
367
+ courseDir,
368
+ manifest,
369
+ outputDir,
370
+ target,
371
+ runtimeClientJs,
372
+ runtimeCss,
373
+ assessmentBundle
374
+ } = options;
148
375
  await mkdir(outputDir, { recursive: true });
149
376
  const mode = target === "scorm12" ? "scorm12" : "standalone";
150
377
  const courseFiles = await collectFiles(courseDir, courseDir);
@@ -158,7 +385,12 @@ async function packageStandaloneDir(options) {
158
385
  await cp(file.fullPath, dest);
159
386
  fileCount++;
160
387
  }
161
- const indexHtml = buildIndexHtml({ manifest, runtimeCss, mode });
388
+ const indexHtml = buildIndexHtml({
389
+ manifest,
390
+ runtimeCss,
391
+ mode,
392
+ assessmentBundle
393
+ });
162
394
  await writeFile(join(outputDir, "index.html"), indexHtml);
163
395
  await writeFile(join(outputDir, "lxpack-runtime.js"), runtimeClientJs);
164
396
  fileCount += 2;
@@ -175,9 +407,19 @@ async function packageStandaloneDir(options) {
175
407
  export {
176
408
  buildIndexHtml,
177
409
  buildManifestFileList,
410
+ buildRuntimeConfig,
411
+ buildScoIndexHtml,
412
+ buildScorm2004ManifestFiles,
178
413
  collectFiles,
414
+ courseSlug,
179
415
  generateImsManifest,
416
+ generateScorm2004Manifest,
417
+ listCourseActivities,
180
418
  manifestIdentifier,
181
419
  packageCourse,
182
- packageStandaloneDir
420
+ packageScorm2004,
421
+ packageStandaloneDir,
422
+ safeJsonForHtml,
423
+ scoLaunchPath,
424
+ shouldSkipCourseFile
183
425
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lxpack/scorm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SCORM and standalone HTML export for LXPack",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "jszip": "^3.10.1",
42
- "@lxpack/validators": "0.1.0"
42
+ "@lxpack/validators": "0.2.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.13.10",