Skip to content
Draft
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
306 changes: 190 additions & 116 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@deco/workers-runtime": "npm:@jsr/deco__workers-runtime@0.20.2",
"@deco/workers-runtime": "file://../cms/packages/runtime/",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
Expand Down
44 changes: 30 additions & 14 deletions plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fs from "fs/promises";
interface PluginConfig {
port?: number;
experimentalAutoGenerateTypes?: boolean;
runtime: "cloudflare" | "bun";
}

const cwd = process.cwd();
Expand Down Expand Up @@ -52,9 +53,11 @@ const OPERATIONS = [
})),
];

async function fixCloudflareBuild(
{ outputDirectory }: { outputDirectory: string },
) {
async function fixCloudflareBuild({
outputDirectory,
}: {
outputDirectory: string;
}) {
const files = await fs.readdir(outputDirectory);

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

const results = await Promise.allSettled(OPERATIONS.map(async (operation) => {
if (operation.type === "remove") {
await fs.rm(path.join(outputDirectory, operation.file));
} else if (operation.type === "rename") {
await fs.rename(
path.join(outputDirectory, operation.oldFile),
path.join(outputDirectory, operation.newFile),
);
}
}));
const results = await Promise.allSettled(
OPERATIONS.map(async (operation) => {
if (operation.type === "remove") {
await fs.rm(path.join(outputDirectory, operation.file));
} else if (operation.type === "rename") {
await fs.rename(
path.join(outputDirectory, operation.oldFile),
path.join(outputDirectory, operation.newFile),
);
}
}),
);

results.forEach((result) => {
if (result.status === "rejected") {
Expand Down Expand Up @@ -154,6 +159,17 @@ export function importSqlStringPlugin(): Plugin {
};
}

const VITE_SERVER_ENVIRONMENT_NAME = "server";

export default function vitePlugins(decoConfig: PluginConfig = {}): Plugin[] {
return [deco(decoConfig), importSqlStringPlugin()];
const cloudflarePlugin =
decoConfig.runtime === "cloudflare"
? cloudflare({
configPath: "wrangler.toml",
viteEnvironment: {
name: VITE_SERVER_ENVIRONMENT_NAME,
},
})
: undefined;
return [deco(decoConfig), importSqlStringPlugin(), cloudflarePlugin];
}
2 changes: 1 addition & 1 deletion server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ import migrations from "../drizzle/migrations";

export const getDb = async (env: Env) => {
const db = drizzle(env);
await migrateWithoutTransaction(db, migrations);
// await migrateWithoutTransaction(db, migrations);
return db;
};
96 changes: 96 additions & 0 deletions server/http/mime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @module
* MIME utility.
*/

export const getMimeType = (
filename: string,
mimes: Record<string, string> = baseMimes,
): string | undefined => {
const regexp = /\.([a-zA-Z0-9]+?)$/;
const match = filename.match(regexp);
if (!match) {
return;
}
let mimeType = mimes[match[1]];
if (mimeType && mimeType.startsWith("text")) {
mimeType += "; charset=utf-8";
}
return mimeType;
};

export const getExtension = (mimeType: string): string | undefined => {
for (const ext in baseMimes) {
if (baseMimes[ext] === mimeType) {
return ext;
}
}
};

export { baseMimes as mimes };

/**
* Union types for BaseMime
*/
export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes];

const _baseMimes = {
aac: "audio/aac",
avi: "video/x-msvideo",
avif: "image/avif",
av1: "video/av1",
bin: "application/octet-stream",
bmp: "image/bmp",
css: "text/css",
csv: "text/csv",
eot: "application/vnd.ms-fontobject",
epub: "application/epub+zip",
gif: "image/gif",
gz: "application/gzip",
htm: "text/html",
html: "text/html",
ico: "image/x-icon",
ics: "text/calendar",
jpeg: "image/jpeg",
jpg: "image/jpeg",
js: "text/javascript",
json: "application/json",
jsonld: "application/ld+json",
map: "application/json",
mid: "audio/x-midi",
midi: "audio/x-midi",
mjs: "text/javascript",
mp3: "audio/mpeg",
mp4: "video/mp4",
mpeg: "video/mpeg",
oga: "audio/ogg",
ogv: "video/ogg",
ogx: "application/ogg",
opus: "audio/opus",
otf: "font/otf",
pdf: "application/pdf",
png: "image/png",
rtf: "application/rtf",
svg: "image/svg+xml",
tif: "image/tiff",
tiff: "image/tiff",
ts: "video/mp2t",
ttf: "font/ttf",
txt: "text/plain",
wasm: "application/wasm",
webm: "video/webm",
weba: "audio/webm",
webmanifest: "application/manifest+json",
webp: "image/webp",
woff: "font/woff",
woff2: "font/woff2",
xhtml: "application/xhtml+xml",
xml: "application/xml",
zip: "application/zip",
"3gp": "video/3gpp",
"3g2": "video/3gpp2",
gltf: "model/gltf+json",
glb: "model/gltf-binary",
} as const;

const baseMimes: Record<string, BaseMime> = _baseMimes;
178 changes: 178 additions & 0 deletions server/http/serveStatic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { getMimeType } from "./mime";
import type { ReadStream, Stats } from "node:fs";
import { createReadStream, lstatSync, existsSync } from "node:fs";
import { join } from "node:path";

export type ServeStaticOptions<E extends Env = Env> = {
/**
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
*/
root?: string;
path?: string;
index?: string; // default is 'index.html'
precompressed?: boolean;
rewriteRequestPath?: (path: string, c: Context<E>) => string;
onNotFound?: (req: Request, env: unknown) => Promise<Response> | Response;
};

const COMPRESSIBLE_CONTENT_TYPE_REGEX =
/^\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;
const ENCODINGS = {
br: ".br",
zstd: ".zst",
gzip: ".gz",
} as const;
const ENCODINGS_ORDERED_KEYS = Object.keys(
ENCODINGS,
) as (keyof typeof ENCODINGS)[];

const createStreamBody = (stream: ReadStream) => {
const body = new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("error", (err) => {
controller.error(err);
});
stream.on("end", () => {
controller.close();
});
},

cancel() {
stream.destroy();
},
});
return body;
};

const getStats = (path: string) => {
let stats: Stats | undefined;
try {
stats = lstatSync(path);
} catch {}
return stats;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const serveStatic = <E extends Env = any>(
options: ServeStaticOptions<E> = { root: "" },
): MiddlewareHandler<E> => {
const root = options.root || "";
const optionPath = options.path;

const onNotFound =
options.onNotFound ||
(() => {
return new Response("Not Found", { status: 404 });
});

if (root !== "" && !existsSync(root)) {
console.error(
`serveStatic: root path '${root}' is not found, are you sure it's correct?`,
);
}

return async (req: Request, env: unknown): Promise<Response> => {
console.log(env);
const url = new URL(req.url);
let filename: string;

if (optionPath) {
filename = optionPath;
} else {
try {
filename = decodeURIComponent(url.pathname);
if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
throw new Error();
}
} catch {
return await onNotFound(req, env);
}
}

let path = join(
root,
!optionPath && options.rewriteRequestPath
? options.rewriteRequestPath(filename, c)
: filename,
);

let stats = getStats(path);

if (stats && stats.isDirectory()) {
const indexFile = options.index ?? "index.html";
path = join(path, indexFile);
stats = getStats(path);
}

if (!stats) {
return await onNotFound(req, env);
}

const mimeType = getMimeType(path);
const headers = new Headers();

headers.set("Content-Type", mimeType || "application/octet-stream");

if (
options.precompressed &&
(!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))
) {
const acceptEncodingSet = new Set(
req.headers
.get("Accept-Encoding")
?.split(",")
.map((encoding) => encoding.trim()),
);

for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) {
continue;
}
const precompressedStats = getStats(path + ENCODINGS[encoding]);
if (precompressedStats) {
headers.set("Content-Encoding", encoding);
headers.append("Vary", "Accept-Encoding");
stats = precompressedStats;
path = path + ENCODINGS[encoding];
break;
}
}
}

const size = stats.size;

if (req.method == "HEAD" || req.method == "OPTIONS") {
headers.set("Content-Length", size.toString());
return new Response(null, { headers, status: 200 });
}

const range = req.headers.get("range") || "";

if (!range) {
headers.set("Content-Length", size.toString());
const stream = createReadStream(path);
return new Response(createStreamBody(stream), { headers, status: 200 });
}

headers.set("Accept-Ranges", "bytes");
headers.set("Date", stats.birthtime.toUTCString());

const parts = range.replace(/bytes=/, "").split("-", 2);
const start = parseInt(parts[0], 10) || 0;
let end = parseInt(parts[1], 10) || size - 1;
if (size < end - start + 1) {
end = size - 1;
}

const chunksize = end - start + 1;
const stream = createReadStream(path, { start, end });

headers.set("Content-Length", chunksize.toString());
headers.set("Content-Range", `bytes ${start}-${end}/${stats.size}`);

return new Response(createStreamBody(stream), { headers, status: 206 });
};
};
Loading