@picloom/create-theme 0.1.0 → 0.1.1
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 +9 -6
- package/dist/template/README.md +6 -3
- package/dist/template/gitignore +3 -0
- package/dist/template/package.json +6 -6
- package/dist/template/tsconfig.json +9 -4
- package/dist/template/vite.config.ts +6 -3
- package/index.js +1 -1
- package/package.json +2 -2
- package/dist/template/dev/context.ts +0 -363
- package/dist/template/dev/helpers.d.ts +0 -18
- package/dist/template/dev/helpers.js +0 -114
- package/dist/template/dev/index.html +0 -12
- package/dist/template/dev/main.ts +0 -179
- package/dist/template/dev/virtual.d.ts +0 -5
- package/dist/template/scripts/build.ts +0 -119
- package/dist/template/scripts/manifest.ts +0 -61
- package/dist/template/scripts/serve.ts +0 -127
- /package/dist/template/{dev → src}/env.d.ts +0 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ system `zip` is required.
|
|
|
7
7
|
Create a project:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
bun create @picloom/theme@0.1.
|
|
10
|
+
bun create @picloom/theme@0.1.1 ./my-theme
|
|
11
11
|
cd my-theme
|
|
12
12
|
bun install
|
|
13
13
|
bun run dev
|
|
@@ -17,18 +17,21 @@ bun run preview
|
|
|
17
17
|
|
|
18
18
|
The target must not exist; its parent must exist. The creator copies files only;
|
|
19
19
|
it does not install dependencies, initialize Git or publish. Both home and login
|
|
20
|
-
pages start from the
|
|
20
|
+
pages start from the template maintained in this package.
|
|
21
21
|
`--help` prints usage.
|
|
22
22
|
|
|
23
23
|
To test local packages, use the `.tgz` workflow in
|
|
24
24
|
[theme tooling](https://llleixx.github.io/picloom/development/theme-tooling.html).
|
|
25
25
|
Maintainers run `bun run build:create-theme` before `bun pm pack --ignore-scripts`.
|
|
26
|
-
The build copies `
|
|
27
|
-
|
|
26
|
+
The build copies `template/` into `dist/template/` without reading the host repository. Preview and ZIP builds are provided
|
|
27
|
+
by `@picloom/theme-tools`; generated projects do not contain a copy of the preview host.
|
|
28
|
+
Do not edit `dist/template/`. Use exact npm version 0.1.1.
|
|
28
29
|
|
|
29
30
|
## 中文
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
基于工具链仓库独立维护的模板创建 Vue 3 主题。提供模拟预览、热更新、生产产物预览与 ZIP 打包。
|
|
32
33
|
创建工具只复制文件,不覆盖已有目录、不安装依赖、不初始化 Git、不发布。
|
|
33
34
|
模拟预览不连接真实服务;上传、登录与权限必须在隔离测试实例安装后验证。
|
|
34
|
-
通过 npm 创建项目,也可按文档使用本地 `.tgz`
|
|
35
|
+
通过 npm 创建项目,也可按文档使用本地 `.tgz` 验证开发版本。编辑 `template/` 维护模板,构建时复制到 `dist/template/`,不依赖主仓库默认主题源码。
|
|
36
|
+
|
|
37
|
+
协议类型来自 `@picloom/theme-sdk`,预览与构建调用 `@picloom/theme-tools`;生成项目不复制通用工具代码。
|
package/dist/template/README.md
CHANGED
|
@@ -40,7 +40,7 @@ there. Increase `theme.json.version` before distributing changed content; packag
|
|
|
40
40
|
versions are immutable on the host. The package builder checks structure and
|
|
41
41
|
resources; full configuration/manifest validation still runs on installation.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
SDK and tool dependencies use exact versions after publication. During local validation, install the matching local tarballs; the current development versions are not yet published.
|
|
44
44
|
Commit your generated `bun.lock` to your own repository after installation.
|
|
45
45
|
|
|
46
46
|
## 中文
|
|
@@ -49,9 +49,12 @@ Commit your generated `bun.lock` to your own repository after installation.
|
|
|
49
49
|
- `bun run dev` 提供模拟首页、登录页、上传状态、语言和明暗模式;只用于界面开发。
|
|
50
50
|
- 修改 Vue 组件及样式会热更新;入口、清单和本地配置修改后刷新预览。
|
|
51
51
|
- 首次启动时按 Schema 默认值创建 `dev/config.json`,之后清空或删除值不会自动补回。
|
|
52
|
-
- `bun run build`
|
|
52
|
+
- `bun run typecheck` 单独检查类型;`bun run build` 构建并生成可安装 ZIP;`bun run preview` 检查已有构建产物。
|
|
53
53
|
- 修改主题 ID、显示名称和版本均以 `theme.json` 为准,静态资源放入 `public/assets/`。
|
|
54
54
|
- 入口导入的样式合并为 `style.css`,须保留清单全局样式声明;页面专属样式通过各页面的 `styles` 引用,切页时分别加载和卸载。
|
|
55
55
|
- 主题包不包含开发面板、本地配置、凭据或源码映射。所有主题配置可能公开,不要保存密钥。
|
|
56
56
|
- 在隔离 Picloom 测试实例安装 ZIP 后验证真实上传和登录;模拟成功不代表真实服务已验证。
|
|
57
|
-
- SDK
|
|
57
|
+
- SDK 和开发工具在发布后使用 npm 精确版本;当前未发布版本使用本地包验证,安装时同步更新锁文件。
|
|
58
|
+
|
|
59
|
+
Local preview and ZIP builds are provided by `@picloom/theme-tools` using this theme’s Vite configuration.
|
|
60
|
+
本地预览和 ZIP 构建由 `@picloom/theme-tools` 统一提供,使用本主题的 Vite 配置,不复制预览宿主源码。
|
package/dist/template/gitignore
CHANGED
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
"bun": ">=1.4.3"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "bun
|
|
10
|
+
"dev": "bun --bun picloom-theme dev",
|
|
11
11
|
"typecheck": "bun --bun vue-tsc --noEmit",
|
|
12
|
-
"build": "bun
|
|
13
|
-
"preview": "bun
|
|
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.
|
|
16
|
+
"@picloom/theme-sdk": "0.1.1",
|
|
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.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
"skipLibCheck": true,
|
|
10
10
|
"resolveJsonModule": true,
|
|
11
11
|
"esModuleInterop": true,
|
|
12
|
-
"lib": [
|
|
13
|
-
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.1.1",
|
|
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 ``;
|
|
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,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
|