-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.js
More file actions
117 lines (97 loc) · 3.78 KB
/
build.js
File metadata and controls
117 lines (97 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env node
/**
* Build script: converts Claude artifact TSX files into standalone HTML pages.
*
* For each subdirectory containing a .tsx file, it:
* 1. Reads the TSX source
* 2. Replaces `import { ... } from "react"` with `const { ... } = React;`
* 3. Strips `export default` from the App component declaration
* 4. Wraps everything in an HTML template with React + Babel standalone
* 5. Writes index.html alongside the TSX file
*
* Usage: node build.js
*/
const fs = require("fs");
const path = require("path");
const ROOT = __dirname;
// Derive a human-readable title from a kebab-case directory name
function dirToTitle(dirName) {
return dirName
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
// Transform artifact TSX source into browser-ready JS (still JSX, compiled by Babel standalone)
function transformSource(source) {
let code = source;
// 1. Replace React imports with destructured globals
// Handles: import { useState, useRef, ... } from "react";
// Also handles single/double quotes and optional semicolon
code = code.replace(
/import\s*\{([^}]+)\}\s*from\s*["']react["'];?\s*\n?/g,
(_, names) => {
const cleaned = names
.split(",")
.map((n) => n.trim())
.filter(Boolean)
.join(", ");
return `const { ${cleaned} } = React;\n`;
}
);
// 2. Remove any other import statements (e.g. CSS modules, other libs)
// that would fail in a browser context — unlikely for these artifacts but safe
code = code.replace(/^import\s+.*from\s+["'].*["'];?\s*$/gm, "// [import removed]");
// 3. Strip `export default` from function declarations
// export default function App() { ... } → function App() { ... }
code = code.replace(/export\s+default\s+function\s+/g, "function ");
// 4. Strip standalone `export default App;` statements
code = code.replace(/^export\s+default\s+\w+;?\s*$/gm, "");
return code;
}
function buildHTML(title, jsCode) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${title}</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"><\/script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"><\/script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"><\/script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
${jsCode}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
<\/script>
</body>
</html>
`;
}
// ── Main ──────────────────────────────────────────────────────────────────────
const entries = fs.readdirSync(ROOT, { withFileTypes: true });
let built = 0;
for (const entry of entries) {
if (!entry.isDirectory()) continue;
// Skip hidden dirs (.git, .claude, etc.)
if (entry.name.startsWith(".")) continue;
const dirPath = path.join(ROOT, entry.name);
const files = fs.readdirSync(dirPath);
const tsxFile = files.find((f) => f.endsWith(".tsx"));
if (!tsxFile) continue;
const tsxPath = path.join(dirPath, tsxFile);
const source = fs.readFileSync(tsxPath, "utf-8");
const title = dirToTitle(entry.name);
const jsCode = transformSource(source);
const html = buildHTML(title, jsCode);
const outPath = path.join(dirPath, "index.html");
fs.writeFileSync(outPath, html, "utf-8");
built++;
console.log(` ${entry.name}/${tsxFile} -> index.html (${title})`);
}
console.log(`\nBuilt ${built} app(s).`);