Make the dist directory configurable
Set it with `dist_dir` in bunext.config.ts or `--dist-dir` on any CLI command (precedence: --dist-dir > BUNEXT_DIST_DIR > dist_dir > .bunext). setupDistDir() resolves it in the CLI's preAction hook and at the start of bunextInit(), so custom servers get the config value too. - Read dist-derived paths from grabDirNames() inside functions instead of at import time; a guard test enforces this. - Derive the client asset URL prefix from the dist dir instead of hard-coding /.bunext. - Make the dev watcher ignore rules and probe follow the dist dir. - Validate the dir: inside the project root, outside src/, public/ and node_modules/, URL-safe characters only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import { describe, it, expect, afterEach, beforeEach } from "bun:test";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
||||
import { DistDirEnvName, resolveDistDir } from "../../utils/grab-dist-dir";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import grabBunextPublicUrlPath from "../../utils/grab-bunext-public-url-path";
|
||||
import setupDistDir from "../../functions/setup-dist-dir";
|
||||
|
||||
const root_dir = "/projects/app";
|
||||
|
||||
describe("resolveDistDir", () => {
|
||||
it("defaults to .bunext", () => {
|
||||
expect(resolveDistDir({ root_dir })).toBe(
|
||||
path.join(root_dir, ".bunext"),
|
||||
);
|
||||
expect(resolveDistDir({ dist_dir: " ", root_dir })).toBe(
|
||||
path.join(root_dir, ".bunext"),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves relative and nested paths against the root", () => {
|
||||
expect(resolveDistDir({ dist_dir: "build", root_dir })).toBe(
|
||||
path.join(root_dir, "build"),
|
||||
);
|
||||
expect(resolveDistDir({ dist_dir: "./out/bunext", root_dir })).toBe(
|
||||
path.join(root_dir, "out", "bunext"),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts absolute paths inside the root", () => {
|
||||
expect(
|
||||
resolveDistDir({ dist_dir: `${root_dir}/build`, root_dir }),
|
||||
).toBe(path.join(root_dir, "build"));
|
||||
});
|
||||
|
||||
it("rejects the root itself and paths outside it", () => {
|
||||
for (const dist_dir of [".", "..", "../other", "/tmp/out", root_dir]) {
|
||||
expect(() => resolveDistDir({ dist_dir, root_dir })).toThrow(
|
||||
/inside the project root/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts names that only start with dots", () => {
|
||||
expect(resolveDistDir({ dist_dir: "..out", root_dir })).toBe(
|
||||
path.join(root_dir, "..out"),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects src, public and node_modules and their subfolders", () => {
|
||||
for (const dist_dir of [
|
||||
"src",
|
||||
"src/out",
|
||||
"public",
|
||||
"public/out",
|
||||
"node_modules/.cache",
|
||||
]) {
|
||||
expect(() => resolveDistDir({ dist_dir, root_dir })).toThrow(
|
||||
/can't be/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects characters that are not URL-safe", () => {
|
||||
for (const dist_dir of ["my out", "out?x", "out#1", "dïst"]) {
|
||||
expect(() => resolveDistDir({ dist_dir, root_dir })).toThrow(
|
||||
/use only/,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupDistDir", () => {
|
||||
const original_env = process.env[DistDirEnvName];
|
||||
const original_cwd = process.cwd();
|
||||
let project_dir: string;
|
||||
|
||||
// A fresh temp dir per test, so the config module cache never collides.
|
||||
function writeConfig(body: string) {
|
||||
project_dir = mkdtempSync(path.join(os.tmpdir(), "bunext-dist-"));
|
||||
writeFileSync(path.join(project_dir, "bunext.config.ts"), body);
|
||||
process.chdir(project_dir);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env[DistDirEnvName];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(original_cwd);
|
||||
if (project_dir) rmSync(project_dir, { recursive: true, force: true });
|
||||
if (original_env === undefined) delete process.env[DistDirEnvName];
|
||||
else process.env[DistDirEnvName] = original_env;
|
||||
});
|
||||
|
||||
it("uses dist_dir from bunext.config.ts", async () => {
|
||||
writeConfig(`export default { dist_dir: "build/out" };\n`);
|
||||
await setupDistDir();
|
||||
|
||||
const dist = path.join(process.cwd(), "build", "out");
|
||||
expect(process.env[DistDirEnvName]).toBe(dist);
|
||||
expect(grabDirNames().BUNX_CWD_DIR).toBe(dist);
|
||||
});
|
||||
|
||||
it("prefers the explicit dist_dir (--dist-dir) over the config", async () => {
|
||||
writeConfig(`export default { dist_dir: "from-config" };\n`);
|
||||
await setupDistDir({ dist_dir: "from-flag" });
|
||||
|
||||
expect(grabDirNames().BUNX_CWD_DIR).toBe(
|
||||
path.join(process.cwd(), "from-flag"),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers an existing BUNEXT_DIST_DIR over the config", async () => {
|
||||
writeConfig(`export default { dist_dir: "from-config" };\n`);
|
||||
process.env[DistDirEnvName] = "from-env";
|
||||
await setupDistDir();
|
||||
|
||||
expect(grabDirNames().BUNX_CWD_DIR).toBe(
|
||||
path.join(process.cwd(), "from-env"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the default without dist_dir", async () => {
|
||||
writeConfig(`export default {};\n`);
|
||||
await setupDistDir();
|
||||
|
||||
expect(process.env[DistDirEnvName]).toBeUndefined();
|
||||
expect(grabDirNames().BUNX_CWD_DIR).toBe(
|
||||
path.join(process.cwd(), ".bunext"),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws on an invalid dist_dir", async () => {
|
||||
writeConfig(`export default { dist_dir: "../outside" };\n`);
|
||||
expect(setupDistDir()).rejects.toThrow(/inside the project root/);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* `bunextInit()` resolves `dist_dir` from the config after the library is
|
||||
* imported (custom servers), so no module may read a dist-dependent path at
|
||||
* import time. Paths derived only from the project root are fine.
|
||||
*/
|
||||
describe("no import-time reads of dist paths", () => {
|
||||
const RootOnlyKeys = new Set([
|
||||
"ROOT_DIR",
|
||||
"SRC_DIR",
|
||||
"PAGES_DIR",
|
||||
"API_DIR",
|
||||
"PUBLIC_DIR",
|
||||
"CONFIG_FILE",
|
||||
"BUNX_ROOT_DIR",
|
||||
"BUNX_ROOT_SRC_DIR",
|
||||
"BUNX_ROOT_PRESETS_DIR",
|
||||
"BUNX_ROOT_500_PRESET_COMPONENT",
|
||||
"BUNX_ROOT_500_FILE_NAME",
|
||||
"BUNX_ROOT_404_PRESET_COMPONENT",
|
||||
"BUNX_ROOT_404_FILE_NAME",
|
||||
]);
|
||||
|
||||
function listSourceFiles(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((name) => {
|
||||
const full = path.join(dir, name);
|
||||
if (name === "__tests__") return [];
|
||||
if (statSync(full).isDirectory()) return listSourceFiles(full);
|
||||
return /\.tsx?$/.test(name) ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
it("only reads root-derived paths at module level", () => {
|
||||
const src_dir = path.resolve(import.meta.dir, "../..");
|
||||
const offenders: string[] = [];
|
||||
const top_level_read =
|
||||
/^(?:export )?(?:const|let|var)\s+(\{[^}]*\}|\w+)\s*=\s*(grabDirNames|grabBunextPublicUrlPath)\(\)/gm;
|
||||
|
||||
for (const file of listSourceFiles(src_dir)) {
|
||||
const source = readFileSync(file, "utf-8");
|
||||
|
||||
for (const match of source.matchAll(top_level_read)) {
|
||||
const [, binding, fn] = match;
|
||||
const keys = binding.startsWith("{")
|
||||
? binding
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map((k) => k.trim().split(":")[0].trim())
|
||||
.filter(Boolean)
|
||||
: [binding];
|
||||
const bad = keys.filter(
|
||||
(k) => fn !== "grabDirNames" || !RootOnlyKeys.has(k),
|
||||
);
|
||||
|
||||
if (bad.length) {
|
||||
offenders.push(
|
||||
`${path.relative(src_dir, file)}: ${bad.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grabDirNames with BUNEXT_DIST_DIR", () => {
|
||||
const original = process.env[DistDirEnvName];
|
||||
|
||||
afterEach(() => {
|
||||
if (original === undefined) delete process.env[DistDirEnvName];
|
||||
else process.env[DistDirEnvName] = original;
|
||||
});
|
||||
|
||||
it("places all artifact paths under the configured directory", () => {
|
||||
process.env[DistDirEnvName] = "build/bunext";
|
||||
const dirs = grabDirNames();
|
||||
const dist = path.join(process.cwd(), "build", "bunext");
|
||||
|
||||
expect(dirs.BUNX_CWD_DIR).toBe(dist);
|
||||
expect(dirs.HYDRATION_DST_DIR).toBe(path.join(dist, "public", "pages"));
|
||||
expect(dirs.BUNDLER_CTX_MAP_JSON.startsWith(dist + path.sep)).toBe(true);
|
||||
expect(dirs.PAGE_LASTMOD_JSON).toBe(path.join(dist, "page-lastmod.json"));
|
||||
expect(grabBunextPublicUrlPath()).toBe("/build/bunext/public");
|
||||
});
|
||||
|
||||
it("serves the default dist directory at /.bunext/public", () => {
|
||||
delete process.env[DistDirEnvName];
|
||||
expect(grabBunextPublicUrlPath()).toBe("/.bunext/public");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
DistDirOptionDescription,
|
||||
DistDirOptionFlags,
|
||||
} from "../dist-dir-option";
|
||||
import { log } from "../../utils/log";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync, writeFileSync } from "fs";
|
||||
@@ -8,19 +12,20 @@ import { v4 } from "uuid";
|
||||
import { updatePageLastmodRecord } from "../../utils/page-lastmod-record";
|
||||
import disposeBundlerContexts from "../../functions/bundler/dispose-bundler-contexts";
|
||||
|
||||
const {
|
||||
HYDRATION_DST_DIR,
|
||||
BUNX_CWD_PAGES_REWRITE_DIR,
|
||||
BUNDLER_CTX_MAP_JSON,
|
||||
SSR_BUNDLER_CTX_MAP_JSON,
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
BUILD_INFO_JSON,
|
||||
} = grabDirNames();
|
||||
|
||||
export default function () {
|
||||
return new Command("build")
|
||||
.description("Build Project")
|
||||
.option(DistDirOptionFlags, DistDirOptionDescription)
|
||||
.action(async () => {
|
||||
const {
|
||||
HYDRATION_DST_DIR,
|
||||
BUNX_CWD_PAGES_REWRITE_DIR,
|
||||
BUNDLER_CTX_MAP_JSON,
|
||||
SSR_BUNDLER_CTX_MAP_JSON,
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
BUILD_INFO_JSON,
|
||||
} = grabDirNames();
|
||||
|
||||
try {
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
|
||||
@@ -5,8 +5,6 @@ import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { rmSync } from "fs";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
// The supervisor only restarts the dev server when this process actually
|
||||
// exits with a code. Registering a listener that merely logs (the previous
|
||||
// behavior) keeps Bun from exiting: the crash is swallowed, no error file is
|
||||
@@ -27,6 +25,9 @@ process.on("unhandledRejection", (reason) => {
|
||||
log.info("Running development server ...");
|
||||
|
||||
try {
|
||||
// The dist directory comes from the parent (`BUNEXT_DIST_DIR`).
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
} catch (error) {}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
DistDirOptionDescription,
|
||||
DistDirOptionFlags,
|
||||
} from "../dist-dir-option";
|
||||
import path from "path";
|
||||
import type { BunSpawnOptions } from "../../types";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
|
||||
const {
|
||||
BUNDLER_CTX_MAP_JSON,
|
||||
SSR_BUNDLER_CTX_MAP_JSON,
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE,
|
||||
} = grabDirNames();
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_RESET_AFTER_MS = 10000;
|
||||
|
||||
export default function () {
|
||||
return new Command("dev")
|
||||
.description("Run development server")
|
||||
.option(DistDirOptionFlags, DistDirOptionDescription)
|
||||
.action(async () => {
|
||||
const {
|
||||
BUNDLER_CTX_MAP_JSON,
|
||||
SSR_BUNDLER_CTX_MAP_JSON,
|
||||
BUNX_BUNDLER_ERROR_EXIT_FILE,
|
||||
} = grabDirNames();
|
||||
|
||||
try {
|
||||
rmSync(SSR_BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
rmSync(BUNDLER_CTX_MAP_JSON, { recursive: true });
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const DistDirOptionFlags = "--dist-dir <path>";
|
||||
export const DistDirOptionDescription =
|
||||
"Build artifact directory, relative to the project root " +
|
||||
"(default: `dist_dir` from bunext.config.ts, else .bunext)";
|
||||
+16
-1
@@ -6,6 +6,7 @@ import dev from "./dev";
|
||||
import build from "./build";
|
||||
import sitemap from "./sitemap";
|
||||
import { log } from "../utils/log";
|
||||
import setupDistDir from "../functions/setup-dist-dir";
|
||||
|
||||
/**
|
||||
* # Describe Program
|
||||
@@ -23,6 +24,20 @@ program.addCommand(start());
|
||||
program.addCommand(build());
|
||||
program.addCommand(sitemap());
|
||||
|
||||
/**
|
||||
* # Resolve the Dist Directory
|
||||
* Before any command runs, from `--dist-dir` or the config. Child server
|
||||
* processes inherit it through `BUNEXT_DIST_DIR`.
|
||||
*/
|
||||
program.hook("preAction", async (_program, action_command) => {
|
||||
try {
|
||||
await setupDistDir({ dist_dir: action_command.opts().distDir });
|
||||
} catch (error: any) {
|
||||
log.error(error?.message || String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* # Handle Unavailable Commands
|
||||
*/
|
||||
@@ -39,4 +54,4 @@ program.on("command:*", () => {
|
||||
/**
|
||||
* # Parse Arguments
|
||||
*/
|
||||
program.parse(Bun.argv);
|
||||
await program.parseAsync(Bun.argv);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
DistDirOptionDescription,
|
||||
DistDirOptionFlags,
|
||||
} from "../dist-dir-option";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
@@ -40,6 +44,7 @@ export default function () {
|
||||
)
|
||||
.option("--origin <url>", "Base URL for sitemap links")
|
||||
.option("--no-robots", "Skip generating robots.txt")
|
||||
.option(DistDirOptionFlags, DistDirOptionDescription)
|
||||
.action(async (options: Options) => {
|
||||
const { ROOT_DIR, PUBLIC_DIR } = grabDirNames();
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
DistDirOptionDescription,
|
||||
DistDirOptionFlags,
|
||||
} from "../dist-dir-option";
|
||||
import path from "path";
|
||||
import type { BunSpawnOptions } from "../../types";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { existsSync, rmSync } from "fs";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_RESET_AFTER_MS = 10000;
|
||||
|
||||
export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.option(DistDirOptionFlags, DistDirOptionDescription)
|
||||
.action(async () => {
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
// A stale error file from a previous session makes the bundler
|
||||
// run in "silent" mode and pages fail with misleading messages.
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
import { log } from "../../utils/log";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
post_build_fn?: (params: {
|
||||
artifacts: BundlerCTXMap[];
|
||||
@@ -58,6 +56,8 @@ function arraysEqual(a: string[], b: string[]): boolean {
|
||||
}
|
||||
|
||||
export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
try {
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindcss from "bun-plugin-tailwind";
|
||||
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
export default async function apiRoutesBundler() {
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
const api_routes = grabAllPages({ api_only: true });
|
||||
const dev = isDevelopment();
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import apiRoutesCTXArtifactTracker from "./plugins/api-routes-ctx-artifact-tracker";
|
||||
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
export default async function apiRoutesContextBundler() {
|
||||
const pages = grabAllPages({ api_only: true });
|
||||
const dev = isDevelopment();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import grabBunextPublicUrlPath from "../../utils/grab-bunext-public-url-path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import path from "path";
|
||||
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||
|
||||
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR } = grabDirNames();
|
||||
|
||||
const VENDOR_ENTRIES: Record<string, string> = {
|
||||
react: `
|
||||
import React from "react";
|
||||
@@ -42,6 +41,8 @@ const VENDOR_ENTRIES: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default async function bunReactModulesBundler() {
|
||||
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||
|
||||
const dev = isDevelopment();
|
||||
|
||||
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||
@@ -72,7 +73,7 @@ export default async function bunReactModulesBundler() {
|
||||
|
||||
rmSync(tmpDir, { force: true, recursive: true });
|
||||
|
||||
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||
const PUBLIC_ROOT = `${grabBunextPublicUrlPath()}/vendor`;
|
||||
|
||||
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||
imports: {
|
||||
|
||||
@@ -16,8 +16,6 @@ import { log } from "../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
import shouldMinifySSR from "../../utils/should-minify-ssr";
|
||||
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
type SSRVirtualEntry = PageFiles & { tsx: string };
|
||||
|
||||
// Current virtual entry contents. The SSR virtual plugin resolves its
|
||||
@@ -53,6 +51,8 @@ async function computeEntryToPage(): Promise<{
|
||||
entryToPage: Map<string, SSRVirtualEntry>;
|
||||
root_file_path: string | undefined;
|
||||
}> {
|
||||
const { BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
const pages = grabAllPages({
|
||||
include_server: true,
|
||||
});
|
||||
@@ -131,6 +131,8 @@ export async function awaitSSRRebuild() {
|
||||
}
|
||||
|
||||
async function doSSRRebuild(params?: RebuildParams) {
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
const force = Boolean(params?.force);
|
||||
const dev = isDevelopment();
|
||||
const config = global.BUNEXT_CONFIG;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
|
||||
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||
import grabBunextPublicUrlPath from "../../../utils/grab-bunext-public-url-path";
|
||||
|
||||
const REACT_MODULES = new Set([
|
||||
"react",
|
||||
@@ -13,8 +12,6 @@ const REACT_MODULES = new Set([
|
||||
"react/jsx-dev-runtime",
|
||||
]);
|
||||
|
||||
const VENDOR_BASE = "/.bunext/public/vendor";
|
||||
|
||||
const REACT_ENTRIES: Record<string, string> = {
|
||||
react: `
|
||||
import React from "react";
|
||||
@@ -57,10 +54,12 @@ const REACT_ENTRIES: Record<string, string> = {
|
||||
// Map bare specifier -> browser path
|
||||
function vendorPath(specifier: string): string {
|
||||
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||
return `${VENDOR_BASE}/${filename}`;
|
||||
return `${grabBunextPublicUrlPath()}/vendor/${filename}`;
|
||||
}
|
||||
|
||||
function vendorOutfile(specifier: string): string {
|
||||
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||
|
||||
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||
return path.join(BUNEXT_VENDOR_DIR, filename);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import cleanupArtifacts from "../../server/cleanup-artifacts";
|
||||
import { pushClientUpdate } from "../../server/server-post-build-fn";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
||||
|
||||
let build_start = 0;
|
||||
|
||||
type Params = {
|
||||
@@ -47,6 +45,8 @@ export default function esbuildCTXArtifactTracker({
|
||||
build_only,
|
||||
refresh_props_on_first_build,
|
||||
}: Params) {
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
||||
|
||||
let force_props_refresh = Boolean(refresh_props_on_first_build);
|
||||
|
||||
const artifactTracker: Plugin = {
|
||||
|
||||
@@ -13,8 +13,6 @@ import { log } from "../../../utils/log";
|
||||
|
||||
let build_start = 0;
|
||||
|
||||
const { BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
entryToPage: Map<
|
||||
string,
|
||||
@@ -25,6 +23,8 @@ type Params = {
|
||||
};
|
||||
|
||||
export default function ssrCTXArtifactTracker({ entryToPage }: Params) {
|
||||
const { BUNX_TMP_DIR } = grabDirNames();
|
||||
|
||||
const artifactTracker: Plugin = {
|
||||
name: "ssr-artifact-tracker",
|
||||
setup(build) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import grabBunextPublicUrlPath from "../../utils/grab-bunext-public-url-path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import path from "path";
|
||||
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||
|
||||
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR, ROOT_DIR } = grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
const VENDOR_ENTRIES: Record<string, string> = {
|
||||
react: `
|
||||
@@ -43,6 +44,8 @@ const VENDOR_ENTRIES: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default async function reactModulesBundler() {
|
||||
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||
|
||||
const dev = isDevelopment();
|
||||
|
||||
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||
@@ -75,7 +78,7 @@ export default async function reactModulesBundler() {
|
||||
|
||||
rmSync(tmpDir, { force: true, recursive: true });
|
||||
|
||||
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||
const PUBLIC_ROOT = `${grabBunextPublicUrlPath()}/vendor`;
|
||||
|
||||
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||
imports: {
|
||||
|
||||
@@ -18,6 +18,7 @@ import grabConstants from "../utils/grab-constants";
|
||||
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
|
||||
import disposeBundlerContexts from "./bundler/dispose-bundler-contexts";
|
||||
import { readFileSync } from "fs";
|
||||
import setupDistDir from "./setup-dist-dir";
|
||||
|
||||
/**
|
||||
* # Declare Global Variables
|
||||
@@ -64,15 +65,21 @@ declare global {
|
||||
var BUNEXT_INITIAL_BUILD_PROMISE: Promise<void> | undefined;
|
||||
}
|
||||
|
||||
const dirNames = grabDirNames();
|
||||
const { PAGES_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } = dirNames;
|
||||
|
||||
type Params = {
|
||||
build_only?: boolean;
|
||||
start?: boolean;
|
||||
};
|
||||
|
||||
export default async function bunextInit(params?: Params) {
|
||||
// First: everything below reads the artifact paths. A no-op under the
|
||||
// CLI, which already resolved it; a custom server gets `dist_dir` from
|
||||
// the config here.
|
||||
await setupDistDir();
|
||||
|
||||
const dirNames = grabDirNames();
|
||||
const { PAGES_DIR, BUNDLER_CTX_MAP_JSON, SSR_BUNDLER_CTX_MAP_JSON } =
|
||||
dirNames;
|
||||
|
||||
const is_dev = isDevelopment();
|
||||
|
||||
if (!is_dev || params?.start) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import grabDirNames from "../utils/grab-dir-names";
|
||||
import grabConstants from "../utils/grab-constants";
|
||||
import { AppData } from "../data/app-data";
|
||||
|
||||
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||
|
||||
export default function cleanupLogsDirs() {
|
||||
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||
|
||||
const logs_dirs = readdirSync(BUNX_LOGS_DIR);
|
||||
const { config } = grabConstants();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AppData } from "../../data/app-data";
|
||||
import { log } from "../../utils/log";
|
||||
import fullRebuild from "./full-rebuild";
|
||||
import grabRouter from "../../utils/grab-router";
|
||||
import grabBunextPublicUrlPath from "../../utils/grab-bunext-public-url-path";
|
||||
import { NoSEORobotsValue } from "../../utils/disable-seo";
|
||||
import generateRobotsTxt from "../../utils/generate-robots-txt";
|
||||
|
||||
@@ -79,7 +80,7 @@ export default async function bunextRequestHandler({
|
||||
response = new Response(generateRobotsTxt({ disallow_all: true }), {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
} else if (url.pathname.startsWith("/.bunext")) {
|
||||
} else if (url.pathname.startsWith(`${grabBunextPublicUrlPath()}/`)) {
|
||||
response = await handleBunextPublicAssets({ req });
|
||||
} else if (url.pathname.startsWith("/api/")) {
|
||||
response = await handleRoutes({ req });
|
||||
|
||||
@@ -7,8 +7,7 @@ type Params = {
|
||||
paradigm: "hydration" | "ssr";
|
||||
};
|
||||
|
||||
const { ROOT_DIR, HYDRATION_DST_DIR, BUNX_CWD_MODULE_CACHE_DIR } =
|
||||
grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
// esbuild's content hashes are 8-char base64-ish (e.g. `CMSI7VSQ`). Entry
|
||||
// outputs are `<dir>/<hash>.js` / `<dir>/<hash>.css`. Shared chunks are
|
||||
@@ -27,6 +26,8 @@ const ENTRY_HASH_FILE = /^[A-Za-z0-9]{8}\.(js|css)$/;
|
||||
* left untouched.
|
||||
*/
|
||||
export default async function cleanupArtifacts({ paradigm }: Params) {
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
const is_hydration = paradigm === "hydration";
|
||||
const root_dir = is_hydration
|
||||
? HYDRATION_DST_DIR
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import grabBunextPublicUrlPath from "../../utils/grab-bunext-public-url-path";
|
||||
import path from "path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { readFileResponse } from "./handle-public";
|
||||
import isSafePath from "../../utils/is-safe-path";
|
||||
|
||||
const { BUNEXT_PUBLIC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
};
|
||||
|
||||
export default async function ({ req }: Params): Promise<Response> {
|
||||
const { BUNEXT_PUBLIC_DIR } = grabDirNames();
|
||||
const BUNEXT_PUBLIC_URL_PATH = grabBunextPublicUrlPath();
|
||||
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
const url = new URL(req.url);
|
||||
|
||||
const file_path = path.join(
|
||||
BUNEXT_PUBLIC_DIR,
|
||||
decodeURIComponent(url.pathname).replace(
|
||||
/\/\.bunext\/public\//,
|
||||
"",
|
||||
decodeURIComponent(url.pathname).slice(
|
||||
BUNEXT_PUBLIC_URL_PATH.length,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,13 +13,18 @@ import serverPostBuildFn from "./server-post-build-fn";
|
||||
import { log } from "../../utils/log";
|
||||
import { logBuildErrorsOnce } from "../../utils/build-error-log";
|
||||
|
||||
const { ROOT_DIR, BUNX_CWD_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } =
|
||||
grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
// The watchdog writes this file and expects the watcher to report it. It
|
||||
// sits under `.bunext` (otherwise ignored), so only this path is watched.
|
||||
const WATCH_PROBE_DIR = path.join(BUNX_CWD_DIR, "watch-probe");
|
||||
const WATCH_PROBE_FILE = path.join(WATCH_PROBE_DIR, "probe");
|
||||
// sits under the dist directory (otherwise ignored), so only this path is
|
||||
// watched.
|
||||
function grabWatchPaths() {
|
||||
const { BUNX_CWD_DIR } = grabDirNames();
|
||||
const WATCH_PROBE_DIR = path.join(BUNX_CWD_DIR, "watch-probe");
|
||||
const WATCH_PROBE_FILE = path.join(WATCH_PROBE_DIR, "probe");
|
||||
|
||||
return { BUNX_CWD_DIR, WATCH_PROBE_DIR, WATCH_PROBE_FILE };
|
||||
}
|
||||
|
||||
// Dedupes near-simultaneous restarts (fullRebuild, watchdog).
|
||||
let last_watcher_restart = 0;
|
||||
@@ -70,6 +75,8 @@ function grabPagesSnapshot(): string | undefined {
|
||||
* project tree.
|
||||
*/
|
||||
export function watcherWatchdogTick() {
|
||||
const { WATCH_PROBE_DIR, WATCH_PROBE_FILE } = grabWatchPaths();
|
||||
|
||||
if (!global.BUNEXT_PAGES_SRC_WATCHER || !watcher_ready) return;
|
||||
|
||||
if (probe_pending) {
|
||||
@@ -85,11 +92,34 @@ export function watcherWatchdogTick() {
|
||||
}
|
||||
|
||||
function isWatchProbePath(p: string) {
|
||||
const { WATCH_PROBE_DIR, WATCH_PROBE_FILE } = grabWatchPaths();
|
||||
|
||||
return (
|
||||
p === WATCH_PROBE_FILE || p === WATCH_PROBE_DIR || p === BUNX_CWD_DIR
|
||||
p === WATCH_PROBE_FILE ||
|
||||
p === WATCH_PROBE_DIR ||
|
||||
isDistDirOrAncestor(p)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The dist directory and the folders between it and the project root must
|
||||
* stay watched for the watcher to reach the probe, even when an ignore rule
|
||||
* matches them (dot folders, `dist`, a custom `dist_dir`).
|
||||
*/
|
||||
function isDistDirOrAncestor(p: string) {
|
||||
const { BUNX_CWD_DIR } = grabDirNames();
|
||||
|
||||
if (p === ROOT_DIR) return false;
|
||||
|
||||
return p === BUNX_CWD_DIR || BUNX_CWD_DIR.startsWith(p + path.sep);
|
||||
}
|
||||
|
||||
function isInDistDir(p: string) {
|
||||
const { BUNX_CWD_DIR } = grabDirNames();
|
||||
|
||||
return p === BUNX_CWD_DIR || p.startsWith(BUNX_CWD_DIR + path.sep);
|
||||
}
|
||||
|
||||
const TARGET_FILES_MATCH = /\.(tsx?|jsx?|css)$/;
|
||||
|
||||
// Rebuild coalescing: editors emit bursts of events per save (change +
|
||||
@@ -219,15 +249,18 @@ function isPathExcluded(filename: string, full_file_path: string): boolean {
|
||||
// Heavy directories are filtered at the watcher level so inotify doesn't
|
||||
// install watches (and deliver events) for trees the bundler never reads.
|
||||
function buildIgnoredPatterns() {
|
||||
const ignored: Array<RegExp | ((p: string) => boolean)> = [
|
||||
(p: string) => !isWatchProbePath(p) && /(^|[\/\\])\../.test(p),
|
||||
const builtin_patterns: RegExp[] = [
|
||||
/(^|[\/\\])\../,
|
||||
/(^|[\/\\])node_modules([\/\\]|$)/,
|
||||
/(^|[\/\\])public([\/\\]|$)/,
|
||||
(p: string) =>
|
||||
!isWatchProbePath(p) && /(^|[\/\\])\.bunext([\/\\]|$)/.test(p),
|
||||
/(^|[\/\\])\.git([\/\\]|$)/,
|
||||
/(^|[\/\\])dist([\/\\]|$)/,
|
||||
/bun\.lockb$/,
|
||||
];
|
||||
|
||||
const ignored: Array<(p: string) => boolean> = [
|
||||
(p: string) =>
|
||||
!isWatchProbePath(p) &&
|
||||
(isInDistDir(p) || builtin_patterns.some((re) => re.test(p))),
|
||||
(p: string) => p.endsWith(AppData["BunextTmpFileExt"]),
|
||||
];
|
||||
|
||||
@@ -252,6 +285,8 @@ function buildIgnoredPatterns() {
|
||||
export default async function watcherEsbuildCTX(params?: {
|
||||
pages_snapshot?: string;
|
||||
}) {
|
||||
const { WATCH_PROBE_DIR } = grabWatchPaths();
|
||||
|
||||
try {
|
||||
mkdirSync(WATCH_PROBE_DIR, { recursive: true });
|
||||
} catch (error) {}
|
||||
@@ -292,6 +327,9 @@ function handleEvent(
|
||||
event: "add" | "change" | "unlink" | "addDir" | "unlinkDir",
|
||||
filePath: string,
|
||||
) {
|
||||
const { WATCH_PROBE_FILE } = grabWatchPaths();
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
if (isWatchProbePath(filePath)) {
|
||||
if (filePath === WATCH_PROBE_FILE) {
|
||||
probe_pending = false;
|
||||
|
||||
@@ -14,7 +14,7 @@ import grabRelativePath from "../../../utils/grab-relative-path";
|
||||
import { logBuildErrorsOnce } from "../../../utils/build-error-log";
|
||||
import { RequestBodyTooLargeError } from "../../../utils/read-request-body";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, ROOT_DIR } = grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
class NotFoundError extends Error {
|
||||
status = 404;
|
||||
@@ -44,6 +44,8 @@ type Params = {
|
||||
export default async function grabPageComponent(
|
||||
params: Params,
|
||||
): Promise<GrabPageComponentRes | Response> {
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
const {
|
||||
req,
|
||||
file_path: passed_file_path,
|
||||
|
||||
@@ -9,11 +9,13 @@ import type {
|
||||
} from "../../../types";
|
||||
import { existsSync, rmSync, unlinkSync } from "fs";
|
||||
|
||||
const { PAGES_DIR, BUNX_CWD_MODULE_CACHE_DIR, ROOT_DIR } = grabDirNames();
|
||||
const { PAGES_DIR, ROOT_DIR } = grabDirNames();
|
||||
|
||||
type Params = GrabTSXModuleSingleParams | GrabTSXModuleBatchParams;
|
||||
|
||||
function toModPath(page_file_path: string) {
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
return path.join(
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
page_file_path.replace(PAGES_DIR, "").replace(/\.(t|j)sx?$/, ".js"),
|
||||
@@ -30,6 +32,8 @@ type BuildEntriesParams = {
|
||||
};
|
||||
|
||||
async function buildEntries({ entries, clean_cache }: BuildEntriesParams) {
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
|
||||
const dev = isDevelopment();
|
||||
|
||||
const toBuild: {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { DistDirEnvName, resolveDistDir } from "../utils/grab-dist-dir";
|
||||
import type { BunextConfig } from "../types";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Takes precedence over the env var and the config (`--dist-dir`)
|
||||
*/
|
||||
dist_dir?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Set the dist directory for this process and its children
|
||||
* Precedence: `params.dist_dir` (the CLI's `--dist-dir`), then an existing
|
||||
* `BUNEXT_DIST_DIR` env var, then `dist_dir` from bunext.config.ts. Stores
|
||||
* the absolute path in `BUNEXT_DIST_DIR`, which `grabDirNames()` reads and
|
||||
* child processes inherit. Runs at the start of every CLI command and of
|
||||
* `bunextInit()`, before anything reads the artifact paths. Throws on an
|
||||
* invalid directory.
|
||||
*/
|
||||
export default async function setupDistDir(params?: Params) {
|
||||
const root_dir = process.cwd();
|
||||
|
||||
const dist_dir =
|
||||
params?.dist_dir ||
|
||||
process.env[DistDirEnvName] ||
|
||||
(await grabConfigDistDir());
|
||||
|
||||
if (!dist_dir) return;
|
||||
|
||||
process.env[DistDirEnvName] = resolveDistDir({ dist_dir, root_dir });
|
||||
}
|
||||
|
||||
async function grabConfigDistDir() {
|
||||
const { CONFIG_FILE } = grabDirNames();
|
||||
|
||||
if (!existsSync(CONFIG_FILE)) return;
|
||||
|
||||
try {
|
||||
const config = (await import(CONFIG_FILE)).default as
|
||||
| BunextConfig
|
||||
| undefined;
|
||||
|
||||
return config?.dist_dir;
|
||||
} catch (error) {
|
||||
// The config is loaded again later, which reports the error.
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ import path from "path";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Only include when the exiting process actually crashed with an error.
|
||||
@@ -30,6 +28,8 @@ export default function writeErrorFile({
|
||||
error,
|
||||
exitCode,
|
||||
}: Params) {
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
|
||||
const has_error = error != null;
|
||||
const abnormal_exit = exitCode != null && exitCode !== 0;
|
||||
|
||||
|
||||
+8
-1
@@ -49,6 +49,12 @@ export type PageModule = {
|
||||
};
|
||||
|
||||
export type BunextConfig = {
|
||||
/**
|
||||
* Build artifact directory, relative to the project root. Defaults to
|
||||
* `.bunext`. Must be inside the project root, outside `src/`, `public/`
|
||||
* and `node_modules/`. Overridden by the `BUNEXT_DIST_DIR` env var and
|
||||
* by `--dist-dir` on the CLI.
|
||||
*/
|
||||
dist_dir?: string;
|
||||
assets_prefix?: string;
|
||||
origin?: string;
|
||||
@@ -553,7 +559,8 @@ export type BunextGenerateSitemapParams = {
|
||||
default_lastmod?: Date | string;
|
||||
/**
|
||||
* Page last-modified record written by `bunext build`, relative to the
|
||||
* project root. Defaults to `.bunext/page-lastmod.json`. Point it at a
|
||||
* project root. Defaults to `page-lastmod.json` in the dist directory
|
||||
* (`.bunext/page-lastmod.json`). Point it at a
|
||||
* cached or committed location when builds start from a fresh checkout.
|
||||
*/
|
||||
lastmod_file?: string;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import path from "path";
|
||||
import grabDirNames from "./grab-dir-names";
|
||||
|
||||
/**
|
||||
* # URL path the dist `public` directory is served at
|
||||
* Its path relative to the project root, e.g. `/.bunext/public`. Bundle
|
||||
* paths in the bundler maps are project-relative, so `/${path}` is their
|
||||
* URL. Kept out of `grabDirNames()`: `init` creates every path listed there.
|
||||
*/
|
||||
export default function grabBunextPublicUrlPath() {
|
||||
const { ROOT_DIR, BUNEXT_PUBLIC_DIR } = grabDirNames();
|
||||
|
||||
return `/${path
|
||||
.relative(ROOT_DIR, BUNEXT_PUBLIC_DIR)
|
||||
.split(path.sep)
|
||||
.join("/")}`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path";
|
||||
import { DistDirEnvName, resolveDistDir } from "./grab-dist-dir";
|
||||
|
||||
export type DirNames = {
|
||||
ROOT_DIR: string;
|
||||
@@ -34,9 +35,28 @@ export type DirNames = {
|
||||
PAGE_LASTMOD_JSON: string;
|
||||
};
|
||||
|
||||
let cached: { key: string; dir_names: DirNames } | undefined;
|
||||
|
||||
/**
|
||||
* # Project and artifact paths
|
||||
* The dist directory comes from `BUNEXT_DIST_DIR`, which `setupDistDir()`
|
||||
* sets from `--dist-dir` / `dist_dir` in the config. Call this where the
|
||||
* paths are used, not at module load: a module loaded before
|
||||
* `setupDistDir()` runs would keep the default directory.
|
||||
*/
|
||||
export default function grabDirNames(): DirNames {
|
||||
if (global.BUNEXT_DIR_NAMES) return global.BUNEXT_DIR_NAMES;
|
||||
|
||||
const key = `${process.cwd()}\0${process.env[DistDirEnvName] || ""}`;
|
||||
|
||||
if (cached?.key !== key) {
|
||||
cached = { key, dir_names: computeDirNames() };
|
||||
}
|
||||
|
||||
return cached.dir_names;
|
||||
}
|
||||
|
||||
function computeDirNames(): DirNames {
|
||||
const ROOT_DIR = process.cwd();
|
||||
const SRC_DIR = path.join(ROOT_DIR, "src");
|
||||
const PAGES_DIR = path.join(SRC_DIR, "pages");
|
||||
@@ -44,7 +64,10 @@ export default function grabDirNames(): DirNames {
|
||||
const PUBLIC_DIR = path.join(ROOT_DIR, "public");
|
||||
const CONFIG_FILE = path.join(ROOT_DIR, "bunext.config.ts");
|
||||
|
||||
const BUNX_CWD_DIR = path.resolve(ROOT_DIR, ".bunext");
|
||||
const BUNX_CWD_DIR = resolveDistDir({
|
||||
dist_dir: process.env[DistDirEnvName],
|
||||
root_dir: ROOT_DIR,
|
||||
});
|
||||
const BUNX_CWD_MODULE_CACHE_DIR = path.resolve(
|
||||
BUNX_CWD_DIR,
|
||||
"module-cache",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import path from "path";
|
||||
import AppNames from "./grab-app-names";
|
||||
|
||||
/**
|
||||
* Env var holding the dist directory. The CLI sets it from `--dist-dir` or
|
||||
* `dist_dir` in bunext.config.ts before any other module loads: most
|
||||
* modules read the directory names once, at import time. Child processes
|
||||
* (dev/start servers) inherit it.
|
||||
*/
|
||||
export const DistDirEnvName = "BUNEXT_DIST_DIR";
|
||||
|
||||
const ReservedDirs = ["src", "public", "node_modules"];
|
||||
|
||||
type Params = {
|
||||
dist_dir?: string;
|
||||
root_dir: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Resolve the dist directory
|
||||
* Returns the absolute path of the build artifact directory. `dist_dir` is
|
||||
* relative to the project root (or absolute) and must sit inside it: the
|
||||
* dev watcher only watches the project root, and checks it is alive through
|
||||
* a probe file in the dist directory. Its relative path is also the URL
|
||||
* path of the client bundles, so it is limited to URL-safe characters.
|
||||
*/
|
||||
export function resolveDistDir({ dist_dir, root_dir }: Params): string {
|
||||
const resolved = path.resolve(
|
||||
root_dir,
|
||||
dist_dir?.trim() || AppNames.defaultDistDir,
|
||||
);
|
||||
|
||||
if (!isInsideDir(resolved, root_dir)) {
|
||||
throw new Error(
|
||||
`Invalid dist directory \`${dist_dir}\`: it must be a directory ` +
|
||||
`inside the project root (${root_dir}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const url_path = path
|
||||
.relative(root_dir, resolved)
|
||||
.split(path.sep)
|
||||
.join("/");
|
||||
|
||||
if (!/^[\w.\-\/]+$/.test(url_path)) {
|
||||
throw new Error(
|
||||
`Invalid dist directory \`${dist_dir}\`: use only letters, ` +
|
||||
`digits, \`.\`, \`_\`, \`-\` and \`/\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const reserved of ReservedDirs) {
|
||||
const reserved_dir = path.join(root_dir, reserved);
|
||||
|
||||
if (resolved === reserved_dir || isInsideDir(resolved, reserved_dir)) {
|
||||
throw new Error(
|
||||
`Invalid dist directory \`${dist_dir}\`: it can't be ` +
|
||||
`\`${reserved}/\` or inside it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `target` is strictly inside `dir` (not `dir` itself)
|
||||
*/
|
||||
function isInsideDir(target: string, dir: string) {
|
||||
const relative = path.relative(dir, target);
|
||||
|
||||
return (
|
||||
Boolean(relative) &&
|
||||
relative !== ".." &&
|
||||
!relative.startsWith(`..${path.sep}`) &&
|
||||
!path.isAbsolute(relative)
|
||||
);
|
||||
}
|
||||
@@ -5,13 +5,15 @@ type Params = {
|
||||
page_path: string;
|
||||
};
|
||||
|
||||
const { ROOT_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
|
||||
/**
|
||||
* # Transform a page path to the destination
|
||||
* path in the .bunext directory
|
||||
*/
|
||||
export default function pagePathTransform({ page_path }: Params) {
|
||||
const { BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
|
||||
const page_path_relative_dir = page_path
|
||||
.replace(ROOT_DIR, "")
|
||||
.replace(/\/src\/pages/, "");
|
||||
|
||||
@@ -2,9 +2,9 @@ import { writeFileSync } from "fs";
|
||||
import grabDirNames from "./grab-dir-names";
|
||||
import path from "path";
|
||||
|
||||
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||
|
||||
export default function writeLogs(log: any) {
|
||||
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
let new_log_name = `${now}`;
|
||||
|
||||
Reference in New Issue
Block a user