@aklinker1/aframe 0.4.12 → 0.5.0

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 CHANGED
@@ -141,5 +141,5 @@ const visible = !isPrerendering();
141
141
  ## Publish Update to NPM
142
142
 
143
143
  ```sh
144
- bun run release
144
+ bun run release patch
145
145
  ```
package/bin/aframe.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { build, createServer } from "../src";
2
2
  import { resolveConfig } from "../src/config";
3
- import { RESET, BOLD, DIM, UNDERLINE, GREEN, CYAN } from "../src/color";
3
+ import { RESET, BOLD, GREEN, CYAN } from "../src/color";
4
4
  import { createTimer } from "../src/timer";
5
5
 
6
6
  const [_bun, _aframe, ...args] = process.argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aklinker1/aframe",
3
- "version": "0.4.12",
3
+ "version": "0.5.0",
4
4
  "packageManager": "bun@1.2.5",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,15 +1,18 @@
1
1
  import type { BunFile } from "bun";
2
- import { resolve } from "node:path";
3
-
4
- const headers = {
5
- "Cache-Control": "max-age=31536000",
6
- };
2
+ import { readFileSync } from "node:fs";
3
+ import { join, extname, basename } from "node:path";
7
4
 
8
5
  export interface AframeServer {
9
6
  listen(port: number): void | never;
10
7
  }
11
8
 
12
- const publicDir = resolve(import.meta.dir, import.meta.publicDir);
9
+ const staticPathsFile = join(import.meta.dir, "static.json");
10
+ const publicDir = join(import.meta.dir, "public");
11
+
12
+ let staticPaths: Record<string, { cacheable: boolean; path: string }> = {};
13
+ try {
14
+ staticPaths = JSON.parse(readFileSync(staticPathsFile, "utf-8"));
15
+ } catch {}
13
16
 
14
17
  /**
15
18
  * Fetches a file from the `public` directory.
@@ -22,48 +25,50 @@ export function fetchStatic(options?: {
22
25
  ) => Promise<Response | undefined> | Response | undefined;
23
26
  }): (request: Request) => Promise<Response> {
24
27
  return async (request) => {
25
- const path = new URL(request.url).pathname.replace(/\/+$/, "");
26
-
27
- const paths = [`${publicDir}${path}`, `${publicDir}${path}/index.html`];
28
-
29
- // Only fallback on the root HTML file when building application
30
- if (import.meta.command === "build") {
31
- paths.push(`${publicDir}/index.html`);
32
- }
28
+ const path = new URL(request.url).pathname.replace(/\/+$/, "") || "/";
33
29
 
34
- for (const path of paths) {
35
- const isHtml = path.includes(".html");
36
- const gzFile = Bun.file(path + ".gz");
37
- const file = Bun.file(path);
30
+ // Fetch file on disk
31
+ if (staticPaths[path]) {
32
+ const filePath = join(import.meta.dir, staticPaths[path].path);
33
+ const file = Bun.file(filePath);
34
+ const gzFile = Bun.file(filePath + ".gz");
38
35
 
39
- if (await isFile(gzFile)) {
40
- const customResponse = await options?.onFetch?.(path, file);
41
- if (customResponse) return customResponse;
42
- return new Response(gzFile.stream(), {
43
- headers: {
44
- ...(isHtml ? {} : headers),
45
- "content-type": file.type,
46
- "content-encoding": "gzip",
47
- },
48
- });
49
- }
36
+ const customResponse = await options?.onFetch?.(path, file);
37
+ if (customResponse) return customResponse;
50
38
 
51
- if (await isFile(file)) {
52
- const customResponse = await options?.onFetch?.(path, file);
53
- if (customResponse) return customResponse;
39
+ return new Response(gzFile.stream(), {
40
+ headers: {
41
+ "Content-Type": file.type,
42
+ "Content-Encoding": "gzip",
43
+ "Cache-Control": "max-age=31536000",
44
+ },
45
+ });
46
+ }
54
47
 
55
- return new Response(file.stream(), { headers });
56
- }
48
+ const ext = extname(basename(path));
49
+ if (ext) {
50
+ return new Response(undefined, { status: 404 });
57
51
  }
58
52
 
53
+ // Fallback to public/index.html file
54
+ if (import.meta.command === "build") {
55
+ const file = Bun.file(join(publicDir, "index.html"));
56
+ const gzFile = Bun.file(join(publicDir, "index.html.gz"));
57
+ return new Response(gzFile.stream(), {
58
+ headers: {
59
+ "Content-Type": file.type,
60
+ "Content-Encoding": "gzip",
61
+ },
62
+ });
63
+ }
59
64
  return new Response(
60
65
  `<html>
61
- <body>
62
- This is a placeholder for your root <code>index.html</code> file during development.
63
- <br/>
64
- In production (or via the app's dev server), this path will fallback on the root <code>index.html</code>.
65
- </body>
66
- </html>`,
66
+ <body>
67
+ This is a placeholder for your root <code>index.html</code> file during development.
68
+ <br/>
69
+ In production (or via the app's dev server), this path will fallback on the root <code>index.html</code>.
70
+ </body>
71
+ </html>`,
67
72
  {
68
73
  headers: {
69
74
  "Content-Type": "text/html",
@@ -72,12 +77,3 @@ export function fetchStatic(options?: {
72
77
  );
73
78
  };
74
79
  }
75
-
76
- async function isFile(file: BunFile): Promise<boolean> {
77
- try {
78
- const stats = await file.stat();
79
- return stats.isFile();
80
- } catch {
81
- return false;
82
- }
83
- }
package/src/config.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as vite from "vite";
2
2
  import { resolve, join, relative } from "node:path/posix";
3
- import { mkdir } from "node:fs/promises";
4
3
  import type { LaunchOptions } from "puppeteer";
5
4
 
6
5
  export type UserConfig = {
@@ -39,7 +38,7 @@ export type ResolvedConfig = {
39
38
  serverDir: string;
40
39
  serverModule: string;
41
40
  serverEntry: string;
42
- prerenderToDir: string;
41
+ prerenderedDir: string;
43
42
  proxyPaths: string[];
44
43
  outDir: string;
45
44
  serverOutDir: string;
@@ -69,10 +68,7 @@ export async function resolveConfig(
69
68
  const outDir = join(rootDir, ".output");
70
69
  const appOutDir = join(outDir, "public");
71
70
  const serverOutDir = outDir;
72
- const prerenderToDir = appOutDir;
73
-
74
- // Ensure required directories exist
75
- await mkdir(prerenderToDir, { recursive: true });
71
+ const prerenderedDir = join(outDir, "prerendered");
76
72
 
77
73
  const configFile = join(rootDir, "aframe.config"); // No file extension to resolve any JS/TS file
78
74
  const relativeConfigFile = "./" + relative(import.meta.dir, configFile);
@@ -120,6 +116,7 @@ export async function resolveConfig(
120
116
  publicDir,
121
117
  envDir: rootDir,
122
118
  build: {
119
+ emptyOutDir: false,
123
120
  outDir: appOutDir,
124
121
  },
125
122
  server: {
@@ -140,7 +137,7 @@ export async function resolveConfig(
140
137
  outDir,
141
138
  serverOutDir,
142
139
  appOutDir,
143
- prerenderToDir,
140
+ prerenderedDir,
144
141
  appPort,
145
142
  serverPort,
146
143
  proxyPaths,
package/src/env.d.ts CHANGED
@@ -2,13 +2,6 @@ import "vite/client";
2
2
 
3
3
  declare global {
4
4
  interface ImportMeta {
5
- /**
6
- * Absolute path or relative path (relative to main server file, not CWD).
7
- * This ensures the public directory path is constant regardless of the CWD.
8
- * It allows dev mode, production builds, and preview mode to all run from
9
- * any working directory.
10
- */
11
- publicDir: string;
12
5
  command: string;
13
6
  }
14
7
  }
package/src/index.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import type { BunPlugin } from "bun";
2
- import { lstatSync } from "node:fs";
3
- import { mkdir, rm } from "node:fs/promises";
2
+ import { createReadStream, createWriteStream, lstatSync } from "node:fs";
3
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
4
4
  import { join, relative } from "node:path/posix";
5
5
  import * as vite from "vite";
6
6
  import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET } from "./color";
7
7
  import type { ResolvedConfig } from "./config";
8
8
  import { createTimer } from "./timer";
9
9
  import { prerenderPages, type PrerenderedRoute } from "./prerenderer";
10
+ import { createGzip } from "node:zlib";
11
+ import { pipeline } from "node:stream/promises";
10
12
 
11
13
  export * from "./config";
12
14
  export * from "./dev-server";
@@ -22,25 +24,55 @@ export async function build(config: ResolvedConfig) {
22
24
  console.log(
23
25
  `${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./app${RESET} with ${GREEN}Vite ${vite.version}${RESET}`,
24
26
  );
25
- const { output: app } = (await vite.build(
26
- config.vite,
27
- )) as vite.Rollup.RollupOutput;
27
+ const appOutput = (await vite.build(config.vite)) as vite.Rollup.RollupOutput;
28
28
  console.log(`${GREEN}✔${RESET} Built in ${appTimer()}`);
29
29
 
30
+ const allAbsoluteAppFiles = (
31
+ await readdir(config.appOutDir, { recursive: true, withFileTypes: true })
32
+ )
33
+ .filter((entry) => entry.isFile())
34
+ .map((entry) => join(entry.parentPath, entry.name));
35
+ const allAppFiles = allAbsoluteAppFiles.map((path) =>
36
+ relative(config.appOutDir, path),
37
+ );
38
+
39
+ const bundledAppFiles = appOutput.output.map((entry) => entry.fileName);
40
+ const bundledAppFileSet = new Set(bundledAppFiles);
41
+ const publicAppFiles = allAppFiles.filter(
42
+ (file) => !bundledAppFileSet.has(file),
43
+ );
44
+
45
+ await gzipFiles(config, allAbsoluteAppFiles);
46
+
47
+ const staticRoutesFile = join(config.serverOutDir, "static.json");
48
+ let staticRoutes = [
49
+ ...Array.from(bundledAppFiles)
50
+ .filter((path) => path !== "index.html")
51
+ .map((path) => [`/${path}`, { cacheable: true, path: `public/${path}` }]),
52
+ ...publicAppFiles
53
+ .filter((path) => path !== "index.html")
54
+ .map((path) => [
55
+ `/${path}`,
56
+ { cacheable: false, path: `public/${path}` },
57
+ ]),
58
+ ];
59
+ await writeFile(
60
+ staticRoutesFile,
61
+ JSON.stringify(Object.fromEntries(staticRoutes)),
62
+ );
63
+
30
64
  console.log();
31
65
 
32
66
  const serverTimer = createTimer();
33
67
  console.log(
34
68
  `${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./server${RESET} with ${MAGENTA}Bun ${Bun.version}${RESET}`,
35
69
  );
36
- const server = await Bun.build({
70
+ await Bun.build({
37
71
  outdir: config.serverOutDir,
38
72
  sourcemap: "external",
39
73
  entrypoints: [config.serverEntry],
40
74
  target: "bun",
41
75
  define: {
42
- // In production, the public directory is inside the CWD
43
- "import.meta.publicDir": `"public"`,
44
76
  "import.meta.command": `"build"`,
45
77
  },
46
78
  plugins: [aframeServerMainBunPlugin(config)],
@@ -65,20 +97,36 @@ export async function build(config: ResolvedConfig) {
65
97
  console.log(`${DIM}${BOLD}→${RESET} Pre-rendering disabled`);
66
98
  }
67
99
 
100
+ await gzipFiles(
101
+ config,
102
+ prerendered.map((entry) => entry.absolutePath),
103
+ );
104
+
105
+ staticRoutes = staticRoutes.concat(
106
+ prerendered.map((entry) => [
107
+ entry.route,
108
+ { cacheable: false, path: `prerendered/${entry.relativePath}` },
109
+ ]),
110
+ );
111
+ await writeFile(
112
+ staticRoutesFile,
113
+ JSON.stringify(Object.fromEntries(staticRoutes)),
114
+ );
115
+
68
116
  console.log();
69
117
 
70
118
  console.log(`${GREEN}✔${RESET} Application built in ${buildTimer()}`);
71
119
  const relativeOutDir = `${relative(config.rootDir, config.outDir)}/`;
72
- const files = [
73
- ...server.outputs.map((output) => output.path),
74
- ...prerendered.map((output) => output.file),
75
- ...app
76
- .filter((output) => output.fileName !== "index.html")
77
- .map((output) => join(config.appOutDir, output.fileName)),
78
- ].map((file): [file: string, size: number] => [
79
- relative(config.outDir, file),
80
- lstatSync(file).size,
81
- ]);
120
+ const files = (
121
+ await readdir(config.outDir, { recursive: true, withFileTypes: true })
122
+ )
123
+ .filter((entry) => entry.isFile())
124
+ .map((entry) => join(entry.parentPath, entry.name))
125
+ .toSorted()
126
+ .map((file): [file: string, size: number] => [
127
+ relative(config.outDir, file),
128
+ lstatSync(file).size,
129
+ ]);
82
130
  const fileColumnCount = files.reduce(
83
131
  (max, [file]) => Math.max(file.length, max),
84
132
  0,
@@ -125,3 +173,19 @@ function prettyBytes(bytes: number) {
125
173
  const value = bytes / Math.pow(base, exponent);
126
174
  return `${unit === "B" ? value : value.toFixed(2)} ${unit}`;
127
175
  }
176
+
177
+ async function gzipFiles(
178
+ config: ResolvedConfig,
179
+ files: string[],
180
+ ): Promise<void> {
181
+ for (const file of files) await gzipFile(config, file);
182
+ }
183
+
184
+ async function gzipFile(config: ResolvedConfig, file: string): Promise<void> {
185
+ await writeFile(`${file}.gz`, "");
186
+ await pipeline(
187
+ createReadStream(file),
188
+ createGzip(),
189
+ createWriteStream(`${file}.gz`),
190
+ );
191
+ }
@@ -1,12 +1,13 @@
1
1
  import {} from "node:url";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import type { Browser } from "puppeteer";
5
5
  import type { ResolvedConfig } from "./config";
6
6
 
7
7
  export type PrerenderedRoute = {
8
8
  route: string;
9
- file: string;
9
+ absolutePath: string;
10
+ relativePath: string;
10
11
  };
11
12
 
12
13
  export async function prerenderPages(
@@ -67,13 +68,15 @@ export async function prerenderPages(
67
68
  throw Error("Vite error prevented page from being rendered.");
68
69
  }
69
70
 
70
- const dir = join(config.appOutDir, route.substring(1));
71
- const file = join(dir, "index.html");
71
+ const relativePath = join(route.substring(1), "index.html");
72
+ const absolutePath = join(config.prerenderedDir, relativePath);
73
+ const dir = dirname(absolutePath);
72
74
  await mkdir(dir, { recursive: true });
73
- await writeFile(file, html);
75
+ await writeFile(absolutePath, html);
74
76
  results.push({
75
- file,
76
77
  route,
78
+ relativePath,
79
+ absolutePath,
77
80
  });
78
81
  }
79
82
  } finally {