@baaqar/artifact-to-pwa 1.0.0 → 1.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/README.md +24 -0
- package/package.json +4 -2
- package/src/detect.js +20 -3
- package/src/generator.js +10 -0
- package/src/storage.js +129 -0
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> Convert any Claude artifact (HTML / React / JSX) or public URL into an installable Progressive Web App — no build step, no Android Studio, no Xcode.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/artifact-to-pwa)
|
|
6
|
+
[](https://www.npmjs.com/package/artifact-to-pwa)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
5
10
|
## Usage
|
|
6
11
|
|
|
7
12
|
```bash
|
|
@@ -37,6 +42,25 @@ my-app-pwa/
|
|
|
37
42
|
└── README.md ← install instructions
|
|
38
43
|
```
|
|
39
44
|
|
|
45
|
+
## Persistent storage — automatic localStorage migration
|
|
46
|
+
|
|
47
|
+
Artifacts that use `localStorage` (todo lists, streak trackers, heatmaps, settings)
|
|
48
|
+
will have their data automatically migrated to **IndexedDB** when converted.
|
|
49
|
+
|
|
50
|
+
No code changes needed. The tool detects `localStorage` usage and injects a
|
|
51
|
+
transparent shim that:
|
|
52
|
+
|
|
53
|
+
- keeps the exact same `localStorage` API your artifact already uses
|
|
54
|
+
- stores all data in IndexedDB instead, which persists across PWA installs and updates
|
|
55
|
+
- pre-populates an in-memory mirror on load so reads stay synchronous
|
|
56
|
+
- briefly hides the page on startup until saved data is ready — preventing a flash of empty state
|
|
57
|
+
|
|
58
|
+
You'll see this in the CLI output when it applies:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
⚡ localStorage detected → auto-migrating to IndexedDB
|
|
62
|
+
```
|
|
63
|
+
|
|
40
64
|
## How to install the PWA
|
|
41
65
|
|
|
42
66
|
### Desktop / Android (Chrome or Edge)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baaqar/artifact-to-pwa",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Convert any Claude artifact (HTML/React/JSX) or public URL into an installable PWA — no build step required",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"installable",
|
|
20
20
|
"offline",
|
|
21
21
|
"cli",
|
|
22
|
-
"converter"
|
|
22
|
+
"converter",
|
|
23
|
+
"indexeddb",
|
|
24
|
+
"localStorage"
|
|
23
25
|
],
|
|
24
26
|
"author": "",
|
|
25
27
|
"license": "MIT",
|
package/src/detect.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasLocalStorage, getStorageShim } from './storage.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Detects whether a string of source code is:
|
|
3
5
|
* "full-html" — a complete <!DOCTYPE html> document
|
|
@@ -35,11 +37,16 @@ export function detectCodeType(code) {
|
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Wraps source code into a complete, PWA-ready index.html.
|
|
38
|
-
* Injects manifest link, theme-color meta,
|
|
40
|
+
* Injects manifest link, theme-color meta, SW registration,
|
|
41
|
+
* and — when localStorage usage is detected — an IndexedDB shim
|
|
42
|
+
* so stored data survives across origins and installs.
|
|
39
43
|
*/
|
|
40
44
|
export function wrapCode(code, { appName, themeColor }) {
|
|
41
45
|
const type = detectCodeType(code);
|
|
42
46
|
|
|
47
|
+
// Inject the shim as the FIRST script so it runs before any app code
|
|
48
|
+
const shim = hasLocalStorage(code) ? getStorageShim() : '';
|
|
49
|
+
|
|
43
50
|
const headInjects = `
|
|
44
51
|
<meta name="theme-color" content="${themeColor}">
|
|
45
52
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
@@ -61,11 +68,19 @@ export function wrapCode(code, { appName, themeColor }) {
|
|
|
61
68
|
if (type === 'full-html') {
|
|
62
69
|
let result = code;
|
|
63
70
|
|
|
64
|
-
// Inject
|
|
71
|
+
// Inject shim as very first thing inside <head> (before any other scripts)
|
|
72
|
+
if (shim) {
|
|
73
|
+
if (/<head[^>]*>/i.test(result)) {
|
|
74
|
+
result = result.replace(/(<head[^>]*>)/i, `$1\n ${shim}`);
|
|
75
|
+
} else {
|
|
76
|
+
result = result.replace(/(<html[^>]*>)/i, `$1\n<head>\n ${shim}\n</head>`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Inject PWA meta + manifest into </head>
|
|
65
81
|
if (/<\/head>/i.test(result)) {
|
|
66
82
|
result = result.replace(/<\/head>/i, ` ${headInjects}\n</head>`);
|
|
67
83
|
} else {
|
|
68
|
-
// No </head>: inject after <html> or at top
|
|
69
84
|
result = result.replace(/(<html[^>]*>)/i, `$1\n<head>\n ${headInjects}\n</head>`);
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -104,6 +119,7 @@ export function wrapCode(code, { appName, themeColor }) {
|
|
|
104
119
|
<meta charset="UTF-8">
|
|
105
120
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
106
121
|
<title>${appName}</title>
|
|
122
|
+
${shim}
|
|
107
123
|
${headInjects}
|
|
108
124
|
<!-- React + Babel (no build step) -->
|
|
109
125
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
@@ -136,6 +152,7 @@ ${renderLine}
|
|
|
136
152
|
<meta charset="UTF-8">
|
|
137
153
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
138
154
|
<title>${appName}</title>
|
|
155
|
+
${shim}
|
|
139
156
|
${headInjects}
|
|
140
157
|
${swScript}
|
|
141
158
|
</head>
|
package/src/generator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join, basename, extname } from 'path';
|
|
3
3
|
import { detectCodeType } from './detect.js';
|
|
4
|
+
import { hasLocalStorage } from './storage.js';
|
|
4
5
|
import { generateSVGIcon } from './icons.js';
|
|
5
6
|
import { buildIndexHTML, buildManifest, buildServiceWorker, buildReadme } from './templates.js';
|
|
6
7
|
|
|
@@ -75,6 +76,15 @@ export async function generatePWA(source, options) {
|
|
|
75
76
|
code = readFileSync(source, 'utf8');
|
|
76
77
|
const detectedType = detectCodeType(code);
|
|
77
78
|
console.log(chalk.gray(` ↳ Detected: `) + chalk.yellow(detectedType));
|
|
79
|
+
|
|
80
|
+
// Warn + auto-fix localStorage usage
|
|
81
|
+
if (hasLocalStorage(code)) {
|
|
82
|
+
console.log(
|
|
83
|
+
chalk.yellow(' ⚡ localStorage detected') +
|
|
84
|
+
chalk.gray(' → auto-migrating to IndexedDB (data will persist across installs)')
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
console.log();
|
|
79
89
|
}
|
|
80
90
|
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage → IndexedDB bridge
|
|
3
|
+
*
|
|
4
|
+
* Problem: localStorage is tied to the page's origin (URL).
|
|
5
|
+
* When an artifact is converted to a PWA and served from a new address,
|
|
6
|
+
* it gets a fresh, empty localStorage — all saved data is lost.
|
|
7
|
+
*
|
|
8
|
+
* Solution: replace localStorage with an IndexedDB-backed shim that:
|
|
9
|
+
* 1. Keeps reads synchronous via an in-memory mirror (same API, no refactor needed)
|
|
10
|
+
* 2. Persists all writes to IndexedDB (survives installs, updates, reboots)
|
|
11
|
+
* 3. Pre-populates the mirror on load, hiding the page briefly to prevent
|
|
12
|
+
* a flash of empty/default state before data is ready
|
|
13
|
+
*
|
|
14
|
+
* The shim is injected as the very first <script> in <head> so it runs
|
|
15
|
+
* before any application code touches window.localStorage.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns true if the source string contains any localStorage usage.
|
|
20
|
+
* @param {string} code
|
|
21
|
+
*/
|
|
22
|
+
export function hasLocalStorage(code) {
|
|
23
|
+
return /localStorage/.test(code);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a self-contained <script> block that replaces window.localStorage
|
|
28
|
+
* with an IndexedDB-backed shim. Safe to inject into any HTML document.
|
|
29
|
+
*/
|
|
30
|
+
export function getStorageShim() {
|
|
31
|
+
return `<script>
|
|
32
|
+
/* ── artifact-to-pwa: localStorage → IndexedDB shim ── */
|
|
33
|
+
(function () {
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
var DB_NAME = '__pwa_storage__';
|
|
37
|
+
var STORE = 'kv';
|
|
38
|
+
var mem = Object.create(null); // in-memory mirror (keeps reads sync)
|
|
39
|
+
var db = null;
|
|
40
|
+
|
|
41
|
+
/* Open (or create) the IndexedDB database */
|
|
42
|
+
function openDB() {
|
|
43
|
+
return new Promise(function (resolve, reject) {
|
|
44
|
+
var req = indexedDB.open(DB_NAME, 1);
|
|
45
|
+
req.onupgradeneeded = function (e) {
|
|
46
|
+
e.target.result.createObjectStore(STORE);
|
|
47
|
+
};
|
|
48
|
+
req.onsuccess = function (e) { resolve(e.target.result); };
|
|
49
|
+
req.onerror = function (e) { reject(e.target.error); };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Convenience: open a transaction and return the object store */
|
|
54
|
+
function store(mode) {
|
|
55
|
+
if (!db) return null;
|
|
56
|
+
try { return db.transaction(STORE, mode).objectStore(STORE); }
|
|
57
|
+
catch (e) { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Drop-in localStorage replacement */
|
|
61
|
+
var shim = {
|
|
62
|
+
getItem: function (k) {
|
|
63
|
+
return Object.prototype.hasOwnProperty.call(mem, k) ? mem[k] : null;
|
|
64
|
+
},
|
|
65
|
+
setItem: function (k, v) {
|
|
66
|
+
mem[k] = String(v);
|
|
67
|
+
var s = store('readwrite');
|
|
68
|
+
if (s) s.put(String(v), k);
|
|
69
|
+
},
|
|
70
|
+
removeItem: function (k) {
|
|
71
|
+
delete mem[k];
|
|
72
|
+
var s = store('readwrite');
|
|
73
|
+
if (s) s.delete(k);
|
|
74
|
+
},
|
|
75
|
+
clear: function () {
|
|
76
|
+
Object.keys(mem).forEach(function (k) { delete mem[k]; });
|
|
77
|
+
var s = store('readwrite');
|
|
78
|
+
if (s) s.clear();
|
|
79
|
+
},
|
|
80
|
+
key: function (n) {
|
|
81
|
+
return Object.keys(mem)[n] !== undefined ? Object.keys(mem)[n] : null;
|
|
82
|
+
},
|
|
83
|
+
get length() { return Object.keys(mem).length; }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/* Replace window.localStorage */
|
|
87
|
+
try {
|
|
88
|
+
Object.defineProperty(window, 'localStorage', {
|
|
89
|
+
value: shim, writable: false, configurable: false
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
window.localStorage = shim; // fallback for older engines
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Hide the page until IndexedDB data is loaded into the mirror.
|
|
97
|
+
* This prevents a flash of empty state (e.g. blank todo list, broken streak)
|
|
98
|
+
* before the async load completes.
|
|
99
|
+
*/
|
|
100
|
+
document.documentElement.style.visibility = 'hidden';
|
|
101
|
+
|
|
102
|
+
openDB().then(function (database) {
|
|
103
|
+
db = database;
|
|
104
|
+
|
|
105
|
+
var txn = db.transaction(STORE, 'readonly');
|
|
106
|
+
var st = txn.objectStore(STORE);
|
|
107
|
+
var vReq = st.getAll(); // all values
|
|
108
|
+
var kReq = st.getAllKeys(); // all keys (same order)
|
|
109
|
+
var vals, keys;
|
|
110
|
+
|
|
111
|
+
function tryReveal() {
|
|
112
|
+
if (vals === undefined || keys === undefined) return;
|
|
113
|
+
keys.forEach(function (k, i) { mem[k] = vals[i]; });
|
|
114
|
+
document.documentElement.style.visibility = '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
vReq.onsuccess = function () { vals = vReq.result; tryReveal(); };
|
|
118
|
+
kReq.onsuccess = function () { keys = kReq.result; tryReveal(); };
|
|
119
|
+
|
|
120
|
+
/* On any error, reveal anyway so the app isn't stuck invisible */
|
|
121
|
+
txn.onerror = function () {
|
|
122
|
+
document.documentElement.style.visibility = '';
|
|
123
|
+
};
|
|
124
|
+
}).catch(function () {
|
|
125
|
+
document.documentElement.style.visibility = '';
|
|
126
|
+
});
|
|
127
|
+
}());
|
|
128
|
+
</script>`;
|
|
129
|
+
}
|