@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
565 lines (456 loc) • 18.4 kB
text/typescript
import fs from "fs";
import path from "path";
export class BuildSnippetsParameters {
scriptPaths: string[];
targetFolderPath: string;
}
const ImportTypes = {
vanilla: [
"MinecraftDimensionTypes",
"MinecraftBlockTypes",
"MinecraftItemTypes",
"MinecraftEntityTypes",
"MinecraftEffectTypes",
],
mathutils: ["Vector3Utils"],
mcui: [
"MessageFormResponse",
"MessageFormData",
"ActionFormData",
"ActionFormResponse",
"ModalFormData",
"ModalFormResponse",
],
mcgt: ["Test", "register"],
mcsa: ["secrets", "variables"],
mcnet: ["http", "HttpRequest", "HttpResponse", "HttpRequestMethod", "HttpHeader"],
mc: [
"world",
"system",
"BlockPermutation",
"BlockSignComponent",
"SignSide",
"DyeColor",
"EntityQueryOptions",
"ButtonPushAfterEvent",
"ItemStack",
"BlockPistonState",
"MolangVariableMap",
"LeverActionAfterEvent",
"EntityInventoryComponent",
"BlockInventoryComponent",
"Enchantment",
"ItemEnchantsComponent",
"EntityHealthComponent",
"EntityOnFireComponent",
"EntityItemComponent",
"EntityEquippableComponent",
"EquipmentSlot",
"EntityItemComponent",
"EntitySpawnAfterEvent",
"PistonActivateBeforeEvent",
"PistonActivateAfterEvent",
"MusicOptions",
"WorldSoundOptions",
"PlayerSoundOptions",
"DisplaySlotId",
"ObjectiveSortOrder",
"TripWireTripAfterEvent",
"Vector3",
"EntityComponentTypes",
"BlockComponentTypes",
"ItemComponentTypes",
"DimensionLocation",
],
};
class CodeSnippet {
name: string;
description?: string;
segments: string[];
function: string;
codeSampleFull: string;
codeSampleInterior: string;
}
class SnippetsBuilder {
_targetFolderPath: string;
_snippets: CodeSnippet[] = [];
_snippetsByModules: { [moduleName: string]: { [snippetName: string]: CodeSnippet[] } } = {};
_scriptLibraryMainCode = "";
_libraryCode: object = {};
constructor(targetFolderPath: string) {
if (!targetFolderPath.endsWith("/") && !targetFolderPath.endsWith("\\")) {
targetFolderPath += "/";
}
this._targetFolderPath = path.join(path.resolve(targetFolderPath));
}
processFolder(folderPath: string, depth?: number) {
if (!fs.existsSync(folderPath)) {
return;
}
console.log("Processing folder '" + folderPath + "'");
if (!depth) {
depth = 1;
}
const results = fs.readdirSync(folderPath);
results.forEach((fileName: string) => {
const filePath = path.join(folderPath, fileName);
const stat = fs.statSync(filePath);
if (stat.isDirectory() && depth < 4) {
this.processFolder(filePath, depth + 1);
} else if (stat.isFile() && fileName.endsWith(".ts")) {
const content = fs.readFileSync(filePath, { encoding: "utf8" });
this.resolve(filePath, content);
}
});
}
addImports(code: string) {
code = "\r\n" + code;
for (const shortHand in ImportTypes) {
const arr = ImportTypes[shortHand];
let importStr = "";
if (arr) {
for (const str of arr) {
if (code.indexOf(shortHand + "." + str) >= 0) {
if (importStr.length > 0) {
importStr += ", ";
}
importStr += str;
} else if (
code.indexOf(str + ".") >= 0 ||
code.indexOf(str + "(") >= 0 ||
code.indexOf(str + ")") >= 0 ||
code.indexOf(str + ";") >= 0 ||
code.indexOf(str + "\r") >= 0 ||
code.indexOf(str + "\n") >= 0 ||
code.indexOf(str + " ") >= 0
) {
if (importStr.length > 0) {
importStr += ", ";
}
importStr += str;
}
}
}
if (importStr.length > 0) {
switch (shortHand) {
case "mc":
code = "import { " + importStr + ' } from "@minecraft/server";\r\n' + code;
break;
case "mcui":
code = "import { " + importStr + ' } from "@minecraft/server-ui";\r\n' + code;
break;
case "vanilla":
code = "import { " + importStr + ' } from "@minecraft/vanilla-data";\r\n' + code;
break;
case "mathutils":
code = "import { " + importStr + ' } from "@minecraft/math";\r\n' + code;
break;
case "mcgt":
code = "import { " + importStr + ' } from "@minecraft/server-gametest";\r\n' + code;
break;
case "mcnet":
code = "import { " + importStr + ' } from "@minecraft/server-net";\r\n' + code;
break;
case "mcsa":
code = "import { " + importStr + ' } from "@minecraft/server-admin";\r\n' + code;
break;
}
}
}
return code;
}
removeNamespaceAliases(code: string) {
code = code.replace(/mcui\./gi, "");
code = code.replace(/mcnet\./gi, "");
code = code.replace(/mcadmin\./gi, "");
code = code.replace(/mcgt\./gi, "");
code = code.replace(/mcgt\r\n/gi, "");
code = code.replace(/mcgt\n/gi, "");
code = code.replace(/ .register/gi, "register");
code = code.replace(/mc\./gi, "");
code = code.replace(/vanilla\./gi, "");
code = code.replace(/mathutils\./gi, "");
return code;
}
resolve(filePath: string, content: string) {
let filePathLower = filePath.toLowerCase();
if (filePathLower.indexOf("samplemanager.") >= 0) {
this._scriptLibraryMainCode = this.stripLinesContaining(content, "import * as mc from ");
this._scriptLibraryMainCode = this.stripLinesContaining(this._scriptLibraryMainCode, "console.");
} else if (filePathLower.endsWith("samplelibrary.ts")) {
let lastSlash = Math.max(filePathLower.lastIndexOf("/"), filePathLower.lastIndexOf("\\"));
if (lastSlash >= 0) {
let libraryContent = content.replace(/sdf\d{1,4}\./g, "");
libraryContent = libraryContent.replace(/sm\./g, "");
libraryContent = this.stripLinesContaining(libraryContent, "import * as sdf");
libraryContent = this.stripLinesContaining(libraryContent, "import * as mc ");
libraryContent = this.stripLinesContaining(libraryContent, "import SampleManager from ");
let moduleName = filePathLower.substring(lastSlash + 1, filePathLower.length - 16);
this._libraryCode["@minecraft/" + moduleName] = libraryContent;
}
}
let seeLinkStart = content.indexOf("@see ");
while (seeLinkStart >= 0) {
let endOfSeeLinkLine = content.indexOf("\n", seeLinkStart);
if (endOfSeeLinkLine > seeLinkStart + 6) {
let seeLinkContents = content
.substring(seeLinkStart + 5, endOfSeeLinkLine)
.trim()
.toLowerCase();
if (seeLinkContents.startsWith("https://learn.microsoft.com/minecraft/creator/scriptapi/")) {
let description: string | undefined;
let thisCommentAreaStart = content.lastIndexOf("/**", seeLinkStart);
if (thisCommentAreaStart >= 0) {
let firstAsterisk = content.indexOf("* ", thisCommentAreaStart + 5);
if (firstAsterisk > thisCommentAreaStart) {
let endOfFirstCommentLine = content.indexOf("\n", firstAsterisk);
if (endOfFirstCommentLine > firstAsterisk) {
description = content.substring(firstAsterisk + 2, endOfFirstCommentLine).trim();
}
}
}
let urlLink = seeLinkContents.substring(56, seeLinkContents.length).replace("#", "/");
let urlSegments = urlLink.split("/");
if (urlSegments.length >= 2 && urlSegments[0].startsWith("minecraft")) {
const nextExport = content.indexOf("export ", endOfSeeLinkLine);
if (nextExport >= 0) {
const nextFunction = content.indexOf(" function", nextExport);
if (nextFunction >= 0) {
const firstParen = content.indexOf("(", nextFunction);
let endOfFunction = content.indexOf("\r\n}", nextFunction);
const gtStart = content.indexOf(".register", nextFunction);
if (gtStart >= 0) {
const gtEnd = content.indexOf(");", gtStart);
if (gtEnd > firstParen && firstParen > nextFunction) {
const name = content.substring(nextFunction + 10, firstParen);
const functionCode = content.substring(nextExport, endOfFunction + 3);
let codeSampleFull = content.substring(nextFunction + 1, gtEnd + 3);
codeSampleFull = this.addImports(codeSampleFull);
codeSampleFull = this.removeNamespaceAliases(codeSampleFull);
let cs = new CodeSnippet();
cs.name = name;
cs.description = description;
cs.segments = urlSegments;
cs.function = functionCode;
cs.codeSampleFull = codeSampleFull;
cs.codeSampleInterior = codeSampleFull;
this._snippets.push(cs);
let moduleKey = urlSegments[0] + "/" + urlSegments[1];
if (!this._snippetsByModules[moduleKey]) {
this._snippetsByModules[moduleKey] = {};
}
if (!this._snippetsByModules[moduleKey][name]) {
this._snippetsByModules[moduleKey][name] = [];
}
this._snippetsByModules[moduleKey][name].push(cs);
console.log(
"Gametest snippet '" + name + "' discovered for " + urlSegments.join(".") + " in " + moduleKey
);
endOfFunction = -1;
}
}
if (endOfFunction > firstParen && firstParen > nextFunction) {
const name = content.substring(nextFunction + 10, firstParen);
const functionCode = content.substring(nextExport, endOfFunction + 3);
const firstBrace = functionCode.indexOf("{");
if (firstBrace >= 0) {
let codeSampleInterior = functionCode.substring(firstBrace + 3, functionCode.length - 3);
let codeSampleFull = "";
if (codeSampleInterior.indexOf("log(") > 0) {
codeSampleFull = content.substring(nextFunction + 1, endOfFunction + 3);
} else {
codeSampleFull = this.stripLinesContaining(
content.substring(nextFunction + 1, endOfFunction + 3),
"log("
)
.replace("log: (message: string, status?: number) => void, ", "")
.replace("log: (message: string, status?: number) => void,\r\n", "");
}
codeSampleFull = this.addImports(codeSampleFull);
codeSampleFull = this.removeNamespaceAliases(codeSampleFull);
codeSampleInterior = this.removeNamespaceAliases(codeSampleInterior);
let cs = new CodeSnippet();
cs.name = name;
cs.description = description;
cs.segments = urlSegments;
cs.function = functionCode;
cs.codeSampleFull = codeSampleFull;
cs.codeSampleInterior = codeSampleInterior;
this._snippets.push(cs);
let moduleKey = urlSegments[0] + "/" + urlSegments[1];
if (!this._snippetsByModules[moduleKey]) {
this._snippetsByModules[moduleKey] = {};
}
if (!this._snippetsByModules[moduleKey][name]) {
this._snippetsByModules[moduleKey][name] = [];
}
this._snippetsByModules[moduleKey][name].push(cs);
console.log("Snippet '" + name + "' discovered for " + urlSegments.join(".") + " in " + moduleKey);
}
}
}
}
}
}
seeLinkStart = content.indexOf("@see ", seeLinkStart + 5);
}
}
}
writeSnippetFiles() {
for (let moduleKey in this._snippetsByModules) {
let snippetsByName = this._snippetsByModules[moduleKey];
const snippetNamesByPath: { [path: string]: string[] } = {};
for (let snippetKey in snippetsByName) {
const snippets = snippetsByName[snippetKey];
if (snippets.length <= 1) {
let coreSnippet = snippets[0];
let coreUrlSegments = Array.from(coreSnippet.segments);
if (coreUrlSegments[0] === coreUrlSegments[1]) {
coreUrlSegments.shift();
}
this.writeFile(
"docsnips/" + coreUrlSegments.join("/") + "/_examples/" + coreSnippet.name + ".ts",
coreSnippet.codeSampleFull
);
} else {
for (const snippet of snippets) {
let snipUrlSegments = Array.from(snippet.segments);
if (snipUrlSegments[0] === snipUrlSegments[1]) {
snipUrlSegments.shift();
}
const path = snipUrlSegments.join("/");
if (snippetNamesByPath[path] === undefined) {
snippetNamesByPath[path] = [];
}
if (!snippetNamesByPath[path].includes(snippet.name + ".ts")) {
snippetNamesByPath[path].push(snippet.name + ".ts");
}
if (snipUrlSegments.length >= 2) {
this.writeFile(
"docsnips/" +
snipUrlSegments[0] +
"/" +
snipUrlSegments[1] +
"/_shared_examples/" +
snippet.name +
".ts",
snippet.codeSampleFull
);
} else {
this.writeFile(
"docsnips/" + snipUrlSegments[0] + "/_shared_examples/" + snippet.name + ".ts",
snippet.codeSampleFull
);
}
}
}
}
for (const sharedSnippetPath in snippetNamesByPath) {
const snippetList = snippetNamesByPath[sharedSnippetPath];
this.writeFile(
"docsnips/" + sharedSnippetPath + "/_example_files.json",
JSON.stringify(snippetList, undefined, 2)
);
}
}
}
writeCatalogFiles() {
for (let moduleKey in this._snippetsByModules) {
let moduleType: string[] = [];
let samplesWritten: { [snippetName: string]: CodeSnippet } = {};
let snippetsByName = this._snippetsByModules[moduleKey];
for (let snippetKey in snippetsByName) {
const snippets = snippetsByName[snippetKey];
for (let i = 0; i < snippets.length; i++) {
let snippet = snippets[i];
if (!samplesWritten[snippet.name]) {
samplesWritten[snippet.name] = snippet;
moduleType.push(snippets[i].function);
}
}
}
let jsonMarkup = "{" + "\r\n";
for (let snippetKey in samplesWritten) {
let snippet = samplesWritten[snippetKey];
if (jsonMarkup.length > 10) {
jsonMarkup += ",\r\n";
}
let sample = '["' + this.getQuoteSafeContent(snippet.codeSampleInterior).replace(/\r\n/g, '",\r\n"') + '"\r\n]';
jsonMarkup +=
'"' +
snippet.name +
'": {\r\n "description": "' +
(snippet.description ? snippet.description + " " : "") +
"See https://learn.microsoft.com/minecraft/creator/scriptapi/" +
snippet.segments.join("/") +
'",\r\n "prefix": ["mc"],\r\n "body": ' +
sample +
"}";
}
jsonMarkup += "\r\n}";
this.writeFile("samplejson/" + moduleKey + "-beta.json", jsonMarkup);
let tsTestFileMarkup =
"/* eslint-disable @typescript-eslint/no-unused-vars */\r\n" +
'import * as mc from "@minecraft/server";\r\n\r\n' +
moduleType.join("\r\n\r\n");
tsTestFileMarkup += "\r\n" + this._scriptLibraryMainCode + "\r\n";
if (this._libraryCode[moduleKey]) {
tsTestFileMarkup += "\r\n" + this._libraryCode[moduleKey] + "\r\n";
}
this.writeFile("typescript/" + moduleKey + "/tests.ts", tsTestFileMarkup);
}
}
getQuoteSafeContent(content: string) {
var newContent = "";
for (const chr of content) {
if (chr === "\\") {
newContent += "/";
} else if (chr === '"') {
newContent += "'";
} else {
newContent += chr;
}
}
// eslint-disable-next-line no-control-regex
newContent = newContent.replace(/[\r\n\x0B\x0C\u0085\u2028\u2029]+/g, '",\n"');
return newContent;
}
stripLinesContaining(content: string, containing: string) {
let i = content.indexOf(containing);
while (i >= 0) {
let previousNewLine = content.lastIndexOf("\n", i);
let nextNewLine = content.indexOf("\n", i);
if (previousNewLine < 0) {
previousNewLine = 0;
}
if (previousNewLine > 0 && content[previousNewLine - 1] === "\r") {
previousNewLine--;
}
if (nextNewLine < 0) {
nextNewLine = content.length - 1;
}
content = content.substring(0, previousNewLine) + content.substring(nextNewLine + 1, content.length);
i = content.indexOf(containing, previousNewLine);
}
return content;
}
writeFile(relativePath: string, contents: string) {
const fullPath = path.join(this._targetFolderPath, relativePath);
const dirName = path.dirname(fullPath);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
fs.writeFileSync(fullPath, contents);
}
}
export function buildSnippets(params: BuildSnippetsParameters) {
return () => {
const snippetsBuilder = new SnippetsBuilder(params.targetFolderPath);
for (const scriptPath of params.scriptPaths) {
console.log("Processing " + scriptPath);
snippetsBuilder.processFolder(scriptPath);
}
snippetsBuilder.writeSnippetFiles();
snippetsBuilder.writeCatalogFiles();
};
}