@picloom/create-theme 0.1.0 → 0.1.2

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 CHANGED
@@ -1,34 +1,22 @@
1
1
  # @picloom/create-theme
2
2
 
3
- Create a standalone Vue 3 Picloom theme with Bun 1.4.3+, simulated preview,
4
- component/style HMR and ZIP packaging. No application checkout, Node/npm CLI or
5
- system `zip` is required.
6
-
7
- Create a project:
3
+ Create a standalone Picloom Vue 3 theme with Bun 1.4.3+.
8
4
 
9
5
  ```sh
10
- bun create @picloom/theme@0.1.0 ./my-theme
6
+ bun create @picloom/theme@0.1.2 ./my-theme
11
7
  cd my-theme
12
8
  bun install
13
9
  bun run dev
10
+ bun run typecheck
14
11
  bun run build
15
- bun run preview
16
12
  ```
17
13
 
18
- The target must not exist; its parent must exist. The creator copies files only;
19
- it does not install dependencies, initialize Git or publish. Both home and login
20
- pages start from the same source used by Picloom's built-in default theme.
21
- `--help` prints usage.
14
+ The target directory must not exist and its parent must exist. Follow the generated README to edit and install the ZIP. The dev server provides a simulated preview. Type checking and building are separate commands.
22
15
 
23
- To test local packages, use the `.tgz` workflow in
24
- [theme tooling](https://llleixx.github.io/picloom/development/theme-tooling.html).
25
- Maintainers run `bun run build:create-theme` before `bun pm pack --ignore-scripts`.
26
- The build copies `themes/default/` into the distribution template and bundles
27
- shared pure helpers into private preview tooling. Do not edit `dist/template/`.
16
+ [Theme tutorial](https://llleixx.github.io/picloom/development/theme-tooling.html)
28
17
 
29
18
  ## 中文
30
19
 
31
- 基于宿主同一份默认主题源码创建独立 Vue 3 主题。提供模拟预览、热更新、生产产物预览与 ZIP 打包。
32
- 创建工具只复制文件,不覆盖已有目录、不安装依赖、不初始化 Git、不发布。
33
- 模拟预览不连接真实服务;上传、登录与权限必须在隔离测试实例安装后验证。
34
- 通过 npm 创建项目,也可按文档使用本地 `.tgz` 验证开发版本。模板在创建工具构建时自动生成,不手工维护第二份默认主题。
20
+ 使用 Bun 1.4.3+ 创建独立 Vue 3 主题。按上方命令创建工程、安装依赖、检查类型并构建。目标目录必须不存在,父目录必须存在。
21
+
22
+ 开发服务器提供模拟预览。类型检查与构建分别执行;修改文件和安装 ZIP 的步骤见生成项目中的 README。
@@ -1,57 +1,50 @@
1
1
  # Picloom theme / 主题开发
2
2
 
3
- Requires Bun 1.4.3+. This project starts from Picloom's built-in default theme,
4
- uses only the public Theme SDK, and bundles its own Vue runtime.
3
+ A Vue 3 theme for Picloom home and login pages. Requires Bun 1.4.3+.
4
+
5
+ ## Develop
5
6
 
6
7
  ```sh
7
8
  bun install
8
9
  bun run dev
10
+ ```
11
+
12
+ Open `http://127.0.0.1:5173/`; use `bun run dev --port 5174` to change the port. The preview panel simulates pages, languages, appearance, login and upload states.
13
+
14
+ - `theme.json`: theme ID, version, localized text and configuration schema.
15
+ - `src/Page.vue`, `src/style.css`: pages and styles, with hot updates.
16
+ - `src/entry.ts`: mounts the theme and disposes it on unmount.
17
+ - `public/assets/`: package resources, read with `context.asset("assets/...")`.
18
+ - `dev/config.json`: created on first preview from schema defaults; edit it to try configuration values. New defaults do not replace existing values.
19
+
20
+ Keep `style.css` in the manifest's global `styles`; it contains CSS imported by the entry. Configuration and packaged resources are public.
21
+
22
+ ## Build and install
23
+
24
+ ```sh
9
25
  bun run typecheck
10
26
  bun run build
11
27
  bun run preview
12
28
  ```
13
29
 
14
- `dev` listens on http://127.0.0.1:5173; use `bun run dev --port 5174` if occupied.
15
- The collapsible preview panel switches pages, languages, appearance, identity and
16
- simulated states. Uploads and login are **simulations**, never real API calls.
17
- Do not use preview success as evidence that a storage service or authentication works.
18
-
19
- Edit `src/Page.vue`, `src/style.css` and `theme.json`. All pages use `src/entry.ts`
20
- (`entry.js` in the package). Put static package resources in `public/assets/` and
21
- resolve them with `context.asset("assets/...")`. Imported image/CSS resources are
22
- also processed by Vite. Keep `style.css` in the manifest's global `styles`: it
23
- contains all CSS imported by the source entry. Additional page-only styles belong
24
- in `public/assets/` and the corresponding page's `styles`; preview loads and
25
- unloads them when changing pages. Arbitrary files outside these inputs are not packaged.
26
-
27
- Vue components and CSS support HMR. Entry changes reload the preview. Manifest
28
- and `dev/config.json` changes reload and remount the theme. Invalid JSON shows a
29
- development error and is never overwritten. The first preview initializes a
30
- missing config from schema defaults; later edits never restore removed values.
31
- There is initially no custom config schema, so the generated config is `{}`.
32
-
33
- `preview` loads the existing `dist/package/` JavaScript and styles through the
34
- same simulated host, without theme source HMR. Build first; source edits are not
35
- visible until you rebuild. The preview toolbar itself is still a local tool.
36
-
37
- The distributable is `dist/<theme-id>-<version>.zip`. Upload it to Picloom's Themes
38
- page, confirm trust, preview and apply. Test real uploads, login and permissions
39
- there. Increase `theme.json.version` before distributing changed content; package
40
- versions are immutable on the host. The package builder checks structure and
41
- resources; full configuration/manifest validation still runs on installation.
42
-
43
- The SDK dependency is pinned to a published npm version.
44
- Commit your generated `bun.lock` to your own repository after installation.
30
+ Type checking is separate. `preview` displays the existing `dist/package/` through the simulated host; rebuild to see source changes.
31
+
32
+ Install `dist/<theme-id>-<version>.zip` as Owner in Picloom's Themes page, configure, preview and apply. Check uploads and login there. Increment `theme.json.version` before installing changed content; an installed ID/version cannot be overwritten.
33
+
34
+ [Theme tutorial](https://llleixx.github.io/picloom/development/theme-tooling.html) · [Theme protocol](https://llleixx.github.io/picloom/development/themes.html)
45
35
 
46
36
  ## 中文
47
37
 
48
- - 使用 Bun 1.4.3+,无需 Node/npm CLI、主仓库或系统 `zip`。
49
- - `bun run dev` 提供模拟首页、登录页、上传状态、语言和明暗模式;只用于界面开发。
50
- - 修改 Vue 组件及样式会热更新;入口、清单和本地配置修改后刷新预览。
51
- - 首次启动时按 Schema 默认值创建 `dev/config.json`,之后清空或删除值不会自动补回。
52
- - `bun run build` 先检查类型,再构建并生成可安装 ZIP;`bun run preview` 检查已有构建产物。
53
- - 修改主题 ID、显示名称和版本均以 `theme.json` 为准,静态资源放入 `public/assets/`。
54
- - 入口导入的样式合并为 `style.css`,须保留清单全局样式声明;页面专属样式通过各页面的 `styles` 引用,切页时分别加载和卸载。
55
- - 主题包不包含开发面板、本地配置、凭据或源码映射。所有主题配置可能公开,不要保存密钥。
56
- - 在隔离 Picloom 测试实例安装 ZIP 后验证真实上传和登录;模拟成功不代表真实服务已验证。
57
- - SDK 依赖固定到 npm 版本;首次安装后将生成的 `bun.lock` 提交到自己的仓库。
38
+ 使用 Bun 1.4.3+。运行 `bun install` 和 `bun run dev`,打开 `http://127.0.0.1:5173/`;可用 `--port 5174` 更换端口。面板模拟页面、语言、外观、登录与上传状态。
39
+
40
+ - `theme.json`:主题 ID、版本、多语言文案与配置 Schema。
41
+ - `src/Page.vue`、`src/style.css`:页面与样式,支持热更新。
42
+ - `src/entry.ts`:挂载主题并在卸载时清理资源。
43
+ - `public/assets/`:包内资源,通过 `context.asset("assets/...")` 读取。
44
+ - `dev/config.json`:首次预览按 Schema 默认值创建,之后手动修改;新默认值不会覆盖已有配置。
45
+
46
+ 入口导入的 CSS 合并到 `style.css`,需保留清单中的全局样式声明。主题配置与包内资源可公开访问。
47
+
48
+ 分别执行 `bun run typecheck`、`bun run build`。`bun run preview` 模拟预览已有构建产物,修改源码后需重新构建。
49
+
50
+ 以 Owner 在 Picloom 主题管理中安装 `dist/<主题ID>-<版本>.zip`,配置、预览并应用,再检查上传和登录。更新时增加 `theme.json.version`;同 ID、同版本不能覆盖。
@@ -2,3 +2,6 @@ node_modules/
2
2
  dist/
3
3
  .env
4
4
  .env.*
5
+
6
+ # Local preview configuration
7
+ **/dev/config.json
@@ -7,21 +7,21 @@
7
7
  "bun": ">=1.4.3"
8
8
  },
9
9
  "scripts": {
10
- "dev": "bun scripts/serve.ts",
10
+ "dev": "bun --bun picloom-theme dev",
11
11
  "typecheck": "bun --bun vue-tsc --noEmit",
12
- "build": "bun run typecheck && bun scripts/build.ts",
13
- "preview": "bun scripts/serve.ts --preview"
12
+ "build": "bun --bun picloom-theme build",
13
+ "preview": "bun --bun picloom-theme preview"
14
14
  },
15
15
  "devDependencies": {
16
- "@picloom/theme-sdk": "0.1.0",
16
+ "@picloom/theme-sdk": "0.1.2",
17
17
  "@tailwindcss/vite": "4.3.3",
18
18
  "@types/bun": "1.3.10",
19
19
  "@vitejs/plugin-vue": "6.0.10",
20
- "fflate": "0.8.2",
21
20
  "tailwindcss": "4.3.3",
22
21
  "typescript": "5.9.3",
23
22
  "vite": "8.3.4",
24
23
  "vue": "3.5.44",
25
- "vue-tsc": "3.3.13"
24
+ "vue-tsc": "3.3.13",
25
+ "@picloom/theme-tools": "0.1.1"
26
26
  }
27
27
  }
@@ -9,14 +9,19 @@
9
9
  "skipLibCheck": true,
10
10
  "resolveJsonModule": true,
11
11
  "esModuleInterop": true,
12
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
13
- "types": ["bun", "vite/client"]
12
+ "lib": [
13
+ "ES2022",
14
+ "DOM",
15
+ "DOM.Iterable"
16
+ ],
17
+ "types": [
18
+ "bun",
19
+ "vite/client"
20
+ ]
14
21
  },
15
22
  "include": [
16
23
  "src/**/*.ts",
17
24
  "src/**/*.vue",
18
- "dev/**/*.ts",
19
- "scripts/**/*.ts",
20
25
  "vite.config.ts"
21
26
  ]
22
27
  }
@@ -2,10 +2,13 @@ import { defineConfig } from "vite";
2
2
  import vue from "@vitejs/plugin-vue";
3
3
  import tailwindcss from "@tailwindcss/vite";
4
4
  import { resolve } from "node:path";
5
- export default defineConfig({
5
+ export default defineConfig(({ command }) => ({
6
6
  plugins: [vue(), tailwindcss()],
7
7
  publicDir: false,
8
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
8
+ define:
9
+ command === "build"
10
+ ? { "process.env.NODE_ENV": JSON.stringify("production") }
11
+ : {},
9
12
  build: {
10
13
  outDir: "dist/package",
11
14
  emptyOutDir: true,
@@ -17,4 +20,4 @@ export default defineConfig({
17
20
  cssFileName: "style",
18
21
  },
19
22
  },
20
- });
23
+ }));
package/index.js CHANGED
@@ -50,7 +50,7 @@ if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
50
50
  await writeFile(packagePath, JSON.stringify(pkg, null, 2) + "\n");
51
51
  const quotedDirectory = "'" + args[0].replaceAll("'", "'\\''") + "'";
52
52
  console.log(
53
- `Created ${target}\n\nNext:\n cd ${quotedDirectory}\n bun install\n bun run dev\n bun run build\n\nUpload dist/${manifest.id}-${manifest.version}.zip to Picloom.`,
53
+ `Created ${target}\n\nNext:\n cd ${quotedDirectory}\n bun install\n bun run typecheck\n bun run dev\n bun run build\n\nUpload dist/${manifest.id}-${manifest.version}.zip to Picloom.`,
54
54
  );
55
55
  } catch (error) {
56
56
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picloom/create-theme",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Create a standalone Picloom Vue theme with local preview and ZIP packaging",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/llleixx/picloom.git",
25
+ "url": "git+https://github.com/llleixx/picloom-tooling.git",
26
26
  "directory": "tools/create-theme"
27
27
  },
28
28
  "publishConfig": {
@@ -1,363 +0,0 @@
1
- import type {
2
- Config,
3
- Manifest,
4
- Page,
5
- Snapshot,
6
- ThemeContext,
7
- Upload,
8
- } from "@picloom/theme-sdk";
9
- import { languages, messageText } from "./helpers.js";
10
- export const scenarios = [
11
- "ready",
12
- "loading",
13
- "uploading",
14
- "success",
15
- "failed",
16
- "no-profiles",
17
- "login-error",
18
- ] as const;
19
- export type Scenario = (typeof scenarios)[number];
20
- const copy = <T>(value: T): T => structuredClone(value);
21
- export function createPreview(
22
- manifest: Manifest,
23
- config: Config,
24
- navigate: (page: Page) => void,
25
- ) {
26
- const listeners = new Set<(snapshot: Snapshot) => void>();
27
- const timers = new Set<ReturnType<typeof setTimeout>>();
28
- const motion = matchMedia("(prefers-color-scheme: dark)");
29
- const state: Snapshot = {
30
- page: location.pathname === "/login" ? "login" : "home",
31
- site: {
32
- name: "Preview gallery",
33
- homeTitle: "Preview gallery",
34
- description: "SIMULATED uploads and login · 模拟上传与登录",
35
- logoUrl: "",
36
- faviconUrl: "",
37
- },
38
- user: null,
39
- locale: manifest.defaultLocale,
40
- locales: manifest.supportedLocales,
41
- languages: languages.filter((language) =>
42
- manifest.supportedLocales.includes(language.tag),
43
- ),
44
- defaultLocale: manifest.defaultLocale,
45
- mode: "auto",
46
- dark: motion.matches,
47
- profiles: [
48
- {
49
- id: "mock",
50
- name: "Simulated / 模拟上传",
51
- enabled: true,
52
- available: true,
53
- maxUploadBytes: 50 * 1024 ** 2,
54
- allowedMimeTypes: [
55
- "image/jpeg",
56
- "image/png",
57
- "image/webp",
58
- "image/svg+xml",
59
- ],
60
- reason: null,
61
- },
62
- ],
63
- selectedProfileId: "mock",
64
- uploads: [],
65
- loading: false,
66
- error: "",
67
- login: { busy: false, error: "", expired: false, unavailable: false },
68
- copy: {
69
- format: "URL",
70
- auto: false,
71
- formats: ["URL", "Markdown", "HTML", "BBCode"],
72
- },
73
- notice: "",
74
- };
75
- let scenario: Scenario = "ready",
76
- alive = true;
77
- const text = (en: string, zh: string) => (state.locale === "zh-CN" ? zh : en);
78
- function emit() {
79
- if (alive) for (const listener of listeners) listener(copy(state));
80
- }
81
- function later(callback: () => void, delay: number) {
82
- const timer = setTimeout(() => {
83
- timers.delete(timer);
84
- if (alive) callback();
85
- }, delay);
86
- timers.add(timer);
87
- }
88
- function clear() {
89
- for (const timer of timers) clearTimeout(timer);
90
- timers.clear();
91
- }
92
- function notify(en: string, zh: string) {
93
- state.notice = text(en, zh);
94
- emit();
95
- }
96
- function result(id: string) {
97
- return { id, url: `https://example.invalid/simulated/${id}.png` };
98
- }
99
- function format(url: string) {
100
- switch (state.copy.format) {
101
- case "Markdown":
102
- return `![](${url})`;
103
- case "HTML":
104
- return `<img src="${url}" alt="">`;
105
- case "BBCode":
106
- return `[img]${url}[/img]`;
107
- default:
108
- return url;
109
- }
110
- }
111
- async function copyText(value: string) {
112
- try {
113
- await navigator.clipboard.writeText(value);
114
- notify("Copied", "已复制");
115
- } catch {
116
- notify("Clipboard unavailable", "剪贴板不可用");
117
- }
118
- }
119
- function run(item: Upload) {
120
- later(() => {
121
- if (
122
- !state.uploads.includes(item) ||
123
- !["queued", "uploading"].includes(item.status)
124
- )
125
- return;
126
- item.status = "uploading";
127
- item.progress = Math.min(100, item.progress + 25);
128
- if (item.progress === 100) {
129
- item.status = "success";
130
- item.result = result(item.id);
131
- if (state.copy.auto) void copyText(format(item.result.url));
132
- } else run(item);
133
- emit();
134
- }, 400);
135
- }
136
- function add(name: string, size: number, sourceUrl?: string) {
137
- if (
138
- !state.profiles.some(
139
- (profile) =>
140
- profile.id === state.selectedProfileId && profile.available,
141
- )
142
- ) {
143
- notify(
144
- "Choose an available simulated profile",
145
- "请选择可用的模拟上传方案",
146
- );
147
- return;
148
- }
149
- const item: Upload = {
150
- id: crypto.randomUUID(),
151
- name,
152
- size,
153
- profileName: state.profiles[0]!.name,
154
- sourceUrl,
155
- status: "queued",
156
- progress: 0,
157
- };
158
- state.uploads = [...state.uploads, item];
159
- run(item);
160
- emit();
161
- }
162
- function setScenario(value: Scenario) {
163
- clear();
164
- scenario = value;
165
- state.uploads = [];
166
- state.loading = value === "loading";
167
- state.login.busy = false;
168
- state.login.error =
169
- value === "login-error"
170
- ? text("Simulated login failure", "模拟登录失败")
171
- : "";
172
- state.notice = "";
173
- state.profiles = state.profiles.map((profile) => ({
174
- ...profile,
175
- available: value !== "no-profiles",
176
- }));
177
- state.selectedProfileId = value === "no-profiles" ? "" : "mock";
178
- if (["uploading", "success", "failed"].includes(value)) {
179
- const item: Upload = {
180
- id: "preview-image",
181
- name: "simulated-image.png",
182
- size: 128 * 1024,
183
- profileName: state.profiles[0]!.name,
184
- status: value as Upload["status"],
185
- progress: value === "uploading" ? 50 : value === "success" ? 100 : 0,
186
- };
187
- if (value === "success") item.result = result(item.id);
188
- if (value === "failed") {
189
- item.error = text("Simulated upload failure", "模拟上传失败");
190
- item.resumable = true;
191
- }
192
- state.uploads = [item];
193
- }
194
- emit();
195
- }
196
- function appearance() {
197
- state.dark = state.mode === "auto" ? motion.matches : state.mode === "dark";
198
- document.documentElement.classList.toggle("dark", state.dark);
199
- emit();
200
- }
201
- motion.addEventListener("change", appearance);
202
- const ctx: ThemeContext = {
203
- apiVersion: 1,
204
- config: copy(config),
205
- getSnapshot: () => copy(state),
206
- subscribe(listener) {
207
- listeners.add(listener);
208
- listener(copy(state));
209
- return () => {
210
- listeners.delete(listener);
211
- };
212
- },
213
- asset(path) {
214
- if (
215
- !path ||
216
- path.startsWith("/") ||
217
- /[\\:?*<>"|]/.test(path) ||
218
- path.split("/").some((part) => !part || part === "." || part === "..")
219
- )
220
- throw Error("Invalid package asset path");
221
- return new URL("/" + path, location.origin).href;
222
- },
223
- t(key, params = {}) {
224
- return messageText(manifest, state.locale, key).replace(
225
- /\{(\w+)\}/g,
226
- (match, name: string) => String(params[name] ?? match),
227
- );
228
- },
229
- localized(value) {
230
- if (typeof value === "string") return value;
231
- if (!value || typeof value !== "object" || Array.isArray(value))
232
- return "";
233
- return typeof value[state.locale] === "string"
234
- ? (value[state.locale] as string)
235
- : "";
236
- },
237
- setLocale(language) {
238
- if (!state.locales.includes(language))
239
- throw Error("Language is not supported");
240
- state.locale = language;
241
- document.documentElement.lang = language;
242
- if (state.login.error)
243
- state.login.error = text("Simulated login failure", "模拟登录失败");
244
- for (const item of state.uploads)
245
- if (item.error)
246
- item.error = text("Simulated upload failure", "模拟上传失败");
247
- emit();
248
- },
249
- setMode(mode) {
250
- state.mode = mode;
251
- appearance();
252
- },
253
- navigate(page) {
254
- if (page === "app")
255
- notify(
256
- "Workspace is unavailable in simulated preview",
257
- "模拟预览不提供管理后台",
258
- );
259
- else navigate(page);
260
- },
261
- selectProfile(id) {
262
- state.selectedProfileId = id;
263
- emit();
264
- },
265
- async refresh() {
266
- state.error = "";
267
- setScenario("ready");
268
- },
269
- uploads: {
270
- add(files) {
271
- for (const file of files) add(file.name, file.size);
272
- },
273
- addUrls(urls) {
274
- for (const sourceUrl of urls) {
275
- try {
276
- const url = new URL(sourceUrl);
277
- if (!["http:", "https:"].includes(url.protocol)) throw Error();
278
- add(
279
- url.pathname.split("/").pop() || "remote-image",
280
- 1024,
281
- sourceUrl,
282
- );
283
- } catch {
284
- notify("Invalid image URL", "图片地址无效");
285
- }
286
- }
287
- },
288
- async cancel(id) {
289
- const item = state.uploads.find((item) => item.id === id);
290
- if (item && ["queued", "uploading"].includes(item.status))
291
- item.status = "cancelled";
292
- emit();
293
- },
294
- resume(id) {
295
- const item = state.uploads.find((item) => item.id === id);
296
- if (item?.resumable) {
297
- item.status = "queued";
298
- item.error = undefined;
299
- run(item);
300
- emit();
301
- }
302
- },
303
- async dismiss(id) {
304
- state.uploads = state.uploads.filter((item) => item.id !== id);
305
- emit();
306
- },
307
- },
308
- links: {
309
- format,
310
- async copy(url) {
311
- await copyText(format(url));
312
- },
313
- async copyAll() {
314
- await copyText(
315
- state.uploads
316
- .flatMap((item) => (item.result ? [format(item.result.url)] : []))
317
- .join("\n"),
318
- );
319
- },
320
- setPreferences(format, auto) {
321
- if (!state.copy.formats.includes(format))
322
- throw Error("Unknown copy format");
323
- state.copy = { ...state.copy, format, auto };
324
- emit();
325
- },
326
- },
327
- async login(username, _password) {
328
- state.login.busy = true;
329
- state.login.error = "";
330
- emit();
331
- await new Promise<void>((resolve) => setTimeout(resolve, 150));
332
- if (!alive) return;
333
- state.login.busy = false;
334
- if (scenario === "login-error")
335
- state.login.error = text("Simulated login failure", "模拟登录失败");
336
- else {
337
- state.user = { name: username || "Preview user" };
338
- navigate("home");
339
- }
340
- emit();
341
- },
342
- };
343
- appearance();
344
- document.documentElement.lang = state.locale;
345
- return {
346
- ctx,
347
- setScenario,
348
- setPage(page: Page) {
349
- state.page = page;
350
- emit();
351
- },
352
- setUser(signedIn: boolean) {
353
- state.user = signedIn ? { name: "Preview user" } : null;
354
- emit();
355
- },
356
- dispose() {
357
- alive = false;
358
- clear();
359
- listeners.clear();
360
- motion.removeEventListener("change", appearance);
361
- },
362
- };
363
- }
@@ -1,18 +0,0 @@
1
- import type { Field, Json } from "@picloom/theme-sdk";
2
- export function initialValue(schema: Field, value?: Json): Json | undefined;
3
- export function messageText(
4
- catalog: {
5
- defaultLocale: string;
6
- messages: Record<string, Record<string, string>>;
7
- },
8
- locale: string,
9
- key: string,
10
- ): string;
11
- export const supportedLocales: readonly string[];
12
- export const languages: readonly {
13
- tag: string;
14
- name: string;
15
- direction: string;
16
- }[];
17
- export function safeName(value: unknown): value is string;
18
- export function assetPath(value: unknown): value is string;
@@ -1,114 +0,0 @@
1
- // ../../shared/field-schema.ts
2
- function cloneJson(value) {
3
- return JSON.parse(JSON.stringify(value));
4
- }
5
- function pointer(key) {
6
- return key.replace(/~/g, "~0").replace(/\//g, "~1");
7
- }
8
- function sensitivePaths(schema, path = "") {
9
- if (schema["x-sensitive"] === true || schema.type === "array" && schema.items && sensitivePaths(schema.items).length > 0)
10
- return [path];
11
- return Object.entries(schema.properties ?? {}).flatMap(([key, rule]) => sensitivePaths(rule, path + "/" + pointer(key)));
12
- }
13
- var keywords = new Set([
14
- "$schema",
15
- "type",
16
- "properties",
17
- "required",
18
- "additionalProperties",
19
- "items",
20
- "enum",
21
- "const",
22
- "default",
23
- "minimum",
24
- "maximum",
25
- "exclusiveMinimum",
26
- "exclusiveMaximum",
27
- "multipleOf",
28
- "minLength",
29
- "maxLength",
30
- "pattern",
31
- "format",
32
- "minItems",
33
- "maxItems",
34
- "uniqueItems",
35
- "if",
36
- "then",
37
- "else",
38
- "allOf",
39
- "anyOf",
40
- "oneOf",
41
- "not",
42
- "x-localized",
43
- "x-sensitive",
44
- "x-immutable",
45
- "x-ui"
46
- ]);
47
- function initialValue(schema, value) {
48
- if (schema["x-sensitive"] === true || schema.type === "array" && sensitivePaths(schema).length)
49
- return;
50
- const seed = value === undefined ? schema.default : value;
51
- if (schema.type === "object") {
52
- if (seed !== undefined && (!seed || typeof seed !== "object" || Array.isArray(seed)))
53
- return cloneJson(seed);
54
- const result = seed === undefined ? {} : cloneJson(seed);
55
- for (const [key, rule] of Object.entries(schema.properties ?? {})) {
56
- const child = initialValue(rule, Object.hasOwn(result, key) ? result[key] : undefined);
57
- if (child !== undefined)
58
- result[key] = child;
59
- else
60
- delete result[key];
61
- }
62
- return seed !== undefined || Object.keys(result).length ? result : undefined;
63
- }
64
- if (schema.type === "array" && Array.isArray(seed))
65
- return seed.map((item) => initialValue(schema.items, item) ?? null);
66
- return seed === undefined ? undefined : cloneJson(seed);
67
- }
68
- // ../../shared/package-localization.ts
69
- function localeCandidates(locale, fallback) {
70
- const tag = Intl.getCanonicalLocales(locale)[0];
71
- return [
72
- ...new Set([
73
- tag,
74
- tag.split("-")[0],
75
- Intl.getCanonicalLocales(fallback)[0]
76
- ])
77
- ];
78
- }
79
- function messageText(resource, locale, key) {
80
- for (const tag of localeCandidates(locale, resource.defaultLocale)) {
81
- const dictionary = resource.messages[tag];
82
- const value = dictionary && Object.hasOwn(dictionary, key) ? dictionary[key] : undefined;
83
- if (value !== undefined)
84
- return value;
85
- }
86
- return key;
87
- }
88
- // ../../shared/locales.ts
89
- var languages = [
90
- {
91
- tag: "zh-CN",
92
- name: "简体中文",
93
- direction: "ltr",
94
- aliases: ["zh"]
95
- },
96
- {
97
- tag: "en-US",
98
- name: "English",
99
- direction: "ltr",
100
- aliases: ["en"]
101
- }
102
- ];
103
- var supportedLocales = languages.map((language) => language.tag);
104
- // ../../shared/theme-package.ts
105
- var safeName = (v) => typeof v === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(v) && !v.endsWith(".");
106
- var assetPath = (v) => typeof v === "string" && v.length <= 240 && !/[\\:< >"|?*\x00-\x1f]/.test(v) && v.split("/").every((p) => !!p && p !== "." && p !== ".." && !/[. ]$/.test(p));
107
- export {
108
- assetPath,
109
- initialValue,
110
- languages,
111
- messageText,
112
- safeName,
113
- supportedLocales
114
- };
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Picloom theme preview</title>
7
- </head>
8
- <body>
9
- <div id="app"></div>
10
- <script type="module" src="/dev/main.ts"></script>
11
- </body>
12
- </html>
@@ -1,179 +0,0 @@
1
- import { manifest, config, production } from "virtual:picloom-preview";
2
- import { createPreview, scenarios, type Scenario } from "./context";
3
- import type { Page, ThemeModule } from "@picloom/theme-sdk";
4
- const root = document.getElementById("app")!;
5
- let instance: { dispose(): void } | undefined,
6
- generation = 0;
7
- const sourceModule = production ? "/entry.js" : "/src/entry.ts";
8
- const runtime = createPreview(manifest, config, navigate);
9
- const preview = document.createElement("aside");
10
- preview.id = "picloom-preview-tools";
11
- const shadow = preview.attachShadow({ mode: "open" });
12
- shadow.innerHTML = `<style>:host{position:fixed;right:12px;bottom:12px;z-index:2147483647;font:13px system-ui;color:#eee}details{background:#15212e;border:1px solid #617080;border-radius:10px;padding:12px;max-width:300px;box-shadow:0 4px 20px #0005}summary{cursor:pointer;font-weight:bold}label{display:grid;gap:4px;margin-top:10px}select{background:#fff;color:#17212c;padding:5px;min-width:220px}p{max-width:260px;font-size:12px}output{display:block;color:#ffd37c;white-space:pre-wrap;overflow-wrap:anywhere}</style>
13
- <details open><summary>Picloom · Preview / 预览</summary><p>${production ? "Production build / 构建产物" : "Source HMR / 源码热更新"}<br>SIMULATED uploads and login<br>模拟上传与登录,不连接真实服务</p>
14
- <label>Page / 页面<select id="page"><option value="home">Home / 首页</option><option value="login">Login / 登录</option></select></label>
15
- <label>Language / 语言<select id="language"></select></label>
16
- <label>Appearance / 外观<select id="mode"><option value="auto">System / 跟随系统</option><option value="light">Light / 浅色</option><option value="dark">Dark / 深色</option></select></label>
17
- <label>Identity / 身份<select id="identity"><option value="guest">Guest / 访客</option><option value="user">Signed in / 已登录</option></select></label>
18
- <label>Scenario / 场景<select id="scenario"></select></label><output role="alert"></output></details>`;
19
- document.body.append(preview);
20
- const select = (id: string) => shadow.getElementById(id) as HTMLSelectElement;
21
- const errors = shadow.querySelector("output")!;
22
- function option(value: string, label: string) {
23
- const node = document.createElement("option");
24
- node.value = value;
25
- node.textContent = label;
26
- return node;
27
- }
28
- for (const language of runtime.ctx.getSnapshot().languages)
29
- select("language").append(option(language.tag, language.name));
30
- const labels: Record<Scenario, string> = {
31
- ready: "Ready / 就绪",
32
- loading: "Loading / 加载中",
33
- uploading: "Uploading / 上传中",
34
- success: "Success / 上传成功",
35
- failed: "Failed / 上传失败",
36
- "no-profiles": "No profiles / 无可用方案",
37
- "login-error": "Login error / 登录失败",
38
- };
39
- for (const scenario of scenarios)
40
- select("scenario").append(option(scenario, labels[scenario]));
41
- select("page").onchange = () => navigate(select("page").value as Page);
42
- select("language").onchange = () =>
43
- runtime.ctx.setLocale(select("language").value);
44
- select("mode").onchange = () =>
45
- runtime.ctx.setMode(select("mode").value as "auto" | "light" | "dark");
46
- select("identity").onchange = () =>
47
- runtime.setUser(select("identity").value === "user");
48
- select("scenario").onchange = () =>
49
- runtime.setScenario(select("scenario").value as Scenario);
50
- const stop = runtime.ctx.subscribe((state) => {
51
- select("page").value = state.page;
52
- select("language").value = state.locale;
53
- select("mode").value = state.mode;
54
- select("identity").value = state.user ? "user" : "guest";
55
- });
56
- function cleanup() {
57
- const previous = instance;
58
- instance = undefined;
59
- try {
60
- previous?.dispose();
61
- } catch (error) {
62
- errors.textContent = String(error);
63
- } finally {
64
- root.replaceChildren();
65
- }
66
- }
67
- async function mount() {
68
- const current = ++generation;
69
- errors.textContent = "";
70
- cleanup();
71
- const subscriptions = new Set<() => void>();
72
- const links: HTMLLinkElement[] = [];
73
- const container = document.createElement("div");
74
- root.append(container);
75
- let mounted: Awaited<ReturnType<ThemeModule["mount"]>> | undefined;
76
- let cleaned = false;
77
- const context = {
78
- ...runtime.ctx,
79
- subscribe(listener: Parameters<typeof runtime.ctx.subscribe>[0]) {
80
- // An async mount may continue after its page has already been unmounted.
81
- if (cleaned) return () => {};
82
- const stop = runtime.ctx.subscribe(listener);
83
- subscriptions.add(stop);
84
- return () => {
85
- stop();
86
- subscriptions.delete(stop);
87
- };
88
- },
89
- };
90
- const dispose = () => {
91
- if (cleaned) return;
92
- cleaned = true;
93
- try {
94
- if (typeof mounted?.dispose === "function") mounted.dispose();
95
- } finally {
96
- for (const stop of subscriptions) stop();
97
- subscriptions.clear();
98
- for (const link of links) link.remove();
99
- container.remove();
100
- }
101
- };
102
- instance = { dispose };
103
- try {
104
- const page = runtime.ctx.getSnapshot().page;
105
- const declaration = manifest.pages[page];
106
- if (!declaration)
107
- throw Error(
108
- `Theme does not declare ${page}; the real host will use its default page`,
109
- );
110
- const styles = new Set([
111
- ...(manifest.styles ?? []),
112
- ...(declaration.styles ?? []),
113
- ]);
114
- await Promise.all(
115
- [...styles]
116
- .filter((path) => production || path !== "style.css")
117
- .map(
118
- (path) =>
119
- new Promise<void>((resolve, reject) => {
120
- // The source entry imports src/style.css; other declared styles are public assets.
121
- const link = document.createElement("link");
122
- link.rel = "stylesheet";
123
- link.href = runtime.ctx.asset(path);
124
- link.onload = () => resolve();
125
- link.onerror = () =>
126
- reject(Error(`Missing theme style: ${path}`));
127
- links.push(link);
128
- document.head.append(link);
129
- }),
130
- ),
131
- );
132
- if (current !== generation || cleaned) return;
133
- const module: ThemeModule = await import(/* @vite-ignore */ sourceModule);
134
- if (typeof module.mount !== "function")
135
- throw Error("Theme must export mount(container, context)");
136
- if (current !== generation || cleaned) return;
137
- mounted = await module.mount(container, context);
138
- if (!mounted || typeof mounted.dispose !== "function")
139
- throw Error("mount() must return { dispose() }");
140
- if (current !== generation || cleaned) {
141
- if (cleaned) mounted.dispose();
142
- else dispose();
143
- }
144
- } catch (error) {
145
- try {
146
- dispose();
147
- } finally {
148
- if (current === generation) errors.textContent = String(error);
149
- }
150
- }
151
- }
152
- function navigate(page: Page) {
153
- history.pushState(null, "", page === "login" ? "/login" : "/");
154
- runtime.setPage(page);
155
- void mount();
156
- }
157
- function popstate() {
158
- runtime.setPage(location.pathname === "/login" ? "login" : "home");
159
- void mount();
160
- }
161
- window.addEventListener("popstate", popstate);
162
- function dispose() {
163
- generation++;
164
- cleanup();
165
- stop();
166
- runtime.dispose();
167
- preview.remove();
168
- window.removeEventListener("popstate", popstate);
169
- }
170
- function pagehide(event: PageTransitionEvent) {
171
- if (!event.persisted) dispose();
172
- }
173
- window.addEventListener("pagehide", pagehide);
174
- import.meta.hot?.dispose(() => {
175
- window.removeEventListener("pagehide", pagehide);
176
- dispose();
177
- });
178
- // Vue SFC/CSS updates use normal HMR. Entry changes invalidate the importer and reload.
179
- await mount();
@@ -1,5 +0,0 @@
1
- declare module "virtual:picloom-preview" {
2
- export const manifest: import("@picloom/theme-sdk").Manifest;
3
- export const config: import("@picloom/theme-sdk").Config;
4
- export const production: boolean;
5
- }
@@ -1,119 +0,0 @@
1
- import {
2
- mkdir,
3
- readdir,
4
- readFile,
5
- lstat,
6
- writeFile,
7
- cp,
8
- rm,
9
- } from "node:fs/promises";
10
- import { resolve, relative, dirname } from "node:path";
11
- import { build } from "vite";
12
- import { zipSync, strToU8 } from "fflate";
13
- import { assetPath, readManifest } from "./manifest";
14
- const manifest = await readManifest();
15
- const output = resolve("dist/package");
16
- const archive = resolve("dist", `${manifest.id}-${manifest.version}.zip`);
17
- // A failed build must not leave an older ZIP looking like the new result.
18
- await rm(archive, { force: true });
19
- await build();
20
- async function files(root: string): Promise<string[]> {
21
- if ((await lstat(root)).isSymbolicLink())
22
- throw Error(`Do not package symbolic links: ${root}`);
23
- const result: string[] = [];
24
- for (const name of await readdir(root)) {
25
- const path = resolve(root, name),
26
- stat = await lstat(path);
27
- if (stat.isSymbolicLink())
28
- throw Error(`Do not package symbolic links: ${path}`);
29
- if (stat.isDirectory()) result.push(...(await files(path)));
30
- else if (stat.isFile()) result.push(path);
31
- else throw Error(`Unsupported file: ${path}`);
32
- }
33
- return result;
34
- }
35
- try {
36
- for (const path of await files(resolve("public/assets"))) {
37
- const name = relative(resolve("public"), path).replaceAll("\\", "/");
38
- if (
39
- !assetPath(name) ||
40
- /(?:^|\/)\./.test(name) ||
41
- /\.(?:map|env|pem|key|ts|vue)$/i.test(name)
42
- )
43
- throw Error(`Not a distributable theme asset: ${name}`);
44
- const destination = resolve(output, name);
45
- if (await Bun.file(destination).exists())
46
- throw Error(`Asset collides with build output: ${name}`);
47
- await mkdir(dirname(destination), { recursive: true });
48
- await cp(path, destination);
49
- }
50
- } catch (error) {
51
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
52
- }
53
- await cp("LICENSE", output + "/LICENSE");
54
- const declaration = JSON.stringify(manifest, null, 2) + "\n";
55
- if (strToU8(declaration).byteLength > 256 * 1024)
56
- throw Error("theme.json exceeds 256 KiB");
57
- await writeFile(output + "/theme.json", declaration);
58
- const entries: Record<string, Uint8Array> = {};
59
- const names = new Set<string>();
60
- let total = 0;
61
- for (const path of await files(output)) {
62
- const name = relative(output, path).replaceAll("\\", "/");
63
- if (
64
- !assetPath(name) ||
65
- names.has(name.toLowerCase()) ||
66
- /(?:^|\/)(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|\/|$)/i.test(name) ||
67
- /(?:^|\/)\./.test(name) ||
68
- name.endsWith(".map")
69
- )
70
- throw Error(`Invalid or duplicate package path: ${name}`);
71
- names.add(name.toLowerCase());
72
- const bytes = new Uint8Array(await readFile(path));
73
- total += bytes.byteLength;
74
- if (total > 50 * 1024 * 1024 || names.size > 512)
75
- throw Error("Theme exceeds 50 MiB expanded or 512 files");
76
- if (/\.m?js$/.test(name)) {
77
- const code = new TextDecoder().decode(bytes);
78
- const imports = new Bun.Transpiler({
79
- loader: "js",
80
- target: "browser",
81
- }).scanImports(code);
82
- if (imports.length)
83
- throw Error(
84
- `Unbundled runtime imports in ${name}; this template produces a self-contained entry`,
85
- );
86
- if (
87
- /["'`]\/(?:@vite|@id|@fs|src|dev)\/|localhost:\d|127\.0\.0\.1:\d|\/\/[#@]\s*sourceMappingURL=/.test(
88
- code,
89
- )
90
- )
91
- throw Error(`Development reference in ${name}`);
92
- }
93
- if (
94
- name.endsWith(".css") &&
95
- /localhost:\d|127\.0\.0\.1:\d|(?:url|@import)\s*\(?["']?\/(?:src|dev|@vite)\//.test(
96
- new TextDecoder().decode(bytes),
97
- )
98
- )
99
- throw Error(`Development reference in ${name}`);
100
- entries[name] = bytes;
101
- }
102
- if (entries["style.css"] && !manifest.styles?.includes("style.css"))
103
- throw Error(
104
- "The template bundles imported CSS into style.css; declare it in theme.json styles",
105
- );
106
- for (const name of [
107
- ...(manifest.styles ?? []),
108
- ...(manifest.preview ? [manifest.preview] : []),
109
- ...Object.values(manifest.pages).flatMap((page) => [
110
- page.entry,
111
- ...(page.styles ?? []),
112
- ]),
113
- ]) {
114
- if (!entries[name]) throw Error(`Missing theme resource: ${name}`);
115
- }
116
- const zip = zipSync(entries);
117
- if (zip.byteLength > 20 * 1024 * 1024) throw Error("Theme ZIP exceeds 20 MiB");
118
- await Bun.write(archive, zip);
119
- console.log(archive);
@@ -1,61 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import type { Manifest } from "@picloom/theme-sdk";
3
- import { supportedLocales, safeName, assetPath } from "../dev/helpers.js";
4
- export { assetPath } from "../dev/helpers.js";
5
- export async function readManifest(path = "theme.json"): Promise<Manifest> {
6
- const value = JSON.parse(await readFile(path, "utf8"));
7
- const object = (v: unknown): v is Record<string, any> =>
8
- !!v && typeof v === "object" && !Array.isArray(v);
9
- if (
10
- !object(value) ||
11
- !safeName(value.id) ||
12
- value.id.startsWith("builtin.") ||
13
- !safeName(value.version) ||
14
- value.apiVersion !== 1 ||
15
- !Array.isArray(value.supportedLocales) ||
16
- !value.supportedLocales.length ||
17
- value.supportedLocales.some((v: string) => !supportedLocales.includes(v)) ||
18
- new Set(value.supportedLocales).size !== value.supportedLocales.length ||
19
- !value.supportedLocales.includes(value.defaultLocale) ||
20
- !object(value.pages) ||
21
- !Object.keys(value.pages).length ||
22
- Object.keys(value.pages).some((key) => !["home", "login"].includes(key)) ||
23
- !object(value.messages) ||
24
- !object(value.messages[value.defaultLocale]) ||
25
- typeof value.nameKey !== "string" ||
26
- typeof value.messages[value.defaultLocale][value.nameKey] !== "string" ||
27
- !value.messages[value.defaultLocale][value.nameKey].trim()
28
- )
29
- throw Error(
30
- "Invalid theme.json: check identity, API, pages and language declarations",
31
- );
32
- for (const page of Object.values(value.pages) as Array<{
33
- entry: string;
34
- styles?: string[];
35
- }>) {
36
- if (!object(page) || page.entry !== "entry.js")
37
- throw Error(
38
- "This template builds src/entry.ts as entry.js; use that entry for each page",
39
- );
40
- if (
41
- page.styles !== undefined &&
42
- (!Array.isArray(page.styles) ||
43
- page.styles.some((p) => !assetPath(p) || !p.endsWith(".css")))
44
- )
45
- throw Error("Invalid page styles");
46
- }
47
- if (
48
- value.styles !== undefined &&
49
- (!Array.isArray(value.styles) ||
50
- value.styles.some((p: unknown) => !assetPath(p) || !p.endsWith(".css")))
51
- )
52
- throw Error("Invalid styles");
53
- if (
54
- value.preview !== undefined &&
55
- (!assetPath(value.preview) || !/\.(png|jpe?g|webp)$/i.test(value.preview))
56
- )
57
- throw Error("Invalid preview image");
58
- if (value.configSchema !== undefined && value.configSchema?.type !== "object")
59
- throw Error("configSchema must be an object schema");
60
- return value as Manifest;
61
- }
@@ -1,127 +0,0 @@
1
- import { createServer, type Plugin } from "vite";
2
- import vue from "@vitejs/plugin-vue";
3
- import tailwindcss from "@tailwindcss/vite";
4
- import { resolve } from "node:path";
5
- import { readFile, mkdir, writeFile } from "node:fs/promises";
6
- import { initialValue } from "../dev/helpers.js";
7
- import { assetPath, readManifest } from "./manifest";
8
- const args = process.argv.slice(2),
9
- production = args.includes("--preview");
10
- const portIndex = args.indexOf("--port");
11
- const port = portIndex === -1 ? 5173 : Number(args[portIndex + 1]);
12
- if (
13
- !Number.isInteger(port) ||
14
- port < 1 ||
15
- port > 65535 ||
16
- args.some(
17
- (arg, i) =>
18
- arg !== "--preview" &&
19
- arg !== "--port" &&
20
- !(portIndex >= 0 && i === portIndex + 1),
21
- )
22
- )
23
- throw Error(
24
- "Usage: bun run dev [--port 5173] / bun run preview [--port 5173]",
25
- );
26
- if (production && !(await Bun.file("dist/package/entry.js").exists()))
27
- throw Error("No production build. Run bun run build first.");
28
- const manifestPath = production ? "dist/package/theme.json" : "theme.json";
29
- try {
30
- if (!(await Bun.file("dev/config.json").exists())) {
31
- const manifest = await readManifest(manifestPath);
32
- await mkdir("dev", { recursive: true });
33
- await writeFile(
34
- "dev/config.json",
35
- JSON.stringify(
36
- initialValue(manifest.configSchema ?? { type: "object" }) ?? {},
37
- null,
38
- 2,
39
- ) + "\n",
40
- { flag: "wx" },
41
- );
42
- }
43
- } catch (error) {
44
- if ((error as NodeJS.ErrnoException).code !== "EEXIST")
45
- console.error(String(error));
46
- }
47
- const virtual = "virtual:picloom-preview",
48
- resolved = "\0" + virtual;
49
- const plugin: Plugin = {
50
- name: "picloom-preview",
51
- resolveId(id) {
52
- if (id === virtual) return resolved;
53
- },
54
- async load(id) {
55
- if (id !== resolved) return;
56
- const manifest = await readManifest(manifestPath);
57
- const config = JSON.parse(await readFile("dev/config.json", "utf8"));
58
- if (!config || Array.isArray(config) || typeof config !== "object")
59
- throw Error("dev/config.json must contain an object");
60
- return `export const manifest=${JSON.stringify(manifest)};export const config=${JSON.stringify(config)};export const production=${production};`;
61
- },
62
- configureServer(server) {
63
- server.watcher.add([manifestPath, "dev/config.json"]);
64
- server.watcher.on("all", (_event, path) => {
65
- if (
66
- [manifestPath, "dev/config.json"].some(
67
- (name) => resolve(path) === resolve(name),
68
- )
69
- ) {
70
- const module = server.moduleGraph.getModuleById(resolved);
71
- if (module) server.moduleGraph.invalidateModule(module);
72
- server.ws.send({ type: "full-reload" });
73
- }
74
- });
75
- server.middlewares.use(async (req, res, next) => {
76
- try {
77
- const path = decodeURIComponent(req.url?.split("?")[0] ?? "");
78
- // Built resources are served verbatim, including encoded names and ?import.
79
- if (production && assetPath(path.slice(1))) {
80
- const file = Bun.file(resolve("dist/package", path.slice(1)));
81
- if (await file.exists()) {
82
- res.setHeader("Content-Type", file.type);
83
- res.setHeader("Cache-Control", "no-cache");
84
- res.end(new Uint8Array(await file.arrayBuffer()));
85
- return;
86
- }
87
- }
88
- if (path !== "/" && path !== "/login") return next();
89
- const html = await server.transformIndexHtml(
90
- req.url!,
91
- await readFile("dev/index.html", "utf8"),
92
- );
93
- res.setHeader("Content-Type", "text/html; charset=utf-8");
94
- res.end(html);
95
- } catch (error) {
96
- next(error);
97
- }
98
- });
99
- },
100
- };
101
- const server = await createServer({
102
- configFile: false,
103
- appType: "mpa",
104
- plugins: [plugin, vue(), tailwindcss()],
105
- publicDir: production ? false : "public",
106
- server: {
107
- host: "127.0.0.1",
108
- port,
109
- strictPort: true,
110
- // Bun's native filesystem watcher can miss repeated replace/write events.
111
- watch: { usePolling: true, interval: 150 },
112
- },
113
- });
114
- await server.listen();
115
- console.log(
116
- `${production ? "Production build" : "Source HMR"} · SIMULATED uploads and login / 模拟上传与登录`,
117
- );
118
- server.printUrls();
119
- let closing = false;
120
- async function close() {
121
- if (closing) return;
122
- closing = true;
123
- await server.close();
124
- process.exit(0);
125
- }
126
- process.on("SIGINT", close);
127
- process.on("SIGTERM", close);
File without changes