Skip to content

Commit bf60fc3

Browse files
authored
Merge branch 'main' into http-error-refine
2 parents 2fa953c + c0e2b80 commit bf60fc3

8 files changed

Lines changed: 229 additions & 25 deletions

File tree

init/src/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ ${GRADIENT_CSS}`;
403403
import { define, type State } from "./utils.ts";
404404
405405
export const app = new App<State>();
406+
406407
app.use(staticFiles());
407408
408409
// this is the same as the /api/:name route defined via a file. feel free to delete this!
@@ -421,7 +422,6 @@ const exampleLoggerMiddleware = define.middleware((ctx) => {
421422
app.use(exampleLoggerMiddleware);
422423
423424
await fsRoutes(app, {
424-
dir: "./",
425425
loadIsland: (path) => import(\`./islands/\${path}\`),
426426
loadRoute: (path) => import(\`./routes/\${path}\`),
427427
});

src/config.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
import * as path from "@std/path";
22

33
export interface FreshConfig {
4+
/**
5+
* The root directory of the Fresh project.
6+
*
7+
* Other paths, such as `build.outDir`, `staticDir`, and `fsRoutes()`
8+
* are resolved relative to this directory.
9+
* @default Deno.cwd()
10+
*/
411
root?: string;
512
build?: {
613
/**
714
* The directory to write generated files to when `dev.ts build` is run.
15+
*
816
* This can be an absolute path, a file URL or a relative path.
17+
* Relative paths are resolved against the `root` option.
18+
* @default "_fresh"
919
*/
1020
outDir?: string;
1121
};
1222
/**
1323
* Serve fresh from a base path instead of from the root.
1424
* "/foo/bar" -> http://localhost:8000/foo/bar
15-
* @default {undefined}
25+
* @default undefined
1626
*/
1727
basePath?: string;
28+
/**
29+
* The directory to serve static files from.
30+
*
31+
* This can be an absolute path, a file URL or a relative path.
32+
* Relative paths are resolved against the `root` option.
33+
* @default "static"
34+
*/
1835
staticDir?: string;
1936
}
2037

@@ -35,35 +52,47 @@ export interface ResolvedFreshConfig {
3552
}
3653

3754
export function parseRootPath(root: string, cwd: string): string {
38-
if (root.startsWith("file://")) {
39-
root = path.fromFileUrl(root);
40-
} else if (!path.isAbsolute(root)) {
41-
root = path.join(cwd, root);
55+
return parseDirPath(root, cwd, true);
56+
}
57+
58+
function parseDirPath(
59+
dirPath: string,
60+
root: string,
61+
fileToDir = false,
62+
): string {
63+
if (dirPath.startsWith("file://")) {
64+
dirPath = path.fromFileUrl(dirPath);
65+
} else if (!path.isAbsolute(dirPath)) {
66+
dirPath = path.join(root, dirPath);
67+
}
68+
69+
if (fileToDir) {
70+
const ext = path.extname(dirPath);
71+
if (
72+
ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" ||
73+
ext === ".mjs"
74+
) {
75+
dirPath = path.dirname(dirPath);
76+
}
4277
}
4378

44-
const ext = path.extname(root);
45-
if (
46-
ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" ||
47-
ext === ".mjs"
48-
) {
49-
root = path.dirname(root);
79+
if (Deno.build.os === "windows") {
80+
dirPath = dirPath.replaceAll("\\", "/");
5081
}
5182

52-
return root;
83+
return dirPath;
5384
}
5485

5586
export function normalizeConfig(options: FreshConfig): ResolvedFreshConfig {
56-
const root = options.root
57-
? parseRootPath(options.root, Deno.cwd())
58-
: Deno.cwd();
87+
const root = parseRootPath(options.root ?? ".", Deno.cwd());
5988

6089
return {
6190
root,
6291
build: {
63-
outDir: options.build?.outDir ?? path.join(root, "_fresh"),
92+
outDir: parseDirPath(options.build?.outDir ?? "_fresh", root),
6493
},
6594
basePath: options.basePath ?? "",
66-
staticDir: options.staticDir ?? path.join(root, "static"),
95+
staticDir: parseDirPath(options.staticDir ?? "static", root),
6796
mode: "production",
6897
};
6998
}

src/config_test.ts

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,108 @@
11
import { expect } from "@std/expect";
2-
import { parseRootPath } from "./config.ts";
2+
import { normalizeConfig, parseRootPath } from "./config.ts";
3+
import type { FreshConfig } from "./mod.ts";
34

4-
// FIXME: Windows
5-
Deno.test.ignore("parseRootPath", () => {
6-
const cwd = Deno.cwd();
5+
Deno.test("parseRootPath", () => {
6+
const cwd = Deno.cwd().replaceAll("\\", "/");
7+
8+
// File paths
79
expect(parseRootPath("file:///foo/bar", cwd)).toEqual("/foo/bar");
810
expect(parseRootPath("file:///foo/bar.ts", cwd)).toEqual("/foo");
11+
if (Deno.build.os === "windows") {
12+
expect(parseRootPath("file:///C:/foo/bar", cwd)).toEqual("C:/foo/bar");
13+
expect(parseRootPath("file:///C:/foo/bar.ts", cwd)).toEqual("C:/foo");
14+
}
15+
16+
// Relative paths
17+
expect(parseRootPath("./foo/bar", cwd)).toEqual(`${cwd}/foo/bar`);
18+
expect(parseRootPath("./foo/bar.ts", cwd)).toEqual(`${cwd}/foo`);
19+
20+
// Absolute paths
921
expect(parseRootPath("/foo/bar", cwd)).toEqual("/foo/bar");
1022
expect(parseRootPath("/foo/bar.ts", cwd)).toEqual("/foo");
1123
expect(parseRootPath("/foo/bar.tsx", cwd)).toEqual("/foo");
1224
expect(parseRootPath("/foo/bar.js", cwd)).toEqual("/foo");
1325
expect(parseRootPath("/foo/bar.jsx", cwd)).toEqual("/foo");
1426
expect(parseRootPath("/foo/bar.mjs", cwd)).toEqual("/foo");
27+
if (Deno.build.os === "windows") {
28+
expect(parseRootPath("C:/foo/bar", cwd)).toEqual("C:/foo/bar");
29+
expect(parseRootPath("C:/foo/bar.ts", cwd)).toEqual("C:/foo");
30+
}
31+
});
32+
33+
Deno.test("normalizeConfig - root", () => {
34+
const cwd = Deno.cwd().replaceAll("\\", "/");
35+
const configRoot = (root?: string) => normalizeConfig({ root }).root;
36+
37+
expect(configRoot()).toEqual(cwd);
38+
expect(configRoot("/foo/bar")).toEqual("/foo/bar");
39+
expect(configRoot("/foo/bar.ts")).toEqual("/foo");
40+
expect(configRoot("file:///foo/bar")).toEqual("/foo/bar");
41+
expect(configRoot("./foo/bar")).toEqual(`${cwd}/foo/bar`);
42+
expect(configRoot("./foo/bar.ts")).toEqual(`${cwd}/foo`);
43+
44+
if (Deno.build.os === "windows") {
45+
expect(configRoot("C:/foo/bar.ts")).toEqual("C:/foo");
46+
expect(configRoot("file:///C:/foo/bar")).toEqual("C:/foo/bar");
47+
}
48+
});
49+
50+
Deno.test("normalizeConfig - build.outDir", () => {
51+
const cwd = Deno.cwd().replaceAll("\\", "/");
52+
const outDir = (options: FreshConfig) =>
53+
normalizeConfig(options).build.outDir;
54+
55+
// Default outDir
56+
expect(outDir({ root: "./src" })).toEqual(`${cwd}/src/_fresh`);
57+
expect(outDir({ root: "/src" })).toEqual("/src/_fresh");
58+
expect(outDir({ root: "file:///src" })).toEqual("/src/_fresh");
59+
60+
// Relative outDir
61+
expect(outDir({ root: "/src", build: { outDir: "dist" } })).toEqual(
62+
"/src/dist",
63+
);
64+
expect(outDir({ root: "/src", build: { outDir: "./dist" } })).toEqual(
65+
"/src/dist",
66+
);
67+
68+
// Absolute outDir
69+
expect(outDir({ root: "/src", build: { outDir: "/dist" } })).toEqual(
70+
"/dist",
71+
);
72+
expect(outDir({ root: "/src", build: { outDir: "/dist/fresh" } })).toEqual(
73+
"/dist/fresh",
74+
);
75+
expect(outDir({ root: "/src", build: { outDir: "file:///dist" } })).toEqual(
76+
"/dist",
77+
);
78+
});
79+
80+
Deno.test("normalizeConfig - staticDir", () => {
81+
const cwd = Deno.cwd().replaceAll("\\", "/");
82+
const staticDir = (options: FreshConfig) =>
83+
normalizeConfig(options).staticDir;
84+
85+
// Default staticDir
86+
expect(staticDir({ root: "./src" })).toEqual(`${cwd}/src/static`);
87+
expect(staticDir({ root: "/src" })).toEqual("/src/static");
88+
expect(staticDir({ root: "file:///src" })).toEqual("/src/static");
89+
90+
// Relative staticDir
91+
expect(staticDir({ root: "/src", staticDir: "public" })).toEqual(
92+
"/src/public",
93+
);
94+
expect(staticDir({ root: "/src", staticDir: "./public" })).toEqual(
95+
"/src/public",
96+
);
97+
98+
// Absolute staticDir
99+
expect(staticDir({ root: "/src", staticDir: "/public" })).toEqual(
100+
"/public",
101+
);
102+
expect(staticDir({ root: "/src", staticDir: "/public/assets" })).toEqual(
103+
"/public/assets",
104+
);
105+
expect(staticDir({ root: "/src", staticDir: "file:///public" })).toEqual(
106+
"/public",
107+
);
15108
});

src/define.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AnyComponent } from "preact";
2-
import type { HandlerByMethod, RouteHandler } from "./handlers.ts";
2+
import type { HandlerByMethod, HandlerFn, RouteHandler } from "./handlers.ts";
33
import type { Middleware } from "./middlewares/mod.ts";
44
import type { PageProps } from "./context.ts";
55

@@ -107,7 +107,9 @@ export interface Define<State> {
107107
page<
108108
// deno-lint-ignore no-explicit-any
109109
Handler extends RouteHandler<any, State> = never,
110-
Data = Handler extends HandlerByMethod<infer Data, State> ? Data : never,
110+
Data = Handler extends HandlerFn<infer Data, State> ? Data
111+
: Handler extends HandlerByMethod<infer Data, State> ? Data
112+
: never,
111113
>(render: AnyComponent<PageProps<Data, State>>): typeof render;
112114

113115
/**

src/define_test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from "@std/expect";
2+
import { createDefine } from "./define.ts";
3+
import { page } from "./handlers.ts";
4+
5+
Deno.test.ignore("createDefine", () => {
6+
const define = createDefine<{ foo: number }>();
7+
8+
// Testing the types
9+
const handlerFn = define.handlers((ctx) => {
10+
ctx.state.foo satisfies number;
11+
return page({ bar: true });
12+
});
13+
const handlerObj = define.handlers({
14+
GET(ctx) {
15+
ctx.state.foo satisfies number;
16+
return page({ bar: [1, 2, 3] });
17+
},
18+
POST: () => {
19+
return page({ baz: "hello" });
20+
},
21+
});
22+
23+
define.page<typeof handlerFn>(({ data }) => {
24+
data.bar satisfies boolean;
25+
return "page";
26+
});
27+
28+
define.page<typeof handlerObj>(({ data }) => {
29+
if ("baz" in data) {
30+
data.baz satisfies string;
31+
} else {
32+
data.bar satisfies number[];
33+
}
34+
return "page";
35+
});
36+
37+
define.middleware((ctx) => {
38+
ctx.state.foo satisfies number;
39+
return new Response("Hello");
40+
});
41+
42+
expect(typeof define.page).toBe("function");
43+
expect(typeof define.handlers).toBe("function");
44+
expect(typeof define.middleware).toBe("function");
45+
});

src/dev/builder_test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@ Deno.test({
9898
sanitizeResources: false,
9999
});
100100

101+
// Issue https://github.com/denoland/fresh/issues/2599
102+
Deno.test({
103+
name: "Builder - hashes CSS urls by default",
104+
fn: async () => {
105+
const builder = new Builder();
106+
const tmp = await Deno.makeTempDir();
107+
await Deno.writeTextFile(
108+
path.join(tmp, "foo.css"),
109+
`:root { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); }`,
110+
);
111+
const app = new App({
112+
staticDir: tmp,
113+
build: {
114+
outDir: path.join(tmp, "dist"),
115+
},
116+
});
117+
await builder.build(app);
118+
119+
const css = await Deno.readTextFile(
120+
path.join(tmp, "dist", "static", "foo.css"),
121+
);
122+
expect(css).toEqual(
123+
`:root { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); }`,
124+
);
125+
},
126+
sanitizeOps: false,
127+
sanitizeResources: false,
128+
});
129+
101130
Deno.test({
102131
name: "Builder - can bundle islands from JSR",
103132
fn: async () => {

src/dev/file_transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export class FreshFileTransformer {
220220
}
221221
}
222222

223-
const CSS_URL_REGEX = /url\((["'][^'"]+["']|[^)]+)\)/g;
223+
const CSS_URL_REGEX = /url\(("[^"]+"|'[^']+'|[^)]+)\)/g;
224224

225225
export function cssAssetHash(transformer: FreshFileTransformer) {
226226
transformer.onTransform({

src/plugins/fs_routes/mod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ function isFreshFile<State>(mod: any): mod is FreshFsItem<State> {
4747
}
4848

4949
export interface FsRoutesOptions {
50+
/**
51+
* Parent directory for the `/routes` and `/islands` folders.
52+
*
53+
* By default, the `root` config option of the provided app is used.
54+
* @default app.config.root
55+
*/
5056
dir?: string;
5157
ignoreFilePattern?: RegExp[];
5258
loadRoute: (path: string) => Promise<unknown>;

0 commit comments

Comments
 (0)