mirror of
https://github.com/2234839/web-font.git
synced 2026-07-04 07:02:32 +08:00
修复上传功能、移除 tjs 运行时支持、添加 vite-plugin-pilot
- 修复 HTTP header 大小写不敏感匹配,解决浏览器上传请求体为空的问题
- 重写 createStreamAfterTarget,修复大文件上传时 body stream 数据流断裂
- 添加 chunked transfer encoding 解码支持
- 读取完 body 后 cancel stream,防止后台循环抛异常炸进程
- 修复 parseHttpRequest 中 split(":") 对含冒号 header value 的错误拆分
- 临时上传同名文件直接覆盖,不再加时间戳前缀
- 移除 tjs (txiki.js) 运行时支持及相关代码
- 安装并配置 vite-plugin-pilot 浏览器测试工具
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
43a1d2cf64
commit
878b54a0fd
3
.gitignore
vendored
3
.gitignore
vendored
@ -29,4 +29,5 @@ tjs
|
||||
app
|
||||
*.tar
|
||||
|
||||
dist_backend
|
||||
dist_backend
|
||||
.pilot
|
||||
7
CLAUDE.md
Normal file
7
CLAUDE.md
Normal file
@ -0,0 +1,7 @@
|
||||
pnpm dev 可以同时启动前后端,并且都会修改代码后自动重载
|
||||
|
||||
## 浏览器测试(vite-plugin-pilot)
|
||||
已安装。`npx pilot run '代码'` 执行 JS(返回结果+日志+快照)、`npx pilot page` 页面状态
|
||||
`npx pilot help` 查看pilot所有功能
|
||||
常用:`__pilot_clickByText("文本")` 点击、`__pilot_typeByPlaceholder("提示文字", "值")` 输入、`__pilot_waitFor("文本")` 等待、`__pilot_findByText("文本")` 查找。snapshot 中 `#N` 是元素索引。
|
||||
多 tab 时用 `npx pilot status` 查看实例列表,`npx pilot run '代码' instance:前缀` 指定目标实例(支持 ID 前缀模糊匹配)。
|
||||
@ -26,7 +26,7 @@ ui 需要展现一些特定的字体,但直接引入字体包又过大,于
|
||||
|
||||
## 安装与使用
|
||||
|
||||
### 使用 node / tjs / llrt 等运行时
|
||||
### 使用 node / llrt 等运行时
|
||||
|
||||
拉取项目,并将字体文件放到项目内的 font 目录下,然后运行:
|
||||
|
||||
@ -34,7 +34,6 @@ pnpm install && pnpm build && pnpm build_backend
|
||||
|
||||
node ./dist_backend/app.cjs
|
||||
llrt ./dist_backend/app.cjs
|
||||
tjs run ./dist_backend/app.cjs
|
||||
|
||||
|
||||
### 使用 docker 安装
|
||||
|
||||
@ -12,12 +12,10 @@ import { enableTempUpload, adminApiKey, fontDirs } from "./config";
|
||||
import { parseMultipart } from "./multipart";
|
||||
import { handleTempUpload, handleAdminUpload } from "./upload";
|
||||
|
||||
let release_name = global.tjs ? "tjs" : globalThis?.process?.release?.name;
|
||||
let release_name = globalThis?.process?.release?.name;
|
||||
|
||||
let runtimeReady: Promise<void>;
|
||||
if (release_name === "tjs") {
|
||||
runtimeReady = import("./server/tjs").then(() => {});
|
||||
} else if (release_name === "node" || release_name === "llrt") {
|
||||
if (release_name === "node" || release_name === "llrt") {
|
||||
runtimeReady = import("./server/node").then(() => {});
|
||||
} else {
|
||||
runtimeReady = Promise.resolve();
|
||||
|
||||
@ -27,13 +27,9 @@ export class SimpleHttpServer {
|
||||
private router: cRouter = new cRouter();
|
||||
|
||||
constructor(options: { port: number; hostname?: string }) {
|
||||
let release_name = global.tjs ? "tjs" : globalThis?.process?.release?.name;
|
||||
const release_name = globalThis?.process?.release?.name;
|
||||
console.log("[release.name]", release_name);
|
||||
if (global.tjs) {
|
||||
this.tjsServer(options);
|
||||
return this;
|
||||
}
|
||||
if (release_name === "llrt" || release_name == "node") {
|
||||
if (release_name === "llrt" || release_name === "node") {
|
||||
import("./tcp_server").then((m) => {
|
||||
const server = m.createTcpServer((socket) => {
|
||||
connectionHandle(socket, (req, res) => this.router.handle(req, res));
|
||||
@ -44,18 +40,6 @@ export class SimpleHttpServer {
|
||||
});
|
||||
}
|
||||
}
|
||||
private async tjsServer(options: { port: number; hostname?: string }) {
|
||||
const listener = (await global.tjs.listen(
|
||||
"tcp",
|
||||
options.hostname ?? "::",
|
||||
options.port,
|
||||
{},
|
||||
)) as tjs.Listener;
|
||||
console.log(`Server is listening on port ${options.port}`);
|
||||
for await (const connection of listener) {
|
||||
connectionHandle(connection, this.router.handle.bind(this.router));
|
||||
}
|
||||
}
|
||||
use(...middlewares: cMiddleware[]) {
|
||||
middlewares.forEach((middleware) => this.router.use(middleware));
|
||||
return this;
|
||||
@ -63,6 +47,18 @@ export class SimpleHttpServer {
|
||||
}
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/** 合并多个 Uint8Array 为单个 ArrayBuffer */
|
||||
function mergeChunks(chunks: Uint8Array[], totalLength: number): ArrayBuffer {
|
||||
const merged = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
merged.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength);
|
||||
}
|
||||
|
||||
// 请求头终止符
|
||||
const target = encoder.encode("\r\n\r\n");
|
||||
async function connectionHandle(
|
||||
@ -81,11 +77,20 @@ async function connectionHandle(
|
||||
const httpHeaderText = decoder.decode(header);
|
||||
const httpHeader = parseHttpRequest(httpHeaderText);
|
||||
const hasBody = httpHeader.method !== "GET" && httpHeader.method !== "HEAD";
|
||||
/** 根据 Content-Length 读取指定长度的 body,避免在 keep-alive 连接上无限等待 */
|
||||
/** 大小写不敏感查找 header */
|
||||
const getHeader = (name: string) => {
|
||||
const lower = name.toLowerCase();
|
||||
for (const key of Object.keys(httpHeader.headers)) {
|
||||
if (key.toLowerCase() === lower) return httpHeader.headers[key];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
/** 读取请求体 */
|
||||
let bodyArrayBuffer: ArrayBuffer | undefined;
|
||||
if (hasBody && body) {
|
||||
const contentLength = parseInt(httpHeader.headers["Content-Length"] ?? "0", 10);
|
||||
const contentLength = parseInt(getHeader("Content-Length") ?? "0", 10);
|
||||
if (contentLength > 0) {
|
||||
/** 根据 Content-Length 读取指定长度的 body */
|
||||
const chunks: Uint8Array[] = [];
|
||||
let received = 0;
|
||||
for await (const chunk of body) {
|
||||
@ -93,18 +98,77 @@ async function connectionHandle(
|
||||
received += chunk.length;
|
||||
if (received >= contentLength) break;
|
||||
}
|
||||
const merged = new Uint8Array(received);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
merged.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
body.cancel?.();
|
||||
bodyArrayBuffer = mergeChunks(chunks, received);
|
||||
} else if (getHeader("Transfer-Encoding") === "chunked") {
|
||||
/** 解码 chunked transfer encoding */
|
||||
const chunks: Uint8Array[] = [];
|
||||
let totalLength = 0;
|
||||
const chunkBuf: number[] = [];
|
||||
let state: "size" | "data" | "crlf_after_data" = "size";
|
||||
let chunkSize = 0;
|
||||
let dataRead = 0;
|
||||
let goto_done = false;
|
||||
for await (const rawChunk of body) {
|
||||
for (const byte of rawChunk) {
|
||||
switch (state) {
|
||||
case "size": {
|
||||
if (byte === 13) continue; // \r
|
||||
if (byte === 10) {
|
||||
// \n — size 行结束
|
||||
const sizeStr = new TextDecoder().decode(new Uint8Array(chunkBuf)).trim();
|
||||
chunkSize = parseInt(sizeStr, 16);
|
||||
chunkBuf.length = 0;
|
||||
if (chunkSize === 0) {
|
||||
state = "crlf_after_data"; // 最后一个空行
|
||||
} else {
|
||||
state = "data";
|
||||
dataRead = 0;
|
||||
}
|
||||
} else {
|
||||
chunkBuf.push(byte);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "data": {
|
||||
chunkBuf.push(byte);
|
||||
dataRead++;
|
||||
if (dataRead >= chunkSize) {
|
||||
const data = new Uint8Array(chunkBuf);
|
||||
chunks.push(data);
|
||||
totalLength += data.length;
|
||||
chunkBuf.length = 0;
|
||||
state = "crlf_after_data";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "crlf_after_data": {
|
||||
// 跳过 trailing \r\n
|
||||
if (byte === 10) {
|
||||
if (chunkSize === 0) {
|
||||
// 结束标记后的 \n
|
||||
state = "size";
|
||||
goto_done = true;
|
||||
} else {
|
||||
state = "size";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (goto_done) break;
|
||||
}
|
||||
if (goto_done) break;
|
||||
}
|
||||
bodyArrayBuffer = merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength);
|
||||
} else if (httpHeader.headers["Transfer-Encoding"] === "chunked") {
|
||||
/** chunked transfer encoding 暂不支持,跳过 body */
|
||||
if (totalLength > 0) {
|
||||
bodyArrayBuffer = mergeChunks(chunks, totalLength);
|
||||
}
|
||||
body.cancel?.();
|
||||
} else {
|
||||
/** 无 Content-Length 且非 chunked,暂不处理 */
|
||||
}
|
||||
}
|
||||
const rawReq = new Request("http://" + httpHeader.headers["Host"] + httpHeader.url, {
|
||||
const rawReq = new Request("http://" + (getHeader("Host") ?? "localhost") + httpHeader.url, {
|
||||
method: httpHeader.method,
|
||||
headers: httpHeader.headers,
|
||||
});
|
||||
@ -121,22 +185,13 @@ async function connectionHandle(
|
||||
const resHeaertText = `HTTP/1.1 ${res.status} OK\r\n${headerText.join("\r\n")}\r\n\r\n`;
|
||||
await resWriter.write(encoder.encode(resHeaertText));
|
||||
if (res.body) {
|
||||
// node 运行时
|
||||
// 释放写入器的锁定
|
||||
/** node 运行时 */
|
||||
resWriter.releaseLock();
|
||||
// https://github.com/saghul/txiki.js/issues/646
|
||||
await res.body?.pipeTo(connection.writable);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
if (res._bodyInit) {
|
||||
// tjs 运行时
|
||||
// @ts-expect-error
|
||||
await resWriter.write(res._bodyInit);
|
||||
} else {
|
||||
// llrt 运行时
|
||||
const buffer = new Uint8Array(await (await res.blob()).arrayBuffer());
|
||||
await resWriter.write(buffer);
|
||||
}
|
||||
/** llrt 运行时 */
|
||||
const buffer = new Uint8Array(await (await res.blob()).arrayBuffer());
|
||||
await resWriter.write(buffer);
|
||||
}
|
||||
if (!resWriter.closed) {
|
||||
await resWriter.close();
|
||||
@ -161,11 +216,13 @@ function parseHttpRequest(requestText: string) {
|
||||
const headers: Record<string, string> = {};
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line === "") break; // 空行表示头部结束
|
||||
if (line === "" || line === "\r") break; // 空行表示头部结束
|
||||
|
||||
const [key, ...valueParts] = line.split(":");
|
||||
const value = valueParts.join(":").trim();
|
||||
headers[key.trim()] = value;
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
const value = line.slice(colonIndex + 1).trim();
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -175,58 +232,69 @@ function parseHttpRequest(requestText: string) {
|
||||
headers,
|
||||
};
|
||||
}
|
||||
async function createStreamAfterTarget(
|
||||
function createStreamAfterTarget(
|
||||
originalStream: ReadableStream<Uint8Array>,
|
||||
target: Uint8Array,
|
||||
) {
|
||||
): Promise<{ header: Uint8Array | null; body: ReadableStream<Uint8Array> }> {
|
||||
const reader = originalStream.getReader();
|
||||
let buffer = new Uint8Array();
|
||||
|
||||
// Function to check if target is found in the buffer
|
||||
function containsTarget(buffer: Uint8Array, target: Uint8Array): number {
|
||||
for (let i = 0; i <= buffer.length - target.length; i++) {
|
||||
if (buffer.slice(i, i + target.length).every((value, index) => value === target[index])) {
|
||||
function containsTarget(buf: Uint8Array, tgt: Uint8Array): number {
|
||||
for (let i = 0; i <= buf.length - tgt.length; i++) {
|
||||
if (buf.slice(i, i + tgt.length).every((value, index) => value === tgt[index])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
let controller = null as unknown as ReadableStreamDefaultController<Uint8Array>;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
break; // Stream ended
|
||||
}
|
||||
if (controller) {
|
||||
controller.enqueue(value);
|
||||
continue;
|
||||
}
|
||||
// Append the new chunk to the buffer
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length);
|
||||
newBuffer.set(buffer);
|
||||
newBuffer.set(value, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
// Check if the target is found in the buffer
|
||||
const targetIndex = containsTarget(buffer, target);
|
||||
if (targetIndex !== -1) {
|
||||
// Found the target data, return the remaining buffer after the target data
|
||||
const start = targetIndex + target.length;
|
||||
const header = buffer.slice(0, start);
|
||||
const remainingData = buffer.slice(start);
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(c) {
|
||||
controller = c;
|
||||
controller.enqueue(remainingData);
|
||||
},
|
||||
return new Promise((resolve) => {
|
||||
function pump() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
resolve({ header: null, body: new ReadableStream<Uint8Array>() });
|
||||
return;
|
||||
}
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length);
|
||||
newBuffer.set(buffer);
|
||||
newBuffer.set(value, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
const targetIndex = containsTarget(buffer, target);
|
||||
if (targetIndex === -1) {
|
||||
pump();
|
||||
return;
|
||||
}
|
||||
const start = targetIndex + target.length;
|
||||
const header = buffer.slice(0, start);
|
||||
const remainingData = buffer.slice(start);
|
||||
|
||||
/** body stream:先写入剩余数据,然后在后台继续从 originalStream 读取 */
|
||||
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(c) {
|
||||
controller = c;
|
||||
if (remainingData.length > 0) {
|
||||
controller.enqueue(remainingData);
|
||||
}
|
||||
/** 后台继续读取 originalStream 并转发到 body stream */
|
||||
(async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) { controller.close(); return; }
|
||||
controller.enqueue(value);
|
||||
}
|
||||
} catch (err) {
|
||||
/** body stream 被消费方 cancel 时预期会抛错 */
|
||||
console.log("[createStreamAfterTarget] body stream closed:", err);
|
||||
}
|
||||
})();
|
||||
},
|
||||
});
|
||||
resolve({ header, body });
|
||||
});
|
||||
// Create a new stream from the remaining data
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { header: null, body: new ReadableStream<Uint8Array>() }; // Return an empty stream if the target is not found
|
||||
pump();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import { implInterface } from "../interface";
|
||||
|
||||
implInterface({
|
||||
async stat(path) {
|
||||
const r = await tjs.stat(path);
|
||||
return {
|
||||
isFile: () => r.isFile,
|
||||
size: r.size,
|
||||
};
|
||||
},
|
||||
readFile(path) {
|
||||
return tjs.readFile(path);
|
||||
},
|
||||
writeFile(path, data) {
|
||||
return tjs.writeFile(path, data);
|
||||
},
|
||||
async readdir(path) {
|
||||
const entries: { isFile: () => boolean; name: string }[] = [];
|
||||
const dir = await tjs.readDir(path);
|
||||
for await (const entry of dir) {
|
||||
entries.push({
|
||||
isFile: () => entry.isFile,
|
||||
name: entry.name,
|
||||
});
|
||||
}
|
||||
await dir.close();
|
||||
return entries;
|
||||
},
|
||||
/** TJS 没有 mkdir,通过写入占位文件来确保目录存在 */
|
||||
async mkdir(path) {
|
||||
const placeholder = path + "/.keep";
|
||||
await tjs.writeFile(placeholder, new Uint8Array(0));
|
||||
},
|
||||
unlink(path) {
|
||||
return tjs.remove(path);
|
||||
},
|
||||
});
|
||||
@ -14,23 +14,6 @@ function sanitizeFilename(filename: string): string {
|
||||
return filename.replace(/[/\\]/g, "").replace(/[\x00-\x1f]/g, "");
|
||||
}
|
||||
|
||||
/** 生成带时间戳前缀的文件名,用于 FIFO 排序 */
|
||||
function generateTempFilename(originalName: string): string {
|
||||
const ts = Date.now().toString(36);
|
||||
const ext = originalName.includes(".") ? "." + originalName.split(".").pop() : ".ttf";
|
||||
const base = originalName.replace(/\.[^.]+$/, "");
|
||||
return `${ts}_${base}${ext}`;
|
||||
}
|
||||
|
||||
/** 获取临时目录中的字体文件列表 */
|
||||
async function getTempFiles(): Promise<string[]> {
|
||||
const entries = await readdir("font/temp");
|
||||
return entries
|
||||
.filter((e) => e.isFile() && isAllowedFontFile(e.name))
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** 确保目录存在,不存在则创建 */
|
||||
async function ensureDir(dir: string) {
|
||||
const { stat, mkdir } = await import("./interface");
|
||||
@ -57,20 +40,24 @@ export async function handleTempUpload(fileData: { data: Uint8Array; filename: s
|
||||
|
||||
await ensureDir("font/temp");
|
||||
|
||||
const existingFiles = await getTempFiles();
|
||||
const filename = sanitizeFilename(fileData.filename);
|
||||
const filePath = path_join("font/temp", filename);
|
||||
|
||||
// FIFO: 超出上限时删除最早的文件
|
||||
if (existingFiles.length >= tempMaxFiles) {
|
||||
const toDelete = existingFiles[0];
|
||||
try {
|
||||
await unlink(path_join("font/temp", toDelete));
|
||||
} catch {
|
||||
// 删除失败不影响上传
|
||||
/** 同名文件直接覆盖,否则检查文件数量上限 */
|
||||
try {
|
||||
await (await import("./interface")).stat(filePath);
|
||||
} catch {
|
||||
const entries = await readdir("font/temp");
|
||||
const count = entries.filter((e) => e.isFile() && isAllowedFontFile(e.name)).length;
|
||||
if (count >= tempMaxFiles) {
|
||||
const toDelete = entries.find((e) => e.isFile() && isAllowedFontFile(e.name));
|
||||
if (toDelete) {
|
||||
try { await unlink(path_join("font/temp", toDelete.name)); } catch { /* 删除失败不影响上传 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filename = generateTempFilename(sanitizeFilename(fileData.filename));
|
||||
await writeFile(path_join("font/temp", filename), fileData.data);
|
||||
await writeFile(filePath, fileData.data);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"typescript": "^6.0.2",
|
||||
"undici": "^8.0.2",
|
||||
"vite": "^8.0.7",
|
||||
"vite-plugin-pilot": "^1.0.19",
|
||||
"vite-plugin-solid": "^2.11.12"
|
||||
}
|
||||
}
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -36,6 +36,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^8.0.7
|
||||
version: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)
|
||||
vite-plugin-pilot:
|
||||
specifier: ^1.0.19
|
||||
version: 1.0.19(magic-string@0.30.21)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7))
|
||||
vite-plugin-solid:
|
||||
specifier: ^2.11.12
|
||||
version: 2.11.12(solid-js@1.9.12)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7))
|
||||
@ -995,6 +998,20 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
vite-plugin-pilot@1.0.19:
|
||||
resolution: {integrity: sha512-8Oa+1P+oP8lV1zemyi2Mzz8I2LRPWEyR7Zi7F8l30yNBrGlYsgJvJePSF73U2l/tUdRaF3V7D4ET3Na68Lqvzg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@vue/compiler-dom': '>=3.3'
|
||||
magic-string: '>=0.30'
|
||||
vite: '>=5'
|
||||
peerDependenciesMeta:
|
||||
'@vue/compiler-dom':
|
||||
optional: true
|
||||
magic-string:
|
||||
optional: true
|
||||
|
||||
vite-plugin-solid@2.11.12:
|
||||
resolution: {integrity: sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==}
|
||||
peerDependencies:
|
||||
@ -1860,6 +1877,12 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
vite-plugin-pilot@1.0.19(magic-string@0.30.21)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)
|
||||
optionalDependencies:
|
||||
magic-string: 0.30.21
|
||||
|
||||
vite-plugin-solid@2.11.12(solid-js@1.9.12)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
import { pilot } from "vite-plugin-pilot";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
plugins: [solid(), pilot({ locale: "zh" })],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user