@swoff/cli 0.1.0 → 0.2.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.
Files changed (98) hide show
  1. package/README.md +49 -0
  2. package/dist/__tests__/assemble-sw.test.js +145 -0
  3. package/dist/__tests__/assemble-sw.test.js.map +1 -0
  4. package/dist/__tests__/cli-integration.test.js +128 -0
  5. package/dist/__tests__/cli-integration.test.js.map +1 -0
  6. package/dist/__tests__/config-types.test.js +82 -0
  7. package/dist/__tests__/config-types.test.js.map +1 -0
  8. package/dist/__tests__/detect-language.test.js +51 -0
  9. package/dist/__tests__/detect-language.test.js.map +1 -0
  10. package/dist/__tests__/file-generators.test.js +271 -0
  11. package/dist/__tests__/file-generators.test.js.map +1 -0
  12. package/dist/__tests__/invalidation-tags.test.js +99 -0
  13. package/dist/__tests__/invalidation-tags.test.js.map +1 -0
  14. package/dist/__tests__/loader.test.js +77 -0
  15. package/dist/__tests__/loader.test.js.map +1 -0
  16. package/dist/__tests__/validator.test.js +232 -0
  17. package/dist/__tests__/validator.test.js.map +1 -0
  18. package/dist/index.js +22 -585
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/cli/colors.js +14 -0
  21. package/dist/lib/cli/colors.js.map +1 -0
  22. package/dist/lib/cli/help.js +63 -0
  23. package/dist/lib/cli/help.js.map +1 -0
  24. package/dist/lib/cli/logger.js +13 -0
  25. package/dist/lib/cli/logger.js.map +1 -0
  26. package/dist/lib/commands/add.js +58 -0
  27. package/dist/lib/commands/add.js.map +1 -0
  28. package/dist/lib/commands/clean.js +39 -0
  29. package/dist/lib/commands/clean.js.map +1 -0
  30. package/dist/lib/commands/generate.js +84 -0
  31. package/dist/lib/commands/generate.js.map +1 -0
  32. package/dist/lib/commands/info.js +44 -0
  33. package/dist/lib/commands/info.js.map +1 -0
  34. package/dist/lib/commands/init.js +46 -0
  35. package/dist/lib/commands/init.js.map +1 -0
  36. package/dist/lib/commands/validate.js +30 -0
  37. package/dist/lib/commands/validate.js.map +1 -0
  38. package/dist/lib/config/loader.js +80 -0
  39. package/dist/lib/config/loader.js.map +1 -0
  40. package/dist/lib/config/validator.js +102 -0
  41. package/dist/lib/config/validator.js.map +1 -0
  42. package/dist/lib/generators/file-generators/background-sync.js +59 -0
  43. package/dist/lib/generators/file-generators/background-sync.js.map +1 -0
  44. package/dist/lib/generators/file-generators/cache.js +55 -0
  45. package/dist/lib/generators/file-generators/cache.js.map +1 -0
  46. package/dist/lib/generators/file-generators/context.js +16 -0
  47. package/dist/lib/generators/file-generators/context.js.map +1 -0
  48. package/dist/lib/generators/file-generators/fetch-wrapper.js +80 -0
  49. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -0
  50. package/dist/lib/generators/file-generators/indexeddb.js +73 -0
  51. package/dist/lib/generators/file-generators/indexeddb.js.map +1 -0
  52. package/dist/lib/generators/file-generators/invalidation-tags.js +91 -0
  53. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -0
  54. package/dist/lib/generators/file-generators/manifest.js +26 -0
  55. package/dist/lib/generators/file-generators/manifest.js.map +1 -0
  56. package/dist/lib/generators/file-generators/mutation-queue.js +226 -0
  57. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -0
  58. package/dist/lib/generators/file-generators/pwa-install.js +102 -0
  59. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -0
  60. package/dist/lib/generators/file-generators/reconcile.js +54 -0
  61. package/dist/lib/generators/file-generators/reconcile.js.map +1 -0
  62. package/dist/lib/generators/file-generators/store.js +75 -0
  63. package/dist/lib/generators/file-generators/store.js.map +1 -0
  64. package/dist/lib/generators/file-generators/sw-generator-build.js +70 -0
  65. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -0
  66. package/dist/lib/generators/file-generators/sw-injector.js +195 -0
  67. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -0
  68. package/dist/lib/generators/file-generators/sw-template.js +143 -0
  69. package/dist/lib/generators/file-generators/sw-template.js.map +1 -0
  70. package/dist/lib/generators/file-generators/type-definitions.js +69 -0
  71. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -0
  72. package/dist/lib/generators/sw-generator.js +33 -569
  73. package/dist/lib/generators/sw-generator.js.map +1 -1
  74. package/dist/lib/generators/sw-sections/activate-handler.js +16 -0
  75. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -0
  76. package/dist/lib/generators/sw-sections/assemble-sw.js +38 -0
  77. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -0
  78. package/dist/lib/generators/sw-sections/background-sync-handler.js +106 -0
  79. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -0
  80. package/dist/lib/generators/sw-sections/config-header.js +15 -0
  81. package/dist/lib/generators/sw-sections/config-header.js.map +1 -0
  82. package/dist/lib/generators/sw-sections/default-template.js +51 -0
  83. package/dist/lib/generators/sw-sections/default-template.js.map +1 -0
  84. package/dist/lib/generators/sw-sections/fetch-handler.js +210 -0
  85. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -0
  86. package/dist/lib/generators/sw-sections/install-handler.js +35 -0
  87. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -0
  88. package/dist/lib/generators/sw-sections/message-handler.js +20 -0
  89. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -0
  90. package/dist/lib/generators/sw-sections/tag-management.js +69 -0
  91. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -0
  92. package/dist/lib/generators/swoff-files-generator.js +78 -1368
  93. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  94. package/dist/lib/shared/config-types.js +87 -0
  95. package/dist/lib/shared/config-types.js.map +1 -0
  96. package/dist/lib/utils/detect-language.js +33 -0
  97. package/dist/lib/utils/detect-language.js.map +1 -0
  98. package/package.json +5 -2
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Config validator - checks swoff.config.json for correctness.
3
+ * Returns an array of error strings (empty if valid).
4
+ */
5
+ import { KNOWN_FEATURES, VALID_STRATEGIES } from "../shared/config-types.js";
6
+ export function validateConfig(config) {
7
+ const errors = [];
8
+ // Required fields
9
+ const requiredFields = ["enabled", "version", "serviceWorker", "features", "build"];
10
+ const missingFields = requiredFields.filter((field) => config[field] === undefined || config[field] === null);
11
+ if (missingFields.length > 0) {
12
+ errors.push(`Missing required fields: ${missingFields.join(", ")}`);
13
+ }
14
+ // serviceWorker
15
+ if (config.serviceWorker) {
16
+ const sw = config.serviceWorker;
17
+ if (sw.autoRegister !== undefined && typeof sw.autoRegister !== "boolean") {
18
+ errors.push("serviceWorker.autoRegister must be a boolean");
19
+ }
20
+ if (sw.autoUpdate !== undefined && typeof sw.autoUpdate !== "boolean") {
21
+ errors.push("serviceWorker.autoUpdate must be a boolean");
22
+ }
23
+ if (sw.defaultStrategy && !VALID_STRATEGIES.includes(sw.defaultStrategy)) {
24
+ errors.push(`Invalid defaultStrategy "${sw.defaultStrategy}". Must be one of: ${VALID_STRATEGIES.join(", ")}`);
25
+ }
26
+ if (sw.strategies && typeof sw.strategies === "object") {
27
+ const strategies = sw.strategies;
28
+ for (const [pattern, strategy] of Object.entries(strategies)) {
29
+ if (!VALID_STRATEGIES.includes(strategy)) {
30
+ errors.push(`Invalid strategy "${strategy}" for pattern "${pattern}". Must be one of: ${VALID_STRATEGIES.join(", ")}`);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ // features
36
+ if (config.features) {
37
+ const features = config.features;
38
+ for (const [key, value] of Object.entries(features)) {
39
+ if (!KNOWN_FEATURES.includes(key)) {
40
+ errors.push(`Unknown feature "${key}"`);
41
+ }
42
+ if (typeof value !== "boolean") {
43
+ errors.push(`Feature "${key}" must be a boolean, got ${typeof value}`);
44
+ }
45
+ }
46
+ }
47
+ // version
48
+ if (config.version && typeof config.version === "string" && config.version !== "from-package") {
49
+ if (!/^\d+\.\d+\.\d+$/.test(config.version)) {
50
+ errors.push(`Invalid version "${config.version}". Must be "from-package" or semver (e.g., "1.0.0")`);
51
+ }
52
+ }
53
+ // minSupportedVersion
54
+ if (config.minSupportedVersion && typeof config.minSupportedVersion === "string") {
55
+ if (!/^\d+\.\d+\.\d+$/.test(config.minSupportedVersion)) {
56
+ errors.push(`Invalid minSupportedVersion "${config.minSupportedVersion}". Must be semver (e.g., "1.0.0")`);
57
+ }
58
+ }
59
+ // build
60
+ if (config.build) {
61
+ const build = config.build;
62
+ if (build.outputDir && typeof build.outputDir !== "string") {
63
+ errors.push("build.outputDir must be a string");
64
+ }
65
+ if (build.swFilename && typeof build.swFilename !== "string") {
66
+ errors.push("build.swFilename must be a string");
67
+ }
68
+ }
69
+ // pwa
70
+ if (config.pwa) {
71
+ const pwa = config.pwa;
72
+ if (pwa.preventDefaultInstall !== undefined && typeof pwa.preventDefaultInstall !== "boolean") {
73
+ errors.push("pwa.preventDefaultInstall must be a boolean");
74
+ }
75
+ }
76
+ // database
77
+ if (config.database) {
78
+ const db = config.database;
79
+ if (db.name && typeof db.name !== "string") {
80
+ errors.push("database.name must be a string");
81
+ }
82
+ if (db.name && typeof db.name === "string" && !/^[a-zA-Z0-9-_]+$/.test(db.name)) {
83
+ errors.push(`database.name "${db.name}" must match pattern ^[a-zA-Z0-9-_]+$`);
84
+ }
85
+ if (db.stores && !Array.isArray(db.stores)) {
86
+ errors.push("database.stores must be an array");
87
+ }
88
+ if (db.stores && Array.isArray(db.stores)) {
89
+ for (const store of db.stores) {
90
+ if (typeof store !== "string") {
91
+ errors.push("database.stores must contain only strings");
92
+ break;
93
+ }
94
+ if (!/^[a-zA-Z0-9-_]+$/.test(store)) {
95
+ errors.push(`database.store "${store}" must match pattern ^[a-zA-Z0-9-_]+$`);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return errors;
101
+ }
102
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../../src/lib/config/validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Generates background-sync.js - Background Sync API registration.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateBackgroundSync(ctx) {
6
+ const code = `/**
7
+ * Swoff Background Sync
8
+ * Register sync events for processing mutation queue after tab close.
9
+ * Falls back to online event listener in unsupported browsers.
10
+ *
11
+ * Usage:
12
+ * import { syncWhenPossible } from './swoff/background-sync.js';
13
+ *
14
+ * await syncWhenPossible({
15
+ * method: "POST",
16
+ * url: "/api/todos",
17
+ * body: { title: "Grocery" },
18
+ * tags: ["todos"],
19
+ * storeName: "todos",
20
+ * tempId: "temp_abc123",
21
+ * });
22
+ */
23
+
24
+ import { queueMutation, processMutationQueue, getPendingCount } from "./mutation-queue.js";
25
+
26
+ const SYNC_TAG = "sync-mutations";
27
+
28
+ async function registerSync() {
29
+ if (!("serviceWorker" in navigator) || !("SyncManager" in window)) {
30
+ window.addEventListener("online", processMutationQueue, { once: true });
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const registration = await navigator.serviceWorker.ready;
36
+ await registration.sync.register(SYNC_TAG);
37
+ } catch {
38
+ window.addEventListener("online", processMutationQueue, { once: true });
39
+ }
40
+ }
41
+
42
+ export async function syncWhenPossible(mutation) {
43
+ await queueMutation(mutation);
44
+ await registerSync();
45
+ }
46
+
47
+ export async function retrySync() {
48
+ if (!("serviceWorker" in navigator) || !("SyncManager" in window)) return;
49
+ const count = await getPendingCount();
50
+ if (count > 0) {
51
+ await registerSync();
52
+ }
53
+ }
54
+
55
+ window.addEventListener("mutation-sync-complete", retrySync);
56
+ `;
57
+ writeFile(ctx, "background-sync.js", code);
58
+ }
59
+ //# sourceMappingURL=background-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background-sync.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/background-sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,sBAAsB,CAAC,GAAqB;IAC1D,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDd,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Generates cache.js - cache tag invalidation and cross-tab sync.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateCache(ctx) {
6
+ const code = `/**
7
+ * Swoff Cache Invalidation & Cross-Tab Sync
8
+ * Framework-agnostic cache tag invalidation and cross-tab synchronization.
9
+ *
10
+ * Usage:
11
+ * import { invalidateByTag, initCrossTabSync } from './swoff/cache.js';
12
+ *
13
+ * // Call once during app init
14
+ * initCrossTabSync();
15
+ *
16
+ * // After a mutation, invalidate related cache
17
+ * await invalidateByTag("todos");
18
+ */
19
+
20
+ export async function invalidateByTag(tag) {
21
+ if (!navigator.serviceWorker?.controller) return;
22
+
23
+ navigator.serviceWorker.controller.postMessage({
24
+ type: "INVALIDATE_TAG",
25
+ tag,
26
+ });
27
+
28
+ window.dispatchEvent(
29
+ new CustomEvent("cache-invalidated", { detail: { tags: [tag] } })
30
+ );
31
+ }
32
+
33
+ export async function invalidateByTags(tags) {
34
+ for (const tag of tags) {
35
+ await invalidateByTag(tag);
36
+ }
37
+ }
38
+
39
+ export function initCrossTabSync() {
40
+ if (!navigator.serviceWorker) return;
41
+
42
+ navigator.serviceWorker.addEventListener("message", (event) => {
43
+ if (event.data.type === "TAG_INVALIDATED" && event.data.tag) {
44
+ window.dispatchEvent(
45
+ new CustomEvent("cache-invalidated", {
46
+ detail: { tags: [event.data.tag] },
47
+ })
48
+ );
49
+ }
50
+ });
51
+ }
52
+ `;
53
+ writeFile(ctx, "cache.js", code);
54
+ }
55
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,aAAa,CAAC,GAAqB;IACjD,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Cd,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared context for file generators.
3
+ * Contains config, paths, and the generated files tracker.
4
+ */
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 });
10
+ }
11
+ export function writeFile(ctx, filename, code) {
12
+ ensureSwoffDir(ctx);
13
+ writeFileSync(join(ctx.swoffDir, filename), code);
14
+ ctx.generatedFiles.push(`swoff/${filename}`);
15
+ }
16
+ //# sourceMappingURL=context.js.map
@@ -0,0 +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;AAW5B,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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Generates fetch-wrapper.js - fetch with cache strategy headers and deduplication.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateFetchWrapper(ctx) {
6
+ const code = `/**
7
+ * Swoff Fetch Wrapper
8
+ * Framework-agnostic fetch with cache strategy, tags, and query deduplication.
9
+ *
10
+ * Usage:
11
+ * import { fetchWithCache } from './swoff/fetch-wrapper.js';
12
+ *
13
+ * // GET - cached with tag
14
+ * const todos = await fetchWithCache("/api/todos", { tags: ["todos"] }).then(r => r.json());
15
+ *
16
+ * // POST - mutation (passes through to server)
17
+ * await fetchWithCache("/api/todos", {
18
+ * method: "POST",
19
+ * body: JSON.stringify({ title: "New task" }),
20
+ * });
21
+ *
22
+ * // Stale-while-revalidate
23
+ * const data = await fetchWithCache("/api/data", {
24
+ * tags: ["data"],
25
+ * staleWhileRevalidate: true,
26
+ * }).then(r => r.json());
27
+ */
28
+
29
+ const inFlightRequests = new Map();
30
+
31
+ export async function fetchWithCache(input, options = {}) {
32
+ const headers = new Headers(options.headers);
33
+ const method = options.method || "GET";
34
+
35
+ if (!headers.has("X-SW-Cache-Strategy")) {
36
+ headers.set(
37
+ "X-SW-Cache-Strategy",
38
+ method === "GET" || method === "HEAD" ? "read" : "mutation"
39
+ );
40
+ }
41
+
42
+ if (options.staleWhileRevalidate) {
43
+ headers.set("X-SW-Stale", "true");
44
+ }
45
+
46
+ if (options.tags && options.tags.length > 0) {
47
+ headers.set("X-SW-Cache-Tags", options.tags.join(","));
48
+ }
49
+
50
+ if (method === "GET" || method === "HEAD") {
51
+ const url = typeof input === "string" ? input : input.url;
52
+ if (inFlightRequests.has(url)) {
53
+ return inFlightRequests.get(url).then((r) => r.clone());
54
+ }
55
+ const promise = fetch(input, { ...options, headers }).finally(() => {
56
+ inFlightRequests.delete(url);
57
+ });
58
+ inFlightRequests.set(url, promise);
59
+ return promise;
60
+ }
61
+
62
+ return fetch(input, { ...options, headers });
63
+ }
64
+
65
+ export async function fetchWithCacheOrQueue(input, options = {}) {
66
+ if (!navigator.onLine) {
67
+ if (options.method === "GET" || options.method === "HEAD") {
68
+ const cached = await caches.match(input);
69
+ if (cached) return cached;
70
+ throw new Error("Offline: no cached data");
71
+ } else {
72
+ throw new Error("Offline: mutation queued");
73
+ }
74
+ }
75
+ return fetchWithCache(input, options);
76
+ }
77
+ `;
78
+ writeFile(ctx, "fetch-wrapper.js", code);
79
+ }
80
+ //# sourceMappingURL=fetch-wrapper.js.map
@@ -0,0 +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,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEd,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Generates indexeddb.js - DB initialization with schema migrations.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateIndexedDB(ctx) {
6
+ const dbName = ctx.config.database?.name || "app-db";
7
+ const stores = ctx.config.database?.stores || [];
8
+ const storesCode = stores.length > 0
9
+ ? stores
10
+ .map((store, i) => ` if (oldVersion < ${i + 1}) {\n db.createObjectStore("${store}", { keyPath: "id" });\n }`)
11
+ .join("\n\n")
12
+ : ` // Create your object stores here:
13
+ // if (oldVersion < 1) {
14
+ // const todos = db.createObjectStore("todos", { keyPath: "id" });
15
+ // todos.createIndex("by-date", "date");
16
+ // }`;
17
+ const code = `/**
18
+ * Swoff IndexedDB Setup
19
+ * Database initialization with schema migrations.
20
+ *
21
+ * Usage:
22
+ * import { openDB } from './swoff/indexeddb.js';
23
+ *
24
+ * const db = await openDB();
25
+ */
26
+
27
+ const DB_NAME = "${dbName}";
28
+ const DB_VERSION = 1;
29
+
30
+ export function openDB() {
31
+ return new Promise((resolve, reject) => {
32
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
33
+
34
+ request.onupgradeneeded = (e) => {
35
+ const db = e.target.result;
36
+ const oldVersion = e.oldVersion;
37
+
38
+ ${storesCode}
39
+ };
40
+
41
+ request.onsuccess = (e) => resolve(e.target.result);
42
+ request.onerror = (e) => reject(e.target.error);
43
+ });
44
+ }
45
+
46
+ export async function requestPersistentStorage() {
47
+ if (!navigator.storage?.persist) return false;
48
+
49
+ const isPersisted = await navigator.storage.persisted();
50
+ if (isPersisted) return true;
51
+
52
+ try {
53
+ return await navigator.storage.persist();
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ export async function monitorStorage() {
60
+ const estimate = await navigator.storage.estimate();
61
+ const ratio = estimate.usage / estimate.quota;
62
+
63
+ return {
64
+ usage: estimate.usage,
65
+ quota: estimate.quota,
66
+ ratio,
67
+ status: ratio >= 0.95 ? "critical" : ratio >= 0.8 ? "warning" : "ok",
68
+ };
69
+ }
70
+ `;
71
+ writeFile(ctx, "indexeddb.js", code);
72
+ }
73
+ //# sourceMappingURL=indexeddb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexeddb.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/indexeddb.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,iBAAiB,CAAC,GAAqB;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,IAAI,QAAQ,CAAC;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,CAAC;IAEjD,MAAM,UAAU,GACd,MAAM,CAAC,MAAM,GAAG,CAAC;QACf,CAAC,CAAC,MAAM;aACH,GAAG,CACF,CAAC,KAAa,EAAE,CAAS,EAAE,EAAE,CAC3B,0BAA0B,CAAC,GAAG,CAAC,sCAAsC,KAAK,iCAAiC,CAC9G;aACA,IAAI,CAAC,MAAM,CAAC;QACjB,CAAC,CAAC;;;;WAIG,CAAC;IAEV,MAAM,IAAI,GAAG;;;;;;;;;;mBAUI,MAAM;;;;;;;;;;;EAWvB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCX,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Generates invalidation-tags.js - URL-based tag generation helper.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateInvalidationTags(ctx) {
6
+ const code = `/**
7
+ * Swoff Invalidation Tags Helper
8
+ * URL-based tag generation from REST endpoints for automatic cache invalidation.
9
+ *
10
+ * Usage:
11
+ * import { generateTags, invalidateUrl } from './swoff/invalidation-tags.js';
12
+ *
13
+ * // Generate tags from URL
14
+ * generateTags("/api/todos"); // ["todos"]
15
+ * generateTags("/api/todos/42"); // ["todos", "todo:42"]
16
+ * generateTags("/api/todos/42/comments"); // ["todos", "todo:42", "comments"]
17
+ *
18
+ * // Use with fetch wrapper
19
+ * const data = await fetchWithCache("/api/todos", {
20
+ * tags: generateTags("/api/todos"),
21
+ * });
22
+ *
23
+ * // Invalidate after mutation
24
+ * await invalidateUrl("/api/todos/42");
25
+ */
26
+
27
+ import { invalidateByTag, invalidateByTags } from "./cache.js";
28
+
29
+ export function generateTags(url) {
30
+ const parsed = typeof url === "string" ? new URL(url, window.location.origin) : url;
31
+ const segments = parsed.pathname.split("/").filter(Boolean);
32
+
33
+ if (segments.length === 0) return ["root"];
34
+
35
+ // Skip common API prefixes
36
+ const skipPrefixes = ["api", "v1", "v2", "v3", "rest", "graphql", "gql"];
37
+ let startIdx = 0;
38
+ while (startIdx < segments.length && skipPrefixes.includes(segments[startIdx])) {
39
+ startIdx++;
40
+ }
41
+
42
+ const resourceSegments = segments.slice(startIdx);
43
+ if (resourceSegments.length === 0) return ["root"];
44
+
45
+ const tags = [];
46
+
47
+ // Collection tag: /api/todos -> "todos"
48
+ tags.push(resourceSegments[0]);
49
+
50
+ // Resource tag: /api/todos/42 -> "todo:42"
51
+ if (resourceSegments.length >= 2 && !isNaN(Number(resourceSegments[1]))) {
52
+ const collection = resourceSegments[0];
53
+ const id = resourceSegments[1];
54
+ const singular = collection.replace(/s$/, "");
55
+ tags.push(\`\${singular}:\${id}\`);
56
+ }
57
+
58
+ // Sub-resource tags: /api/todos/42/comments -> "comments"
59
+ for (let i = 2; i < resourceSegments.length; i++) {
60
+ if (isNaN(Number(resourceSegments[i]))) {
61
+ tags.push(resourceSegments[i]);
62
+ }
63
+ }
64
+
65
+ return tags;
66
+ }
67
+
68
+ export function generateTagsFromMethod(method, url) {
69
+ const tags = generateTags(url);
70
+
71
+ if (method === "GET" || method === "HEAD") {
72
+ return tags;
73
+ }
74
+
75
+ // For mutations, add method prefix
76
+ return tags.map((tag) => \`\${method.toLowerCase()}-\${tag}\`);
77
+ }
78
+
79
+ export async function invalidateUrl(url) {
80
+ const tags = generateTags(url);
81
+ await invalidateByTags(tags);
82
+ }
83
+
84
+ export async function invalidateByMethod(method, url) {
85
+ const tags = generateTagsFromMethod(method, url);
86
+ await invalidateByTags(tags);
87
+ }
88
+ `;
89
+ writeFile(ctx, "invalidation-tags.js", code);
90
+ }
91
+ //# sourceMappingURL=invalidation-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidation-tags.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/invalidation-tags.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,wBAAwB,CAAC,GAAqB;IAC5D,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFd,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,sBAAsB,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Generates manifest.json - PWA web app manifest.
3
+ */
4
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ export function generateManifest(ctx) {
7
+ const outputDir = join(ctx.projectRoot, "public");
8
+ if (!existsSync(outputDir))
9
+ mkdirSync(outputDir, { recursive: true });
10
+ const manifest = {
11
+ name: "Swoff App",
12
+ short_name: "Swoff",
13
+ description: "Offline-first web application",
14
+ start_url: "/",
15
+ display: "standalone",
16
+ background_color: "#ffffff",
17
+ theme_color: "#000000",
18
+ icons: [
19
+ { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
20
+ { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
21
+ ],
22
+ };
23
+ writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
24
+ ctx.generatedFiles.push("public/manifest.json");
25
+ }
26
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/manifest.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,gBAAgB,CAAC,GAAqB;IACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtE,MAAM,QAAQ,GAAG;QACf,IAAI,EAAE,WAAW;QACjB,UAAU,EAAE,OAAO;QACnB,WAAW,EAAE,+BAA+B;QAC5C,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,YAAY;QACrB,gBAAgB,EAAE,SAAS;QAC3B,WAAW,EAAE,SAAS;QACtB,KAAK,EAAE;YACL,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE;YAC7D,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE;SAC9D;KACF,CAAC;IAEF,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACnF,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;AAClD,CAAC"}