@lingfan-engine/create 0.0.0-stage → 0.1.5
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/LICENSE +201 -0
- package/bin/create.mjs +18 -0
- package/package.json +44 -3
- package/src/cli/cli.ts +302 -0
- package/src/cli/prompts.ts +115 -0
- package/src/generate/dotfiles.ts +27 -0
- package/src/generate/index.ts +20 -0
- package/src/generate/project.ts +279 -0
- package/src/generate/replace.ts +91 -0
- package/src/index.ts +46 -0
- package/src/overlay/index.ts +10 -0
- package/src/overlay/merge.ts +143 -0
- package/src/registry/index.ts +15 -0
- package/src/registry/template-meta.ts +107 -0
- package/src/registry/versions.ts +109 -0
- package/src/shared/guards.ts +85 -0
- package/src/shared/index.ts +7 -0
- package/src/shared/texts.ts +55 -0
- package/template/README.md +138 -0
- package/template/__PROJECT__/Resources/Audio/.gitkeep +0 -0
- package/template/__PROJECT__/Resources/Fonts/README.md +10 -0
- package/template/__PROJECT__/Resources/Images/.gitkeep +0 -0
- package/template/__PROJECT__/Resources/Lang/.gitkeep +0 -0
- package/template/__PROJECT__/Resources/Stories/title/title_main.story +24 -0
- package/template/__PROJECT__/Resources/Video/.gitkeep +0 -0
- package/template/__PROJECT__/Resources/project.json +7 -0
- package/template/__PROJECT__/gitignore +8 -0
- package/template/__PROJECT__/index.html +34 -0
- package/template/__PROJECT__/package.json +22 -0
- package/template/__PROJECT__/pnpm-workspace.yaml +8 -0
- package/template/__PROJECT__/src/host/boot.ts +181 -0
- package/template/__PROJECT__/src/host/dialogue-view.ts +235 -0
- package/template/__PROJECT__/src/host/effects.ts +68 -0
- package/template/__PROJECT__/src/host/engine-wiring.ts +206 -0
- package/template/__PROJECT__/src/host/frame-loop.ts +42 -0
- package/template/__PROJECT__/src/host/index.ts +27 -0
- package/template/__PROJECT__/src/host/input-map.ts +51 -0
- package/template/__PROJECT__/src/host/layer-z.ts +50 -0
- package/template/__PROJECT__/src/host/save-slots.ts +38 -0
- package/template/__PROJECT__/src/host/stage-dom.ts +74 -0
- package/template/__PROJECT__/src/host/theme-fonts.ts +60 -0
- package/template/__PROJECT__/src/host/toast.ts +29 -0
- package/template/__PROJECT__/src/main.ts +61 -0
- package/template/__PROJECT__/src/styles/stage.css +183 -0
- package/template/__PROJECT__/src/vite-env.d.ts +14 -0
- package/template/__PROJECT__/tsconfig.json +19 -0
- package/template/__PROJECT__/tsconfig.node.json +14 -0
- package/template/__PROJECT__/vite.config.ts +64 -0
- package/template/overlays/README.md +70 -0
- package/template/overlays/miniprogram/README.md +104 -0
- package/template/overlays/miniprogram/index.html +15 -0
- package/template/overlays/miniprogram/package.patch.json +40 -0
- package/template/overlays/miniprogram/scripts/serve-project.mjs +273 -0
- package/template/overlays/miniprogram/src/App.vue +23 -0
- package/template/overlays/miniprogram/src/components/ChoiceLayer.vue +208 -0
- package/template/overlays/miniprogram/src/components/DialogueLayer.vue +183 -0
- package/template/overlays/miniprogram/src/components/NodeTree.vue +44 -0
- package/template/overlays/miniprogram/src/components/NoticeLayer.vue +87 -0
- package/template/overlays/miniprogram/src/components/SaveLoadBar.vue +168 -0
- package/template/overlays/miniprogram/src/components/StageView.vue +50 -0
- package/template/overlays/miniprogram/src/env.d.ts +1 -0
- package/template/overlays/miniprogram/src/main.ts +12 -0
- package/template/overlays/miniprogram/src/manifest.json +21 -0
- package/template/overlays/miniprogram/src/mp/frame.ts +51 -0
- package/template/overlays/miniprogram/src/mp/guards.ts +28 -0
- package/template/overlays/miniprogram/src/mp/host.ts +252 -0
- package/template/overlays/miniprogram/src/mp/index.ts +9 -0
- package/template/overlays/miniprogram/src/mp/platform.ts +210 -0
- package/template/overlays/miniprogram/src/mp/ports.ts +84 -0
- package/template/overlays/miniprogram/src/mp/project-source.ts +77 -0
- package/template/overlays/miniprogram/src/mp/slots.ts +64 -0
- package/template/overlays/miniprogram/src/mp/surface.ts +239 -0
- package/template/overlays/miniprogram/src/mp/video-surface.ts +104 -0
- package/template/overlays/miniprogram/src/mp/view.ts +91 -0
- package/template/overlays/miniprogram/src/pages/index/index.vue +299 -0
- package/template/overlays/miniprogram/src/pages.json +18 -0
- package/template/overlays/miniprogram/tsconfig.json +20 -0
- package/template/overlays/miniprogram/vite.config.ts +87 -0
- package/template/overlays/react/index.html +16 -0
- package/template/overlays/react/package.patch.json +18 -0
- package/template/overlays/react/src/App.tsx +32 -0
- package/template/overlays/react/src/main.tsx +74 -0
- package/template/overlays/react/src/vite-env.d.ts +14 -0
- package/template/overlays/react/tsconfig.json +20 -0
- package/template/overlays/react/vite.config.ts +67 -0
- package/template/overlays/tauri/README.md +59 -0
- package/template/overlays/tauri/package.patch.json +14 -0
- package/template/overlays/tauri/src/main.ts +58 -0
- package/template/overlays/tauri/src-tauri/Cargo.toml +16 -0
- package/template/overlays/tauri/src-tauri/build.rs +6 -0
- package/template/overlays/tauri/src-tauri/capabilities/default.json +7 -0
- package/template/overlays/tauri/src-tauri/icons/128x128.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/128x128@2x.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/32x32.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square107x107Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square142x142Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square150x150Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square284x284Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square30x30Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square310x310Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square44x44Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square71x71Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/Square89x89Logo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/StoreLogo.png +0 -0
- package/template/overlays/tauri/src-tauri/icons/icon.icns +0 -0
- package/template/overlays/tauri/src-tauri/icons/icon.ico +0 -0
- package/template/overlays/tauri/src-tauri/icons/icon.png +0 -0
- package/template/overlays/tauri/src-tauri/src/lib.rs +26 -0
- package/template/overlays/tauri/src-tauri/src/main.rs +7 -0
- package/template/overlays/tauri/src-tauri/src/preferences.rs +50 -0
- package/template/overlays/tauri/src-tauri/src/save.rs +178 -0
- package/template/overlays/tauri/src-tauri/tauri.conf.json +35 -0
- package/template/overlays/vue/index.html +16 -0
- package/template/overlays/vue/package.patch.json +16 -0
- package/template/overlays/vue/src/App.vue +30 -0
- package/template/overlays/vue/src/main.ts +68 -0
- package/template/overlays/vue/src/vite-env.d.ts +24 -0
- package/template/overlays/vue/tsconfig.json +19 -0
- package/template/overlays/vue/vite.config.ts +67 -0
- package/template/pack-template.ps1 +65 -0
- package/template/template-meta.json +40 -0
- package/README.md +0 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineConfig, type Plugin } from "vite";
|
|
4
|
+
|
|
5
|
+
/** 字体文件扩展名集合(与 stories-build 的字体名注册表同口径) */
|
|
6
|
+
const FONT_EXTENSIONS = [".ttf", ".otf", ".woff", ".woff2"] as const;
|
|
7
|
+
|
|
8
|
+
/** 字体清单条目:file = 资源文件名,name = 字体名(文件名去扩展名) */
|
|
9
|
+
interface StoryFontEntry {
|
|
10
|
+
file: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resources/Fonts 扫描:点文件与未知扩展名跳过,文件名字典序稳定 */
|
|
15
|
+
function scanFonts(fontsDir: string): readonly StoryFontEntry[] {
|
|
16
|
+
if (!existsSync(fontsDir)) return [];
|
|
17
|
+
return readdirSync(fontsDir)
|
|
18
|
+
.filter((file) => {
|
|
19
|
+
const dot = file.lastIndexOf(".");
|
|
20
|
+
return (
|
|
21
|
+
dot > 0 &&
|
|
22
|
+
(FONT_EXTENSIONS as readonly string[]).includes(
|
|
23
|
+
file.slice(dot).toLowerCase(),
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
})
|
|
27
|
+
.sort()
|
|
28
|
+
.map((file) => ({ file, name: file.slice(0, file.lastIndexOf(".")) }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 字体清单虚拟模块:构建期扫描 Resources/Fonts 产出字体名清单,
|
|
33
|
+
* 宿主据此注入 @font-face 与项目默认字体——字体名注册表与 stories:build 同源,
|
|
34
|
+
* 零新增依赖(脚手架工程保持可独立安装)。
|
|
35
|
+
*/
|
|
36
|
+
function storyFontsVirtual(): Plugin {
|
|
37
|
+
const moduleId = "virtual:story-fonts";
|
|
38
|
+
const fontsDir = fileURLToPath(new URL("./Resources/Fonts", import.meta.url));
|
|
39
|
+
return {
|
|
40
|
+
name: "story-fonts",
|
|
41
|
+
resolveId(id) {
|
|
42
|
+
if (id === moduleId) return `\0${moduleId}`;
|
|
43
|
+
return null;
|
|
44
|
+
},
|
|
45
|
+
load(id) {
|
|
46
|
+
if (id === `\0${moduleId}`) {
|
|
47
|
+
return `export const storyFonts = ${JSON.stringify(scanFonts(fontsDir))};`;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 脚手架工程的 Vite 配置:仅约定资源根与宿主端口,其余随宿主需要自行扩展。 */
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
// 项目资源根即静态根 —— 故事里的逻辑路径 `Audio/x.mp3` 解析为 `/Audio/x.mp3`,
|
|
57
|
+
// 开发与打包产物一致,不依赖进程工作目录。资源根之外无隐式查找路径。
|
|
58
|
+
publicDir: "Resources",
|
|
59
|
+
|
|
60
|
+
// 宿主端口与 Tauri 约定一致;换平台壳时只改这里
|
|
61
|
+
server: { port: 1420, strictPort: true },
|
|
62
|
+
|
|
63
|
+
plugins: [storyFontsVirtual()],
|
|
64
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# 叠加层
|
|
2
|
+
|
|
3
|
+
每个子目录是一层可选的工程内容,目录名就是 `template-meta.json` 里 `frameworks[]` / `shells[]` 的 `id`。
|
|
4
|
+
生成器按「骨架 → 框架层 → 壳层」的顺序合并,同路径文件由后一层覆盖前一层。
|
|
5
|
+
|
|
6
|
+
## 缺省项没有目录
|
|
7
|
+
|
|
8
|
+
`none`(框架无关)与 `browser`(浏览器)表示「这一层什么都不加」,所以这里看不到它们。
|
|
9
|
+
缺目录不是错误,只是没有内容可叠。
|
|
10
|
+
|
|
11
|
+
## 现有层
|
|
12
|
+
|
|
13
|
+
- `vue/` —— 页面结构换成单文件组件:`index.html` 变成挂载点,新增 `src/App.vue` 与 Vue 版组合根,
|
|
14
|
+
`package.patch.json` 补 `vue` / `@vitejs/plugin-vue` / `vue-tsc`,并把 `typecheck` 换成
|
|
15
|
+
`vue-tsc --noEmit && tsc --noEmit -p tsconfig.node.json`。`src/host/**` 一行不改,
|
|
16
|
+
样式仍用骨架的 `src/styles/stage.css`。
|
|
17
|
+
- `react/` —— 页面结构换成 JSX 组件:`index.html` 变成挂载点(`#app`),新增 `src/App.tsx`
|
|
18
|
+
(渲染骨架 `must()` 要的全部节点)、React 版组合根 `src/main.tsx`(先 `createRoot` 挂载再读节点句柄,
|
|
19
|
+
然后 `bootHost`)与 `src/vite-env.d.ts`,`vite.config.ts` 在骨架配置上多一个 `@vitejs/plugin-react`
|
|
20
|
+
(`publicDir`、`server.port` 与字体虚拟模块逐项保留),`tsconfig.json` 打开 `jsx: react-jsx`。
|
|
21
|
+
`package.patch.json` 补 `react` / `react-dom` 与 `@types/react` / `@types/react-dom` /
|
|
22
|
+
`@vitejs/plugin-react` / `@types/node`,并把 `typecheck` 换成
|
|
23
|
+
`tsc --noEmit && tsc --noEmit -p tsconfig.node.json`。`src/host/**` 一行不改,
|
|
24
|
+
样式仍用骨架的 `src/styles/stage.css`。
|
|
25
|
+
- `miniprogram/` —— 壳层,换掉整套页面与宿主:页面结构由 `src/components/**` 承接(`stage-canvas`、
|
|
26
|
+
`stage-video` 两个 id 落在组件模板里),宿主装配在 `src/mp/**`(`uni.*` 只在
|
|
27
|
+
`src/mp/platform.ts` 出现一处),`src/main.ts` 按 UniApp 约定返回 `createSSRApp`,
|
|
28
|
+
另有 `src/pages.json`、`src/manifest.json` 与开发期内容服务 `scripts/serve-project.mjs`。
|
|
29
|
+
`vite.config.ts` 保留 `publicDir: "Resources"` 供 H5 形态取资源,同时用
|
|
30
|
+
`build.copyPublicDir: false` 让小程序包体只有代码;`vite` 与 `vue` 钉在 UniApp 插件要求的
|
|
31
|
+
那一代(`create.pinned` 保住),`tsconfig.json` 排除 `src/host/**`——那是浏览器形态的宿主,
|
|
32
|
+
本形态不用它。
|
|
33
|
+
- `tauri/` —— 壳层,只加桌面外壳、不碰页面:新增 `src-tauri/`(`Cargo.toml`、`build.rs`、
|
|
34
|
+
`tauri.conf.json`、`capabilities/default.json`、`src/{main,lib,save,preferences}.rs`)与
|
|
35
|
+
一份覆盖骨架的组合根 `src/main.ts`(`ports.saves` 换成 `createTauriSavePort()`,其余端口不换)。
|
|
36
|
+
`package.patch.json` 补 `@tauri-apps/api` / `@tauri-apps/cli` 与 `tauri` 脚本,
|
|
37
|
+
`@tauri-apps/*` 用 `create.pinned` 钉住。`index.html`、`vite.config.ts` 与 `src/host/**`
|
|
38
|
+
全部沿用骨架:`Resources/` 是 `publicDir`,打包时复制进 `dist/`,所以工程文件与资源仍按
|
|
39
|
+
同一套路径取,不需要额外命令。
|
|
40
|
+
|
|
41
|
+
## 钉住版本
|
|
42
|
+
|
|
43
|
+
`package.patch.json` 可以带一个生成期字段 `create.pinned`:数组里点名的依赖不参与
|
|
44
|
+
「查 registry 写最新版」,版本区间保持骨架记录的原值,写盘前这个字段本身会被抹掉。
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{ "create": { "pinned": ["typescript"] } }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
存在的理由:模板希望依赖尽量新,但上游并非都兼容——`vue-tsc` 在 `typescript` 7 上会因
|
|
51
|
+
`./lib/tsc` 不再导出而直接崩,这类约束只有层作者知道。只有确实需要时才钉,钉住的包不会再跟到
|
|
52
|
+
最新版。
|
|
53
|
+
|
|
54
|
+
## 一层里能放什么
|
|
55
|
+
|
|
56
|
+
- 任意工程文件:路径相对工程根,与骨架同名即覆盖。
|
|
57
|
+
- `package.patch.json`(只在层根):深合并进产物 `package.json`。依赖表按字段合并,
|
|
58
|
+
数组与标量整体取代——数组拼接会得到既非骨架也非叠加层的第三种值。
|
|
59
|
+
|
|
60
|
+
新增依赖只写进 `package.patch.json`,不要复制整份 `package.json`:整份复制会让骨架后来加的字段
|
|
61
|
+
在这一层永久丢失。生成器会在合并之后统一查一次 registry,把依赖写成当下最新的 `^x.y.z`。
|
|
62
|
+
|
|
63
|
+
## 新增一层
|
|
64
|
+
|
|
65
|
+
1. 在 `template-meta.json` 的 `frameworks` 或 `shells` 里加一项 `{ id, label, hint }`。
|
|
66
|
+
2. 建同名目录,放入该层独有的文件与 `package.patch.json`。
|
|
67
|
+
3. 生成器不改:它对目录是缺席即跳过,不需要为新 `id` 写分支。
|
|
68
|
+
|
|
69
|
+
一层只加自己真正需要的东西。能被骨架覆盖的差异(比如只换一个入口文件的 import)优先留在骨架里,
|
|
70
|
+
叠加层越多,同一处逻辑就有越多份副本要跟着改。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# **PROJECT**
|
|
2
|
+
|
|
3
|
+
灵泛叙事引擎工程的小程序形态:页面层是 UniApp(Vue 3)单文件组件,宿主装配在 `src/mp/**`,
|
|
4
|
+
内容(故事与素材)不进包体,走 HTTP 从工程接口取。
|
|
5
|
+
|
|
6
|
+
## 目录
|
|
7
|
+
|
|
8
|
+
| 路径 | 作用 |
|
|
9
|
+
| ------------------------------------- | -------------------------------------------------------------------- |
|
|
10
|
+
| `src/pages/index/index.vue` | 页面装配:取画布、把宿主补丁写进页面状态、把手势转回覆盖层 |
|
|
11
|
+
| `src/components/**` | 舞台、对话、选择、通知四层,加存读条与递归节点树 |
|
|
12
|
+
| `src/mp/**` | 小程序宿主:`uni` 能力封装、端口、渲染数据、覆盖层、视频、帧源、守卫 |
|
|
13
|
+
| `src/App.vue`、`src/main.ts` | 应用外壳与 UniApp 约定入口 |
|
|
14
|
+
| `src/pages.json`、`src/manifest.json` | 页面与平台声明 |
|
|
15
|
+
| `Resources/` | 内容根:清单、故事、音频、视频、图片(**不随包体分发**) |
|
|
16
|
+
| `scripts/serve-project.mjs` | 开发期内容服务 |
|
|
17
|
+
| `src/host/**` | 浏览器形态的宿主,本形态不使用(见「已知残留」) |
|
|
18
|
+
|
|
19
|
+
## 起工程
|
|
20
|
+
|
|
21
|
+
先起内容服务:它把 `Resources/` 按宿主的取法提供出去(`GET /project` 一次返回
|
|
22
|
+
`{manifest, stories}`,`GET /<资源 id>` 取素材本体)。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node scripts/serve-project.mjs # 缺省 127.0.0.1:5273,--port / --host / --root 可改
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
再编译页面,用微信开发者工具导入产物目录:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm dev:mp # 等价 uni -p mp-weixin,产物在 dist/dev/mp-weixin
|
|
32
|
+
pnpm build:mp # 生产产物在 dist/build/mp-weixin
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
想在浏览器里先看效果(同一份页面代码):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm dev:h5 # 等价 uni
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
开发者工具里需要勾上「不校验合法域名」(开发期),生产换成自己的 HTTPS 域名;
|
|
42
|
+
`manifest.json` 的 `appid` 是空的,导入时填自己的。
|
|
43
|
+
|
|
44
|
+
内容服务只是开发期的替代品:生产形态由静态托管 + 后端接口承担同一套取法
|
|
45
|
+
(`GET <基址>/project`、`GET <基址>/<资源 id>`),宿主只换基址。基址用环境变量
|
|
46
|
+
`VITE_PROJECT_BASE_URL` 覆盖,缺省指向上面那个本地端口。
|
|
47
|
+
|
|
48
|
+
## 与浏览器形态的差别
|
|
49
|
+
|
|
50
|
+
- **谁渲染页面结构**:浏览器形态由 `index.html` 的静态节点承接 `must("#id")` 契约;这里由
|
|
51
|
+
`src/components/**` 承接(`stage-canvas`、`stage-video` 两个 id 落在组件模板里)。
|
|
52
|
+
- **帧源**:浏览器用 `requestAnimationFrame`;这里取 `canvas` 的 2d 节点,取不到(或 300ms
|
|
53
|
+
内没拿到)就退回 16ms 定时器,叙事照常推进,动画粒度略粗。
|
|
54
|
+
- **尺寸单位**:页面样式按平台惯例用 `rpx`(宽度基准 750),横屏媒体查询里换回 `px`。舞台盒
|
|
55
|
+
的缩放与平移仍由 `computeStageLayout` 算,与单位无关。
|
|
56
|
+
- **平台能力入口**:`uni.*` 只在 `src/mp/platform.ts` 出现一处,其余全是端口;换端只改那一处。
|
|
57
|
+
|
|
58
|
+
## 包体只有代码
|
|
59
|
+
|
|
60
|
+
故事与素材不随包分发:内容走工程接口,素材走资源端口按 id 取(`Audio/x.mp3` →
|
|
61
|
+
`<基址>/Audio/x.mp3`)。改内容不必重新提审发版。
|
|
62
|
+
|
|
63
|
+
构建配置因此是 `publicDir: "Resources"` + `build.copyPublicDir: false`:资源根仍指向同一个
|
|
64
|
+
目录(H5 形态直接读它),但不进包体。
|
|
65
|
+
|
|
66
|
+
## 有意不做的事
|
|
67
|
+
|
|
68
|
+
- **不写原生 WXML/WXSS**:页面一律是 Vue 3 SFC,由 uni 编译成平台节点,H5 与小程序共用同一份
|
|
69
|
+
页面代码。
|
|
70
|
+
- **不做节点树以外的渲染**:舞台元素走递归节点树(`view`/`text`/`rich-text`/`image`),不接管
|
|
71
|
+
WebGL/Canvas 绘制。需要自定义绘制时另开端口,别塞进宿主。
|
|
72
|
+
- **不内建登录/支付/分享**:这里只做叙事播放面,平台能力按需另接。
|
|
73
|
+
|
|
74
|
+
## 依赖钉版
|
|
75
|
+
|
|
76
|
+
`@dcloudio/vite-plugin-uni` 的 peer 把 `vite` 钉在 `5.2.8`、`@vue/shared` 钉在 `3.4.21`,
|
|
77
|
+
与仓库根的 `vite ^8` / `vue ^3.5` 不是一代。本工程因此自带一份 `vite 5.2.8` 与 `vue 3.4.21`,
|
|
78
|
+
靠包管理器的隔离安装各用各的(`package.json` 里是把这些包写进 `create.pinned` 才保住的:
|
|
79
|
+
不钉住,脚手架会统一改写成 `^最新`,peer 立刻就破)。
|
|
80
|
+
|
|
81
|
+
代价:`vite.config.ts` 不能跟着升到 8,脚本与插件类型按 5 的形态写;上游哪天跟进新 vite,
|
|
82
|
+
这三处(`vite`、`vue`、插件)要成对改完再放开钉版。
|
|
83
|
+
|
|
84
|
+
## 已知残留
|
|
85
|
+
|
|
86
|
+
`src/host/**` 与 `src/styles/stage.css` 是骨架的浏览器宿主,本形态不使用:没有人 import 它们,
|
|
87
|
+
`tsconfig.json` 已把两者排除,类型检查不会碰。脚手架目前只做「同路径覆盖」、没有删除能力,
|
|
88
|
+
所以它们会留在工程里;删掉不影响运行。
|
|
89
|
+
|
|
90
|
+
## 验收这条链路
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pnpm typecheck # vue-tsc + tsc(含 vite.config.ts)
|
|
94
|
+
node scripts/serve-project.mjs --root Resources
|
|
95
|
+
curl http://127.0.0.1:5273/project | head -c 200
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`pnpm dev:mp` 之后用开发者工具导入 `dist/dev/mp-weixin`,模拟器里应当直接进标题页。
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
本工程由脚手架按「框架无关骨架 + 小程序叠加层」生成:`src/mp/**`、`src/components/**`、
|
|
103
|
+
`src/pages/**`、`index.html` 与 `package.json` 的增量来自该层,其余来自骨架。
|
|
104
|
+
改模板请在模板仓库里改,不要只改这一个工程。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
8
|
+
/>
|
|
9
|
+
<title>__PROJECT__</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app"></div>
|
|
13
|
+
<script type="module" src="/src/main.ts"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@dcloudio/uni-app": "3.0.0-4080720251210001",
|
|
4
|
+
"@dcloudio/uni-components": "3.0.0-4080720251210001",
|
|
5
|
+
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
|
|
6
|
+
"@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
|
|
7
|
+
"@lingfan-engine/shell": "^0.1.0",
|
|
8
|
+
"vue": "3.4.21"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@dcloudio/types": "3.4.19",
|
|
12
|
+
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
|
|
13
|
+
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
|
|
14
|
+
"vite": "5.2.8",
|
|
15
|
+
"vue-tsc": "^3.3.5"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "uni -p mp-weixin",
|
|
19
|
+
"dev:h5": "uni",
|
|
20
|
+
"dev:mp": "uni -p mp-weixin",
|
|
21
|
+
"build": "uni build -p mp-weixin",
|
|
22
|
+
"build:h5": "uni build -p h5",
|
|
23
|
+
"build:mp": "uni build -p mp-weixin",
|
|
24
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json"
|
|
25
|
+
},
|
|
26
|
+
"create": {
|
|
27
|
+
"pinned": [
|
|
28
|
+
"@dcloudio/types",
|
|
29
|
+
"@dcloudio/uni-app",
|
|
30
|
+
"@dcloudio/uni-cli-shared",
|
|
31
|
+
"@dcloudio/uni-components",
|
|
32
|
+
"@dcloudio/uni-h5",
|
|
33
|
+
"@dcloudio/uni-mp-weixin",
|
|
34
|
+
"@dcloudio/vite-plugin-uni",
|
|
35
|
+
"typescript",
|
|
36
|
+
"vite",
|
|
37
|
+
"vue"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 本地开发服务:把工程根下的 `Resources/` 按小程序宿主的取法提供出去(仅开发用)。
|
|
4
|
+
*
|
|
5
|
+
* 两条路由,对应宿主的两类请求:
|
|
6
|
+
* - `GET /project` → `{ manifest, stories }`:清单 + 全部故事原文合成一次响应
|
|
7
|
+
* (故事原文不解析,解析归引擎的组装器——与线上接口的约定一致);
|
|
8
|
+
* - `GET /<资源 id>` → 资源文件本体(`Audio/x.mp3`、`Video/x.mp4`…),支持 Range
|
|
9
|
+
* (视频拖动、在线播放都要它)。
|
|
10
|
+
*
|
|
11
|
+
* 内容根缺省指工程的资源根 `Resources/`:与骨架 `vite.config.ts` 的 `publicDir` 同一处,
|
|
12
|
+
* 一个工程只有一份内容,开发服务与浏览器形态读的是同一批文件。
|
|
13
|
+
*
|
|
14
|
+
* 生产形态不需要这个脚本:同一套取法由静态托管 + 后端接口承担,宿主只换基址。
|
|
15
|
+
*
|
|
16
|
+
* 用法:`node scripts/serve-project.mjs [--port 5273] [--host 127.0.0.1] [--root <目录>]`
|
|
17
|
+
*/
|
|
18
|
+
import { createReadStream } from "node:fs";
|
|
19
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import { extname, join, resolve, sep } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
/** 内容根(缺省 = 工程根下的 `Resources/`) */
|
|
25
|
+
const DEFAULT_ROOT = resolve(
|
|
26
|
+
fileURLToPath(new URL(".", import.meta.url)),
|
|
27
|
+
"..",
|
|
28
|
+
"Resources",
|
|
29
|
+
);
|
|
30
|
+
/** 缺省端口:与宿主 `project-source.ts` 的缺省基址一致 */
|
|
31
|
+
const DEFAULT_PORT = 5273;
|
|
32
|
+
/** 缺省只绑回环:开发服务不对外 */
|
|
33
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
34
|
+
/** 故事目录(相对内容根) */
|
|
35
|
+
const STORIES_DIR = "Stories";
|
|
36
|
+
/** 扩展名 → Content-Type */
|
|
37
|
+
const MIME_TYPES = new Map([
|
|
38
|
+
[".json", "application/json; charset=utf-8"],
|
|
39
|
+
[".story", "text/plain; charset=utf-8"],
|
|
40
|
+
[".mp3", "audio/mpeg"],
|
|
41
|
+
[".mp4", "video/mp4"],
|
|
42
|
+
[".webm", "video/webm"],
|
|
43
|
+
[".m4a", "audio/mp4"],
|
|
44
|
+
[".ogg", "audio/ogg"],
|
|
45
|
+
[".png", "image/png"],
|
|
46
|
+
[".jpg", "image/jpeg"],
|
|
47
|
+
[".jpeg", "image/jpeg"],
|
|
48
|
+
[".webp", "image/webp"],
|
|
49
|
+
[".ttf", "font/ttf"],
|
|
50
|
+
[".otf", "font/otf"],
|
|
51
|
+
[".woff2", "font/woff2"],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const options = parseArgs(process.argv.slice(2));
|
|
55
|
+
const root = resolve(options.root ?? DEFAULT_ROOT);
|
|
56
|
+
|
|
57
|
+
const server = createServer((request, response) => {
|
|
58
|
+
handle(request, response).catch((error) => {
|
|
59
|
+
console.error(
|
|
60
|
+
`[serve-project] ${request.method} ${request.url} 失败:${error}`,
|
|
61
|
+
);
|
|
62
|
+
sendJson(response, 500, { error: "服务内部错误" });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.listen(
|
|
67
|
+
options.port ?? DEFAULT_PORT,
|
|
68
|
+
options.host ?? DEFAULT_HOST,
|
|
69
|
+
() => {
|
|
70
|
+
const address = server.address();
|
|
71
|
+
const port = typeof address === "object" && address ? address.port : "?";
|
|
72
|
+
console.log(
|
|
73
|
+
`[serve-project] 已启动:http://${options.host ?? DEFAULT_HOST}:${port}/`,
|
|
74
|
+
);
|
|
75
|
+
console.log(`[serve-project] 内容根:${root}`);
|
|
76
|
+
console.log(`[serve-project] 工程接口:http://127.0.0.1:${port}/project`);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 路由:`/project` 是工程接口,其余按静态资源处理。
|
|
82
|
+
* @param {import("node:http").IncomingMessage} request
|
|
83
|
+
* @param {import("node:http").ServerResponse} response
|
|
84
|
+
*/
|
|
85
|
+
async function handle(request, response) {
|
|
86
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
87
|
+
sendJson(response, 405, { error: "只支持 GET/HEAD" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const pathname = decodeURIComponent(
|
|
91
|
+
new URL(request.url ?? "/", "http://127.0.0.1").pathname,
|
|
92
|
+
);
|
|
93
|
+
if (pathname === "/project") {
|
|
94
|
+
await sendProject(response);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await sendFile(response, pathname, request.headers.range);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 工程接口:清单与全部故事原文 */
|
|
101
|
+
async function sendProject(response) {
|
|
102
|
+
const manifestText = await readFile(join(root, "project.json"), "utf8");
|
|
103
|
+
let manifest;
|
|
104
|
+
try {
|
|
105
|
+
manifest = JSON.parse(manifestText);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("project.json 不是合法 JSON");
|
|
108
|
+
}
|
|
109
|
+
const stories = await readStories(root);
|
|
110
|
+
sendJson(response, 200, { manifest, stories });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 递归收齐故事文件:逻辑路径(`Stories/…`)→ 原始文本。
|
|
115
|
+
* @param {string} directory 内容根
|
|
116
|
+
* @returns {Promise<Record<string, string>>}
|
|
117
|
+
*/
|
|
118
|
+
async function readStories(directory) {
|
|
119
|
+
const storiesRoot = join(directory, STORIES_DIR);
|
|
120
|
+
const files = await walk(storiesRoot);
|
|
121
|
+
const stories = {};
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const relative = file
|
|
124
|
+
.slice(directory.length + 1)
|
|
125
|
+
.split(sep)
|
|
126
|
+
.join("/");
|
|
127
|
+
stories[relative] = await readFile(file, "utf8");
|
|
128
|
+
}
|
|
129
|
+
return stories;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 递归列文件(只收 .json 与 .story:故事文件的两种形态)。
|
|
134
|
+
* @param {string} directory
|
|
135
|
+
* @returns {Promise<string[]>}
|
|
136
|
+
*/
|
|
137
|
+
async function walk(directory) {
|
|
138
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
139
|
+
const files = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const path = join(directory, entry.name);
|
|
142
|
+
if (entry.isDirectory()) files.push(...(await walk(path)));
|
|
143
|
+
else if (
|
|
144
|
+
extname(entry.name) === ".json" ||
|
|
145
|
+
extname(entry.name) === ".story"
|
|
146
|
+
)
|
|
147
|
+
files.push(path);
|
|
148
|
+
}
|
|
149
|
+
return files.sort();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 静态资源:路径必须落在内容根之内(拒绝 `..` 与绝对路径逃逸),带 Range 支持。
|
|
154
|
+
* @param {import("node:http").ServerResponse} response
|
|
155
|
+
* @param {string} pathname 请求路径(已解码)
|
|
156
|
+
* @param {string | undefined} rangeHeader
|
|
157
|
+
*/
|
|
158
|
+
async function sendFile(response, pathname, rangeHeader) {
|
|
159
|
+
const target = resolve(join(root, pathname));
|
|
160
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
161
|
+
sendJson(response, 400, { error: "路径越出内容根" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let info;
|
|
165
|
+
try {
|
|
166
|
+
info = await stat(target);
|
|
167
|
+
} catch {
|
|
168
|
+
sendJson(response, 404, { error: `没有这个资源:${pathname}` });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!info.isFile()) {
|
|
172
|
+
sendJson(response, 404, { error: `不是文件:${pathname}` });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const type =
|
|
176
|
+
MIME_TYPES.get(extname(target).toLowerCase()) ?? "application/octet-stream";
|
|
177
|
+
const headers = {
|
|
178
|
+
"Content-Type": type,
|
|
179
|
+
"Accept-Ranges": "bytes",
|
|
180
|
+
"Cache-Control": "no-store",
|
|
181
|
+
"Access-Control-Allow-Origin": "*",
|
|
182
|
+
};
|
|
183
|
+
const range = parseRange(rangeHeader, info.size);
|
|
184
|
+
if (range === "invalid") {
|
|
185
|
+
headers["Content-Range"] = `bytes */${info.size}`;
|
|
186
|
+
sendJson(response, 416, { error: "Range 不合法" }, headers);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (range === null) {
|
|
190
|
+
headers["Content-Length"] = String(info.size);
|
|
191
|
+
response.writeHead(200, headers);
|
|
192
|
+
createReadStream(target).pipe(response);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
headers["Content-Range"] = `bytes ${range.start}-${range.end}/${info.size}`;
|
|
196
|
+
headers["Content-Length"] = String(range.end - range.start + 1);
|
|
197
|
+
response.writeHead(206, headers);
|
|
198
|
+
createReadStream(target, { start: range.start, end: range.end }).pipe(
|
|
199
|
+
response,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 解析 `Range: bytes=a-b`:返回闭区间、`null`(无 Range)或 `"invalid"`。
|
|
205
|
+
* @param {string | undefined} header
|
|
206
|
+
* @param {number} size
|
|
207
|
+
* @returns {{ start: number; end: number } | null | "invalid"}
|
|
208
|
+
*/
|
|
209
|
+
function parseRange(header, size) {
|
|
210
|
+
if (typeof header !== "string" || !header.startsWith("bytes=")) return null;
|
|
211
|
+
const spec = header.slice("bytes=".length).split(",")[0].trim();
|
|
212
|
+
const dash = spec.indexOf("-");
|
|
213
|
+
if (dash < 0) return "invalid";
|
|
214
|
+
const startText = spec.slice(0, dash).trim();
|
|
215
|
+
const endText = spec.slice(dash + 1).trim();
|
|
216
|
+
let start;
|
|
217
|
+
let end;
|
|
218
|
+
if (startText === "") {
|
|
219
|
+
// `bytes=-N`:最后 N 字节
|
|
220
|
+
const tail = Number(endText);
|
|
221
|
+
if (!Number.isFinite(tail) || tail <= 0) return "invalid";
|
|
222
|
+
start = Math.max(0, size - tail);
|
|
223
|
+
end = size - 1;
|
|
224
|
+
} else {
|
|
225
|
+
start = Number(startText);
|
|
226
|
+
end = endText === "" ? size - 1 : Number(endText);
|
|
227
|
+
}
|
|
228
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return "invalid";
|
|
229
|
+
if (start < 0 || end < start || start >= size) return "invalid";
|
|
230
|
+
return { start, end: Math.min(end, size - 1) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 回一份 JSON。
|
|
235
|
+
* @param {import("node:http").ServerResponse} response
|
|
236
|
+
* @param {number} status
|
|
237
|
+
* @param {unknown} body
|
|
238
|
+
* @param {import("node:http").OutgoingHttpHeaders} [extra]
|
|
239
|
+
*/
|
|
240
|
+
function sendJson(response, status, body, extra = {}) {
|
|
241
|
+
const text = JSON.stringify(body);
|
|
242
|
+
response.writeHead(status, {
|
|
243
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
244
|
+
"Content-Length": Buffer.byteLength(text),
|
|
245
|
+
"Access-Control-Allow-Origin": "*",
|
|
246
|
+
"Cache-Control": "no-store",
|
|
247
|
+
...extra,
|
|
248
|
+
});
|
|
249
|
+
response.end(text);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 命令行参数:`--port` / `--host` / `--root`,也认 `--key=value` 形态。
|
|
254
|
+
* @param {string[]} argv
|
|
255
|
+
* @returns {{ port?: number; host?: string; root?: string }}
|
|
256
|
+
*/
|
|
257
|
+
function parseArgs(argv) {
|
|
258
|
+
/** @type {{ port?: number; host?: string; root?: string }} */
|
|
259
|
+
const out = {};
|
|
260
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
261
|
+
const arg = argv[index];
|
|
262
|
+
const paired = arg.startsWith("--") && arg.includes("=");
|
|
263
|
+
const key = paired ? arg.slice(2, arg.indexOf("=")) : arg.slice(2);
|
|
264
|
+
const value = paired ? arg.slice(arg.indexOf("=") + 1) : argv[index + 1];
|
|
265
|
+
if (!paired) index += 1;
|
|
266
|
+
if (value === undefined) throw new Error(`参数缺少取值:${arg}`);
|
|
267
|
+
if (key === "port") out.port = Number(value);
|
|
268
|
+
else if (key === "host") out.host = value;
|
|
269
|
+
else if (key === "root") out.root = value;
|
|
270
|
+
else throw new Error(`未知参数:${arg}`);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 应用外壳:只定全局底色与文字默认值。
|
|
4
|
+
*
|
|
5
|
+
* 与桌面宿主同一套口径——层自己带外观,不靠继承:小程序里 `page` 就是那层「窗口底色」,
|
|
6
|
+
* 舞台层与 UI 层各自把自己需要的颜色写清楚。
|
|
7
|
+
*/
|
|
8
|
+
import { onLaunch } from "@dcloudio/uni-app";
|
|
9
|
+
|
|
10
|
+
onLaunch(() => {
|
|
11
|
+
// 宿主自检入口:装载与装配都归页面(`pages/index/index.vue`),这里不做叙事接线。
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
page {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
background: #0a0a10;
|
|
20
|
+
color: #e6e6f0;
|
|
21
|
+
font-size: 30rpx;
|
|
22
|
+
}
|
|
23
|
+
</style>
|