Skip to content

Commit e842a12

Browse files
authored
feat: static img files (#320)
1 parent 12e8385 commit e842a12

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

packages/aixyz-cli/build/AixyzServerPlugin.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,36 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
33
import { resolve, relative, join } from "path";
44
import { getAixyzConfig } from "@aixyz/config";
55

6+
/**
7+
* Generate code that scans for static assets at startup and builds a `Bun.serve({ static })` map.
8+
* At runtime, `import.meta.dir` resolves to the directory containing the bundled server.js (or compiled binary).
9+
* Static files live alongside it: `icon.png` and `public/` (copied there by the build step).
10+
*/
11+
function generateStaticSetup(): { imports: string; setup: string } {
12+
const imports = [
13+
'import { existsSync as __existsSync, readdirSync as __readdirSync } from "node:fs";',
14+
'import { resolve as __resolve, join as __join } from "node:path";',
15+
].join("\n");
16+
17+
const setup = [
18+
`const __aixyzStatic: Record<string, Response> = {};`,
19+
`const __iconPath = __resolve(import.meta.dir, "icon.png");`,
20+
`if (__existsSync(__iconPath)) __aixyzStatic["/icon.png"] = new Response(Bun.file(__iconPath));`,
21+
`const __publicDir = __resolve(import.meta.dir, "public");`,
22+
`if (__existsSync(__publicDir)) {`,
23+
` (function __walk(dir: string, prefix: string) {`,
24+
` for (const e of __readdirSync(dir, { withFileTypes: true })) {`,
25+
` const p = __join(dir, e.name);`,
26+
` if (e.isFile()) __aixyzStatic[prefix + "/" + e.name] = new Response(Bun.file(p));`,
27+
` else if (e.isDirectory()) __walk(p, prefix + "/" + e.name);`,
28+
` }`,
29+
` })(__publicDir, "");`,
30+
`}`,
31+
].join("\n");
32+
33+
return { imports, setup };
34+
}
35+
636
export function AixyzServerPlugin(
737
entrypoint: string,
838
mode: "vercel" | "standalone" | "executable",
@@ -27,23 +57,26 @@ export function AixyzServerPlugin(
2757
const identifierRe = /export\s+default\s+(\w+)\s*;/;
2858
const expressionRe = /export\s+default\s+/;
2959

60+
const { imports: staticImports, setup: staticSetup } = generateStaticSetup();
61+
const bunServe = (fetchExpr: string) =>
62+
`${staticSetup}\nconst __server = Bun.serve({ port: parseInt(process.env.PORT || "3000", 10), static: __aixyzStatic, fetch: ${fetchExpr} } as Parameters<typeof Bun.serve>[0]);\nconsole.log(\`Server listening on port \${__server.port}\`);`;
63+
3064
let transformed: string;
3165
const identifierMatch = source.match(identifierRe);
3266
if (identifierMatch) {
33-
transformed = source.replace(
34-
identifierRe,
35-
`const __server = Bun.serve({ port: parseInt(process.env.PORT || "3000", 10), fetch: ${identifierMatch[1]}.fetch });\nconsole.log(\`Server listening on port \${__server.port}\`);`,
36-
);
67+
transformed = source.replace(identifierRe, bunServe(`${identifierMatch[1]}.fetch`));
3768
} else if (expressionRe.test(source)) {
3869
transformed = source.replace(expressionRe, `const __app = `);
39-
transformed += `\nconst __server = Bun.serve({ port: parseInt(process.env.PORT || "3000", 10), fetch: __app.fetch });\nconsole.log(\`Server listening on port \${__server.port}\`);`;
70+
transformed += "\n" + bunServe("__app.fetch");
4071
} else {
4172
throw new Error(
4273
`[aixyz] Could not find \`export default\` in entrypoint ${args.path}. ` +
4374
`Standalone and executable builds require the server entrypoint to use \`export default app;\` ` +
4475
`or \`export default new AixyzApp({...});\`.`,
4576
);
4677
}
78+
79+
transformed = staticImports + "\n" + transformed;
4780
return { contents: transformed, loader: "ts" };
4881
});
4982
},

packages/aixyz-cli/dev/worker.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,39 @@
11
import chalk from "chalk";
2+
import { existsSync, readdirSync } from "node:fs";
3+
import { resolve, join } from "node:path";
4+
5+
/** Build a static file map for Bun.serve({ static }) from app/icon.* and public/. */
6+
function buildStaticMap(): Record<string, Response> {
7+
const cwd = process.cwd();
8+
const staticMap: Record<string, Response> = {};
9+
10+
// Serve app/icon.* at /icon.png (raw file — no build-time conversion in dev)
11+
const iconExts = ["png", "svg", "jpeg", "jpg"];
12+
for (const ext of iconExts) {
13+
const iconPath = resolve(cwd, "app", `icon.${ext}`);
14+
if (existsSync(iconPath)) {
15+
staticMap["/icon.png"] = new Response(Bun.file(iconPath));
16+
break;
17+
}
18+
}
19+
20+
// Serve public/ directory contents at root paths (e.g. public/robots.txt → /robots.txt)
21+
const publicDir = resolve(cwd, "public");
22+
if (existsSync(publicDir)) {
23+
(function walk(dir: string, prefix: string) {
24+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
25+
const filePath = join(dir, entry.name);
26+
if (entry.isFile()) {
27+
staticMap[prefix + "/" + entry.name] = new Response(Bun.file(filePath));
28+
} else if (entry.isDirectory()) {
29+
walk(filePath, prefix + "/" + entry.name);
30+
}
31+
}
32+
})(publicDir, "");
33+
}
34+
35+
return staticMap;
36+
}
237

338
async function main() {
439
const entrypoint = process.argv[2];
@@ -31,7 +66,11 @@ async function main() {
3166
process.exit(1);
3267
}
3368

34-
const server = Bun.serve({ port, fetch: app.fetch });
69+
const server = Bun.serve({
70+
port,
71+
static: buildStaticMap(),
72+
fetch: app.fetch,
73+
} as Parameters<typeof Bun.serve>[0]);
3574

3675
const duration = Math.round(performance.now() - startTime);
3776
console.log(chalk.blueBright("✓") + ` Ready in ${duration}ms`);

0 commit comments

Comments
 (0)