@illuma-ai/code-sandbox 1.4.0 → 1.4.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.
@@ -1,607 +0,0 @@
1
- /**
2
- * Template registry — built-in project templates.
3
- *
4
- * The "express-react" template is a proper full-stack app:
5
- * - Express backend with API routes that proxy JSONPlaceholder
6
- * - React frontend as separate component files under public/
7
- * - Express serves static files via fs.readFileSync (Nodepod doesn't support
8
- * res.sendFile or express.static)
9
- * - React + Tailwind loaded from CDN — no build step needed
10
- *
11
- * WHY THIS ARCHITECTURE:
12
- * Nodepod can't run Vite/Next.js/Webpack. But Express works perfectly.
13
- * By loading React/Tailwind from CDN and splitting the React app into
14
- * separate script files, we get a realistic full-stack project structure
15
- * that users can explore and edit in the code sandbox.
16
- */
17
-
18
- import type { FileMap } from "../types";
19
- import { fullstackStarterTemplate } from "./fullstack-starter";
20
-
21
- // ---------------------------------------------------------------------------
22
- // server.js — Express backend
23
- // ---------------------------------------------------------------------------
24
-
25
- const SERVER_JS = `const express = require('express');
26
- const fs = require('fs');
27
- const path = require('path');
28
- const app = express();
29
- const PORT = 3000;
30
-
31
- const API_BASE = 'https://jsonplaceholder.typicode.com';
32
-
33
- // Middleware
34
- app.use(express.json());
35
-
36
- // -----------------------------------------------------------------------
37
- // Static file serving
38
- // Nodepod doesn't support express.static() or res.sendFile(), so we
39
- // serve files manually via fs.readFileSync + res.send.
40
- // -----------------------------------------------------------------------
41
-
42
- const MIME_TYPES = {
43
- '.html': 'text/html',
44
- '.js': 'application/javascript',
45
- '.css': 'text/css',
46
- '.json': 'application/json',
47
- '.svg': 'image/svg+xml',
48
- '.png': 'image/png',
49
- };
50
-
51
- function serveStatic(req, res, next) {
52
- // Only serve GET requests for paths with extensions
53
- if (req.method !== 'GET') return next();
54
-
55
- var filePath = path.join(__dirname, 'public', req.path === '/' ? 'index.html' : req.path);
56
- var ext = path.extname(filePath);
57
-
58
- // If no extension, try .html
59
- if (!ext) {
60
- filePath += '.html';
61
- ext = '.html';
62
- }
63
-
64
- try {
65
- var data = fs.readFileSync(filePath, ext === '.png' ? undefined : 'utf-8');
66
- res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
67
- res.send(data);
68
- } catch (e) {
69
- next();
70
- }
71
- }
72
-
73
- // -----------------------------------------------------------------------
74
- // API proxy routes — proxy to JSONPlaceholder
75
- // -----------------------------------------------------------------------
76
-
77
- app.get('/api/posts', async function(req, res) {
78
- try {
79
- var userId = req.query.userId;
80
- var url = userId ? API_BASE + '/posts?userId=' + userId : API_BASE + '/posts';
81
- var resp = await fetch(url);
82
- var data = await resp.json();
83
- res.json(data);
84
- } catch (err) {
85
- res.status(500).json({ error: 'Failed to fetch posts', details: err.message });
86
- }
87
- });
88
-
89
- app.get('/api/posts/:id', async function(req, res) {
90
- try {
91
- var resp = await fetch(API_BASE + '/posts/' + req.params.id);
92
- var data = await resp.json();
93
- res.json(data);
94
- } catch (err) {
95
- res.status(500).json({ error: 'Failed to fetch post', details: err.message });
96
- }
97
- });
98
-
99
- app.get('/api/posts/:id/comments', async function(req, res) {
100
- try {
101
- var resp = await fetch(API_BASE + '/posts/' + req.params.id + '/comments');
102
- var data = await resp.json();
103
- res.json(data);
104
- } catch (err) {
105
- res.status(500).json({ error: 'Failed to fetch comments', details: err.message });
106
- }
107
- });
108
-
109
- app.get('/api/users', async function(req, res) {
110
- try {
111
- var resp = await fetch(API_BASE + '/users');
112
- var data = await resp.json();
113
- res.json(data);
114
- } catch (err) {
115
- res.status(500).json({ error: 'Failed to fetch users', details: err.message });
116
- }
117
- });
118
-
119
- app.get('/api/users/:id', async function(req, res) {
120
- try {
121
- var resp = await fetch(API_BASE + '/users/' + req.params.id);
122
- var data = await resp.json();
123
- res.json(data);
124
- } catch (err) {
125
- res.status(500).json({ error: 'Failed to fetch user', details: err.message });
126
- }
127
- });
128
-
129
- // -----------------------------------------------------------------------
130
- // Catch-all: serve static files (must be after API routes)
131
- // -----------------------------------------------------------------------
132
- app.use(serveStatic);
133
-
134
- app.listen(PORT, function() {
135
- console.log('Server running on port ' + PORT);
136
- });
137
- `;
138
-
139
- // ---------------------------------------------------------------------------
140
- // public/index.html — HTML shell that loads React app
141
- // ---------------------------------------------------------------------------
142
-
143
- const INDEX_HTML = `<!DOCTYPE html>
144
- <html lang="en">
145
- <head>
146
- <meta charset="UTF-8">
147
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
148
- <title>JSONPlaceholder Explorer</title>
149
- <script src="https://cdn.tailwindcss.com"><\/script>
150
- <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"><\/script>
151
- <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"><\/script>
152
- <link rel="stylesheet" href="/styles.css">
153
- </head>
154
- <body class="bg-gray-50 min-h-screen">
155
- <div id="root"></div>
156
- <!-- Load components in dependency order -->
157
- <script src="/components/PostCard.js"><\/script>
158
- <script src="/components/UserCard.js"><\/script>
159
- <script src="/components/CommentCard.js"><\/script>
160
- <script src="/components/PostDetail.js"><\/script>
161
- <script src="/components/UserDetail.js"><\/script>
162
- <script src="/App.js"><\/script>
163
- </body>
164
- </html>`;
165
-
166
- // ---------------------------------------------------------------------------
167
- // public/styles.css — App styles
168
- // ---------------------------------------------------------------------------
169
-
170
- const STYLES_CSS = `.tab-active {
171
- border-bottom: 2px solid #3b82f6;
172
- color: #3b82f6;
173
- }
174
- .card-hover:hover {
175
- transform: translateY(-2px);
176
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
177
- }
178
- .fade-in {
179
- animation: fadeIn 0.3s ease-in;
180
- }
181
- @keyframes fadeIn {
182
- from { opacity: 0; transform: translateY(8px); }
183
- to { opacity: 1; transform: translateY(0); }
184
- }
185
- `;
186
-
187
- // ---------------------------------------------------------------------------
188
- // public/components/PostCard.js
189
- // ---------------------------------------------------------------------------
190
-
191
- const POST_CARD_JS = `var e = React.createElement;
192
-
193
- /**
194
- * PostCard — Renders a single post as a clickable card.
195
- */
196
- window.PostCard = function PostCard(props) {
197
- var post = props.post;
198
- var onSelect = props.onSelect;
199
-
200
- return e('div', {
201
- className: 'bg-white rounded-lg p-5 border border-gray-200 card-hover transition-all duration-200 cursor-pointer fade-in',
202
- onClick: function() { onSelect(post); }
203
- },
204
- e('div', { className: 'flex items-start justify-between mb-2' },
205
- e('span', { className: 'text-xs font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full' },
206
- 'Post #' + post.id),
207
- e('span', { className: 'text-xs text-gray-400' }, 'User ' + post.userId)
208
- ),
209
- e('h3', { className: 'font-semibold text-gray-800 mb-2 line-clamp-1' }, post.title),
210
- e('p', { className: 'text-sm text-gray-500 line-clamp-2' }, post.body)
211
- );
212
- };
213
- `;
214
-
215
- // ---------------------------------------------------------------------------
216
- // public/components/UserCard.js
217
- // ---------------------------------------------------------------------------
218
-
219
- const USER_CARD_JS = `var e = React.createElement;
220
-
221
- /**
222
- * UserCard — Renders a user profile card with avatar.
223
- */
224
- window.UserCard = function UserCard(props) {
225
- var user = props.user;
226
- var onSelect = props.onSelect;
227
-
228
- return e('div', {
229
- className: 'bg-white rounded-lg p-5 border border-gray-200 card-hover transition-all duration-200 cursor-pointer fade-in',
230
- onClick: function() { onSelect(user); }
231
- },
232
- e('div', { className: 'flex items-center gap-3 mb-3' },
233
- e('div', {
234
- className: 'w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white font-bold text-sm'
235
- }, user.name.charAt(0)),
236
- e('div', null,
237
- e('h3', { className: 'font-semibold text-gray-800' }, user.name),
238
- e('p', { className: 'text-xs text-gray-400' }, '@' + user.username)
239
- )
240
- ),
241
- e('div', { className: 'space-y-1 text-sm text-gray-500' },
242
- e('p', null, user.email),
243
- e('p', null, user.company ? user.company.name : '')
244
- )
245
- );
246
- };
247
- `;
248
-
249
- // ---------------------------------------------------------------------------
250
- // public/components/CommentCard.js
251
- // ---------------------------------------------------------------------------
252
-
253
- const COMMENT_CARD_JS = `var e = React.createElement;
254
-
255
- /**
256
- * CommentCard — Renders a single comment.
257
- */
258
- window.CommentCard = function CommentCard(props) {
259
- var c = props.comment;
260
-
261
- return e('div', { className: 'bg-gray-50 rounded-lg p-4 border border-gray-100 fade-in' },
262
- e('div', { className: 'flex items-center gap-2 mb-2' },
263
- e('div', {
264
- className: 'w-6 h-6 rounded-full bg-gray-300 flex items-center justify-center text-[10px] font-bold text-gray-600'
265
- }, c.name.charAt(0).toUpperCase()),
266
- e('span', { className: 'text-xs font-medium text-gray-600 truncate flex-1' }, c.name),
267
- e('span', { className: 'text-xs text-gray-400 truncate' }, c.email)
268
- ),
269
- e('p', { className: 'text-sm text-gray-600' }, c.body)
270
- );
271
- };
272
- `;
273
-
274
- // ---------------------------------------------------------------------------
275
- // public/components/PostDetail.js
276
- // ---------------------------------------------------------------------------
277
-
278
- const POST_DETAIL_JS = `var e = React.createElement;
279
-
280
- /**
281
- * PostDetail — Full post view with comments.
282
- */
283
- window.PostDetail = function PostDetail(props) {
284
- var post = props.post;
285
- var onBack = props.onBack;
286
- var comments = props.comments;
287
- var loading = props.loading;
288
-
289
- return e('div', { className: 'fade-in' },
290
- e('button', {
291
- onClick: onBack,
292
- className: 'text-sm text-blue-600 hover:text-blue-700 mb-4 flex items-center gap-1'
293
- }, '\\u2190 Back to posts'),
294
-
295
- e('div', { className: 'bg-white rounded-lg p-6 border border-gray-200 mb-6' },
296
- e('div', { className: 'flex items-center gap-2 mb-3' },
297
- e('span', { className: 'text-xs font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full' },
298
- 'Post #' + post.id),
299
- e('span', { className: 'text-xs text-gray-400' }, 'User ' + post.userId)
300
- ),
301
- e('h2', { className: 'text-xl font-bold text-gray-800 mb-3' }, post.title),
302
- e('p', { className: 'text-gray-600 leading-relaxed' }, post.body)
303
- ),
304
-
305
- e('h3', { className: 'text-lg font-semibold text-gray-700 mb-3' },
306
- 'Comments (' + (comments ? comments.length : '...') + ')'
307
- ),
308
-
309
- loading
310
- ? e('div', { className: 'text-center py-8 text-gray-400' }, 'Loading comments...')
311
- : comments && comments.length > 0
312
- ? e('div', { className: 'space-y-3' },
313
- comments.map(function(c) {
314
- return e(window.CommentCard, { key: c.id, comment: c });
315
- })
316
- )
317
- : e('p', { className: 'text-gray-400 text-sm' }, 'No comments found.')
318
- );
319
- };
320
- `;
321
-
322
- // ---------------------------------------------------------------------------
323
- // public/components/UserDetail.js
324
- // ---------------------------------------------------------------------------
325
-
326
- const USER_DETAIL_JS = `var e = React.createElement;
327
-
328
- /**
329
- * UserDetail — Full user profile with their posts.
330
- */
331
- window.UserDetail = function UserDetail(props) {
332
- var user = props.user;
333
- var posts = props.posts;
334
- var onBack = props.onBack;
335
- var onSelectPost = props.onSelectPost;
336
- var loading = props.loading;
337
-
338
- return e('div', { className: 'fade-in' },
339
- e('button', {
340
- onClick: onBack,
341
- className: 'text-sm text-blue-600 hover:text-blue-700 mb-4 flex items-center gap-1'
342
- }, '\\u2190 Back to users'),
343
-
344
- e('div', { className: 'bg-white rounded-lg p-6 border border-gray-200 mb-6' },
345
- e('div', { className: 'flex items-center gap-4 mb-4' },
346
- e('div', {
347
- className: 'w-14 h-14 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white font-bold text-xl'
348
- }, user.name.charAt(0)),
349
- e('div', null,
350
- e('h2', { className: 'text-xl font-bold text-gray-800' }, user.name),
351
- e('p', { className: 'text-sm text-gray-400' }, '@' + user.username)
352
- )
353
- ),
354
- e('div', { className: 'grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm' },
355
- e('div', null,
356
- e('span', { className: 'text-gray-400' }, 'Email'),
357
- e('p', { className: 'text-gray-700' }, user.email)
358
- ),
359
- e('div', null,
360
- e('span', { className: 'text-gray-400' }, 'Phone'),
361
- e('p', { className: 'text-gray-700' }, user.phone || 'N/A')
362
- ),
363
- e('div', null,
364
- e('span', { className: 'text-gray-400' }, 'Website'),
365
- e('p', { className: 'text-gray-700' }, user.website || 'N/A')
366
- ),
367
- e('div', null,
368
- e('span', { className: 'text-gray-400' }, 'Company'),
369
- e('p', { className: 'text-gray-700' }, user.company ? user.company.name : 'N/A')
370
- )
371
- )
372
- ),
373
-
374
- e('h3', { className: 'text-lg font-semibold text-gray-700 mb-3' },
375
- 'Posts by ' + user.name + ' (' + (posts ? posts.length : '...') + ')'
376
- ),
377
-
378
- loading
379
- ? e('div', { className: 'text-center py-8 text-gray-400' }, 'Loading posts...')
380
- : posts && posts.length > 0
381
- ? e('div', { className: 'grid gap-3' },
382
- posts.map(function(p) {
383
- return e(window.PostCard, { key: p.id, post: p, onSelect: onSelectPost });
384
- })
385
- )
386
- : e('p', { className: 'text-gray-400 text-sm' }, 'No posts found.')
387
- );
388
- };
389
- `;
390
-
391
- // ---------------------------------------------------------------------------
392
- // public/App.js — Main application component + mount
393
- // ---------------------------------------------------------------------------
394
-
395
- const APP_JS = `var e = React.createElement;
396
-
397
- /**
398
- * App — Main application component.
399
- * Manages tabs (Posts/Users), fetches data from the Express API,
400
- * and renders detail views when items are selected.
401
- */
402
- function App() {
403
- var TAB_POSTS = 'posts';
404
- var TAB_USERS = 'users';
405
-
406
- var tabState = React.useState(TAB_POSTS);
407
- var activeTab = tabState[0], setActiveTab = tabState[1];
408
-
409
- var postsState = React.useState(null);
410
- var posts = postsState[0], setPosts = postsState[1];
411
-
412
- var usersState = React.useState(null);
413
- var users = usersState[0], setUsers = usersState[1];
414
-
415
- var loadingState = React.useState(true);
416
- var loading = loadingState[0], setLoading = loadingState[1];
417
-
418
- var selectedPostState = React.useState(null);
419
- var selectedPost = selectedPostState[0], setSelectedPost = selectedPostState[1];
420
-
421
- var selectedUserState = React.useState(null);
422
- var selectedUser = selectedUserState[0], setSelectedUser = selectedUserState[1];
423
-
424
- var commentsState = React.useState(null);
425
- var comments = commentsState[0], setComments = commentsState[1];
426
-
427
- var userPostsState = React.useState(null);
428
- var userPosts = userPostsState[0], setUserPosts = userPostsState[1];
429
-
430
- var detailLoadingState = React.useState(false);
431
- var detailLoading = detailLoadingState[0], setDetailLoading = detailLoadingState[1];
432
-
433
- // Fetch posts on mount
434
- React.useEffect(function() {
435
- setLoading(true);
436
- fetch('/api/posts')
437
- .then(function(r) { return r.json(); })
438
- .then(function(data) { setPosts(data); setLoading(false); })
439
- .catch(function() { setLoading(false); });
440
- }, []);
441
-
442
- // Fetch users when tab switches
443
- React.useEffect(function() {
444
- if (activeTab === TAB_USERS && !users) {
445
- setLoading(true);
446
- fetch('/api/users')
447
- .then(function(r) { return r.json(); })
448
- .then(function(data) { setUsers(data); setLoading(false); })
449
- .catch(function() { setLoading(false); });
450
- }
451
- }, [activeTab]);
452
-
453
- function handleSelectPost(post) {
454
- setSelectedPost(post);
455
- setSelectedUser(null);
456
- setComments(null);
457
- setDetailLoading(true);
458
- fetch('/api/posts/' + post.id + '/comments')
459
- .then(function(r) { return r.json(); })
460
- .then(function(data) { setComments(data); setDetailLoading(false); })
461
- .catch(function() { setDetailLoading(false); });
462
- }
463
-
464
- function handleSelectUser(user) {
465
- setSelectedUser(user);
466
- setSelectedPost(null);
467
- setUserPosts(null);
468
- setDetailLoading(true);
469
- fetch('/api/posts?userId=' + user.id)
470
- .then(function(r) { return r.json(); })
471
- .then(function(data) { setUserPosts(data); setDetailLoading(false); })
472
- .catch(function() { setDetailLoading(false); });
473
- }
474
-
475
- function handleBackFromPost() { setSelectedPost(null); setComments(null); }
476
- function handleBackFromUser() { setSelectedUser(null); setUserPosts(null); }
477
- function switchTab(tab) { setActiveTab(tab); setSelectedPost(null); setSelectedUser(null); }
478
-
479
- // Loading spinner
480
- function Spinner() {
481
- return e('div', { className: 'text-center py-12' },
482
- e('div', { className: 'inline-block w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-2' }),
483
- e('p', { className: 'text-gray-400 text-sm' }, 'Loading...')
484
- );
485
- }
486
-
487
- // Render
488
- return e('div', { className: 'min-h-screen bg-gray-50' },
489
- // Header
490
- e('header', { className: 'bg-white border-b border-gray-200 sticky top-0 z-10' },
491
- e('div', { className: 'max-w-4xl mx-auto px-4 py-4' },
492
- e('div', { className: 'flex items-center justify-between mb-3' },
493
- e('div', null,
494
- e('h1', { className: 'text-xl font-bold text-gray-800' }, 'JSONPlaceholder Explorer'),
495
- e('p', { className: 'text-xs text-gray-400' }, 'Full-stack Express + React')
496
- ),
497
- e('span', {
498
- className: 'text-xs text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full font-medium'
499
- }, '\\u2022 Live')
500
- ),
501
- // Tab bar
502
- e('div', { className: 'flex gap-0 border-b border-gray-200 -mb-[1px]' },
503
- e('button', {
504
- onClick: function() { switchTab(TAB_POSTS); },
505
- className: 'px-4 py-2 text-sm font-medium transition-colors ' +
506
- (activeTab === TAB_POSTS ? 'tab-active' : 'text-gray-500 hover:text-gray-700')
507
- }, 'Posts'),
508
- e('button', {
509
- onClick: function() { switchTab(TAB_USERS); },
510
- className: 'px-4 py-2 text-sm font-medium transition-colors ' +
511
- (activeTab === TAB_USERS ? 'tab-active' : 'text-gray-500 hover:text-gray-700')
512
- }, 'Users')
513
- )
514
- )
515
- ),
516
-
517
- // Content
518
- e('main', { className: 'max-w-4xl mx-auto px-4 py-6' },
519
- selectedPost
520
- ? e(window.PostDetail, { post: selectedPost, comments: comments, loading: detailLoading, onBack: handleBackFromPost })
521
- : selectedUser
522
- ? e(window.UserDetail, { user: selectedUser, posts: userPosts, loading: detailLoading, onBack: handleBackFromUser, onSelectPost: handleSelectPost })
523
- : activeTab === TAB_POSTS
524
- ? (loading ? e(Spinner) : posts && posts.length > 0
525
- ? e('div', { className: 'grid gap-3 sm:grid-cols-2' },
526
- posts.slice(0, 20).map(function(p) {
527
- return e(window.PostCard, { key: p.id, post: p, onSelect: handleSelectPost });
528
- })
529
- )
530
- : e('p', { className: 'text-gray-400 text-center py-12' }, 'No posts found.'))
531
- : activeTab === TAB_USERS
532
- ? (loading ? e(Spinner) : users && users.length > 0
533
- ? e('div', { className: 'grid gap-3 sm:grid-cols-2' },
534
- users.map(function(u) {
535
- return e(window.UserCard, { key: u.id, user: u, onSelect: handleSelectUser });
536
- })
537
- )
538
- : e('p', { className: 'text-gray-400 text-center py-12' }, 'No users found.'))
539
- : null
540
- )
541
- );
542
- }
543
-
544
- // Mount
545
- ReactDOM.createRoot(document.getElementById('root')).render(e(App));
546
- `;
547
-
548
- // ---------------------------------------------------------------------------
549
- // Template registry
550
- // ---------------------------------------------------------------------------
551
-
552
- /**
553
- * Template registry — maps template names to file maps.
554
- */
555
- export const templates: Record<
556
- string,
557
- { files: FileMap; entryCommand: string; port: number }
558
- > = {
559
- "express-react": {
560
- entryCommand: "node server.js",
561
- port: 3000,
562
- files: {
563
- "package.json": JSON.stringify(
564
- {
565
- name: "sandbox-app",
566
- version: "1.0.0",
567
- dependencies: {
568
- express: "^4.18.0",
569
- },
570
- },
571
- null,
572
- 2,
573
- ),
574
-
575
- "server.js": SERVER_JS,
576
-
577
- // Frontend files — served by Express's static middleware
578
- "public/index.html": INDEX_HTML,
579
- "public/styles.css": STYLES_CSS,
580
- "public/App.js": APP_JS,
581
- "public/components/PostCard.js": POST_CARD_JS,
582
- "public/components/UserCard.js": USER_CARD_JS,
583
- "public/components/CommentCard.js": COMMENT_CARD_JS,
584
- "public/components/PostDetail.js": POST_DETAIL_JS,
585
- "public/components/UserDetail.js": USER_DETAIL_JS,
586
- },
587
- },
588
-
589
- "fullstack-starter": fullstackStarterTemplate,
590
- };
591
-
592
- /**
593
- * Get a project template by name.
594
- *
595
- * @param name - Template name (e.g., 'express-react')
596
- * @returns Template config or null if not found
597
- */
598
- export function getTemplate(
599
- name: string,
600
- ): { files: FileMap; entryCommand: string; port: number } | null {
601
- return templates[name] || null;
602
- }
603
-
604
- /** List available template names */
605
- export function listTemplates(): string[] {
606
- return Object.keys(templates);
607
- }