@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/README.md +694 -0
- package/package.json +61 -0
- package/renderer.js +397 -0
- package/router.js +325 -0
- package/serve.rip +143 -0
- package/stash.js +413 -0
- package/ui.js +208 -0
- package/vfs.js +215 -0
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;
|