@swoff/cli 0.1.0 → 0.2.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.
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 +272 -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 +231 -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 +201 -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 +203 -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
package/dist/index.js CHANGED
@@ -1,19 +1,26 @@
1
1
  /**
2
2
  * Swoff CLI - Main Entry Point
3
3
  *
4
- * Command-line interface for managing Swoff in your project.
5
- *
6
4
  * Usage:
7
5
  * swoff init Initialize Swoff in current directory
8
6
  * swoff generate Generate service worker and files
9
7
  * swoff validate Validate swoff.config.json
10
8
  * swoff add <feature> Add specific feature files
9
+ * swoff info Show configuration summary
10
+ * swoff clean Remove old versioned SW files
11
11
  * swoff --help Show help
12
12
  */
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
13
+ import { readFileSync } from "fs";
14
14
  import { join, dirname } from "path";
15
15
  import { fileURLToPath } from "url";
16
- import { spawn } from "child_process";
16
+ import { log } from "./lib/cli/logger.js";
17
+ import { showHelp } from "./lib/cli/help.js";
18
+ import { initCommand } from "./lib/commands/init.js";
19
+ import { generateCommand } from "./lib/commands/generate.js";
20
+ import { validateCommand } from "./lib/commands/validate.js";
21
+ import { addCommand } from "./lib/commands/add.js";
22
+ import { infoCommand } from "./lib/commands/info.js";
23
+ import { cleanCommand } from "./lib/commands/clean.js";
17
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
25
  const packageDir = join(__dirname, "..");
19
26
  const projectRoot = process.cwd();
@@ -26,579 +33,9 @@ const cliVersion = (() => {
26
33
  return "unknown";
27
34
  }
28
35
  })();
29
- const colors = {
30
- reset: "\x1b[0m",
31
- bright: "\x1b[1m",
32
- dim: "\x1b[2m",
33
- green: "\x1b[32m",
34
- yellow: "\x1b[33m",
35
- blue: "\x1b[34m",
36
- red: "\x1b[31m",
37
- cyan: "\x1b[36m",
38
- };
39
- const log = {
40
- info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
41
- success: (msg) => console.log(`${colors.green}✅${colors.reset} ${msg}`),
42
- warn: (msg) => console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`),
43
- error: (msg) => console.log(`${colors.red}❌${colors.reset} ${msg}`),
44
- help: (msg) => console.log(` ${colors.cyan}${msg}${colors.reset}`),
45
- header: (msg) => console.log(`\n${colors.bright}${msg}${colors.reset}\n`),
46
- };
47
36
  const args = process.argv.slice(2);
48
37
  const command = args[0];
49
38
  const options = args.slice(1);
50
- const commands = {
51
- init: {
52
- description: "Initialize Swoff in current directory",
53
- usage: "swoff init [--framework react-vite|nextjs|vue-vite]",
54
- examples: ["swoff init", "swoff init --framework react-vite"],
55
- },
56
- generate: {
57
- description: "Generate service worker and supporting files",
58
- usage: "swoff generate [--sw-only|--files-only]",
59
- examples: ["swoff generate", "swoff generate --sw-only", "swoff generate --files-only"],
60
- },
61
- validate: {
62
- description: "Validate swoff.config.json",
63
- usage: "swoff validate",
64
- examples: ["swoff validate"],
65
- },
66
- add: {
67
- description: "Add specific feature files",
68
- usage: "swoff add <feature>",
69
- examples: ["swoff add offline", "swoff add pwa", "swoff add mutation-queue"],
70
- },
71
- info: {
72
- description: "Show Swoff configuration summary",
73
- usage: "swoff info",
74
- examples: ["swoff info"],
75
- },
76
- clean: {
77
- description: "Remove old versioned service worker files",
78
- usage: "swoff clean",
79
- examples: ["swoff clean"],
80
- },
81
- help: {
82
- description: "Show help information",
83
- usage: "swoff help [command]",
84
- examples: ["swoff help", "swoff help init"],
85
- },
86
- };
87
- function showHelp(commandName) {
88
- if (commandName && commands[commandName]) {
89
- const cmd = commands[commandName];
90
- log.header(`Swoff ${commandName} Command`);
91
- console.log(`Description: ${cmd.description}`);
92
- console.log(`\nUsage: ${cmd.usage}`);
93
- console.log(`\nExamples:`);
94
- cmd.examples.forEach((ex) => console.log(` ${ex}`));
95
- }
96
- else {
97
- log.header("Swoff CLI");
98
- console.log(`${colors.dim}Swoff${colors.reset} - Offline-first web apps made easy\n`);
99
- console.log(`Usage: ${colors.cyan}swoff <command> [options]${colors.reset}\n`);
100
- console.log("Commands:");
101
- Object.entries(commands).forEach(([name, cmd]) => {
102
- console.log(` ${colors.green}${name.padEnd(12)}${colors.reset} ${cmd.description}`);
103
- });
104
- console.log(`\nRun ${colors.cyan}swoff help <command>${colors.reset} for more details on a specific command.`);
105
- }
106
- }
107
- async function initCommand(framework) {
108
- log.header("Initializing Swoff");
109
- const configFiles = ["swoff.config.json", "swoff.config.js"];
110
- const existingConfig = configFiles.find((f) => existsSync(join(projectRoot, f)));
111
- if (existingConfig) {
112
- log.warn(`Found existing ${existingConfig}. Skipping init.`);
113
- log.info("To reinitialize, delete the config file first.");
114
- return;
115
- }
116
- const defaultConfig = {
117
- $schema: "https://swoff.netlify.app/schema/v1.json",
118
- enabled: true,
119
- version: "from-package",
120
- minSupportedVersion: "1.0.0",
121
- serviceWorker: {
122
- autoRegister: true,
123
- autoUpdate: false,
124
- defaultStrategy: "cache-first",
125
- strategies: {
126
- "/api/*": "network-first",
127
- "/static/*": "cache-first",
128
- },
129
- },
130
- features: {
131
- versionedSw: true,
132
- offlineReads: true,
133
- mutationQueue: false,
134
- backgroundSync: false,
135
- pwa: true,
136
- auth: false,
137
- crossTabSync: true,
138
- tagInvalidation: true,
139
- clientRegistration: true,
140
- indexeddb: false,
141
- },
142
- pwa: {
143
- preventDefaultInstall: false,
144
- },
145
- build: {
146
- outputDir: "dist",
147
- swFilename: "sw",
148
- },
149
- };
150
- if (framework === "react-vite" || framework === "nextjs") {
151
- defaultConfig.features.mutationQueue = true;
152
- defaultConfig.serviceWorker.strategies = {
153
- "/api/*": "network-first",
154
- "/static/*": "cache-first",
155
- "/assets/*": "cache-first",
156
- };
157
- }
158
- else if (framework === "vue-vite") {
159
- defaultConfig.features.mutationQueue = true;
160
- defaultConfig.serviceWorker.strategies = {
161
- "/api/*": "network-first",
162
- "/static/*": "cache-first",
163
- "/assets/*": "cache-first",
164
- };
165
- }
166
- const configPath = join(projectRoot, "swoff.config.json");
167
- writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
168
- log.success(`Created swoff.config.json`);
169
- const dirs = ["swoff"];
170
- for (const dir of dirs) {
171
- const dirPath = join(projectRoot, dir);
172
- if (!existsSync(dirPath)) {
173
- mkdirSync(dirPath, { recursive: true });
174
- log.info(`Created ${dir}/`);
175
- }
176
- }
177
- log.success("Swoff initialized successfully!");
178
- log.info(`Next steps:`);
179
- log.help("1. Review swoff.config.json and customize as needed");
180
- log.help("2. Run: swoff generate");
181
- log.help("3. Import the SW injector in your app entry point:");
182
- log.help(" import { initServiceWorker, shouldRegisterSW } from './swoff/sw-injector.js';");
183
- log.help(" if (shouldRegisterSW()) initServiceWorker();");
184
- log.help("4. Read the docs: https://swoff.netlify.app/docs");
185
- }
186
- async function generateCommand(options = {}) {
187
- const { swOnly = false, filesOnly = false, language } = options;
188
- log.header("Generating Swoff Files");
189
- const configFiles = ["swoff.config.json", "swoff.config.js"];
190
- let config = null;
191
- let configPath = null;
192
- for (const file of configFiles) {
193
- const path = join(projectRoot, file);
194
- if (existsSync(path)) {
195
- configPath = path;
196
- if (file.endsWith(".json")) {
197
- config = JSON.parse(readFileSync(path, "utf8"));
198
- }
199
- break;
200
- }
201
- }
202
- if (!config) {
203
- log.warn('No swoff.config.json found. Run "swoff init" first.');
204
- return;
205
- }
206
- log.info(`Using config: ${configPath}`);
207
- const detectedLang = language ?? detectProjectLanguage();
208
- log.info(`Detected project language: ${detectedLang}`);
209
- if (!filesOnly) {
210
- log.info("Generating service worker...");
211
- try {
212
- await runGenerator("sw-generator.js", [
213
- "--project-root", projectRoot,
214
- "--package-dir", packageDir,
215
- "--config-path", configPath,
216
- ]);
217
- }
218
- catch (err) {
219
- log.error(`Service worker generation failed: ${err instanceof Error ? err.message : String(err)}`);
220
- }
221
- }
222
- if (!swOnly) {
223
- log.info("Generating supporting files...");
224
- try {
225
- await runGenerator("swoff-files-generator.js", [
226
- "--project-root", projectRoot,
227
- "--package-dir", packageDir,
228
- "--language", detectedLang,
229
- "--config-path", configPath,
230
- ]);
231
- }
232
- catch (err) {
233
- log.error(`File generation failed: ${err instanceof Error ? err.message : String(err)}`);
234
- }
235
- }
236
- log.success("Generation complete!");
237
- log.info(`Next steps:`);
238
- log.help("1. Import the SW injector in your app entry point:");
239
- log.help(" import { initServiceWorker, shouldRegisterSW } from './swoff/sw-injector.js';");
240
- log.help(" if (shouldRegisterSW()) initServiceWorker();");
241
- log.help("2. Use the fetch wrapper for API calls:");
242
- log.help(" import { fetchWithCache } from './swoff/fetch-wrapper.js';");
243
- log.help(" const data = await fetchWithCache('/api/data', { tags: ['data'] }).then(r => r.json());");
244
- log.help("3. Add to your build script:");
245
- log.help(" \"build\": \"your-build && node swoff/sw-generator.js\"");
246
- log.help("4. Read the docs: https://swoff.netlify.app/docs");
247
- }
248
- function runGenerator(generatorName, extraArgs = []) {
249
- return new Promise((resolve, reject) => {
250
- const generatorPath = join(packageDir, "dist", "lib", "generators", generatorName);
251
- if (!existsSync(generatorPath)) {
252
- reject(new Error(`Generator not found: ${generatorPath}`));
253
- return;
254
- }
255
- const proc = spawn("node", [generatorPath, ...extraArgs], {
256
- cwd: projectRoot,
257
- stdio: "inherit",
258
- });
259
- proc.on("close", (code) => {
260
- if (code === 0)
261
- resolve();
262
- else
263
- reject(new Error(`Generator exited with code ${code}`));
264
- });
265
- proc.on("error", (err) => reject(err));
266
- });
267
- }
268
- async function validateCommand() {
269
- log.header("Validating Swoff Configuration");
270
- const configFiles = ["swoff.config.json", "swoff.config.js"];
271
- let config = null;
272
- let configPath = null;
273
- for (const file of configFiles) {
274
- const path = join(projectRoot, file);
275
- if (existsSync(path)) {
276
- configPath = path;
277
- if (file.endsWith(".json")) {
278
- try {
279
- config = JSON.parse(readFileSync(path, "utf8"));
280
- }
281
- catch (err) {
282
- log.error(`Invalid JSON in ${file}: ${err instanceof Error ? err.message : String(err)}`);
283
- return;
284
- }
285
- }
286
- break;
287
- }
288
- }
289
- if (!config) {
290
- log.warn('No swoff.config.json found. Run "swoff init" first.');
291
- return;
292
- }
293
- log.info(`Validating ${configPath}...`);
294
- const errors = [];
295
- const requiredFields = ["enabled", "version", "serviceWorker", "features", "build"];
296
- const missingFields = requiredFields.filter((field) => config[field] === undefined || config[field] === null);
297
- if (missingFields.length > 0) {
298
- errors.push(`Missing required fields: ${missingFields.join(", ")}`);
299
- }
300
- if (config.serviceWorker) {
301
- const sw = config.serviceWorker;
302
- const validStrategies = ["cache-first", "network-first", "stale-while-revalidate", "cache-only", "network-only"];
303
- if (sw.autoRegister !== undefined && typeof sw.autoRegister !== "boolean") {
304
- errors.push(`serviceWorker.autoRegister must be a boolean`);
305
- }
306
- if (sw.autoUpdate !== undefined && typeof sw.autoUpdate !== "boolean") {
307
- errors.push(`serviceWorker.autoUpdate must be a boolean`);
308
- }
309
- if (sw.defaultStrategy && !validStrategies.includes(sw.defaultStrategy)) {
310
- errors.push(`Invalid defaultStrategy "${sw.defaultStrategy}". Must be one of: ${validStrategies.join(", ")}`);
311
- }
312
- if (sw.strategies && typeof sw.strategies === "object") {
313
- const strategies = sw.strategies;
314
- for (const [pattern, strategy] of Object.entries(strategies)) {
315
- if (!validStrategies.includes(strategy)) {
316
- errors.push(`Invalid strategy "${strategy}" for pattern "${pattern}". Must be one of: ${validStrategies.join(", ")}`);
317
- }
318
- }
319
- }
320
- }
321
- if (config.features) {
322
- const features = config.features;
323
- const knownFeatures = ["versionedSw", "offlineReads", "mutationQueue", "backgroundSync", "pwa", "auth", "crossTabSync", "tagInvalidation", "clientRegistration", "indexeddb"];
324
- for (const [key, value] of Object.entries(features)) {
325
- if (!knownFeatures.includes(key)) {
326
- errors.push(`Unknown feature "${key}"`);
327
- }
328
- if (typeof value !== "boolean") {
329
- errors.push(`Feature "${key}" must be a boolean, got ${typeof value}`);
330
- }
331
- }
332
- }
333
- if (config.version && typeof config.version === "string" && config.version !== "from-package") {
334
- const semverRegex = /^\d+\.\d+\.\d+$/;
335
- if (!semverRegex.test(config.version)) {
336
- errors.push(`Invalid version "${config.version}". Must be "from-package" or semver (e.g., "1.0.0")`);
337
- }
338
- }
339
- if (config.minSupportedVersion && typeof config.minSupportedVersion === "string") {
340
- const semverRegex = /^\d+\.\d+\.\d+$/;
341
- if (!semverRegex.test(config.minSupportedVersion)) {
342
- errors.push(`Invalid minSupportedVersion "${config.minSupportedVersion}". Must be semver (e.g., "1.0.0")`);
343
- }
344
- }
345
- if (config.build) {
346
- const build = config.build;
347
- if (build.outputDir && typeof build.outputDir !== "string") {
348
- errors.push(`build.outputDir must be a string`);
349
- }
350
- if (build.swFilename && typeof build.swFilename !== "string") {
351
- errors.push(`build.swFilename must be a string`);
352
- }
353
- }
354
- if (config.pwa) {
355
- const pwa = config.pwa;
356
- if (pwa.preventDefaultInstall !== undefined && typeof pwa.preventDefaultInstall !== "boolean") {
357
- errors.push(`pwa.preventDefaultInstall must be a boolean`);
358
- }
359
- }
360
- if (config.database) {
361
- const db = config.database;
362
- if (db.name && typeof db.name !== "string") {
363
- errors.push(`database.name must be a string`);
364
- }
365
- if (db.name && typeof db.name === "string" && !/^[a-zA-Z0-9-_]+$/.test(db.name)) {
366
- errors.push(`database.name "${db.name}" must match pattern ^[a-zA-Z0-9-_]+$`);
367
- }
368
- if (db.stores && !Array.isArray(db.stores)) {
369
- errors.push(`database.stores must be an array`);
370
- }
371
- if (db.stores && Array.isArray(db.stores)) {
372
- for (const store of db.stores) {
373
- if (typeof store !== "string") {
374
- errors.push(`database.stores must contain only strings`);
375
- break;
376
- }
377
- if (!/^[a-zA-Z0-9-_]+$/.test(store)) {
378
- errors.push(`database.store "${store}" must match pattern ^[a-zA-Z0-9-_]+$`);
379
- }
380
- }
381
- }
382
- }
383
- if (errors.length > 0) {
384
- log.error(`Validation failed with ${errors.length} error(s):`);
385
- errors.forEach((e) => log.help(` - ${e}`));
386
- return;
387
- }
388
- log.success("Configuration is valid!");
389
- log.info(`\nConfig summary:`);
390
- log.help(`Version: ${config.version}`);
391
- log.help(`Default strategy: ${config.serviceWorker.defaultStrategy}`);
392
- log.help(`Features enabled: ${Object.entries(config.features)
393
- .filter(([_, v]) => v)
394
- .map(([k]) => k)
395
- .join(", ")}`);
396
- }
397
- async function addCommand(feature) {
398
- log.header(`Adding ${feature} feature`);
399
- const featureMap = {
400
- offline: { offlineReads: true },
401
- "mutation-queue": { mutationQueue: true },
402
- mutationqueue: { mutationQueue: true },
403
- pwa: { pwa: true },
404
- "cross-tab": { crossTabSync: true },
405
- crosstab: { crossTabSync: true },
406
- auth: { auth: true },
407
- "tag-invalidation": { tagInvalidation: true },
408
- taginvalidation: { tagInvalidation: true },
409
- "background-sync": { backgroundSync: true },
410
- backgroundsync: { backgroundSync: true },
411
- indexeddb: { indexeddb: true },
412
- };
413
- const configUpdate = featureMap[feature.toLowerCase()];
414
- if (!configUpdate) {
415
- log.error(`Unknown feature: ${feature}`);
416
- log.info(`Available features: offline, mutation-queue, pwa, cross-tab, auth, tag-invalidation, background-sync, indexeddb`);
417
- return;
418
- }
419
- let config = null;
420
- let configPath = join(projectRoot, "swoff.config.json");
421
- for (const file of ["swoff.config.json", "swoff.config.js"]) {
422
- const path = join(projectRoot, file);
423
- if (existsSync(path)) {
424
- configPath = path;
425
- if (file.endsWith(".json")) {
426
- config = JSON.parse(readFileSync(path, "utf8"));
427
- }
428
- break;
429
- }
430
- }
431
- if (!config) {
432
- log.warn("No config found. Creating new config with feature...");
433
- config = {
434
- $schema: "https://swoff.netlify.app/schema/v1.json",
435
- enabled: true,
436
- version: "from-package",
437
- minSupportedVersion: "0.0.0",
438
- serviceWorker: {
439
- autoRegister: true,
440
- autoUpdate: false,
441
- defaultStrategy: "cache-first",
442
- strategies: {},
443
- },
444
- features: {
445
- versionedSw: true,
446
- offlineReads: false,
447
- mutationQueue: false,
448
- backgroundSync: false,
449
- pwa: false,
450
- auth: false,
451
- crossTabSync: false,
452
- tagInvalidation: true,
453
- clientRegistration: true,
454
- indexeddb: false,
455
- },
456
- pwa: {
457
- preventDefaultInstall: false,
458
- },
459
- build: {
460
- outputDir: "dist",
461
- swFilename: "sw",
462
- },
463
- };
464
- }
465
- config.features = { ...config.features, ...configUpdate };
466
- writeFileSync(configPath, JSON.stringify(config, null, 2));
467
- log.success(`Updated swoff.config.json with ${feature} feature`);
468
- await generateCommand({ swOnly: false, filesOnly: false });
469
- log.success(`${feature} feature added successfully!`);
470
- }
471
- function detectProjectLanguage() {
472
- if (existsSync(join(projectRoot, "tsconfig.json")))
473
- return "ts";
474
- const pkgPath = join(projectRoot, "package.json");
475
- if (existsSync(pkgPath)) {
476
- try {
477
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
478
- if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript)
479
- return "ts";
480
- }
481
- catch { }
482
- }
483
- const srcDir = join(projectRoot, "src");
484
- if (existsSync(srcDir)) {
485
- const tsFiles = ["ts", "tsx"].some((ext) => {
486
- try {
487
- return readdirSync(srcDir, { withFileTypes: true }).some((entry) => entry.isFile() && entry.name.endsWith(`.${ext}`));
488
- }
489
- catch {
490
- return false;
491
- }
492
- });
493
- if (tsFiles)
494
- return "ts";
495
- }
496
- return "js";
497
- }
498
- async function infoCommand() {
499
- log.header("Swoff Configuration Summary");
500
- const configFiles = ["swoff.config.json", "swoff.config.js"];
501
- let config = null;
502
- for (const file of configFiles) {
503
- const path = join(projectRoot, file);
504
- if (existsSync(path)) {
505
- if (file.endsWith(".json")) {
506
- config = JSON.parse(readFileSync(path, "utf8"));
507
- }
508
- break;
509
- }
510
- }
511
- if (!config) {
512
- log.warn('No swoff.config.json found. Run "swoff init" first.');
513
- return;
514
- }
515
- const sw = config.serviceWorker;
516
- const features = config.features;
517
- const build = config.build;
518
- log.info(`Version: ${config.version}`);
519
- log.info(`SW Version: ${config.version === "from-package" ? "(from package.json)" : config.version}`);
520
- log.info(`Default Strategy: ${sw.defaultStrategy}`);
521
- log.info(`Auto Register: ${sw.autoRegister}`);
522
- log.info(`Auto Update: ${sw.autoUpdate}`);
523
- const enabledFeatures = Object.entries(features)
524
- .filter(([_, v]) => v)
525
- .map(([k]) => k);
526
- log.info(`\nFeatures Enabled:`);
527
- enabledFeatures.forEach((f) => log.help(` ${f}`));
528
- log.info(`\nGenerated Files:`);
529
- const swoffDir = join(projectRoot, "swoff");
530
- if (existsSync(swoffDir)) {
531
- const files = readdirSync(swoffDir);
532
- files.forEach((f) => log.help(` swoff/${f}`));
533
- }
534
- const manifestPath = join(projectRoot, "public", "manifest.json");
535
- if (existsSync(manifestPath)) {
536
- log.help(" public/manifest.json");
537
- }
538
- const outputDir = build.outputDir || "dist";
539
- const swFilename = build.swFilename || "sw";
540
- const versionPath = join(projectRoot, outputDir, "version.json");
541
- if (existsSync(versionPath)) {
542
- const versionInfo = JSON.parse(readFileSync(versionPath, "utf8"));
543
- log.info(`\nService Worker: ${outputDir}/${swFilename}-v${versionInfo.version}.js`);
544
- log.info(`Version Info: ${outputDir}/version.json`);
545
- }
546
- }
547
- async function cleanCommand() {
548
- log.header("Cleaning Old Service Worker Files");
549
- const configFiles = ["swoff.config.json", "swoff.config.js"];
550
- let config = null;
551
- for (const file of configFiles) {
552
- const path = join(projectRoot, file);
553
- if (existsSync(path)) {
554
- if (file.endsWith(".json")) {
555
- config = JSON.parse(readFileSync(path, "utf8"));
556
- }
557
- break;
558
- }
559
- }
560
- if (!config) {
561
- log.warn('No swoff.config.json found. Run "swoff init" first.');
562
- return;
563
- }
564
- const build = config.build;
565
- const outputDir = build.outputDir || "dist";
566
- const swFilename = build.swFilename || "sw";
567
- const distPath = join(projectRoot, outputDir);
568
- if (!existsSync(distPath)) {
569
- log.info(`No ${outputDir}/ directory found. Nothing to clean.`);
570
- return;
571
- }
572
- const currentVersionPath = join(distPath, "version.json");
573
- let currentVersion = null;
574
- if (existsSync(currentVersionPath)) {
575
- const versionInfo = JSON.parse(readFileSync(currentVersionPath, "utf8"));
576
- currentVersion = versionInfo.version;
577
- }
578
- const files = readdirSync(distPath);
579
- const swPattern = new RegExp(`^${swFilename}-v\\d+\\.\\d+\\.\\d+\\.js$`);
580
- const swFiles = files.filter((f) => swPattern.test(f));
581
- if (swFiles.length === 0) {
582
- log.info("No versioned service worker files found.");
583
- return;
584
- }
585
- let deleted = 0;
586
- for (const file of swFiles) {
587
- const versionMatch = file.match(/v(\d+\.\d+\.\d+)\.js$/);
588
- if (versionMatch && versionMatch[1] !== currentVersion) {
589
- const filePath = join(distPath, file);
590
- import("fs").then((fs) => fs.unlinkSync(filePath));
591
- log.info(`Deleted: ${outputDir}/${file}`);
592
- deleted++;
593
- }
594
- }
595
- if (deleted === 0) {
596
- log.info("No old service worker files to clean.");
597
- }
598
- else {
599
- log.success(`Cleaned ${deleted} old service worker file(s).`);
600
- }
601
- }
602
39
  async function main() {
603
40
  if (command === "--version" || command === "-v") {
604
41
  console.log(cliVersion);
@@ -612,17 +49,17 @@ async function main() {
612
49
  case "init": {
613
50
  const frameworkIdx = options.indexOf("--framework");
614
51
  const framework = frameworkIdx !== -1 ? options[frameworkIdx + 1] : undefined;
615
- await initCommand(framework);
52
+ await initCommand(projectRoot, framework);
616
53
  break;
617
54
  }
618
55
  case "generate": {
619
56
  const swOnly = options.includes("--sw-only");
620
57
  const filesOnly = options.includes("--files-only");
621
- await generateCommand({ swOnly, filesOnly });
58
+ await generateCommand(projectRoot, { swOnly, filesOnly });
622
59
  break;
623
60
  }
624
61
  case "validate":
625
- await validateCommand();
62
+ await validateCommand(projectRoot);
626
63
  break;
627
64
  case "add": {
628
65
  const feature = options[0];
@@ -632,23 +69,23 @@ async function main() {
632
69
  log.info("Features: offline, mutation-queue, pwa, cross-tab, auth, tag-invalidation, background-sync, indexeddb");
633
70
  process.exit(1);
634
71
  }
635
- await addCommand(feature);
72
+ await addCommand(projectRoot, feature);
636
73
  break;
637
74
  }
75
+ case "info":
76
+ await infoCommand(projectRoot);
77
+ break;
78
+ case "clean":
79
+ await cleanCommand(projectRoot);
80
+ break;
638
81
  case "help":
639
82
  case "--help":
640
83
  case "-h":
641
84
  showHelp(options[0]);
642
85
  break;
643
- case "info":
644
- await infoCommand();
645
- break;
646
- case "clean":
647
- await cleanCommand();
648
- break;
649
86
  default:
650
87
  log.error(`Unknown command: ${command}`);
651
- log.info(`Run "swoff help" for available commands`);
88
+ log.info('Run "swoff help" for available commands');
652
89
  process.exit(1);
653
90
  }
654
91
  }