zelune 1.6.3 → 1.6.4

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 (39) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.mjs +13 -53
  3. package/dist/assets/CheckoutScreen-6IdrpKgm.js +229 -0
  4. package/dist/assets/HomeScreen-Dxe9L4I2.js +316 -0
  5. package/dist/assets/LoginScreen-Dp1awI4F.js +189 -0
  6. package/dist/assets/ProductDetailScreen-Wa5_nlRP.js +196 -0
  7. package/dist/assets/ProfileScreen-riAU6vVh.js +185 -0
  8. package/dist/assets/index-hEzd4JVy.js +44697 -0
  9. package/{index.html → dist/index.html} +3 -1
  10. package/mcp/server.mjs +1324 -29
  11. package/package.json +10 -13
  12. package/plugin.json +1 -1
  13. package/socket.yml +11 -0
  14. package/mcp/server.ts +0 -446
  15. package/plugins/viteReactNativeFallbackPlugin.ts +0 -522
  16. package/plugins/viteRnSourceInject.ts +0 -105
  17. package/plugins/viteVirtualScreensPlugin.ts +0 -216
  18. package/src/canvas/CanvasApp.tsx +0 -1655
  19. package/src/canvas/FlowArrowsLayer.tsx +0 -239
  20. package/src/canvas/InspectorPanel.tsx +0 -1494
  21. package/src/canvas/ScreenArtboard.tsx +0 -1491
  22. package/src/canvas/types.ts +0 -23
  23. package/src/main.tsx +0 -80
  24. package/src/runtime/MockNavigationContext.tsx +0 -1869
  25. package/src/runtime/ScreenErrorBoundary.tsx +0 -86
  26. package/src/runtime/nativewindShim.ts +0 -997
  27. package/src/runtime/reactNativeShim.ts +0 -1674
  28. package/src/screens/CheckoutScreen.tsx +0 -229
  29. package/src/screens/HomeScreen.tsx +0 -288
  30. package/src/screens/LoginScreen.tsx +0 -176
  31. package/src/screens/ProductDetailScreen.tsx +0 -207
  32. package/src/screens/ProfileScreen.tsx +0 -172
  33. package/src/screens/index.ts +0 -1
  34. package/src/server/apiMiddleware.ts +0 -378
  35. package/src/server/astEngine.ts +0 -2624
  36. package/src/vite-env.d.ts +0 -7
  37. package/tsconfig.json +0 -23
  38. package/vite.config.ts +0 -339
  39. /package/{src/styles.css → dist/assets/index-C7nrS82N.css} +0 -0
@@ -1,378 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import type { IncomingMessage, ServerResponse } from 'node:http';
4
- import type { Plugin } from 'vite';
5
- import {
6
- discoverScreens,
7
- getActiveProjectRoot,
8
- inspectElement,
9
- patchElement,
10
- reorderElement,
11
- insertElement,
12
- duplicateElement,
13
- deleteElement,
14
- undoLastPatch,
15
- redoLastPatch,
16
- getHistoryStatus,
17
- type PatchPayload,
18
- type ReorderPayload,
19
- type InsertElementPayload,
20
- type DuplicateElementPayload,
21
- type DeleteElementPayload,
22
- } from './astEngine';
23
-
24
- const MAX_BODY_BYTES = 1_000_000;
25
- const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
26
- const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
27
-
28
- export function isLoopbackHostHeader(hostHeader: string | undefined): boolean {
29
- if (!hostHeader) return false;
30
- const trimmed = hostHeader.trim().toLowerCase();
31
- if (trimmed.startsWith('[')) {
32
- const closeIdx = trimmed.indexOf(']');
33
- if (closeIdx === -1) return false;
34
- const ipv6Host = trimmed.slice(0, closeIdx + 1);
35
- return LOOPBACK_HOSTS.has(ipv6Host);
36
- }
37
- const hostname = trimmed.split(':')[0];
38
- return LOOPBACK_HOSTS.has(hostname);
39
- }
40
-
41
- export function verifyApiRequestSecurity(req: {
42
- method?: string;
43
- headers: Record<string, string | string[] | undefined>;
44
- }): { allowed: boolean; reason?: string } {
45
- const host = typeof req.headers.host === 'string' ? req.headers.host : undefined;
46
- if (!isLoopbackHostHeader(host)) {
47
- return {
48
- allowed: false,
49
- reason: 'Forbidden Host header: Local API only accepts loopback connections (127.0.0.1, localhost, [::1])',
50
- };
51
- }
52
-
53
- const secFetchSite =
54
- typeof req.headers['sec-fetch-site'] === 'string'
55
- ? req.headers['sec-fetch-site'].toLowerCase()
56
- : undefined;
57
- if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'none') {
58
- return {
59
- allowed: false,
60
- reason: `Forbidden cross-site request (Sec-Fetch-Site: ${secFetchSite})`,
61
- };
62
- }
63
-
64
- const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
65
- if (origin) {
66
- try {
67
- const parsedOrigin = new URL(origin);
68
- if (!LOOPBACK_HOSTS.has(parsedOrigin.hostname.toLowerCase())) {
69
- return {
70
- allowed: false,
71
- reason: `Forbidden cross-origin request from ${origin}`,
72
- };
73
- }
74
- } catch {
75
- return {
76
- allowed: false,
77
- reason: 'Malformed Origin header',
78
- };
79
- }
80
- }
81
-
82
- if (req.method === 'POST') {
83
- const contentType =
84
- typeof req.headers['content-type'] === 'string'
85
- ? req.headers['content-type'].toLowerCase()
86
- : '';
87
- if (!contentType.startsWith('application/json')) {
88
- return {
89
- allowed: false,
90
- reason: 'POST requests to /api/* must specify Content-Type: application/json',
91
- };
92
- }
93
- }
94
-
95
- return { allowed: true };
96
- }
97
-
98
- export function sanitizeLayoutPayload(
99
- input: unknown
100
- ): Record<string, { x: number; y: number; width: number; height: number }> {
101
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
102
- throw new Error('Invalid layout payload: expected an object map of screen coordinates');
103
- }
104
-
105
- const sanitized: Record<string, { x: number; y: number; width: number; height: number }> =
106
- Object.create(null);
107
- const entries = Object.entries(input as Record<string, unknown>);
108
- if (entries.length > 200) {
109
- throw new Error('Invalid layout payload: too many screen entries');
110
- }
111
-
112
- for (const [screenId, rawBox] of entries) {
113
- if (!screenId || FORBIDDEN_KEYS.has(screenId) || screenId.length > 120) {
114
- throw new Error(`Invalid screen key in layout payload: "${screenId}"`);
115
- }
116
- if (!rawBox || typeof rawBox !== 'object' || Array.isArray(rawBox)) {
117
- throw new Error(`Invalid layout coordinates for screen "${screenId}"`);
118
- }
119
- const box = rawBox as Record<string, unknown>;
120
- const x = Number(box.x);
121
- const y = Number(box.y);
122
- const width = Number(box.width);
123
- const height = Number(box.height);
124
-
125
- if (
126
- !Number.isFinite(x) ||
127
- !Number.isFinite(y) ||
128
- !Number.isFinite(width) ||
129
- !Number.isFinite(height) ||
130
- Math.abs(x) > 100_000 ||
131
- Math.abs(y) > 100_000 ||
132
- width < 100 ||
133
- width > 5_000 ||
134
- height < 100 ||
135
- height > 5_000
136
- ) {
137
- throw new Error(`Out-of-bounds layout coordinates for screen "${screenId}"`);
138
- }
139
-
140
- sanitized[screenId] = {
141
- x: Math.round(x),
142
- y: Math.round(y),
143
- width: Math.round(width),
144
- height: Math.round(height),
145
- };
146
- }
147
-
148
- return sanitized;
149
- }
150
-
151
- export async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
152
- return new Promise((resolve, reject) => {
153
- let raw = '';
154
- let byteCount = 0;
155
- let aborted = false;
156
-
157
- req.on('data', (chunk: Buffer | string) => {
158
- if (aborted) return;
159
- const chunkBytes =
160
- typeof chunk === 'string' ? Buffer.byteLength(chunk, 'utf-8') : chunk.length;
161
- byteCount += chunkBytes;
162
-
163
- if (byteCount > MAX_BODY_BYTES) {
164
- aborted = true;
165
- req.destroy();
166
- reject(new Error('Request payload too large'));
167
- return;
168
- }
169
-
170
- raw += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
171
- });
172
-
173
- req.on('end', () => {
174
- if (aborted) return;
175
- try {
176
- resolve(raw ? (JSON.parse(raw) as T) : ({} as T));
177
- } catch (err) {
178
- reject(err);
179
- }
180
- });
181
-
182
- req.on('error', (err) => {
183
- if (!aborted) {
184
- aborted = true;
185
- reject(err);
186
- }
187
- });
188
- });
189
- }
190
-
191
- function sendJson(res: ServerResponse, status: number, payload: unknown) {
192
- res.statusCode = status;
193
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
194
- res.setHeader('X-Content-Type-Options', 'nosniff');
195
- res.end(JSON.stringify(payload));
196
- }
197
-
198
- export function viteCanvasApiMiddleware(workspaceRoot: string): Plugin {
199
- async function resolveConnectedProjectName(): Promise<string> {
200
- const activeRoot = getActiveProjectRoot(workspaceRoot);
201
- const dirName = path.basename(activeRoot);
202
- try {
203
- const appJsonRaw = await fs.readFile(path.join(activeRoot, 'app.json'), 'utf-8');
204
- const appJson = JSON.parse(appJsonRaw) as {
205
- expo?: { name?: string };
206
- name?: string;
207
- };
208
- if (appJson?.expo?.name) return appJson.expo.name;
209
- if (appJson?.name) return appJson.name;
210
- } catch {
211
- // No app.json present
212
- }
213
- try {
214
- const pkgRaw = await fs.readFile(path.join(activeRoot, 'package.json'), 'utf-8');
215
- const pkg = JSON.parse(pkgRaw) as { name?: string };
216
- if (pkg?.name && pkg.name.toLowerCase() !== 'zelune') {
217
- return pkg.name;
218
- }
219
- } catch {
220
- // Fallback to directory name
221
- }
222
- return dirName;
223
- }
224
-
225
- return {
226
- name: 'vite-canvas-api-middleware',
227
- configureServer(server) {
228
- server.middlewares.use(async (req, res, next) => {
229
- if (!req.url || !req.url.startsWith('/api/')) {
230
- return next();
231
- }
232
-
233
- const securityCheck = verifyApiRequestSecurity(req);
234
- if (!securityCheck.allowed) {
235
- return sendJson(res, 403, { ok: false, error: securityCheck.reason });
236
- }
237
-
238
- const activeRoot = getActiveProjectRoot(workspaceRoot);
239
- const layoutFilePath = path.resolve(activeRoot, '.canvas-layout.json');
240
- const qIndex = req.url.indexOf('?');
241
- const pathname = qIndex === -1 ? req.url : req.url.slice(0, qIndex);
242
- const searchParams = new URLSearchParams(
243
- qIndex === -1 ? '' : req.url.slice(qIndex + 1)
244
- );
245
-
246
- try {
247
- // GET /api/screens
248
- if (req.method === 'GET' && pathname === '/api/screens') {
249
- const [screens, projectName] = await Promise.all([
250
- discoverScreens(workspaceRoot),
251
- resolveConnectedProjectName(),
252
- ]);
253
- const history = getHistoryStatus();
254
- return sendJson(res, 200, { ok: true, projectName, screens, history });
255
- }
256
-
257
- // GET /api/inspect?file=...&astPath=...&line=...
258
- if (req.method === 'GET' && pathname === '/api/inspect') {
259
- const file = searchParams.get('file') ?? '';
260
- const astPath = searchParams.get('astPath') ?? '';
261
- const lineParam = searchParams.get('line');
262
- const line = lineParam ? Number.parseInt(lineParam, 10) : undefined;
263
-
264
- const element = await inspectElement(workspaceRoot, { file, astPath, line });
265
- const history = getHistoryStatus();
266
- return sendJson(res, 200, { ok: true, element, history });
267
- }
268
-
269
- // POST /api/patch
270
- if (req.method === 'POST' && pathname === '/api/patch') {
271
- const body = await readJsonBody<PatchPayload>(req);
272
- const result = await patchElement(workspaceRoot, body);
273
- return sendJson(res, 200, {
274
- ok: true,
275
- element: result.element,
276
- history: {
277
- undoAvailable: result.undoAvailable,
278
- redoAvailable: result.redoAvailable,
279
- },
280
- });
281
- }
282
-
283
- // POST /api/reorder
284
- if (req.method === 'POST' && pathname === '/api/reorder') {
285
- const body = await readJsonBody<ReorderPayload>(req);
286
- const result = await reorderElement(workspaceRoot, body);
287
- return sendJson(res, 200, {
288
- ok: true,
289
- element: result.element,
290
- history: {
291
- undoAvailable: result.undoAvailable,
292
- redoAvailable: result.redoAvailable,
293
- },
294
- });
295
- }
296
-
297
- // POST /api/insert
298
- if (req.method === 'POST' && pathname === '/api/insert') {
299
- const body = await readJsonBody<InsertElementPayload>(req);
300
- const result = await insertElement(workspaceRoot, body);
301
- return sendJson(res, 200, {
302
- ok: true,
303
- element: result.element,
304
- history: {
305
- undoAvailable: result.undoAvailable,
306
- redoAvailable: result.redoAvailable,
307
- },
308
- });
309
- }
310
-
311
- // POST /api/duplicate
312
- if (req.method === 'POST' && pathname === '/api/duplicate') {
313
- const body = await readJsonBody<DuplicateElementPayload>(req);
314
- const result = await duplicateElement(workspaceRoot, body);
315
- return sendJson(res, 200, {
316
- ok: true,
317
- element: result.element,
318
- history: {
319
- undoAvailable: result.undoAvailable,
320
- redoAvailable: result.redoAvailable,
321
- },
322
- });
323
- }
324
-
325
- // POST /api/delete
326
- if (req.method === 'POST' && pathname === '/api/delete') {
327
- const body = await readJsonBody<DeleteElementPayload>(req);
328
- const result = await deleteElement(workspaceRoot, body);
329
- return sendJson(res, 200, {
330
- ok: true,
331
- deletedAstPath: result.deletedAstPath,
332
- history: {
333
- undoAvailable: result.undoAvailable,
334
- redoAvailable: result.redoAvailable,
335
- },
336
- });
337
- }
338
-
339
- // POST /api/undo
340
- if (req.method === 'POST' && pathname === '/api/undo') {
341
- const result = await undoLastPatch(workspaceRoot);
342
- return sendJson(res, 200, { ok: true, ...result });
343
- }
344
-
345
- // POST /api/redo
346
- if (req.method === 'POST' && pathname === '/api/redo') {
347
- const result = await redoLastPatch(workspaceRoot);
348
- return sendJson(res, 200, { ok: true, ...result });
349
- }
350
-
351
- // GET /api/layout
352
- if (req.method === 'GET' && pathname === '/api/layout') {
353
- try {
354
- const raw = await fs.readFile(layoutFilePath, 'utf-8');
355
- const validated = sanitizeLayoutPayload(JSON.parse(raw));
356
- return sendJson(res, 200, { ok: true, layout: validated });
357
- } catch {
358
- return sendJson(res, 200, { ok: true, layout: null });
359
- }
360
- }
361
-
362
- // POST /api/layout
363
- if (req.method === 'POST' && pathname === '/api/layout') {
364
- const body = await readJsonBody<Record<string, unknown>>(req);
365
- const validated = sanitizeLayoutPayload(body);
366
- await fs.writeFile(layoutFilePath, JSON.stringify(validated, null, 2), 'utf-8');
367
- return sendJson(res, 200, { ok: true });
368
- }
369
-
370
- return sendJson(res, 404, { ok: false, error: `Unknown API route: ${pathname}` });
371
- } catch (err) {
372
- const message = err instanceof Error ? err.message : String(err);
373
- return sendJson(res, 400, { ok: false, error: message });
374
- }
375
- });
376
- },
377
- };
378
- }