@swoff/cli 0.3.5 → 0.3.6

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.
Files changed (91) hide show
  1. package/README.md +327 -25
  2. package/dist/__tests__/assemble-sw.test.js +15 -1
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  4. package/dist/__tests__/cli-integration.test.js +9 -8
  5. package/dist/__tests__/cli-integration.test.js.map +1 -1
  6. package/dist/__tests__/config-types.test.js +6 -6
  7. package/dist/__tests__/config-types.test.js.map +1 -1
  8. package/dist/__tests__/file-generators.test.js +67 -59
  9. package/dist/__tests__/file-generators.test.js.map +1 -1
  10. package/dist/__tests__/loader.test.js +12 -13
  11. package/dist/__tests__/loader.test.js.map +1 -1
  12. package/dist/__tests__/validator.test.js +41 -21
  13. package/dist/__tests__/validator.test.js.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/cli/help.js +3 -3
  17. package/dist/lib/cli/help.js.map +1 -1
  18. package/dist/lib/commands/add.js +7 -1
  19. package/dist/lib/commands/add.js.map +1 -1
  20. package/dist/lib/commands/clean.js +5 -0
  21. package/dist/lib/commands/clean.js.map +1 -1
  22. package/dist/lib/commands/generate-guide.js +12 -9
  23. package/dist/lib/commands/generate-guide.js.map +1 -1
  24. package/dist/lib/commands/generate.js +7 -3
  25. package/dist/lib/commands/generate.js.map +1 -1
  26. package/dist/lib/commands/info.js +135 -32
  27. package/dist/lib/commands/info.js.map +1 -1
  28. package/dist/lib/commands/validate.js +3 -1
  29. package/dist/lib/commands/validate.js.map +1 -1
  30. package/dist/lib/config/validator.js +25 -16
  31. package/dist/lib/config/validator.js.map +1 -1
  32. package/dist/lib/generators/file-generators/auth-fetch.js +29 -34
  33. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  34. package/dist/lib/generators/file-generators/auth-state.js +8 -5
  35. package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
  36. package/dist/lib/generators/file-generators/auth-store.js +71 -40
  37. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  38. package/dist/lib/generators/file-generators/auth-user.js +27 -31
  39. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  40. package/dist/lib/generators/file-generators/background-sync.js +9 -5
  41. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  42. package/dist/lib/generators/file-generators/cache.js +7 -2
  43. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  44. package/dist/lib/generators/file-generators/client-injector.js +113 -0
  45. package/dist/lib/generators/file-generators/client-injector.js.map +1 -0
  46. package/dist/lib/generators/file-generators/context.js +7 -6
  47. package/dist/lib/generators/file-generators/context.js.map +1 -1
  48. package/dist/lib/generators/file-generators/fetch-wrapper.js +170 -40
  49. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  50. package/dist/lib/generators/file-generators/generate-hooks.js +61 -3
  51. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  52. package/dist/lib/generators/file-generators/guide-generator.js +333 -0
  53. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -0
  54. package/dist/lib/generators/file-generators/invalidation-tags.js +12 -10
  55. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  56. package/dist/lib/generators/file-generators/mutation-queue.js +34 -84
  57. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  58. package/dist/lib/generators/file-generators/mutation-reconcile.js +57 -0
  59. package/dist/lib/generators/file-generators/mutation-reconcile.js.map +1 -0
  60. package/dist/lib/generators/file-generators/pwa-install.js +41 -9
  61. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  62. package/dist/lib/generators/file-generators/quick-readme.js +78 -0
  63. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -0
  64. package/dist/lib/generators/file-generators/reconcile.js +7 -12
  65. package/dist/lib/generators/file-generators/reconcile.js.map +1 -1
  66. package/dist/lib/generators/file-generators/store.js +30 -20
  67. package/dist/lib/generators/file-generators/store.js.map +1 -1
  68. package/dist/lib/generators/file-generators/sw-generator-build.js +37 -16
  69. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  70. package/dist/lib/generators/file-generators/sw-injector.js +81 -110
  71. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  72. package/dist/lib/generators/file-generators/sw-template.js +1 -1
  73. package/dist/lib/generators/file-generators/type-definitions.js +33 -10
  74. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  75. package/dist/lib/generators/sw-generator.js +19 -10
  76. package/dist/lib/generators/sw-generator.js.map +1 -1
  77. package/dist/lib/generators/sw-sections/assemble-sw.js +32 -12
  78. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  79. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  80. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  81. package/dist/lib/generators/sw-sections/fetch-handler.js +25 -10
  82. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  83. package/dist/lib/generators/sw-sections/tag-management.js +25 -5
  84. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  85. package/dist/lib/generators/swoff-files-generator.js +7 -5
  86. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  87. package/dist/lib/shared/config-types.js +21 -6
  88. package/dist/lib/shared/config-types.js.map +1 -1
  89. package/dist/lib/utils/build-script.js +35 -0
  90. package/dist/lib/utils/build-script.js.map +1 -0
  91. package/package.json +1 -1
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Generates client-injector.{js|ts} - orchestrator that wires feature modules together.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateClientInjector(ctx) {
6
+ const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const pwaEnabled = ctx.config.features.pwa.enabled;
9
+ const mutationQueueEnabled = ctx.config.features.mutationQueue;
10
+ const crossTabSync = ctx.config.features.crossTabSync;
11
+ const clientRegistration = ctx.config.features.clientRegistration;
12
+ const pwaImport = pwaEnabled
13
+ ? `import { setupPwaInstall } from "./pwa/install.${ext}";
14
+ `
15
+ : "";
16
+ const pwaCall = pwaEnabled ? `setupPwaInstall();\n` : "";
17
+ const mutationImport = mutationQueueEnabled
18
+ ? `import { processMutationQueue } from "./mutation-queue.${ext}";
19
+ `
20
+ : "";
21
+ const mutationOnlineListener = mutationQueueEnabled
22
+ ? `
23
+ // --- Mutation Queue Online Listener ---
24
+ window.addEventListener("online", processMutationQueue);
25
+ `
26
+ : "";
27
+ const crossTabHandler = crossTabSync
28
+ ? `
29
+ if (event.data.type === "TAG_INVALIDATED" && event.data.tag) {
30
+ window.dispatchEvent(
31
+ new CustomEvent("cache-invalidated", {
32
+ detail: { tags: [event.data.tag] },
33
+ })
34
+ );
35
+ }
36
+ `
37
+ : "";
38
+ const swImport = clientRegistration
39
+ ? `import {
40
+ initServiceWorker as swInit,
41
+ handleUpdateApproved,
42
+ skipWaiting,
43
+ } from "./sw/injector.${ext}";
44
+ `
45
+ : "";
46
+ const swExports = clientRegistration
47
+ ? `
48
+ export { handleUpdateApproved, skipWaiting };
49
+ `
50
+ : "";
51
+ const code = `/**
52
+ * Swoff Client Injector
53
+ * Orchestrates SW registration, PWA install, and cross-tab sync.
54
+ * Single entry point for all Swoff client-side features.
55
+ *
56
+ * Usage:
57
+ * import { initServiceWorker } from './swoff/client-injector.${ext}';
58
+ * initServiceWorker();
59
+ *
60
+ * Window events (dispatched by this module):
61
+ * sw-progress - Download progress (detail: { percent, downloaded, total })
62
+ * sw-ready - SW active and controlling page
63
+ * sw-error - SW registration failed
64
+ * cache-invalidated - Cache entries cleared (detail: { tags })
65
+ * mutation-sync-complete - Queued mutations synced (detail: { succeeded, failed })
66
+ * mutation-queue-changed - Queue modified
67
+ *
68
+ * Window events (dispatched by feature modules):
69
+ * pwa-installable - PWA can be installed (detail: { isInstallable: true })
70
+ * pwa-installed - User accepted install (detail: { outcome: 'accepted' })
71
+ * sw-auth-unauthorized - 401 response received
72
+ * sw-auth-state-change - Login or logout (detail: { authenticated: boolean })
73
+ * mutation-rollback - Mutation exhausted retries
74
+ * sw-update-available - New version ready (detail: { version })
75
+ * sw-version-detected - Version info available
76
+ */
77
+ ${pwaImport}${mutationImport}${swImport}
78
+ ${pwaCall}${mutationOnlineListener}
79
+ // --- SW Message Listener ---
80
+ if (typeof window !== "undefined" && "serviceWorker" in navigator) {
81
+ navigator.serviceWorker.addEventListener("message", (event) => {
82
+ if (event.data.type === "SW_PROGRESS") {
83
+ const { percent, downloaded, total } = event.data;
84
+ window.dispatchEvent(
85
+ new CustomEvent("sw-progress", {
86
+ detail: { percent, downloaded, total },
87
+ })
88
+ );
89
+ }
90
+ if (event.data.type === "BACKGROUND_SYNC_COMPLETE") {
91
+ const { succeeded, failed, tags } = event.data.detail;
92
+ window.dispatchEvent(
93
+ new CustomEvent("mutation-sync-complete", {
94
+ detail: { succeeded, failed },
95
+ })
96
+ );
97
+ if (tags && tags.length > 0) {
98
+ window.dispatchEvent(
99
+ new CustomEvent("cache-invalidated", { detail: { tags } })
100
+ );
101
+ }
102
+ window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
103
+ }${crossTabHandler} });
104
+ }
105
+
106
+ /** Initialize SW registration and all client-side features (PWA install, mutation queue, cross-tab sync). Call once at app startup. */
107
+ export async function initServiceWorker()${ts ? ": Promise<void>" : " "}{
108
+ ${clientRegistration ? " await swInit();" : " // No SW registration configured"}
109
+ }${swExports}
110
+ `;
111
+ writeFile(ctx, `client-injector.${ext}`, code);
112
+ }
113
+ //# sourceMappingURL=client-injector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-injector.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/client-injector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,sBAAsB,CAAC,GAAqB;IAC1D,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;IACnD,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;IAC/D,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;IACtD,MAAM,kBAAkB,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAElE,MAAM,SAAS,GAAG,UAAU;QAC1B,CAAC,CAAC,kDAAkD,GAAG;CAC1D;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;IAEzD,MAAM,cAAc,GAAG,oBAAoB;QACzC,CAAC,CAAC,0DAA0D,GAAG;CAClE;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,sBAAsB,GAAG,oBAAoB;QACjD,CAAC,CAAC;;;CAGL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,eAAe,GAAG,YAAY;QAClC,CAAC,CAAC;;;;;;;;CAQL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,QAAQ,GAAG,kBAAkB;QACjC,CAAC,CAAC;;;;wBAIkB,GAAG;CAC1B;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,SAAS,GAAG,kBAAkB;QAClC,CAAC,CAAC;;CAEL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,IAAI,GAAG;;;;;;kEAMmD,GAAG;;;;;;;;;;;;;;;;;;;;EAoBnE,SAAS,GAAG,cAAc,GAAG,QAAQ;EACrC,OAAO,GAAG,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;OAyB3B,eAAe;;;;2CAIqB,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG;EACrE,kBAAkB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,oCAAoC;GAC9E,SAAS;CACX,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACjD,CAAC"}
@@ -3,14 +3,15 @@
3
3
  * Contains config, paths, and the generated files tracker.
4
4
  */
5
5
  import { existsSync, mkdirSync, writeFileSync } from "fs";
6
- import { join } from "path";
7
- export function ensureSwoffDir(ctx) {
8
- if (!existsSync(ctx.swoffDir))
9
- mkdirSync(ctx.swoffDir, { recursive: true });
6
+ import { join, dirname } from "path";
7
+ export function ensureDir(dir) {
8
+ if (!existsSync(dir))
9
+ mkdirSync(dir, { recursive: true });
10
10
  }
11
11
  export function writeFile(ctx, filename, code) {
12
- ensureSwoffDir(ctx);
13
- writeFileSync(join(ctx.swoffDir, filename), code);
12
+ const fullPath = join(ctx.swoffDir, filename);
13
+ ensureDir(dirname(fullPath));
14
+ writeFileSync(fullPath, code);
14
15
  ctx.generatedFiles.push(`swoff/${filename}`);
15
16
  }
16
17
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAY5B,MAAM,UAAU,cAAc,CAAC,GAAqB;IAClD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAqB,EAAE,QAAgB,EAAE,IAAY;IAC7E,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;IAClD,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAYrC,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAqB,EAAE,QAAgB,EAAE,IAAY;IAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9C,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7B,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9B,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;AAC/C,CAAC"}
@@ -1,79 +1,209 @@
1
1
  /**
2
- * Generates fetch-wrapper.js - fetch with cache strategy headers and deduplication.
2
+ * Generates fetch-wrapper.ts/js unified fetch with caching, auth, offline queue, and auto-invalidation.
3
3
  */
4
4
  import { writeFile } from "./context.js";
5
5
  export function generateFetchWrapper(ctx) {
6
6
  const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const T = (type) => (ts ? `: ${type}` : "");
9
+ const R = (type) => (ts ? `: ${type} ` : " ");
10
+ const G = (type) => (ts ? `<${type}>` : "");
11
+ const tagInvalidation = ctx.config.features.tagInvalidation;
12
+ const authEnabled = ctx.config.features.auth.enabled;
13
+ const mutationQueue = ctx.config.features.mutationQueue;
14
+ const importLines = [
15
+ tagInvalidation
16
+ ? `import { generateTags } from "./invalidation-tags.${ext}";`
17
+ : "",
18
+ tagInvalidation ? `import { invalidateByTags } from "./cache.${ext}";` : "",
19
+ authEnabled
20
+ ? `import { getAuth, clearAuth } from "./auth/store.${ext}";`
21
+ : "",
22
+ mutationQueue
23
+ ? `import { queueMutation } from "./mutation-queue.${ext}";`
24
+ : "",
25
+ ]
26
+ .filter(Boolean)
27
+ .join("\n");
28
+ const interfaceBlock = ts
29
+ ? `
30
+ export interface FetchWithCacheResult {
31
+ response${T("Response")};
32
+ fromCache${T("boolean")};
33
+ }
34
+ `
35
+ : "";
36
+ const optionsType = ts ? `FetchWithCacheOptions` : `options`;
37
+ const optionsInterface = ts
38
+ ? `
39
+ export interface FetchWithCacheOptions extends RequestInit {
40
+ tags?: string[];
41
+ staleWhileRevalidate?: boolean;
42
+ auth?: boolean;
43
+ queueOffline?: boolean;
44
+ invalidate?: 'auto' | string[] | false;
45
+ }
46
+ `
47
+ : "";
48
+ const authBlock = authEnabled
49
+ ? `
50
+ if (options.auth) {
51
+ const auth = await getAuth();
52
+ if (auth?.token) {
53
+ headers.set("Authorization", \`Bearer \${auth.token}\`);
54
+ }
55
+ }`
56
+ : "";
57
+ const authUrlsBlock = authEnabled
58
+ ? `
59
+ // Auth endpoints bypass SW cache
60
+ const authPaths = ["/login", "/logout", "/register"];
61
+ if (options.auth && authPaths.some((p) => url.includes(p)) && !headers.has("X-SW-Cache-Strategy")) {
62
+ headers.set("X-SW-Cache-Strategy", "mutation");
63
+ }`
64
+ : "";
65
+ const auth401Block = authEnabled
66
+ ? `
67
+ if (options.auth && response.status === 401) {
68
+ await clearAuth();
69
+ window.dispatchEvent(new CustomEvent("sw-auth-unauthorized"));
70
+ }`
71
+ : "";
72
+ const offlineReadBlock = ` if (isRead) {
73
+ const cached = await caches.match(input);
74
+ if (cached) return { response: cached, fromCache: true };
75
+ throw new Error("Offline: no cached data available");
76
+ }`;
77
+ const offlineWriteBlock = mutationQueue
78
+ ? ` // Write offline — auto-queue
79
+ if (options.queueOffline !== false) {
80
+ const headerObj: Record<string, string> = {};
81
+ headers.forEach((value, key) => {
82
+ headerObj[key] = value;
83
+ });
84
+ await queueMutation({
85
+ method,
86
+ url,
87
+ body: options.body,
88
+ headers: headerObj,
89
+ tags: options.tags || [],
90
+ timestamp: Date.now(),
91
+ });
92
+ return {
93
+ response: new Response(null, { status: 202, statusText: "Queued" }),
94
+ fromCache: false,
95
+ };
96
+ }
97
+ throw new Error("Offline: write operation failed");`
98
+ : ` throw new Error("Offline: write operation failed");`;
99
+ const autoInvalidateBlock = tagInvalidation
100
+ ? `
101
+ // Auto-invalidate after mutation success
102
+ if (!isRead && response.ok) {
103
+ const invalidateSetting = options.invalidate !== false ? (options.invalidate || 'auto') : false;
104
+ if (invalidateSetting !== false) {
105
+ const tagsToInvalidate${T("string[]")} = Array.isArray(invalidateSetting)
106
+ ? invalidateSetting
107
+ : (options.tags || generateTags(url));
108
+ if (tagsToInvalidate.length > 0) {
109
+ await invalidateByTags(tagsToInvalidate);
110
+ }
111
+ }
112
+ }`
113
+ : "";
114
+ const autoTagsBlock = tagInvalidation
115
+ ? `
116
+ // Auto-generate tags from URL if not provided
117
+ if (!options.tags && isRead) {
118
+ const urlTags = generateTags(url);
119
+ if (urlTags.length > 0) {
120
+ headers.set("X-SW-Cache-Tags", urlTags.join(","));
121
+ }
122
+ }`
123
+ : "";
7
124
  const code = `/**
8
125
  * Swoff Fetch Wrapper
9
- * Framework-agnostic fetch with cache strategy, tags, and query deduplication.
126
+ * Unified fetch with caching, auth, offline queue, and auto-invalidation.
10
127
  *
11
128
  * Usage:
12
129
  * import { fetchWithCache } from './swoff/fetch-wrapper.${ext}';
13
130
  *
14
- * // GET - cached with tag
15
- * const todos = await fetchWithCache("/api/todos", { tags: ["todos"] }).then(r => r.json());
131
+ * // GET cached with auto-generated tags
132
+ * const { response } = await fetchWithCache("/api/todos");
133
+ * const data = await response.json();
16
134
  *
17
- * // POST - mutation (passes through to server)
135
+ * // POST mutation with auto-invalidation
18
136
  * await fetchWithCache("/api/todos", {
19
137
  * method: "POST",
20
138
  * body: JSON.stringify({ title: "New task" }),
21
139
  * });
22
140
  *
23
- * // Stale-while-revalidate
24
- * const data = await fetchWithCache("/api/data", {
141
+ * // Authenticated request
142
+ * const { response: userRes } = await fetchWithCache("/api/me", { auth: true });
143
+ *
144
+ * // Custom tags + stale-while-revalidate
145
+ * const { response: staleRes, fromCache } = await fetchWithCache("/api/data", {
25
146
  * tags: ["data"],
26
147
  * staleWhileRevalidate: true,
27
- * }).then(r => r.json());
148
+ * });
149
+ *
150
+ * // Offline: auto-queues writes (disable with queueOffline: false)
151
+ * await fetchWithCache("/api/todos", {
152
+ * method: "POST",
153
+ * body: JSON.stringify({ title: "Offline task" }),
154
+ * });
155
+ * // When back online, processMutationQueue() replays them
28
156
  */
29
157
 
30
- const inFlightRequests = new Map();
158
+ ${importLines}
159
+ ${interfaceBlock}${optionsInterface}
160
+ const inFlightRequests = new Map${G("string, Promise<Response>")}();
161
+
162
+ /** Fetch with caching, auth, offline queue, and auto-invalidation. Returns { response, fromCache }. */
163
+ export async function fetchWithCache(input${T("RequestInfo")}, options${T("RequestInit & { tags?: string[]; staleWhileRevalidate?: boolean; auth?: boolean; queueOffline?: boolean; invalidate?: 'auto' | string[] | false }")} = {})${R("Promise<FetchWithCacheResult>")}{
164
+ const method = (options.method || "GET").toUpperCase();
165
+ const isRead = method === "GET" || method === "HEAD";
166
+ const url = typeof input === "string" ? input : input.url;
31
167
 
32
- export async function fetchWithCache(input, options = {}) {
33
168
  const headers = new Headers(options.headers);
34
- const method = options.method || "GET";
35
169
 
170
+ // Set cache strategy
36
171
  if (!headers.has("X-SW-Cache-Strategy")) {
37
- headers.set(
38
- "X-SW-Cache-Strategy",
39
- method === "GET" || method === "HEAD" ? "read" : "mutation"
40
- );
172
+ headers.set("X-SW-Cache-Strategy", isRead ? "read" : "mutation");
173
+ }
174
+ ${autoTagsBlock}
175
+ // Custom tags override auto-generated
176
+ if (options.tags && options.tags.length > 0) {
177
+ headers.set("X-SW-Cache-Tags", options.tags.join(","));
41
178
  }
42
179
 
43
180
  if (options.staleWhileRevalidate) {
44
181
  headers.set("X-SW-Stale", "true");
45
182
  }
183
+ ${authBlock}${authUrlsBlock}
184
+ const fetchOptions${T("RequestInit")} = { ...options, headers };
46
185
 
47
- if (options.tags && options.tags.length > 0) {
48
- headers.set("X-SW-Cache-Tags", options.tags.join(","));
186
+ // Offline handling
187
+ if (!navigator.onLine) {
188
+ ${offlineReadBlock}
189
+ ${offlineWriteBlock}
49
190
  }
50
191
 
51
- if (method === "GET" || method === "HEAD") {
52
- const url = typeof input === "string" ? input : input.url;
53
- if (inFlightRequests.has(url)) {
54
- return inFlightRequests.get(url).then((r) => r.clone());
192
+ // Deduplicate in-flight GET requests
193
+ let responsePromise${T("Promise<Response>")};
194
+ if (isRead && inFlightRequests.has(url)) {
195
+ responsePromise = inFlightRequests.get(url)!.then((r) => r.clone());
196
+ } else {
197
+ responsePromise = fetch(input, fetchOptions);
198
+ if (isRead) {
199
+ inFlightRequests.set(url, responsePromise.finally(() => inFlightRequests.delete(url)));
55
200
  }
56
- const promise = fetch(input, { ...options, headers }).finally(() => {
57
- inFlightRequests.delete(url);
58
- });
59
- inFlightRequests.set(url, promise);
60
- return promise;
61
201
  }
62
202
 
63
- return fetch(input, { ...options, headers });
64
- }
65
-
66
- export async function fetchWithCacheOrQueue(input, options = {}) {
67
- if (!navigator.onLine) {
68
- if (options.method === "GET" || options.method === "HEAD") {
69
- const cached = await caches.match(input);
70
- if (cached) return cached;
71
- throw new Error("Offline: no cached data");
72
- } else {
73
- throw new Error("Offline: mutation queued");
74
- }
75
- }
76
- return fetchWithCache(input, options);
203
+ const response = await responsePromise;
204
+ ${autoInvalidateBlock}${auth401Block}
205
+ const fromCache = response.headers.get("X-SW-From-Cache") === "true";
206
+ return { response, fromCache };
77
207
  }
78
208
  `;
79
209
  writeFile(ctx, `fetch-wrapper.${ext}`, code);
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-wrapper.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/fetch-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,oBAAoB,CAAC,GAAqB;IACxD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,IAAI,GAAG;;;;;6DAK8C,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkE/D,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"fetch-wrapper.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/fetch-wrapper.ts"],"names":[],"mappings":"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"}
@@ -17,9 +17,11 @@ export function generateHooks(ctx) {
17
17
  if (config.features.mutationQueue) {
18
18
  writeMutationQueueHook(hooksDir, ext);
19
19
  }
20
+ writeCachedFetchHook(hooksDir, ext, config.features.tagInvalidation);
20
21
  }
21
22
  function writePWAHooks(dir, ext) {
22
23
  writeFileSync(join(dir, `usePWAUpdate.${ext}x`), `import { useState, useEffect, useCallback } from "react";
24
+ import { handleUpdateApproved } from "../client-injector.${ext}";
23
25
 
24
26
  export function usePWAUpdate() {
25
27
  const [state, setState] = useState({
@@ -62,7 +64,6 @@ export function usePWAUpdate() {
62
64
 
63
65
  const acceptUpdate = useCallback(async () => {
64
66
  if (!state.availableVersion) return;
65
- const { handleUpdateApproved } = await import("../sw-injector.${ext}");
66
67
  await handleUpdateApproved(state.availableVersion);
67
68
  }, [state.availableVersion]);
68
69
 
@@ -102,7 +103,7 @@ export function useSWProgress() {
102
103
  }
103
104
  function writeAuthHook(dir, ext) {
104
105
  writeFileSync(join(dir, `useAuth.${ext}x`), `import { useState, useEffect } from "react";
105
- import { getAuthState } from "../auth-state.${ext}";
106
+ import { getAuthState } from "../auth/state.${ext}";
106
107
 
107
108
  export function useAuth() {
108
109
  const [state, setState] = useState({
@@ -133,6 +134,63 @@ export function useAuth() {
133
134
  }
134
135
  `);
135
136
  }
137
+ function writeCachedFetchHook(dir, ext, tagInvalidation) {
138
+ const ts = ext === "ts";
139
+ const extraEffect = tagInvalidation
140
+ ? `
141
+ useEffect(() => {
142
+ const onInvalidated${ts ? ": EventListener" : ""} = (e${ts ? ": Event" : ""}) => {
143
+ const detail = (e as CustomEvent).detail;
144
+ const tags = detail?.tags;
145
+ if (!tags || !Array.isArray(tags)) return;
146
+ const urlTags = generateTags(url);
147
+ if (tags.some((t) => urlTags.includes(t))) {
148
+ setRefetchCount((c) => c + 1);
149
+ }
150
+ };
151
+ window.addEventListener("cache-invalidated", onInvalidated);
152
+ return () => window.removeEventListener("cache-invalidated", onInvalidated);
153
+ }, [url]);`
154
+ : "";
155
+ const importTags = tagInvalidation
156
+ ? `import { generateTags } from "../invalidation-tags.${ext}";\n`
157
+ : "";
158
+ const stringType = ts ? ": string" : "";
159
+ const respType = ts ? "<Response | null>" : "";
160
+ writeFileSync(join(dir, `useCachedFetch.${ext}x`), `import { useState, useEffect, useCallback } from "react";
161
+ import { fetchWithCache } from "../fetch-wrapper.${ext}";
162
+ ${importTags}
163
+ export function useCachedFetch(url${stringType}, options = {}) {
164
+ const [data, setData] = useState${respType}(null);
165
+ const [error, setError] = useState(null);
166
+ const [loading, setLoading] = useState(true);
167
+ const [refetchCount, setRefetchCount] = useState(0);
168
+
169
+ const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
170
+
171
+ useEffect(() => {
172
+ let cancelled = false;
173
+ setLoading(true);
174
+ fetchWithCache(url, options)
175
+ .then(({ response }) => {
176
+ if (cancelled) return;
177
+ setData(response);
178
+ setError(null);
179
+ })
180
+ .catch((err) => {
181
+ if (cancelled) return;
182
+ setError(err);
183
+ })
184
+ .finally(() => {
185
+ if (!cancelled) setLoading(false);
186
+ });
187
+ return () => { cancelled = true; };
188
+ }, [url, refetchCount]);
189
+ ${extraEffect}
190
+ return { data, error, loading, refetch };
191
+ }
192
+ `);
193
+ }
136
194
  function writeMutationQueueHook(dir, ext) {
137
195
  writeFileSync(join(dir, `useMutationQueue.${ext}x`), `import { useState, useEffect } from "react";
138
196
  import { getPendingCount } from "../mutation-queue.${ext}";
@@ -150,7 +208,7 @@ export function useMutationQueue() {
150
208
  pending: 0,
151
209
  lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
152
210
  });
153
- const onChange = async () => setState((s) => ({ ...s, pending: await getPendingCount() }));
211
+ const onChange = async () => { const count = await getPendingCount(); setState((s) => ({ ...s, pending: count })); };
154
212
 
155
213
  window.addEventListener("mutation-sync-complete", onSync as EventListener);
156
214
  window.addEventListener("mutation-queue-changed", onChange);
@@ -1 +1 @@
1
- {"version":3,"file":"generate-hooks.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/generate-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,MAAM,UAAU,aAAa,CAAC,GAAqB;IACjD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,GAAG,CAAC;IACrD,IAAI,aAAa,KAAK,OAAO;QAAE,OAAO;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAChC,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;QAClC,sBAAsB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,GAAW,EAAE,GAAW;IAC7C,aAAa,CACX,IAAI,CAAC,GAAG,EAAE,gBAAgB,GAAG,GAAG,CAAC,EACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA2CgE,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCtE,CACE,CAAC;IAEF,6CAA6C;AAC/C,CAAC;AAED,SAAS,aAAa,CAAC,GAAW,EAAE,GAAW;IAC7C,aAAa,CACX,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,GAAG,CAAC,EAC5B;8CAC0C,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BhD,CACE,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,GAAW,EAAE,GAAW;IACtD,aAAa,CACX,IAAI,CAAC,GAAG,EAAE,oBAAoB,GAAG,GAAG,CAAC,EACrC;qDACiD,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BvD,CACE,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"generate-hooks.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/generate-hooks.ts"],"names":[],"mappings":"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"}