@lxpack/scorm 0.1.0 → 0.1.1
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 +137 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +79 -23
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @lxpack/scorm
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@lxpack/scorm)
|
|
4
|
+
[](https://github.com/eddiethedean/lxpack/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
SCORM 1.2 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.
|
|
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
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @lxpack/scorm
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js 20+.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { readFile } from "node:fs/promises";
|
|
30
|
+
import type { CourseManifest } from "@lxpack/validators";
|
|
31
|
+
import { buildRuntimeAssessmentBundle } from "@lxpack/validators";
|
|
32
|
+
import {
|
|
33
|
+
packageCourse,
|
|
34
|
+
packageStandaloneDir,
|
|
35
|
+
buildIndexHtml,
|
|
36
|
+
collectFiles,
|
|
37
|
+
safeJsonForHtml,
|
|
38
|
+
courseSlug,
|
|
39
|
+
} from "@lxpack/scorm";
|
|
40
|
+
|
|
41
|
+
const courseDir = "/path/to/my-course";
|
|
42
|
+
const manifest: CourseManifest = /* from validateCourse */;
|
|
43
|
+
const assessmentBundle = await buildRuntimeAssessmentBundle(courseDir, manifest);
|
|
44
|
+
|
|
45
|
+
const runtimeClientJs = await readFile("path/to/client.js", "utf8");
|
|
46
|
+
const runtimeCss = await readFile("path/to/styles.css", "utf8");
|
|
47
|
+
|
|
48
|
+
await packageCourse({
|
|
49
|
+
courseDir,
|
|
50
|
+
manifest,
|
|
51
|
+
target: "scorm12",
|
|
52
|
+
outputPath: "/path/to/output/course-scorm12.zip",
|
|
53
|
+
runtimeClientJs,
|
|
54
|
+
runtimeCss,
|
|
55
|
+
assessmentBundle,
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Standalone directory
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await packageStandaloneDir({
|
|
63
|
+
courseDir,
|
|
64
|
+
manifest,
|
|
65
|
+
target: "standalone",
|
|
66
|
+
outputDir: "/path/to/output/standalone",
|
|
67
|
+
runtimeClientJs,
|
|
68
|
+
runtimeCss,
|
|
69
|
+
assessmentBundle,
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Most users should use `lxpack build` from [`@lxpack/cli`](../cli/README.md) instead of calling these APIs directly.
|
|
74
|
+
|
|
75
|
+
## Exports
|
|
76
|
+
|
|
77
|
+
| Export | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `packageCourse(options)` | Build a SCORM 1.2 or standalone ZIP |
|
|
80
|
+
| `packageStandaloneDir(options)` | Write an unpacked directory (ZIP or SCORM layout) |
|
|
81
|
+
| `generateImsManifest(manifest, files)` | SCORM 1.2 `imsmanifest.xml` |
|
|
82
|
+
| `buildIndexHtml(options)` | Course shell HTML with embedded config |
|
|
83
|
+
| `buildRuntimeConfig(options)` | Config object passed to `safeJsonForHtml` |
|
|
84
|
+
| `collectFiles(courseDir, baseDir)` | Course assets for packaging (respects skip rules) |
|
|
85
|
+
| `shouldSkipCourseFile(rel)` | Whether a relative path is omitted from exports |
|
|
86
|
+
| `buildManifestFileList(courseFiles)` | File list for `imsmanifest.xml` |
|
|
87
|
+
| `safeJsonForHtml(value)` | JSON safe for `<script type="application/json">` blocks |
|
|
88
|
+
| `courseSlug(manifest)` | Stable slug for ZIP names and manifest identifiers |
|
|
89
|
+
| `ExportTarget` | `"scorm12"` \| `"standalone"` |
|
|
90
|
+
|
|
91
|
+
### `PackageOptions`
|
|
92
|
+
|
|
93
|
+
| Field | Description |
|
|
94
|
+
|-------|-------------|
|
|
95
|
+
| `courseDir` | Course root directory |
|
|
96
|
+
| `manifest` | Parsed `course.yaml` |
|
|
97
|
+
| `outputPath` | Destination ZIP path (`packageCourse`) |
|
|
98
|
+
| `target` | `scorm12` or `standalone` |
|
|
99
|
+
| `runtimeClientJs` | Contents of `@lxpack/runtime/client` bundle |
|
|
100
|
+
| `runtimeCss` | Runtime stylesheet |
|
|
101
|
+
| `assessmentBundle` | Optional; from `buildRuntimeAssessmentBundle()` |
|
|
102
|
+
|
|
103
|
+
## What gets packaged
|
|
104
|
+
|
|
105
|
+
**Included**
|
|
106
|
+
|
|
107
|
+
- Lessons, interactions, and assets referenced by the manifest
|
|
108
|
+
- `index.html` — shell with embedded manifest + assessment config
|
|
109
|
+
- `lxpack-runtime.js` — browser client bundle
|
|
110
|
+
- `imsmanifest.xml` — SCORM 1.2 target only
|
|
111
|
+
|
|
112
|
+
**Excluded** (`shouldSkipCourseFile`)
|
|
113
|
+
|
|
114
|
+
- `course.yaml`, `lxpack.config.json`, `.lxpack/`
|
|
115
|
+
- `assessments/**` — author YAML; assessments are embedded in config instead
|
|
116
|
+
- Root `index.html` if present (packager generates the entry page)
|
|
117
|
+
|
|
118
|
+
Answer keys are only present inside the JSON config block (with `<` escaped via `safeJsonForHtml`), not as downloadable files.
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
From the monorepo root:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm --filter @lxpack/scorm build
|
|
126
|
+
pnpm --filter @lxpack/scorm test
|
|
127
|
+
pnpm --filter @lxpack/scorm typecheck
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Links
|
|
131
|
+
|
|
132
|
+
- [LXPack repository](https://github.com/eddiethedean/lxpack)
|
|
133
|
+
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
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;
|
|
@@ -7,7 +7,15 @@ interface BuildHtmlOptions {
|
|
|
7
7
|
manifest: CourseManifest;
|
|
8
8
|
runtimeCss: string;
|
|
9
9
|
mode: "standalone" | "scorm12";
|
|
10
|
+
assessmentBundle?: RuntimeAssessmentBundle;
|
|
10
11
|
}
|
|
12
|
+
declare function buildRuntimeConfig(options: BuildHtmlOptions): {
|
|
13
|
+
manifest: CourseManifest;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
mode: BuildHtmlOptions["mode"];
|
|
16
|
+
assessments?: RuntimeAssessmentBundle["assessments"];
|
|
17
|
+
answerKeys?: RuntimeAssessmentBundle["answerKeys"];
|
|
18
|
+
};
|
|
11
19
|
declare function buildIndexHtml(options: BuildHtmlOptions): string;
|
|
12
20
|
|
|
13
21
|
type ExportTarget = "scorm12" | "standalone";
|
|
@@ -18,7 +26,9 @@ interface PackageOptions {
|
|
|
18
26
|
target: ExportTarget;
|
|
19
27
|
runtimeClientJs: string;
|
|
20
28
|
runtimeCss: 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;
|
|
@@ -37,4 +47,10 @@ declare function packageStandaloneDir(options: Omit<PackageOptions, "outputPath"
|
|
|
37
47
|
fileCount: number;
|
|
38
48
|
}>;
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
/** JSON safe to embed in HTML script blocks (prevents `</script>` breakout). */
|
|
51
|
+
declare function safeJsonForHtml(value: unknown): string;
|
|
52
|
+
|
|
53
|
+
/** Stable slug for ZIP filenames and SCORM identifiers. */
|
|
54
|
+
declare function courseSlug(manifest: CourseManifest): string;
|
|
55
|
+
|
|
56
|
+
export { type BuildHtmlOptions, type ExportTarget, type PackageOptions, buildIndexHtml, buildManifestFileList, buildRuntimeConfig, collectFiles, courseSlug, generateImsManifest, manifestIdentifier, packageCourse, packageStandaloneDir, safeJsonForHtml, shouldSkipCourseFile };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
function
|
|
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
|
-
|
|
7
|
-
|
|
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,27 @@ function escapeXml(text) {
|
|
|
46
52
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
51
|
-
const { manifest,
|
|
52
|
-
|
|
61
|
+
function buildRuntimeConfig(options) {
|
|
62
|
+
const { manifest, mode, assessmentBundle } = options;
|
|
63
|
+
return {
|
|
53
64
|
manifest,
|
|
54
65
|
baseUrl: ".",
|
|
55
|
-
mode
|
|
56
|
-
|
|
66
|
+
mode,
|
|
67
|
+
...assessmentBundle ? {
|
|
68
|
+
assessments: assessmentBundle.assessments,
|
|
69
|
+
answerKeys: assessmentBundle.answerKeys
|
|
70
|
+
} : {}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function buildIndexHtml(options) {
|
|
74
|
+
const { manifest, runtimeCss } = options;
|
|
75
|
+
const config = safeJsonForHtml(buildRuntimeConfig(options));
|
|
57
76
|
return `<!DOCTYPE html>
|
|
58
77
|
<html lang="en">
|
|
59
78
|
<head>
|
|
@@ -77,7 +96,8 @@ function escapeHtml(text) {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
// src/package.ts
|
|
80
|
-
import { readFile, readdir } from "fs/promises";
|
|
99
|
+
import { readFile, readdir, writeFile, mkdir, cp } from "fs/promises";
|
|
100
|
+
import { existsSync } from "fs";
|
|
81
101
|
import { dirname, join, relative } from "path";
|
|
82
102
|
import JSZip from "jszip";
|
|
83
103
|
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
@@ -85,6 +105,18 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
|
85
105
|
"lxpack.config.ts",
|
|
86
106
|
"lxpack.config.json"
|
|
87
107
|
]);
|
|
108
|
+
function shouldSkipCourseFile(rel) {
|
|
109
|
+
if (SKIP_FILES.has(rel) || rel.startsWith(".lxpack")) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (rel === "index.html") {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (rel.startsWith("assessments/")) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
88
120
|
async function collectFiles(dir, baseDir) {
|
|
89
121
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
90
122
|
const files = [];
|
|
@@ -97,10 +129,7 @@ async function collectFiles(dir, baseDir) {
|
|
|
97
129
|
files.push(...await collectFiles(fullPath, baseDir));
|
|
98
130
|
} else if (entry.isFile()) {
|
|
99
131
|
const rel = relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
100
|
-
if (
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (rel === "index.html") {
|
|
132
|
+
if (shouldSkipCourseFile(rel)) {
|
|
104
133
|
continue;
|
|
105
134
|
}
|
|
106
135
|
files.push({ path: rel, fullPath });
|
|
@@ -112,7 +141,15 @@ function buildManifestFileList(courseFiles) {
|
|
|
112
141
|
return ["index.html", "lxpack-runtime.js", ...courseFiles.map((f) => f.path)];
|
|
113
142
|
}
|
|
114
143
|
async function packageCourse(options) {
|
|
115
|
-
const {
|
|
144
|
+
const {
|
|
145
|
+
courseDir,
|
|
146
|
+
manifest,
|
|
147
|
+
outputPath,
|
|
148
|
+
target,
|
|
149
|
+
runtimeClientJs,
|
|
150
|
+
runtimeCss,
|
|
151
|
+
assessmentBundle
|
|
152
|
+
} = options;
|
|
116
153
|
const zip = new JSZip();
|
|
117
154
|
const mode = target === "scorm12" ? "scorm12" : "standalone";
|
|
118
155
|
const courseFiles = await collectFiles(courseDir, courseDir);
|
|
@@ -120,7 +157,12 @@ async function packageCourse(options) {
|
|
|
120
157
|
const content = await readFile(file.fullPath);
|
|
121
158
|
zip.file(file.path, content);
|
|
122
159
|
}
|
|
123
|
-
const indexHtml = buildIndexHtml({
|
|
160
|
+
const indexHtml = buildIndexHtml({
|
|
161
|
+
manifest,
|
|
162
|
+
runtimeCss,
|
|
163
|
+
mode,
|
|
164
|
+
assessmentBundle
|
|
165
|
+
});
|
|
124
166
|
zip.file("index.html", indexHtml);
|
|
125
167
|
zip.file("lxpack-runtime.js", runtimeClientJs);
|
|
126
168
|
if (target === "scorm12") {
|
|
@@ -134,7 +176,6 @@ async function packageCourse(options) {
|
|
|
134
176
|
type: "nodebuffer",
|
|
135
177
|
compression: "DEFLATE"
|
|
136
178
|
});
|
|
137
|
-
const { writeFile } = await import("fs/promises");
|
|
138
179
|
await writeFile(outputPath, buffer);
|
|
139
180
|
return {
|
|
140
181
|
outputPath,
|
|
@@ -142,9 +183,15 @@ async function packageCourse(options) {
|
|
|
142
183
|
};
|
|
143
184
|
}
|
|
144
185
|
async function packageStandaloneDir(options) {
|
|
145
|
-
const {
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
const {
|
|
187
|
+
courseDir,
|
|
188
|
+
manifest,
|
|
189
|
+
outputDir,
|
|
190
|
+
target,
|
|
191
|
+
runtimeClientJs,
|
|
192
|
+
runtimeCss,
|
|
193
|
+
assessmentBundle
|
|
194
|
+
} = options;
|
|
148
195
|
await mkdir(outputDir, { recursive: true });
|
|
149
196
|
const mode = target === "scorm12" ? "scorm12" : "standalone";
|
|
150
197
|
const courseFiles = await collectFiles(courseDir, courseDir);
|
|
@@ -158,7 +205,12 @@ async function packageStandaloneDir(options) {
|
|
|
158
205
|
await cp(file.fullPath, dest);
|
|
159
206
|
fileCount++;
|
|
160
207
|
}
|
|
161
|
-
const indexHtml = buildIndexHtml({
|
|
208
|
+
const indexHtml = buildIndexHtml({
|
|
209
|
+
manifest,
|
|
210
|
+
runtimeCss,
|
|
211
|
+
mode,
|
|
212
|
+
assessmentBundle
|
|
213
|
+
});
|
|
162
214
|
await writeFile(join(outputDir, "index.html"), indexHtml);
|
|
163
215
|
await writeFile(join(outputDir, "lxpack-runtime.js"), runtimeClientJs);
|
|
164
216
|
fileCount += 2;
|
|
@@ -175,9 +227,13 @@ async function packageStandaloneDir(options) {
|
|
|
175
227
|
export {
|
|
176
228
|
buildIndexHtml,
|
|
177
229
|
buildManifestFileList,
|
|
230
|
+
buildRuntimeConfig,
|
|
178
231
|
collectFiles,
|
|
232
|
+
courseSlug,
|
|
179
233
|
generateImsManifest,
|
|
180
234
|
manifestIdentifier,
|
|
181
235
|
packageCourse,
|
|
182
|
-
packageStandaloneDir
|
|
236
|
+
packageStandaloneDir,
|
|
237
|
+
safeJsonForHtml,
|
|
238
|
+
shouldSkipCourseFile
|
|
183
239
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lxpack/scorm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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.
|
|
42
|
+
"@lxpack/validators": "0.1.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.13.10",
|