@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 };
|