Skip to content

Commit 85f93e0

Browse files
bartlomiejuclaude
andcommitted
fix: address basePath review feedback
1. Make basePath a module-level variable (like BUILD_ID) instead of threading it through asset()/assetSrcSet() parameters. The public API no longer exposes basePath — it's set once via setBasePath() during app.handler() initialization. 2. Guard applyBasePath against double-prefixing — if the path already starts with basePath, return it unchanged. 3. Improve basePath validation — use URL round-trip check instead of an incomplete character regex, catching all invalid path characters. 4. Island CSS paths confirmed safe — they are root-relative from the build system and never include basePath, so applyBasePath (now via asset()) is correct. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fb17c14 commit 85f93e0

4 files changed

Lines changed: 35 additions & 18 deletions

File tree

packages/fresh/src/app.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { trace } from "@opentelemetry/api";
22

33
import { DENO_DEPLOYMENT_ID } from "@fresh/build-id";
44
import * as colors from "@std/fmt/colors";
5+
import { setBasePath } from "./runtime/shared.ts";
56
import {
67
type MaybeLazyMiddleware,
78
type Middleware,
@@ -218,10 +219,17 @@ export class App<State> {
218219
);
219220
}
220221

221-
const hasInvalidChars = /[@#?&=\s]/.test(basePath);
222-
if (hasInvalidChars) {
222+
// Validate by round-tripping through URL — catches all invalid path chars
223+
try {
224+
const url = new URL(basePath, "https://localhost");
225+
if (url.pathname !== basePath) {
226+
throw new Error(
227+
`Invalid basePath: "${basePath}". Contains characters that require encoding`,
228+
);
229+
}
230+
} catch {
223231
throw new Error(
224-
`Invalid basePath: "${basePath}". Contains invalid characters`,
232+
`Invalid basePath: "${basePath}". Must be a valid URL path segment`,
225233
);
226234
}
227235
}
@@ -422,6 +430,8 @@ export class App<State> {
422430
}
423431
}
424432

433+
setBasePath(this.config.basePath);
434+
425435
const router = new UrlPatternRouter<MaybeLazyMiddleware<State>>();
426436

427437
const { rootMiddlewares } = applyCommands(

packages/fresh/src/runtime/server/preact_hooks.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { asset, Partial, type PartialProps } from "../shared.ts";
1616
import { stringify } from "../../jsonify/stringify.ts";
1717
import type { Island } from "../../context.ts";
1818
import {
19-
applyBasePath,
2019
assetHashingHook,
2120
CLIENT_NAV_ATTR,
2221
DATA_FRESH_KEY,
@@ -116,8 +115,7 @@ options[OptionsType.VNODE] = (vnode) => {
116115
setActiveUrl(vnode, RENDER_STATE.ctx.url.pathname);
117116
}
118117
}
119-
const basePath = RENDER_STATE?.ctx.config.basePath;
120-
assetHashingHook(vnode, BUILD_ID, basePath);
118+
assetHashingHook(vnode, BUILD_ID, RENDER_STATE?.ctx.config.basePath);
121119

122120
if (typeof vnode.type === "function") {
123121
if (vnode.type === Partial) {
@@ -295,7 +293,6 @@ options[OptionsType.DIFF] = (vnode) => {
295293
RENDER_STATE!.renderedHtmlHead = true;
296294

297295
const entryAssets = RENDER_STATE.buildCache.getEntryAssets();
298-
const basePath = RENDER_STATE.ctx.config.basePath;
299296
// deno-lint-ignore no-explicit-any
300297
const items: VNode<any>[] = [];
301298
if (entryAssets.length > 0) {
@@ -307,7 +304,7 @@ options[OptionsType.DIFF] = (vnode) => {
307304
h(
308305
"link",
309306
// deno-lint-ignore no-explicit-any
310-
{ rel: "stylesheet", href: asset(id, basePath) } as any,
307+
{ rel: "stylesheet", href: asset(id) } as any,
311308
),
312309
);
313310
}
@@ -467,7 +464,6 @@ options[OptionsType.DIFFED] = (vnode) => {
467464

468465
function RemainingHead() {
469466
if (RENDER_STATE !== null) {
470-
const basePath = RENDER_STATE.ctx.config.basePath;
471467
// deno-lint-ignore no-explicit-any
472468
const items: VNode<any>[] = [];
473469
if (RENDER_STATE.headComponents.size > 0) {
@@ -477,16 +473,15 @@ function RemainingHead() {
477473
RENDER_STATE.islands.forEach((island) => {
478474
if (island.css.length > 0) {
479475
for (let i = 0; i < island.css.length; i++) {
480-
const css = island.css[i];
481-
const fullPath = applyBasePath(css, basePath);
482-
items.push(h("link", { rel: "stylesheet", href: fullPath }));
476+
items.push(
477+
h("link", { rel: "stylesheet", href: asset(island.css[i]) }),
478+
);
483479
}
484480
}
485481
});
486482

487483
RENDER_STATE.islandAssets.forEach((css) => {
488-
const fullPath = applyBasePath(css, basePath);
489-
items.push(h("link", { rel: "stylesheet", href: fullPath }));
484+
items.push(h("link", { rel: "stylesheet", href: asset(css) }));
490485
});
491486

492487
if (items.length > 0) {

packages/fresh/src/runtime/shared.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { ComponentChildren, VNode } from "preact";
22
import { BUILD_ID } from "@fresh/build-id";
33
import { assetInternal, assetSrcSetInternal } from "./shared_internal.ts";
44

5+
let BASE_PATH = "";
6+
57
export { HttpError } from "../error.ts";
68

79
/**
@@ -22,18 +24,23 @@ export { HttpError } from "../error.ts";
2224
*/
2325
export const IS_BROWSER = typeof document !== "undefined";
2426

27+
/** @internal Set the base path for asset URLs. Called once during app init. */
28+
export function setBasePath(basePath: string) {
29+
BASE_PATH = basePath;
30+
}
31+
2532
/**
2633
* Create a "locked" asset path. This differs from a plain path in that it is
2734
* specific to the current version of the application, and as such can be safely
2835
* served with a very long cache lifetime (1 year).
2936
*/
30-
export function asset(path: string, basePath?: string): string {
31-
return assetInternal(path, BUILD_ID, basePath);
37+
export function asset(path: string): string {
38+
return assetInternal(path, BUILD_ID, BASE_PATH);
3239
}
3340

3441
/** Apply the `asset` function to urls in a `srcset` attribute. */
35-
export function assetSrcSet(srcset: string, basePath?: string): string {
36-
return assetSrcSetInternal(srcset, BUILD_ID, basePath);
42+
export function assetSrcSet(srcset: string): string {
43+
return assetSrcSetInternal(srcset, BUILD_ID, BASE_PATH);
3744
}
3845

3946
export interface PartialProps {

packages/fresh/src/runtime/shared_internal.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,10 @@ export function applyBasePath(path: string, basePath?: string): string {
176176
return path;
177177
}
178178

179+
// Avoid double-prefixing if the path already starts with basePath
180+
if (path.startsWith(basePath + "/") || path === basePath) {
181+
return path;
182+
}
183+
179184
return basePath + path;
180185
}

0 commit comments

Comments
 (0)