@rip-lang/ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/vfs.js ADDED
@@ -0,0 +1,215 @@
1
+ // =============================================================================
2
+ // Virtual File System — Browser-local file storage
3
+ //
4
+ // Phase 1: In-memory Map-based storage (works everywhere)
5
+ // Phase 2: OPFS-backed persistence (93%+ browser support)
6
+ //
7
+ // Usage:
8
+ // const fs = vfs()
9
+ // fs.write('pages/index.rip', 'component Home\n render\n h1 "Hello"')
10
+ // fs.read('pages/index.rip') // returns source string
11
+ // fs.list('pages/') // ['index.rip', 'about.rip']
12
+ // fs.exists('pages/index.rip') // true
13
+ // fs.watch('pages/', callback) // notified on changes
14
+ //
15
+ // Author: Steve Shreeve <steve.shreeve@gmail.com>
16
+ // Date: February 2026
17
+ // =============================================================================
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Normalize paths — always forward slash, no leading slash, no trailing slash
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function normalize(path) {
24
+ return path.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
25
+ }
26
+
27
+ function dirname(path) {
28
+ const i = path.lastIndexOf('/');
29
+ return i >= 0 ? path.slice(0, i) : '';
30
+ }
31
+
32
+ function basename(path) {
33
+ const i = path.lastIndexOf('/');
34
+ return i >= 0 ? path.slice(i + 1) : path;
35
+ }
36
+
37
+ function join(...parts) {
38
+ return normalize(parts.filter(Boolean).join('/'));
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // VFS — In-memory virtual file system
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export function vfs(initial = {}) {
46
+ const files = new Map(); // path -> content (string)
47
+ const watchers = new Map(); // directory -> Set<callback>
48
+ const compiled = new Map(); // path -> { source, code } (compile cache)
49
+
50
+ // Populate initial files
51
+ for (const [path, content] of Object.entries(initial)) {
52
+ files.set(normalize(path), content);
53
+ }
54
+
55
+ // Notify watchers for a path
56
+ function notify(path, event = 'change') {
57
+ const dir = dirname(path);
58
+ const name = basename(path);
59
+
60
+ // Notify exact directory watchers
61
+ const dirWatchers = watchers.get(dir);
62
+ if (dirWatchers) {
63
+ for (const cb of dirWatchers) cb({ event, path, dir, name });
64
+ }
65
+
66
+ // Notify ancestor directory watchers (walk up to root)
67
+ let parent = dir;
68
+ while (true) {
69
+ const next = dirname(parent);
70
+ if (next === parent) break; // reached root
71
+ const pw = watchers.get(next);
72
+ if (pw) for (const cb of pw) cb({ event, path, dir, name });
73
+ parent = next;
74
+ }
75
+ // Notify root watchers (empty string key)
76
+ if (dir !== '') {
77
+ const rw = watchers.get('');
78
+ if (rw) for (const cb of rw) cb({ event, path, dir, name });
79
+ }
80
+
81
+ // Invalidate compile cache
82
+ compiled.delete(path);
83
+ }
84
+
85
+ return {
86
+ // Read a file — returns content string or undefined
87
+ read(path) {
88
+ return files.get(normalize(path));
89
+ },
90
+
91
+ // Write a file — creates intermediate directories implicitly
92
+ write(path, content) {
93
+ path = normalize(path);
94
+ const existed = files.has(path);
95
+ files.set(path, content);
96
+ notify(path, existed ? 'change' : 'create');
97
+ return content;
98
+ },
99
+
100
+ // Delete a file
101
+ delete(path) {
102
+ path = normalize(path);
103
+ if (files.has(path)) {
104
+ files.delete(path);
105
+ compiled.delete(path);
106
+ notify(path, 'delete');
107
+ return true;
108
+ }
109
+ return false;
110
+ },
111
+
112
+ // Check if a file exists
113
+ exists(path) {
114
+ return files.has(normalize(path));
115
+ },
116
+
117
+ // List files in a directory (non-recursive)
118
+ list(dir = '') {
119
+ dir = normalize(dir);
120
+ const prefix = dir ? dir + '/' : '';
121
+ const result = new Set();
122
+ for (const path of files.keys()) {
123
+ if (dir && !path.startsWith(prefix)) continue;
124
+ if (!dir && path.includes('/')) {
125
+ // Root listing — show top-level entries
126
+ result.add(path.split('/')[0]);
127
+ } else if (dir) {
128
+ const rest = path.slice(prefix.length);
129
+ if (rest.includes('/')) result.add(rest.split('/')[0]);
130
+ else result.add(rest);
131
+ } else {
132
+ result.add(path);
133
+ }
134
+ }
135
+ return [...result].sort();
136
+ },
137
+
138
+ // List all files (recursive, with optional prefix filter)
139
+ listAll(dir = '') {
140
+ dir = normalize(dir);
141
+ const prefix = dir ? dir + '/' : '';
142
+ const result = [];
143
+ for (const path of files.keys()) {
144
+ if (!dir || path.startsWith(prefix)) result.push(path);
145
+ }
146
+ return result.sort();
147
+ },
148
+
149
+ // List directories at a path
150
+ dirs(dir = '') {
151
+ dir = normalize(dir);
152
+ const prefix = dir ? dir + '/' : '';
153
+ const result = new Set();
154
+ for (const path of files.keys()) {
155
+ if (dir && !path.startsWith(prefix)) continue;
156
+ const rest = dir ? path.slice(prefix.length) : path;
157
+ if (rest.includes('/')) result.add(rest.split('/')[0]);
158
+ }
159
+ return [...result].sort();
160
+ },
161
+
162
+ // Watch a directory for changes
163
+ watch(dir, callback) {
164
+ dir = normalize(dir ?? '');
165
+ if (!watchers.has(dir)) watchers.set(dir, new Set());
166
+ watchers.get(dir).add(callback);
167
+ return () => watchers.get(dir)?.delete(callback); // unwatch
168
+ },
169
+
170
+ // Get/set compiled cache for a file
171
+ getCompiled(path) { return compiled.get(normalize(path)); },
172
+ setCompiled(path, result) { compiled.set(normalize(path), result); return result; },
173
+
174
+ // Bulk load files (from server manifest, etc.)
175
+ load(fileMap) {
176
+ for (const [path, content] of Object.entries(fileMap)) {
177
+ files.set(normalize(path), content);
178
+ }
179
+ },
180
+
181
+ // Fetch a file from a URL and store it in the VFS
182
+ async fetch(path, url) {
183
+ const response = await globalThis.fetch(url ?? '/' + path);
184
+ if (!response.ok) throw new Error(`VFS fetch failed: ${url ?? path} (${response.status})`);
185
+ const content = await response.text();
186
+ this.write(path, content);
187
+ return content;
188
+ },
189
+
190
+ // Fetch multiple files from a manifest
191
+ async fetchManifest(manifest) {
192
+ const entries = Array.isArray(manifest) ? manifest.map(p => [p, '/' + p]) : Object.entries(manifest);
193
+ await Promise.all(entries.map(([path, url]) => this.fetch(path, url)));
194
+ },
195
+
196
+ // Export all files as a plain object
197
+ toJSON() {
198
+ const obj = {};
199
+ for (const [k, v] of files) obj[k] = v;
200
+ return obj;
201
+ },
202
+
203
+ // Stats
204
+ get size() { return files.size; },
205
+
206
+ // Path utilities
207
+ normalize,
208
+ dirname,
209
+ basename,
210
+ join
211
+ };
212
+ }
213
+
214
+ // Default export
215
+ export default vfs;