@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.
Files changed (40) hide show
  1. package/README.md +12 -8
  2. package/dist/assets.d.mts +97 -33
  3. package/dist/assets.mjs +350 -110
  4. package/dist/{commands-mzE9PX0H.d.mts → commands-DuwCxK5t.d.mts} +5 -4
  5. package/dist/control.d.mts +4 -3
  6. package/dist/control.mjs +2 -2
  7. package/dist/{define-BbgXv4cy.d.mts → define-B6pHpdBi.d.mts} +4 -3
  8. package/dist/headless-BsNdJs9W.mjs +186 -0
  9. package/dist/headless-SCQXJxQf.d.mts +181 -0
  10. package/dist/hot.d.mts +4 -2
  11. package/dist/hot.mjs +8 -6
  12. package/dist/index-BO9kZzY3.d.mts +1611 -0
  13. package/dist/index.d.mts +6 -1596
  14. package/dist/index.mjs +4 -34334
  15. package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
  16. package/dist/inspect.d.mts +7 -6
  17. package/dist/inspect.mjs +2 -2
  18. package/dist/jsx-dev-runtime.d.mts +1 -1
  19. package/dist/jsx-runtime.d.mts +1 -1
  20. package/dist/lint.d.mts +55 -4
  21. package/dist/lint.mjs +993 -40
  22. package/dist/project.d.mts +57 -41
  23. package/dist/project.mjs +994 -505
  24. package/dist/{run-Bcyg32M5.mjs → run-CofJEdYx.mjs} +4 -187
  25. package/dist/{runtime-HTlmRb81.d.mts → runtime-CdNJ0UBm.d.mts} +1 -1
  26. package/dist/{sources-DVWJ8nfr.mjs → sources-DNnzyCYf.mjs} +2 -2
  27. package/dist/src-536arXjV.mjs +34644 -0
  28. package/dist/testing.d.mts +85 -283
  29. package/dist/testing.mjs +158 -2109
  30. package/dist/{types-BMEn0tSC.d.mts → types-B0v5VVa3.d.mts} +10 -9
  31. package/dist/{types-ClF8say0.d.mts → types-CQueODzo.d.mts} +2 -2
  32. package/dist/{types-CYYKjvLn.d.mts → types-DUMoYPYe.d.mts} +5 -182
  33. package/dist/{types-CXyipKhE.d.mts → types-VZVWwqCg.d.mts} +42 -4
  34. package/dist/{types-BaUUcIaN.d.mts → types-xXfyk56Q.d.mts} +1 -1
  35. package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
  36. package/dist/visual.d.mts +330 -0
  37. package/dist/visual.mjs +2142 -0
  38. package/llms.txt +59 -21
  39. package/package.json +7 -8
  40. 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 re-export to the module behind it, a star export to the first module
881
- * that has the name, until the name is declared, handed out by the kit, or comes from a package.
882
- * This is how a definer is recognised by binding (a renamed import still is the definer) and how
883
- * a node of a flow table reaches the file that declares it.
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
- * Resolves a relative module specifier to an indexed file, the way a bundler does: the path as
904
- * written, then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
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 from - The importing file.
908
- * @param specifier - The module specifier.
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 resolveModulePath(modules, from, specifier) {
912
- if (!isRelative(specifier)) return void 0;
913
- const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
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
- base,
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 modules - The module facts by path.
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(modules, from, ref, hops) {
933
- if (!isRelative(ref.specifier)) return {
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(modules, from, ref.specifier);
939
- return target === void 0 ? void 0 : resolveExport(modules, target, ref.imported, hops + 1);
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 modules - The module facts by path.
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(modules, file, name, hops = 0) {
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(modules, file, ref.local, hops + 1);
955
- if (ref !== void 0) return followImport(modules, file, ref, hops);
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(modules, file, {
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 modules - The module facts by path.
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(modules, file, local, hops = 0) {
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(modules, file, ref, hops);
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 modules - The module facts by path.
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(modules, file, namespace, specifier, definers) {
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(modules, file, specifier);
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(modules, target ?? "", name));
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 modules - The module facts by path.
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(modules, file) {
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(modules, file, local, ref.specifier, definers);
1532
+ addNamespace(resolution, file, local, ref.specifier, definers);
1040
1533
  continue;
1041
1534
  }
1042
- const definer = definerOf(resolveBinding(modules, file, local));
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(assembly.modules, file, ref);
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
- const reason = `node "${table.flow}/${name}": "${ref}" is not declared in a file of the root`;
1157
- unresolved.push({
1158
- path: file,
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(assembly.modules, file, local);
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, and the module facts of the good files.
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 modules - The module facts of every good file.
2341
+ * @param resolution - The module facts of every good file, and the aliases.
1835
2342
  */
1836
- function refreshExtractions(catalog, modules) {
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(modules, path);
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, modules);
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
- if (!isInside(root, real)) throw leavesRoot(file, root);
2142
- return real;
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 the bytes of a root-relative file through the guard. A path that leaves the root reads as
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 root - The real root.
2149
- * @param file - The root-relative path.
2150
- * @returns The bytes, or `undefined` when the file is gone or lies outside the root.
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
- async function readInside(root, file) {
2153
- try {
2154
- const real = await resolveInside(root, file);
2155
- if (real !== void 0) return await readFile(real);
2156
- } catch {}
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, as
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 catalog - The catalog of the project.
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(catalog, file) {
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(modules, file);
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.catalog, anchor.path)
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 current index and a queue that runs one update at a time. `syncWithDisk` is the batch
2521
- * walk of a watch batch: it walks the root, hashes the files whose stamp moved and every new one,
2522
- * drops the vanished ones and rebuilds the index when bytes changed. `reindexPath` reads one path
2523
- * now for `changed`.
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 every source file and builds the
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 path leaves the root,
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, and rebuilds the index when bytes changed. A save with the same bytes changes
2612
- * nothing.
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
- * @returns The change, or `undefined` when the index stayed the same.
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 syncWithDisk(session) {
2618
- const walk = await walkRoot(session.root);
2619
- const files = [];
2620
- for (const file of session.catalog.records.keys()) if (!walk.files.has(file) && dropFile(session.catalog, file)) files.push(file);
2621
- const previous = session.stamps;
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 manifest = await presentManifest(session.root, session.manifest);
2629
- if (files.length === 0 && manifest === session.index.manifest) return void 0;
2630
- return rebuild(session, files);
3303
+ for (const place of found) ui.line(whereLine(place));
3304
+ return 0;
2631
3305
  }
2632
3306
  /**
2633
- * Reads one root-relative path now: a changed or new source file is parsed, a deleted one is
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 file - The root-relative path.
2638
- * @returns The index after the read.
2639
- * @throws {Error} When the path leaves the root.
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 reindexPath(session, file) {
2642
- const relative = toPosix(file);
2643
- const real = await resolveInside(session.root, relative);
2644
- const isLinked = real !== void 0 && real !== path.join(session.root, relative);
2645
- if (path.isAbsolute(relative) || isLinked || !isIndexedFile(relative)) return session.index;
2646
- const stamp = real === void 0 ? void 0 : await stampOf(real);
2647
- if (stamp !== void 0) session.stamps.set(relative, stamp);
2648
- if (!await readIntoCatalog(session, relative)) return session.index;
2649
- await rebuild(session, [relative]);
2650
- return session.index;
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 and the quiet period of `watch`.
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 fixture game and answers where a node and a text style live.
2866
- * const project = await openProject({ root: "tests/integration/merge-game" });
2867
- * Object.keys(project.index.files).length; // 111
2868
- * (await project.find("textStyle:ui.title"))[0]?.line; // 39, in "features/ui/styles.ts"
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; `find` reads the line of a key from the file on
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/`.