clawfire 0.6.5 → 0.6.7

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/dist/dev.cjs CHANGED
@@ -1153,6 +1153,62 @@ var PageCompiler = class {
1153
1153
  path: pathname
1154
1154
  };
1155
1155
  }
1156
+ /**
1157
+ * Build all pages into static HTML for production deployment.
1158
+ * Walks app/pages/ recursively, compiles each page, and writes to outputDir.
1159
+ *
1160
+ * - Skips _layout.html (wrapper-only, not standalone)
1161
+ * - _404.html → 404.html (Firebase auto-serves for 404s)
1162
+ * - Injects optional script (e.g. production router) before </body>
1163
+ */
1164
+ buildForProduction(outputDir, scriptToInject) {
1165
+ const pages = [];
1166
+ const errors = [];
1167
+ if (!this.isActive()) {
1168
+ return { pages, errors };
1169
+ }
1170
+ const walk = (dir) => {
1171
+ const entries = (0, import_node_fs.readdirSync)(dir);
1172
+ for (const entry of entries) {
1173
+ const fullPath = (0, import_node_path.join)(dir, entry);
1174
+ const stat = (0, import_node_fs.statSync)(fullPath);
1175
+ if (stat.isDirectory()) {
1176
+ walk(fullPath);
1177
+ continue;
1178
+ }
1179
+ if (!entry.endsWith(".html")) continue;
1180
+ if (entry === "_layout.html") continue;
1181
+ try {
1182
+ const compiled = this.compile(fullPath);
1183
+ let html = compiled.html;
1184
+ if (scriptToInject) {
1185
+ if (html.includes("</body>")) {
1186
+ html = html.replace("</body>", scriptToInject + "\n</body>");
1187
+ } else {
1188
+ html += scriptToInject;
1189
+ }
1190
+ }
1191
+ const relPath = (0, import_node_path.relative)(this.pagesDir, fullPath);
1192
+ let outputPath;
1193
+ if (entry === "_404.html") {
1194
+ const parentRel = (0, import_node_path.relative)(this.pagesDir, dir);
1195
+ outputPath = parentRel ? (0, import_node_path.join)(outputDir, parentRel, "404.html") : (0, import_node_path.join)(outputDir, "404.html");
1196
+ } else {
1197
+ outputPath = (0, import_node_path.join)(outputDir, relPath);
1198
+ }
1199
+ const outputDirPath = (0, import_node_path.dirname)(outputPath);
1200
+ (0, import_node_fs.mkdirSync)(outputDirPath, { recursive: true });
1201
+ (0, import_node_fs.writeFileSync)(outputPath, html, "utf-8");
1202
+ pages.push(relPath);
1203
+ } catch (err) {
1204
+ const relPath = (0, import_node_path.relative)(this.pagesDir, fullPath);
1205
+ errors.push(`${relPath}: ${err instanceof Error ? err.message : "Unknown error"}`);
1206
+ }
1207
+ }
1208
+ };
1209
+ walk(this.pagesDir);
1210
+ return { pages, errors };
1211
+ }
1156
1212
  // ─── Internal Methods ────────────────────────────────────────────
1157
1213
  /**
1158
1214
  * Extract <!-- @key: value --> metadata from HTML.
@@ -2268,8 +2324,9 @@ function generateDashboardHtml(options) {
2268
2324
  }
2269
2325
 
2270
2326
  btn.disabled = true;
2271
- btn.textContent = 'Setting...';
2272
- status.style.display = 'none';
2327
+ btn.textContent = 'Setting up...';
2328
+ status.textContent = 'Selecting project, detecting web app, auto-filling config...';
2329
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a0a1c;border:1px solid #3b82f6;color:#93c5fd;';
2273
2330
 
2274
2331
  fetch(API + '/__dev/setup/select-project', {
2275
2332
  method: 'POST',
@@ -2279,8 +2336,11 @@ function generateDashboardHtml(options) {
2279
2336
  .then(function(r) { return r.json(); })
2280
2337
  .then(function(data) {
2281
2338
  if (data.success) {
2282
- status.textContent = data.message;
2283
- status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2339
+ var msg = data.steps ? data.steps.join('\\n') : data.message;
2340
+ status.textContent = msg;
2341
+ status.style.whiteSpace = 'pre-line';
2342
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;white-space:pre-line;';
2343
+ btn.textContent = 'Done';
2284
2344
  setTimeout(refreshSetupStatus, 1000);
2285
2345
  } else {
2286
2346
  status.textContent = data.message;
@@ -3372,7 +3432,11 @@ var FirebaseSetup = class {
3372
3432
  config.hosting = {
3373
3433
  public: "public",
3374
3434
  ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
3375
- rewrites: [{ source: "**", destination: "/index.html" }]
3435
+ cleanUrls: true,
3436
+ rewrites: [
3437
+ { source: "/api/**", function: "api" },
3438
+ { source: "**", destination: "/index.html" }
3439
+ ]
3376
3440
  };
3377
3441
  }
3378
3442
  break;
@@ -3531,41 +3595,48 @@ https://console.developers.google.com/apis/api/firestore.googleapis.com/overview
3531
3595
  * Handles "ALREADY_EXISTS" gracefully — returns success.
3532
3596
  */
3533
3597
  async createFirestoreDatabase(location = "nam5") {
3534
- try {
3535
- await this.execTimeout(
3536
- "firebase",
3537
- ["firestore:databases:create", "(default)", "--location", location, "--json"],
3538
- 6e4
3539
- );
3540
- return { success: true, message: `Firestore database created (location: ${location}).` };
3541
- } catch (err) {
3542
- const msg = err instanceof Error ? err.message : "Unknown error";
3543
- if (msg.includes("ALREADY_EXISTS") || msg.includes("already exists")) {
3544
- return { success: true, message: "Firestore database already exists." };
3545
- }
3546
- if (msg.includes("403") || msg.includes("has not been used") || msg.includes("is disabled")) {
3547
- const enableResult = await this.enableFirestoreApi();
3548
- if (!enableResult.success) {
3549
- return enableResult;
3598
+ const dbArgs = ["firestore:databases:create", "(default)", "--location", location, "--json"];
3599
+ const tryCreate = async () => {
3600
+ try {
3601
+ await this.execTimeout("firebase", dbArgs, 6e4);
3602
+ return { ok: true, alreadyExists: false, needsApi: false, msg: "" };
3603
+ } catch (err) {
3604
+ const msg = err instanceof Error ? err.message : "Unknown error";
3605
+ if (msg.includes("ALREADY_EXISTS") || msg.includes("already exists")) {
3606
+ return { ok: true, alreadyExists: true, needsApi: false, msg };
3550
3607
  }
3551
- await new Promise((r) => setTimeout(r, 5e3));
3552
- try {
3553
- await this.execTimeout(
3554
- "firebase",
3555
- ["firestore:databases:create", "(default)", "--location", location, "--json"],
3556
- 6e4
3557
- );
3558
- return { success: true, message: `Firestore API enabled and database created (location: ${location}).` };
3559
- } catch (retryErr) {
3560
- const retryMsg = retryErr instanceof Error ? retryErr.message : "Unknown error";
3561
- if (retryMsg.includes("ALREADY_EXISTS") || retryMsg.includes("already exists")) {
3562
- return { success: true, message: "Firestore database already exists." };
3563
- }
3564
- return { success: false, message: `Failed to create Firestore database after enabling API: ${retryMsg}` };
3608
+ if (msg.includes("403") || msg.includes("has not been used") || msg.includes("is disabled")) {
3609
+ return { ok: false, alreadyExists: false, needsApi: true, msg };
3565
3610
  }
3611
+ return { ok: false, alreadyExists: false, needsApi: false, msg };
3612
+ }
3613
+ };
3614
+ const first = await tryCreate();
3615
+ if (first.ok) {
3616
+ return { success: true, message: first.alreadyExists ? "Firestore database already exists." : `Firestore database created (location: ${location}).` };
3617
+ }
3618
+ if (!first.needsApi) {
3619
+ return { success: false, message: `Failed to create Firestore database: ${first.msg}` };
3620
+ }
3621
+ const enableResult = await this.enableFirestoreApi();
3622
+ if (!enableResult.success) {
3623
+ return enableResult;
3624
+ }
3625
+ const waits = [5e3, 5e3, 1e4, 1e4, 15e3];
3626
+ for (let i = 0; i < waits.length; i++) {
3627
+ await new Promise((r) => setTimeout(r, waits[i]));
3628
+ const retry = await tryCreate();
3629
+ if (retry.ok) {
3630
+ return { success: true, message: `Firestore API enabled and database created (location: ${location}).` };
3631
+ }
3632
+ if (!retry.needsApi) {
3633
+ return { success: false, message: `Failed to create Firestore database: ${retry.msg}` };
3566
3634
  }
3567
- return { success: false, message: `Failed to create Firestore database: ${msg}` };
3568
3635
  }
3636
+ return {
3637
+ success: false,
3638
+ message: "Firestore API was enabled but database creation timed out waiting for propagation. Please wait a minute and try again."
3639
+ };
3569
3640
  }
3570
3641
  /**
3571
3642
  * Deploy open Firestore security rules for dev testing.
@@ -3841,6 +3912,92 @@ function generateRouterScript() {
3841
3912
  })();
3842
3913
  </script>`;
3843
3914
  }
3915
+ function generateProductionRouterScript() {
3916
+ return `
3917
+ <script data-clawfire-router>
3918
+ (function() {
3919
+ function updateActiveNav() {
3920
+ var path = location.pathname;
3921
+ var links = document.querySelectorAll('#nav-links a[href]');
3922
+ for (var i = 0; i < links.length; i++) {
3923
+ var href = links[i].getAttribute('href');
3924
+ if (!href || href.startsWith('http')) continue;
3925
+ var isActive = (path === '/' && href === '/') || (href !== '/' && path.startsWith(href));
3926
+ links[i].setAttribute('data-active', isActive ? 'true' : 'false');
3927
+ }
3928
+ }
3929
+
3930
+ function navigate(url) {
3931
+ var target = new URL(url, location.href);
3932
+ if (target.origin !== location.origin) return false;
3933
+ if (target.pathname === location.pathname && target.hash) return false;
3934
+
3935
+ fetch(target.pathname)
3936
+ .then(function(res) {
3937
+ if (!res.ok) throw new Error('Page not found');
3938
+ return res.text();
3939
+ })
3940
+ .then(function(html) {
3941
+ // Parse the fetched full HTML and extract #clawfire-page content
3942
+ var parser = new DOMParser();
3943
+ var doc = parser.parseFromString(html, 'text/html');
3944
+ var newPage = doc.getElementById('clawfire-page');
3945
+ if (!newPage) throw new Error('No #clawfire-page found');
3946
+
3947
+ var container = document.getElementById('clawfire-page');
3948
+ if (container) {
3949
+ container.innerHTML = newPage.innerHTML;
3950
+ // Execute scripts in new content
3951
+ var scripts = container.querySelectorAll('script');
3952
+ for (var i = 0; i < scripts.length; i++) {
3953
+ var newScript = document.createElement('script');
3954
+ if (scripts[i].src) {
3955
+ newScript.src = scripts[i].src;
3956
+ } else {
3957
+ newScript.textContent = scripts[i].textContent;
3958
+ }
3959
+ scripts[i].parentNode.replaceChild(newScript, scripts[i]);
3960
+ }
3961
+ }
3962
+ // Update title from fetched document
3963
+ var newTitle = doc.querySelector('title');
3964
+ if (newTitle) document.title = newTitle.textContent || '';
3965
+ // Update URL
3966
+ history.pushState(null, '', target.pathname);
3967
+ updateActiveNav();
3968
+ document.dispatchEvent(new CustomEvent('clawfire:navigate', { detail: { path: target.pathname } }));
3969
+ window.scrollTo(0, 0);
3970
+ })
3971
+ .catch(function() {
3972
+ // Fallback: full page navigation
3973
+ location.href = url;
3974
+ });
3975
+
3976
+ return true;
3977
+ }
3978
+
3979
+ document.addEventListener('click', function(e) {
3980
+ var anchor = e.target.closest ? e.target.closest('a[href]') : null;
3981
+ if (!anchor) return;
3982
+ var href = anchor.getAttribute('href');
3983
+ if (!href) return;
3984
+ if (href.startsWith('http') || href.startsWith('//')) return;
3985
+ if (anchor.target === '_blank') return;
3986
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
3987
+ if (anchor.hasAttribute('download')) return;
3988
+
3989
+ e.preventDefault();
3990
+ navigate(href);
3991
+ });
3992
+
3993
+ window.addEventListener('popstate', function() {
3994
+ navigate(location.pathname);
3995
+ });
3996
+
3997
+ updateActiveNav();
3998
+ })();
3999
+ </script>`;
4000
+ }
3844
4001
  var DevServer = class {
3845
4002
  frontendServer = null;
3846
4003
  apiServer = null;
@@ -4585,10 +4742,57 @@ ${liveReloadScript}
4585
4742
  sendJson({ success: false, message: "projectId is required" }, 400);
4586
4743
  return;
4587
4744
  }
4588
- this.firebaseSetup.selectProject(data.projectId).then((result) => {
4745
+ (async () => {
4746
+ const selectResult = await this.firebaseSetup.selectProject(data.projectId);
4747
+ if (!selectResult.success) {
4748
+ sendJson(selectResult);
4749
+ return;
4750
+ }
4751
+ const steps = [selectResult.message];
4752
+ try {
4753
+ const { apps } = await this.firebaseSetup.listWebApps();
4754
+ let webAppId = "";
4755
+ if (apps.length > 0) {
4756
+ webAppId = apps[0].appId;
4757
+ this.firebaseSetup.selectWebApp(apps[0].appId, apps[0].displayName);
4758
+ steps.push(`Web app "${apps[0].displayName}" selected.`);
4759
+ } else {
4760
+ const createResult = await this.firebaseSetup.createWebApp(data.projectId);
4761
+ if (createResult.success && createResult.appId) {
4762
+ webAppId = createResult.appId;
4763
+ steps.push(`Web app "${data.projectId}" created.`);
4764
+ } else {
4765
+ steps.push(`Web app creation skipped: ${createResult.message}`);
4766
+ }
4767
+ }
4768
+ if (webAppId) {
4769
+ try {
4770
+ const sdkConfig = await fetchFirebaseSdkConfig(this.options.projectDir, webAppId);
4771
+ const fields = {};
4772
+ for (const [key, value] of Object.entries(sdkConfig)) {
4773
+ if (value && typeof value === "string") {
4774
+ fields[key] = value;
4775
+ }
4776
+ }
4777
+ if (Object.keys(fields).length > 0) {
4778
+ for (const [key, value] of Object.entries(fields)) {
4779
+ try {
4780
+ this.updateProjectConfig(key, value);
4781
+ } catch {
4782
+ }
4783
+ }
4784
+ steps.push("Config auto-filled in clawfire.config.ts.");
4785
+ }
4786
+ } catch (configErr) {
4787
+ steps.push(`Config auto-fill skipped: ${configErr instanceof Error ? configErr.message : "unknown error"}`);
4788
+ }
4789
+ }
4790
+ } catch (autoFillErr) {
4791
+ steps.push(`Auto-setup partial: ${autoFillErr instanceof Error ? autoFillErr.message : "unknown error"}`);
4792
+ }
4589
4793
  clearFirebaseStatusCache();
4590
- sendJson(result);
4591
- }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4794
+ sendJson({ success: true, message: steps.join(" "), steps });
4795
+ })().catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4592
4796
  } catch {
4593
4797
  sendJson({ success: false, message: "Invalid JSON body" }, 400);
4594
4798
  }
@@ -4622,10 +4826,25 @@ ${liveReloadScript}
4622
4826
  return;
4623
4827
  }
4624
4828
  if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
4625
- this.firebaseSetup.deployHosting().then((result) => {
4626
- clearFirebaseStatusCache();
4627
- sendJson(result);
4628
- }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4829
+ (async () => {
4830
+ try {
4831
+ if (this.pageCompiler.isActive()) {
4832
+ const routerScript = generateProductionRouterScript();
4833
+ const buildResult = this.pageCompiler.buildForProduction(this.publicDir, routerScript);
4834
+ console.log(` \x1B[32m\u2713\x1B[0m Built ${buildResult.pages.length} pages for production`);
4835
+ if (buildResult.errors.length > 0) {
4836
+ for (const err of buildResult.errors) {
4837
+ console.log(` \x1B[31m\u2717\x1B[0m ${err}`);
4838
+ }
4839
+ }
4840
+ }
4841
+ const result = await this.firebaseSetup.deployHosting();
4842
+ clearFirebaseStatusCache();
4843
+ sendJson(result);
4844
+ } catch (err) {
4845
+ sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500);
4846
+ }
4847
+ })();
4629
4848
  return;
4630
4849
  }
4631
4850
  if (url.pathname === "/__dev/enable-service" && req.method === "POST") {