clawfire 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +386 -88
- package/dist/admin.d.cts +1 -1
- package/dist/admin.d.ts +1 -1
- package/dist/cli.js +23 -5
- package/dist/{config-QMBJRn9G.d.cts → config-D-Chojiu.d.cts} +7 -0
- package/dist/{config-QMBJRn9G.d.ts → config-D-Chojiu.d.ts} +7 -0
- package/dist/{dev-server-QAVWINAT.js → dev-server-ZGXJARNY.js} +712 -99
- package/dist/dev.cjs +712 -99
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +35 -36
- package/dist/dev.d.ts +35 -36
- package/dist/dev.js +712 -99
- package/dist/dev.js.map +1 -1
- package/dist/functions.d.cts +1 -1
- package/dist/functions.d.ts +1 -1
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +18 -1
- package/templates/clawfire.config.ts +4 -0
- package/templates/starter/CLAUDE.md +85 -5
- package/templates/starter/app/components/footer.html +5 -0
- package/templates/starter/app/components/nav.html +14 -0
- package/templates/starter/app/pages/_404.html +9 -0
- package/templates/starter/app/pages/_layout.html +46 -0
- package/templates/starter/app/pages/about.html +84 -0
- package/templates/starter/app/pages/index.html +192 -0
- package/templates/starter/app/pages/todos/index.html +173 -0
- package/templates/starter/clawfire.config.ts +4 -0
- package/templates/starter/dev.ts +2 -1
- package/templates/starter/public/index.html +9 -345
package/dist/dev.cjs
CHANGED
|
@@ -38,8 +38,8 @@ module.exports = __toCommonJS(dev_exports);
|
|
|
38
38
|
|
|
39
39
|
// src/dev/dev-server.ts
|
|
40
40
|
var import_node_http = __toESM(require("http"), 1);
|
|
41
|
-
var
|
|
42
|
-
var
|
|
41
|
+
var import_node_path2 = require("path");
|
|
42
|
+
var import_node_fs2 = require("fs");
|
|
43
43
|
var import_node_url = require("url");
|
|
44
44
|
|
|
45
45
|
// src/core/schema.ts
|
|
@@ -946,6 +946,55 @@ var FileWatcher = class extends import_events.EventEmitter {
|
|
|
946
946
|
this.watchers.push(watcher);
|
|
947
947
|
return this;
|
|
948
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* 프론트엔드 디렉터리 감시 (확장자별 이벤트 타입 자동 결정)
|
|
951
|
+
*/
|
|
952
|
+
watchDirFrontend(dir) {
|
|
953
|
+
if (!(0, import_fs2.existsSync)(dir)) return this;
|
|
954
|
+
try {
|
|
955
|
+
const watcher = (0, import_fs2.watch)(dir, { recursive: true }, (event, filename) => {
|
|
956
|
+
if (!filename) return;
|
|
957
|
+
const filePath = (0, import_path2.join)(dir, filename);
|
|
958
|
+
if (!this.isWatchedFile(filePath)) return;
|
|
959
|
+
const ext = (0, import_path2.extname)(filePath);
|
|
960
|
+
const eventType = ext === ".css" ? "css-change" : "frontend-change";
|
|
961
|
+
this.emitDebounced(filePath, eventType);
|
|
962
|
+
});
|
|
963
|
+
watcher.on("error", (err) => {
|
|
964
|
+
if (err.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM") {
|
|
965
|
+
this.watchDirFrontendRecursiveManual(dir);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
this.watchers.push(watcher);
|
|
969
|
+
} catch {
|
|
970
|
+
this.watchDirFrontendRecursiveManual(dir);
|
|
971
|
+
}
|
|
972
|
+
return this;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Linux fallback: 프론트엔드 디렉터리 수동 재귀 감시
|
|
976
|
+
*/
|
|
977
|
+
watchDirFrontendRecursiveManual(dir) {
|
|
978
|
+
if (!(0, import_fs2.existsSync)(dir)) return;
|
|
979
|
+
const watcher = (0, import_fs2.watch)(dir, (event, filename) => {
|
|
980
|
+
if (!filename) return;
|
|
981
|
+
const filePath = (0, import_path2.join)(dir, filename);
|
|
982
|
+
if (!this.isWatchedFile(filePath)) return;
|
|
983
|
+
const ext = (0, import_path2.extname)(filePath);
|
|
984
|
+
const eventType = ext === ".css" ? "css-change" : "frontend-change";
|
|
985
|
+
this.emitDebounced(filePath, eventType);
|
|
986
|
+
});
|
|
987
|
+
this.watchers.push(watcher);
|
|
988
|
+
try {
|
|
989
|
+
const entries = (0, import_fs2.readdirSync)(dir, { withFileTypes: true });
|
|
990
|
+
for (const entry of entries) {
|
|
991
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
992
|
+
this.watchDirFrontendRecursiveManual((0, import_path2.join)(dir, entry.name));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
997
|
+
}
|
|
949
998
|
/**
|
|
950
999
|
* Linux fallback: 디렉터리 수동 재귀 감시
|
|
951
1000
|
*/
|
|
@@ -973,7 +1022,7 @@ var FileWatcher = class extends import_events.EventEmitter {
|
|
|
973
1022
|
*/
|
|
974
1023
|
isWatchedFile(filePath) {
|
|
975
1024
|
const ext = (0, import_path2.extname)(filePath);
|
|
976
|
-
return [".ts", ".tsx", ".js", ".jsx", ".json"].includes(ext);
|
|
1025
|
+
return [".ts", ".tsx", ".js", ".jsx", ".json", ".html", ".css", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".woff", ".woff2"].includes(ext);
|
|
977
1026
|
}
|
|
978
1027
|
/**
|
|
979
1028
|
* 디바운스 이벤트 발생
|
|
@@ -1007,78 +1056,486 @@ var FileWatcher = class extends import_events.EventEmitter {
|
|
|
1007
1056
|
}
|
|
1008
1057
|
};
|
|
1009
1058
|
|
|
1059
|
+
// src/dev/page-compiler.ts
|
|
1060
|
+
var import_node_path = require("path");
|
|
1061
|
+
var import_node_fs = require("fs");
|
|
1062
|
+
var MAX_COMPONENT_DEPTH = 10;
|
|
1063
|
+
var META_REGEX = /<!--\s*@(\w+):\s*(.+?)\s*-->/g;
|
|
1064
|
+
var COMPONENT_REGEX = /<c-([a-z][a-z0-9-]*)\s*\/>/g;
|
|
1065
|
+
var SLOT_MARKER = "<slot />";
|
|
1066
|
+
var PageCompiler = class {
|
|
1067
|
+
constructor(projectDir) {
|
|
1068
|
+
this.projectDir = projectDir;
|
|
1069
|
+
this.pagesDir = (0, import_node_path.resolve)(projectDir, "app/pages");
|
|
1070
|
+
this.componentsDir = (0, import_node_path.resolve)(projectDir, "app/components");
|
|
1071
|
+
}
|
|
1072
|
+
pagesDir;
|
|
1073
|
+
componentsDir;
|
|
1074
|
+
/**
|
|
1075
|
+
* Check if the page system is active (app/pages/ exists)
|
|
1076
|
+
*/
|
|
1077
|
+
isActive() {
|
|
1078
|
+
return (0, import_node_fs.existsSync)(this.pagesDir);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Resolve a URL pathname to a page file path.
|
|
1082
|
+
* Returns null if no matching page exists.
|
|
1083
|
+
*
|
|
1084
|
+
* / → app/pages/index.html
|
|
1085
|
+
* /about → app/pages/about.html OR app/pages/about/index.html
|
|
1086
|
+
* /todos → app/pages/todos/index.html OR app/pages/todos.html
|
|
1087
|
+
*/
|
|
1088
|
+
resolve(pathname) {
|
|
1089
|
+
if (!this.isActive()) return null;
|
|
1090
|
+
const clean = pathname === "/" ? "" : pathname.replace(/\/+$/, "");
|
|
1091
|
+
const segments = clean ? clean.split("/").filter(Boolean) : [];
|
|
1092
|
+
const candidates = [];
|
|
1093
|
+
if (segments.length === 0) {
|
|
1094
|
+
candidates.push((0, import_node_path.join)(this.pagesDir, "index.html"));
|
|
1095
|
+
} else {
|
|
1096
|
+
const pathPart = segments.join("/");
|
|
1097
|
+
candidates.push((0, import_node_path.join)(this.pagesDir, `${pathPart}.html`));
|
|
1098
|
+
candidates.push((0, import_node_path.join)(this.pagesDir, pathPart, "index.html"));
|
|
1099
|
+
}
|
|
1100
|
+
for (const candidate of candidates) {
|
|
1101
|
+
if (!candidate.startsWith(this.pagesDir)) continue;
|
|
1102
|
+
const name = (0, import_node_path.basename)(candidate);
|
|
1103
|
+
if (name.startsWith("_")) continue;
|
|
1104
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolve the 404 page file path.
|
|
1110
|
+
*/
|
|
1111
|
+
resolve404() {
|
|
1112
|
+
const path404 = (0, import_node_path.join)(this.pagesDir, "_404.html");
|
|
1113
|
+
return (0, import_node_fs.existsSync)(path404) ? path404 : null;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Compile a full page with layouts and components.
|
|
1117
|
+
*/
|
|
1118
|
+
compile(pagePath) {
|
|
1119
|
+
let pageHtml = (0, import_node_fs.readFileSync)(pagePath, "utf-8");
|
|
1120
|
+
const meta = this.extractMeta(pageHtml);
|
|
1121
|
+
pageHtml = this.stripMeta(pageHtml);
|
|
1122
|
+
pageHtml = this.processComponents(pageHtml);
|
|
1123
|
+
pageHtml = this.wrapPageContent(pageHtml);
|
|
1124
|
+
const layouts = this.collectLayouts(pagePath);
|
|
1125
|
+
let html = pageHtml;
|
|
1126
|
+
for (const layoutPath of layouts) {
|
|
1127
|
+
let layoutHtml = (0, import_node_fs.readFileSync)(layoutPath, "utf-8");
|
|
1128
|
+
layoutHtml = this.processComponents(layoutHtml);
|
|
1129
|
+
html = layoutHtml.replace(SLOT_MARKER, html);
|
|
1130
|
+
}
|
|
1131
|
+
if (meta.title) {
|
|
1132
|
+
html = this.setTitle(html, meta.title);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
html,
|
|
1136
|
+
meta,
|
|
1137
|
+
filePath: pagePath,
|
|
1138
|
+
layouts
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Compile a partial page for SPA navigation.
|
|
1143
|
+
* Returns only the page content (no layout) plus metadata.
|
|
1144
|
+
*/
|
|
1145
|
+
compilePartial(pagePath, pathname) {
|
|
1146
|
+
let pageHtml = (0, import_node_fs.readFileSync)(pagePath, "utf-8");
|
|
1147
|
+
const meta = this.extractMeta(pageHtml);
|
|
1148
|
+
pageHtml = this.stripMeta(pageHtml);
|
|
1149
|
+
pageHtml = this.processComponents(pageHtml);
|
|
1150
|
+
return {
|
|
1151
|
+
html: pageHtml,
|
|
1152
|
+
meta,
|
|
1153
|
+
path: pathname
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
// ─── Internal Methods ────────────────────────────────────────────
|
|
1157
|
+
/**
|
|
1158
|
+
* Extract <!-- @key: value --> metadata from HTML.
|
|
1159
|
+
*/
|
|
1160
|
+
extractMeta(html) {
|
|
1161
|
+
const meta = {};
|
|
1162
|
+
let match;
|
|
1163
|
+
const regex = new RegExp(META_REGEX.source, META_REGEX.flags);
|
|
1164
|
+
while ((match = regex.exec(html)) !== null) {
|
|
1165
|
+
meta[match[1]] = match[2];
|
|
1166
|
+
}
|
|
1167
|
+
return meta;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Strip metadata comments from HTML.
|
|
1171
|
+
*/
|
|
1172
|
+
stripMeta(html) {
|
|
1173
|
+
return html.replace(META_REGEX, "").trim();
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Process <c-name /> component tags by replacing them with component file contents.
|
|
1177
|
+
* Supports nested components up to MAX_COMPONENT_DEPTH.
|
|
1178
|
+
*/
|
|
1179
|
+
processComponents(html, depth = 0) {
|
|
1180
|
+
if (depth >= MAX_COMPONENT_DEPTH) return html;
|
|
1181
|
+
if (!COMPONENT_REGEX.test(html)) return html;
|
|
1182
|
+
const regex = new RegExp(COMPONENT_REGEX.source, COMPONENT_REGEX.flags);
|
|
1183
|
+
const result = html.replace(regex, (_match, name) => {
|
|
1184
|
+
const componentPath = (0, import_node_path.join)(this.componentsDir, `${name}.html`);
|
|
1185
|
+
if (!(0, import_node_fs.existsSync)(componentPath)) {
|
|
1186
|
+
return `<!-- component "${name}" not found -->`;
|
|
1187
|
+
}
|
|
1188
|
+
const content = (0, import_node_fs.readFileSync)(componentPath, "utf-8").trim();
|
|
1189
|
+
return this.processComponents(content, depth + 1);
|
|
1190
|
+
});
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Wrap page content in the router target div.
|
|
1195
|
+
*/
|
|
1196
|
+
wrapPageContent(html) {
|
|
1197
|
+
return `<div id="clawfire-page">
|
|
1198
|
+
${html}
|
|
1199
|
+
</div>`;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Collect layout files from page directory up to pages root.
|
|
1203
|
+
* Returns innermost layout first (closest to page).
|
|
1204
|
+
*/
|
|
1205
|
+
collectLayouts(pagePath) {
|
|
1206
|
+
const layouts = [];
|
|
1207
|
+
let dir = (0, import_node_path.dirname)(pagePath);
|
|
1208
|
+
while (dir.startsWith(this.pagesDir)) {
|
|
1209
|
+
const layoutPath = (0, import_node_path.join)(dir, "_layout.html");
|
|
1210
|
+
if ((0, import_node_fs.existsSync)(layoutPath)) {
|
|
1211
|
+
layouts.push(layoutPath);
|
|
1212
|
+
}
|
|
1213
|
+
if (dir === this.pagesDir) break;
|
|
1214
|
+
dir = (0, import_node_path.dirname)(dir);
|
|
1215
|
+
}
|
|
1216
|
+
return layouts;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Set or update <title> tag in HTML.
|
|
1220
|
+
*/
|
|
1221
|
+
setTitle(html, title) {
|
|
1222
|
+
if (html.includes("<title>")) {
|
|
1223
|
+
return html.replace(/<title>[^<]*<\/title>/, `<title>${title}</title>`);
|
|
1224
|
+
}
|
|
1225
|
+
if (html.includes("</head>")) {
|
|
1226
|
+
return html.replace("</head>", ` <title>${title}</title>
|
|
1227
|
+
</head>`);
|
|
1228
|
+
}
|
|
1229
|
+
return html;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1010
1233
|
// src/dev/dev-server.ts
|
|
1234
|
+
var MIME_TYPES = {
|
|
1235
|
+
html: "text/html; charset=utf-8",
|
|
1236
|
+
css: "text/css; charset=utf-8",
|
|
1237
|
+
js: "application/javascript; charset=utf-8",
|
|
1238
|
+
mjs: "application/javascript; charset=utf-8",
|
|
1239
|
+
json: "application/json; charset=utf-8",
|
|
1240
|
+
png: "image/png",
|
|
1241
|
+
jpg: "image/jpeg",
|
|
1242
|
+
jpeg: "image/jpeg",
|
|
1243
|
+
gif: "image/gif",
|
|
1244
|
+
svg: "image/svg+xml",
|
|
1245
|
+
ico: "image/x-icon",
|
|
1246
|
+
webp: "image/webp",
|
|
1247
|
+
woff: "font/woff",
|
|
1248
|
+
woff2: "font/woff2",
|
|
1249
|
+
ttf: "font/ttf",
|
|
1250
|
+
eot: "application/vnd.ms-fontobject",
|
|
1251
|
+
mp4: "video/mp4",
|
|
1252
|
+
webm: "video/webm",
|
|
1253
|
+
mp3: "audio/mpeg",
|
|
1254
|
+
wav: "audio/wav",
|
|
1255
|
+
pdf: "application/pdf",
|
|
1256
|
+
txt: "text/plain; charset=utf-8",
|
|
1257
|
+
xml: "application/xml; charset=utf-8"
|
|
1258
|
+
};
|
|
1259
|
+
var STATIC_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1260
|
+
".css",
|
|
1261
|
+
".js",
|
|
1262
|
+
".mjs",
|
|
1263
|
+
".json",
|
|
1264
|
+
".png",
|
|
1265
|
+
".jpg",
|
|
1266
|
+
".jpeg",
|
|
1267
|
+
".gif",
|
|
1268
|
+
".svg",
|
|
1269
|
+
".ico",
|
|
1270
|
+
".webp",
|
|
1271
|
+
".woff",
|
|
1272
|
+
".woff2",
|
|
1273
|
+
".ttf",
|
|
1274
|
+
".eot",
|
|
1275
|
+
".mp4",
|
|
1276
|
+
".webm",
|
|
1277
|
+
".mp3",
|
|
1278
|
+
".wav",
|
|
1279
|
+
".pdf",
|
|
1280
|
+
".txt",
|
|
1281
|
+
".xml",
|
|
1282
|
+
".map"
|
|
1283
|
+
]);
|
|
1284
|
+
function generateHmrScript(port) {
|
|
1285
|
+
return `
|
|
1286
|
+
<script data-clawfire-hmr>
|
|
1287
|
+
(function() {
|
|
1288
|
+
var dot, status;
|
|
1289
|
+
function createBanner() {
|
|
1290
|
+
var banner = document.createElement('div');
|
|
1291
|
+
banner.id = 'clawfire-dev-banner';
|
|
1292
|
+
banner.style.cssText = 'position:fixed;bottom:0;left:0;right:0;padding:6px 16px;background:#1a1a2e;color:#f97316;font-size:12px;font-family:monospace;z-index:99999;display:flex;align-items:center;gap:8px;border-top:1px solid #2a2a2a;';
|
|
1293
|
+
banner.innerHTML = '<span style="width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block;" id="clawfire-dot"></span><span>Clawfire Dev</span><span style="color:#666;margin-left:auto;" id="clawfire-status">Connected</span>';
|
|
1294
|
+
document.body.appendChild(banner);
|
|
1295
|
+
dot = document.getElementById('clawfire-dot');
|
|
1296
|
+
status = document.getElementById('clawfire-status');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function refreshCss() {
|
|
1300
|
+
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
1301
|
+
for (var i = 0; i < links.length; i++) {
|
|
1302
|
+
var href = links[i].getAttribute('href');
|
|
1303
|
+
if (href) {
|
|
1304
|
+
var url = new URL(href, location.href);
|
|
1305
|
+
url.searchParams.set('_hmr', Date.now().toString());
|
|
1306
|
+
links[i].setAttribute('href', url.toString());
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
var styles = document.querySelectorAll('style[data-href]');
|
|
1310
|
+
for (var j = 0; j < styles.length; j++) {
|
|
1311
|
+
var dataHref = styles[j].getAttribute('data-href');
|
|
1312
|
+
if (dataHref) {
|
|
1313
|
+
fetch(dataHref + '?_hmr=' + Date.now())
|
|
1314
|
+
.then(function(r) { return r.text(); })
|
|
1315
|
+
.then(function(css) { styles[j].textContent = css; })
|
|
1316
|
+
.catch(function() {});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (status) status.textContent = 'CSS updated';
|
|
1320
|
+
setTimeout(function() { if (status) status.textContent = 'Connected'; }, 1500);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
var reconnectTimer;
|
|
1324
|
+
function connect() {
|
|
1325
|
+
var es = new EventSource('http://localhost:${port}/__dev/events');
|
|
1326
|
+
es.onopen = function() {
|
|
1327
|
+
if (!dot) createBanner();
|
|
1328
|
+
dot.style.background = '#22c55e';
|
|
1329
|
+
status.textContent = 'Connected';
|
|
1330
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1331
|
+
};
|
|
1332
|
+
es.onmessage = function(e) {
|
|
1333
|
+
try {
|
|
1334
|
+
var data = JSON.parse(e.data);
|
|
1335
|
+
if (data.type === 'connected') return;
|
|
1336
|
+
if (data.type === 'css-change') {
|
|
1337
|
+
refreshCss();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (data.type === 'error') {
|
|
1341
|
+
if (dot) dot.style.background = '#ef4444';
|
|
1342
|
+
if (status) status.textContent = 'Error: ' + (data.message || 'reload failed');
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
// frontend-change, route-change, page-change, component-change \u2192 full reload
|
|
1346
|
+
if (dot) dot.style.background = '#eab308';
|
|
1347
|
+
if (status) status.textContent = 'Reloading...';
|
|
1348
|
+
setTimeout(function() { location.reload(); }, 300);
|
|
1349
|
+
} catch(err) {}
|
|
1350
|
+
};
|
|
1351
|
+
es.onerror = function() {
|
|
1352
|
+
es.close();
|
|
1353
|
+
if (dot) dot.style.background = '#ef4444';
|
|
1354
|
+
if (status) status.textContent = 'Disconnected';
|
|
1355
|
+
reconnectTimer = setTimeout(connect, 2000);
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (document.readyState === 'loading') {
|
|
1360
|
+
document.addEventListener('DOMContentLoaded', function() { connect(); });
|
|
1361
|
+
} else {
|
|
1362
|
+
connect();
|
|
1363
|
+
}
|
|
1364
|
+
})();
|
|
1365
|
+
</script>`;
|
|
1366
|
+
}
|
|
1367
|
+
function generateRouterScript() {
|
|
1368
|
+
return `
|
|
1369
|
+
<script data-clawfire-router>
|
|
1370
|
+
(function() {
|
|
1371
|
+
function updateActiveNav() {
|
|
1372
|
+
var path = location.pathname;
|
|
1373
|
+
var links = document.querySelectorAll('#nav-links a[href]');
|
|
1374
|
+
for (var i = 0; i < links.length; i++) {
|
|
1375
|
+
var href = links[i].getAttribute('href');
|
|
1376
|
+
if (!href || href.startsWith('http')) continue;
|
|
1377
|
+
var isActive = (path === '/' && href === '/') || (href !== '/' && path.startsWith(href));
|
|
1378
|
+
links[i].setAttribute('data-active', isActive ? 'true' : 'false');
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function navigate(url) {
|
|
1383
|
+
var target = new URL(url, location.href);
|
|
1384
|
+
// Only handle same-origin, non-hash navigation
|
|
1385
|
+
if (target.origin !== location.origin) return false;
|
|
1386
|
+
if (target.pathname === location.pathname && target.hash) return false;
|
|
1387
|
+
|
|
1388
|
+
fetch(target.pathname, {
|
|
1389
|
+
headers: { 'X-Clawfire-Partial': 'true' }
|
|
1390
|
+
})
|
|
1391
|
+
.then(function(res) {
|
|
1392
|
+
if (!res.ok) throw new Error('Page not found');
|
|
1393
|
+
return res.json();
|
|
1394
|
+
})
|
|
1395
|
+
.then(function(data) {
|
|
1396
|
+
// Update page content
|
|
1397
|
+
var container = document.getElementById('clawfire-page');
|
|
1398
|
+
if (container) {
|
|
1399
|
+
container.innerHTML = data.html;
|
|
1400
|
+
// Execute scripts in new content
|
|
1401
|
+
var scripts = container.querySelectorAll('script');
|
|
1402
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
1403
|
+
var newScript = document.createElement('script');
|
|
1404
|
+
if (scripts[i].src) {
|
|
1405
|
+
newScript.src = scripts[i].src;
|
|
1406
|
+
} else {
|
|
1407
|
+
newScript.textContent = scripts[i].textContent;
|
|
1408
|
+
}
|
|
1409
|
+
scripts[i].parentNode.replaceChild(newScript, scripts[i]);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
// Update title
|
|
1413
|
+
if (data.meta && data.meta.title) {
|
|
1414
|
+
document.title = data.meta.title;
|
|
1415
|
+
}
|
|
1416
|
+
// Update URL
|
|
1417
|
+
history.pushState(null, '', target.pathname);
|
|
1418
|
+
// Update nav active state
|
|
1419
|
+
updateActiveNav();
|
|
1420
|
+
// Dispatch event for page scripts
|
|
1421
|
+
document.dispatchEvent(new CustomEvent('clawfire:navigate', { detail: { path: data.path } }));
|
|
1422
|
+
// Scroll to top
|
|
1423
|
+
window.scrollTo(0, 0);
|
|
1424
|
+
})
|
|
1425
|
+
.catch(function(err) {
|
|
1426
|
+
// Fallback: full page navigation
|
|
1427
|
+
location.href = url;
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Intercept link clicks
|
|
1434
|
+
document.addEventListener('click', function(e) {
|
|
1435
|
+
var anchor = e.target.closest ? e.target.closest('a[href]') : null;
|
|
1436
|
+
if (!anchor) return;
|
|
1437
|
+
var href = anchor.getAttribute('href');
|
|
1438
|
+
if (!href) return;
|
|
1439
|
+
// Skip external links, new-tab links, modified clicks
|
|
1440
|
+
if (href.startsWith('http') || href.startsWith('//')) return;
|
|
1441
|
+
if (anchor.target === '_blank') return;
|
|
1442
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
|
1443
|
+
if (anchor.hasAttribute('download')) return;
|
|
1444
|
+
|
|
1445
|
+
e.preventDefault();
|
|
1446
|
+
navigate(href);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// Handle back/forward
|
|
1450
|
+
window.addEventListener('popstate', function() {
|
|
1451
|
+
navigate(location.pathname);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// Set initial active nav state
|
|
1455
|
+
updateActiveNav();
|
|
1456
|
+
})();
|
|
1457
|
+
</script>`;
|
|
1458
|
+
}
|
|
1011
1459
|
var DevServer = class {
|
|
1012
|
-
|
|
1460
|
+
frontendServer = null;
|
|
1461
|
+
apiServer = null;
|
|
1013
1462
|
router;
|
|
1014
1463
|
watcher = null;
|
|
1015
|
-
|
|
1464
|
+
frontendSseClients = [];
|
|
1465
|
+
apiSseClients = [];
|
|
1016
1466
|
sseIdCounter = 0;
|
|
1017
1467
|
options;
|
|
1018
1468
|
routesDir;
|
|
1019
1469
|
schemasDir;
|
|
1470
|
+
publicDir;
|
|
1471
|
+
pagesDir;
|
|
1472
|
+
componentsDir;
|
|
1020
1473
|
playgroundHtml = "";
|
|
1021
1474
|
importCounter = 0;
|
|
1022
|
-
// ESM 캐시 버스팅용
|
|
1023
1475
|
isReloading = false;
|
|
1476
|
+
pageCompiler;
|
|
1024
1477
|
constructor(options = {}) {
|
|
1025
1478
|
this.options = {
|
|
1026
1479
|
projectDir: options.projectDir || process.cwd(),
|
|
1027
|
-
port: options.port ||
|
|
1480
|
+
port: options.port || 3e3,
|
|
1481
|
+
apiPort: options.apiPort || 3456,
|
|
1028
1482
|
routerOptions: options.routerOptions || {},
|
|
1029
1483
|
hotReload: options.hotReload !== false,
|
|
1030
1484
|
debounceMs: options.debounceMs || 150,
|
|
1031
1485
|
onSetupRoutes: options.onSetupRoutes || (() => {
|
|
1032
1486
|
})
|
|
1033
1487
|
};
|
|
1034
|
-
this.routesDir = (0,
|
|
1035
|
-
this.schemasDir = (0,
|
|
1488
|
+
this.routesDir = (0, import_node_path2.resolve)(this.options.projectDir, "app/routes");
|
|
1489
|
+
this.schemasDir = (0, import_node_path2.resolve)(this.options.projectDir, "app/schemas");
|
|
1490
|
+
this.publicDir = (0, import_node_path2.resolve)(this.options.projectDir, "public");
|
|
1491
|
+
this.pagesDir = (0, import_node_path2.resolve)(this.options.projectDir, "app/pages");
|
|
1492
|
+
this.componentsDir = (0, import_node_path2.resolve)(this.options.projectDir, "app/components");
|
|
1493
|
+
this.pageCompiler = new PageCompiler(this.options.projectDir);
|
|
1036
1494
|
this.router = createRouter({
|
|
1037
1495
|
cors: ["*"],
|
|
1038
|
-
// dev에서는 모든 origin 허용
|
|
1039
1496
|
rateLimit: 0,
|
|
1040
|
-
// dev에서는 rate limit 비활성화
|
|
1041
1497
|
...this.options.routerOptions
|
|
1042
1498
|
});
|
|
1043
1499
|
}
|
|
1044
|
-
|
|
1045
|
-
* 개발 서버 시작
|
|
1046
|
-
*/
|
|
1500
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
1047
1501
|
async start() {
|
|
1048
1502
|
await this.loadRoutes();
|
|
1049
1503
|
this.regeneratePlayground();
|
|
1050
|
-
this.
|
|
1504
|
+
this.apiServer = import_node_http.default.createServer((req, res) => this.handleApiRequest(req, res));
|
|
1505
|
+
this.frontendServer = import_node_http.default.createServer((req, res) => this.handleFrontendRequest(req, res));
|
|
1051
1506
|
if (this.options.hotReload) {
|
|
1052
1507
|
this.startWatcher();
|
|
1053
1508
|
}
|
|
1054
|
-
await new Promise((
|
|
1055
|
-
this.
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1509
|
+
await new Promise((resolve4, reject) => {
|
|
1510
|
+
this.apiServer.listen(this.options.apiPort, () => resolve4());
|
|
1511
|
+
this.apiServer.on("error", reject);
|
|
1512
|
+
});
|
|
1513
|
+
await new Promise((resolve4, reject) => {
|
|
1514
|
+
this.frontendServer.listen(this.options.port, () => resolve4());
|
|
1515
|
+
this.frontendServer.on("error", reject);
|
|
1059
1516
|
});
|
|
1060
1517
|
this.printStartupBanner();
|
|
1061
1518
|
}
|
|
1062
|
-
/**
|
|
1063
|
-
* 서버 종료
|
|
1064
|
-
*/
|
|
1065
1519
|
async stop() {
|
|
1066
1520
|
this.watcher?.close();
|
|
1067
|
-
for (const client of this.
|
|
1521
|
+
for (const client of [...this.frontendSseClients, ...this.apiSseClients]) {
|
|
1068
1522
|
client.res.end();
|
|
1069
1523
|
}
|
|
1070
|
-
this.
|
|
1524
|
+
this.frontendSseClients = [];
|
|
1525
|
+
this.apiSseClients = [];
|
|
1071
1526
|
this.router.destroy();
|
|
1072
|
-
if (this.
|
|
1073
|
-
await new Promise((
|
|
1074
|
-
this.
|
|
1527
|
+
if (this.apiServer) {
|
|
1528
|
+
await new Promise((resolve4) => {
|
|
1529
|
+
this.apiServer.close(() => resolve4());
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
if (this.frontendServer) {
|
|
1533
|
+
await new Promise((resolve4) => {
|
|
1534
|
+
this.frontendServer.close(() => resolve4());
|
|
1075
1535
|
});
|
|
1076
1536
|
}
|
|
1077
1537
|
}
|
|
1078
1538
|
// ─── Route Loading ─────────────────────────────────────────────────
|
|
1079
|
-
/**
|
|
1080
|
-
* 라우트 파일 로딩 (또는 수동 콜백)
|
|
1081
|
-
*/
|
|
1082
1539
|
async loadRoutes() {
|
|
1083
1540
|
this.router.destroy();
|
|
1084
1541
|
this.router = createRouter({
|
|
@@ -1090,11 +1547,11 @@ var DevServer = class {
|
|
|
1090
1547
|
await this.options.onSetupRoutes(this.router);
|
|
1091
1548
|
if (this.router.getRoutes().length > 0) return;
|
|
1092
1549
|
}
|
|
1093
|
-
if (!(0,
|
|
1550
|
+
if (!(0, import_node_fs2.existsSync)(this.routesDir)) return;
|
|
1094
1551
|
const discovered = discoverRoutes(this.routesDir);
|
|
1095
1552
|
for (const route of discovered) {
|
|
1096
1553
|
try {
|
|
1097
|
-
const fullPath = (0,
|
|
1554
|
+
const fullPath = (0, import_node_path2.resolve)(this.routesDir, route.filePath);
|
|
1098
1555
|
const fileUrl = (0, import_node_url.pathToFileURL)(fullPath).href;
|
|
1099
1556
|
const mod = await import(`${fileUrl}?v=${++this.importCounter}`);
|
|
1100
1557
|
const contract = mod.default;
|
|
@@ -1106,13 +1563,10 @@ var DevServer = class {
|
|
|
1106
1563
|
}
|
|
1107
1564
|
}
|
|
1108
1565
|
}
|
|
1109
|
-
/**
|
|
1110
|
-
* 라우트 핫 리로드
|
|
1111
|
-
*/
|
|
1112
1566
|
async reloadRoutes(event) {
|
|
1113
1567
|
if (this.isReloading) return;
|
|
1114
1568
|
this.isReloading = true;
|
|
1115
|
-
const relPath = (0,
|
|
1569
|
+
const relPath = (0, import_node_path2.relative)(this.options.projectDir, event.filePath);
|
|
1116
1570
|
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1117
1571
|
console.log(`
|
|
1118
1572
|
\x1B[33m[${timestamp}]\x1B[0m \x1B[36m${relPath}\x1B[0m changed`);
|
|
@@ -1122,7 +1576,13 @@ var DevServer = class {
|
|
|
1122
1576
|
this.regeneratePlayground();
|
|
1123
1577
|
const routeCount = this.router.getRoutes().length;
|
|
1124
1578
|
console.log(` \x1B[32m\u2713\x1B[0m ${routeCount} routes loaded`);
|
|
1125
|
-
this.broadcastSSE({
|
|
1579
|
+
this.broadcastSSE(this.apiSseClients, {
|
|
1580
|
+
type: event.type,
|
|
1581
|
+
file: relPath,
|
|
1582
|
+
timestamp: event.timestamp,
|
|
1583
|
+
routes: routeCount
|
|
1584
|
+
});
|
|
1585
|
+
this.broadcastSSE(this.frontendSseClients, {
|
|
1126
1586
|
type: event.type,
|
|
1127
1587
|
file: relPath,
|
|
1128
1588
|
timestamp: event.timestamp,
|
|
@@ -1130,34 +1590,71 @@ var DevServer = class {
|
|
|
1130
1590
|
});
|
|
1131
1591
|
} catch (err) {
|
|
1132
1592
|
console.log(` \x1B[31m\u2717\x1B[0m Reload failed:`, err);
|
|
1133
|
-
|
|
1593
|
+
const errorData = {
|
|
1134
1594
|
type: "error",
|
|
1135
1595
|
file: relPath,
|
|
1136
1596
|
message: err instanceof Error ? err.message : "Unknown error"
|
|
1137
|
-
}
|
|
1597
|
+
};
|
|
1598
|
+
this.broadcastSSE(this.apiSseClients, errorData);
|
|
1599
|
+
this.broadcastSSE(this.frontendSseClients, errorData);
|
|
1138
1600
|
} finally {
|
|
1139
1601
|
this.isReloading = false;
|
|
1140
1602
|
}
|
|
1141
1603
|
}
|
|
1604
|
+
// ─── Frontend Change Handler ───────────────────────────────────────
|
|
1605
|
+
handleFrontendChange(event) {
|
|
1606
|
+
const relPath = (0, import_node_path2.relative)(this.options.projectDir, event.filePath);
|
|
1607
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1608
|
+
if (event.type === "css-change") {
|
|
1609
|
+
console.log(`
|
|
1610
|
+
\x1B[33m[${timestamp}]\x1B[0m \x1B[35m${relPath}\x1B[0m CSS updated`);
|
|
1611
|
+
this.broadcastSSE(this.frontendSseClients, {
|
|
1612
|
+
type: "css-change",
|
|
1613
|
+
file: relPath,
|
|
1614
|
+
timestamp: event.timestamp
|
|
1615
|
+
});
|
|
1616
|
+
} else {
|
|
1617
|
+
console.log(`
|
|
1618
|
+
\x1B[33m[${timestamp}]\x1B[0m \x1B[35m${relPath}\x1B[0m changed \u2192 reload`);
|
|
1619
|
+
this.broadcastSSE(this.frontendSseClients, {
|
|
1620
|
+
type: event.type,
|
|
1621
|
+
file: relPath,
|
|
1622
|
+
timestamp: event.timestamp
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1142
1626
|
// ─── File Watcher ──────────────────────────────────────────────────
|
|
1143
1627
|
startWatcher() {
|
|
1144
1628
|
this.watcher = new FileWatcher(this.options.debounceMs);
|
|
1145
|
-
if ((0,
|
|
1629
|
+
if ((0, import_node_fs2.existsSync)(this.routesDir)) {
|
|
1146
1630
|
this.watcher.watchDir(this.routesDir, "route-change");
|
|
1147
1631
|
}
|
|
1148
|
-
if ((0,
|
|
1632
|
+
if ((0, import_node_fs2.existsSync)(this.schemasDir)) {
|
|
1149
1633
|
this.watcher.watchDir(this.schemasDir, "schema-change");
|
|
1150
1634
|
}
|
|
1151
|
-
const configFile = (0,
|
|
1152
|
-
if ((0,
|
|
1635
|
+
const configFile = (0, import_node_path2.resolve)(this.options.projectDir, "clawfire.config.ts");
|
|
1636
|
+
if ((0, import_node_fs2.existsSync)(configFile)) {
|
|
1153
1637
|
this.watcher.watchFile(configFile, "config-change");
|
|
1154
1638
|
}
|
|
1155
|
-
|
|
1156
|
-
this.
|
|
1157
|
-
}
|
|
1639
|
+
if ((0, import_node_fs2.existsSync)(this.publicDir)) {
|
|
1640
|
+
this.watcher.watchDirFrontend(this.publicDir);
|
|
1641
|
+
}
|
|
1642
|
+
if ((0, import_node_fs2.existsSync)(this.pagesDir)) {
|
|
1643
|
+
this.watcher.watchDir(this.pagesDir, "page-change");
|
|
1644
|
+
}
|
|
1645
|
+
if ((0, import_node_fs2.existsSync)(this.componentsDir)) {
|
|
1646
|
+
this.watcher.watchDir(this.componentsDir, "component-change");
|
|
1647
|
+
}
|
|
1648
|
+
this.watcher.on("route-change", (event) => this.reloadRoutes(event));
|
|
1649
|
+
this.watcher.on("schema-change", (event) => this.reloadRoutes(event));
|
|
1650
|
+
this.watcher.on("config-change", (event) => this.reloadRoutes(event));
|
|
1651
|
+
this.watcher.on("frontend-change", (event) => this.handleFrontendChange(event));
|
|
1652
|
+
this.watcher.on("css-change", (event) => this.handleFrontendChange(event));
|
|
1653
|
+
this.watcher.on("page-change", (event) => this.handleFrontendChange(event));
|
|
1654
|
+
this.watcher.on("component-change", (event) => this.handleFrontendChange(event));
|
|
1158
1655
|
}
|
|
1159
1656
|
// ─── SSE (Server-Sent Events) ──────────────────────────────────────
|
|
1160
|
-
handleSSE(req, res) {
|
|
1657
|
+
handleSSE(req, res, clients) {
|
|
1161
1658
|
res.writeHead(200, {
|
|
1162
1659
|
"Content-Type": "text/event-stream",
|
|
1163
1660
|
"Cache-Control": "no-cache",
|
|
@@ -1166,19 +1663,20 @@ var DevServer = class {
|
|
|
1166
1663
|
});
|
|
1167
1664
|
const clientId = ++this.sseIdCounter;
|
|
1168
1665
|
const client = { id: clientId, res };
|
|
1169
|
-
|
|
1666
|
+
clients.push(client);
|
|
1170
1667
|
res.write(`data: ${JSON.stringify({ type: "connected", id: clientId })}
|
|
1171
1668
|
|
|
1172
1669
|
`);
|
|
1173
1670
|
req.on("close", () => {
|
|
1174
|
-
|
|
1671
|
+
const idx = clients.indexOf(client);
|
|
1672
|
+
if (idx !== -1) clients.splice(idx, 1);
|
|
1175
1673
|
});
|
|
1176
1674
|
}
|
|
1177
|
-
broadcastSSE(data) {
|
|
1675
|
+
broadcastSSE(clients, data) {
|
|
1178
1676
|
const message = `data: ${JSON.stringify(data)}
|
|
1179
1677
|
|
|
1180
1678
|
`;
|
|
1181
|
-
for (const client of
|
|
1679
|
+
for (const client of clients) {
|
|
1182
1680
|
try {
|
|
1183
1681
|
client.res.write(message);
|
|
1184
1682
|
} catch {
|
|
@@ -1189,95 +1687,206 @@ var DevServer = class {
|
|
|
1189
1687
|
regeneratePlayground() {
|
|
1190
1688
|
const baseHtml = generatePlaygroundHtml({
|
|
1191
1689
|
title: "Clawfire Dev Playground",
|
|
1192
|
-
apiBaseUrl: `http://localhost:${this.options.
|
|
1690
|
+
apiBaseUrl: `http://localhost:${this.options.apiPort}`
|
|
1193
1691
|
});
|
|
1194
1692
|
const liveReloadScript = `
|
|
1195
1693
|
<script>
|
|
1196
1694
|
(function() {
|
|
1197
|
-
|
|
1695
|
+
var banner = document.createElement('div');
|
|
1198
1696
|
banner.id = 'dev-banner';
|
|
1199
1697
|
banner.style.cssText = 'position:fixed;bottom:0;left:0;right:0;padding:6px 16px;background:#1a1a2e;color:#f97316;font-size:12px;font-family:monospace;z-index:9999;display:flex;align-items:center;gap:8px;border-top:1px solid #2a2a2a;';
|
|
1200
1698
|
banner.innerHTML = '<span style="width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block;" id="dev-dot"></span><span>Clawfire Dev Server</span><span style="color:#666;margin-left:auto;" id="dev-status">Connected</span>';
|
|
1201
1699
|
document.body.appendChild(banner);
|
|
1202
1700
|
|
|
1203
|
-
|
|
1701
|
+
var reconnectTimer;
|
|
1204
1702
|
function connect() {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
es.onopen = () => {
|
|
1703
|
+
var es = new EventSource('http://localhost:${this.options.apiPort}/__dev/events');
|
|
1704
|
+
es.onopen = function() {
|
|
1208
1705
|
document.getElementById('dev-dot').style.background = '#22c55e';
|
|
1209
1706
|
document.getElementById('dev-status').textContent = 'Connected';
|
|
1210
1707
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
1211
1708
|
};
|
|
1212
|
-
|
|
1213
|
-
es.onmessage = (e) => {
|
|
1709
|
+
es.onmessage = function(e) {
|
|
1214
1710
|
try {
|
|
1215
|
-
|
|
1711
|
+
var data = JSON.parse(e.data);
|
|
1216
1712
|
if (data.type === 'connected') return;
|
|
1217
|
-
|
|
1218
1713
|
if (data.type === 'error') {
|
|
1219
1714
|
document.getElementById('dev-dot').style.background = '#ef4444';
|
|
1220
1715
|
document.getElementById('dev-status').textContent = 'Error: ' + (data.message || 'reload failed');
|
|
1221
1716
|
return;
|
|
1222
1717
|
}
|
|
1223
|
-
|
|
1224
|
-
// \uD30C\uC77C \uBCC0\uACBD \u2192 \uD398\uC774\uC9C0 \uC0C8\uB85C\uACE0\uCE68
|
|
1225
1718
|
document.getElementById('dev-dot').style.background = '#eab308';
|
|
1226
1719
|
document.getElementById('dev-status').textContent = 'Reloading...';
|
|
1227
|
-
setTimeout(()
|
|
1228
|
-
} catch {}
|
|
1720
|
+
setTimeout(function() { window.location.reload(); }, 300);
|
|
1721
|
+
} catch(err) {}
|
|
1229
1722
|
};
|
|
1230
|
-
|
|
1231
|
-
es.onerror = () => {
|
|
1723
|
+
es.onerror = function() {
|
|
1232
1724
|
es.close();
|
|
1233
1725
|
document.getElementById('dev-dot').style.background = '#ef4444';
|
|
1234
1726
|
document.getElementById('dev-status').textContent = 'Disconnected \u2014 reconnecting...';
|
|
1235
1727
|
reconnectTimer = setTimeout(connect, 2000);
|
|
1236
1728
|
};
|
|
1237
1729
|
}
|
|
1238
|
-
|
|
1239
1730
|
connect();
|
|
1240
1731
|
})();
|
|
1241
1732
|
</script>`;
|
|
1242
1733
|
this.playgroundHtml = baseHtml.replace("</body>", liveReloadScript + "\n</body>");
|
|
1243
1734
|
}
|
|
1244
|
-
// ───
|
|
1245
|
-
|
|
1735
|
+
// ─── Script Injection ──────────────────────────────────────────────
|
|
1736
|
+
/**
|
|
1737
|
+
* Inject HMR and (optionally) client-side router scripts before </body>.
|
|
1738
|
+
*/
|
|
1739
|
+
injectScripts(html, includeRouter) {
|
|
1740
|
+
const hmr = generateHmrScript(this.options.port);
|
|
1741
|
+
const router = includeRouter ? generateRouterScript() : "";
|
|
1742
|
+
const scripts = router + hmr;
|
|
1743
|
+
if (html.includes("</body>")) {
|
|
1744
|
+
return html.replace("</body>", scripts + "\n</body>");
|
|
1745
|
+
}
|
|
1746
|
+
return html + scripts;
|
|
1747
|
+
}
|
|
1748
|
+
// ─── Static File Serving ──────────────────────────────────────────
|
|
1749
|
+
serveStaticFile(filePath, res) {
|
|
1750
|
+
if (!(0, import_node_fs2.existsSync)(filePath)) return false;
|
|
1751
|
+
try {
|
|
1752
|
+
const content = (0, import_node_fs2.readFileSync)(filePath);
|
|
1753
|
+
const ext = (0, import_node_path2.extname)(filePath).slice(1).toLowerCase();
|
|
1754
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
1755
|
+
if (ext === "html") {
|
|
1756
|
+
const html = content.toString("utf-8");
|
|
1757
|
+
const injected = this.injectScripts(html, false);
|
|
1758
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
1759
|
+
res.end(injected);
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
1763
|
+
res.end(content);
|
|
1764
|
+
return true;
|
|
1765
|
+
} catch {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// ─── API Proxy ────────────────────────────────────────────────────
|
|
1770
|
+
proxyToApiServer(req, res) {
|
|
1771
|
+
const proxyReq = import_node_http.default.request(
|
|
1772
|
+
{
|
|
1773
|
+
hostname: "127.0.0.1",
|
|
1774
|
+
port: this.options.apiPort,
|
|
1775
|
+
path: req.url,
|
|
1776
|
+
method: req.method,
|
|
1777
|
+
headers: {
|
|
1778
|
+
...req.headers,
|
|
1779
|
+
host: `localhost:${this.options.apiPort}`
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
(proxyRes) => {
|
|
1783
|
+
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
|
1784
|
+
proxyRes.pipe(res, { end: true });
|
|
1785
|
+
}
|
|
1786
|
+
);
|
|
1787
|
+
proxyReq.on("error", (err) => {
|
|
1788
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
1789
|
+
res.end(JSON.stringify({ error: { code: "PROXY_ERROR", message: "API server unavailable" } }));
|
|
1790
|
+
});
|
|
1791
|
+
req.pipe(proxyReq, { end: true });
|
|
1792
|
+
}
|
|
1793
|
+
// ─── Frontend Request Handler ─────────────────────────────────────
|
|
1794
|
+
handleFrontendRequest(req, res) {
|
|
1246
1795
|
const url = new URL(req.url || "/", `http://localhost:${this.options.port}`);
|
|
1247
1796
|
if (url.pathname === "/__dev/events") {
|
|
1248
|
-
this.handleSSE(req, res);
|
|
1797
|
+
this.handleSSE(req, res, this.frontendSseClients);
|
|
1249
1798
|
return;
|
|
1250
1799
|
}
|
|
1251
|
-
if (url.pathname
|
|
1252
|
-
|
|
1253
|
-
res.end(this.playgroundHtml);
|
|
1800
|
+
if (url.pathname.startsWith("/api")) {
|
|
1801
|
+
this.proxyToApiServer(req, res);
|
|
1254
1802
|
return;
|
|
1255
1803
|
}
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1804
|
+
if (url.pathname.startsWith("/__")) {
|
|
1805
|
+
this.proxyToApiServer(req, res);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const ext = (0, import_node_path2.extname)(url.pathname);
|
|
1809
|
+
if (ext && STATIC_EXTENSIONS.has(ext)) {
|
|
1810
|
+
const filePath2 = (0, import_node_path2.resolve)(this.publicDir, url.pathname.slice(1));
|
|
1811
|
+
if (filePath2.startsWith(this.publicDir) && this.serveStaticFile(filePath2, res)) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
res.writeHead(404);
|
|
1815
|
+
res.end("Not found");
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (this.pageCompiler.isActive()) {
|
|
1819
|
+
const isPartial = req.headers["x-clawfire-partial"] === "true";
|
|
1820
|
+
const pagePath = this.pageCompiler.resolve(url.pathname);
|
|
1821
|
+
if (pagePath) {
|
|
1260
1822
|
try {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
};
|
|
1274
|
-
res.
|
|
1275
|
-
|
|
1276
|
-
|
|
1823
|
+
if (isPartial) {
|
|
1824
|
+
const partial = this.pageCompiler.compilePartial(pagePath, url.pathname);
|
|
1825
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
1826
|
+
res.end(JSON.stringify(partial));
|
|
1827
|
+
} else {
|
|
1828
|
+
const compiled = this.pageCompiler.compile(pagePath);
|
|
1829
|
+
const html = this.injectScripts(compiled.html, true);
|
|
1830
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1831
|
+
res.end(html);
|
|
1832
|
+
}
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
logger.warn(`Page compilation error: ${url.pathname}`, err);
|
|
1835
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
1836
|
+
res.end(`<h1>500 \u2014 Page Compilation Error</h1><pre>${err instanceof Error ? err.message : "Unknown error"}</pre>`);
|
|
1837
|
+
}
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const page404 = this.pageCompiler.resolve404();
|
|
1841
|
+
if (page404) {
|
|
1842
|
+
try {
|
|
1843
|
+
if (isPartial) {
|
|
1844
|
+
const partial = this.pageCompiler.compilePartial(page404, url.pathname);
|
|
1845
|
+
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
|
1846
|
+
res.end(JSON.stringify(partial));
|
|
1847
|
+
} else {
|
|
1848
|
+
const compiled = this.pageCompiler.compile(page404);
|
|
1849
|
+
const html = this.injectScripts(compiled.html, true);
|
|
1850
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
1851
|
+
res.end(html);
|
|
1852
|
+
}
|
|
1277
1853
|
} catch {
|
|
1854
|
+
res.writeHead(404);
|
|
1855
|
+
res.end("Not found");
|
|
1278
1856
|
}
|
|
1857
|
+
return;
|
|
1279
1858
|
}
|
|
1280
1859
|
}
|
|
1860
|
+
const requestedPath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
1861
|
+
const filePath = (0, import_node_path2.resolve)(this.publicDir, requestedPath);
|
|
1862
|
+
if (!filePath.startsWith(this.publicDir)) {
|
|
1863
|
+
res.writeHead(403);
|
|
1864
|
+
res.end("Forbidden");
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (this.serveStaticFile(filePath, res)) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const indexPath = (0, import_node_path2.resolve)(this.publicDir, "index.html");
|
|
1871
|
+
if ((0, import_node_fs2.existsSync)(indexPath)) {
|
|
1872
|
+
this.serveStaticFile(indexPath, res);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
res.writeHead(404);
|
|
1876
|
+
res.end("Not found");
|
|
1877
|
+
}
|
|
1878
|
+
// ─── API Request Handler ──────────────────────────────────────────
|
|
1879
|
+
handleApiRequest(req, res) {
|
|
1880
|
+
const url = new URL(req.url || "/", `http://localhost:${this.options.apiPort}`);
|
|
1881
|
+
if (url.pathname === "/__dev/events") {
|
|
1882
|
+
this.handleSSE(req, res, this.apiSseClients);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (url.pathname === "/" || url.pathname === "/__playground") {
|
|
1886
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1887
|
+
res.end(this.playgroundHtml);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1281
1890
|
if (url.pathname.startsWith("/api")) {
|
|
1282
1891
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1283
1892
|
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
@@ -1342,12 +1951,14 @@ var DevServer = class {
|
|
|
1342
1951
|
printStartupBanner() {
|
|
1343
1952
|
const routes = this.router.getRoutes();
|
|
1344
1953
|
const watching = this.options.hotReload;
|
|
1954
|
+
const pagesActive = this.pageCompiler.isActive();
|
|
1345
1955
|
console.log("");
|
|
1346
1956
|
console.log(" \x1B[1m\x1B[33m\u26A1 Clawfire Dev Server\x1B[0m");
|
|
1347
1957
|
console.log(" \x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
1348
|
-
console.log(` \x1B[
|
|
1349
|
-
console.log(` \x1B[36mAPI\x1B[0m : http://localhost:${this.options.
|
|
1350
|
-
console.log(` \x1B[
|
|
1958
|
+
console.log(` \x1B[36mApp\x1B[0m : http://localhost:${this.options.port}`);
|
|
1959
|
+
console.log(` \x1B[36mAPI\x1B[0m : http://localhost:${this.options.apiPort}/api/...`);
|
|
1960
|
+
console.log(` \x1B[36mPlayground\x1B[0m : http://localhost:${this.options.apiPort}`);
|
|
1961
|
+
console.log(` \x1B[36mPages\x1B[0m : ${pagesActive ? "\x1B[32mON\x1B[0m (app/pages/)" : "\x1B[2mOFF\x1B[0m"}`);
|
|
1351
1962
|
console.log("");
|
|
1352
1963
|
console.log(` \x1B[32mRoutes (${routes.length})\x1B[0m:`);
|
|
1353
1964
|
for (const route of routes) {
|
|
@@ -1357,8 +1968,10 @@ var DevServer = class {
|
|
|
1357
1968
|
}
|
|
1358
1969
|
console.log("");
|
|
1359
1970
|
if (watching) {
|
|
1971
|
+
const watchDirs = ["app/routes/", "app/schemas/", "public/"];
|
|
1972
|
+
if (pagesActive) watchDirs.push("app/pages/", "app/components/");
|
|
1360
1973
|
console.log(` \x1B[35mHot Reload\x1B[0m : \x1B[32mON\x1B[0m`);
|
|
1361
|
-
console.log(` \x1B[2mWatching:
|
|
1974
|
+
console.log(` \x1B[2mWatching: ${watchDirs.join(", ")}\x1B[0m`);
|
|
1362
1975
|
} else {
|
|
1363
1976
|
console.log(` \x1B[35mHot Reload\x1B[0m : OFF`);
|
|
1364
1977
|
}
|