@jxrstudios/jxr 1.0.3 → 1.0.5
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 +6 -2
- package/bin/jxr.js +60 -0
- package/dist/deployer.d.ts +8 -12
- package/dist/deployer.d.ts.map +1 -1
- package/dist/deployer.js +69 -106
- package/dist/deployer.js.map +1 -1
- package/dist/enhanced-transpiler.d.ts +36 -0
- package/dist/enhanced-transpiler.d.ts.map +1 -0
- package/dist/enhanced-transpiler.js +272 -0
- package/dist/enhanced-transpiler.js.map +1 -0
- package/dist/entry-point-detection.d.ts +22 -0
- package/dist/entry-point-detection.d.ts.map +1 -0
- package/dist/entry-point-detection.js +415 -0
- package/dist/entry-point-detection.js.map +1 -0
- package/dist/index.d.ts +19 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2119 -16
- package/dist/index.js.map +1 -1
- package/dist/jxr-server-manager.d.ts +32 -0
- package/dist/jxr-server-manager.d.ts.map +1 -0
- package/dist/jxr-server-manager.js +353 -0
- package/dist/jxr-server-manager.js.map +1 -0
- package/dist/runtime.d.ts +9 -9
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +3 -3
- package/package.json +15 -4
- package/src/deployer.ts +231 -0
- package/src/enhanced-transpiler.ts +331 -0
- package/src/entry-point-detection.ts +470 -0
- package/src/index.ts +63 -0
- package/src/jxr-server-manager.ts +410 -0
- package/src/module-resolver.ts +520 -0
- package/src/moq-transport.ts +267 -0
- package/src/runtime.ts +188 -0
- package/src/web-crypto.ts +279 -0
- package/src/worker-pool.ts +321 -0
- package/zzz_react_template/App.tsx +160 -0
- package/zzz_react_template/index.css +16 -0
- package/zzz_react_template/index.html +12 -0
- package/zzz_react_template/main.tsx +10 -0
- package/zzz_react_template/package.json +25 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JXR.js — Module Resolver & Virtual File System
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Design: LavaFlow OS — Thermal Precision + Edge Command
|
|
5
|
+
* Layer: Core Runtime / Module System
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* Zero-build-step module resolution pipeline:
|
|
9
|
+
* 1. VirtualFS: In-memory file system with change notification
|
|
10
|
+
* 2. ModuleResolver: Resolves imports to VirtualFS entries
|
|
11
|
+
* 3. JSXTransformer: Browser-native JSX → JS transform (no Babel/esbuild)
|
|
12
|
+
* 4. ImportMapBuilder: Generates browser-native import maps for esm.sh CDN
|
|
13
|
+
* 5. ModuleCache: LRU cache with crypto integrity verification
|
|
14
|
+
*
|
|
15
|
+
* The resolver produces browser-executable ES modules from JSX/TSX source
|
|
16
|
+
* without any build step, using the esm.sh CDN for npm package resolution.
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface VirtualFile {
|
|
21
|
+
path: string;
|
|
22
|
+
content: string;
|
|
23
|
+
language: 'tsx' | 'ts' | 'jsx' | 'js' | 'css' | 'json' | 'md' | 'html';
|
|
24
|
+
lastModified: number;
|
|
25
|
+
size: number;
|
|
26
|
+
dirty: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VirtualDirectory {
|
|
30
|
+
path: string;
|
|
31
|
+
name: string;
|
|
32
|
+
children: (VirtualFile | VirtualDirectory)[];
|
|
33
|
+
expanded: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResolvedModule {
|
|
37
|
+
path: string;
|
|
38
|
+
source: string;
|
|
39
|
+
transformed: string;
|
|
40
|
+
objectUrl: string | null;
|
|
41
|
+
dependencies: string[];
|
|
42
|
+
resolvedAt: number;
|
|
43
|
+
transformMs: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ImportMap {
|
|
47
|
+
imports: Record<string, string>;
|
|
48
|
+
scopes?: Record<string, Record<string, string>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type FileChangeHandler = (file: VirtualFile, event: 'create' | 'update' | 'delete') => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* VirtualFS — In-memory file system with reactive change notifications
|
|
55
|
+
*/
|
|
56
|
+
export class VirtualFS {
|
|
57
|
+
private files: Map<string, VirtualFile> = new Map();
|
|
58
|
+
private changeHandlers: Set<FileChangeHandler> = new Set();
|
|
59
|
+
|
|
60
|
+
constructor(initialFiles?: VirtualFile[]) {
|
|
61
|
+
if (initialFiles) {
|
|
62
|
+
for (const file of initialFiles) {
|
|
63
|
+
this.files.set(file.path, file);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
write(path: string, content: string): VirtualFile {
|
|
69
|
+
const existing = this.files.get(path);
|
|
70
|
+
const language = this.detectLanguage(path);
|
|
71
|
+
const file: VirtualFile = {
|
|
72
|
+
path,
|
|
73
|
+
content,
|
|
74
|
+
language,
|
|
75
|
+
lastModified: Date.now(),
|
|
76
|
+
size: new TextEncoder().encode(content).byteLength,
|
|
77
|
+
dirty: true,
|
|
78
|
+
};
|
|
79
|
+
this.files.set(path, file);
|
|
80
|
+
this.emit(file, existing ? 'update' : 'create');
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
read(path: string): VirtualFile | null {
|
|
85
|
+
return this.files.get(path) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
delete(path: string): boolean {
|
|
89
|
+
const file = this.files.get(path);
|
|
90
|
+
if (!file) return false;
|
|
91
|
+
this.files.delete(path);
|
|
92
|
+
this.emit(file, 'delete');
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
list(prefix?: string): VirtualFile[] {
|
|
97
|
+
const all = Array.from(this.files.values());
|
|
98
|
+
return prefix ? all.filter((f) => f.path.startsWith(prefix)) : all;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
exists(path: string): boolean {
|
|
102
|
+
return this.files.has(path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onChange(handler: FileChangeHandler): () => void {
|
|
106
|
+
this.changeHandlers.add(handler);
|
|
107
|
+
return () => this.changeHandlers.delete(handler);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private emit(file: VirtualFile, event: 'create' | 'update' | 'delete'): void {
|
|
111
|
+
this.changeHandlers.forEach((h) => h(file, event));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
buildTree(rootPath = '/'): VirtualDirectory {
|
|
115
|
+
const files = this.list();
|
|
116
|
+
const root: VirtualDirectory = {
|
|
117
|
+
path: rootPath,
|
|
118
|
+
name: rootPath === '/' ? 'project' : rootPath.split('/').pop()!,
|
|
119
|
+
children: [],
|
|
120
|
+
expanded: true,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const dirs = new Map<string, VirtualDirectory>();
|
|
124
|
+
dirs.set(rootPath, root);
|
|
125
|
+
|
|
126
|
+
// Sort files to ensure parent dirs are created first
|
|
127
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
128
|
+
|
|
129
|
+
for (const file of sorted) {
|
|
130
|
+
const parts = file.path.replace(rootPath, '').split('/').filter(Boolean);
|
|
131
|
+
let current = root;
|
|
132
|
+
let currentPath = rootPath;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
135
|
+
currentPath = `${currentPath}${parts[i]}/`;
|
|
136
|
+
if (!dirs.has(currentPath)) {
|
|
137
|
+
const dir: VirtualDirectory = {
|
|
138
|
+
path: currentPath,
|
|
139
|
+
name: parts[i],
|
|
140
|
+
children: [],
|
|
141
|
+
expanded: true,
|
|
142
|
+
};
|
|
143
|
+
dirs.set(currentPath, dir);
|
|
144
|
+
current.children.push(dir);
|
|
145
|
+
}
|
|
146
|
+
current = dirs.get(currentPath)!;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
current.children.push(file);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return root;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toJSON(): Record<string, string> {
|
|
156
|
+
const result: Record<string, string> = {};
|
|
157
|
+
for (const [path, file] of Array.from(this.files.entries())) {
|
|
158
|
+
result[path] = file.content;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private detectLanguage(path: string): VirtualFile['language'] {
|
|
164
|
+
const ext = path.split('.').pop()?.toLowerCase();
|
|
165
|
+
const map: Record<string, VirtualFile['language']> = {
|
|
166
|
+
tsx: 'tsx', ts: 'ts', jsx: 'jsx', js: 'js',
|
|
167
|
+
css: 'css', json: 'json', md: 'md', html: 'html',
|
|
168
|
+
};
|
|
169
|
+
return map[ext ?? ''] ?? 'js';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* JSXTransformer — Zero-dependency JSX → JS transform
|
|
175
|
+
*
|
|
176
|
+
* Uses a lightweight regex-based transform for simple JSX,
|
|
177
|
+
* with Sucrase-style transforms for production accuracy.
|
|
178
|
+
* Falls back to esm.sh/sucrase for complex transforms.
|
|
179
|
+
*/
|
|
180
|
+
export class JSXTransformer {
|
|
181
|
+
private objectUrls: Map<string, string> = new Map();
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Transform JSX/TSX source to browser-executable ES module
|
|
185
|
+
* Uses the automatic JSX runtime (React 17+)
|
|
186
|
+
*/
|
|
187
|
+
transform(source: string, filePath: string): string {
|
|
188
|
+
const isTS = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
189
|
+
let result = source;
|
|
190
|
+
|
|
191
|
+
// Step 1: Strip TypeScript type annotations
|
|
192
|
+
if (isTS) {
|
|
193
|
+
result = this.stripTypeScript(result);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Step 2: Transform JSX to React.createElement calls
|
|
197
|
+
if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) {
|
|
198
|
+
result = this.transformJSX(result);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Step 3: Rewrite imports to use esm.sh CDN
|
|
202
|
+
result = this.rewriteImports(result);
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private stripTypeScript(source: string): string {
|
|
208
|
+
let result = source;
|
|
209
|
+
|
|
210
|
+
// Remove type imports: import type { ... } from '...'
|
|
211
|
+
result = result.replace(/^import\s+type\s+.*?from\s+['"][^'"]+['"]\s*;?\s*$/gm, '');
|
|
212
|
+
|
|
213
|
+
// Remove inline type imports: import { type Foo, Bar }
|
|
214
|
+
result = result.replace(/\{\s*type\s+\w+\s*,?\s*/g, '{ ');
|
|
215
|
+
result = result.replace(/,\s*type\s+\w+\s*/g, ', ');
|
|
216
|
+
|
|
217
|
+
// Remove type assertions: as Type
|
|
218
|
+
result = result.replace(/\s+as\s+[A-Z][A-Za-z<>\[\],\s|&]+(?=[,)\s;])/g, '');
|
|
219
|
+
|
|
220
|
+
// Remove generic type parameters from function calls: fn<Type>(...)
|
|
221
|
+
result = result.replace(/<[A-Z][A-Za-z<>\[\],\s|&]*>\s*\(/g, '(');
|
|
222
|
+
|
|
223
|
+
// Remove interface declarations
|
|
224
|
+
result = result.replace(/^(export\s+)?interface\s+\w+[^{]*\{[^}]*\}/gm, '');
|
|
225
|
+
|
|
226
|
+
// Remove type alias declarations
|
|
227
|
+
result = result.replace(/^(export\s+)?type\s+\w+\s*=\s*[^;]+;/gm, '');
|
|
228
|
+
|
|
229
|
+
// Remove function parameter type annotations: (x: Type) => ...
|
|
230
|
+
result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*(?=[,)=])/g, '');
|
|
231
|
+
|
|
232
|
+
// Remove return type annotations: ): Type {
|
|
233
|
+
result = result.replace(/\)\s*:\s*[A-Za-z<>\[\],\s|&]+\s*\{/g, ') {');
|
|
234
|
+
|
|
235
|
+
// Remove variable type annotations: const x: Type =
|
|
236
|
+
result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*\s*=/g, ' =');
|
|
237
|
+
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private transformJSX(source: string): string {
|
|
242
|
+
// Add React import if not present (for createElement)
|
|
243
|
+
const hasReactImport = /import\s+React/.test(source) ||
|
|
244
|
+
/import\s+\*\s+as\s+React/.test(source);
|
|
245
|
+
|
|
246
|
+
let result = source;
|
|
247
|
+
|
|
248
|
+
if (!hasReactImport) {
|
|
249
|
+
result = `import React from 'react';\n` + result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Transform JSX self-closing tags: <Component />
|
|
253
|
+
result = result.replace(/<([A-Z][A-Za-z.]*)\s*\/>/g, 'React.createElement($1, null)');
|
|
254
|
+
|
|
255
|
+
// Transform JSX self-closing with props: <Component prop="val" />
|
|
256
|
+
result = result.replace(
|
|
257
|
+
/<([A-Z][A-Za-z.]*)\s+([^>]+?)\s*\/>/g,
|
|
258
|
+
(_, tag, props) => `React.createElement(${tag}, {${this.parseProps(props)}})`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Transform lowercase self-closing: <div />
|
|
262
|
+
result = result.replace(/<([a-z][a-z-]*)\s*\/>/g, `React.createElement('$1', null)`);
|
|
263
|
+
|
|
264
|
+
// Transform JSX fragments: <> ... </>
|
|
265
|
+
result = result.replace(/<>/g, 'React.createElement(React.Fragment, null,');
|
|
266
|
+
result = result.replace(/<\/>/g, ')');
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private parseProps(propsStr: string): string {
|
|
272
|
+
const props: string[] = [];
|
|
273
|
+
const regex = /(\w+)(?:=(?:"([^"]*?)"|'([^']*?)'|\{([^}]*?)\}))?/g;
|
|
274
|
+
let match;
|
|
275
|
+
while ((match = regex.exec(propsStr)) !== null) {
|
|
276
|
+
const [, name, strDouble, strSingle, expr] = match;
|
|
277
|
+
if (strDouble !== undefined) props.push(`${name}: "${strDouble}"`);
|
|
278
|
+
else if (strSingle !== undefined) props.push(`${name}: '${strSingle}'`);
|
|
279
|
+
else if (expr !== undefined) props.push(`${name}: ${expr}`);
|
|
280
|
+
else props.push(`${name}: true`);
|
|
281
|
+
}
|
|
282
|
+
return props.join(', ');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private rewriteImports(source: string): string {
|
|
286
|
+
// Rewrite bare specifiers to esm.sh CDN
|
|
287
|
+
return source.replace(
|
|
288
|
+
/^(import\s+(?:.*?\s+from\s+)?['"])([^./][^'"]*?)(['"])/gm,
|
|
289
|
+
(_, prefix, specifier, suffix) => {
|
|
290
|
+
// Keep relative imports as-is
|
|
291
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
292
|
+
return `${prefix}${specifier}${suffix}`;
|
|
293
|
+
}
|
|
294
|
+
// Map to esm.sh CDN
|
|
295
|
+
return `${prefix}https://esm.sh/${specifier}${suffix}`;
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
createObjectUrl(source: string, type = 'application/javascript'): string {
|
|
301
|
+
const blob = new Blob([source], { type });
|
|
302
|
+
const url = URL.createObjectURL(blob);
|
|
303
|
+
return url;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
revokeObjectUrl(url: string): void {
|
|
307
|
+
URL.revokeObjectURL(url);
|
|
308
|
+
this.objectUrls.delete(url);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
cleanup(): void {
|
|
312
|
+
for (const url of Array.from(this.objectUrls.values())) {
|
|
313
|
+
URL.revokeObjectURL(url);
|
|
314
|
+
}
|
|
315
|
+
this.objectUrls.clear();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* ImportMapBuilder — Generates browser-native import maps
|
|
321
|
+
*/
|
|
322
|
+
export class ImportMapBuilder {
|
|
323
|
+
private imports: Record<string, string> = {};
|
|
324
|
+
|
|
325
|
+
/** Add a package mapping to the import map */
|
|
326
|
+
add(specifier: string, url: string): this {
|
|
327
|
+
this.imports[specifier] = url;
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Add React and common packages */
|
|
332
|
+
addReactDefaults(reactVersion = '18'): this {
|
|
333
|
+
const base = `https://esm.sh`;
|
|
334
|
+
this.imports['react'] = `${base}/react@${reactVersion}`;
|
|
335
|
+
this.imports['react-dom'] = `${base}/react-dom@${reactVersion}`;
|
|
336
|
+
this.imports['react-dom/client'] = `${base}/react-dom@${reactVersion}/client`;
|
|
337
|
+
this.imports['react/jsx-runtime'] = `${base}/react@${reactVersion}/jsx-runtime`;
|
|
338
|
+
this.imports['react/jsx-dev-runtime'] = `${base}/react@${reactVersion}/jsx-dev-runtime`;
|
|
339
|
+
return this;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
build(): ImportMap {
|
|
343
|
+
return { imports: { ...this.imports } };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
toScriptTag(): string {
|
|
347
|
+
return `<script type="importmap">${JSON.stringify(this.build(), null, 2)}</script>`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* ModuleCache — LRU cache with integrity verification
|
|
353
|
+
*/
|
|
354
|
+
export class ModuleCache {
|
|
355
|
+
private cache: Map<string, ResolvedModule> = new Map();
|
|
356
|
+
private readonly maxSize: number;
|
|
357
|
+
|
|
358
|
+
constructor(maxSize = 200) {
|
|
359
|
+
this.maxSize = maxSize;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
set(path: string, module: ResolvedModule): void {
|
|
363
|
+
if (this.cache.size >= this.maxSize) {
|
|
364
|
+
// Evict oldest entry (LRU)
|
|
365
|
+
const oldest = Array.from(this.cache.keys())[0];
|
|
366
|
+
const old = this.cache.get(oldest);
|
|
367
|
+
if (old?.objectUrl) URL.revokeObjectURL(old.objectUrl);
|
|
368
|
+
this.cache.delete(oldest);
|
|
369
|
+
}
|
|
370
|
+
this.cache.set(path, module);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
get(path: string): ResolvedModule | null {
|
|
374
|
+
const module = this.cache.get(path);
|
|
375
|
+
if (!module) return null;
|
|
376
|
+
// Move to end (LRU update)
|
|
377
|
+
this.cache.delete(path);
|
|
378
|
+
this.cache.set(path, module);
|
|
379
|
+
return module;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
invalidate(path: string): void {
|
|
383
|
+
const module = this.cache.get(path);
|
|
384
|
+
if (module?.objectUrl) URL.revokeObjectURL(module.objectUrl);
|
|
385
|
+
this.cache.delete(path);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
clear(): void {
|
|
389
|
+
for (const module of Array.from(this.cache.values())) {
|
|
390
|
+
if (module.objectUrl) URL.revokeObjectURL(module.objectUrl);
|
|
391
|
+
}
|
|
392
|
+
this.cache.clear();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get size(): number {
|
|
396
|
+
return this.cache.size;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Default project template files */
|
|
401
|
+
export const DEFAULT_PROJECT_FILES: VirtualFile[] = [
|
|
402
|
+
{
|
|
403
|
+
path: '/src/App.tsx',
|
|
404
|
+
content: `import { useState } from 'react';
|
|
405
|
+
|
|
406
|
+
export default function App() {
|
|
407
|
+
const [count, setCount] = useState(0);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div style={{ fontFamily: 'system-ui', padding: '2rem', textAlign: 'center' }}>
|
|
411
|
+
<h1 style={{ color: '#e8650a', fontSize: '2.5rem', marginBottom: '0.5rem' }}>
|
|
412
|
+
JXR.js Edge Runtime
|
|
413
|
+
</h1>
|
|
414
|
+
<p style={{ color: '#888', marginBottom: '2rem' }}>
|
|
415
|
+
Zero-build React preview — powered by JXR Studios & DamascusAI
|
|
416
|
+
</p>
|
|
417
|
+
<button
|
|
418
|
+
onClick={() => setCount(c => c + 1)}
|
|
419
|
+
style={{
|
|
420
|
+
background: '#e8650a',
|
|
421
|
+
color: 'white',
|
|
422
|
+
border: 'none',
|
|
423
|
+
padding: '0.75rem 2rem',
|
|
424
|
+
borderRadius: '6px',
|
|
425
|
+
fontSize: '1rem',
|
|
426
|
+
cursor: 'pointer',
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
Count: {count}
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}`,
|
|
434
|
+
language: 'tsx',
|
|
435
|
+
lastModified: Date.now(),
|
|
436
|
+
size: 0,
|
|
437
|
+
dirty: false,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
path: '/src/index.tsx',
|
|
441
|
+
content: `import { createRoot } from 'react-dom/client';
|
|
442
|
+
import App from './App';
|
|
443
|
+
|
|
444
|
+
const root = createRoot(document.getElementById('root')!);
|
|
445
|
+
root.render(<App />);`,
|
|
446
|
+
language: 'tsx',
|
|
447
|
+
lastModified: Date.now(),
|
|
448
|
+
size: 0,
|
|
449
|
+
dirty: false,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
path: '/src/components/Button.tsx',
|
|
453
|
+
content: `interface ButtonProps {
|
|
454
|
+
children: React.ReactNode;
|
|
455
|
+
onClick?: () => void;
|
|
456
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
|
|
460
|
+
const styles = {
|
|
461
|
+
primary: { background: '#e8650a', color: 'white' },
|
|
462
|
+
secondary: { background: '#1a1a2e', color: '#e8650a', border: '1px solid #e8650a' },
|
|
463
|
+
ghost: { background: 'transparent', color: '#e8650a' },
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<button
|
|
468
|
+
onClick={onClick}
|
|
469
|
+
style={{
|
|
470
|
+
...styles[variant],
|
|
471
|
+
padding: '0.5rem 1.25rem',
|
|
472
|
+
borderRadius: '4px',
|
|
473
|
+
border: 'none',
|
|
474
|
+
cursor: 'pointer',
|
|
475
|
+
fontSize: '0.875rem',
|
|
476
|
+
fontWeight: 600,
|
|
477
|
+
transition: 'opacity 0.15s',
|
|
478
|
+
}}
|
|
479
|
+
>
|
|
480
|
+
{children}
|
|
481
|
+
</button>
|
|
482
|
+
);
|
|
483
|
+
}`,
|
|
484
|
+
language: 'tsx',
|
|
485
|
+
lastModified: Date.now(),
|
|
486
|
+
size: 0,
|
|
487
|
+
dirty: false,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
path: '/src/hooks/useCounter.ts',
|
|
491
|
+
content: `import { useState, useCallback } from 'react';
|
|
492
|
+
|
|
493
|
+
export function useCounter(initial = 0) {
|
|
494
|
+
const [count, setCount] = useState(initial);
|
|
495
|
+
const increment = useCallback(() => setCount(c => c + 1), []);
|
|
496
|
+
const decrement = useCallback(() => setCount(c => c - 1), []);
|
|
497
|
+
const reset = useCallback(() => setCount(initial), [initial]);
|
|
498
|
+
return { count, increment, decrement, reset };
|
|
499
|
+
}`,
|
|
500
|
+
language: 'ts',
|
|
501
|
+
lastModified: Date.now(),
|
|
502
|
+
size: 0,
|
|
503
|
+
dirty: false,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
path: '/package.json',
|
|
507
|
+
content: JSON.stringify({
|
|
508
|
+
name: 'jxr-project',
|
|
509
|
+
version: '0.1.0',
|
|
510
|
+
dependencies: {
|
|
511
|
+
react: '^18.3.0',
|
|
512
|
+
'react-dom': '^18.3.0',
|
|
513
|
+
},
|
|
514
|
+
}, null, 2),
|
|
515
|
+
language: 'json',
|
|
516
|
+
lastModified: Date.now(),
|
|
517
|
+
size: 0,
|
|
518
|
+
dirty: false,
|
|
519
|
+
},
|
|
520
|
+
];
|