@lerret/cli 0.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/dist-studio/.bundle-stamp +34 -0
  3. package/dist-studio/assets/asset-runtime-MFjDKvQD.js +129 -0
  4. package/dist-studio/assets/cli-project-source-9dNA_gVa.js +1 -0
  5. package/dist-studio/assets/dev-harness-BH6a8T7l.js +18 -0
  6. package/dist-studio/assets/hosted-project-source-dVGq_8c6.js +135 -0
  7. package/dist-studio/assets/index-BNmJ8c2t.css +1 -0
  8. package/dist-studio/assets/index-EslqdOhg.js +10 -0
  9. package/dist-studio/assets/leaf-marker-command.png +0 -0
  10. package/dist-studio/assets/leaf-marker-comment-box.png +0 -0
  11. package/dist-studio/assets/leaf-marker-homescreen.png +0 -0
  12. package/dist-studio/assets/leafmarker-icon-dark-128.png +0 -0
  13. package/dist-studio/assets/leafmarker-logo-transparent.png +0 -0
  14. package/dist-studio/assets/leafmarker-logo.png +0 -0
  15. package/dist-studio/assets/lerret-logo.png +0 -0
  16. package/dist-studio/assets/lerret-wordmark.svg +3 -0
  17. package/dist-studio/assets/logo-angular.svg +1 -0
  18. package/dist-studio/assets/logo-claude.svg +7 -0
  19. package/dist-studio/assets/logo-codex.svg +1 -0
  20. package/dist-studio/assets/logo-cursor.svg +1 -0
  21. package/dist-studio/assets/logo-javascript.svg +1 -0
  22. package/dist-studio/assets/logo-react.svg +1 -0
  23. package/dist-studio/assets/logo-svelte.svg +1 -0
  24. package/dist-studio/assets/logo-vue.svg +1 -0
  25. package/dist-studio/assets/open-folder-D5OR7eLb.js +8 -0
  26. package/dist-studio/assets/project-studio-BjNaIuRb.js +795 -0
  27. package/dist-studio/assets/project-studio-CKuMOMsC.css +1 -0
  28. package/dist-studio/assets/superwhisper-logo.png +0 -0
  29. package/dist-studio/index.html +47 -0
  30. package/dist-studio/module-sw.js +275 -0
  31. package/package.json +51 -0
  32. package/src/dev.js +373 -0
  33. package/src/export.js +1386 -0
  34. package/src/fs/node-backend.js +631 -0
  35. package/src/lerret.js +143 -0
  36. package/src/resolve-project.js +178 -0
  37. package/src/vite-plugin-lerret-project.js +986 -0
  38. package/src/watcher.js +214 -0
@@ -0,0 +1 @@
1
+ .lm-menu-popover{padding:var(--lm-space-1);background:var(--lm-popover-bg);-webkit-backdrop-filter:blur(16px)saturate(120%);border:1px solid var(--lm-popover-border);border-radius:var(--lm-radius-lg);min-width:200px;max-width:320px;max-height:320px;box-shadow:var(--lm-shadow-popup);z-index:80;font-family:var(--lm-font-sans);color:var(--lm-text-primary);animation:lm-menu-open var(--lm-duration-fast) var(--lm-ease) both;outline:none;margin:0;list-style:none;position:fixed;overflow-y:auto}@keyframes lm-menu-open{0%{opacity:0;transform:scale(.96)translateY(4px)}to{opacity:1;transform:scale(1)translateY(0)}}@media (prefers-reduced-motion:reduce){.lm-menu-popover{animation:none}}.lm-menu-item{align-items:center;gap:var(--lm-space-2);width:100%;padding:var(--lm-space-2) var(--lm-space-3);border-radius:var(--lm-radius-md);color:var(--lm-text-primary);font-family:var(--lm-font-sans);font-size:var(--lm-size-body);font-weight:var(--lm-weight-medium);line-height:var(--lm-lh-body);text-align:left;cursor:pointer;transition:background var(--lm-duration-fast), box-shadow var(--lm-duration-fast);background:0 0;border:none;outline:none;list-style:none;display:flex}@media (prefers-reduced-motion:reduce){.lm-menu-item{transition:none}}.lm-menu-item[data-active=true]{background:var(--lm-accent-light);box-shadow:inset 0 0 0 1.5px var(--lm-accent)}.lm-menu-item:hover:not([aria-disabled=true]){background:var(--lm-bg-tertiary)}.lm-menu-item[aria-disabled=true]{color:var(--lm-text-muted);cursor:not-allowed}.lm-menu-item-icon{opacity:.7;font-size:var(--lm-size-body-sm);flex:none;display:inline-flex}.lm-menu-item-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.lm-menu-item-reason{font-size:var(--lm-size-body-sm);color:var(--lm-text-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:none;max-width:160px;overflow:hidden}.lm-menu-separator{height:1px;margin:var(--lm-space-1) var(--lm-space-3);background:var(--lm-border-light);list-style:none}.lm-field{gap:var(--lm-space-1,4px);font-family:var(--lm-font-sans);flex-direction:column;display:flex}.lm-field__label-row{align-items:center;gap:var(--lm-space-1,4px);display:flex}.lm-field__label{font-size:var(--lm-size-hint,10px);font-weight:var(--lm-weight-semibold,600);color:var(--lm-text-tertiary,#6e6960);text-transform:uppercase;letter-spacing:var(--lm-tracking-caps,.5px);line-height:1}.lm-field__required{color:var(--lm-error,#a8412b);font-size:var(--lm-size-hint,10px);-webkit-user-select:none;user-select:none;line-height:1}.lm-field__description{font-size:var(--lm-size-body-sm,12px);color:var(--lm-text-muted,#b8b3a8);line-height:var(--lm-lh-body,1.45);margin:0}.lm-field__invalid-msg{align-items:center;gap:var(--lm-space-1,4px);font-size:var(--lm-size-body-sm,12px);color:var(--lm-error,#a8412b);line-height:var(--lm-lh-body,1.45);display:flex}.lm-field__invalid-icon{flex-shrink:0;width:12px;height:12px}.lm-input{box-sizing:border-box;width:100%;padding:var(--lm-space-1,4px) var(--lm-space-2,8px);font-family:var(--lm-font-sans);font-size:var(--lm-size-body,13px);font-weight:var(--lm-weight-regular,400);color:var(--lm-text-primary,#1a1714);background:var(--lm-bg-primary,#faf8f2);border:1px solid var(--lm-border,#ddd7ca);border-radius:var(--lm-radius-sm,6px);transition:border-color var(--lm-duration-fast,.12s) var(--lm-ease);appearance:none;outline:none}.lm-input::placeholder{color:var(--lm-text-muted,#b8b3a8);opacity:1}.lm-input:hover:not(:disabled){border-color:var(--lm-accent-border,#b85b3333)}.lm-input:focus{border-color:var(--lm-accent,#b85b33);box-shadow:var(--lm-focus-ring)}.lm-input:disabled{opacity:.45;cursor:not-allowed;background:var(--lm-bg-tertiary,#e8e2d4)}.lm-input--invalid{border-color:var(--lm-error-border,#a8412b33);background:var(--lm-error-light,#a8412b1a)}.lm-input--invalid:focus{border-color:var(--lm-error,#a8412b);box-shadow:0 0 0 2px var(--lm-error-border,#a8412b33)}.lm-select{cursor:pointer}.lm-toggle{align-items:center;gap:var(--lm-space-2,8px);cursor:pointer;-webkit-user-select:none;user-select:none;display:flex}.lm-toggle__track{border-radius:var(--lm-radius-pill,999px);background:var(--lm-bg-tertiary,#e8e2d4);border:1px solid var(--lm-border,#ddd7ca);width:32px;height:18px;transition:background var(--lm-duration-fast,.12s) var(--lm-ease), border-color var(--lm-duration-fast,.12s) var(--lm-ease);outline:none;flex-shrink:0;align-items:center;display:inline-flex;position:relative}.lm-toggle__track:focus-visible{box-shadow:var(--lm-focus-ring);border-color:var(--lm-accent,#b85b33)}.lm-toggle__track--on{background:var(--lm-accent,#b85b33);border-color:var(--lm-accent,#b85b33)}.lm-toggle__track--invalid{border-color:var(--lm-error,#a8412b)}.lm-toggle__thumb{width:12px;height:12px;box-shadow:var(--lm-shadow-xs);transition:transform var(--lm-duration-fast,.12s) var(--lm-ease);background:#fff;border-radius:50%;position:absolute;left:2px}.lm-toggle__track--on .lm-toggle__thumb{transform:translate(14px)}.lm-toggle__track--disabled{opacity:.45;cursor:not-allowed}.lm-toggle__label{font-size:var(--lm-size-body,13px);color:var(--lm-text-secondary,#3a3530)}.lm-array{gap:var(--lm-space-2,8px);flex-direction:column;display:flex}.lm-array__items{gap:var(--lm-space-1,4px);flex-direction:column;display:flex}.lm-array__item{align-items:center;gap:var(--lm-space-1,4px);padding:var(--lm-space-1,4px) var(--lm-space-2,8px);background:var(--lm-bg-secondary,#f2eee6);border:1px solid var(--lm-border,#ddd7ca);border-radius:var(--lm-radius-sm,6px);display:flex}.lm-array__item-field{flex:1;min-width:0}.lm-array__item-actions{gap:var(--lm-space-1,4px);flex-shrink:0;display:flex}.lm-array__add{align-items:center;gap:var(--lm-space-1,4px);padding:var(--lm-space-1,4px) var(--lm-space-2,8px);font-family:var(--lm-font-sans);font-size:var(--lm-size-body-sm,12px);font-weight:var(--lm-weight-medium,500);color:var(--lm-accent,#b85b33);border:1px dashed var(--lm-accent-border,#b85b3333);border-radius:var(--lm-radius-sm,6px);cursor:pointer;width:100%;transition:background var(--lm-duration-fast,.12s) var(--lm-ease), border-color var(--lm-duration-fast,.12s) var(--lm-ease);background:0 0;outline:none;justify-content:center;display:flex}.lm-array__add:hover:not(:disabled){background:var(--lm-accent-light,#b85b331a);border-color:var(--lm-accent,#b85b33)}.lm-array__add:focus-visible{box-shadow:var(--lm-focus-ring);border-color:var(--lm-accent,#b85b33)}.lm-array__add:disabled{opacity:.45;cursor:not-allowed}.lm-object{gap:var(--lm-space-2,8px);padding:var(--lm-space-2,8px);background:var(--lm-bg-secondary,#f2eee6);border:1px solid var(--lm-border,#ddd7ca);border-radius:var(--lm-radius-sm,6px);flex-direction:column;display:flex}.lm-icon-btn{border-radius:var(--lm-radius-xs,4px);cursor:pointer;width:22px;height:22px;color:var(--lm-text-tertiary,#6e6960);transition:background var(--lm-duration-fast,.12s) var(--lm-ease), color var(--lm-duration-fast,.12s) var(--lm-ease);background:0 0;border:1px solid #0000;outline:none;justify-content:center;align-items:center;padding:0;font-size:14px;line-height:1;display:inline-flex}.lm-icon-btn:hover:not(:disabled){background:var(--lm-bg-tertiary,#e8e2d4);color:var(--lm-text-primary,#1a1714)}.lm-icon-btn:focus-visible{box-shadow:var(--lm-focus-ring);border-color:var(--lm-accent,#b85b33)}.lm-icon-btn:disabled{opacity:.35;cursor:not-allowed}.lm-icon-btn--danger:hover:not(:disabled){background:var(--lm-error-light,#a8412b1a);color:var(--lm-error,#a8412b);border-color:var(--lm-error-border,#a8412b33)}
@@ -0,0 +1,47 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Lerret — Studio</title>
7
+ <!--
8
+ Hosted-mode import map placeholder (FR13).
9
+ ───────────────────────────────────────────────────
10
+ In hosted mode user assets are transformed in-browser by Sucrase and
11
+ served as ES modules by the studio's service worker
12
+ (`src/runtime/module-sw.js`). Sucrase's automatic JSX runtime emits
13
+ `import { jsx } from "react/jsx-runtime"`, and user assets are free to
14
+ write `import { useState } from "react"`. Those are BARE specifiers,
15
+ not URLs — the browser needs an import map to resolve them.
16
+
17
+ The bundled studio's React module URLs are only known after Vite has
18
+ built the studio (Rolldown produces hashed chunk paths). So the import
19
+ map is INJECTED AT RUNTIME by `src/runtime/sucrase-runtime.js`'s
20
+ `injectReactImportMap()` helper, called by the hosted entry layer
21
+ once it knows the right URLs. The placeholder here is left
22
+ empty so that:
23
+ - in CLI mode (`lerret dev`) it doesn't shadow Vite's own
24
+ dependency-pre-bundle resolution (which handles bare specifiers
25
+ via the bundler, not via the import map);
26
+ - the hosted entry layer's injected import map takes effect first,
27
+ before any asset module is dynamically imported.
28
+
29
+ Chrome 89+ propagates a page's import map into SW-served modules, so
30
+ the same map covers Sucrase-output asset modules.
31
+ -->
32
+
33
+ <style>
34
+ body {
35
+ background: #f1f5f9;
36
+ }
37
+ </style>
38
+ <script type="importmap" id="lerret-import-map">
39
+ { "imports": {} }
40
+ </script>
41
+ <script type="module" crossorigin src="./assets/index-EslqdOhg.js"></script>
42
+ <link rel="stylesheet" crossorigin href="./assets/index-BNmJ8c2t.css">
43
+ </head>
44
+ <body>
45
+ <div id="root"></div>
46
+ </body>
47
+ </html>
@@ -0,0 +1,275 @@
1
+ // module-sw.js — the production service worker for hosted-mode asset modules.
2
+ //
3
+ // This SW participates in the hosted runtime as the module-graph layer:
4
+ // the main thread reads asset files via the FSA backend, transforms them with
5
+ // Sucrase (`sucrase-transform.js`), then pre-registers the transformed source
6
+ // with this SW via `postMessage`. When the main thread issues a dynamic
7
+ // `import()` for the module's virtual URL, this SW's `fetch` handler
8
+ // intercepts it and serves the cached source as `text/javascript`.
9
+ //
10
+ // ── Why a service worker ─────────────────────────────────────────────────
11
+ // A blob URL module cannot reliably resolve relative `import` specifiers
12
+ // against other blob URLs — the URL is opaque. A service worker, scoped over
13
+ // the studio's origin, intercepts ANY `fetch` (including dynamic `import`)
14
+ // inside that scope, so we can serve a coherent module graph where each
15
+ // asset's `./logo.png` or `../shared/Card.jsx` resolves to another SW-served
16
+ // URL. This was validated by the spike.
17
+ //
18
+ // ── Pre-register protocol (validated by the spike, refined for production) ──
19
+ // The main thread sends one of these messages:
20
+ //
21
+ // { type: 'PING', id }
22
+ // A handshake the main thread uses to confirm the SW is alive and
23
+ // responding. The SW replies with `{ type: 'PONG', id }` via the
24
+ // `event.source` MessagePort. Optional but useful for entry-screen
25
+ // diagnostics.
26
+ //
27
+ // { type: 'REGISTER_MODULE', url, code, contentType? }
28
+ // Pre-register a transformed asset module's source at `url`. The next
29
+ // dynamic `import(url)` will be served the cached `code`. `contentType`
30
+ // defaults to `'text/javascript'`; the runtime overrides it for non-JS
31
+ // resources (e.g. CSS sources served as `'text/css'`).
32
+ //
33
+ // { type: 'INVALIDATE', url }
34
+ // Drop a single cached URL — used when the runtime knows a URL was
35
+ // replaced by a new cache-busted one.
36
+ //
37
+ // { type: 'INVALIDATE_PREFIX', prefix }
38
+ // Drop every cached URL whose key starts with `prefix`. Used when the
39
+ // project is unmounted / re-mounted to clear stale entries en masse.
40
+ //
41
+ // { type: 'CLAIM' }
42
+ // Force this SW to claim all clients in scope immediately. The main
43
+ // thread sends this once after `register()` resolves, so the very first
44
+ // transformed module the page imports is intercepted (otherwise the
45
+ // uncontrolled-page race lets the first fetch bypass the SW).
46
+ //
47
+ // ── Module URL scheme ────────────────────────────────────────────────────
48
+ // Modules are served under `/__lerret/asset/<path>?h=<hash>`. The leading
49
+ // `/__lerret/` segment makes interception unambiguous and keeps SW concerns
50
+ // from colliding with the studio's own routes. The `?h=<hash>` cache-buster
51
+ // is added by the runtime (content-hash from `sucrase-transform.js`); a new
52
+ // transform produces a new URL so the browser re-fetches a fresh module
53
+ // instance (full remount on reload — see FINDINGS §4.3).
54
+ //
55
+ // ── Eviction ─────────────────────────────────────────────────────────────
56
+ // The spike noted that cached URLs accumulate forever — fine for a spike, not
57
+ // for a long-running session. This SW caps the in-memory map at MAX_MODULES.
58
+ // When full, the oldest registered entry is evicted (FIFO via Map iteration
59
+ // order — Map preserves insertion order in JS). A session with hundreds of
60
+ // edits stays at a bounded memory footprint.
61
+ //
62
+ // ── Globals ──────────────────────────────────────────────────────────────
63
+ // Service workers run in a special global scope where `self`, `clients`,
64
+ // `skipWaiting` are defined by the SW spec. ESLint's `no-undef` doesn't know
65
+ // about them. The block below declares them for the linter.
66
+
67
+ /* global clients */
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Constants
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * URL prefix the SW intercepts. Any fetch for a URL containing this prefix
75
+ * is treated as a request for a registered asset module. The leading
76
+ * `/__lerret/` segment is unlikely to collide with anything in the studio's
77
+ * own routes (the studio is a single-page app under `/`).
78
+ */
79
+ const ASSET_URL_PREFIX = '/__lerret/asset/';
80
+
81
+ /**
82
+ * Maximum number of registered modules to keep in memory before FIFO
83
+ * eviction kicks in. A realistic project has ≤200 assets × ≤a-handful of
84
+ * cache-busted versions; 2000 is generous and bounds the memory footprint.
85
+ */
86
+ const MAX_MODULES = 2000;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // In-memory module store
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * @typedef {{ code: string, contentType: string, registeredAt: number }} ModuleEntry
94
+ */
95
+
96
+ /** @type {Map<string, ModuleEntry>} */
97
+ const moduleStore = new Map();
98
+
99
+ /**
100
+ * Insert `entry` for `url`, evicting the oldest entry if the store would
101
+ * exceed {@link MAX_MODULES}.
102
+ *
103
+ * @param {string} url
104
+ * @param {ModuleEntry} entry
105
+ */
106
+ function storeModule(url, entry) {
107
+ // If the URL is already present, delete-then-set so the entry moves to the
108
+ // end (most-recent) of Map iteration order.
109
+ if (moduleStore.has(url)) {
110
+ moduleStore.delete(url);
111
+ } else if (moduleStore.size >= MAX_MODULES) {
112
+ // FIFO eviction: the first key in iteration order is the oldest.
113
+ const oldestKey = moduleStore.keys().next().value;
114
+ if (oldestKey !== undefined) {
115
+ moduleStore.delete(oldestKey);
116
+ }
117
+ }
118
+ moduleStore.set(url, entry);
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Lifecycle handlers
123
+ // ---------------------------------------------------------------------------
124
+
125
+ self.addEventListener('install', (event) => {
126
+ // Skip waiting so the new SW activates immediately — the studio's hosted
127
+ // entry layer is the only caller, and it expects this SW to
128
+ // be live by the time it tries to register the first module.
129
+ event.waitUntil(self.skipWaiting());
130
+ });
131
+
132
+ self.addEventListener('activate', (event) => {
133
+ // Claim all clients in this scope so the very first fetch the page makes
134
+ // after activation is intercepted — without `claim()`, the uncontrolled
135
+ // page would bypass the SW on its first dynamic import.
136
+ event.waitUntil(clients.claim());
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Message handler — pre-register protocol
141
+ // ---------------------------------------------------------------------------
142
+
143
+ self.addEventListener('message', (event) => {
144
+ const data = event.data;
145
+ if (!data || typeof data !== 'object') return;
146
+
147
+ switch (data.type) {
148
+ case 'PING': {
149
+ // Reply to the sender with a PONG carrying the same id. Used by the
150
+ // entry layer to confirm the SW is alive before mounting
151
+ // the hosted runtime.
152
+ if (event.source && typeof event.source.postMessage === 'function') {
153
+ event.source.postMessage({ type: 'PONG', id: data.id });
154
+ }
155
+ break;
156
+ }
157
+ case 'REGISTER_MODULE': {
158
+ if (typeof data.url !== 'string' || typeof data.code !== 'string') return;
159
+ const contentType =
160
+ typeof data.contentType === 'string' && data.contentType.length > 0
161
+ ? data.contentType
162
+ : 'text/javascript';
163
+ storeModule(data.url, {
164
+ code: data.code,
165
+ contentType,
166
+ registeredAt: Date.now(),
167
+ });
168
+ break;
169
+ }
170
+ case 'INVALIDATE': {
171
+ if (typeof data.url !== 'string') return;
172
+ moduleStore.delete(data.url);
173
+ break;
174
+ }
175
+ case 'INVALIDATE_PREFIX': {
176
+ if (typeof data.prefix !== 'string' || data.prefix.length === 0) return;
177
+ // Iterate the snapshot of keys (don't mutate during iteration).
178
+ const toDelete = [];
179
+ for (const key of moduleStore.keys()) {
180
+ if (key.startsWith(data.prefix)) toDelete.push(key);
181
+ }
182
+ for (const key of toDelete) moduleStore.delete(key);
183
+ break;
184
+ }
185
+ case 'CLAIM': {
186
+ // Idempotent — the page may call this multiple times after a reload.
187
+ // `clients.claim()` returns a Promise; we don't `event.waitUntil` here
188
+ // because `message` events don't support it. The Promise is fire-and-
189
+ // forget — the page will retry register-and-import if a race loses.
190
+ try {
191
+ clients.claim();
192
+ } catch {
193
+ // Older browsers may not allow `claim()` outside the activate handler;
194
+ // the studio gracefully degrades to whatever interception the page
195
+ // already had.
196
+ }
197
+ break;
198
+ }
199
+ default:
200
+ // Unknown type — ignore. Future protocol evolution stays additive.
201
+ break;
202
+ }
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Fetch handler — intercept dynamic import() of asset URLs
207
+ // ---------------------------------------------------------------------------
208
+
209
+ self.addEventListener('fetch', (event) => {
210
+ const url = event.request.url;
211
+ // Cheap substring check first to avoid URL parsing for every studio fetch.
212
+ if (url.indexOf(ASSET_URL_PREFIX) === -1) return;
213
+
214
+ // The pathname is the lookup key (origin / scheme are runtime-specific).
215
+ let pathAndQuery;
216
+ try {
217
+ const parsed = new URL(url);
218
+ pathAndQuery = parsed.pathname + parsed.search;
219
+ } catch {
220
+ return;
221
+ }
222
+ if (pathAndQuery.indexOf(ASSET_URL_PREFIX) !== 0) return;
223
+
224
+ event.respondWith(serveModule(pathAndQuery));
225
+ });
226
+
227
+ /**
228
+ * Serve a registered module by URL. A miss returns a JavaScript stub that
229
+ * throws on evaluation — the dynamic `import()` then rejects with the same
230
+ * stub message, which the runtime catches and surfaces as a per-asset error
231
+ * (NFR8). The stub is JavaScript so the browser parses it as a module rather
232
+ * than reporting a low-level network failure.
233
+ *
234
+ * @param {string} key The pathname-plus-search the runtime registered.
235
+ * @returns {Promise<Response>}
236
+ */
237
+ async function serveModule(key) {
238
+ const entry = moduleStore.get(key);
239
+ if (entry) {
240
+ return new Response(entry.code, {
241
+ status: 200,
242
+ headers: {
243
+ 'Content-Type': entry.contentType,
244
+ // The SW is the source of truth for cache-busting; never let the
245
+ // HTTP cache hold a stale copy.
246
+ 'Cache-Control': 'no-store',
247
+ },
248
+ });
249
+ }
250
+ // Miss — emit a JS stub so the import rejects cleanly with a readable
251
+ // message rather than a generic network error.
252
+ const safeKey = JSON.stringify(key);
253
+ const body =
254
+ `// Lerret hosted runtime: no module registered at ${safeKey}.\n` +
255
+ `throw new Error(${JSON.stringify(`Hosted runtime: no module registered at ${key}`)});`;
256
+ return new Response(body, {
257
+ status: 404,
258
+ headers: {
259
+ 'Content-Type': 'text/javascript',
260
+ 'Cache-Control': 'no-store',
261
+ },
262
+ });
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Documented exports — for the build to detect the file is a valid module
267
+ // ---------------------------------------------------------------------------
268
+ //
269
+ // A service worker file is loaded by `navigator.serviceWorker.register(url)`,
270
+ // not by `import`, so this file is normally side-effect-only. Vite's
271
+ // production build, however, doesn't know to copy a side-effect-only
272
+ // `.js` file into the dist/ output unless it is referenced. The runtime's
273
+ // `sucrase-runtime.js` imports the URL of this file with Vite's `?worker&url`
274
+ // idiom (`import swUrl from './module-sw.js?worker&url'`), so the bundler
275
+ // emits it to a stable hashed URL. No re-export is needed here.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@lerret/cli",
3
+ "version": "0.1.0",
4
+ "description": "The `lerret` design canvas CLI — a folder of plain React component files renders as a visual canvas. Includes the Vite dev server, headless export, and the bundled studio.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20.19.0"
9
+ },
10
+ "bin": {
11
+ "lerret": "./src/lerret.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "dist-studio",
16
+ "!src/**/*.test.js",
17
+ "!src/**/*.test.jsx"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/belikely-united/lerret.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ "homepage": "https://lerret.belikely.com",
25
+ "bugs": {
26
+ "url": "https://github.com/belikely-united/lerret/issues"
27
+ },
28
+ "keywords": [
29
+ "lerret",
30
+ "design-canvas",
31
+ "design-tool",
32
+ "react",
33
+ "vite",
34
+ "cli",
35
+ "local-first"
36
+ ],
37
+ "dependencies": {
38
+ "@vitejs/plugin-react": "^6.0.2",
39
+ "chokidar": "^5.0.0",
40
+ "playwright-core": "^1.60.0",
41
+ "vite": "^8.0.0",
42
+ "@lerret/core": "^0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "vitest": "^4.1.6"
46
+ },
47
+ "scripts": {
48
+ "test": "vitest run",
49
+ "build": "pnpm --filter @lerret/studio build:cli && node scripts/bundle-studio.js"
50
+ }
51
+ }