Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/build.mjs
1779 views
1
/* global console */
2
import fs from "fs";
3
import path from "path";
4
import esbuild from "esbuild";
5
import { Command } from "commander";
6
import { ZipFile } from "yazl";
7
import { glob } from "glob";
8
import { argv, env, exit } from "process";
9
import { copyDir, ensureDir, removeDirIfExists } from "./tools/utils.mjs";
10
import { infoPlugin, transformerPlugin } from "./tools/plugins.mjs";
11
import buildManifest from "./tools/process_manifest.mjs";
12
import buildLang from "./tools/po2lang.mjs";
13
import buildConfig from "./tools/process_config.mjs";
14
15
const tsconfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf8"));
16
17
const program = new Command();
18
program
19
.option("-w, --watch <type>", "Whether to continually build and where to sync the project while editing it.")
20
.action((type) => {
21
if (["stable", "preview", "server"].includes(type)) {
22
console.error("Invalid fs.watch type specified. Valid options are: []");
23
exit(1);
24
}
25
})
26
.option("--server <path>", "The path to the server to build for.")
27
.option("--target <type>", "Whether to build the addon in debug or release mode.")
28
.action((type) => {
29
if (["debug", "release", "server"].includes(type)) {
30
console.error("Invalid fs.watch type specified. Valid options are: []");
31
exit(1);
32
}
33
})
34
.option("-p, --package-only", "Only package what's already there.")
35
.option("-g --gametest", "Whether to build with gametest enabled.")
36
.option("-e, --editor", "Whether to build for editor mode.");
37
program.parse(argv);
38
39
const args = program.opts();
40
const srcDir = path.resolve("src");
41
const scriptOutputDir = path.resolve("BP/scripts");
42
const buildsDir = path.resolve("builds");
43
const packName = "WorldEdit";
44
const modulePaths = tsconfig.compilerOptions.paths || {};
45
46
function syncChange(eventType, srcRoot, destRoot, filename) {
47
if (!filename) return;
48
const srcPath = path.join(srcRoot, filename);
49
const destPath = path.join(destRoot, filename);
50
51
if (fs.existsSync(srcPath)) {
52
const stat = fs.statSync(srcPath);
53
if (stat.isDirectory()) {
54
copyDir(srcPath, destPath);
55
} else {
56
ensureDir(path.join(destPath, ".."));
57
fs.copyFileSync(srcPath, destPath);
58
}
59
} else {
60
if (fs.existsSync(destPath)) {
61
const stat = fs.statSync(destPath);
62
if (stat.isDirectory()) {
63
removeFilesRecursively(destPath);
64
fs.rmdirSync(destPath);
65
} else {
66
fs.unlinkSync(destPath);
67
}
68
}
69
}
70
}
71
72
function watchAndSync(srcRoot, destRoot) {
73
copyDir(srcRoot, destRoot);
74
fs.watch(srcRoot, { recursive: true }, (eventType, filename) => {
75
if (filename) syncChange(eventType, srcRoot, destRoot, filename);
76
});
77
}
78
79
function zipWriteDir(zip, dirname, arcname) {
80
function addDir(dir, base) {
81
for (const entry of fs.readdirSync(dir)) {
82
const fullPath = path.join(dir, entry);
83
const relPath = path.join(base, entry);
84
if (fs.statSync(fullPath).isDirectory()) {
85
addDir(fullPath, relPath);
86
} else {
87
zip.addFile(fullPath, path.join(arcname, path.relative(dirname, fullPath)));
88
}
89
}
90
}
91
addDir(dirname, "");
92
}
93
94
if (!fs.existsSync(srcDir)) throw "The src folder does not exist in the current working directory!";
95
if (!fs.existsSync(scriptOutputDir)) throw "The output scripts folder does not exist in the current working directory!";
96
97
// Calculate sync location when in fs.watch mode.
98
if (args.watch === "stable") {
99
args.syncDir = env.LOCALAPPDATA + "\\Packages\\Microsoft.MinecraftUWP_8wekyb3d8bbwe\\LocalState\\games\\com.mojang";
100
} else if (args.watch === "preview") {
101
args.syncDir = env.APPDATA + "\\Minecraft Bedrock Preview\\Users\\Shared\\games\\com.mojang";
102
} else if (args.watch === "server") {
103
if (!args.server) {
104
console.error("You must specify a server path when using the --server option.");
105
exit(1);
106
}
107
args.syncDir = args.server;
108
}
109
// Enable gametest when in watch mode.
110
if (args.watch) args.gametest = true;
111
112
// Clear the script output folder.
113
const removeFilesRecursively = (dir) => {
114
for (const file of fs.readdirSync(dir)) {
115
const filePath = path.join(dir, file);
116
const stat = fs.statSync(filePath);
117
if (stat.isDirectory()) {
118
removeFilesRecursively(filePath);
119
fs.rmdirSync(filePath);
120
} else if (!file.endsWith(".txt")) {
121
fs.unlinkSync(filePath);
122
}
123
}
124
};
125
removeFilesRecursively(scriptOutputDir);
126
127
// Build manifest files
128
buildManifest(args);
129
130
// Build config files
131
buildConfig(args);
132
133
// Build lang files
134
buildLang(args);
135
136
const buildArgs = {
137
outdir: scriptOutputDir,
138
entryPoints: await glob("src/**/*.{ts,js}", { ignore: ["src/**/*.d.ts", args.gametest ? "" : "src/gametest/**", args.editor ? "" : "src/editor/**"] }),
139
bundle: false,
140
platform: "node",
141
target: ["es2020"],
142
tsconfig: "tsconfig.json",
143
format: "esm",
144
plugins: [
145
infoPlugin(),
146
transformerPlugin(/\.(js|ts)$/, [
147
function resolveIndexImports(filePath, contents) {
148
// Transform folder imports to explicit index file imports
149
const importRegex = /(?:from\s+|import\s+)(['"`])(\.[^'"`]+)\1/g;
150
151
contents = contents.replace(importRegex, (match, quote, importPath) => {
152
// Get the directory of the current file
153
const currentDir = path.dirname(filePath);
154
// Resolve the import path relative to the current file
155
const resolvedPath = path.resolve(currentDir, importPath);
156
157
// Check if it's a directory with an index file
158
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
159
const indexTs = path.join(resolvedPath, "index.ts");
160
const indexJs = path.join(resolvedPath, "index.js");
161
if (fs.existsSync(indexTs) || fs.existsSync(indexJs)) {
162
// Add /index to the import path
163
return match.replace(importPath, `${importPath}/index`);
164
}
165
}
166
return match;
167
});
168
return contents;
169
},
170
function remapImports(filePath, contents) {
171
for (const [alias, paths] of Object.entries(modulePaths)) {
172
// Remove trailing /* from alias and path if present
173
const aliasPattern = alias.replace(/\/\*$/, "");
174
const target = paths[0].replace(/\/\*$/, "");
175
// Replace both import and require statements
176
const importRegex = new RegExp(`(['"\`])${aliasPattern}(\\/[^'"\`]*)?\\1`, "g");
177
contents = contents.replace(importRegex, (match, quote, subPath = "") => {
178
return `${quote}${target}${subPath}${quote}`;
179
});
180
}
181
return contents;
182
},
183
function gametest(filePath, contents) {
184
if (!path.normalize(filePath).endsWith(path.normalize("src/index.ts")) || !args.gametest) return;
185
contents += `\nimport "gametest/index.js";`;
186
return contents;
187
},
188
function editor(filePath, contents) {
189
if (!path.normalize(filePath).endsWith(path.normalize("src/index.ts")) || !args.editor) return;
190
contents += `\nimport "editor/index.js";`;
191
return contents;
192
},
193
]),
194
],
195
};
196
197
if (args.watch) {
198
// Sync the BP and RP directories with the development pack folders
199
const bpDest = path.join(args.syncDir, `development_behavior_packs/${packName}BP`);
200
const rpDest = path.join(args.syncDir, `development_resource_packs/${packName}RP`);
201
202
watchAndSync("BP", bpDest);
203
watchAndSync("RP", rpDest);
204
205
// Build the scripts and fs.watch for changes
206
const ctx = await esbuild.context({
207
...buildArgs,
208
sourcemap: true,
209
minify: false,
210
plugins: [
211
...buildArgs.plugins,
212
{
213
name: "sync-on-rebuild",
214
setup(build) {
215
build.onEnd(() => {
216
// Re-sync the entire BP folder after each build to ensure consistency
217
try {
218
copyDir("BP", bpDest);
219
} catch (err) {
220
console.error("Failed to sync BP folder:", err);
221
}
222
});
223
},
224
},
225
],
226
});
227
await ctx.watch();
228
} else {
229
// Build the scripts and bundle them into the script output folder.
230
const ctx = await esbuild.context({ ...buildArgs, sourcemap: false, minify: true });
231
await ctx.rebuild();
232
await ctx.dispose();
233
234
ensureDir(buildsDir);
235
removeDirIfExists(path.join(buildsDir, `${packName}BP`));
236
removeDirIfExists(path.join(buildsDir, `${packName}RP`));
237
238
copyDir("BP", path.join(buildsDir, `${packName}BP`));
239
copyDir("RP", path.join(buildsDir, `${packName}RP`));
240
241
let exportName = args.target === "debug" ? `${packName}.beta` : packName;
242
exportName = args.editor ? `${exportName}.editor` : exportName;
243
exportName = args.target === "server" ? `${exportName}.server.zip` : `${exportName}.mcaddon`;
244
const zipPath = path.join(buildsDir, exportName);
245
const zip = new ZipFile();
246
if (args.target === "server") {
247
const variablesPath = path.join(buildsDir, "variables.json");
248
if (fs.existsSync(variablesPath)) zip.addFile(variablesPath, "variables.json");
249
}
250
zipWriteDir(zip, path.join(buildsDir, `${packName}BP`), `${packName}BP`);
251
zipWriteDir(zip, path.join(buildsDir, `${packName}RP`), `${packName}RP`);
252
zip.outputStream.pipe(fs.createWriteStream(zipPath)).on("close", () => {});
253
zip.end();
254
}
255
256