@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
263 lines (262 loc) • 20.2 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonSchemaItemInfoGeneratorTest = void 0;
const ProjectInfoItem_1 = __importDefault(require("./ProjectInfoItem"));
const IInfoItemData_1 = require("./IInfoItemData");
const Database_1 = __importDefault(require("../minecraft/Database"));
const CreatorToolsHost_1 = __importDefault(require("../app/CreatorToolsHost"));
const axios_1 = __importDefault(require("axios"));
const Utilities_1 = __importDefault(require("../core/Utilities"));
const MinecraftDefinitions_1 = __importDefault(require("../minecraft/MinecraftDefinitions"));
const ProjectItemUtilities_1 = __importDefault(require("../app/ProjectItemUtilities"));
const json_schema_1 = require("json-schema");
const JsonSchemaErrorBase = 100;
const NotCurrentFormatVersionBase = 1100;
var JsonSchemaItemInfoGeneratorTest;
(function (JsonSchemaItemInfoGeneratorTest) {
JsonSchemaItemInfoGeneratorTest[JsonSchemaItemInfoGeneratorTest["couldNotParseJson"] = 1] = "couldNotParseJson";
})(JsonSchemaItemInfoGeneratorTest || (exports.JsonSchemaItemInfoGeneratorTest = JsonSchemaItemInfoGeneratorTest = {}));
/**
* Validates JSON files against official JSON schema definitions at public/schemas.
*
* @see {@link ../../public/data/forms/mctoolsval/json.form.json} for topic definitions
*/
class JsonSchemaItemInfoGenerator {
id = "JSON";
title = "JSON Schema Validation";
canAlwaysProcess = true;
constructor() {
this.loadSchema = this.loadSchema.bind(this);
}
summarize(info, infoSet) { }
async loadSchema(uri) {
const res = await axios_1.default.get(Utilities_1.default.ensureEndsWithSlash(CreatorToolsHost_1.default.contentWebRoot) + uri);
return res.data;
}
async generate(projectItem, contentIndex) {
const items = [];
if (projectItem.primaryFile &&
projectItem.primaryFile.content &&
typeof projectItem.primaryFile.content === "string") {
const schemaPath = projectItem.getOfficialSchemaPath();
if (schemaPath) {
let verIsCurrent = await MinecraftDefinitions_1.default.formatVersionIsCurrent(projectItem);
if (verIsCurrent) {
let schemaContents = (await Database_1.default.getOfficialSchema(schemaPath));
if (schemaContents) {
let content = projectItem.primaryFile.content;
let contentObj = undefined;
content = Utilities_1.default.fixJsonContent(content);
try {
contentObj = JSON.parse(content);
const results = (0, json_schema_1.validate)(contentObj, schemaContents);
for (const err of results.errors) {
let errorTitle = `JSON structure error`;
let errorDetail = `(${err.property}) ${err.message}`;
// Sanitize [object Object] from error messages - the jsonschema library
// bakes toString() of object values into the message. Resolve the actual value
// from contentObj using the property path and show a truncated JSON representation.
const errMessage = err.message
? err.message.replace(/\[object Object\]/g, () => {
try {
// Resolve the value from the parsed content using the error's property path.
// Paths look like "minecraft:spawn_rules.conditions[0]"
let resolved = contentObj;
if (err.property) {
const parts = err.property.replace(/\[(\d+)\]/g, ".$1").split(".");
for (const part of parts) {
if (resolved == null)
break;
resolved = resolved[part];
}
}
if (resolved !== undefined && resolved !== null && typeof resolved === "object") {
const json = JSON.stringify(resolved);
return json.length > 30 ? json.substring(0, 30) + "..." : json;
}
return "(object)";
}
catch {
return "(object)";
}
})
: "";
// Minecraft format_version fields may legitimately be either a three-number
// array (e.g., [1,0,0]) OR a string (e.g., "1.0.2") - both forms are valid
// across current Bedrock content. The schema validator can flag one form
// as a type mismatch against the other, producing a false-positive warning
// that we suppress below. Only the array-vs-string mismatch is suppressed;
// genuinely unexpected types (number, boolean, object, ...) still produce a
// clear "stringified or array form expected" warning so the creator knows.
const propForCheck = err.property ? err.property.replace("instance.", "") : "";
const isFormatVersionProperty = propForCheck === "format_version" || propForCheck.endsWith(".format_version");
const isTypeMismatch = !!errMessage && errMessage.includes("is not of a type");
if (isFormatVersionProperty && isTypeMismatch) {
// Resolve the actual value of format_version so we can decide whether the
// type-mismatch is a benign array<->string mix or a genuinely wrong type.
// Use the stripped property path (without "instance." prefix) so the
// walk actually finds the value in `contentObj` — otherwise resolution
// always fails and every format_version produces a false-positive
// "Version format needs updating" warning.
let formatVersionValue = undefined;
try {
let resolved = contentObj;
if (propForCheck) {
const parts = propForCheck.replace(/\[(\d+)\]/g, ".$1").split(".");
for (const part of parts) {
if (resolved == null)
break;
resolved = resolved[part];
}
}
formatVersionValue = resolved;
}
catch {
// ignore - fall through to schema error reporting
}
const isStringForm = typeof formatVersionValue === "string";
const isArrayForm = Array.isArray(formatVersionValue) &&
formatVersionValue.every((v) => typeof v === "number");
if (isStringForm || isArrayForm) {
// Both array and string forms are acceptable for format_version - skip.
continue;
}
// Truly unexpected type (number, boolean, object, null, ...). Surface a
// targeted warning instead of the generic "is not of a type" message.
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, JsonSchemaErrorBase + projectItem.itemType, `Version format needs updating`, projectItem, `The "format_version" value should be a version string (e.g. "1.21.0") or a three-number array (e.g. [1, 21, 0]).`));
continue;
}
if (err.property &&
err.property.includes("version") &&
errMessage &&
errMessage.includes("object value found") &&
errMessage.includes("string is required")) {
// Bedrock manifest "version" fields (header.version, header.min_engine_version,
// modules[].version, dependencies[].version) accept BOTH a three-number array
// (e.g. [1, 4, 12]) and a three-number triplet string (e.g. "1.4.12") — but the
// accepted form depends on the manifest's top-level `format_version`:
//
// format_version 1 / 2 → array form is the canonical/required form.
// String form is invalid; fail loudly.
// format_version 3+ → string form "1.4.12" is the modern/recommended form.
// Array form still works but the schema flags it; we
// emit a recommendation (not a misleading "wrong type").
//
// Resolve the value AND the manifest's top-level format_version, then decide.
let versionValue = undefined;
try {
let resolved = contentObj;
if (err.property) {
const parts = err.property.replace("instance.", "").replace(/\[(\d+)\]/g, ".$1").split(".");
for (const part of parts) {
if (resolved == null || part === "")
break;
resolved = resolved[part];
}
}
versionValue = resolved;
}
catch {
// ignore - fall through to generic structure issue handling
}
const isStringForm = typeof versionValue === "string";
const isThreeNumberArray = Array.isArray(versionValue) &&
versionValue.length === 3 &&
versionValue.every((v) => typeof v === "number");
// Top-level manifest format_version (NOT to be confused with header.version).
// When non-numeric or absent, treat as legacy (1/2) to keep the conservative path.
const manifestFormatVersion = typeof contentObj?.format_version === "number" ? contentObj.format_version : undefined;
if (manifestFormatVersion !== undefined && manifestFormatVersion >= 3) {
// Modern manifest: string form is recommended.
if (isStringForm) {
// Already in modern form — fully correct, suppress.
continue;
}
if (isThreeNumberArray) {
// Valid legacy form on a modern manifest — emit a RECOMMENDATION (not error)
// to upgrade. This is safe to follow and matches the manifest spec.
const propPath = err.property.replace("instance.", "");
const recommended = `"${versionValue[0]}.${versionValue[1]}.${versionValue[2]}"`;
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.recommendation, this.id, JsonSchemaErrorBase + projectItem.itemType, `Version can be upgraded to string form`, projectItem, `In manifest format_version ${manifestFormatVersion}, "${propPath}" can be expressed as the string ${recommended} instead of the array [${versionValue.join(", ")}]. Both work, but the string form is the modern convention.`));
continue;
}
}
else {
// Legacy manifest (format_version 1/2): array form is required.
if (isThreeNumberArray) {
// Correct — suppress the false-positive schema warning.
continue;
}
if (isStringForm) {
// Genuinely wrong: legacy manifest with a string version. Tell the truth.
const propPath = err.property.replace("instance.", "");
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, JsonSchemaErrorBase + projectItem.itemType, `Version format incompatible with manifest format_version`, projectItem, `Manifest format_version ${manifestFormatVersion ?? "(unset)"} requires "${propPath}" to be an array like [1, 0, 0], not the string "${versionValue}". Either change the value to the array form OR upgrade the top-level "format_version" to 3 to use the string form.`));
continue;
}
}
// Truly unexpected type (object, boolean, null, etc.). Surface a non-misleading
// warning that does NOT push the user toward the wrong form.
errorTitle = `Version format unrecognized`;
errorDetail = `The "${err.property.replace("instance.", "")}" value is not in a recognized format. Use a three-number array like [1, 0, 0] (manifest format_version 1/2) or a three-number string like "1.0.0" (manifest format_version 3+).`;
}
else if (errMessage && errMessage.includes("is not one of enum values")) {
// Make enum errors friendlier
const propName = err.property ? err.property.replace("instance.", "") : "a field";
errorTitle = `Invalid value`;
errorDetail = `The value for "${propName}" isn't recognized. Check for typos or see the documentation for valid options.`;
}
else if (errMessage && errMessage.includes("requires property")) {
// Make required property errors friendlier
const match = errMessage.match(/requires property "([^"]+)"/);
const missingProp = match ? match[1] : "a required field";
errorTitle = `Missing required field`;
errorDetail = `This item is missing the "${missingProp}" field, which is needed for it to work properly.`;
}
else if (errMessage && errMessage.includes("is not of a type")) {
// Make type mismatch errors friendlier
const propName = err.property ? err.property.replace("instance.", "") : "a field";
errorTitle = `Wrong value type`;
errorDetail = `The value for "${propName}" is the wrong type. ${errMessage.includes("string") ? "It should be text." : errMessage.includes("number") ? "It should be a number." : errMessage.includes("boolean") ? "It should be true or false." : "Check the expected format."}`;
}
else {
// General case - still improve the property path display
const propName = err.property ? err.property.replace("instance.", "") : "";
errorTitle = `Structure issue`;
errorDetail = propName ? `In "${propName}": ${errMessage}` : errMessage || "Unexpected structure";
}
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.warning, this.id, JsonSchemaErrorBase + projectItem.itemType, errorTitle, projectItem, errorDetail));
}
}
catch (e) {
let errorMess = e;
if (e.message) {
errorMess = e.message;
}
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.error, this.id, JsonSchemaItemInfoGeneratorTest.couldNotParseJson, "This file has a syntax error and can't be read as JSON. Check for missing commas, brackets, or quotes. Details: " +
errorMess, projectItem));
}
if (contentObj) {
}
}
}
else {
let fvStr = "";
const fv = await MinecraftDefinitions_1.default.getFormatVersion(projectItem);
if (fv) {
fvStr = " (is at " + fv.join(".") + ")";
}
items.push(new ProjectInfoItem_1.default(IInfoItemData_1.InfoItemType.info, this.id, NotCurrentFormatVersionBase + projectItem.itemType, ProjectItemUtilities_1.default.getDescriptionForType(projectItem.itemType) +
" is not at a current format version" +
fvStr, projectItem));
}
}
}
return items;
}
}
exports.default = JsonSchemaItemInfoGenerator;