create-next-imagicma 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -13
- package/bin/create-next-imagicma.mjs +138 -22
- package/package.json +2 -1
- package/template/app/globals.css +331 -0
- package/template/app/layout.tsx +4 -6
- package/template/app/page.tsx +18 -40
- package/template/package.json +1 -1
- package/template/public/imagicma-picker-bridge.js +374 -0
- package/template-hono/.env.example +8 -0
- package/template-hono/.imagicma/AGENTS.md +7 -0
- package/template-hono/.imagicma/port.json +5 -0
- package/template-hono/AGENTS.md +39 -0
- package/template-hono/README.md +48 -0
- package/template-hono/client/src/App.tsx +13 -0
- package/template-hono/client/src/components/ErrorBoundary.tsx +74 -0
- package/template-hono/client/src/components/HelloClient.tsx +69 -0
- package/template-hono/client/src/globals.css +767 -0
- package/template-hono/client/src/main.tsx +24 -0
- package/template-hono/client/src/pages/HelloPage.tsx +22 -0
- package/template-hono/client/src/pages/HomePage.tsx +30 -0
- package/template-hono/client/src/providers.tsx +21 -0
- package/template-hono/components/ui/accordion.tsx +58 -0
- package/template-hono/components/ui/alert-dialog.tsx +141 -0
- package/template-hono/components/ui/alert.tsx +61 -0
- package/template-hono/components/ui/aspect-ratio.tsx +7 -0
- package/template-hono/components/ui/avatar.tsx +51 -0
- package/template-hono/components/ui/badge.tsx +40 -0
- package/template-hono/components/ui/breadcrumb.tsx +117 -0
- package/template-hono/components/ui/button.tsx +64 -0
- package/template-hono/components/ui/calendar.tsx +72 -0
- package/template-hono/components/ui/card.tsx +87 -0
- package/template-hono/components/ui/carousel.tsx +262 -0
- package/template-hono/components/ui/chart.tsx +365 -0
- package/template-hono/components/ui/checkbox.tsx +30 -0
- package/template-hono/components/ui/collapsible.tsx +11 -0
- package/template-hono/components/ui/command.tsx +153 -0
- package/template-hono/components/ui/context-menu.tsx +200 -0
- package/template-hono/components/ui/dialog.tsx +122 -0
- package/template-hono/components/ui/drawer.tsx +118 -0
- package/template-hono/components/ui/dropdown-menu.tsx +200 -0
- package/template-hono/components/ui/form.tsx +178 -0
- package/template-hono/components/ui/hover-card.tsx +29 -0
- package/template-hono/components/ui/input-otp.tsx +71 -0
- package/template-hono/components/ui/input.tsx +25 -0
- package/template-hono/components/ui/label.tsx +26 -0
- package/template-hono/components/ui/menubar.tsx +256 -0
- package/template-hono/components/ui/navigation-menu.tsx +130 -0
- package/template-hono/components/ui/pagination.tsx +119 -0
- package/template-hono/components/ui/popover.tsx +31 -0
- package/template-hono/components/ui/progress.tsx +28 -0
- package/template-hono/components/ui/radio-group.tsx +44 -0
- package/template-hono/components/ui/resizable.tsx +45 -0
- package/template-hono/components/ui/scroll-area.tsx +48 -0
- package/template-hono/components/ui/select.tsx +160 -0
- package/template-hono/components/ui/separator.tsx +31 -0
- package/template-hono/components/ui/sheet.tsx +140 -0
- package/template-hono/components/ui/sidebar.tsx +732 -0
- package/template-hono/components/ui/skeleton.tsx +17 -0
- package/template-hono/components/ui/slider.tsx +28 -0
- package/template-hono/components/ui/switch.tsx +29 -0
- package/template-hono/components/ui/table.tsx +119 -0
- package/template-hono/components/ui/tabs.tsx +55 -0
- package/template-hono/components/ui/textarea.tsx +24 -0
- package/template-hono/components/ui/toast.tsx +129 -0
- package/template-hono/components/ui/toaster.tsx +35 -0
- package/template-hono/components/ui/toggle-group.tsx +61 -0
- package/template-hono/components/ui/toggle.tsx +45 -0
- package/template-hono/components/ui/tooltip.tsx +30 -0
- package/template-hono/drizzle.config.ts +50 -0
- package/template-hono/eslint.config.mjs +13 -0
- package/template-hono/gitignore +40 -0
- package/template-hono/hooks/use-greeting.ts +15 -0
- package/template-hono/hooks/use-mobile.ts +21 -0
- package/template-hono/hooks/use-toast.ts +194 -0
- package/template-hono/index.html +13 -0
- package/template-hono/lib/queryClient.ts +59 -0
- package/template-hono/lib/theme/default-theme.ts +11 -0
- package/template-hono/lib/utils.ts +6 -0
- package/template-hono/package.json +82 -0
- package/template-hono/pnpm-lock.yaml +5162 -0
- package/template-hono/postcss.config.mjs +7 -0
- package/template-hono/process-compose.yaml +13 -0
- package/template-hono/public/favicon.ico +0 -0
- package/template-hono/public/file.svg +1 -0
- package/template-hono/public/globe.svg +1 -0
- package/template-hono/public/imagicma-picker-bridge.js +374 -0
- package/template-hono/public/next.svg +1 -0
- package/template-hono/public/vercel.svg +1 -0
- package/template-hono/public/window.svg +1 -0
- package/template-hono/scripts/imagicma-common.mjs +118 -0
- package/template-hono/scripts/imagicma-dev.mjs +29 -0
- package/template-hono/scripts/imagicma-guard.mjs +17 -0
- package/template-hono/scripts/imagicma-start.mjs +24 -0
- package/template-hono/server/app.ts +40 -0
- package/template-hono/server/db.ts +22 -0
- package/template-hono/server/dev-app.ts +5 -0
- package/template-hono/server/index.ts +94 -0
- package/template-hono/server/routes/greeting.ts +25 -0
- package/template-hono/server/storage.ts +39 -0
- package/template-hono/shared/routes.ts +13 -0
- package/template-hono/shared/schema.ts +17 -0
- package/template-hono/tailwind.config.mjs +97 -0
- package/template-hono/tsconfig.json +39 -0
- package/template-hono/tsconfig.server.json +15 -0
- package/template-hono/types/pg.d.ts +19 -0
- package/template-hono/vite.config.ts +125 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { serve } from "@hono/node-server";
|
|
4
|
+
import { createApp } from "./app";
|
|
5
|
+
|
|
6
|
+
const PORT_FILE = path.resolve(process.cwd(), ".imagicma", "port.json");
|
|
7
|
+
const LAUNCH_TOKEN_FILE = path.resolve(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
".imagicma",
|
|
10
|
+
"launch-token.json",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function isScriptLaunch(mode: "dev" | "start") {
|
|
14
|
+
return (
|
|
15
|
+
process.env.IMAGICMA_SCRIPT_LAUNCH === "1" &&
|
|
16
|
+
process.env.IMAGICMA_LAUNCH_MODE === mode
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function consumeLaunchToken(mode: "dev" | "start") {
|
|
21
|
+
let raw: string;
|
|
22
|
+
try {
|
|
23
|
+
raw = await fs.readFile(LAUNCH_TOKEN_FILE, "utf8");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
26
|
+
if (error.code === "ENOENT") return false;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const token = JSON.parse(raw);
|
|
33
|
+
const valid =
|
|
34
|
+
token?.mode === mode &&
|
|
35
|
+
Number.isInteger(token?.expiresAt) &&
|
|
36
|
+
token.expiresAt >= Date.now();
|
|
37
|
+
|
|
38
|
+
await fs.rm(LAUNCH_TOKEN_FILE, { force: true });
|
|
39
|
+
return valid;
|
|
40
|
+
} catch {
|
|
41
|
+
await fs.rm(LAUNCH_TOKEN_FILE, { force: true });
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function assertStartAuthorized() {
|
|
47
|
+
if (isScriptLaunch("start")) return;
|
|
48
|
+
if (await consumeLaunchToken("start")) return;
|
|
49
|
+
|
|
50
|
+
throw new Error(
|
|
51
|
+
"[imagicma] 禁止直接使用 node 启动。请使用 package.json scripts:pnpm start",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readLockedPort() {
|
|
56
|
+
const raw = await fs.readFile(PORT_FILE, "utf8");
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
const port = Number(data?.port);
|
|
59
|
+
|
|
60
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[imagicma] 无效端口配置:${PORT_FILE}(port=${JSON.stringify(data?.port)})`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (data?.locked !== true) {
|
|
67
|
+
throw new Error(`[imagicma] 端口配置未锁定:${PORT_FILE}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return port;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
await assertStartAuthorized();
|
|
75
|
+
|
|
76
|
+
const app = createApp({ serveClient: true });
|
|
77
|
+
const port = await readLockedPort();
|
|
78
|
+
|
|
79
|
+
serve(
|
|
80
|
+
{
|
|
81
|
+
fetch: app.fetch,
|
|
82
|
+
port,
|
|
83
|
+
},
|
|
84
|
+
(info) => {
|
|
85
|
+
console.log(`Server is running on http://localhost:${info.port}`);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main().catch((error) => {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
console.error(message);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { api } from "../../shared/routes";
|
|
3
|
+
import { storage } from "../storage";
|
|
4
|
+
|
|
5
|
+
const greetingRoute = new Hono();
|
|
6
|
+
|
|
7
|
+
greetingRoute.get(api.greeting.get.path, async (c) => {
|
|
8
|
+
try {
|
|
9
|
+
const message = await storage.getMessage();
|
|
10
|
+
const body = { message };
|
|
11
|
+
|
|
12
|
+
api.greeting.get.responses[200].parse(body);
|
|
13
|
+
|
|
14
|
+
return c.json(body);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("GET /api/greeting failed:", error);
|
|
17
|
+
|
|
18
|
+
const message =
|
|
19
|
+
error instanceof Error ? error.message : "Internal Server Error";
|
|
20
|
+
|
|
21
|
+
return c.json({ message }, 500);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export { greetingRoute };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { desc } from "drizzle-orm";
|
|
2
|
+
import { messages } from "../shared/schema";
|
|
3
|
+
import { db } from "./db";
|
|
4
|
+
|
|
5
|
+
export interface IStorage {
|
|
6
|
+
getMessage(): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DatabaseStorage implements IStorage {
|
|
10
|
+
async getMessage(): Promise<string> {
|
|
11
|
+
if (!db) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"DATABASE_URL 未设置:请在 .env.local 中配置 Postgres 连接串,然后执行 `npm run db:push`。",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const rows = await db
|
|
19
|
+
.select({ content: messages.content })
|
|
20
|
+
.from(messages)
|
|
21
|
+
.orderBy(desc(messages.id))
|
|
22
|
+
.limit(1);
|
|
23
|
+
|
|
24
|
+
return rows[0]?.content ?? "Hello from the Backend API!";
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("DatabaseStorage.getMessage() failed:", error);
|
|
27
|
+
|
|
28
|
+
if (error instanceof Error && error.message.includes("DATABASE_URL")) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(
|
|
33
|
+
"数据库读取失败:请确认已设置 DATABASE_URL,并先执行 `npm run db:push` 创建表结构。",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const storage = new DatabaseStorage();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { pgTable, text, serial, boolean } from "drizzle-orm/pg-core";
|
|
2
|
+
import { createInsertSchema } from "drizzle-zod";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const messages = pgTable("messages", {
|
|
6
|
+
id: serial("id").primaryKey(),
|
|
7
|
+
content: text("content").notNull(),
|
|
8
|
+
isRead: boolean("is_read").default(false),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const insertMessageSchema = createInsertSchema(messages).pick({
|
|
12
|
+
content: true,
|
|
13
|
+
isRead: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type Message = typeof messages.$inferSelect;
|
|
17
|
+
export type InsertMessage = z.infer<typeof insertMessageSchema>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import animate from "tailwindcss-animate";
|
|
2
|
+
|
|
3
|
+
/** @type {import("tailwindcss").Config} */
|
|
4
|
+
const config = {
|
|
5
|
+
darkMode: ["class"],
|
|
6
|
+
content: [
|
|
7
|
+
"./index.html",
|
|
8
|
+
"./client/src/**/*.{ts,tsx}",
|
|
9
|
+
"./components/**/*.{ts,tsx}",
|
|
10
|
+
"./hooks/**/*.{ts,tsx}",
|
|
11
|
+
"./lib/**/*.{ts,tsx}",
|
|
12
|
+
"./shared/**/*.{ts,tsx}",
|
|
13
|
+
],
|
|
14
|
+
theme: {
|
|
15
|
+
extend: {
|
|
16
|
+
borderRadius: {
|
|
17
|
+
lg: "var(--radius)",
|
|
18
|
+
md: "calc(var(--radius) - 2px)",
|
|
19
|
+
sm: "calc(var(--radius) - 4px)",
|
|
20
|
+
},
|
|
21
|
+
colors: {
|
|
22
|
+
background: "hsl(var(--background) / <alpha-value>)",
|
|
23
|
+
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
|
24
|
+
border: "hsl(var(--border) / <alpha-value>)",
|
|
25
|
+
input: "hsl(var(--input) / <alpha-value>)",
|
|
26
|
+
ring: "hsl(var(--ring) / <alpha-value>)",
|
|
27
|
+
card: {
|
|
28
|
+
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
|
29
|
+
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
|
|
30
|
+
border: "hsl(var(--card-border) / <alpha-value>)",
|
|
31
|
+
},
|
|
32
|
+
popover: {
|
|
33
|
+
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
|
34
|
+
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
|
|
35
|
+
border: "hsl(var(--popover-border) / <alpha-value>)",
|
|
36
|
+
},
|
|
37
|
+
primary: {
|
|
38
|
+
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
|
39
|
+
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
|
|
40
|
+
border: "var(--primary-border)",
|
|
41
|
+
},
|
|
42
|
+
secondary: {
|
|
43
|
+
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
|
44
|
+
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
|
|
45
|
+
border: "var(--secondary-border)",
|
|
46
|
+
},
|
|
47
|
+
muted: {
|
|
48
|
+
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
|
49
|
+
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
|
|
50
|
+
border: "var(--muted-border)",
|
|
51
|
+
},
|
|
52
|
+
accent: {
|
|
53
|
+
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
|
54
|
+
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
|
|
55
|
+
border: "var(--accent-border)",
|
|
56
|
+
},
|
|
57
|
+
destructive: {
|
|
58
|
+
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
|
59
|
+
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
|
60
|
+
border: "var(--destructive-border)",
|
|
61
|
+
},
|
|
62
|
+
chart: {
|
|
63
|
+
"1": "hsl(var(--chart-1) / <alpha-value>)",
|
|
64
|
+
"2": "hsl(var(--chart-2) / <alpha-value>)",
|
|
65
|
+
"3": "hsl(var(--chart-3) / <alpha-value>)",
|
|
66
|
+
"4": "hsl(var(--chart-4) / <alpha-value>)",
|
|
67
|
+
"5": "hsl(var(--chart-5) / <alpha-value>)",
|
|
68
|
+
},
|
|
69
|
+
sidebar: {
|
|
70
|
+
DEFAULT: "hsl(var(--sidebar) / <alpha-value>)",
|
|
71
|
+
foreground: "hsl(var(--sidebar-foreground) / <alpha-value>)",
|
|
72
|
+
border: "hsl(var(--sidebar-border) / <alpha-value>)",
|
|
73
|
+
ring: "hsl(var(--sidebar-ring) / <alpha-value>)",
|
|
74
|
+
accent: "hsl(var(--accent) / <alpha-value>)",
|
|
75
|
+
"accent-foreground": "hsl(var(--accent-foreground) / <alpha-value>)",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
keyframes: {
|
|
79
|
+
"accordion-down": {
|
|
80
|
+
from: { height: "0" },
|
|
81
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
82
|
+
},
|
|
83
|
+
"accordion-up": {
|
|
84
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
85
|
+
to: { height: "0" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
animation: {
|
|
89
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
90
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
plugins: [animate],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default config;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"types": ["node", "vite/client"],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"],
|
|
19
|
+
"@shared/*": ["./shared/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": [
|
|
23
|
+
"client/src/**/*.ts",
|
|
24
|
+
"client/src/**/*.tsx",
|
|
25
|
+
"components/**/*.ts",
|
|
26
|
+
"components/**/*.tsx",
|
|
27
|
+
"hooks/**/*.ts",
|
|
28
|
+
"hooks/**/*.tsx",
|
|
29
|
+
"lib/**/*.ts",
|
|
30
|
+
"lib/**/*.tsx",
|
|
31
|
+
"shared/**/*.ts",
|
|
32
|
+
"shared/**/*.tsx",
|
|
33
|
+
"server/**/*.ts",
|
|
34
|
+
"types/**/*.d.ts",
|
|
35
|
+
"vite.config.ts",
|
|
36
|
+
"drizzle.config.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": ["node_modules", "dist"]
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["server/**/*.ts", "shared/**/*.ts", "types/**/*.d.ts"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare module "pg" {
|
|
2
|
+
export type PoolConfig = {
|
|
3
|
+
connectionString?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class Pool {
|
|
8
|
+
constructor(config?: PoolConfig);
|
|
9
|
+
query: (...args: unknown[]) => Promise<unknown>;
|
|
10
|
+
end: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pg: {
|
|
14
|
+
Pool: typeof Pool;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default pg;
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { defineConfig } from "vite";
|
|
5
|
+
import react from "@vitejs/plugin-react";
|
|
6
|
+
import devServer, { defaultOptions } from "@hono/vite-dev-server";
|
|
7
|
+
import nodeAdapter from "@hono/vite-dev-server/node";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const PORT_FILE = path.resolve(__dirname, ".imagicma", "port.json");
|
|
13
|
+
const LAUNCH_TOKEN_FILE = path.resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
".imagicma",
|
|
16
|
+
"launch-token.json",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
function isScriptLaunch(mode: "dev" | "start") {
|
|
20
|
+
return (
|
|
21
|
+
process.env.IMAGICMA_SCRIPT_LAUNCH === "1" &&
|
|
22
|
+
process.env.IMAGICMA_LAUNCH_MODE === mode
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readLockedPort() {
|
|
27
|
+
const raw = await fs.readFile(PORT_FILE, "utf8");
|
|
28
|
+
const data = JSON.parse(raw);
|
|
29
|
+
const port = Number(data?.port);
|
|
30
|
+
|
|
31
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[imagicma] 无效端口配置:${PORT_FILE}(port=${JSON.stringify(data?.port)})`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (data?.locked !== true) {
|
|
38
|
+
throw new Error(`[imagicma] 端口配置未锁定:${PORT_FILE}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function consumeLaunchToken(mode: "dev" | "start") {
|
|
45
|
+
let raw: string;
|
|
46
|
+
try {
|
|
47
|
+
raw = await fs.readFile(LAUNCH_TOKEN_FILE, "utf8");
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
50
|
+
if (error.code === "ENOENT") return false;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const token = JSON.parse(raw);
|
|
57
|
+
const valid =
|
|
58
|
+
token?.mode === mode &&
|
|
59
|
+
Number.isInteger(token?.expiresAt) &&
|
|
60
|
+
token.expiresAt >= Date.now();
|
|
61
|
+
|
|
62
|
+
await fs.rm(LAUNCH_TOKEN_FILE, { force: true });
|
|
63
|
+
return valid;
|
|
64
|
+
} catch {
|
|
65
|
+
await fs.rm(LAUNCH_TOKEN_FILE, { force: true });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function assertLaunchAuthorized(mode: "dev" | "start") {
|
|
71
|
+
if (isScriptLaunch(mode)) return;
|
|
72
|
+
if (await consumeLaunchToken(mode)) return;
|
|
73
|
+
|
|
74
|
+
throw new Error(
|
|
75
|
+
"[imagicma] 禁止直接使用 vite 启动。请使用 package.json scripts:pnpm dev",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default defineConfig(async ({ command }) => {
|
|
80
|
+
const lockedPort = await readLockedPort();
|
|
81
|
+
|
|
82
|
+
if (command === "serve") {
|
|
83
|
+
await assertLaunchAuthorized("dev");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
server: {
|
|
88
|
+
host: "0.0.0.0",
|
|
89
|
+
port: lockedPort,
|
|
90
|
+
strictPort: true,
|
|
91
|
+
},
|
|
92
|
+
plugins: [
|
|
93
|
+
react(),
|
|
94
|
+
devServer({
|
|
95
|
+
entry: "server/dev-app.ts",
|
|
96
|
+
adapter: nodeAdapter,
|
|
97
|
+
exclude: [/^\/(?!api(?:\/|$)).*/, ...defaultOptions.exclude],
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
name: "imagicma-lock-port",
|
|
101
|
+
configResolved(resolvedConfig: import("vite").ResolvedConfig) {
|
|
102
|
+
if (resolvedConfig.command !== "serve") return;
|
|
103
|
+
if (resolvedConfig.server.port !== lockedPort) {
|
|
104
|
+
console.warn(
|
|
105
|
+
`[imagicma] 检测到 dev 端口被覆盖(${resolvedConfig.server.port}),已强制恢复为 ${lockedPort}`,
|
|
106
|
+
);
|
|
107
|
+
resolvedConfig.server.port = lockedPort;
|
|
108
|
+
}
|
|
109
|
+
resolvedConfig.server.strictPort = true;
|
|
110
|
+
resolvedConfig.server.host = "0.0.0.0";
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
resolve: {
|
|
115
|
+
alias: {
|
|
116
|
+
"@": path.resolve(__dirname, "."),
|
|
117
|
+
"@shared": path.resolve(__dirname, "shared"),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
build: {
|
|
121
|
+
outDir: path.resolve(__dirname, "dist/client"),
|
|
122
|
+
emptyOutDir: true,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|