@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 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
+ [![npm version](https://img.shields.io/npm/v/artifact-to-pwa)](https://www.npmjs.com/package/artifact-to-pwa)
6
+ [![npm downloads](https://img.shields.io/npm/dw/artifact-to-pwa)](https://www.npmjs.com/package/artifact-to-pwa)
7
+ [![license](https://img.shields.io/npm/l/artifact-to-pwa)](./LICENSE)
8
+ [![node](https://img.shields.io/node/v/artifact-to-pwa)](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.0.0",
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, and SW registration.
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 into existing <head>
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
+ }