@swoff/cli 0.0.1

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.
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Swoff Service Worker Generator
3
+ *
4
+ * Generates a service worker based on swoff.config.json configuration.
5
+ * Can be run as CLI or imported as a module.
6
+ *
7
+ * CLI Usage:
8
+ * node sw-generator.js [--project-root <path>] [--package-dir <path>]
9
+ *
10
+ * Module Usage:
11
+ * import { generate } from './sw-generator.js';
12
+ * generate({ projectRoot: '/path/to/project' });
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, existsSync } from "fs";
16
+ import { fileURLToPath } from "url";
17
+ import { join, dirname } from "path";
18
+
19
+ // Parse CLI arguments
20
+ const args = process.argv.slice(2);
21
+ const projectRootArg = args.findIndex(arg => arg === '--project-root');
22
+ const packageDirArg = args.findIndex(arg => arg === '--package-dir');
23
+
24
+ const passedProjectRoot = projectRootArg !== -1 ? args[projectRootArg + 1] : null;
25
+ const passedPackageDir = packageDirArg !== -1 ? args[packageDirArg + 1] : null;
26
+
27
+ // Get package directory (where this script is located)
28
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
29
+ const packageDir = passedPackageDir || join(scriptDir, '..');
30
+ const projectRoot = passedProjectRoot || process.cwd();
31
+
32
+ /**
33
+ * Generate the service worker
34
+ * @param {Object} options - Generator options
35
+ * @param {string} options.projectRoot - Root directory of the user's project
36
+ * @param {string} options.packageDir - Directory where swoff package is installed
37
+ */
38
+ export function generate(options = {}) {
39
+ const {
40
+ projectRoot: optProjectRoot = projectRoot,
41
+ packageDir: optPackageDir = packageDir
42
+ } = options;
43
+
44
+ // Find package.json in user's project
45
+ const pkgPath = join(optProjectRoot, "package.json");
46
+ let pkg = { version: "1.0.0" };
47
+
48
+ if (existsSync(pkgPath)) {
49
+ try {
50
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
51
+ } catch (e) {
52
+ console.warn("Could not read package.json, using default version");
53
+ }
54
+ }
55
+
56
+ // Load template from package directory
57
+ const templatePath = join(optPackageDir, 'src/lib/templates/sw-template.js');
58
+ let template;
59
+
60
+ if (existsSync(templatePath)) {
61
+ template = readFileSync(templatePath, "utf8");
62
+ } else {
63
+ // Fallback: inline template
64
+ template = getDefaultTemplate();
65
+ }
66
+
67
+ // Default configuration
68
+ const defaultConfig = {
69
+ enabled: true,
70
+ version: "from-package",
71
+ minSupportedVersion: "0.0.0",
72
+ serviceWorker: {
73
+ autoUpdate: false,
74
+ defaultStrategy: "cache-first",
75
+ maxCacheEntries: 100,
76
+ maxCacheAge: 7 * 24 * 60 * 60 * 1000,
77
+ runtimeCacheName: "swoff-runtime",
78
+ },
79
+ features: {
80
+ versionedSw: true,
81
+ offlineReads: true,
82
+ mutationQueue: false,
83
+ backgroundSync: false,
84
+ pwa: true,
85
+ auth: false,
86
+ crossTabSync: true,
87
+ tagInvalidation: true,
88
+ },
89
+ build: {
90
+ outputDir: "dist",
91
+ swFilename: "sw",
92
+ },
93
+ };
94
+
95
+ // Load user config
96
+ let userConfig = {};
97
+ let configSource = "defaults";
98
+
99
+ const configPath = join(optProjectRoot, "swoff.config.json");
100
+
101
+ if (existsSync(configPath)) {
102
+ try {
103
+ userConfig = JSON.parse(readFileSync(configPath, "utf8"));
104
+ configSource = "JSON";
105
+ } catch (e) {
106
+ console.warn("Could not parse swoff.config.json, using defaults");
107
+ }
108
+ } else {
109
+ // Check for JS config
110
+ const jsConfigPath = join(optProjectRoot, "swoff.config.js");
111
+ if (existsSync(jsConfigPath)) {
112
+ try {
113
+ const module = await import(jsConfigPath);
114
+ userConfig = module.default || module;
115
+ configSource = "JavaScript";
116
+ } catch (e) {
117
+ console.warn("Could not load swoff.config.js, using defaults");
118
+ }
119
+ }
120
+ }
121
+
122
+ const config = { ...defaultConfig, ...userConfig };
123
+
124
+ // Check if config generation is disabled
125
+ if (!config.enabled) {
126
+ console.log("Swoff config generation disabled. Using custom code mode.");
127
+ return;
128
+ }
129
+
130
+ // Get version
131
+ const version = config.version === "from-package" ? (pkg.version || "1.0.0") : config.version;
132
+
133
+ // Generate service worker
134
+ const sw = generateServiceWorker(config, version);
135
+
136
+ // Write output files
137
+ const outputDir = join(optProjectRoot, config.build.outputDir);
138
+ const swFilename = config.build.swFilename;
139
+
140
+ // Ensure output directory exists
141
+ if (!existsSync(outputDir)) {
142
+ // Just try to write - will fail gracefully if parent doesn't exist
143
+ }
144
+
145
+ try {
146
+ writeFileSync(join(outputDir, `${swFilename}-v${version}.js`), sw);
147
+ writeFileSync(join(outputDir, "version.json"), JSON.stringify({
148
+ version: version,
149
+ minSupportedVersion: config.minSupportedVersion,
150
+ generatedAt: new Date().toISOString(),
151
+ configEnabled: config.enabled,
152
+ configSource: configSource,
153
+ }, null, 2));
154
+
155
+ console.log(`✅ Swoff service worker generated successfully!`);
156
+ console.log(`📁 Output: ${outputDir}/${swFilename}-v${version}.js`);
157
+ console.log(`📄 Version info: ${outputDir}/version.json`);
158
+ console.log(`ℹ️ Configuration source: ${configSource}`);
159
+ } catch (err) {
160
+ console.error(`Error writing files: ${err.message}`);
161
+ console.log("Make sure the output directory exists:");
162
+ console.log(` mkdir -p ${outputDir}`);
163
+ }
164
+ }
165
+
166
+ function generateServiceWorker(config, version) {
167
+ const { serviceWorker, features } = config;
168
+
169
+ // Basic assets to cache
170
+ const baseAssets = ['/', '/index.html'];
171
+ const pwaAssets = features.pwa ? ['/manifest.json'] : [];
172
+ const ASSETS_TO_CACHE = [...baseAssets, ...pwaAssets];
173
+
174
+ let sw = getDefaultTemplate();
175
+
176
+ // Replace placeholders
177
+ sw = sw.replace(
178
+ "// [[CACHE_NAME]]",
179
+ `CACHE_NAME = 'sw-v${version}'`
180
+ );
181
+
182
+ sw = sw.replace(
183
+ "// [[ASSETS_LIST]]",
184
+ `ASSETS_TO_CACHE = ${JSON.stringify(ASSETS_TO_CACHE, null, 2)}`
185
+ );
186
+
187
+ // Add fetch handler
188
+ const fetchHandler = generateFetchHandler(serviceWorker, features);
189
+ sw = sw.replace("// [[FETCH_HANDLER]]", fetchHandler);
190
+
191
+ // Add activate handler
192
+ const activateHandler = generateActivateHandler(features.versionedSw);
193
+ sw = sw.replace("// [[ACTIVATE_HANDLER]]", activateHandler);
194
+
195
+ // Add install handler
196
+ const installHandler = generateInstallHandler(features);
197
+ sw = sw.replace("// [[INSTALL_HANDLER]]", installHandler);
198
+
199
+ // Add config header
200
+ const configHeader = generateConfigHeader(config, config);
201
+ sw = configHeader + "\n\n" + sw;
202
+
203
+ return sw;
204
+ }
205
+
206
+ function generateFetchHandler(swConfig, features) {
207
+ const { defaultStrategy, strategies } = swConfig;
208
+
209
+ return `
210
+ // Enhanced fetch handler with configurable caching strategies
211
+ self.addEventListener("fetch", (event) => {
212
+ if (event.request.method !== "GET") return;
213
+
214
+ const strategy = determineCacheStrategy(event.request.url, ${JSON.stringify(strategies || {})}, "${defaultStrategy}");
215
+
216
+ event.respondWith(handleRequestWithStrategy(event.request, strategy));
217
+ });
218
+
219
+ function determineCacheStrategy(url, customStrategies, defaultStrategy) {
220
+ for (const [pattern, strategy] of Object.entries(customStrategies)) {
221
+ if (url.includes(pattern)) return strategy;
222
+ }
223
+ return defaultStrategy;
224
+ }
225
+
226
+ async function handleRequestWithStrategy(request, strategy) {
227
+ const cache = await caches.open(CACHE_NAME);
228
+
229
+ switch (strategy) {
230
+ case "cache-first": return cacheFirstStrategy(request, cache);
231
+ case "network-first": return networkFirstStrategy(request, cache);
232
+ case "stale-while-revalidate": return staleWhileRevalidateStrategy(request, cache);
233
+ case "cache-only": return cacheOnlyStrategy(request, cache);
234
+ case "network-only": return networkOnlyStrategy(request, cache);
235
+ default: return cacheFirstStrategy(request, cache);
236
+ }
237
+ }
238
+
239
+ async function cacheFirstStrategy(request, cache) {
240
+ const cached = await cache.match(request);
241
+ if (cached) return cached;
242
+ try {
243
+ const response = await fetch(request);
244
+ if (response.ok) await cache.put(request, response.clone());
245
+ return response;
246
+ } catch (error) {
247
+ return new Response("Offline", { status: 503 });
248
+ }
249
+ }
250
+
251
+ async function networkFirstStrategy(request, cache) {
252
+ try {
253
+ const response = await fetch(request);
254
+ if (response.ok) await cache.put(request, response.clone());
255
+ return response;
256
+ } catch (error) {
257
+ const cached = await cache.match(request);
258
+ return cached || new Response("Offline", { status: 503 });
259
+ }
260
+ }
261
+
262
+ async function staleWhileRevalidateStrategy(request, cache) {
263
+ const cached = await cache.match(request);
264
+ fetch(request).then(response => {
265
+ if (response.ok) cache.put(request, response.clone());
266
+ });
267
+ return cached || fetch(request);
268
+ }
269
+
270
+ async function cacheOnlyStrategy(request, cache) {
271
+ return cache.match(request) || new Response("Not in cache", { status: 404 });
272
+ }
273
+
274
+ async function networkOnlyStrategy(request, cache) {
275
+ try {
276
+ return await fetch(request);
277
+ } catch (error) {
278
+ return new Response("Network error", { status: 503 });
279
+ }
280
+ }`;
281
+ }
282
+
283
+ function generateActivateHandler(versionedSw) {
284
+ if (versionedSw) {
285
+ return `
286
+ self.addEventListener("activate", (event) => {
287
+ event.waitUntil(
288
+ caches.keys().then((keys) =>
289
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
290
+ )
291
+ );
292
+ });`;
293
+ }
294
+ return `
295
+ self.addEventListener("activate", (event) => {
296
+ event.waitUntil(Promise.resolve());
297
+ });`;
298
+ }
299
+
300
+ function generateInstallHandler(features) {
301
+ let code = `
302
+ self.addEventListener("install", (event) => {
303
+ event.waitUntil(
304
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
305
+ );
306
+ }`;
307
+
308
+ if (features.offlineReads || features.mutationQueue) {
309
+ code += `\n event.waitUntil(initializeOfflineSupport());`;
310
+ }
311
+
312
+ return code;
313
+ }
314
+
315
+ function generateConfigHeader(config, fullConfig) {
316
+ return `/**
317
+ * Swoff Service Worker - Auto-Generated
318
+ *
319
+ * This file was automatically generated from swoff.config.json
320
+ * DO NOT EDIT MANUALLY - changes will be overwritten on next build
321
+ *
322
+ * Configuration used:
323
+ * • Version: ${config.version}
324
+ * • Versioned SW: ${config.features.versionedSw ? "Enabled" : "Disabled"}
325
+ * • Offline Reads: ${config.features.offlineReads ? "Enabled" : "Disabled"}
326
+ * • Mutation Queue: ${config.features.mutationQueue ? "Enabled" : "Disabled"}
327
+ * • Default Strategy: ${config.serviceWorker.defaultStrategy}
328
+ * • Auto Update: ${config.serviceWorker.autoUpdate ? "Enabled" : "Disabled"}
329
+ * • PWA Support: ${config.features.pwa ? "Enabled" : "Disabled"}
330
+ *
331
+ * Features can be configured in swoff.config.json
332
+ * See documentation for more details: https://swoff.netlify.app/docs
333
+ */`;
334
+ }
335
+
336
+ function getDefaultTemplate() {
337
+ return `let CACHE_NAME = "";
338
+ let ASSETS_TO_CACHE = [];
339
+
340
+ // [[INSTALL_HANDLER]]
341
+ // [[ACTIVATE_HANDLER]]
342
+ // [[FETCH_HANDLER]]
343
+
344
+ const SWOFF = {
345
+ cache: {
346
+ async get(key) {
347
+ const cache = await caches.open(CACHE_NAME);
348
+ return cache.match(key);
349
+ },
350
+ async put(request, response) {
351
+ const cache = await caches.open(CACHE_NAME);
352
+ await cache.put(request, response);
353
+ }
354
+ }
355
+ };
356
+
357
+ if (typeof self !== 'undefined') {
358
+ self.SWOFF = SWOFF;
359
+ }`;
360
+ }
361
+
362
+ // Run if executed directly
363
+ if (import.meta.url === `file://${process.argv[1]}`) {
364
+ generate();
365
+ }
366
+
367
+ export default { generate };