hotstaq 0.9.21 → 0.9.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hotstaq",
3
- "version": "0.9.21",
3
+ "version": "0.9.22",
4
4
  "description": "A friendly web framework that fits nicely into devops and CI/CD pipelines.",
5
5
  "bin": {
6
6
  "hotstaq": "./bin/hotstaq"
package/src/HotStaq.ts CHANGED
@@ -238,7 +238,7 @@ export class HotStaq implements IHotStaq
238
238
  /**
239
239
  * The current version of HotStaq.
240
240
  */
241
- static version: string = "0.9.21";
241
+ static version: string = "0.9.22";
242
242
  /**
243
243
  * Indicates if this is a web build.
244
244
  */
@@ -297,9 +297,23 @@ export class HotStaq implements IHotStaq
297
297
  */
298
298
  static onBeforeNavigate: (path: string) => boolean | Promise<boolean> = null;
299
299
  /**
300
- * Event fired after SPA navigation completes.
300
+ * Event fired after an SPA navigation has rendered the new content,
301
+ * but before HotStaq executes that content's inline <script> tags.
302
+ *
303
+ * Return `false` to take ownership of running those scripts yourself —
304
+ * HotStaq will then skip its own execution pass. Returning `true`,
305
+ * `undefined`, or having no handler at all lets HotStaq execute them
306
+ * (the default), which is what makes inline page scripts run on SPA
307
+ * navigation. See `onAfterUseOutput` for the full-render equivalent.
308
+ */
309
+ static onAfterNavigate: (path: string) => void | boolean | Promise<void | boolean> = null;
310
+ /**
311
+ * Event fired after `useOutput` has rendered a full document, but before
312
+ * HotStaq executes that document's inline <script> tags. Same return-value
313
+ * gate as `onAfterNavigate`: return `false` to run the scripts yourself;
314
+ * otherwise HotStaq executes them.
301
315
  */
302
- static onAfterNavigate: (path: string) => void | Promise<void> = null;
316
+ static onAfterUseOutput: (output: string) => void | boolean | Promise<void | boolean> = null;
303
317
  /**
304
318
  * Override the framework's default 404 rendering.
305
319
  *
@@ -2848,72 +2862,98 @@ export class HotStaq implements IHotStaq
2848
2862
  }
2849
2863
 
2850
2864
  /**
2851
- * Replace the current HTML page with the output.
2852
- * This is meant for web browser use only.
2865
+ * Execute the <script> elements found within `root`.
2866
+ *
2867
+ * A <script> inserted into the DOM via `innerHTML` is inert — the HTML
2868
+ * spec only runs a script when it is *inserted into the document with
2869
+ * its content already present*. So after we swap new markup in (full
2870
+ * render or SPA partial swap), its inline scripts never ran. For each
2871
+ * not-yet-executed <script>, build a fresh element, copy `type`/`src`
2872
+ * and — for inline scripts — set `.text` BEFORE insertion, then swap it
2873
+ * in so the browser runs it.
2874
+ *
2875
+ * Each executed script is tagged `data-hotstaq-executed` and skipped on
2876
+ * any later pass, so calling this twice (or alongside an app that runs
2877
+ * some scripts itself) never double-fires one.
2853
2878
  */
2854
- static async useOutput (output: string): Promise<void>
2879
+ protected static async executeInlineScripts (root: Document | HTMLElement): Promise<void>
2855
2880
  {
2856
- if (HotStaq.onOutputReceived != null)
2857
- HotStaq.onOutputReceived (output);
2881
+ let tmpScripts = root.getElementsByTagName ("script");
2858
2882
 
2859
- let parser = new DOMParser ();
2860
- let child = parser.parseFromString (output, "text/html");
2861
- let htmlObj: HTMLHtmlElement = document.getElementsByTagName('html')[0];
2883
+ if (tmpScripts.length < 1)
2884
+ return;
2862
2885
 
2863
- htmlObj.innerHTML = child.getElementsByTagName('html')[0].innerHTML;
2886
+ // Snapshot first — the loop mutates the DOM while iterating.
2887
+ let scripts: HTMLScriptElement[] = [];
2864
2888
 
2865
- // Thanks to newfurniturey at:
2866
- // https://stackoverflow.com/questions/22945884/domparser-appending-script-tags-to-head-body-but-not-executing
2867
- let tmpScripts = document.getElementsByTagName('script');
2868
- if (tmpScripts.length > 0) {
2869
- // push all of the document's script tags into an array
2870
- // (to prevent dom manipulation while iterating over dom nodes)
2871
- let scripts: HTMLScriptElement[] = [];
2872
- for (let i = 0; i < tmpScripts.length; i++) {
2873
- scripts.push(tmpScripts[i]);
2874
- }
2889
+ for (let i = 0; i < tmpScripts.length; i++)
2890
+ scripts.push (tmpScripts[i]);
2875
2891
 
2876
- // iterate over all script tags and create duplicate tags for each
2877
- for (let i = 0; i < scripts.length; i++) {
2878
- let s: HTMLScriptElement = document.createElement('script');
2892
+ for (let i = 0; i < scripts.length; i++)
2893
+ {
2894
+ let orig: HTMLScriptElement = scripts[i];
2879
2895
 
2880
- // add the new node to the page
2881
- scripts[i].parentNode.appendChild(s);
2896
+ if (orig.getAttribute ("data-hotstaq-executed") != null)
2897
+ continue;
2882
2898
 
2883
- // remove the original (non-executing) node from the page
2884
- scripts[i].parentNode.removeChild(scripts[i]);
2899
+ let src: string = orig.getAttribute ("src");
2900
+ let hasSrc: boolean = (src != null) && (src !== "");
2901
+ let content: string = orig.textContent || "";
2885
2902
 
2886
- await new Promise<void> ((resolve2, reject2) =>
2887
- {
2888
- s.onload = () =>
2889
- {
2890
- resolve2 ();
2891
- };
2903
+ // Nothing to run just mark it so we don't revisit it.
2904
+ if ((hasSrc === false) && (content === ""))
2905
+ {
2906
+ orig.setAttribute ("data-hotstaq-executed", "1");
2892
2907
 
2893
- let hasSrc: boolean = false;
2908
+ continue;
2909
+ }
2894
2910
 
2895
- if (scripts[i].getAttribute ("src") != null)
2896
- {
2897
- if (scripts[i].getAttribute ("src") !== "")
2898
- {
2899
- s.setAttribute ("src", scripts[i].getAttribute ("src"));
2900
- hasSrc = true;
2901
- }
2902
- }
2911
+ let s: HTMLScriptElement = document.createElement ("script");
2912
+ let type: string = orig.getAttribute ("type");
2903
2913
 
2904
- if (scripts[i].getAttribute ("type") != null)
2905
- {
2906
- if (scripts[i].getAttribute ("type") !== "")
2907
- s.setAttribute ("type", scripts[i].getAttribute ("type"));
2908
- }
2914
+ if ((type != null) && (type !== ""))
2915
+ s.setAttribute ("type", type);
2909
2916
 
2910
- s.innerHTML = scripts[i].innerHTML;
2917
+ if (hasSrc === true)
2918
+ s.setAttribute ("src", src);
2919
+ else
2920
+ // MUST be set before insertion, otherwise the script is inert.
2921
+ s.text = content;
2911
2922
 
2912
- if (hasSrc === false)
2913
- resolve2 ();
2914
- });
2915
- }
2923
+ s.setAttribute ("data-hotstaq-executed", "1");
2924
+
2925
+ await new Promise<void> ((resolve2, reject2) =>
2926
+ {
2927
+ s.onload = () => { resolve2 (); };
2928
+ s.onerror = () => { resolve2 (); };
2929
+
2930
+ if (orig.parentNode != null)
2931
+ orig.parentNode.replaceChild (s, orig);
2932
+ else
2933
+ document.head.appendChild (s);
2934
+
2935
+ // Inline scripts execute synchronously on insertion; only
2936
+ // src scripts need to wait for onload.
2937
+ if (hasSrc === false)
2938
+ resolve2 ();
2939
+ });
2916
2940
  }
2941
+ }
2942
+
2943
+ /**
2944
+ * Replace the current HTML page with the output.
2945
+ * This is meant for web browser use only.
2946
+ */
2947
+ static async useOutput (output: string): Promise<void>
2948
+ {
2949
+ if (HotStaq.onOutputReceived != null)
2950
+ HotStaq.onOutputReceived (output);
2951
+
2952
+ let parser = new DOMParser ();
2953
+ let child = parser.parseFromString (output, "text/html");
2954
+ let htmlObj: HTMLHtmlElement = document.getElementsByTagName('html')[0];
2955
+
2956
+ htmlObj.innerHTML = child.getElementsByTagName('html')[0].innerHTML;
2917
2957
 
2918
2958
  if (HotStaq.dispatchReadyEvents === true)
2919
2959
  {
@@ -2923,6 +2963,27 @@ export class HotStaq implements IHotStaq
2923
2963
 
2924
2964
  if (HotStaq.onReadyEvent != null)
2925
2965
  HotStaq.onReadyEvent (output);
2966
+
2967
+ // Run the rendered document's inline scripts last — and let the app
2968
+ // gate it. onAfterUseOutput returning false means the app will run
2969
+ // them itself, so HotStaq stands down. Otherwise HotStaq executes
2970
+ // them (the default), which is what makes inline scripts in a
2971
+ // useOutput-rendered page actually run.
2972
+ let runScripts: boolean = true;
2973
+
2974
+ if (HotStaq.onAfterUseOutput != null)
2975
+ {
2976
+ let result: any = HotStaq.onAfterUseOutput (output);
2977
+
2978
+ if (result instanceof Promise)
2979
+ result = await result;
2980
+
2981
+ if (result === false)
2982
+ runScripts = false;
2983
+ }
2984
+
2985
+ if (runScripts === true)
2986
+ await HotStaq.executeInlineScripts (document);
2926
2987
  }
2927
2988
 
2928
2989
  /**
@@ -3091,55 +3152,6 @@ export class HotStaq implements IHotStaq
3091
3152
  // Replace only the target content.
3092
3153
  targetEl.innerHTML = contentHTML;
3093
3154
 
3094
- // Execute any script tags that were in the new content.
3095
- let tmpScripts = targetEl.getElementsByTagName ('script');
3096
-
3097
- if (tmpScripts.length > 0)
3098
- {
3099
- let scripts: HTMLScriptElement[] = [];
3100
-
3101
- for (let i = 0; i < tmpScripts.length; i++)
3102
- scripts.push (tmpScripts[i]);
3103
-
3104
- for (let i = 0; i < scripts.length; i++)
3105
- {
3106
- let s: HTMLScriptElement = document.createElement ('script');
3107
-
3108
- scripts[i].parentNode.appendChild (s);
3109
- scripts[i].parentNode.removeChild (scripts[i]);
3110
-
3111
- await new Promise<void> ((resolve2, reject2) =>
3112
- {
3113
- s.onload = () =>
3114
- {
3115
- resolve2 ();
3116
- };
3117
-
3118
- let hasSrc: boolean = false;
3119
-
3120
- if (scripts[i].getAttribute ("src") != null)
3121
- {
3122
- if (scripts[i].getAttribute ("src") !== "")
3123
- {
3124
- s.setAttribute ("src", scripts[i].getAttribute ("src"));
3125
- hasSrc = true;
3126
- }
3127
- }
3128
-
3129
- if (scripts[i].getAttribute ("type") != null)
3130
- {
3131
- if (scripts[i].getAttribute ("type") !== "")
3132
- s.setAttribute ("type", scripts[i].getAttribute ("type"));
3133
- }
3134
-
3135
- s.innerHTML = scripts[i].innerHTML;
3136
-
3137
- if (hasSrc === false)
3138
- resolve2 ();
3139
- });
3140
- }
3141
- }
3142
-
3143
3155
  if (HotStaq.dispatchReadyEvents === true)
3144
3156
  {
3145
3157
  document.dispatchEvent (new Event ("DOMContentLoaded"));
@@ -3149,14 +3161,26 @@ export class HotStaq implements IHotStaq
3149
3161
  if (HotStaq.onReadyEvent != null)
3150
3162
  HotStaq.onReadyEvent (output);
3151
3163
 
3152
- // Fire onAfterNavigate hook.
3164
+ // Fire onAfterNavigate hook, then execute the new content's inline
3165
+ // scripts — unless the hook returned false, in which case the app is
3166
+ // running them itself and HotStaq stands down. Running them after the
3167
+ // hook lets the app prep the DOM first; running them at all is the
3168
+ // fix for inline page scripts silently failing on SPA navigation.
3169
+ let runScripts: boolean = true;
3170
+
3153
3171
  if (HotStaq.onAfterNavigate != null)
3154
3172
  {
3155
- let result = HotStaq.onAfterNavigate (path);
3173
+ let result: any = HotStaq.onAfterNavigate (path);
3156
3174
 
3157
3175
  if (result instanceof Promise)
3158
- await result;
3176
+ result = await result;
3177
+
3178
+ if (result === false)
3179
+ runScripts = false;
3159
3180
  }
3181
+
3182
+ if (runScripts === true)
3183
+ await HotStaq.executeInlineScripts (targetEl);
3160
3184
  }
3161
3185
 
3162
3186
  /**