Skip to content

feat: add configurable rscPayloadDir option#75

Merged
uhyo merged 4 commits intomasterfrom
claude/configurable-artifact-directory-CQmZR
Mar 9, 2026
Merged

feat: add configurable rscPayloadDir option#75
uhyo merged 4 commits intomasterfrom
claude/configurable-artifact-directory-CQmZR

Conversation

@uhyo
Copy link
Owner

@uhyo uhyo commented Mar 9, 2026

Add a new rscPayloadDir plugin option (default: "fun:rsc-payload")
to control the directory name used for RSC payload files. This allows
platforms like Cloudflare Workers to avoid the colon in the default
directory name, which causes unnecessary redirects to percent-encoded
URLs.

https://claude.ai/code/session_01XA4ndHQWu7o59WdkECDnG9

claude added 2 commits March 9, 2026 10:56
Add a new `rscPayloadDir` plugin option (default: "fun:rsc-payload")
to control the directory name used for RSC payload files. This allows
platforms like Cloudflare Workers to avoid the colon in the default
directory name, which causes unnecessary redirects to percent-encoded
URLs.

https://claude.ai/code/session_01XA4ndHQWu7o59WdkECDnG9
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new rscPayloadDir option to @funstack/static to make the RSC payload output directory configurable (defaulting to fun:rsc-payload), primarily to support platforms that dislike colons in path segments (e.g. Cloudflare Workers).

Changes:

  • Introduces rscPayloadDir as a plugin option (defaulting to defaultRscPayloadDir) and exposes it via virtual:funstack/config.
  • Threads rscPayloadDir through RSC payload ID/path generation in both runtime (defer) and build-time emission (buildApp/rscProcessor/rscPath).
  • Updates docs and loosens e2e assertions to avoid hard-coding the default directory name.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/static/src/virtual.d.ts Adds rscPayloadDir typing to the virtual config module.
packages/static/src/rsc/rscModule.ts Defines defaultRscPayloadDir and makes payload IDs directory-based.
packages/static/src/rsc/defer.tsx Uses rscPayloadDir from virtual config when generating deferred payload IDs.
packages/static/src/plugin/index.ts Adds rscPayloadDir option, exports it via virtual config, passes it into buildApp.
packages/static/src/build/rscProcessor.ts Requires rscPayloadDir to hash/emit deferred component payload IDs consistently.
packages/static/src/build/rscPath.ts Requires rscPayloadDir to compute final payload paths.
packages/static/src/build/buildApp.ts Threads rscPayloadDir into component processing and payload file emission.
packages/static/e2e/tests/multi-entry.spec.ts Relaxes assertions around payload directory naming.
packages/static/e2e/tests/hydration.spec.ts Relaxes request filtering to catch payload fetches without hard-coded dir.
packages/static/e2e/tests/build.spec.ts Relaxes HTML/path assertions and payload path regex.
packages/docs/src/pages/learn/HowItWorks.mdx Mentions that the payload directory name is configurable.
packages/docs/src/pages/api/FunstackStatic.mdx Documents the new rscPayloadDir option with an example.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 24 to 35
/**
* Processes RSC components by replacing temporary UUIDs with content-based hashes.
*
* @param deferRegistryIterator - Iterator yielding components with { id, data }
* @param appRscStream - The main RSC stream
* @param context - Optional context for logging warnings
*/
export async function processRscComponents(
deferRegistryIterator: AsyncIterable<RawComponent>,
appRscStream: ReadableStream,
rscPayloadDir: string,
context?: { warn: (message: string) => void },
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rscPayloadDir is now a required parameter but the function docstring’s @param list wasn’t updated to describe it. Please add an entry explaining what the directory is used for (and any expected format constraints) so the signature and docs stay in sync.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +77
const {
publicOutDir = "dist/public",
ssr = false,
clientInit,
rscPayloadDir = defaultRscPayloadDir,
} = options;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rscPayloadDir is interpolated into module IDs and later used to form filesystem paths via getModulePathFor(...) + path.join(...). Since it’s now user-configurable, values containing path separators, leading slashes, or .. segments can escape the intended funstack__/ folder or even write outside baseDir. Please validate/normalize this option (e.g., require a non-empty single path segment with no / or .., and trim leading/trailing slashes) before using it.

Copilot uses AI. Check for mistakes.
Comment on lines 6 to 15
/**
* Add prefix to raw ID to form payload ID so that the ID is
* distinguishable from other possible IDs.
*/
export function getPayloadIDFor(rawId: string): string {
return `${rscPayloadIDPrefix}${rawId}`;
export function getPayloadIDFor(
rawId: string,
rscPayloadDir: string = defaultRscPayloadDir,
): string {
return `${rscPayloadDir}/${rawId}`;
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above getPayloadIDFor still talks about “Add prefix to raw ID”, but the implementation now builds ${rscPayloadDir}/${rawId} (directory + raw ID). Updating the wording would make it clearer what is actually being prefixed and why.

Copilot uses AI. Check for mistakes.
Comment on lines 23 to 35
@@ -31,9 +31,7 @@ test.describe("Build output verification", () => {
const html = await indexResponse.text();

// Look for the RSC payload in preload link or FUNSTACK config
const rscPayloadMatch = html.match(
/funstack__\/fun:rsc-payload\/[^"'\s]+\.txt/,
);
const rscPayloadMatch = html.match(/funstack__\/[^"'\s]+\.txt/);
expect(rscPayloadMatch).not.toBeNull();
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions were loosened to match any funstack__/...*.txt path, which can hide regressions (e.g., payloads being written to an unexpected subdirectory, or unrelated .txt files under funstack__/ satisfying the regex). Consider extracting the expected payload path from the preload/config and asserting it includes the configured rscPayloadDir, and add a dedicated e2e case that sets a non-default rscPayloadDir and verifies the emitted paths use it.

Copilot uses AI. Check for mistakes.
Comment on lines 14 to 40
@@ -24,7 +24,7 @@ test.describe("Multi-entry build output", () => {
expect(html).toContain("<!DOCTYPE html>");
expect(html).toContain("<html");
expect(html).toContain("__FUNSTACK_APP_ENTRY__");
expect(html).toContain("funstack__/fun:rsc-payload/");
expect(html).toContain("funstack__/");
});

test("each page has its own RSC payload", async ({ request }) => {
@@ -35,12 +35,8 @@ test.describe("Multi-entry build output", () => {
const aboutHtml = await aboutResponse.text();

// Both pages should reference RSC payloads
const homePayloadMatch = homeHtml.match(
/funstack__\/fun:rsc-payload\/[^"'\s]+\.txt/,
);
const aboutPayloadMatch = aboutHtml.match(
/funstack__\/fun:rsc-payload\/[^"'\s]+\.txt/,
);
const homePayloadMatch = homeHtml.match(/funstack__\/[^"'\s]+\.txt/);
const aboutPayloadMatch = aboutHtml.match(/funstack__\/[^"'\s]+\.txt/);

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated checks only assert that some .txt under funstack__/ exists. That makes the test less specific and doesn’t verify the new rscPayloadDir option works (or that the default directory is still used when unset). Consider adding a scenario that configures rscPayloadDir in the multi-entry fixture and then assert both pages reference /funstack__/{rscPayloadDir}/...txt (and that those files are fetchable).

Copilot uses AI. Check for mistakes.
Comment on lines 63 to 68
page.on("request", (request) => {
const url = request.url();
if (url.includes("funstack__/fun:rsc-payload/") && url.endsWith(".txt")) {
if (url.includes("funstack__/") && url.endsWith(".txt")) {
rscRequests.push(url);
}
});
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This request filter now matches any .txt request under funstack__/, which may overcount unrelated files and doesn’t actually prove the app fetched the main RSC payload path referenced by the HTML. To keep the test robust with configurable rscPayloadDir, consider extracting the expected payload URL from the document (preload link / manifest) and assert that exact path was requested.

Copilot uses AI. Check for mistakes.
uhyo and others added 2 commits March 9, 2026 22:36
- Validate rscPayloadDir to prevent path traversal (reject slashes, .., empty)
- Add @param docstring for rscPayloadDir in rscProcessor
- Update comment in rscModule to reflect directory-based payload IDs
- Restore specific fun:rsc-payload/ assertions in e2e tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The value is used for string replacement during build, so it must not
appear in the application's source code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@uhyo uhyo merged commit 68213cc into master Mar 9, 2026
1 check passed
@uhyo uhyo deleted the claude/configurable-artifact-directory-CQmZR branch March 9, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants