Skip to content

Commit 119f539

Browse files
committed
wip
1 parent de36556 commit 119f539

File tree

8 files changed

+511
-150
lines changed

8 files changed

+511
-150
lines changed

bun.lock

Lines changed: 190 additions & 116 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"db:generate": "drizzle-kit generate"
1414
},
1515
"dependencies": {
16-
"@deco/workers-runtime": "npm:@jsr/deco__workers-runtime@0.20.2",
16+
"@deco/workers-runtime": "file://../cms/packages/runtime/",
1717
"@radix-ui/react-collapsible": "^1.1.12",
1818
"@radix-ui/react-popover": "^1.1.15",
1919
"@radix-ui/react-slot": "^1.2.3",

plugin.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import fs from "fs/promises";
77
interface PluginConfig {
88
port?: number;
99
experimentalAutoGenerateTypes?: boolean;
10+
runtime: "cloudflare" | "bun";
1011
}
1112

1213
const cwd = process.cwd();
@@ -52,9 +53,11 @@ const OPERATIONS = [
5253
})),
5354
];
5455

55-
async function fixCloudflareBuild(
56-
{ outputDirectory }: { outputDirectory: string },
57-
) {
56+
async function fixCloudflareBuild({
57+
outputDirectory,
58+
}: {
59+
outputDirectory: string;
60+
}) {
5861
const files = await fs.readdir(outputDirectory);
5962

6063
const isCloudflareViteBuild = files.some((file) => file === "wrangler.json");
@@ -63,16 +66,18 @@ async function fixCloudflareBuild(
6366
return;
6467
}
6568

66-
const results = await Promise.allSettled(OPERATIONS.map(async (operation) => {
67-
if (operation.type === "remove") {
68-
await fs.rm(path.join(outputDirectory, operation.file));
69-
} else if (operation.type === "rename") {
70-
await fs.rename(
71-
path.join(outputDirectory, operation.oldFile),
72-
path.join(outputDirectory, operation.newFile),
73-
);
74-
}
75-
}));
69+
const results = await Promise.allSettled(
70+
OPERATIONS.map(async (operation) => {
71+
if (operation.type === "remove") {
72+
await fs.rm(path.join(outputDirectory, operation.file));
73+
} else if (operation.type === "rename") {
74+
await fs.rename(
75+
path.join(outputDirectory, operation.oldFile),
76+
path.join(outputDirectory, operation.newFile),
77+
);
78+
}
79+
}),
80+
);
7681

7782
results.forEach((result) => {
7883
if (result.status === "rejected") {
@@ -154,6 +159,17 @@ export function importSqlStringPlugin(): Plugin {
154159
};
155160
}
156161

162+
const VITE_SERVER_ENVIRONMENT_NAME = "server";
163+
157164
export default function vitePlugins(decoConfig: PluginConfig = {}): Plugin[] {
158-
return [deco(decoConfig), importSqlStringPlugin()];
165+
const cloudflarePlugin =
166+
decoConfig.runtime === "cloudflare"
167+
? cloudflare({
168+
configPath: "wrangler.toml",
169+
viteEnvironment: {
170+
name: VITE_SERVER_ENVIRONMENT_NAME,
171+
},
172+
})
173+
: undefined;
174+
return [deco(decoConfig), importSqlStringPlugin(), cloudflarePlugin];
159175
}

server/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ import migrations from "../drizzle/migrations";
77

88
export const getDb = async (env: Env) => {
99
const db = drizzle(env);
10-
await migrateWithoutTransaction(db, migrations);
10+
// await migrateWithoutTransaction(db, migrations);
1111
return db;
1212
};

server/http/mime.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @module
3+
* MIME utility.
4+
*/
5+
6+
export const getMimeType = (
7+
filename: string,
8+
mimes: Record<string, string> = baseMimes,
9+
): string | undefined => {
10+
const regexp = /\.([a-zA-Z0-9]+?)$/;
11+
const match = filename.match(regexp);
12+
if (!match) {
13+
return;
14+
}
15+
let mimeType = mimes[match[1]];
16+
if (mimeType && mimeType.startsWith("text")) {
17+
mimeType += "; charset=utf-8";
18+
}
19+
return mimeType;
20+
};
21+
22+
export const getExtension = (mimeType: string): string | undefined => {
23+
for (const ext in baseMimes) {
24+
if (baseMimes[ext] === mimeType) {
25+
return ext;
26+
}
27+
}
28+
};
29+
30+
export { baseMimes as mimes };
31+
32+
/**
33+
* Union types for BaseMime
34+
*/
35+
export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes];
36+
37+
const _baseMimes = {
38+
aac: "audio/aac",
39+
avi: "video/x-msvideo",
40+
avif: "image/avif",
41+
av1: "video/av1",
42+
bin: "application/octet-stream",
43+
bmp: "image/bmp",
44+
css: "text/css",
45+
csv: "text/csv",
46+
eot: "application/vnd.ms-fontobject",
47+
epub: "application/epub+zip",
48+
gif: "image/gif",
49+
gz: "application/gzip",
50+
htm: "text/html",
51+
html: "text/html",
52+
ico: "image/x-icon",
53+
ics: "text/calendar",
54+
jpeg: "image/jpeg",
55+
jpg: "image/jpeg",
56+
js: "text/javascript",
57+
json: "application/json",
58+
jsonld: "application/ld+json",
59+
map: "application/json",
60+
mid: "audio/x-midi",
61+
midi: "audio/x-midi",
62+
mjs: "text/javascript",
63+
mp3: "audio/mpeg",
64+
mp4: "video/mp4",
65+
mpeg: "video/mpeg",
66+
oga: "audio/ogg",
67+
ogv: "video/ogg",
68+
ogx: "application/ogg",
69+
opus: "audio/opus",
70+
otf: "font/otf",
71+
pdf: "application/pdf",
72+
png: "image/png",
73+
rtf: "application/rtf",
74+
svg: "image/svg+xml",
75+
tif: "image/tiff",
76+
tiff: "image/tiff",
77+
ts: "video/mp2t",
78+
ttf: "font/ttf",
79+
txt: "text/plain",
80+
wasm: "application/wasm",
81+
webm: "video/webm",
82+
weba: "audio/webm",
83+
webmanifest: "application/manifest+json",
84+
webp: "image/webp",
85+
woff: "font/woff",
86+
woff2: "font/woff2",
87+
xhtml: "application/xhtml+xml",
88+
xml: "application/xml",
89+
zip: "application/zip",
90+
"3gp": "video/3gpp",
91+
"3g2": "video/3gpp2",
92+
gltf: "model/gltf+json",
93+
glb: "model/gltf-binary",
94+
} as const;
95+
96+
const baseMimes: Record<string, BaseMime> = _baseMimes;

server/http/serveStatic.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { getMimeType } from "./mime";
2+
import type { ReadStream, Stats } from "node:fs";
3+
import { createReadStream, lstatSync, existsSync } from "node:fs";
4+
import { join } from "node:path";
5+
6+
export type ServeStaticOptions<E extends Env = Env> = {
7+
/**
8+
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
9+
*/
10+
root?: string;
11+
path?: string;
12+
index?: string; // default is 'index.html'
13+
precompressed?: boolean;
14+
rewriteRequestPath?: (path: string, c: Context<E>) => string;
15+
onNotFound?: (req: Request, env: unknown) => Promise<Response> | Response;
16+
};
17+
18+
const COMPRESSIBLE_CONTENT_TYPE_REGEX =
19+
/^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
20+
const ENCODINGS = {
21+
br: ".br",
22+
zstd: ".zst",
23+
gzip: ".gz",
24+
} as const;
25+
const ENCODINGS_ORDERED_KEYS = Object.keys(
26+
ENCODINGS,
27+
) as (keyof typeof ENCODINGS)[];
28+
29+
const createStreamBody = (stream: ReadStream) => {
30+
const body = new ReadableStream({
31+
start(controller) {
32+
stream.on("data", (chunk) => {
33+
controller.enqueue(chunk);
34+
});
35+
stream.on("error", (err) => {
36+
controller.error(err);
37+
});
38+
stream.on("end", () => {
39+
controller.close();
40+
});
41+
},
42+
43+
cancel() {
44+
stream.destroy();
45+
},
46+
});
47+
return body;
48+
};
49+
50+
const getStats = (path: string) => {
51+
let stats: Stats | undefined;
52+
try {
53+
stats = lstatSync(path);
54+
} catch {}
55+
return stats;
56+
};
57+
58+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59+
export const serveStatic = <E extends Env = any>(
60+
options: ServeStaticOptions<E> = { root: "" },
61+
): MiddlewareHandler<E> => {
62+
const root = options.root || "";
63+
const optionPath = options.path;
64+
65+
const onNotFound =
66+
options.onNotFound ||
67+
(() => {
68+
return new Response("Not Found", { status: 404 });
69+
});
70+
71+
if (root !== "" && !existsSync(root)) {
72+
console.error(
73+
`serveStatic: root path '${root}' is not found, are you sure it's correct?`,
74+
);
75+
}
76+
77+
return async (req: Request, env: unknown): Promise<Response> => {
78+
console.log(env);
79+
const url = new URL(req.url);
80+
let filename: string;
81+
82+
if (optionPath) {
83+
filename = optionPath;
84+
} else {
85+
try {
86+
filename = decodeURIComponent(url.pathname);
87+
if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
88+
throw new Error();
89+
}
90+
} catch {
91+
return await onNotFound(req, env);
92+
}
93+
}
94+
95+
let path = join(
96+
root,
97+
!optionPath && options.rewriteRequestPath
98+
? options.rewriteRequestPath(filename, c)
99+
: filename,
100+
);
101+
102+
let stats = getStats(path);
103+
104+
if (stats && stats.isDirectory()) {
105+
const indexFile = options.index ?? "index.html";
106+
path = join(path, indexFile);
107+
stats = getStats(path);
108+
}
109+
110+
if (!stats) {
111+
return await onNotFound(req, env);
112+
}
113+
114+
const mimeType = getMimeType(path);
115+
const headers = new Headers();
116+
117+
headers.set("Content-Type", mimeType || "application/octet-stream");
118+
119+
if (
120+
options.precompressed &&
121+
(!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))
122+
) {
123+
const acceptEncodingSet = new Set(
124+
req.headers
125+
.get("Accept-Encoding")
126+
?.split(",")
127+
.map((encoding) => encoding.trim()),
128+
);
129+
130+
for (const encoding of ENCODINGS_ORDERED_KEYS) {
131+
if (!acceptEncodingSet.has(encoding)) {
132+
continue;
133+
}
134+
const precompressedStats = getStats(path + ENCODINGS[encoding]);
135+
if (precompressedStats) {
136+
headers.set("Content-Encoding", encoding);
137+
headers.append("Vary", "Accept-Encoding");
138+
stats = precompressedStats;
139+
path = path + ENCODINGS[encoding];
140+
break;
141+
}
142+
}
143+
}
144+
145+
const size = stats.size;
146+
147+
if (req.method == "HEAD" || req.method == "OPTIONS") {
148+
headers.set("Content-Length", size.toString());
149+
return new Response(null, { headers, status: 200 });
150+
}
151+
152+
const range = req.headers.get("range") || "";
153+
154+
if (!range) {
155+
headers.set("Content-Length", size.toString());
156+
const stream = createReadStream(path);
157+
return new Response(createStreamBody(stream), { headers, status: 200 });
158+
}
159+
160+
headers.set("Accept-Ranges", "bytes");
161+
headers.set("Date", stats.birthtime.toUTCString());
162+
163+
const parts = range.replace(/bytes=/, "").split("-", 2);
164+
const start = parseInt(parts[0], 10) || 0;
165+
let end = parseInt(parts[1], 10) || size - 1;
166+
if (size < end - start + 1) {
167+
end = size - 1;
168+
}
169+
170+
const chunksize = end - start + 1;
171+
const stream = createReadStream(path, { start, end });
172+
173+
headers.set("Content-Length", chunksize.toString());
174+
headers.set("Content-Range", `bytes ${start}-${end}/${stats.size}`);
175+
176+
return new Response(createStreamBody(stream), { headers, status: 206 });
177+
};
178+
};

0 commit comments

Comments
 (0)