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
48 changes: 0 additions & 48 deletions packages/docs/public/sitemap.xml

This file was deleted.

43 changes: 43 additions & 0 deletions packages/docs/src/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { writeFile } from "node:fs/promises";
import path from "node:path";
import type { BuildEntryFunction } from "@funstack/static/server";
import type { RouteDefinition } from "@funstack/router/server";
import { routes } from "./App";

const siteUrl = "https://static.funstack.work";

function collectPaths(routes: RouteDefinition[]): string[] {
const paths: string[] = [];
for (const route of routes) {
if (route.children) {
paths.push(...collectPaths(route.children));
} else if (route.path !== undefined && route.path !== "*") {
paths.push(route.path);
}
}
return paths;
}

function generateSitemap(paths: string[]): string {
const urls = paths
.map((p) => {
const loc = p === "/" ? siteUrl + "/" : `${siteUrl}${p}`;
return ` <url>\n <loc>${loc}</loc>\n </url>`;
})
.join("\n");

return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>
`;
}

export default (async ({ build, outDir }) => {
const paths = collectPaths(routes);

await Promise.all([
build(),
writeFile(path.join(outDir, "sitemap.xml"), generateSitemap(paths)),
]);
}) satisfies BuildEntryFunction;
1 change: 1 addition & 0 deletions packages/docs/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineConfig(async () => {
funstackStatic({
entries: "./src/entries.tsx",
ssr: true,
build: "./src/build.ts",
}),
{
// to make .mdx loading lazy
Expand Down
92 changes: 50 additions & 42 deletions packages/static/src/build/buildApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,55 +25,63 @@ export async function buildApp(
const baseDir = config.environments.client.build.outDir;
const base = normalizeBase(config.base);

const { entries, deferRegistry } = await entry.build();

// Validate all entry paths
const paths: string[] = [];
for (const result of entries) {
const error = validateEntryPath(result.path);
if (error) {
throw new Error(error);
async function doBuild() {
const { entries, deferRegistry } = await entry.build();

// Validate all entry paths
const paths: string[] = [];
for (const result of entries) {
const error = validateEntryPath(result.path);
if (error) {
throw new Error(error);
}
paths.push(result.path);
}
const dupError = checkDuplicatePaths(paths);
if (dupError) {
throw new Error(dupError);
}
paths.push(result.path);
}
const dupError = checkDuplicatePaths(paths);
if (dupError) {
throw new Error(dupError);
}

// Process all deferred components once across all entries.
// We pass a dummy empty stream since we handle per-entry RSC payloads separately.
const dummyStream = new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
});
const { components, idMapping } = await processRscComponents(
deferRegistry.loadAll(),
dummyStream,
options.rscPayloadDir,
context,
);

// Write each entry's HTML and RSC payload
for (const result of entries) {
await buildSingleEntry(
result,
idMapping,
baseDir,
base,
// Process all deferred components once across all entries.
// We pass a dummy empty stream since we handle per-entry RSC payloads separately.
const dummyStream = new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
});
const { components, idMapping } = await processRscComponents(
deferRegistry.loadAll(),
dummyStream,
options.rscPayloadDir,
context,
);

// Write each entry's HTML and RSC payload
for (const result of entries) {
await buildSingleEntry(
result,
idMapping,
baseDir,
base,
options.rscPayloadDir,
context,
);
}

// Write all deferred component payloads
for (const { finalId, finalContent, name } of components) {
const filePath = path.join(
baseDir,
getModulePathFor(finalId).replace(/^\//, ""),
);
await writeFileNormal(filePath, finalContent, context, name);
}
}

// Write all deferred component payloads
for (const { finalId, finalContent, name } of components) {
const filePath = path.join(
baseDir,
getModulePathFor(finalId).replace(/^\//, ""),
);
await writeFileNormal(filePath, finalContent, context, name);
if (entry.buildEntry) {
await entry.buildEntry({ build: doBuild, outDir: baseDir });
} else {
await doBuild();
}
}

Expand Down
23 changes: 23 additions & 0 deletions packages/static/src/buildEntryDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Context passed to the build entry function.
*/
export interface BuildEntryContext {
/**
* Performs the default build flow (rendering entries and writing output files).
* Call this to execute the standard build process.
* You can run additional work before, after, or in parallel with this function.
*/
build: () => Promise<void>;
/**
* Absolute path to the output directory where built files are written.
* Use this to write additional files (e.g. sitemap.xml) alongside the build output.
*/
outDir: string;
}

/**
* The build entry module should default-export a function with this signature.
*/
export type BuildEntryFunction = (
context: BuildEntryContext,
) => Promise<void> | void;
4 changes: 4 additions & 0 deletions packages/static/src/entries/server.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { defer, type DeferOptions } from "../rsc/defer";
export type {
BuildEntryContext,
BuildEntryFunction,
} from "../buildEntryDefinition";
28 changes: 28 additions & 0 deletions packages/static/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ interface FunstackStaticBaseOptions {
* @default "fun__rsc-payload"
*/
rscPayloadDir?: string;
/**
* Path to a module that customizes the build process.
* The module should `export default` an async function that receives
* `{ build }` where `build` is a function that performs the default
* build flow.
*
* This allows you to run additional work before/after the build,
* or to control the build execution (e.g. parallel work).
* Only called during production builds, not in dev mode.
*
* The module runs in the RSC environment.
*/
build?: string;
}

interface SingleEntryOptions {
Expand Down Expand Up @@ -95,6 +108,7 @@ export default function funstackStatic(

let resolvedEntriesModule: string = "__uninitialized__";
let resolvedClientInitEntry: string | undefined;
let resolvedBuildEntry: string | undefined;

// Determine whether user specified entries or root+app
const isMultiEntry = "entries" in options && options.entries !== undefined;
Expand Down Expand Up @@ -152,6 +166,11 @@ export default function funstackStatic(
path.resolve(config.root, clientInit),
);
}
if (options.build) {
resolvedBuildEntry = normalizePath(
path.resolve(config.root, options.build),
);
}
},
configEnvironment(_name, config) {
if (!config.optimizeDeps) {
Expand Down Expand Up @@ -189,6 +208,9 @@ export default function funstackStatic(
if (id === "virtual:funstack/client-init") {
return "\0virtual:funstack/client-init";
}
if (id === "virtual:funstack/build-entry") {
return "\0virtual:funstack/build-entry";
}
},
load(id) {
if (id === "\0virtual:funstack/entries") {
Expand Down Expand Up @@ -218,6 +240,12 @@ export default function funstackStatic(
}
return "";
}
if (id === "\0virtual:funstack/build-entry") {
if (resolvedBuildEntry) {
return `export { default } from "${resolvedBuildEntry}";`;
}
return "export default undefined;";
}
},
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/static/src/rsc/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export async function build() {
}

export { defer } from "./defer";
export { default as buildEntry } from "virtual:funstack/build-entry";

if (import.meta.hot) {
import.meta.hot.accept();
Expand Down
5 changes: 5 additions & 0 deletions packages/static/src/virtual.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ declare module "virtual:funstack/config" {
export const rscPayloadDir: string;
}
declare module "virtual:funstack/client-init" {}
declare module "virtual:funstack/build-entry" {
import type { BuildEntryFunction } from "./buildEntryDefinition";
const buildEntry: BuildEntryFunction | undefined;
export default buildEntry;
}