print all build errors at the end

This commit is contained in:
codetypes committed 2025-10-17 16:34:02 +08:00
1 parent 05cce8f91f
commit c137b880b3
8 files changed
+427 -96

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.5", "version": "1.8.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.5", "version": "1.8.7",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.5", "version": "1.8.7",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "行为树编辑器", "description": "行为树编辑器",
"author": "zhandouxiaojiji & codetypes", "author": "zhandouxiaojiji & codetypes",
+1 -1
View File
@@ -549,7 +549,7 @@ const validateArg = (
} else if (value === null && !required) { } else if (value === null && !required) {
value = undefined; value = undefined;
} }
if (!checkNodeArgValue(node, arg, value, true)) { if (!checkNodeArgValue(node, arg, value, console.error)) {
return Promise.reject(new Error(i18n.t("node.invalidValue"))); return Promise.reject(new Error(i18n.t("node.invalidValue")));
} }
return Promise.resolve(value); return Promise.resolve(value);
+1 -1
View File
@@ -748,7 +748,7 @@ class TreeNode extends Rect {
!b3util.nodeDefs.has(data.name) || !b3util.nodeDefs.has(data.name) ||
(data.path && !node.children?.length) || (data.path && !node.children?.length) ||
(nodeDef.group && !nodeDef.group.some((g) => b3util.usingGroups?.[g])) || (nodeDef.group && !nodeDef.group.some((g) => b3util.usingGroups?.[g])) ||
!b3util.checkTreeData(data) || !b3util.isValidNodeData(data) ||
foundUndefined(data.input) || foundUndefined(data.input) ||
foundUndefined(data.output) || foundUndefined(data.output) ||
foundUndefinedInArgs(nodeDef, data) || foundUndefinedInArgs(nodeDef, data) ||
+2 -1
View File
@@ -1,6 +1,6 @@
import { NodeDef } from "../behavior3/src/behavior3"; import { NodeDef } from "../behavior3/src/behavior3";
export const VERSION = "1.8.5"; export const VERSION = "1.8.7";
export const keyWords = ["true", "false", "null", "undefined", "NaN", "Infinity"]; export const keyWords = ["true", "false", "null", "undefined", "NaN", "Infinity"];
@@ -73,6 +73,7 @@ export interface TreeData {
group: string[]; group: string[];
import: string[]; import: string[];
vars: VarDecl[]; vars: VarDecl[];
custom: Record<string, string | number | boolean | object>;
root: NodeData; root: NodeData;
$override: { $override: {
+83 -89
View File
@@ -20,10 +20,11 @@ import {
VERSION, VERSION,
} from "./b3type"; } from "./b3type";
import Path from "./path"; import Path from "./path";
import { stringifyJson } from "./stringify";
import { nanoid, readJson, readTree, readWorkspace } from "./util"; import { nanoid, readJson, readTree, readWorkspace } from "./util";
export class NodeDefs extends Map<string, NodeDef> { export class NodeDefs extends Map<string, NodeDef> {
get(key: string): NodeDef { override get(key: string): NodeDef {
return super.get(key) ?? unknownNodeDef; return super.get(key) ?? unknownNodeDef;
} }
} }
@@ -188,8 +189,10 @@ export const isNodeEqual = (node1: NodeData, node2: NodeData) => {
return false; return false;
}; };
const error = (data: NodeData, msg: string) => { type ErrorPrinter = (msg: string) => void;
console.error(`check ${data.id}|${data.name}: ${msg}`);
const formatError = (data: NodeData, msg: string) => {
return `check ${data.id}|${data.name}: ${msg}`;
}; };
export const getNodeArgRawType = (arg: NodeArg) => { export const getNodeArgRawType = (arg: NodeArg) => {
@@ -208,75 +211,62 @@ export const checkNodeArgValue = (
data: NodeData, data: NodeData,
arg: NodeArg, arg: NodeArg,
value: unknown, value: unknown,
verbose?: boolean printer?: ErrorPrinter
) => { ) => {
let hasError = false; let hasError = false;
const type = getNodeArgRawType(arg); const type = getNodeArgRawType(arg);
const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg));
if (isFloatType(type)) { if (isFloatType(type)) {
const isNumber = typeof value === "number"; const isNumber = typeof value === "number";
const isOptional = value === undefined && isNodeArgOptional(arg); const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isNumber || isOptional)) { if (!(isNumber || isOptional)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not a number`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a number`);
}
hasError = true; hasError = true;
} }
} else if (isIntType(type)) { } else if (isIntType(type)) {
const isInt = typeof value === "number" && value === Math.floor(value); const isInt = typeof value === "number" && value === Math.floor(value);
const isOptional = value === undefined && isNodeArgOptional(arg); const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isInt || isOptional)) { if (!(isInt || isOptional)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not a int`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a int`);
}
hasError = true; hasError = true;
} }
} else if (isStringType(type)) { } else if (isStringType(type)) {
const isString = typeof value === "string" && value; const isString = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg); const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isString || isOptional)) { if (!(isString || isOptional)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not a string`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a string`);
}
hasError = true; hasError = true;
} }
} else if (isExprType(type)) { } else if (isExprType(type)) {
const isExpr = typeof value === "string" && value; const isExpr = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg); const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isExpr || isOptional)) { if (!(isExpr || isOptional)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not an expr string`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not an expr string`);
}
hasError = true; hasError = true;
} }
} else if (isJsonType(type)) { } else if (isJsonType(type)) {
const isJson = value !== undefined && value !== ""; const isJson = value !== undefined && value !== "";
const isOptional = isNodeArgOptional(arg); const isOptional = isNodeArgOptional(arg);
if (!(isJson || isOptional)) { if (!(isJson || isOptional)) {
if (verbose) { error(`'${arg.name}=${value}' is not an invalid object`);
error(data, `'${arg.name}=${value}' is not an invalid object`);
}
hasError = true; hasError = true;
} }
} else if (isBoolType(type)) { } else if (isBoolType(type)) {
const isBool = typeof value === "boolean" || value === undefined; const isBool = typeof value === "boolean" || value === undefined;
if (!isBool) { if (!isBool) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not a boolean`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a boolean`);
}
hasError = true; hasError = true;
} }
} else { } else {
hasError = true; hasError = true;
error(data, `unknown arg type '${arg.type}'`); error(`unknown arg type '${arg.type}'`);
} }
if (hasArgOptions(arg)) { if (hasArgOptions(arg)) {
const found = !!arg.options?.find((option) => option.value === value); const found = !!arg.options?.find((option) => option.value === value);
const isOptional = value === undefined && isNodeArgOptional(arg); const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(found || isOptional)) { if (!(found || isOptional)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);
}
hasError = true; hasError = true;
} }
} }
@@ -284,37 +274,34 @@ export const checkNodeArgValue = (
return !hasError; return !hasError;
}; };
export const checkNodeArg = (data: NodeData, conf: NodeDef, i: number, verbose?: boolean) => { export const checkNodeArg = (data: NodeData, conf: NodeDef, i: number, printer?: ErrorPrinter) => {
let hasError = false; let hasError = false;
const arg = conf.args![i] as NodeArg; const arg = conf.args![i] as NodeArg;
const value = data.args?.[arg.name]; const value = data.args?.[arg.name];
const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg));
if (isNodeArgArray(arg)) { if (isNodeArgArray(arg)) {
if (!Array.isArray(value) || value.length === 0) { if (!Array.isArray(value) || value.length === 0) {
if (!isNodeArgOptional(arg)) { if (!isNodeArgOptional(arg)) {
if (verbose) { error(`'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`);
error(data, `'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`);
}
hasError = true; hasError = true;
} }
} else { } else {
for (let j = 0; j < value.length; j++) { for (let j = 0; j < value.length; j++) {
if (!checkNodeArgValue(data, arg, value[j], verbose)) { if (!checkNodeArgValue(data, arg, value[j], printer)) {
hasError = true; hasError = true;
} }
} }
} }
} else if (!checkNodeArgValue(data, arg, value, verbose)) { } else if (!checkNodeArgValue(data, arg, value, printer)) {
hasError = true; hasError = true;
} }
if (arg.oneof !== undefined) { if (arg.oneof !== undefined) {
const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1; const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1;
if (!checkOneof(arg, data.args?.[arg.name], data.input?.[idx])) { if (!checkOneof(arg, data.args?.[arg.name], data.input?.[idx])) {
if (verbose) { error(
error( `only one is allowed for between argument '${arg.name}' and input '${data.input?.[idx]}'`
data, );
`only one is allowed for between argument '${arg.name}' and input '${data.input?.[idx]}'`
);
}
hasError = true; hasError = true;
} }
} }
@@ -333,13 +320,44 @@ export const checkOneof = (arg: NodeArg, argValue: unknown, inputValue: unknown)
return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== ""); return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== "");
}; };
export const checkNodeData = (data: NodeData | null | undefined) => { export const isValidNodeData = (data: NodeData) => {
const def = nodeDefs.get(data.name);
if (def.input) {
for (let i = 0; i < def.input.length; i++) {
if (!isValidInputOrOutput(def.input, data.input, i)) {
return false;
}
}
}
if (def.output) {
for (let i = 0; i < def.output.length; i++) {
if (!isValidInputOrOutput(def.output, data.output, i)) {
return false;
}
}
}
if (!isValidChildren(data)) {
return false;
}
if (def.args) {
for (let i = 0; i < def.args.length; i++) {
if (!checkNodeArg(data, def, i)) {
return false;
}
}
}
return true;
};
const checkNodeData = (data: NodeData | null | undefined, printer: ErrorPrinter) => {
if (!data) { if (!data) {
return false; return false;
} }
const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg));
const conf = nodeDefs.get(data.name); const conf = nodeDefs.get(data.name);
if (conf.name === unknownNodeDef.name) { if (conf.name === unknownNodeDef.name) {
error(data, `undefined node: ${data.name}`); error(`undefined node: ${data.name}`);
return false; return false;
} }
@@ -347,7 +365,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
if (conf.group) { if (conf.group) {
if (!conf.group.some((g) => usingGroups?.[g])) { if (!conf.group.some((g) => usingGroups?.[g])) {
error(data, `node group '${conf.group}' is not enabled`); error(`node group '${conf.group}' is not enabled`);
hasError = true; hasError = true;
} }
} }
@@ -356,7 +374,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
if (data.input) { if (data.input) {
for (const v of data.input) { for (const v of data.input) {
if (v && !usingVars[v]) { if (v && !usingVars[v]) {
error(data, `input variable '${v}' is not defined`); error(`input variable '${v}' is not defined`);
hasError = true; hasError = true;
} }
} }
@@ -364,7 +382,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
if (data.output) { if (data.output) {
for (const v of data.output) { for (const v of data.output) {
if (v && !usingVars[v]) { if (v && !usingVars[v]) {
error(data, `output variable '${v}' is not defined`); error(`output variable '${v}' is not defined`);
hasError = true; hasError = true;
} }
} }
@@ -386,7 +404,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
} }
for (const v of vars) { for (const v of vars) {
if (v && !usingVars[v]) { if (v && !usingVars[v]) {
error(data, `expr variable '${arg.name}' is not defined`); error(`expr variable '${arg.name}' is not defined`);
hasError = true; hasError = true;
} }
} }
@@ -403,11 +421,11 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
for (const expr of exprs) { for (const expr of exprs) {
try { try {
if (!new ExpressionEvaluator(expr).dryRun()) { if (!new ExpressionEvaluator(expr).dryRun()) {
error(data, `expr '${expr}' is not valid`); error(`expr '${expr}' is not valid`);
hasError = true; hasError = true;
} }
} catch (e) { } catch (e) {
error(data, `expr '${expr}' is not valid`); error(`expr '${expr}' is not valid`);
hasError = true; hasError = true;
} }
} }
@@ -420,7 +438,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
const count = data.children?.length || 0; const count = data.children?.length || 0;
if (conf.children !== count) { if (conf.children !== count) {
hasError = true; hasError = true;
error(data, `expect ${conf.children} children, but got ${count}`); error(`expect ${conf.children} children, but got ${count}`);
} }
} }
@@ -435,14 +453,13 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
} }
if (data.input[i] && !isValidVariableName(data.input[i])) { if (data.input[i] && !isValidVariableName(data.input[i])) {
error( error(
data,
`input field '${data.input[i]}' is not a valid variable name,` + `input field '${data.input[i]}' is not a valid variable name,` +
`should start with a letter or underscore` `should start with a letter or underscore`
); );
hasError = true; hasError = true;
} }
if (!isValidInputOrOutput(conf.input, data.input, i)) { if (!isValidInputOrOutput(conf.input, data.input, i)) {
error(data, `intput field '${conf.input[i]}' is required`); error(`intput field '${conf.input[i]}' is required`);
hasError = true; hasError = true;
} }
if (i === conf.input.length - 1 && conf.input.at(-1)?.endsWith("...")) { if (i === conf.input.length - 1 && conf.input.at(-1)?.endsWith("...")) {
@@ -465,14 +482,13 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
} }
if (data.output[i] && !isValidVariableName(data.output[i])) { if (data.output[i] && !isValidVariableName(data.output[i])) {
error( error(
data,
`output field '${data.output[i]}' is not a valid variable name,` + `output field '${data.output[i]}' is not a valid variable name,` +
`should start with a letter or underscore` `should start with a letter or underscore`
); );
hasError = true; hasError = true;
} }
if (!isValidInputOrOutput(conf.output, data.output, i)) { if (!isValidInputOrOutput(conf.output, data.output, i)) {
error(data, `output field '${conf.output[i]}' is required`); error(`output field '${conf.output[i]}' is required`);
hasError = true; hasError = true;
} }
if (i === conf.output.length - 1 && conf.output.at(-1)?.endsWith("...")) { if (i === conf.output.length - 1 && conf.output.at(-1)?.endsWith("...")) {
@@ -496,7 +512,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
args[key] = value; args[key] = value;
} }
if (!checkNodeArg(data, conf, i, true)) { if (!checkNodeArg(data, conf, i, printer)) {
hasError = true; hasError = true;
} }
} }
@@ -505,7 +521,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => {
if (data.children) { if (data.children) {
for (const child of data.children) { for (const child of data.children) {
if (!checkNodeData(child)) { if (!checkNodeData(child, printer)) {
hasError = true; hasError = true;
} }
} }
@@ -665,36 +681,6 @@ const isValidInputOrOutput = (def: string[], data: string[] | undefined, index:
return def[index].includes("?") || data?.[index] || isVariadic(def, index); return def[index].includes("?") || data?.[index] || isVariadic(def, index);
}; };
export const checkTreeData = (data: NodeData) => {
const def = nodeDefs.get(data.name);
if (def.input) {
for (let i = 0; i < def.input.length; i++) {
if (!isValidInputOrOutput(def.input, data.input, i)) {
return false;
}
}
}
if (def.output) {
for (let i = 0; i < def.output.length; i++) {
if (!isValidInputOrOutput(def.output, data.output, i)) {
return false;
}
}
}
if (!isValidChildren(data)) {
return false;
}
if (def.args) {
for (let i = 0; i < def.args.length; i++) {
if (!checkNodeArg(data, def, i, false)) {
return false;
}
}
}
return true;
};
export const refreshNodeData = (tree: TreeData, node: NodeData, id: number) => { export const refreshNodeData = (tree: TreeData, node: NodeData, id: number) => {
node.id = (id++).toString(); node.id = (id++).toString();
node.$size = calcSize(node); node.$size = calcSize(node);
@@ -721,8 +707,8 @@ export const refreshNodeData = (tree: TreeData, node: NodeData, id: number) => {
const subtreePath = workdir + "/" + node.path; const subtreePath = workdir + "/" + node.path;
const subtree = readTree(subtreePath); const subtree = readTree(subtreePath);
id = refreshNodeData(subtree, subtree.root, --id); id = refreshNodeData(subtree, subtree.root, --id);
node.name = subtree.name; node.name = subtree.root.name;
node.desc = subtree.desc; node.desc = subtree.root.desc;
node.args = subtree.root.args; node.args = subtree.root.args;
node.input = subtree.root.input; node.input = subtree.root.input;
node.output = subtree.root.output; node.output = subtree.root.output;
@@ -761,6 +747,8 @@ export const createBuildData = (path: string) => {
dfs(treeModel.root, (node) => (node.id = treeModel.prefix + node.id)); dfs(treeModel.root, (node) => (node.id = treeModel.prefix + node.id));
treeModel.name = Path.basenameWithoutExt(path); treeModel.name = Path.basenameWithoutExt(path);
treeModel.root = createFileData(treeModel.root, true); treeModel.root = createFileData(treeModel.root, true);
dfs(treeModel.root, (node) => delete (node as Partial<NodeData>).$id);
delete (treeModel as Partial<TreeData>).$override;
return treeModel as TreeData; return treeModel as TreeData;
} catch (e) { } catch (e) {
console.log("build error:", path, e); console.log("build error:", path, e);
@@ -826,6 +814,7 @@ export const buildProject = async (project: string, buildDir: string) => {
}); });
} }
const allErrors: string[] = [];
for (const path of Path.ls(Path.dirname(project), true)) { for (const path of Path.ls(Path.dirname(project), true)) {
if (path.endsWith(".json")) { if (path.endsWith(".json")) {
const buildpath = buildDir + "/" + path.substring(workdir.length + 1); const buildpath = buildDir + "/" + path.substring(workdir.length + 1);
@@ -843,7 +832,6 @@ export const buildProject = async (project: string, buildDir: string) => {
} }
console.log("build:", buildpath); console.log("build:", buildpath);
if (errors.length) { if (errors.length) {
errors.forEach((msg) => console.error(msg));
hasError = true; hasError = true;
} }
const declare: FileVarDecl = { const declare: FileVarDecl = {
@@ -852,14 +840,19 @@ export const buildProject = async (project: string, buildDir: string) => {
subtree: [], subtree: [],
}; };
refreshVarDecl(tree.root, tree.group, declare); refreshVarDecl(tree.root, tree.group, declare);
if (!checkNodeData(tree?.root)) { if (!checkNodeData(tree?.root, (msg) => errors.push(msg))) {
hasError = true; hasError = true;
} }
if (errors.length) {
allErrors.push(`${path}:`);
errors.forEach((v) => allErrors.push(` ${v}`));
}
buildScript?.onWriteFile?.(buildpath, tree); buildScript?.onWriteFile?.(buildpath, tree);
fs.mkdirSync(Path.dirname(buildpath), { recursive: true }); fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
fs.writeFileSync(buildpath, JSON.stringify(tree, null, 2)); fs.writeFileSync(buildpath, stringifyJson(tree, { indent: 2 }));
} }
} }
allErrors.forEach((v) => console.error(v));
buildScript?.onComplete?.(hasError ? "failure" : "success"); buildScript?.onComplete?.(hasError ? "failure" : "success");
return hasError; return hasError;
}; };
@@ -930,6 +923,7 @@ export const createNewTree = (path: string) => {
name: "Sequence", name: "Sequence",
$id: nanoid(), $id: nanoid(),
}, },
custom: {},
$override: {}, $override: {},
}; };
return tree; return tree;
@@ -1017,7 +1011,7 @@ const collectSubtree = (data: NodeData) => {
export const refreshVarDecl = (root: NodeData, group: string[], declare: FileVarDecl) => { export const refreshVarDecl = (root: NodeData, group: string[], declare: FileVarDecl) => {
const filter: Record<string, boolean> = {}; const filter: Record<string, boolean> = {};
const vars: Array<VarDecl> = new (class extends Array { const vars: Array<VarDecl> = new (class extends Array {
push(...items: VarDecl[]): number { override push(...items: VarDecl[]): number {
for (const v of items) { for (const v of items) {
if (filter[v.name]) { if (filter[v.name]) {
continue; continue;
+332
View File
@@ -0,0 +1,332 @@
import * as fs from "node:fs";
import { basename, dirname, extname } from "node:path";
export type Tag = {
/** data name */
["!name"]?: string;
/** type tag */
["!type"]?: string;
/** special stringify function */
["!stringify"]?: (v: TValue, ctx: StringifyContext) => void;
/** ignore fields when stringify */
["!ignore"]?: { [k: string]: boolean };
};
export type TValue = boolean | number | string | null | undefined | TObject | TArray;
export type TObject = { [k: string | number]: TValue } & Tag;
export type TArray = TValue[] & Tag;
export class StringBuffer {
readonly data: string[] = [];
private _indent: number;
private _indentCount: number = 0;
constructor(indent: number) {
this._indent = indent;
}
get indentCount() {
return this._indentCount;
}
indent() {
this._indentCount += this._indent;
}
unindent() {
this._indentCount -= this._indent;
}
padding() {
if (this._indent > 0) {
this.data.push(" ".repeat(this._indentCount));
}
}
linefeed() {
if (this._indent > 0) {
this.data.push("\n");
}
}
writeLine(value: string) {
this.padding();
this.data.push(value);
this.linefeed();
}
writeLines(value: string) {
for (const line of value.split("\n")) {
this.writeLine(line);
}
}
writeString(value: string) {
this.data.push(value);
}
toString() {
return this.data.join("");
}
}
export const isNumericKey = (key: string) => {
if (typeof key !== "string") return false;
// integer or bigint
if (/^-?\d+$/.test(key)) {
try {
return String(BigInt(key)) === key;
} catch {
return false;
}
}
// float
const num = Number(key);
return !isNaN(num) && String(num) === key;
};
export const escape = (value: string) => {
return value
.replaceAll("\r\n", "\n")
.replaceAll("\\", "\\\\")
.replaceAll('"', '\\"')
.replaceAll("\n", "\\n")
.replaceAll("\t", "\\t")
.replaceAll("\r", "\\r")
.replaceAll("\b", "\\b")
.replaceAll("\f", "\\f");
};
export const outdent = (value: string) => {
value = value.replace(/^\n/, "");
value = value.replace(/\n *$/, "");
const space = value.match(/^ +/gm)?.[0];
return space ? value.replace(new RegExp(`^${space}`, "gm"), "") : value;
};
export const format = (str: string, vars: Record<string, string>) => {
const lines: string[] = [];
for (const line of str.split(/\n|\r\n/)) {
if (line.match(/^\s*%{\w+}\s*$/)) {
const [_, space, key] = line.match(/^(\s*)%{(\w+)}$/)!;
if (vars[key] !== undefined && vars[key] !== null) {
for (const l of vars[key].split(/\n|\r\n/)) {
lines.push(space + l);
}
} else {
throw new Error(`variable '${key}' not found`);
}
} else {
lines.push(
line.replaceAll(/%{(\w+)}/g, (_, key) => {
if (vars[key] !== undefined && vars[key] !== null) {
return vars[key];
}
throw new Error(`variable '${key}' not found`);
})
);
}
}
return lines.join("\n");
};
export const keys = (
o: object,
filter?: (v: TValue) => boolean,
ignore?: { [k: string]: boolean }
) => {
const value = o as TObject;
const ks = Object.keys(value).filter(
(k) => !k.startsWith("!") && (!ignore || !ignore[k]) && (!filter || filter(value[k]))
);
const numKeys: string[] = [];
const strKeys: string[] = [];
for (const k of ks) {
const num = Number(k);
if (!isNaN(num) && isFinite(num)) {
numKeys.push(k);
} else {
strKeys.push(k);
}
}
numKeys.sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
strKeys.sort((a, b) => a.localeCompare(b));
return [...numKeys, ...strKeys];
};
export const values = <T>(
o: TObject,
filter?: (v: TValue) => boolean,
ignore?: { [k: string]: boolean }
): T[] => {
return keys(o, filter, ignore).map((k) => o[k] as T);
};
export const toPascalCase = (str: string): string => {
return str
.replace(/^_+/, "")
.replace(/_([a-zA-Z])/g, (_, letter) => letter.toUpperCase())
.replace(/^[a-zA-Z]/, (match) => match.toUpperCase());
};
export const readFile = (path: string) => {
if (fs.existsSync(path)) {
return fs.readFileSync(path, "utf-8");
}
return null;
};
export const writeFile = (path: string, data: string) => {
const dir = dirname(path);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(path) && readFile(path) === data) {
console.log(`up-to-date: ${path}`);
} else {
console.log(`write: ${path}`);
fs.writeFileSync(path, data, { encoding: "utf-8" });
}
};
export const writeJson = (path: string, data: TValue, options?: JsonStringifyOption) => {
options = options ?? {};
options.indent = options.indent ?? 2;
options.precision = options.precision ?? 10;
writeFile(path, stringifyJson(data, options));
};
export const filename = (path: string, suffix: boolean = false) => {
return basename(path, !suffix ? extname(path) : undefined);
};
export type StringifyContext = {
readonly format: "json" | "lua" | "ts" | string;
readonly indent: number;
readonly precision?: number;
readonly buffer: StringBuffer;
readonly writeValue: (value: TValue) => void;
readonly writeArray: (value: TArray) => void;
readonly writeObject: (value: TObject) => void;
};
const numberToString = (value: number, precision?: number) => {
if (value === (value | 0)) {
return value.toFixed(0);
} else {
return value.toFixed(precision).replace(/\.?0+$/, "");
}
};
//-----------------------------------------------------------------------------
// Json
//-----------------------------------------------------------------------------
export type JsonStringifyOption = {
indent?: number;
precision?: number;
};
export const stringifyJson = (data: unknown, option?: JsonStringifyOption) => {
const stacks: string[] = [];
option = option ?? {};
option.indent = Math.max(option.indent ?? 4, 0);
option.precision = option.precision ?? 10;
const buffer = new StringBuffer(option.indent);
const ctx: StringifyContext = {
format: "json",
indent: option.indent,
precision: option.precision,
buffer,
writeValue: writeJsonValue,
writeArray: writeJsonArray,
writeObject: writeJsonObject,
};
function writeJsonValue(value: TValue) {
if (typeof value === "number") {
buffer.writeString(numberToString(value, ctx.precision));
} else if (typeof value === "boolean") {
buffer.writeString(value.toString());
} else if (value === null || value === undefined) {
buffer.writeString("null");
} else if (typeof value === "string") {
buffer.writeString('"');
buffer.writeString(escape(value));
buffer.writeString('"');
} else if (Array.isArray(value)) {
writeJsonArray(value);
} else {
if (typeof value !== "object" || value === null || value === undefined) {
writeJsonValue(value);
} else if (Array.isArray(value)) {
writeJsonArray(value);
} else {
writeJsonObject(value as TObject);
}
}
}
function writeJsonObject(value: TObject) {
if (stacks.length > 256) {
throw new Error(`json stringify stack overflow: ${stacks.join("->")}`);
}
if (value["!stringify"]) {
value["!stringify"](value, ctx);
return;
}
const ks = keys(value, undefined, value["!ignore"]);
const space = ctx.indent > 0 ? " " : "";
buffer.writeString("{");
buffer.linefeed();
buffer.indent();
for (let i = 0; i < ks.length; i++) {
const k = ks[i];
const v = value[k];
stacks.push(k);
buffer.padding();
buffer.writeString(`"${k}":${space}`);
writeJsonValue(v);
if (i < ks.length - 1) {
buffer.writeString(",");
}
buffer.linefeed();
stacks.pop();
}
buffer.unindent();
buffer.padding();
buffer.writeString("}");
}
function writeJsonArray(value: TArray) {
if (value["!stringify"]) {
value["!stringify"](value, ctx);
return;
}
buffer.writeString("[");
buffer.linefeed();
buffer.indent();
for (let i = 0; i < value.length; i++) {
const v = value[i];
buffer.padding();
writeJsonValue(v);
if (i < value.length - 1) {
buffer.writeString(",");
}
buffer.linefeed();
}
buffer.unindent();
buffer.padding();
buffer.writeString("]");
}
writeJsonValue(data as TValue);
return buffer.toString();
};
+5 -1
View File
@@ -4,6 +4,7 @@ import { type WorkspaceModel } from "../contexts/workspace-context";
import { VERSION, type TreeData } from "./b3type"; import { VERSION, type TreeData } from "./b3type";
import { dfs } from "./b3util"; import { dfs } from "./b3util";
import Path from "./path"; import Path from "./path";
import { stringifyJson } from "./stringify";
export const nanoid = customAlphabet( export const nanoid = customAlphabet(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
@@ -16,7 +17,7 @@ export const readJson = <T>(path: string): T => {
}; };
export const writeJson = <T>(path: string, data: T) => { export const writeJson = <T>(path: string, data: T) => {
const str = JSON.stringify(data, undefined, 2); const str = stringifyJson(data, { indent: 2 });
fs.writeFileSync(path, str, "utf-8"); fs.writeFileSync(path, str, "utf-8");
}; };
@@ -34,6 +35,8 @@ export const readTree = (path: string) => {
data.import = data.import || []; data.import = data.import || [];
data.vars = data.vars || []; data.vars = data.vars || [];
data.root = data.root || {}; data.root = data.root || {};
data.$override = data.$override || {};
data.custom = data.custom || {};
// compatible with old version // compatible with old version
dfs(data.root, (node) => { dfs(data.root, (node) => {
@@ -57,6 +60,7 @@ export const writeTree = (path: string, data: TreeData) => {
import: data.import, import: data.import,
vars: data.vars, vars: data.vars,
root: data.root, root: data.root,
custom: data.custom,
$override: data.$override, $override: data.$override,
}); });
}; };