@moku-labs/game 0.8.0 → 0.10.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 +88 -26
- package/bin/moku-game.mjs +4 -0
- package/dist/app/page.d.mts +41 -0
- package/dist/app/page.mjs +376 -0
- package/dist/app/system.d.mts +129 -0
- package/dist/app/system.mjs +307 -0
- package/dist/app.d.mts +37 -0
- package/dist/app.mjs +235 -0
- package/dist/assets-BHwTLVta.mjs +7391 -0
- package/dist/assets.d.mts +97 -33
- package/dist/assets.mjs +1 -7150
- package/dist/{capture-BX5Gfoty.mjs → capture-fpd83Mi5.mjs} +384 -1
- package/dist/cli.d.mts +70 -0
- package/dist/cli.mjs +1650 -0
- package/dist/{commands-C1kIfOG5.d.mts → commands-C4QlqijJ.d.mts} +5 -6
- package/dist/config-CdARrrhU.mjs +81 -0
- package/dist/control.d.mts +11 -10
- package/dist/control.mjs +3 -3
- package/dist/{define-BPpZcpxW.d.mts → define-CCc0ozKe.d.mts} +2 -2
- package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
- package/dist/{memory-CvgdnsQO.mjs → drafts-BawpSaVp.mjs} +1 -104
- package/dist/fake-MpzwZhIZ.mjs +87 -0
- package/dist/{headless-CvamCUcR.mjs → headless-BsNdJs9W.mjs} +1 -1
- package/dist/{headless-q98SwADR.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
- package/dist/hot.d.mts +8 -8
- package/dist/hot.mjs +15 -14
- package/dist/index-xxSxKJJW.d.mts +1611 -0
- package/dist/index.d.mts +6 -1597
- package/dist/index.mjs +4 -34343
- package/dist/inspect-la_KkjCS.mjs +1 -0
- package/dist/inspect.d.mts +7 -8
- package/dist/inspect.mjs +3 -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 +999 -42
- package/dist/memory-CWlqhS7l.mjs +136 -0
- package/dist/memory-DwDhfFWK.d.mts +50 -0
- package/dist/project.d.mts +29 -13
- package/dist/project.mjs +1120 -631
- package/dist/{run-CUPSFM85.mjs → run-wYty_Lwo.mjs} +4 -4
- package/dist/{runtime-CvDZZg1S.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
- package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
- package/dist/{sources-BBUd15vQ.mjs → sources-Duzj6lhV.mjs} +3 -3
- package/dist/src-DSl1eNB1.mjs +34366 -0
- package/dist/testing.d.mts +115 -34
- package/dist/testing.mjs +175 -70
- package/dist/{types-DlgsfOmW.d.mts → types-BM4shh8K.d.mts} +300 -263
- package/dist/{types-DgdMLZnV.d.mts → types-BUwDHKXm.d.mts} +1372 -1373
- package/dist/{types-DF5kkW-C.d.mts → types-CefgYzFt.d.mts} +4 -5
- package/dist/{types-CDSKa0vl.d.mts → types-DAdwEJMp.d.mts} +2 -2
- package/dist/{types-BWzCSRM2.d.mts → types-ZQLYXo5X.d.mts} +2983 -20
- package/dist/types-hQgI_wrq.d.mts +544 -0
- package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
- package/dist/visual.d.mts +10 -11
- package/dist/visual.mjs +5 -5
- package/llms.txt +170 -57
- package/package.json +30 -2
- package/dist/registry-DlpRCibU.mjs +0 -384
- package/dist/types-ERb2Y9U5.d.mts +0 -2967
package/dist/project.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
1
|
+
import { watch } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
6
6
|
//#region src/project/extract/scope.ts
|
|
7
7
|
/**
|
|
8
8
|
* Unwraps the expressions that change no value: parentheses, `as`, `satisfies`, `!` and `<T>`.
|
|
@@ -874,234 +874,727 @@ function readModule(typescript, source) {
|
|
|
874
874
|
return draft;
|
|
875
875
|
}
|
|
876
876
|
//#endregion
|
|
877
|
-
//#region src/project/
|
|
877
|
+
//#region src/project/paths.ts
|
|
878
878
|
/**
|
|
879
|
-
* @file project
|
|
880
|
-
*
|
|
881
|
-
*
|
|
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.
|
|
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.
|
|
884
882
|
*/
|
|
885
|
-
/**
|
|
886
|
-
const
|
|
887
|
-
/** The
|
|
888
|
-
const
|
|
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.";
|
|
889
891
|
/**
|
|
890
|
-
* Whether a
|
|
892
|
+
* Whether a folder name is one the index never walks into.
|
|
891
893
|
*
|
|
892
|
-
* @param
|
|
893
|
-
* @returns True for
|
|
894
|
+
* @param name - One folder name.
|
|
895
|
+
* @returns True for `node_modules`, `dist`, `generated`, `.moku`, `.git`, `__tests__` and `tests`.
|
|
894
896
|
* @example
|
|
895
897
|
* ```ts
|
|
896
|
-
*
|
|
898
|
+
* isSkippedFolder("generated"); // true
|
|
897
899
|
* ```
|
|
898
900
|
*/
|
|
899
|
-
function
|
|
900
|
-
return
|
|
901
|
+
function isSkippedFolder(name) {
|
|
902
|
+
return SKIPPED_FOLDER.test(name);
|
|
901
903
|
}
|
|
902
904
|
/**
|
|
903
|
-
*
|
|
904
|
-
* written, then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
|
|
905
|
+
* Whether a root-relative path sits under a skipped folder.
|
|
905
906
|
*
|
|
906
|
-
* @param
|
|
907
|
-
* @
|
|
908
|
-
* @
|
|
909
|
-
*
|
|
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
|
+
* ```
|
|
910
913
|
*/
|
|
911
|
-
function
|
|
912
|
-
|
|
913
|
-
const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
|
|
914
|
-
const stem = base.replace(SCRIPT_EXTENSION, "");
|
|
915
|
-
return [
|
|
916
|
-
base,
|
|
917
|
-
`${stem}.ts`,
|
|
918
|
-
`${stem}.tsx`,
|
|
919
|
-
`${stem}/index.ts`,
|
|
920
|
-
`${stem}/index.tsx`
|
|
921
|
-
].find((candidate) => modules.has(candidate));
|
|
914
|
+
function isSkippedPath(file) {
|
|
915
|
+
return file.split(/[/\\]/).slice(0, -1).some((folder) => isSkippedFolder(folder));
|
|
922
916
|
}
|
|
923
917
|
/**
|
|
924
|
-
*
|
|
918
|
+
* Whether the index reads a root-relative path: a `.ts` or `.tsx` file outside the skipped
|
|
919
|
+
* folders that is not a test.
|
|
925
920
|
*
|
|
926
|
-
* @param
|
|
927
|
-
* @
|
|
928
|
-
* @
|
|
929
|
-
*
|
|
930
|
-
*
|
|
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
|
+
* ```
|
|
931
927
|
*/
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
kind: "package",
|
|
935
|
-
...ref
|
|
936
|
-
};
|
|
937
|
-
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);
|
|
928
|
+
function isIndexedFile(file) {
|
|
929
|
+
return SOURCE_FILE.test(file) && !TEST_FILE.test(file) && !isSkippedPath(file);
|
|
940
930
|
}
|
|
941
931
|
/**
|
|
942
|
-
*
|
|
932
|
+
* Turns a path into its root-relative POSIX form.
|
|
943
933
|
*
|
|
944
|
-
* @param
|
|
945
|
-
* @
|
|
946
|
-
* @
|
|
947
|
-
*
|
|
948
|
-
*
|
|
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
|
+
* ```
|
|
949
940
|
*/
|
|
950
|
-
function
|
|
951
|
-
|
|
952
|
-
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
953
|
-
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);
|
|
956
|
-
for (const specifier of facts.stars) {
|
|
957
|
-
const found = followImport(modules, file, {
|
|
958
|
-
specifier,
|
|
959
|
-
imported: name
|
|
960
|
-
}, hops);
|
|
961
|
-
if (found !== void 0) return found;
|
|
962
|
-
}
|
|
941
|
+
function toPosix(file) {
|
|
942
|
+
return path.posix.normalize(file.replaceAll("\\", "/"));
|
|
963
943
|
}
|
|
964
944
|
/**
|
|
965
|
-
*
|
|
966
|
-
* or an import brings it.
|
|
945
|
+
* Whether a file system error says nothing is at the path.
|
|
967
946
|
*
|
|
968
|
-
* @param
|
|
969
|
-
* @
|
|
970
|
-
* @param local - The name as the file writes it.
|
|
971
|
-
* @param hops - How many hops were taken so far.
|
|
972
|
-
* @returns Where the name ends up, or `undefined` when it cannot be followed.
|
|
947
|
+
* @param error - What was thrown.
|
|
948
|
+
* @returns True for `ENOENT` and `ENOTDIR`.
|
|
973
949
|
*/
|
|
974
|
-
function
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
const definer = facts.kit.get(local);
|
|
978
|
-
if (definer !== void 0) return {
|
|
979
|
-
kind: "definer",
|
|
980
|
-
definer
|
|
981
|
-
};
|
|
982
|
-
if (facts.declared.has(local)) return {
|
|
983
|
-
kind: "declared",
|
|
984
|
-
path: file,
|
|
985
|
-
binding: local
|
|
986
|
-
};
|
|
987
|
-
const ref = facts.imports.get(local);
|
|
988
|
-
return ref === void 0 ? void 0 : followImport(modules, file, ref, hops);
|
|
950
|
+
function isMissing(error) {
|
|
951
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
952
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
989
953
|
}
|
|
990
954
|
/**
|
|
991
|
-
*
|
|
955
|
+
* Whether an absolute path lies inside a folder.
|
|
992
956
|
*
|
|
993
|
-
* @param
|
|
994
|
-
* @
|
|
995
|
-
* @
|
|
996
|
-
* ```ts
|
|
997
|
-
* definerOf({ kind: "package", specifier: "@moku-labs/game", imported: "defineStyle" }); // "defineStyle"
|
|
998
|
-
* ```
|
|
957
|
+
* @param folder - The absolute folder.
|
|
958
|
+
* @param file - The absolute path.
|
|
959
|
+
* @returns True for the folder itself and anything below it.
|
|
999
960
|
*/
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return isDefiner(target.imported) ? target.imported : void 0;
|
|
961
|
+
function isInside(folder, file) {
|
|
962
|
+
const relative = path.relative(folder, file);
|
|
963
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
1004
964
|
}
|
|
1005
965
|
/**
|
|
1006
|
-
*
|
|
966
|
+
* The error of a path that leaves the root.
|
|
1007
967
|
*
|
|
1008
|
-
* @param
|
|
1009
|
-
* @param
|
|
1010
|
-
* @
|
|
1011
|
-
* @param specifier - The module it imports.
|
|
1012
|
-
* @param definers - The map to add to.
|
|
968
|
+
* @param file - The path as it was given.
|
|
969
|
+
* @param root - The real root.
|
|
970
|
+
* @returns The error to throw.
|
|
1013
971
|
*/
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1016
|
-
for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
const target = resolveModulePath(modules, file, specifier);
|
|
1020
|
-
const names = target === void 0 ? [] : [...modules.get(target)?.exports.keys() ?? []];
|
|
1021
|
-
for (const name of names) {
|
|
1022
|
-
const definer = definerOf(resolveExport(modules, target ?? "", name));
|
|
1023
|
-
if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
|
|
1024
|
-
}
|
|
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.`);
|
|
1025
974
|
}
|
|
1026
975
|
/**
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
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.
|
|
1029
978
|
*
|
|
1030
|
-
* @param
|
|
1031
|
-
* @
|
|
1032
|
-
* @
|
|
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.
|
|
1033
982
|
*/
|
|
1034
|
-
function
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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;
|
|
1044
1008
|
}
|
|
1045
|
-
|
|
1009
|
+
if (!isInside(root, real)) throw leavesRoot(file, root);
|
|
1010
|
+
return real;
|
|
1046
1011
|
}
|
|
1047
|
-
//#endregion
|
|
1048
|
-
//#region src/project/hash.ts
|
|
1049
1012
|
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
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.
|
|
1052
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
|
+
};
|
|
1053
1045
|
/**
|
|
1054
|
-
*
|
|
1046
|
+
* Whether a specifier names the engine, which is a package even when the paths map it.
|
|
1055
1047
|
*
|
|
1056
|
-
* @param
|
|
1057
|
-
* @returns
|
|
1048
|
+
* @param specifier - A module specifier.
|
|
1049
|
+
* @returns True for `@moku-labs/game` and its subpaths.
|
|
1058
1050
|
* @example
|
|
1059
1051
|
* ```ts
|
|
1060
|
-
*
|
|
1052
|
+
* isEngine("@moku-labs/game/testing"); // true
|
|
1061
1053
|
* ```
|
|
1062
1054
|
*/
|
|
1063
|
-
function
|
|
1064
|
-
return
|
|
1055
|
+
function isEngine(specifier) {
|
|
1056
|
+
return specifier === "@moku-labs/game" || specifier.startsWith(`@moku-labs/game/`);
|
|
1065
1057
|
}
|
|
1066
1058
|
/**
|
|
1067
|
-
*
|
|
1059
|
+
* How many `*` a key or a target holds.
|
|
1068
1060
|
*
|
|
1069
|
-
* @param
|
|
1070
|
-
* @returns The
|
|
1061
|
+
* @param text - A key or a target of `paths`.
|
|
1062
|
+
* @returns The count.
|
|
1071
1063
|
* @example
|
|
1072
1064
|
* ```ts
|
|
1073
|
-
*
|
|
1065
|
+
* starsOf("@features/*"); // 1
|
|
1074
1066
|
* ```
|
|
1075
1067
|
*/
|
|
1076
|
-
function
|
|
1077
|
-
return
|
|
1068
|
+
function starsOf(text) {
|
|
1069
|
+
return text.split("*").length - 1;
|
|
1078
1070
|
}
|
|
1079
1071
|
/**
|
|
1080
|
-
*
|
|
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`.
|
|
1081
1075
|
*
|
|
1082
|
-
* @param
|
|
1083
|
-
* @returns
|
|
1076
|
+
* @param diagnostic - A diagnostic of the read.
|
|
1077
|
+
* @returns True for a syntax error of a file.
|
|
1084
1078
|
*/
|
|
1085
|
-
function
|
|
1086
|
-
|
|
1087
|
-
holePatterns: [],
|
|
1088
|
-
wildPatterns: [],
|
|
1089
|
-
idProps: []
|
|
1090
|
-
};
|
|
1091
|
-
for (const [key, entry] of Object.entries(index.symbols)) {
|
|
1092
|
-
if (!key.startsWith("jsx:")) continue;
|
|
1093
|
-
for (const anchor of entry.def) {
|
|
1094
|
-
const pattern = anchor.key ?? "";
|
|
1095
|
-
if (anchor.kind === "idProp") shapes.idProps.push(anchor);
|
|
1096
|
-
else if (hasHole(pattern)) shapes.holePatterns.push(anchor);
|
|
1097
|
-
else if (pattern.includes("*")) shapes.wildPatterns.push(anchor);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
return shapes;
|
|
1079
|
+
function isSyntaxError(diagnostic) {
|
|
1080
|
+
return diagnostic.file !== void 0 && diagnostic.code >= SYNTAX_CODES.from && diagnostic.code < SYNTAX_CODES.below;
|
|
1101
1081
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
|
1328
|
+
//#region src/project/extract/resolve.ts
|
|
1329
|
+
/**
|
|
1330
|
+
* @file project, extract — following a name across files with the module facts only: an import
|
|
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.
|
|
1336
|
+
*/
|
|
1337
|
+
/** How many imports and re-exports a name is followed through before giving up. */
|
|
1338
|
+
const MAX_HOPS = 12;
|
|
1339
|
+
/** The extensions a TypeScript import may name instead of the source file. */
|
|
1340
|
+
const SCRIPT_EXTENSION = /\.(?:[cm]?js|jsx)$/;
|
|
1341
|
+
/**
|
|
1342
|
+
* Whether a module specifier is relative to the importing file.
|
|
1343
|
+
*
|
|
1344
|
+
* @param specifier - The module specifier.
|
|
1345
|
+
* @returns True for `./…` and `../…`.
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```ts
|
|
1348
|
+
* isRelative("../kit"); // true
|
|
1349
|
+
* ```
|
|
1350
|
+
*/
|
|
1351
|
+
function isRelative(specifier) {
|
|
1352
|
+
return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
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`.
|
|
1357
|
+
*
|
|
1358
|
+
* @param modules - The module facts by path.
|
|
1359
|
+
* @param base - A root-relative path.
|
|
1360
|
+
* @returns The path the index holds, or `undefined`.
|
|
1361
|
+
*/
|
|
1362
|
+
function indexedFile(modules, base) {
|
|
1363
|
+
const normal = path.posix.normalize(base);
|
|
1364
|
+
const stem = normal.replace(SCRIPT_EXTENSION, "");
|
|
1365
|
+
return [
|
|
1366
|
+
normal,
|
|
1367
|
+
`${stem}.ts`,
|
|
1368
|
+
`${stem}.tsx`,
|
|
1369
|
+
`${stem}/index.ts`,
|
|
1370
|
+
`${stem}/index.tsx`
|
|
1371
|
+
].find((candidate) => modules.has(candidate));
|
|
1372
|
+
}
|
|
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
|
+
/**
|
|
1403
|
+
* Follows an imported name into the module that the specifier names.
|
|
1404
|
+
*
|
|
1405
|
+
* @param resolution - The module facts and the aliases.
|
|
1406
|
+
* @param from - The importing file.
|
|
1407
|
+
* @param ref - The specifier and the name.
|
|
1408
|
+
* @param hops - How many hops were taken so far.
|
|
1409
|
+
* @returns Where the name ends up.
|
|
1410
|
+
*/
|
|
1411
|
+
function followImport(resolution, from, ref, hops) {
|
|
1412
|
+
if (isPackage(resolution, ref.specifier)) return {
|
|
1413
|
+
kind: "package",
|
|
1414
|
+
...ref
|
|
1415
|
+
};
|
|
1416
|
+
if (ref.imported === "*") return void 0;
|
|
1417
|
+
const target = resolveModulePath(resolution, from, ref.specifier);
|
|
1418
|
+
return target === void 0 ? void 0 : resolveExport(resolution, target, ref.imported, hops + 1);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Follows an exported name of a module: a local binding, a re-export, or a star export.
|
|
1422
|
+
*
|
|
1423
|
+
* @param resolution - The module facts and the aliases.
|
|
1424
|
+
* @param file - The module.
|
|
1425
|
+
* @param name - The exported name.
|
|
1426
|
+
* @param hops - How many hops were taken so far.
|
|
1427
|
+
* @returns Where the name ends up.
|
|
1428
|
+
*/
|
|
1429
|
+
function resolveExport(resolution, file, name, hops = 0) {
|
|
1430
|
+
const facts = resolution.modules.get(file);
|
|
1431
|
+
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
1432
|
+
const ref = facts.exports.get(name);
|
|
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);
|
|
1435
|
+
for (const specifier of facts.stars) {
|
|
1436
|
+
const found = followImport(resolution, file, {
|
|
1437
|
+
specifier,
|
|
1438
|
+
imported: name
|
|
1439
|
+
}, hops);
|
|
1440
|
+
if (found !== void 0) return found;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Follows a name used in a file to where it ends up: the kit hands it out, the file declares it,
|
|
1445
|
+
* or an import brings it.
|
|
1446
|
+
*
|
|
1447
|
+
* @param resolution - The module facts and the aliases.
|
|
1448
|
+
* @param file - The file that uses the name.
|
|
1449
|
+
* @param local - The name as the file writes it.
|
|
1450
|
+
* @param hops - How many hops were taken so far.
|
|
1451
|
+
* @returns Where the name ends up, or `undefined` when it cannot be followed.
|
|
1452
|
+
*/
|
|
1453
|
+
function resolveBinding(resolution, file, local, hops = 0) {
|
|
1454
|
+
const facts = resolution.modules.get(file);
|
|
1455
|
+
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
1456
|
+
const definer = facts.kit.get(local);
|
|
1457
|
+
if (definer !== void 0) return {
|
|
1458
|
+
kind: "definer",
|
|
1459
|
+
definer
|
|
1460
|
+
};
|
|
1461
|
+
if (facts.declared.has(local)) return {
|
|
1462
|
+
kind: "declared",
|
|
1463
|
+
path: file,
|
|
1464
|
+
binding: local
|
|
1465
|
+
};
|
|
1466
|
+
const ref = facts.imports.get(local);
|
|
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;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* The definer a target is, if any: one the kit hands out, or one imported from the engine.
|
|
1485
|
+
*
|
|
1486
|
+
* @param target - Where a name ends up.
|
|
1487
|
+
* @returns The definer, or `undefined`.
|
|
1488
|
+
* @example
|
|
1489
|
+
* ```ts
|
|
1490
|
+
* definerOf({ kind: "package", specifier: "@moku-labs/game", imported: "defineStyle" }); // "defineStyle"
|
|
1491
|
+
* ```
|
|
1492
|
+
*/
|
|
1493
|
+
function definerOf(target) {
|
|
1494
|
+
if (target?.kind === "definer") return target.definer;
|
|
1495
|
+
if (target?.kind !== "package" || target.specifier !== "@moku-labs/game") return void 0;
|
|
1496
|
+
return isDefiner(target.imported) ? target.imported : void 0;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Adds the definers a namespace import reaches as `ns.name`.
|
|
1500
|
+
*
|
|
1501
|
+
* @param resolution - The module facts and the aliases.
|
|
1502
|
+
* @param file - The importing file.
|
|
1503
|
+
* @param namespace - The local name of the namespace.
|
|
1504
|
+
* @param specifier - The module it imports.
|
|
1505
|
+
* @param definers - The map to add to.
|
|
1506
|
+
*/
|
|
1507
|
+
function addNamespace(resolution, file, namespace, specifier, definers) {
|
|
1508
|
+
if (specifier === "@moku-labs/game") {
|
|
1509
|
+
for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const target = resolveModulePath(resolution, file, specifier);
|
|
1513
|
+
const names = target === void 0 ? [] : [...resolution.modules.get(target)?.exports.keys() ?? []];
|
|
1514
|
+
for (const name of names) {
|
|
1515
|
+
const definer = definerOf(resolveExport(resolution, target ?? "", name));
|
|
1516
|
+
if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* The definers a file can call, by the callee as the file writes it: `defineFlow`, a renamed
|
|
1521
|
+
* `scene`, or `kit.defineScene` through a namespace.
|
|
1522
|
+
*
|
|
1523
|
+
* @param resolution - The module facts and the aliases.
|
|
1524
|
+
* @param file - The file.
|
|
1525
|
+
* @returns The callee text to the definer.
|
|
1526
|
+
*/
|
|
1527
|
+
function definersOf(resolution, file) {
|
|
1528
|
+
const facts = resolution.modules.get(file);
|
|
1529
|
+
const definers = new Map(facts?.kit);
|
|
1530
|
+
for (const [local, ref] of facts?.imports ?? []) {
|
|
1531
|
+
if (ref.imported === "*") {
|
|
1532
|
+
addNamespace(resolution, file, local, ref.specifier, definers);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const definer = definerOf(resolveBinding(resolution, file, local));
|
|
1536
|
+
if (definer !== void 0) definers.set(local, definer);
|
|
1537
|
+
}
|
|
1538
|
+
return definers;
|
|
1539
|
+
}
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/project/hash.ts
|
|
1542
|
+
/**
|
|
1543
|
+
* @file project — the two hashes of the index: the sha1 of a file's bytes, the same value the
|
|
1544
|
+
* editor uses as a file version, and the revision over every file of the index.
|
|
1545
|
+
*/
|
|
1546
|
+
/**
|
|
1547
|
+
* Hashes bytes or text with sha1.
|
|
1548
|
+
*
|
|
1549
|
+
* @param bytes - The file bytes, or a text.
|
|
1550
|
+
* @returns The hex digest.
|
|
1551
|
+
* @example
|
|
1552
|
+
* ```ts
|
|
1553
|
+
* sha1(""); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
|
1554
|
+
* ```
|
|
1555
|
+
*/
|
|
1556
|
+
function sha1(bytes) {
|
|
1557
|
+
return createHash("sha1").update(bytes).digest("hex");
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* The revision of an index: the sha1 of the lines `${path}\0${hash}\n`, sorted by path.
|
|
1561
|
+
*
|
|
1562
|
+
* @param files - Every indexed file with its hash.
|
|
1563
|
+
* @returns The hex digest.
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```ts
|
|
1566
|
+
* revisionOf({}); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
|
1567
|
+
* ```
|
|
1568
|
+
*/
|
|
1569
|
+
function revisionOf(files) {
|
|
1570
|
+
return sha1(Object.entries(files).map(([file, entry]) => `${file}\0${entry.hash}\n`).toSorted().join(""));
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* The anchors of the JSX patterns and of the literal key-carrying props of an index.
|
|
1574
|
+
*
|
|
1575
|
+
* @param index - The index.
|
|
1576
|
+
* @returns The prop-hole patterns, the `*` patterns and the `idProp` anchors.
|
|
1577
|
+
*/
|
|
1578
|
+
function jsxShapes(index) {
|
|
1579
|
+
const shapes = {
|
|
1580
|
+
holePatterns: [],
|
|
1581
|
+
wildPatterns: [],
|
|
1582
|
+
idProps: []
|
|
1583
|
+
};
|
|
1584
|
+
for (const [key, entry] of Object.entries(index.symbols)) {
|
|
1585
|
+
if (!key.startsWith("jsx:")) continue;
|
|
1586
|
+
for (const anchor of entry.def) {
|
|
1587
|
+
const pattern = anchor.key ?? "";
|
|
1588
|
+
if (anchor.kind === "idProp") shapes.idProps.push(anchor);
|
|
1589
|
+
else if (hasHole(pattern)) shapes.holePatterns.push(anchor);
|
|
1590
|
+
else if (pattern.includes("*")) shapes.wildPatterns.push(anchor);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return shapes;
|
|
1594
|
+
}
|
|
1595
|
+
//#endregion
|
|
1596
|
+
//#region src/project/assemble.ts
|
|
1597
|
+
/** The prefix of a node key. */
|
|
1105
1598
|
const NODE = "node:";
|
|
1106
1599
|
/**
|
|
1107
1600
|
* The entry of a key, made on first use.
|
|
@@ -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
|
|
@@ -1991,169 +2503,19 @@ function locateFactory(typescript, source, anchor, context) {
|
|
|
1991
2503
|
* @param context - The kind of its key, the JSX hits of the parse and the definers of the file.
|
|
1992
2504
|
* @returns Every place of the anchor in the file; none when the file no longer holds it.
|
|
1993
2505
|
*/
|
|
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;
|
|
2140
|
-
}
|
|
2141
|
-
if (!isInside(root, real)) throw leavesRoot(file, root);
|
|
2142
|
-
return real;
|
|
2143
|
-
}
|
|
2144
|
-
/**
|
|
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.
|
|
2147
|
-
*
|
|
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.
|
|
2151
|
-
*/
|
|
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
|
|
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
|
/**
|
|
@@ -2601,53 +3063,288 @@ async function openSession(options) {
|
|
|
2601
3063
|
* @param task - The update.
|
|
2602
3064
|
* @returns What the update returns.
|
|
2603
3065
|
*/
|
|
2604
|
-
function serialize(session, task) {
|
|
2605
|
-
const result = session.queue.then(task);
|
|
2606
|
-
session.queue = result.catch(() => {});
|
|
2607
|
-
return result;
|
|
3066
|
+
function serialize(session, task) {
|
|
3067
|
+
const result = session.queue.then(task);
|
|
3068
|
+
session.queue = result.catch(() => {});
|
|
3069
|
+
return result;
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
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.
|
|
3290
|
+
*
|
|
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`.
|
|
3296
|
+
*/
|
|
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;
|
|
3302
|
+
}
|
|
3303
|
+
for (const place of found) ui.line(whereLine(place));
|
|
3304
|
+
return 0;
|
|
2608
3305
|
}
|
|
2609
3306
|
/**
|
|
2610
|
-
*
|
|
2611
|
-
* vanished ones, and rebuilds the index when bytes changed. A save with the same bytes changes
|
|
2612
|
-
* nothing.
|
|
3307
|
+
* Runs the one command of the line on an open project.
|
|
2613
3308
|
*
|
|
2614
|
-
* @param session - The project.
|
|
2615
|
-
* @
|
|
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.
|
|
2616
3313
|
*/
|
|
2617
|
-
async function
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
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);
|
|
2627
|
-
}
|
|
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);
|
|
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;
|
|
2631
3320
|
}
|
|
2632
3321
|
/**
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
3322
|
+
* Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
|
|
3323
|
+
* `process.exit`; the caller does.
|
|
2635
3324
|
*
|
|
2636
|
-
* @param
|
|
2637
|
-
* @param
|
|
2638
|
-
* @returns The
|
|
2639
|
-
*
|
|
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
|
+
* ```
|
|
2640
3335
|
*/
|
|
2641
|
-
async function
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
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,7 +3553,8 @@ 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.`
|
|
@@ -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 mini game lives.
|
|
3079
|
-
* await runCli(["--root", "tests/fixtures/mini-game", "where", "node:infoPopup/count"]);
|
|
3080
|
-
* // 0, and the console printed "nodes/count.ts:7"
|
|
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/`.
|