@aklinker1/aframe 0.5.0 → 1.0.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
@@ -28,8 +28,8 @@ export default defineConfig({
28
28
  },
29
29
  // List of routes to pre-render.
30
30
  prerenderedRoutes: ["/"],
31
- // See https://github.com/Tofandel/prerenderer?tab=readme-ov-file#prerenderer-options
32
- prerenderer: {
31
+ // Configure how prerendering works, or set to `false` to disable
32
+ prerender: {
33
33
  // ...
34
34
  },
35
35
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aklinker1/aframe",
3
- "version": "0.5.0",
4
- "packageManager": "bun@1.2.5",
3
+ "version": "1.0.0",
4
+ "packageManager": "bun@1.3.2",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -21,11 +21,10 @@
21
21
  "check": "check",
22
22
  "aframe": "bun --silent bin/aframe.ts",
23
23
  "dev": "bun aframe demo",
24
- "build": "bun aframe build demo",
24
+ "build": "bun aframe build demo && bun run demo/post-build.js",
25
25
  "preview": "bun --cwd demo/.output --env-file ../.env server-entry.js",
26
26
  "release": "bun run scripts/release.ts"
27
27
  },
28
- "dependencies": {},
29
28
  "devDependencies": {
30
29
  "@aklinker1/check": "^1.4.5",
31
30
  "@types/bun": "latest",
@@ -39,5 +38,10 @@
39
38
  "peerDependencies": {
40
39
  "vite": "*",
41
40
  "puppeteer": "*"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "puppeteer": {
44
+ "optional": true
45
+ }
42
46
  }
43
47
  }
@@ -6,8 +6,8 @@ export interface AframeServer {
6
6
  listen(port: number): void | never;
7
7
  }
8
8
 
9
- const staticPathsFile = join(import.meta.dir, "static.json");
10
- const publicDir = join(import.meta.dir, "public");
9
+ const staticPathsFile = join(aframe.rootDir, "static.json");
10
+ const publicDir = aframe.publicDir;
11
11
 
12
12
  let staticPaths: Record<string, { cacheable: boolean; path: string }> = {};
13
13
  try {
@@ -29,7 +29,7 @@ export function fetchStatic(options?: {
29
29
 
30
30
  // Fetch file on disk
31
31
  if (staticPaths[path]) {
32
- const filePath = join(import.meta.dir, staticPaths[path].path);
32
+ const filePath = join(aframe.rootDir, staticPaths[path].path);
33
33
  const file = Bun.file(filePath);
34
34
  const gzFile = Bun.file(filePath + ".gz");
35
35
 
@@ -45,35 +45,40 @@ export function fetchStatic(options?: {
45
45
  });
46
46
  }
47
47
 
48
+ // If the path is asking for a file (e.g., it has an extension), return a
49
+ // 404 if it wasn't in the static list
48
50
  const ext = extname(basename(path));
49
51
  if (ext) {
50
52
  return new Response(undefined, { status: 404 });
51
53
  }
52
54
 
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
- }
64
- return new Response(
65
- `<html>
55
+ // During development, render a fallback HTML page since Vite should handle
56
+ // all these routes before proxying the request to the server.
57
+ if (aframe.command === "serve") {
58
+ return new Response(
59
+ `<html>
66
60
  <body>
67
61
  This is a placeholder for your root <code>index.html</code> file during development.
68
62
  <br/>
69
63
  In production (or via the app's dev server), this path will fallback on the root <code>index.html</code>.
70
64
  </body>
71
65
  </html>`,
72
- {
73
- headers: {
74
- "Content-Type": "text/html",
66
+ {
67
+ headers: {
68
+ "Content-Type": "text/html",
69
+ },
75
70
  },
71
+ );
72
+ }
73
+
74
+ // 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",
76
81
  },
77
- );
82
+ });
78
83
  };
79
84
  }
package/src/config.ts CHANGED
@@ -2,6 +2,10 @@ import * as vite from "vite";
2
2
  import { resolve, join, relative } from "node:path/posix";
3
3
  import type { LaunchOptions } from "puppeteer";
4
4
 
5
+ export type AframeHooks = {
6
+ afterServerBuild?: (config: ResolvedConfig) => Promise<void> | void;
7
+ };
8
+
5
9
  export type UserConfig = {
6
10
  vite?: vite.UserConfigExport;
7
11
  /**
@@ -10,12 +14,13 @@ export type UserConfig = {
10
14
  */
11
15
  proxyPaths?: string[];
12
16
  prerenderedRoutes?: string[];
13
- prerenderer?: PrerendererConfig | false;
17
+ prerender?: PrerenderConfig | false;
14
18
  appPort?: number;
15
19
  serverPort?: number;
20
+ hooks?: AframeHooks;
16
21
  };
17
22
 
18
- export type PrerendererConfig = {
23
+ export type PrerenderConfig = {
19
24
  /** Wait for an selector`document.querySelector` to be in the DOM before grabbing the HTML. */
20
25
  waitForSelector?: string;
21
26
  /** When `waitForSelector` is set, also wait for the element to be visible before grabbing the HTML. */
@@ -33,11 +38,11 @@ export type PrerendererConfig = {
33
38
 
34
39
  export type ResolvedConfig = {
35
40
  rootDir: string;
41
+ packageJsonPath: string;
36
42
  appDir: string;
37
43
  publicDir: string;
38
44
  serverDir: string;
39
45
  serverModule: string;
40
- serverEntry: string;
41
46
  prerenderedDir: string;
42
47
  proxyPaths: string[];
43
48
  outDir: string;
@@ -47,7 +52,8 @@ export type ResolvedConfig = {
47
52
  serverPort: number;
48
53
  vite: vite.InlineConfig;
49
54
  prerenderedRoutes: string[];
50
- prerenderer: PrerendererConfig | false;
55
+ prerender: PrerenderConfig | false;
56
+ hooks: AframeHooks | undefined;
51
57
  };
52
58
 
53
59
  export function defineConfig(config: UserConfig): UserConfig {
@@ -60,14 +66,14 @@ export async function resolveConfig(
60
66
  mode: string,
61
67
  ): Promise<ResolvedConfig> {
62
68
  const rootDir = root ? resolve(root) : process.cwd();
69
+ const packageJsonPath = join(rootDir, "package.json");
63
70
  const appDir = join(rootDir, "app");
64
71
  const serverDir = join(rootDir, "server");
65
72
  const serverModule = join(serverDir, "main.ts");
66
- const serverEntry = join(import.meta.dir, "server-entry.ts");
67
73
  const publicDir = join(rootDir, "public");
68
74
  const outDir = join(rootDir, ".output");
69
75
  const appOutDir = join(outDir, "public");
70
- const serverOutDir = outDir;
76
+ const serverOutDir = join(outDir, "server");
71
77
  const prerenderedDir = join(outDir, "prerendered");
72
78
 
73
79
  const configFile = join(rootDir, "aframe.config"); // No file extension to resolve any JS/TS file
@@ -129,11 +135,11 @@ export async function resolveConfig(
129
135
 
130
136
  return {
131
137
  rootDir,
138
+ packageJsonPath,
132
139
  appDir,
133
140
  publicDir,
134
141
  serverDir,
135
142
  serverModule,
136
- serverEntry,
137
143
  outDir,
138
144
  serverOutDir,
139
145
  appOutDir,
@@ -144,6 +150,7 @@ export async function resolveConfig(
144
150
 
145
151
  prerenderedRoutes: userConfig.prerenderedRoutes ?? ["/"],
146
152
  vite: viteConfig,
147
- prerenderer: userConfig.prerenderer ?? {},
153
+ prerender: userConfig.prerender ?? {},
154
+ hooks: userConfig.hooks,
148
155
  };
149
156
  }
package/src/dev-server.ts CHANGED
@@ -11,21 +11,18 @@ export async function createServer(
11
11
 
12
12
  let serverProcess: Subprocess | undefined;
13
13
  const startServer = () => {
14
- const js = [
15
- `import server from '${config.serverModule}';`,
16
- `server.listen(${config.serverPort});`,
17
- ].join("\n");
14
+ const js = `globalThis.aframe = {
15
+ command: "serve",
16
+ rootDir: "${config.rootDir}",
17
+ publicDir: "${config.publicDir}",
18
+ };
19
+
20
+ const { default: server } = await import('${config.serverModule}');
21
+
22
+ server.listen(${config.serverPort});
23
+ `;
18
24
  return Bun.spawn({
19
- cmd: [
20
- "bun",
21
- "--watch",
22
- "--define",
23
- `import.meta.publicDir:"${config.publicDir}"`,
24
- "--define",
25
- `import.meta.command:"serve"`,
26
- "--eval",
27
- js,
28
- ],
25
+ cmd: ["bun", "--watch", "--eval", js],
29
26
  stdio: ["inherit", "inherit", "inherit"],
30
27
  cwd: config.rootDir,
31
28
  });
package/src/env.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import "vite/client";
2
2
 
3
3
  declare global {
4
- interface ImportMeta {
5
- command: string;
6
- }
4
+ declare var aframe: {
5
+ command: "build" | "serve";
6
+ rootDir: string;
7
+ publicDir: string;
8
+ };
7
9
  }
8
10
 
9
11
  export {};
package/src/index.ts CHANGED
@@ -1,12 +1,11 @@
1
- import type { BunPlugin } from "bun";
2
1
  import { createReadStream, createWriteStream, lstatSync } from "node:fs";
3
- import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
4
3
  import { join, relative } from "node:path/posix";
5
4
  import * as vite from "vite";
6
- import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET } from "./color";
5
+ import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET, YELLOW } from "./color";
7
6
  import type { ResolvedConfig } from "./config";
8
7
  import { createTimer } from "./timer";
9
- import { prerenderPages, type PrerenderedRoute } from "./prerenderer";
8
+ import { prerenderPages, type PrerenderedRoute } from "./prerender";
10
9
  import { createGzip } from "node:zlib";
11
10
  import { pipeline } from "node:stream/promises";
12
11
 
@@ -44,7 +43,7 @@ export async function build(config: ResolvedConfig) {
44
43
 
45
44
  await gzipFiles(config, allAbsoluteAppFiles);
46
45
 
47
- const staticRoutesFile = join(config.serverOutDir, "static.json");
46
+ const staticRoutesFile = join(config.outDir, "static.json");
48
47
  let staticRoutes = [
49
48
  ...Array.from(bundledAppFiles)
50
49
  .filter((path) => path !== "index.html")
@@ -64,26 +63,14 @@ export async function build(config: ResolvedConfig) {
64
63
  console.log();
65
64
 
66
65
  const serverTimer = createTimer();
67
- console.log(
68
- `${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./server${RESET} with ${MAGENTA}Bun ${Bun.version}${RESET}`,
69
- );
70
- await Bun.build({
71
- outdir: config.serverOutDir,
72
- sourcemap: "external",
73
- entrypoints: [config.serverEntry],
74
- target: "bun",
75
- define: {
76
- "import.meta.command": `"build"`,
77
- },
78
- plugins: [aframeServerMainBunPlugin(config)],
79
- throw: true,
80
- });
66
+ console.log(`${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./server${RESET}`);
67
+ await buildServer(config);
81
68
  console.log(`${GREEN}✔${RESET} Built in ${serverTimer()}`);
82
69
 
83
70
  console.log();
84
71
 
85
72
  let prerendered: PrerenderedRoute[] = [];
86
- if (config.prerenderer !== false) {
73
+ if (config.prerender !== false) {
87
74
  const prerenderTimer = createTimer();
88
75
  console.log(
89
76
  `${BOLD}${CYAN}ℹ${RESET} Prerendering...\n` +
@@ -94,7 +81,7 @@ export async function build(config: ResolvedConfig) {
94
81
  prerendered = await prerenderPages(config);
95
82
  console.log(`${GREEN}✔${RESET} Prerendered in ${prerenderTimer()}`);
96
83
  } else {
97
- console.log(`${DIM}${BOLD}→${RESET} Pre-rendering disabled`);
84
+ console.log(`${DIM}${BOLD}→${RESET} Pre-render disabled`);
98
85
  }
99
86
 
100
87
  await gzipFiles(
@@ -118,10 +105,8 @@ export async function build(config: ResolvedConfig) {
118
105
  console.log(`${GREEN}✔${RESET} Application built in ${buildTimer()}`);
119
106
  const relativeOutDir = `${relative(config.rootDir, config.outDir)}/`;
120
107
  const files = (
121
- await readdir(config.outDir, { recursive: true, withFileTypes: true })
108
+ await listDirFiles(config.outDir, (path) => !path.includes("node_modules"))
122
109
  )
123
- .filter((entry) => entry.isFile())
124
- .map((entry) => join(entry.parentPath, entry.name))
125
110
  .toSorted()
126
111
  .map((file): [file: string, size: number] => [
127
112
  relative(config.outDir, file),
@@ -142,21 +127,79 @@ export async function build(config: ResolvedConfig) {
142
127
  );
143
128
  console.log(`${CYAN}Σ Total size:${RESET} ${prettyBytes(totalSize)}`);
144
129
  console.log();
130
+
131
+ console.log(
132
+ `To preview production build, run:
133
+
134
+ ${GREEN}bun run ${relative(process.cwd(), config.outDir)}/server-entry.ts${RESET}
135
+ `,
136
+ );
145
137
  }
146
138
 
147
- function aframeServerMainBunPlugin(config: ResolvedConfig): BunPlugin {
148
- return {
149
- name: "aframe:resolve-server-main",
150
- setup(bun) {
151
- bun.onResolve({ filter: /^aframe:server-main$/ }, () => ({
152
- path: config.serverModule,
153
- }));
139
+ async function buildServer(config: ResolvedConfig): Promise<void> {
140
+ await cp(config.serverDir, config.serverOutDir, {
141
+ recursive: true,
142
+ filter: (src) =>
143
+ !src.includes("__tests__") &&
144
+ !src.includes(".test.") &&
145
+ !src.includes(".spec."),
146
+ });
147
+ await Promise.all(
148
+ ["bun.lock", "bun.lockb", "tsconfig.json"].map((file) =>
149
+ cp(join(config.rootDir, file), join(config.outDir, file)).catch(() => {
150
+ // Ignore errors
151
+ }),
152
+ ),
153
+ );
154
+ const packageJson = await Bun.file(config.packageJsonPath)
155
+ .json()
156
+ .catch(() => ({}));
157
+ await Bun.write(
158
+ join(config.outDir, "package.json"),
159
+ JSON.stringify(
160
+ {
161
+ dependencies: packageJson.dependencies,
162
+ devDependencies: packageJson.devDependencies,
163
+ },
164
+ null,
165
+ 2,
166
+ ),
167
+ );
168
+ await Bun.write(
169
+ join(config.outDir, "server-entry.ts"),
170
+ `import { resolve } from 'node:path';
171
+
172
+ globalThis.aframe = {
173
+ command: "build",
174
+ rootDir: import.meta.dir,
175
+ publicDir: resolve(import.meta.dir, "public"),
176
+ };
177
+
178
+ const { default: server } = await import("./server/main");
179
+
180
+ const port = Number(process.env.PORT) || 3000;
181
+ console.log(\`Server running @ http://localhost:\${port}\`);
182
+ server.listen(port);
183
+ `,
184
+ );
185
+ const installProc = Bun.spawn(
186
+ ["bun", "i", "--production", "--frozen-lockfile"],
187
+ {
188
+ cwd: config.outDir,
154
189
  },
155
- };
190
+ );
191
+ const installStatus = await installProc.exited;
192
+ if (installStatus !== 0) {
193
+ throw new Error(`Failed to run "bun i --production" in ${config.outDir}`);
194
+ }
195
+
196
+ await config.hooks?.afterServerBuild?.(config);
156
197
  }
157
198
 
158
199
  function getColor(file: string) {
159
200
  if (file.endsWith(".js")) return CYAN;
201
+ if (file.endsWith(".ts")) return CYAN;
202
+ if (file.endsWith(".json")) return YELLOW;
160
203
  if (file.endsWith(".html")) return GREEN;
161
204
  if (file.endsWith(".css")) return MAGENTA;
162
205
  if (file.endsWith(".map")) return DIM;
@@ -189,3 +232,26 @@ async function gzipFile(config: ResolvedConfig, file: string): Promise<void> {
189
232
  createWriteStream(`${file}.gz`),
190
233
  );
191
234
  }
235
+
236
+ async function listDirFiles(
237
+ dir: string,
238
+ filter: (path: string) => boolean,
239
+ ): Promise<string[]> {
240
+ const entries = await readdir(dir, { withFileTypes: true });
241
+ const files: string[] = [];
242
+
243
+ for (const entry of entries) {
244
+ const fullPath = join(entry.parentPath, entry.name);
245
+
246
+ if (!filter(fullPath)) continue;
247
+
248
+ if (entry.isFile()) {
249
+ files.push(fullPath);
250
+ } else if (entry.isDirectory()) {
251
+ const subFiles = await listDirFiles(fullPath, filter);
252
+ files.push(...subFiles);
253
+ }
254
+ }
255
+
256
+ return files;
257
+ }
@@ -1,4 +1,3 @@
1
- import {} from "node:url";
2
1
  import { dirname, join } from "node:path";
3
2
  import { mkdir, writeFile } from "node:fs/promises";
4
3
  import type { Browser } from "puppeteer";
@@ -13,7 +12,7 @@ export type PrerenderedRoute = {
13
12
  export async function prerenderPages(
14
13
  config: ResolvedConfig,
15
14
  ): Promise<PrerenderedRoute[]> {
16
- if (config.prerenderer === false) return [];
15
+ if (config.prerender === false) return [];
17
16
 
18
17
  const puppeteer = await import("puppeteer");
19
18
  const {
@@ -22,10 +21,10 @@ export async function prerenderPages(
22
21
  waitForSelector,
23
22
  waitForSelectorVisible,
24
23
  waitForTimeout,
25
- } = config.prerenderer ?? {};
24
+ } = config.prerender ?? {};
26
25
 
27
26
  const server = Bun.spawn({
28
- cmd: ["bun", join(config.serverOutDir, "server-entry.js")],
27
+ cmd: ["bun", join(config.outDir, "server-entry.ts")],
29
28
  cwd: config.rootDir,
30
29
  stdio: ["inherit", "inherit", "inherit"],
31
30
  });
@@ -1,6 +0,0 @@
1
- // This import is resolved by a Bun plugin pointing to your project's `server/main.ts` file.
2
- import server from "aframe:server-main";
3
-
4
- const port = Number(process.env.PORT) || 3000;
5
- console.log(`Server running @ http://localhost:${port}`);
6
- server.listen(port);
@@ -1,4 +0,0 @@
1
- declare module "aframe:server-main" {
2
- const server: import("./server").AframeServer;
3
- export default server;
4
- }