@lxpack/scorm 0.1.1 → 0.2.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 +48 -13
- package/dist/index.d.ts +45 -10
- package/dist/index.js +322 -63
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
[](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
7
|
|
|
8
|
-
SCORM 1.2 and standalone HTML export for LXPack courses.
|
|
8
|
+
SCORM 1.2, SCORM 2004 (multi-SCO), and standalone HTML export for LXPack courses.
|
|
9
9
|
|
|
10
|
-
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
10
|
+
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime (**v0.2.0**).
|
|
11
11
|
|
|
12
12
|
| Related | Package |
|
|
13
13
|
|---------|---------|
|
|
14
14
|
| CLI | [`@lxpack/cli`](../cli/README.md) |
|
|
15
15
|
| Manifest validation | [`@lxpack/validators`](../validators/README.md) |
|
|
16
16
|
| Embedded client | [`@lxpack/runtime`](../runtime/README.md) |
|
|
17
|
+
| Components bundle | [`@lxpack/components`](../components/README.md) |
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -25,6 +26,8 @@ Requires Node.js 20+.
|
|
|
25
26
|
|
|
26
27
|
## Usage
|
|
27
28
|
|
|
29
|
+
### SCORM 1.2 or standalone ZIP
|
|
30
|
+
|
|
28
31
|
```ts
|
|
29
32
|
import { readFile } from "node:fs/promises";
|
|
30
33
|
import type { CourseManifest } from "@lxpack/validators";
|
|
@@ -44,6 +47,7 @@ const assessmentBundle = await buildRuntimeAssessmentBundle(courseDir, manifest)
|
|
|
44
47
|
|
|
45
48
|
const runtimeClientJs = await readFile("path/to/client.js", "utf8");
|
|
46
49
|
const runtimeCss = await readFile("path/to/styles.css", "utf8");
|
|
50
|
+
const componentsJs = await readFile("path/to/bundle.js", "utf8"); // optional
|
|
47
51
|
|
|
48
52
|
await packageCourse({
|
|
49
53
|
courseDir,
|
|
@@ -52,10 +56,31 @@ await packageCourse({
|
|
|
52
56
|
outputPath: "/path/to/output/course-scorm12.zip",
|
|
53
57
|
runtimeClientJs,
|
|
54
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,
|
|
55
78
|
assessmentBundle,
|
|
56
79
|
});
|
|
57
80
|
```
|
|
58
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
|
+
|
|
59
84
|
### Standalone directory
|
|
60
85
|
|
|
61
86
|
```ts
|
|
@@ -66,6 +91,7 @@ await packageStandaloneDir({
|
|
|
66
91
|
outputDir: "/path/to/output/standalone",
|
|
67
92
|
runtimeClientJs,
|
|
68
93
|
runtimeCss,
|
|
94
|
+
componentsJs,
|
|
69
95
|
assessmentBundle,
|
|
70
96
|
});
|
|
71
97
|
```
|
|
@@ -77,16 +103,21 @@ Most users should use `lxpack build` from [`@lxpack/cli`](../cli/README.md) inst
|
|
|
77
103
|
| Export | Description |
|
|
78
104
|
|--------|-------------|
|
|
79
105
|
| `packageCourse(options)` | Build a SCORM 1.2 or standalone ZIP |
|
|
80
|
-
| `
|
|
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 |
|
|
81
109
|
| `generateImsManifest(manifest, files)` | SCORM 1.2 `imsmanifest.xml` |
|
|
82
|
-
| `
|
|
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`) |
|
|
83
114
|
| `buildRuntimeConfig(options)` | Config object passed to `safeJsonForHtml` |
|
|
84
115
|
| `collectFiles(courseDir, baseDir)` | Course assets for packaging (respects skip rules) |
|
|
85
116
|
| `shouldSkipCourseFile(rel)` | Whether a relative path is omitted from exports |
|
|
86
117
|
| `buildManifestFileList(courseFiles)` | File list for `imsmanifest.xml` |
|
|
87
118
|
| `safeJsonForHtml(value)` | JSON safe for `<script type="application/json">` blocks |
|
|
88
119
|
| `courseSlug(manifest)` | Stable slug for ZIP names and manifest identifiers |
|
|
89
|
-
| `ExportTarget` | `"scorm12"` \| `"standalone"` |
|
|
120
|
+
| `ExportTarget` | `"scorm12"` \| `"scorm2004"` \| `"standalone"` |
|
|
90
121
|
|
|
91
122
|
### `PackageOptions`
|
|
92
123
|
|
|
@@ -94,28 +125,30 @@ Most users should use `lxpack build` from [`@lxpack/cli`](../cli/README.md) inst
|
|
|
94
125
|
|-------|-------------|
|
|
95
126
|
| `courseDir` | Course root directory |
|
|
96
127
|
| `manifest` | Parsed `course.yaml` |
|
|
97
|
-
| `outputPath` | Destination ZIP path
|
|
98
|
-
| `target` | `scorm12` or `standalone` |
|
|
128
|
+
| `outputPath` | Destination ZIP path |
|
|
129
|
+
| `target` | `scorm12`, `scorm2004`, or `standalone` |
|
|
99
130
|
| `runtimeClientJs` | Contents of `@lxpack/runtime/client` bundle |
|
|
100
131
|
| `runtimeCss` | Runtime stylesheet |
|
|
101
|
-
| `
|
|
132
|
+
| `componentsJs` | Optional `@lxpack/components/bundle` for component lessons |
|
|
133
|
+
| `assessmentBundle` | From `buildRuntimeAssessmentBundle()` (assessments, keys, configs, feedback) |
|
|
102
134
|
|
|
103
135
|
## What gets packaged
|
|
104
136
|
|
|
105
137
|
**Included**
|
|
106
138
|
|
|
107
|
-
- Lessons, interactions, and
|
|
108
|
-
- `index.html` — shell with embedded manifest + assessment config
|
|
139
|
+
- Lessons, interactions, assets, and component overrides referenced by the manifest
|
|
109
140
|
- `lxpack-runtime.js` — browser client bundle
|
|
110
|
-
- `
|
|
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`
|
|
111
144
|
|
|
112
145
|
**Excluded** (`shouldSkipCourseFile`)
|
|
113
146
|
|
|
114
147
|
- `course.yaml`, `lxpack.config.json`, `.lxpack/`
|
|
115
148
|
- `assessments/**` — author YAML; assessments are embedded in config instead
|
|
116
|
-
- Root `index.html` if present (packager generates
|
|
149
|
+
- Root `index.html` if present (packager generates entry pages)
|
|
117
150
|
|
|
118
|
-
Answer keys are only present inside the JSON config block (with `<` escaped via `safeJsonForHtml`), not as downloadable files.
|
|
151
|
+
Answer keys and feedback text are only present inside the JSON config block (with `<` escaped via `safeJsonForHtml`), not as downloadable files.
|
|
119
152
|
|
|
120
153
|
## Development
|
|
121
154
|
|
|
@@ -130,6 +163,8 @@ pnpm --filter @lxpack/scorm typecheck
|
|
|
130
163
|
## Links
|
|
131
164
|
|
|
132
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)
|
|
133
168
|
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
134
169
|
|
|
135
170
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CourseManifest, RuntimeAssessmentBundle } from '@lxpack/validators';
|
|
2
|
+
export { CourseActivity, enumerateActivities as listCourseActivities } from '@lxpack/validators';
|
|
2
3
|
|
|
3
4
|
declare function manifestIdentifier(manifest: CourseManifest): string;
|
|
4
5
|
declare function generateImsManifest(manifest: CourseManifest, files: string[], launchUrl?: string): string;
|
|
@@ -6,19 +7,27 @@ declare function generateImsManifest(manifest: CourseManifest, files: string[],
|
|
|
6
7
|
interface BuildHtmlOptions {
|
|
7
8
|
manifest: CourseManifest;
|
|
8
9
|
runtimeCss: string;
|
|
9
|
-
mode: "standalone" | "scorm12";
|
|
10
|
+
mode: "standalone" | "scorm12" | "scorm2004";
|
|
11
|
+
activityId?: string;
|
|
10
12
|
assessmentBundle?: RuntimeAssessmentBundle;
|
|
13
|
+
componentsScript?: string;
|
|
11
14
|
}
|
|
12
|
-
declare function buildRuntimeConfig(options: BuildHtmlOptions):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
assessments?: RuntimeAssessmentBundle["assessments"];
|
|
17
|
-
answerKeys?: RuntimeAssessmentBundle["answerKeys"];
|
|
18
|
-
};
|
|
15
|
+
declare function buildRuntimeConfig(options: BuildHtmlOptions): Record<string, unknown>;
|
|
16
|
+
declare function buildScoIndexHtml(options: BuildHtmlOptions & {
|
|
17
|
+
activityId: string;
|
|
18
|
+
}): string;
|
|
19
19
|
declare function buildIndexHtml(options: BuildHtmlOptions): string;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface PageTemplateOptions {
|
|
22
|
+
title: string;
|
|
23
|
+
runtimeCss: string;
|
|
24
|
+
configJson: string;
|
|
25
|
+
runtimeScript: string;
|
|
26
|
+
componentsScript?: string;
|
|
27
|
+
}
|
|
28
|
+
declare function buildLearnerPageHtml(options: PageTemplateOptions): string;
|
|
29
|
+
|
|
30
|
+
type ExportTarget = "scorm12" | "scorm2004" | "standalone";
|
|
22
31
|
interface PackageOptions {
|
|
23
32
|
courseDir: string;
|
|
24
33
|
manifest: CourseManifest;
|
|
@@ -26,6 +35,7 @@ interface PackageOptions {
|
|
|
26
35
|
target: ExportTarget;
|
|
27
36
|
runtimeClientJs: string;
|
|
28
37
|
runtimeCss: string;
|
|
38
|
+
componentsBundleJs?: string;
|
|
29
39
|
assessmentBundle?: RuntimeAssessmentBundle;
|
|
30
40
|
}
|
|
31
41
|
declare function shouldSkipCourseFile(rel: string): boolean;
|
|
@@ -36,6 +46,22 @@ declare function collectFiles(dir: string, baseDir: string): Promise<Array<{
|
|
|
36
46
|
declare function buildManifestFileList(courseFiles: Array<{
|
|
37
47
|
path: string;
|
|
38
48
|
}>): string[];
|
|
49
|
+
interface PackageSink {
|
|
50
|
+
writeFile(relPath: string, content: string | Buffer): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
declare function assemblePackage(options: Omit<PackageOptions, "outputPath">, sink: PackageSink): Promise<{
|
|
53
|
+
fileCount: number;
|
|
54
|
+
}>;
|
|
55
|
+
declare function packageScorm2004(options: PackageOptions): Promise<{
|
|
56
|
+
outputPath: string;
|
|
57
|
+
fileCount: number;
|
|
58
|
+
}>;
|
|
59
|
+
declare function packageScorm2004Dir(options: Omit<PackageOptions, "outputPath"> & {
|
|
60
|
+
outputDir: string;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
outputDir: string;
|
|
63
|
+
fileCount: number;
|
|
64
|
+
}>;
|
|
39
65
|
declare function packageCourse(options: PackageOptions): Promise<{
|
|
40
66
|
outputPath: string;
|
|
41
67
|
fileCount: number;
|
|
@@ -47,10 +73,19 @@ declare function packageStandaloneDir(options: Omit<PackageOptions, "outputPath"
|
|
|
47
73
|
fileCount: number;
|
|
48
74
|
}>;
|
|
49
75
|
|
|
76
|
+
/** Omit embedded quiz data from lesson SCOs; limit assessment SCOs to one quiz. */
|
|
77
|
+
declare function sliceAssessmentBundleForActivity(bundle: RuntimeAssessmentBundle, activityId: string, activityKind: "lesson" | "assessment"): RuntimeAssessmentBundle;
|
|
78
|
+
|
|
50
79
|
/** JSON safe to embed in HTML script blocks (prevents `</script>` breakout). */
|
|
51
80
|
declare function safeJsonForHtml(value: unknown): string;
|
|
52
81
|
|
|
53
82
|
/** Stable slug for ZIP filenames and SCORM identifiers. */
|
|
54
83
|
declare function courseSlug(manifest: CourseManifest): string;
|
|
55
84
|
|
|
56
|
-
|
|
85
|
+
declare function scoLaunchPath(activityId: string): string;
|
|
86
|
+
declare function buildScorm2004ManifestFiles(manifest: CourseManifest, courseFiles: string[], hasComponentsBundle?: boolean): string[];
|
|
87
|
+
declare function generateScorm2004Manifest(manifest: CourseManifest, courseFiles: string[], options?: {
|
|
88
|
+
hasComponentsBundle?: boolean;
|
|
89
|
+
}): string;
|
|
90
|
+
|
|
91
|
+
export { type BuildHtmlOptions, type ExportTarget, type PackageOptions, type PackageSink, type PageTemplateOptions, assemblePackage, buildIndexHtml, buildLearnerPageHtml, buildManifestFileList, buildRuntimeConfig, buildScoIndexHtml, buildScorm2004ManifestFiles, collectFiles, courseSlug, generateImsManifest, generateScorm2004Manifest, manifestIdentifier, packageCourse, packageScorm2004, packageScorm2004Dir, packageStandaloneDir, safeJsonForHtml, scoLaunchPath, shouldSkipCourseFile, sliceAssessmentBundleForActivity };
|
package/dist/index.js
CHANGED
|
@@ -57,49 +57,183 @@ function safeJsonForHtml(value) {
|
|
|
57
57
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// src/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
manifest,
|
|
65
|
-
baseUrl: ".",
|
|
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));
|
|
60
|
+
// src/page-template.ts
|
|
61
|
+
import { escapeHtml } from "@lxpack/validators";
|
|
62
|
+
function buildLearnerPageHtml(options) {
|
|
63
|
+
const componentsTag = options.componentsScript ? `<script type="module" src="${escapeHtml(options.componentsScript)}"></script>` : "";
|
|
76
64
|
return `<!DOCTYPE html>
|
|
77
65
|
<html lang="en">
|
|
78
66
|
<head>
|
|
79
67
|
<meta charset="UTF-8">
|
|
80
68
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
81
|
-
<title>${escapeHtml(
|
|
82
|
-
<style>${runtimeCss}</style>
|
|
69
|
+
<title>${escapeHtml(options.title)}</title>
|
|
70
|
+
<style>${options.runtimeCss}</style>
|
|
83
71
|
</head>
|
|
84
72
|
<body>
|
|
85
73
|
<div id="lxpack-app"></div>
|
|
86
|
-
<script type="application/json" id="lxpack-config">${
|
|
74
|
+
<script type="application/json" id="lxpack-config">${options.configJson}</script>
|
|
87
75
|
<script>
|
|
88
76
|
window.__LXPACK_CONFIG__ = JSON.parse(document.getElementById('lxpack-config').textContent);
|
|
89
77
|
</script>
|
|
90
|
-
|
|
78
|
+
${componentsTag}
|
|
79
|
+
<script type="module" src="${escapeHtml(options.runtimeScript)}"></script>
|
|
91
80
|
</body>
|
|
92
81
|
</html>`;
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
|
|
84
|
+
// src/build-html.ts
|
|
85
|
+
function buildRuntimeConfig(options) {
|
|
86
|
+
const { manifest, mode, assessmentBundle, activityId } = options;
|
|
87
|
+
return {
|
|
88
|
+
manifest,
|
|
89
|
+
baseUrl: activityId ? "../.." : ".",
|
|
90
|
+
mode,
|
|
91
|
+
...activityId ? { activityId } : {},
|
|
92
|
+
...assessmentBundle ? {
|
|
93
|
+
...Object.keys(assessmentBundle.assessments).length ? { assessments: assessmentBundle.assessments } : {},
|
|
94
|
+
...Object.keys(assessmentBundle.answerKeys).length ? { answerKeys: assessmentBundle.answerKeys } : {},
|
|
95
|
+
...Object.keys(assessmentBundle.configs ?? {}).length ? { assessmentConfigs: assessmentBundle.configs } : {},
|
|
96
|
+
...Object.keys(assessmentBundle.feedback ?? {}).length ? { assessmentFeedback: assessmentBundle.feedback } : {}
|
|
97
|
+
} : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildScoIndexHtml(options) {
|
|
101
|
+
const { manifest, runtimeCss, activityId, componentsScript } = options;
|
|
102
|
+
const config = safeJsonForHtml(buildRuntimeConfig({ ...options, activityId }));
|
|
103
|
+
return buildLearnerPageHtml({
|
|
104
|
+
title: `${manifest.title} \u2014 ${activityId}`,
|
|
105
|
+
runtimeCss,
|
|
106
|
+
configJson: config,
|
|
107
|
+
runtimeScript: "../../lxpack-runtime.js",
|
|
108
|
+
componentsScript
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function buildIndexHtml(options) {
|
|
112
|
+
const { manifest, runtimeCss, componentsScript } = options;
|
|
113
|
+
const config = safeJsonForHtml(buildRuntimeConfig(options));
|
|
114
|
+
return buildLearnerPageHtml({
|
|
115
|
+
title: manifest.title,
|
|
116
|
+
runtimeCss,
|
|
117
|
+
configJson: config,
|
|
118
|
+
runtimeScript: "./lxpack-runtime.js",
|
|
119
|
+
componentsScript
|
|
120
|
+
});
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
// src/package.ts
|
|
99
|
-
import { readFile, readdir, writeFile, mkdir
|
|
124
|
+
import { readFile, readdir, writeFile, mkdir } from "fs/promises";
|
|
100
125
|
import { existsSync } from "fs";
|
|
101
126
|
import { dirname, join, relative } from "path";
|
|
102
127
|
import JSZip from "jszip";
|
|
128
|
+
|
|
129
|
+
// src/assessment-slice.ts
|
|
130
|
+
function sliceAssessmentBundleForActivity(bundle, activityId, activityKind) {
|
|
131
|
+
if (activityKind === "assessment") {
|
|
132
|
+
const assessment = bundle.assessments[activityId];
|
|
133
|
+
if (!assessment) {
|
|
134
|
+
return { assessments: {}, answerKeys: {}, configs: {}, feedback: {} };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
assessments: { [activityId]: assessment },
|
|
138
|
+
answerKeys: bundle.answerKeys[activityId] ? { [activityId]: bundle.answerKeys[activityId] } : {},
|
|
139
|
+
configs: bundle.configs[activityId] ? { [activityId]: bundle.configs[activityId] } : {},
|
|
140
|
+
feedback: bundle.feedback[activityId] ? { [activityId]: bundle.feedback[activityId] } : {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
assessments: {},
|
|
145
|
+
answerKeys: {},
|
|
146
|
+
configs: {},
|
|
147
|
+
feedback: {}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/activities.ts
|
|
152
|
+
import {
|
|
153
|
+
enumerateActivities
|
|
154
|
+
} from "@lxpack/validators";
|
|
155
|
+
|
|
156
|
+
// src/scorm2004-manifest.ts
|
|
157
|
+
function escapeXml2(text) {
|
|
158
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
159
|
+
}
|
|
160
|
+
function scoLaunchPath(activityId) {
|
|
161
|
+
return `sco/${activityId}/index.html`;
|
|
162
|
+
}
|
|
163
|
+
function buildScorm2004ManifestFiles(manifest, courseFiles, hasComponentsBundle = false) {
|
|
164
|
+
const activities = enumerateActivities(manifest);
|
|
165
|
+
const scoFiles = activities.map((a) => scoLaunchPath(a.id));
|
|
166
|
+
return [
|
|
167
|
+
"lxpack-runtime.js",
|
|
168
|
+
...hasComponentsBundle ? ["lxpack-components.js"] : [],
|
|
169
|
+
...scoFiles,
|
|
170
|
+
...courseFiles
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
function generateScorm2004Manifest(manifest, courseFiles, options) {
|
|
174
|
+
const hasComponentsBundle = options?.hasComponentsBundle ?? false;
|
|
175
|
+
const identifier = manifestIdentifier(manifest);
|
|
176
|
+
const orgId = `${identifier}-org`;
|
|
177
|
+
const activities = enumerateActivities(manifest);
|
|
178
|
+
const itemEntries = activities.map((activity) => {
|
|
179
|
+
const itemId = `item_${activity.id}`;
|
|
180
|
+
const resourceId = `res_${activity.id}`;
|
|
181
|
+
const sequencing = activity.kind === "assessment" ? `
|
|
182
|
+
<imsss:sequencing>
|
|
183
|
+
<imsss:deliveryControls tracked="true" completionSetByContent="true" objectiveSetByContent="true"/>
|
|
184
|
+
</imsss:sequencing>` : `
|
|
185
|
+
<imsss:sequencing>
|
|
186
|
+
<imsss:deliveryControls tracked="true" completionSetByContent="true"/>
|
|
187
|
+
</imsss:sequencing>`;
|
|
188
|
+
return ` <item identifier="${escapeXml2(itemId)}" identifierref="${escapeXml2(resourceId)}">
|
|
189
|
+
<title>${escapeXml2(activity.title)}</title>${sequencing}
|
|
190
|
+
</item>`;
|
|
191
|
+
}).join("\n");
|
|
192
|
+
const resources = activities.map((activity) => {
|
|
193
|
+
const resourceId = `res_${activity.id}`;
|
|
194
|
+
const href = scoLaunchPath(activity.id);
|
|
195
|
+
const componentFile = hasComponentsBundle ? `
|
|
196
|
+
<file href="lxpack-components.js"/>` : "";
|
|
197
|
+
return ` <resource identifier="${escapeXml2(resourceId)}" type="webcontent" adlcp:scormType="sco" href="${escapeXml2(href)}">
|
|
198
|
+
<file href="${escapeXml2(href)}"/>
|
|
199
|
+
<file href="lxpack-runtime.js"/>${componentFile}
|
|
200
|
+
</resource>`;
|
|
201
|
+
}).join("\n");
|
|
202
|
+
const orgSequencing = `
|
|
203
|
+
<imsss:sequencing>
|
|
204
|
+
<imsss:controlMode choice="true" flow="true"/>
|
|
205
|
+
</imsss:sequencing>`;
|
|
206
|
+
const uniqueCourseFiles = [...new Set(courseFiles)].filter((f) => !f.startsWith("sco/")).map(
|
|
207
|
+
(href) => ` <file href="${escapeXml2(href)}"/>`
|
|
208
|
+
).join("\n");
|
|
209
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
210
|
+
<manifest identifier="${escapeXml2(identifier)}" version="${escapeXml2(manifest.version)}"
|
|
211
|
+
xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
|
|
212
|
+
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_v1p3"
|
|
213
|
+
xmlns:adlseq="http://www.adlnet.org/xsd/adlseq_v1p3"
|
|
214
|
+
xmlns:imsss="http://www.imsglobal.org/xsd/imsss"
|
|
215
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
216
|
+
<metadata>
|
|
217
|
+
<schema>ADL SCORM</schema>
|
|
218
|
+
<schemaversion>2004 4th Edition</schemaversion>
|
|
219
|
+
</metadata>
|
|
220
|
+
<organizations default="${escapeXml2(orgId)}">
|
|
221
|
+
<organization identifier="${escapeXml2(orgId)}">
|
|
222
|
+
<title>${escapeXml2(manifest.title)}</title>${orgSequencing}
|
|
223
|
+
${itemEntries}
|
|
224
|
+
</organization>
|
|
225
|
+
</organizations>
|
|
226
|
+
<resources>
|
|
227
|
+
${resources}
|
|
228
|
+
<resource identifier="shared_assets" type="webcontent" adlcp:scormType="asset">
|
|
229
|
+
${uniqueCourseFiles}
|
|
230
|
+
<file href="lxpack-runtime.js"/>${hasComponentsBundle ? '\n <file href="lxpack-components.js"/>' : ""}
|
|
231
|
+
</resource>
|
|
232
|
+
</resources>
|
|
233
|
+
</manifest>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/package.ts
|
|
103
237
|
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
104
238
|
"course.yaml",
|
|
105
239
|
"lxpack.config.ts",
|
|
@@ -140,38 +274,119 @@ async function collectFiles(dir, baseDir) {
|
|
|
140
274
|
function buildManifestFileList(courseFiles) {
|
|
141
275
|
return ["index.html", "lxpack-runtime.js", ...courseFiles.map((f) => f.path)];
|
|
142
276
|
}
|
|
143
|
-
async function
|
|
277
|
+
async function writeSingleScoArtifacts(options) {
|
|
144
278
|
const {
|
|
145
279
|
courseDir,
|
|
146
280
|
manifest,
|
|
147
|
-
outputPath,
|
|
148
281
|
target,
|
|
149
282
|
runtimeClientJs,
|
|
150
283
|
runtimeCss,
|
|
151
|
-
assessmentBundle
|
|
284
|
+
assessmentBundle,
|
|
285
|
+
componentsBundleJs,
|
|
286
|
+
writeFile: writeArtifact
|
|
152
287
|
} = options;
|
|
153
|
-
const zip = new JSZip();
|
|
154
288
|
const mode = target === "scorm12" ? "scorm12" : "standalone";
|
|
155
289
|
const courseFiles = await collectFiles(courseDir, courseDir);
|
|
290
|
+
let fileCount = 0;
|
|
156
291
|
for (const file of courseFiles) {
|
|
157
292
|
const content = await readFile(file.fullPath);
|
|
158
|
-
|
|
293
|
+
await writeArtifact(file.path, content);
|
|
294
|
+
fileCount++;
|
|
159
295
|
}
|
|
160
296
|
const indexHtml = buildIndexHtml({
|
|
161
297
|
manifest,
|
|
162
298
|
runtimeCss,
|
|
163
299
|
mode,
|
|
164
|
-
assessmentBundle
|
|
300
|
+
assessmentBundle,
|
|
301
|
+
componentsScript: componentsBundleJs ? "./lxpack-components.js" : void 0
|
|
165
302
|
});
|
|
166
|
-
|
|
167
|
-
|
|
303
|
+
await writeArtifact("index.html", indexHtml);
|
|
304
|
+
await writeArtifact("lxpack-runtime.js", runtimeClientJs);
|
|
305
|
+
fileCount += 2;
|
|
306
|
+
if (componentsBundleJs) {
|
|
307
|
+
await writeArtifact("lxpack-components.js", componentsBundleJs);
|
|
308
|
+
fileCount++;
|
|
309
|
+
}
|
|
168
310
|
if (target === "scorm12") {
|
|
169
311
|
const manifestFiles = buildManifestFileList(courseFiles);
|
|
170
|
-
|
|
312
|
+
await writeArtifact(
|
|
171
313
|
"imsmanifest.xml",
|
|
172
314
|
generateImsManifest(manifest, manifestFiles)
|
|
173
315
|
);
|
|
316
|
+
fileCount++;
|
|
317
|
+
}
|
|
318
|
+
return { fileCount };
|
|
319
|
+
}
|
|
320
|
+
async function assemblePackage(options, sink) {
|
|
321
|
+
if (options.target === "scorm2004") {
|
|
322
|
+
return writeScorm2004Artifacts({ ...options, writeFile: sink.writeFile });
|
|
174
323
|
}
|
|
324
|
+
return writeSingleScoArtifacts({ ...options, writeFile: sink.writeFile });
|
|
325
|
+
}
|
|
326
|
+
async function writeScorm2004Artifacts(options) {
|
|
327
|
+
const {
|
|
328
|
+
courseDir,
|
|
329
|
+
manifest,
|
|
330
|
+
runtimeClientJs,
|
|
331
|
+
runtimeCss,
|
|
332
|
+
componentsBundleJs,
|
|
333
|
+
assessmentBundle,
|
|
334
|
+
writeFile: writeArtifact
|
|
335
|
+
} = options;
|
|
336
|
+
const courseFiles = await collectFiles(courseDir, courseDir);
|
|
337
|
+
let fileCount = 0;
|
|
338
|
+
for (const file of courseFiles) {
|
|
339
|
+
const content = await readFile(file.fullPath);
|
|
340
|
+
await writeArtifact(file.path, content);
|
|
341
|
+
fileCount++;
|
|
342
|
+
}
|
|
343
|
+
await writeArtifact("lxpack-runtime.js", runtimeClientJs);
|
|
344
|
+
fileCount++;
|
|
345
|
+
if (componentsBundleJs) {
|
|
346
|
+
await writeArtifact("lxpack-components.js", componentsBundleJs);
|
|
347
|
+
fileCount++;
|
|
348
|
+
}
|
|
349
|
+
const activities = enumerateActivities(manifest);
|
|
350
|
+
for (const activity of activities) {
|
|
351
|
+
const scoBundle = assessmentBundle != null ? sliceAssessmentBundleForActivity(
|
|
352
|
+
assessmentBundle,
|
|
353
|
+
activity.id,
|
|
354
|
+
activity.kind
|
|
355
|
+
) : void 0;
|
|
356
|
+
const html = buildScoIndexHtml({
|
|
357
|
+
manifest,
|
|
358
|
+
runtimeCss,
|
|
359
|
+
mode: "scorm2004",
|
|
360
|
+
activityId: activity.id,
|
|
361
|
+
assessmentBundle: scoBundle,
|
|
362
|
+
componentsScript: componentsBundleJs ? "../../lxpack-components.js" : void 0
|
|
363
|
+
});
|
|
364
|
+
await writeArtifact(`sco/${activity.id}/index.html`, html);
|
|
365
|
+
fileCount++;
|
|
366
|
+
}
|
|
367
|
+
const manifestFiles = buildScorm2004ManifestFiles(
|
|
368
|
+
manifest,
|
|
369
|
+
courseFiles.map((f) => f.path),
|
|
370
|
+
Boolean(componentsBundleJs)
|
|
371
|
+
);
|
|
372
|
+
await writeArtifact(
|
|
373
|
+
"imsmanifest.xml",
|
|
374
|
+
generateScorm2004Manifest(manifest, manifestFiles, {
|
|
375
|
+
hasComponentsBundle: Boolean(componentsBundleJs)
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
fileCount++;
|
|
379
|
+
return { fileCount };
|
|
380
|
+
}
|
|
381
|
+
async function packageScorm2004(options) {
|
|
382
|
+
const { outputPath, ...rest } = options;
|
|
383
|
+
const zip = new JSZip();
|
|
384
|
+
const { fileCount } = await writeScorm2004Artifacts({
|
|
385
|
+
...rest,
|
|
386
|
+
writeFile: async (relPath, content) => {
|
|
387
|
+
zip.file(relPath, content);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
175
390
|
const buffer = await zip.generateAsync({
|
|
176
391
|
type: "nodebuffer",
|
|
177
392
|
compression: "DEFLATE"
|
|
@@ -179,7 +394,44 @@ async function packageCourse(options) {
|
|
|
179
394
|
await writeFile(outputPath, buffer);
|
|
180
395
|
return {
|
|
181
396
|
outputPath,
|
|
182
|
-
fileCount: Object.keys(zip.files).length
|
|
397
|
+
fileCount: Object.keys(zip.files).length || fileCount
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async function packageScorm2004Dir(options) {
|
|
401
|
+
const { outputDir, ...rest } = options;
|
|
402
|
+
await mkdir(outputDir, { recursive: true });
|
|
403
|
+
const { fileCount } = await writeScorm2004Artifacts({
|
|
404
|
+
...rest,
|
|
405
|
+
writeFile: async (relPath, content) => {
|
|
406
|
+
const dest = join(outputDir, relPath);
|
|
407
|
+
const destDir = dirname(dest);
|
|
408
|
+
if (!existsSync(destDir)) {
|
|
409
|
+
await mkdir(destDir, { recursive: true });
|
|
410
|
+
}
|
|
411
|
+
await writeFile(dest, content);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return { outputDir, fileCount };
|
|
415
|
+
}
|
|
416
|
+
async function packageCourse(options) {
|
|
417
|
+
if (options.target === "scorm2004") {
|
|
418
|
+
return packageScorm2004(options);
|
|
419
|
+
}
|
|
420
|
+
const { outputPath, ...rest } = options;
|
|
421
|
+
const zip = new JSZip();
|
|
422
|
+
const { fileCount } = await assemblePackage(rest, {
|
|
423
|
+
writeFile: async (relPath, content) => {
|
|
424
|
+
zip.file(relPath, content);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
const buffer = await zip.generateAsync({
|
|
428
|
+
type: "nodebuffer",
|
|
429
|
+
compression: "DEFLATE"
|
|
430
|
+
});
|
|
431
|
+
await writeFile(outputPath, buffer);
|
|
432
|
+
return {
|
|
433
|
+
outputPath,
|
|
434
|
+
fileCount: Object.keys(zip.files).length || fileCount
|
|
183
435
|
};
|
|
184
436
|
}
|
|
185
437
|
async function packageStandaloneDir(options) {
|
|
@@ -190,50 +442,57 @@ async function packageStandaloneDir(options) {
|
|
|
190
442
|
target,
|
|
191
443
|
runtimeClientJs,
|
|
192
444
|
runtimeCss,
|
|
193
|
-
assessmentBundle
|
|
445
|
+
assessmentBundle,
|
|
446
|
+
componentsBundleJs
|
|
194
447
|
} = options;
|
|
195
448
|
await mkdir(outputDir, { recursive: true });
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
449
|
+
const { fileCount } = await assemblePackage(
|
|
450
|
+
{
|
|
451
|
+
courseDir,
|
|
452
|
+
manifest,
|
|
453
|
+
target,
|
|
454
|
+
runtimeClientJs,
|
|
455
|
+
runtimeCss,
|
|
456
|
+
assessmentBundle,
|
|
457
|
+
componentsBundleJs
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
writeFile: async (relPath, content) => {
|
|
461
|
+
const dest = join(outputDir, relPath);
|
|
462
|
+
const destDir = dirname(dest);
|
|
463
|
+
if (!existsSync(destDir)) {
|
|
464
|
+
await mkdir(destDir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
if (typeof content === "string") {
|
|
467
|
+
await writeFile(dest, content);
|
|
468
|
+
} else {
|
|
469
|
+
await writeFile(dest, content);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
204
472
|
}
|
|
205
|
-
|
|
206
|
-
fileCount++;
|
|
207
|
-
}
|
|
208
|
-
const indexHtml = buildIndexHtml({
|
|
209
|
-
manifest,
|
|
210
|
-
runtimeCss,
|
|
211
|
-
mode,
|
|
212
|
-
assessmentBundle
|
|
213
|
-
});
|
|
214
|
-
await writeFile(join(outputDir, "index.html"), indexHtml);
|
|
215
|
-
await writeFile(join(outputDir, "lxpack-runtime.js"), runtimeClientJs);
|
|
216
|
-
fileCount += 2;
|
|
217
|
-
if (target === "scorm12") {
|
|
218
|
-
const manifestFiles = buildManifestFileList(courseFiles);
|
|
219
|
-
await writeFile(
|
|
220
|
-
join(outputDir, "imsmanifest.xml"),
|
|
221
|
-
generateImsManifest(manifest, manifestFiles)
|
|
222
|
-
);
|
|
223
|
-
fileCount++;
|
|
224
|
-
}
|
|
473
|
+
);
|
|
225
474
|
return { outputDir, fileCount };
|
|
226
475
|
}
|
|
227
476
|
export {
|
|
477
|
+
assemblePackage,
|
|
228
478
|
buildIndexHtml,
|
|
479
|
+
buildLearnerPageHtml,
|
|
229
480
|
buildManifestFileList,
|
|
230
481
|
buildRuntimeConfig,
|
|
482
|
+
buildScoIndexHtml,
|
|
483
|
+
buildScorm2004ManifestFiles,
|
|
231
484
|
collectFiles,
|
|
232
485
|
courseSlug,
|
|
233
486
|
generateImsManifest,
|
|
487
|
+
generateScorm2004Manifest,
|
|
488
|
+
enumerateActivities as listCourseActivities,
|
|
234
489
|
manifestIdentifier,
|
|
235
490
|
packageCourse,
|
|
491
|
+
packageScorm2004,
|
|
492
|
+
packageScorm2004Dir,
|
|
236
493
|
packageStandaloneDir,
|
|
237
494
|
safeJsonForHtml,
|
|
238
|
-
|
|
495
|
+
scoLaunchPath,
|
|
496
|
+
shouldSkipCourseFile,
|
|
497
|
+
sliceAssessmentBundleForActivity
|
|
239
498
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lxpack/scorm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.
|
|
42
|
+
"@lxpack/validators": "0.2.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.13.10",
|