@starterlib/cli 0.14.0 → 0.15.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/README.md CHANGED
@@ -104,6 +104,14 @@ par un point d'entree vide dans la configuration `production`).
104
104
  `["src/app/gabarits/coquille.ts", "src/app/pages/accueil.ts"]`), cherchees dans `browser-stats.json` que la
105
105
  cible esbuild d'`angular.json` ecrit avec `statsJson: true` ; sans elles, le premier ecran compte toutes les pages
106
106
  de route (D171) ;
107
+ - pour l'hote seul (0.15.0, lot P09-49 d'Aplomb), `starterWeb.serviceWorker: true` : `starter-web construire` pose a
108
+ la racine de sa sortie `sw.js` (une navigation que le reseau ne sert pas recoit la page hors ligne ; aucune page,
109
+ aucune reponse d'API ni du BFF, aucun manifeste en cache, aucune file hors ligne ; sa version est le condense de la
110
+ page construite), `sw-retrait.js` (l'interrupteur de retrait, que le serveur sert sous `/sw.js` a sa place), et la
111
+ page hors ligne `hors-ligne.html` avec sa feuille `hors-ligne.css`, statiques, que l'application remplace en posant
112
+ les siennes dans `public/` (`src/construction/service-worker.mjs`). Le serveur sert `/sw.js` sans cache et admet
113
+ `worker-src 'self'` ; l'hote l'enregistre par `enregistrerLeServiceWorker` (`@starterlib/core/navigateur`), sous la
114
+ politique Trusted Types `starter#service-worker`. Refuse pour un remote ou une application autonome ;
107
115
  - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
108
116
  - les formulations que l'application interdit dans ses textes : `starterWeb.formulationsInterdites`,
109
117
  chemin d'un fichier JSON relatif a sa racine, qui liste des `formulations` (un `id`, des `textes`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Chaine de verification et de construction des SPA du socle frontal starter : starter-web verify, construire, sprite, manifeste, precompresser. Jamais en dependance de production.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -7,7 +7,9 @@
7
7
  * PRERENDUE (`starterWeb.prerendu`, 0.11.4) le pose dans chaque page, et sort de ses pages les feuilles des
8
8
  * composants et les attributs `style` (`prerendu.mjs`) ;
9
9
  * 4. le manifeste (il renomme les catalogues i18n) ;
10
- * 5. la precompression (apres le renommage, sans quoi des variantes resteraient orphelines).
10
+ * 5. le service worker minimal d'un hote qui le demande (`starterWeb.serviceWorker`, P09-49 d'Aplomb) : sa version
11
+ * est le condense de la page definitive ;
12
+ * 6. la precompression (apres le renommage, sans quoi des variantes resteraient orphelines).
11
13
  */
12
14
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
13
15
  import { join } from 'node:path';
@@ -19,6 +21,7 @@ import {
19
21
  prechargerLaPoliceDansLesPages,
20
22
  resumerLExternalisation,
21
23
  } from './prerendu.mjs';
24
+ import { poserLeServiceWorker } from './service-worker.mjs';
22
25
  import { poserLeSprite } from './sprite.mjs';
23
26
 
24
27
  /** Inter latin, graisse variable, style normal : le nom que `polices.css` du socle donne. */
@@ -100,6 +103,12 @@ export function construire(
100
103
  ` manifest.json : ${manifeste.nom} ${manifeste.version}, socle ${manifeste.socle.construit}`,
101
104
  );
102
105
  }
106
+ // Le service worker minimal d'un hote qui le demande (P09-49) : apres la page definitive, avant la precompression.
107
+ if (projet.serviceWorker) {
108
+ const { version, gardes } = poserLeServiceWorker(projet.sortie);
109
+ const propres = gardes.length === 0 ? '' : ` ; de l'application : ${gardes.join(', ')}`;
110
+ journal.info(` service worker : sw.js ${version}, sw-retrait.js, page hors ligne${propres}`);
111
+ }
103
112
  const { resume, defauts } = juger(precompresser(projet.sortie));
104
113
  journal.info(` precompression : ${resume}`);
105
114
  for (const defaut of defauts) journal.erreur(` - ${defaut}`);
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Feuille de la page hors ligne du socle frontal (lot P09-49 d'Aplomb) : gardee par le service worker avec sa page, sans
3
+ * police ni image a charger (le reseau manque). Les deux themes suivent le systeme.
4
+ */
5
+ :root {
6
+ color-scheme: light dark;
7
+ font-family:
8
+ system-ui,
9
+ -apple-system,
10
+ 'Segoe UI',
11
+ Roboto,
12
+ sans-serif;
13
+ line-height: 1.5;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ background: Canvas;
19
+ color: CanvasText;
20
+ }
21
+
22
+ .hors-ligne {
23
+ box-sizing: border-box;
24
+ max-inline-size: 36rem;
25
+ margin-inline: auto;
26
+ padding-block: 4rem;
27
+ padding-inline: 1rem;
28
+ }
29
+
30
+ h1 {
31
+ font-size: 1.5rem;
32
+ line-height: 1.25;
33
+ }
34
+
35
+ h2 {
36
+ font-size: 1.125rem;
37
+ line-height: 1.3;
38
+ }
39
+
40
+ a {
41
+ color: LinkText;
42
+ }
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <!--
3
+ Page hors ligne du socle frontal (@starterlib ; lot P09-49 d'Aplomb) : le service worker la rend a une navigation que
4
+ le reseau ne sert pas. Statique : aucun script, aucun style en ligne (la politique de la page n'en admet pas sans
5
+ nonce), aucune donnee. Une application la remplace par la sienne en posant `hors-ligne.html` et `hors-ligne.css`
6
+ dans `public/`.
7
+ -->
8
+ <html lang="fr">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ <title>Hors ligne &middot; Offline</title>
13
+ <link rel="stylesheet" href="/hors-ligne.css" />
14
+ </head>
15
+ <body>
16
+ <main class="hors-ligne">
17
+ <h1>Vous &ecirc;tes hors ligne</h1>
18
+ <p>
19
+ La page demand&eacute;e n&rsquo;a pas pu se charger. Rien n&rsquo;a &eacute;t&eacute;
20
+ envoy&eacute;. V&eacute;rifiez votre connexion, puis rechargez la page.
21
+ </p>
22
+ <div lang="en">
23
+ <h2>You are offline</h2>
24
+ <p>
25
+ The page could not load, and nothing was sent. Check your connection, then reload the
26
+ page.
27
+ </p>
28
+ </div>
29
+ <p>
30
+ <a href="">Recharger la page &middot; <span lang="en">Reload the page</span></a>
31
+ </p>
32
+ </main>
33
+ </body>
34
+ </html>
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Interrupteur de retrait du service worker du socle frontal (@starterlib ; lot P09-49 d'Aplomb) : le BFF le sert sous
3
+ * `/sw.js` a la place du service worker quand l'exploitant le retire. Le navigateur, qui revoit `/sw.js` a chaque
4
+ * navigation, l'installe a la place de l'ancien : il efface les caches que celui-ci avait poses, puis se desinscrit.
5
+ * Les pages ouvertes continuent sur le reseau ; rien n'est recharge de force, rien n'est garde.
6
+ */
7
+ 'use strict';
8
+
9
+ self.addEventListener('install', () => {
10
+ self.skipWaiting();
11
+ });
12
+
13
+ self.addEventListener('activate', (evenement) => {
14
+ evenement.waitUntil(
15
+ (async () => {
16
+ for (const cle of await caches.keys()) {
17
+ if (cle.startsWith('starter-hors-ligne-')) await caches.delete(cle);
18
+ }
19
+ await self.registration.unregister();
20
+ })(),
21
+ );
22
+ });
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Service worker minimal du socle frontal (@starterlib ; lot P09-49 d'Aplomb, ratification « pwa-legere » de DEC-062),
3
+ * pose par `starter-web construire` dans la sortie d'un hote qui le demande (`starterWeb.serviceWorker`).
4
+ *
5
+ * Ce qu'il fait, et rien d'autre : une NAVIGATION que le reseau ne sert pas recoit la page hors ligne statique.
6
+ *
7
+ * Ce qu'il ne fait jamais : garder une page, une reponse `/api/**` ou `/bff/**`, un `manifest.json` ou un
8
+ * `remoteEntry.json` ; mettre un geste en file pour plus tard. Chaque navigation va au reseau : le BFF rend la coquille
9
+ * avec le nonce de SA reponse, les versions et la maintenance restent les siennes. Les seuls fichiers gardes sont la
10
+ * page hors ligne et sa feuille, statiques, sans donnee de la personne.
11
+ *
12
+ * Le BFF le sert sous `/sw.js`, sans cache (`Cache-Control: no-cache`) ; son interrupteur de retrait sert a la place
13
+ * `sw-retrait.js`, qui le desinscrit de chaque navigateur a la verification suivante.
14
+ */
15
+ 'use strict';
16
+
17
+ /** La version de la construction, posee par `starter-web construire` : une construction nouvelle, un worker nouveau. */
18
+ const VERSION = '__VERSION__';
19
+ const CACHE = `starter-hors-ligne-${VERSION}`;
20
+ const PAGE_HORS_LIGNE = '/hors-ligne.html';
21
+ const FEUILLE_HORS_LIGNE = '/hors-ligne.css';
22
+
23
+ self.addEventListener('install', (evenement) => {
24
+ evenement.waitUntil(
25
+ caches
26
+ .open(CACHE)
27
+ .then((cache) =>
28
+ cache.addAll(
29
+ [PAGE_HORS_LIGNE, FEUILLE_HORS_LIGNE].map(
30
+ (chemin) => new Request(chemin, { cache: 'reload' }),
31
+ ),
32
+ ),
33
+ )
34
+ .then(() => self.skipWaiting()),
35
+ );
36
+ });
37
+
38
+ self.addEventListener('activate', (evenement) => {
39
+ evenement.waitUntil(
40
+ (async () => {
41
+ // Les caches d'une version precedente partent : un seul jeu de fichiers hors ligne a la fois.
42
+ for (const cle of await caches.keys()) {
43
+ if (cle.startsWith('starter-hors-ligne-') && cle !== CACHE) await caches.delete(cle);
44
+ }
45
+ if (self.registration.navigationPreload) await self.registration.navigationPreload.enable();
46
+ await self.clients.claim();
47
+ })(),
48
+ );
49
+ });
50
+
51
+ self.addEventListener('fetch', (evenement) => {
52
+ const requete = evenement.request;
53
+ const adresse = new URL(requete.url);
54
+ // Une autre origine, une autre methode : le navigateur seul, comme sans service worker.
55
+ if (adresse.origin !== self.location.origin || requete.method !== 'GET') return;
56
+
57
+ if (requete.mode === 'navigate') {
58
+ evenement.respondWith(
59
+ (async () => {
60
+ try {
61
+ // Le prechargement de la navigation, s'il a eu lieu, sinon le reseau : jamais une copie.
62
+ const prechargee = await evenement.preloadResponse;
63
+ return prechargee ?? (await fetch(requete));
64
+ } catch {
65
+ return (await caches.match(PAGE_HORS_LIGNE, { cacheName: CACHE })) ?? Response.error();
66
+ }
67
+ })(),
68
+ );
69
+ return;
70
+ }
71
+
72
+ // La feuille de la page hors ligne, et elle seule : lue au cache quand le reseau manque.
73
+ if (adresse.pathname === FEUILLE_HORS_LIGNE) {
74
+ evenement.respondWith(
75
+ fetch(requete).catch(
76
+ async () =>
77
+ (await caches.match(FEUILLE_HORS_LIGNE, { cacheName: CACHE })) ?? Response.error(),
78
+ ),
79
+ );
80
+ }
81
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Le service worker minimal d'un hote (lot P09-49 d'Aplomb ; ratification « pwa-legere » de DEC-062), pose dans sa
3
+ * sortie par `starter-web construire` quand `starterWeb.serviceWorker` le demande.
4
+ *
5
+ * - `sw.js` : une navigation que le reseau ne sert pas recoit la page hors ligne ; rien d'autre n'est garde ni rejoue
6
+ * (`service-worker/sw.js`). Sa VERSION est le condense de la page de la construction : une construction nouvelle
7
+ * change ses octets, et le navigateur installe le worker nouveau a la navigation suivante.
8
+ * - `sw-retrait.js` : l'interrupteur de retrait, que le BFF sert sous `/sw.js` a la place du worker.
9
+ * - `hors-ligne.html` et `hors-ligne.css` : la page hors ligne du socle, statique ; celle de l'application, posee dans
10
+ * `public/` (donc deja dans la sortie), est gardee.
11
+ *
12
+ * A poser APRES la page definitive (police prechargee) et AVANT la precompression, qui compresse aussi ces fichiers.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const MODELES = fileURLToPath(new URL('./service-worker/', import.meta.url));
20
+
21
+ /** Les fichiers du service worker, a la racine de la sortie : le BFF les sert depuis la meme origine. */
22
+ export const FICHIERS_DU_SERVICE_WORKER = Object.freeze([
23
+ 'sw.js',
24
+ 'sw-retrait.js',
25
+ 'hors-ligne.html',
26
+ 'hors-ligne.css',
27
+ ]);
28
+
29
+ /**
30
+ * La politique Trusted Types de l'enregistrement, que `enregistrerLeServiceWorker` (`@starterlib/core/navigateur`)
31
+ * cree sous le meme nom : la politique de la page de l'hote doit la porter nommement.
32
+ */
33
+ export const POLITIQUE_DU_SERVICE_WORKER = 'starter#service-worker';
34
+
35
+ /** Ce que l'application peut fournir a la place du socle : sa page hors ligne et sa feuille. */
36
+ const FOURNIS_PAR_L_APPLICATION = new Set(['hors-ligne.html', 'hors-ligne.css']);
37
+
38
+ const MARQUE_DE_VERSION = "'__VERSION__'";
39
+
40
+ /** La version d'une construction : le condense de sa page, qui nomme chacun de ses morceaux haches. */
41
+ export function versionDeLaConstruction(sortie) {
42
+ const page = join(sortie, 'index.html');
43
+ if (!existsSync(page)) {
44
+ throw new Error(`service worker : ${page} absent, construire d'abord.`);
45
+ }
46
+ return createHash('sha256').update(readFileSync(page)).digest('hex').slice(0, 16);
47
+ }
48
+
49
+ /**
50
+ * Pose les fichiers du service worker dans `sortie` ; rend la version posee et les fichiers que l'application fournit
51
+ * elle-meme (gardes). Idempotent : une seconde pose sur la meme sortie ecrit les memes octets.
52
+ */
53
+ export function poserLeServiceWorker(sortie) {
54
+ const version = versionDeLaConstruction(sortie);
55
+ const gardes = [];
56
+ for (const nom of FICHIERS_DU_SERVICE_WORKER) {
57
+ const cible = join(sortie, nom);
58
+ const modele = readFileSync(join(MODELES, nom), 'utf8');
59
+ if (
60
+ FOURNIS_PAR_L_APPLICATION.has(nom) &&
61
+ existsSync(cible) &&
62
+ readFileSync(cible, 'utf8') !== modele
63
+ ) {
64
+ gardes.push(nom);
65
+ continue;
66
+ }
67
+ if (nom === 'sw.js' && !modele.includes(MARQUE_DE_VERSION)) {
68
+ throw new Error('service worker : le modele sw.js ne porte plus sa marque de version.');
69
+ }
70
+ writeFileSync(
71
+ cible,
72
+ nom === 'sw.js' ? modele.replace(MARQUE_DE_VERSION, `'${version}'`) : modele,
73
+ 'utf8',
74
+ );
75
+ }
76
+ return { version, gardes };
77
+ }
@@ -29,6 +29,7 @@
29
29
  import { readFileSync } from 'node:fs';
30
30
  import { dirname, join } from 'node:path';
31
31
  import { fichiers, lireJson, relatif } from '../commun.mjs';
32
+ import { POLITIQUE_DU_SERVICE_WORKER } from '../construction/service-worker.mjs';
32
33
  import { natureDesFichiers } from '../livraison.mjs';
33
34
 
34
35
  /** Les politiques que la CSP du BFF laisse creer par defaut (application.yml du BFF). */
@@ -360,6 +361,19 @@ export function jugerUnFichierLivre(
360
361
  return defauts;
361
362
  }
362
363
 
364
+ /**
365
+ * Les politiques Trusted Types que l'application declare : un remote (lot 270), que le BFF retire s'il en exige une
366
+ * que sa CSP n'admet pas ; une application autonome (0.11.2), dont la page doit les porter nommement (la page d'ui-auth
367
+ * d'Aplomb et le widget de verification humaine) ; l'hote qui enregistre le service worker du socle (0.15.0, lot
368
+ * P09-49 d'Aplomb), celle de son enregistrement, que la politique de sa page doit porter avec `worker-src 'self'`.
369
+ * L'etape le dit des la construction.
370
+ */
371
+ export function politiquesDeclarees(projet) {
372
+ if (projet.role === 'remote') return projet.source?.trustedTypes ?? [];
373
+ if (projet.role === 'autonome') return projet.paquet.starterWeb?.trustedTypes ?? [];
374
+ return projet.serviceWorker === true ? [POLITIQUE_DU_SERVICE_WORKER] : [];
375
+ }
376
+
363
377
  export default {
364
378
  nom: 'securite',
365
379
  roles: ['remote', 'hote', 'autonome'],
@@ -374,23 +388,15 @@ export default {
374
388
  };
375
389
  }
376
390
  const { partages } = natureDesFichiers(lireJson(join(projet.sortie, 'remoteEntry.json')));
377
- // Les politiques que l'application declare : un remote (lot 270), que le BFF retire s'il en exige une
378
- // que sa CSP n'admet pas ; une application autonome (0.11.2), dont la page doit les porter nommement
379
- // (la page d'ui-auth d'Aplomb et le widget de verification humaine). L'etape le dit des la construction.
380
- const declarees =
381
- projet.role === 'remote'
382
- ? (projet.source?.trustedTypes ?? [])
383
- : projet.role === 'autonome'
384
- ? (projet.paquet.starterWeb?.trustedTypes ?? [])
385
- : [];
391
+ const declarees = politiquesDeclarees(projet);
386
392
  const avertissements = declarees
387
393
  .filter((politique) => !POLITIQUES_ADMISES.includes(politique))
388
394
  .map((politique) =>
389
- projet.role === 'autonome'
395
+ projet.role === 'remote'
390
396
  ? `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
391
- '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : la page de l application doit la porter nommement.'
397
+ '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.'
392
398
  : `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
393
- '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
399
+ '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : la page de l application doit la porter nommement.',
394
400
  );
395
401
  const defauts = [];
396
402
  // 0.11.7 : les admissions nommees, et la carte de la construction qui dit les sources de chaque morceau.
package/src/projet.mjs CHANGED
@@ -11,6 +11,7 @@
11
11
  * - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
12
12
  * - le prefixe : `prefix` du projet d'`angular.json`, ou `starterWeb.prefixe` ;
13
13
  * - le prerendu : `starterWeb.prerendu` (une application autonome servie en pages statiques, 0.11.4) ;
14
+ * - le service worker minimal : `starterWeb.serviceWorker` (l'hote seul, lot P09-49 d'Aplomb, 0.15.0) ;
14
15
  * - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser`.
15
16
  *
16
17
  * Les options de la ligne de commande (`--nom`, `--prefixe`, `--pleine-page`) les remplacent : ce
@@ -76,6 +77,14 @@ export function lireLeProjet(racine, options = {}) {
76
77
  `starterWeb.prerendu : reserve a une application autonome, pas a un role "${role}".`,
77
78
  );
78
79
  }
80
+ // Le service worker minimal (lot P09-49 d'Aplomb) : celui d'un hote, qui sert ses navigations ; jamais celui d'un
81
+ // remote (il vit sous le perimetre de l'hote) ni d'une application autonome (l'ecran anonyme, l'urgence).
82
+ const serviceWorker = reglages.serviceWorker === true;
83
+ if (serviceWorker && role !== 'hote') {
84
+ throw new ProjetIllisible(
85
+ `starterWeb.serviceWorker : reserve a l'hote, pas a un role "${role}".`,
86
+ );
87
+ }
79
88
  const source = role === 'remote' ? lireJson(cheminSource) : null;
80
89
  if (role === 'remote' && source === null) {
81
90
  throw new ProjetIllisible('manifest.source.json absent ou illisible : un remote en a un.');
@@ -100,6 +109,7 @@ export function lireLeProjet(racine, options = {}) {
100
109
  prefixe: options.prefixe ?? reglages.prefixe ?? projet.prefix ?? null,
101
110
  pleinePage: options.pleinePage ?? source?.pleinePage === true,
102
111
  prerendu,
112
+ serviceWorker,
103
113
  source,
104
114
  nomDuProjetAngular: nomDuProjet,
105
115
  sortieRelative: sortie.split('\\').join('/'),