Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/build/_extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// prettier-ignore
export const BUILD_EXTENSIONS = [".ts", ".mjs", ".js", ".json", ".node", ".tsx", ".jsx"];
4 changes: 2 additions & 2 deletions src/build/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Nitro, NitroImportMeta } from "nitro/types";
import { pkgDir } from "nitro/meta";
import { pathRegExp, toPathRegExp } from "../utils/regex.ts";
import { BUILD_EXTENSIONS } from "./_extensions.ts";
import { resolveBuildEnv } from "./env.ts";
import { isUnenvId } from "./plugins/unenv.ts";

Expand All @@ -9,8 +10,7 @@ export type BaseBuildConfig = Awaited<ReturnType<typeof baseBuildConfig>>;
const ROOT_ALIAS = "@";

export async function baseBuildConfig(nitro: Nitro) {
// prettier-ignore
const extensions: string[] = [".ts", ".mjs", ".js", ".json", ".node", ".tsx", ".jsx" ];
const extensions: string[] = [...BUILD_EXTENSIONS];

const isNodeless = nitro.options.node === false;

Expand Down
29 changes: 28 additions & 1 deletion src/build/info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Nitro, NitroBuildInfo, WorkerAddress } from "nitro/types";
import { resolveModulePath } from "exsolve";
import { join, relative, resolve } from "pathe";
import { version as nitroVersion } from "nitro/meta";
import { presetsWithConfig } from "../presets/_types.gen.ts";
Expand All @@ -7,6 +8,7 @@ import { mkdir, readFile, stat } from "node:fs/promises";
import { dirname } from "node:path";
import type { RolldownOutput } from "rolldown";
import type { RollupOutput } from "rollup";
import { BUILD_EXTENSIONS } from "./_extensions.ts";

const NITRO_WELLKNOWN_DIR = "node_modules/.nitro";

Expand Down Expand Up @@ -59,7 +61,7 @@ export async function writeBuildInfo(
nitro: Nitro,
output: RolldownOutput | RollupOutput | undefined
): Promise<NitroBuildInfo> {
const serverEntryName = output?.output?.find((o) => o.type === "chunk" && o.isEntry)?.fileName;
const serverEntryName = resolveNitroServerEntry(nitro, output);

const buildInfoPath = resolve(nitro.options.output.dir, "nitro.json");
const buildInfo: NitroBuildInfo = {
Expand Down Expand Up @@ -117,3 +119,28 @@ export async function writeDevBuildInfo(nitro: Nitro, addr?: WorkerAddress): Pro
};
await writeFile(buildInfoPath, JSON.stringify(buildInfo, null, 2));
}

function resolveNitroServerEntry(
nitro: Nitro,
output: RolldownOutput | RollupOutput | undefined
): string | undefined {
const entries = output?.output.filter((item) => item.type === "chunk" && item.isEntry) ?? [];
if (entries.length < 2) {
return entries[0]?.fileName;
}
const nitroEntry = resolve(
(!nitro.options.entry.startsWith("#") &&
resolveModulePath(nitro.options.entry, { try: true, extensions: BUILD_EXTENSIONS })) ||
nitro.options.entry
);
const match = entries.find(
(item) =>
item.type === "chunk" && !!item.facadeModuleId && resolve(item.facadeModuleId) === nitroEntry
);
if (!match) {
nitro.logger.warn(
`Could not find the output chunk for Nitro entry \`${nitro.options.entry}\`. Using \`${entries[0].fileName}\` as server entry.`
);
}
return (match || entries[0]).fileName;
}
118 changes: 118 additions & 0 deletions test/unit/build-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "pathe";
import { rolldown } from "rolldown";
import type { RollupOutput } from "rollup";
import type { Nitro } from "nitro/types";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeBuildInfo } from "../../src/build/info.ts";

describe("writeBuildInfo", () => {
let rootDir: string;
let warn: ReturnType<typeof vi.fn>;

beforeEach(async () => {
rootDir = await realpath(await mkdtemp(join(tmpdir(), "nitro-build-info-")));
warn = vi.fn();
await writeFile(join(rootDir, "server.ts"), `export const server = "nitro server";\n`);
await writeFile(join(rootDir, "server.js"), `export const server = "unrelated entry";\n`);
await writeFile(join(rootDir, "exposed.ts"), `export const exposed = "exposed";\n`);
});

afterEach(async () => {
await rm(rootDir, { recursive: true, force: true });
});

function createNitro() {
return {
logger: { warn },
options: {
rootDir,
entry: join(rootDir, "server"),
output: {
dir: join(rootDir, ".output"),
serverDir: join(rootDir, ".output/server"),
publicDir: join(rootDir, ".output/public"),
},
commands: {},
},
} as unknown as Nitro;
}

async function buildInfoFor(entries: { fileName: string; facadeModuleId: string | null }[]) {
const output = {
output: entries.map((entry) => ({ type: "chunk", isEntry: true, ...entry })),
} as unknown as RollupOutput;

const info = await writeBuildInfo(createNitro(), output);
const saved = JSON.parse(await readFile(join(rootDir, ".output/nitro.json"), "utf8"));
expect(saved.serverEntry).toBe(info.serverEntry);
return info;
}

it("records Nitro's entry when another entry chunk appears first", async () => {
const info = await buildInfoFor([
{ fileName: "_chunks/app.mjs", facadeModuleId: join(rootDir, "app/app.ts") },
{ fileName: "index.mjs", facadeModuleId: join(rootDir, "server.ts") },
]);
expect(info.serverEntry).toBe("server/index.mjs");
expect(warn).not.toHaveBeenCalled();
});

it("uses Nitro's entry even when the output name is customized", async () => {
const info = await buildInfoFor([
{ fileName: "_chunks/app.mjs", facadeModuleId: join(rootDir, "app/app.ts") },
{ fileName: "worker.mjs", facadeModuleId: join(rootDir, "server.ts") },
]);
expect(info.serverEntry).toBe("server/worker.mjs");
});

it("falls back to the first entry with a warning when facade IDs are unavailable", async () => {
const info = await buildInfoFor([
{ fileName: "worker.mjs", facadeModuleId: null },
{ fileName: "index.mjs", facadeModuleId: null },
]);
expect(info.serverEntry).toBe("server/worker.mjs");
expect(warn).toHaveBeenCalledOnce();
});

it("selects Nitro's entry from real Rolldown output with another entry first", async () => {
const build = await rolldown({
input: [join(rootDir, "exposed.ts"), join(rootDir, "server.ts")],
});
const output = await build.write({
dir: join(rootDir, ".output/server"),
entryFileNames: "[name].mjs",
});
await build.close();

const entries = output.output.filter((item) => item.type === "chunk" && item.isEntry);
expect(entries.map((item) => item.fileName)).toEqual(["exposed.mjs", "server.mjs"]);

const info = await writeBuildInfo(createNitro(), output);
expect(info.serverEntry).toBe("server/server.mjs");
});

it("distinguishes entry files with the same path stem", async () => {
const build = await rolldown({
input: { a: join(rootDir, "server.js"), b: join(rootDir, "server") },
resolve: { extensions: [".ts", ".js"] },
});
const output = await build.write({
dir: join(rootDir, ".output/server"),
entryFileNames: "[name].mjs",
});
await build.close();

const entries = output.output.flatMap((item) =>
item.type === "chunk" && item.isEntry ? [item] : []
);
expect(entries.map((item) => resolve(item.facadeModuleId!))).toEqual([
join(rootDir, "server.js"),
join(rootDir, "server.ts"),
]);

const info = await writeBuildInfo(createNitro(), output);
expect(info.serverEntry).toBe("server/b.mjs");
});
});
Loading