@aklinker1/aframe 1.0.3 → 1.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aklinker1/aframe",
3
- "version": "1.0.3",
4
- "packageManager": "bun@1.3.2",
3
+ "version": "1.1.0",
4
+ "packageManager": "bun@1.3.5",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -21,13 +21,13 @@
21
21
  "check": "check",
22
22
  "aframe": "bun --silent bin/aframe.ts",
23
23
  "dev": "bun aframe demo",
24
- "build": "bun aframe build demo && bun run demo/post-build.js",
24
+ "build": "bun aframe build demo",
25
25
  "preview": "bun --cwd demo/.output --env-file ../.env server-entry.js",
26
26
  "release": "bun run scripts/release.ts"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@aklinker1/check": "^1.4.5",
30
- "@types/bun": "latest",
30
+ "@types/bun": "^1.3.5",
31
31
  "oxlint": "^0.15.11",
32
32
  "prettier": "^3.5.2",
33
33
  "publint": "^0.3.6",
@@ -1,19 +1,10 @@
1
1
  import type { BunFile } from "bun";
2
- import { readFileSync } from "node:fs";
3
- import { join, extname, basename } from "node:path";
2
+ import { basename, extname, join } from "node:path";
4
3
 
5
4
  export interface AframeServer {
6
5
  listen(port: number): void | never;
7
6
  }
8
7
 
9
- const staticPathsFile = join(aframe.rootDir, "static.json");
10
- const publicDir = aframe.publicDir;
11
-
12
- let staticPaths: Record<string, { cacheable: boolean; path: string }> = {};
13
- try {
14
- staticPaths = JSON.parse(readFileSync(staticPathsFile, "utf-8"));
15
- } catch {}
16
-
17
8
  /**
18
9
  * Fetches a file from the `public` directory.
19
10
  */
@@ -28,11 +19,8 @@ export function fetchStatic(options?: {
28
19
  const path = new URL(request.url).pathname.replace(/\/+$/, "") || "/";
29
20
 
30
21
  // Fetch file on disk
31
- if (staticPaths[path]) {
32
- const filePath = join(aframe.rootDir, staticPaths[path].path);
33
- const file = Bun.file(filePath);
34
- const gzFile = Bun.file(filePath + ".gz");
35
-
22
+ if (aframe.static?.[path]) {
23
+ const { file, gzFile } = aframe.static[path];
36
24
  const customResponse = await options?.onFetch?.(path, file);
37
25
  if (customResponse) return customResponse;
38
26
 
@@ -72,13 +60,22 @@ export function fetchStatic(options?: {
72
60
  }
73
61
 
74
62
  // Fallback to public/index.html file
75
- const file = Bun.file(join(publicDir, "index.html"));
76
- const gzFile = Bun.file(join(publicDir, "index.html.gz"));
77
- return new Response(gzFile.stream(), {
78
- headers: {
79
- "Content-Type": file.type,
80
- "Content-Encoding": "gzip",
81
- },
82
- });
63
+ if (aframe.static?.["fallback"]) {
64
+ const { file, gzFile } = aframe.static["fallback"];
65
+ return createGzipResponse(file, gzFile);
66
+ }
67
+
68
+ const file = Bun.file(join(aframe.publicDir, "index.html"));
69
+ const gzFile = Bun.file(join(aframe.publicDir, "index.html.gz"));
70
+ return createGzipResponse(file, gzFile);
83
71
  };
84
72
  }
73
+
74
+ function createGzipResponse(file: BunFile, gzFile: BunFile): Response {
75
+ return new Response(gzFile.stream(), {
76
+ headers: {
77
+ "Content-Type": file.type,
78
+ "Content-Encoding": "gzip",
79
+ },
80
+ });
81
+ }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import * as vite from "vite";
2
- import { resolve, join, relative } from "node:path/posix";
1
+ import { join, relative, resolve } from "node:path/posix";
3
2
  import type { LaunchOptions } from "puppeteer";
3
+ import * as vite from "vite";
4
4
 
5
5
  export type AframeHooks = {
6
6
  afterServerBuild?: (config: ResolvedConfig) => Promise<void> | void;
@@ -18,6 +18,11 @@ export type UserConfig = {
18
18
  appPort?: number;
19
19
  serverPort?: number;
20
20
  hooks?: AframeHooks;
21
+ /**
22
+ * Compile the app into a single binary using Bun.
23
+ * @default true
24
+ */
25
+ compile?: boolean;
21
26
  };
22
27
 
23
28
  export type PrerenderConfig = {
@@ -54,6 +59,9 @@ export type ResolvedConfig = {
54
59
  prerenderedRoutes: string[];
55
60
  prerender: PrerenderConfig | false;
56
61
  hooks: AframeHooks | undefined;
62
+ serverEntryPath: string;
63
+ compileOutputPath: string;
64
+ compile: boolean;
57
65
  };
58
66
 
59
67
  export function defineConfig(config: UserConfig): UserConfig {
@@ -75,6 +83,8 @@ export async function resolveConfig(
75
83
  const appOutDir = join(outDir, "public");
76
84
  const serverOutDir = join(outDir, "server");
77
85
  const prerenderedDir = join(outDir, "prerendered");
86
+ const serverEntryPath = join(outDir, "server-entry.ts");
87
+ const compileOutputPath = join(outDir, "server-entry");
78
88
 
79
89
  const configFile = join(rootDir, "aframe.config"); // No file extension to resolve any JS/TS file
80
90
  const relativeConfigFile = "./" + relative(import.meta.dir, configFile);
@@ -147,10 +157,13 @@ export async function resolveConfig(
147
157
  appPort,
148
158
  serverPort,
149
159
  proxyPaths,
160
+ serverEntryPath,
161
+ compileOutputPath,
150
162
 
151
163
  prerenderedRoutes: userConfig.prerenderedRoutes ?? ["/"],
152
164
  vite: viteConfig,
153
165
  prerender: userConfig.prerender ?? {},
154
166
  hooks: userConfig.hooks,
167
+ compile: userConfig.compile ?? true,
155
168
  };
156
169
  }
package/src/env.d.ts CHANGED
@@ -1,10 +1,14 @@
1
+ import type { BunFile } from "bun";
1
2
  import "vite/client";
2
3
 
3
4
  declare global {
4
5
  declare var aframe: {
5
6
  command: "build" | "serve";
6
- rootDir: string;
7
7
  publicDir: string;
8
+ static?: Record<
9
+ string,
10
+ { file: BunFile; gzFile: BunFile; cacheable: boolean }
11
+ >;
8
12
  };
9
13
  }
10
14
 
package/src/index.ts CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  } from "node:fs";
7
7
  import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
8
8
  import { join, relative } from "node:path/posix";
9
+ import { pipeline } from "node:stream/promises";
10
+ import { createGzip } from "node:zlib";
9
11
  import * as vite from "vite";
10
12
  import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET, YELLOW } from "./color";
11
13
  import type { ResolvedConfig } from "./config";
12
- import { createTimer } from "./timer";
13
14
  import { prerenderPages, type PrerenderedRoute } from "./prerender";
14
- import { createGzip } from "node:zlib";
15
- import { pipeline } from "node:stream/promises";
15
+ import { createTimer } from "./timer";
16
16
 
17
17
  export * from "./config";
18
18
  export * from "./dev-server";
@@ -46,31 +46,33 @@ export async function build(config: ResolvedConfig) {
46
46
  (file) => !bundledAppFileSet.has(file),
47
47
  );
48
48
 
49
- await gzipFiles(config, allAbsoluteAppFiles);
49
+ await gzipFiles(allAbsoluteAppFiles);
50
50
 
51
- const staticRoutesFile = join(config.outDir, "static.json");
52
- let staticRoutes = [
51
+ const staticRoutes: StaticRouteArray = [
53
52
  ...Array.from(bundledAppFiles)
54
53
  .filter((path) => path !== "index.html")
55
- .map((path) => [`/${path}`, { cacheable: true, path: `public/${path}` }]),
54
+ .map((path) => ({
55
+ route: `/${path}`,
56
+ cacheable: true,
57
+ filePath: `public/${path}`,
58
+ gzPath: `public/${path}.gz`,
59
+ })),
56
60
  ...publicAppFiles
57
61
  .filter((path) => path !== "index.html")
58
- .map((path) => [
59
- `/${path}`,
60
- { cacheable: false, path: `public/${path}` },
61
- ]),
62
+ .flatMap((path) => ({
63
+ route: `/${path}`,
64
+ cacheable: false,
65
+ filePath: `public/${path}`,
66
+ gzPath: `public/${path}.gz`,
67
+ })),
62
68
  ];
63
- await writeFile(
64
- staticRoutesFile,
65
- JSON.stringify(Object.fromEntries(staticRoutes)),
66
- );
67
69
 
68
70
  console.log();
69
71
 
70
72
  const serverTimer = createTimer();
71
- console.log(`${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./server${RESET}`);
72
- await buildServer(config);
73
- console.log(`${GREEN}✔${RESET} Built in ${serverTimer()}`);
73
+ console.log(`${BOLD}${CYAN}ℹ${RESET} Preparing ${CYAN}./server${RESET}`);
74
+ await buildServer(config, staticRoutes);
75
+ console.log(`${GREEN}✔${RESET} Done in ${serverTimer()}`);
74
76
 
75
77
  console.log();
76
78
 
@@ -89,25 +91,27 @@ export async function build(config: ResolvedConfig) {
89
91
  console.log(`${DIM}${BOLD}→${RESET} Pre-render disabled`);
90
92
  }
91
93
 
92
- await gzipFiles(
93
- config,
94
- prerendered.map((entry) => entry.absolutePath),
95
- );
96
-
97
- staticRoutes = staticRoutes.concat(
98
- prerendered.map((entry) => [
99
- entry.route,
100
- { cacheable: false, path: `prerendered/${entry.relativePath}` },
101
- ]),
102
- );
103
- await writeFile(
104
- staticRoutesFile,
105
- JSON.stringify(Object.fromEntries(staticRoutes)),
106
- );
94
+ await gzipFiles(prerendered.map((entry) => entry.absolutePath));
95
+
96
+ staticRoutes.push({
97
+ route: "fallback",
98
+ cacheable: false,
99
+ filePath: "public/index.html",
100
+ gzPath: "public/index.html.gz",
101
+ });
102
+ for (const entry of prerendered) {
103
+ staticRoutes.push({
104
+ route: entry.route,
105
+ cacheable: false,
106
+ filePath: `prerendered/${entry.relativePath}`,
107
+ gzPath: `prerendered/${entry.relativePath}.gz`,
108
+ });
109
+ }
110
+ await writeServerEntry(config, staticRoutes);
107
111
 
108
112
  console.log();
109
113
 
110
- console.log(`${GREEN}✔${RESET} Application built in ${buildTimer()}`);
114
+ console.log(`${GREEN}✔${RESET} Bundled in ${buildTimer()}`);
111
115
  const relativeOutDir = `${relative(config.rootDir, config.outDir)}/`;
112
116
  const files = (
113
117
  await listDirFiles(config.outDir, (path) => !path.includes("node_modules"))
@@ -126,22 +130,51 @@ export async function build(config: ResolvedConfig) {
126
130
  files
127
131
  .map(([file, size], i, array) => {
128
132
  const boxChar = i === array.length - 1 ? "└" : "├";
129
- return ` ${DIM}${boxChar}─ ${relativeOutDir}${RESET}${getColor(file)}${file.padEnd(fileColumnCount)}${RESET} ${DIM}${prettyBytes(size)}${RESET}`;
133
+ return ` ${DIM}${boxChar}─ ${relativeOutDir}${RESET}${getColor(file)}${file.padEnd(fileColumnCount)}${RESET} ${DIM}${prettyBytes(size)}${RESET}`;
130
134
  })
131
135
  .join("\n"),
132
136
  );
133
137
  console.log(`${CYAN}Σ Total size:${RESET} ${prettyBytes(totalSize)}`);
134
138
  console.log();
135
139
 
136
- console.log(
137
- `To preview production build, run:
140
+ if (config.compile) {
141
+ const compileTimer = createTimer();
142
+ console.log(`${BOLD}${CYAN}ℹ${RESET} Compiling single binary`);
138
143
 
139
- ${GREEN}bun run ${relative(process.cwd(), config.outDir)}/server-entry.ts${RESET}
144
+ await writeServerEntry(config, staticRoutes);
145
+ await Bun.build({
146
+ compile: {
147
+ outfile: config.compileOutputPath,
148
+ },
149
+ entrypoints: [config.serverEntryPath],
150
+ bytecode: true,
151
+ });
152
+ console.log(`${GREEN}✔${RESET} Compiled in ${compileTimer()}`);
153
+ console.log(
154
+ ` ${DIM}└─${RESET} ${BLUE}${relative(process.cwd(), config.compileOutputPath)}${RESET} ${DIM}${prettyBytes(lstatSync(config.compileOutputPath).size)}${RESET}`,
155
+ );
156
+ console.log();
157
+
158
+ console.log(
159
+ `To preview production build, run:
160
+
161
+ ${GREEN}./${relative(process.cwd(), config.compileOutputPath)}${RESET}
140
162
  `,
141
- );
163
+ );
164
+ } else {
165
+ console.log(
166
+ `To preview production build, run:
167
+
168
+ ${GREEN}bun run ${relative(process.cwd(), config.serverEntryPath)}${RESET}
169
+ `,
170
+ );
171
+ }
142
172
  }
143
173
 
144
- async function buildServer(config: ResolvedConfig): Promise<void> {
174
+ async function buildServer(
175
+ config: ResolvedConfig,
176
+ staticRoutes: StaticRouteArray,
177
+ ): Promise<void> {
145
178
  const cpDirOptions: CopyOptions = {
146
179
  recursive: true,
147
180
  filter: (src) =>
@@ -162,7 +195,7 @@ async function buildServer(config: ResolvedConfig): Promise<void> {
162
195
  }),
163
196
  ),
164
197
  // Copy root files
165
- ...["bun.lock", "bun.lockb", "tsconfig.json"].map((file) =>
198
+ ...["bun.lock", "bun.lockb", "tsconfig.json", ".npmrc"].map((file) =>
166
199
  cp(join(config.rootDir, file), join(config.outDir, file)).catch(() => {
167
200
  // Ignore errors
168
201
  }),
@@ -172,7 +205,7 @@ async function buildServer(config: ResolvedConfig): Promise<void> {
172
205
  const packageJson = await Bun.file(config.packageJsonPath)
173
206
  .json()
174
207
  .catch(() => ({}));
175
- await Bun.write(
208
+ await writeFile(
176
209
  join(config.outDir, "package.json"),
177
210
  JSON.stringify(
178
211
  {
@@ -185,23 +218,7 @@ async function buildServer(config: ResolvedConfig): Promise<void> {
185
218
  ),
186
219
  );
187
220
 
188
- await Bun.write(
189
- join(config.outDir, "server-entry.ts"),
190
- `import { resolve } from 'node:path';
191
-
192
- globalThis.aframe = {
193
- command: "build",
194
- rootDir: import.meta.dir,
195
- publicDir: resolve(import.meta.dir, "public"),
196
- };
197
-
198
- const { default: server } = await import("./server/main");
199
-
200
- const port = Number(process.env.PORT) || 3000;
201
- console.log(\`Server running @ http://localhost:\${port}\`);
202
- server.listen(port);
203
- `,
204
- );
221
+ await writeServerEntry(config, staticRoutes);
205
222
 
206
223
  const installProc = Bun.spawn(
207
224
  ["bun", "i", "--production", "--frozen-lockfile"],
@@ -217,6 +234,45 @@ server.listen(port);
217
234
  await config.hooks?.afterServerBuild?.(config);
218
235
  }
219
236
 
237
+ async function writeServerEntry(
238
+ config: ResolvedConfig,
239
+ staticRoutes: StaticRouteArray,
240
+ ): Promise<void> {
241
+ const staticDef: string[] = [];
242
+ if (staticRoutes.length > 0) {
243
+ staticDef.push(" static: {");
244
+ staticRoutes.forEach((entry, i) => {
245
+ staticDef.push(
246
+ ` "${entry.route}": { file: Bun.file(file${i}), cacheable: ${entry.cacheable}, gzFile: Bun.file(gzFile${i}) },`,
247
+ );
248
+ });
249
+ staticDef.push(" },");
250
+ }
251
+
252
+ await writeFile(
253
+ join(config.outDir, "server-entry.ts"),
254
+ `import { resolve } from 'node:path';
255
+ ${staticRoutes
256
+ .flatMap(({ filePath }, i) => [
257
+ `import file${i} from "./${filePath}" with { type: "file" };`,
258
+ `import gzFile${i} from "./${filePath}.gz" with { type: "file" };`,
259
+ ])
260
+ .join("\n")}
261
+ import server from "./server/main";
262
+
263
+ globalThis.aframe = {
264
+ command: "build",
265
+ publicDir: resolve(import.meta.dir, "public"),
266
+ ${staticDef.join("\n")}
267
+ };
268
+
269
+ const port = Number(process.env.PORT) || 3000;
270
+ console.log(\`Server running @ http://localhost:\${port}\`);
271
+ server.listen(port);
272
+ `,
273
+ );
274
+ }
275
+
220
276
  function getColor(file: string) {
221
277
  if (file.endsWith(".js")) return CYAN;
222
278
  if (file.endsWith(".ts")) return CYAN;
@@ -238,14 +294,11 @@ function prettyBytes(bytes: number) {
238
294
  return `${unit === "B" ? value : value.toFixed(2)} ${unit}`;
239
295
  }
240
296
 
241
- async function gzipFiles(
242
- config: ResolvedConfig,
243
- files: string[],
244
- ): Promise<void> {
245
- for (const file of files) await gzipFile(config, file);
297
+ async function gzipFiles(files: string[]): Promise<void> {
298
+ for (const file of files) await gzipFile(file);
246
299
  }
247
300
 
248
- async function gzipFile(config: ResolvedConfig, file: string): Promise<void> {
301
+ async function gzipFile(file: string): Promise<void> {
249
302
  await writeFile(`${file}.gz`, "");
250
303
  await pipeline(
251
304
  createReadStream(file),
@@ -276,3 +329,11 @@ async function listDirFiles(
276
329
 
277
330
  return files;
278
331
  }
332
+
333
+ type StaticRouteEntry = {
334
+ route: string;
335
+ filePath: string;
336
+ gzPath: string;
337
+ cacheable: boolean;
338
+ };
339
+ type StaticRouteArray = StaticRouteEntry[];
package/src/prerender.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { dirname, join } from "node:path";
2
1
  import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
3
  import type { Browser } from "puppeteer";
4
4
  import type { ResolvedConfig } from "./config";
5
5