@swoff/cli 0.0.2 ā 0.2.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/README.md +49 -0
- package/bin/swoff +0 -0
- package/dist/__tests__/assemble-sw.test.js +145 -0
- package/dist/__tests__/assemble-sw.test.js.map +1 -0
- package/dist/__tests__/cli-integration.test.js +128 -0
- package/dist/__tests__/cli-integration.test.js.map +1 -0
- package/dist/__tests__/config-types.test.js +82 -0
- package/dist/__tests__/config-types.test.js.map +1 -0
- package/dist/__tests__/detect-language.test.js +51 -0
- package/dist/__tests__/detect-language.test.js.map +1 -0
- package/dist/__tests__/file-generators.test.js +271 -0
- package/dist/__tests__/file-generators.test.js.map +1 -0
- package/dist/__tests__/invalidation-tags.test.js +99 -0
- package/dist/__tests__/invalidation-tags.test.js.map +1 -0
- package/dist/__tests__/loader.test.js +77 -0
- package/dist/__tests__/loader.test.js.map +1 -0
- package/dist/__tests__/validator.test.js +232 -0
- package/dist/__tests__/validator.test.js.map +1 -0
- package/dist/index.js +36 -348
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/colors.js +14 -0
- package/dist/lib/cli/colors.js.map +1 -0
- package/dist/lib/cli/help.js +63 -0
- package/dist/lib/cli/help.js.map +1 -0
- package/dist/lib/cli/logger.js +13 -0
- package/dist/lib/cli/logger.js.map +1 -0
- package/dist/lib/commands/add.js +58 -0
- package/dist/lib/commands/add.js.map +1 -0
- package/dist/lib/commands/clean.js +39 -0
- package/dist/lib/commands/clean.js.map +1 -0
- package/dist/lib/commands/generate.js +84 -0
- package/dist/lib/commands/generate.js.map +1 -0
- package/dist/lib/commands/info.js +44 -0
- package/dist/lib/commands/info.js.map +1 -0
- package/dist/lib/commands/init.js +46 -0
- package/dist/lib/commands/init.js.map +1 -0
- package/dist/lib/commands/validate.js +30 -0
- package/dist/lib/commands/validate.js.map +1 -0
- package/dist/lib/config/loader.js +80 -0
- package/dist/lib/config/loader.js.map +1 -0
- package/dist/lib/config/validator.js +102 -0
- package/dist/lib/config/validator.js.map +1 -0
- package/dist/lib/generators/file-generators/background-sync.js +59 -0
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -0
- package/dist/lib/generators/file-generators/cache.js +55 -0
- package/dist/lib/generators/file-generators/cache.js.map +1 -0
- package/dist/lib/generators/file-generators/context.js +16 -0
- package/dist/lib/generators/file-generators/context.js.map +1 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js +80 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/indexeddb.js +73 -0
- package/dist/lib/generators/file-generators/indexeddb.js.map +1 -0
- package/dist/lib/generators/file-generators/invalidation-tags.js +91 -0
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -0
- package/dist/lib/generators/file-generators/manifest.js +26 -0
- package/dist/lib/generators/file-generators/manifest.js.map +1 -0
- package/dist/lib/generators/file-generators/mutation-queue.js +226 -0
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -0
- package/dist/lib/generators/file-generators/pwa-install.js +102 -0
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -0
- package/dist/lib/generators/file-generators/reconcile.js +54 -0
- package/dist/lib/generators/file-generators/reconcile.js.map +1 -0
- package/dist/lib/generators/file-generators/store.js +75 -0
- package/dist/lib/generators/file-generators/store.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +70 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-injector.js +195 -0
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-template.js +143 -0
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -0
- package/dist/lib/generators/file-generators/type-definitions.js +69 -0
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -0
- package/dist/lib/generators/sw-generator.js +37 -239
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +16 -0
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/assemble-sw.js +38 -0
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -0
- package/dist/lib/generators/sw-sections/background-sync-handler.js +106 -0
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/config-header.js +15 -0
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -0
- package/dist/lib/generators/sw-sections/default-template.js +51 -0
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -0
- package/dist/lib/generators/sw-sections/fetch-handler.js +210 -0
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/install-handler.js +35 -0
- package/dist/lib/generators/sw-sections/install-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/message-handler.js +20 -0
- package/dist/lib/generators/sw-sections/message-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +69 -0
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -0
- package/dist/lib/generators/swoff-files-generator.js +80 -1043
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +87 -0
- package/dist/lib/shared/config-types.js.map +1 -0
- package/dist/lib/utils/detect-language.js +33 -0
- package/dist/lib/utils/detect-language.js.map +1 -0
- package/package.json +6 -2
- package/src/lib/templates/sw-template.js +23 -6
|
@@ -1,1060 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Swoff Files Generator
|
|
3
3
|
*
|
|
4
|
-
* Generates
|
|
5
|
-
*
|
|
4
|
+
* Generates framework-agnostic pattern files based on swoff.config.json features.
|
|
5
|
+
* Thin orchestrator - delegates to file-generators for each template.
|
|
6
6
|
*
|
|
7
7
|
* CLI Usage:
|
|
8
|
-
* node swoff-files-generator.js --project-root <path> --
|
|
8
|
+
* node swoff-files-generator.js --project-root <path> --language <ts|js> --config-path <path>
|
|
9
|
+
*
|
|
10
|
+
* Module Usage:
|
|
11
|
+
* import { generateFiles } from './swoff-files-generator.js';
|
|
12
|
+
* const files = generateFiles(ctx, (name) => { ... });
|
|
9
13
|
*/
|
|
10
|
-
import {
|
|
14
|
+
import { join } from "path";
|
|
11
15
|
import { fileURLToPath } from "url";
|
|
12
|
-
import {
|
|
16
|
+
import { loadConfig } from "../config/loader.js";
|
|
17
|
+
import { generateSwTemplate } from "./file-generators/sw-template.js";
|
|
18
|
+
import { generateSwInjector } from "./file-generators/sw-injector.js";
|
|
19
|
+
import { generateFetchWrapper } from "./file-generators/fetch-wrapper.js";
|
|
20
|
+
import { generateCache } from "./file-generators/cache.js";
|
|
21
|
+
import { generateMutationQueue } from "./file-generators/mutation-queue.js";
|
|
22
|
+
import { generateStore } from "./file-generators/store.js";
|
|
23
|
+
import { generateReconcile } from "./file-generators/reconcile.js";
|
|
24
|
+
import { generateBackgroundSync } from "./file-generators/background-sync.js";
|
|
25
|
+
import { generateIndexedDB } from "./file-generators/indexeddb.js";
|
|
26
|
+
import { generatePwaInstall } from "./file-generators/pwa-install.js";
|
|
27
|
+
import { generateManifest } from "./file-generators/manifest.js";
|
|
28
|
+
import { generateInvalidationTags } from "./file-generators/invalidation-tags.js";
|
|
29
|
+
import { generateSwGeneratorBuild } from "./file-generators/sw-generator-build.js";
|
|
30
|
+
import { generateTypeDefinitions } from "./file-generators/type-definitions.js";
|
|
31
|
+
export function generateFiles(ctx, onFile) {
|
|
32
|
+
const steps = [
|
|
33
|
+
{ name: "sw-template", gen: () => generateSwTemplate(ctx), enabled: true },
|
|
34
|
+
{ name: "sw-injector", gen: () => generateSwInjector(ctx), enabled: ctx.config.features.clientRegistration },
|
|
35
|
+
{ name: "fetch-wrapper", gen: () => generateFetchWrapper(ctx), enabled: true },
|
|
36
|
+
{ name: "cache", gen: () => generateCache(ctx), enabled: ctx.config.features.tagInvalidation || ctx.config.features.crossTabSync },
|
|
37
|
+
{ name: "store", gen: () => generateStore(ctx), enabled: ctx.config.features.mutationQueue },
|
|
38
|
+
{ name: "reconcile", gen: () => generateReconcile(ctx), enabled: ctx.config.features.mutationQueue },
|
|
39
|
+
{ name: "mutation-queue", gen: () => generateMutationQueue(ctx), enabled: ctx.config.features.mutationQueue },
|
|
40
|
+
{ name: "background-sync", gen: () => generateBackgroundSync(ctx), enabled: ctx.config.features.backgroundSync },
|
|
41
|
+
{ name: "indexeddb", gen: () => generateIndexedDB(ctx), enabled: ctx.config.features.indexeddb },
|
|
42
|
+
{ name: "sw-generator", gen: () => generateSwGeneratorBuild(ctx), enabled: true },
|
|
43
|
+
{ name: "swoff.d.ts", gen: () => generateTypeDefinitions(ctx), enabled: ctx.ext === "ts" },
|
|
44
|
+
{ name: "pwa-install", gen: () => generatePwaInstall(ctx), enabled: ctx.config.features.pwa },
|
|
45
|
+
{ name: "manifest.json", gen: () => generateManifest(ctx), enabled: ctx.config.features.pwa },
|
|
46
|
+
{ name: "invalidation-tags", gen: () => generateInvalidationTags(ctx), enabled: ctx.config.features.tagInvalidation },
|
|
47
|
+
];
|
|
48
|
+
for (const step of steps) {
|
|
49
|
+
if (!step.enabled)
|
|
50
|
+
continue;
|
|
51
|
+
onFile?.(step.name);
|
|
52
|
+
step.gen();
|
|
53
|
+
}
|
|
54
|
+
return ctx.generatedFiles;
|
|
55
|
+
}
|
|
56
|
+
// --- CLI entry point ---
|
|
13
57
|
const args = process.argv.slice(2);
|
|
14
58
|
function getArg(name) {
|
|
15
59
|
const idx = args.findIndex((arg) => arg === `--${name}`);
|
|
16
60
|
return idx !== -1 ? args[idx + 1] : null;
|
|
17
61
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const language = getArg("language") || "ts";
|
|
21
|
-
const configPath = getArg("config-path") || join(projectRoot, "swoff.config.json");
|
|
22
|
-
const
|
|
23
|
-
enabled
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
pwa: true,
|
|
37
|
-
auth: false,
|
|
38
|
-
crossTabSync: true,
|
|
39
|
-
tagInvalidation: true,
|
|
40
|
-
clientRegistration: true,
|
|
41
|
-
},
|
|
42
|
-
build: {
|
|
43
|
-
outputDir: "dist",
|
|
44
|
-
swFilename: "sw",
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
let config = defaultConfig;
|
|
48
|
-
if (existsSync(configPath)) {
|
|
49
|
-
try {
|
|
50
|
-
config = { ...defaultConfig, ...JSON.parse(readFileSync(configPath, "utf8")) };
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
console.warn("Could not parse config, using defaults");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.log("No config found, using defaults");
|
|
58
|
-
}
|
|
59
|
-
const ext = language === "ts" ? "ts" : "js";
|
|
60
|
-
const generatedFiles = [];
|
|
61
|
-
function generateSwInjector() {
|
|
62
|
-
const outputDir = join(projectRoot, "src");
|
|
63
|
-
if (!existsSync(outputDir))
|
|
64
|
-
mkdirSync(outputDir, { recursive: true });
|
|
65
|
-
if (language === "ts") {
|
|
66
|
-
const code = `import { useEffect, useState } from 'react';
|
|
67
|
-
|
|
68
|
-
export function shouldRegister(): boolean {
|
|
69
|
-
return typeof window !== 'undefined' && 'serviceWorker' in navigator;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function useServiceWorkerRegistration(options: {
|
|
73
|
-
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
|
74
|
-
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
|
75
|
-
onError?: (error: Error) => void;
|
|
76
|
-
swPath?: string;
|
|
77
|
-
} = {}) {
|
|
78
|
-
const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
|
|
79
|
-
const [isReady, setIsReady] = useState(false);
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (!shouldRegister()) return;
|
|
83
|
-
|
|
84
|
-
const register = async () => {
|
|
85
|
-
try {
|
|
86
|
-
const swPath = options.swPath || '/sw.js';
|
|
87
|
-
const reg = await navigator.serviceWorker.register(swPath);
|
|
88
|
-
|
|
89
|
-
reg.addEventListener('updatefound', () => {
|
|
90
|
-
const newWorker = reg.installing;
|
|
91
|
-
if (newWorker) {
|
|
92
|
-
newWorker.addEventListener('statechange', () => {
|
|
93
|
-
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
94
|
-
options.onUpdate?.(reg);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (reg.active) {
|
|
101
|
-
options.onSuccess?.(reg);
|
|
102
|
-
} else {
|
|
103
|
-
reg.addEventListener('statechange', (e) => {
|
|
104
|
-
if ((e.target as ServiceWorker).state === 'activated') {
|
|
105
|
-
options.onSuccess?.(reg);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
setRegistration(reg);
|
|
111
|
-
setIsReady(true);
|
|
112
|
-
} catch (err) {
|
|
113
|
-
options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
register();
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
return { registration, isReady };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function registerServiceWorker(swPath?: string): Promise<ServiceWorkerRegistration> {
|
|
124
|
-
if (!shouldRegister()) {
|
|
125
|
-
return Promise.reject(new Error('Service workers not supported'));
|
|
126
|
-
}
|
|
127
|
-
return navigator.serviceWorker.register(swPath || '/sw.js');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function unregisterServiceWorker(): Promise<void> {
|
|
131
|
-
return navigator.serviceWorker.ready.then((reg) => {
|
|
132
|
-
return reg.unregister();
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
`;
|
|
136
|
-
writeFileSync(join(outputDir, `sw-injector.${ext}`), code);
|
|
137
|
-
generatedFiles.push(`src/sw-injector.${ext}`);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
const code = `export function shouldRegister() {
|
|
141
|
-
return typeof window !== 'undefined' && 'serviceWorker' in navigator;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function registerServiceWorker(swPath) {
|
|
145
|
-
if (!shouldRegister()) {
|
|
146
|
-
throw new Error('Service workers not supported');
|
|
147
|
-
}
|
|
148
|
-
return navigator.serviceWorker.register(swPath || '/sw.js');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function unregisterServiceWorker() {
|
|
152
|
-
const reg = await navigator.serviceWorker.ready;
|
|
153
|
-
return reg.unregister();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function useServiceWorkerRegistration(options = {}) {
|
|
157
|
-
const [registration, setRegistration] = useState(null);
|
|
158
|
-
const [isReady, setIsReady] = useState(false);
|
|
159
|
-
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
if (!shouldRegister()) return;
|
|
162
|
-
|
|
163
|
-
registerServiceWorker(options.swPath)
|
|
164
|
-
.then((reg) => {
|
|
165
|
-
setRegistration(reg);
|
|
166
|
-
setIsReady(true);
|
|
167
|
-
options.onSuccess?.(reg);
|
|
168
|
-
})
|
|
169
|
-
.catch((err) => {
|
|
170
|
-
options.onError?.(err);
|
|
171
|
-
});
|
|
172
|
-
}, []);
|
|
173
|
-
|
|
174
|
-
return { registration, isReady };
|
|
175
|
-
}
|
|
176
|
-
`;
|
|
177
|
-
writeFileSync(join(outputDir, `sw-injector.${ext}`), code);
|
|
178
|
-
generatedFiles.push(`src/sw-injector.${ext}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
function generateSwGeneratorBuildScript() {
|
|
182
|
-
const outputDir = join(projectRoot, "swoff");
|
|
183
|
-
if (!existsSync(outputDir))
|
|
184
|
-
mkdirSync(outputDir, { recursive: true });
|
|
185
|
-
if (language === "ts") {
|
|
186
|
-
const code = `#!/usr/bin/env node
|
|
187
|
-
|
|
188
|
-
import { spawn } from 'child_process';
|
|
189
|
-
import { existsSync } from 'fs';
|
|
190
|
-
import { join } from 'path';
|
|
191
|
-
|
|
192
|
-
const SWOFF_CONFIG = 'swoff.config.json';
|
|
193
|
-
const DIST_DIR = 'dist';
|
|
194
|
-
|
|
195
|
-
if (!existsSync(SWOFF_CONFIG)) {
|
|
196
|
-
console.error('Error: swoff.config.json not found');
|
|
197
|
-
console.log('Run "npx @swoff/cli init" to create one');
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log('š§ Building service worker...');
|
|
202
|
-
|
|
203
|
-
const proc = spawn(
|
|
204
|
-
'npx',
|
|
205
|
-
['@swoff/cli', 'generate', '--sw-only'],
|
|
206
|
-
{ stdio: 'inherit', shell: true }
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
proc.on('close', (code) => {
|
|
210
|
-
if (code === 0) {
|
|
211
|
-
console.log('ā
Service worker build complete');
|
|
212
|
-
} else {
|
|
213
|
-
console.error('ā Build failed');
|
|
214
|
-
process.exit(code || 1);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
`;
|
|
218
|
-
writeFileSync(join(outputDir, `sw-generator.${ext}`), code);
|
|
219
|
-
generatedFiles.push(`swoff/sw-generator.${ext}`);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
const code = `#!/usr/bin/env node
|
|
223
|
-
|
|
224
|
-
const { spawn } = require('child_process');
|
|
225
|
-
const { existsSync } = require('fs');
|
|
226
|
-
const path = require('path');
|
|
227
|
-
|
|
228
|
-
const SWOFF_CONFIG = 'swoff.config.json';
|
|
229
|
-
|
|
230
|
-
if (!existsSync(SWOFF_CONFIG)) {
|
|
231
|
-
console.error('Error: swoff.config.json not found');
|
|
232
|
-
console.log('Run "npx @swoff/cli init" to create one');
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.log('š§ Building service worker...');
|
|
237
|
-
|
|
238
|
-
const proc = spawn(
|
|
239
|
-
'npx',
|
|
240
|
-
['@swoff/cli', 'generate', '--sw-only'],
|
|
241
|
-
{ stdio: 'inherit', shell: true }
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
proc.on('close', (code) => {
|
|
245
|
-
if (code === 0) {
|
|
246
|
-
console.log('ā
Service worker build complete');
|
|
247
|
-
} else {
|
|
248
|
-
console.error('ā Build failed');
|
|
249
|
-
process.exit(code || 1);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
`;
|
|
253
|
-
writeFileSync(join(outputDir, `sw-generator.${ext}`), code);
|
|
254
|
-
generatedFiles.push(`swoff/sw-generator.${ext}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
function generateSwTemplate() {
|
|
258
|
-
const outputDir = join(projectRoot, "swoff");
|
|
259
|
-
if (!existsSync(outputDir))
|
|
260
|
-
mkdirSync(outputDir, { recursive: true });
|
|
261
|
-
const template = `let CACHE_NAME = "";
|
|
262
|
-
let ASSETS_TO_CACHE = [];
|
|
263
|
-
|
|
264
|
-
// [[INSTALL_HANDLER]]
|
|
265
|
-
// [[ACTIVATE_HANDLER]]
|
|
266
|
-
// [[FETCH_HANDLER]]
|
|
267
|
-
|
|
268
|
-
const SWOFF = {
|
|
269
|
-
cache: {
|
|
270
|
-
async get(key) {
|
|
271
|
-
const cache = await caches.open(CACHE_NAME);
|
|
272
|
-
return cache.match(key);
|
|
273
|
-
},
|
|
274
|
-
async put(request, response) {
|
|
275
|
-
const cache = await caches.open(CACHE_NAME);
|
|
276
|
-
await cache.put(request, response.clone());
|
|
277
|
-
return response;
|
|
278
|
-
},
|
|
279
|
-
async delete(key) {
|
|
280
|
-
const cache = await caches.open(CACHE_NAME);
|
|
281
|
-
return cache.delete(key);
|
|
282
|
-
},
|
|
283
|
-
async keys() {
|
|
284
|
-
const cache = await caches.open(CACHE_NAME);
|
|
285
|
-
return cache.keys();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
if (typeof self !== 'undefined') {
|
|
291
|
-
self.SWOFF = SWOFF;
|
|
292
|
-
}
|
|
293
|
-
`;
|
|
294
|
-
writeFileSync(join(outputDir, "sw-template.js"), template);
|
|
295
|
-
generatedFiles.push("swoff/sw-template.js");
|
|
296
|
-
}
|
|
297
|
-
function generateTypeDefinitions() {
|
|
298
|
-
if (language !== "ts")
|
|
299
|
-
return;
|
|
300
|
-
const outputDir = join(projectRoot, "src");
|
|
301
|
-
if (!existsSync(outputDir))
|
|
302
|
-
mkdirSync(outputDir, { recursive: true });
|
|
303
|
-
const code = `declare module '*.service-worker' {
|
|
304
|
-
const serviceWorker: ServiceWorkerGlobalScope;
|
|
305
|
-
export default serviceWorker;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
interface SWOFFCache {
|
|
309
|
-
get(key: Request | string): Promise<Response | undefined>;
|
|
310
|
-
put(request: Request | string, response: Response): Promise<Response>;
|
|
311
|
-
delete(key: Request | string): Promise<boolean>;
|
|
312
|
-
keys(): Promise<Request[]>;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
interface SWOFF {
|
|
316
|
-
cache: SWOFFCache;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
declare const SWOFF: SWOFF | undefined;
|
|
320
|
-
|
|
321
|
-
declare let CACHE_NAME: string;
|
|
322
|
-
declare let ASSETS_TO_CACHE: string[];
|
|
323
|
-
`;
|
|
324
|
-
writeFileSync(join(outputDir, "swoff.d.ts"), code);
|
|
325
|
-
generatedFiles.push("src/swoff.d.ts");
|
|
326
|
-
}
|
|
327
|
-
function generateOfflineHooks() {
|
|
328
|
-
const outputDir = join(projectRoot, "src", "hooks");
|
|
329
|
-
if (!existsSync(outputDir))
|
|
330
|
-
mkdirSync(outputDir, { recursive: true });
|
|
331
|
-
const useOffline = language === "ts"
|
|
332
|
-
? `import { useState, useEffect } from 'react';
|
|
333
|
-
|
|
334
|
-
export interface UseOfflineResult {
|
|
335
|
-
isOnline: boolean;
|
|
336
|
-
isOffline: boolean;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function useOffline(): UseOfflineResult {
|
|
340
|
-
const [isOnline, setIsOnline] = useState(() => navigator.onLine);
|
|
341
|
-
|
|
342
|
-
useEffect(() => {
|
|
343
|
-
const handleOnline = () => setIsOnline(true);
|
|
344
|
-
const handleOffline = () => setIsOnline(false);
|
|
345
|
-
|
|
346
|
-
window.addEventListener('online', handleOnline);
|
|
347
|
-
window.addEventListener('offline', handleOffline);
|
|
348
|
-
|
|
349
|
-
return () => {
|
|
350
|
-
window.removeEventListener('online', handleOnline);
|
|
351
|
-
window.removeEventListener('offline', handleOffline);
|
|
352
|
-
};
|
|
353
|
-
}, []);
|
|
354
|
-
|
|
355
|
-
return { isOnline, isOffline: !isOnline };
|
|
356
|
-
}
|
|
357
|
-
`
|
|
358
|
-
: `import { useState, useEffect } from 'react';
|
|
359
|
-
|
|
360
|
-
export function useOffline() {
|
|
361
|
-
const [isOnline, setIsOnline] = useState(() => navigator.onLine);
|
|
362
|
-
|
|
363
|
-
useEffect(() => {
|
|
364
|
-
const handleOnline = () => setIsOnline(true);
|
|
365
|
-
const handleOffline = () => setIsOnline(false);
|
|
366
|
-
|
|
367
|
-
window.addEventListener('online', handleOnline);
|
|
368
|
-
window.addEventListener('offline', handleOffline);
|
|
369
|
-
|
|
370
|
-
return () => {
|
|
371
|
-
window.removeEventListener('online', handleOnline);
|
|
372
|
-
window.removeEventListener('offline', handleOffline);
|
|
373
|
-
};
|
|
374
|
-
}, []);
|
|
375
|
-
|
|
376
|
-
return { isOnline, isOffline: !isOnline };
|
|
377
|
-
}
|
|
378
|
-
`;
|
|
379
|
-
writeFileSync(join(outputDir, `useOffline.${ext}`), useOffline);
|
|
380
|
-
generatedFiles.push(`src/hooks/useOffline.${ext}`);
|
|
381
|
-
const useApiData = language === "ts"
|
|
382
|
-
? `import { useState, useEffect, useCallback } from 'react';
|
|
383
|
-
|
|
384
|
-
export interface UseApiDataOptions extends RequestInit {
|
|
385
|
-
skip?: boolean;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export interface UseApiDataResult<T> {
|
|
389
|
-
data: T | null;
|
|
390
|
-
loading: boolean;
|
|
391
|
-
error: Error | null;
|
|
392
|
-
refetch: () => Promise<T | null>;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function useApiData<T = unknown>(
|
|
396
|
-
endpoint: string,
|
|
397
|
-
options: UseApiDataOptions = {}
|
|
398
|
-
): UseApiDataResult<T> {
|
|
399
|
-
const [data, setData] = useState<T | null>(null);
|
|
400
|
-
const [loading, setLoading] = useState(true);
|
|
401
|
-
const [error, setError] = useState<Error | null>(null);
|
|
402
|
-
|
|
403
|
-
const fetchData = useCallback(async (fetchOptions: RequestInit = {}) => {
|
|
404
|
-
try {
|
|
405
|
-
setLoading(true);
|
|
406
|
-
setError(null);
|
|
407
|
-
|
|
408
|
-
const response = await fetch(endpoint, { ...options, ...fetchOptions });
|
|
409
|
-
|
|
410
|
-
if (!response.ok) {
|
|
411
|
-
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const result: T = await response.json();
|
|
415
|
-
setData(result);
|
|
416
|
-
return result;
|
|
417
|
-
} catch (err) {
|
|
418
|
-
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
419
|
-
setError(new Error(errorMessage));
|
|
420
|
-
|
|
421
|
-
if (!navigator.onLine && data) {
|
|
422
|
-
return data;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return null;
|
|
426
|
-
} finally {
|
|
427
|
-
setLoading(false);
|
|
428
|
-
}
|
|
429
|
-
}, [endpoint, JSON.stringify(options), data]);
|
|
430
|
-
|
|
431
|
-
useEffect(() => {
|
|
432
|
-
if (!options.skip) {
|
|
433
|
-
fetchData();
|
|
434
|
-
}
|
|
435
|
-
}, [endpoint]);
|
|
436
|
-
|
|
437
|
-
return { data, loading, error, refetch: fetchData };
|
|
438
|
-
}
|
|
439
|
-
`
|
|
440
|
-
: `import { useState, useEffect, useCallback } from 'react';
|
|
441
|
-
|
|
442
|
-
export function useApiData(endpoint, options = {}) {
|
|
443
|
-
const [data, setData] = useState(null);
|
|
444
|
-
const [loading, setLoading] = useState(true);
|
|
445
|
-
const [error, setError] = useState(null);
|
|
446
|
-
|
|
447
|
-
const fetchData = useCallback(async (fetchOptions = {}) => {
|
|
448
|
-
try {
|
|
449
|
-
setLoading(true);
|
|
450
|
-
setError(null);
|
|
451
|
-
|
|
452
|
-
const response = await fetch(endpoint, { ...options, ...fetchOptions });
|
|
453
|
-
|
|
454
|
-
if (!response.ok) {
|
|
455
|
-
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const result = await response.json();
|
|
459
|
-
setData(result);
|
|
460
|
-
return result;
|
|
461
|
-
} catch (err) {
|
|
462
|
-
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
463
|
-
setError(new Error(errorMessage));
|
|
464
|
-
|
|
465
|
-
if (!navigator.onLine && data) {
|
|
466
|
-
return data;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return null;
|
|
470
|
-
} finally {
|
|
471
|
-
setLoading(false);
|
|
472
|
-
}
|
|
473
|
-
}, [endpoint, JSON.stringify(options), data]);
|
|
474
|
-
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (!options.skip) {
|
|
477
|
-
fetchData();
|
|
478
|
-
}
|
|
479
|
-
}, [endpoint]);
|
|
480
|
-
|
|
481
|
-
return { data, loading, error, refetch: fetchData };
|
|
482
|
-
}
|
|
483
|
-
`;
|
|
484
|
-
writeFileSync(join(outputDir, `useApiData.${ext}`), useApiData);
|
|
485
|
-
generatedFiles.push(`src/hooks/useApiData.${ext}`);
|
|
486
|
-
}
|
|
487
|
-
function generateMutationQueueHooks() {
|
|
488
|
-
const outputDir = join(projectRoot, "src", "hooks");
|
|
489
|
-
if (!existsSync(outputDir))
|
|
490
|
-
mkdirSync(outputDir, { recursive: true });
|
|
491
|
-
const useMutationQueue = language === "ts"
|
|
492
|
-
? `import { useState, useEffect, useCallback } from 'react';
|
|
493
|
-
|
|
494
|
-
export interface Mutation {
|
|
495
|
-
id: string;
|
|
496
|
-
timestamp: number;
|
|
497
|
-
status: 'pending' | 'synced' | 'failed';
|
|
498
|
-
retries: number;
|
|
499
|
-
data: unknown;
|
|
500
|
-
endpoint: string;
|
|
501
|
-
method?: string;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export interface UseMutationQueueOptions {
|
|
505
|
-
onSync?: (mutation: Mutation) => Promise<void>;
|
|
506
|
-
maxRetries?: number;
|
|
507
|
-
storageKey?: string;
|
|
508
|
-
autoSync?: boolean;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export interface UseMutationQueueResult {
|
|
512
|
-
queueMutation: (mutation: Omit<Mutation, 'id' | 'timestamp' | 'status' | 'retries'>) => string;
|
|
513
|
-
pendingMutations: Mutation[];
|
|
514
|
-
isSyncing: boolean;
|
|
515
|
-
syncMutations: () => Promise<void>;
|
|
516
|
-
clearQueue: () => void;
|
|
517
|
-
retryMutation: (id: string) => Promise<void>;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
export function useMutationQueue(options: UseMutationQueueOptions = {}): UseMutationQueueResult {
|
|
521
|
-
const {
|
|
522
|
-
onSync,
|
|
523
|
-
maxRetries = 3,
|
|
524
|
-
storageKey = 'swoff-mutation-queue',
|
|
525
|
-
autoSync = true
|
|
526
|
-
} = options;
|
|
527
|
-
|
|
528
|
-
const [pendingMutations, setPendingMutations] = useState<Mutation[]>([]);
|
|
529
|
-
const [isSyncing, setIsSyncing] = useState(false);
|
|
530
|
-
|
|
531
|
-
useEffect(() => {
|
|
532
|
-
try {
|
|
533
|
-
const stored = localStorage.getItem(storageKey);
|
|
534
|
-
if (stored) {
|
|
535
|
-
const parsed = JSON.parse(stored);
|
|
536
|
-
if (Array.isArray(parsed)) {
|
|
537
|
-
setPendingMutations(parsed);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} catch {
|
|
541
|
-
// ignore
|
|
542
|
-
}
|
|
543
|
-
}, [storageKey]);
|
|
544
|
-
|
|
545
|
-
useEffect(() => {
|
|
546
|
-
try {
|
|
547
|
-
localStorage.setItem(storageKey, JSON.stringify(pendingMutations));
|
|
548
|
-
} catch {
|
|
549
|
-
// ignore
|
|
550
|
-
}
|
|
551
|
-
}, [pendingMutations, storageKey]);
|
|
552
|
-
|
|
553
|
-
const queueMutation = useCallback((
|
|
554
|
-
mutation: Omit<Mutation, 'id' | 'timestamp' | 'status' | 'retries'>
|
|
555
|
-
): string => {
|
|
556
|
-
const newMutation: Mutation = {
|
|
557
|
-
id: \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
|
|
558
|
-
timestamp: Date.now(),
|
|
559
|
-
status: 'pending',
|
|
560
|
-
retries: 0,
|
|
561
|
-
...mutation
|
|
62
|
+
if (fileURLToPath(import.meta.url) === fileURLToPath(new URL(process.argv[1], "file:"))) {
|
|
63
|
+
const projectRoot = getArg("project-root") || process.cwd();
|
|
64
|
+
const language = getArg("language") || "ts";
|
|
65
|
+
const configPath = getArg("config-path") || join(projectRoot, "swoff.config.json");
|
|
66
|
+
const { config } = loadConfig(projectRoot, configPath);
|
|
67
|
+
if (!config.enabled) {
|
|
68
|
+
console.log("Config generation disabled");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
const ext = language === "ts" ? "ts" : "js";
|
|
72
|
+
const swoffDir = join(projectRoot, "swoff");
|
|
73
|
+
const generatedFiles = [];
|
|
74
|
+
const ctx = {
|
|
75
|
+
config,
|
|
76
|
+
projectRoot,
|
|
77
|
+
swoffDir,
|
|
78
|
+
ext,
|
|
79
|
+
generatedFiles,
|
|
562
80
|
};
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
syncMutations();
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return newMutation.id;
|
|
571
|
-
}, [autoSync]);
|
|
572
|
-
|
|
573
|
-
const syncMutations = useCallback(async () => {
|
|
574
|
-
if (isSyncing || !navigator.onLine) return;
|
|
575
|
-
|
|
576
|
-
const pending = pendingMutations.filter((m) => m.status === 'pending');
|
|
577
|
-
if (pending.length === 0) return;
|
|
578
|
-
|
|
579
|
-
setIsSyncing(true);
|
|
580
|
-
|
|
581
|
-
for (const mutation of pending) {
|
|
582
|
-
try {
|
|
583
|
-
if (onSync) {
|
|
584
|
-
await onSync(mutation);
|
|
585
|
-
} else {
|
|
586
|
-
await fetch(mutation.endpoint, {
|
|
587
|
-
method: mutation.method || 'POST',
|
|
588
|
-
headers: { 'Content-Type': 'application/json' },
|
|
589
|
-
body: JSON.stringify(mutation.data),
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
setPendingMutations((prev) =>
|
|
593
|
-
prev.map((m) => (m.id === mutation.id ? { ...m, status: 'synced' } : m))
|
|
594
|
-
);
|
|
595
|
-
} catch {
|
|
596
|
-
setPendingMutations((prev) =>
|
|
597
|
-
prev.map((m) =>
|
|
598
|
-
m.id === mutation.id
|
|
599
|
-
? { ...m, retries: m.retries + 1, status: m.retries >= maxRetries ? 'failed' : 'pending' }
|
|
600
|
-
: m
|
|
601
|
-
)
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
setIsSyncing(false);
|
|
607
|
-
}, [isSyncing, onSync, maxRetries, pendingMutations]);
|
|
608
|
-
|
|
609
|
-
useEffect(() => {
|
|
610
|
-
if (!autoSync) return;
|
|
611
|
-
|
|
612
|
-
const handleOnline = () => syncMutations();
|
|
613
|
-
window.addEventListener('online', handleOnline);
|
|
614
|
-
return () => window.removeEventListener('online', handleOnline);
|
|
615
|
-
}, [syncMutations, autoSync]);
|
|
616
|
-
|
|
617
|
-
const clearQueue = useCallback(() => {
|
|
618
|
-
setPendingMutations([]);
|
|
619
|
-
localStorage.removeItem(storageKey);
|
|
620
|
-
}, [storageKey]);
|
|
621
|
-
|
|
622
|
-
const retryMutation = useCallback(async (id: string) => {
|
|
623
|
-
setPendingMutations((prev) =>
|
|
624
|
-
prev.map((m) => (m.id === id ? { ...m, status: 'pending', retries: 0 } : m))
|
|
625
|
-
);
|
|
626
|
-
await syncMutations();
|
|
627
|
-
}, [syncMutations]);
|
|
628
|
-
|
|
629
|
-
return { queueMutation, pendingMutations, isSyncing, syncMutations, clearQueue, retryMutation };
|
|
630
|
-
}
|
|
631
|
-
`
|
|
632
|
-
: `import { useState, useEffect, useCallback } from 'react';
|
|
633
|
-
|
|
634
|
-
export function useMutationQueue(options = {}) {
|
|
635
|
-
const {
|
|
636
|
-
onSync,
|
|
637
|
-
maxRetries = 3,
|
|
638
|
-
storageKey = 'swoff-mutation-queue',
|
|
639
|
-
autoSync = true
|
|
640
|
-
} = options;
|
|
641
|
-
|
|
642
|
-
const [pendingMutations, setPendingMutations] = useState([]);
|
|
643
|
-
const [isSyncing, setIsSyncing] = useState(false);
|
|
644
|
-
|
|
645
|
-
useEffect(() => {
|
|
646
|
-
try {
|
|
647
|
-
const stored = localStorage.getItem(storageKey);
|
|
648
|
-
if (stored) {
|
|
649
|
-
const parsed = JSON.parse(stored);
|
|
650
|
-
if (Array.isArray(parsed)) {
|
|
651
|
-
setPendingMutations(parsed);
|
|
81
|
+
const ttyStatus = process.stdout.isTTY
|
|
82
|
+
? (msg) => {
|
|
83
|
+
const cols = process.stdout.columns || 80;
|
|
84
|
+
process.stdout.write(`\r${" ".repeat(cols - 1)}\r ${msg}`);
|
|
652
85
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
useEffect(() => {
|
|
660
|
-
try {
|
|
661
|
-
localStorage.setItem(storageKey, JSON.stringify(pendingMutations));
|
|
662
|
-
} catch {
|
|
663
|
-
// ignore
|
|
664
|
-
}
|
|
665
|
-
}, [pendingMutations, storageKey]);
|
|
666
|
-
|
|
667
|
-
const queueMutation = useCallback((mutation) => {
|
|
668
|
-
const newMutation = {
|
|
669
|
-
id: \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
|
|
670
|
-
timestamp: Date.now(),
|
|
671
|
-
status: 'pending',
|
|
672
|
-
retries: 0,
|
|
673
|
-
...mutation
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
setPendingMutations((prev) => [...prev, newMutation]);
|
|
677
|
-
|
|
678
|
-
if (autoSync && navigator.onLine) {
|
|
679
|
-
syncMutations();
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return newMutation.id;
|
|
683
|
-
}, [autoSync]);
|
|
684
|
-
|
|
685
|
-
const syncMutations = useCallback(async () => {
|
|
686
|
-
if (isSyncing || !navigator.onLine) return;
|
|
687
|
-
|
|
688
|
-
const pending = pendingMutations.filter((m) => m.status === 'pending');
|
|
689
|
-
if (pending.length === 0) return;
|
|
690
|
-
|
|
691
|
-
setIsSyncing(true);
|
|
692
|
-
|
|
693
|
-
for (const mutation of pending) {
|
|
694
|
-
try {
|
|
695
|
-
if (onSync) {
|
|
696
|
-
await onSync(mutation);
|
|
697
|
-
} else {
|
|
698
|
-
await fetch(mutation.endpoint, {
|
|
699
|
-
method: mutation.method || 'POST',
|
|
700
|
-
headers: { 'Content-Type': 'application/json' },
|
|
701
|
-
body: JSON.stringify(mutation.data),
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
setPendingMutations((prev) =>
|
|
705
|
-
prev.map((m) => (m.id === mutation.id ? { ...m, status: 'synced' } : m))
|
|
706
|
-
);
|
|
707
|
-
} catch {
|
|
708
|
-
setPendingMutations((prev) =>
|
|
709
|
-
prev.map((m) =>
|
|
710
|
-
m.id === mutation.id
|
|
711
|
-
? { ...m, retries: m.retries + 1, status: m.retries >= maxRetries ? 'failed' : 'pending' }
|
|
712
|
-
: m
|
|
713
|
-
)
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
setIsSyncing(false);
|
|
719
|
-
}, [isSyncing, onSync, maxRetries, pendingMutations]);
|
|
720
|
-
|
|
721
|
-
useEffect(() => {
|
|
722
|
-
if (!autoSync) return;
|
|
723
|
-
|
|
724
|
-
const handleOnline = () => syncMutations();
|
|
725
|
-
window.addEventListener('online', handleOnline);
|
|
726
|
-
return () => window.removeEventListener('online', handleOnline);
|
|
727
|
-
}, [syncMutations, autoSync]);
|
|
728
|
-
|
|
729
|
-
const clearQueue = useCallback(() => {
|
|
730
|
-
setPendingMutations([]);
|
|
731
|
-
localStorage.removeItem(storageKey);
|
|
732
|
-
}, [storageKey]);
|
|
733
|
-
|
|
734
|
-
const retryMutation = useCallback(async (id) => {
|
|
735
|
-
setPendingMutations((prev) =>
|
|
736
|
-
prev.map((m) => (m.id === id ? { ...m, status: 'pending', retries: 0 } : m))
|
|
737
|
-
);
|
|
738
|
-
await syncMutations();
|
|
739
|
-
}, [syncMutations]);
|
|
740
|
-
|
|
741
|
-
return { queueMutation, pendingMutations, isSyncing, syncMutations, clearQueue, retryMutation };
|
|
742
|
-
}
|
|
743
|
-
`;
|
|
744
|
-
writeFileSync(join(outputDir, `useMutationQueue.${ext}`), useMutationQueue);
|
|
745
|
-
generatedFiles.push(`src/hooks/useMutationQueue.${ext}`);
|
|
746
|
-
}
|
|
747
|
-
function generatePWAComponents() {
|
|
748
|
-
const outputDir = join(projectRoot, "src", "components");
|
|
749
|
-
if (!existsSync(outputDir))
|
|
750
|
-
mkdirSync(outputDir, { recursive: true });
|
|
751
|
-
const offlineIndicator = `import { useOffline } from '../hooks/useOffline';
|
|
752
|
-
|
|
753
|
-
export interface OfflineIndicatorProps {
|
|
754
|
-
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
755
|
-
message?: string;
|
|
756
|
-
className?: string;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const positionClasses = {
|
|
760
|
-
'top-left': 'top-4 left-4',
|
|
761
|
-
'top-right': 'top-4 right-4',
|
|
762
|
-
'bottom-left': 'bottom-4 left-4',
|
|
763
|
-
'bottom-right': 'bottom-4 right-4'
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
export function OfflineIndicator({
|
|
767
|
-
position = 'bottom-right',
|
|
768
|
-
message = 'You are offline. Some features may be limited.',
|
|
769
|
-
className = ''
|
|
770
|
-
}: OfflineIndicatorProps) {
|
|
771
|
-
const { isOffline } = useOffline();
|
|
772
|
-
|
|
773
|
-
if (!isOffline) return null;
|
|
774
|
-
|
|
775
|
-
return (
|
|
776
|
-
<div
|
|
777
|
-
className={\`fixed \${positionClasses[position]} bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 flex items-center gap-2 \${className}\`}
|
|
778
|
-
role="alert"
|
|
779
|
-
aria-live="polite"
|
|
780
|
-
>
|
|
781
|
-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
782
|
-
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd"/>
|
|
783
|
-
</svg>
|
|
784
|
-
<span className="text-sm font-medium">{message}</span>
|
|
785
|
-
</div>
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
`;
|
|
789
|
-
writeFileSync(join(outputDir, "OfflineIndicator.tsx"), offlineIndicator);
|
|
790
|
-
generatedFiles.push("src/components/OfflineIndicator.tsx");
|
|
791
|
-
const pwaInstallButton = `import { useState, useEffect } from 'react';
|
|
792
|
-
|
|
793
|
-
export interface PWAInstallButtonProps {
|
|
794
|
-
installLabel?: string;
|
|
795
|
-
installedLabel?: string;
|
|
796
|
-
className?: string;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
export function PWAInstallButton({
|
|
800
|
-
installLabel = 'Install App',
|
|
801
|
-
installedLabel = 'Installed',
|
|
802
|
-
className = ''
|
|
803
|
-
}: PWAInstallButtonProps) {
|
|
804
|
-
const [isInstallable, setIsInstallable] = useState(false);
|
|
805
|
-
const [isInstalled, setIsInstalled] = useState(false);
|
|
806
|
-
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
807
|
-
|
|
808
|
-
useEffect(() => {
|
|
809
|
-
const handleBeforeInstallPrompt = (e: Event) => {
|
|
810
|
-
e.preventDefault();
|
|
811
|
-
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
|
812
|
-
setIsInstallable(true);
|
|
813
|
-
};
|
|
814
|
-
|
|
815
|
-
const handleAppInstalled = () => {
|
|
816
|
-
setIsInstalled(true);
|
|
817
|
-
setIsInstallable(false);
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
821
|
-
window.addEventListener('appinstalled', handleAppInstalled);
|
|
822
|
-
|
|
823
|
-
return () => {
|
|
824
|
-
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
825
|
-
window.removeEventListener('appinstalled', handleAppInstalled);
|
|
826
|
-
};
|
|
827
|
-
}, []);
|
|
828
|
-
|
|
829
|
-
const handleInstall = async () => {
|
|
830
|
-
if (!deferredPrompt) return;
|
|
831
|
-
|
|
832
|
-
await deferredPrompt.prompt();
|
|
833
|
-
const { outcome } = await deferredPrompt.userChoice;
|
|
834
|
-
|
|
835
|
-
if (outcome === 'accepted') {
|
|
836
|
-
setIsInstalled(true);
|
|
837
|
-
}
|
|
838
|
-
setIsInstallable(false);
|
|
839
|
-
setDeferredPrompt(null);
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
if (!isInstallable && !isInstalled) return null;
|
|
843
|
-
|
|
844
|
-
return (
|
|
845
|
-
<button
|
|
846
|
-
onClick={handleInstall}
|
|
847
|
-
disabled={isInstalled}
|
|
848
|
-
className={\`fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-600 disabled:bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg font-medium z-50 \${className}\`}
|
|
849
|
-
>
|
|
850
|
-
{isInstalled ? installedLabel : installLabel}
|
|
851
|
-
</button>
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
interface BeforeInstallPromptEvent extends Event {
|
|
856
|
-
prompt(): Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
|
857
|
-
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
|
858
|
-
}
|
|
859
|
-
`;
|
|
860
|
-
writeFileSync(join(outputDir, "PWAInstallButton.tsx"), pwaInstallButton);
|
|
861
|
-
generatedFiles.push("src/components/PWAInstallButton.tsx");
|
|
862
|
-
}
|
|
863
|
-
function generateCrossTabSync() {
|
|
864
|
-
const outputDir = join(projectRoot, "src", "utils");
|
|
865
|
-
if (!existsSync(outputDir))
|
|
866
|
-
mkdirSync(outputDir, { recursive: true });
|
|
867
|
-
const crossTabSync = language === "ts"
|
|
868
|
-
? `export class CrossTabSync {
|
|
869
|
-
private channel: BroadcastChannel;
|
|
870
|
-
private listeners: Map<string, Set<(value: unknown, type: string) => void>>;
|
|
871
|
-
|
|
872
|
-
constructor(channelName = 'swoff-sync') {
|
|
873
|
-
this.channel = new BroadcastChannel(channelName);
|
|
874
|
-
this.listeners = new Map();
|
|
875
|
-
|
|
876
|
-
this.channel.onmessage = (event: MessageEvent) => {
|
|
877
|
-
const { key, value, type } = event.data;
|
|
878
|
-
const callbacks = this.listeners.get(key);
|
|
879
|
-
if (callbacks) {
|
|
880
|
-
callbacks.forEach((callback) => callback(value, type));
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
subscribe(
|
|
886
|
-
key: string,
|
|
887
|
-
callback: (value: unknown, type: string) => void
|
|
888
|
-
): () => void {
|
|
889
|
-
if (!this.listeners.has(key)) {
|
|
890
|
-
this.listeners.set(key, new Set());
|
|
891
|
-
}
|
|
892
|
-
this.listeners.get(key)!.add(callback);
|
|
893
|
-
|
|
894
|
-
return () => {
|
|
895
|
-
this.listeners.get(key)?.delete(callback);
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
set(key: string, value: unknown): void {
|
|
900
|
-
try {
|
|
901
|
-
localStorage.setItem(\`swoff-sync-\${key}\`, JSON.stringify(value));
|
|
902
|
-
} catch {
|
|
903
|
-
// ignore
|
|
86
|
+
: (msg) => console.log(` ${msg}`);
|
|
87
|
+
console.log(`Generating Swoff files (${language})...`);
|
|
88
|
+
generateFiles(ctx, (name) => ttyStatus(`ā ${name}...`));
|
|
89
|
+
if (process.stdout.isTTY) {
|
|
90
|
+
const cols = process.stdout.columns || 80;
|
|
91
|
+
process.stdout.write(`\r${" ".repeat(cols - 1)}\r`);
|
|
904
92
|
}
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
get(key: string): unknown {
|
|
909
|
-
try {
|
|
910
|
-
const stored = localStorage.getItem(\`swoff-sync-\${key}\`);
|
|
911
|
-
return stored ? JSON.parse(stored) : null;
|
|
912
|
-
} catch {
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
delete(key: string): void {
|
|
918
|
-
localStorage.removeItem(\`swoff-sync-\${key}\`);
|
|
919
|
-
this.channel.postMessage({ type: 'delete', key, value: null });
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
close(): void {
|
|
923
|
-
this.channel.close();
|
|
924
|
-
this.listeners.clear();
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
export function createCrossTabSync(channelName = 'swoff-sync'): CrossTabSync {
|
|
929
|
-
return new CrossTabSync(channelName);
|
|
930
|
-
}
|
|
931
|
-
`
|
|
932
|
-
: `export class CrossTabSync {
|
|
933
|
-
constructor(channelName = 'swoff-sync') {
|
|
934
|
-
this.channel = new BroadcastChannel(channelName);
|
|
935
|
-
this.listeners = new Map();
|
|
936
|
-
|
|
937
|
-
this.channel.onmessage = (event) => {
|
|
938
|
-
const { key, value, type } = event.data;
|
|
939
|
-
const callbacks = this.listeners.get(key);
|
|
940
|
-
if (callbacks) {
|
|
941
|
-
callbacks.forEach((callback) => callback(value, type));
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
subscribe(key, callback) {
|
|
947
|
-
if (!this.listeners.has(key)) {
|
|
948
|
-
this.listeners.set(key, new Set());
|
|
949
|
-
}
|
|
950
|
-
this.listeners.get(key).add(callback);
|
|
951
|
-
|
|
952
|
-
return () => {
|
|
953
|
-
this.listeners.get(key)?.delete(callback);
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
set(key, value) {
|
|
958
|
-
try {
|
|
959
|
-
localStorage.setItem(\`swoff-sync-\${key}\`, JSON.stringify(value));
|
|
960
|
-
} catch {
|
|
961
|
-
// ignore
|
|
962
|
-
}
|
|
963
|
-
this.channel.postMessage({ type: 'set', key, value });
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
get(key) {
|
|
967
|
-
try {
|
|
968
|
-
const stored = localStorage.getItem(\`swoff-sync-\${key}\`);
|
|
969
|
-
return stored ? JSON.parse(stored) : null;
|
|
970
|
-
} catch {
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
delete(key) {
|
|
976
|
-
localStorage.removeItem(\`swoff-sync-\${key}\`);
|
|
977
|
-
this.channel.postMessage({ type: 'delete', key, value: null });
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
close() {
|
|
981
|
-
this.channel.close();
|
|
982
|
-
this.listeners.clear();
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
export function createCrossTabSync(channelName = 'swoff-sync') {
|
|
987
|
-
return new CrossTabSync(channelName);
|
|
988
|
-
}
|
|
989
|
-
`;
|
|
990
|
-
writeFileSync(join(outputDir, `crossTabSync.${ext}`), crossTabSync);
|
|
991
|
-
generatedFiles.push(`src/utils/crossTabSync.${ext}`);
|
|
992
|
-
}
|
|
993
|
-
function generateManifest() {
|
|
994
|
-
const outputDir = join(projectRoot, "public");
|
|
995
|
-
if (!existsSync(outputDir))
|
|
996
|
-
mkdirSync(outputDir, { recursive: true });
|
|
997
|
-
const manifest = {
|
|
998
|
-
name: 'Swoff App',
|
|
999
|
-
short_name: 'Swoff',
|
|
1000
|
-
description: 'Offline-first web application',
|
|
1001
|
-
start_url: '/',
|
|
1002
|
-
display: 'standalone',
|
|
1003
|
-
background_color: '#ffffff',
|
|
1004
|
-
theme_color: '#000000',
|
|
1005
|
-
icons: [
|
|
1006
|
-
{
|
|
1007
|
-
src: '/icon-192.png',
|
|
1008
|
-
sizes: '192x192',
|
|
1009
|
-
type: 'image/png',
|
|
1010
|
-
},
|
|
1011
|
-
{
|
|
1012
|
-
src: '/icon-512.png',
|
|
1013
|
-
sizes: '512x512',
|
|
1014
|
-
type: 'image/png',
|
|
1015
|
-
},
|
|
1016
|
-
],
|
|
1017
|
-
};
|
|
1018
|
-
writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1019
|
-
generatedFiles.push("public/manifest.json");
|
|
1020
|
-
}
|
|
1021
|
-
console.log('š§ Swoff Files Generator');
|
|
1022
|
-
console.log('========================\n');
|
|
1023
|
-
console.log(`Language: ${language}`);
|
|
1024
|
-
console.log(`Project: ${projectRoot}`);
|
|
1025
|
-
console.log('');
|
|
1026
|
-
if (!config.enabled) {
|
|
1027
|
-
console.log('ā ļø Config generation disabled');
|
|
1028
|
-
process.exit(0);
|
|
1029
|
-
}
|
|
1030
|
-
console.log('š¦ Generating pattern files...');
|
|
1031
|
-
generateSwInjector();
|
|
1032
|
-
console.log(' ⢠sw-injector');
|
|
1033
|
-
generateSwGeneratorBuildScript();
|
|
1034
|
-
console.log(' ⢠sw-generator build script');
|
|
1035
|
-
generateSwTemplate();
|
|
1036
|
-
console.log(' ⢠sw-template.js');
|
|
1037
|
-
generateTypeDefinitions();
|
|
1038
|
-
console.log(' ⢠swoff.d.ts');
|
|
1039
|
-
if (config.features.offlineReads) {
|
|
1040
|
-
generateOfflineHooks();
|
|
1041
|
-
console.log(' ⢠offline hooks (useOffline, useApiData)');
|
|
1042
|
-
}
|
|
1043
|
-
if (config.features.mutationQueue) {
|
|
1044
|
-
generateMutationQueueHooks();
|
|
1045
|
-
console.log(' ⢠mutation queue hook (useMutationQueue)');
|
|
1046
|
-
}
|
|
1047
|
-
if (config.features.pwa) {
|
|
1048
|
-
generatePWAComponents();
|
|
1049
|
-
console.log(' ⢠PWA components (OfflineIndicator, PWAInstallButton)');
|
|
1050
|
-
generateManifest();
|
|
1051
|
-
console.log(' ⢠manifest.json');
|
|
1052
|
-
}
|
|
1053
|
-
if (config.features.crossTabSync) {
|
|
1054
|
-
generateCrossTabSync();
|
|
1055
|
-
console.log(' ⢠cross-tab sync (crossTabSync)');
|
|
93
|
+
console.log("Generated files:");
|
|
94
|
+
generatedFiles.forEach((file) => console.log(` ${file}`));
|
|
95
|
+
console.log(`\nTotal: ${generatedFiles.length} files`);
|
|
1056
96
|
}
|
|
1057
|
-
console.log('\nā
Generated files:');
|
|
1058
|
-
generatedFiles.forEach((file) => console.log(` ⢠${file}`));
|
|
1059
|
-
console.log("\n⨠Total: " + generatedFiles.length + " files generated");
|
|
1060
97
|
//# sourceMappingURL=swoff-files-generator.js.map
|