@patch-adams/core 1.4.37 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -115,7 +115,9 @@ var PatchAdamsConfigSchema = zod.z.object({
115
115
  /** LRS Bridge configuration for xAPI/LRS communication with Bravais */
116
116
  lrsBridge: LrsBridgeConfigSchema.default({}),
117
117
  /** Plugin configurations - each plugin is keyed by its name */
118
- plugins: PluginsConfigSchema
118
+ plugins: PluginsConfigSchema,
119
+ /** Optional skin name — adds 'pa-skinned' + skin class to <html>, loads skin CSS/JS after core files */
120
+ skin: zod.z.string().min(1).optional()
119
121
  });
120
122
 
121
123
  // src/config/defaults.ts
@@ -4386,7 +4388,7 @@ function escapeJs(str) {
4386
4388
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4387
4389
  }
4388
4390
  function generateJsBeforeLoader(options) {
4389
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4391
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4390
4392
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4391
4393
  const courseLines = [];
4392
4394
  if (metadata) {
@@ -4429,12 +4431,13 @@ ${courseLines.join("\n")}
4429
4431
  window.pa_patcher = window.pa_patcher || {
4430
4432
  version: '1.0.25',
4431
4433
  htmlClass: '${htmlClass}',
4432
- loadingClass: '${loadingClass}',${courseBlock}
4434
+ loadingClass: '${loadingClass}',${skin ? `
4435
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4433
4436
  loaded: {
4434
4437
  cssBefore: false,
4435
4438
  cssAfter: false,
4436
4439
  jsBefore: false,
4437
- jsAfter: false
4440
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4438
4441
  }
4439
4442
  };
4440
4443
 
@@ -4555,7 +4558,8 @@ function buildJsBeforeOptions(config, metadata) {
4555
4558
  htmlClass: config.htmlClass,
4556
4559
  loadingClass: config.loadingClass,
4557
4560
  metadata: metadata ?? null,
4558
- lrsBridge
4561
+ lrsBridge,
4562
+ skin: config.skin
4559
4563
  };
4560
4564
  }
4561
4565
 
@@ -4678,6 +4682,196 @@ function buildJsAfterOptions(config) {
4678
4682
  };
4679
4683
  }
4680
4684
 
4685
+ // src/templates/skin-css.ts
4686
+ function generateSkinCssLoader(options) {
4687
+ const { remoteUrl, localPath, timeout } = options;
4688
+ return `<!-- === PATCH-ADAMS: SKIN CSS (async with fallback) === -->
4689
+ <script data-pa="skin-css-loader">
4690
+ (function() {
4691
+ 'use strict';
4692
+ var REMOTE_URL = "${remoteUrl}";
4693
+ var LOCAL_PATH = "${localPath}";
4694
+ var TIMEOUT = ${timeout};
4695
+
4696
+ function loadCSS(url, onSuccess, onError) {
4697
+ var link = document.createElement('link');
4698
+ link.rel = 'stylesheet';
4699
+ link.href = url;
4700
+ link.setAttribute('data-pa', 'skin-css');
4701
+
4702
+ link.onload = function() {
4703
+ if (onSuccess) onSuccess();
4704
+ };
4705
+
4706
+ link.onerror = function() {
4707
+ if (onError) onError();
4708
+ };
4709
+
4710
+ document.head.appendChild(link);
4711
+ return link;
4712
+ }
4713
+
4714
+ function loadCSSWithFallback() {
4715
+ var loaded = false;
4716
+ var timeoutId;
4717
+
4718
+ // Try remote first
4719
+ var remoteLink = loadCSS(
4720
+ REMOTE_URL,
4721
+ function() {
4722
+ if (loaded) return;
4723
+ loaded = true;
4724
+ clearTimeout(timeoutId);
4725
+ console.log('[PA-Patcher] Skin CSS loaded from remote:', REMOTE_URL);
4726
+ },
4727
+ function() {
4728
+ if (loaded) return;
4729
+ loaded = true;
4730
+ clearTimeout(timeoutId);
4731
+ loadLocalFallback();
4732
+ }
4733
+ );
4734
+
4735
+ // Timeout fallback
4736
+ timeoutId = setTimeout(function() {
4737
+ if (loaded) return;
4738
+ loaded = true;
4739
+ console.warn('[PA-Patcher] Skin CSS timed out, using local fallback');
4740
+ if (remoteLink.parentNode) {
4741
+ document.head.removeChild(remoteLink);
4742
+ }
4743
+ loadLocalFallback();
4744
+ }, TIMEOUT);
4745
+ }
4746
+
4747
+ function loadLocalFallback() {
4748
+ loadCSS(
4749
+ LOCAL_PATH,
4750
+ function() {
4751
+ console.log('[PA-Patcher] Skin CSS loaded from local fallback:', LOCAL_PATH);
4752
+ },
4753
+ function() {
4754
+ console.error('[PA-Patcher] Skin CSS failed to load from both remote and local');
4755
+ }
4756
+ );
4757
+ }
4758
+
4759
+ // Execute immediately
4760
+ loadCSSWithFallback();
4761
+ })();
4762
+ </script>`;
4763
+ }
4764
+ function buildSkinCssOptions(config) {
4765
+ if (!config.skin) return null;
4766
+ return {
4767
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.css`,
4768
+ localPath: `skin/${config.skin}.css`,
4769
+ timeout: config.cssAfter.timeout
4770
+ // reuse cssAfter timeout
4771
+ };
4772
+ }
4773
+
4774
+ // src/templates/skin-js.ts
4775
+ function generateSkinJsLoader(options) {
4776
+ const { remoteUrl, localPath, timeout } = options;
4777
+ return `<!-- === PATCH-ADAMS: SKIN JS (async with fallback) === -->
4778
+ <script data-pa="skin-js-loader">
4779
+ (function() {
4780
+ 'use strict';
4781
+ var REMOTE_URL = "${remoteUrl}";
4782
+ var LOCAL_PATH = "${localPath}";
4783
+ var TIMEOUT = ${timeout};
4784
+
4785
+ function loadJS(url, onSuccess, onError) {
4786
+ var script = document.createElement('script');
4787
+ script.src = url;
4788
+ script.async = true;
4789
+ script.setAttribute('data-pa', 'skin-js');
4790
+
4791
+ script.onload = function() {
4792
+ if (onSuccess) onSuccess();
4793
+ };
4794
+
4795
+ script.onerror = function() {
4796
+ if (onError) onError();
4797
+ };
4798
+
4799
+ document.body.appendChild(script);
4800
+ return script;
4801
+ }
4802
+
4803
+ function loadJSWithFallback() {
4804
+ var loaded = false;
4805
+ var timeoutId;
4806
+
4807
+ // Try remote first
4808
+ var remoteScript = loadJS(
4809
+ REMOTE_URL,
4810
+ function() {
4811
+ if (loaded) return;
4812
+ loaded = true;
4813
+ clearTimeout(timeoutId);
4814
+ console.log('[PA-Patcher] Skin JS loaded from remote:', REMOTE_URL);
4815
+ if (window.pa_patcher && window.pa_patcher.loaded) {
4816
+ window.pa_patcher.loaded.skinJs = true;
4817
+ }
4818
+ },
4819
+ function() {
4820
+ if (loaded) return;
4821
+ loaded = true;
4822
+ clearTimeout(timeoutId);
4823
+ loadLocalFallback();
4824
+ }
4825
+ );
4826
+
4827
+ // Timeout fallback
4828
+ timeoutId = setTimeout(function() {
4829
+ if (loaded) return;
4830
+ loaded = true;
4831
+ console.warn('[PA-Patcher] Skin JS timed out, using local fallback');
4832
+ if (remoteScript.parentNode) {
4833
+ document.body.removeChild(remoteScript);
4834
+ }
4835
+ loadLocalFallback();
4836
+ }, TIMEOUT);
4837
+ }
4838
+
4839
+ function loadLocalFallback() {
4840
+ loadJS(
4841
+ LOCAL_PATH,
4842
+ function() {
4843
+ console.log('[PA-Patcher] Skin JS loaded from local fallback:', LOCAL_PATH);
4844
+ if (window.pa_patcher && window.pa_patcher.loaded) {
4845
+ window.pa_patcher.loaded.skinJs = true;
4846
+ }
4847
+ },
4848
+ function() {
4849
+ console.error('[PA-Patcher] Skin JS failed to load from both remote and local');
4850
+ }
4851
+ );
4852
+ }
4853
+
4854
+ // Execute after DOM is ready
4855
+ if (document.readyState === 'loading') {
4856
+ document.addEventListener('DOMContentLoaded', function() {
4857
+ setTimeout(loadJSWithFallback, 150);
4858
+ });
4859
+ } else {
4860
+ setTimeout(loadJSWithFallback, 150);
4861
+ }
4862
+ })();
4863
+ </script>`;
4864
+ }
4865
+ function buildSkinJsOptions(config) {
4866
+ if (!config.skin) return null;
4867
+ return {
4868
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.js`,
4869
+ localPath: `skin/${config.skin}.js`,
4870
+ timeout: config.jsAfter.timeout
4871
+ // reuse jsAfter timeout
4872
+ };
4873
+ }
4874
+
4681
4875
  // src/patcher/html-injector.ts
4682
4876
  var HtmlInjector = class {
4683
4877
  config;
@@ -4741,6 +4935,10 @@ var HtmlInjector = class {
4741
4935
  if (this.config.jsAfter.enabled) {
4742
4936
  result = this.injectJsAfter(result);
4743
4937
  }
4938
+ if (this.config.skin) {
4939
+ result = this.injectSkinCss(result);
4940
+ result = this.injectSkinJs(result);
4941
+ }
4744
4942
  if (this.pluginAssets) {
4745
4943
  result = this.injectPluginAssets(result);
4746
4944
  }
@@ -4787,8 +4985,8 @@ ${pluginJs}
4787
4985
  * Also adds data attributes for course metadata
4788
4986
  */
4789
4987
  addHtmlAttributes(html) {
4790
- const { htmlClass, loadingClass } = this.config;
4791
- const classes = `${htmlClass} ${loadingClass}`;
4988
+ const { htmlClass, loadingClass, skin } = this.config;
4989
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
4792
4990
  const dataAttrs = [];
4793
4991
  if (this.metadata) {
4794
4992
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -4797,6 +4995,9 @@ ${pluginJs}
4797
4995
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
4798
4996
  }
4799
4997
  }
4998
+ if (skin) {
4999
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5000
+ }
4800
5001
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
4801
5002
  const htmlTagPattern = /<html([^>]*)>/i;
4802
5003
  const match = html.match(htmlTagPattern);
@@ -4875,6 +5076,26 @@ ${loader}`);
4875
5076
  ${loader}`);
4876
5077
  }
4877
5078
  return html.replace(/<\/body>/i, `${loader}
5079
+ </body>`);
5080
+ }
5081
+ /**
5082
+ * Inject Skin CSS loader after CSS After (before </head>)
5083
+ */
5084
+ injectSkinCss(html) {
5085
+ const options = buildSkinCssOptions(this.config);
5086
+ if (!options) return html;
5087
+ const loader = generateSkinCssLoader(options);
5088
+ return html.replace(/<\/head>/i, `${loader}
5089
+ </head>`);
5090
+ }
5091
+ /**
5092
+ * Inject Skin JS loader after JS After (before </body>)
5093
+ */
5094
+ injectSkinJs(html) {
5095
+ const options = buildSkinJsOptions(this.config);
5096
+ if (!options) return html;
5097
+ const loader = generateSkinJsLoader(options);
5098
+ return html.replace(/<\/body>/i, `${loader}
4878
5099
  </body>`);
4879
5100
  }
4880
5101
  };
@@ -4918,6 +5139,10 @@ var StorylineHtmlInjector = class {
4918
5139
  if (this.config.jsAfter.enabled) {
4919
5140
  result = this.injectJsAfter(result);
4920
5141
  }
5142
+ if (this.config.skin) {
5143
+ result = this.injectSkinCss(result);
5144
+ result = this.injectSkinJs(result);
5145
+ }
4921
5146
  if (this.pluginAssets) {
4922
5147
  result = this.injectPluginAssets(result);
4923
5148
  }
@@ -4962,8 +5187,8 @@ ${pluginJs}
4962
5187
  * Also adds data attributes for course metadata
4963
5188
  */
4964
5189
  addHtmlAttributes(html) {
4965
- const { htmlClass, loadingClass } = this.config;
4966
- const classes = `${htmlClass} ${loadingClass}`;
5190
+ const { htmlClass, loadingClass, skin } = this.config;
5191
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
4967
5192
  const dataAttrs = [];
4968
5193
  if (this.metadata) {
4969
5194
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -4973,6 +5198,9 @@ ${pluginJs}
4973
5198
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
4974
5199
  }
4975
5200
  }
5201
+ if (skin) {
5202
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5203
+ }
4976
5204
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
4977
5205
  const htmlTagPattern = /<html([^>]*)>/i;
4978
5206
  const match = html.match(htmlTagPattern);
@@ -5050,6 +5278,26 @@ ${loader}`);
5050
5278
  const options = buildJsAfterOptions(this.config);
5051
5279
  const loader = generateJsAfterLoader(options);
5052
5280
  return html.replace(/<\/body>/i, `${loader}
5281
+ </body>`);
5282
+ }
5283
+ /**
5284
+ * Inject Skin CSS loader after CSS After (before </head>)
5285
+ */
5286
+ injectSkinCss(html) {
5287
+ const options = buildSkinCssOptions(this.config);
5288
+ if (!options) return html;
5289
+ const loader = generateSkinCssLoader(options);
5290
+ return html.replace(/<\/head>/i, `${loader}
5291
+ </head>`);
5292
+ }
5293
+ /**
5294
+ * Inject Skin JS loader after JS After (before </body>)
5295
+ */
5296
+ injectSkinJs(html) {
5297
+ const options = buildSkinJsOptions(this.config);
5298
+ if (!options) return html;
5299
+ const loader = generateSkinJsLoader(options);
5300
+ return html.replace(/<\/body>/i, `${loader}
5053
5301
  </body>`);
5054
5302
  }
5055
5303
  };
@@ -5100,7 +5348,9 @@ var ManifestUpdater = class {
5100
5348
  paths.cssBefore,
5101
5349
  paths.cssAfter,
5102
5350
  paths.jsBefore,
5103
- paths.jsAfter
5351
+ paths.jsAfter,
5352
+ paths.skinCss,
5353
+ paths.skinJs
5104
5354
  ].filter(Boolean);
5105
5355
  if (filesToAdd.length === 0) {
5106
5356
  return [];
@@ -5890,6 +6140,20 @@ var Patcher = class {
5890
6140
  })
5891
6141
  );
5892
6142
  }
6143
+ if (this.config.skin) {
6144
+ const skinCssUrl = `${remoteDomain}/skin/${this.config.skin}.css`;
6145
+ fetchPromises.push(
6146
+ this.fetchFile(skinCssUrl).then((content) => {
6147
+ fetched.skinCss = content;
6148
+ })
6149
+ );
6150
+ const skinJsUrl = `${remoteDomain}/skin/${this.config.skin}.js`;
6151
+ fetchPromises.push(
6152
+ this.fetchFile(skinJsUrl).then((content) => {
6153
+ fetched.skinJs = content;
6154
+ })
6155
+ );
6156
+ }
5893
6157
  await Promise.all(fetchPromises);
5894
6158
  return fetched;
5895
6159
  }
@@ -5973,6 +6237,16 @@ var Patcher = class {
5973
6237
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5974
6238
  }
5975
6239
  }
6240
+ const effectiveSkin = options.skin || this.config.skin;
6241
+ if (options.skin && options.skin !== this.config.skin) {
6242
+ this.config.skin = options.skin;
6243
+ this.riseHtmlInjector = new HtmlInjector(this.config);
6244
+ this.storylineHtmlInjector = new StorylineHtmlInjector(this.config);
6245
+ console.log(`[Patcher] Using per-call skin override: ${options.skin}`);
6246
+ }
6247
+ if (effectiveSkin) {
6248
+ console.log(`[Patcher] Skin: ${effectiveSkin}`);
6249
+ }
5976
6250
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
5977
6251
  htmlInjector.setMetadata(metadata);
5978
6252
  let fetchedFallbacks = {};
@@ -6101,7 +6375,11 @@ var Patcher = class {
6101
6375
  buildManifestPaths(addedFiles) {
6102
6376
  const paths = {};
6103
6377
  for (const filePath of addedFiles) {
6104
- if (filePath.endsWith(this.config.cssBefore.filename)) {
6378
+ if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.css`)) {
6379
+ paths.skinCss = filePath;
6380
+ } else if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.js`)) {
6381
+ paths.skinJs = filePath;
6382
+ } else if (filePath.endsWith(this.config.cssBefore.filename)) {
6105
6383
  paths.cssBefore = filePath;
6106
6384
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6107
6385
  paths.cssAfter = filePath;
@@ -6185,6 +6463,22 @@ var Patcher = class {
6185
6463
  added.push(path);
6186
6464
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6187
6465
  }
6466
+ if (this.config.skin) {
6467
+ const skinCssPath = `${basePath}skin/${this.config.skin}.css`;
6468
+ const skinCssContent = fetched.skinCss ?? `/* PA-Patcher: Skin CSS (${this.config.skin}) */
6469
+ `;
6470
+ zip.addFile(skinCssPath, Buffer.from(skinCssContent, "utf-8"));
6471
+ added.push(skinCssPath);
6472
+ if (fetched.skinCss) console.log(`[Patcher] Using fetched skin CSS for ${skinCssPath}`);
6473
+ else console.log(`[Patcher] Added placeholder skin CSS: ${skinCssPath}`);
6474
+ const skinJsPath = `${basePath}skin/${this.config.skin}.js`;
6475
+ const skinJsContent = fetched.skinJs ?? `// PA-Patcher: Skin JS (${this.config.skin})
6476
+ `;
6477
+ zip.addFile(skinJsPath, Buffer.from(skinJsContent, "utf-8"));
6478
+ added.push(skinJsPath);
6479
+ if (fetched.skinJs) console.log(`[Patcher] Using fetched skin JS for ${skinJsPath}`);
6480
+ else console.log(`[Patcher] Added placeholder skin JS: ${skinJsPath}`);
6481
+ }
6188
6482
  return added;
6189
6483
  }
6190
6484
  /**
@@ -6199,9 +6493,9 @@ var Patcher = class {
6199
6493
  * @param buffer - Input ZIP file as Buffer
6200
6494
  * @returns Patched ZIP file as Buffer
6201
6495
  */
6202
- async patchBuffer(buffer) {
6496
+ async patchBuffer(buffer, options) {
6203
6497
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6204
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6498
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6205
6499
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6206
6500
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6207
6501
  return patchedBuffer;