@absolutejs/absolute 0.19.0-beta.173 → 0.19.0-beta.174

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/ROADMAP.md CHANGED
@@ -4,29 +4,6 @@ Features missing from AbsoluteJS that Next.js provides, ordered by priority. Eac
4
4
 
5
5
  ---
6
6
 
7
- ## P0 — Static Site Generation (SSG)
8
-
9
- **What Next.js does:**
10
- Pages can be pre-rendered at build time into static HTML. `generateStaticParams()` tells the framework which dynamic routes to pre-render. The output is plain HTML + JS that can be served from a CDN with zero server runtime. Next.js also supports a full `output: 'export'` mode that produces a completely static site.
11
-
12
- **What AbsoluteJS has today:**
13
- Nothing. Every page is rendered at request time via streaming SSR. The `handleHTMLPageRequest` serves pre-written HTML files, but there's no mechanism to run a React/Svelte/Vue component through SSR at build time and save the output.
14
-
15
- **What needs to be built:**
16
- - A `static` option per-route or per-page that tells the build to render the component and write the HTML to disk
17
- - For dynamic routes, a way to declare the set of params to pre-render (equivalent to `generateStaticParams`)
18
- - A static export mode that produces a directory of HTML/CSS/JS with no server dependency
19
- - The build pipeline already has streaming SSR for all frameworks — the core work is calling those renderers during `build()` instead of at request time, and writing the output to files
20
- - Static pages should still hydrate on the client (same as today, just the initial HTML comes from disk instead of runtime SSR)
21
-
22
- **Files likely involved:**
23
- - `src/core/build.ts` — add a static rendering pass after bundling
24
- - Each framework's `pageHandler.ts` — extract the rendering logic so it can be called at build time without an HTTP request
25
- - `src/build/generateManifest.ts` — static pages need manifest entries pointing to `.html` files
26
- - New: a config option or per-page export to opt into static rendering
27
-
28
- ---
29
-
30
7
  ## P0 — Metadata API / SEO
31
8
 
32
9
  **What Next.js does:**
package/dist/cli/index.js CHANGED
@@ -529,12 +529,27 @@ var init_devCert = __esm(() => {
529
529
  // src/core/prerender.ts
530
530
  var exports_prerender = {};
531
531
  __export(exports_prerender, {
532
+ routeToFilename: () => routeToFilename,
533
+ rerenderRoute: () => rerenderRoute,
534
+ readTimestamp: () => readTimestamp,
532
535
  prerenderWithServer: () => prerenderWithServer,
533
- prerender: () => prerender
536
+ prerender: () => prerender,
537
+ PRERENDER_BYPASS_HEADER: () => PRERENDER_BYPASS_HEADER
534
538
  });
535
539
  import { mkdirSync as mkdirSync3 } from "fs";
536
540
  import { join as join5 } from "path";
537
- var routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1).replace(/\//g, "-")}.html`, crawlRoutes = async (baseUrl, log) => {
541
+ var PRERENDER_BYPASS_HEADER = "X-Absolute-Prerender-Bypass", routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1).replace(/\//g, "-")}.html`, writeTimestamp = async (htmlPath) => {
542
+ const metaPath = htmlPath.replace(/\.html$/, ".meta");
543
+ await Bun.write(metaPath, String(Date.now()));
544
+ }, readTimestamp = (htmlPath) => {
545
+ const metaPath = htmlPath.replace(/\.html$/, ".meta");
546
+ try {
547
+ const content = __require("fs").readFileSync(metaPath, "utf-8");
548
+ return Number(content) || 0;
549
+ } catch {
550
+ return 0;
551
+ }
552
+ }, crawlRoutes = async (baseUrl) => {
538
553
  const visited = new Set;
539
554
  const queue = ["/"];
540
555
  const routes = [];
@@ -563,6 +578,22 @@ var routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1
563
578
  } catch {}
564
579
  }
565
580
  return routes;
581
+ }, rerenderRoute = async (route, port, prerenderDir) => {
582
+ try {
583
+ const res = await fetch(`http://localhost:${port}${route}`, {
584
+ headers: { [PRERENDER_BYPASS_HEADER]: "1" }
585
+ });
586
+ if (!res.ok)
587
+ return false;
588
+ const html = await res.text();
589
+ const fileName = routeToFilename(route);
590
+ const filePath = join5(prerenderDir, fileName);
591
+ await Bun.write(filePath, html);
592
+ await writeTimestamp(filePath);
593
+ return true;
594
+ } catch {
595
+ return false;
596
+ }
566
597
  }, prerender = async (port, outDir, staticConfig, log) => {
567
598
  const prerenderDir = join5(outDir, "_prerendered");
568
599
  mkdirSync3(prerenderDir, { recursive: true });
@@ -570,7 +601,7 @@ var routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1
570
601
  let routes;
571
602
  if (staticConfig.routes === "all") {
572
603
  log?.("Crawling routes...");
573
- routes = await crawlRoutes(baseUrl, log);
604
+ routes = await crawlRoutes(baseUrl);
574
605
  } else {
575
606
  routes = staticConfig.routes;
576
607
  }
@@ -589,6 +620,7 @@ var routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1
589
620
  const fileName = routeToFilename(route);
590
621
  const filePath = join5(prerenderDir, fileName);
591
622
  await Bun.write(filePath, html);
623
+ await writeTimestamp(filePath);
592
624
  result.routes.set(route, filePath);
593
625
  log?.(` Pre-rendered ${route} \u2192 ${fileName} (${html.length} bytes)`);
594
626
  } catch {
@@ -637,7 +669,7 @@ import {
637
669
  readFileSync as readFileSync7,
638
670
  unlinkSync
639
671
  } from "fs";
640
- import { basename as basename3, join as join6, relative, resolve as resolve6 } from "path";
672
+ import { basename as basename2, join as join6, relative, resolve as resolve6 } from "path";
641
673
  var cliTag3 = (color, message) => `\x1B[2m${formatTimestamp()}\x1B[0m ${color}[cli]\x1B[0m ${color}${message}\x1B[0m`, collectFiles2 = (dir) => {
642
674
  const results = [];
643
675
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -668,7 +700,7 @@ var cliTag3 = (color, message) => `\x1B[2m${formatTimestamp()}\x1B[0m ${color}[c
668
700
  return;
669
701
  }, generateEntrypoint = (distDir, serverEntry, prerenderMap, version2) => {
670
702
  const allFiles = collectFiles2(distDir);
671
- const serverBundleName = basename3(serverEntry).replace(/\.[^.]+$/, "") + ".js";
703
+ const serverBundleName = basename2(serverEntry).replace(/\.[^.]+$/, "") + ".js";
672
704
  const skip = new Set([
673
705
  serverBundleName,
674
706
  "manifest.json",
@@ -837,7 +869,7 @@ console.log(\`
837
869
  }), FRAMEWORK_EXTERNALS, compile = async (serverEntry, outdir, outfile, configPath2) => {
838
870
  const prerenderPort = Number(env3.COMPILE_PORT) || Number(env3.PORT) || DEFAULT_PORT + 1;
839
871
  killStaleProcesses(prerenderPort);
840
- const entryName = basename3(serverEntry).replace(/\.[^.]+$/, "");
872
+ const entryName = basename2(serverEntry).replace(/\.[^.]+$/, "");
841
873
  const resolvedOutdir = resolve6(outdir ?? "dist");
842
874
  const resolvedOutfile = resolve6(outfile ?? "compiled-server");
843
875
  const absoluteVersion = resolvePackageVersion2([
@@ -920,7 +952,7 @@ console.log(\`
920
952
  const size = (Bun.file(resolvedOutfile).size / 1048576).toFixed(0);
921
953
  const totalDuration = getDurationString(performance.now() - totalStart);
922
954
  console.log(cliTag3("\x1B[32m", `Compiled to ${resolvedOutfile} (${size}MB) in ${totalDuration}`));
923
- console.log(cliTag3("\x1B[2m", `Run with: ./${basename3(resolvedOutfile)}`));
955
+ console.log(cliTag3("\x1B[2m", `Run with: ./${basename2(resolvedOutfile)}`));
924
956
  sendTelemetryEvent("compile:complete", {
925
957
  durationMs: Math.round(performance.now() - totalStart),
926
958
  entry: serverEntry,
@@ -1803,7 +1835,7 @@ init_telemetryEvent();
1803
1835
  init_utils();
1804
1836
  var {env: env2 } = globalThis.Bun;
1805
1837
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1806
- import { basename as basename2, resolve as resolve5 } from "path";
1838
+ import { basename, resolve as resolve5 } from "path";
1807
1839
  var cliTag2 = (color, message) => `\x1B[2m${formatTimestamp()}\x1B[0m ${color}[cli]\x1B[0m ${color}${message}\x1B[0m`;
1808
1840
  var resolvePackageVersion = (candidates) => {
1809
1841
  for (const candidate of candidates) {
@@ -1857,7 +1889,7 @@ var handleBundleFailure = (serverBundle, bundleStart, serverEntry) => {
1857
1889
  var start = async (serverEntry, outdir, configPath2) => {
1858
1890
  const port = Number(env2.PORT) || DEFAULT_PORT;
1859
1891
  killStaleProcesses(port);
1860
- const entryName = basename2(serverEntry).replace(/\.[^.]+$/, "");
1892
+ const entryName = basename(serverEntry).replace(/\.[^.]+$/, "");
1861
1893
  const resolvedOutdir = resolve5(outdir ?? "dist");
1862
1894
  const absoluteVersion = resolvePackageVersion([
1863
1895
  resolve5(import.meta.dir, "..", "..", "..", "package.json"),
package/dist/index.js CHANGED
@@ -205630,6 +205630,137 @@ var init_hmr = __esm(() => {
205630
205630
  init_webSocket();
205631
205631
  });
205632
205632
 
205633
+ // src/core/prerender.ts
205634
+ var exports_prerender = {};
205635
+ __export(exports_prerender, {
205636
+ routeToFilename: () => routeToFilename,
205637
+ rerenderRoute: () => rerenderRoute,
205638
+ readTimestamp: () => readTimestamp,
205639
+ prerenderWithServer: () => prerenderWithServer,
205640
+ prerender: () => prerender,
205641
+ PRERENDER_BYPASS_HEADER: () => PRERENDER_BYPASS_HEADER
205642
+ });
205643
+ import { mkdirSync as mkdirSync10 } from "fs";
205644
+ import { join as join17 } from "path";
205645
+ var PRERENDER_BYPASS_HEADER = "X-Absolute-Prerender-Bypass", routeToFilename = (route) => route === "/" ? "index.html" : `${route.slice(1).replace(/\//g, "-")}.html`, writeTimestamp = async (htmlPath) => {
205646
+ const metaPath = htmlPath.replace(/\.html$/, ".meta");
205647
+ await Bun.write(metaPath, String(Date.now()));
205648
+ }, readTimestamp = (htmlPath) => {
205649
+ const metaPath = htmlPath.replace(/\.html$/, ".meta");
205650
+ try {
205651
+ const content = __require("fs").readFileSync(metaPath, "utf-8");
205652
+ return Number(content) || 0;
205653
+ } catch {
205654
+ return 0;
205655
+ }
205656
+ }, crawlRoutes = async (baseUrl) => {
205657
+ const visited = new Set;
205658
+ const queue = ["/"];
205659
+ const routes = [];
205660
+ while (queue.length > 0) {
205661
+ const path = queue.shift();
205662
+ if (visited.has(path))
205663
+ continue;
205664
+ visited.add(path);
205665
+ try {
205666
+ const res = await fetch(`${baseUrl}${path}`);
205667
+ if (!res.ok)
205668
+ continue;
205669
+ const contentType = res.headers.get("content-type") ?? "";
205670
+ if (!contentType.includes("text/html"))
205671
+ continue;
205672
+ const html = await res.text();
205673
+ routes.push(path);
205674
+ const linkRegex = /href=["'](\/[^"']*?)["']/g;
205675
+ let match;
205676
+ while ((match = linkRegex.exec(html)) !== null) {
205677
+ const href = match[1] ?? "";
205678
+ if (!href || href.includes(".") || href.includes("#") || visited.has(href))
205679
+ continue;
205680
+ queue.push(href);
205681
+ }
205682
+ } catch {}
205683
+ }
205684
+ return routes;
205685
+ }, rerenderRoute = async (route, port, prerenderDir) => {
205686
+ try {
205687
+ const res = await fetch(`http://localhost:${port}${route}`, {
205688
+ headers: { [PRERENDER_BYPASS_HEADER]: "1" }
205689
+ });
205690
+ if (!res.ok)
205691
+ return false;
205692
+ const html = await res.text();
205693
+ const fileName = routeToFilename(route);
205694
+ const filePath = join17(prerenderDir, fileName);
205695
+ await Bun.write(filePath, html);
205696
+ await writeTimestamp(filePath);
205697
+ return true;
205698
+ } catch {
205699
+ return false;
205700
+ }
205701
+ }, prerender = async (port, outDir, staticConfig, log2) => {
205702
+ const prerenderDir = join17(outDir, "_prerendered");
205703
+ mkdirSync10(prerenderDir, { recursive: true });
205704
+ const baseUrl = `http://localhost:${port}`;
205705
+ let routes;
205706
+ if (staticConfig.routes === "all") {
205707
+ log2?.("Crawling routes...");
205708
+ routes = await crawlRoutes(baseUrl);
205709
+ } else {
205710
+ routes = staticConfig.routes;
205711
+ }
205712
+ const result = {
205713
+ routes: new Map,
205714
+ dir: prerenderDir
205715
+ };
205716
+ for (const route of routes) {
205717
+ try {
205718
+ const res = await fetch(`${baseUrl}${route}`);
205719
+ if (!res.ok) {
205720
+ log2?.(` Skipped ${route} (HTTP ${res.status})`);
205721
+ continue;
205722
+ }
205723
+ const html = await res.text();
205724
+ const fileName = routeToFilename(route);
205725
+ const filePath = join17(prerenderDir, fileName);
205726
+ await Bun.write(filePath, html);
205727
+ await writeTimestamp(filePath);
205728
+ result.routes.set(route, filePath);
205729
+ log2?.(` Pre-rendered ${route} \u2192 ${fileName} (${html.length} bytes)`);
205730
+ } catch {
205731
+ log2?.(` Failed to pre-render ${route}`);
205732
+ }
205733
+ }
205734
+ return result;
205735
+ }, prerenderWithServer = async (serverBundlePath, port, outDir, staticConfig, env3, log2) => {
205736
+ const serverProcess = Bun.spawn(["bun", "run", serverBundlePath], {
205737
+ cwd: process.cwd(),
205738
+ env: { ...process.env, ...env3, PORT: String(port) },
205739
+ stdout: "pipe",
205740
+ stderr: "pipe"
205741
+ });
205742
+ let ready = false;
205743
+ for (let i = 0;i < 50; i++) {
205744
+ try {
205745
+ const res = await fetch(`http://localhost:${port}/`);
205746
+ if (res.ok) {
205747
+ ready = true;
205748
+ break;
205749
+ }
205750
+ } catch {}
205751
+ await Bun.sleep(100);
205752
+ }
205753
+ if (!ready) {
205754
+ serverProcess.kill();
205755
+ throw new Error("Server failed to start for pre-rendering");
205756
+ }
205757
+ const result = await prerender(port, outDir, staticConfig, log2);
205758
+ serverProcess.kill();
205759
+ await serverProcess.exited;
205760
+ return result;
205761
+ };
205762
+ var init_prerender = () => {};
205763
+
205633
205764
  // src/dev/devCert.ts
205634
205765
  var exports_devCert = {};
205635
205766
  __export(exports_devCert, {
@@ -205639,9 +205770,9 @@ __export(exports_devCert, {
205639
205770
  hasCert: () => hasCert,
205640
205771
  ensureDevCert: () => ensureDevCert
205641
205772
  });
205642
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
205773
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
205643
205774
  import { platform as platform3 } from "os";
205644
- import { join as join18 } from "path";
205775
+ import { join as join19 } from "path";
205645
205776
  var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[36m[dev]\x1B[0m ${msg}`), devWarn = (msg) => console.log(`\x1B[2m${new Date().toLocaleTimeString()}\x1B[0m \x1B[33m[dev]\x1B[0m \x1B[33m${msg}\x1B[0m`), certFilesExist = () => existsSync17(CERT_PATH) && existsSync17(KEY_PATH), isCertExpired = () => {
205646
205777
  try {
205647
205778
  const certPem = readFileSync11(CERT_PATH, "utf-8");
@@ -205713,7 +205844,7 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
205713
205844
  generateSelfSigned();
205714
205845
  }
205715
205846
  }, ensureDevCert = () => {
205716
- mkdirSync10(CERT_DIR, { recursive: true });
205847
+ mkdirSync11(CERT_DIR, { recursive: true });
205717
205848
  if (hasCert()) {
205718
205849
  return { cert: CERT_PATH, key: KEY_PATH };
205719
205850
  }
@@ -205840,16 +205971,16 @@ var CERT_DIR, CERT_PATH, KEY_PATH, CERT_VALIDITY_DAYS = 365, devLog = (msg) => c
205840
205971
  }
205841
205972
  rmSync2(CERT_PATH, { force: true });
205842
205973
  rmSync2(KEY_PATH, { force: true });
205843
- mkdirSync10(CERT_DIR, { recursive: true });
205974
+ mkdirSync11(CERT_DIR, { recursive: true });
205844
205975
  generateWithMkcert();
205845
205976
  console.log("");
205846
205977
  devLog("mkcert installed \u2014 HTTPS certificates are now locally trusted");
205847
205978
  return true;
205848
205979
  };
205849
205980
  var init_devCert = __esm(() => {
205850
- CERT_DIR = join18(process.cwd(), ".absolutejs");
205851
- CERT_PATH = join18(CERT_DIR, "cert.pem");
205852
- KEY_PATH = join18(CERT_DIR, "key.pem");
205981
+ CERT_DIR = join19(process.cwd(), ".absolutejs");
205982
+ CERT_PATH = join19(CERT_DIR, "cert.pem");
205983
+ KEY_PATH = join19(CERT_DIR, "key.pem");
205853
205984
  });
205854
205985
  // types/client.ts
205855
205986
  var hmrState = {
@@ -205877,7 +206008,7 @@ var handleHTMLPageRequest = (pagePath) => file(pagePath);
205877
206008
  var handleHTMXPageRequest = (pagePath) => file(pagePath);
205878
206009
  // src/core/prepare.ts
205879
206010
  import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
205880
- import { basename as basename9, join as join17, relative as relative10, resolve as resolve23 } from "path";
206011
+ import { basename as basename9, join as join18, relative as relative10, resolve as resolve23 } from "path";
205881
206012
 
205882
206013
  // src/utils/loadConfig.ts
205883
206014
  import { resolve } from "path";
@@ -206015,7 +206146,7 @@ var loadPrerenderMap = (prerenderDir) => {
206015
206146
  continue;
206016
206147
  const name = basename9(entry, ".html");
206017
206148
  const route = name === "index" ? "/" : `/${name}`;
206018
- map.set(route, join17(prerenderDir, entry));
206149
+ map.set(route, join18(prerenderDir, entry));
206019
206150
  }
206020
206151
  } catch {}
206021
206152
  return map;
@@ -206030,15 +206161,33 @@ var prepare = async (configOrPath) => {
206030
206161
  const manifest = JSON.parse(readFileSync10(`${buildDir}/manifest.json`, "utf-8"));
206031
206162
  const { staticPlugin } = await import("@elysiajs/static");
206032
206163
  const staticFiles = staticPlugin({ assets: buildDir, prefix: "" });
206033
- const prerenderDir = join17(buildDir, "_prerendered");
206164
+ const prerenderDir = join18(buildDir, "_prerendered");
206034
206165
  const prerenderMap = loadPrerenderMap(prerenderDir);
206035
206166
  if (prerenderMap.size > 0) {
206036
206167
  const { Elysia } = await import("elysia");
206168
+ const {
206169
+ PRERENDER_BYPASS_HEADER: PRERENDER_BYPASS_HEADER2,
206170
+ readTimestamp: readTimestamp2,
206171
+ rerenderRoute: rerenderRoute2
206172
+ } = await Promise.resolve().then(() => (init_prerender(), exports_prerender));
206173
+ const revalidateMs = config.static?.revalidate ? config.static.revalidate * 1000 : 0;
206174
+ const port = Number(process.env.PORT) || 3000;
206175
+ const rerendering = new Set;
206037
206176
  const prerenderPlugin = new Elysia({ name: "prerendered-pages" }).onRequest(({ request }) => {
206038
206177
  const url = new URL(request.url);
206178
+ if (request.headers.get(PRERENDER_BYPASS_HEADER2))
206179
+ return;
206039
206180
  const filePath = prerenderMap.get(url.pathname);
206040
206181
  if (!filePath)
206041
206182
  return;
206183
+ if (revalidateMs > 0 && !rerendering.has(url.pathname)) {
206184
+ const renderedAt = readTimestamp2(filePath);
206185
+ const age = Date.now() - renderedAt;
206186
+ if (age > revalidateMs) {
206187
+ rerendering.add(url.pathname);
206188
+ rerenderRoute2(url.pathname, port, prerenderDir).finally(() => rerendering.delete(url.pathname));
206189
+ }
206190
+ }
206042
206191
  return new Response(Bun.file(filePath), {
206043
206192
  headers: { "content-type": "text/html; charset=utf-8" }
206044
206193
  });
@@ -206229,5 +206378,5 @@ export {
206229
206378
  ANGULAR_INIT_TIMEOUT_MS
206230
206379
  };
206231
206380
 
206232
- //# debugId=B51D68800393AB0664756E2164756E21
206381
+ //# debugId=D0959AF290C10FEC64756E2164756E21
206233
206382
  //# sourceMappingURL=index.js.map