loader.js

/**
 * Ce fichier est le seul qui devrait être appelé en cross-domain, il est es5 et doit être chargé par un tag script (de type module ou pas, async ou pas).
 *
 * Il en global toutes nos fonctions de chargement, mais sans rien charger de plus au départ
 * - j3pLoad (player v1)
 * - editGraphe (éditeur v1)
 * - showParcours (afficheur de parcours v1)
 * - spEdit (éditeur v2)
 * - spPlay (player v2)
 * - spView (afficheur de parcours v2)
 * - start (interface de la home)
 *
 * L’appel d’une de ces fonctions globales va déclencher le chargement de son code js,
 * cf {@tutorial chargement}
 *
 * ATTENTION, ce fichier doit rester en ES5 et ne pas avoir d’import statique ni dynamique,
 * il est appelé dans un tag script, ordinaire ou de type module
 * (les deux sont possibles pour compatibilité ascendante).
 *
 * Il n’est pas traité par vite mais copié dans build après chaque build de vite,
 * pour gérer les chargements depuis un autre domaine (cf scripts/fixLoader.js).
 *
 * 1) loader.js
 *    - déclare nos fonctions en global (pour mémoriser d’éventuels appels et les refaire quand lazyLoader sera chargé)
 *    - ajoute un <script type="module" src="http…/lazyLoader.js"></script>
 *
 * 2) lazyLoader remplace les fonctions globales précédentes par une fct qui prend les arguments, charge le js de la fct puis l’appelle.
 *
 * On aurait pu mettre tout le code de lazyLoader dans ce fichier,
 * mais ce loader était destiné à orienter entre deux lazyLoader,
 * une version moderne et une version legacy (on verra pour le remettre
 * quand il n'y aura plus de code v1 pour voir si le build passe avec le
 * plugin legacy de vite).
 * Et par ailleurs c'est plus confortable d'avoir lazyLoader en ts
 * @fileOverview
 */

// une bonne vieille IIFE pour emballer notre code
(function preload () {
  'use strict'

  /**
   * Détection basique de browser obsolète. On ne teste pas toutes les fonctionnalités définie
   * comme étant la baseline (cf https://web-platform-dx.github.io/web-features/), mais les plus fréquentes.
   * Baseline est la target de la compilation vite, donc les browsers qui n'en font pas partie
   * pouraient avoir des problèmes au runtime
   * @return {boolean}
   */
  function isBrowserInBaseline () {
    try {
      var s = document.createElement('script')
      // Support des modules ES
      if (!('noModule' in s)) return false

      // Primitifs/objets globaux indispensables
      if (typeof globalThis === 'undefined') return false
      if (typeof Promise === 'undefined' || typeof Promise.prototype === 'undefined' || typeof Promise.prototype.finally !== 'function') return false
      if (typeof Symbol === 'undefined' || typeof Symbol.iterator === 'undefined') return false
      if (typeof Map === 'undefined' || typeof Set === 'undefined' || typeof WeakSet === 'undefined') return false

      // Réseau / URL modernes
      if (typeof fetch !== 'function') return false
      if (typeof URL === 'undefined' || typeof URLSearchParams === 'undefined') return false

      // Méthodes ES2015+ fréquemment utilisées par les toolchains modernes
      if (typeof Object.assign !== 'function') return false
      if (typeof Array.from !== 'function') return false
      if (!Array.prototype || typeof Array.prototype.includes !== 'function') return false
      if (!String.prototype || typeof String.prototype.startsWith !== 'function') return false
      if (!String.prototype || typeof String.prototype.endsWith !== 'function') return false

      // Microtasks et observer modernes utiles aux libs (facultatif mais discriminant)
      // queueMicrotask est largement disponible sur les navigateurs "baseline"
      if (typeof queueMicrotask !== 'function') return false
      // IntersectionObserver est dans la baseline depuis longtemps et souvent requis par libs UI
      if (typeof IntersectionObserver === 'undefined') return false

      // On arrête là nos tests en supposant que tout devrait bien se passer
      return true
    } catch (e) {
      return false
    }
  }

  // appelle le loader dès qu'il est chargé, avec les mêmes arguments
  function waitForLoader () {
    var loader = arguments[0]
    // le 1er argument est le loader, imposé par le bind plus bas, les suivants ceux passés au loader
    // on met en attente que le timeout soit atteint ou pas (s’il est atteint parce que la connexion est très mauvaise
    // l’utilisateur aura le message mais ça finira par être lancé, sinon ça ne sera jamais traité
    // au cas où ce serait déjà là
    if (typeof window[loader] === 'function') {
      if (window.spLoading[loader] === 'preload') {
        var originalArgs = arguments
        nbWait++
        if (nbWait > 200) return console.error(Error('Loader ' + loader + ' toujours pas chargé après 20s, ABANDON'))
        setTimeout(function () {
          waitForLoader.apply(null, originalArgs)
        }, 100)
        return
      }
      // c'est plus notre preLoader, on l'appelle en virant le 1er argument qui est son nom
      // avec un try/catch en cas de throw sync du loader
      try {
        var result = window[loader].apply(null, Array.from(arguments).slice(1))
        if (result instanceof Promise) {
          result.catch(function (error) { console.error(error) })
        }
      } catch (error) {
        console.error(error)
      }
    } else {
      console.error(Error('Le loader ' + loader + ' n’existe plus'))
    }
  }

  // ajoute le script de chargement du lazyLoader dans le dom
  function addScript () {
    if (script.inDom) return console.error(Error('tag script déjà dans le dom'))
    try {
      document.body.appendChild(script)
    } catch (error) {
      console.error(error)
      return setTimeout(addScript, 200)
    }
    // appendChild a fonctionné
    script.inDom = true
    script.src = loaderUrl
  }

  // MAIN code
  if (window.spLoading) {
    console.warn('loader.js a déjà été chargé dans ce DOM')
    return
  }
  window.spLoading = {}
  var script, timeoutId, loaderUrl
  var nbWait = 0
  try {
    script = document.createElement('script')
    // on a pas vraiment de moyen fiable de détecter si l'import dynamique est supporté par le navigateur
    // (cf https://stackoverflow.com/questions/60317251/how-to-feature-detect-whether-a-browser-supports-dynamic-es6-module-loading)
    // on détecte ici les cas triviaux (pas de support du type module ou pas de Promise),
    // et pour les autres l'import dynamique du lazyLoader marchera pas et ça va s'arrêter là
    if (!isBrowserInBaseline()) {
      // eslint-disable-next-line no-alert
      return alert('Désolé, ce navigateur ne permet pas d’exécuter les exercices interactifs, essayez avec un navigateur plus récent.')
    }
    timeoutId = setTimeout(function timeout () {
      // eslint-disable-next-line no-alert
      alert('Après 30s d’attente le chargement de j3p n’est toujours pas terminé, c’est probablement inutile d’attendre davantage.')
    }, 30000)

    // pas de for…of en es5
    ;['editGraphe', 'j3pLoad', 'showParcours', 'spEdit', 'spPlay', 'spView', 'spValidate'].forEach(function (loader) {
      if (typeof window[loader] !== 'function') {
        window[loader] = waitForLoader.bind(null, loader)
        // pour marquer l'étape de chargement (et permettre de vérifier qu’il a bien été écrasé par l’original)
        window.spLoading[loader] = 'preload'
      } else {
        console.error(Error(loader + ' était déjà déclaré en global'))
      }
    })

    // on charge lazyLoader en module (pour qu’il puisse faire des imports)
    // NE PAS MODIFIER ces lignes, elles seront complétées par scripts/postBuild.sh
    // (j3pVersion sera utile au bugsnag mis par la bibli pour détecter les changements de version de j3p)
    loaderUrl = ''
    window.j3pVersion = ''
    // fin lignes à ne pas modifier

    script.type = 'module'
    // ça c’est dans tous les cas
    script.crossOrigin = 'anonymous'
    // quand lazyLoader sera chargé
    script.addEventListener('load', function onLazyLoaderLoaded () {
      clearTimeout(timeoutId)
    })
    // c’est mieux de faire dans cet ordre pour une meilleure compatibilité avec tous les navigateurs
    // (script elt puis insertion dans le dom puis affectation src)
    // mais avant d'ajouter le script dans body, il faut vérifier que le dom est chargé
    // (pas forcément le cas si on est chargé dans un <head>)
    if (document.readyState === 'interactive' || document.readyState === 'complete') addScript()
    else document.addEventListener('DOMContentLoaded', addScript)
  } catch (error) {
    console.error(error)
  }
})()