@illuminis/comprism 0.1.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/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one shape both readers emit, and the only thing that ever leaves the
|
|
3
|
+
* machine.
|
|
4
|
+
*
|
|
5
|
+
* An object and a link, each carrying a file and a line. Nothing else. No file
|
|
6
|
+
* contents, no source text, no string a person wrote. That is what makes
|
|
7
|
+
* sending the map to the service a different act from sending the code: the
|
|
8
|
+
* service learns that a function called `price_of` exists at line 40 of a named
|
|
9
|
+
* file and what calls it, and it never learns what that function says.
|
|
10
|
+
*/
|
|
11
|
+
export interface GraphObject {
|
|
12
|
+
/** Unique within a project. A file is its own path; a symbol is `file::name`. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** file, function, method, class, component, model, table, route. */
|
|
15
|
+
kind: string;
|
|
16
|
+
name: string;
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
[extra: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface GraphLink {
|
|
22
|
+
from: string;
|
|
23
|
+
/** imports, defines, calls, renders, inherits, references, maps_to,
|
|
24
|
+
* handled_by, declares, calls_endpoint, browser_route. */
|
|
25
|
+
kind: string;
|
|
26
|
+
/** Set when the reader could resolve the far end itself. */
|
|
27
|
+
to?: string;
|
|
28
|
+
/** Set when it could not, and the service joins it up. */
|
|
29
|
+
to_name?: string;
|
|
30
|
+
[extra: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface FactSet {
|
|
33
|
+
objects: GraphObject[];
|
|
34
|
+
links: GraphLink[];
|
|
35
|
+
/** A file that declares routers, and what each is mounted under. Python only:
|
|
36
|
+
* an endpoint's path is written relative to a router that is mounted with a
|
|
37
|
+
* prefix in a different file, and without both halves the front end and the
|
|
38
|
+
* back end never join up. */
|
|
39
|
+
routers?: {
|
|
40
|
+
prefixes: Record<string, string>;
|
|
41
|
+
mounts: {
|
|
42
|
+
target: string;
|
|
43
|
+
prefix: string;
|
|
44
|
+
line: number;
|
|
45
|
+
}[];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The one shape both readers emit, and the only thing that ever leaves the
|
|
4
|
+
* machine.
|
|
5
|
+
*
|
|
6
|
+
* An object and a link, each carrying a file and a line. Nothing else. No file
|
|
7
|
+
* contents, no source text, no string a person wrote. That is what makes
|
|
8
|
+
* sending the map to the service a different act from sending the code: the
|
|
9
|
+
* service learns that a function called `price_of` exists at line 40 of a named
|
|
10
|
+
* file and what calls it, and it never learns what that function says.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** How many files one project may have indexed. A ceiling, not a guess: a
|
|
2
|
+
* monorepo of forty thousand files would take minutes to read and produce a
|
|
3
|
+
* map too large to be worth sending, and half a map that claims to be whole is
|
|
4
|
+
* the failure this whole feature exists to avoid. Past this the map says
|
|
5
|
+
* plainly that it covers part of the project. */
|
|
6
|
+
export declare const MAX_FILES = 6000;
|
|
7
|
+
/** How large a single file may be before it is skipped. A generated bundle or
|
|
8
|
+
* a checked-in minified file is not code anybody navigates. */
|
|
9
|
+
export declare const MAX_FILE_BYTES = 1500000;
|
|
10
|
+
export declare const PY_EXT: Set<string>;
|
|
11
|
+
export declare const TS_EXT: Set<string>;
|
|
12
|
+
export interface IndexedFile {
|
|
13
|
+
/** Path relative to the project root, forward slashes, always. */
|
|
14
|
+
rel: string;
|
|
15
|
+
size: number;
|
|
16
|
+
mtimeMs: number;
|
|
17
|
+
lang: "python" | "typescript" | "translations";
|
|
18
|
+
}
|
|
19
|
+
/** Every file the map covers, in a stable order.
|
|
20
|
+
*
|
|
21
|
+
* Sorted, and that is not cosmetic: the fingerprint below is a hash over this
|
|
22
|
+
* list, so an unstable order would report the code had moved every time the
|
|
23
|
+
* filesystem felt like returning entries differently. */
|
|
24
|
+
export declare function indexedFiles(root: string): IndexedFile[];
|
|
25
|
+
/** One short string per file that changes whenever that file does.
|
|
26
|
+
*
|
|
27
|
+
* Size and modified time rather than a hash of the contents: reading six
|
|
28
|
+
* thousand files to find out whether any of them changed costs more than
|
|
29
|
+
* rebuilding the map. A file edited back to its original contents within the
|
|
30
|
+
* same millisecond would be missed, which is a case that does not occur in
|
|
31
|
+
* practice and is corrected by the next ordinary edit. */
|
|
32
|
+
export declare function stampOf(file: IndexedFile): string;
|
|
33
|
+
/** One short string for the whole project. Saying the code has not moved is a
|
|
34
|
+
* claim, and this is the evidence for it. */
|
|
35
|
+
export declare function fingerprint(files: IndexedFile[]): {
|
|
36
|
+
hash: string;
|
|
37
|
+
files: number;
|
|
38
|
+
};
|
|
39
|
+
/** The commit the project is standing on, when it is a repository at all.
|
|
40
|
+
*
|
|
41
|
+
* Read with a plain file read rather than by running git, because this is
|
|
42
|
+
* called at the start of every job and spawning a process there is a cost paid
|
|
43
|
+
* on work that has nothing to do with the map. A project with no git still
|
|
44
|
+
* gets a usable map; it simply cannot stamp an answer with a commit. */
|
|
45
|
+
export declare function headCommit(root: string): string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TS_EXT = exports.PY_EXT = exports.MAX_FILE_BYTES = exports.MAX_FILES = void 0;
|
|
37
|
+
exports.indexedFiles = indexedFiles;
|
|
38
|
+
exports.stampOf = stampOf;
|
|
39
|
+
exports.fingerprint = fingerprint;
|
|
40
|
+
exports.headCommit = headCommit;
|
|
41
|
+
/**
|
|
42
|
+
* What the code map covers, and how to tell in milliseconds whether the code
|
|
43
|
+
* has moved since it was last read.
|
|
44
|
+
*
|
|
45
|
+
* Everything here runs on the person's own machine, because it is the one place
|
|
46
|
+
* their source files exist. Nothing in this folder decides, prices or
|
|
47
|
+
* classifies anything: it reads files and produces facts. That is the division
|
|
48
|
+
* the whole product rests on, and a function here that formed an opinion would
|
|
49
|
+
* be the first crack in it.
|
|
50
|
+
*/
|
|
51
|
+
const crypto = __importStar(require("crypto"));
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const read_locales_1 = require("./read-locales");
|
|
55
|
+
/** Folders never worth reading: somebody else's code, or build output. */
|
|
56
|
+
const SKIP_DIRS = new Set([
|
|
57
|
+
"node_modules", ".git", "__pycache__", ".venv", "venv", "env",
|
|
58
|
+
"site-packages", "dist", "build", ".pytest_cache", ".mypy_cache",
|
|
59
|
+
"htmlcov", ".ruff_cache", "coverage", ".vite", ".next", ".turbo",
|
|
60
|
+
"playwright-report", "test-results", "vendor", "target", ".cache",
|
|
61
|
+
"scratchpad", ".comprism", ".completionprism",
|
|
62
|
+
]);
|
|
63
|
+
/** Folder names that are build output only when they sit beside a package
|
|
64
|
+
* manifest. `out` is where TypeScript writes its JavaScript by convention, but
|
|
65
|
+
* it is also a perfectly ordinary name for a source folder, so skipping it
|
|
66
|
+
* everywhere would quietly remove real code from somebody's map. Next to a
|
|
67
|
+
* package.json it is almost always generated.
|
|
68
|
+
*
|
|
69
|
+
* Found by mapping this repository and getting 83 changed files from one edit:
|
|
70
|
+
* the terminal client's own compiled output was in the map, so every rebuild
|
|
71
|
+
* looked like eighty three source files moving. */
|
|
72
|
+
const BUILT_BESIDE_MANIFEST = new Set(["out", "output", "lib", "es", "esm"]);
|
|
73
|
+
/** How many files one project may have indexed. A ceiling, not a guess: a
|
|
74
|
+
* monorepo of forty thousand files would take minutes to read and produce a
|
|
75
|
+
* map too large to be worth sending, and half a map that claims to be whole is
|
|
76
|
+
* the failure this whole feature exists to avoid. Past this the map says
|
|
77
|
+
* plainly that it covers part of the project. */
|
|
78
|
+
exports.MAX_FILES = 6000;
|
|
79
|
+
/** How large a single file may be before it is skipped. A generated bundle or
|
|
80
|
+
* a checked-in minified file is not code anybody navigates. */
|
|
81
|
+
exports.MAX_FILE_BYTES = 1_500_000;
|
|
82
|
+
exports.PY_EXT = new Set([".py"]);
|
|
83
|
+
exports.TS_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
84
|
+
function isCode(rel, name) {
|
|
85
|
+
if (name.endsWith(".d.ts"))
|
|
86
|
+
return null;
|
|
87
|
+
if (name.endsWith(".min.js"))
|
|
88
|
+
return null;
|
|
89
|
+
const ext = path.extname(name);
|
|
90
|
+
if (exports.PY_EXT.has(ext))
|
|
91
|
+
return "python";
|
|
92
|
+
if (exports.TS_EXT.has(ext))
|
|
93
|
+
return "typescript";
|
|
94
|
+
// Translation files. Not code, and in the map for a reason no code parser can
|
|
95
|
+
// serve: the label a person sees is a key here and a sentence in ten language
|
|
96
|
+
// files, and "which languages are missing this one" is the question asked
|
|
97
|
+
// about it. See `read-locales.ts`.
|
|
98
|
+
if ((0, read_locales_1.looksLikeTranslations)(rel))
|
|
99
|
+
return "translations";
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/** Every file the map covers, in a stable order.
|
|
103
|
+
*
|
|
104
|
+
* Sorted, and that is not cosmetic: the fingerprint below is a hash over this
|
|
105
|
+
* list, so an unstable order would report the code had moved every time the
|
|
106
|
+
* filesystem felt like returning entries differently. */
|
|
107
|
+
function indexedFiles(root) {
|
|
108
|
+
const out = [];
|
|
109
|
+
const walk = (dir) => {
|
|
110
|
+
if (out.length >= exports.MAX_FILES)
|
|
111
|
+
return;
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (out.length >= exports.MAX_FILES)
|
|
121
|
+
return;
|
|
122
|
+
const full = path.join(dir, entry.name);
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (entry.name.startsWith(".") && entry.name !== ".github")
|
|
125
|
+
continue;
|
|
126
|
+
if (SKIP_DIRS.has(entry.name))
|
|
127
|
+
continue;
|
|
128
|
+
if (BUILT_BESIDE_MANIFEST.has(entry.name)
|
|
129
|
+
&& fs.existsSync(path.join(dir, "package.json")))
|
|
130
|
+
continue;
|
|
131
|
+
walk(full);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!entry.isFile())
|
|
135
|
+
continue;
|
|
136
|
+
const rel = path.relative(root, full).split(path.sep).join("/");
|
|
137
|
+
const lang = isCode(rel, entry.name);
|
|
138
|
+
if (!lang)
|
|
139
|
+
continue;
|
|
140
|
+
let stat;
|
|
141
|
+
try {
|
|
142
|
+
stat = fs.statSync(full);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (stat.size > exports.MAX_FILE_BYTES)
|
|
148
|
+
continue;
|
|
149
|
+
out.push({
|
|
150
|
+
rel,
|
|
151
|
+
size: stat.size,
|
|
152
|
+
mtimeMs: Math.round(stat.mtimeMs),
|
|
153
|
+
lang,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(root);
|
|
158
|
+
out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/** One short string per file that changes whenever that file does.
|
|
162
|
+
*
|
|
163
|
+
* Size and modified time rather than a hash of the contents: reading six
|
|
164
|
+
* thousand files to find out whether any of them changed costs more than
|
|
165
|
+
* rebuilding the map. A file edited back to its original contents within the
|
|
166
|
+
* same millisecond would be missed, which is a case that does not occur in
|
|
167
|
+
* practice and is corrected by the next ordinary edit. */
|
|
168
|
+
function stampOf(file) {
|
|
169
|
+
return `${file.size}:${file.mtimeMs}`;
|
|
170
|
+
}
|
|
171
|
+
/** One short string for the whole project. Saying the code has not moved is a
|
|
172
|
+
* claim, and this is the evidence for it. */
|
|
173
|
+
function fingerprint(files) {
|
|
174
|
+
const hash = crypto.createHash("sha1");
|
|
175
|
+
for (const file of files)
|
|
176
|
+
hash.update(`${file.rel}:${stampOf(file)}\n`);
|
|
177
|
+
return { hash: hash.digest("hex").slice(0, 16), files: files.length };
|
|
178
|
+
}
|
|
179
|
+
/** The commit the project is standing on, when it is a repository at all.
|
|
180
|
+
*
|
|
181
|
+
* Read with a plain file read rather than by running git, because this is
|
|
182
|
+
* called at the start of every job and spawning a process there is a cost paid
|
|
183
|
+
* on work that has nothing to do with the map. A project with no git still
|
|
184
|
+
* gets a usable map; it simply cannot stamp an answer with a commit. */
|
|
185
|
+
function headCommit(root) {
|
|
186
|
+
try {
|
|
187
|
+
const head = fs.readFileSync(path.join(root, ".git", "HEAD"), "utf8").trim();
|
|
188
|
+
if (!head.startsWith("ref:"))
|
|
189
|
+
return head.slice(0, 40);
|
|
190
|
+
const ref = head.slice(4).trim();
|
|
191
|
+
const direct = path.join(root, ".git", ref);
|
|
192
|
+
if (fs.existsSync(direct))
|
|
193
|
+
return fs.readFileSync(direct, "utf8").trim().slice(0, 40);
|
|
194
|
+
const packed = path.join(root, ".git", "packed-refs");
|
|
195
|
+
if (fs.existsSync(packed)) {
|
|
196
|
+
for (const line of fs.readFileSync(packed, "utf8").split("\n")) {
|
|
197
|
+
const [sha, name] = line.trim().split(" ");
|
|
198
|
+
if (name === ref && sha)
|
|
199
|
+
return sha.slice(0, 40);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
/* not a repository, or an unusual layout. Neither is an error. */
|
|
205
|
+
}
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FactSet } from "./facts";
|
|
2
|
+
/** Is this JSON file a translation file?
|
|
3
|
+
*
|
|
4
|
+
* Both conditions, not either: a `data/en.json` of sample records is not a
|
|
5
|
+
* translation file, and a `locales/schema.json` is not one either. Indexing
|
|
6
|
+
* the wrong JSON would fill the map with things nobody asked about. */
|
|
7
|
+
export declare function looksLikeTranslations(rel: string): boolean;
|
|
8
|
+
/** The language a translation file is for, from its own name. */
|
|
9
|
+
export declare function languageOf(rel: string): string;
|
|
10
|
+
interface KeyAt {
|
|
11
|
+
key: string;
|
|
12
|
+
line: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every leaf key in a translation file, with the line it sits on.
|
|
16
|
+
*
|
|
17
|
+
* A single pass over the text, tracking strings, escapes, nesting and lines. It
|
|
18
|
+
* reads the structure rather than the values: what a translation SAYS never
|
|
19
|
+
* leaves the machine, only that the key exists and where.
|
|
20
|
+
*/
|
|
21
|
+
export declare function scanKeys(text: string): KeyAt[];
|
|
22
|
+
export declare function readLocales(root: string, rels: string[]): {
|
|
23
|
+
facts: Record<string, FactSet>;
|
|
24
|
+
failures: {
|
|
25
|
+
file: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.looksLikeTranslations = looksLikeTranslations;
|
|
37
|
+
exports.languageOf = languageOf;
|
|
38
|
+
exports.scanKeys = scanKeys;
|
|
39
|
+
exports.readLocales = readLocales;
|
|
40
|
+
/**
|
|
41
|
+
* Read translation files into code map facts.
|
|
42
|
+
*
|
|
43
|
+
* ## Why these are in the map at all
|
|
44
|
+
*
|
|
45
|
+
* A label a person sees on screen is not written in the code. It is a key in
|
|
46
|
+
* the code and a sentence in ten language files, and the question people
|
|
47
|
+
* actually ask about it is one no code parser can answer: *where is this label,
|
|
48
|
+
* and which languages are missing it.* An agent asked to add a field to a form
|
|
49
|
+
* has to put the same line in ten files, and a map that indexes only Python and
|
|
50
|
+
* TypeScript sends it to search for them one at a time.
|
|
51
|
+
*
|
|
52
|
+
* ## One object per key, not one per key per file
|
|
53
|
+
*
|
|
54
|
+
* Ten language files times three thousand keys is thirty one thousand objects,
|
|
55
|
+
* and almost all of that is the same key repeated. Each file reports its own
|
|
56
|
+
* keys here, because facts are read per file and a file has to be re-readable
|
|
57
|
+
* on its own. The service then collapses them into ONE object per key carrying
|
|
58
|
+
* which languages have it and which do not, which is both far smaller and the
|
|
59
|
+
* answer somebody wanted.
|
|
60
|
+
*
|
|
61
|
+
* ## Line numbers are tracked by hand, and that is deliberate
|
|
62
|
+
*
|
|
63
|
+
* `JSON.parse` gives no positions, and "the key is in this file somewhere" is
|
|
64
|
+
* not a useful answer when the file is three thousand lines. So the text is
|
|
65
|
+
* scanned once, tracking the line of every key as it goes. It is a scanner, not
|
|
66
|
+
* a guess: the same file always produces the same lines.
|
|
67
|
+
*/
|
|
68
|
+
const fs = __importStar(require("fs"));
|
|
69
|
+
const path = __importStar(require("path"));
|
|
70
|
+
/** Folders whose JSON files are translations. */
|
|
71
|
+
const LOCALE_DIRS = new Set(["locales", "locale", "i18n", "lang", "translations"]);
|
|
72
|
+
/** A file named like a language: `en`, `en-US`, `pt_BR`. */
|
|
73
|
+
const LANGUAGE_NAME = /^[a-z]{2}([-_][A-Za-z]{2,4})?$/;
|
|
74
|
+
/** Is this JSON file a translation file?
|
|
75
|
+
*
|
|
76
|
+
* Both conditions, not either: a `data/en.json` of sample records is not a
|
|
77
|
+
* translation file, and a `locales/schema.json` is not one either. Indexing
|
|
78
|
+
* the wrong JSON would fill the map with things nobody asked about. */
|
|
79
|
+
function looksLikeTranslations(rel) {
|
|
80
|
+
if (!rel.endsWith(".json"))
|
|
81
|
+
return false;
|
|
82
|
+
const parts = rel.split("/");
|
|
83
|
+
const base = path.basename(rel, ".json");
|
|
84
|
+
const parent = parts.length > 1 ? parts[parts.length - 2] ?? "" : "";
|
|
85
|
+
return LOCALE_DIRS.has(parent.toLowerCase()) && LANGUAGE_NAME.test(base);
|
|
86
|
+
}
|
|
87
|
+
/** The language a translation file is for, from its own name. */
|
|
88
|
+
function languageOf(rel) {
|
|
89
|
+
return path.basename(rel, ".json");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every leaf key in a translation file, with the line it sits on.
|
|
93
|
+
*
|
|
94
|
+
* A single pass over the text, tracking strings, escapes, nesting and lines. It
|
|
95
|
+
* reads the structure rather than the values: what a translation SAYS never
|
|
96
|
+
* leaves the machine, only that the key exists and where.
|
|
97
|
+
*/
|
|
98
|
+
function scanKeys(text) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const stack = [];
|
|
101
|
+
let line = 1;
|
|
102
|
+
let i = 0;
|
|
103
|
+
/** The key whose value has not been seen yet. Until the value turns up we do
|
|
104
|
+
* not know whether this key is a group or a leaf. */
|
|
105
|
+
let pending = null;
|
|
106
|
+
let pendingLine = 0;
|
|
107
|
+
const at = (n) => text[n] ?? "";
|
|
108
|
+
/** Consume a string starting at the opening quote, counting any lines in it. */
|
|
109
|
+
const readString = () => {
|
|
110
|
+
i += 1;
|
|
111
|
+
let value = "";
|
|
112
|
+
while (i < text.length) {
|
|
113
|
+
const ch = at(i);
|
|
114
|
+
if (ch === "\\") {
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ch === '"') {
|
|
119
|
+
i += 1;
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
if (ch === "\n")
|
|
123
|
+
line += 1;
|
|
124
|
+
value += ch;
|
|
125
|
+
i += 1;
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
/** Consume an array, whatever is nested in it. Arrays hold no keys we want. */
|
|
130
|
+
const skipArray = () => {
|
|
131
|
+
let depth = 0;
|
|
132
|
+
while (i < text.length) {
|
|
133
|
+
const ch = at(i);
|
|
134
|
+
if (ch === '"') {
|
|
135
|
+
readString();
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ch === "\n")
|
|
139
|
+
line += 1;
|
|
140
|
+
if (ch === "[")
|
|
141
|
+
depth += 1;
|
|
142
|
+
if (ch === "]") {
|
|
143
|
+
depth -= 1;
|
|
144
|
+
i += 1;
|
|
145
|
+
if (depth === 0)
|
|
146
|
+
return;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
while (i < text.length) {
|
|
153
|
+
const ch = at(i);
|
|
154
|
+
if (ch === "\n") {
|
|
155
|
+
line += 1;
|
|
156
|
+
i += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === " " || ch === "\t" || ch === "\r" || ch === ",") {
|
|
160
|
+
i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// A key is waiting, so whatever comes next is its VALUE. This is checked
|
|
164
|
+
// first, and that is the whole correctness of the scanner: a string value
|
|
165
|
+
// looks exactly like a key until you know which position you are in.
|
|
166
|
+
if (pending !== null) {
|
|
167
|
+
if (ch === "{") {
|
|
168
|
+
stack.push(pending);
|
|
169
|
+
pending = null;
|
|
170
|
+
i += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
out.push({ key: [...stack, pending].join("."), line: pendingLine });
|
|
174
|
+
pending = null;
|
|
175
|
+
if (ch === '"') {
|
|
176
|
+
readString();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === "[") {
|
|
180
|
+
skipArray();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
while (i < text.length && !/[,}\]\s]/.test(at(i)))
|
|
184
|
+
i += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === '"') {
|
|
188
|
+
const startLine = line;
|
|
189
|
+
const name = readString();
|
|
190
|
+
let j = i;
|
|
191
|
+
while (j < text.length && /\s/.test(at(j))) {
|
|
192
|
+
if (at(j) === "\n")
|
|
193
|
+
line += 1;
|
|
194
|
+
j += 1;
|
|
195
|
+
}
|
|
196
|
+
if (at(j) === ":") {
|
|
197
|
+
pending = name;
|
|
198
|
+
pendingLine = startLine;
|
|
199
|
+
i = j + 1;
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ch === "}") {
|
|
204
|
+
stack.pop();
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
i += 1;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function readOne(root, rel) {
|
|
213
|
+
const text = fs.readFileSync(path.join(root, rel), "utf8");
|
|
214
|
+
const language = languageOf(rel);
|
|
215
|
+
const objects = [{
|
|
216
|
+
id: rel, kind: "file", name: path.basename(rel), file: rel, line: 1,
|
|
217
|
+
lang: "translations", language,
|
|
218
|
+
lines: text.split("\n").length,
|
|
219
|
+
}];
|
|
220
|
+
for (const { key, line } of scanKeys(text)) {
|
|
221
|
+
objects.push({
|
|
222
|
+
// Unique per file, because facts are stored per file. The service
|
|
223
|
+
// collapses every file's copy of one key into a single object.
|
|
224
|
+
id: `i18n:${key}@${rel}`,
|
|
225
|
+
kind: "translation_key",
|
|
226
|
+
name: key,
|
|
227
|
+
file: rel,
|
|
228
|
+
line,
|
|
229
|
+
language,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { objects, links: [] };
|
|
233
|
+
}
|
|
234
|
+
function readLocales(root, rels) {
|
|
235
|
+
const facts = {};
|
|
236
|
+
const failures = [];
|
|
237
|
+
for (const rel of rels) {
|
|
238
|
+
try {
|
|
239
|
+
facts[rel] = readOne(root, rel);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
failures.push({ file: rel, reason: err?.name || "Error" });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { facts, failures };
|
|
246
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FactSet } from "./facts";
|
|
2
|
+
/** Which Python this machine has, or null. Asked once per process. */
|
|
3
|
+
export declare function findPython(): string | null;
|
|
4
|
+
export declare function readPython(root: string, rels: string[]): {
|
|
5
|
+
facts: Record<string, FactSet>;
|
|
6
|
+
failures: {
|
|
7
|
+
file: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
}[];
|
|
10
|
+
available: boolean;
|
|
11
|
+
};
|