@lls/offline 24.20.0-debug → 24.20.0-debug2

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
@@ -3,3 +3,27 @@
3
3
  handles offline as webworker for android/ios and desktop
4
4
 
5
5
  Likely used by core, cocore
6
+
7
+ ## Getting started
8
+
9
+ Deux types de workers pour gérer le offline:
10
+
11
+ - un web worker (WW): offlineWorker.ts
12
+ - un service worker (SW): fetch.worker.ts
13
+
14
+ Le main (useOfflineWorker) instancie les deux workers.
15
+ Dû à la nature du SW qui ne peut pas poster en retour au main, on crèe un lien entre WW et SW via MessageChannel afin qu'ils puissent communiquer entre eux.
16
+
17
+ SW a pour responsabilité d'intercepter les calls (GQL, assets)
18
+ Il demande le contenu à WW.
19
+
20
+ WW a pour responsabilité de
21
+ - fetcher le contenu offline (.zip sur s3)
22
+ - gérer l'opfs (stockage navigateur du contenu)
23
+ - lire dans les archives pour servir SW
24
+
25
+ ## Testing
26
+
27
+ Les WW et SW ont pour interface (offlineWorker.ts, fetch.worker.ts) et délèguent les méthodes métiers aux autres fichiers worker agnostiques (donc testable)
28
+
29
+
@@ -16,7 +16,6 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
16
16
  }
17
17
  methodName && cbks[methodName]?.(arg);
18
18
  };
19
- offlineWorker2.onmessage = fn;
20
19
  const makeFn = (methodName) => {
21
20
  return ({ ports, ...arg } = {}) => {
22
21
  return new Promise((resolve, reject) => {
@@ -31,27 +30,53 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
31
30
  cbks[key] = fn2;
32
31
  }
33
32
  });
34
- return obj;
33
+ return {
34
+ ...obj,
35
+ handler: {
36
+ canHandle: (methodName) => {
37
+ return methodName in cbks || methodNames.includes(methodName);
38
+ },
39
+ handle: fn
40
+ }
41
+ };
35
42
  };
36
43
  var a = (a2) => a2;
37
44
  var makeSyncHandler = a(
38
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
39
- try {
40
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
41
- const results = await services[methodName](arg, { ports });
42
- sender.postMessage({ eventId, methodName, results });
43
- return;
45
+ (sender, services) => ({
46
+ canHandle: (methodName) => {
47
+ return methodName in services;
48
+ },
49
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
50
+ try {
51
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
52
+ const results = await services[methodName](arg, { ports });
53
+ sender.postMessage({ eventId, methodName, results });
54
+ return;
55
+ }
56
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
57
+ } catch (e) {
58
+ console.debug("failed", e);
59
+ sender.postMessage({ eventId, methodName, error: e });
44
60
  }
45
- throw new Error(`invalid methodName ${String(methodName)}`);
46
- } catch (e) {
47
- console.debug("failed", e);
48
- sender.postMessage({ eventId, methodName, error: e });
49
61
  }
50
- }
62
+ })
51
63
  );
52
64
  var jsonToBuf = (json) => new TextEncoder().encode(JSON.stringify(json));
53
65
 
54
66
  // src/worker/swFetchScreenCall.ts
67
+ var zipSlugName = (str) => str.replace(/[^a-zA-Z0-9-.]/g, "");
68
+ var cocoreFetchPartialPageScreen = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables, knownLinks: knownLinks2 }) => {
69
+ const key = `gqls/${zipSlugName(variables.slugs[0] || "")}.json`;
70
+ const entryId = knownLinks2.get(key);
71
+ if (!entryId) {
72
+ throw new Error(`slug not found ${key}`);
73
+ }
74
+ const gqlPage = await offlineWorkerSync2.loadPageContent({ entryId });
75
+ return { data: gqlPage };
76
+ };
77
+ var cocoreFetchPartialPageSiblingsScreen = async (_ev) => {
78
+ return { data: { viewer: {} } };
79
+ };
55
80
  var coreFetchBookDrawerScreen = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables }) => {
56
81
  const slug = variables.slugs?.[0];
57
82
  const uris = variables.uris;
@@ -161,22 +186,25 @@ var coreFetchPagesApi = async (ev, { offlineWorkerSync: offlineWorkerSync2, vari
161
186
  }
162
187
  }
163
188
  };
164
- var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2 }) => {
189
+ var handles = {
190
+ coreFetchBookDrawerScreen,
191
+ coreFetchPagesApi,
192
+ cocoreFetchPartialPageScreen,
193
+ cocoreFetchPartialPageSiblingsScreen
194
+ };
195
+ var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).test(url);
196
+ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
165
197
  try {
166
198
  const variables = (
167
199
  // @ts-ignore TODO: handle variables from body(stream) properly!!
168
200
  ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
169
201
  );
170
202
  console.debug("variables are", variables, "vs", ev.request.body);
171
- const handles = {
172
- coreFetchBookDrawerScreen,
173
- coreFetchPagesApi
174
- };
175
203
  const key = ev.request.url.match(Object.keys(handles).join("|"))?.[0];
176
204
  if (!key) {
177
205
  throw new Error("unknown ev");
178
206
  }
179
- const json = await handles[key](ev, { offlineWorkerSync: offlineWorkerSync2, variables });
207
+ const json = await handles[key](ev, { offlineWorkerSync: offlineWorkerSync2, variables, knownLinks: knownLinks2 });
180
208
  return { status: 200, json };
181
209
  } catch (e) {
182
210
  console.error(e);
@@ -184,7 +212,7 @@ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2 }) =>
184
212
  }
185
213
  };
186
214
 
187
- // src/worker/fetch.worker.ts
215
+ // src/worker/fetch-worker.ts
188
216
  var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
189
217
  try {
190
218
  const buf = await offlineWorkerSync2.readZipEntry(entryId);
@@ -212,9 +240,9 @@ var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorke
212
240
  });
213
241
  }
214
242
  };
215
- var fetchScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2 }) => {
243
+ var fetchScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
216
244
  try {
217
- const { status, json } = await handleScreenCall(ev, { offlineWorkerSync: offlineWorkerSync2 });
245
+ const { status, json } = await handleScreenCall(ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 });
218
246
  return new Response(jsonToBuf(json), {
219
247
  headers: {
220
248
  "Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
@@ -255,23 +283,23 @@ swSelf.addEventListener("fetch", (ev) => {
255
283
  const url = ev.request.url;
256
284
  const extension = url.match(/\.[^.]+$/)?.[0];
257
285
  const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
258
- const screenCall = /coreFetchPagesApi|coreFetchBookDrawerScreen/.test(url);
259
- if (!mimeType && !screenCall) {
260
- if (!/\.(css|tsx?|jsx?|woff2|fr)/.test(url)) {
261
- console.debug("no ext", url.match(/\.[^.]+$/)?.[0]);
286
+ const isValidScreenCall = canHandleScreenCall(url);
287
+ if (!mimeType && !isValidScreenCall) {
288
+ if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
289
+ console.debug("no ext", extension || url);
262
290
  }
263
291
  return;
264
292
  }
265
- const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, ""));
266
- if (!entryId && !screenCall) {
293
+ const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
294
+ if (!entryId && !isValidScreenCall) {
267
295
  if (/lls|lelivrescolaire/.test(url)) {
268
296
  console.info("no link for", url.replace(/https:\/\/.*?\//, ""));
269
297
  }
270
298
  return;
271
299
  }
272
- if (screenCall) {
300
+ if (isValidScreenCall) {
273
301
  console.debug("screencall!");
274
- return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync }));
302
+ return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
275
303
  }
276
304
  return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
277
305
  });
@@ -284,13 +312,15 @@ swSelf.addEventListener("activate", (_event) => {
284
312
  var offlineWorker;
285
313
  var offlineWorkerSync;
286
314
  var initChannel = async (__, { ports }) => {
287
- console.info(`FETCH WORKER innitChannel v${13}`);
315
+ console.info(`FETCH WORKER innitChannel v${/* @__PURE__ */ new Date()}`);
288
316
  offlineWorker = ports[0];
289
- offlineWorkerSync = makeSyncWorker(
317
+ const { handler, ...sender } = makeSyncWorker(
290
318
  offlineWorker,
291
319
  ["readZipEntry", "loadBook", "slugToFileMeta", "loadPageContent", "searchBooks"],
292
320
  "fetchworker"
293
321
  );
322
+ offlineWorkerSync = sender;
323
+ offlineWorker.onmessage = handler.handle;
294
324
  offlineWorkerSync.on("setKnownLinks", setKnownLinks);
295
325
  offlineWorker.postMessage({ methodName: "sendAppReady" });
296
326
  };
@@ -316,7 +346,7 @@ swSelf.onmessage = async ({
316
346
  await services[methodName](arg, { ports });
317
347
  return;
318
348
  }
319
- throw new Error(`fetchworker:invalid methodName ${methodName}`);
349
+ throw new Error(`fetchworker:invalid methodName ${Object.keys(services)} ${methodName}`);
320
350
  } catch (e) {
321
351
  console.error("failed", e);
322
352
  }
@@ -1,21 +1,77 @@
1
1
  // src/utils/worker.ts
2
+ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
3
+ const dic = {};
4
+ const cbks = {};
5
+ const fn = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
6
+ if (eventId && eventId in dic) {
7
+ if (error) {
8
+ dic[eventId].reject(error);
9
+ } else {
10
+ dic[eventId].resolve(results);
11
+ }
12
+ } else {
13
+ if (methodName && !methodName.startsWith("async") && !(methodName in cbks)) {
14
+ console.error(`syncWorker: ${dbg}`, eventId, methodName, error);
15
+ }
16
+ }
17
+ methodName && cbks[methodName]?.(arg);
18
+ };
19
+ const makeFn = (methodName) => {
20
+ return ({ ports, ...arg } = {}) => {
21
+ return new Promise((resolve, reject) => {
22
+ const eventId = `${methodName}:${Date.now()}`;
23
+ dic[eventId] = { resolve, reject };
24
+ offlineWorker.postMessage({ eventId, methodName, arg }, ports);
25
+ });
26
+ };
27
+ };
28
+ const obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
29
+ on: (key2, fn2) => {
30
+ cbks[key2] = fn2;
31
+ }
32
+ });
33
+ return {
34
+ ...obj,
35
+ handler: {
36
+ canHandle: (methodName) => {
37
+ return methodName in cbks || methodNames.includes(methodName);
38
+ },
39
+ handle: fn
40
+ }
41
+ };
42
+ };
2
43
  var a = (a2) => a2;
3
44
  var makeSyncHandler = a(
4
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
5
- try {
6
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
7
- const results = await services[methodName](arg, { ports });
8
- sender.postMessage({ eventId, methodName, results });
9
- return;
45
+ (sender2, services) => ({
46
+ canHandle: (methodName) => {
47
+ return methodName in services;
48
+ },
49
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
50
+ try {
51
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
52
+ const results = await services[methodName](arg, { ports });
53
+ sender2.postMessage({ eventId, methodName, results });
54
+ return;
55
+ }
56
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
57
+ } catch (e) {
58
+ console.debug("failed", e);
59
+ sender2.postMessage({ eventId, methodName, error: e });
10
60
  }
11
- throw new Error(`invalid methodName ${String(methodName)}`);
12
- } catch (e) {
13
- console.debug("failed", e);
14
- sender.postMessage({ eventId, methodName, error: e });
15
61
  }
16
- }
62
+ })
17
63
  );
18
64
  var bufToJson = (buf) => JSON.parse(new TextDecoder("UTF-8").decode(new Uint8Array(buf)));
65
+ var combineHandlers = (...handlers) => {
66
+ return ({ data: { methodName, ...data }, ports }) => {
67
+ for (const h of handlers) {
68
+ if (h.canHandle(methodName)) {
69
+ return h.handle({ data: { methodName, ...data }, ports });
70
+ }
71
+ }
72
+ console.error("no handlers matched", methodName);
73
+ };
74
+ };
19
75
 
20
76
  // src/worker/wwApi.ts
21
77
  var STATE = {
@@ -40,7 +96,9 @@ var slugToFileMeta = async ({ slug, json }) => {
40
96
  return { pageId, chapterId };
41
97
  };
42
98
  var loadPageContent = async ({ chapterId, json }) => {
43
- json.viewer.pages.hits[0].chapter = STATE.chapters[chapterId] || json.viewer.pages.hits[0].chapter;
99
+ if (chapterId) {
100
+ json.viewer.pages.hits[0].chapter = STATE.chapters[chapterId] || json.viewer.pages.hits[0].chapter;
101
+ }
44
102
  return json;
45
103
  };
46
104
  var searchBooks = async ({
@@ -83,14 +141,7 @@ var loadPreload = (json) => {
83
141
  });
84
142
  };
85
143
 
86
- // ../../node_modules/.pnpm/fflate@0.8.2/node_modules/fflate/esm/index.mjs
87
- import { createRequire } from "module";
88
- var require2 = createRequire("/");
89
- var Worker;
90
- try {
91
- Worker = require2("worker_threads").Worker;
92
- } catch (e) {
93
- }
144
+ // ../../node_modules/.pnpm/fflate@0.8.2/node_modules/fflate/esm/browser.js
94
145
  var u8 = Uint8Array;
95
146
  var u16 = Uint16Array;
96
147
  var i32 = Int32Array;
@@ -739,6 +790,9 @@ var loadZip = async (buf, bookId) => {
739
790
  const unzipper = new Unzip();
740
791
  unzipper.register(UnzipInflate);
741
792
  unzipper.onfile = (file) => {
793
+ if (!file.name.includes(".")) {
794
+ return;
795
+ }
742
796
  m.set(file.name, {
743
797
  decoded: null,
744
798
  zipEntry: {
@@ -762,17 +816,32 @@ var loadZip = async (buf, bookId) => {
762
816
  }
763
817
  });
764
818
  };
765
- unzipper.push(buf);
819
+ const chunkSize = 32 * 1024 * 1024;
820
+ const nChunks = Math.max(1, Math.floor(buf.byteLength / chunkSize));
821
+ const sleep = new Promise((resolve) => setTimeout(resolve, 0));
822
+ for (let i = 0; i < nChunks - 1; ++i) {
823
+ unzipper.push(buf.slice(i * chunkSize, (i + 1) * chunkSize));
824
+ await sleep;
825
+ }
826
+ unzipper.push(buf.slice((nChunks - 1) * chunkSize, buf.byteLength), true);
766
827
  BOOKS[bookId] = m;
767
828
  return [...m.keys()];
768
829
  };
769
830
 
770
- // src/worker/offlineWorker.ts
771
- var CHECK_UPDATES = false;
772
- var fetchZipFile = async (zipUrl) => {
773
- const opfsRoot = await navigator.storage.getDirectory();
831
+ // src/worker/offline-worker.ts
832
+ var CHECK_UPDATES = true;
833
+ var NATIVE = false;
834
+ var LOADED_BOOKS = /* @__PURE__ */ new Set();
835
+ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
774
836
  const fileName = zipUrl.split("/").slice(-1)[0];
775
837
  const readArrayBuffer = async () => {
838
+ if (NATIVE) {
839
+ console.debug("try reading native", fileName);
840
+ const back = await sender.nativeReadFile({ fname: fileName });
841
+ console.debug("done readnig");
842
+ return back;
843
+ }
844
+ const opfsRoot = await navigator.storage.getDirectory();
776
845
  const handle = await opfsRoot.getFileHandle(fileName);
777
846
  const accessHandle = await handle.createSyncAccessHandle();
778
847
  const size = accessHandle.getSize();
@@ -782,6 +851,9 @@ var fetchZipFile = async (zipUrl) => {
782
851
  return arrayBuffer;
783
852
  };
784
853
  try {
854
+ if (forceRefresh) {
855
+ throw new Error("forceRefresh");
856
+ }
785
857
  const buf = await readArrayBuffer();
786
858
  if (CHECK_UPDATES) {
787
859
  const response = await fetch(zipUrl, { method: "HEAD" });
@@ -795,13 +867,37 @@ var fetchZipFile = async (zipUrl) => {
795
867
  }
796
868
  return buf;
797
869
  } catch (e) {
798
- console.debug("opfs db not found, creating it", e, zipUrl);
799
- const arrayBuffer = await fetch(zipUrl).then((res) => res.arrayBuffer());
870
+ if (e.message !== "forceRefresh" && !e.message.includes("refetching")) {
871
+ console.debug("opfs db not found, creating it", e, zipUrl);
872
+ }
873
+ const response = await fetch(zipUrl);
874
+ const contentLength = +response.headers.get("Content-Length");
875
+ const reader = response.body.getReader();
876
+ let position = 0;
877
+ const arrayBuffer = new Uint8Array(contentLength);
878
+ let prevPosition = 0;
879
+ while (true) {
880
+ const { done, value: chunk } = await reader.read();
881
+ if (done) {
882
+ break;
883
+ }
884
+ arrayBuffer.set(chunk, position);
885
+ position += chunk.length;
886
+ if (position - prevPosition > 1e6) {
887
+ prevPosition = position;
888
+ self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: position } });
889
+ }
890
+ }
800
891
  if (arrayBuffer.byteLength < 1e4) {
801
- const error = `likely ${zipUrl} was not fetched`;
892
+ const error = `likely ${zipUrl} was not fetched ${arrayBuffer.byteLength}`;
802
893
  console.debug(error);
803
894
  throw new Error(error);
804
895
  }
896
+ if (NATIVE) {
897
+ await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
898
+ return await readArrayBuffer();
899
+ }
900
+ const opfsRoot = await navigator.storage.getDirectory();
805
901
  const handle = await opfsRoot.getFileHandle(fileName, { create: true });
806
902
  const accessHandle = await handle.createSyncAccessHandle();
807
903
  await accessHandle.truncate(0);
@@ -817,29 +913,46 @@ var slugToFileMeta2 = async ({ slug }) => {
817
913
  return slugToFileMeta({ slug, json });
818
914
  };
819
915
  var PRELOAD_AS_BOOK_ID = "preload";
820
- var loadBook2 = async (bookId) => {
821
- const buf = await fetchZipFile(`https://ci.lls.fr/artefacts/content/production/${bookId}.zip`);
916
+ var isOldZip = (bookId) => typeof bookId === "number";
917
+ var loadBook2 = async ({ id: bookId, forceRefresh }) => {
918
+ if (LOADED_BOOKS.has(bookId)) {
919
+ self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
920
+ return;
921
+ }
922
+ LOADED_BOOKS.add(bookId);
923
+ const buf = await fetchZipFile(`https://ci.lls.fr/artefacts/content/development/${bookId}.zip`, { forceRefresh });
822
924
  try {
823
925
  const entryNames = await loadZip(new Uint8Array(buf), bookId);
824
926
  fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
825
- const json = await readZipEntry({ entryName: `preload/${bookId}.json`, bookId: PRELOAD_AS_BOOK_ID, asJson: true });
826
- loadBook(json);
927
+ if (isOldZip(bookId)) {
928
+ const json = await readZipEntry({ entryName: `preload/${bookId}.json`, bookId: PRELOAD_AS_BOOK_ID, asJson: true });
929
+ loadBook(json);
930
+ }
827
931
  } catch (e) {
828
932
  console.error("failed", e);
829
933
  }
830
934
  };
831
935
  var loadPageContent2 = async ({
832
- bookId,
936
+ bookId: iBookId,
833
937
  chapterId,
834
- pageId
938
+ pageId,
939
+ entryId
835
940
  }) => {
836
- const json = await readZipEntry({ entryName: `${chapterId}/${pageId}.json`, bookId, asJson: true });
941
+ const bookId = entryId ? entryId.bookId : iBookId;
942
+ if (!bookId) {
943
+ throw new Error("missing bookId: expect entryId or bookId");
944
+ }
945
+ const json = await readZipEntry({
946
+ entryName: entryId ? entryId.entryName : `${chapterId}/${pageId}.json`,
947
+ bookId,
948
+ asJson: true
949
+ });
837
950
  return await loadPageContent({ chapterId, json });
838
951
  };
839
952
  var fetchWorker;
840
953
  var initChannel = (__, { ports }) => {
841
954
  fetchWorker = ports[0];
842
- const defaultHandler = makeSyncHandler(fetchWorker, {
955
+ const { canHandle, handle } = makeSyncHandler(fetchWorker, {
843
956
  readZipEntry,
844
957
  loadBook: loadBook2,
845
958
  slugToFileMeta: slugToFileMeta2,
@@ -851,20 +964,45 @@ var initChannel = (__, { ports }) => {
851
964
  self.postMessage({ methodName: "ready" });
852
965
  return;
853
966
  }
854
- defaultHandler(dataObj);
967
+ if (canHandle(dataObj.data?.methodName)) {
968
+ return handle(dataObj);
969
+ }
970
+ console.error("unknown methodName", dataObj.data?.methodName);
855
971
  };
856
972
  fetchWorker.onmessage = fn;
857
973
  };
858
974
  var loadBooks = async () => {
859
- const opfsRoot = await navigator.storage.getDirectory();
860
- const entries = await opfsRoot.values();
975
+ let entries = [];
976
+ if (NATIVE) {
977
+ entries = await sender.nativeListZips();
978
+ } else {
979
+ const opfsRoot = await navigator.storage.getDirectory();
980
+ entries = await opfsRoot.values();
981
+ }
861
982
  for await (const entry of entries) {
983
+ if (/preload/.test(entry.name)) {
984
+ continue;
985
+ }
862
986
  if (/\d+\.zip$/.test(entry.name)) {
863
987
  const bookId = entry.name.match(/\d+/)[0];
864
- await loadBook2(Number.parseInt(bookId, 10));
988
+ await loadBook2({ id: Number.parseInt(bookId, 10) });
989
+ } else if (/zip$/.test(entry.name)) {
990
+ const methodUri = entry.name.replace(".zip", "");
991
+ await loadBook2({ id: methodUri });
992
+ } else {
993
+ console.info(`invalid book entry ${entry.name}`);
865
994
  }
866
995
  }
867
996
  };
997
+ var configure = ({ native, checkUpdates }) => {
998
+ console.info("configuring!!", native, checkUpdates);
999
+ if (typeof native !== "undefined") {
1000
+ NATIVE = !!native;
1001
+ }
1002
+ if (typeof native !== "undefined") {
1003
+ CHECK_UPDATES = !!checkUpdates;
1004
+ }
1005
+ };
868
1006
  var loadPreload2 = async () => {
869
1007
  const zipUrl = "https://lls-ci-fr.s3.eu-west-3.amazonaws.com/artefacts/content/development/preload.zip";
870
1008
  const buf = await fetchZipFile(zipUrl);
@@ -877,9 +1015,13 @@ var loadPreload2 = async () => {
877
1015
  console.error("failed", e);
878
1016
  }
879
1017
  };
880
- self.onmessage = makeSyncHandler(self, {
1018
+ var mainHandle = makeSyncHandler(self, {
881
1019
  initChannel,
882
1020
  loadPreload: loadPreload2,
883
1021
  readZipEntry,
884
- loadBooks
1022
+ loadBooks,
1023
+ loadBook: loadBook2,
1024
+ configure
885
1025
  });
1026
+ var { handler, ...sender } = makeSyncWorker(self, ["nativeWriteFile", "nativeReadFile", "nativeListZips"]);
1027
+ self.onmessage = combineHandlers(mainHandle, handler);
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "@lls/offline"
package/lib/index.js CHANGED
@@ -1,319 +1,8 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __commonJS = (cb, mod) => function __require() {
8
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
- // If the importer is in node compatibility mode or this is not an ESM
20
- // file that has been converted to a CommonJS file using a Babel-
21
- // compatible transform (i.e. "__esModule" has not been set), then set
22
- // "default" to the CommonJS "module.exports" for node compatibility.
23
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
- mod
25
- ));
26
-
27
- // ../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
28
- var require_react_production_min = __commonJS({
29
- "../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js"(exports) {
30
- "use strict";
31
- var l = Symbol.for("react.element");
32
- var n = Symbol.for("react.portal");
33
- var p = Symbol.for("react.fragment");
34
- var q = Symbol.for("react.strict_mode");
35
- var r = Symbol.for("react.profiler");
36
- var t = Symbol.for("react.provider");
37
- var u = Symbol.for("react.context");
38
- var v = Symbol.for("react.forward_ref");
39
- var w = Symbol.for("react.suspense");
40
- var x = Symbol.for("react.memo");
41
- var y = Symbol.for("react.lazy");
42
- var z = Symbol.iterator;
43
- function A(a2) {
44
- if (null === a2 || "object" !== typeof a2) return null;
45
- a2 = z && a2[z] || a2["@@iterator"];
46
- return "function" === typeof a2 ? a2 : null;
47
- }
48
- var B = { isMounted: function() {
49
- return false;
50
- }, enqueueForceUpdate: function() {
51
- }, enqueueReplaceState: function() {
52
- }, enqueueSetState: function() {
53
- } };
54
- var C = Object.assign;
55
- var D = {};
56
- function E(a2, b, e) {
57
- this.props = a2;
58
- this.context = b;
59
- this.refs = D;
60
- this.updater = e || B;
61
- }
62
- E.prototype.isReactComponent = {};
63
- E.prototype.setState = function(a2, b) {
64
- if ("object" !== typeof a2 && "function" !== typeof a2 && null != a2) throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");
65
- this.updater.enqueueSetState(this, a2, b, "setState");
66
- };
67
- E.prototype.forceUpdate = function(a2) {
68
- this.updater.enqueueForceUpdate(this, a2, "forceUpdate");
69
- };
70
- function F() {
71
- }
72
- F.prototype = E.prototype;
73
- function G(a2, b, e) {
74
- this.props = a2;
75
- this.context = b;
76
- this.refs = D;
77
- this.updater = e || B;
78
- }
79
- var H = G.prototype = new F();
80
- H.constructor = G;
81
- C(H, E.prototype);
82
- H.isPureReactComponent = true;
83
- var I = Array.isArray;
84
- var J = Object.prototype.hasOwnProperty;
85
- var K = { current: null };
86
- var L = { key: true, ref: true, __self: true, __source: true };
87
- function M(a2, b, e) {
88
- var d, c = {}, k = null, h = null;
89
- if (null != b) for (d in void 0 !== b.ref && (h = b.ref), void 0 !== b.key && (k = "" + b.key), b) J.call(b, d) && !L.hasOwnProperty(d) && (c[d] = b[d]);
90
- var g = arguments.length - 2;
91
- if (1 === g) c.children = e;
92
- else if (1 < g) {
93
- for (var f = Array(g), m = 0; m < g; m++) f[m] = arguments[m + 2];
94
- c.children = f;
95
- }
96
- if (a2 && a2.defaultProps) for (d in g = a2.defaultProps, g) void 0 === c[d] && (c[d] = g[d]);
97
- return { $$typeof: l, type: a2, key: k, ref: h, props: c, _owner: K.current };
98
- }
99
- function N(a2, b) {
100
- return { $$typeof: l, type: a2.type, key: b, ref: a2.ref, props: a2.props, _owner: a2._owner };
101
- }
102
- function O(a2) {
103
- return "object" === typeof a2 && null !== a2 && a2.$$typeof === l;
104
- }
105
- function escape(a2) {
106
- var b = { "=": "=0", ":": "=2" };
107
- return "$" + a2.replace(/[=:]/g, function(a3) {
108
- return b[a3];
109
- });
110
- }
111
- var P = /\/+/g;
112
- function Q(a2, b) {
113
- return "object" === typeof a2 && null !== a2 && null != a2.key ? escape("" + a2.key) : b.toString(36);
114
- }
115
- function R(a2, b, e, d, c) {
116
- var k = typeof a2;
117
- if ("undefined" === k || "boolean" === k) a2 = null;
118
- var h = false;
119
- if (null === a2) h = true;
120
- else switch (k) {
121
- case "string":
122
- case "number":
123
- h = true;
124
- break;
125
- case "object":
126
- switch (a2.$$typeof) {
127
- case l:
128
- case n:
129
- h = true;
130
- }
131
- }
132
- if (h) return h = a2, c = c(h), a2 = "" === d ? "." + Q(h, 0) : d, I(c) ? (e = "", null != a2 && (e = a2.replace(P, "$&/") + "/"), R(c, b, e, "", function(a3) {
133
- return a3;
134
- })) : null != c && (O(c) && (c = N(c, e + (!c.key || h && h.key === c.key ? "" : ("" + c.key).replace(P, "$&/") + "/") + a2)), b.push(c)), 1;
135
- h = 0;
136
- d = "" === d ? "." : d + ":";
137
- if (I(a2)) for (var g = 0; g < a2.length; g++) {
138
- k = a2[g];
139
- var f = d + Q(k, g);
140
- h += R(k, b, e, f, c);
141
- }
142
- else if (f = A(a2), "function" === typeof f) for (a2 = f.call(a2), g = 0; !(k = a2.next()).done; ) k = k.value, f = d + Q(k, g++), h += R(k, b, e, f, c);
143
- else if ("object" === k) throw b = String(a2), Error("Objects are not valid as a React child (found: " + ("[object Object]" === b ? "object with keys {" + Object.keys(a2).join(", ") + "}" : b) + "). If you meant to render a collection of children, use an array instead.");
144
- return h;
145
- }
146
- function S(a2, b, e) {
147
- if (null == a2) return a2;
148
- var d = [], c = 0;
149
- R(a2, d, "", "", function(a3) {
150
- return b.call(e, a3, c++);
151
- });
152
- return d;
153
- }
154
- function T(a2) {
155
- if (-1 === a2._status) {
156
- var b = a2._result;
157
- b = b();
158
- b.then(function(b2) {
159
- if (0 === a2._status || -1 === a2._status) a2._status = 1, a2._result = b2;
160
- }, function(b2) {
161
- if (0 === a2._status || -1 === a2._status) a2._status = 2, a2._result = b2;
162
- });
163
- -1 === a2._status && (a2._status = 0, a2._result = b);
164
- }
165
- if (1 === a2._status) return a2._result.default;
166
- throw a2._result;
167
- }
168
- var U = { current: null };
169
- var V = { transition: null };
170
- var W = { ReactCurrentDispatcher: U, ReactCurrentBatchConfig: V, ReactCurrentOwner: K };
171
- function X() {
172
- throw Error("act(...) is not supported in production builds of React.");
173
- }
174
- exports.Children = { map: S, forEach: function(a2, b, e) {
175
- S(a2, function() {
176
- b.apply(this, arguments);
177
- }, e);
178
- }, count: function(a2) {
179
- var b = 0;
180
- S(a2, function() {
181
- b++;
182
- });
183
- return b;
184
- }, toArray: function(a2) {
185
- return S(a2, function(a3) {
186
- return a3;
187
- }) || [];
188
- }, only: function(a2) {
189
- if (!O(a2)) throw Error("React.Children.only expected to receive a single React element child.");
190
- return a2;
191
- } };
192
- exports.Component = E;
193
- exports.Fragment = p;
194
- exports.Profiler = r;
195
- exports.PureComponent = G;
196
- exports.StrictMode = q;
197
- exports.Suspense = w;
198
- exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = W;
199
- exports.act = X;
200
- exports.cloneElement = function(a2, b, e) {
201
- if (null === a2 || void 0 === a2) throw Error("React.cloneElement(...): The argument must be a React element, but you passed " + a2 + ".");
202
- var d = C({}, a2.props), c = a2.key, k = a2.ref, h = a2._owner;
203
- if (null != b) {
204
- void 0 !== b.ref && (k = b.ref, h = K.current);
205
- void 0 !== b.key && (c = "" + b.key);
206
- if (a2.type && a2.type.defaultProps) var g = a2.type.defaultProps;
207
- for (f in b) J.call(b, f) && !L.hasOwnProperty(f) && (d[f] = void 0 === b[f] && void 0 !== g ? g[f] : b[f]);
208
- }
209
- var f = arguments.length - 2;
210
- if (1 === f) d.children = e;
211
- else if (1 < f) {
212
- g = Array(f);
213
- for (var m = 0; m < f; m++) g[m] = arguments[m + 2];
214
- d.children = g;
215
- }
216
- return { $$typeof: l, type: a2.type, key: c, ref: k, props: d, _owner: h };
217
- };
218
- exports.createContext = function(a2) {
219
- a2 = { $$typeof: u, _currentValue: a2, _currentValue2: a2, _threadCount: 0, Provider: null, Consumer: null, _defaultValue: null, _globalName: null };
220
- a2.Provider = { $$typeof: t, _context: a2 };
221
- return a2.Consumer = a2;
222
- };
223
- exports.createElement = M;
224
- exports.createFactory = function(a2) {
225
- var b = M.bind(null, a2);
226
- b.type = a2;
227
- return b;
228
- };
229
- exports.createRef = function() {
230
- return { current: null };
231
- };
232
- exports.forwardRef = function(a2) {
233
- return { $$typeof: v, render: a2 };
234
- };
235
- exports.isValidElement = O;
236
- exports.lazy = function(a2) {
237
- return { $$typeof: y, _payload: { _status: -1, _result: a2 }, _init: T };
238
- };
239
- exports.memo = function(a2, b) {
240
- return { $$typeof: x, type: a2, compare: void 0 === b ? null : b };
241
- };
242
- exports.startTransition = function(a2) {
243
- var b = V.transition;
244
- V.transition = {};
245
- try {
246
- a2();
247
- } finally {
248
- V.transition = b;
249
- }
250
- };
251
- exports.unstable_act = X;
252
- exports.useCallback = function(a2, b) {
253
- return U.current.useCallback(a2, b);
254
- };
255
- exports.useContext = function(a2) {
256
- return U.current.useContext(a2);
257
- };
258
- exports.useDebugValue = function() {
259
- };
260
- exports.useDeferredValue = function(a2) {
261
- return U.current.useDeferredValue(a2);
262
- };
263
- exports.useEffect = function(a2, b) {
264
- return U.current.useEffect(a2, b);
265
- };
266
- exports.useId = function() {
267
- return U.current.useId();
268
- };
269
- exports.useImperativeHandle = function(a2, b, e) {
270
- return U.current.useImperativeHandle(a2, b, e);
271
- };
272
- exports.useInsertionEffect = function(a2, b) {
273
- return U.current.useInsertionEffect(a2, b);
274
- };
275
- exports.useLayoutEffect = function(a2, b) {
276
- return U.current.useLayoutEffect(a2, b);
277
- };
278
- exports.useMemo = function(a2, b) {
279
- return U.current.useMemo(a2, b);
280
- };
281
- exports.useReducer = function(a2, b, e) {
282
- return U.current.useReducer(a2, b, e);
283
- };
284
- exports.useRef = function(a2) {
285
- return U.current.useRef(a2);
286
- };
287
- exports.useState = function(a2) {
288
- return U.current.useState(a2);
289
- };
290
- exports.useSyncExternalStore = function(a2, b, e) {
291
- return U.current.useSyncExternalStore(a2, b, e);
292
- };
293
- exports.useTransition = function() {
294
- return U.current.useTransition();
295
- };
296
- exports.version = "18.3.1";
297
- }
298
- });
299
-
300
- // ../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js
301
- var require_react = __commonJS({
302
- "../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js"(exports, module) {
303
- "use strict";
304
- if (true) {
305
- module.exports = require_react_production_min();
306
- } else {
307
- module.exports = null;
308
- }
309
- }
310
- });
311
-
312
1
  // src/utils/worker.ts
313
- var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
2
+ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
314
3
  const dic = {};
315
4
  const cbks = {};
316
- const fn2 = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
5
+ const fn = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
317
6
  if (eventId && eventId in dic) {
318
7
  if (error) {
319
8
  dic[eventId].reject(error);
@@ -327,46 +16,69 @@ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
327
16
  }
328
17
  methodName && cbks[methodName]?.(arg);
329
18
  };
330
- offlineWorker.onmessage = fn2;
331
19
  const makeFn = (methodName) => {
332
20
  return ({ ports, ...arg } = {}) => {
333
21
  return new Promise((resolve, reject) => {
334
22
  const eventId = `${methodName}:${Date.now()}`;
335
23
  dic[eventId] = { resolve, reject };
336
- offlineWorker.postMessage({ eventId, methodName, arg }, ports);
24
+ offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
337
25
  });
338
26
  };
339
27
  };
340
28
  const obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
341
- on: (key, fn3) => {
342
- cbks[key] = fn3;
29
+ on: (key, fn2) => {
30
+ cbks[key] = fn2;
343
31
  }
344
32
  });
345
- return obj;
33
+ return {
34
+ ...obj,
35
+ handler: {
36
+ canHandle: (methodName) => {
37
+ return methodName in cbks || methodNames.includes(methodName);
38
+ },
39
+ handle: fn
40
+ }
41
+ };
346
42
  };
347
43
  var a = (a2) => a2;
348
44
  var makeSyncHandler = a(
349
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
350
- try {
351
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
352
- const results = await services[methodName](arg, { ports });
353
- sender.postMessage({ eventId, methodName, results });
354
- return;
45
+ (sender, services) => ({
46
+ canHandle: (methodName) => {
47
+ return methodName in services;
48
+ },
49
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
50
+ try {
51
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
52
+ const results = await services[methodName](arg, { ports });
53
+ sender.postMessage({ eventId, methodName, results });
54
+ return;
55
+ }
56
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
57
+ } catch (e) {
58
+ console.debug("failed", e);
59
+ sender.postMessage({ eventId, methodName, error: e });
355
60
  }
356
- throw new Error(`invalid methodName ${String(methodName)}`);
357
- } catch (e) {
358
- console.debug("failed", e);
359
- sender.postMessage({ eventId, methodName, error: e });
360
61
  }
361
- }
62
+ })
362
63
  );
64
+ var combineHandlers = (...handlers) => {
65
+ return ({ data: { methodName, ...data }, ports }) => {
66
+ for (const h of handlers) {
67
+ if (h.canHandle(methodName)) {
68
+ return h.handle({ data: { methodName, ...data }, ports });
69
+ }
70
+ }
71
+ console.error("no handlers matched", methodName);
72
+ };
73
+ };
363
74
 
364
75
  // src/hooks/useOfflineWorker.ts
365
- var import_react = __toESM(require_react(), 1);
366
- var offlineWorkerSync = makeSyncWorker(
76
+ import { useCallback, useEffect, useState } from "react";
77
+ var offlineWorker = new Worker("/src/worker/offline-worker.js", { type: "module" });
78
+ var { handler, ...offlineWorkerSync } = makeSyncWorker(
367
79
  // @ts-ignore: TODO
368
- new Worker("/src/worker/offlineWorker.js", { type: "module" }),
369
- ["initChannel", "readZipEntry", "loadPreload", "loadBooks"],
80
+ offlineWorker,
81
+ ["initChannel", "readZipEntry", "configure", "loadPreload", "loadBooks", "loadBook"],
370
82
  "app:offlineWorker"
371
83
  );
372
84
  var setWorkersReady;
@@ -377,54 +89,67 @@ var setBooksLoaded;
377
89
  var setBooksLoadedPromise = new Promise((resolve) => {
378
90
  setBooksLoaded = resolve;
379
91
  });
380
- var fn = async () => {
92
+ var installWorker = async ({
93
+ skipPreload = false,
94
+ nativeReadFile,
95
+ nativeWriteFile,
96
+ nativeListZips,
97
+ checkUpdates
98
+ } = {}) => {
381
99
  let fetchWorker = null;
382
100
  await navigator.serviceWorker.getRegistrations().then(async (registrations) => {
383
101
  for (const registration of registrations) {
384
102
  await registration.unregister();
385
103
  }
386
104
  });
387
- await navigator.serviceWorker.register("/fetch.worker.js", { type: "module" }).then((reg) => reg.update());
105
+ await navigator.serviceWorker.register("/fetch-worker.js", { type: "module" }).then((reg) => reg.update());
388
106
  await navigator.serviceWorker.ready.then(async (reg) => {
389
107
  console.debug("Service Worker registered:", reg.scope);
390
108
  fetchWorker = reg.active;
391
109
  }).catch((err) => console.error("Service Worker registration failed:", err));
392
110
  if (!fetchWorker) {
111
+ console.debug("failed to get fetch worker");
393
112
  return;
394
113
  }
114
+ if (nativeReadFile) {
115
+ const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
116
+ offlineWorker.onmessage = combineHandlers(handler, wwHandler);
117
+ offlineWorkerSync.configure({ native: true, checkUpdates });
118
+ } else {
119
+ offlineWorkerSync.configure({ checkUpdates });
120
+ offlineWorker.onmessage = handler.handle;
121
+ }
395
122
  offlineWorkerSync.on("ready", setWorkersReady);
396
123
  const channel = new MessageChannel();
397
124
  offlineWorkerSync.initChannel({ ports: [channel.port1] });
398
125
  fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
399
- await offlineWorkerSync.loadPreload();
126
+ offlineWorkerSync.on("opfsProgress", () => {
127
+ });
128
+ if (!skipPreload) {
129
+ await offlineWorkerSync.loadPreload();
130
+ }
400
131
  await offlineWorkerSync.loadBooks();
401
132
  setBooksLoaded();
402
133
  };
403
- fn();
404
- var useOfflineWorker_default = () => {
405
- const [ready, setReady] = (0, import_react.useState)(false);
406
- (0, import_react.useEffect)(() => {
407
- const fn2 = async () => {
134
+ var useOfflineWorker_default = ({ skipPreload = false, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates } = {}) => {
135
+ const [ready, setReady] = useState(false);
136
+ useEffect(() => {
137
+ const fn = async () => {
138
+ await installWorker({ skipPreload, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates });
408
139
  await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
409
140
  setReady(true);
410
141
  };
411
- fn2();
142
+ fn();
143
+ }, [skipPreload, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates]);
144
+ const loadBook = useCallback(async ({ id, forceRefresh }, cbk) => {
145
+ offlineWorkerSync.on("opfsProgress", cbk);
146
+ return await offlineWorkerSync.loadBook({ id, forceRefresh });
412
147
  }, []);
413
- return ready;
148
+ return {
149
+ ready,
150
+ loadBook
151
+ };
414
152
  };
415
153
  export {
416
154
  useOfflineWorker_default as useOfflineWorker
417
155
  };
418
- /*! Bundled license information:
419
-
420
- react/cjs/react.production.min.js:
421
- (**
422
- * @license React
423
- * react.production.min.js
424
- *
425
- * Copyright (c) Facebook, Inc. and its affiliates.
426
- *
427
- * This source code is licensed under the MIT license found in the
428
- * LICENSE file in the root directory of this source tree.
429
- *)
430
- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lls/offline",
3
- "version": "24.20.0-debug",
3
+ "version": "24.20.0-debug2",
4
4
  "description": "offline",
5
5
  "main": "./lib/index.js",
6
6
  "module": "./lib/index.js",
@@ -36,10 +36,12 @@
36
36
  "devDependencies": {
37
37
  "@biomejs/biome": "1.9.4",
38
38
  "@happy-dom/global-registrator": "15.11.0",
39
+ "@ladle/react": "5.0.3",
39
40
  "@testing-library/react": "16.3.0",
40
41
  "@types/react": "18.3.12",
41
42
  "@types/react-dom": "18.3.1",
42
43
  "@vitejs/plugin-react": "4.5.1",
44
+ "@vitejs/plugin-react-swc": "3.10.1",
43
45
  "esbuild": "0.24.0",
44
46
  "happy-dom": "15.11.0",
45
47
  "lodash": "4.17.21",
@@ -48,17 +50,21 @@
48
50
  "react-dom": "18.3.1",
49
51
  "typescript": "5.8.3",
50
52
  "vite": "7.0.0",
53
+ "vite-tsconfig-paths": "5.1.4",
51
54
  "zod": "3.25.50",
52
- "@lls/vitescripts": "24.20.0"
55
+ "@lls/vitescripts": "24.22.0"
53
56
  },
54
57
  "dependencies": {
55
58
  "fflate": "0.8.2"
56
59
  },
57
60
  "scripts": {
58
- "start": "echo no devserver",
61
+ "start": "pnpm ladle serve",
59
62
  "dts": "pnpm tsc --project tsconfig.production.json",
60
- "build": "node build",
61
- "test": "bun test --bail",
62
- "lint": "pnpm biome check --apply src __tests__"
63
+ "build": "NODE_ENV=production node build.js",
64
+ "deploy-ladle": "PUBLISH=1 pnpm ladle build",
65
+ "deploy-ladle-pr": "pnpm ladle build && s5cmd sync --delete --acl public-read --destination-region eu-west-1 'dist/*' s3://build.lelivrescolaire.fr/storybook/viewer-pr-$PR_NUMBER/",
66
+ "test": "bun test --bail __tests__/unit __tests__/e2e",
67
+ "lint": "pnpm biome check --apply src __tests__ stories",
68
+ "precommit": "pnpm lint"
63
69
  }
64
70
  }