feat: rebuild app using vike

This commit is contained in:
2024-02-22 12:15:04 +00:00
parent 87ef4ca1e3
commit 9777ae1676
104 changed files with 2214 additions and 2976 deletions
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
import preview from "./preview";
import trpc from "./trpc/handler";
const api = Router();
api.use("/trpc", trpc);
api.use("/preview", preview);
export default api;
+51
View File
@@ -0,0 +1,51 @@
import { getFileExt } from "~/lib/utils";
import db from "~/server/db";
import { file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
import { serveHtml } from "./serve-html";
import { Mime } from "mime/lite";
import standardTypes from "mime/types/standard.js";
import otherTypes from "mime/types/other.js";
import { serveJs } from "./serve-js";
import { type Request, type Response, Router } from "express";
const mime = new Mime(standardTypes, otherTypes);
mime.define({ "text/javascript": ["jsx", "tsx"] }, true);
const get = async (req: Request, res: Response) => {
const { slug, ...pathParams } = req.params as any;
const path = pathParams[0];
const fileData = await db.query.file.findFirst({
where: and(eq(file.path, path), isNull(file.deletedAt)),
});
if (!fileData) {
return res.status(404).send("File not found!");
}
const ext = getFileExt(fileData.filename);
let content = fileData.content || "";
if (["html", "htm"].includes(ext)) {
content = await serveHtml(fileData);
}
if (["js", "ts", "jsx", "tsx"].includes(ext)) {
content = await serveJs(fileData);
}
res.setHeader(
"Content-Type",
mime.getType(fileData.filename) || "application/octet-stream"
);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.send(content);
};
const router = Router();
router.get("/:slug/*", get);
export default router;
+51
View File
@@ -0,0 +1,51 @@
import db from "~/server/db";
import { FileSchema, file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
export const serveHtml = async (fileData: FileSchema) => {
const layout = await db.query.file.findFirst({
where: and(
eq(file.filename, "_layout.html"),
fileData.parentId
? eq(file.parentId, fileData.parentId)
: isNull(file.parentId),
isNull(file.deletedAt)
),
});
let content = fileData.content || "";
if (!layout?.content) {
return content;
}
content = layout.content.replace("{CONTENT}", content);
const bodyOpeningTagIdx = content.indexOf("<body");
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
const importMaps = [
{ name: "react", url: "https://esm.sh/[email protected]" },
{ name: "react-dom/client", url: "https://esm.sh/[email protected]/client" },
];
if (importMaps.length > 0) {
const imports = importMaps.reduce((a: any, b) => {
a[b.name] = b.url;
return a;
}, {});
const json = JSON.stringify({ imports });
injectScripts.unshift(`<script type="importmap">${json}</script>`);
}
if (firstScriptTagIdx >= 0 && injectScripts.length > 0) {
content =
content.substring(0, firstScriptTagIdx) +
injectScripts.filter((i) => !!i).join("") +
content.substring(firstScriptTagIdx);
}
// content = content.replace(/ {2}|\r\n|\n|\r/gm, "");
return content;
};
+11
View File
@@ -0,0 +1,11 @@
import { FileSchema } from "~/server/db/schema/file";
import { transformJs } from "~/server/lib/transform-js";
export const serveJs = async (file: FileSchema) => {
let content = file.content || "";
// transform js
content = await transformJs(content);
return content;
};
+10
View File
@@ -0,0 +1,10 @@
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
export const createContext = async ({
req,
res,
}: CreateExpressContextOptions) => {
return {};
};
export type Context = Awaited<ReturnType<typeof createContext>>;
+10
View File
@@ -0,0 +1,10 @@
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "~/server/routers/_app";
import { createContext } from "./context";
const trpc = createExpressMiddleware({
router: appRouter,
createContext,
});
export default trpc;
+7
View File
@@ -0,0 +1,7 @@
import { initTRPC } from "@trpc/server";
import { Context } from "./context";
const t = initTRPC.context<Context>().create();
// Base router and procedure helpers
export const { router, procedure, createCallerFactory } = t;
+7
View File
@@ -0,0 +1,7 @@
import { createContext } from "./context";
import { appRouter } from "../../routers/_app";
import { createCallerFactory } from ".";
const trpcServer = createCallerFactory(appRouter)(createContext);
export default trpcServer;
+9
View File
@@ -0,0 +1,9 @@
import { drizzle } from "drizzle-orm/better-sqlite3";
import path from "node:path";
import Database from "better-sqlite3";
import * as schema from "./schema/_schema";
const sqlite = new Database(path.join(process.cwd(), "storage/database.db"));
const db = drizzle(sqlite, { schema });
export default db;
+5
View File
@@ -0,0 +1,5 @@
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db from "./index";
migrate(db, { migrationsFolder: __dirname + "/drizzle" });
process.exit();
+2
View File
@@ -0,0 +1,2 @@
export { user } from "./user";
export { file } from "./file";
+47
View File
@@ -0,0 +1,47 @@
import { sql } from "drizzle-orm";
import {
integer,
sqliteTable,
text,
foreignKey,
} from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { user } from "./user";
export const file = sqliteTable(
"files",
{
id: integer("id").primaryKey({ autoIncrement: true }),
parentId: integer("parent_id"),
userId: integer("user_id")
.notNull()
.references(() => user.id),
path: text("path").notNull().unique(),
filename: text("filename").notNull(),
isDirectory: integer("is_directory", { mode: "boolean" })
.notNull()
.default(false),
isFile: integer("is_file", { mode: "boolean" }).notNull().default(false),
isPinned: integer("is_pinned", { mode: "boolean" })
.notNull()
.default(false),
content: text("content"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
deletedAt: text("deleted_at"),
},
(table) => ({
parentFk: foreignKey({
columns: [table.parentId],
foreignColumns: [table.id],
name: "parent_id_fk",
}),
})
);
export const insertFileSchema = createInsertSchema(file);
export const selectFileSchema = createSelectSchema(file);
export type FileSchema = z.infer<typeof selectFileSchema>;
+19
View File
@@ -0,0 +1,19 @@
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
export const user = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull().unique(),
password: text("password").notNull(),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
deletedAt: text("deleted_at"),
});
export const insertUserSchema = createInsertSchema(user);
export const selectUserSchema = createSelectSchema(user);
export type UserSchema = z.infer<typeof selectUserSchema>;
+63
View File
@@ -0,0 +1,63 @@
import db from ".";
import { hashPassword } from "../lib/crypto";
import { file } from "./schema/file";
import { user } from "./schema/user";
const main = async () => {
const [adminUser] = await db
.insert(user)
.values({
email: "[email protected]",
password: await hashPassword("123456"),
})
.returning();
await db
.insert(file)
.values([
{
userId: adminUser.id,
path: "index.html",
filename: "index.html",
content: '<p class="text-lg text-red-500">Hello world!</p>',
},
{
userId: adminUser.id,
path: "styles.css",
filename: "styles.css",
content: "body { padding: 16px; }",
},
{
userId: adminUser.id,
path: "script.js",
filename: "script.js",
content: "console.log('hello world!');",
},
{
userId: adminUser.id,
path: "_layout.html",
filename: "_layout.html",
content: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
{CONTENT}
<script src="script.js" type="module" defer></script>
</body>
</html>`,
},
])
.execute();
process.exit();
};
main();
+54
View File
@@ -0,0 +1,54 @@
import express from "express";
import { renderPage } from "vike/server";
import { IS_DEV } from "./lib/consts";
import api from "./api";
async function createServer() {
const app = express();
const root = process.cwd();
if (IS_DEV) {
// start vite development server
const { createServer: createViteServer } = await import("vite");
const vite = await createViteServer({
root,
server: { middlewareMode: true },
appType: "custom",
});
app.use(vite.middlewares);
} else {
// serve client assets
app.use(express.static(root + "/dist/client"));
}
app.use("/api", api);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
const pageContext = {};
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
const { httpResponse } = ctx;
if (!httpResponse) {
return next();
}
const { body, statusCode, headers, earlyHints } = httpResponse;
if (res.writeEarlyHints) {
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
}
headers.forEach(([name, value]) => res.setHeader(name, value));
res.status(statusCode).send(body);
});
const host = process.env.HOST || "127.0.0.1";
const port = Number(process.env.PORT) || 3000;
app.listen(port, host, () => {
console.log(`Server listening on http://${host}:${port}`);
});
}
createServer();
+2
View File
@@ -0,0 +1,2 @@
export const IS_DEV = process.env.NODE_ENV !== "production";
export const BASE_URL = process.env.BASE_URL || "";
+13
View File
@@ -0,0 +1,13 @@
import bcrypt from "bcrypt";
export const hashPassword = (password: string) => {
return bcrypt.hash(password, 10);
};
export const verifyPassword = async (hash: string, password: string) => {
try {
return await bcrypt.compare(password, hash);
} catch (err) {
return false;
}
};
+19
View File
@@ -0,0 +1,19 @@
import * as swc from "@swc/core";
export const transformJs = async (code: string) => {
try {
const result = await swc.transform(code, {
jsc: {
parser: {
jsx: true,
syntax: "ecmascript",
},
target: "es5",
},
});
return result.code;
} catch (err) {
return code;
}
};
+9
View File
@@ -0,0 +1,9 @@
import { router } from "../api/trpc";
import file from "./file";
export const appRouter = router({
file,
});
// export type definition of API
export type AppRouter = typeof appRouter;
+183
View File
@@ -0,0 +1,183 @@
import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm";
import db from "../db";
import { procedure, router } from "../api/trpc";
import { file, insertFileSchema, selectFileSchema } from "../db/schema/file";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
const fileRouter = router({
getAll: procedure
.input(
z
.object({ id: z.number().array().min(1), isPinned: z.boolean() })
.partial()
.optional()
)
.query(async ({ input: opt }) => {
const files = await db.query.file.findMany({
where: and(
isNull(file.deletedAt),
opt?.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
opt?.isPinned ? eq(file.isPinned, true) : undefined
),
orderBy: [desc(file.isDirectory), asc(file.filename)],
columns: { content: false },
});
return files;
}),
getById: procedure.input(z.number()).query(async ({ input }) => {
return db.query.file.findFirst({
where: and(eq(file.id, input), isNull(file.deletedAt)),
});
}),
create: procedure
.input(
insertFileSchema.pick({
parentId: true,
filename: true,
isDirectory: true,
})
)
.mutation(async ({ input }) => {
let basePath = "";
if (input.parentId) {
const parent = await db.query.file.findFirst({
where: and(eq(file.id, input.parentId), isNull(file.deletedAt)),
});
if (!parent?.isDirectory) {
throw new Error("Parent is not a directory!");
}
basePath = parent.path + "/";
}
const data: z.infer<typeof insertFileSchema> = {
userId: 1,
parentId: input.parentId,
path: basePath + input.filename,
filename: input.filename,
isDirectory: input.isDirectory,
};
const [result] = await db.insert(file).values(data).returning();
return result;
}),
update: procedure
.input(selectFileSchema.partial().required({ id: true }))
.mutation(async ({ input }) => {
const fileData = await db.query.file.findFirst({
where: and(eq(file.id, input.id), isNull(file.deletedAt)),
});
if (!fileData) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return db.transaction(async (tx) => {
const data = { ...input };
// also rename path
if (data.filename) {
const path = fileData.path.split("/");
const idx = path.length - 1;
path[idx] = data.filename;
data.path = path.join("/");
// recursively update files path in directory
if (data.isDirectory) {
await renameFilesPath(fileData.id, idx, data.filename, tx);
}
}
const [result] = await tx
.update(file)
.set(data)
.where(and(eq(file.id, input.id), isNull(file.deletedAt)))
.returning();
return result;
});
}),
delete: procedure.input(z.number()).mutation(async ({ input }) => {
const fileData = await db.query.file.findFirst({
where: and(eq(file.id, input), isNull(file.deletedAt)),
});
if (!fileData) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return db.transaction(async (tx) => {
const [result] = await tx
.update(file)
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(file.id, fileData.id))
.returning();
if (fileData.isDirectory) {
await deleteChildrenFiles(fileData.id, tx);
}
return result;
});
}),
});
const renameFilesPath = async (
id: number,
idx: number,
pathname: string,
tx: typeof db
) => {
const files = await tx.query.file.findMany({
where: and(eq(file.parentId, id), isNull(file.deletedAt)),
});
if (!files.length) {
return;
}
for (const f of files) {
const path = f.path.split("/");
if (idx >= 0 && idx < path.length) {
path[idx] = pathname;
}
await tx
.update(file)
.set({ path: path.join("/") })
.where(and(eq(file.id, f.id), isNull(file.deletedAt)))
.execute();
if (f.isDirectory) {
await renameFilesPath(f.id, idx, pathname, tx);
}
}
};
const deleteChildrenFiles = async (id: number, tx: typeof db) => {
const files = await tx.query.file.findMany({
where: and(eq(file.parentId, id), isNull(file.deletedAt)),
});
if (!files.length) {
return;
}
for (const f of files) {
await tx
.update(file)
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(file.id, f.id))
.execute();
if (f.isDirectory) {
await deleteChildrenFiles(f.id, tx);
}
}
};
export default fileRouter;