@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.
package/bin/swoff ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Swoff CLI Entry Point
5
+ * Uses tsx to run TypeScript directly
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const cliPath = join(__dirname, '../src/index.ts');
14
+
15
+ const child = spawn('npx', ['tsx', cliPath, ...process.argv.slice(2)], {
16
+ stdio: 'inherit',
17
+ cwd: process.cwd()
18
+ });
19
+
20
+ child.on('exit', (code) => {
21
+ process.exit(code ?? 0);
22
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@swoff/cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "CLI for Swoff - Offline-first web apps made easy",
6
+ "bin": {
7
+ "swoff": "bin/swoff"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx src/index.ts"
11
+ },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "src/"
18
+ ],
19
+ "keywords": [
20
+ "service-worker",
21
+ "offline",
22
+ "pwa",
23
+ "caching"
24
+ ],
25
+ "author": "Swoff",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "tsx": "^4.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Swoff CLI - Main Entry Point
3
+ *
4
+ * Command-line interface for managing Swoff in your project.
5
+ *
6
+ * Usage:
7
+ * swoff init Initialize Swoff in current directory
8
+ * swoff generate Generate service worker and files
9
+ * swoff validate Validate swoff.config.json
10
+ * swoff add <feature> Add specific feature files
11
+ * swoff --help Show help
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
15
+ import { join, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { spawn } from "child_process";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const packageDir = join(__dirname, '..');
21
+ const projectRoot = process.cwd();
22
+
23
+ // Colors for console output
24
+ const colors = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ red: '\x1b[31m',
32
+ cyan: '\x1b[36m'
33
+ };
34
+
35
+ const log = {
36
+ info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
37
+ success: (msg) => console.log(`${colors.green}✅${colors.reset} ${msg}`),
38
+ warn: (msg) => console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`),
39
+ error: (msg) => console.log(`${colors.red}❌${colors.reset} ${msg}`),
40
+ help: (msg) => console.log(` ${colors.cyan}${msg}${colors.reset}`),
41
+ header: (msg) => console.log(`\n${colors.bright}${msg}${colors.reset}\n`)
42
+ };
43
+
44
+ // Parse command line arguments
45
+ const args = process.argv.slice(2);
46
+ const command = args[0];
47
+ const options = args.slice(1);
48
+
49
+ // CLI Commands
50
+ const commands = {
51
+ init: {
52
+ description: 'Initialize Swoff in current directory',
53
+ usage: 'swoff init [--framework react-vite|nextjs|vue-vite]',
54
+ examples: [
55
+ 'swoff init',
56
+ 'swoff init --framework react-vite'
57
+ ]
58
+ },
59
+ generate: {
60
+ description: 'Generate service worker and supporting files',
61
+ usage: 'swoff generate [--sw-only|--files-only]',
62
+ examples: [
63
+ 'swoff generate',
64
+ 'swoff generate --sw-only',
65
+ 'swoff generate --files-only'
66
+ ]
67
+ },
68
+ validate: {
69
+ description: 'Validate swoff.config.json',
70
+ usage: 'swoff validate',
71
+ examples: [
72
+ 'swoff validate'
73
+ ]
74
+ },
75
+ add: {
76
+ description: 'Add specific feature files',
77
+ usage: 'swoff add <feature>',
78
+ examples: [
79
+ 'swoff add offline',
80
+ 'swoff add pwa',
81
+ 'swoff add mutation-queue'
82
+ ]
83
+ },
84
+ help: {
85
+ description: 'Show help information',
86
+ usage: 'swoff help [command]',
87
+ examples: [
88
+ 'swoff help',
89
+ 'swoff help init'
90
+ ]
91
+ }
92
+ };
93
+
94
+ // Show help
95
+ function showHelp(commandName = null) {
96
+ if (commandName && commands[commandName]) {
97
+ const cmd = commands[commandName];
98
+ log.header(`Swoff ${commandName} Command`);
99
+ console.log(`Description: ${cmd.description}`);
100
+ console.log(`\nUsage: ${cmd.usage}`);
101
+ console.log(`\nExamples:`);
102
+ cmd.examples.forEach(ex => console.log(` ${ex}`));
103
+ } else {
104
+ log.header('Swoff CLI');
105
+ console.log(`${colors.dim}Swoff${colors.reset} - Offline-first web apps made easy\n`);
106
+ console.log(`Usage: ${colors.cyan}swoff <command> [options]${colors.reset}\n`);
107
+ console.log('Commands:');
108
+ Object.entries(commands).forEach(([name, cmd]) => {
109
+ console.log(` ${colors.green}${name.padEnd(12)}${colors.reset} ${cmd.description}`);
110
+ });
111
+ console.log(`\nRun ${colors.cyan}swoff help <command>${colors.reset} for more details on a specific command.`);
112
+ }
113
+ }
114
+
115
+ // Init command - Create config file and directory structure
116
+ async function initCommand(framework = null) {
117
+ log.header('Initializing Swoff');
118
+
119
+ // Check for existing config
120
+ const configFiles = ['swoff.config.json', 'swoff.config.js'];
121
+ const existingConfig = configFiles.find(f => existsSync(join(projectRoot, f)));
122
+
123
+ if (existingConfig) {
124
+ log.warn(`Found existing ${existingConfig}. Skipping init.`);
125
+ log.info('To reinitialize, delete the config file first.');
126
+ return;
127
+ }
128
+
129
+ // Create config based on framework or default
130
+ const defaultConfig = {
131
+ "$schema": "https://swoff.netlify.app/schema/v1.json",
132
+ "enabled": true,
133
+ "version": "from-package",
134
+ "minSupportedVersion": "1.0.0",
135
+ "serviceWorker": {
136
+ "autoUpdate": false,
137
+ "defaultStrategy": "cache-first",
138
+ "strategies": {
139
+ "/api/*": "network-first",
140
+ "/static/*": "cache-first"
141
+ }
142
+ },
143
+ "features": {
144
+ "versionedSw": true,
145
+ "offlineReads": true,
146
+ "mutationQueue": false,
147
+ "backgroundSync": false,
148
+ "pwa": true,
149
+ "auth": false,
150
+ "crossTabSync": true,
151
+ "tagInvalidation": true
152
+ },
153
+ "build": {
154
+ "outputDir": "dist",
155
+ "swFilename": "sw"
156
+ }
157
+ };
158
+
159
+ // Framework-specific adjustments
160
+ if (framework === 'react-vite' || framework === 'react-nextjs') {
161
+ defaultConfig.features.mutationQueue = true;
162
+ defaultConfig.serviceWorker.strategies['/assets/*'] = 'cache-first';
163
+ } else if (framework === 'vue-vite') {
164
+ defaultConfig.features.mutationQueue = true;
165
+ defaultConfig.serviceWorker.strategies['/assets/*'] = 'cache-first';
166
+ }
167
+
168
+ // Write config file
169
+ const configPath = join(projectRoot, 'swoff.config.json');
170
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
171
+ log.success(`Created swoff.config.json`);
172
+
173
+ // Create directory structure
174
+ const dirs = ['src/hooks', 'src/components', 'src/utils'];
175
+ dirs.forEach(dir => {
176
+ const dirPath = join(projectRoot, dir);
177
+ if (!existsSync(dirPath)) {
178
+ mkdirSync(dirPath, { recursive: true });
179
+ log.info(`Created ${dir}/`);
180
+ }
181
+ });
182
+
183
+ log.success('Swoff initialized successfully!');
184
+ log.info(`\nNext steps:`);
185
+ log.help('1. Review swoff.config.json and customize as needed');
186
+ log.help('2. Run: swoff generate');
187
+ log.help('3. Read the docs: https://swoff.netlify.app/docs');
188
+ }
189
+
190
+ // Generate command - Generate SW and/or files
191
+ async function generateCommand(options = {}) {
192
+ const { swOnly = false, filesOnly = false } = options;
193
+
194
+ log.header('Generating Swoff Files');
195
+
196
+ // Try to load config
197
+ const configFiles = ['swoff.config.json', 'swoff.config.js'];
198
+ let config = null;
199
+ let configPath = null;
200
+
201
+ for (const file of configFiles) {
202
+ const path = join(projectRoot, file);
203
+ if (existsSync(path)) {
204
+ configPath = path;
205
+ if (file.endsWith('.json')) {
206
+ config = JSON.parse(readFileSync(path, 'utf8'));
207
+ }
208
+ break;
209
+ }
210
+ }
211
+
212
+ if (!config) {
213
+ log.warn('No swoff.config.json found. Run "swoff init" first.');
214
+ return;
215
+ }
216
+
217
+ log.info(`Using config: ${configPath}`);
218
+
219
+ // Generate service worker
220
+ if (!filesOnly) {
221
+ log.info('Generating service worker...');
222
+ try {
223
+ await runGenerator('sw-generator.js');
224
+ } catch (err) {
225
+ log.error(`Service worker generation failed: ${err.message}`);
226
+ }
227
+ }
228
+
229
+ // Generate supporting files
230
+ if (!swOnly) {
231
+ log.info('Generating supporting files...');
232
+ try {
233
+ await runGenerator('swoff-files-generator.js', [
234
+ '--project-root', projectRoot,
235
+ '--package-dir', packageDir
236
+ ]);
237
+ } catch (err) {
238
+ log.error(`File generation failed: ${err.message}`);
239
+ }
240
+ }
241
+
242
+ log.success('Generation complete!');
243
+ }
244
+
245
+ // Helper to run generators
246
+ function runGenerator(generatorName, extraArgs = []) {
247
+ return new Promise((resolve, reject) => {
248
+ const generatorPath = join(packageDir, 'src/lib/generators', generatorName);
249
+
250
+ // Check if generator exists
251
+ if (!existsSync(generatorPath)) {
252
+ reject(new Error(`Generator not found: ${generatorPath}`));
253
+ return;
254
+ }
255
+
256
+ const proc = spawn('node', [generatorPath, ...extraArgs], {
257
+ cwd: projectRoot,
258
+ stdio: 'inherit'
259
+ });
260
+
261
+ proc.on('close', (code) => {
262
+ if (code === 0) resolve();
263
+ else reject(new Error(`Generator exited with code ${code}`));
264
+ });
265
+ proc.on('error', reject);
266
+ });
267
+ }
268
+
269
+ // Validate command - Validate config file
270
+ async function validateCommand() {
271
+ log.header('Validating Swoff Configuration');
272
+
273
+ const configFiles = ['swoff.config.json', 'swoff.config.js'];
274
+ let config = null;
275
+ let configPath = null;
276
+
277
+ for (const file of configFiles) {
278
+ const path = join(projectRoot, file);
279
+ if (existsSync(path)) {
280
+ configPath = path;
281
+ if (file.endsWith('.json')) {
282
+ try {
283
+ config = JSON.parse(readFileSync(path, 'utf8'));
284
+ } catch (err) {
285
+ log.error(`Invalid JSON in ${file}: ${err.message}`);
286
+ return;
287
+ }
288
+ }
289
+ break;
290
+ }
291
+ }
292
+
293
+ if (!config) {
294
+ log.warn('No swoff.config.json found. Run "swoff init" first.');
295
+ return;
296
+ }
297
+
298
+ log.info(`Validating ${configPath}...`);
299
+
300
+ // Validate required fields
301
+ const requiredFields = ['enabled', 'version', 'serviceWorker', 'features', 'build'];
302
+ const missingFields = requiredFields.filter(field => !config[field]);
303
+
304
+ if (missingFields.length > 0) {
305
+ log.error(`Missing required fields: ${missingFields.join(', ')}`);
306
+ return;
307
+ }
308
+
309
+ // Validate service worker config
310
+ const swRequired = ['defaultStrategy', 'autoUpdate'];
311
+ const swMissing = swRequired.filter(field => !config.serviceWorker[field]);
312
+
313
+ if (swMissing.length > 0) {
314
+ log.error(`Missing serviceWorker fields: ${swMissing.join(', ')}`);
315
+ return;
316
+ }
317
+
318
+ // Validate features
319
+ const featureDefaults = ['versionedSw', 'offlineReads', 'pwa'];
320
+ featureDefaults.forEach(feature => {
321
+ if (config.features[feature] === undefined) {
322
+ log.warn(`Feature "${feature}" not set, using default: false`);
323
+ }
324
+ });
325
+
326
+ // Validate cache strategies
327
+ const validStrategies = ['cache-first', 'network-first', 'stale-while-revalidate', 'cache-only', 'network-only'];
328
+ if (config.serviceWorker.strategies) {
329
+ for (const [pattern, strategy] of Object.entries(config.serviceWorker.strategies)) {
330
+ if (!validStrategies.includes(strategy)) {
331
+ log.error(`Invalid strategy "${strategy}" for pattern "${pattern}". Valid: ${validStrategies.join(', ')}`);
332
+ return;
333
+ }
334
+ }
335
+ }
336
+
337
+ log.success('Configuration is valid!');
338
+ log.info(`\nConfig summary:`);
339
+ log.help(`Version: ${config.version}`);
340
+ log.help(`Default strategy: ${config.serviceWorker.defaultStrategy}`);
341
+ log.help(`Features enabled: ${Object.entries(config.features).filter(([_, v]) => v).map(([k]) => k).join(', ')}`);
342
+ }
343
+
344
+ // Add command - Add specific feature files
345
+ async function addCommand(feature) {
346
+ log.header(`Adding ${feature} feature`);
347
+
348
+ // Map feature names to config updates
349
+ const featureMap = {
350
+ 'offline': { offlineReads: true },
351
+ 'mutation-queue': { mutationQueue: true },
352
+ 'mutationqueue': { mutationQueue: true },
353
+ 'pwa': { pwa: true },
354
+ 'cross-tab': { crossTabSync: true },
355
+ 'crosstab': { crossTabSync: true },
356
+ 'auth': { auth: true }
357
+ };
358
+
359
+ const configUpdate = featureMap[feature.toLowerCase()];
360
+
361
+ if (!configUpdate) {
362
+ log.error(`Unknown feature: ${feature}`);
363
+ log.info(`Available features: offline, mutation-queue, pwa, cross-tab, auth`);
364
+ return;
365
+ }
366
+
367
+ // Load or create config
368
+ let config = null;
369
+ let configPath = null;
370
+
371
+ for (const file of ['swoff.config.json', 'swoff.config.js']) {
372
+ const path = join(projectRoot, file);
373
+ if (existsSync(path)) {
374
+ configPath = path;
375
+ if (file.endsWith('.json')) {
376
+ config = JSON.parse(readFileSync(path, 'utf8'));
377
+ }
378
+ break;
379
+ }
380
+ }
381
+
382
+ if (!config) {
383
+ log.warn('No config found. Creating new config with feature...');
384
+ config = {
385
+ "$schema": "https://swoff.netlify.app/schema/v1.json",
386
+ "enabled": true,
387
+ "version": "from-package",
388
+ "minSupportedVersion": "1.0.0",
389
+ "serviceWorker": {
390
+ "autoUpdate": false,
391
+ "defaultStrategy": "cache-first",
392
+ "strategies": {}
393
+ },
394
+ "features": {
395
+ "versionedSw": true,
396
+ "offlineReads": false,
397
+ "mutationQueue": false,
398
+ "backgroundSync": false,
399
+ "pwa": false,
400
+ "auth": false,
401
+ "crossTabSync": false,
402
+ "tagInvalidation": true
403
+ },
404
+ "build": {
405
+ "outputDir": "dist",
406
+ "swFilename": "sw"
407
+ }
408
+ };
409
+ configPath = join(projectRoot, 'swoff.config.json');
410
+ }
411
+
412
+ // Update config with feature
413
+ config.features = { ...config.features, ...configUpdate };
414
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
415
+ log.success(`Updated swoff.config.json with ${feature} feature`);
416
+
417
+ // Generate files
418
+ await generateCommand({ swOnly: false, filesOnly: false });
419
+
420
+ log.success(`${feature} feature added successfully!`);
421
+ }
422
+
423
+ // Main entry point
424
+ async function main() {
425
+ if (!command) {
426
+ showHelp();
427
+ process.exit(0);
428
+ }
429
+
430
+ switch (command) {
431
+ case 'init':
432
+ const framework = options.includes('--framework')
433
+ ? options[options.indexOf('--framework') + 1]
434
+ : null;
435
+ await initCommand(framework);
436
+ break;
437
+
438
+ case 'generate':
439
+ const swOnly = options.includes('--sw-only');
440
+ const filesOnly = options.includes('--files-only');
441
+ await generateCommand({ swOnly, filesOnly });
442
+ break;
443
+
444
+ case 'validate':
445
+ await validateCommand();
446
+ break;
447
+
448
+ case 'add':
449
+ const feature = options[0];
450
+ if (!feature) {
451
+ log.error('Please specify a feature to add');
452
+ log.info('Usage: swoff add <feature>');
453
+ log.info('Features: offline, mutation-queue, pwa, cross-tab, auth');
454
+ process.exit(1);
455
+ }
456
+ await addCommand(feature);
457
+ break;
458
+
459
+ case 'help':
460
+ case '--help':
461
+ case '-h':
462
+ showHelp(options[0]);
463
+ break;
464
+
465
+ default:
466
+ log.error(`Unknown command: ${command}`);
467
+ log.info(`Run "swoff help" for available commands`);
468
+ process.exit(1);
469
+ }
470
+ }
471
+
472
+ main().catch(err => {
473
+ log.error(`Error: ${err.message}`);
474
+ process.exit(1);
475
+ });