├── LICENSE.md ├── README.md ├── index.html └── typescript ├── .gitignore ├── README.md ├── bun.lock ├── client ├── Builder │ ├── esbuild-worker.js │ ├── index.html │ └── index.tsx ├── index.tsx └── sample_apps │ ├── cubes │ ├── App.tsx │ └── index.tsx │ └── example_app.ts ├── index.html ├── index.ts ├── package.json └── tsconfig.json /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Dual License for OpenArtifacts 2 | 3 | Copyright (c) [2024] Murat Ayfer 4 | 5 | ## Open Source License (For Non-Commercial Use) 6 | 7 | This project is licensed under the terms of the MIT License for non-commercial use: 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | ## Commercial License 16 | 17 | For commercial use of this software, a separate commercial license is required. Please contact murat@ayfer.net for licensing terms and pricing. 18 | 19 | Commercial use includes, but is not limited to: 20 | 21 | 1. Using the software in a commercial product or service 22 | 2. Using the software to provide commercial services 23 | 3. Using the software in a business or organizational context that creates commercial value 24 | 25 | The commercial license provides additional rights and support, including: 26 | 27 | 1. The right to use the software in commercial products and services 28 | 2. Priority support and bug fixes 29 | 3. Indemnification against intellectual property claims 30 | 31 | Failure to obtain a commercial license for commercial use of this software constitutes a breach of this license agreement and may result in legal action. 32 | 33 | ## License Selection 34 | 35 | By using this software, you agree to abide by the terms of either the Open Source License for non-commercial use or the Commercial License for commercial use. If you are unsure which license applies to your use case, please contact [contact information] for clarification. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenArtifacts 2 | 3 | Single HTML file for prompting an LLM and running the resulting front-end code immediately in an iframe. Supports JSX & external libraries. [Demo](http://mayfer.github.io/open-artifacts/) 4 | 5 | # Instructions 6 | 7 | Just load `index.html` on a browser lol 8 | 9 | # How does it work? 10 | 11 | It uses `esbuild-wasm` to bundle your JSX React code right on the browser, uses CDNs to load all npm packages, and runs it in an iframe. It's hacky but it works (usually) 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | OpenArtifacts 10 | 11 | 12 | 13 | 14 | 15 | 16 | 54 | 290 | 291 | 292 | 293 |
294 |
295 | 296 | 297 | 298 | 304 | 305 |
306 | 307 |
308 |
309 |
310 | 311 |
312 | OpenAI, 313 | 314 | Llama.cpp, 315 | Ollama, 316 | OpenRouter, 317 | Groq, 318 | Claude (coming soon) 319 |
320 |
321 |
322 | 323 | 325 |
326 |
327 | 328 | 329 |
330 |
331 | 332 | 333 |
334 |
335 | 336 | 338 |
339 |
340 | 341 |
342 |
343 |
344 | by murat
345 | clone on github 346 |
347 |
348 |
349 |
350 |
351 |
    352 |
353 | 356 | 357 | 364 | 365 |
366 |
367 |
368 |
369 | 370 |
371 |
372 | 387 | 388 |
389 |
390 |
391 |

OpenArtifacts

392 |

393 | OpenArtifacts is a clone of Antrhopic's Claude Artifacts which immediately runs the generated code in the browser. 394 |

395 |

396 | While the Claude version relies on a server to bundle the code, OpenArtifacts runs the bundler (esbuild-wasm) straight in the browser, and uses CDNs for all module dependencies instead of npm-style installers. 397 |

398 |

399 | The result is a single html file that can be opened in any browser, and the code is immediately runnable, even by double clicking the file instead of running a web server. 400 |

401 |

Limitations

402 |

403 | Due to some hacky tricks required to get the bundler working on browser-side directly, there may be some libraries or file types that are not supported. If you find any, please open an issue on github. 404 |

405 |

Credits

406 |

407 | OpenArtifcats is a project by murat. This is a complement project to AppCrapper, which is a similar project that generates both frontend & backend code but requires a server to execute. 408 |

409 |

410 | Clone on github 411 |

412 |
413 |
414 |
415 | 416 | 1021 | 1022 | 1023 | -------------------------------------------------------------------------------- /typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /typescript/README.md: -------------------------------------------------------------------------------- 1 | # typescript 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /typescript/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "typescript", 6 | "dependencies": { 7 | "@types/mime-types": "^2.1.4", 8 | "@types/prismjs": "^1.26.3", 9 | "mime-types": "^2.1.35", 10 | "prismjs": "^1.29.0", 11 | "react": "^19.0.0", 12 | "react-dom": "^19.0.0", 13 | "styled-components": "^6.1.15", 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "latest", 17 | "@types/react": "^19.0.10", 18 | "@types/react-dom": "^19.0.4", 19 | }, 20 | "peerDependencies": { 21 | "typescript": "^5", 22 | }, 23 | }, 24 | }, 25 | "packages": { 26 | "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.2.2", "", { "dependencies": { "@emotion/memoize": "^0.8.1" } }, "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw=="], 27 | 28 | "@emotion/memoize": ["@emotion/memoize@0.8.1", "", {}, "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="], 29 | 30 | "@emotion/unitless": ["@emotion/unitless@0.8.1", "", {}, "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="], 31 | 32 | "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], 33 | 34 | "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 35 | 36 | "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], 37 | 38 | "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], 39 | 40 | "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], 41 | 42 | "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], 43 | 44 | "@types/stylis": ["@types/stylis@4.2.5", "", {}, "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="], 45 | 46 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], 47 | 48 | "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], 49 | 50 | "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], 51 | 52 | "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], 53 | 54 | "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], 55 | 56 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 57 | 58 | "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 59 | 60 | "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 61 | 62 | "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], 63 | 64 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 65 | 66 | "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], 67 | 68 | "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], 69 | 70 | "prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="], 71 | 72 | "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], 73 | 74 | "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], 75 | 76 | "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], 77 | 78 | "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], 79 | 80 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 81 | 82 | "styled-components": ["styled-components@6.1.15", "", { "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" } }, "sha512-PpOTEztW87Ua2xbmLa7yssjNyUF9vE7wdldRfn1I2E6RTkqknkBYpj771OxM/xrvRGinLy2oysa7GOd7NcZZIA=="], 83 | 84 | "stylis": ["stylis@4.3.2", "", {}, "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="], 85 | 86 | "tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], 87 | 88 | "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], 89 | 90 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /typescript/client/Builder/esbuild-worker.js: -------------------------------------------------------------------------------- 1 | console.log('esbuild-worker.js'); 2 | importScripts('https://cdn.jsdelivr.net/npm/esbuild-wasm@0.23.0'); 3 | 4 | let initialized = false; 5 | 6 | async function initializeEsbuild() { 7 | if (!initialized) { 8 | await esbuild.initialize({ wasmURL: 'https://cdn.jsdelivr.net/npm/esbuild-wasm@0.23.0/esbuild.wasm' }); 9 | initialized = true; 10 | } 11 | } 12 | 13 | const virtualModules = { 14 | name: 'virtual-modules', 15 | setup(build) { 16 | build.onResolve({ filter: /.*/ }, (args) => { 17 | const resolveImport = (importPath) => { 18 | const normalizedPath = importPath.startsWith('/') ? importPath : `/${importPath}`; 19 | const possiblePaths = [ 20 | normalizedPath, 21 | `${normalizedPath}.js`, 22 | `${normalizedPath}.jsx`, 23 | `${normalizedPath}.ts`, 24 | `${normalizedPath}.tsx`, 25 | `${normalizedPath}.css`, 26 | `${normalizedPath}/index.js`, 27 | `${normalizedPath}/index.jsx`, 28 | `${normalizedPath}/index.ts`, 29 | `${normalizedPath}/index.tsx` 30 | ]; 31 | 32 | if (importPath.startsWith('.')) { 33 | const currentDir = args.importer.split('/').slice(0, -1).join('/'); 34 | const absolutePath = normalizePath(`${currentDir}/${importPath}`); 35 | possiblePaths.push( 36 | absolutePath, 37 | `${absolutePath}.js`, 38 | `${absolutePath}.jsx`, 39 | `${absolutePath}.ts`, 40 | `${absolutePath}.tsx`, 41 | `${absolutePath}.css`, 42 | `${absolutePath}/index.js`, 43 | `${absolutePath}/index.jsx`, 44 | `${absolutePath}/index.ts`, 45 | `${absolutePath}/index.tsx` 46 | ); 47 | } 48 | 49 | for (const path of possiblePaths) { 50 | if (path in self.fileContents) { 51 | return path; 52 | } 53 | } 54 | return null; 55 | }; 56 | 57 | const resolvedPath = resolveImport(args.path); 58 | if (resolvedPath) { 59 | return { path: resolvedPath, namespace: 'virtual-modules' }; 60 | } 61 | return undefined; 62 | }); 63 | 64 | build.onLoad({ filter: /.*/, namespace: 'virtual-modules' }, (args) => { 65 | if (args.path in self.fileContents) { 66 | const pathWithoutLeadingDot = args.path.startsWith('.') ? args.path.slice(1) : args.path; 67 | let loader = 'js'; 68 | if (args.path.endsWith('.jsx')) loader = 'jsx'; 69 | if (args.path.endsWith('.ts')) loader = 'ts'; 70 | if (args.path.endsWith('.tsx')) loader = 'tsx'; 71 | return { 72 | contents: self.fileContents[pathWithoutLeadingDot], 73 | loader 74 | }; 75 | } 76 | }); 77 | }, 78 | }; 79 | 80 | function normalizePath(path) { 81 | const parts = path.split('/'); 82 | const result = []; 83 | for (const part of parts) { 84 | if (part === '..') { 85 | result.pop(); 86 | } else if (part !== '.' && part !== '') { 87 | result.push(part); 88 | } 89 | } 90 | return '/' + result.join('/'); 91 | } 92 | 93 | const openartifactPlugin = { 94 | name: 'openartifact-plugin', 95 | setup(build) { 96 | const cache = {}; 97 | const module_host = 'https://cdn.jsdelivr.net'; 98 | const module_url_prefix = '/npm/'; 99 | 100 | build.onResolve({ filter: /.*/ }, async (args) => { 101 | let packageIdentifier = extractPackageIdentifier(args.path); 102 | let packageName = packageIdentifier.includes('react') || packageIdentifier.includes('three') 103 | ? packageIdentifier.replace(/@[\d.]+/, '') 104 | : packageIdentifier; 105 | 106 | const cached = cache[packageName]; 107 | if (cached) { 108 | return { path: cached, namespace: 'cdn' }; 109 | } else { 110 | const packageUrl = `${module_host}${module_url_prefix}${packageIdentifier}/+esm`; 111 | cache[packageName] = packageUrl; 112 | return { path: packageUrl, namespace: 'cdn' }; 113 | } 114 | }); 115 | 116 | build.onLoad({ filter: /^https:\/\/cdn.jsdelivr.net\// }, async (args) => { 117 | const response = await fetch(args.path); 118 | const contents = await response.text(); 119 | return { contents, loader: 'js' }; 120 | }); 121 | }, 122 | }; 123 | 124 | function extractPackageIdentifier(input) { 125 | let cleaned = input.replace(/^\/npm\//, '').replace(/\/\+esm$/, ''); 126 | const pattern = /^(@?[\w-]+(?:\/[\w-]+)?)(.*)$/; 127 | const match = cleaned.match(pattern); 128 | return match ? match[1] + match[2] : null; 129 | } 130 | 131 | self.onmessage = async function(e) { 132 | if (e.data.type === 'build') { 133 | self.fileContents = e.data.fileContents; 134 | 135 | try { 136 | await initializeEsbuild(); 137 | const result = await esbuild.build({ 138 | stdin: { 139 | contents: e.data.fileContents['/index.tsx'] || e.data.fileContents['/index.jsx'], 140 | resolveDir: '', 141 | loader: 'tsx' 142 | }, 143 | bundle: true, 144 | format: 'esm', 145 | minify: false, 146 | keepNames: true, 147 | jsx: 'automatic', 148 | target: 'es2020', 149 | platform: 'browser', 150 | plugins: [virtualModules, openartifactPlugin], 151 | write: false, 152 | loader: { 153 | '.js': 'jsx', 154 | '.ts': 'ts', 155 | '.tsx': 'tsx', 156 | '.css': 'css' 157 | } 158 | }); 159 | 160 | console.log('result', result); 161 | const jsFile = result.outputFiles[0].text 162 | 163 | self.postMessage({ type: 'success', output: JSON.stringify({ js: jsFile, css: '' }) }); 164 | 165 | } catch (error) { 166 | self.postMessage({ type: 'error', error: error.message }); 167 | } 168 | } else if (e.data.type === 'terminate') { 169 | console.log('Worker: Termination requested, disposing esbuild...'); 170 | await esbuild.dispose(); 171 | console.log('Worker: Final esbuild disposal complete'); 172 | self.close(); 173 | } 174 | }; -------------------------------------------------------------------------------- /typescript/client/Builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tsingtao Output 7 | 11 | 12 | 13 |
14 | 28 | 29 | -------------------------------------------------------------------------------- /typescript/client/Builder/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledSpinner = styled.svg` 5 | 6 | transform-origin: center; 7 | animation: spin .75s infinite linear; 8 | 9 | @keyframes spin { 10 | 100% { transform: rotate(360deg); } 11 | } 12 | `; 13 | 14 | const Container = styled.div` 15 | position: relative; 16 | width: 100%; 17 | overflow: hidden; 18 | `; 19 | 20 | const IframeWrapper = styled.iframe` 21 | width: 100%; 22 | height: 100vh; 23 | border: none; 24 | display: block; 25 | `; 26 | 27 | interface BuilderProps { 28 | initialFiles: Record; 29 | onResize?: (height: number) => void; 30 | } 31 | 32 | interface BuilderState { 33 | isLoading: boolean; 34 | } 35 | 36 | export default class Builder extends Component { 37 | private buildWorkerRef: Worker | null = null; 38 | private iframeRef = createRef(); 39 | private buildStarted = false; 40 | private resizeObserver: ResizeObserver | null = null; 41 | 42 | state = { 43 | isLoading: false 44 | }; 45 | 46 | componentDidMount() { 47 | if (!this.buildStarted) { 48 | this.buildStarted = true; 49 | this.renderGeneratedApp(this.props.initialFiles); 50 | } 51 | 52 | window.addEventListener('message', this.handleIframeMessage); 53 | } 54 | 55 | componentWillUnmount() { 56 | if (this.buildWorkerRef) { 57 | this.buildWorkerRef.terminate(); 58 | } 59 | window.removeEventListener('message', this.handleIframeMessage); 60 | if (this.resizeObserver) { 61 | this.resizeObserver.disconnect(); 62 | } 63 | } 64 | 65 | private handleIframeMessage = (event: MessageEvent) => { 66 | if (event.data.type === 'resize') { 67 | const height = event.data.height; 68 | if (this.iframeRef.current) { 69 | // this.iframeRef.current.style.height = `${height}px`; 70 | } 71 | this.props.onResize?.(height); 72 | } 73 | }; 74 | 75 | private initWorker = async () => { 76 | if (!this.buildWorkerRef) { 77 | const response = await fetch('/client/Builder/esbuild-worker.js'); 78 | const workerContent = await response.text(); 79 | const blob = new Blob([workerContent], { type: 'application/javascript' }); 80 | const workerUrl = URL.createObjectURL(blob); 81 | this.buildWorkerRef = new Worker(workerUrl); 82 | } 83 | return this.buildWorkerRef; 84 | }; 85 | 86 | private adjustIframeHeight = () => { 87 | const iframe = this.iframeRef.current; 88 | if (iframe && iframe.contentWindow) { 89 | try { 90 | const sendHeight = () => { 91 | const body = iframe.contentWindow?.document.body; 92 | const html = iframe.contentWindow?.document.documentElement; 93 | if (body && html) { 94 | const height = Math.max( 95 | body.scrollHeight, 96 | body.offsetHeight, 97 | html.clientHeight, 98 | html.scrollHeight, 99 | html.offsetHeight 100 | ); 101 | iframe.style.height = `${height}px`; 102 | this.props.onResize?.(height); 103 | } 104 | }; 105 | 106 | if (this.resizeObserver) { 107 | this.resizeObserver.disconnect(); 108 | } 109 | 110 | this.resizeObserver = new ResizeObserver(sendHeight); 111 | this.resizeObserver.observe(iframe.contentWindow.document.body); 112 | sendHeight(); 113 | } catch (error) { 114 | console.error('Could not adjust iframe height:', error); 115 | } 116 | } 117 | }; 118 | 119 | private renderGeneratedApp = async (fileContents: Record) => { 120 | this.setState({ isLoading: true }); 121 | console.log('Rendering generated app with files:', fileContents); 122 | const worker = await this.initWorker(); 123 | 124 | try { 125 | const result = await new Promise((resolve, reject) => { 126 | worker.onmessage = (e) => { 127 | if (e.data.type === 'success') { 128 | resolve(e.data.output); 129 | } else if (e.data.type === 'error') { 130 | reject(e.data.error); 131 | } 132 | }; 133 | 134 | worker.postMessage({ 135 | type: 'build', 136 | fileContents 137 | }); 138 | }); 139 | 140 | console.log('fileContents', fileContents); 141 | 142 | const parsedResult = JSON.parse(result); 143 | 144 | const htmlWrapper = await fetch('/client/Builder/index.html').then(res => res.text()); 145 | 146 | let newAppHtml = htmlWrapper.split('/* [[APP_CODE]] */').join(parsedResult.js); 147 | const newCss = parsedResult.css ? `` : ''; 148 | newAppHtml = newAppHtml.split('/* [[APP_CSS]] */').join(newCss); 149 | 150 | const appHtmlBlob = new Blob([newAppHtml], { type: 'text/html' }); 151 | const appHtmlUrl = URL.createObjectURL(appHtmlBlob); 152 | 153 | if (this.iframeRef.current) { 154 | // wipe iframe clean first 155 | this.iframeRef.current.contentWindow?.document.open(); 156 | this.iframeRef.current.contentWindow?.document.write(''); 157 | this.iframeRef.current.contentWindow?.document.close(); 158 | 159 | this.iframeRef.current.src = appHtmlUrl; 160 | this.iframeRef.current.onload = () => { 161 | URL.revokeObjectURL(appHtmlUrl); 162 | 163 | const iframe = this.iframeRef.current; 164 | if (iframe) { 165 | // iframe.contentWindow?.addEventListener('resize', this.adjustIframeHeight); 166 | // this.adjustIframeHeight(); 167 | } 168 | }; 169 | } 170 | 171 | worker.postMessage({ type: 'terminate' }); 172 | worker.terminate(); 173 | this.buildWorkerRef = null; 174 | 175 | } catch (error) { 176 | console.error('Build failed:', error); 177 | } finally { 178 | this.setState({ isLoading: false }); 179 | } 180 | }; 181 | 182 | render() { 183 | return ( 184 | 185 | {this.state.isLoading && ( 186 |
193 | 194 | 195 | 196 | 197 |
198 | )} 199 | 200 |
201 | ); 202 | } 203 | } -------------------------------------------------------------------------------- /typescript/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import Builder from './Builder'; 4 | import styled from 'styled-components'; 5 | import Prism from 'prismjs'; 6 | import 'prismjs/themes/prism.css'; 7 | import 'prismjs/components/prism-typescript'; 8 | import 'prismjs/components/prism-jsx'; 9 | import 'prismjs/components/prism-tsx'; 10 | 11 | const widgetFiles: Record = {}; 12 | const sampleDir = '/client/sample_apps/cubes'; 13 | const files = [ 14 | '/index.tsx', 15 | '/App.tsx' 16 | ]; 17 | 18 | // Fetch and populate widget files 19 | await Promise.all(files.map(async (file) => { 20 | const response = await fetch(`${sampleDir}/${file}`); 21 | const content = await response.text(); 22 | widgetFiles[file] = content; 23 | })); 24 | 25 | const Layout = styled.div` 26 | display: flex; 27 | gap: 20px; 28 | padding: 20px; 29 | height: calc(100vh - 100px); // Account for header 30 | `; 31 | 32 | const LeftColumn = styled.div` 33 | flex: 1; 34 | overflow-y: auto; 35 | padding: 20px; 36 | background: #f5f5f5; 37 | border-radius: 10px; 38 | min-height: 500px; 39 | `; 40 | 41 | const RightColumn = styled.div` 42 | flex: 2; 43 | min-width: 0; 44 | `; 45 | 46 | const BuilderContainer = styled.div` 47 | border: 1px solid #ccc; 48 | border-radius: 10px; 49 | overflow: hidden; 50 | min-height: 500px; 51 | position: relative; 52 | height: 100%; 53 | `; 54 | 55 | const Main = styled.div` 56 | font-family: monospace; 57 | `; 58 | 59 | const FileContent = styled.div` 60 | margin-bottom: 20px; 61 | padding: 15px; 62 | background: white; 63 | border-radius: 8px; 64 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 65 | max-height: 400px; 66 | overflow-y: auto; 67 | 68 | pre { 69 | margin: 0; 70 | white-space: pre-wrap; 71 | word-break: break-word; 72 | font-size: 13px; 73 | } 74 | `; 75 | 76 | const FileTitle = styled.div` 77 | font-size: 14px; 78 | font-weight: bold; 79 | margin-bottom: 10px; 80 | `; 81 | 82 | function Clock() { 83 | useEffect(() => { 84 | Prism.highlightAll(); 85 | }, []); 86 | const [height, setHeight] = useState('auto'); 87 | 88 | return ( 89 |
90 |

Browser-side Typescript & TSX

91 | 92 | 93 | {Object.entries(widgetFiles).map(([filename, content]) => ( 94 | 95 | {filename} 96 |
 97 |             
 98 |               {content}
 99 |             
100 |           
101 |
102 | ))} 103 |
104 | 105 | 106 | setHeight(`${height}px`)} 109 | /> 110 | 111 | 112 |
113 |
114 | ); 115 | } 116 | 117 | const root = createRoot(document.getElementById('root')!); 118 | root.render(); -------------------------------------------------------------------------------- /typescript/client/sample_apps/cubes/App.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas, useFrame } from '@react-three/fiber' 2 | import { OrbitControls } from '@react-three/drei' 3 | import { useMemo, useRef } from 'react' 4 | import * as THREE from 'three' 5 | 6 | const CUBE_COUNT = 1000 7 | const GRID_SIZE = 10 8 | const SPACING = 4 9 | 10 | function InstancedCubes() { 11 | const meshRef = useRef() 12 | const tempMatrix = useMemo(() => new THREE.Matrix4(), []) 13 | const tempPosition = useMemo(() => new THREE.Vector3(), []) 14 | const tempRotation = useMemo(() => new THREE.Euler(), []) 15 | const tempQuaternion = useMemo(() => new THREE.Quaternion(), []) 16 | const tempScale = useMemo(() => new THREE.Vector3(1, 1, 1), []) 17 | 18 | // Generate initial positions 19 | const [matrices, colors] = useMemo(() => { 20 | const tempMatrices = [] 21 | const tempColors = [] 22 | let index = 0 23 | 24 | for (let x = 0; x < GRID_SIZE; x++) { 25 | for (let y = 0; y < GRID_SIZE; y++) { 26 | for (let z = 0; z < GRID_SIZE; z++) { 27 | const matrix = new THREE.Matrix4() 28 | matrix.setPosition( 29 | (x - GRID_SIZE / 2) * SPACING, 30 | (y - GRID_SIZE / 2) * SPACING, 31 | (z - GRID_SIZE / 2) * SPACING 32 | ) 33 | tempMatrices.push(matrix) 34 | 35 | // Generate random color 36 | const color = new THREE.Color( 37 | Math.random(), 38 | Math.random(), 39 | Math.random() 40 | ) 41 | tempColors.push(color) 42 | index++ 43 | } 44 | } 45 | } 46 | return [tempMatrices, tempColors] 47 | }, []) 48 | 49 | useFrame((state) => { 50 | const { clock } = state 51 | const time = clock.getElapsedTime() 52 | 53 | let index = 0 54 | for (let x = 0; x < GRID_SIZE; x++) { 55 | for (let y = 0; y < GRID_SIZE; y++) { 56 | for (let z = 0; z < GRID_SIZE; z++) { 57 | // Calculate position 58 | tempPosition.set( 59 | (x - GRID_SIZE / 2) * SPACING, 60 | (y - GRID_SIZE / 2) * SPACING, 61 | (z - GRID_SIZE / 2) * SPACING 62 | ) 63 | 64 | // Calculate rotation 65 | tempRotation.set( 66 | time * 0.5 + x * 0.1, 67 | time * 0.3 + y * 0.1, 68 | time * 0.2 + z * 0.1 69 | ) 70 | tempQuaternion.setFromEuler(tempRotation) 71 | 72 | // Compose matrix 73 | tempMatrix.compose(tempPosition, tempQuaternion, tempScale) 74 | meshRef.current.setMatrixAt(index, tempMatrix) 75 | index++ 76 | } 77 | } 78 | } 79 | meshRef.current.instanceMatrix.needsUpdate = true 80 | }) 81 | 82 | return ( 83 | 84 | 85 | [c.r, c.g, c.b])), 3]} 88 | /> 89 | 90 | 91 | 92 | ) 93 | } 94 | 95 | export default function Scene() { 96 | return ( 97 |
98 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | ) 111 | } -------------------------------------------------------------------------------- /typescript/client/sample_apps/cubes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | 5 | console.log('cubes height', window.innerHeight) 6 | 7 | const root = createRoot(document.getElementById('root') as HTMLElement); 8 | root.render(); -------------------------------------------------------------------------------- /typescript/client/sample_apps/example_app.ts: -------------------------------------------------------------------------------- 1 | export const example_app = { 2 | '/index.tsx': ` 3 | import React, { useRef, useLayoutEffect } from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import { Canvas, useThree } from "@react-three/fiber" 6 | import * as THREE from 'three' 7 | 8 | interface BoxProps { 9 | position: [number, number, number]; 10 | color: string; 11 | } 12 | 13 | function Box({ position, color }: BoxProps) { 14 | const ref = useRef(null) 15 | 16 | useLayoutEffect(() => { 17 | if (ref.current) { 18 | ref.current.rotation.x = 0.01 19 | ref.current.rotation.y = 0.01 20 | } 21 | }, []) 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function Scene() { 32 | const { gl, scene, camera } = useThree() 33 | 34 | useLayoutEffect(() => { 35 | gl.render(scene, camera) 36 | gl.setAnimationLoop(null) 37 | }, [gl, scene, camera]) 38 | 39 | return ( 40 | <> 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | function App(): JSX.Element { 49 | return ( 50 | 51 | 52 | 53 | ) 54 | } 55 | 56 | const root = createRoot(document.getElementById('root') as HTMLElement); 57 | root.render(); 58 | ` 59 | } -------------------------------------------------------------------------------- /typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tsingtao 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /typescript/index.ts: -------------------------------------------------------------------------------- 1 | import { serve, sql } from "bun"; 2 | import App from "./index.html"; 3 | import mime from "mime-types"; 4 | 5 | serve({ 6 | port: 3000, 7 | routes: { 8 | "/client/*": (req) => { 9 | const url = new URL(req.url); 10 | const pathname = url.pathname; 11 | const filePath = pathname.replace("/client/", "client/"); 12 | 13 | const file = Bun.file(filePath); 14 | const mimeType = mime.lookup(pathname) || 'application/octet-stream'; 15 | return new Response(file, { 16 | headers: { "Content-Type": mimeType }, 17 | }); 18 | }, 19 | "/*": App, 20 | }, 21 | fetch: () => new Response("Not Found", { status: 404 }) 22 | }); 23 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsingtao", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | "@types/react": "^19.0.10", 9 | "@types/react-dom": "^19.0.4" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5" 13 | }, 14 | "dependencies": { 15 | "@types/mime-types": "^2.1.4", 16 | "@types/prismjs": "^1.26.3", 17 | "mime-types": "^2.1.35", 18 | "prismjs": "^1.29.0", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0", 21 | "styled-components": "^6.1.15" 22 | } 23 | } -------------------------------------------------------------------------------- /typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | }, 27 | "include": ["./*.ts", "./*.tsx", "client/index.tsx"], 28 | "exclude": ["client/sample_apps/*"] 29 | } 30 | --------------------------------------------------------------------------------