├── 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 |
OpenArtifacts
296 |
Form
297 |
Code
298 |
299 | App
300 |
301 | 🔄
302 |
303 |
304 |
?
305 |
306 |
307 |
348 |
371 |
372 |
373 |
374 |
383 |
384 |
385 |
386 |
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 |
--------------------------------------------------------------------------------