@aklinker1/aframe 0.5.0 → 1.0.1

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.1",
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,16 @@
1
- import type { BunPlugin } from "bun";
2
- import { createReadStream, createWriteStream, lstatSync } from "node:fs";
3
- import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
1
+ import {
2
+ createReadStream,
3
+ createWriteStream,
4
+ lstatSync,
5
+ type CopyOptions,
6
+ } from "node:fs";
7
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
4
8
  import { join, relative } from "node:path/posix";
5
9
  import * as vite from "vite";
6
- import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET } from "./color";
10
+ import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET, YELLOW } from "./color";
7
11
  import type { ResolvedConfig } from "./config";
8
12
  import { createTimer } from "./timer";
9
- import { prerenderPages, type PrerenderedRoute } from "./prerenderer";
13
+ import { prerenderPages, type PrerenderedRoute } from "./prerender";
10
14
  import { createGzip } from "node:zlib";
11
15
  import { pipeline } from "node:stream/promises";
12
16
 
@@ -44,7 +48,7 @@ export async function build(config: ResolvedConfig) {
44
48
 
45
49
  await gzipFiles(config, allAbsoluteAppFiles);
46
50
 
47
- const staticRoutesFile = join(config.serverOutDir, "static.json");
51
+ const staticRoutesFile = join(config.outDir, "static.json");
48
52
  let staticRoutes = [
49
53
  ...Array.from(bundledAppFiles)
50
54
  .filter((path) => path !== "index.html")
@@ -64,26 +68,14 @@ export async function build(config: ResolvedConfig) {
64
68
  console.log();
65
69
 
66
70
  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
- });
71
+ console.log(`${BOLD}${CYAN}ℹ${RESET} Building ${CYAN}./server${RESET}`);
72
+ await buildServer(config);
81
73
  console.log(`${GREEN}✔${RESET} Built in ${serverTimer()}`);
82
74
 
83
75
  console.log();
84
76
 
85
77
  let prerendered: PrerenderedRoute[] = [];
86
- if (config.prerenderer !== false) {
78
+ if (config.prerender !== false) {
87
79
  const prerenderTimer = createTimer();
88
80
  console.log(
89
81
  `${BOLD}${CYAN}ℹ${RESET} Prerendering...\n` +
@@ -94,7 +86,7 @@ export async function build(config: ResolvedConfig) {
94
86
  prerendered = await prerenderPages(config);
95
87
  console.log(`${GREEN}✔${RESET} Prerendered in ${prerenderTimer()}`);
96
88
  } else {
97
- console.log(`${DIM}${BOLD}→${RESET} Pre-rendering disabled`);
89
+ console.log(`${DIM}${BOLD}→${RESET} Pre-render disabled`);
98
90
  }
99
91
 
100
92
  await gzipFiles(
@@ -118,10 +110,8 @@ export async function build(config: ResolvedConfig) {
118
110
  console.log(`${GREEN}✔${RESET} Application built in ${buildTimer()}`);
119
111
  const relativeOutDir = `${relative(config.rootDir, config.outDir)}/`;
120
112
  const files = (
121
- await readdir(config.outDir, { recursive: true, withFileTypes: true })
113
+ await listDirFiles(config.outDir, (path) => !path.includes("node_modules"))
122
114
  )
123
- .filter((entry) => entry.isFile())
124
- .map((entry) => join(entry.parentPath, entry.name))
125
115
  .toSorted()
126
116
  .map((file): [file: string, size: number] => [
127
117
  relative(config.outDir, file),
@@ -142,21 +132,90 @@ export async function build(config: ResolvedConfig) {
142
132
  );
143
133
  console.log(`${CYAN}Σ Total size:${RESET} ${prettyBytes(totalSize)}`);
144
134
  console.log();
135
+
136
+ console.log(
137
+ `To preview production build, run:
138
+
139
+ ${GREEN}bun run ${relative(process.cwd(), config.outDir)}/server-entry.ts${RESET}
140
+ `,
141
+ );
145
142
  }
146
143
 
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
- }));
154
- },
144
+ async function buildServer(config: ResolvedConfig): Promise<void> {
145
+ const cpOptions: CopyOptions = {
146
+ recursive: true,
147
+ filter: (src) =>
148
+ !src.includes("__tests__") &&
149
+ !src.includes(".test.") &&
150
+ !src.includes(".spec."),
155
151
  };
152
+
153
+ await Promise.all([
154
+ // Copy dirs
155
+ ...[config.serverDir, join(config.rootDir, "shared")].map((src) =>
156
+ cp(src, config.serverOutDir, cpOptions).catch(() => {
157
+ // Ignore errors
158
+ }),
159
+ ),
160
+ // Copy root files
161
+ ...["bun.lock", "bun.lockb", "tsconfig.json"].map((file) =>
162
+ cp(join(config.rootDir, file), join(config.outDir, file)).catch(() => {
163
+ // Ignore errors
164
+ }),
165
+ ),
166
+ ]);
167
+
168
+ const packageJson = await Bun.file(config.packageJsonPath)
169
+ .json()
170
+ .catch(() => ({}));
171
+ await Bun.write(
172
+ join(config.outDir, "package.json"),
173
+ JSON.stringify(
174
+ {
175
+ dependencies: packageJson.dependencies,
176
+ devDependencies: packageJson.devDependencies,
177
+ },
178
+ null,
179
+ 2,
180
+ ),
181
+ );
182
+
183
+ await Bun.write(
184
+ join(config.outDir, "server-entry.ts"),
185
+ `import { resolve } from 'node:path';
186
+
187
+ globalThis.aframe = {
188
+ command: "build",
189
+ rootDir: import.meta.dir,
190
+ publicDir: resolve(import.meta.dir, "public"),
191
+ };
192
+
193
+ const { default: server } = await import("./server/main");
194
+
195
+ const port = Number(process.env.PORT) || 3000;
196
+ console.log(\`Server running @ http://localhost:\${port}\`);
197
+ server.listen(port);
198
+ `,
199
+ );
200
+
201
+ const installProc = Bun.spawn(
202
+ ["bun", "i", "--production", "--frozen-lockfile"],
203
+ {
204
+ cwd: config.outDir,
205
+ },
206
+ );
207
+ const installStatus = await installProc.exited;
208
+ if (installStatus !== 0) {
209
+ throw new Error(`Failed to run "bun i --production" in ${config.outDir}`);
210
+ }
211
+
212
+ await config.hooks?.afterServerBuild?.(config);
156
213
  }
157
214
 
158
215
  function getColor(file: string) {
159
216
  if (file.endsWith(".js")) return CYAN;
217
+ if (file.endsWith(".ts")) return CYAN;
218
+ if (file.endsWith(".json")) return YELLOW;
160
219
  if (file.endsWith(".html")) return GREEN;
161
220
  if (file.endsWith(".css")) return MAGENTA;
162
221
  if (file.endsWith(".map")) return DIM;
@@ -189,3 +248,26 @@ async function gzipFile(config: ResolvedConfig, file: string): Promise<void> {
189
248
  createWriteStream(`${file}.gz`),
190
249
  );
191
250
  }
251
+
252
+ async function listDirFiles(
253
+ dir: string,
254
+ filter: (path: string) => boolean,
255
+ ): Promise<string[]> {
256
+ const entries = await readdir(dir, { withFileTypes: true });
257
+ const files: string[] = [];
258
+
259
+ for (const entry of entries) {
260
+ const fullPath = join(entry.parentPath, entry.name);
261
+
262
+ if (!filter(fullPath)) continue;
263
+
264
+ if (entry.isFile()) {
265
+ files.push(fullPath);
266
+ } else if (entry.isDirectory()) {
267
+ const subFiles = await listDirFiles(fullPath, filter);
268
+ files.push(...subFiles);
269
+ }
270
+ }
271
+
272
+ return files;
273
+ }
@@ -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
- }