@aklinker1/aframe 0.1.0 → 0.2.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 +26 -3
- package/bin/aframe.ts +6 -5
- package/package.json +7 -6
- package/src/client/app.ts +6 -0
- package/src/{server.ts → client/server.ts} +15 -4
- package/src/config.ts +150 -0
- package/src/{mod.ts → index.ts} +13 -155
package/README.md
CHANGED
|
@@ -8,14 +8,13 @@ Simple wrapper around Vite for creating pre-rendered, client-side web apps with
|
|
|
8
8
|
```html
|
|
9
9
|
📂 {rootDir}/
|
|
10
10
|
📁 app/
|
|
11
|
-
📄 .env
|
|
12
11
|
📄 index.html
|
|
13
12
|
📄 main.ts
|
|
14
13
|
📁 public/
|
|
15
14
|
📄 favicon.ico
|
|
16
15
|
📁 server/
|
|
17
|
-
📄 .env
|
|
18
16
|
📄 main.ts
|
|
17
|
+
📄 .env
|
|
19
18
|
📄 aframe.config.ts
|
|
20
19
|
```
|
|
21
20
|
|
|
@@ -72,7 +71,7 @@ export default app;
|
|
|
72
71
|
"scripts": {
|
|
73
72
|
"dev": "aframe",
|
|
74
73
|
"build": "aframe build",
|
|
75
|
-
"preview": "bun --cwd .output server-entry.js"
|
|
74
|
+
"preview": "bun --cwd .output --env-file ../.env server-entry.js"
|
|
76
75
|
},
|
|
77
76
|
"dependencies": {
|
|
78
77
|
"@aklinker1/aframe": "@latest",
|
|
@@ -83,3 +82,27 @@ export default app;
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
```
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
### Environment
|
|
89
|
+
|
|
90
|
+
Create a single `.env` file in your project root. `app` secrets must be prefixed with `APP_`, whereas `server` secrets don't need any prefix:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# {rootDir}/.env
|
|
94
|
+
APP_DEFAULT_MODEL=gemini-2.0-flash
|
|
95
|
+
OPENAI_API_KEY=...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
In this case, you could use `import.meta.env.VITE_DEFAULT_MODEL` in your `app` code and `process.env.OPENAI_API_KEY` in your server code.
|
|
99
|
+
|
|
100
|
+
### Import files as text
|
|
101
|
+
|
|
102
|
+
When importing a file as text, like an HTML template or a `.gql` schema, you should use `with { type: "text"
|
|
103
|
+
}`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// server/main.ts
|
|
107
|
+
import welcomeEmailTemplate from "./assets/email-templates/welcome.html" with { type: "text" };
|
|
108
|
+
```
|
package/bin/aframe.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { build, createServer
|
|
1
|
+
import { build, createServer } from "../src";
|
|
2
|
+
import { resolveConfig } from "../src/config";
|
|
2
3
|
import { RESET, BOLD, DIM, UNDERLINE, GREEN, CYAN } from "../src/color";
|
|
3
4
|
import { createTimer } from "../src/timer";
|
|
4
5
|
|
|
5
|
-
const [_bun, _aframe, ...args] =
|
|
6
|
+
const [_bun, _aframe, ...args] = process.argv;
|
|
6
7
|
|
|
7
8
|
async function dev(root?: string) {
|
|
8
9
|
const devServerTimer = createTimer();
|
|
@@ -26,8 +27,8 @@ async function dev(root?: string) {
|
|
|
26
27
|
"--eval",
|
|
27
28
|
js,
|
|
28
29
|
],
|
|
29
|
-
|
|
30
|
-
cwd: config.
|
|
30
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
31
|
+
cwd: config.rootDir,
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
|
|
@@ -44,7 +45,7 @@ async function dev(root?: string) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
async function help() {
|
|
47
|
-
console.log("Help");
|
|
48
|
+
console.log("Help: TODO");
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
if (args[0] === "build") {
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aklinker1/aframe",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"packageManager": "bun@1.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./src/
|
|
9
|
-
"./server": "./src/server.ts",
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./server": "./src/client/server.ts",
|
|
10
|
+
"./app": "./src/client/app.ts",
|
|
10
11
|
"./env": "./src/env.d.ts"
|
|
11
12
|
},
|
|
12
13
|
"bin": {
|
|
@@ -18,9 +19,9 @@
|
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"check": "check",
|
|
21
|
-
"dev": "bun bin/aframe.ts demo",
|
|
22
|
-
"build": "bun bin/aframe.ts build demo",
|
|
23
|
-
"preview": "bun --cwd demo/.output server-entry.js"
|
|
22
|
+
"dev": "bun node_modules/@aklinker1/aframe/bin/aframe.ts demo",
|
|
23
|
+
"build": "bun node_modules/@aklinker1/aframe/bin/aframe.ts build demo",
|
|
24
|
+
"preview": "bun --cwd demo/.output --env-file ../.env server-entry.js"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {},
|
|
26
27
|
"devDependencies": {
|
|
@@ -22,9 +22,19 @@ export async function fetchStatic(request: Request): Promise<Response> {
|
|
|
22
22
|
|
|
23
23
|
function fetchRootHtml() {
|
|
24
24
|
if (import.meta.command === "serve") {
|
|
25
|
-
return Response
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
return new Response(
|
|
26
|
+
`<html>
|
|
27
|
+
<body>
|
|
28
|
+
This is a placeholder for your root <code>index.html</code> file during development.
|
|
29
|
+
<br/>
|
|
30
|
+
In production (or via the app's dev server), this path will fallback on the root <code>index.html</code>.
|
|
31
|
+
</body>
|
|
32
|
+
</html>`,
|
|
33
|
+
{
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": "text/html",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
28
38
|
);
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -33,7 +43,8 @@ function fetchRootHtml() {
|
|
|
33
43
|
|
|
34
44
|
async function isFile(file: BunFile): Promise<boolean> {
|
|
35
45
|
try {
|
|
36
|
-
|
|
46
|
+
const stats = await file.stat();
|
|
47
|
+
return stats.isFile();
|
|
37
48
|
} catch {
|
|
38
49
|
return false;
|
|
39
50
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as vite from "vite";
|
|
2
|
+
import type { PrerendererOptions } from "@prerenderer/prerenderer";
|
|
3
|
+
import { resolve, join, relative } from "node:path/posix";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
export type UserConfig = {
|
|
7
|
+
vite?: vite.UserConfigExport;
|
|
8
|
+
prerenderedRoutes?: string[];
|
|
9
|
+
prerenderer?: PrerendererOptions;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ResolvedConfig = {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
appDir: string;
|
|
15
|
+
publicDir: string;
|
|
16
|
+
serverDir: string;
|
|
17
|
+
serverModule: string;
|
|
18
|
+
serverEntry: string;
|
|
19
|
+
prerenderToDir: string;
|
|
20
|
+
outDir: string;
|
|
21
|
+
serverOutDir: string;
|
|
22
|
+
appOutDir: string;
|
|
23
|
+
appPort: number;
|
|
24
|
+
serverPort: number;
|
|
25
|
+
vite: vite.InlineConfig;
|
|
26
|
+
prerenderedRoutes: string[];
|
|
27
|
+
prerenderer: () => Promise<PrerendererOptions>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function defineConfig(config: UserConfig): UserConfig {
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function resolveConfig(
|
|
35
|
+
root: string | undefined,
|
|
36
|
+
command: "build" | "serve",
|
|
37
|
+
mode: string,
|
|
38
|
+
): Promise<ResolvedConfig> {
|
|
39
|
+
const rootDir = root ? resolve(root) : process.cwd();
|
|
40
|
+
const appDir = join(rootDir, "app");
|
|
41
|
+
const serverDir = join(rootDir, "server");
|
|
42
|
+
const serverModule = join(serverDir, "main.ts");
|
|
43
|
+
const serverEntry = join(import.meta.dir, "server-entry.ts");
|
|
44
|
+
const publicDir = join(rootDir, "public");
|
|
45
|
+
const outDir = join(rootDir, ".output");
|
|
46
|
+
const appOutDir = join(outDir, "public");
|
|
47
|
+
const serverOutDir = outDir;
|
|
48
|
+
const prerenderToDir = appOutDir;
|
|
49
|
+
const appPort = 3000;
|
|
50
|
+
const serverPort = 3001;
|
|
51
|
+
|
|
52
|
+
// Ensure required directories exist
|
|
53
|
+
await mkdir(prerenderToDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const configFile = join(rootDir, "aframe.config"); // No file extension to resolve any JS/TS file
|
|
56
|
+
const relativeConfigFile = "./" + relative(import.meta.dir, configFile);
|
|
57
|
+
const { default: userConfig }: { default: UserConfig } = await import(
|
|
58
|
+
relativeConfigFile
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let viteConfig = await vite.defineConfig((await userConfig.vite) ?? {});
|
|
62
|
+
if (typeof viteConfig === "function") {
|
|
63
|
+
viteConfig = await viteConfig({ command, mode });
|
|
64
|
+
}
|
|
65
|
+
// Apply opinionated config that can be overwritten
|
|
66
|
+
|
|
67
|
+
viteConfig = vite.mergeConfig<vite.InlineConfig, vite.InlineConfig>(
|
|
68
|
+
// Defaults
|
|
69
|
+
{
|
|
70
|
+
envPrefix: "APP_",
|
|
71
|
+
build: {
|
|
72
|
+
emptyOutDir: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
// Overrides
|
|
76
|
+
viteConfig,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Override required config
|
|
80
|
+
viteConfig = vite.mergeConfig<vite.InlineConfig, vite.InlineConfig>(
|
|
81
|
+
// Defaults
|
|
82
|
+
viteConfig,
|
|
83
|
+
// Overrides
|
|
84
|
+
{
|
|
85
|
+
logLevel: "warn",
|
|
86
|
+
configFile: false,
|
|
87
|
+
root: appDir,
|
|
88
|
+
publicDir,
|
|
89
|
+
envDir: rootDir,
|
|
90
|
+
build: {
|
|
91
|
+
outDir: appOutDir,
|
|
92
|
+
},
|
|
93
|
+
server: {
|
|
94
|
+
port: appPort,
|
|
95
|
+
strictPort: true,
|
|
96
|
+
proxy: {
|
|
97
|
+
"/api": {
|
|
98
|
+
target: `http://localhost:${serverPort}`,
|
|
99
|
+
changeOrigin: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const prerenderer = async (): Promise<PrerendererOptions> => {
|
|
107
|
+
const rendererModule =
|
|
108
|
+
tryResolve("@prerenderer/renderer-puppeteer") ??
|
|
109
|
+
tryResolve("@prerenderer/renderer-jsdom");
|
|
110
|
+
if (!rendererModule)
|
|
111
|
+
throw Error(
|
|
112
|
+
`No renderer installed. Did you forget to install @prerenderer/renderer-puppeteer or @prerenderer/renderer-jsdom?`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { default: Renderer } = await import(rendererModule);
|
|
116
|
+
const renderer = new Renderer();
|
|
117
|
+
return {
|
|
118
|
+
...userConfig.prerenderer,
|
|
119
|
+
renderer,
|
|
120
|
+
staticDir: appOutDir,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
rootDir,
|
|
126
|
+
appDir,
|
|
127
|
+
publicDir,
|
|
128
|
+
serverDir,
|
|
129
|
+
serverModule,
|
|
130
|
+
serverEntry,
|
|
131
|
+
outDir,
|
|
132
|
+
serverOutDir,
|
|
133
|
+
appOutDir,
|
|
134
|
+
prerenderToDir,
|
|
135
|
+
appPort,
|
|
136
|
+
serverPort,
|
|
137
|
+
|
|
138
|
+
prerenderedRoutes: userConfig.prerenderedRoutes ?? ["/"],
|
|
139
|
+
prerenderer,
|
|
140
|
+
vite: viteConfig,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tryResolve(specifier: string): string | undefined {
|
|
145
|
+
try {
|
|
146
|
+
return import.meta.resolve(specifier);
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/{mod.ts → index.ts}
RENAMED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import * as vite from "vite";
|
|
2
1
|
import Prerenderer from "@prerenderer/prerenderer";
|
|
3
|
-
import type { PrerendererOptions } from "@prerenderer/prerenderer";
|
|
4
|
-
import { resolve, join, relative } from "node:path/posix";
|
|
5
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import { lstatSync } from "node:fs";
|
|
7
2
|
import type { BunPlugin } from "bun";
|
|
8
|
-
import {
|
|
3
|
+
import { lstatSync } from "node:fs";
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join, relative } from "node:path/posix";
|
|
6
|
+
import * as vite from "vite";
|
|
7
|
+
import { BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RESET } from "./color";
|
|
8
|
+
import type { ResolvedConfig } from "./config";
|
|
9
9
|
import { createTimer } from "./timer";
|
|
10
10
|
|
|
11
|
+
export * from "./config";
|
|
12
|
+
|
|
11
13
|
export async function createServer(
|
|
12
14
|
config: ResolvedConfig,
|
|
13
15
|
): Promise<vite.ViteDevServer> {
|
|
@@ -63,7 +65,11 @@ export async function build(config: ResolvedConfig) {
|
|
|
63
65
|
const prerenderer = new Prerenderer(await config.prerenderer());
|
|
64
66
|
const prerendered = await prerenderer
|
|
65
67
|
.initialize()
|
|
66
|
-
.then(() =>
|
|
68
|
+
.then(() =>
|
|
69
|
+
prerenderer.renderRoutes(
|
|
70
|
+
config.prerenderedRoutes.map((route) => `${route}?prerender`),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
67
73
|
.then((renderedRoutes) =>
|
|
68
74
|
Promise.all(
|
|
69
75
|
renderedRoutes.map(async (route) => {
|
|
@@ -145,151 +151,3 @@ function prettyBytes(bytes: number) {
|
|
|
145
151
|
const value = bytes / Math.pow(base, exponent);
|
|
146
152
|
return `${unit === "B" ? value : value.toFixed(2)} ${unit}`;
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
///
|
|
150
|
-
/// CONFIG
|
|
151
|
-
///
|
|
152
|
-
|
|
153
|
-
export type UserConfig = {
|
|
154
|
-
vite?: vite.UserConfigExport;
|
|
155
|
-
prerenderedRoutes?: string[];
|
|
156
|
-
prerenderer?: PrerendererOptions;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
export type ResolvedConfig = {
|
|
160
|
-
rootDir: string;
|
|
161
|
-
appDir: string;
|
|
162
|
-
publicDir: string;
|
|
163
|
-
serverDir: string;
|
|
164
|
-
serverModule: string;
|
|
165
|
-
serverEntry: string;
|
|
166
|
-
prerenderToDir: string;
|
|
167
|
-
outDir: string;
|
|
168
|
-
serverOutDir: string;
|
|
169
|
-
appOutDir: string;
|
|
170
|
-
appPort: number;
|
|
171
|
-
serverPort: number;
|
|
172
|
-
vite: vite.InlineConfig;
|
|
173
|
-
prerenderedRoutes: string[];
|
|
174
|
-
prerenderer: () => Promise<PrerendererOptions>;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
export function defineConfig(config: UserConfig): UserConfig {
|
|
178
|
-
return config;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function resolveConfig(
|
|
182
|
-
root: string | undefined,
|
|
183
|
-
command: "build" | "serve",
|
|
184
|
-
mode: string,
|
|
185
|
-
): Promise<ResolvedConfig> {
|
|
186
|
-
const rootDir = root ? resolve(root) : process.cwd();
|
|
187
|
-
const appDir = join(rootDir, "app");
|
|
188
|
-
const serverDir = join(rootDir, "server");
|
|
189
|
-
const serverModule = join(serverDir, "main.ts");
|
|
190
|
-
const serverEntry = join(import.meta.dir, "server-entry.ts");
|
|
191
|
-
const publicDir = join(rootDir, "public");
|
|
192
|
-
const outDir = join(rootDir, ".output");
|
|
193
|
-
const appOutDir = join(outDir, "public");
|
|
194
|
-
const serverOutDir = outDir;
|
|
195
|
-
const prerenderToDir = appOutDir;
|
|
196
|
-
const appPort = 3000;
|
|
197
|
-
const serverPort = 3001;
|
|
198
|
-
|
|
199
|
-
// Ensure required directories exist
|
|
200
|
-
await mkdir(prerenderToDir, { recursive: true });
|
|
201
|
-
|
|
202
|
-
const configFile = join(rootDir, "aframe.config"); // No file extension to resolve any JS/TS file
|
|
203
|
-
const relativeConfigFile = "./" + relative(import.meta.dir, configFile);
|
|
204
|
-
const { default: userConfig }: { default: UserConfig } = await import(
|
|
205
|
-
relativeConfigFile
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
let viteConfig = await vite.defineConfig((await userConfig.vite) ?? {});
|
|
209
|
-
if (typeof viteConfig === "function") {
|
|
210
|
-
viteConfig = await viteConfig({ command, mode });
|
|
211
|
-
}
|
|
212
|
-
// Apply opinionated config that can be overwritten
|
|
213
|
-
|
|
214
|
-
viteConfig = vite.mergeConfig<vite.InlineConfig, vite.InlineConfig>(
|
|
215
|
-
// Defaults
|
|
216
|
-
{
|
|
217
|
-
build: {
|
|
218
|
-
emptyOutDir: true,
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
// Overrides
|
|
222
|
-
viteConfig,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// Override required config
|
|
226
|
-
viteConfig = vite.mergeConfig<vite.InlineConfig, vite.InlineConfig>(
|
|
227
|
-
// Defaults
|
|
228
|
-
viteConfig,
|
|
229
|
-
// Overrides
|
|
230
|
-
{
|
|
231
|
-
logLevel: "warn",
|
|
232
|
-
configFile: false,
|
|
233
|
-
root: appDir,
|
|
234
|
-
publicDir,
|
|
235
|
-
build: {
|
|
236
|
-
outDir: appOutDir,
|
|
237
|
-
},
|
|
238
|
-
server: {
|
|
239
|
-
port: appPort,
|
|
240
|
-
strictPort: true,
|
|
241
|
-
proxy: {
|
|
242
|
-
"/api": {
|
|
243
|
-
target: `http://localhost:${serverPort}`,
|
|
244
|
-
changeOrigin: true,
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const prerenderer = async (): Promise<PrerendererOptions> => {
|
|
252
|
-
const rendererModule =
|
|
253
|
-
tryResolve("@prerenderer/renderer-puppeteer") ??
|
|
254
|
-
tryResolve("@prerenderer/renderer-jsdom");
|
|
255
|
-
if (!rendererModule)
|
|
256
|
-
throw Error(
|
|
257
|
-
`No renderer installed. Did you forget to install @prerenderer/renderer-puppeteer or @prerenderer/renderer-jsdom?`,
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
const { default: Renderer } = await import(rendererModule);
|
|
261
|
-
const renderer = new Renderer();
|
|
262
|
-
return {
|
|
263
|
-
...userConfig.prerenderer,
|
|
264
|
-
renderer,
|
|
265
|
-
staticDir: appOutDir,
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
rootDir,
|
|
271
|
-
appDir,
|
|
272
|
-
publicDir,
|
|
273
|
-
serverDir,
|
|
274
|
-
serverModule,
|
|
275
|
-
serverEntry,
|
|
276
|
-
outDir,
|
|
277
|
-
serverOutDir,
|
|
278
|
-
appOutDir,
|
|
279
|
-
prerenderToDir,
|
|
280
|
-
appPort,
|
|
281
|
-
serverPort,
|
|
282
|
-
|
|
283
|
-
prerenderedRoutes: userConfig.prerenderedRoutes ?? ["/"],
|
|
284
|
-
prerenderer,
|
|
285
|
-
vite: viteConfig,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function tryResolve(specifier: string): string | undefined {
|
|
290
|
-
try {
|
|
291
|
-
return import.meta.resolve(specifier);
|
|
292
|
-
} catch {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
}
|