@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 点文件名还原。
|
|
3
|
+
*
|
|
4
|
+
* npm 把包安装到 `node_modules` 时,会把包内的 `.gitignore` 改名成 `.npmignore`
|
|
5
|
+
* (免得某个依赖的忽略规则反过来影响使用者的仓库),但 tarball 里仍然是原名——
|
|
6
|
+
* 同一个文件在「打包后」与「安装后」两个时刻叫法不同。
|
|
7
|
+
*
|
|
8
|
+
* 生成器读的是安装后的目录,所以模板里把这类文件存成不带点开头的普通名字,
|
|
9
|
+
* 由本表在产物里改回去。不还原的后果是具体的:新工程拿不到 `.gitignore`
|
|
10
|
+
* (`node_modules/`、`dist/`、`Resources/Saves/` 会被误提交进版本库),
|
|
11
|
+
* 却多出一个在工程里没有任何作用的 `.npmignore`。
|
|
12
|
+
*
|
|
13
|
+
* 只处理工程根的那一层:嵌套目录里的同名文件由使用者自己管,模板不带。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 安装期被 npm 改名过的文件名 → 产物里应有的名字 */
|
|
17
|
+
const RESTORED = new Map([["gitignore", ".gitignore"]]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 把模板里为了躲开改名而写的普通文件名还原成点文件名。
|
|
21
|
+
*
|
|
22
|
+
* 只认工程根:`path` 里出现分隔符时原样返回,避免动到使用者目录里的同名文件。
|
|
23
|
+
*/
|
|
24
|
+
export function restoreDotfile(path: string): string {
|
|
25
|
+
if (path.includes("/")) return path;
|
|
26
|
+
return RESTORED.get(path) ?? path;
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成域的公共出口。
|
|
3
|
+
*
|
|
4
|
+
* 只做转发:域外的调用方(CLI、测试)只认这一处,内部文件改位置不会外溢。
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
collectDependencyNames,
|
|
8
|
+
generateProject,
|
|
9
|
+
layerBases,
|
|
10
|
+
pinnedNames,
|
|
11
|
+
SKELETON_DIR,
|
|
12
|
+
} from "./project.ts";
|
|
13
|
+
export type { GenerateOptions, GenerateResult } from "./project.ts";
|
|
14
|
+
export {
|
|
15
|
+
isTextPath,
|
|
16
|
+
PLACEHOLDERS,
|
|
17
|
+
replaceInFile,
|
|
18
|
+
replacePlaceholders,
|
|
19
|
+
} from "./replace.ts";
|
|
20
|
+
export { restoreDotfile } from "./dotfiles.ts";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成编排:读模板 → 合叠加层 → 落版本 → 替换占位符 → 写盘。
|
|
3
|
+
*
|
|
4
|
+
* 顺序是有依赖的,不能互换:
|
|
5
|
+
* 1. 合并拿到完整文件表(叠加层已覆盖骨架的同名文件);
|
|
6
|
+
* 2. 把 `package.patch.json` 的增量深合进 `package.json`,并在此之上写版本号——
|
|
7
|
+
* 叠加层新增的依赖也要被解析到最新版,所以版本注入必须发生在合并之后;
|
|
8
|
+
* 3. 最后做占位符替换,产物文本因此与磁盘上写出的字节完全一致。
|
|
9
|
+
*
|
|
10
|
+
* 写盘是「全有或全无」的:先算出全部产物再逐个写,中途不会因为某个文件失败而留下半个工程。
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import {
|
|
21
|
+
listFiles,
|
|
22
|
+
mergeLayers,
|
|
23
|
+
PATCH_SUFFIX,
|
|
24
|
+
type PendingFile,
|
|
25
|
+
} from "../overlay/index.ts";
|
|
26
|
+
import {
|
|
27
|
+
applyResolvedVersions,
|
|
28
|
+
overlayDirOf,
|
|
29
|
+
readTemplateMeta,
|
|
30
|
+
} from "../registry/index.ts";
|
|
31
|
+
import { replaceInFile } from "./replace.ts";
|
|
32
|
+
import { restoreDotfile } from "./dotfiles.ts";
|
|
33
|
+
|
|
34
|
+
/** 骨架目录名:占位符本身,也是模板里唯一随框架无关的目录 */
|
|
35
|
+
export const SKELETON_DIR = "__PROJECT__";
|
|
36
|
+
|
|
37
|
+
/** 生成参数 */
|
|
38
|
+
export interface GenerateOptions {
|
|
39
|
+
/** 模板根目录(含 `template-meta.json` 与骨架目录) */
|
|
40
|
+
readonly templateRoot: string;
|
|
41
|
+
/** 生成到哪个目录下 */
|
|
42
|
+
readonly targetBase: string;
|
|
43
|
+
/** 工程名:目录名、包名、清单 id 与页面标题都用它 */
|
|
44
|
+
readonly projectName: string;
|
|
45
|
+
/** 框架叠加层 id */
|
|
46
|
+
readonly framework: string;
|
|
47
|
+
/** 壳叠加层 id */
|
|
48
|
+
readonly shell: string;
|
|
49
|
+
/** 引擎包的最新版本;缺席项沿用模板记录的值 */
|
|
50
|
+
readonly resolved: ReadonlyMap<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 生成结果 */
|
|
54
|
+
export interface GenerateResult {
|
|
55
|
+
/** 生成出来的工程根目录绝对路径 */
|
|
56
|
+
readonly projectDir: string;
|
|
57
|
+
/** 写出的文件数 */
|
|
58
|
+
readonly fileCount: number;
|
|
59
|
+
/** 实际采用的框架与壳 */
|
|
60
|
+
readonly framework: string;
|
|
61
|
+
readonly shell: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 生成一个工程。
|
|
66
|
+
*
|
|
67
|
+
* 只会新建目录:目标已存在且非空时抛错,不做覆盖也不做增量合并——覆盖别人的工程
|
|
68
|
+
* 无法撤销,宁可让使用者先自己删。
|
|
69
|
+
*/
|
|
70
|
+
export function generateProject(options: GenerateOptions): GenerateResult {
|
|
71
|
+
const meta = readTemplateMeta(options.templateRoot);
|
|
72
|
+
const projectDir = join(options.targetBase, options.projectName);
|
|
73
|
+
if (isOccupied(projectDir)) {
|
|
74
|
+
throw new Error(`目标目录已存在且非空:${projectDir}`);
|
|
75
|
+
}
|
|
76
|
+
const merged = mergeLayers(layerBases(options));
|
|
77
|
+
const files = finalize(
|
|
78
|
+
merged.files,
|
|
79
|
+
merged.patch,
|
|
80
|
+
options.resolved,
|
|
81
|
+
meta.placeholder,
|
|
82
|
+
options.projectName,
|
|
83
|
+
);
|
|
84
|
+
mkdirSync(projectDir, { recursive: true });
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const target = join(projectDir, file.path);
|
|
87
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
88
|
+
writeFileSync(target, file.content);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
projectDir,
|
|
92
|
+
fileCount: files.length,
|
|
93
|
+
framework: options.framework,
|
|
94
|
+
shell: options.shell,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 依次列出要合并的目录:骨架在前,框架层次之,壳层最后。
|
|
100
|
+
*
|
|
101
|
+
* 缺席的叠加层直接跳过——缺省项(`none` / `browser`)本就没有目录,
|
|
102
|
+
* 这不是错误,只是「这一层什么都不加」。
|
|
103
|
+
*/
|
|
104
|
+
export function layerBases(options: GenerateOptions): string[] {
|
|
105
|
+
const bases = [join(options.templateRoot, SKELETON_DIR)];
|
|
106
|
+
for (const id of unique([options.framework, options.shell])) {
|
|
107
|
+
const dir = overlayDirOf(options.templateRoot, id);
|
|
108
|
+
if (dir !== undefined && !bases.includes(dir)) bases.push(dir);
|
|
109
|
+
}
|
|
110
|
+
return bases;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 目标目录是否已被占用:存在且含有内容即算占用,空目录可以复用 */
|
|
114
|
+
function isOccupied(dir: string): boolean {
|
|
115
|
+
if (!existsSync(dir)) return false;
|
|
116
|
+
return readdirSync(dir).length > 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 收集这些层里出现过的依赖名。
|
|
121
|
+
*
|
|
122
|
+
* 只读 `package.json` 与根目录的 `*.patch.json`:前者是骨架写的依赖,后者是叠加层
|
|
123
|
+
* 追加的依赖,两处之外不会有依赖声明。顺序去重,供版本查询一次问完。
|
|
124
|
+
*/
|
|
125
|
+
export function collectDependencyNames(bases: readonly string[]): string[] {
|
|
126
|
+
const names = new Set<string>();
|
|
127
|
+
for (const base of bases) {
|
|
128
|
+
if (!existsSync(base)) continue;
|
|
129
|
+
for (const path of listFiles(base)) {
|
|
130
|
+
const isManifest = path === "package.json";
|
|
131
|
+
const isPatch = !path.includes("/") && path.endsWith(PATCH_SUFFIX);
|
|
132
|
+
if (!isManifest && !isPatch) continue;
|
|
133
|
+
const parsed = JSON.parse(
|
|
134
|
+
readFileSync(join(base, path), "utf8"),
|
|
135
|
+
) as Record<string, unknown>;
|
|
136
|
+
for (const field of ["dependencies", "devDependencies"] as const) {
|
|
137
|
+
const table = parsed[field];
|
|
138
|
+
if (isStringMap(table)) {
|
|
139
|
+
for (const name of Object.keys(table)) names.add(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...names];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 收尾:工程清单合并版本、点文件名还原、全部文件替换占位符。
|
|
149
|
+
*
|
|
150
|
+
* 单独拆出来是为了可测——测试要能在不写盘的前提下断言产物字节,
|
|
151
|
+
* 写盘那一步与「算出该写什么」分开后,判据就不必碰真实文件系统。
|
|
152
|
+
*
|
|
153
|
+
* 点文件还原必须在这里发生:模板里存的是躲开 npm 改名的普通名(见 `dotfiles.ts`),
|
|
154
|
+
* 产物路径要写回带点的形态,占位符替换才按产物路径判文本/二进制。
|
|
155
|
+
*/
|
|
156
|
+
export function finalize(
|
|
157
|
+
files: readonly PendingFile[],
|
|
158
|
+
patch: Readonly<Record<string, unknown>> | undefined,
|
|
159
|
+
resolved: ReadonlyMap<string, string>,
|
|
160
|
+
placeholder: string,
|
|
161
|
+
projectName: string,
|
|
162
|
+
): PendingFile[] {
|
|
163
|
+
return files.map((file) => {
|
|
164
|
+
const path = restoreDotfile(file.path);
|
|
165
|
+
const content =
|
|
166
|
+
file.path === "package.json"
|
|
167
|
+
? renderManifest(file.content, patch, resolved)
|
|
168
|
+
: file.content;
|
|
169
|
+
return {
|
|
170
|
+
path,
|
|
171
|
+
content: replaceInFile(path, content, placeholder, projectName),
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 把增量与版本号写进 `package.json`。
|
|
178
|
+
*
|
|
179
|
+
* 依赖表只改「已经存在的键」:模板决定依赖集合,生成器不是包管理器,
|
|
180
|
+
* 不会凭 registry 上的存在性往工程里塞新包。叠加层 `create.pinned` 点名的包
|
|
181
|
+
* 不参与版本解析,保留模板记录的区间——上游版本兼容性只有模板作者知道。
|
|
182
|
+
*/
|
|
183
|
+
function renderManifest(
|
|
184
|
+
content: Buffer,
|
|
185
|
+
patch: Readonly<Record<string, unknown>> | undefined,
|
|
186
|
+
resolved: ReadonlyMap<string, string>,
|
|
187
|
+
): Buffer {
|
|
188
|
+
const manifest = JSON.parse(content.toString("utf8")) as Record<
|
|
189
|
+
string,
|
|
190
|
+
unknown
|
|
191
|
+
>;
|
|
192
|
+
const withPatch =
|
|
193
|
+
patch === undefined ? manifest : mergeManifest(manifest, patch);
|
|
194
|
+
delete withPatch[CREATE_FIELD];
|
|
195
|
+
const pinned = new Set(pinnedNames(patch));
|
|
196
|
+
for (const field of ["dependencies", "devDependencies"] as const) {
|
|
197
|
+
const table = withPatch[field];
|
|
198
|
+
if (isStringMap(table)) {
|
|
199
|
+
withPatch[field] = applyResolvedVersions(
|
|
200
|
+
table,
|
|
201
|
+
withoutNames(resolved, pinned),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return Buffer.from(`${JSON.stringify(withPatch, null, 2)}\n`, "utf8");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 去掉被钉住的包:它们不进解析结果表,版本区间因此保持模板原值 */
|
|
209
|
+
function withoutNames(
|
|
210
|
+
resolved: ReadonlyMap<string, string>,
|
|
211
|
+
names: ReadonlySet<string>,
|
|
212
|
+
): Map<string, string> {
|
|
213
|
+
if (names.size === 0) return new Map(resolved);
|
|
214
|
+
const out = new Map<string, string>();
|
|
215
|
+
for (const [name, version] of resolved) {
|
|
216
|
+
if (!names.has(name)) out.set(name, version);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 顶层深合并:嵌套对象递归,其余(含数组)由增量整体取代 */
|
|
222
|
+
function mergeManifest(
|
|
223
|
+
base: Record<string, unknown>,
|
|
224
|
+
patch: Readonly<Record<string, unknown>>,
|
|
225
|
+
): Record<string, unknown> {
|
|
226
|
+
const out: Record<string, unknown> = { ...base };
|
|
227
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
228
|
+
const current = out[key];
|
|
229
|
+
out[key] =
|
|
230
|
+
isStringMap(current) && isStringMap(value)
|
|
231
|
+
? { ...current, ...value }
|
|
232
|
+
: value;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 生成器专用字段名。
|
|
239
|
+
*
|
|
240
|
+
* 叠加层可以用它声明「生成期要做的事」,这些内容不属于产物清单,写盘前必须抹掉——
|
|
241
|
+
* 否则新工程的 `package.json` 里会多出一个 npm 不认识、读者也不懂的字段。
|
|
242
|
+
*/
|
|
243
|
+
const CREATE_FIELD = "create";
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 读叠加层声明的「不要跟最新版」的依赖名。
|
|
247
|
+
*
|
|
248
|
+
* 存在的理由:模板希望依赖尽量新,但上游并非都兼容——`vue-tsc` 在 `typescript` 7 上
|
|
249
|
+
* 会因 `./lib/tsc` 不再导出而直接崩,这类约束只有模板作者知道。钉住的包保留模板
|
|
250
|
+
* 记录的版本区间,其余照常解析到最新。
|
|
251
|
+
*/
|
|
252
|
+
export function pinnedNames(
|
|
253
|
+
patch: Readonly<Record<string, unknown>> | undefined,
|
|
254
|
+
): string[] {
|
|
255
|
+
if (patch === undefined) return [];
|
|
256
|
+
const section = patch[CREATE_FIELD];
|
|
257
|
+
if (!isPlainRecord(section)) return [];
|
|
258
|
+
const pinned = section.pinned;
|
|
259
|
+
if (!Array.isArray(pinned)) return [];
|
|
260
|
+
return pinned.filter((item): item is string => typeof item === "string");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 只认普通对象:数组与 null 不算 */
|
|
264
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 只认「字符串到字符串」的表:依赖就是这个形状 */
|
|
269
|
+
function isStringMap(value: unknown): value is Record<string, string> {
|
|
270
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return Object.values(value).every((item) => typeof item === "string");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** 去重但保序:框架与壳偶有同名层(如都用 browser),只要合并一次 */
|
|
277
|
+
function unique(values: readonly string[]): string[] {
|
|
278
|
+
return [...new Set(values)];
|
|
279
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 占位符替换。
|
|
3
|
+
*
|
|
4
|
+
* 模板里的占位符有两套大小写形态:`__PROJECT__`(清单 id/name、页面标题、目录名)与
|
|
5
|
+
* `__project__`(`package.json` 的 name,npm 要求小写)。工程名本身只允许小写,
|
|
6
|
+
* 因此两者最终得到同一个字符串;分开列出来是为了让「哪里用了哪种形态」一眼可查,
|
|
7
|
+
* 而不是靠一次全量替换把不该动的字节也改了。
|
|
8
|
+
*
|
|
9
|
+
* 替换面只覆盖**文本文件**:模板里有字体、图片这类二进制资源,逐字节做字符串替换
|
|
10
|
+
* 会把它们改坏(一旦命中就整块错位),所以先判类型再决定是否处理。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 模板二进制资源的扩展名;命中即按字节原样复制,不做文本替换 */
|
|
14
|
+
const BINARY_EXTENSIONS = new Set([
|
|
15
|
+
".png",
|
|
16
|
+
".jpg",
|
|
17
|
+
".jpeg",
|
|
18
|
+
".gif",
|
|
19
|
+
".webp",
|
|
20
|
+
".bmp",
|
|
21
|
+
".avif",
|
|
22
|
+
".tiff",
|
|
23
|
+
".ico",
|
|
24
|
+
".icns",
|
|
25
|
+
".ttf",
|
|
26
|
+
".otf",
|
|
27
|
+
".ttc",
|
|
28
|
+
".woff",
|
|
29
|
+
".woff2",
|
|
30
|
+
".mp3",
|
|
31
|
+
".ogg",
|
|
32
|
+
".wav",
|
|
33
|
+
".m4a",
|
|
34
|
+
".flac",
|
|
35
|
+
".mp4",
|
|
36
|
+
".webm",
|
|
37
|
+
".pdf",
|
|
38
|
+
".zip",
|
|
39
|
+
".gz",
|
|
40
|
+
".7z",
|
|
41
|
+
".rar",
|
|
42
|
+
".wasm",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** 已知的模板占位符形态 → 说明(生成时全部替换为工程名) */
|
|
46
|
+
export const PLACEHOLDERS = ["__PROJECT__", "__project__"] as const;
|
|
47
|
+
|
|
48
|
+
/** 按扩展名判是否文本;未知扩展名一律当文本(模板里的无扩展名文件都是文本) */
|
|
49
|
+
export function isTextPath(path: string): boolean {
|
|
50
|
+
const dot = path.lastIndexOf(".");
|
|
51
|
+
if (dot < 0) return true;
|
|
52
|
+
return !BINARY_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 在文本里替换全部占位符。
|
|
57
|
+
*
|
|
58
|
+
* `placeholder` 是模板声明的权威占位符(当前为 `__PROJECT__`);同时替换它的小写形态,
|
|
59
|
+
* 因为 `package.json` 的 name 必须小写,模板里就写成 `__project__`——只替换大写形态
|
|
60
|
+
* 会让新工程的包名仍然是占位符。
|
|
61
|
+
*/
|
|
62
|
+
export function replacePlaceholders(
|
|
63
|
+
text: string,
|
|
64
|
+
placeholder: string,
|
|
65
|
+
projectName: string,
|
|
66
|
+
): string {
|
|
67
|
+
const forms = new Set([placeholder, placeholder.toLowerCase()]);
|
|
68
|
+
let out = text;
|
|
69
|
+
for (const form of forms) {
|
|
70
|
+
out = out.split(form).join(projectName);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 对一份文件内容做占位符替换:文本走替换,二进制原样返回。
|
|
77
|
+
*
|
|
78
|
+
* 返回 `Buffer` 是为了让调用方写盘时不必关心来源是文本还是字节。
|
|
79
|
+
*/
|
|
80
|
+
export function replaceInFile(
|
|
81
|
+
path: string,
|
|
82
|
+
content: Buffer,
|
|
83
|
+
placeholder: string,
|
|
84
|
+
projectName: string,
|
|
85
|
+
): Buffer {
|
|
86
|
+
if (!isTextPath(path)) return content;
|
|
87
|
+
return Buffer.from(
|
|
88
|
+
replacePlaceholders(content.toString("utf8"), placeholder, projectName),
|
|
89
|
+
"utf8",
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包唯一公共入口。
|
|
3
|
+
*
|
|
4
|
+
* 只做转发:库里没有「怎么问」与「怎么打印」的实现,那些属于 CLI;
|
|
5
|
+
* 这里暴露的是「给定参数就能算出产物」的那一层,供测试与将来的图形化前端复用。
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
collectDependencyNames,
|
|
9
|
+
generateProject,
|
|
10
|
+
layerBases,
|
|
11
|
+
SKELETON_DIR,
|
|
12
|
+
isTextPath,
|
|
13
|
+
PLACEHOLDERS,
|
|
14
|
+
replaceInFile,
|
|
15
|
+
replacePlaceholders,
|
|
16
|
+
} from "./generate/index.ts";
|
|
17
|
+
export type { GenerateOptions, GenerateResult } from "./generate/index.ts";
|
|
18
|
+
export {
|
|
19
|
+
deepMerge,
|
|
20
|
+
listFiles,
|
|
21
|
+
mergeLayers,
|
|
22
|
+
readLayer,
|
|
23
|
+
PATCH_SUFFIX,
|
|
24
|
+
} from "./overlay/index.ts";
|
|
25
|
+
export type { MergedLayer, PendingFile } from "./overlay/index.ts";
|
|
26
|
+
export {
|
|
27
|
+
applyResolvedVersions,
|
|
28
|
+
fetchLatestVersion,
|
|
29
|
+
overlayDirOf,
|
|
30
|
+
readTemplateMeta,
|
|
31
|
+
resolveTemplateRoot,
|
|
32
|
+
resolveVersions,
|
|
33
|
+
OVERLAY_DIR,
|
|
34
|
+
} from "./registry/index.ts";
|
|
35
|
+
export type {
|
|
36
|
+
ResolvedVersion,
|
|
37
|
+
TemplateMeta,
|
|
38
|
+
TemplateOption,
|
|
39
|
+
} from "./registry/index.ts";
|
|
40
|
+
export {
|
|
41
|
+
caretRange,
|
|
42
|
+
isValidProjectName,
|
|
43
|
+
suggestProjectName,
|
|
44
|
+
} from "./shared/index.ts";
|
|
45
|
+
export { defaultCreateTexts } from "./shared/index.ts";
|
|
46
|
+
export type { CreateTexts } from "./shared/index.ts";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 叠加层合并规则。
|
|
3
|
+
*
|
|
4
|
+
* 模板分两层:骨架(`__PROJECT__`,框架无关,任何框架都要)与叠加层
|
|
5
|
+
* (`overlays/<id>`,只含该框架/壳特有的文件)。合并只有三条规则,全部在这里:
|
|
6
|
+
*
|
|
7
|
+
* 1. **同路径覆盖**——叠加层里与骨架同名的文件整体替换骨架那份。
|
|
8
|
+
* 2. **`*.patch.json` 深合并**——叠加层里名为 `package.patch.json` 的文件不是产物,
|
|
9
|
+
* 而是增量:解析成对象后深合进同目录的 `package.json`(这个目录下就是工程根)。
|
|
10
|
+
* 用增量而不是整份替换,是为了让骨架的脚本与依赖始终是唯一事实来源,
|
|
11
|
+
* 叠加层只说「我要多一个依赖、多一条脚本」。
|
|
12
|
+
* 3. 其余文件按相对路径复制。
|
|
13
|
+
*
|
|
14
|
+
* 占位符替换不在这里做——它是纯文本层面的最后一刀,见 `generate` 域。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { join, relative, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** 叠加层的增量文件名;该名本身不进产物 */
|
|
20
|
+
export const PATCH_SUFFIX = ".patch.json";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 一份待写文件:工程内相对路径(posix 分隔)+ 内容。
|
|
24
|
+
*
|
|
25
|
+
* 内容一律是字节:模板里有字体、图片这类资源,按文本读会在遇到非法 UTF-8 时
|
|
26
|
+
* 被静默替换成替换符,写回去就把资源改坏了。文本文件在需要时再解码。
|
|
27
|
+
*/
|
|
28
|
+
export interface PendingFile {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly content: Buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 递归列出一个目录下的全部文件(相对 `base` 的 posix 路径)。
|
|
35
|
+
*
|
|
36
|
+
* 跳过 `node_modules`、`dist`、`.git` 与 Saves:它们是运行/构建副产物,
|
|
37
|
+
* 不该被复制进新工程。判定按目录名精确匹配,不做后缀猜测。
|
|
38
|
+
*/
|
|
39
|
+
export function listFiles(base: string): string[] {
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
const walk = (dir: string): void => {
|
|
42
|
+
for (const entry of readdirSync(dir)) {
|
|
43
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
44
|
+
const full = join(dir, entry);
|
|
45
|
+
if (statSync(full).isDirectory()) {
|
|
46
|
+
walk(full);
|
|
47
|
+
} else {
|
|
48
|
+
out.push(relative(base, full).split(sep).join("/"));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
walk(base);
|
|
53
|
+
return out.sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", "Saves"]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 把一层目录读成待写文件表;`*.patch.json` 被解析成增量单独返回。
|
|
60
|
+
*
|
|
61
|
+
* 增量不混进文件表——它没有对应的产物文件,合并对象语义与「写一份文本」不同,
|
|
62
|
+
* 混在一起会让调用方每次都要多判一次。
|
|
63
|
+
*/
|
|
64
|
+
export function readLayer(base: string): {
|
|
65
|
+
readonly files: PendingFile[];
|
|
66
|
+
readonly patch: Readonly<Record<string, unknown>> | undefined;
|
|
67
|
+
} {
|
|
68
|
+
const files: PendingFile[] = [];
|
|
69
|
+
let patch: Record<string, unknown> | undefined;
|
|
70
|
+
for (const path of listFiles(base)) {
|
|
71
|
+
const name = path.split("/").pop() ?? path;
|
|
72
|
+
if (name.endsWith(PATCH_SUFFIX)) {
|
|
73
|
+
// 只认工程根的那份:嵌套目录里的同名文件按普通文件处理,
|
|
74
|
+
// 免得某个框架在子目录放一个只对子目录包生效的清单时被误当工程清单。
|
|
75
|
+
if (!path.includes("/")) {
|
|
76
|
+
patch = JSON.parse(readFileSync(join(base, path), "utf8")) as Record<
|
|
77
|
+
string,
|
|
78
|
+
unknown
|
|
79
|
+
>;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
files.push({ path, content: readFileSync(join(base, path)) });
|
|
84
|
+
}
|
|
85
|
+
return { files, patch };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 合并结果:文件表(后写覆盖先写)+ 合并后的工程清单增量 */
|
|
89
|
+
export interface MergedLayer {
|
|
90
|
+
readonly files: readonly PendingFile[];
|
|
91
|
+
readonly patch: Readonly<Record<string, unknown>> | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 依次合并多层目录,先出现的层在前(骨架先,叠加层后)。
|
|
96
|
+
*
|
|
97
|
+
* 同路径后者覆盖前者;增量逐层深合并。返回的文件表保持「每个路径只出现一次」,
|
|
98
|
+
* 值是最后写入的那份——调用方可以直接顺序写盘,不需要自己判重。
|
|
99
|
+
*/
|
|
100
|
+
export function mergeLayers(bases: readonly string[]): MergedLayer {
|
|
101
|
+
const byPath = new Map<string, PendingFile>();
|
|
102
|
+
let patch: Record<string, unknown> = {};
|
|
103
|
+
let hasPatch = false;
|
|
104
|
+
for (const base of bases) {
|
|
105
|
+
if (!existsSync(base)) continue;
|
|
106
|
+
const layer = readLayer(base);
|
|
107
|
+
for (const file of layer.files) byPath.set(file.path, file);
|
|
108
|
+
if (layer.patch) {
|
|
109
|
+
patch = deepMerge(patch, layer.patch);
|
|
110
|
+
hasPatch = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
files: [...byPath.values()],
|
|
115
|
+
patch: hasPatch ? patch : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 深合并:普通对象逐键递归,数组与标量由后者整体取代。
|
|
121
|
+
*
|
|
122
|
+
* 数组不拼接是刻意的——`files`、`keywords` 这类字段拼接会得到既非骨架也非叠加层的
|
|
123
|
+
* 第三种值;想改就该整份覆盖,语义可预测。
|
|
124
|
+
*/
|
|
125
|
+
export function deepMerge(
|
|
126
|
+
base: Record<string, unknown>,
|
|
127
|
+
patch: Record<string, unknown>,
|
|
128
|
+
): Record<string, unknown> {
|
|
129
|
+
const out: Record<string, unknown> = { ...base };
|
|
130
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
131
|
+
const current = out[key];
|
|
132
|
+
out[key] =
|
|
133
|
+
isPlainObject(current) && isPlainObject(value)
|
|
134
|
+
? deepMerge(current, value)
|
|
135
|
+
: value;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 只认「对象字面量语义」的普通对象:数组与 null 都不算 */
|
|
141
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
142
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** registry 域唯一出口:模板元数据定位 + npm 版本查询。 */
|
|
2
|
+
export {
|
|
3
|
+
overlayDirOf,
|
|
4
|
+
readTemplateMeta,
|
|
5
|
+
resolveTemplateRoot,
|
|
6
|
+
OVERLAY_DIR,
|
|
7
|
+
type TemplateMeta,
|
|
8
|
+
type TemplateOption,
|
|
9
|
+
} from "./template-meta.ts";
|
|
10
|
+
export {
|
|
11
|
+
applyResolvedVersions,
|
|
12
|
+
fetchLatestVersion,
|
|
13
|
+
resolveVersions,
|
|
14
|
+
type ResolvedVersion,
|
|
15
|
+
} from "./versions.ts";
|