@patch-adams/core 1.0.5 → 1.0.7

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