Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/start-nitro-v2-vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ import type { PluginOption, Rollup } from "vite";

let ssrBundle: Rollup.OutputBundle;
let ssrEntryFile: string;
let viteBase: string = "/";

export type UserNitroConfig = Omit<NitroConfig, "dev" | "publicAssets" | "renderer">;

export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
return [
{
name: "solid-start-vite-plugin-nitro",
configResolved(config) {
// Capture the resolved Vite base so the Nitro build can default its
// baseURL to the same subpath. A user-supplied nitroConfig.baseURL
// still wins; this just removes the need to set the same prefix twice.
viteBase = config.base ?? "/";
},
generateBundle: {
handler(_options, bundle) {
if (this.environment.name !== "ssr") {
Expand Down Expand Up @@ -78,6 +85,7 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
compatibilityDate: "2024-11-19",
logLevel: 3,
preset: "node-server",
baseURL: viteBase,
typescript: {
generateTsConfig: false,
generateRuntimeConfigTypes: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/start/src/server/manifest/dev-client-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from "pathe";
export function getClientDevManifest() {
return {
import(id) {
return import(/* @vite-ignore */ join("/", id));
return import(/* @vite-ignore */ join(import.meta.env.BASE_URL, id));
},
async getAssets(id) {
const assetsPath = `/@manifest/client/${Date.now()}/assets?id=${id}`;
Expand Down
110 changes: 110 additions & 0 deletions packages/start/src/server/manifest/prod-ssr-manifest.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Fixture: a vite manifest that exposes one entry chunk, one dynamic-entry
// route chunk with a stylesheet, and one nested import-only chunk.
const fixtureManifest = {
"src/entry-client.tsx": {
file: "assets/entry-client-abc.js",
isEntry: true,
css: ["assets/entry-client-xyz.css"],
imports: ["_shared.js"],
},
"_shared.js": {
file: "assets/shared-def.js",
css: [],
},
"src/routes/foo.tsx": {
file: "assets/foo-ghi.js",
isDynamicEntry: true,
css: ["assets/foo-jkl.css"],
imports: ["_shared.js"],
},
};

vi.mock("solid-start:client-vite-manifest", () => ({
clientViteManifest: fixtureManifest,
}));

async function loadModule() {
// Top-level reads of import.meta.env need to be captured at import time,
// so isolate the module cache to pick up the current stubs.
vi.resetModules();
return await import("./prod-ssr-manifest.ts");
}

describe("getSsrProdManifest under a deploy base", () => {
beforeEach(() => {
vi.stubEnv("BASE_URL", "/sub/");
vi.stubEnv("START_CLIENT_ENTRY", "./src/entry-client.tsx");
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("path(id) returns the asset URL prefixed with BASE_URL", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();

expect(m.path("src/entry-client.tsx")).toBe("/sub/assets/entry-client-abc.js");
});

it("path(id) normalizes a leading './' the same as the unprefixed form", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();

expect(m.path("./src/entry-client.tsx")).toBe("/sub/assets/entry-client-abc.js");
});

it("getAssets(id) returns asset hrefs prefixed with BASE_URL", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();
const assets = await m.getAssets("src/routes/foo.tsx");
const hrefs = assets.map(a => a.attrs.href as string).sort();

// Every emitted href must sit under the deploy base; none may be
// root-absolute at "/assets/...".
for (const href of hrefs) {
expect(href.startsWith("/sub/")).toBe(true);
}
});

it("json() serializes each entry with a base-prefixed output path", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();
const json = await m.json();

expect(json["src/entry-client.tsx"].output).toBe("/sub/assets/entry-client-abc.js");
expect(json["src/routes/foo.tsx"].output).toBe("/sub/assets/foo-ghi.js");
});
});

describe("getSsrProdManifest with BASE_URL = '/' (no subpath)", () => {
beforeEach(() => {
vi.stubEnv("BASE_URL", "/");
vi.stubEnv("START_CLIENT_ENTRY", "./src/entry-client.tsx");
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("path(id) returns a root-absolute URL (no '//' prefix)", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();

expect(m.path("src/entry-client.tsx")).toBe("/assets/entry-client-abc.js");
});

it("getAssets(id) returns single-slash hrefs", async () => {
const { getSsrProdManifest } = await loadModule();
const m = getSsrProdManifest();
const assets = await m.getAssets("src/routes/foo.tsx");

for (const a of assets) {
const href = a.attrs.href as string;
expect(href.startsWith("//")).toBe(false);
expect(href.startsWith("/")).toBe(true);
}
});
});
6 changes: 3 additions & 3 deletions packages/start/src/server/manifest/prod-ssr-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function getSsrProdManifest() {
const viteManifestEntry = clientViteManifest[id /*import.meta.env.START_CLIENT_ENTRY*/];
if (!viteManifestEntry) throw new Error(`No entry found in vite manifest for '${id}'`);

return join("/", viteManifestEntry.file);
return join(import.meta.env.BASE_URL, viteManifestEntry.file);
},
async getAssets(id) {
if (id.startsWith("./")) id = id.slice(2);
Expand All @@ -29,7 +29,7 @@ export function getSsrProdManifest() {

for (const entryKey of entryKeys) {
json[entryKey] = {
output: join("/", viteManifest[entryKey]!.file),
output: join(import.meta.env.BASE_URL, viteManifest[entryKey]!.file),
assets: await this.getAssets(entryKey),
};
}
Expand All @@ -54,7 +54,7 @@ function createHtmlTagsForAssets(assets: string[]) {
.map<Asset>(asset => ({
tag: "link",
attrs: {
href: "/" + asset,
href: join(import.meta.env.BASE_URL, asset),
key: asset,
...(asset.endsWith(".css") ? { rel: "stylesheet" } : { rel: "modulepreload" }),
},
Expand Down
Loading