@moku-labs/game 0.7.1 → 0.9.0
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/README.md +12 -8
- package/dist/assets.d.mts +97 -33
- package/dist/assets.mjs +350 -110
- package/dist/{commands-mzE9PX0H.d.mts → commands-DuwCxK5t.d.mts} +5 -4
- package/dist/control.d.mts +4 -3
- package/dist/control.mjs +2 -2
- package/dist/{define-BbgXv4cy.d.mts → define-B6pHpdBi.d.mts} +4 -3
- package/dist/headless-BsNdJs9W.mjs +186 -0
- package/dist/headless-SCQXJxQf.d.mts +181 -0
- package/dist/hot.d.mts +4 -2
- package/dist/hot.mjs +8 -6
- package/dist/index-BO9kZzY3.d.mts +1611 -0
- package/dist/index.d.mts +6 -1596
- package/dist/index.mjs +4 -34334
- package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
- package/dist/inspect.d.mts +7 -6
- package/dist/inspect.mjs +2 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +55 -4
- package/dist/lint.mjs +993 -40
- package/dist/project.d.mts +57 -41
- package/dist/project.mjs +994 -505
- package/dist/{run-Bcyg32M5.mjs → run-CofJEdYx.mjs} +4 -187
- package/dist/{runtime-HTlmRb81.d.mts → runtime-CdNJ0UBm.d.mts} +1 -1
- package/dist/{sources-DVWJ8nfr.mjs → sources-DNnzyCYf.mjs} +2 -2
- package/dist/src-536arXjV.mjs +34644 -0
- package/dist/testing.d.mts +85 -283
- package/dist/testing.mjs +158 -2109
- package/dist/{types-BMEn0tSC.d.mts → types-B0v5VVa3.d.mts} +10 -9
- package/dist/{types-ClF8say0.d.mts → types-CQueODzo.d.mts} +2 -2
- package/dist/{types-CYYKjvLn.d.mts → types-DUMoYPYe.d.mts} +5 -182
- package/dist/{types-CXyipKhE.d.mts → types-VZVWwqCg.d.mts} +42 -4
- package/dist/{types-BaUUcIaN.d.mts → types-xXfyk56Q.d.mts} +1 -1
- package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
- package/dist/visual.d.mts +330 -0
- package/dist/visual.mjs +2142 -0
- package/llms.txt +59 -21
- package/package.json +7 -8
- package/dist/memory-CvgdnsQO.mjs +0 -259
package/dist/project.mjs
CHANGED
|
@@ -874,13 +874,465 @@ function readModule(typescript, source) {
|
|
|
874
874
|
return draft;
|
|
875
875
|
}
|
|
876
876
|
//#endregion
|
|
877
|
+
//#region src/project/paths.ts
|
|
878
|
+
/**
|
|
879
|
+
* @file project — which files the index reads, and the guard on every path it reads. Paths in and
|
|
880
|
+
* out are root-relative POSIX. A read resolves the path against the real root with `realpath`, so
|
|
881
|
+
* a `..` or a symlink that leaves the root is refused by name instead of read.
|
|
882
|
+
*/
|
|
883
|
+
/** The folders a game never edits by hand: dependencies, builds, generated code and tests. */
|
|
884
|
+
const SKIPPED_FOLDER = /^(?:node_modules|dist|generated|\.moku|\.git|__tests__|tests)$/;
|
|
885
|
+
/** The source files the index reads. */
|
|
886
|
+
const SOURCE_FILE = /\.tsx?$/;
|
|
887
|
+
/** Test files, skipped wherever they sit. */
|
|
888
|
+
const TEST_FILE = /\.(?:test|spec)\.tsx?$/;
|
|
889
|
+
/** The hint under every root error. */
|
|
890
|
+
const ROOT_HINT = "Pass the folder of the game as root.";
|
|
891
|
+
/**
|
|
892
|
+
* Whether a folder name is one the index never walks into.
|
|
893
|
+
*
|
|
894
|
+
* @param name - One folder name.
|
|
895
|
+
* @returns True for `node_modules`, `dist`, `generated`, `.moku`, `.git`, `__tests__` and `tests`.
|
|
896
|
+
* @example
|
|
897
|
+
* ```ts
|
|
898
|
+
* isSkippedFolder("generated"); // true
|
|
899
|
+
* ```
|
|
900
|
+
*/
|
|
901
|
+
function isSkippedFolder(name) {
|
|
902
|
+
return SKIPPED_FOLDER.test(name);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Whether a root-relative path sits under a skipped folder.
|
|
906
|
+
*
|
|
907
|
+
* @param file - A path with `/` or `\` separators.
|
|
908
|
+
* @returns True when one of its folders is skipped.
|
|
909
|
+
* @example
|
|
910
|
+
* ```ts
|
|
911
|
+
* isSkippedPath("node_modules/pixi.js/index.ts"); // true
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
function isSkippedPath(file) {
|
|
915
|
+
return file.split(/[/\\]/).slice(0, -1).some((folder) => isSkippedFolder(folder));
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Whether the index reads a root-relative path: a `.ts` or `.tsx` file outside the skipped
|
|
919
|
+
* folders that is not a test.
|
|
920
|
+
*
|
|
921
|
+
* @param file - A root-relative POSIX path.
|
|
922
|
+
* @returns True for a source file of the index.
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* isIndexedFile("features/ui/kit.tsx"); // true
|
|
926
|
+
* ```
|
|
927
|
+
*/
|
|
928
|
+
function isIndexedFile(file) {
|
|
929
|
+
return SOURCE_FILE.test(file) && !TEST_FILE.test(file) && !isSkippedPath(file);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Turns a path into its root-relative POSIX form.
|
|
933
|
+
*
|
|
934
|
+
* @param file - A path with any separators.
|
|
935
|
+
* @returns The path with `/` separators, `.` segments resolved.
|
|
936
|
+
* @example
|
|
937
|
+
* ```ts
|
|
938
|
+
* toPosix("./features\\ui\\kit.tsx"); // "features/ui/kit.tsx"
|
|
939
|
+
* ```
|
|
940
|
+
*/
|
|
941
|
+
function toPosix(file) {
|
|
942
|
+
return path.posix.normalize(file.replaceAll("\\", "/"));
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Whether a file system error says nothing is at the path.
|
|
946
|
+
*
|
|
947
|
+
* @param error - What was thrown.
|
|
948
|
+
* @returns True for `ENOENT` and `ENOTDIR`.
|
|
949
|
+
*/
|
|
950
|
+
function isMissing(error) {
|
|
951
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
952
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Whether an absolute path lies inside a folder.
|
|
956
|
+
*
|
|
957
|
+
* @param folder - The absolute folder.
|
|
958
|
+
* @param file - The absolute path.
|
|
959
|
+
* @returns True for the folder itself and anything below it.
|
|
960
|
+
*/
|
|
961
|
+
function isInside(folder, file) {
|
|
962
|
+
const relative = path.relative(folder, file);
|
|
963
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* The error of a path that leaves the root.
|
|
967
|
+
*
|
|
968
|
+
* @param file - The path as it was given.
|
|
969
|
+
* @param root - The real root.
|
|
970
|
+
* @returns The error to throw.
|
|
971
|
+
*/
|
|
972
|
+
function leavesRoot(file, root) {
|
|
973
|
+
return /* @__PURE__ */ new Error(`[game] The path "${file}" leaves the project root.\n Pass a path inside "${root}", relative to it.`);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Checks the root of a project and resolves it: it exists, it is a directory, and its real path
|
|
977
|
+
* is the one every read is held to.
|
|
978
|
+
*
|
|
979
|
+
* @param root - The root as given, absolute or relative to the working directory.
|
|
980
|
+
* @returns The real absolute path of the root.
|
|
981
|
+
* @throws {Error} When the root does not exist or is not a directory.
|
|
982
|
+
*/
|
|
983
|
+
async function openRoot(root) {
|
|
984
|
+
const absolute = path.resolve(root);
|
|
985
|
+
if (!(await stat(absolute).catch((error) => {
|
|
986
|
+
if (!isMissing(error)) throw error;
|
|
987
|
+
throw new Error(`[game] The project root "${absolute}" does not exist.\n ${ROOT_HINT}`);
|
|
988
|
+
})).isDirectory()) throw new Error(`[game] The project root "${absolute}" is not a directory.\n ${ROOT_HINT}`);
|
|
989
|
+
return realpath(absolute);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Resolves a root-relative path to the real file, refusing any path that leaves the root.
|
|
993
|
+
*
|
|
994
|
+
* @param root - The real root.
|
|
995
|
+
* @param file - The root-relative path.
|
|
996
|
+
* @returns The real absolute path, or `undefined` when nothing is there.
|
|
997
|
+
* @throws {Error} When the path leaves the root, by `..`, as an absolute path or by a symlink.
|
|
998
|
+
*/
|
|
999
|
+
async function resolveInside(root, file) {
|
|
1000
|
+
const absolute = path.resolve(root, file);
|
|
1001
|
+
if (!isInside(root, absolute)) throw leavesRoot(file, root);
|
|
1002
|
+
let real;
|
|
1003
|
+
try {
|
|
1004
|
+
real = await realpath(absolute);
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
if (isMissing(error)) return;
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
if (!isInside(root, real)) throw leavesRoot(file, root);
|
|
1010
|
+
return real;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Reads the bytes of a root-relative file through the guard. A path that leaves the root reads as
|
|
1014
|
+
* nothing here; `changed` refuses it by name with `resolveInside` first.
|
|
1015
|
+
*
|
|
1016
|
+
* @param root - The real root.
|
|
1017
|
+
* @param file - The root-relative path.
|
|
1018
|
+
* @returns The bytes, or `undefined` when the file is gone or lies outside the root.
|
|
1019
|
+
*/
|
|
1020
|
+
async function readInside(root, file) {
|
|
1021
|
+
try {
|
|
1022
|
+
const real = await resolveInside(root, file);
|
|
1023
|
+
if (real !== void 0) return await readFile(real);
|
|
1024
|
+
} catch {}
|
|
1025
|
+
}
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region src/project/aliases.ts
|
|
1028
|
+
/**
|
|
1029
|
+
* @file project — the tsconfig `paths` of a game, the one alias resolver of the index. A v15 game
|
|
1030
|
+
* imports across its layers through aliases (`@core/kit`, `@features/home`, `@shared`). The
|
|
1031
|
+
* tsconfig is read with TypeScript, so JSONC and the `extends` chain are TypeScript's own; a
|
|
1032
|
+
* specifier is matched the way TypeScript matches it: an exact key first, then the `*` key with
|
|
1033
|
+
* the longest prefix. The engine is a package by definition and is never matched. Paths in and
|
|
1034
|
+
* out are root-relative POSIX.
|
|
1035
|
+
*/
|
|
1036
|
+
/** A specifier relative to the importing file, or an absolute path: never an alias. */
|
|
1037
|
+
const NOT_BARE = /^(?:\.{1,2}(?:\/|$)|\/)/;
|
|
1038
|
+
/** The hint under a tsconfig that does not parse. */
|
|
1039
|
+
const PARSE_HINT = "Fix the file, or name another one with the tsconfig option or --tsconfig.";
|
|
1040
|
+
/** TypeScript numbers its syntax diagnostics from 1000 to 1999. */
|
|
1041
|
+
const SYNTAX_CODES = {
|
|
1042
|
+
from: 1e3,
|
|
1043
|
+
below: 2e3
|
|
1044
|
+
};
|
|
1045
|
+
/**
|
|
1046
|
+
* Whether a specifier names the engine, which is a package even when the paths map it.
|
|
1047
|
+
*
|
|
1048
|
+
* @param specifier - A module specifier.
|
|
1049
|
+
* @returns True for `@moku-labs/game` and its subpaths.
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```ts
|
|
1052
|
+
* isEngine("@moku-labs/game/testing"); // true
|
|
1053
|
+
* ```
|
|
1054
|
+
*/
|
|
1055
|
+
function isEngine(specifier) {
|
|
1056
|
+
return specifier === "@moku-labs/game" || specifier.startsWith(`@moku-labs/game/`);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* How many `*` a key or a target holds.
|
|
1060
|
+
*
|
|
1061
|
+
* @param text - A key or a target of `paths`.
|
|
1062
|
+
* @returns The count.
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```ts
|
|
1065
|
+
* starsOf("@features/*"); // 1
|
|
1066
|
+
* ```
|
|
1067
|
+
*/
|
|
1068
|
+
function starsOf(text) {
|
|
1069
|
+
return text.split("*").length - 1;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Whether a diagnostic of a tsconfig says a file does not parse: a syntax error in the tsconfig
|
|
1073
|
+
* or in a file it extends. TypeScript also reports an unknown option, a missing `extends` and
|
|
1074
|
+
* the empty file list of a read that lists no files; none of them hides the `paths`.
|
|
1075
|
+
*
|
|
1076
|
+
* @param diagnostic - A diagnostic of the read.
|
|
1077
|
+
* @returns True for a syntax error of a file.
|
|
1078
|
+
*/
|
|
1079
|
+
function isSyntaxError(diagnostic) {
|
|
1080
|
+
return diagnostic.file !== void 0 && diagnostic.code >= SYNTAX_CODES.from && diagnostic.code < SYNTAX_CODES.below;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* The error of a tsconfig that does not parse, naming the file and the place as
|
|
1084
|
+
* `path:line:col message`.
|
|
1085
|
+
*
|
|
1086
|
+
* @param typescript - The TypeScript module.
|
|
1087
|
+
* @param root - The real root.
|
|
1088
|
+
* @param file - The tsconfig, root-relative.
|
|
1089
|
+
* @param diagnostic - The syntax error.
|
|
1090
|
+
* @returns The error to throw.
|
|
1091
|
+
*/
|
|
1092
|
+
function doesNotParse(typescript, root, file, diagnostic) {
|
|
1093
|
+
const text = typescript.flattenDiagnosticMessageText(diagnostic.messageText, " ");
|
|
1094
|
+
const message = text.endsWith(".") ? text : `${text}.`;
|
|
1095
|
+
const source = diagnostic.file;
|
|
1096
|
+
let place = "";
|
|
1097
|
+
if (source !== void 0) {
|
|
1098
|
+
const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
1099
|
+
place = `${toPosix(path.relative(root, source.fileName))}:${line + 1}:${character + 1} `;
|
|
1100
|
+
}
|
|
1101
|
+
return /* @__PURE__ */ new Error(`[game] The tsconfig "${file}" does not parse: ${place}${message}\n ${PARSE_HINT}`);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* The targets of one key of `paths`, root-relative: each string target with one `*` at most,
|
|
1105
|
+
* resolved against the base folder. TypeScript does not check the shape of `paths`, so the value
|
|
1106
|
+
* is read as the JSON it is.
|
|
1107
|
+
*
|
|
1108
|
+
* @param root - The real root.
|
|
1109
|
+
* @param base - The absolute folder the targets are relative to.
|
|
1110
|
+
* @param value - The value of the key.
|
|
1111
|
+
* @returns The targets, `*` kept.
|
|
1112
|
+
*/
|
|
1113
|
+
function targetsOf(root, base, value) {
|
|
1114
|
+
if (!Array.isArray(value)) return [];
|
|
1115
|
+
return value.filter((target) => typeof target === "string" && starsOf(target) <= 1).map((target) => toPosix(path.relative(root, path.resolve(base, target))));
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* The patterns of a `paths` block: one per key with one `*` at most and a target. A key or a
|
|
1119
|
+
* target with two `*` never matches in TypeScript, so it is dropped here.
|
|
1120
|
+
*
|
|
1121
|
+
* @param root - The real root.
|
|
1122
|
+
* @param base - The absolute folder the targets are relative to.
|
|
1123
|
+
* @param paths - The `paths` of the merged options.
|
|
1124
|
+
* @returns The patterns, in the order of the keys.
|
|
1125
|
+
*/
|
|
1126
|
+
function patternsOf(root, base, paths) {
|
|
1127
|
+
const patterns = [];
|
|
1128
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
1129
|
+
const targets = targetsOf(root, base, value);
|
|
1130
|
+
const star = key.indexOf("*");
|
|
1131
|
+
if (targets.length === 0 || starsOf(key) > 1) continue;
|
|
1132
|
+
patterns.push(star === -1 ? {
|
|
1133
|
+
prefix: key,
|
|
1134
|
+
targets
|
|
1135
|
+
} : {
|
|
1136
|
+
prefix: key.slice(0, star),
|
|
1137
|
+
suffix: key.slice(star + 1),
|
|
1138
|
+
targets
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return patterns;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* The files TypeScript read for a tsconfig that lie inside the root, outside `node_modules`,
|
|
1145
|
+
* root-relative and each once. A file it tried and did not find is kept: its arrival changes the
|
|
1146
|
+
* aliases.
|
|
1147
|
+
*
|
|
1148
|
+
* @param root - The real root.
|
|
1149
|
+
* @param read - The absolute paths TypeScript asked for, in order.
|
|
1150
|
+
* @returns The root-relative paths.
|
|
1151
|
+
*/
|
|
1152
|
+
function sourcesInside(root, read) {
|
|
1153
|
+
const inside = read.map((file) => toPosix(path.relative(root, file))).filter((file) => !file.startsWith("../") && !path.isAbsolute(file)).filter((file) => !file.split("/").includes("node_modules"));
|
|
1154
|
+
return [...new Set(inside)];
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Parses a tsconfig with TypeScript: comments and trailing commas included, its `extends` chain
|
|
1158
|
+
* merged, and every file TypeScript reads for that chain noted. No file is listed, so the parse
|
|
1159
|
+
* stays cheap.
|
|
1160
|
+
*
|
|
1161
|
+
* @param typescript - The TypeScript module.
|
|
1162
|
+
* @param root - The real root.
|
|
1163
|
+
* @param file - The tsconfig, root-relative POSIX.
|
|
1164
|
+
* @param text - The text of the tsconfig.
|
|
1165
|
+
* @returns The merged options and the files TypeScript read.
|
|
1166
|
+
* @throws {Error} When the tsconfig or a file it extends does not parse.
|
|
1167
|
+
*/
|
|
1168
|
+
function parseTsconfigOptions(typescript, root, file, text) {
|
|
1169
|
+
const absolute = path.join(root, file);
|
|
1170
|
+
const parsed = typescript.parseConfigFileTextToJson(absolute, text);
|
|
1171
|
+
if (parsed.error !== void 0) throw doesNotParse(typescript, root, file, parsed.error);
|
|
1172
|
+
const read = [];
|
|
1173
|
+
const host = {
|
|
1174
|
+
...typescript.sys,
|
|
1175
|
+
readDirectory: () => [],
|
|
1176
|
+
readFile: (name) => {
|
|
1177
|
+
read.push(name);
|
|
1178
|
+
return typescript.sys.readFile(name);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
const { options, errors } = typescript.parseJsonConfigFileContent(parsed.config, host, path.dirname(absolute), void 0, absolute);
|
|
1182
|
+
const syntax = errors.find((diagnostic) => isSyntaxError(diagnostic));
|
|
1183
|
+
if (syntax !== void 0) throw doesNotParse(typescript, root, file, syntax);
|
|
1184
|
+
return {
|
|
1185
|
+
options,
|
|
1186
|
+
read
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The folder TypeScript resolves `paths` against when no `baseUrl` is set: the folder of the
|
|
1191
|
+
* config that declares them, which may be a file the tsconfig extends. TypeScript keeps it on the
|
|
1192
|
+
* merged options as `pathsBasePath`, outside its published types.
|
|
1193
|
+
*
|
|
1194
|
+
* @param options - The merged options.
|
|
1195
|
+
* @returns The absolute folder, or `undefined` when no config declares `paths`.
|
|
1196
|
+
*/
|
|
1197
|
+
function pathsBaseOf(options) {
|
|
1198
|
+
const base = options.pathsBasePath;
|
|
1199
|
+
return typeof base === "string" ? base : void 0;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Reads the tsconfig `paths` of a game. TypeScript parses the file and merges its `extends`
|
|
1203
|
+
* chain. The targets resolve against `baseUrl` when one is set, else against the folder of the
|
|
1204
|
+
* config that declares `paths`, as TypeScript resolves them: the folder of the tsconfig, or of the
|
|
1205
|
+
* file it extends that holds the block.
|
|
1206
|
+
*
|
|
1207
|
+
* @param typescript - The TypeScript module.
|
|
1208
|
+
* @param root - The real root.
|
|
1209
|
+
* @param file - The tsconfig, root-relative POSIX.
|
|
1210
|
+
* @returns The aliases, or `undefined` when the file is missing. A tsconfig without `paths` has
|
|
1211
|
+
* no patterns and still lists its sources, so a `paths` block added to a file it extends is seen.
|
|
1212
|
+
* @throws {Error} When the tsconfig or a file it extends does not parse.
|
|
1213
|
+
*/
|
|
1214
|
+
async function readAliases(typescript, root, file) {
|
|
1215
|
+
const bytes = await readInside(root, file);
|
|
1216
|
+
if (bytes === void 0) return void 0;
|
|
1217
|
+
const { options, read } = parseTsconfigOptions(typescript, root, file, new TextDecoder().decode(bytes));
|
|
1218
|
+
const patterns = patternsOf(root, options.baseUrl ?? pathsBaseOf(options) ?? path.dirname(path.join(root, file)), options.paths ?? {});
|
|
1219
|
+
return {
|
|
1220
|
+
file,
|
|
1221
|
+
sources: [.../* @__PURE__ */ new Set([file, ...sourcesInside(root, read)])],
|
|
1222
|
+
patterns
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Whether a `*` key matches a specifier.
|
|
1227
|
+
*
|
|
1228
|
+
* @param pattern - A key with `*`.
|
|
1229
|
+
* @param suffix - Its suffix.
|
|
1230
|
+
* @param specifier - The module specifier.
|
|
1231
|
+
* @returns True when the specifier starts with the prefix and ends with the suffix.
|
|
1232
|
+
* @example
|
|
1233
|
+
* ```ts
|
|
1234
|
+
* matchesStar({ prefix: "@core/", suffix: "", targets: ["core/*"] }, "", "@core/kit"); // true
|
|
1235
|
+
* ```
|
|
1236
|
+
*/
|
|
1237
|
+
function matchesStar(pattern, suffix, specifier) {
|
|
1238
|
+
return specifier.length >= pattern.prefix.length + suffix.length && specifier.startsWith(pattern.prefix) && specifier.endsWith(suffix);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Whether a specifier is never an alias: a path relative to the importing file, an absolute path,
|
|
1242
|
+
* or the engine.
|
|
1243
|
+
*
|
|
1244
|
+
* @param specifier - The module specifier as written.
|
|
1245
|
+
* @returns True when no key of `paths` may match it.
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```ts
|
|
1248
|
+
* isNeverAlias("@moku-labs/game/testing"); // true
|
|
1249
|
+
* ```
|
|
1250
|
+
*/
|
|
1251
|
+
function isNeverAlias(specifier) {
|
|
1252
|
+
return NOT_BARE.test(specifier) || isEngine(specifier);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Whether a key of `paths` is a `*` key that matches a specifier with a longer prefix than the
|
|
1256
|
+
* best match so far.
|
|
1257
|
+
*
|
|
1258
|
+
* @param pattern - A key of `paths`.
|
|
1259
|
+
* @param best - The best `*` match so far, if any.
|
|
1260
|
+
* @param specifier - The module specifier.
|
|
1261
|
+
* @returns True when the key is the better match.
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```ts
|
|
1264
|
+
* isBetterMatch({ prefix: "@core/", suffix: "", targets: ["core/*"] }, undefined, "@core/kit"); // true
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
function isBetterMatch(pattern, best, specifier) {
|
|
1268
|
+
const isLonger = best === void 0 || pattern.prefix.length > best.prefix.length;
|
|
1269
|
+
return pattern.suffix !== void 0 && isLonger && matchesStar(pattern, pattern.suffix, specifier);
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* The targets a specifier maps to, the way TypeScript maps it: a key without `*` equal to the
|
|
1273
|
+
* specifier wins; else the `*` key with the longest prefix, its `*` filled into every target. A
|
|
1274
|
+
* relative or absolute specifier, the engine (`@moku-labs/game` and its subpaths) and a specifier
|
|
1275
|
+
* no key matches map to nothing.
|
|
1276
|
+
*
|
|
1277
|
+
* @param aliases - The aliases of the game, if it declares some.
|
|
1278
|
+
* @param specifier - The module specifier as written.
|
|
1279
|
+
* @returns The root-relative targets in the order of the tsconfig; `[]` for no match.
|
|
1280
|
+
* @example
|
|
1281
|
+
* ```ts
|
|
1282
|
+
* // map: the aliases of tests/fixtures/layout-game/tsconfig.json
|
|
1283
|
+
* aliasTargets(map, "@features/orders"); // ["features/orders/index.ts"]
|
|
1284
|
+
* aliasTargets(map, "@moku-labs/game/testing"); // []
|
|
1285
|
+
* ```
|
|
1286
|
+
*/
|
|
1287
|
+
function aliasTargets(aliases, specifier) {
|
|
1288
|
+
if (aliases === void 0 || isNeverAlias(specifier)) return [];
|
|
1289
|
+
const exact = aliases.patterns.find((pattern) => pattern.suffix === void 0 && pattern.prefix === specifier);
|
|
1290
|
+
if (exact !== void 0) return [...exact.targets];
|
|
1291
|
+
let best;
|
|
1292
|
+
for (const pattern of aliases.patterns) if (isBetterMatch(pattern, best, specifier)) best = pattern;
|
|
1293
|
+
if (best === void 0) return [];
|
|
1294
|
+
const hole = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
|
|
1295
|
+
return best.targets.map((target) => target.replace("*", () => hole));
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Whether two keys of `paths` are the same key with the same targets.
|
|
1299
|
+
*
|
|
1300
|
+
* @param first - One pattern.
|
|
1301
|
+
* @param second - The other, if any.
|
|
1302
|
+
* @returns True when prefix, suffix and targets are equal.
|
|
1303
|
+
* @example
|
|
1304
|
+
* ```ts
|
|
1305
|
+
* samePattern({ prefix: "@kit", targets: ["kit.ts"] }, { prefix: "@kit", targets: ["kit.ts"] }); // true
|
|
1306
|
+
* ```
|
|
1307
|
+
*/
|
|
1308
|
+
function samePattern(first, second) {
|
|
1309
|
+
return second !== void 0 && first.prefix === second.prefix && first.suffix === second.suffix && first.targets.length === second.targets.length && first.targets.every((target, at) => target === second.targets[at]);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Whether two reads of the aliases resolve alike: the same tsconfig and the same patterns in the
|
|
1313
|
+
* same order. The sources are left out: they feed the stamps, not the resolution.
|
|
1314
|
+
*
|
|
1315
|
+
* @param first - The aliases before, or `undefined` for none.
|
|
1316
|
+
* @param second - The aliases after, or `undefined` for none.
|
|
1317
|
+
* @returns True when the index built with either is the same.
|
|
1318
|
+
* @example
|
|
1319
|
+
* ```ts
|
|
1320
|
+
* sameAliases(undefined, undefined); // true
|
|
1321
|
+
* ```
|
|
1322
|
+
*/
|
|
1323
|
+
function sameAliases(first, second) {
|
|
1324
|
+
if (first === void 0 || second === void 0) return first === second;
|
|
1325
|
+
return first.file === second.file && first.patterns.length === second.patterns.length && first.patterns.every((pattern, at) => samePattern(pattern, second.patterns[at]));
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
877
1328
|
//#region src/project/extract/resolve.ts
|
|
878
1329
|
/**
|
|
879
1330
|
* @file project, extract — following a name across files with the module facts only: an import
|
|
880
|
-
* to the module it names, a
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* a node of a flow table reaches the
|
|
1331
|
+
* to the module it names, through a tsconfig alias when the game declares one, a re-export to the
|
|
1332
|
+
* module behind it, a star export to the first module that has the name, until the name is
|
|
1333
|
+
* declared, handed out by the kit, or comes from a package. This is how a definer is recognised
|
|
1334
|
+
* by binding (a renamed import still is the definer) and how a node of a flow table reaches the
|
|
1335
|
+
* file that declares it.
|
|
884
1336
|
*/
|
|
885
1337
|
/** How many imports and re-exports a name is followed through before giving up. */
|
|
886
1338
|
const MAX_HOPS = 12;
|
|
@@ -900,20 +1352,18 @@ function isRelative(specifier) {
|
|
|
900
1352
|
return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
|
|
901
1353
|
}
|
|
902
1354
|
/**
|
|
903
|
-
*
|
|
904
|
-
*
|
|
1355
|
+
* The indexed file a root-relative path names, the way a bundler finds it: the path as written,
|
|
1356
|
+
* then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
|
|
905
1357
|
*
|
|
906
1358
|
* @param modules - The module facts by path.
|
|
907
|
-
* @param
|
|
908
|
-
* @
|
|
909
|
-
* @returns The root-relative path, or `undefined` for a package or a file the index does not hold.
|
|
1359
|
+
* @param base - A root-relative path.
|
|
1360
|
+
* @returns The path the index holds, or `undefined`.
|
|
910
1361
|
*/
|
|
911
|
-
function
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
const stem = base.replace(SCRIPT_EXTENSION, "");
|
|
1362
|
+
function indexedFile(modules, base) {
|
|
1363
|
+
const normal = path.posix.normalize(base);
|
|
1364
|
+
const stem = normal.replace(SCRIPT_EXTENSION, "");
|
|
915
1365
|
return [
|
|
916
|
-
|
|
1366
|
+
normal,
|
|
917
1367
|
`${stem}.ts`,
|
|
918
1368
|
`${stem}.tsx`,
|
|
919
1369
|
`${stem}/index.ts`,
|
|
@@ -921,40 +1371,69 @@ function resolveModulePath(modules, from, specifier) {
|
|
|
921
1371
|
].find((candidate) => modules.has(candidate));
|
|
922
1372
|
}
|
|
923
1373
|
/**
|
|
1374
|
+
* Resolves a module specifier to an indexed file: a relative one against the importing file, any
|
|
1375
|
+
* other through the targets of the tsconfig alias it matches, the first target the index holds
|
|
1376
|
+
* winning. Each candidate is tried as written, then with `.ts`, `.tsx`, `/index.ts` and
|
|
1377
|
+
* `/index.tsx`.
|
|
1378
|
+
*
|
|
1379
|
+
* @param resolution - The module facts and the aliases.
|
|
1380
|
+
* @param from - The importing file.
|
|
1381
|
+
* @param specifier - The module specifier.
|
|
1382
|
+
* @returns The root-relative path, or `undefined` for a package or a file the index does not hold.
|
|
1383
|
+
*/
|
|
1384
|
+
function resolveModulePath(resolution, from, specifier) {
|
|
1385
|
+
const { modules } = resolution;
|
|
1386
|
+
if (isRelative(specifier)) return indexedFile(modules, path.posix.join(path.posix.dirname(from), specifier));
|
|
1387
|
+
for (const target of aliasTargets(resolution.aliases, specifier)) {
|
|
1388
|
+
const found = indexedFile(modules, target);
|
|
1389
|
+
if (found !== void 0) return found;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Whether a specifier names a package: it is not relative and no tsconfig alias matches it.
|
|
1394
|
+
*
|
|
1395
|
+
* @param resolution - The module facts and the aliases.
|
|
1396
|
+
* @param specifier - The module specifier.
|
|
1397
|
+
* @returns True for a package.
|
|
1398
|
+
*/
|
|
1399
|
+
function isPackage(resolution, specifier) {
|
|
1400
|
+
return !isRelative(specifier) && aliasTargets(resolution.aliases, specifier).length === 0;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
924
1403
|
* Follows an imported name into the module that the specifier names.
|
|
925
1404
|
*
|
|
926
|
-
* @param
|
|
1405
|
+
* @param resolution - The module facts and the aliases.
|
|
927
1406
|
* @param from - The importing file.
|
|
928
1407
|
* @param ref - The specifier and the name.
|
|
929
1408
|
* @param hops - How many hops were taken so far.
|
|
930
1409
|
* @returns Where the name ends up.
|
|
931
1410
|
*/
|
|
932
|
-
function followImport(
|
|
933
|
-
if (
|
|
1411
|
+
function followImport(resolution, from, ref, hops) {
|
|
1412
|
+
if (isPackage(resolution, ref.specifier)) return {
|
|
934
1413
|
kind: "package",
|
|
935
1414
|
...ref
|
|
936
1415
|
};
|
|
937
1416
|
if (ref.imported === "*") return void 0;
|
|
938
|
-
const target = resolveModulePath(
|
|
939
|
-
return target === void 0 ? void 0 : resolveExport(
|
|
1417
|
+
const target = resolveModulePath(resolution, from, ref.specifier);
|
|
1418
|
+
return target === void 0 ? void 0 : resolveExport(resolution, target, ref.imported, hops + 1);
|
|
940
1419
|
}
|
|
941
1420
|
/**
|
|
942
1421
|
* Follows an exported name of a module: a local binding, a re-export, or a star export.
|
|
943
1422
|
*
|
|
944
|
-
* @param
|
|
1423
|
+
* @param resolution - The module facts and the aliases.
|
|
945
1424
|
* @param file - The module.
|
|
946
1425
|
* @param name - The exported name.
|
|
947
1426
|
* @param hops - How many hops were taken so far.
|
|
948
1427
|
* @returns Where the name ends up.
|
|
949
1428
|
*/
|
|
950
|
-
function resolveExport(
|
|
951
|
-
const facts = modules.get(file);
|
|
1429
|
+
function resolveExport(resolution, file, name, hops = 0) {
|
|
1430
|
+
const facts = resolution.modules.get(file);
|
|
952
1431
|
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
953
1432
|
const ref = facts.exports.get(name);
|
|
954
|
-
if (ref !== void 0 && "local" in ref) return resolveBinding(
|
|
955
|
-
if (ref !== void 0) return followImport(
|
|
1433
|
+
if (ref !== void 0 && "local" in ref) return resolveBinding(resolution, file, ref.local, hops + 1);
|
|
1434
|
+
if (ref !== void 0) return followImport(resolution, file, ref, hops);
|
|
956
1435
|
for (const specifier of facts.stars) {
|
|
957
|
-
const found = followImport(
|
|
1436
|
+
const found = followImport(resolution, file, {
|
|
958
1437
|
specifier,
|
|
959
1438
|
imported: name
|
|
960
1439
|
}, hops);
|
|
@@ -965,14 +1444,14 @@ function resolveExport(modules, file, name, hops = 0) {
|
|
|
965
1444
|
* Follows a name used in a file to where it ends up: the kit hands it out, the file declares it,
|
|
966
1445
|
* or an import brings it.
|
|
967
1446
|
*
|
|
968
|
-
* @param
|
|
1447
|
+
* @param resolution - The module facts and the aliases.
|
|
969
1448
|
* @param file - The file that uses the name.
|
|
970
1449
|
* @param local - The name as the file writes it.
|
|
971
1450
|
* @param hops - How many hops were taken so far.
|
|
972
1451
|
* @returns Where the name ends up, or `undefined` when it cannot be followed.
|
|
973
1452
|
*/
|
|
974
|
-
function resolveBinding(
|
|
975
|
-
const facts = modules.get(file);
|
|
1453
|
+
function resolveBinding(resolution, file, local, hops = 0) {
|
|
1454
|
+
const facts = resolution.modules.get(file);
|
|
976
1455
|
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
977
1456
|
const definer = facts.kit.get(local);
|
|
978
1457
|
if (definer !== void 0) return {
|
|
@@ -985,7 +1464,21 @@ function resolveBinding(modules, file, local, hops = 0) {
|
|
|
985
1464
|
binding: local
|
|
986
1465
|
};
|
|
987
1466
|
const ref = facts.imports.get(local);
|
|
988
|
-
return ref === void 0 ? void 0 : followImport(
|
|
1467
|
+
return ref === void 0 ? void 0 : followImport(resolution, file, ref, hops);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* The alias a name of a file is imported through when that alias reaches no indexed file: a
|
|
1471
|
+
* tsconfig key matched the specifier, and none of its targets is a file of the root.
|
|
1472
|
+
*
|
|
1473
|
+
* @param resolution - The module facts and the aliases.
|
|
1474
|
+
* @param file - The file that imports the name.
|
|
1475
|
+
* @param local - The name as the file writes it.
|
|
1476
|
+
* @returns The specifier as written, or `undefined` when the import is not such an alias.
|
|
1477
|
+
*/
|
|
1478
|
+
function missedAlias(resolution, file, local) {
|
|
1479
|
+
const ref = resolution.modules.get(file)?.imports.get(local);
|
|
1480
|
+
if (!(ref !== void 0 && !isRelative(ref.specifier) && !isPackage(resolution, ref.specifier))) return void 0;
|
|
1481
|
+
return resolveModulePath(resolution, file, ref.specifier) === void 0 ? ref.specifier : void 0;
|
|
989
1482
|
}
|
|
990
1483
|
/**
|
|
991
1484
|
* The definer a target is, if any: one the kit hands out, or one imported from the engine.
|
|
@@ -1005,21 +1498,21 @@ function definerOf(target) {
|
|
|
1005
1498
|
/**
|
|
1006
1499
|
* Adds the definers a namespace import reaches as `ns.name`.
|
|
1007
1500
|
*
|
|
1008
|
-
* @param
|
|
1501
|
+
* @param resolution - The module facts and the aliases.
|
|
1009
1502
|
* @param file - The importing file.
|
|
1010
1503
|
* @param namespace - The local name of the namespace.
|
|
1011
1504
|
* @param specifier - The module it imports.
|
|
1012
1505
|
* @param definers - The map to add to.
|
|
1013
1506
|
*/
|
|
1014
|
-
function addNamespace(
|
|
1507
|
+
function addNamespace(resolution, file, namespace, specifier, definers) {
|
|
1015
1508
|
if (specifier === "@moku-labs/game") {
|
|
1016
1509
|
for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
|
|
1017
1510
|
return;
|
|
1018
1511
|
}
|
|
1019
|
-
const target = resolveModulePath(
|
|
1020
|
-
const names = target === void 0 ? [] : [...modules.get(target)?.exports.keys() ?? []];
|
|
1512
|
+
const target = resolveModulePath(resolution, file, specifier);
|
|
1513
|
+
const names = target === void 0 ? [] : [...resolution.modules.get(target)?.exports.keys() ?? []];
|
|
1021
1514
|
for (const name of names) {
|
|
1022
|
-
const definer = definerOf(resolveExport(
|
|
1515
|
+
const definer = definerOf(resolveExport(resolution, target ?? "", name));
|
|
1023
1516
|
if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
|
|
1024
1517
|
}
|
|
1025
1518
|
}
|
|
@@ -1027,19 +1520,19 @@ function addNamespace(modules, file, namespace, specifier, definers) {
|
|
|
1027
1520
|
* The definers a file can call, by the callee as the file writes it: `defineFlow`, a renamed
|
|
1028
1521
|
* `scene`, or `kit.defineScene` through a namespace.
|
|
1029
1522
|
*
|
|
1030
|
-
* @param
|
|
1523
|
+
* @param resolution - The module facts and the aliases.
|
|
1031
1524
|
* @param file - The file.
|
|
1032
1525
|
* @returns The callee text to the definer.
|
|
1033
1526
|
*/
|
|
1034
|
-
function definersOf(
|
|
1035
|
-
const facts = modules.get(file);
|
|
1527
|
+
function definersOf(resolution, file) {
|
|
1528
|
+
const facts = resolution.modules.get(file);
|
|
1036
1529
|
const definers = new Map(facts?.kit);
|
|
1037
1530
|
for (const [local, ref] of facts?.imports ?? []) {
|
|
1038
1531
|
if (ref.imported === "*") {
|
|
1039
|
-
addNamespace(
|
|
1532
|
+
addNamespace(resolution, file, local, ref.specifier, definers);
|
|
1040
1533
|
continue;
|
|
1041
1534
|
}
|
|
1042
|
-
const definer = definerOf(resolveBinding(
|
|
1535
|
+
const definer = definerOf(resolveBinding(resolution, file, local));
|
|
1043
1536
|
if (definer !== void 0) definers.set(local, definer);
|
|
1044
1537
|
}
|
|
1045
1538
|
return definers;
|
|
@@ -1121,6 +1614,20 @@ function entryOf(symbols, key) {
|
|
|
1121
1614
|
return entry;
|
|
1122
1615
|
}
|
|
1123
1616
|
/**
|
|
1617
|
+
* Why a name of a flow table cannot be followed: the alias it is imported through reaches no
|
|
1618
|
+
* file, or it is declared in no file of the root.
|
|
1619
|
+
*
|
|
1620
|
+
* @param resolution - The module facts and the aliases.
|
|
1621
|
+
* @param file - The flow file.
|
|
1622
|
+
* @param node - The key of the entry, `main/home`.
|
|
1623
|
+
* @param ref - The name the entry holds.
|
|
1624
|
+
* @returns The reason for the unresolved list.
|
|
1625
|
+
*/
|
|
1626
|
+
function unfollowed(resolution, file, node, ref) {
|
|
1627
|
+
const alias = missedAlias(resolution, file, ref);
|
|
1628
|
+
return alias === void 0 ? `node "${node}": "${ref}" is not declared in a file of the root` : `node "${node}": "${ref}" is imported from "${alias}", which names no file of the root`;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1124
1631
|
* Adds the `node:` keys of one flow table. A plain name is followed to the file that declares
|
|
1125
1632
|
* it, and the table is its use; a slot or an inline node is anchored at the table. A name that
|
|
1126
1633
|
* cannot be followed is anchored at the table and listed as unresolved.
|
|
@@ -1136,13 +1643,17 @@ function addTable(assembly, symbols, unresolved, file, table) {
|
|
|
1136
1643
|
path: file,
|
|
1137
1644
|
binding: table.binding
|
|
1138
1645
|
};
|
|
1646
|
+
const resolution = {
|
|
1647
|
+
modules: assembly.modules,
|
|
1648
|
+
aliases: assembly.aliases
|
|
1649
|
+
};
|
|
1139
1650
|
for (const { name, ref } of table.entries) {
|
|
1140
1651
|
const entry = entryOf(symbols, `${NODE}${table.flow}/${name}`);
|
|
1141
1652
|
const atTable = {
|
|
1142
1653
|
...use,
|
|
1143
1654
|
key: name
|
|
1144
1655
|
};
|
|
1145
|
-
const target = ref === void 0 ? void 0 : resolveBinding(
|
|
1656
|
+
const target = ref === void 0 ? void 0 : resolveBinding(resolution, file, ref);
|
|
1146
1657
|
if (target?.kind === "declared") {
|
|
1147
1658
|
entry.def.push({
|
|
1148
1659
|
path: target.path,
|
|
@@ -1152,13 +1663,10 @@ function addTable(assembly, symbols, unresolved, file, table) {
|
|
|
1152
1663
|
continue;
|
|
1153
1664
|
}
|
|
1154
1665
|
entry.def.push(atTable);
|
|
1155
|
-
if (ref !== void 0) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
reason
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1666
|
+
if (ref !== void 0) unresolved.push({
|
|
1667
|
+
path: file,
|
|
1668
|
+
reason: unfollowed(resolution, file, `${table.flow}/${name}`, ref)
|
|
1669
|
+
});
|
|
1162
1670
|
}
|
|
1163
1671
|
}
|
|
1164
1672
|
/**
|
|
@@ -1168,8 +1676,12 @@ function addTable(assembly, symbols, unresolved, file, table) {
|
|
|
1168
1676
|
* @param symbols - The keys so far.
|
|
1169
1677
|
*/
|
|
1170
1678
|
function addStyleUses(assembly, symbols) {
|
|
1679
|
+
const resolution = {
|
|
1680
|
+
modules: assembly.modules,
|
|
1681
|
+
aliases: assembly.aliases
|
|
1682
|
+
};
|
|
1171
1683
|
for (const [file, record] of assembly.files) for (const [local, ref] of record.good?.module.imports ?? []) {
|
|
1172
|
-
const target = ref.imported === "*" ? void 0 : resolveBinding(
|
|
1684
|
+
const target = ref.imported === "*" ? void 0 : resolveBinding(resolution, file, local);
|
|
1173
1685
|
(target?.kind === "declared" ? symbols.get(`style:${target.path}#${target.binding}`) : void 0)?.uses.push({
|
|
1174
1686
|
path: file,
|
|
1175
1687
|
binding: local
|
|
@@ -1258,9 +1770,9 @@ function deepFreeze(value) {
|
|
|
1258
1770
|
return value;
|
|
1259
1771
|
}
|
|
1260
1772
|
/**
|
|
1261
|
-
* Assembles the index of a catalog.
|
|
1773
|
+
* Assembles the index of a catalog. The index names the tsconfig when the assembly has aliases.
|
|
1262
1774
|
*
|
|
1263
|
-
* @param assembly - The records sorted by path,
|
|
1775
|
+
* @param assembly - The records sorted by path, the module facts of the good files, the aliases.
|
|
1264
1776
|
* @param manifest - The root-relative manifest path when the file exists.
|
|
1265
1777
|
* @returns The index, frozen.
|
|
1266
1778
|
*/
|
|
@@ -1292,6 +1804,7 @@ function assembleIndex(assembly, manifest) {
|
|
|
1292
1804
|
schemaVersion: 1,
|
|
1293
1805
|
revision: revisionOf(files),
|
|
1294
1806
|
...manifest === void 0 ? {} : { manifest },
|
|
1807
|
+
...assembly.aliases === void 0 ? {} : { tsconfig: assembly.aliases.file },
|
|
1295
1808
|
symbols: finishSymbols(symbols),
|
|
1296
1809
|
files,
|
|
1297
1810
|
unresolved
|
|
@@ -1736,13 +2249,6 @@ function parseFile(typescript, path, text) {
|
|
|
1736
2249
|
}
|
|
1737
2250
|
//#endregion
|
|
1738
2251
|
//#region src/project/catalog.ts
|
|
1739
|
-
/**
|
|
1740
|
-
* @file project — the catalog: one record per indexed file and a cache of parses. A file is
|
|
1741
|
-
* parsed once per version of its bytes; a file that stops parsing keeps its last good parse, so
|
|
1742
|
-
* its keys stay in the index while it is broken. Building the index reads the module facts of
|
|
1743
|
-
* every good file, re-extracts only the files whose bytes or reachable definers changed, and
|
|
1744
|
-
* assembles the result.
|
|
1745
|
-
*/
|
|
1746
2252
|
/** How many parses the cache keeps; the oldest goes first. */
|
|
1747
2253
|
const PARSE_CACHE_SIZE = 512;
|
|
1748
2254
|
/**
|
|
@@ -1828,16 +2334,17 @@ function dropFile(catalog, path) {
|
|
|
1828
2334
|
}
|
|
1829
2335
|
/**
|
|
1830
2336
|
* Brings the keys of every good file up to date: a file is read again when its good version or
|
|
1831
|
-
* the definers it reaches changed since it was last read.
|
|
2337
|
+
* the definers it reaches changed since it was last read. An alias change that moves the
|
|
2338
|
+
* definers of a file moves its stamp, so a tsconfig edit re-reads exactly the files it affects.
|
|
1832
2339
|
*
|
|
1833
2340
|
* @param catalog - The catalog.
|
|
1834
|
-
* @param
|
|
2341
|
+
* @param resolution - The module facts of every good file, and the aliases.
|
|
1835
2342
|
*/
|
|
1836
|
-
function refreshExtractions(catalog,
|
|
2343
|
+
function refreshExtractions(catalog, resolution) {
|
|
1837
2344
|
for (const [path, record] of catalog.records) {
|
|
1838
2345
|
const good = record.good;
|
|
1839
2346
|
if (good === void 0) continue;
|
|
1840
|
-
const definers = definersOf(
|
|
2347
|
+
const definers = definersOf(resolution, path);
|
|
1841
2348
|
const signature = [...definers].map(([callee, definer]) => `${callee}=${definer}`).join(";");
|
|
1842
2349
|
const stamp = `${good.hash}|${signature}`;
|
|
1843
2350
|
if (record.extracted?.definers === stamp) continue;
|
|
@@ -1853,16 +2360,21 @@ function refreshExtractions(catalog, modules) {
|
|
|
1853
2360
|
*
|
|
1854
2361
|
* @param catalog - The catalog.
|
|
1855
2362
|
* @param manifest - The root-relative manifest path when the file exists.
|
|
2363
|
+
* @param aliases - The tsconfig aliases when the game declares some.
|
|
1856
2364
|
* @returns The index, frozen.
|
|
1857
2365
|
*/
|
|
1858
|
-
function buildIndex(catalog, manifest) {
|
|
2366
|
+
function buildIndex(catalog, manifest, aliases) {
|
|
1859
2367
|
const files = [...catalog.records].toSorted(([first], [second]) => first < second ? -1 : 1);
|
|
1860
2368
|
const modules = /* @__PURE__ */ new Map();
|
|
1861
2369
|
for (const [path, record] of files) if (record.good !== void 0) modules.set(path, record.good.module);
|
|
1862
|
-
refreshExtractions(catalog,
|
|
2370
|
+
refreshExtractions(catalog, {
|
|
2371
|
+
modules,
|
|
2372
|
+
aliases
|
|
2373
|
+
});
|
|
1863
2374
|
return assembleIndex({
|
|
1864
2375
|
files,
|
|
1865
|
-
modules
|
|
2376
|
+
modules,
|
|
2377
|
+
aliases
|
|
1866
2378
|
}, manifest);
|
|
1867
2379
|
}
|
|
1868
2380
|
//#endregion
|
|
@@ -1977,183 +2489,33 @@ function locateFactory(typescript, source, anchor, context) {
|
|
|
1977
2489
|
if (anchor.key !== void 0) {
|
|
1978
2490
|
const property = propertyIn(typescript, factory.node, anchor.key);
|
|
1979
2491
|
if (property === void 0 || !typescript.isPropertyAssignment(property)) return [];
|
|
1980
|
-
return [placeOf(source, property, unwrap(typescript, property.initializer))];
|
|
1981
|
-
}
|
|
1982
|
-
const definers = context.definers?.() ?? /* @__PURE__ */ new Map();
|
|
1983
|
-
return styleCallsIn(typescript, factory.node, definers).map((call) => placeOf(source, call, call));
|
|
1984
|
-
}
|
|
1985
|
-
/**
|
|
1986
|
-
* Reads an anchor on one parse of its file.
|
|
1987
|
-
*
|
|
1988
|
-
* @param typescript - The TypeScript module.
|
|
1989
|
-
* @param source - The parsed file.
|
|
1990
|
-
* @param anchor - The anchor.
|
|
1991
|
-
* @param context - The kind of its key, the JSX hits of the parse and the definers of the file.
|
|
1992
|
-
* @returns Every place of the anchor in the file; none when the file no longer holds it.
|
|
1993
|
-
*/
|
|
1994
|
-
function locate(typescript, source, anchor, context = {}) {
|
|
1995
|
-
if (anchor.kind !== void 0) return (context.jsxHits?.() ?? collectJsx(typescript, source)).filter((hit) => hit.key === anchor.key && hit.kind === anchor.kind && hit.component === anchor.component && hit.prop === anchor.prop).map((hit) => placeOf(source, hit.attribute, hit.element));
|
|
1996
|
-
if (anchor.binding === void 0) {
|
|
1997
|
-
const place = anchor.key === void 0 ? void 0 : writtenAt(typescript, source, anchor.key);
|
|
1998
|
-
return place === void 0 ? [] : [place];
|
|
1999
|
-
}
|
|
2000
|
-
const built = context.keyKind === STYLE_KIND ? locateFactory(typescript, source, anchor, context) : [];
|
|
2001
|
-
if (built.length > 0) return built;
|
|
2002
|
-
const binding = anchor.binding;
|
|
2003
|
-
const statement = source.statements.find((item) => declares(typescript, item, binding));
|
|
2004
|
-
if (statement === void 0) return [];
|
|
2005
|
-
const node = (anchor.key === void 0 ? void 0 : propertyIn(typescript, statement, anchor.key)) ?? statement;
|
|
2006
|
-
return [placeOf(source, node, node)];
|
|
2007
|
-
}
|
|
2008
|
-
//#endregion
|
|
2009
|
-
//#region src/project/paths.ts
|
|
2010
|
-
/**
|
|
2011
|
-
* @file project — which files the index reads, and the guard on every path it reads. Paths in and
|
|
2012
|
-
* out are root-relative POSIX. A read resolves the path against the real root with `realpath`, so
|
|
2013
|
-
* a `..` or a symlink that leaves the root is refused by name instead of read.
|
|
2014
|
-
*/
|
|
2015
|
-
/** The folders a game never edits by hand: dependencies, builds, generated code and tests. */
|
|
2016
|
-
const SKIPPED_FOLDER = /^(?:node_modules|dist|generated|\.moku|\.git|__tests__|tests)$/;
|
|
2017
|
-
/** The source files the index reads. */
|
|
2018
|
-
const SOURCE_FILE = /\.tsx?$/;
|
|
2019
|
-
/** Test files, skipped wherever they sit. */
|
|
2020
|
-
const TEST_FILE = /\.(?:test|spec)\.tsx?$/;
|
|
2021
|
-
/** The hint under every root error. */
|
|
2022
|
-
const ROOT_HINT = "Pass the folder of the game as root.";
|
|
2023
|
-
/**
|
|
2024
|
-
* Whether a folder name is one the index never walks into.
|
|
2025
|
-
*
|
|
2026
|
-
* @param name - One folder name.
|
|
2027
|
-
* @returns True for `node_modules`, `dist`, `generated`, `.moku`, `.git`, `__tests__` and `tests`.
|
|
2028
|
-
* @example
|
|
2029
|
-
* ```ts
|
|
2030
|
-
* isSkippedFolder("generated"); // true
|
|
2031
|
-
* ```
|
|
2032
|
-
*/
|
|
2033
|
-
function isSkippedFolder(name) {
|
|
2034
|
-
return SKIPPED_FOLDER.test(name);
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* Whether a root-relative path sits under a skipped folder.
|
|
2038
|
-
*
|
|
2039
|
-
* @param file - A path with `/` or `\` separators.
|
|
2040
|
-
* @returns True when one of its folders is skipped.
|
|
2041
|
-
* @example
|
|
2042
|
-
* ```ts
|
|
2043
|
-
* isSkippedPath("node_modules/pixi.js/index.ts"); // true
|
|
2044
|
-
* ```
|
|
2045
|
-
*/
|
|
2046
|
-
function isSkippedPath(file) {
|
|
2047
|
-
return file.split(/[/\\]/).slice(0, -1).some((folder) => isSkippedFolder(folder));
|
|
2048
|
-
}
|
|
2049
|
-
/**
|
|
2050
|
-
* Whether the index reads a root-relative path: a `.ts` or `.tsx` file outside the skipped
|
|
2051
|
-
* folders that is not a test.
|
|
2052
|
-
*
|
|
2053
|
-
* @param file - A root-relative POSIX path.
|
|
2054
|
-
* @returns True for a source file of the index.
|
|
2055
|
-
* @example
|
|
2056
|
-
* ```ts
|
|
2057
|
-
* isIndexedFile("features/ui/kit.tsx"); // true
|
|
2058
|
-
* ```
|
|
2059
|
-
*/
|
|
2060
|
-
function isIndexedFile(file) {
|
|
2061
|
-
return SOURCE_FILE.test(file) && !TEST_FILE.test(file) && !isSkippedPath(file);
|
|
2062
|
-
}
|
|
2063
|
-
/**
|
|
2064
|
-
* Turns a path into its root-relative POSIX form.
|
|
2065
|
-
*
|
|
2066
|
-
* @param file - A path with any separators.
|
|
2067
|
-
* @returns The path with `/` separators, `.` segments resolved.
|
|
2068
|
-
* @example
|
|
2069
|
-
* ```ts
|
|
2070
|
-
* toPosix("./features\\ui\\kit.tsx"); // "features/ui/kit.tsx"
|
|
2071
|
-
* ```
|
|
2072
|
-
*/
|
|
2073
|
-
function toPosix(file) {
|
|
2074
|
-
return path.posix.normalize(file.replaceAll("\\", "/"));
|
|
2075
|
-
}
|
|
2076
|
-
/**
|
|
2077
|
-
* Whether a file system error says nothing is at the path.
|
|
2078
|
-
*
|
|
2079
|
-
* @param error - What was thrown.
|
|
2080
|
-
* @returns True for `ENOENT` and `ENOTDIR`.
|
|
2081
|
-
*/
|
|
2082
|
-
function isMissing(error) {
|
|
2083
|
-
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
2084
|
-
return code === "ENOENT" || code === "ENOTDIR";
|
|
2085
|
-
}
|
|
2086
|
-
/**
|
|
2087
|
-
* Whether an absolute path lies inside a folder.
|
|
2088
|
-
*
|
|
2089
|
-
* @param folder - The absolute folder.
|
|
2090
|
-
* @param file - The absolute path.
|
|
2091
|
-
* @returns True for the folder itself and anything below it.
|
|
2092
|
-
*/
|
|
2093
|
-
function isInside(folder, file) {
|
|
2094
|
-
const relative = path.relative(folder, file);
|
|
2095
|
-
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
2096
|
-
}
|
|
2097
|
-
/**
|
|
2098
|
-
* The error of a path that leaves the root.
|
|
2099
|
-
*
|
|
2100
|
-
* @param file - The path as it was given.
|
|
2101
|
-
* @param root - The real root.
|
|
2102
|
-
* @returns The error to throw.
|
|
2103
|
-
*/
|
|
2104
|
-
function leavesRoot(file, root) {
|
|
2105
|
-
return /* @__PURE__ */ new Error(`[game] The path "${file}" leaves the project root.\n Pass a path inside "${root}", relative to it.`);
|
|
2106
|
-
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Checks the root of a project and resolves it: it exists, it is a directory, and its real path
|
|
2109
|
-
* is the one every read is held to.
|
|
2110
|
-
*
|
|
2111
|
-
* @param root - The root as given, absolute or relative to the working directory.
|
|
2112
|
-
* @returns The real absolute path of the root.
|
|
2113
|
-
* @throws {Error} When the root does not exist or is not a directory.
|
|
2114
|
-
*/
|
|
2115
|
-
async function openRoot(root) {
|
|
2116
|
-
const absolute = path.resolve(root);
|
|
2117
|
-
if (!(await stat(absolute).catch((error) => {
|
|
2118
|
-
if (!isMissing(error)) throw error;
|
|
2119
|
-
throw new Error(`[game] The project root "${absolute}" does not exist.\n ${ROOT_HINT}`);
|
|
2120
|
-
})).isDirectory()) throw new Error(`[game] The project root "${absolute}" is not a directory.\n ${ROOT_HINT}`);
|
|
2121
|
-
return realpath(absolute);
|
|
2122
|
-
}
|
|
2123
|
-
/**
|
|
2124
|
-
* Resolves a root-relative path to the real file, refusing any path that leaves the root.
|
|
2125
|
-
*
|
|
2126
|
-
* @param root - The real root.
|
|
2127
|
-
* @param file - The root-relative path.
|
|
2128
|
-
* @returns The real absolute path, or `undefined` when nothing is there.
|
|
2129
|
-
* @throws {Error} When the path leaves the root, by `..`, as an absolute path or by a symlink.
|
|
2130
|
-
*/
|
|
2131
|
-
async function resolveInside(root, file) {
|
|
2132
|
-
const absolute = path.resolve(root, file);
|
|
2133
|
-
if (!isInside(root, absolute)) throw leavesRoot(file, root);
|
|
2134
|
-
let real;
|
|
2135
|
-
try {
|
|
2136
|
-
real = await realpath(absolute);
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
if (isMissing(error)) return;
|
|
2139
|
-
throw error;
|
|
2492
|
+
return [placeOf(source, property, unwrap(typescript, property.initializer))];
|
|
2140
2493
|
}
|
|
2141
|
-
|
|
2142
|
-
return
|
|
2494
|
+
const definers = context.definers?.() ?? /* @__PURE__ */ new Map();
|
|
2495
|
+
return styleCallsIn(typescript, factory.node, definers).map((call) => placeOf(source, call, call));
|
|
2143
2496
|
}
|
|
2144
2497
|
/**
|
|
2145
|
-
* Reads
|
|
2146
|
-
* nothing here; `changed` refuses it by name with `resolveInside` first.
|
|
2498
|
+
* Reads an anchor on one parse of its file.
|
|
2147
2499
|
*
|
|
2148
|
-
* @param
|
|
2149
|
-
* @param
|
|
2150
|
-
* @
|
|
2500
|
+
* @param typescript - The TypeScript module.
|
|
2501
|
+
* @param source - The parsed file.
|
|
2502
|
+
* @param anchor - The anchor.
|
|
2503
|
+
* @param context - The kind of its key, the JSX hits of the parse and the definers of the file.
|
|
2504
|
+
* @returns Every place of the anchor in the file; none when the file no longer holds it.
|
|
2151
2505
|
*/
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2506
|
+
function locate(typescript, source, anchor, context = {}) {
|
|
2507
|
+
if (anchor.kind !== void 0) return (context.jsxHits?.() ?? collectJsx(typescript, source)).filter((hit) => hit.key === anchor.key && hit.kind === anchor.kind && hit.component === anchor.component && hit.prop === anchor.prop).map((hit) => placeOf(source, hit.attribute, hit.element));
|
|
2508
|
+
if (anchor.binding === void 0) {
|
|
2509
|
+
const place = anchor.key === void 0 ? void 0 : writtenAt(typescript, source, anchor.key);
|
|
2510
|
+
return place === void 0 ? [] : [place];
|
|
2511
|
+
}
|
|
2512
|
+
const built = context.keyKind === STYLE_KIND ? locateFactory(typescript, source, anchor, context) : [];
|
|
2513
|
+
if (built.length > 0) return built;
|
|
2514
|
+
const binding = anchor.binding;
|
|
2515
|
+
const statement = source.statements.find((item) => declares(typescript, item, binding));
|
|
2516
|
+
if (statement === void 0) return [];
|
|
2517
|
+
const node = (anchor.key === void 0 ? void 0 : propertyIn(typescript, statement, anchor.key)) ?? statement;
|
|
2518
|
+
return [placeOf(source, node, node)];
|
|
2157
2519
|
}
|
|
2158
2520
|
//#endregion
|
|
2159
2521
|
//#region src/project/find.ts
|
|
@@ -2302,17 +2664,20 @@ async function viewOf(session, file) {
|
|
|
2302
2664
|
};
|
|
2303
2665
|
}
|
|
2304
2666
|
/**
|
|
2305
|
-
* The definers a file can call, recognised by binding on the module facts of the catalog
|
|
2306
|
-
* extraction does.
|
|
2667
|
+
* The definers a file can call, recognised by binding on the module facts of the catalog and the
|
|
2668
|
+
* tsconfig aliases of the session, as extraction does.
|
|
2307
2669
|
*
|
|
2308
|
-
* @param
|
|
2670
|
+
* @param session - The open project.
|
|
2309
2671
|
* @param file - The root-relative path.
|
|
2310
2672
|
* @returns The callee text to the definer.
|
|
2311
2673
|
*/
|
|
2312
|
-
function definersAt(
|
|
2674
|
+
function definersAt(session, file) {
|
|
2313
2675
|
const modules = /* @__PURE__ */ new Map();
|
|
2314
|
-
for (const [path, record] of catalog.records) if (record.good !== void 0) modules.set(path, record.good.module);
|
|
2315
|
-
return definersOf(
|
|
2676
|
+
for (const [path, record] of session.catalog.records) if (record.good !== void 0) modules.set(path, record.good.module);
|
|
2677
|
+
return definersOf({
|
|
2678
|
+
modules,
|
|
2679
|
+
aliases: session.aliases
|
|
2680
|
+
}, file);
|
|
2316
2681
|
}
|
|
2317
2682
|
/**
|
|
2318
2683
|
* The kind of a key: the text before its first colon.
|
|
@@ -2361,7 +2726,7 @@ async function findKey(session, key) {
|
|
|
2361
2726
|
const answers = locate(session.catalog.typescript, view.source, anchor, {
|
|
2362
2727
|
keyKind,
|
|
2363
2728
|
jsxHits: view.jsx,
|
|
2364
|
-
definers: () => definersAt(session
|
|
2729
|
+
definers: () => definersAt(session, anchor.path)
|
|
2365
2730
|
}).map((place) => ({
|
|
2366
2731
|
...anchor,
|
|
2367
2732
|
...place,
|
|
@@ -2517,13 +2882,16 @@ async function walkRoot(root) {
|
|
|
2517
2882
|
//#region src/project/session.ts
|
|
2518
2883
|
/**
|
|
2519
2884
|
* @file project — an open project: the real root, the catalog of its files, the stamps of the last
|
|
2520
|
-
* walk, the
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2523
|
-
*
|
|
2885
|
+
* walk, the tsconfig aliases with the stamps of the config files they came from, the current index
|
|
2886
|
+
* and a queue that runs one update at a time. `syncWithDisk` is the batch walk of a watch batch:
|
|
2887
|
+
* it walks the root, hashes the files whose stamp moved and every new one, drops the vanished
|
|
2888
|
+
* ones, reads the aliases again when a config file moved, and rebuilds the index when bytes or
|
|
2889
|
+
* aliases changed. `reindexPath` reads one path now for `changed`.
|
|
2524
2890
|
*/
|
|
2525
2891
|
/** The manifest the asset scanner writes by default. */
|
|
2526
2892
|
const MANIFEST_FILE = "manifest.json";
|
|
2893
|
+
/** The tsconfig whose `paths` are read by default. */
|
|
2894
|
+
const TSCONFIG_FILE = "tsconfig.json";
|
|
2527
2895
|
/**
|
|
2528
2896
|
* The manifest path to report, when the file exists.
|
|
2529
2897
|
*
|
|
@@ -2537,6 +2905,93 @@ async function presentManifest(root, manifest) {
|
|
|
2537
2905
|
} catch {}
|
|
2538
2906
|
}
|
|
2539
2907
|
/**
|
|
2908
|
+
* The stamp of a config file, read through the guard.
|
|
2909
|
+
*
|
|
2910
|
+
* @param root - The real root.
|
|
2911
|
+
* @param file - The root-relative path.
|
|
2912
|
+
* @returns The stamp, or `undefined` when nothing is there or the path left the root.
|
|
2913
|
+
*/
|
|
2914
|
+
async function configStampOf(root, file) {
|
|
2915
|
+
try {
|
|
2916
|
+
const real = await resolveInside(root, file);
|
|
2917
|
+
return real === void 0 ? void 0 : await stampOf(real);
|
|
2918
|
+
} catch {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* The stamps of config files.
|
|
2924
|
+
*
|
|
2925
|
+
* @param root - The real root.
|
|
2926
|
+
* @param files - The root-relative config files.
|
|
2927
|
+
* @returns Path to stamp, `undefined` for a file that is not there.
|
|
2928
|
+
*/
|
|
2929
|
+
async function stampConfigs(root, files) {
|
|
2930
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
2931
|
+
for (const file of files) stamps.set(file, await configStampOf(root, file));
|
|
2932
|
+
return stamps;
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* The config files a read of the tsconfig touched: its sources, or the tsconfig alone when it is
|
|
2936
|
+
* missing.
|
|
2937
|
+
*
|
|
2938
|
+
* @param tsconfig - The root-relative tsconfig path.
|
|
2939
|
+
* @param read - The read, `undefined` for a missing tsconfig.
|
|
2940
|
+
* @returns The root-relative files to stamp.
|
|
2941
|
+
* @example
|
|
2942
|
+
* ```ts
|
|
2943
|
+
* configsOf("tsconfig.json", undefined); // ["tsconfig.json"]
|
|
2944
|
+
* ```
|
|
2945
|
+
*/
|
|
2946
|
+
function configsOf(tsconfig, read) {
|
|
2947
|
+
return read?.sources ?? [tsconfig];
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* The aliases the index follows: a tsconfig without patterns maps nothing, as no tsconfig does.
|
|
2951
|
+
*
|
|
2952
|
+
* @param read - The read, `undefined` for a missing tsconfig.
|
|
2953
|
+
* @returns The read when it holds a pattern, else `undefined`.
|
|
2954
|
+
* @example
|
|
2955
|
+
* ```ts
|
|
2956
|
+
* followedAliases({ file: "tsconfig.json", sources: ["tsconfig.json"], patterns: [] }); // undefined
|
|
2957
|
+
* ```
|
|
2958
|
+
*/
|
|
2959
|
+
function followedAliases(read) {
|
|
2960
|
+
return read === void 0 || read.patterns.length === 0 ? void 0 : read;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Whether a config file moved, appeared or vanished since its stamp.
|
|
2964
|
+
*
|
|
2965
|
+
* @param session - The project.
|
|
2966
|
+
* @returns True when a stamp of `configStamps` is not the one on disk now.
|
|
2967
|
+
*/
|
|
2968
|
+
async function configsMoved(session) {
|
|
2969
|
+
for (const [file, known] of session.configStamps) {
|
|
2970
|
+
const now = await configStampOf(session.root, file);
|
|
2971
|
+
if (known === void 0 || now === void 0 ? known !== now : !sameStamp(known, now)) return true;
|
|
2972
|
+
}
|
|
2973
|
+
return false;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Reads the aliases again and stamps the config files the read touched. A tsconfig that stops
|
|
2977
|
+
* parsing keeps the aliases it had until it parses again, as a broken source file keeps its keys,
|
|
2978
|
+
* and the files of the last good read are stamped again.
|
|
2979
|
+
*
|
|
2980
|
+
* @param session - The project.
|
|
2981
|
+
* @returns True when the aliases resolve differently now.
|
|
2982
|
+
*/
|
|
2983
|
+
async function rereadAliases(session) {
|
|
2984
|
+
const before = session.aliases;
|
|
2985
|
+
let configs = [...session.configStamps.keys()];
|
|
2986
|
+
try {
|
|
2987
|
+
const read = await readAliases(session.catalog.typescript, session.root, session.tsconfig);
|
|
2988
|
+
session.aliases = followedAliases(read);
|
|
2989
|
+
configs = configsOf(session.tsconfig, read);
|
|
2990
|
+
} catch {}
|
|
2991
|
+
session.configStamps = await stampConfigs(session.root, configs);
|
|
2992
|
+
return !sameAliases(before, session.aliases);
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2540
2995
|
* Reads one file into the catalog, or drops it when it is gone or cannot be read.
|
|
2541
2996
|
*
|
|
2542
2997
|
* @param session - The project.
|
|
@@ -2561,37 +3016,44 @@ async function readIntoCatalog(session, file) {
|
|
|
2561
3016
|
async function rebuild(session, files) {
|
|
2562
3017
|
const before = session.index;
|
|
2563
3018
|
const manifest = await presentManifest(session.root, session.manifest);
|
|
2564
|
-
session.index = buildIndex(session.catalog, manifest);
|
|
3019
|
+
session.index = buildIndex(session.catalog, manifest, session.aliases);
|
|
2565
3020
|
return diffIndexes(before, session.index, files);
|
|
2566
3021
|
}
|
|
2567
3022
|
/**
|
|
2568
|
-
* Opens a project: checks the root, loads TypeScript, reads
|
|
2569
|
-
* index.
|
|
3023
|
+
* Opens a project: checks the root, loads TypeScript, reads the tsconfig aliases and every source
|
|
3024
|
+
* file, and builds the index.
|
|
2570
3025
|
*
|
|
2571
|
-
* @param options - The root, the manifest and the debounce.
|
|
3026
|
+
* @param options - The root, the manifest, the tsconfig and the debounce.
|
|
2572
3027
|
* @returns The open project.
|
|
2573
|
-
* @throws {Error} When the root is missing or not a directory, the manifest
|
|
2574
|
-
* or TypeScript is not installed.
|
|
3028
|
+
* @throws {Error} When the root is missing or not a directory, the manifest or the tsconfig path
|
|
3029
|
+
* leaves the root, the tsconfig does not parse, or TypeScript is not installed.
|
|
2575
3030
|
*/
|
|
2576
3031
|
async function openSession(options) {
|
|
2577
3032
|
const root = await openRoot(options.root);
|
|
2578
3033
|
const typescript = await loadTypeScript();
|
|
2579
3034
|
const manifest = toPosix(options.manifest ?? MANIFEST_FILE);
|
|
3035
|
+
const tsconfig = toPosix(options.tsconfig ?? TSCONFIG_FILE);
|
|
2580
3036
|
await resolveInside(root, manifest);
|
|
3037
|
+
await resolveInside(root, tsconfig);
|
|
3038
|
+
const read = await readAliases(typescript, root, tsconfig);
|
|
3039
|
+
const aliases = followedAliases(read);
|
|
2581
3040
|
const walk = await walkRoot(root);
|
|
2582
3041
|
const catalog = createCatalog(typescript);
|
|
2583
3042
|
const session = {
|
|
2584
3043
|
root,
|
|
2585
3044
|
manifest,
|
|
3045
|
+
tsconfig,
|
|
3046
|
+
aliases,
|
|
3047
|
+
configStamps: await stampConfigs(root, configsOf(tsconfig, read)),
|
|
2586
3048
|
catalog,
|
|
2587
3049
|
stamps: walk.files,
|
|
2588
3050
|
folders: walk.folders,
|
|
2589
|
-
index: buildIndex(catalog),
|
|
3051
|
+
index: buildIndex(catalog, void 0, aliases),
|
|
2590
3052
|
shapes: /* @__PURE__ */ new WeakMap(),
|
|
2591
3053
|
queue: Promise.resolve()
|
|
2592
3054
|
};
|
|
2593
3055
|
for (const file of [...walk.files.keys()].toSorted()) await readIntoCatalog(session, file);
|
|
2594
|
-
session.index = buildIndex(catalog, await presentManifest(root, manifest));
|
|
3056
|
+
session.index = buildIndex(catalog, await presentManifest(root, manifest), aliases);
|
|
2595
3057
|
return session;
|
|
2596
3058
|
}
|
|
2597
3059
|
/**
|
|
@@ -2607,47 +3069,282 @@ function serialize(session, task) {
|
|
|
2607
3069
|
return result;
|
|
2608
3070
|
}
|
|
2609
3071
|
/**
|
|
2610
|
-
* The batch walk: walks the root, hashes each file whose stamp moved and each new one, drops the
|
|
2611
|
-
* vanished ones,
|
|
2612
|
-
*
|
|
3072
|
+
* The batch walk: walks the root, hashes each file whose stamp moved and each new one, drops the
|
|
3073
|
+
* vanished ones, reads the aliases again when a config file moved, appeared or vanished, and
|
|
3074
|
+
* rebuilds the index when bytes or aliases changed. A save with the same bytes, or a tsconfig
|
|
3075
|
+
* edit that leaves the aliases as they were, changes nothing. New aliases list the tsconfig
|
|
3076
|
+
* among the changed files.
|
|
3077
|
+
*
|
|
3078
|
+
* @param session - The project.
|
|
3079
|
+
* @returns The change, or `undefined` when the index stayed the same.
|
|
3080
|
+
*/
|
|
3081
|
+
async function syncWithDisk(session) {
|
|
3082
|
+
const walk = await walkRoot(session.root);
|
|
3083
|
+
const files = [];
|
|
3084
|
+
for (const file of session.catalog.records.keys()) if (!walk.files.has(file) && dropFile(session.catalog, file)) files.push(file);
|
|
3085
|
+
const previous = session.stamps;
|
|
3086
|
+
session.stamps = walk.files;
|
|
3087
|
+
session.folders = walk.folders;
|
|
3088
|
+
for (const [file, stamp] of walk.files) {
|
|
3089
|
+
const known = previous.get(file);
|
|
3090
|
+
if (known !== void 0 && sameStamp(known, stamp)) continue;
|
|
3091
|
+
if (await readIntoCatalog(session, file)) files.push(file);
|
|
3092
|
+
}
|
|
3093
|
+
if (await configsMoved(session) && await rereadAliases(session)) files.push(session.tsconfig);
|
|
3094
|
+
const manifest = await presentManifest(session.root, session.manifest);
|
|
3095
|
+
if (files.length === 0 && manifest === session.index.manifest) return void 0;
|
|
3096
|
+
return rebuild(session, files);
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Reads one root-relative path now: a changed or new source file is parsed, a deleted one is
|
|
3100
|
+
* dropped, the tsconfig or a file it extends reads the aliases again, and anything else changes
|
|
3101
|
+
* nothing.
|
|
3102
|
+
*
|
|
3103
|
+
* @param session - The project.
|
|
3104
|
+
* @param file - The root-relative path.
|
|
3105
|
+
* @returns The index after the read.
|
|
3106
|
+
* @throws {Error} When the path leaves the root.
|
|
3107
|
+
*/
|
|
3108
|
+
async function reindexPath(session, file) {
|
|
3109
|
+
const relative = toPosix(file);
|
|
3110
|
+
const real = await resolveInside(session.root, relative);
|
|
3111
|
+
if (path.isAbsolute(relative)) return session.index;
|
|
3112
|
+
if (session.configStamps.has(relative)) {
|
|
3113
|
+
if (await rereadAliases(session)) await rebuild(session, [session.tsconfig]);
|
|
3114
|
+
return session.index;
|
|
3115
|
+
}
|
|
3116
|
+
if (real !== void 0 && real !== path.join(session.root, relative) || !isIndexedFile(relative)) return session.index;
|
|
3117
|
+
const stamp = real === void 0 ? void 0 : await stampOf(real);
|
|
3118
|
+
if (stamp !== void 0) session.stamps.set(relative, stamp);
|
|
3119
|
+
if (!await readIntoCatalog(session, relative)) return session.index;
|
|
3120
|
+
await rebuild(session, [relative]);
|
|
3121
|
+
return session.index;
|
|
3122
|
+
}
|
|
3123
|
+
//#endregion
|
|
3124
|
+
//#region src/project/cli.ts
|
|
3125
|
+
/**
|
|
3126
|
+
* @file project — the command line `moku-game-index`. It opens the project of `--root` and runs one
|
|
3127
|
+
* command: `--json` prints the index, `where <key>` prints one `path:line` per place, `--check`
|
|
3128
|
+
* reports broken files and conflicts (exit 1), names the tsconfig aliases and lists the
|
|
3129
|
+
* unresolved items as info. A run answers once and watches nothing, so it opens a session, not a
|
|
3130
|
+
* handle. Output goes through the branded console of `@moku-labs/common`; `runCli` returns the
|
|
3131
|
+
* exit code instead of taking it.
|
|
3132
|
+
*/
|
|
3133
|
+
/** The usage line under every flag error. */
|
|
3134
|
+
const USAGE = "Run: moku-game-index --root <dir> [--manifest <path>] [--tsconfig <path>] --json | --check | where <key>.";
|
|
3135
|
+
/** Pretty JSON indent of `--json`. */
|
|
3136
|
+
const JSON_INDENT = 2;
|
|
3137
|
+
/**
|
|
3138
|
+
* Wraps a command line problem in the message shape of the framework.
|
|
3139
|
+
*
|
|
3140
|
+
* @param message - One sentence naming the flag.
|
|
3141
|
+
* @returns The error to throw.
|
|
3142
|
+
*/
|
|
3143
|
+
function problem(message) {
|
|
3144
|
+
return /* @__PURE__ */ new Error(`[game] index: ${message}\n ${USAGE}`);
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Reads the message of anything that was thrown.
|
|
3148
|
+
*
|
|
3149
|
+
* @param failure - What the `catch` caught.
|
|
3150
|
+
* @returns The message.
|
|
3151
|
+
*/
|
|
3152
|
+
function messageOf(failure) {
|
|
3153
|
+
return failure instanceof Error ? failure.message : String(failure);
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* The value after a flag that takes one.
|
|
3157
|
+
*
|
|
3158
|
+
* @param argv - The arguments.
|
|
3159
|
+
* @param at - The index of the flag.
|
|
3160
|
+
* @param what - What the flag needs, for the error.
|
|
3161
|
+
* @returns The value.
|
|
3162
|
+
* @throws {Error} When the value is missing.
|
|
3163
|
+
*/
|
|
3164
|
+
function valueAfter(argv, at, what) {
|
|
3165
|
+
const flag = argv[at] ?? "";
|
|
3166
|
+
const value = argv[at + 1];
|
|
3167
|
+
if (value === void 0 || value.startsWith("--")) throw problem(`"${flag}" needs ${what}.`);
|
|
3168
|
+
return value;
|
|
3169
|
+
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Reads one flag or command into the flags.
|
|
3172
|
+
*
|
|
3173
|
+
* @param argv - The arguments.
|
|
3174
|
+
* @param at - The index of the flag.
|
|
3175
|
+
* @param flags - The flags so far.
|
|
3176
|
+
* @returns How many arguments the flag took.
|
|
3177
|
+
* @throws {Error} When the flag is unknown or misses its value.
|
|
3178
|
+
*/
|
|
3179
|
+
function readFlag(argv, at, flags) {
|
|
3180
|
+
const flag = argv[at];
|
|
3181
|
+
switch (flag) {
|
|
3182
|
+
case "--json":
|
|
3183
|
+
flags.commands.push({ kind: "json" });
|
|
3184
|
+
return 1;
|
|
3185
|
+
case "--check":
|
|
3186
|
+
flags.commands.push({ kind: "check" });
|
|
3187
|
+
return 1;
|
|
3188
|
+
case "where":
|
|
3189
|
+
flags.commands.push({
|
|
3190
|
+
kind: "where",
|
|
3191
|
+
key: valueAfter(argv, at, "a key, such as node:board/merge")
|
|
3192
|
+
});
|
|
3193
|
+
return 2;
|
|
3194
|
+
case "--root":
|
|
3195
|
+
flags.root = valueAfter(argv, at, "a path");
|
|
3196
|
+
return 2;
|
|
3197
|
+
case "--manifest":
|
|
3198
|
+
flags.manifest = valueAfter(argv, at, "a path");
|
|
3199
|
+
return 2;
|
|
3200
|
+
case "--tsconfig":
|
|
3201
|
+
flags.tsconfig = valueAfter(argv, at, "a path");
|
|
3202
|
+
return 2;
|
|
3203
|
+
default: throw problem(`unknown option "${String(flag)}".`);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Reads the arguments.
|
|
3208
|
+
*
|
|
3209
|
+
* @param argv - The arguments after the script name.
|
|
3210
|
+
* @returns The root, the manifest, the tsconfig and the one command.
|
|
3211
|
+
* @throws {Error} When a flag is unknown, a value is missing, `--root` is missing or the commands
|
|
3212
|
+
* are not exactly one.
|
|
3213
|
+
*/
|
|
3214
|
+
function parseArgv(argv) {
|
|
3215
|
+
const flags = { commands: [] };
|
|
3216
|
+
for (let at = 0; at < argv.length;) at += readFlag(argv, at, flags);
|
|
3217
|
+
if (flags.root === void 0) throw new Error("[game] index: \"--root\" is required.\n Name the game folder: moku-game-index --root <dir> where <key>.");
|
|
3218
|
+
const [command] = flags.commands;
|
|
3219
|
+
if (command === void 0 || flags.commands.length > 1) throw problem("name one command: --json, --check or where <key>.");
|
|
3220
|
+
return {
|
|
3221
|
+
root: flags.root,
|
|
3222
|
+
command,
|
|
3223
|
+
...flags.manifest === void 0 ? {} : { manifest: flags.manifest },
|
|
3224
|
+
...flags.tsconfig === void 0 ? {} : { tsconfig: flags.tsconfig }
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* One line of a `where` answer.
|
|
3229
|
+
*
|
|
3230
|
+
* @param found - One place of the key.
|
|
3231
|
+
* @returns `path:line`, with ` (broken)` when the line comes from the last good parse.
|
|
3232
|
+
* @example
|
|
3233
|
+
* ```ts
|
|
3234
|
+
* whereLine({ path: "nodes/merge.ts", line: 17, range: [17, 1, 38, 4], hash: "ab" }); // "nodes/merge.ts:17"
|
|
3235
|
+
* ```
|
|
3236
|
+
*/
|
|
3237
|
+
function whereLine(found) {
|
|
3238
|
+
return `${found.path}:${found.line}${found.broken === true ? " (broken)" : ""}`;
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* Counts a thing in words, so the summary reads as a sentence.
|
|
3242
|
+
*
|
|
3243
|
+
* @param count - How many.
|
|
3244
|
+
* @param word - The singular.
|
|
3245
|
+
* @returns The count and the word.
|
|
3246
|
+
* @example
|
|
3247
|
+
* ```ts
|
|
3248
|
+
* plural(1, "key"); // "1 key"
|
|
3249
|
+
* ```
|
|
3250
|
+
*/
|
|
3251
|
+
function plural(count, word) {
|
|
3252
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3253
|
+
}
|
|
3254
|
+
/**
|
|
3255
|
+
* The line of `--check` that names the aliases: the tsconfig and how many keys of `paths` it
|
|
3256
|
+
* holds, or `none` for a tsconfig without them. A root with no tsconfig gets no line.
|
|
3257
|
+
*
|
|
3258
|
+
* @param session - The open project.
|
|
3259
|
+
* @returns The line, or `undefined`.
|
|
3260
|
+
*/
|
|
3261
|
+
function aliasesLine(session) {
|
|
3262
|
+
const { aliases } = session;
|
|
3263
|
+
if (aliases !== void 0) return `aliases: ${aliases.file}, ${plural(aliases.patterns.length, "pattern")}`;
|
|
3264
|
+
return session.configStamps.get(session.tsconfig) !== void 0 ? "aliases: none (no tsconfig paths)" : void 0;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* The `--check` report: a summary, the aliases, every broken file and conflict as an error, every
|
|
3268
|
+
* unresolved item as info.
|
|
3269
|
+
*
|
|
3270
|
+
* @param session - The open project.
|
|
3271
|
+
* @param ui - Where the lines go.
|
|
3272
|
+
* @returns `1` when a file is broken or a key is in conflict, else `0`.
|
|
3273
|
+
*/
|
|
3274
|
+
function printCheck(session, ui) {
|
|
3275
|
+
const { index } = session;
|
|
3276
|
+
const aliases = aliasesLine(session);
|
|
3277
|
+
const files = Object.entries(index.files);
|
|
3278
|
+
const symbols = Object.entries(index.symbols);
|
|
3279
|
+
const broken = files.filter(([, file]) => file.state === "broken");
|
|
3280
|
+
const conflicts = symbols.filter(([, symbol]) => symbol.conflict === true);
|
|
3281
|
+
ui.info(`${plural(files.length, "file")}, ${plural(symbols.length, "key")}: ${broken.length} broken, ${conflicts.length} in conflict, ${index.unresolved.length} unresolved.`);
|
|
3282
|
+
if (aliases !== void 0) ui.info(aliases);
|
|
3283
|
+
for (const [file, entry] of broken) ui.error(`broken ${file}: ${entry.error ?? ""}`);
|
|
3284
|
+
for (const [key, symbol] of conflicts) ui.error(`conflict ${key}: ${symbol.def.map((anchor) => anchor.path).join(", ")}`);
|
|
3285
|
+
for (const item of index.unresolved) ui.info(`unresolved ${item.path}: ${item.reason}`);
|
|
3286
|
+
return broken.length + conflicts.length > 0 ? 1 : 0;
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* The `where` answer: one line per place of the key.
|
|
2613
3290
|
*
|
|
2614
|
-
* @param session - The project.
|
|
2615
|
-
* @
|
|
3291
|
+
* @param session - The open project.
|
|
3292
|
+
* @param key - The key.
|
|
3293
|
+
* @param root - The root as given, for the message.
|
|
3294
|
+
* @param ui - Where the lines go.
|
|
3295
|
+
* @returns `1` when the key is unknown, else `0`.
|
|
2616
3296
|
*/
|
|
2617
|
-
async function
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
session.stamps = walk.files;
|
|
2623
|
-
session.folders = walk.folders;
|
|
2624
|
-
for (const [file, stamp] of walk.files) {
|
|
2625
|
-
const known = previous.get(file);
|
|
2626
|
-
if (!(known !== void 0 && sameStamp(known, stamp)) && await readIntoCatalog(session, file)) files.push(file);
|
|
3297
|
+
async function printWhere(session, key, root, ui) {
|
|
3298
|
+
const found = await findKey(session, key);
|
|
3299
|
+
if (found.length === 0) {
|
|
3300
|
+
ui.error(`[game] The key "${key}" is not in the index of "${root}".\n Run "moku-game-index --root ${root} --json" to list the keys.`);
|
|
3301
|
+
return 1;
|
|
2627
3302
|
}
|
|
2628
|
-
const
|
|
2629
|
-
|
|
2630
|
-
return rebuild(session, files);
|
|
3303
|
+
for (const place of found) ui.line(whereLine(place));
|
|
3304
|
+
return 0;
|
|
2631
3305
|
}
|
|
2632
3306
|
/**
|
|
2633
|
-
*
|
|
2634
|
-
* dropped, and anything the index does not read changes nothing.
|
|
3307
|
+
* Runs the one command of the line on an open project.
|
|
2635
3308
|
*
|
|
2636
|
-
* @param session - The project.
|
|
2637
|
-
* @param
|
|
2638
|
-
* @
|
|
2639
|
-
* @
|
|
3309
|
+
* @param session - The open project.
|
|
3310
|
+
* @param options - What the flags asked for.
|
|
3311
|
+
* @param ui - Where the lines go.
|
|
3312
|
+
* @returns The exit code.
|
|
2640
3313
|
*/
|
|
2641
|
-
async function
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3314
|
+
async function runCommand(session, options, ui) {
|
|
3315
|
+
const { command } = options;
|
|
3316
|
+
if (command.kind === "where") return printWhere(session, command.key, options.root, ui);
|
|
3317
|
+
if (command.kind === "check") return printCheck(session, ui);
|
|
3318
|
+
ui.line(JSON.stringify(session.index, void 0, JSON_INDENT));
|
|
3319
|
+
return 0;
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
|
|
3323
|
+
* `process.exit`; the caller does.
|
|
3324
|
+
*
|
|
3325
|
+
* @param argv - The arguments after the script name.
|
|
3326
|
+
* @param ui - Where the lines go. The branded console by default.
|
|
3327
|
+
* @returns The exit code: `1` for a bad flag, a root that cannot be opened, an unknown key, or a
|
|
3328
|
+
* `--check` that found a broken file or a conflict; else `0`.
|
|
3329
|
+
* @example
|
|
3330
|
+
* ```ts
|
|
3331
|
+
* // An agent asks where a node of the mini game lives.
|
|
3332
|
+
* await runCli(["--root", "tests/fixtures/mini-game", "where", "node:infoPopup/count"]);
|
|
3333
|
+
* // 0, and the console printed "nodes/count.ts:7"
|
|
3334
|
+
* ```
|
|
3335
|
+
*/
|
|
3336
|
+
async function runCli(argv, ui = createBrandConsole()) {
|
|
3337
|
+
try {
|
|
3338
|
+
const options = parseArgv(argv);
|
|
3339
|
+
return await runCommand(await openSession({
|
|
3340
|
+
root: options.root,
|
|
3341
|
+
...options.manifest === void 0 ? {} : { manifest: options.manifest },
|
|
3342
|
+
...options.tsconfig === void 0 ? {} : { tsconfig: options.tsconfig }
|
|
3343
|
+
}), options, ui);
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
ui.error(messageOf(error));
|
|
3346
|
+
return 1;
|
|
3347
|
+
}
|
|
2651
3348
|
}
|
|
2652
3349
|
//#endregion
|
|
2653
3350
|
//#region src/project/watch.ts
|
|
@@ -2856,16 +3553,17 @@ const DEFAULT_DEBOUNCE_MS = 75;
|
|
|
2856
3553
|
* path), loads TypeScript, parses every source file and builds the index. Nothing is written to
|
|
2857
3554
|
* disk and nothing is inferred from the working directory but the root itself.
|
|
2858
3555
|
*
|
|
2859
|
-
* @param options - The game root, the manifest path
|
|
3556
|
+
* @param options - The game root, the manifest path, the tsconfig whose `paths` the index follows
|
|
3557
|
+
* (`tsconfig`, root-relative, `tsconfig.json` by default) and the quiet period of `watch`.
|
|
2860
3558
|
* @returns The handle; close it when the game closes.
|
|
2861
3559
|
* @throws {Error} When the root is missing or not a directory, or the `typescript` package is not
|
|
2862
3560
|
* installed: `[game] The project index needs the "typescript" package.`
|
|
2863
3561
|
* @example
|
|
2864
3562
|
* ```ts
|
|
2865
|
-
* // The editor opens the
|
|
2866
|
-
* const project = await openProject({ root: "tests/
|
|
2867
|
-
* Object.keys(project.index.files).length; //
|
|
2868
|
-
* (await project.find("textStyle:ui.
|
|
3563
|
+
* // The editor opens the mini game and answers where a node and a text style live.
|
|
3564
|
+
* const project = await openProject({ root: "tests/fixtures/mini-game" });
|
|
3565
|
+
* Object.keys(project.index.files).length; // 21
|
|
3566
|
+
* (await project.find("textStyle:ui.counter"))[0]?.line; // 16, in "features/home/styles.ts"
|
|
2869
3567
|
* project.close();
|
|
2870
3568
|
* ```
|
|
2871
3569
|
*/
|
|
@@ -2888,222 +3586,13 @@ async function openProject(options) {
|
|
|
2888
3586
|
};
|
|
2889
3587
|
}
|
|
2890
3588
|
//#endregion
|
|
2891
|
-
//#region src/project/cli.ts
|
|
2892
|
-
/**
|
|
2893
|
-
* @file project — the command line `moku-game-index`. It opens the project of `--root` and runs one
|
|
2894
|
-
* command: `--json` prints the index, `where <key>` prints one `path:line` per place, `--check`
|
|
2895
|
-
* reports broken files and conflicts (exit 1) and lists the unresolved items as info. Output goes
|
|
2896
|
-
* through the branded console of `@moku-labs/common`; `runCli` returns the exit code instead of
|
|
2897
|
-
* taking it.
|
|
2898
|
-
*/
|
|
2899
|
-
/** The usage line under every flag error. */
|
|
2900
|
-
const USAGE = "Run: moku-game-index --root <dir> [--manifest <path>] --json | --check | where <key>.";
|
|
2901
|
-
/** Pretty JSON indent of `--json`. */
|
|
2902
|
-
const JSON_INDENT = 2;
|
|
2903
|
-
/**
|
|
2904
|
-
* Wraps a command line problem in the message shape of the framework.
|
|
2905
|
-
*
|
|
2906
|
-
* @param message - One sentence naming the flag.
|
|
2907
|
-
* @returns The error to throw.
|
|
2908
|
-
*/
|
|
2909
|
-
function problem(message) {
|
|
2910
|
-
return /* @__PURE__ */ new Error(`[game] index: ${message}\n ${USAGE}`);
|
|
2911
|
-
}
|
|
2912
|
-
/**
|
|
2913
|
-
* Reads the message of anything that was thrown.
|
|
2914
|
-
*
|
|
2915
|
-
* @param failure - What the `catch` caught.
|
|
2916
|
-
* @returns The message.
|
|
2917
|
-
*/
|
|
2918
|
-
function messageOf(failure) {
|
|
2919
|
-
return failure instanceof Error ? failure.message : String(failure);
|
|
2920
|
-
}
|
|
2921
|
-
/**
|
|
2922
|
-
* The value after a flag that takes one.
|
|
2923
|
-
*
|
|
2924
|
-
* @param argv - The arguments.
|
|
2925
|
-
* @param at - The index of the flag.
|
|
2926
|
-
* @param what - What the flag needs, for the error.
|
|
2927
|
-
* @returns The value.
|
|
2928
|
-
* @throws {Error} When the value is missing.
|
|
2929
|
-
*/
|
|
2930
|
-
function valueAfter(argv, at, what) {
|
|
2931
|
-
const flag = argv[at] ?? "";
|
|
2932
|
-
const value = argv[at + 1];
|
|
2933
|
-
if (value === void 0 || value.startsWith("--")) throw problem(`"${flag}" needs ${what}.`);
|
|
2934
|
-
return value;
|
|
2935
|
-
}
|
|
2936
|
-
/**
|
|
2937
|
-
* Reads one flag or command into the flags.
|
|
2938
|
-
*
|
|
2939
|
-
* @param argv - The arguments.
|
|
2940
|
-
* @param at - The index of the flag.
|
|
2941
|
-
* @param flags - The flags so far.
|
|
2942
|
-
* @returns How many arguments the flag took.
|
|
2943
|
-
* @throws {Error} When the flag is unknown or misses its value.
|
|
2944
|
-
*/
|
|
2945
|
-
function readFlag(argv, at, flags) {
|
|
2946
|
-
const flag = argv[at];
|
|
2947
|
-
switch (flag) {
|
|
2948
|
-
case "--json":
|
|
2949
|
-
flags.commands.push({ kind: "json" });
|
|
2950
|
-
return 1;
|
|
2951
|
-
case "--check":
|
|
2952
|
-
flags.commands.push({ kind: "check" });
|
|
2953
|
-
return 1;
|
|
2954
|
-
case "where":
|
|
2955
|
-
flags.commands.push({
|
|
2956
|
-
kind: "where",
|
|
2957
|
-
key: valueAfter(argv, at, "a key, such as node:board/merge")
|
|
2958
|
-
});
|
|
2959
|
-
return 2;
|
|
2960
|
-
case "--root":
|
|
2961
|
-
flags.root = valueAfter(argv, at, "a path");
|
|
2962
|
-
return 2;
|
|
2963
|
-
case "--manifest":
|
|
2964
|
-
flags.manifest = valueAfter(argv, at, "a path");
|
|
2965
|
-
return 2;
|
|
2966
|
-
default: throw problem(`unknown option "${String(flag)}".`);
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
/**
|
|
2970
|
-
* Reads the arguments.
|
|
2971
|
-
*
|
|
2972
|
-
* @param argv - The arguments after the script name.
|
|
2973
|
-
* @returns The root, the manifest and the one command.
|
|
2974
|
-
* @throws {Error} When a flag is unknown, a value is missing, `--root` is missing or the commands
|
|
2975
|
-
* are not exactly one.
|
|
2976
|
-
*/
|
|
2977
|
-
function parseArgv(argv) {
|
|
2978
|
-
const flags = { commands: [] };
|
|
2979
|
-
for (let at = 0; at < argv.length;) at += readFlag(argv, at, flags);
|
|
2980
|
-
if (flags.root === void 0) throw new Error("[game] index: \"--root\" is required.\n Name the game folder: moku-game-index --root <dir> where <key>.");
|
|
2981
|
-
const [command] = flags.commands;
|
|
2982
|
-
if (command === void 0 || flags.commands.length > 1) throw problem("name one command: --json, --check or where <key>.");
|
|
2983
|
-
return {
|
|
2984
|
-
root: flags.root,
|
|
2985
|
-
command,
|
|
2986
|
-
...flags.manifest === void 0 ? {} : { manifest: flags.manifest }
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
/**
|
|
2990
|
-
* One line of a `where` answer.
|
|
2991
|
-
*
|
|
2992
|
-
* @param found - One place of the key.
|
|
2993
|
-
* @returns `path:line`, with ` (broken)` when the line comes from the last good parse.
|
|
2994
|
-
* @example
|
|
2995
|
-
* ```ts
|
|
2996
|
-
* whereLine({ path: "nodes/merge.ts", line: 17, range: [17, 1, 38, 4], hash: "ab" }); // "nodes/merge.ts:17"
|
|
2997
|
-
* ```
|
|
2998
|
-
*/
|
|
2999
|
-
function whereLine(found) {
|
|
3000
|
-
return `${found.path}:${found.line}${found.broken === true ? " (broken)" : ""}`;
|
|
3001
|
-
}
|
|
3002
|
-
/**
|
|
3003
|
-
* Counts a thing in words, so the summary reads as a sentence.
|
|
3004
|
-
*
|
|
3005
|
-
* @param count - How many.
|
|
3006
|
-
* @param word - The singular.
|
|
3007
|
-
* @returns The count and the word.
|
|
3008
|
-
* @example
|
|
3009
|
-
* ```ts
|
|
3010
|
-
* plural(1, "key"); // "1 key"
|
|
3011
|
-
* ```
|
|
3012
|
-
*/
|
|
3013
|
-
function plural(count, word) {
|
|
3014
|
-
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
3015
|
-
}
|
|
3016
|
-
/**
|
|
3017
|
-
* The `--check` report: a summary, every broken file and conflict as an error, every unresolved
|
|
3018
|
-
* item as info.
|
|
3019
|
-
*
|
|
3020
|
-
* @param index - The index.
|
|
3021
|
-
* @param ui - Where the lines go.
|
|
3022
|
-
* @returns `1` when a file is broken or a key is in conflict, else `0`.
|
|
3023
|
-
*/
|
|
3024
|
-
function printCheck(index, ui) {
|
|
3025
|
-
const files = Object.entries(index.files);
|
|
3026
|
-
const symbols = Object.entries(index.symbols);
|
|
3027
|
-
const broken = files.filter(([, file]) => file.state === "broken");
|
|
3028
|
-
const conflicts = symbols.filter(([, symbol]) => symbol.conflict === true);
|
|
3029
|
-
ui.info(`${plural(files.length, "file")}, ${plural(symbols.length, "key")}: ${broken.length} broken, ${conflicts.length} in conflict, ${index.unresolved.length} unresolved.`);
|
|
3030
|
-
for (const [file, entry] of broken) ui.error(`broken ${file}: ${entry.error ?? ""}`);
|
|
3031
|
-
for (const [key, symbol] of conflicts) ui.error(`conflict ${key}: ${symbol.def.map((anchor) => anchor.path).join(", ")}`);
|
|
3032
|
-
for (const item of index.unresolved) ui.info(`unresolved ${item.path}: ${item.reason}`);
|
|
3033
|
-
return broken.length + conflicts.length > 0 ? 1 : 0;
|
|
3034
|
-
}
|
|
3035
|
-
/**
|
|
3036
|
-
* The `where` answer: one line per place of the key.
|
|
3037
|
-
*
|
|
3038
|
-
* @param project - The open project.
|
|
3039
|
-
* @param key - The key.
|
|
3040
|
-
* @param root - The root as given, for the message.
|
|
3041
|
-
* @param ui - Where the lines go.
|
|
3042
|
-
* @returns `1` when the key is unknown, else `0`.
|
|
3043
|
-
*/
|
|
3044
|
-
async function printWhere(project, key, root, ui) {
|
|
3045
|
-
const found = await project.find(key);
|
|
3046
|
-
if (found.length === 0) {
|
|
3047
|
-
ui.error(`[game] The key "${key}" is not in the index of "${root}".\n Run "moku-game-index --root ${root} --json" to list the keys.`);
|
|
3048
|
-
return 1;
|
|
3049
|
-
}
|
|
3050
|
-
for (const place of found) ui.line(whereLine(place));
|
|
3051
|
-
return 0;
|
|
3052
|
-
}
|
|
3053
|
-
/**
|
|
3054
|
-
* Runs the one command of the line on an open project.
|
|
3055
|
-
*
|
|
3056
|
-
* @param project - The open project.
|
|
3057
|
-
* @param options - What the flags asked for.
|
|
3058
|
-
* @param ui - Where the lines go.
|
|
3059
|
-
* @returns The exit code.
|
|
3060
|
-
*/
|
|
3061
|
-
async function runCommand(project, options, ui) {
|
|
3062
|
-
const { command } = options;
|
|
3063
|
-
if (command.kind === "where") return printWhere(project, command.key, options.root, ui);
|
|
3064
|
-
if (command.kind === "check") return printCheck(project.index, ui);
|
|
3065
|
-
ui.line(JSON.stringify(project.index, void 0, JSON_INDENT));
|
|
3066
|
-
return 0;
|
|
3067
|
-
}
|
|
3068
|
-
/**
|
|
3069
|
-
* Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
|
|
3070
|
-
* `process.exit`; the caller does.
|
|
3071
|
-
*
|
|
3072
|
-
* @param argv - The arguments after the script name.
|
|
3073
|
-
* @param ui - Where the lines go. The branded console by default.
|
|
3074
|
-
* @returns The exit code: `1` for a bad flag, a root that cannot be opened, an unknown key, or a
|
|
3075
|
-
* `--check` that found a broken file or a conflict; else `0`.
|
|
3076
|
-
* @example
|
|
3077
|
-
* ```ts
|
|
3078
|
-
* // An agent asks where a node of the fixture game lives.
|
|
3079
|
-
* await runCli(["--root", "tests/integration/merge-game", "where", "node:board/merge"]);
|
|
3080
|
-
* // 0, and the console printed "nodes/merge.ts:17"
|
|
3081
|
-
* ```
|
|
3082
|
-
*/
|
|
3083
|
-
async function runCli(argv, ui = createBrandConsole()) {
|
|
3084
|
-
try {
|
|
3085
|
-
const options = parseArgv(argv);
|
|
3086
|
-
const project = await openProject({
|
|
3087
|
-
root: options.root,
|
|
3088
|
-
...options.manifest === void 0 ? {} : { manifest: options.manifest }
|
|
3089
|
-
});
|
|
3090
|
-
try {
|
|
3091
|
-
return await runCommand(project, options, ui);
|
|
3092
|
-
} finally {
|
|
3093
|
-
project.close();
|
|
3094
|
-
}
|
|
3095
|
-
} catch (error) {
|
|
3096
|
-
ui.error(messageOf(error));
|
|
3097
|
-
return 1;
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
//#endregion
|
|
3101
3589
|
//#region src/project.ts
|
|
3102
3590
|
/**
|
|
3103
3591
|
* @file The project door of the engine (subpath `./project`, node and bun only): the static index
|
|
3104
3592
|
* of a game's sources. `openProject` parses the game with TypeScript (an optional peer, loaded on
|
|
3105
3593
|
* the first open) and maps every engine id (flows, nodes, features, scenes, projections, emitters,
|
|
3106
|
-
* text styles, styles and JSX keys) to anchors
|
|
3594
|
+
* text styles, styles and JSX keys) to anchors, following names through relative imports and the
|
|
3595
|
+
* tsconfig `paths` aliases of the game; `find` reads the line of a key from the file on
|
|
3107
3596
|
* disk at the call, `watch` keeps the index fresh while agents edit files. The package bin
|
|
3108
3597
|
* `moku-game-index` runs `runCli`. Re-exports only: the code lives in `project/`, and this is the
|
|
3109
3598
|
* one door to it in `src/`.
|