@patch-adams/core 1.0.6 → 1.0.8

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/dist/index.cjs ADDED
@@ -0,0 +1,1049 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+ var url = require('url');
7
+ var AdmZip = require('adm-zip');
8
+ var yazl = require('yazl');
9
+ var fastXmlParser = require('fast-xml-parser');
10
+ var chalk = require('chalk');
11
+
12
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
+
14
+ var AdmZip__default = /*#__PURE__*/_interopDefault(AdmZip);
15
+ var yazl__default = /*#__PURE__*/_interopDefault(yazl);
16
+ var chalk__default = /*#__PURE__*/_interopDefault(chalk);
17
+
18
+ // src/config/schema.ts
19
+ var BlockingAssetConfigSchema = zod.z.object({
20
+ /** Filename for the asset */
21
+ filename: zod.z.string().min(1),
22
+ /** Whether this asset is enabled */
23
+ enabled: zod.z.boolean().default(true)
24
+ });
25
+ var AsyncAssetConfigSchema = zod.z.object({
26
+ /** Filename for the asset */
27
+ filename: zod.z.string().min(1),
28
+ /** Whether this asset is enabled */
29
+ enabled: zod.z.boolean().default(true),
30
+ /** Timeout in milliseconds for remote loading before falling back to local */
31
+ timeout: zod.z.number().min(1e3).max(3e4).default(5e3)
32
+ });
33
+ var LocalFoldersConfigSchema = zod.z.object({
34
+ /** Folder name for CSS files within scormcontent */
35
+ css: zod.z.string().default("css"),
36
+ /** Folder name for JS files within scormcontent */
37
+ js: zod.z.string().default("js")
38
+ });
39
+ var PatchAdamsConfigSchema = zod.z.object({
40
+ /** Remote domain for assets (e.g., "https://cdn.example.com/rise-overrides") */
41
+ remoteDomain: zod.z.string().url(),
42
+ /** CSS class added to <html> tag for specificity (default: "pa-patched") */
43
+ htmlClass: zod.z.string().default("pa-patched"),
44
+ /** CSS class added during loading, removed when ready (default: "pa-loading") */
45
+ loadingClass: zod.z.string().default("pa-loading"),
46
+ /** CSS loaded at start of <head> - blocking, prevents FOUC */
47
+ cssBefore: BlockingAssetConfigSchema.default({
48
+ filename: "before.css",
49
+ enabled: true
50
+ }),
51
+ /** CSS loaded at end of <head> - async with fallback, for overrides */
52
+ cssAfter: AsyncAssetConfigSchema.default({
53
+ filename: "after.css",
54
+ enabled: true,
55
+ timeout: 5e3
56
+ }),
57
+ /** JS loaded at start of <head> - blocking, for setup/interception */
58
+ jsBefore: BlockingAssetConfigSchema.default({
59
+ filename: "before.js",
60
+ enabled: true
61
+ }),
62
+ /** JS loaded at end of <body> - async with fallback, removes loading class */
63
+ jsAfter: AsyncAssetConfigSchema.default({
64
+ filename: "after.js",
65
+ enabled: true,
66
+ timeout: 5e3
67
+ }),
68
+ /** Local folder names for fallback files */
69
+ localFolders: LocalFoldersConfigSchema.default({}),
70
+ /** Whether to update manifest files (imsmanifest.xml, etc.) for SCORM compliance */
71
+ updateManifests: zod.z.boolean().default(true)
72
+ });
73
+
74
+ // src/config/defaults.ts
75
+ var defaultConfig = {
76
+ remoteDomain: "https://cdn.example.com/rise-overrides",
77
+ htmlClass: "pa-patched",
78
+ loadingClass: "pa-loading",
79
+ cssBefore: {
80
+ filename: "before.css",
81
+ enabled: true
82
+ },
83
+ cssAfter: {
84
+ filename: "after.css",
85
+ enabled: true,
86
+ timeout: 5e3
87
+ },
88
+ jsBefore: {
89
+ filename: "before.js",
90
+ enabled: true
91
+ },
92
+ jsAfter: {
93
+ filename: "after.js",
94
+ enabled: true,
95
+ timeout: 5e3
96
+ },
97
+ localFolders: {
98
+ css: "css",
99
+ js: "js"
100
+ },
101
+ updateManifests: true
102
+ };
103
+ async function loadConfig(configPath) {
104
+ const absolutePath = path.resolve(configPath);
105
+ if (!fs.existsSync(absolutePath)) {
106
+ throw new Error(`Configuration file not found: ${absolutePath}`);
107
+ }
108
+ const ext = path.extname(absolutePath).toLowerCase();
109
+ let rawConfig;
110
+ if (ext === ".json") {
111
+ const content = fs.readFileSync(absolutePath, "utf-8");
112
+ rawConfig = JSON.parse(content);
113
+ } else if (ext === ".ts" || ext === ".js" || ext === ".mjs") {
114
+ const fileUrl = url.pathToFileURL(absolutePath).href;
115
+ const module = await import(fileUrl);
116
+ rawConfig = module.default ?? module;
117
+ } else {
118
+ throw new Error(`Unsupported config file extension: ${ext}. Use .ts, .js, .mjs, or .json`);
119
+ }
120
+ const validated = PatchAdamsConfigSchema.parse(rawConfig);
121
+ return validated;
122
+ }
123
+ function mergeWithDefaults(partial) {
124
+ return PatchAdamsConfigSchema.parse({
125
+ ...defaultConfig,
126
+ ...partial
127
+ });
128
+ }
129
+ function generateConfigTemplate() {
130
+ return `import type { PatchAdamsConfig } from '@patch-adams/core';
131
+
132
+ const config: PatchAdamsConfig = {
133
+ // Remote domain where your CSS/JS files are hosted
134
+ remoteDomain: 'https://cdn.example.com/rise-overrides',
135
+
136
+ // HTML classes for specificity and loading state
137
+ htmlClass: 'pa-patched', // Always present on <html>
138
+ loadingClass: 'pa-loading', // Removed when assets are loaded
139
+
140
+ // CSS loaded at start of <head> - BLOCKING
141
+ // Use this to hide content and prevent flash of unstyled content
142
+ cssBefore: {
143
+ filename: 'before.css',
144
+ enabled: true,
145
+ },
146
+
147
+ // CSS loaded at end of <head> - ASYNC with fallback
148
+ // Use this for style overrides
149
+ cssAfter: {
150
+ filename: 'after.css',
151
+ enabled: true,
152
+ timeout: 5000, // ms before falling back to local
153
+ },
154
+
155
+ // JS loaded at start of <head> - BLOCKING
156
+ // Use this for setup, API interception, globals
157
+ jsBefore: {
158
+ filename: 'before.js',
159
+ enabled: true,
160
+ },
161
+
162
+ // JS loaded at end of <body> - ASYNC with fallback
163
+ // Use this for DOM manipulation after Rise loads
164
+ // This should remove the loadingClass when done
165
+ jsAfter: {
166
+ filename: 'after.js',
167
+ enabled: true,
168
+ timeout: 5000, // ms before falling back to local
169
+ },
170
+
171
+ // Folder names for local fallback files within scormcontent/
172
+ localFolders: {
173
+ css: 'css',
174
+ js: 'js',
175
+ },
176
+
177
+ // Update manifest files for SCORM compliance
178
+ updateManifests: true,
179
+ };
180
+
181
+ export default config;
182
+ `;
183
+ }
184
+
185
+ // src/templates/css-before.ts
186
+ function generateCssBeforeLoader(options) {
187
+ const { remoteUrl, localPath, htmlClass, loadingClass } = options;
188
+ return `<!-- === PATCH-ADAMS: CSS BEFORE (blocking) === -->
189
+ <style data-pa="css-before-critical">
190
+ /* Critical: Hide content until ready */
191
+ html.${htmlClass}.${loadingClass} body {
192
+ visibility: hidden !important;
193
+ }
194
+ </style>
195
+ <script data-pa="css-before-loader">
196
+ (function() {
197
+ 'use strict';
198
+ var REMOTE_URL = "${remoteUrl}";
199
+ var LOCAL_PATH = "${localPath}";
200
+
201
+ function loadCSSSync(url) {
202
+ var link = document.createElement('link');
203
+ link.rel = 'stylesheet';
204
+ link.href = url;
205
+ link.setAttribute('data-pa', 'css-before');
206
+ document.head.appendChild(link);
207
+ return link;
208
+ }
209
+
210
+ // Try remote first
211
+ var link = loadCSSSync(REMOTE_URL);
212
+
213
+ link.onerror = function() {
214
+ console.warn('[Patch-Adams] CSS before failed to load from remote, using local fallback');
215
+ document.head.removeChild(link);
216
+ loadCSSSync(LOCAL_PATH);
217
+ };
218
+
219
+ link.onload = function() {
220
+ console.log('[Patch-Adams] CSS before loaded from remote:', REMOTE_URL);
221
+ };
222
+ })();
223
+ </script>`;
224
+ }
225
+ function buildCssBeforeOptions(config) {
226
+ return {
227
+ remoteUrl: `${config.remoteDomain}/${config.cssBefore.filename}`,
228
+ localPath: `${config.localFolders.css}/${config.cssBefore.filename}`,
229
+ htmlClass: config.htmlClass,
230
+ loadingClass: config.loadingClass
231
+ };
232
+ }
233
+
234
+ // src/templates/css-after.ts
235
+ function generateCssAfterLoader(options) {
236
+ const { remoteUrl, localPath, timeout } = options;
237
+ return `<!-- === PATCH-ADAMS: CSS AFTER (async with fallback) === -->
238
+ <script data-pa="css-after-loader">
239
+ (function() {
240
+ 'use strict';
241
+ var REMOTE_URL = "${remoteUrl}";
242
+ var LOCAL_PATH = "${localPath}";
243
+ var TIMEOUT = ${timeout};
244
+
245
+ function loadCSS(url, onSuccess, onError) {
246
+ var link = document.createElement('link');
247
+ link.rel = 'stylesheet';
248
+ link.href = url;
249
+ link.setAttribute('data-pa', 'css-after');
250
+
251
+ link.onload = function() {
252
+ if (onSuccess) onSuccess();
253
+ };
254
+
255
+ link.onerror = function() {
256
+ if (onError) onError();
257
+ };
258
+
259
+ document.head.appendChild(link);
260
+ return link;
261
+ }
262
+
263
+ function loadCSSWithFallback() {
264
+ var loaded = false;
265
+ var timeoutId;
266
+
267
+ // Try remote first
268
+ var remoteLink = loadCSS(
269
+ REMOTE_URL,
270
+ function() {
271
+ if (loaded) return;
272
+ loaded = true;
273
+ clearTimeout(timeoutId);
274
+ console.log('[Patch-Adams] CSS after loaded from remote:', REMOTE_URL);
275
+ },
276
+ function() {
277
+ if (loaded) return;
278
+ loaded = true;
279
+ clearTimeout(timeoutId);
280
+ loadLocalFallback();
281
+ }
282
+ );
283
+
284
+ // Timeout fallback
285
+ timeoutId = setTimeout(function() {
286
+ if (loaded) return;
287
+ loaded = true;
288
+ console.warn('[Patch-Adams] CSS after timed out, using local fallback');
289
+ if (remoteLink.parentNode) {
290
+ document.head.removeChild(remoteLink);
291
+ }
292
+ loadLocalFallback();
293
+ }, TIMEOUT);
294
+ }
295
+
296
+ function loadLocalFallback() {
297
+ loadCSS(
298
+ LOCAL_PATH,
299
+ function() {
300
+ console.log('[Patch-Adams] CSS after loaded from local fallback:', LOCAL_PATH);
301
+ },
302
+ function() {
303
+ console.error('[Patch-Adams] CSS after failed to load from both remote and local');
304
+ }
305
+ );
306
+ }
307
+
308
+ // Execute immediately
309
+ loadCSSWithFallback();
310
+ })();
311
+ </script>`;
312
+ }
313
+ function buildCssAfterOptions(config) {
314
+ return {
315
+ remoteUrl: `${config.remoteDomain}/${config.cssAfter.filename}`,
316
+ localPath: `${config.localFolders.css}/${config.cssAfter.filename}`,
317
+ timeout: config.cssAfter.timeout
318
+ };
319
+ }
320
+
321
+ // src/templates/js-before.ts
322
+ function generateJsBeforeLoader(options) {
323
+ const { remoteUrl, localPath, htmlClass, loadingClass } = options;
324
+ return `<!-- === PATCH-ADAMS: JS BEFORE (blocking) === -->
325
+ <script data-pa="js-before-loader">
326
+ (function() {
327
+ 'use strict';
328
+ var REMOTE_URL = "${remoteUrl}";
329
+ var LOCAL_PATH = "${localPath}";
330
+
331
+ // Initialize Patch-Adams global namespace
332
+ window.PatchAdams = window.PatchAdams || {
333
+ version: '1.0.0',
334
+ htmlClass: '${htmlClass}',
335
+ loadingClass: '${loadingClass}',
336
+ loaded: {
337
+ cssBefore: false,
338
+ cssAfter: false,
339
+ jsBefore: false,
340
+ jsAfter: false
341
+ }
342
+ };
343
+
344
+ function loadJSSync(url) {
345
+ var script = document.createElement('script');
346
+ script.src = url;
347
+ script.setAttribute('data-pa', 'js-before');
348
+ // Note: For truly blocking behavior, we use document.write
349
+ // but that's fragile. Instead we'll handle errors gracefully.
350
+ document.head.appendChild(script);
351
+ return script;
352
+ }
353
+
354
+ // Try remote first
355
+ var script = loadJSSync(REMOTE_URL);
356
+
357
+ script.onerror = function() {
358
+ console.warn('[Patch-Adams] JS before failed to load from remote, using local fallback');
359
+ document.head.removeChild(script);
360
+ var fallback = loadJSSync(LOCAL_PATH);
361
+ fallback.onload = function() {
362
+ window.PatchAdams.loaded.jsBefore = true;
363
+ console.log('[Patch-Adams] JS before loaded from local fallback:', LOCAL_PATH);
364
+ };
365
+ fallback.onerror = function() {
366
+ console.error('[Patch-Adams] JS before failed to load from both remote and local');
367
+ };
368
+ };
369
+
370
+ script.onload = function() {
371
+ window.PatchAdams.loaded.jsBefore = true;
372
+ console.log('[Patch-Adams] JS before loaded from remote:', REMOTE_URL);
373
+ };
374
+ })();
375
+ </script>`;
376
+ }
377
+ function buildJsBeforeOptions(config) {
378
+ return {
379
+ remoteUrl: `${config.remoteDomain}/${config.jsBefore.filename}`,
380
+ localPath: `${config.localFolders.js}/${config.jsBefore.filename}`,
381
+ htmlClass: config.htmlClass,
382
+ loadingClass: config.loadingClass
383
+ };
384
+ }
385
+
386
+ // src/templates/js-after.ts
387
+ function generateJsAfterLoader(options) {
388
+ const { remoteUrl, localPath, timeout, loadingClass } = options;
389
+ return `<!-- === PATCH-ADAMS: JS AFTER (async with fallback) === -->
390
+ <script data-pa="js-after-loader">
391
+ (function() {
392
+ 'use strict';
393
+ var REMOTE_URL = "${remoteUrl}";
394
+ var LOCAL_PATH = "${localPath}";
395
+ var TIMEOUT = ${timeout};
396
+ var LOADING_CLASS = "${loadingClass}";
397
+
398
+ function loadJS(url, onSuccess, onError) {
399
+ var script = document.createElement('script');
400
+ script.src = url;
401
+ script.async = true;
402
+ script.setAttribute('data-pa', 'js-after');
403
+
404
+ script.onload = function() {
405
+ if (onSuccess) onSuccess();
406
+ };
407
+
408
+ script.onerror = function() {
409
+ if (onError) onError();
410
+ };
411
+
412
+ document.body.appendChild(script);
413
+ return script;
414
+ }
415
+
416
+ function removeLoadingClass() {
417
+ document.documentElement.classList.remove(LOADING_CLASS);
418
+ if (window.PatchAdams) {
419
+ window.PatchAdams.loaded.jsAfter = true;
420
+ }
421
+ console.log('[Patch-Adams] Loading complete, content revealed');
422
+ }
423
+
424
+ function loadJSWithFallback() {
425
+ var loaded = false;
426
+ var timeoutId;
427
+
428
+ // Try remote first
429
+ var remoteScript = loadJS(
430
+ REMOTE_URL,
431
+ function() {
432
+ if (loaded) return;
433
+ loaded = true;
434
+ clearTimeout(timeoutId);
435
+ console.log('[Patch-Adams] JS after loaded from remote:', REMOTE_URL);
436
+ // Give the script a moment to execute, then remove loading class
437
+ setTimeout(removeLoadingClass, 50);
438
+ },
439
+ function() {
440
+ if (loaded) return;
441
+ loaded = true;
442
+ clearTimeout(timeoutId);
443
+ loadLocalFallback();
444
+ }
445
+ );
446
+
447
+ // Timeout fallback
448
+ timeoutId = setTimeout(function() {
449
+ if (loaded) return;
450
+ loaded = true;
451
+ console.warn('[Patch-Adams] JS after timed out, using local fallback');
452
+ if (remoteScript.parentNode) {
453
+ document.body.removeChild(remoteScript);
454
+ }
455
+ loadLocalFallback();
456
+ }, TIMEOUT);
457
+ }
458
+
459
+ function loadLocalFallback() {
460
+ loadJS(
461
+ LOCAL_PATH,
462
+ function() {
463
+ console.log('[Patch-Adams] JS after loaded from local fallback:', LOCAL_PATH);
464
+ setTimeout(removeLoadingClass, 50);
465
+ },
466
+ function() {
467
+ console.error('[Patch-Adams] JS after failed to load from both remote and local');
468
+ // Still remove loading class so content is visible even if JS fails
469
+ removeLoadingClass();
470
+ }
471
+ );
472
+ }
473
+
474
+ // Execute after DOM is ready to ensure Rise has loaded
475
+ if (document.readyState === 'loading') {
476
+ document.addEventListener('DOMContentLoaded', function() {
477
+ // Small delay to ensure Rise has initialized
478
+ setTimeout(loadJSWithFallback, 100);
479
+ });
480
+ } else {
481
+ // DOM is already ready
482
+ setTimeout(loadJSWithFallback, 100);
483
+ }
484
+ })();
485
+ </script>`;
486
+ }
487
+ function buildJsAfterOptions(config) {
488
+ return {
489
+ remoteUrl: `${config.remoteDomain}/${config.jsAfter.filename}`,
490
+ localPath: `${config.localFolders.js}/${config.jsAfter.filename}`,
491
+ timeout: config.jsAfter.timeout,
492
+ loadingClass: config.loadingClass
493
+ };
494
+ }
495
+
496
+ // src/patcher/html-injector.ts
497
+ var HtmlInjector = class {
498
+ config;
499
+ constructor(config) {
500
+ this.config = config;
501
+ }
502
+ /**
503
+ * Inject all loaders into HTML
504
+ */
505
+ inject(html) {
506
+ let result = html;
507
+ result = this.addHtmlClasses(result);
508
+ if (this.config.cssBefore.enabled) {
509
+ result = this.injectCssBefore(result);
510
+ }
511
+ if (this.config.jsBefore.enabled) {
512
+ result = this.injectJsBefore(result);
513
+ }
514
+ if (this.config.cssAfter.enabled) {
515
+ result = this.injectCssAfter(result);
516
+ }
517
+ if (this.config.jsAfter.enabled) {
518
+ result = this.injectJsAfter(result);
519
+ }
520
+ return result;
521
+ }
522
+ /**
523
+ * Add pa-patched and pa-loading classes to <html> tag
524
+ */
525
+ addHtmlClasses(html) {
526
+ const { htmlClass, loadingClass } = this.config;
527
+ const classes = `${htmlClass} ${loadingClass}`;
528
+ const htmlTagPattern = /<html([^>]*)>/i;
529
+ const match = html.match(htmlTagPattern);
530
+ if (!match) {
531
+ return html;
532
+ }
533
+ const attributes = match[1];
534
+ if (/class\s*=\s*["'][^"']*["']/i.test(attributes)) {
535
+ return html.replace(
536
+ htmlTagPattern,
537
+ (_match, attrs) => {
538
+ const newAttrs = attrs.replace(
539
+ /class\s*=\s*["']([^"']*)["']/i,
540
+ (_fullMatch, existingClasses) => `class="${existingClasses} ${classes}"`
541
+ );
542
+ return `<html${newAttrs}>`;
543
+ }
544
+ );
545
+ } else {
546
+ return html.replace(htmlTagPattern, `<html${attributes} class="${classes}">`);
547
+ }
548
+ }
549
+ /**
550
+ * Inject CSS Before loader at start of <head>
551
+ */
552
+ injectCssBefore(html) {
553
+ const options = buildCssBeforeOptions(this.config);
554
+ const loader = generateCssBeforeLoader(options);
555
+ return html.replace(/<head([^>]*)>/i, `<head$1>
556
+ ${loader}`);
557
+ }
558
+ /**
559
+ * Inject JS Before loader at start of <head> (after CSS Before if present)
560
+ */
561
+ injectJsBefore(html) {
562
+ const options = buildJsBeforeOptions(this.config);
563
+ const loader = generateJsBeforeLoader(options);
564
+ const cssBeforeMarker = "<!-- === PATCH-ADAMS: CSS BEFORE";
565
+ if (html.includes(cssBeforeMarker)) {
566
+ const cssBeforeEndPattern = /<\/script>\s*(?=\n|<)/;
567
+ const cssBeforeStart = html.indexOf(cssBeforeMarker);
568
+ const afterCssBefore = html.substring(cssBeforeStart);
569
+ const endMatch = afterCssBefore.match(cssBeforeEndPattern);
570
+ if (endMatch && endMatch.index !== void 0) {
571
+ const insertPos = cssBeforeStart + endMatch.index + endMatch[0].length;
572
+ return html.slice(0, insertPos) + "\n" + loader + html.slice(insertPos);
573
+ }
574
+ }
575
+ return html.replace(/<head([^>]*)>/i, `<head$1>
576
+ ${loader}`);
577
+ }
578
+ /**
579
+ * Inject CSS After loader at end of <head>
580
+ */
581
+ injectCssAfter(html) {
582
+ const options = buildCssAfterOptions(this.config);
583
+ const loader = generateCssAfterLoader(options);
584
+ return html.replace(/<\/head>/i, `${loader}
585
+ </head>`);
586
+ }
587
+ /**
588
+ * Inject JS After loader at end of <body> (after __loadEntry)
589
+ */
590
+ injectJsAfter(html) {
591
+ const options = buildJsAfterOptions(this.config);
592
+ const loader = generateJsAfterLoader(options);
593
+ const loadEntryPattern = /(<script>__loadEntry\(\)<\/script>)/i;
594
+ if (loadEntryPattern.test(html)) {
595
+ return html.replace(loadEntryPattern, `$1
596
+ ${loader}`);
597
+ }
598
+ return html.replace(/<\/body>/i, `${loader}
599
+ </body>`);
600
+ }
601
+ };
602
+ var ManifestUpdater = class {
603
+ config;
604
+ parser;
605
+ builder;
606
+ constructor(config) {
607
+ this.config = config;
608
+ this.parser = new fastXmlParser.XMLParser({
609
+ ignoreAttributes: false,
610
+ attributeNamePrefix: "@_",
611
+ preserveOrder: false,
612
+ parseAttributeValue: false,
613
+ trimValues: true
614
+ });
615
+ this.builder = new fastXmlParser.XMLBuilder({
616
+ ignoreAttributes: false,
617
+ attributeNamePrefix: "@_",
618
+ format: true,
619
+ indentBy: " ",
620
+ suppressEmptyNode: true
621
+ });
622
+ }
623
+ /**
624
+ * Update manifest files based on package format
625
+ */
626
+ update(zip, format, paths) {
627
+ const modified = [];
628
+ switch (format) {
629
+ case "scorm12":
630
+ case "scorm2004-3":
631
+ case "scorm2004-4":
632
+ modified.push(...this.updateImsManifest(zip, paths));
633
+ break;
634
+ case "cmi5":
635
+ modified.push(...this.updateImsManifest(zip, paths));
636
+ break;
637
+ case "xapi":
638
+ break;
639
+ case "aicc":
640
+ const aiccManifest = zip.getEntry("imsmanifest.xml");
641
+ if (aiccManifest) {
642
+ modified.push(...this.updateImsManifest(zip, paths));
643
+ }
644
+ break;
645
+ }
646
+ return modified;
647
+ }
648
+ /**
649
+ * Update imsmanifest.xml to include new file references
650
+ */
651
+ updateImsManifest(zip, paths) {
652
+ const entry = zip.getEntry("imsmanifest.xml");
653
+ if (!entry) {
654
+ return [];
655
+ }
656
+ try {
657
+ const xmlContent = entry.getData().toString("utf-8");
658
+ const parsed = this.parser.parse(xmlContent);
659
+ this.addFilesToManifest(parsed, paths);
660
+ const xmlDeclaration = this.extractXmlDeclaration(xmlContent);
661
+ const updatedXml = xmlDeclaration + this.builder.build(parsed);
662
+ zip.updateFile("imsmanifest.xml", Buffer.from(updatedXml, "utf-8"));
663
+ return ["imsmanifest.xml"];
664
+ } catch (error) {
665
+ console.error("[Patch-Adams] Failed to update imsmanifest.xml:", error);
666
+ return [];
667
+ }
668
+ }
669
+ /**
670
+ * Extract XML declaration from original content
671
+ */
672
+ extractXmlDeclaration(xml) {
673
+ const match = xml.match(/<\?xml[^?]*\?>/);
674
+ return match ? match[0] + "\n" : '<?xml version="1.0" encoding="UTF-8"?>\n';
675
+ }
676
+ /**
677
+ * Add file entries to the manifest
678
+ */
679
+ addFilesToManifest(parsed, paths) {
680
+ const manifest = parsed.manifest;
681
+ if (!manifest) return;
682
+ const resources = manifest.resources;
683
+ if (!resources) return;
684
+ let resource = resources.resource;
685
+ if (!resource) return;
686
+ if (Array.isArray(resource)) {
687
+ resource = resource[0];
688
+ }
689
+ if (!resource.file) {
690
+ resource.file = [];
691
+ }
692
+ let files = resource.file;
693
+ if (!Array.isArray(files)) {
694
+ files = [files];
695
+ resource.file = files;
696
+ }
697
+ const filesToAdd = [
698
+ paths.cssBefore,
699
+ paths.cssAfter,
700
+ paths.jsBefore,
701
+ paths.jsAfter
702
+ ].filter(Boolean);
703
+ for (const filePath of filesToAdd) {
704
+ const exists = files.some((f) => f["@_href"] === filePath);
705
+ if (!exists) {
706
+ files.push({ "@_href": filePath });
707
+ }
708
+ }
709
+ }
710
+ /**
711
+ * Get the file paths that will be added to the package
712
+ */
713
+ getFilePaths() {
714
+ const paths = {};
715
+ if (this.config.cssBefore.enabled) {
716
+ paths.cssBefore = `scormcontent/${this.config.localFolders.css}/${this.config.cssBefore.filename}`;
717
+ }
718
+ if (this.config.cssAfter.enabled) {
719
+ paths.cssAfter = `scormcontent/${this.config.localFolders.css}/${this.config.cssAfter.filename}`;
720
+ }
721
+ if (this.config.jsBefore.enabled) {
722
+ paths.jsBefore = `scormcontent/${this.config.localFolders.js}/${this.config.jsBefore.filename}`;
723
+ }
724
+ if (this.config.jsAfter.enabled) {
725
+ paths.jsAfter = `scormcontent/${this.config.localFolders.js}/${this.config.jsAfter.filename}`;
726
+ }
727
+ return paths;
728
+ }
729
+ };
730
+
731
+ // src/detectors/format-detector.ts
732
+ var FormatDetector = class {
733
+ /**
734
+ * Detect the package format from a ZIP file
735
+ */
736
+ detect(zip) {
737
+ if (zip.getEntry("cmi5.xml")) {
738
+ return "cmi5";
739
+ }
740
+ if (zip.getEntry("tincan.xml")) {
741
+ return "xapi";
742
+ }
743
+ const manifest = zip.getEntry("imsmanifest.xml");
744
+ if (manifest) {
745
+ const content = manifest.getData().toString("utf-8");
746
+ return this.detectScormVersion(content);
747
+ }
748
+ if (this.hasAiccFiles(zip)) {
749
+ return "aicc";
750
+ }
751
+ return "unknown";
752
+ }
753
+ /**
754
+ * Detect SCORM version from manifest content
755
+ */
756
+ detectScormVersion(content) {
757
+ if (content.includes("4th Edition") || content.includes("CAM 1.4")) {
758
+ return "scorm2004-4";
759
+ }
760
+ if (content.includes("3rd Edition") || content.includes("2004 3rd") || content.includes("adlcp_v1p3")) {
761
+ return "scorm2004-3";
762
+ }
763
+ if (content.includes("2004") && content.includes("adlseq")) {
764
+ return "scorm2004-3";
765
+ }
766
+ if (content.includes("1.2") || content.includes("adlcp_rootv1p2") || content.includes("imscp_rootv1p1p2")) {
767
+ return "scorm12";
768
+ }
769
+ if (content.toLowerCase().includes("aicc")) {
770
+ return "aicc";
771
+ }
772
+ return "scorm12";
773
+ }
774
+ /**
775
+ * Check for AICC-specific files
776
+ */
777
+ hasAiccFiles(zip) {
778
+ const aiccExtensions = [".crs", ".au", ".des", ".cst"];
779
+ const entries = zip.getEntries();
780
+ for (const entry of entries) {
781
+ const name = entry.entryName.toLowerCase();
782
+ if (aiccExtensions.some((ext) => name.endsWith(ext))) {
783
+ return true;
784
+ }
785
+ }
786
+ return false;
787
+ }
788
+ /**
789
+ * Get a human-readable format name
790
+ */
791
+ getFormatDisplayName(format) {
792
+ const names = {
793
+ scorm12: "SCORM 1.2",
794
+ "scorm2004-3": "SCORM 2004 3rd Edition",
795
+ "scorm2004-4": "SCORM 2004 4th Edition",
796
+ cmi5: "cmi5",
797
+ xapi: "xAPI (Tin Can)",
798
+ aicc: "AICC",
799
+ unknown: "Unknown"
800
+ };
801
+ return names[format];
802
+ }
803
+ };
804
+
805
+ // src/patcher/index.ts
806
+ var DEFAULT_CSS_BEFORE = `/* Patch-Adams: CSS Before (blocking)
807
+ * This file loads at the start of <head> and blocks rendering.
808
+ * Use it to hide content and prevent flash of unstyled content.
809
+ *
810
+ * Example:
811
+ * html.pa-patched.pa-loading body {
812
+ * visibility: hidden !important;
813
+ * }
814
+ */
815
+ `;
816
+ var DEFAULT_CSS_AFTER = `/* Patch-Adams: CSS After (async)
817
+ * This file loads at the end of <head> with remote fallback.
818
+ * Use it for style overrides that take precedence over Rise styles.
819
+ *
820
+ * Example:
821
+ * html.pa-patched .blocks-text {
822
+ * font-family: 'Your Font', sans-serif !important;
823
+ * }
824
+ */
825
+ `;
826
+ var DEFAULT_JS_BEFORE = `// Patch-Adams: JS Before (blocking)
827
+ // This file loads at the start of <head> and blocks rendering.
828
+ // Use it for setup, API interception, and preparing globals.
829
+ //
830
+ // Example:
831
+ // window.PatchAdams.customConfig = {
832
+ // theme: 'dark',
833
+ // analytics: true
834
+ // };
835
+
836
+ console.log('[Patch-Adams] JS Before loaded');
837
+ `;
838
+ var DEFAULT_JS_AFTER = `// Patch-Adams: JS After (async)
839
+ // This file loads at the end of <body> with remote fallback.
840
+ // Use it for DOM manipulation after Rise has initialized.
841
+ //
842
+ // IMPORTANT: This script should NOT remove the loading class.
843
+ // The loader script handles that automatically after this script loads.
844
+ //
845
+ // Example:
846
+ // document.querySelectorAll('.blocks-text').forEach(function(el) {
847
+ // // Your modifications here
848
+ // });
849
+
850
+ console.log('[Patch-Adams] JS After loaded');
851
+ `;
852
+ var Patcher = class {
853
+ config;
854
+ htmlInjector;
855
+ manifestUpdater;
856
+ formatDetector;
857
+ constructor(config) {
858
+ this.config = config;
859
+ this.htmlInjector = new HtmlInjector(config);
860
+ this.manifestUpdater = new ManifestUpdater(config);
861
+ this.formatDetector = new FormatDetector();
862
+ }
863
+ /**
864
+ * Patch a Rise course ZIP file
865
+ * @param inputBuffer - Input ZIP file as Buffer
866
+ * @param options - Additional patching options
867
+ * @returns Patched ZIP file as Buffer and result details
868
+ */
869
+ async patch(inputBuffer, options = {}) {
870
+ const result = {
871
+ success: false,
872
+ format: "unknown",
873
+ formatDisplayName: "Unknown",
874
+ filesModified: [],
875
+ filesAdded: [],
876
+ errors: [],
877
+ warnings: []
878
+ };
879
+ try {
880
+ const zip = new AdmZip__default.default(inputBuffer);
881
+ result.format = this.formatDetector.detect(zip);
882
+ result.formatDisplayName = this.formatDetector.getFormatDisplayName(result.format);
883
+ if (result.format === "unknown") {
884
+ result.warnings.push("Could not determine package format, proceeding anyway");
885
+ }
886
+ const indexHtmlEntry = zip.getEntry("scormcontent/index.html");
887
+ if (!indexHtmlEntry) {
888
+ throw new Error("Could not find scormcontent/index.html in package. Is this a valid Rise export?");
889
+ }
890
+ const originalHtml = indexHtmlEntry.getData().toString("utf-8");
891
+ const patchedHtml = this.htmlInjector.inject(originalHtml);
892
+ zip.updateFile("scormcontent/index.html", Buffer.from(patchedHtml, "utf-8"));
893
+ result.filesModified.push("scormcontent/index.html");
894
+ const addedFiles = this.addFallbackFiles(zip, options);
895
+ result.filesAdded.push(...addedFiles);
896
+ if (this.config.updateManifests) {
897
+ const manifestPaths = this.manifestUpdater.getFilePaths();
898
+ const modifiedManifests = this.manifestUpdater.update(zip, result.format, manifestPaths);
899
+ result.filesModified.push(...modifiedManifests);
900
+ }
901
+ const outputBuffer = await this.writeZipWithYazl(zip);
902
+ result.success = true;
903
+ return { buffer: outputBuffer, result };
904
+ } catch (error) {
905
+ const message = error instanceof Error ? error.message : String(error);
906
+ result.errors.push(message);
907
+ throw error;
908
+ }
909
+ }
910
+ /**
911
+ * Write ZIP using yazl for better compatibility
912
+ * adm-zip's toBuffer() sets the data descriptor bit flag which causes
913
+ * issues with some ZIP parsers. yazl produces standard ZIP output.
914
+ */
915
+ writeZipWithYazl(admZip) {
916
+ return new Promise((resolve2, reject) => {
917
+ const zipFile = new yazl__default.default.ZipFile();
918
+ const entries = admZip.getEntries();
919
+ for (const entry of entries) {
920
+ const entryName = entry.entryName;
921
+ const mtime = entry.header.time ? new Date(entry.header.time) : /* @__PURE__ */ new Date();
922
+ if (entry.isDirectory) {
923
+ zipFile.addEmptyDirectory(entryName, { mtime });
924
+ continue;
925
+ }
926
+ const entryData = entry.getData();
927
+ zipFile.addBuffer(entryData, entryName, {
928
+ compress: true,
929
+ mtime
930
+ });
931
+ }
932
+ const chunks = [];
933
+ zipFile.outputStream.on("data", (chunk) => chunks.push(chunk));
934
+ zipFile.outputStream.on("end", () => resolve2(Buffer.concat(chunks)));
935
+ zipFile.outputStream.on("error", reject);
936
+ zipFile.end();
937
+ });
938
+ }
939
+ /**
940
+ * Add local fallback files to the ZIP
941
+ */
942
+ addFallbackFiles(zip, options) {
943
+ const added = [];
944
+ const cssFolder = `scormcontent/${this.config.localFolders.css}`;
945
+ const jsFolder = `scormcontent/${this.config.localFolders.js}`;
946
+ if (this.config.cssBefore.enabled) {
947
+ const path = `${cssFolder}/${this.config.cssBefore.filename}`;
948
+ const content = options.cssBeforeContent ?? DEFAULT_CSS_BEFORE;
949
+ zip.addFile(path, Buffer.from(content, "utf-8"));
950
+ added.push(path);
951
+ }
952
+ if (this.config.cssAfter.enabled) {
953
+ const path = `${cssFolder}/${this.config.cssAfter.filename}`;
954
+ const content = options.cssAfterContent ?? DEFAULT_CSS_AFTER;
955
+ zip.addFile(path, Buffer.from(content, "utf-8"));
956
+ added.push(path);
957
+ }
958
+ if (this.config.jsBefore.enabled) {
959
+ const path = `${jsFolder}/${this.config.jsBefore.filename}`;
960
+ const content = options.jsBeforeContent ?? DEFAULT_JS_BEFORE;
961
+ zip.addFile(path, Buffer.from(content, "utf-8"));
962
+ added.push(path);
963
+ }
964
+ if (this.config.jsAfter.enabled) {
965
+ const path = `${jsFolder}/${this.config.jsAfter.filename}`;
966
+ const content = options.jsAfterContent ?? DEFAULT_JS_AFTER;
967
+ zip.addFile(path, Buffer.from(content, "utf-8"));
968
+ added.push(path);
969
+ }
970
+ return added;
971
+ }
972
+ /**
973
+ * Get the current configuration
974
+ */
975
+ getConfig() {
976
+ return this.config;
977
+ }
978
+ /**
979
+ * Convenience method to patch a buffer and return only the patched buffer.
980
+ * Used by package-uploader integration.
981
+ * @param buffer - Input ZIP file as Buffer
982
+ * @returns Patched ZIP file as Buffer
983
+ */
984
+ async patchBuffer(buffer) {
985
+ console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
986
+ const { buffer: patchedBuffer, result } = await this.patch(buffer);
987
+ console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
988
+ console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
989
+ return patchedBuffer;
990
+ }
991
+ };
992
+ var Logger = class {
993
+ level;
994
+ constructor(level = "info") {
995
+ this.level = level;
996
+ }
997
+ shouldLog(level) {
998
+ const levels = ["debug", "info", "warn", "error", "silent"];
999
+ return levels.indexOf(level) >= levels.indexOf(this.level);
1000
+ }
1001
+ debug(...args) {
1002
+ if (this.shouldLog("debug")) {
1003
+ console.log(chalk__default.default.gray("[DEBUG]"), ...args);
1004
+ }
1005
+ }
1006
+ info(...args) {
1007
+ if (this.shouldLog("info")) {
1008
+ console.log(chalk__default.default.blue("[INFO]"), ...args);
1009
+ }
1010
+ }
1011
+ success(...args) {
1012
+ if (this.shouldLog("info")) {
1013
+ console.log(chalk__default.default.green("[SUCCESS]"), ...args);
1014
+ }
1015
+ }
1016
+ warn(...args) {
1017
+ if (this.shouldLog("warn")) {
1018
+ console.log(chalk__default.default.yellow("[WARN]"), ...args);
1019
+ }
1020
+ }
1021
+ error(...args) {
1022
+ if (this.shouldLog("error")) {
1023
+ console.error(chalk__default.default.red("[ERROR]"), ...args);
1024
+ }
1025
+ }
1026
+ setLevel(level) {
1027
+ this.level = level;
1028
+ }
1029
+ };
1030
+
1031
+ exports.AsyncAssetConfigSchema = AsyncAssetConfigSchema;
1032
+ exports.BlockingAssetConfigSchema = BlockingAssetConfigSchema;
1033
+ exports.FormatDetector = FormatDetector;
1034
+ exports.HtmlInjector = HtmlInjector;
1035
+ exports.LocalFoldersConfigSchema = LocalFoldersConfigSchema;
1036
+ exports.Logger = Logger;
1037
+ exports.ManifestUpdater = ManifestUpdater;
1038
+ exports.PatchAdamsConfigSchema = PatchAdamsConfigSchema;
1039
+ exports.Patcher = Patcher;
1040
+ exports.defaultConfig = defaultConfig;
1041
+ exports.generateConfigTemplate = generateConfigTemplate;
1042
+ exports.generateCssAfterLoader = generateCssAfterLoader;
1043
+ exports.generateCssBeforeLoader = generateCssBeforeLoader;
1044
+ exports.generateJsAfterLoader = generateJsAfterLoader;
1045
+ exports.generateJsBeforeLoader = generateJsBeforeLoader;
1046
+ exports.loadConfig = loadConfig;
1047
+ exports.mergeWithDefaults = mergeWithDefaults;
1048
+ //# sourceMappingURL=index.cjs.map
1049
+ //# sourceMappingURL=index.cjs.map