@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 +172 -0
- package/dist/index.d.ts +35 -4
- package/dist/index.js +265 -23
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
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, 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
|
-
|
|
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/
|
|
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,54 @@ 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, 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 (
|
|
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
|
-
|
|
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({
|
|
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 {
|
|
146
|
-
|
|
147
|
-
|
|
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({
|
|
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
|
-
|
|
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.
|
|
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.
|
|
42
|
+
"@lxpack/validators": "0.2.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.13.10",
|