@swoff/cli 0.0.1 → 0.1.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.
- package/bin/swoff +14 -12
- package/dist/index.js +659 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/generators/sw-generator.js +621 -0
- package/dist/lib/generators/sw-generator.js.map +1 -0
- package/dist/lib/generators/swoff-files-generator.js +1387 -0
- package/dist/lib/generators/swoff-files-generator.js.map +1 -0
- package/package.json +9 -7
- package/src/lib/templates/sw-template.js +23 -6
- package/src/index.ts +0 -475
- package/src/lib/generators/sw-generator.ts +0 -367
- package/src/lib/generators/swoff-files-generator.ts +0 -461
- package/src/lib/schema/swoff-config.schema.json +0 -171
package/bin/swoff
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Swoff CLI Entry Point
|
|
5
|
-
* Uses tsx to run TypeScript directly
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
3
|
import { spawn } from 'child_process';
|
|
9
|
-
import {
|
|
4
|
+
import { existsSync } from 'fs';
|
|
10
5
|
import { dirname, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const distDir = join(binDir, '..', 'dist');
|
|
10
|
+
const entryPoint = join(distDir, 'index.js');
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (!existsSync(entryPoint)) {
|
|
13
|
+
console.error('Error: CLI not built. Run: npm run build');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const proc = spawn('node', [entryPoint, ...process.argv.slice(2)], {
|
|
16
18
|
stdio: 'inherit',
|
|
17
|
-
cwd: process.cwd()
|
|
19
|
+
cwd: process.cwd(),
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
process.exit(code
|
|
22
|
+
proc.on('exit', (code) => {
|
|
23
|
+
process.exit(code || 0);
|
|
22
24
|
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
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
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { spawn } from "child_process";
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const packageDir = join(__dirname, "..");
|
|
19
|
+
const projectRoot = process.cwd();
|
|
20
|
+
const cliVersion = (() => {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
23
|
+
return pkg.version || "unknown";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
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
|
+
const args = process.argv.slice(2);
|
|
48
|
+
const command = args[0];
|
|
49
|
+
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
|
+
async function main() {
|
|
603
|
+
if (command === "--version" || command === "-v") {
|
|
604
|
+
console.log(cliVersion);
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
if (!command) {
|
|
608
|
+
showHelp();
|
|
609
|
+
process.exit(0);
|
|
610
|
+
}
|
|
611
|
+
switch (command) {
|
|
612
|
+
case "init": {
|
|
613
|
+
const frameworkIdx = options.indexOf("--framework");
|
|
614
|
+
const framework = frameworkIdx !== -1 ? options[frameworkIdx + 1] : undefined;
|
|
615
|
+
await initCommand(framework);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "generate": {
|
|
619
|
+
const swOnly = options.includes("--sw-only");
|
|
620
|
+
const filesOnly = options.includes("--files-only");
|
|
621
|
+
await generateCommand({ swOnly, filesOnly });
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case "validate":
|
|
625
|
+
await validateCommand();
|
|
626
|
+
break;
|
|
627
|
+
case "add": {
|
|
628
|
+
const feature = options[0];
|
|
629
|
+
if (!feature) {
|
|
630
|
+
log.error("Please specify a feature to add");
|
|
631
|
+
log.info("Usage: swoff add <feature>");
|
|
632
|
+
log.info("Features: offline, mutation-queue, pwa, cross-tab, auth, tag-invalidation, background-sync, indexeddb");
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
await addCommand(feature);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case "help":
|
|
639
|
+
case "--help":
|
|
640
|
+
case "-h":
|
|
641
|
+
showHelp(options[0]);
|
|
642
|
+
break;
|
|
643
|
+
case "info":
|
|
644
|
+
await infoCommand();
|
|
645
|
+
break;
|
|
646
|
+
case "clean":
|
|
647
|
+
await cleanCommand();
|
|
648
|
+
break;
|
|
649
|
+
default:
|
|
650
|
+
log.error(`Unknown command: ${command}`);
|
|
651
|
+
log.info(`Run "swoff help" for available commands`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
main().catch((err) => {
|
|
656
|
+
log.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
});
|
|
659
|
+
//# sourceMappingURL=index.js.map
|