mirror of
https://github.com/khairul169/code-share.git
synced 2026-10-24 11:53:58 +07:00
feat: rebuild app using vike
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>>;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createContext } from "./context";
|
||||
import { appRouter } from "../../routers/_app";
|
||||
import { createCallerFactory } from ".";
|
||||
|
||||
const trpcServer = createCallerFactory(appRouter)(createContext);
|
||||
|
||||
export default trpcServer;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import db from "./index";
|
||||
|
||||
migrate(db, { migrationsFolder: __dirname + "/drizzle" });
|
||||
process.exit();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { user } from "./user";
|
||||
export { file } from "./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>;
|
||||
@@ -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>;
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -0,0 +1,2 @@
|
||||
export const IS_DEV = process.env.NODE_ENV !== "production";
|
||||
export const BASE_URL = process.env.BASE_URL || "";
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user