@sfmc-bds/devkit 0.1.0-beta.2 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +7 -8
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/index.js.map +4 -4
- package/package.json +13 -15
- package/dist/scaffold.d.ts +0 -14
- package/dist/scaffold.js +0 -55
- package/scripts/new-module.mjs +0 -659
package/scripts/new-module.mjs
DELETED
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @ts-check
|
|
3
|
-
/**
|
|
4
|
-
* @sfmc-bds/devkit — 生成最小模块骨架(单包根)
|
|
5
|
-
*
|
|
6
|
-
* - 写入 **当前工作目录**(须为空)—— 与 Tanya7z/sfmc-module-template 同构
|
|
7
|
-
* - 传入 `--root` / `SFMC_MODULES_ROOT` 会直接退出(sfmc-modules 仅为 index)
|
|
8
|
-
* - `--official` → `@sfmc-bds/module-<id>`;默认 `@CHANGE_ME/sfmc-module-<id>`
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* mkdir my-mod && cd my-mod
|
|
12
|
-
* node packages/devkit/scripts/new-module.mjs my-mod --name "我的模块"
|
|
13
|
-
* npx sfmc-new-module my-mod --name "我的模块"
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import fs from "node:fs";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import process from "node:process";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {string} msg
|
|
22
|
-
*/
|
|
23
|
-
function die(msg, code = 1) {
|
|
24
|
-
console.error(`[new-module] ${msg}`);
|
|
25
|
-
process.exit(code);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {string | any[]} argv
|
|
30
|
-
*/
|
|
31
|
-
function parseArgs(argv) {
|
|
32
|
-
/** @type {{ name: string | null, root: string | null, template: string, listTemplates: boolean, official: boolean }} */
|
|
33
|
-
const flags = { name: null, root: null, template: "minimal", listTemplates: false, official: false };
|
|
34
|
-
/** @type {string[]} */
|
|
35
|
-
const positional = [];
|
|
36
|
-
for (let i = 0; i < argv.length; i++) {
|
|
37
|
-
const a = argv[i];
|
|
38
|
-
if (a === "--name") flags.name = argv[++i] ?? null;
|
|
39
|
-
else if (a === "--root") flags.root = argv[++i] ?? null;
|
|
40
|
-
else if (a === "--template") flags.template = argv[++i] ?? "minimal";
|
|
41
|
-
else if (a === "--list-templates") flags.listTemplates = true;
|
|
42
|
-
else if (a === "--official") flags.official = true;
|
|
43
|
-
else if (a.startsWith("--name=")) flags.name = a.slice("--name=".length);
|
|
44
|
-
else if (a.startsWith("--root=")) flags.root = a.slice("--root=".length);
|
|
45
|
-
else if (a.startsWith("--template=")) flags.template = a.slice("--template=".length);
|
|
46
|
-
else if (a.startsWith("--")) die(`未知参数: ${a}`);
|
|
47
|
-
else positional.push(a);
|
|
48
|
-
}
|
|
49
|
-
return { flags, positional };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 模板清单 —— 这是 sfmc-modules 可用模板的唯一权威源(OCP)。
|
|
54
|
-
* 加新模板只需:
|
|
55
|
-
* 1) 在下方数组里加一条(配合 buildManifest 分支处理)
|
|
56
|
-
* 2) 在 sfmc CLI 的 i18n 中加 modwiz.tpl.<id> / modwiz.tpl.<id>Hint
|
|
57
|
-
*
|
|
58
|
-
* 注意:不在此处放显示文案;本地化由 sfmc i18n 接管,本工具只暴露机器可读清单。
|
|
59
|
-
*
|
|
60
|
-
* 输出格式:每行 `<id>` 或 `<id>\tdefault` —— 单源、纯文本、无 JSON 依赖。
|
|
61
|
-
*/
|
|
62
|
-
const TEMPLATES = [
|
|
63
|
-
{ id: "minimal", isDefault: true },
|
|
64
|
-
{ id: "db", isDefault: false },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
function emitTemplateList() {
|
|
68
|
-
for (const tpl of TEMPLATES) {
|
|
69
|
-
process.stdout.write(`${tpl.id}${tpl.isDefault ? "\tdefault" : ""}\n`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {string} id
|
|
75
|
-
*/
|
|
76
|
-
function isValidFolderId(id) {
|
|
77
|
-
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(id);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 决定模块骨架落盘位置:写到 cwd(单包根,与 Tanya7z/sfmc-module-template 同构)。
|
|
82
|
-
* 传入 `--root` / `SFMC_MODULES_ROOT` 会退出。
|
|
83
|
-
* @param {{ name?: string | null; root: any; template?: string; listTemplates?: boolean; official?: boolean; }} flags
|
|
84
|
-
*/
|
|
85
|
-
function resolveTargetDir(flags) {
|
|
86
|
-
if (flags.root || process.env.SFMC_MODULES_ROOT) {
|
|
87
|
-
die(
|
|
88
|
-
`--root / SFMC_MODULES_ROOT 已移除:sfmc-modules 仅为 index。请在空目录运行本工具生成单包根,或使用 Tanya7z/sfmc-module-template。`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return path.resolve(process.cwd());
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @param {string} folderId
|
|
96
|
-
* @param {{ official: boolean }} opts
|
|
97
|
-
*/
|
|
98
|
-
function buildPackageJson(folderId, opts) {
|
|
99
|
-
const base = {
|
|
100
|
-
name: opts.official ? `@sfmc-bds/module-${folderId}` : `@CHANGE_ME/sfmc-module-${folderId}`,
|
|
101
|
-
version: "0.1.0",
|
|
102
|
-
type: "module",
|
|
103
|
-
description: `SAPI module: ${folderId}`,
|
|
104
|
-
main: "sapi/src/index.ts",
|
|
105
|
-
exports: {
|
|
106
|
-
".": "./sapi/src/index.ts",
|
|
107
|
-
},
|
|
108
|
-
files: ["sapi", "test", "README.md", "LICENSE"],
|
|
109
|
-
};
|
|
110
|
-
/* 自包含单包根(与 Tanya7z/sfmc-module-template 同构) */
|
|
111
|
-
return {
|
|
112
|
-
...base,
|
|
113
|
-
scripts: {
|
|
114
|
-
build: "npm run typecheck",
|
|
115
|
-
typecheck: "tsc --noEmit -p sapi/tsconfig.json",
|
|
116
|
-
test: "node --test --import @sfmc-bds/sdk/testing/minecraft-loader --import tsx/esm test/*.test.ts",
|
|
117
|
-
lint: 'eslint "sapi/**/*.ts" "test/**/*.ts"',
|
|
118
|
-
format: "prettier --write .",
|
|
119
|
-
},
|
|
120
|
-
devDependencies: {
|
|
121
|
-
"@minecraft/server": "2.10.0-beta.1.26.40-preview.30",
|
|
122
|
-
"@minecraft/server-net": "1.0.0-beta.11940b24",
|
|
123
|
-
"@minecraft/server-ui": "2.2.0-beta.1.26.40-preview.30",
|
|
124
|
-
"@minecraft/vanilla-data": "1.26.40-preview.30",
|
|
125
|
-
"@sfmc-bds/eslint-plugin": "^0.1.0",
|
|
126
|
-
"@sfmc-bds/sdk": "^0.2.0-beta.7",
|
|
127
|
-
"@types/node": "^22.13.0",
|
|
128
|
-
"@typescript-eslint/eslint-plugin": "^8.64.0",
|
|
129
|
-
"@typescript-eslint/parser": "^8.64.0",
|
|
130
|
-
eslint: "^10.7.0",
|
|
131
|
-
prettier: "^3.9.5",
|
|
132
|
-
"prettier-plugin-organize-imports": "^4.3.0",
|
|
133
|
-
tsx: "^4.19.0",
|
|
134
|
-
typescript: "^5.6.0",
|
|
135
|
-
},
|
|
136
|
-
peerDependencies: { "@sfmc-bds/sdk": ">=0.2.0" },
|
|
137
|
-
engines: { node: ">=22.13.0" },
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @param {string} folderId
|
|
143
|
-
* @param {string} displayName
|
|
144
|
-
* @param {"minimal"|"db"} template
|
|
145
|
-
* @param {string} schemaRel $schema 相对 sapi/ 的路径(由调用方按落盘位置计算)
|
|
146
|
-
*/
|
|
147
|
-
function buildManifest(folderId, displayName, template, schemaRel) {
|
|
148
|
-
if (folderId.startsWith("feature-") || folderId.startsWith("core-")) {
|
|
149
|
-
die(`folder 须为短名(不含 feature-/core- 前缀),例如 area 而非 feature-area`);
|
|
150
|
-
}
|
|
151
|
-
const logicalId = `feature-${folderId}`;
|
|
152
|
-
const configKey = folderId.replace(/-/g, "_");
|
|
153
|
-
/** @type {Record<string, unknown>} */
|
|
154
|
-
const base = {
|
|
155
|
-
$schema: schemaRel,
|
|
156
|
-
schemaVersion: 2,
|
|
157
|
-
id: logicalId,
|
|
158
|
-
name: displayName,
|
|
159
|
-
type: "feature",
|
|
160
|
-
configKey,
|
|
161
|
-
requires: [],
|
|
162
|
-
permissions: [`config:read:${configKey}`],
|
|
163
|
-
services: { provides: [], requires: [] },
|
|
164
|
-
notes: `由 @sfmc-bds/devkit new-module 脚手架生成(template=${template})`,
|
|
165
|
-
};
|
|
166
|
-
if (template === "db") {
|
|
167
|
-
base.permissions = [`db:read:sfmc_${configKey}`, `db:write:sfmc_${configKey}`, `config:read:${configKey}`];
|
|
168
|
-
base.notes =
|
|
169
|
-
`由 @sfmc-bds/devkit new-module 脚手架生成(含 db 权限占位)。` +
|
|
170
|
-
`请在 sapi/manifest.json 中补全 routes/migrations,并实现 db 表。`;
|
|
171
|
-
}
|
|
172
|
-
return base;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* 自包含 sapi/tsconfig.json —— 不依赖平台仓 tsconfig.base.json;
|
|
177
|
-
* SDK 类型由 npm 装入 node_modules/@sfmc-bds/sdk 时随附。
|
|
178
|
-
*/
|
|
179
|
-
function buildTsConfigStandalone() {
|
|
180
|
-
return {
|
|
181
|
-
compilerOptions: {
|
|
182
|
-
module: "nodenext",
|
|
183
|
-
moduleResolution: "nodenext",
|
|
184
|
-
target: "es2022",
|
|
185
|
-
lib: ["es2022"],
|
|
186
|
-
types: ["node"],
|
|
187
|
-
strict: true,
|
|
188
|
-
esModuleInterop: true,
|
|
189
|
-
skipLibCheck: true,
|
|
190
|
-
noEmit: true,
|
|
191
|
-
rootDir: "./src",
|
|
192
|
-
},
|
|
193
|
-
include: ["src/**/*"],
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function buildRootTsConfig() {
|
|
198
|
-
return {
|
|
199
|
-
compilerOptions: {
|
|
200
|
-
module: "nodenext",
|
|
201
|
-
moduleResolution: "nodenext",
|
|
202
|
-
target: "es2022",
|
|
203
|
-
lib: ["es2022"],
|
|
204
|
-
types: ["node"],
|
|
205
|
-
strict: true,
|
|
206
|
-
esModuleInterop: true,
|
|
207
|
-
skipLibCheck: true,
|
|
208
|
-
noEmit: true,
|
|
209
|
-
},
|
|
210
|
-
include: ["sapi/**/*", "test/**/*"],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* @param {string} folderId
|
|
216
|
-
* @param {string} displayName
|
|
217
|
-
* @param {string} pkgName
|
|
218
|
-
*/
|
|
219
|
-
function buildIndexTs(folderId, displayName, pkgName) {
|
|
220
|
-
const logicalId = `feature-${folderId}`;
|
|
221
|
-
const perm = folderId.replace(/-/g, "_");
|
|
222
|
-
return `/**
|
|
223
|
-
* ${pkgName} — ${displayName}
|
|
224
|
-
* 由 @sfmc-bds/devkit new-module 脚手架生成。
|
|
225
|
-
*/
|
|
226
|
-
|
|
227
|
-
import { ModuleRegistry, type ModuleDescriptor } from "@sfmc-bds/sdk/module-loader";
|
|
228
|
-
import { Command, Msg, Permission } from "@sfmc-bds/sdk/sapi/runtime";
|
|
229
|
-
|
|
230
|
-
/** 与 sapi/manifest.json 的 id 一致(逻辑 id,非文件夹短名)。 */
|
|
231
|
-
export const MODULE_ID = "${logicalId}";
|
|
232
|
-
|
|
233
|
-
/** 命令权限名。 */
|
|
234
|
-
export const PERM = "${perm}.use";
|
|
235
|
-
|
|
236
|
-
function registerPermissions(): void {
|
|
237
|
-
Permission.register(PERM, Permission.Any);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function registerCommands(): void {
|
|
241
|
-
Command.register(
|
|
242
|
-
"${perm}",
|
|
243
|
-
PERM,
|
|
244
|
-
(player) => {
|
|
245
|
-
if (!player) return;
|
|
246
|
-
Msg.info("模块 ${displayName} 已就绪", player);
|
|
247
|
-
},
|
|
248
|
-
"${displayName}",
|
|
249
|
-
MODULE_ID
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function registerEvents(): void {
|
|
254
|
-
/* 事件订阅放在本阶段,不要放进 init()。 */
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function init(): void {
|
|
258
|
-
/* TODO: 读取 configs/${perm}.json、注册 db 表等 */
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function cleanup(): void {
|
|
262
|
-
/* TODO: 取消事件订阅、关闭 handle、清理定时器。 */
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export const DESCRIPTOR: ModuleDescriptor = {
|
|
266
|
-
id: MODULE_ID,
|
|
267
|
-
afterWorldLoad: false,
|
|
268
|
-
lifecycle: {
|
|
269
|
-
registerPermissions,
|
|
270
|
-
registerCommands,
|
|
271
|
-
registerEvents,
|
|
272
|
-
init,
|
|
273
|
-
cleanup,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
ModuleRegistry.register(DESCRIPTOR);
|
|
278
|
-
`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* @param {string} folderId
|
|
283
|
-
* @param {string} displayName
|
|
284
|
-
*/
|
|
285
|
-
function buildExampleTest(folderId, displayName) {
|
|
286
|
-
const logicalId = `feature-${folderId}`;
|
|
287
|
-
const cmdName = folderId.replace(/-/g, "_");
|
|
288
|
-
const readyMsg = `模块 ${displayName} 已就绪`;
|
|
289
|
-
return `/**
|
|
290
|
-
* test/${folderId}.test.ts — 模块 lifecycle + 命令冒烟(假引擎)
|
|
291
|
-
*
|
|
292
|
-
* 跑法:npm test(SDK minecraft-loader + createSandbox)
|
|
293
|
-
*/
|
|
294
|
-
|
|
295
|
-
import assert from "node:assert/strict";
|
|
296
|
-
import { readFileSync } from "node:fs";
|
|
297
|
-
import { test } from "node:test";
|
|
298
|
-
import { fileURLToPath } from "node:url";
|
|
299
|
-
import { assertMsg, createSandbox, runCleanup } from "@sfmc-bds/sdk/testing";
|
|
300
|
-
|
|
301
|
-
import { DESCRIPTOR, MODULE_ID, PERM } from "../sapi/src/index.js";
|
|
302
|
-
|
|
303
|
-
const MANIFEST_PATH = fileURLToPath(new URL("../sapi/manifest.json", import.meta.url));
|
|
304
|
-
|
|
305
|
-
function readManifest(): {
|
|
306
|
-
id: string;
|
|
307
|
-
configKey: string;
|
|
308
|
-
permissions?: string[];
|
|
309
|
-
} {
|
|
310
|
-
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8")) as {
|
|
311
|
-
id: string;
|
|
312
|
-
configKey: string;
|
|
313
|
-
permissions?: string[];
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
test("descriptor / MODULE_ID 与 sapi/manifest.json 一致", () => {
|
|
318
|
-
const manifest = readManifest();
|
|
319
|
-
assert.equal(DESCRIPTOR.id, MODULE_ID);
|
|
320
|
-
assert.equal(MODULE_ID, manifest.id, "MODULE_ID 必须等于 manifest.id");
|
|
321
|
-
assert.equal(DESCRIPTOR.id, manifest.id, "DESCRIPTOR.id 必须等于 manifest.id");
|
|
322
|
-
assert.equal(MODULE_ID, "${logicalId}");
|
|
323
|
-
assert.equal(DESCRIPTOR.afterWorldLoad, false);
|
|
324
|
-
assert.equal(typeof DESCRIPTOR.lifecycle.registerPermissions, "function");
|
|
325
|
-
assert.equal(typeof DESCRIPTOR.lifecycle.registerCommands, "function");
|
|
326
|
-
assert.equal(typeof DESCRIPTOR.lifecycle.registerEvents, "function");
|
|
327
|
-
assert.equal(typeof DESCRIPTOR.lifecycle.init, "function");
|
|
328
|
-
assert.equal(typeof DESCRIPTOR.lifecycle.cleanup, "function");
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("PERM / 命令名与 manifest.configKey 对齐", () => {
|
|
332
|
-
const manifest = readManifest();
|
|
333
|
-
assert.ok(manifest.configKey, "manifest.configKey 必填");
|
|
334
|
-
assert.equal(PERM, \`\${manifest.configKey}.use\`);
|
|
335
|
-
assert.ok(
|
|
336
|
-
Array.isArray(manifest.permissions) &&
|
|
337
|
-
manifest.permissions.includes(\`config:read:\${manifest.configKey}\`),
|
|
338
|
-
\`manifest.permissions 应含 config:read:\${manifest.configKey}\`
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("createSandbox lifecycle 跑通", async (t) => {
|
|
343
|
-
const sb = await createSandbox({ module: DESCRIPTOR });
|
|
344
|
-
t.after(() => sb.dispose());
|
|
345
|
-
assert.ok(sb.world);
|
|
346
|
-
assert.ok(sb.system);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("命令 ${cmdName} 触发后,玩家收到 Msg.info", async (t) => {
|
|
350
|
-
const sb = await createSandbox({ module: DESCRIPTOR });
|
|
351
|
-
t.after(() => sb.dispose());
|
|
352
|
-
const player = sb.addPlayer({ id: "tester-1", name: "tester", op: true });
|
|
353
|
-
await sb.triggerCommand("${cmdName}", player);
|
|
354
|
-
assert.ok(assertMsg(player, "${readyMsg}", "§"), "玩家 log 应含预期文本");
|
|
355
|
-
assert.equal(player.log.length, 1);
|
|
356
|
-
assert.match(player.log[0]!, /^§f\\[\\*\\] /);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("cleanup 不抛错", async () => {
|
|
360
|
-
const r = await runCleanup(DESCRIPTOR);
|
|
361
|
-
assert.equal(r.ok, true, \`cleanup 抛出: \${r.error instanceof Error ? r.error.message : String(r.error)}\`);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("PERM 格式正确", () => {
|
|
365
|
-
assert.match(PERM, /^[a-z][a-z0-9_]*\\.use$/);
|
|
366
|
-
});
|
|
367
|
-
`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** 与 sfmc-module-template/eslint.config.js 同构 */
|
|
371
|
-
function buildEslintConfigJs() {
|
|
372
|
-
return `// SFMC 模块 ESLint 配置
|
|
373
|
-
import sfmc from "@sfmc-bds/eslint-plugin";
|
|
374
|
-
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
375
|
-
import tsParser from "@typescript-eslint/parser";
|
|
376
|
-
|
|
377
|
-
export default [
|
|
378
|
-
{
|
|
379
|
-
ignores: ["**/dist/**", "**/node_modules/**", "**/build/**", "**/*.d.ts"],
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
files: ["sapi/**/*.ts", "test/**/*.ts"],
|
|
383
|
-
languageOptions: {
|
|
384
|
-
parser: tsParser,
|
|
385
|
-
parserOptions: {
|
|
386
|
-
ecmaVersion: 2022,
|
|
387
|
-
sourceType: "module",
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
plugins: {
|
|
391
|
-
"@typescript-eslint": tsPlugin,
|
|
392
|
-
"@sfmc-bds": sfmc,
|
|
393
|
-
},
|
|
394
|
-
rules: {
|
|
395
|
-
"no-undef": "off",
|
|
396
|
-
"no-unused-vars": "off",
|
|
397
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
398
|
-
"@typescript-eslint/no-unused-vars": [
|
|
399
|
-
"warn",
|
|
400
|
-
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
401
|
-
],
|
|
402
|
-
"@typescript-eslint/ban-ts-comment": "warn",
|
|
403
|
-
"@typescript-eslint/no-require-imports": "error",
|
|
404
|
-
...sfmc.configs.recommended.rules,
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
/* 旧版 eslint-plugin 静态白名单可能未含 testing;测试文件允许 SDK testing 入口 */
|
|
409
|
-
files: ["test/**/*.ts"],
|
|
410
|
-
rules: {
|
|
411
|
-
"@sfmc-bds/no-sdk-private-export": "off",
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
];
|
|
415
|
-
`;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function buildGitignore() {
|
|
419
|
-
return `node_modules/
|
|
420
|
-
dist/
|
|
421
|
-
*.tgz
|
|
422
|
-
*.log
|
|
423
|
-
.DS_Store
|
|
424
|
-
.idea/
|
|
425
|
-
`;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function buildPrettierRc() {
|
|
429
|
-
return {
|
|
430
|
-
trailingComma: "es5",
|
|
431
|
-
tabWidth: 2,
|
|
432
|
-
semi: true,
|
|
433
|
-
singleQuote: false,
|
|
434
|
-
bracketSpacing: true,
|
|
435
|
-
arrowParens: "always",
|
|
436
|
-
printWidth: 120,
|
|
437
|
-
endOfLine: "crlf",
|
|
438
|
-
plugins: ["prettier-plugin-organize-imports"],
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function buildPrettierIgnore() {
|
|
443
|
-
return `node_modules/
|
|
444
|
-
dist/
|
|
445
|
-
*.tgz
|
|
446
|
-
package-lock.json
|
|
447
|
-
`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function buildLicense() {
|
|
451
|
-
return `ISC License
|
|
452
|
-
|
|
453
|
-
Copyright (c) ${new Date().getFullYear()}, ScriptsForMinecraftServer contributors
|
|
454
|
-
|
|
455
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
456
|
-
purpose with or without fee is hereby granted, provided that the above
|
|
457
|
-
copyright notice and this permission notice appear in all copies.
|
|
458
|
-
|
|
459
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
460
|
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
461
|
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
462
|
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
463
|
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
464
|
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
465
|
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
466
|
-
`;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* @param {string} folderId
|
|
471
|
-
* @param {string} displayName
|
|
472
|
-
* @param {string} pkgName
|
|
473
|
-
*/
|
|
474
|
-
function buildReadme(folderId, displayName, pkgName) {
|
|
475
|
-
return `# ${folderId}
|
|
476
|
-
|
|
477
|
-
${displayName}(\`${pkgName}\`)— SFMC SAPI 模块。
|
|
478
|
-
|
|
479
|
-
## 最短成功路径
|
|
480
|
-
|
|
481
|
-
\`\`\`bash
|
|
482
|
-
npm install
|
|
483
|
-
npm run typecheck
|
|
484
|
-
npm test
|
|
485
|
-
npm run lint
|
|
486
|
-
\`\`\`
|
|
487
|
-
|
|
488
|
-
用 VS Code / Cursor **单独打开本仓根**。推荐扩展:ESLint、Prettier、SFMC Module、Node.js Test Runner。
|
|
489
|
-
|
|
490
|
-
1. \`npm test\`(假引擎;不依赖 \`sfmc.root\`)
|
|
491
|
-
2. 可视化编排:独立应用 **Sapience**
|
|
492
|
-
3. 真机联调:设 \`sfmc.root\` 为 SFMC **工作目录**(含 \`configs/\`、\`modules/\`),再 Start Watch / Reload to BDS
|
|
493
|
-
4. link:\`sfmc mod install ${folderId} --from dir:<本仓绝对路径> --link\`
|
|
494
|
-
|
|
495
|
-
| 命令 | 作用 |
|
|
496
|
-
| --- | --- |
|
|
497
|
-
| \`npm run build\` / \`typecheck\` | tsc --noEmit |
|
|
498
|
-
| \`npm test\` | createSandbox + DESCRIPTOR |
|
|
499
|
-
| \`npm run lint\` / \`format\` | ESLint / Prettier |
|
|
500
|
-
|
|
501
|
-
\`DESCRIPTOR.id\` 须与 \`sapi/manifest.json\` 的 \`id\`(\`feature-${folderId}\`)一致。
|
|
502
|
-
`;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* @param {fs.PathOrFileDescriptor} filePath
|
|
507
|
-
* @param {Record<string, unknown>} data
|
|
508
|
-
*/
|
|
509
|
-
function writeJson(filePath, data) {
|
|
510
|
-
// @ts-ignore
|
|
511
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
512
|
-
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* @param {fs.PathOrFileDescriptor} filePath
|
|
517
|
-
* @param {string | NodeJS.ArrayBufferView<ArrayBufferLike>} content
|
|
518
|
-
*/
|
|
519
|
-
function writeText(filePath, content) {
|
|
520
|
-
// @ts-ignore
|
|
521
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
522
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* @param {string} target
|
|
527
|
-
*/
|
|
528
|
-
function writeVscodeWorkspace(target) {
|
|
529
|
-
writeJson(path.join(target, ".vscode", "extensions.json"), {
|
|
530
|
-
recommendations: [
|
|
531
|
-
"dbaeumer.vscode-eslint",
|
|
532
|
-
"esbenp.prettier-vscode",
|
|
533
|
-
"sfmc-bds.sfmc-module",
|
|
534
|
-
"connor4312.nodejs-testing",
|
|
535
|
-
],
|
|
536
|
-
});
|
|
537
|
-
writeJson(path.join(target, ".vscode", "settings.json"), {
|
|
538
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
539
|
-
"editor.formatOnSave": true,
|
|
540
|
-
"eslint.useFlatConfig": true,
|
|
541
|
-
"eslint.validate": ["javascript", "typescript"],
|
|
542
|
-
"nodejs-testing.include": ["./test"],
|
|
543
|
-
"nodejs-testing.extensions": [
|
|
544
|
-
{
|
|
545
|
-
extensions: ["ts"],
|
|
546
|
-
parameters: ["--import", "@sfmc-bds/sdk/testing/minecraft-loader", "--import", "tsx/esm"],
|
|
547
|
-
},
|
|
548
|
-
],
|
|
549
|
-
});
|
|
550
|
-
writeJson(path.join(target, ".vscode", "launch.json"), {
|
|
551
|
-
version: "0.2.0",
|
|
552
|
-
configurations: [
|
|
553
|
-
{
|
|
554
|
-
type: "node",
|
|
555
|
-
request: "launch",
|
|
556
|
-
name: "Debug Module Tests",
|
|
557
|
-
runtimeArgs: [
|
|
558
|
-
"--test",
|
|
559
|
-
"--import",
|
|
560
|
-
"@sfmc-bds/sdk/testing/minecraft-loader",
|
|
561
|
-
"--import",
|
|
562
|
-
"tsx/esm",
|
|
563
|
-
],
|
|
564
|
-
args: ["${workspaceFolder}/test"],
|
|
565
|
-
cwd: "${workspaceFolder}",
|
|
566
|
-
console: "integratedTerminal",
|
|
567
|
-
sourceMaps: true,
|
|
568
|
-
},
|
|
569
|
-
],
|
|
570
|
-
});
|
|
571
|
-
writeJson(path.join(target, ".vscode", "tasks.json"), {
|
|
572
|
-
version: "2.0.0",
|
|
573
|
-
tasks: [
|
|
574
|
-
{
|
|
575
|
-
type: "npm",
|
|
576
|
-
script: "typecheck",
|
|
577
|
-
group: "build",
|
|
578
|
-
problemMatcher: ["$tsc"],
|
|
579
|
-
label: "npm: typecheck",
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
type: "npm",
|
|
583
|
-
script: "test",
|
|
584
|
-
group: { kind: "test", isDefault: true },
|
|
585
|
-
problemMatcher: [],
|
|
586
|
-
label: "npm: test",
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
type: "npm",
|
|
590
|
-
script: "lint",
|
|
591
|
-
problemMatcher: ["$eslint-stylish"],
|
|
592
|
-
label: "npm: lint",
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function main() {
|
|
599
|
-
const { flags, positional } = parseArgs(process.argv.slice(2));
|
|
600
|
-
|
|
601
|
-
/* 模板清单查询:走 stdout 后立刻退出 —— 主流程不要走到此分支 */
|
|
602
|
-
if (flags.listTemplates) {
|
|
603
|
-
emitTemplateList();
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const folderId = positional[0];
|
|
608
|
-
if (!folderId) {
|
|
609
|
-
die(
|
|
610
|
-
"用法: new-module.mjs <id> [--name <名>] [--template minimal|db] [--official]\n" +
|
|
611
|
-
" 在空目录(单包根)运行;与 Tanya7z/sfmc-module-template 同构。"
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
if (!isValidFolderId(folderId)) {
|
|
615
|
-
die(`id 须为小写 kebab-case,例如 my-mod(收到: ${folderId})`);
|
|
616
|
-
}
|
|
617
|
-
if (folderId.startsWith("feature-") || folderId.startsWith("core-")) {
|
|
618
|
-
die(`id 须为短名(不含 feature-/core- 前缀),例如 area 而非 feature-area`);
|
|
619
|
-
}
|
|
620
|
-
const target = resolveTargetDir(flags);
|
|
621
|
-
if (fs.existsSync(path.join(target, "sapi")) || fs.existsSync(path.join(target, "package.json"))) {
|
|
622
|
-
die(`目标已含模块骨架: ${target}(请用空目录)`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const displayName = flags.name?.trim() || folderId;
|
|
626
|
-
const template = flags.template === "db" ? "db" : "minimal";
|
|
627
|
-
const official = Boolean(flags.official);
|
|
628
|
-
const pkgName = official ? `@sfmc-bds/module-${folderId}` : `@CHANGE_ME/sfmc-module-${folderId}`;
|
|
629
|
-
|
|
630
|
-
const sapiDir = path.join(target, "sapi");
|
|
631
|
-
const schemaRel =
|
|
632
|
-
"https://cdn.jsdelivr.net/gh/DogeLakeDev/ScriptsForMinecraftServer@latest/modules/sdk/%40sfmc-sdk/schemas/sapi-manifest.v2.schema.json";
|
|
633
|
-
|
|
634
|
-
writeJson(path.join(target, "package.json"), buildPackageJson(folderId, { official }));
|
|
635
|
-
writeJson(path.join(sapiDir, "manifest.json"), buildManifest(folderId, displayName, template, schemaRel));
|
|
636
|
-
writeJson(path.join(sapiDir, "tsconfig.json"), buildTsConfigStandalone());
|
|
637
|
-
writeJson(path.join(target, "tsconfig.json"), buildRootTsConfig());
|
|
638
|
-
writeText(path.join(sapiDir, "src", "index.ts"), buildIndexTs(folderId, displayName, pkgName));
|
|
639
|
-
writeText(path.join(target, "test", `${folderId}.test.ts`), buildExampleTest(folderId, displayName));
|
|
640
|
-
writeText(path.join(target, "eslint.config.js"), buildEslintConfigJs());
|
|
641
|
-
writeText(path.join(target, ".gitignore"), buildGitignore());
|
|
642
|
-
writeJson(path.join(target, ".prettierrc.json"), buildPrettierRc());
|
|
643
|
-
writeText(path.join(target, ".prettierignore"), buildPrettierIgnore());
|
|
644
|
-
writeText(path.join(target, "LICENSE"), buildLicense());
|
|
645
|
-
writeText(path.join(target, "README.md"), buildReadme(folderId, displayName, pkgName));
|
|
646
|
-
writeVscodeWorkspace(target);
|
|
647
|
-
|
|
648
|
-
console.log(`[new-module] 已创建 ${target}`);
|
|
649
|
-
console.log(`[new-module] 模式: 单包根`);
|
|
650
|
-
console.log(`[new-module] npm: ${pkgName}`);
|
|
651
|
-
console.log(`[new-module] manifest id: feature-${folderId}`);
|
|
652
|
-
console.log(`[new-module] 下一步:`);
|
|
653
|
-
console.log(`[new-module] npm install && npm run typecheck && npm test && npm run lint`);
|
|
654
|
-
console.log(
|
|
655
|
-
`[new-module] sfmc mod install ${folderId} --from dir:${target} --link (在 SFMC 工作目录执行)`
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
main();
|